Compare commits

...

402 Commits

Author SHA1 Message Date
Jonas Kvinge 6446942e73 Add error handling to playlist parsers 2024-06-24 20:20:49 +02:00
Jonas Kvinge 0038cf8c4e CollectionWatcher: Make sure periodic scan is stopped 2024-06-24 19:43:09 +02:00
Jonas Kvinge 7f177aef08 CollectionModel: Always separate albums by different artists
Fixes #1276
2024-06-24 19:21:24 +02:00
Jonas Kvinge a7a42ea5ec AnalyzerContainer: Use constexpr 2024-06-22 00:48:21 +02:00
Jonas Kvinge 14cddfd42f AnalyzerContainer: Add parameter 2024-06-22 00:48:03 +02:00
Jonas Kvinge ae0ce65674 AnalyzerContainer: Remove unused declaration 2024-06-22 00:47:52 +02:00
Jonas Kvinge 9c9926d5a7 PlaylistHeader: Cast column to int
Fixes #1468
2024-06-22 00:43:20 +02:00
Jonas Kvinge 95a3c41baa CI: Disable Spotify for MinGW and macOS 2024-06-20 23:39:42 +02:00
Jonas Kvinge f2518baef9 nsi: Add spotify plugin for MSVC 2024-06-20 23:39:23 +02:00
Jonas Kvinge 4be9265546 PlaylistView: Use `Playlist::ColumnCount` 2024-06-20 23:00:33 +02:00
Jonas Kvinge 9f9c46e370 Update individual playlist columns, use enum class 2024-06-20 22:52:27 +02:00
Jonas Kvinge 5816d0bb12 About: Update contributors 2024-06-20 16:48:45 +02:00
Jonas Kvinge 70c2b99771 ContextAlbum: Delete timeline to delete previous cover
QTimeLine was holding the previous covers shared pointer in the signal/slot connection, which caused it to never be free'd even though it's removed from the previous_covers_ list.
To fix this, make sure the QTimeLine is deleted.

This fixes a huge memory leak.

Addresses issue #1464
2024-06-20 16:05:07 +02:00
Jonas Kvinge 6177d4a2c4 ContextAlbum: Use const reference for image parameter 2024-06-20 15:59:34 +02:00
Jonas Kvinge 05f012e590 ContextAlbum: Formatting 2024-06-20 15:58:24 +02:00
Jonas Kvinge cc0506490f ContextAlbum: Use constexpr for kFadeTimeLineMs 2024-06-20 15:57:53 +02:00
Jonas Kvinge 06114c9835 ContextAlbum: Add explicit for PreviousCover 2024-06-20 15:57:12 +02:00
Jonas Kvinge 2518e4d47d ContextAlbum: Remove unused function declaration 2024-06-20 15:56:49 +02:00
Jonas Kvinge ceea805196 main: Remove `QCoreApplication::setQuitLockEnabled(false);`
This was a workaround for QTBUG-124386.
2024-06-19 21:33:01 +02:00
Jonas Kvinge ae7e515945 Update Changelog 2024-06-19 00:39:13 +02:00
Jonas Kvinge b275f91a58 PlaylistView: Set new default column sizes 2024-06-18 19:52:51 +02:00
Jonas Kvinge b8ef96028c StretchHeaderView: Refactor code and improve header view
Save what sections are visible, and always save sizes.
Do not set section size to zero when hiding sections.
When resizing columns in stretch mode, only resize the right column to fit the left column.

Fixes #1085
2024-06-18 19:52:34 +02:00
Jonas Kvinge 6ba1fdb744 CI: Remove openSUSE 15.5 2024-06-16 18:18:43 +02:00
Jonas Kvinge dcef38427b CI: Remove protobuf workaround for openSUSE 2024-06-15 01:48:20 +02:00
Jonas Kvinge 20d7ae7144 CI: Fix setting ENABLE_WIN32_CONSOLE for MSVC 2024-06-15 01:20:57 +02:00
Jonas Kvinge d576777d94 CueParser: Always set track 2024-06-14 21:19:18 +02:00
Jonas Kvinge 1f7344ca1b CueParser: Move artist / album variables
Fixes #1463
2024-06-14 21:19:04 +02:00
Jonas Kvinge 87c69f7456 CueParser: Formatting 2024-06-14 21:17:59 +02:00
Jonas Kvinge a684b35203 ParserBase: Always read file, CUE depends on it 2024-06-14 21:03:52 +02:00
Jonas Kvinge 37855fe836 CollectionBackend: Remove QUrl::FullyDecoded from QUrl::toString() 2024-06-14 18:46:48 +02:00
Jonas Kvinge f596695f61 CollectionModel: Don't process model updates when loading 2024-06-14 18:40:52 +02:00
Jonas Kvinge 076d065f7c nsi: Replace libxml2-2.dll with libxml2.dll 2024-06-14 00:20:33 +02:00
Jonas Kvinge 70a7a7bbdd CI: Cleanup PATH for MSVC build 2024-06-13 23:01:33 +02:00
Jonas Kvinge 5f540a4c08 Add Spotify support 2024-06-13 17:09:06 +02:00
Jonas Kvinge f33b30fe79 OrganizeFormat: Replace QLatin1String with QStringLiteral 2024-06-13 00:40:08 +02:00
Jonas Kvinge 2f546f214d Replace QLatin1String with QStringLiteral 2024-06-12 23:51:09 +02:00
Jonas Kvinge 7ba4fda346 SnapDialog: Replace QLatin1String with QStringLiteral 2024-06-12 23:23:30 +02:00
Jonas Kvinge 299415a889 Rename "Internet" to "Streaming" 2024-06-12 22:23:05 +02:00
Jonas Kvinge 718af984ab Move LocalRedirectServer to core 2024-06-12 21:21:11 +02:00
Jonas Kvinge 5d51657f32 Drop FTS tables 2024-06-12 21:17:01 +02:00
Jonas Kvinge a2958ba808 ListenBrainzScrobbler: Replace QLatin1String with QStringLiteral 2024-06-12 21:00:25 +02:00
Jonas Kvinge 79c2130152 ScrobblingAPI20: Replace QLatin1String with QStringLiteral 2024-06-12 20:59:09 +02:00
Jonas Kvinge 98d3cc2637 AnalyzerBase: Add static_cast 2024-06-12 20:58:51 +02:00
Jonas Kvinge 8339aa0934 CI: Remove Fedora 41 2024-06-12 20:32:34 +02:00
Jonas Kvinge 5451c110b1 Replace QStringLiteral with QLatin1String 2024-06-12 20:30:36 +02:00
Jonas Kvinge 20595a11bc SmartPlaylistSearchTermWidget: Add const 2024-06-12 18:56:21 +02:00
Jonas Kvinge c92a1b516c GstEngine: Fix swapped media_url / stream_url 2024-06-12 18:52:53 +02:00
Jonas Kvinge a8f1a881ff GioLister: Remove useless else 2024-06-12 18:52:33 +02:00
Jonas Kvinge ec21a55271 CollectionModelTest: Remove unused test 2024-06-12 18:14:37 +02:00
Jonas Kvinge 89990624ec CollectionBackendTest: Use std::make_shared 2024-06-12 18:14:24 +02:00
Jonas Kvinge 6caf7f356b SubsonicRequest: Add const 2024-06-12 18:12:20 +02:00
Jonas Kvinge 241a6c5818 EditTagDialog: Initialize cover_menu_ 2024-06-12 18:12:08 +02:00
Jonas Kvinge 57fb52e8f0 Add LL 2024-06-12 18:11:43 +02:00
Jonas Kvinge 7b00385155 Udisks2Lister: Add static_cast 2024-06-12 18:11:10 +02:00
Jonas Kvinge 2b4aa1d6b2 AlbumCoverChoiceController: Add missing close 2024-06-12 18:09:59 +02:00
Jonas Kvinge 4ba5113842 Remove const 2024-06-12 18:09:23 +02:00
Jonas Kvinge a36bf2df65 Replace QStringLiteral with QLatin1String 2024-06-12 18:08:54 +02:00
Jonas Kvinge f5002cae36 Make static 2024-06-12 18:07:58 +02:00
Jonas Kvinge cb8022c55d WaveRubber: use static_cast 2024-06-12 18:06:37 +02:00
Jonas Kvinge 2a65e00988 WaveRubber: Remove trailing whitespaces and fix formatting 2024-06-12 17:45:01 +02:00
Jonas Kvinge 05358cdfe4 Add default to switch 2024-06-12 17:41:17 +02:00
Jonas Kvinge 7b43a94055 CollectionBackend: Use static `QMetaObject::invokeMethod` 2024-06-12 17:40:08 +02:00
Jonas Kvinge 36e19e82e7 FHT: Remove void 2024-06-12 17:39:46 +02:00
Jonas Kvinge c52a802b83 AnalyzerBase: Remove static_cast 2024-06-12 17:39:37 +02:00
Jonas Kvinge b233600b8c Remove useless else 2024-06-12 17:38:58 +02:00
Jonas Kvinge 93df859aa4 About: Replace QLatin1String with QStringLiteral 2024-06-12 17:31:17 +02:00
Jonas Kvinge f1f79fb961 Move default constructor to header 2024-06-12 17:30:40 +02:00
Jonas Kvinge 92fa75b6c2 gstmoodbarplugin: Remove namespace 2024-06-12 17:28:23 +02:00
Jonas Kvinge 1d5f3a0486 CI: Remove MSVC abseil workaround 2024-06-12 17:09:21 +02:00
Jonas Kvinge b89c200076 Replace QStringLiteral with QLatin1String 2024-06-12 02:13:27 +02:00
Jonas Kvinge 597a8cd6c8 Remove QStringBuilder include 2024-06-12 00:44:48 +02:00
Jonas Kvinge e477449cd4 Rewrite collection model and search
Fixes #392
2024-06-11 23:18:38 +02:00
Jonas Kvinge ea1e4541c0 CI: Use MSVC protoc from debug to workaround a crash 2024-06-09 23:03:57 +02:00
Guzpido 50572ac40f WaveRubber: a simple waveform analyzer
This kinda elastic analyzer uses no transformations and is colored by a gradient based on the user QT "Highlight" palette.

WaveRubber: a kinda elastic waveform analyzer

Update src/analyzer/waverubber.cpp

better pointer declaration

Co-authored-by: Jonas Kvinge <jonas@jkvinge.net>
2024-06-09 18:01:28 +02:00
Guzpido fd81036909 GstEnginePipeline: Divide samples and format by channels for buffer duration 2024-06-09 15:56:29 +02:00
Jonas Kvinge 0e27886e28 CI: Add Boost_INCLUDE_DIR for MSVC build 2024-06-09 14:12:38 +02:00
whatwareweb 45bad3be04 Fix integer underflow bug 2024-06-08 11:49:35 +02:00
Jonas Kvinge 30b268dc3a Remove msvc toolset 2024-06-08 00:48:43 +02:00
jj ef99f0ef36 Add volume increment setting 2024-06-06 21:53:16 +02:00
Jonas Kvinge e357ba0125 GstEngine: Check individual classes when parsing outputs 2024-06-04 19:43:54 +02:00
Jonas Kvinge 36b75a5928 CI: Manually codesign libraries 2024-06-03 17:13:28 +02:00
Jonas Kvinge 64d3ea2804 Udisks2Lister: Fix mountpoint 2024-06-03 17:12:23 +02:00
Jonas Kvinge 0a93affeef SongLoader: Use QObject::connect 2024-06-03 00:00:20 +02:00
Jonas Kvinge 402d13a322 CI: Bump macOS to 12 2024-06-02 12:49:05 +02:00
Jonas Kvinge adf0efc859 CI: Remove Fedora 38 2024-06-02 12:48:23 +02:00
Jonas Kvinge d1c65fd273 Bump default LSMinimumSystemVersion to 12.0 2024-06-02 12:30:48 +02:00
Jonas Kvinge 8a27c6a52f GstEnginePipeline: Use playbin3 with GStreamer 1.24 and higher
playbin3 is buggy with GStreamer 1.22, for some reason the bug is only reproducible on Gnome.

https://forum.strawberrymusicplayer.org/topic/1506/buffering-forever/23
2024-06-02 12:09:38 +02:00
Jonas Kvinge d7cc52bc99 EngineBase: Use fully qualified namespace in StateChanged
Makes sure the metatype matches with Qt 5:
QObject::connect: Cannot queue arguments of type 'State'
(Make sure 'State' is registered using qRegisterMetaType().)

Fixes #1446
2024-05-30 20:09:32 +02:00
Jonas Kvinge f0f5300891 ParserBase: Make the path absolute and try canonical path
Somehow I got this mixed up in commit 2953f9e :(

Fixes #1448
2024-05-29 00:18:39 +02:00
Jonas Kvinge 6e90e72b4a CollectionModel: Add content to fake header for pixmap cache
QNetworkCacheMetaData requires this now.
2024-05-26 02:49:26 +02:00
Jonas Kvinge c655963483 Use checkStateChanged(Qt::CheckState) with Qt >= 6.7 2024-05-24 02:13:48 +02:00
Jonas Kvinge 9f2e4ac312 CueParser: Detect and handle different text encodings
Fixes #1429
2024-05-19 01:49:37 +02:00
Jonas Kvinge 9e25366f85 Add function for detecting text encoding 2024-05-19 01:47:44 +02:00
Jonas Kvinge c102d8731a Require ICU 2024-05-19 01:45:19 +02:00
Jonas Kvinge 0983ba1339 MoodbarLoader: Add header name for disk cache 2024-05-13 00:44:37 +02:00
dependabot[bot] 0a99eca7cd Bump apple-actions/import-codesign-certs from 2 to 3
Bumps [apple-actions/import-codesign-certs](https://github.com/apple-actions/import-codesign-certs) from 2 to 3.
- [Release notes](https://github.com/apple-actions/import-codesign-certs/releases)
- [Commits](https://github.com/apple-actions/import-codesign-certs/compare/v2...v3)

---
updated-dependencies:
- dependency-name: apple-actions/import-codesign-certs
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 00:27:33 +02:00
Jonas Kvinge 116bbec73e Replace qPrintable with qUtf8Printable
Fixes #1440
2024-05-13 00:05:58 +02:00
Jonas Kvinge bf19540f8d Set LSMinimumSystemVersion from MACOSX_DEPLOYMENT_TARGET
Fixes #1436
2024-05-12 23:10:43 +02:00
Jonas Kvinge dff3ae7410 CI: Remove --skip-broken from dnf for Fedora 2024-05-12 21:41:38 +02:00
Jonas Kvinge 76614bcde0 Only apply collection directories changes on save 2024-05-12 21:40:51 +02:00
Jonas Kvinge 2953f9eefc ParserBase: Use original paths 2024-05-12 21:38:59 +02:00
Jonas Kvinge 4a24605361 FileViewList: Use original paths instead of canonical paths 2024-05-12 21:38:15 +02:00
Jonas Kvinge decabe8d47 CommandlineOptions: Use original paths instead of canonical paths 2024-05-12 21:37:54 +02:00
Jonas Kvinge 51adcf0f1e MainWindow: Use original paths instead of canonical paths 2024-05-12 21:37:32 +02:00
Jonas Kvinge 2a6a07fef6 CollectionBackendTest: Remove use of QFileInfo::canonicalFilePath 2024-05-12 21:36:52 +02:00
Jonas Kvinge 315cf63118 CI: Remove Windows 32 bit 2024-05-11 23:23:56 +02:00
Wedone e28d362aad Update fr.po 2024-05-04 16:22:51 +02:00
Jonas Kvinge e0d9b8f715 ResizableTextEdit: Remove tab 2024-05-04 16:21:59 +02:00
Jonas Kvinge eff6b75c43 nsi: Remove /SOLID from SetCompressor for debug 2024-04-24 02:25:05 +02:00
Robert Gingras e8be0adf37 TagReaderTagLib: Remove redundant ID3v2 validity check
TagLib will have created a valid ID3v2 tag on this file by this point in the code, due to the way it handles the tag() method for WAV::File. Thus the null pointer check is redundant and the hasID3v2() call is at best redundant and at worst will cause tags to not save when they otherwise should have
2024-04-24 01:23:51 +02:00
Robert Gingras 9f4a82bb62 TagReaderTagLib: Remove file_mpeg argument from the SetEmbeddedArt ID3v2 overload 2024-04-24 01:23:51 +02:00
Robert Gingras 8d7e14f21d Song: Added WAV to list of supported filetypes 2024-04-24 01:23:51 +02:00
Robert Gingras d03d3622aa TagReaderTagLib: Have RIFF WAV files save ID3v2 tags, when applicable 2024-04-24 01:23:51 +02:00
Robert Gingras 821c32992d TagReaderTagLib: Refactor ID3v2 saving to a dedicated function 2024-04-24 01:23:51 +02:00
Robert Gingras b52cf9f3cd TagReaderTagLib: Reposition ParseID3v2Tag 2024-04-24 01:23:51 +02:00
Robert Gingras ab8e687f96 TagReaderTagLib: Add id3v2 parsing for RIFF WAV files 2024-04-24 01:23:51 +02:00
Robert Gingras 90703703aa TagReaderTagLib: Make id3v2 parsing reusable 2024-04-24 01:23:51 +02:00
Jonas Kvinge 176984afe0 RadioPlaylistItem: Set correct col position for InitFromQuery
Fixes #1430
2024-04-23 22:34:00 +02:00
Jonas Kvinge 542efa17ff Fix position for song and internet playlist items
Fixes #1430
2024-04-23 20:18:28 +02:00
Jonas Kvinge 6a2c2dbba1 Song: Fix loading length 2024-04-23 20:16:41 +02:00
Jonas Kvinge 426de61525 Add const and std::as_const 2024-04-23 17:15:42 +02:00
Jonas Kvinge 24c8d06d41 SongPlaylistItem: Use static_cast 2024-04-23 17:00:10 +02:00
Jonas Kvinge 227f5e5176 Replace QStringLiteral with QLatin1String 2024-04-23 16:57:49 +02:00
Jonas Kvinge 92e39a3e21 GlobalShortcut: Use optional 2024-04-23 16:54:54 +02:00
Jonas Kvinge fb2300e2fa EBUR128State: Add missing const reference 2024-04-23 16:54:22 +02:00
Jonas Kvinge 78becae5e9 AlbumCoverLoader: Use fully qualified namespace in slot 2024-04-23 16:53:40 +02:00
Jonas Kvinge d10eb4370e Player: Use chrono_literals 2024-04-23 16:53:12 +02:00
Jonas Kvinge 9c92ef941f CollectionModel: Remove redundant const_cast 2024-04-23 16:52:17 +02:00
Jonas Kvinge 7aefe3d71b Change QList<QString> to QStringList 2024-04-23 16:51:42 +02:00
Jonas Kvinge 398db964b8 Use QDateTime::currentSecsSinceEpoch 2024-04-23 16:48:51 +02:00
Jonas Kvinge 579349b104 ResizableTextEdit: Add Q_OBJECT macro 2024-04-23 16:44:44 +02:00
Jonas Kvinge 5f9a83871d Playlist: Cast to int 2024-04-23 02:45:27 +02:00
Jonas Kvinge da0c5e67c5 Playlist: Use optional::has_value 2024-04-23 02:45:11 +02:00
Jonas Kvinge a0cfde18c3 PlaylistSequence: Use constexpr 2024-04-23 02:44:48 +02:00
Jonas Kvinge 0ad4889936 PlaylistSequence: Remove unused member variable 2024-04-23 02:44:36 +02:00
Jonas Kvinge 36e809d530 InternetPlaylistItem: Cast to int 2024-04-23 02:18:24 +02:00
Jonas Kvinge 78096658e2 Chromaprinter: Remove useless cast 2024-04-23 02:18:08 +02:00
Jonas Kvinge 3edd218e3a Remove redundant casts 2024-04-23 01:58:08 +02:00
Jonas Kvinge 569bf6335b CollectionView: Add action_search_for_this_ to initialization list 2024-04-23 01:57:16 +02:00
Jonas Kvinge 7b8919d706 CommandlineOptions: Add const 2024-04-23 01:56:55 +02:00
Jonas Kvinge 147fd87d8c MainWindow: Only pass progress to UpdateTaskbarProgress 2024-04-23 01:56:30 +02:00
Jonas Kvinge ac0926d40b Song: Add ColumnIndex helper function 2024-04-23 01:55:57 +02:00
Jonas Kvinge 105d472f5d README: Fix badge 2024-04-21 22:35:15 +02:00
Jonas Kvinge c1a49da385 tests: Use QStringLiteral 2024-04-21 19:37:39 +02:00
Jonas Kvinge c3f596e64e CI: Add devel-tools-building repo for Leap 2024-04-21 17:03:06 +02:00
Jonas Kvinge adfda84c41 nsi: Bump icu from 74 to 75 2024-04-21 16:44:07 +02:00
Jonas Kvinge 345cc118a3 CI: Use different mirror for Mageia 2024-04-21 16:10:26 +02:00
Jonas Kvinge df070ac0cf Optimize `Song::InitFromQuery`
Use `QSqlQuery::value(int)` or `QSqlRecord::value(int)` instead of `QSqlQuery::value(QString)`.
Make `SqlRow` use `QSqlRecord` directly instead iterating over all columns.
2024-04-21 15:42:29 +02:00
Jonas Kvinge 7b88be2635 CollectionModel: Only set grouping if it's selected 2024-04-21 15:42:29 +02:00
Kientz Arnaud c30a39d29a Fix infinitive in french translations 2024-04-16 00:24:59 +02:00
Jonas Kvinge 36db41a1f0 Add sidebar background 2024-04-13 23:47:48 +02:00
Jonas Kvinge 8b249dc06a QSearchField: Remove NSSearchField workaround
This was a workaround for QTBUG-124160.
2024-04-13 05:39:19 +02:00
Jonas Kvinge 0c6872b352 Disable automatic conversions from 8-bit strings 2024-04-13 05:05:33 +02:00
Jonas Kvinge 58944993b8 Use QStringLiteral 2024-04-09 23:20:26 +02:00
Olivier HUMBERT 3cfffa5fbb Adds French to the menu item 2024-04-07 19:26:23 +02:00
Jonas Kvinge 4873b8b413 CI: Move OpenMandriva if false 2024-04-06 21:24:19 +02:00
Jonas Kvinge 0b85f5192c CI: Enable Mageia 2024-04-06 21:19:03 +02:00
Jonas Kvinge 3e9a1776a1 CI: Run upload and attach independent of failure 2024-04-06 21:10:10 +02:00
Jonas Kvinge e1fbe9ae54 Resolve song from collection using track with Cue in XSPF
Fixes #1181
2024-04-04 22:22:02 +02:00
Jonas Kvinge f48d1a8017 OrganizeDialog: Don't save settings unless button is pressed
Fixes #1404
2024-04-04 21:43:57 +02:00
rimasx 0debc90695 Create estonian translation
I hope this is OK. Strawberry compiled without any problems and Estonian was automatically selected. I mostly used the Clementine translation, which I tweaked a bit. Many thanks to the Clementine translators, especially Priit Jõerüüt.
2024-04-04 21:29:48 +02:00
Jonas Kvinge 8d42ea7cfd README: Remove link to zanata 2024-04-04 21:18:57 +02:00
Jonas Kvinge 1dae80a633 Add scrobbler option for stripping "remastered" etc
Fixes #1387
2024-04-04 21:17:07 +02:00
Jonas Kvinge d398c86b0c Make showing song progress on taskbar optional 2024-04-04 16:49:53 +02:00
Jonas Kvinge 70809e0647 MainWindow: Add error dialog when file deletion fails
Fixes #1384
2024-04-03 21:37:20 +02:00
Jonas Kvinge 4c1a5168f0 CollectionModel: Reset model before deletion 2024-04-03 21:17:20 +02:00
Jonas Kvinge f9acfbc224 SimpleTreeModel: Handle null root 2024-04-03 21:17:20 +02:00
Jonas Kvinge 5f78e1a983 MergedProxyModel: Fix beginRemoveRows first
Fixes #1314
2024-04-03 21:17:06 +02:00
Jonas Kvinge 7bc5579fb7 Song: Check that filetype is supported for writing tags
Fixes #1413
2024-04-03 20:45:52 +02:00
Jonas Kvinge 57750efcb2 CI: Remove openSUSE Leap 15.5
Protobuf is broken
2024-04-03 20:09:07 +02:00
Jonas Kvinge a33ee1cda9 CI: Use GCC 13 for Leap 2024-04-03 20:08:05 +02:00
Jonas Kvinge cd20a0679a CI: Replace qwindowsvistastyle with qmodernwindowsstyle 2024-04-02 17:37:58 +02:00
Jonas Kvinge 20e546e02b nsi: Replace qwindowsvistastyle with qmodernwindowsstyle 2024-04-02 17:37:38 +02:00
Jonas Kvinge f5547f093e Player: Use timer for saving volume
Fixes #1272
2024-04-02 17:16:29 +02:00
Jonas Kvinge c00d95242d Utilities: Handle missing XDG_DATA_DIRS variable 2024-04-02 16:39:48 +02:00
Jonas Kvinge 05c4d23df6 Utilities: Remove `--new-window` parameter from dolphin
Fixes #1412
2024-04-02 00:48:29 +02:00
Jonas Kvinge 06fa17f33f CI: Run apt upgrade for debian/ubuntu 2024-04-01 00:38:21 +02:00
Jonas Kvinge 194285289c Update Changelog 2024-03-31 22:56:46 +02:00
Jonas Kvinge a61fa61330 CI: Disable OpenMandriva 2024-03-31 01:49:30 +01:00
Jonas Kvinge 68c922ee12 SmartPlaylistWizard: Set classic style if using fusion on Windows
Workaround a Qt bug.

Fixes #1399
2024-03-29 02:54:25 +01:00
Jonas Kvinge d1042b276b GstEnginePipeline: Set volume_set_ to false in ElementRemovedCallback 2024-03-24 19:44:47 +01:00
Jonas Kvinge 9bbffe150f GstEnginePipeline: Add back volume sync for auto
We need to remove the volume sync when the element is deleted on "deep-element-removed", then re-add it on the next "deep-element-added" that isn't a fakesink.

Fixes #1123
2024-03-24 19:36:32 +01:00
Jonas Kvinge b95be526d3 HtmlLyricsProvider: Use QNetworkRequest::UserAgentHeader 2024-03-24 07:04:07 +01:00
Jonas Kvinge 165f9d769b MusixmatchCoverProvider: Fix parsing 2024-03-24 06:59:01 +01:00
Jonas Kvinge a0ea75b74e NetworkAccessManager: Use QNetworkRequest::setHeader 2024-03-24 06:58:33 +01:00
Jonas Kvinge 4075f92eec OpenTidalCoverProvider: Adjust settings 2024-03-24 05:27:43 +01:00
Jonas Kvinge 035aff5454 Add Open Tidal cover provider 2024-03-24 05:23:35 +01:00
Jonas Kvinge 52dc7ad259 CI: Add Fedora 41 2024-03-23 14:18:17 +01:00
Jonas Kvinge c3c83f608c CI: Add Leap 15.6 and remove 15.4 2024-03-23 14:05:11 +01:00
Strawbs Bot ffba351a16 Update translations 2024-03-23 01:43:24 +01:00
Jonas Kvinge a12623e142 Update Changelog 2024-03-23 01:04:29 +01:00
Jonas Kvinge 1a691a103e Fix Qt 5 and mpris2 build errors 2024-03-22 20:26:13 +01:00
Jonas Kvinge 5e725e0bbe Fix playlist shuffle
- Shuffle all indexes
- Use persistent indexes to store play history
- Update virtual items to keep original shuffle order when the playlist is reordered
- Make sure to always set virtual index on manual shuffle
- Ignore repeat and shuffle when dynamic playlist is activated

Fixes #707
Fixes #1381
Fixes #1366
Fixes #1353
2024-03-22 20:00:12 +01:00
Jonas Kvinge 93c2fa4c73 MusixmatchLyricsProvider: Parse metadata 2024-03-17 23:41:05 +01:00
Jonas Kvinge f412fb21d6 SettingsPage: Double spinboxes are double, not int 2024-03-17 21:56:39 +01:00
Jonas Kvinge bd4b6c1f01 main: Add `QCoreApplication::setQuitLockEnabled(false);`
Fixes #1401
2024-03-17 21:46:15 +01:00
Jonas Kvinge d1839d87e7 MusixmatchLyricsProvider: Fix parsing lyrics 2024-03-16 22:46:45 +01:00
Jonas Kvinge 1fc163eb5f Playlist: Comments formatting 2024-03-13 21:59:17 +01:00
Reverier-Xu cd2b3cb73e mpris2: Fix mpris:trackid type with Plasma 6 2024-03-13 21:31:25 +01:00
Reverier-Xu 88b5cf2461 mpris2: Fix mpris:trackid type with Plasma 6 2024-03-13 21:31:25 +01:00
Jonas Kvinge 2ccb0af75e Song: Only include mpris when built with DBUS 2024-03-13 18:15:51 +01:00
Célestin Matte 27ee6e7643 EditTagDialog: Add button to fetch lyrics
Co-Authored-By: Jonas Kvinge <jonas@jkvinge.net>
2024-03-13 17:54:19 +01:00
Jonas Kvinge a3207a5703 macgstcopy: Update gstreamer plugins list 2024-03-12 18:25:06 +01:00
Jonas Kvinge 72ff64a7f8 nsi: Remove unused gstreamer plugins 2024-03-12 16:29:48 +01:00
Jonas Kvinge 9c06d1d0ae nsi: Add gstreamer dsd plugin 2024-03-12 00:52:49 +01:00
Jonas Kvinge f11afd4414 GstEnginePipeline: Add default to switch 2024-03-12 00:40:11 +01:00
Sami Boukortt 2aa70b6ab8 Add an option not to skip “The” when sorting artist names 2024-03-11 23:34:42 +01:00
Jonas Kvinge 4626a6f609 GstEnginePipeline: Use playbin3 with gstreamer >= 1.22 2024-03-08 18:52:22 +01:00
Jonas Kvinge 9152f8559f Song: Split remastered and explicit regex 2024-03-03 01:50:05 +01:00
Jonas Kvinge 7f4c61b15a Improve album and title disc, remastered, etc matching
Don't partial remove things like "(Mono / Remastered)".

Fixes #1387
2024-03-02 19:48:19 +01:00
Jonas Kvinge b365131363 Playlist: Remove veto listeners
We have never used this, it's basically dead code.
2024-02-28 23:00:24 +01:00
Jonas Kvinge a6ea4dd0d7 Remove unused includes 2024-02-28 21:37:14 +01:00
Jonas Kvinge 9c6649f077 Add letras lyrics provider 2024-02-28 21:33:30 +01:00
Jonas Kvinge 04ba202e12 HtmlLyricsProvider: Use browser-like user-agent 2024-02-25 04:32:09 +01:00
Jonas Kvinge 352a6c5691 Remove lyricsmode.com provider
They have a "Verifying you are human" thing now.
2024-02-25 04:23:04 +01:00
Jonas Kvinge 72bccad82d Add accessible name for QToolButton css
Make sure it does not apply to other buttons.

Fixes #1255
2024-02-25 02:50:40 +01:00
Jonas Kvinge 6d52a2b409 QSearchField: Replace QToolButton with QPushButton 2024-02-25 02:46:51 +01:00
Jonas Kvinge 9b1035a5f2 Translations: Fix generating pot with NMake
Using GETTEXT_XGETTEXT_EXECUTABLE causes issues with NMake, possibly because of spaces in path.
2024-02-24 19:45:58 +01:00
Jonas Kvinge ce7c3e8039 CI: Use gh instead of hub 2024-02-24 01:18:02 +01:00
Jonas Kvinge 9baec288c3 CI: Set FFTW3_DIR 2024-02-23 22:57:00 +01:00
Jonas Kvinge 82894b94f4 CI: Set vsversion to 2022 2024-02-23 21:59:06 +01:00
Jonas Kvinge fb00d68aa7 Update Changelog 2024-02-20 22:47:55 +01:00
Jonas Kvinge 12288a2622 BackendSettingsPage: Fix enabling/disabling exclusive mode 2024-02-20 01:22:40 +01:00
Jonas Kvinge f84ce3f1d1 Add exclusive mode option for WASAPI 2024-02-20 01:08:00 +01:00
Jonas Kvinge 306b3f72d8 SettingsPage: Pass on scroll event to page
If the settings widget does not have focus, pass the event to the page for scrolling down instead of changing the setting.

Fixes #1380
2024-02-19 23:07:27 +01:00
buckmelanoma 593a04d047 InternetSearchView: Use DescriptionForSource instead of TextForSource
DescriptionForSource provides uppercase names in the filter menu instead of the lowercase names provided by TextForSource
2024-02-18 20:14:01 +01:00
buckmelanoma 667548f3ed InternetSongsView: Use DescriptionForSource instead of TextForSource
DescriptionForSource provides uppercase names in the filter menu instead of the lowercase names provided by TextForSource
2024-02-18 20:14:01 +01:00
Jonas Kvinge 8f89bf6402 Replace tabs with spaces 2024-02-18 14:24:20 +01:00
Jonas Kvinge ff28e7c86e Add ASIO device finder 2024-02-17 00:40:55 +01:00
Jonas Kvinge 67cc69179b nsi: Update gstreamer plugins 2024-02-16 21:51:45 +01:00
Jonas Kvinge 06fac2b7a3 CI: Copy all gstreamer plugins for mingw and msvc 2024-02-16 21:51:45 +01:00
Jonas Kvinge a354f6bdc5 GstEnginePipeline: Set device-clsid 2024-02-16 21:38:33 +01:00
Jonas Kvinge cb44c71733 DirectSoundDeviceFinder: Add waveformsink 2024-02-16 21:38:08 +01:00
Jonas Kvinge 6b1c14f875 GstEngine: Make sure asiosink is detected 2024-02-16 00:29:17 +01:00
Jonas Kvinge 7770aba877 GstEngine: Add pipewiresink 2024-02-14 18:46:23 +01:00
Jonas Kvinge 05381096aa RadioParadiseService: Use API to receive streams 2024-02-12 16:57:51 +01:00
Jonas Kvinge 6bdd9ad4dd GstEnginePipeline: Only hard-code playbin3 with gst 1.22 2024-02-11 23:52:22 +01:00
Jonas Kvinge 19836e8898 CI: Fix variable typo 2024-02-11 12:24:29 +01:00
Jonas Kvinge 5e4b193260 CI: Fix greping variable 2024-02-11 12:17:04 +01:00
Jonas Kvinge 923d0f2b7a CI: Use common SSH upload job 2024-02-11 12:12:54 +01:00
Jonas Kvinge 56f1a93c4e CI: Fix tag name variable typo 2024-02-11 01:19:39 +01:00
Jonas Kvinge 0168182af5 CddaDevice: Add missing override 2024-02-11 00:14:19 +01:00
Jonas Kvinge 679f0e1cd8 CI: Trigger on release 2024-02-11 00:10:36 +01:00
Adam Hill dd6b9bb38d MainWindow: Add function to display progress on taskbar 2024-02-09 21:48:12 +01:00
Jonas Kvinge 5bd8f35dc0 Update Changelog 2024-02-07 01:46:12 +01:00
Jonas Kvinge 53fc939e35 ScrobblingAPI20: Ignore permission related error
Last.fm returns permission denied error when servers are overloaded, ignore this error instead.

Fixes #442
2024-02-07 01:34:46 +01:00
Strawbs Bot 42d6c79710 Update translations 2024-02-01 21:43:21 +01:00
Jonas Kvinge 882869255b CI: Remove Ubuntu Lunar 2024-01-30 18:42:32 +01:00
Jonas Kvinge 19903751c1 CI: Codesign png.framework for macOS private 2024-01-27 17:39:26 +01:00
Jonas Kvinge 836074fb60 CI: Manually codesign png framework 2024-01-26 18:58:10 +01:00
Jonas Kvinge 5865a70c2e CI: Add --skip-broken to dnf install for Fedora 2024-01-26 00:29:25 +01:00
Jacob Henner fa057ee9d3 CollectionView: Add "search for this"
Add selection search in the collection view, to allow users to quickly
search for similarly-named songs, artists, albums, etc.
2024-01-25 19:16:14 +01:00
Jonas Kvinge 2d88fcb249 CI: Add Ubuntu Noble 2024-01-24 23:05:46 +01:00
Jonas Kvinge 920bb04b00 CMakeLists: Find TagLib using CMake 2024-01-24 21:30:10 +01:00
Jonas Kvinge a915e62e2c GPodDevice: Fix error message 2024-01-24 19:52:24 +01:00
Jonas Kvinge 226a6c50e0 Add better error messages for device and organize
Fixes #1364
2024-01-24 19:27:30 +01:00
Jonas Kvinge 269f13de76 MtpLoader: Allow empty artist 2024-01-24 19:21:02 +01:00
Jonas Kvinge 3ee796a663 macgstcopy: Add adaptivedemux2 and mpeg mux/demux 2024-01-22 19:26:21 +01:00
Jonas Kvinge d24af2f4db nsi: Add adaptivedemux2 and mpeg demux/mux plugins 2024-01-22 16:53:43 +01:00
Enrique Garcia f86c3cfd91 SubsonicSettingsPage: Add recommended to MD5 authentication 2024-01-21 04:25:31 +01:00
William Andrea 11061bdd07 CONTRIBUTING: Clarify "Commit messages" section.
Specifically:
- Clarify what "class" is referring to
- Use an actual example, which requires moving out two bits of explanation
2024-01-19 23:43:44 +01:00
Jonas Kvinge f901f802bb CollectionView: Implement add to playlist with enter
Fixes #1360
2024-01-19 23:04:03 +01:00
Jonas Kvinge a98c209101 AutoExpandingTreeView: Remove doubleClicked on enter 2024-01-19 23:03:37 +01:00
Jonas Kvinge 6a459682ca Playlist: Check for valid index
Fixes #1359
2024-01-19 22:37:14 +01:00
Jonas Kvinge 1a7194b3a9 Turn on git revision 2024-01-11 22:32:36 +01:00
Jonas Kvinge b6c9ef4a15 Release 1.0.23 2024-01-11 20:44:55 +01:00
Jonas Kvinge 20e550bc7d Update Changelog 2024-01-11 17:40:09 +01:00
Jonas Kvinge a4b7766947 DeviceManager: Add nullptr check for connected device
Possible fix for #1313
2024-01-11 17:03:00 +01:00
Jonas Kvinge 8d1a0071b6 Playlist: Use effective original year for sorting
Fixes #1349
2024-01-11 16:40:12 +01:00
Jonas Kvinge a3c00e607b README: Update sponsoring info 2024-01-09 18:08:40 +01:00
Jonas Kvinge de7ca8b736 CI: Enable OpenMandriva 2024-01-04 03:05:44 +01:00
Jonas Kvinge 04e593dc62 CollectionWatcher: Add unavailable song restored logging 2024-01-03 00:45:30 +01:00
Jonas Kvinge 2294c38aa9 CollectionBackend: Rename SqlQuery variable 2024-01-03 00:44:54 +01:00
Jonas Kvinge 6f41d39a9c CI: Remove Mageia from upload release 2024-01-02 23:47:15 +01:00
Jonas Kvinge 7c4e33b676 GstEngine: Treat all stream errors as non-fatal
Fixes #1347
2024-01-02 19:54:19 +01:00
Jonas Kvinge 4cc66bccad CI: Disable Mageia and OpenMandriva
OpenMandriva Cooker has package conflict issues. Mageia 8 is end of life and there is no docker image for Mageia 9 yet.
2024-01-02 19:49:10 +01:00
Jonas Kvinge 55b1d34f48 CI: Use unique artifact names 2024-01-01 06:56:53 +01:00
dependabot[bot] 4da0e8d8ad Bump actions/download-artifact from 3 to 4
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-01 04:48:34 +01:00
dependabot[bot] b95bfba676 Bump actions/upload-artifact from 3 to 4
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-01 04:48:19 +01:00
Jonas Kvinge 1ff2bfd390 Organize: Only update song path for collection songs
Possible fix for #1341
2023-12-28 23:30:07 +01:00
Jonas Kvinge a35fa5b158 Require KDSingleApplication 1.1.0 2023-12-26 23:30:25 +01:00
Strawbs Bot 22169bda0d Update translations 2023-12-13 23:59:26 +01:00
Jonas Kvinge faf5f6c69d CI: Remove Fedora 37 and add 40 2023-12-09 23:51:07 +01:00
Jonas Kvinge 1ae01d4078 Turn on git revision 2023-12-09 23:50:14 +01:00
Jonas Kvinge eb6289dd1c Release 1.0.22 2023-12-09 23:21:35 +01:00
Jonas Kvinge 671df6eafb Update Changelog 2023-12-09 23:17:19 +01:00
Jonas Kvinge 579563b32c
README: Fix emojis 2023-12-09 21:02:18 +01:00
Jonas Kvinge 135b93a5af StretchHeaderView: Set default section size
As of Qt 6.6.1, style changes are resetting the column sizes. To prevent this, we set a default section size.

Fixes #1328
2023-12-09 01:47:37 +01:00
BetterCallMolly 84c6e09c42 Player: Fix crossfade crash when decoding fails
When the decoding of a track fails, `current_item_` is set to an invalid address, if the Crossfade option is enabled, the `Player::TrackAboutToEnd` method does not check whether `current_item_` is a valid pointer or not, causing a segmentation fault.

Player: Removed extra space
2023-12-03 22:07:09 +01:00
Strawbs Bot b4f9808d11 Update translations 2023-12-02 04:30:54 +01:00
Jonas Kvinge d96d4224a2 Collection: Use normal pointer for watcher
Fixes #1316
2023-11-29 22:26:40 +01:00
Jonas Kvinge f65927e308 metatypes: Register QAbstractSocket::SocketState 2023-11-29 22:25:03 +01:00
Jonas Kvinge eeeea8566e nsi: Handle silent uninstall
Fixes #1323
2023-11-26 21:35:48 +01:00
Jonas Kvinge 54c42b276f GstEnginePipeline: Increase thread priority 2023-11-26 13:12:03 +01:00
Jonas Kvinge 8e4b4d6e41 nsi: Fix faad dll filename 2023-11-15 16:42:02 +01:00
Jonas Kvinge ac9fd9070f GstEnginePipeline: Only set max size buffer if > 0
Fixes #1302
2023-11-12 22:04:31 +01:00
Jonas Kvinge 6348649bc6 GstEnginePipeline: Run QTimer::singleShot in main thread
Partial fix for #1302
2023-11-12 21:57:59 +01:00
Strawbs Bot c95886d8db Update translations 2023-11-05 22:10:17 +01:00
Michał Walenciak 117b965a7b Translate number of tracks with nice plural forms 2023-11-05 19:37:56 +01:00
Michał Walenciak 1b6b5f9afa Use plurals 2023-11-05 19:37:56 +01:00
Michał Walenciak 83bc8d9e86 Select target language 2023-11-05 19:37:56 +01:00
Michał Walenciak 33f0421d3f Use 'n' for proper plurar form 2023-11-05 19:37:56 +01:00
Sergei B 661615e546 Allow drag and drop of songs to favorite playlists
- allows adding songs from active playlist
to any favorite by drag & drop
- after 500msec hovering with the songs over
desired playlist it becomes current
- drag & drop multiple songs is supported
2023-11-04 21:06:04 +01:00
Jonas Kvinge c52fc90306 CI: Don't run SSH upload or macOS codesign on forks 2023-11-03 22:47:25 +01:00
Jonas Kvinge eeb55fbc42 nsi: Update icu to 74 2023-11-01 23:07:47 +01:00
Jonas Kvinge 5d77eb1901 Change URL for KDSingleApplication submodule to https 2023-10-28 19:26:11 +02:00
Jonas Kvinge 654a94fe3d nsi: Add libabsl_kernel_timeout_internal.dll 2023-10-28 15:32:49 +02:00
Jonas Kvinge 4238708226 README: Update build instructions 2023-10-28 00:56:08 +02:00
Jonas Kvinge 5f02072bf3 CMake: Fix KDSingleApplication version check
Fixes #1300
2023-10-23 23:56:59 +02:00
Jonas Kvinge eec5b448b6 CI: Add fftw3-devel for openSUSE 2023-10-23 19:45:50 +02:00
Jonas Kvinge 5cda756f92 CI: Remove get release version for macOS private 2023-10-23 19:37:40 +02:00
Jonas Kvinge e9588dd85b CI: Remove SSH key setup from macOS private 2023-10-22 21:34:12 +02:00
Jonas Kvinge dda0e4aa06 CI: Use MACOS_KEYCHAIN_PASSWORD 2023-10-22 21:13:28 +02:00
Jonas Kvinge 2282406166 CI: Build on self-hosted runner only on private repo 2023-10-22 18:51:23 +02:00
Jonas Kvinge b3f0dee8e9 CI: Add "if: true" to easily disable steps 2023-10-22 17:20:28 +02:00
Jonas Kvinge f9f7381247 CI: Fix APPLE_DEVELOPER_ID echo 2023-10-22 17:15:49 +02:00
Jonas Kvinge 48685325e6 Use KDSingleApplication as a submodule 2023-10-22 16:32:55 +02:00
Jonas Kvinge e8bcaf415c CI: Only run SSH upload on this repo 2023-10-22 15:26:23 +02:00
Ondrej Mosnáček c9197e8df7 CMake: Fix KDSingleApplication package name for Qt5
The name of the Qt5 KDSingleApplication CMake package is just
"KDSingleApplication", not "KDSingleApplication-qt".

Signed-off-by: Ondrej Mosnáček <omosnacek@gmail.com>
2023-10-22 14:24:52 +02:00
Jonas Kvinge 2bb09cf575 Song: Handle MP2 in Song::FiletypeByDescription 2023-10-21 05:07:25 +02:00
Jonas Kvinge 7bf4ad3884 Song: Handle MP2 in Song::FiletypeByExtension 2023-10-21 04:59:50 +02:00
Jonas Kvinge 5154d7ac84 Song: Rename MP3 to MPEG 2023-10-21 04:59:33 +02:00
Jonas Kvinge 9299653722 Turn on git revision 2023-10-21 04:57:53 +02:00
Jonas Kvinge b0f0133d29 Release 1.0.21 2023-10-21 03:53:40 +02:00
Jonas Kvinge 9211b6f0c0 GstStartup: Remove macOS libsoup workarounds 2023-10-21 03:05:04 +02:00
Strawbs Bot ab6a0ed6dd Update translations 2023-10-21 01:33:19 +02:00
Jonas Kvinge c975c1e4aa CI: Remove unused homebrew and macports build 2023-10-20 00:53:49 +02:00
Jonas Kvinge f9a593dc74 CI: Remove conflicting files for MSVC 2023-10-19 23:25:06 +02:00
Jonas Kvinge 9151520d50 CI: Remove uninstalling mingw 2023-10-18 23:19:50 +02:00
Jonas Kvinge 8f72d877bd CI: Manually sign libraries missing signature 2023-10-18 20:47:17 +02:00
Jonas Kvinge e1990c9315 macgstcopy: Add rpath to gst plugin scanner 2023-10-18 20:46:24 +02:00
Jonas Kvinge 4cd5dcbfcf Update Changelog 2023-10-15 20:21:41 +02:00
Jonas Kvinge 3ee2125e8f CI: Verify code-signing 2023-10-15 16:51:13 +02:00
Jonas Kvinge 4652f3b449 CI: Manually codesign libsoup dependencies 2023-10-15 16:51:04 +02:00
Jonas Kvinge 697717eb1e CI: Use apple-actions/import-codesign-certs 2023-10-15 16:09:25 +02:00
Jonas Kvinge cbde71f5f1 CI: Remove macOS lconvert and jenkins workaround 2023-10-15 07:48:20 +02:00
Jonas Kvinge bf52afa21d GstStartup: Add back LIBSOUP3_LIBRARY_PATH 2023-10-15 07:46:49 +02:00
Jonas Kvinge 2083e008ab CI: Add macOS code-signing 2023-10-15 06:28:38 +02:00
Jonas Kvinge 2be0d23b1b macgstcopy: Change ID with install_name_tool 2023-10-14 23:24:56 +02:00
Jonas Kvinge fda56dda25 main: Use Utilities::SetEnv 2023-10-14 23:18:16 +02:00
Jonas Kvinge ed259781e9 CI: Remove rsync install and -DBUILD_WERROR 2023-10-14 23:17:58 +02:00
Jonas Kvinge 0c7fcd5a7a CMake: Fix USE_BUNDLE default 2023-10-14 22:09:14 +02:00
Jonas Kvinge 310b7b9065 CollectionQuery: Add F for float 2023-10-14 22:08:50 +02:00
Jonas Kvinge cd534bbda7 CMake: Remove USE_BUNDLE_DIR 2023-10-14 03:30:09 +02:00
Jonas Kvinge 1a66eaf7bf GstStartup: Refactor environment code 2023-10-14 03:29:54 +02:00
Jonas Kvinge a94d6e3dd8 CI: macgstcopy.sh copies libsoup now 2023-10-14 03:29:14 +02:00
Jonas Kvinge 54cfb2bbc4 main: Don't override library paths 2023-10-14 03:28:28 +02:00
Jonas Kvinge 00bc3f76cf macgstcopy: Copy libsoup 2023-10-14 03:27:57 +02:00
Jonas Kvinge 71a6d378d9 workerpool: Always search plugin path for tagreader on macOS 2023-10-14 03:27:32 +02:00
Jonas Kvinge 99a5aee8b3 GstEnginePipeline: Change debug logging for active/inactive 2023-10-13 23:38:19 +02:00
Jonas Kvinge 89d2a23dac CollectionBackend: Use QString::arg() 2023-10-13 23:06:29 +02:00
Jonas Kvinge ee1bf47f5c DeviceInfo: Simplify hint 2023-10-13 22:58:53 +02:00
Jonas Kvinge 13ac20f8b3 Add/remove reference for parameters 2023-10-13 22:58:18 +02:00
Jonas Kvinge adef05bbdf Use QString::arg() 2023-10-13 22:55:20 +02:00
Jonas Kvinge f03ff452b8 SavePlaylistsDialog: Add parent to ctor 2023-10-13 22:53:27 +02:00
Jonas Kvinge c39489060b Mpris2: Add static_cast 2023-10-13 22:52:36 +02:00
Jonas Kvinge 002fa8f4aa Fix mismatched definition 2023-10-13 22:49:20 +02:00
Jonas Kvinge d2c747258c Song: Add MPC to FiletypeByMimetype and FiletypeByDescription 2023-10-12 01:16:40 +02:00
Jonas Kvinge 53e3664726 CI: Install hub in upload release step 2023-10-11 17:37:47 +02:00
Jonas Kvinge 26ff9f6b53 Update Changelog 2023-10-10 23:44:34 +02:00
Jonas Kvinge f542f1c854 GstEnginePipeline: Remove volume sync for Auto
Workaround crash in #1123
2023-10-10 23:17:03 +02:00
Jonas Kvinge 33041ffa75 GstEnginePipeline: Delay seek when when resetting next URI
When seeking after the next URI is set, we set the state to READY to switch the URI back. The seek in after going to ready sometimes does not work, delay the seek to workaround this.

Fixes #1258
2023-10-10 23:00:11 +02:00
Jonas Kvinge 1493164df9 CollectionQuery: Strip off whitespaces after colon and simplify code
Fixes #1290
2023-10-10 19:15:20 +02:00
Strawbs Bot 8ffef558ff Update translations 2023-10-10 01:37:19 +02:00
Jonas Kvinge 8b3f44ffca Update Changelog 2023-10-10 01:32:33 +02:00
Jonas Kvinge 2706529006 DeviceDatabaseBackend: Add missing ebur128 fields 2023-10-10 01:27:39 +02:00
Jonas Kvinge 7e331a2055 DeviceManager: Fix creating connected device 2023-10-10 01:26:47 +02:00
Jonas Kvinge 505329730c Improve lyrics match 2023-10-08 23:55:05 +02:00
Jonas Kvinge 1a07404c10 Simplify RPM spec file 2023-10-08 16:15:59 +02:00
Jonas Kvinge b02adc7758 Simplify RPM spec file 2023-10-08 13:54:36 +02:00
Jonas Kvinge 952252ebcd Simplify RPM spec file 2023-10-08 02:55:22 +02:00
Jonas Kvinge eee0c40132 Playlist: Use InternetServicePtr 2023-10-07 17:05:51 +02:00
Jonas Kvinge 567bad33e1 Playlist: Use PlaylistItemPtr 2023-10-07 17:05:36 +02:00
Jonas Kvinge b5c0e93989 FancyTabWidget: Use QApplication::style(), not style() 2023-10-07 15:36:49 +02:00
Jonas Kvinge ac17df2a86 PlaylistContainer: Remove unused signals 2023-10-07 15:34:37 +02:00
Jonas Kvinge a9a5899252 FancyTabWidgetProxyStyle: Create proxy style from application style 2023-10-07 15:23:41 +02:00
Jonas Kvinge 395d85c1b4 Move PlaylistProxyStyle to it's own file 2023-10-07 15:16:39 +02:00
Jonas Kvinge 52ba1ce17f PlaylistView: Fix build with Qt 5 2023-10-07 15:04:00 +02:00
Jonas Kvinge 604a246fe8 PlaylistProxyStyle: Use CE_HeaderLabel instead of CE_Header 2023-10-07 14:50:46 +02:00
Jonas Kvinge e172c4871c PlaylistView: Create proxy style based on application style
Fixes #1275
2023-10-07 14:48:40 +02:00
Jonas Kvinge 2a9b32690d Update Changelog 2023-10-07 02:52:47 +02:00
Jonas Kvinge 76fa4745d0 GstEnginePipeline: Only update last known position when possible
Fixes flaky seeking where gst_element_query_position() returns -1 when seeking.
2023-10-07 02:47:12 +02:00
Strawbs Bot 6f4d26e9d3 Update translations 2023-10-04 00:33:42 +02:00
Jonas Kvinge f40f43861d EngineBase: Use enum class for TrackChangeType 2023-10-03 20:18:52 +02:00
Jonas Kvinge 717ebbbb24 CI: Install openssh-clients for openSUSE 2023-10-02 21:32:46 +02:00
Jonas Kvinge 79c69e1b1e CollectionWatcher: Match extension case-insensitive 2023-10-02 17:39:10 +02:00
Jonas Kvinge 8fc95e08dc CollectionWatcher: Ignore compressed files
Fixes #1274
2023-10-02 17:23:47 +02:00
Jonas Kvinge 3f06528ba3 Fix version without git tags 2023-10-02 17:04:15 +02:00
Jonas Kvinge 0e44b10eec Add Ko-fi to funding options 2023-09-28 16:55:25 +02:00
Jonas Kvinge d74fe92ce8 Use system KDSingleApplication when available 2023-09-27 20:19:43 +02:00
Jonas Kvinge 8037948f7f Dmg: Remove extra macdeployqt executable parameters
macdeployqt is patched and should handle .so libraries too now.
2023-09-26 23:46:11 +02:00
Jonas Kvinge ab29170972 CI: Disable macOS homebrew build 2023-09-26 23:45:11 +02:00
Jonas Kvinge 6f843e2499 CI: Disable macports build
Issue #1278
2023-09-26 18:22:43 +02:00
Jonas Kvinge 6b8a816ce6 macgstcopy: Check for both .dylib and .so extensions for plugins
Require at least coreelements to be found in plugins directory
2023-09-26 17:04:20 +02:00
Jonas Kvinge 2c0541fb79 CI: Delete conflicting ICU case-insensitive 2023-09-26 00:34:13 +02:00
Jonas Kvinge cb22890d79 CI: Simplify setting CMAKE_BUILD_TYPE and ENABLE_WIN32_CONSOLE 2023-09-26 00:34:13 +02:00
Jonas Kvinge 5a9346cc80 Add libebur128 to strawberry.spec 2023-09-25 20:50:43 +02:00
Jonas Kvinge 1c85220ffa Add libebur128-dev to debian control 2023-09-25 20:50:43 +02:00
Jonas Kvinge 39d4818def CI: Fix PPA upload 2023-09-25 20:16:26 +02:00
Jonas Kvinge db0cc66ba0 CI: Fix Mageia rpm upload 2023-09-25 20:15:56 +02:00
Strawbs Bot c10a64f08a Update translations 2023-09-25 02:37:58 +02:00
Jonas Kvinge 5a3d60b203 Turn on git revision 2023-09-25 00:10:41 +02:00
586 changed files with 40649 additions and 26045 deletions

1
.github/FUNDING.yml vendored
View File

@ -1,3 +1,4 @@
github: jonaski
patreon: jonaskvinge
ko_fi: jonaskvinge
custom: https://paypal.me/jonaskvinge

File diff suppressed because it is too large Load Diff

4
.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule "3rdparty/kdsingleapplication/KDSingleApplication"]
path = 3rdparty/kdsingleapplication/KDSingleApplication
url = https://github.com/KDAB/KDSingleApplication.git
branch = master

View File

@ -62,7 +62,7 @@ enum ENUM_ORDERING {
//
//
// Ansi structures and functions follow
// Ansi structures and functions follow
//
//
@ -409,7 +409,7 @@ int _getopt_long_only_r_a(int argc, char *const *argv, const char *options, cons
//
//
// Unicode Structures and Functions
// Unicode Structures and Functions
//
//
@ -750,4 +750,4 @@ int _getopt_long_r_w(int argc, wchar_t *const *argv, const wchar_t *options, con
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,6 +1,6 @@
cmake_minimum_required(VERSION 3.7)
set(SOURCES kdsingleapplication.cpp kdsingleapplication_localsocket.cpp)
set(HEADERS kdsingleapplication.h kdsingleapplication_localsocket_p.h)
set(SOURCES KDSingleApplication/src/kdsingleapplication.cpp KDSingleApplication/src/kdsingleapplication_localsocket.cpp)
set(HEADERS KDSingleApplication/src/kdsingleapplication.h KDSingleApplication/src/kdsingleapplication_localsocket_p.h)
qt_wrap_cpp(MOC ${HEADERS})
add_library(kdsingleapplication STATIC ${SOURCES} ${MOC})
target_compile_definitions(kdsingleapplication PRIVATE -DKDSINGLEAPPLICATION_STATIC_BUILD)

@ -0,0 +1 @@
Subproject commit cb0c664b40d3b31bad30aa3521eff603162ed0bd

View File

@ -1,6 +0,0 @@
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.

View File

@ -1,11 +0,0 @@
Copyright (c) <year> <owner>.
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.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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.

View File

@ -1,9 +0,0 @@
MIT License
Copyright (c) <year> <copyright holders>
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.

View File

@ -1,53 +0,0 @@
# 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

@ -1,106 +0,0 @@
/*
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

@ -1,48 +0,0 @@
/*
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

@ -1,23 +0,0 @@
/*
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

@ -1,324 +0,0 @@
/*
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
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments"
#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);
}
#ifdef __clang__
#pragma clang diagnostic pop
#endif

View File

@ -1,126 +0,0 @@
/*
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

@ -99,23 +99,14 @@ if(CCACHE_EXECUTABLE)
SET_PROPERTY(GLOBAL PROPERTY RULE_LAUNCH_LINK ${CCACHE_EXECUTABLE})
endif()
option(USE_ICU "Use ICU" ON)
find_package(PkgConfig REQUIRED)
find_package(Boost REQUIRED)
find_package(Threads)
find_package(Threads REQUIRED)
find_package(Backtrace)
if(Backtrace_FOUND)
set(HAVE_BACKTRACE ON)
endif()
if(USE_ICU)
find_package(ICU COMPONENTS uc i18n REQUIRED)
if(ICU_FOUND)
set(HAVE_ICU ON)
endif()
else()
find_package(Iconv)
endif()
find_package(Boost REQUIRED)
find_package(ICU COMPONENTS uc i18n REQUIRED)
find_package(Protobuf CONFIG)
if(NOT Protobuf_FOUND)
find_package(Protobuf REQUIRED)
@ -266,25 +257,19 @@ if(X11_FOUND)
endif(X11_FOUND)
option(USE_TAGLIB "Build with TagLib" OFF)
option(USE_TAGLIB "Build with TagLib" ON)
option(USE_TAGPARSER "Build with TagParser" OFF)
if(NOT USE_TAGLIB AND NOT USE_TAGPARSER)
set(USE_TAGLIB ON)
endif()
# TAGLIB
if(USE_TAGLIB)
pkg_check_modules(TAGLIB REQUIRED taglib>=1.11.1)
if(TAGLIB_FOUND)
find_path(HAVE_TAGLIB_DSFFILE_H taglib/dsffile.h)
find_path(HAVE_TAGLIB_DSDIFFFILE_H taglib/dsdifffile.h)
if(HAVE_TAGLIB_DSFFILE_H)
set(HAVE_TAGLIB_DSFFILE ON)
endif(HAVE_TAGLIB_DSFFILE_H)
if(HAVE_TAGLIB_DSDIFFFILE_H)
set(HAVE_TAGLIB_DSDIFFFILE ON)
endif(HAVE_TAGLIB_DSDIFFFILE_H)
find_package(TagLib 2.0)
if(TARGET TagLib::TagLib)
set(TAGLIB_FOUND ON)
set(TAGLIB_LIBRARIES TagLib::TagLib)
set(HAVE_TAGLIB_DSFFILE ON)
set(HAVE_TAGLIB_DSDIFFFILE ON)
else()
pkg_check_modules(TAGLIB REQUIRED taglib>=1.11.1)
endif()
endif()
@ -300,10 +285,28 @@ if(NOT TAGLIB_FOUND AND NOT TAGPARSER_FOUND)
endif()
# SingleApplication
add_subdirectory(3rdparty/kdsingleapplication)
set(SINGLEAPPLICATION_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/kdsingleapplication)
set(SINGLEAPPLICATION_LIBRARIES kdsingleapplication)
add_definitions(-DKDSINGLEAPPLICATION_STATIC_BUILD)
if(QT_VERSION_MAJOR EQUAL 5)
set(KDSINGLEAPPLICATION_NAME "KDSingleApplication")
else()
set(KDSINGLEAPPLICATION_NAME "KDSingleApplication-qt${QT_VERSION_MAJOR}")
endif()
find_package(${KDSINGLEAPPLICATION_NAME} 1.1.0)
if(TARGET KDAB::kdsingleapplication)
if(QT_VERSION_MAJOR EQUAL 5)
set(KDSINGLEAPPLICATION_VERSION "${KDSingleApplication_VERSION}")
elseif(QT_VERSION_MAJOR EQUAL 6)
set(KDSINGLEAPPLICATION_VERSION "${KDSingleApplication-qt6_VERSION}")
endif()
message(STATUS "Using system KDSingleApplication (Version ${KDSINGLEAPPLICATION_VERSION})")
set(SINGLEAPPLICATION_LIBRARIES KDAB::kdsingleapplication)
else()
message(STATUS "Using 3rdparty KDSingleApplication")
set(HAVE_KDSINGLEAPPLICATION_OPTIONS ON)
add_subdirectory(3rdparty/kdsingleapplication)
set(SINGLEAPPLICATION_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/kdsingleapplication/KDSingleApplication/src)
set(SINGLEAPPLICATION_LIBRARIES kdsingleapplication)
add_definitions(-DKDSINGLEAPPLICATION_STATIC_BUILD)
endif()
if(APPLE)
add_subdirectory(3rdparty/SPMediaKeyTap)
@ -427,6 +430,7 @@ option(INSTALL_TRANSLATIONS "Install translations" OFF)
optional_component(SUBSONIC ON "Streaming: Subsonic")
optional_component(TIDAL ON "Streaming: Tidal")
optional_component(SPOTIFY ON "Streaming: Spotify" DEPENDS "gstreamer" GSTREAMER_FOUND)
optional_component(QOBUZ ON "Streaming: Qobuz")
optional_component(MOODBAR ON "Moodbar"
@ -440,19 +444,11 @@ optional_component(EBUR128 ON "EBU R 128 loudness normalization"
)
if(APPLE OR WIN32)
option(USE_BUNDLE "Bundle dependencies" ON)
set(USE_BUNDLE_DEFAULT ON)
else()
option(USE_BUNDLE "Bundle dependencies" OFF)
endif()
if(USE_BUNDLE AND NOT USE_BUNDLE_DIR)
if(LINUX)
set(USE_BUNDLE_DIR "../plugins")
endif()
if(APPLE)
set(USE_BUNDLE_DIR "../PlugIns")
endif()
set(USE_BUNDLE_DEFAULT OFF)
endif()
option(USE_BUNDLE "Bundle dependencies" ${USE_BUNDLE_DEFAULT})
if(NOT CMAKE_CROSSCOMPILING)
# Check that we have Qt with sqlite driver
@ -472,25 +468,6 @@ if(NOT CMAKE_CROSSCOMPILING)
"
QT_SQLITE_TEST
)
if(QT_SQLITE_TEST)
# Check that we have sqlite3 with FTS5
check_cxx_source_runs("
#include <QSqlDatabase>
#include <QSqlQuery>
int main() {
QSqlDatabase db = QSqlDatabase::addDatabase(\"QSQLITE\");
db.setDatabaseName(\":memory:\");
if (!db.open()) { return 1; }
QSqlQuery q(db);
q.prepare(\"CREATE VIRTUAL TABLE test_fts USING fts5(test, tokenize = 'unicode61 remove_diacritics 0');\");
if (!q.exec()) return 1;
}
"
SQLITE_FTS5_TEST
)
endif()
unset(CMAKE_REQUIRED_FLAGS)
unset(CMAKE_REQUIRED_LIBRARIES)
endif()
# Set up definitions
@ -504,6 +481,8 @@ add_definitions(
-DQT_NO_CAST_TO_ASCII
-DQT_NO_NARROWING_CONVERSIONS_IN_CONNECT
-DQT_NO_FOREACH
-DQT_ASCII_CAST_WARNINGS
-DQT_NO_CAST_FROM_ASCII
)
if(WIN32)
@ -549,11 +528,7 @@ if(QT_VERSION_MAJOR EQUAL 5)
endif()
if(NOT CMAKE_CROSSCOMPILING)
if(QT_SQLITE_TEST)
if(NOT SQLITE_FTS5_TEST)
message(WARNING "sqlite must be enabled with FTS5. See: https://www.sqlite.org/fts5.html")
endif()
else()
if(NOT QT_SQLITE_TEST)
message(WARNING "The Qt sqlite driver test failed.")
endif()
endif()

View File

@ -48,24 +48,27 @@ small as possible.
### 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
The first line should start with the name of the class that is changed
followed by a colon then a short explanation of the commit.
Don't use a trailing period after the first line.
If this change affects more than one class, omit the class name 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.
It should explain exactly what's changed, why it's changed,
and what bugs were fixed.
An example of the expected format for git commit messages is as follows:
```
class: Short explanation of the commit
StretchHeaderView: Set default section size
Longer explanation explaining exactly what's changed, why it's changed,
and what bugs were fixed.
As of Qt 6.6.1, style changes are resetting the column sizes. To prevent this, we set a default section size.
Fixes #1234
Fixes #1328
```

101
Changelog
View File

@ -2,6 +2,107 @@ Strawberry Music Player
=======================
ChangeLog
Unreleased:
Bugfixes:
* Fixed crash when pressing CTRL + C (#1359).
* Pass on scroll events to page in settings to avoid changing settings when scrolling with mouse (#1380).
* Fixed misredered playlist search field with Wayland using scaling (#1255).
* Fixed Azlyrics lyrics provider because of website changes.
* Fixed Musixmatch lyrics provider because of website changes.
* Fixed application exiting when closing file dialog (#1401).
* Fixed playlist shuffle randomness (#707).
* Fixed playlist shuffle order always the same when restarting playback (#1381).
* Fixed dynamic random mix not always ignoring shuffle and repeat mode (#1366).
* Fixed manual shuffle while playing not setting current song to new index (##1353).
* Fixed volume sync with PA when output is set to auto (#1123).
* Fixed mpris:trackid type with KDE 6 (#1397).
* Fixed open in file manager feature not handling missing XDG_DATA_DIRS variable.
* Fixed collection pixmap disk cache and moodbar cache with newer Qt versions.
* Fixed reading common metadata CUE's with multiple files (#1463).
* Fixed playlist header stretch mode to only resize the right column of the column being resized.
* Fixed adding columns to playlist header not working when not using stretch mode (#1085).
* Removed -new-window parameter from dolphin command for open in file manager (#1412).
* Only use playbin3 with GStreamer 1.24 and higher, not with GStreamer 1.22 or lower.
* (macOS/Windows) Fixed dash and hls streaming, plugins were missing.
* (Windows) Fixed incorrect colors in smart playlist wizard with Fusion in dark mode (#1399).
Enhancements:
* Improve error messages when connecting and copying to devices.
* Allow enter to be used with multiselection to add songs to playlist (#1360)
* Add song progress to taskbar using D-Bus.
* Use API to receive Radio Paradise channels.
* Added letras lyrics provider.
* Added Open Tidal API (openapi.tidal.com) cover provider.
* Added button for fetching lyrics to tag editor (#1391).
* Added option not to skip "A", "An" and "The” when sorting artist names in collection (#1393).
* Improved album and title disc, remastered, etc matching and stripping (#1387).
* Save volume to settings when adjusting (#1272).
* Resolve song from collection using track with Cue in XSPF (#1181).
* Added background image to sidebar.
* Read metadata from RIFF WAV files (#1424).
* Use original path instead of canonical path when adding directories to the collection.
* Only apply added/removed collection directories when settings are saved.
* Detect and handle different text encodings when reading CUE files (#1429).
* The collection has been rewritten and improved (model/filter/search) (#392).
* (Unix) Add experimental GStreamer pipewire support.
* (Windows) Add experimental exclusive mode for WASAPI.
* (Windows MSVC) Add ASIO support.
* (Windows MSVC) Add back WASAPI2.
New features:
* WaveRubber analyzer.
* Spotify support.
Removed features:
* Removed now broken lyricsmode.com lyrics provider because of website changes.
Version 1.0.23 (2024.01.11):
Bugfixes:
* Fixed possible duplication of song entries after organizing (#1341).
* Fixed possible crash when connecting devices (#1313).
* Fixed playlist sorting of original year (#1349).
* (macOS) Fixed crash when adding collection directory (QTBUG-120469) (#1350).
Enhancements:
* Treat all stream errors as non-fatal (#1347).
* Require KDSingleApplication 1.1.0.
* Fix logging of restored unavailable songs.
Version 1.0.22 (2023.12.09):
Bugfixes:
* Fixed KDSingleApplication cmake version check.
* Fixed KDSingleApplication Qt 5 detection (#1299).
* Fixed timer started in wrong thread (#1302).
* Fixed erratic seeking behaviour if buffer duration is set to zero (#1302).
* Fixed SCollection related crash on exit with Qt 5 (#1316).
* Fixed track about to end related crash on playback failure (#1332).
* Fixed playlist column widths not remembered if stretch mode is off with Qt 6.6.1 and higher (#1328).
* (Windows) Properly handle silent uninstall (#1323).
Enhancements:
* Increase thread priority for playback threads.
* Allow drag and drop of songs to favorite playlists.
Version 1.0.21 (2023.10.21):
Bugfixes:
* Fixed seekbar position resetting to zero before showing actual position when seeking.
* Fixed compressed files showing up in collection (#1274).
* Fixed connecting devices (#1288).
* Fixed device schema missing ebur128 fields.
* Fixed collection search by tag not working with space between colon and search term (#1290).
* Fixed seeking when 5 seconds is remaining of the song resetting position to beginning (#1258).
* Fixed intermittent crash when seeking with Auto as output (#1123).
* (Windows) Fixed playlist header colors in dark mode (#1275).
Enhancements:
* Support using system KDSingleApplication when available.
* Improved lyrics matching.
* (macOS) Fully codesign binaries and DMG.
Version 1.0.20 (2023.09.24):
Bugfixes:

View File

@ -1,4 +1,4 @@
:strawberry: Strawberry Music Player [![Build Status](https://github.com/strawberrymusicplayer/strawberry/workflows/build/badge.svg)](https://github.com/strawberrymusicplayer/strawberry/actions)
:strawberry: Strawberry Music Player [![Build Status](https://github.com/strawberrymusicplayer/strawberry/workflows/Build/badge.svg)](https://github.com/strawberrymusicplayer/strawberry/actions)
=======================
[![Sponsor](https://img.shields.io/badge/-Sponsor-green?logo=github)](https://github.com/sponsors/jonaski)
[![Patreon](https://img.shields.io/badge/patreon-donate-green.svg)](https://patreon.com/jonaskvinge)
@ -19,9 +19,8 @@ Resources:
* openSUSE buildservice: https://build.opensuse.org/package/show/home:jonaski:audio/strawberry
* Ubuntu PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry
* Ubuntu Unstable PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry-unstable
* Translations: https://translate.zanata.org/iteration/view/strawberry/master
### :bangbang: Opening an issue:
### :bangbang: Opening an issue
* Read the FAQ: https://wiki.strawberrymusicplayer.org/wiki/FAQ
* Search for the issue to see if it is already solved, or if there is an open issue for it already. If there is an open issue already, you can comment on it if you have additional information that could be useful to us.
@ -29,18 +28,19 @@ Resources:
* We do not take feature requests from users on GitHub. Any issues related to feature requests will be closed. This does not necessarily mean that we won't add new features, but we don't have time to take feature requests or answer questions about new features from users. It is still possible to suggest or discuss new features on the forum (https://forum.strawberrymusicplayer.org/).
* We do not maintain the Flatpak package. Do not report issues related to Flatpak unless the issue can be reproduced with a native package, use Flatpak support instead https://flatpak.org/about/
### :moneybag: Sponsoring:
### :moneybag: Sponsoring
The program is free software, released under GPL. If you like this program and can make use of it, consider sponsoring or donating to help fund the project.
There are currently 3 options for sponsoring:
There are currently 4 options for sponsoring:
1. [GitHub Sponsors](https://github.com/sponsors/jonaski)
1. [GitHub](https://github.com/sponsors/jonaski)
2. [Patreon](https://www.patreon.com/jonaskvinge)
3. [PayPal](https://paypal.me/jonaskvinge)
3. [Ko-fi](https://ko-fi.com/jonaskvinge)
4. [PayPal](https://paypal.me/jonaskvinge)
Funding developers is a way to contribute to open source projects you appreciate, it helps developers get the resources they need, and recognize contributors working behind the scenes to make open source better for everyone.
### :heavy_check_mark: Features:
### :heavy_check_mark: Features
* Play and organize music
* Supports WAV, FLAC, WavPack, Ogg FLAC, Ogg Vorbis, Ogg Opus, Ogg Speex, MPC, TrueAudio, AIFF, MP4, MP3, ASF and Monkey's Audio.
@ -53,18 +53,18 @@ 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 [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics.com](https://www.lololyrics.com/), [songlyrics.com](https://www.songlyrics.com/), [azlyrics.com](https://www.azlyrics.com/), [elyrics.net](https://www.elyrics.net/) and [lyricsmode.com](https://www.lyricsmode.com/)
* Song lyrics from [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics.com](https://www.lololyrics.com/), [songlyrics.com](https://www.songlyrics.com/), [azlyrics.com](https://www.azlyrics.com/) and [elyrics.net](https://www.elyrics.net/)
* Support for multiple backends
* Audio analyzer
* Audio equalizer
* Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
* Scrobbler with support for [Last.fm](https://www.last.fm/), [Libre.fm](https://libre.fm/) and [ListenBrainz](https://listenbrainz.org/)
* Subsonic, Tidal and Qobuz streaming support
* Subsonic, Tidal, Spotify and Qobuz streaming support
It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows.
**macOS releases are currently limited to sponsors. This is because macOS releases require a developer account, Apple hardware and maintaining all libraries strawberry depends on. If you are sponsoring strawberry, e-mail support@strawberrymusicplayer.org for access to downloads.**
**macOS releases are currently limited to sponsors. This is because Strawberry mainly has one contributor/developer and supporting macOS requires Apple hardware, building libraries Strawberry depends and a Apple developer account for signing releases. If you are sponsoring strawberry through Patreon, releases are available directly on Patreon, if you are sponsoring through GitHub, Ko-fi or Paypal, please e-mail support@strawberrymusicplayer.org for access to downloads.**
### :heavy_exclamation_mark: Requirements
@ -76,7 +76,7 @@ To build Strawberry from source you need the following installed on your system
* [Boost](https://www.boost.org/)
* [GLib](https://developer.gnome.org/glib/)
* [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)
* [SQLite 3.9 or newer](https://www.sqlite.org)
* [Protobuf](https://developers.google.com/protocol-buffers/)
* [ALSA (Required on Linux)](https://www.alsa-project.org/)
* [D-Bus (Required on Linux)](https://www.freedesktop.org/wiki/Software/dbus/)
@ -96,11 +96,11 @@ Optional dependencies:
You should also install the gstreamer plugins base and good, and optionally bad, ugly and libav to support all audio formats.
### :wrench: Compiling from source
### :wrench: Compiling from source
### Get the code:
git clone https://github.com/strawberrymusicplayer/strawberry
git clone --recursive https://github.com/strawberrymusicplayer/strawberry
### Compile and install:
@ -117,6 +117,6 @@ Strawberry is backwards compatible with Qt 5, to compile with Qt 5 use:
To compile on Windows with Visual Studio 2019 or 2022, see https://github.com/strawberrymusicplayer/strawberry-msvc
### :penguin: Packaging status
### :penguin: Packaging status
[![Packaging status](https://repology.org/badge/vertical-allrepos/strawberry.svg?exclude_unsupported=1)](https://repology.org/metapackage/strawberry/versions)

View File

@ -27,7 +27,7 @@ if(MACDEPLOYQT_EXECUTABLE)
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/Info.plist ${CMAKE_BINARY_DIR}/strawberry.app/Contents/
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/strawberry.icns ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources/
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh ${CMAKE_BINARY_DIR}/strawberry.app
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/strawberry-tagreader -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner -executable=strawberry.app/Contents/PlugIns/gio-modules/libgioopenssl.so -executable=strawberry.app/Contents/PlugIns/gio-modules/libgiognutls.so ${MACDEPLOYQT_CODESIGN}
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/strawberry-tagreader -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner ${MACDEPLOYQT_CODESIGN}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
DEPENDS strawberry strawberry-tagreader
)

View File

@ -1,21 +1,33 @@
find_program(GETTEXT_XGETTEXT_EXECUTABLE xgettext)
if(NOT GETTEXT_XGETTEXT_EXECUTABLE)
message(FATAL_ERROR "Could not find xgettext executable")
endif(NOT GETTEXT_XGETTEXT_EXECUTABLE)
find_program(GETTEXT_XGETTEXT_EXECUTABLE xgettext REQUIRED)
find_program(CAT_EXECUTABLE cat REQUIRED)
set (XGETTEXT_OPTIONS
--qt
--keyword=tr:1,2c
--keyword=tr --flag=tr:1:pass-c-format --flag=tr:1:pass-qt-format
--keyword=trUtf8 --flag=tr:1:pass-c-format --flag=tr:1:pass-qt-format
--keyword=translate:2,3c
--keyword=translate:2 --flag=translate:2:pass-c-format --flag=translate:2:pass-qt-format
--keyword=QT_TR_NOOP --flag=QT_TR_NOOP:1:pass-c-format --flag=QT_TR_NOOP:1:pass-qt-format
--keyword=QT_TRANSLATE_NOOP:2 --flag=QT_TRANSLATE_NOOP:2:pass-c-format --flag=QT_TRANSLATE_NOOP:2:pass-qt-format
--keyword=_ --flag=_:1:pass-c-format --flag=_:1:pass-qt-format
--keyword=N_ --flag=N_:1:pass-c-format --flag=N_:1:pass-qt-format
--from-code=utf-8
)
list(APPEND XGETTEXT_OPTIONS
--qt
--keyword=tr:1,2c
--keyword=tr
--flag=tr:1:pass-c-format
--flag=tr:1:pass-qt-format
--keyword=trUtf8
--flag=tr:1:pass-c-format
--flag=tr:1:pass-qt-format
--keyword=translate:2,3c
--keyword=translate:2
--flag=translate:2:pass-c-format
--flag=translate:2:pass-qt-format
--keyword=QT_TR_NOOP
--flag=QT_TR_NOOP:1:pass-c-format
--flag=QT_TR_NOOP:1:pass-qt-format
--keyword=QT_TRANSLATE_NOOP:2
--flag=QT_TRANSLATE_NOOP:2:pass-c-format
--flag=QT_TRANSLATE_NOOP:2:pass-qt-format
--keyword=_
--flag=_:1:pass-c-format
--flag=_:1:pass-qt-format
--keyword=N_
--flag=N_:1:pass-c-format
--flag=N_:1:pass-qt-format
--from-code=utf-8
)
execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/translations)
@ -32,7 +44,7 @@ macro(add_pot outfiles header pot)
add_custom_command(
OUTPUT ${pot}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND ${GETTEXT_XGETTEXT_EXECUTABLE} ${XGETTEXT_OPTIONS} -s -C --omit-header --output=${CMAKE_CURRENT_BINARY_DIR}/pot.temp ${add_pot_sources}
COMMAND xgettext ${XGETTEXT_OPTIONS} -s -C --omit-header --output="${CMAKE_CURRENT_BINARY_DIR}/pot.temp" ${add_pot_sources}
COMMAND cat ${header} ${CMAKE_CURRENT_BINARY_DIR}/pot.temp > ${pot}
DEPENDS ${add_pot_sources} ${header}
)
@ -59,7 +71,7 @@ macro(add_po outfiles po_prefix)
# Convert the .po files to .qm files
add_custom_command(
OUTPUT ${_qm_filepath}
COMMAND ${QT_LCONVERT_EXECUTABLE} ARGS ${_po_filepath} -o ${_qm_filepath} -of qm
COMMAND ${QT_LCONVERT_EXECUTABLE} ARGS ${_po_filepath} -o ${_qm_filepath} -of qm -target-language ${_lang}
DEPENDS ${_po_filepath} ${_po_filepath}
)

View File

@ -1,9 +1,9 @@
set(STRAWBERRY_VERSION_MAJOR 1)
set(STRAWBERRY_VERSION_MINOR 0)
set(STRAWBERRY_VERSION_PATCH 20)
set(STRAWBERRY_VERSION_PATCH 23)
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
set(INCLUDE_GIT_REVISION OFF)
set(INCLUDE_GIT_REVISION ON)
set(majorminorpatch "${STRAWBERRY_VERSION_MAJOR}.${STRAWBERRY_VERSION_MINOR}.${STRAWBERRY_VERSION_PATCH}")
@ -25,7 +25,7 @@ if(INCLUDE_GIT_REVISION AND EXISTS "${CMAKE_SOURCE_DIR}/.git")
find_program(GIT_EXECUTABLE git)
if(NOT GIT_EXECUTABLE OR GIT_EXECUTABLE-NOTFOUND)
message(FATAL_ERROR "Missing GIT executable." )
message(FATAL_ERROR "Missing Git executable." )
endif()
# Get the current working branch
@ -48,7 +48,7 @@ if(INCLUDE_GIT_REVISION AND EXISTS "${CMAKE_SOURCE_DIR}/.git")
)
if(NOT ${GIT_CMD_RESULT_REVISION} EQUAL 0)
message(FATAL_ERROR "GIT command failed to get revision string '${GIT_REVISION}'")
message(FATAL_ERROR "Git command failed to get revision string '${GIT_REVISION}'")
endif()
endif()
@ -67,7 +67,7 @@ if(GIT_REVISION)
list(LENGTH GIT_PARTS GIT_PARTS_LENGTH)
if(NOT GIT_PARTS_LENGTH EQUAL 3)
message(FATAL_ERROR "Failed to parse git revision string '${GIT_REVISION}'")
set(GIT_PARTS "${majorminorpatch};0;${GIT_REVISION}")
endif()
list(GET GIT_PARTS 0 GIT_TAGNAME)

View File

@ -10,6 +10,8 @@
<file>schema/schema-16.sql</file>
<file>schema/schema-17.sql</file>
<file>schema/schema-18.sql</file>
<file>schema/schema-19.sql</file>
<file>schema/schema-20.sql</file>
<file>schema/device-schema.sql</file>
<file>style/strawberry.css</file>
<file>style/smartplaylistsearchterm.css</file>
@ -42,5 +44,6 @@
<file>pictures/star-off.png</file>
<file>mood/sample.mood</file>
<file>text/ghosts.txt</file>
<file>pictures/sidebar-background.png</file>
</qresource>
</RCC>

View File

@ -91,6 +91,7 @@
<file>icons/128x128/love.png</file>
<file>icons/128x128/subsonic.png</file>
<file>icons/128x128/tidal.png</file>
<file>icons/128x128/spotify.png</file>
<file>icons/128x128/qobuz.png</file>
<file>icons/128x128/multimedia-player-ipod-standard-black.png</file>
<file>icons/128x128/radio.png</file>
@ -189,6 +190,7 @@
<file>icons/64x64/love.png</file>
<file>icons/64x64/subsonic.png</file>
<file>icons/64x64/tidal.png</file>
<file>icons/64x64/spotify.png</file>
<file>icons/64x64/qobuz.png</file>
<file>icons/64x64/multimedia-player-ipod-standard-black.png</file>
<file>icons/64x64/radio.png</file>
@ -290,8 +292,9 @@
<file>icons/48x48/moodbar.png</file>
<file>icons/48x48/love.png</file>
<file>icons/48x48/subsonic.png</file>
<file>icons/48x48/tidal.png</file>
<file>icons/48x48/qobuz.png</file>
<file>icons/48x48/tidal.png</file>
<file>icons/48x48/spotify.png</file>
<file>icons/48x48/qobuz.png</file>
<file>icons/48x48/multimedia-player-ipod-standard-black.png</file>
<file>icons/48x48/radio.png</file>
<file>icons/48x48/somafm.png</file>
@ -393,6 +396,7 @@
<file>icons/32x32/love.png</file>
<file>icons/32x32/subsonic.png</file>
<file>icons/32x32/tidal.png</file>
<file>icons/32x32/spotify.png</file>
<file>icons/32x32/qobuz.png</file>
<file>icons/32x32/multimedia-player-ipod-standard-black.png</file>
<file>icons/32x32/radio.png</file>
@ -495,6 +499,7 @@
<file>icons/22x22/love.png</file>
<file>icons/22x22/subsonic.png</file>
<file>icons/22x22/tidal.png</file>
<file>icons/22x22/spotify.png</file>
<file>icons/22x22/qobuz.png</file>
<file>icons/22x22/multimedia-player-ipod-standard-black.png</file>
<file>icons/22x22/radio.png</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
data/icons/full/spotify.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -18,7 +18,7 @@ CREATE TABLE device_%deviceid_songs (
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT 0,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
@ -83,7 +83,10 @@ CREATE TABLE device_%deviceid_songs (
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL
);
@ -96,4 +99,4 @@ CREATE VIRTUAL TABLE device_%deviceid_fts USING fts5(
tokenize = "unicode61 remove_diacritics 1"
);
UPDATE devices SET schema_version=4 WHERE ROWID=%deviceid;
UPDATE devices SET schema_version=5 WHERE ROWID=%deviceid;

19
data/schema/schema-19.sql Normal file
View File

@ -0,0 +1,19 @@
DROP TABLE IF EXISTS %allsongstables_fts;
DROP TABLE IF EXISTS songs_fts;
DROP TABLE IF EXISTS subsonic_songs_fts;
DROP TABLE IF EXISTS tidal_artists_songs_fts;
DROP TABLE IF EXISTS tidal_albums_songs_fts;
DROP TABLE IF EXISTS tidal_songs_fts;
DROP TABLE IF EXISTS qobuz_artists_songs_fts;
DROP TABLE IF EXISTS qobuz_albums_songs_fts;
DROP TABLE IF EXISTS qobuz_songs_fts;
UPDATE schema_version SET version=19;

244
data/schema/schema-20.sql Normal file
View File

@ -0,0 +1,244 @@
CREATE TABLE IF NOT EXISTS spotify_artists_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
fingerprint TEXT,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
lastseen INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_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,
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,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL
);
CREATE TABLE IF NOT EXISTS spotify_albums_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
fingerprint TEXT,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
lastseen INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_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,
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,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL
);
CREATE TABLE IF NOT EXISTS spotify_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
fingerprint TEXT,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
lastseen INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_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,
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,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL
);
UPDATE schema_version SET version=20;

View File

@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
DELETE FROM schema_version;
INSERT INTO schema_version (version) VALUES (18);
INSERT INTO schema_version (version) VALUES (19);
CREATE TABLE IF NOT EXISTS directories (
path TEXT NOT NULL,
@ -422,6 +422,249 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
);
CREATE TABLE IF NOT EXISTS spotify_artists_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
fingerprint TEXT,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
lastseen INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_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,
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,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL
);
CREATE TABLE IF NOT EXISTS spotify_albums_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
fingerprint TEXT,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
lastseen INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_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,
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,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL
);
CREATE TABLE IF NOT EXISTS spotify_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
fingerprint TEXT,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
lastseen INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_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,
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,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL
);
CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
title TEXT,
@ -796,138 +1039,3 @@ CREATE INDEX IF NOT EXISTS idx_album ON songs (album);
CREATE INDEX IF NOT EXISTS idx_title ON songs (title);
CREATE VIEW IF NOT EXISTS duplicated_songs as select artist dup_artist, album dup_album, title dup_title from songs as inner_songs where artist != '' and album != '' and title != '' and unavailable = 0 group by artist, album , title having count(*) > 1;
CREATE VIRTUAL TABLE IF NOT EXISTS songs_fts USING fts5(
ftstitle,
ftsalbum,
ftsartist,
ftsalbumartist,
ftscomposer,
ftsperformer,
ftsgrouping,
ftsgenre,
ftscomment,
tokenize = "unicode61 remove_diacritics 1"
);
CREATE VIRTUAL TABLE IF NOT EXISTS subsonic_songs_fts USING fts5(
ftstitle,
ftsalbum,
ftsartist,
ftsalbumartist,
ftscomposer,
ftsperformer,
ftsgrouping,
ftsgenre,
ftscomment,
tokenize = "unicode61 remove_diacritics 1"
);
CREATE VIRTUAL TABLE IF NOT EXISTS tidal_artists_songs_fts USING fts5(
ftstitle,
ftsalbum,
ftsartist,
ftsalbumartist,
ftscomposer,
ftsperformer,
ftsgrouping,
ftsgenre,
ftscomment,
tokenize = "unicode61 remove_diacritics 1"
);
CREATE VIRTUAL TABLE IF NOT EXISTS tidal_albums_songs_fts USING fts5(
ftstitle,
ftsalbum,
ftsartist,
ftsalbumartist,
ftscomposer,
ftsperformer,
ftsgrouping,
ftsgenre,
ftscomment,
tokenize = "unicode61 remove_diacritics 1"
);
CREATE VIRTUAL TABLE IF NOT EXISTS tidal_songs_fts USING fts5(
ftstitle,
ftsalbum,
ftsartist,
ftsalbumartist,
ftscomposer,
ftsperformer,
ftsgrouping,
ftsgenre,
ftscomment,
tokenize = "unicode61 remove_diacritics 1"
);
CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_artists_songs_fts USING fts5(
ftstitle,
ftsalbum,
ftsartist,
ftsalbumartist,
ftscomposer,
ftsperformer,
ftsgrouping,
ftsgenre,
ftscomment,
tokenize = "unicode61 remove_diacritics 1"
);
CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_albums_songs_fts USING fts5(
ftstitle,
ftsalbum,
ftsartist,
ftsalbumartist,
ftscomposer,
ftsperformer,
ftsgrouping,
ftsgenre,
ftscomment,
tokenize = "unicode61 remove_diacritics 1"
);
CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_songs_fts USING fts5(
ftstitle,
ftsalbum,
ftsartist,
ftsalbumartist,
ftscomposer,
ftsperformer,
ftsgrouping,
ftsgenre,
ftscomment,
tokenize = "unicode61 remove_diacritics 1"
);
CREATE VIRTUAL TABLE IF NOT EXISTS %allsongstables_fts USING fts5(
ftstitle,
ftsalbum,
ftsartist,
ftsalbumartist,
ftscomposer,
ftsperformer,
ftsgrouping,
ftsgenre,
ftscomment,
tokenize = "unicode61 remove_diacritics 1"
);

View File

@ -35,32 +35,32 @@
background-color: %palette-base;
}
QToolButton {
QToolButton[accessibleName="MenuPopupToolButton"] {
border: 2px solid transparent;
border-radius: 3px;
padding: 1px;
}
QToolButton:hover {
QToolButton:hover[accessibleName="MenuPopupToolButton"] {
border: 2px solid %palette-highlight;
background-color: %palette-highlight-lighter;
}
QToolButton:pressed {
QToolButton:pressed[accessibleName="MenuPopupToolButton"] {
border: 2px solid %palette-highlight-darker;
background-color: %palette-highlight-lighter;
}
QToolButton[popupMode="MenuButtonPopup"], QToolButton:hover[popupMode="MenuButtonPopup"], QToolButton:pressed[popupMode="MenuButtonPopup"] {
QToolButton[popupMode="MenuButtonPopup"][accessibleName="MenuPopupToolButton"], QToolButton:hover[popupMode="MenuButtonPopup"][accessibleName="MenuPopupToolButton"], QToolButton:pressed[popupMode="MenuButtonPopup"][accessibleName="MenuPopupToolButton"] {
padding-right: 16px;
}
/* For backwards compatibility with Qt 5 as it does not support property name */
QToolButton[popupMode="1"], QToolButton:hover[popupMode="1"], QToolButton:pressed[popupMode="1"] {
QToolButton[popupMode="1"][accessibleName="MenuPopupToolButton"], QToolButton:hover[popupMode="1"][accessibleName="MenuPopupToolButton"], QToolButton:pressed[popupMode="1"][accessibleName="MenuPopupToolButton"] {
padding-right: 16px;
}
QToolButton::menu-button {
QToolButton::menu-button[accessibleName="MenuPopupToolButton"] {
width: 16px;
border: none;
}

5
debian/control.in vendored
View File

@ -25,7 +25,8 @@ Build-Depends: debhelper (>= 11),
libgpod-dev,
libmtp-dev,
libchromaprint-dev,
libfftw3-dev
libfftw3-dev,
libebur128-dev
Standards-Version: 4.6.1
Package: strawberry
@ -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 Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net and lyricsmode.com
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
- Audio analyzer
- Audio equalizer
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic

5
dist/CMakeLists.txt vendored
View File

@ -4,6 +4,11 @@ if(RPM_DISTRO AND RPM_DATE)
endif(RPM_DISTRO AND RPM_DATE)
if(APPLE)
if(DEFINED ENV{MACOSX_DEPLOYMENT_TARGET})
set(LSMinimumSystemVersion $ENV{MACOSX_DEPLOYMENT_TARGET})
else()
set(LSMinimumSystemVersion 12.0)
endif()
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist)
endif(APPLE)

View File

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

View File

@ -23,6 +23,26 @@ if [ "${GST_PLUGIN_PATH}" = "" ]; then
exit 1
fi
if ! [ -e "${GIO_EXTRA_MODULES}/libgiognutls.so" ] && ! [ -e "${GIO_EXTRA_MODULES}/libgioopenssl.so" ]; then
echo "Error: Missing ${GIO_EXTRA_MODULES}/libgiognutls.so or ${GIO_EXTRA_MODULES}/libgioopenssl.so."
exit 1
fi
if ! [ -e "${GST_PLUGIN_SCANNER}" ]; then
echo "Error: Missing ${GST_PLUGIN_SCANNER}"
exit 1
fi
if ! [ -d "${GST_PLUGIN_PATH}" ]; then
echo "Error: GStreamer plugins path ${GST_PLUGIN_PATH} does not exist."
exit 1
fi
if ! [ -e "${GST_PLUGIN_PATH}/libgstcoreelements.so" ] && ! [ -e "${GST_PLUGIN_PATH}/libgstcoreelements.dylib" ]; then
echo "Error: Missing libgstcoreelements.{so,dylib} in GStreamer plugins path ${GST_PLUGIN_PATH}."
exit 1
fi
mkdir -p "${bundledir}/Contents/PlugIns/gio-modules" || exit 1
mkdir -p "${bundledir}/Contents/PlugIns/gstreamer" || exit 1
@ -38,82 +58,94 @@ else
echo "Warning: Missing ${GIO_EXTRA_MODULES}/libgioopenssl.so"
fi
if ! [ -e "${GST_PLUGIN_SCANNER}" ]; then
echo "Error: Missing ${GST_PLUGIN_SCANNER}"
exit 1
fi
cp -v -f "${GST_PLUGIN_SCANNER}" "${bundledir}/Contents/PlugIns/" || exit 1
if ! [ -d "${GST_PLUGIN_PATH}" ]; then
echo "Error: GStreamer plugins path ${GST_PLUGIN_PATH} does not exist."
exit 1
fi
install_name_tool -add_rpath "@loader_path/../Frameworks" "${bundledir}/Contents/PlugIns/$(basename ${GST_PLUGIN_SCANNER})" || exit 1
gst_plugins="
libgstaes.dylib
libgstaiff.dylib
libgstapetag.dylib
libgstapp.dylib
libgstasf.dylib
libgstasfmux.dylib
libgstaudioconvert.dylib
libgstaudiofx.dylib
libgstaudiomixer.dylib
libgstaudioparsers.dylib
libgstaudiorate.dylib
libgstaudioresample.dylib
libgstaudiotestsrc.dylib
libgstautodetect.dylib
libgstbs2b.dylib
libgstcdio.dylib
libgstcoreelements.dylib
libgstdash.dylib
libgstequalizer.dylib
libgstfaac.dylib
libgstfaad.dylib
libgstfdkaac.dylib
libgstflac.dylib
libgstgio.dylib
libgsthls.dylib
libgsticydemux.dylib
libgstid3demux.dylib
libgstid3tag.dylib
libgstisomp4.dylib
libgstlame.dylib
libgstlibav.dylib
libgstmpg123.dylib
libgstmusepack.dylib
libgstogg.dylib
libgstopenmpt.dylib
libgstopus.dylib
libgstopusparse.dylib
libgstosxaudio.dylib
libgstpbtypes.dylib
libgstplayback.dylib
libgstreplaygain.dylib
libgstrtp.dylib
libgstrtsp.dylib
libgstsoup.dylib
libgstspectrum.dylib
libgstspeex.dylib
libgsttaglib.dylib
libgsttcp.dylib
libgsttwolame.dylib
libgsttypefindfunctions.dylib
libgstudp.dylib
libgstvolume.dylib
libgstvorbis.dylib
libgstwavenc.dylib
libgstwavpack.dylib
libgstwavparse.dylib
libgstxingmux.dylib
libgstadaptivedemux2
libgstaes
libgstaiff
libgstapetag
libgstapp
libgstasf
libgstasfmux
libgstaudioconvert
libgstaudiofx
libgstaudioparsers
libgstaudioresample
libgstautodetect
libgstbs2b
libgstcdio
libgstcoreelements
libgstdash
libgstdsd
libgstequalizer
libgstfaac
libgstfaad
libgstfdkaac
libgstflac
libgstgio
libgsthls
libgsticydemux
libgstid3demux
libgstid3tag
libgstisomp4
libgstlame
libgstmpegpsdemux
libgstmpegpsmux
libgstmpegtsdemux
libgstmpegtsmux
libgstlibav
libgstmpg123
libgstmusepack
libgstogg
libgstopenmpt
libgstopus
libgstopusparse
libgstosxaudio
libgstpbtypes
libgstplayback
libgstreplaygain
libgstrtp
libgstrtsp
libgstsoup
libgstspectrum
libgstspeex
libgsttaglib
libgsttcp
libgsttwolame
libgsttypefindfunctions
libgstudp
libgstvolume
libgstvorbis
libgstwavenc
libgstwavpack
libgstwavparse
libgstxingmux
"
gst_plugins=$(echo "$gst_plugins" | tr '\n' ' ' | sed -e 's/^ //g' | sed -e 's/ / /g')
for gst_plugin in $gst_plugins; do
if [ -e "${GST_PLUGIN_PATH}/${gst_plugin}" ]; then
cp -v -f "${GST_PLUGIN_PATH}/${gst_plugin}" "${bundledir}/Contents/PlugIns/gstreamer/" || exit 1
if [ -e "${GST_PLUGIN_PATH}/${gst_plugin}.dylib" ]; then
cp -v -f "${GST_PLUGIN_PATH}/${gst_plugin}.dylib" "${bundledir}/Contents/PlugIns/gstreamer/" || exit 1
install_name_tool -id "@rpath/${gst_plugin}.dylib" "${bundledir}/Contents/PlugIns/gstreamer/${gst_plugin}.dylib"
elif [ -e "${GST_PLUGIN_PATH}/${gst_plugin}.so" ]; then
cp -v -f "${GST_PLUGIN_PATH}/${gst_plugin}.so" "${bundledir}/Contents/PlugIns/gstreamer/" || exit 1
install_name_tool -id "@rpath/${gst_plugin}.so" "${bundledir}/Contents/PlugIns/gstreamer/${gst_plugin}.so"
else
echo "Warning: Missing gstreamer plugin ${gst_plugin}"
echo "Warning: Missing gstreamer plugin ${gst_plugin}."
fi
done
# libsoup is dynamically loaded by gstreamer, so it needs to be copied.
if [ "${LIBSOUP_LIBRARY_PATH}" = "" ]; then
echo "Warning: Set the LIBSOUP_LIBRARY_PATH environment variable for copying libsoup."
else
if [ -e "${LIBSOUP_LIBRARY_PATH}" ]; then
mkdir -p "${bundledir}/Contents/Frameworks" || exit 1
cp -v -f "${LIBSOUP_LIBRARY_PATH}" "${bundledir}/Contents/Frameworks/" || exit 1
install_name_tool -id "@rpath/$(basename ${LIBSOUP_LIBRARY_PATH})" "${bundledir}/Contents/Frameworks/$(basename ${LIBSOUP_LIBRARY_PATH})"
else
echo "Warning: Missing libsoup ${LIBSOUP_LIBRARY_PATH}."
fi
fi

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 Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net and lyricsmode.com</li>
<li>Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net</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,9 @@
</screenshots>
<update_contact>eclipseo@fedoraproject.org</update_contact>
<releases>
<release version="1.0.23" date="2024-01-11"/>
<release version="1.0.22" date="2023-12-09"/>
<release version="1.0.21" date="2023-10-21"/>
<release version="1.0.20" date="2023-09-24"/>
<release version="1.0.19" date="2023-09-24"/>
<release version="1.0.18" date="2023-07-02"/>

View File

@ -3,8 +3,10 @@ Version=1.0
Type=Application
Name=Strawberry
GenericName=Strawberry Music Player
GenericName[fr]=Lecteur de musique Strawberry
GenericName[ru]=Музыкальный проигрыватель Strawberry
Comment=Plays music
Comment[fr]=Joue de la musique
Comment[ru]=Прослушивание музыки
Exec=strawberry %U
TryExec=strawberry

View File

@ -1,9 +1,9 @@
Name: strawberry
Version: @STRAWBERRY_VERSION_RPM_V@
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos} || "%{?_vendor}" == "openmandriva"
Release: @STRAWBERRY_VERSION_RPM_R@%{?dist}
%else
%if 0%{?suse_version}
Release: @STRAWBERRY_VERSION_RPM_R@.@RPM_DISTRO@
%else
Release: @STRAWBERRY_VERSION_RPM_R@%{?dist}
%endif
Summary: A music player and music collection organizer
Group: Productivity/Multimedia/Sound/Players
@ -11,7 +11,7 @@ License: GPL-3.0+
URL: https://www.strawberrymusicplayer.org/
Source0: %{name}-@STRAWBERRY_VERSION_PACKAGE@.tar.xz
%if 0%{?suse_version} && 0%{?suse_version} > 1325
%if 0%{?suse_version}
BuildRequires: libboost_headers-devel
%else
BuildRequires: boost-devel
@ -25,15 +25,13 @@ BuildRequires: gettext
BuildRequires: desktop-file-utils
%if 0%{?suse_version}
BuildRequires: update-desktop-files
%endif
%if 0%{?suse_version}
BuildRequires: appstream-glib
%else
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos}
%endif
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos}
BuildRequires: libappstream-glib
%else
%endif
%if 0%{?mageia} || "%{?_vendor}" == "openmandriva"
BuildRequires: appstream-util
%endif
%endif
BuildRequires: pkgconfig
BuildRequires: pkgconfig(glib-2.0)
@ -44,48 +42,31 @@ BuildRequires: pkgconfig(dbus-1)
BuildRequires: pkgconfig(alsa)
BuildRequires: pkgconfig(protobuf)
BuildRequires: pkgconfig(sqlite3) >= 3.9
%if ! 0%{?centos} && ! 0%{?mageia}
BuildRequires: pkgconfig(taglib)
%endif
BuildRequires: pkgconfig(fftw3)
BuildRequires: pkgconfig(icu-uc)
BuildRequires: pkgconfig(icu-i18n)
%if "@QT_VERSION_MAJOR@" == "5" && ( 0%{?fedora} || 0%{?rhel_version} || 0%{?centos} )
BuildRequires: pkgconfig(Qt@QT_VERSION_MAJOR@Core)
BuildRequires: pkgconfig(Qt@QT_VERSION_MAJOR@Gui)
BuildRequires: pkgconfig(Qt@QT_VERSION_MAJOR@Widgets)
BuildRequires: pkgconfig(Qt@QT_VERSION_MAJOR@Concurrent)
BuildRequires: pkgconfig(Qt@QT_VERSION_MAJOR@Network)
BuildRequires: pkgconfig(Qt@QT_VERSION_MAJOR@Sql)
BuildRequires: pkgconfig(Qt@QT_VERSION_MAJOR@DBus)
BuildRequires: pkgconfig(Qt@QT_VERSION_MAJOR@Test)
BuildRequires: pkgconfig(Qt@QT_VERSION_MAJOR@X11Extras)
%else
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Core)
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Gui)
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Widgets)
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Concurrent)
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Network)
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Sql)
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@DBus)
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Gui)
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Widgets)
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Test)
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@LinguistTools)
%if "@QT_VERSION_MAJOR@" == "5"
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@X11Extras)
%endif
%endif
%if 0%{?suse_version} || 0%{?fedora_version} || 0%{?mageia}
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@LinguistTools)
%endif
BuildRequires: pkgconfig(gstreamer-1.0)
BuildRequires: pkgconfig(gstreamer-app-1.0)
BuildRequires: pkgconfig(gstreamer-audio-1.0)
BuildRequires: pkgconfig(gstreamer-base-1.0)
BuildRequires: pkgconfig(gstreamer-tag-1.0)
%if ! 0%{?centos}
BuildRequires: pkgconfig(libchromaprint)
%endif
BuildRequires: pkgconfig(libpulse)
BuildRequires: pkgconfig(libcdio)
BuildRequires: pkgconfig(libebur128)
BuildRequires: pkgconfig(libgpod-1.0)
BuildRequires: pkgconfig(libmtp)
%if 0%{?suse_version} || 0%{?fedora_version}
@ -118,7 +99,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 Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net and lyricsmode.com
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
- Support for multiple backends
- Audio analyzer
- Audio equalizer
@ -138,22 +119,19 @@ Features:
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos}
export CXXFLAGS="-fPIC $RPM_OPT_FLAGS"
%endif
%{cmake} -DQT_VERSION_MAJOR=@QT_VERSION_MAJOR@
%if 0%{?centos} || (0%{?mageia} && 0%{?mageia} <= 7) || "%{?_vendor}" == "openmandriva"
%make_build
%if "%{?_vendor}" == "openmandriva"
%{cmake} -DQT_VERSION_MAJOR=@QT_VERSION_MAJOR@ -DENABLE_TRANSLATIONS=OFF
%make_build
%else
%cmake_build
%{cmake} -DQT_VERSION_MAJOR=@QT_VERSION_MAJOR@
%cmake_build
%endif
%install
%if 0%{?centos}
%make_install
%if "%{?_vendor}" == "openmandriva"
%make_install -C build
%else
%if 0%{?mageia} || "%{?_vendor}" == "openmandriva"
%make_install -C build
%else
%cmake_install
%endif
%cmake_install
%endif
%if 0%{?suse_version}
@ -162,10 +140,10 @@ Features:
%check
desktop-file-validate %{buildroot}%{_datadir}/applications/org.strawberrymusicplayer.strawberry.desktop
%if 0%{?fedora_version}
appstream-util validate-relax --nonet %{buildroot}%{_metainfodir}/org.strawberrymusicplayer.strawberry.appdata.xml
%else
%if 0%{?suse_version}
appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/org.strawberrymusicplayer.strawberry.appdata.xml
%else
appstream-util validate-relax --nonet %{buildroot}%{_metainfodir}/org.strawberrymusicplayer.strawberry.appdata.xml
%endif
%files
@ -176,13 +154,13 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/org.strawberrymusicpl
%{_bindir}/strawberry-tagreader
%{_datadir}/applications/*.desktop
%{_datadir}/icons/hicolor/*/apps/strawberry.*
%if 0%{?fedora_version}
%{_metainfodir}/*.appdata.xml
%else
%{_datadir}/metainfo/*.appdata.xml
%endif
%{_mandir}/man1/%{name}.1.*
%{_mandir}/man1/%{name}-tagreader.1.*
%if 0%{?suse_version}
%{_datadir}/metainfo/*.appdata.xml
%else
%{_metainfodir}/*.appdata.xml
%endif
%changelog
* @RPM_DATE@ Jonas Kvinge <jonas@jkvinge.net> - @STRAWBERRY_VERSION_RPM_V@

View File

@ -109,7 +109,11 @@
Unicode True
!ifdef debug
SetCompressor lzma
!else
SetCompressor /SOLID lzma
!endif
!include "MUI2.nsh"
!include "FileAssociation.nsh"
@ -136,8 +140,6 @@ SetCompressor /SOLID lzma
ReserveFile "${NSISDIR}\Plugins\x86-unicode\INetC.dll"
!endif
!define LockedListPATH $InstallDir
; Installer pages
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE COPYING
@ -184,19 +186,22 @@ FunctionEnd
Function CheckPreviousInstall
ReadRegStr $R0 ${PRODUCT_UNINST_ROOT_KEY} ${PRODUCT_UNINST_KEY} "UninstallString"
StrCmp $R0 "" done
StrCmp $R0 "" Done
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION \
"${PRODUCT_NAME} is already installed. $\n$\nClick `OK` to remove the \
previous version or `Cancel` to cancel this upgrade." \
IDOK uninst
${IfNot} ${Silent}
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "${PRODUCT_NAME} is already installed. $\n$\nClick `OK` to remove the previous version or `Cancel` to cancel this upgrade." IDOK Uninstall
Abort
; Run the uninstaller
uninst:
ClearErrors
ExecWait '$R0' ; Do not copy the uninstaller to a temp file
${EndIf}
done:
Uninstall:
ClearErrors
${If} ${Silent}
ExecWait '$R0 /S'
${Else}
ExecWait '$R0'
${EndIf}
Done:
FunctionEnd
@ -281,8 +286,10 @@ Section "Strawberry" Strawberry
File "libgstaudio-1.0-0.dll"
File "libgstbadaudio-1.0-0.dll"
File "libgstbase-1.0-0.dll"
File "libgstcodecparsers-1.0-0.dll"
File "libgstfft-1.0-0.dll"
File "libgstisoff-1.0-0.dll"
File "libgstmpegts-1.0-0.dll"
File "libgstnet-1.0-0.dll"
File "libgstpbutils-1.0-0.dll"
File "libgstreamer-1.0-0.dll"
@ -299,6 +306,7 @@ Section "Strawberry" Strawberry
File "libidn2-0.dll"
File "libintl-8.dll"
File "libjpeg-9.dll"
File "libkdsingleapplication-qt6.dll"
File "liblzma-5.dll"
File "libmp3lame-0.dll"
File "libmpcdec.dll"
@ -326,7 +334,6 @@ Section "Strawberry" Strawberry
File "libvorbisfile-3.dll"
File "libwavpack-1.dll"
File "libwinpthread-1.dll"
File "libxml2-2.dll"
File "libzstd.dll"
File "zlib1.dll"
@ -343,6 +350,7 @@ Section "Strawberry" Strawberry
File "libabsl_examine_stack.dll"
File "libabsl_hash.dll"
File "libabsl_int128.dll"
File "libabsl_kernel_timeout_internal.dll"
File "libabsl_log_globals.dll"
File "libabsl_log_internal_check_op.dll"
File "libabsl_log_internal_conditions.dll"
@ -406,7 +414,7 @@ Section "Strawberry" Strawberry
File "brotlidec.dll"
File "chromaprint.dll"
File "ebur128.dll"
File "faad.dll"
File "faad-2.dll"
File "fdk-aac.dll"
File "ffi-7.dll"
File "gio-2.0-0.dll"
@ -419,8 +427,10 @@ Section "Strawberry" Strawberry
File "gstaudio-1.0-0.dll"
File "gstbadaudio-1.0-0.dll"
File "gstbase-1.0-0.dll"
File "gstcodecparsers-1.0-0.dll"
File "gstfft-1.0-0.dll"
File "gstisoff-1.0-0.dll"
File "gstmpegts-1.0-0.dll"
File "gstnet-1.0-0.dll"
File "gstpbutils-1.0-0.dll"
File "gstreamer-1.0-0.dll"
@ -435,6 +445,7 @@ Section "Strawberry" Strawberry
File "harfbuzz.dll"
File "intl-8.dll"
File "jpeg62.dll"
File "kdsingleapplication-qt6.dll"
File "libbs2b.dll"
File "libfaac_dll.dll"
File "liblzma.dll"
@ -461,7 +472,6 @@ Section "Strawberry" Strawberry
File "libiconv.dll"
File "libpng16.dll"
File "libspeex.dll"
File "libxml2.dll"
File "pcre2-8.dll"
File "pcre2-16.dll"
File "twolame.dll"
@ -472,7 +482,6 @@ Section "Strawberry" Strawberry
File "libiconvd.dll"
File "libpng16d.dll"
File "libspeexd.dll"
File "libxml2d.dll"
File "pcre2-8d.dll"
File "pcre2-16d.dll"
File "twolamed.dll"
@ -489,7 +498,7 @@ Section "Strawberry" Strawberry
; Common files
File "icudt73.dll"
File "icudt75.dll"
File "libfftw3-3.dll"
!ifdef debug
File "libprotobufd.dll"
@ -497,8 +506,9 @@ Section "Strawberry" Strawberry
File "libprotobuf.dll"
!endif
!ifdef msvc && debug
File "icuin73d.dll"
File "icuuc73d.dll"
File "icuin75d.dll"
File "icuuc75d.dll"
File "libxml2d.dll"
File "Qt6Concurrentd.dll"
File "Qt6Cored.dll"
File "Qt6Guid.dll"
@ -506,8 +516,9 @@ Section "Strawberry" Strawberry
File "Qt6Sqld.dll"
File "Qt6Widgetsd.dll"
!else
File "icuin73.dll"
File "icuuc73.dll"
File "icuin75.dll"
File "icuuc75.dll"
File "libxml2.dll"
File "Qt6Concurrent.dll"
File "Qt6Core.dll"
File "Qt6Gui.dll"
@ -580,9 +591,9 @@ SectionEnd
Section "Qt styles" styles
SetOutPath "$INSTDIR\styles"
!ifdef msvc && debug
File "/oname=qwindowsvistastyled.dll" "styles\qwindowsvistastyled.dll"
File "/oname=qmodernwindowsstyled.dll" "styles\qmodernwindowsstyled.dll"
!else
File "/oname=qwindowsvistastyle.dll" "styles\qwindowsvistastyle.dll"
File "/oname=qmodernwindowsstyle.dll" "styles\qmodernwindowsstyle.dll"
!endif
SectionEnd
@ -623,6 +634,7 @@ Section "Gstreamer plugins" gstreamer-plugins
SetOutPath "$INSTDIR\gstreamer-plugins"
!ifdef mingw
File "/oname=libgstadaptivedemux2.dll" "gstreamer-plugins\libgstadaptivedemux2.dll"
File "/oname=libgstaes.dll" "gstreamer-plugins\libgstaes.dll"
File "/oname=libgstaiff.dll" "gstreamer-plugins\libgstaiff.dll"
File "/oname=libgstapetag.dll" "gstreamer-plugins\libgstapetag.dll"
@ -631,16 +643,14 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=libgstasfmux.dll" "gstreamer-plugins\libgstasfmux.dll"
File "/oname=libgstaudioconvert.dll" "gstreamer-plugins\libgstaudioconvert.dll"
File "/oname=libgstaudiofx.dll" "gstreamer-plugins\libgstaudiofx.dll"
File "/oname=libgstaudiomixer.dll" "gstreamer-plugins\libgstaudiomixer.dll"
File "/oname=libgstaudioparsers.dll" "gstreamer-plugins\libgstaudioparsers.dll"
File "/oname=libgstaudiorate.dll" "gstreamer-plugins\libgstaudiorate.dll"
File "/oname=libgstaudioresample.dll" "gstreamer-plugins\libgstaudioresample.dll"
File "/oname=libgstaudiotestsrc.dll" "gstreamer-plugins\libgstaudiotestsrc.dll"
File "/oname=libgstautodetect.dll" "gstreamer-plugins\libgstautodetect.dll"
File "/oname=libgstbs2b.dll" "gstreamer-plugins\libgstbs2b.dll"
File "/oname=libgstcoreelements.dll" "gstreamer-plugins\libgstcoreelements.dll"
File "/oname=libgstdash.dll" "gstreamer-plugins\libgstdash.dll"
File "/oname=libgstdirectsound.dll" "gstreamer-plugins\libgstdirectsound.dll"
File "/oname=libgstdsd.dll" "gstreamer-plugins\libgstdsd.dll"
File "/oname=libgstequalizer.dll" "gstreamer-plugins\libgstequalizer.dll"
File "/oname=libgstfaac.dll" "gstreamer-plugins\libgstfaac.dll"
File "/oname=libgstfaad.dll" "gstreamer-plugins\libgstfaad.dll"
@ -655,6 +665,10 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=libgstisomp4.dll" "gstreamer-plugins\libgstisomp4.dll"
File "/oname=libgstlame.dll" "gstreamer-plugins\libgstlame.dll"
File "/oname=libgstlibav.dll" "gstreamer-plugins\libgstlibav.dll"
File "/oname=libgstmpegpsdemux.dll" "gstreamer-plugins\libgstmpegpsdemux.dll"
File "/oname=libgstmpegpsmux.dll" "gstreamer-plugins\libgstmpegpsmux.dll"
File "/oname=libgstmpegtsdemux.dll" "gstreamer-plugins\libgstmpegtsdemux.dll"
File "/oname=libgstmpegtsmux.dll" "gstreamer-plugins\libgstmpegtsmux.dll"
File "/oname=libgstmpg123.dll" "gstreamer-plugins\libgstmpg123.dll"
File "/oname=libgstmusepack.dll" "gstreamer-plugins\libgstmusepack.dll"
File "/oname=libgstogg.dll" "gstreamer-plugins\libgstogg.dll"
@ -677,6 +691,7 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=libgstvolume.dll" "gstreamer-plugins\libgstvolume.dll"
File "/oname=libgstvorbis.dll" "gstreamer-plugins\libgstvorbis.dll"
File "/oname=libgstwasapi.dll" "gstreamer-plugins\libgstwasapi.dll"
File "/oname=libgstwaveform.dll" "gstreamer-plugins\libgstwaveform.dll"
File "/oname=libgstwavenc.dll" "gstreamer-plugins\libgstwavenc.dll"
File "/oname=libgstwavpack.dll" "gstreamer-plugins\libgstwavpack.dll"
File "/oname=libgstwavparse.dll" "gstreamer-plugins\libgstwavparse.dll"
@ -684,24 +699,24 @@ Section "Gstreamer plugins" gstreamer-plugins
!endif ; MinGW
!ifdef msvc
File "/oname=gstadaptivedemux2.dll" "gstreamer-plugins\gstadaptivedemux2.dll"
File "/oname=gstaes.dll" "gstreamer-plugins\gstaes.dll"
File "/oname=gstaiff.dll" "gstreamer-plugins\gstaiff.dll"
File "/oname=gstapetag.dll" "gstreamer-plugins\gstapetag.dll"
File "/oname=gstapp.dll" "gstreamer-plugins\gstapp.dll"
File "/oname=gstasf.dll" "gstreamer-plugins\gstasf.dll"
File "/oname=gstasfmux.dll" "gstreamer-plugins\gstasfmux.dll"
File "/oname=gstasio.dll" "gstreamer-plugins\gstasio.dll"
File "/oname=gstaudioconvert.dll" "gstreamer-plugins\gstaudioconvert.dll"
File "/oname=gstaudiofx.dll" "gstreamer-plugins\gstaudiofx.dll"
File "/oname=gstaudiomixer.dll" "gstreamer-plugins\gstaudiomixer.dll"
File "/oname=gstaudioparsers.dll" "gstreamer-plugins\gstaudioparsers.dll"
File "/oname=gstaudiorate.dll" "gstreamer-plugins\gstaudiorate.dll"
File "/oname=gstaudioresample.dll" "gstreamer-plugins\gstaudioresample.dll"
File "/oname=gstaudiotestsrc.dll" "gstreamer-plugins\gstaudiotestsrc.dll"
File "/oname=gstautodetect.dll" "gstreamer-plugins\gstautodetect.dll"
File "/oname=gstbs2b.dll" "gstreamer-plugins\gstbs2b.dll"
File "/oname=gstcoreelements.dll" "gstreamer-plugins\gstcoreelements.dll"
File "/oname=gstdash.dll" "gstreamer-plugins\gstdash.dll"
File "/oname=gstdirectsound.dll" "gstreamer-plugins\gstdirectsound.dll"
File "/oname=gstdsd.dll" "gstreamer-plugins\gstdsd.dll"
File "/oname=gstequalizer.dll" "gstreamer-plugins\gstequalizer.dll"
File "/oname=gstfaac.dll" "gstreamer-plugins\gstfaac.dll"
File "/oname=gstfaad.dll" "gstreamer-plugins\gstfaad.dll"
@ -716,6 +731,10 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=gstisomp4.dll" "gstreamer-plugins\gstisomp4.dll"
File "/oname=gstlame.dll" "gstreamer-plugins\gstlame.dll"
File "/oname=gstlibav.dll" "gstreamer-plugins\gstlibav.dll"
File "/oname=gstmpegpsdemux.dll" "gstreamer-plugins\gstmpegpsdemux.dll"
File "/oname=gstmpegpsmux.dll" "gstreamer-plugins\gstmpegpsmux.dll"
File "/oname=gstmpegtsdemux.dll" "gstreamer-plugins\gstmpegtsdemux.dll"
File "/oname=gstmpegtsmux.dll" "gstreamer-plugins\gstmpegtsmux.dll"
File "/oname=gstmpg123.dll" "gstreamer-plugins\gstmpg123.dll"
File "/oname=gstmusepack.dll" "gstreamer-plugins\gstmusepack.dll"
File "/oname=gstogg.dll" "gstreamer-plugins\gstogg.dll"
@ -738,12 +757,15 @@ 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"
; Disable wasapi2 until issue (https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/2870) is fixed.
;File "/oname=gstwasapi2.dll" "gstreamer-plugins\gstwasapi2.dll"
File "/oname=gstwasapi2.dll" "gstreamer-plugins\gstwasapi2.dll"
File "/oname=gstwaveform.dll" "gstreamer-plugins\gstwaveform.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"
File "/oname=gstxingmux.dll" "gstreamer-plugins\gstxingmux.dll"
!ifdef arch_x64
File "/oname=gstspotify.dll" "gstreamer-plugins\gstspotify.dll"
!endif
!endif ; MSVC
SectionEnd
@ -835,8 +857,10 @@ Section "Uninstall"
Delete "$INSTDIR\libgstaudio-1.0-0.dll"
Delete "$INSTDIR\libgstbadaudio-1.0-0.dll"
Delete "$INSTDIR\libgstbase-1.0-0.dll"
Delete "$INSTDIR\libgstcodecparsers-1.0-0.dll"
Delete "$INSTDIR\libgstfft-1.0-0.dll"
Delete "$INSTDIR\libgstisoff-1.0-0.dll"
Delete "$INSTDIR\libgstmpegts-1.0-0.dll"
Delete "$INSTDIR\libgstnet-1.0-0.dll"
Delete "$INSTDIR\libgstpbutils-1.0-0.dll"
Delete "$INSTDIR\libgstreamer-1.0-0.dll"
@ -853,6 +877,7 @@ Section "Uninstall"
Delete "$INSTDIR\libidn2-0.dll"
Delete "$INSTDIR\libintl-8.dll"
Delete "$INSTDIR\libjpeg-9.dll"
Delete "$INSTDIR\libkdsingleapplication-qt6.dll"
Delete "$INSTDIR\liblzma-5.dll"
Delete "$INSTDIR\libmp3lame-0.dll"
Delete "$INSTDIR\libmpcdec.dll"
@ -880,7 +905,6 @@ Section "Uninstall"
Delete "$INSTDIR\libvorbisfile-3.dll"
Delete "$INSTDIR\libwavpack-1.dll"
Delete "$INSTDIR\libwinpthread-1.dll"
Delete "$INSTDIR\libxml2-2.dll"
Delete "$INSTDIR\libzstd.dll"
Delete "$INSTDIR\zlib1.dll"
@ -897,6 +921,7 @@ Section "Uninstall"
Delete "$INSTDIR\libabsl_examine_stack.dll"
Delete "$INSTDIR\libabsl_hash.dll"
Delete "$INSTDIR\libabsl_int128.dll"
Delete "$INSTDIR\libabsl_kernel_timeout_internal.dll"
Delete "$INSTDIR\libabsl_log_globals.dll"
Delete "$INSTDIR\libabsl_log_internal_check_op.dll"
Delete "$INSTDIR\libabsl_log_internal_conditions.dll"
@ -960,7 +985,7 @@ Section "Uninstall"
Delete "$INSTDIR\brotlidec.dll"
Delete "$INSTDIR\chromaprint.dll"
Delete "$INSTDIR\ebur128.dll"
Delete "$INSTDIR\faad.dll"
Delete "$INSTDIR\faad-2.dll"
Delete "$INSTDIR\fdk-aac.dll"
Delete "$INSTDIR\ffi-7.dll"
Delete "$INSTDIR\gio-2.0-0.dll"
@ -973,8 +998,10 @@ Section "Uninstall"
Delete "$INSTDIR\gstaudio-1.0-0.dll"
Delete "$INSTDIR\gstbadaudio-1.0-0.dll"
Delete "$INSTDIR\gstbase-1.0-0.dll"
Delete "$INSTDIR\gstcodecparsers-1.0-0.dll"
Delete "$INSTDIR\gstfft-1.0-0.dll"
Delete "$INSTDIR\gstisoff-1.0-0.dll"
Delete "$INSTDIR\gstmpegts-1.0-0.dll"
Delete "$INSTDIR\gstnet-1.0-0.dll"
Delete "$INSTDIR\gstpbutils-1.0-0.dll"
Delete "$INSTDIR\gstreamer-1.0-0.dll"
@ -989,6 +1016,7 @@ Section "Uninstall"
Delete "$INSTDIR\harfbuzz.dll"
Delete "$INSTDIR\intl-8.dll"
Delete "$INSTDIR\jpeg62.dll"
Delete "$INSTDIR\kdsingleapplication-qt6.dll"
Delete "$INSTDIR\libbs2b.dll"
Delete "$INSTDIR\libfaac_dll.dll"
Delete "$INSTDIR\liblzma.dll"
@ -1015,7 +1043,6 @@ Section "Uninstall"
Delete "$INSTDIR\libiconv.dll"
Delete "$INSTDIR\libpng16.dll"
Delete "$INSTDIR\libspeex.dll"
Delete "$INSTDIR\libxml2.dll"
Delete "$INSTDIR\pcre2-8.dll"
Delete "$INSTDIR\pcre2-16.dll"
Delete "$INSTDIR\twolame.dll"
@ -1026,7 +1053,6 @@ Section "Uninstall"
Delete "$INSTDIR\libiconvd.dll"
Delete "$INSTDIR\libpng16d.dll"
Delete "$INSTDIR\libspeexd.dll"
Delete "$INSTDIR\libxml2d.dll"
Delete "$INSTDIR\pcre2-8d.dll"
Delete "$INSTDIR\pcre2-16d.dll"
Delete "$INSTDIR\twolamed.dll"
@ -1042,7 +1068,7 @@ Section "Uninstall"
; Common files
Delete "$INSTDIR\icudt73.dll"
Delete "$INSTDIR\icudt75.dll"
Delete "$INSTDIR\libfftw3-3.dll"
!ifdef debug
Delete "$INSTDIR\libprotobufd.dll"
@ -1050,8 +1076,9 @@ Section "Uninstall"
Delete "$INSTDIR\libprotobuf.dll"
!endif
!ifdef msvc && debug
Delete "$INSTDIR\icuin73d.dll"
Delete "$INSTDIR\icuuc73d.dll"
Delete "$INSTDIR\icuin75d.dll"
Delete "$INSTDIR\icuuc75d.dll"
Delete "$INSTDIR\libxml2d.dll"
Delete "$INSTDIR\Qt6Concurrentd.dll"
Delete "$INSTDIR\Qt6Cored.dll"
Delete "$INSTDIR\Qt6Guid.dll"
@ -1059,8 +1086,9 @@ Section "Uninstall"
Delete "$INSTDIR\Qt6Sqld.dll"
Delete "$INSTDIR\Qt6Widgetsd.dll"
!else
Delete "$INSTDIR\icuin73.dll"
Delete "$INSTDIR\icuuc73.dll"
Delete "$INSTDIR\icuin75.dll"
Delete "$INSTDIR\icuuc75.dll"
Delete "$INSTDIR\libxml2.dll"
Delete "$INSTDIR\Qt6Concurrent.dll"
Delete "$INSTDIR\Qt6Core.dll"
Delete "$INSTDIR\Qt6Gui.dll"
@ -1088,7 +1116,7 @@ Section "Uninstall"
!ifdef msvc && debug
Delete "$INSTDIR\platforms\qwindowsd.dll"
Delete "$INSTDIR\styles\qwindowsvistastyled.dll"
Delete "$INSTDIR\styles\qmodernwindowsstyled.dll"
Delete "$INSTDIR\tls\qschannelbackendd.dll"
Delete "$INSTDIR\tls\qopensslbackendd.dll"
Delete "$INSTDIR\sqldrivers\qsqlited.dll"
@ -1097,7 +1125,7 @@ Section "Uninstall"
Delete "$INSTDIR\imageformats\qjpegd.dll"
!else
Delete "$INSTDIR\platforms\qwindows.dll"
Delete "$INSTDIR\styles\qwindowsvistastyle.dll"
Delete "$INSTDIR\styles\qmodernwindowsstyle.dll"
Delete "$INSTDIR\tls\qschannelbackend.dll"
Delete "$INSTDIR\tls\qopensslbackend.dll"
Delete "$INSTDIR\sqldrivers\qsqlite.dll"
@ -1109,6 +1137,7 @@ Section "Uninstall"
; MinGW GStreamer plugins
!ifdef mingw
Delete "$INSTDIR\gstreamer-plugins\libgstadaptivedemux2.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstaes.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstaiff.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstapetag.dll"
@ -1117,16 +1146,14 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\libgstasfmux.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstaudioconvert.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstaudiofx.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstaudiomixer.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstaudioparsers.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstaudiorate.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstaudioresample.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstaudiotestsrc.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstautodetect.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstbs2b.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstcoreelements.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstdash.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstdirectsound.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstdsd.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstequalizer.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstfaac.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstfaad.dll"
@ -1141,6 +1168,10 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\libgstisomp4.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstlame.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstlibav.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstmpegpsdemux.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstmpegpsmux.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstmpegtsdemux.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstmpegtsmux.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstmpg123.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstmusepack.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstogg.dll"
@ -1163,6 +1194,7 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\libgstvolume.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstvorbis.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstwaveform.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstwavenc.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstwavpack.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstwavparse.dll"
@ -1172,24 +1204,24 @@ Section "Uninstall"
; MSVC GStreamer plugins
!ifdef msvc
Delete "$INSTDIR\gstreamer-plugins\gstadaptivedemux2.dll"
Delete "$INSTDIR\gstreamer-plugins\gstaes.dll"
Delete "$INSTDIR\gstreamer-plugins\gstaiff.dll"
Delete "$INSTDIR\gstreamer-plugins\gstapetag.dll"
Delete "$INSTDIR\gstreamer-plugins\gstapp.dll"
Delete "$INSTDIR\gstreamer-plugins\gstasf.dll"
Delete "$INSTDIR\gstreamer-plugins\gstasfmux.dll"
Delete "$INSTDIR\gstreamer-plugins\gstasio.dll"
Delete "$INSTDIR\gstreamer-plugins\gstaudioconvert.dll"
Delete "$INSTDIR\gstreamer-plugins\gstaudiofx.dll"
Delete "$INSTDIR\gstreamer-plugins\gstaudiomixer.dll"
Delete "$INSTDIR\gstreamer-plugins\gstaudioparsers.dll"
Delete "$INSTDIR\gstreamer-plugins\gstaudiorate.dll"
Delete "$INSTDIR\gstreamer-plugins\gstaudioresample.dll"
Delete "$INSTDIR\gstreamer-plugins\gstaudiotestsrc.dll"
Delete "$INSTDIR\gstreamer-plugins\gstautodetect.dll"
Delete "$INSTDIR\gstreamer-plugins\gstbs2b.dll"
Delete "$INSTDIR\gstreamer-plugins\gstcoreelements.dll"
Delete "$INSTDIR\gstreamer-plugins\gstdash.dll"
Delete "$INSTDIR\gstreamer-plugins\gstdirectsound.dll"
Delete "$INSTDIR\gstreamer-plugins\gstdsd.dll"
Delete "$INSTDIR\gstreamer-plugins\gstequalizer.dll"
Delete "$INSTDIR\gstreamer-plugins\gstfaac.dll"
Delete "$INSTDIR\gstreamer-plugins\gstfaad.dll"
@ -1204,6 +1236,10 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\gstisomp4.dll"
Delete "$INSTDIR\gstreamer-plugins\gstlame.dll"
Delete "$INSTDIR\gstreamer-plugins\gstlibav.dll"
Delete "$INSTDIR\gstreamer-plugins\gstmpegpsdemux.dll"
Delete "$INSTDIR\gstreamer-plugins\gstmpegpsmux.dll"
Delete "$INSTDIR\gstreamer-plugins\gstmpegtsdemux.dll"
Delete "$INSTDIR\gstreamer-plugins\gstmpegtsmux.dll"
Delete "$INSTDIR\gstreamer-plugins\gstmpg123.dll"
Delete "$INSTDIR\gstreamer-plugins\gstmusepack.dll"
Delete "$INSTDIR\gstreamer-plugins\gstogg.dll"
@ -1227,10 +1263,14 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\gstvorbis.dll"
Delete "$INSTDIR\gstreamer-plugins\gstwasapi.dll"
Delete "$INSTDIR\gstreamer-plugins\gstwasapi2.dll"
Delete "$INSTDIR\gstreamer-plugins\gstwaveform.dll"
Delete "$INSTDIR\gstreamer-plugins\gstwavenc.dll"
Delete "$INSTDIR\gstreamer-plugins\gstwavpack.dll"
Delete "$INSTDIR\gstreamer-plugins\gstwavparse.dll"
Delete "$INSTDIR\gstreamer-plugins\gstxingmux.dll"
!ifdef arch_x64
Delete "$INSTDIR\gstreamer-plugins\gstspotify.dll"
!endif
!endif ; msvc
Delete "$INSTDIR\Uninstall.exe"

View File

@ -21,8 +21,6 @@
#include "gstfastspectrum.h"
#include "gstmoodbarplugin.h"
namespace {
static gboolean gst_moodbar_plugin_init(GstPlugin *plugin) {
if (!gst_element_register(plugin, "fastspectrum", GST_RANK_NONE, GST_TYPE_FASTSPECTRUM)) {
@ -32,8 +30,6 @@ static gboolean gst_moodbar_plugin_init(GstPlugin *plugin) {
return TRUE;
}
} // namespace
int gstfastspectrum_register_static() {
return gst_plugin_register_static(

View File

@ -67,8 +67,8 @@ static QIODevice *sNullDevice = nullptr;
const char *kDefaultLogLevels = "*:3";
static const char *kMessageHandlerMagic = "__logging_message__";
static const size_t kMessageHandlerMagicLength = strlen(kMessageHandlerMagic);
static constexpr char kMessageHandlerMagic[] = "__logging_message__";
static const size_t kMessageHandlerMagicLen = strlen(kMessageHandlerMagic);
static QtMessageHandler sOriginalMessageHandler = nullptr;
template<class T>
@ -135,9 +135,9 @@ class LoggedDebug : public DebugBase<LoggedDebug> {
static void MessageHandler(QtMsgType type, const QMessageLogContext&, const QString &message) {
if (message.startsWith(kMessageHandlerMagic)) {
if (message.startsWith(QLatin1String(kMessageHandlerMagic))) {
QByteArray message_data = message.toUtf8();
fprintf(type == QtCriticalMsg || type == QtFatalMsg ? stderr : stdout, "%s\n", message_data.constData() + kMessageHandlerMagicLength);
fprintf(type == QtCriticalMsg || type == QtFatalMsg ? stderr : stdout, "%s\n", message_data.constData() + kMessageHandlerMagicLen);
fflush(type == QtCriticalMsg || type == QtFatalMsg ? stderr : stdout);
return;
}
@ -157,8 +157,8 @@ static void MessageHandler(QtMsgType type, const QMessageLogContext&, const QStr
break;
}
for (const QString &line : message.split('\n')) {
BufferedDebug d = CreateLogger<BufferedDebug>(level, "unknown", -1, nullptr);
for (const QString &line : message.split(QLatin1Char('\n'))) {
BufferedDebug d = CreateLogger<BufferedDebug>(level, QStringLiteral("unknown"), -1, nullptr);
d << line.toLocal8Bit().constData();
if (d.buf_) {
d.buf_->close();
@ -193,8 +193,8 @@ void SetLevels(const QString &levels) {
if (!sClassLevels) return;
for (const QString &item : levels.split(',')) {
const QStringList class_level = item.split(':');
for (const QString &item : levels.split(QLatin1Char(','))) {
const QStringList class_level = item.split(QLatin1Char(':'));
QString class_name;
bool ok = false;
@ -212,7 +212,7 @@ void SetLevels(const QString &levels) {
continue;
}
if (class_name.isEmpty() || class_name == "*") {
if (class_name.isEmpty() || class_name == QLatin1Char('*')) {
sDefaultLevel = static_cast<Level>(level);
}
else {
@ -225,10 +225,10 @@ void SetLevels(const QString &levels) {
static QString ParsePrettyFunction(const char *pretty_function) {
// Get the class name out of the function name.
QString class_name = pretty_function;
const qint64 paren = class_name.indexOf('(');
QString class_name = QLatin1String(pretty_function);
const qint64 paren = class_name.indexOf(QLatin1Char('('));
if (paren != -1) {
const qint64 colons = class_name.lastIndexOf("::", paren);
const qint64 colons = class_name.lastIndexOf(QLatin1String("::"), paren);
if (colons != -1) {
class_name = class_name.left(colons);
}
@ -237,7 +237,7 @@ static QString ParsePrettyFunction(const char *pretty_function) {
}
}
const qint64 space = class_name.lastIndexOf(' ');
const qint64 space = class_name.lastIndexOf(QLatin1Char(' '));
if (space != -1) {
class_name = class_name.mid(space + 1);
}
@ -259,7 +259,7 @@ static T CreateLogger(Level level, const QString &class_name, int line, const ch
case Level_Fatal: level_name = " FATAL "; break;
}
QString filter_category = (category != nullptr) ? category : class_name;
QString filter_category = (category != nullptr) ? QLatin1String(category) : class_name;
// Check the settings to see if we're meant to show or hide this message.
Level threshold_level = sDefaultLevel;
if (sClassLevels && sClassLevels->contains(filter_category)) {
@ -272,10 +272,10 @@ static T CreateLogger(Level level, const QString &class_name, int line, const ch
QString function_line = class_name;
if (line != -1) {
function_line += ":" + QString::number(line);
function_line += QLatin1Char(':') + QString::number(line);
}
if (category) {
function_line += "(" + QString(category) + ")";
function_line += QLatin1Char('(') + QLatin1String(category) + QLatin1Char(')');
}
QtMsgType type = QtDebugMsg;
@ -284,7 +284,7 @@ static T CreateLogger(Level level, const QString &class_name, int line, const ch
}
T ret(type);
ret.nospace() << QDateTime::currentDateTime().toString("hh:mm:ss.zzz").toLatin1().constData() << level_name << function_line.leftJustified(32).toLatin1().constData();
ret.nospace() << QDateTime::currentDateTime().toString(QStringLiteral("hh:mm:ss.zzz")).toLatin1().constData() << level_name << function_line.leftJustified(32).toLatin1().constData();
return ret.space();
@ -310,7 +310,7 @@ QString CXXDemangle(const QString &mangled_function) {
QString LinuxDemangle(const QString &symbol);
QString LinuxDemangle(const QString &symbol) {
QRegularExpression regex("\\(([^+]+)");
QRegularExpression regex(QStringLiteral("\\(([^+]+)"));
QRegularExpressionMatch match = regex.match(symbol);
if (!match.hasMatch()) {
return symbol;
@ -326,9 +326,9 @@ QString DarwinDemangle(const QString &symbol);
QString DarwinDemangle(const QString &symbol) {
# if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QStringList split = symbol.split(' ', Qt::SkipEmptyParts);
QStringList split = symbol.split(QLatin1Char(' '), Qt::SkipEmptyParts);
# else
QStringList split = symbol.split(' ', QString::SkipEmptyParts);
QStringList split = symbol.split(QLatin1Char(' '), QString::SkipEmptyParts);
# endif
QString mangled_function = split[3];
return CXXDemangle(mangled_function);
@ -392,7 +392,7 @@ namespace {
template<typename T>
QString print_duration(T duration, const std::string &unit) {
return QString("%1%2").arg(duration.count()).arg(unit.c_str());
return QStringLiteral("%1%2").arg(duration.count()).arg(QString::fromStdString(unit));
}
} // namespace

View File

@ -23,6 +23,7 @@
#include <cstdio>
#include <cstddef>
#include <utility>
#include <QtGlobal>
#include <QObject>
@ -31,6 +32,7 @@
#include <QMutex>
#include <QLocalServer>
#include <QProcess>
#include <QDir>
#include <QFile>
#include <QList>
#include <QQueue>
@ -170,7 +172,7 @@ WorkerPool<HandlerType>::WorkerPool(QObject *parent)
local_server_name_ = qApp->applicationName().toLower();
if (local_server_name_.isEmpty()) {
local_server_name_ = "workerpool";
local_server_name_ = QStringLiteral("workerpool");
}
}
@ -242,15 +244,15 @@ void WorkerPool<HandlerType>::DoStart() {
QStringList search_path;
search_path << QCoreApplication::applicationDirPath();
#if defined(Q_OS_UNIX)
search_path << "/usr/libexec";
search_path << "/usr/local/libexec";
search_path << QStringLiteral("/usr/libexec");
search_path << QStringLiteral("/usr/local/libexec");
#endif
#if defined(Q_OS_MACOS) && defined(USE_BUNDLE)
search_path << QCoreApplication::applicationDirPath() + "/" + USE_BUNDLE_DIR;
#if defined(Q_OS_MACOS)
search_path << QDir::cleanPath(QCoreApplication::applicationDirPath() + QStringLiteral("/../PlugIns"));
#endif
for (const QString &path_prefix : search_path) {
const QString executable_path = path_prefix + "/" + executable_name_;
for (const QString &path_prefix : std::as_const(search_path)) {
const QString executable_path = path_prefix + QLatin1Char('/') + executable_name_;
if (QFile::exists(executable_path)) {
executable_path_ = executable_path;
qLog(Debug) << "Using worker" << executable_name_ << "from" << path_prefix;
@ -293,7 +295,7 @@ void WorkerPool<HandlerType>::StartOneWorker(Worker *worker) {
// Create a server, find an unused name and start listening
forever {
const quint32 unique_number = QRandomGenerator::global()->bounded(static_cast<quint32>(quint64(this) & 0xFFFFFFFF));
const QString name = QString("%1_%2").arg(local_server_name_).arg(unique_number);
const QString name = QStringLiteral("%1_%2").arg(local_server_name_).arg(unique_number);
if (worker->local_server_->listen(name)) {
break;

View File

@ -31,15 +31,15 @@
#include "tagreaderbase.h"
TagReaderBase::TagReaderBase() = default;
TagReaderBase::~TagReaderBase() = default;
float TagReaderBase::ConvertPOPMRating(const int POPM_rating) {
if (POPM_rating < 0x01) return 0.0F;
else if (POPM_rating < 0x40) return 0.20F;
else if (POPM_rating < 0x80) return 0.40F;
else if (POPM_rating < 0xC0) return 0.60F;
else if (POPM_rating < 0xFC) return 0.80F;
if (POPM_rating < 0x40) return 0.20F;
if (POPM_rating < 0x80) return 0.40F;
if (POPM_rating < 0xC0) return 0.60F;
if (POPM_rating < 0xFC) return 0.80F;
return 1.0F;
@ -48,10 +48,10 @@ float TagReaderBase::ConvertPOPMRating(const int POPM_rating) {
int TagReaderBase::ConvertToPOPMRating(const float rating) {
if (rating < 0.20) return 0x00;
else if (rating < 0.40) return 0x01;
else if (rating < 0.60) return 0x40;
else if (rating < 0.80) return 0x80;
else if (rating < 1.0) return 0xC0;
if (rating < 0.40) return 0x01;
if (rating < 0.60) return 0x40;
if (rating < 0.80) return 0x80;
if (rating < 1.0) return 0xC0;
return 0xFF;
@ -74,7 +74,7 @@ TagReaderBase::Cover TagReaderBase::LoadCoverFromRequest(const spb::tagreader::S
}
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()));
cover_mime_type = QString::fromStdString(request.cover_mime_type());
}
return LoadCoverFromRequest(song_filename, cover_filename, cover_data, cover_mime_type);
@ -94,7 +94,7 @@ TagReaderBase::Cover TagReaderBase::LoadCoverFromRequest(const spb::tagreader::S
}
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()));
cover_mime_type = QString::fromStdString(request.cover_mime_type());
}
return LoadCoverFromRequest(song_filename, cover_filename, cover_data, cover_mime_type);
@ -118,24 +118,28 @@ TagReaderBase::Cover TagReaderBase::LoadCoverFromRequest(const QString &song_fil
if (cover_mime_type.isEmpty()) {
cover_mime_type = QMimeDatabase().mimeTypeForData(cover_data).name();
}
if (cover_mime_type == "image/jpeg") {
if (cover_mime_type == QLatin1String("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") {
if (cover_mime_type == QLatin1String("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);
QImage cover_image;
if (!cover_image.loadFromData(cover_data)) {
qLog(Error) << "Failed to load image from cover data for" << song_filename;
return Cover();
}
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(cover_data, QStringLiteral("image/jpeg"));
}
return Cover();

View File

@ -34,7 +34,7 @@
class TagReaderBase {
public:
explicit TagReaderBase();
~TagReaderBase();
~TagReaderBase() = default;
class Cover {
public:

View File

@ -36,16 +36,16 @@
#include "tagreadertaglib.h"
bool GME::IsSupportedFormat(const QFileInfo &file_info) {
return file_info.exists() && (file_info.completeSuffix().endsWith("spc", Qt::CaseInsensitive) || file_info.completeSuffix().endsWith("vgm"), Qt::CaseInsensitive);
return file_info.exists() && (file_info.completeSuffix().endsWith(QLatin1String("spc"), Qt::CaseInsensitive) || file_info.completeSuffix().endsWith(QLatin1String("vgm")), Qt::CaseInsensitive);
}
bool GME::ReadFile(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info) {
if (file_info.completeSuffix().endsWith("spc"), Qt::CaseInsensitive) {
if (file_info.completeSuffix().endsWith(QLatin1String("spc")), Qt::CaseInsensitive) {
SPC::Read(file_info, song_info);
return true;
}
if (file_info.completeSuffix().endsWith("vgm", Qt::CaseInsensitive)) {
if (file_info.completeSuffix().endsWith(QLatin1String("vgm"), Qt::CaseInsensitive)) {
VGM::Read(file_info, song_info);
return true;
}
@ -75,7 +75,7 @@ void GME::SPC::Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *so
qLog(Debug) << "Reading tags from SPC file" << file_info.fileName();
// Check for header -- more reliable than file name alone.
if (!file.read(33).startsWith(QString("SNES-SPC700").toLatin1())) return;
if (!file.read(33).startsWith(QStringLiteral("SNES-SPC700").toLatin1())) return;
// First order of business -- get any tag values that exist within the core file information.
// These only allow for a certain number of bytes per field,
@ -125,7 +125,7 @@ void GME::SPC::Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *so
// Check for XID6 data -- this is infrequently used, but being able to fill in data from this is ideal before trying to rely on APETAG values.
// XID6 format follows EA's binary file format standard named "IFF"
file.seek(XID6_OFFSET);
if (has_id6 && file.read(4) == QString("xid6").toLatin1()) {
if (has_id6 && file.read(4) == QStringLiteral("xid6").toLatin1()) {
QByteArray xid6_head_data = file.read(4);
if (xid6_head_data.size() >= 4) {
qint64 xid6_size = xid6_head_data[0] | (xid6_head_data[1] << 8) | (xid6_head_data[2] << 16) | xid6_head_data[3];
@ -195,7 +195,7 @@ void GME::VGM::Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *so
qLog(Debug) << "Reading tags from VGM file" << file_info.fileName();
if (!file.read(4).startsWith(QString("Vgm ").toLatin1())) return;
if (!file.read(4).startsWith(QStringLiteral("Vgm ").toLatin1())) return;
file.seek(GD3_TAG_PTR);
QByteArray gd3_head = file.read(4);
@ -226,7 +226,7 @@ void GME::VGM::Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *so
#else
fileTagStream.setCodec("UTF-16");
#endif
QStringList strings = fileTagStream.readLine(0).split(QChar('\0'));
QStringList strings = fileTagStream.readLine(0).split(QLatin1Char('\0'));
if (strings.count() < 10) return;
// VGM standard dictates string tag data exist in specific order.
@ -267,7 +267,7 @@ bool GME::VGM::GetPlaybackLength(const QByteArray &sample_count_bytes, const QBy
}
TagReaderGME::TagReaderGME() = default;
TagReaderGME::~TagReaderGME() = default;
bool TagReaderGME::IsMediaFile(const QString &filename) const {
QFileInfo fileinfo(filename);

View File

@ -34,7 +34,7 @@ namespace GME {
bool IsSupportedFormat(const QFileInfo &file_info);
bool ReadFile(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info);
uint32_t UnpackBytes32(const char *const arr, size_t length);
uint32_t UnpackBytes32(const char *const bytes, size_t length);
namespace SPC {
// SPC SPEC: http://vspcplay.raphnet.net/spc_file_format.txt
@ -102,7 +102,7 @@ class TagReaderGME : public TagReaderBase {
public:
explicit TagReaderGME();
~TagReaderGME();
~TagReaderGME() = default;
bool IsMediaFile(const QString &filename) const override;

View File

@ -248,7 +248,7 @@ bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta
song->set_ctime(song->mtime());
}
song->set_lastseen(QDateTime::currentDateTime().toSecsSinceEpoch());
song->set_lastseen(QDateTime::currentSecsSinceEpoch());
std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (fileref->isNull()) {
@ -328,137 +328,8 @@ bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta
}
else if (TagLib::MPEG::File *file_mpeg = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
if (file_mpeg->ID3v2Tag()) {
const TagLib::ID3v2::FrameListMap &map = file_mpeg->ID3v2Tag()->frameListMap();
if (map.contains("TPOS")) disc = TStringToQString(map["TPOS"].front()->toString()).trimmed();
if (map.contains("TCOM")) TStringToStdString(map["TCOM"].front()->toString(), song->mutable_composer());
// content group
if (map.contains("TIT1")) TStringToStdString(map["TIT1"].front()->toString(), song->mutable_grouping());
// original artist/performer
if (map.contains("TOPE")) TStringToStdString(map["TOPE"].front()->toString(), song->mutable_performer());
// Skip TPE1 (which is the artist) here because we already fetched it
// non-standard: Apple, Microsoft
if (map.contains("TPE2")) TStringToStdString(map["TPE2"].front()->toString(), song->mutable_albumartist());
if (map.contains("TCMP")) compilation = TStringToQString(map["TCMP"].front()->toString()).trimmed();
if (map.contains("TDOR")) {
song->set_originalyear(map["TDOR"].front()->toString().substr(0, 4).toInt());
}
else if (map.contains("TORY")) {
song->set_originalyear(map["TORY"].front()->toString().substr(0, 4).toInt());
}
if (map.contains("USLT")) {
TStringToStdString(map["USLT"].front()->toString(), song->mutable_lyrics());
}
else if (map.contains("SYLT")) {
TStringToStdString(map["SYLT"].front()->toString(), song->mutable_lyrics());
}
if (map.contains("APIC")) song->set_art_embedded(true);
// Find a suitable comment tag. For now we ignore iTunNORM comments.
for (uint i = 0; i < map["COMM"].size(); ++i) {
const TagLib::ID3v2::CommentsFrame *frame = dynamic_cast<const TagLib::ID3v2::CommentsFrame*>(map["COMM"][i]);
if (frame && TStringToQString(frame->description()) != "iTunNORM") {
TStringToStdString(frame->text(), song->mutable_comment());
break;
}
}
if (TagLib::ID3v2::UserTextIdentificationFrame *frame_fmps_playcount = TagLib::ID3v2::UserTextIdentificationFrame::find(file_mpeg->ID3v2Tag(), "FMPS_Playcount")) {
TagLib::StringList frame_field_list = frame_fmps_playcount->fieldList();
if (frame_field_list.size() > 1) {
int playcount = TStringToQString(frame_field_list[1]).toInt();
if (song->playcount() <= 0 && playcount > 0) {
song->set_playcount(playcount);
}
}
}
if (TagLib::ID3v2::UserTextIdentificationFrame *frame_fmps_rating = TagLib::ID3v2::UserTextIdentificationFrame::find(file_mpeg->ID3v2Tag(), "FMPS_Rating")) {
TagLib::StringList frame_field_list = frame_fmps_rating->fieldList();
if (frame_field_list.size() > 1) {
float rating = TStringToQString(frame_field_list[1]).toFloat();
if (song->rating() <= 0 && rating > 0 && rating <= 1.0) {
song->set_rating(rating);
}
}
}
if (map.contains("POPM")) {
const TagLib::ID3v2::PopularimeterFrame *frame = dynamic_cast<const TagLib::ID3v2::PopularimeterFrame*>(map["POPM"].front());
if (frame) {
if (song->playcount() <= 0 && frame->counter() > 0) {
song->set_playcount(frame->counter());
}
if (song->rating() <= 0 && frame->rating() > 0) {
song->set_rating(ConvertPOPMRating(frame->rating()));
}
}
}
if (map.contains("UFID")) {
for (uint i = 0; i < map["UFID"].size(); ++i) {
if (TagLib::ID3v2::UniqueFileIdentifierFrame *frame = dynamic_cast<TagLib::ID3v2::UniqueFileIdentifierFrame*>(map["UFID"][i])) {
const TagLib::PropertyMap property_map = frame->asProperties();
if (property_map.contains(kID3v2_MusicBrainz_RecordingID)) {
TStringToStdString(property_map[kID3v2_MusicBrainz_RecordingID].toString(), song->mutable_musicbrainz_recording_id());
}
}
}
}
if (map.contains("TXXX")) {
for (uint i = 0; i < map["TXXX"].size(); ++i) {
if (TagLib::ID3v2::UserTextIdentificationFrame *frame = dynamic_cast<TagLib::ID3v2::UserTextIdentificationFrame*>(map["TXXX"][i])) {
const TagLib::StringList frame_field_list = frame->fieldList();
if (frame_field_list.size() != 2) continue;
if (frame->description() == kID3v2_AcoustID_ID) {
TStringToStdString(frame_field_list.back(), song->mutable_acoustid_id());
}
if (frame->description() == kID3v2_AcoustID_Fingerprint) {
TStringToStdString(frame_field_list.back(), song->mutable_acoustid_fingerprint());
}
if (frame->description() == kID3v2_MusicBrainz_AlbumArtistID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_album_artist_id());
}
if (frame->description() == kID3v2_MusicBrainz_ArtistID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_artist_id());
}
if (frame->description() == kID3v2_MusicBrainz_OriginalArtistID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_original_artist_id());
}
if (frame->description() == kID3v2_MusicBrainz_AlbumID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_album_id());
}
if (frame->description() == kID3v2_MusicBrainz_OriginalAlbumID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_original_album_id());
}
if (frame->description() == kID3v2_MusicBrainz_TrackID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_track_id());
}
if (frame->description() == kID3v2_MusicBrainz_DiscID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_disc_id());
}
if (frame->description() == kID3v2_MusicBrainz_ReleaseGroupID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_release_group_id());
}
if (frame->description() == kID3v2_MusicBrainz_WorkID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_work_id());
}
}
}
}
if (file_mpeg->hasID3v2Tag()) {
ParseID3v2Tag(file_mpeg->ID3v2Tag(), &disc, &compilation, song);
}
}
@ -658,12 +529,18 @@ bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta
if (tag) TStringToStdString(tag->comment(), song->mutable_comment());
}
else if (TagLib::RIFF::WAV::File *file_wav = dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file())) {
if (file_wav->hasID3v2Tag()) {
ParseID3v2Tag(file_wav->ID3v2Tag(), &disc, &compilation, song);
}
}
else if (tag) {
TStringToStdString(tag->comment(), song->mutable_comment());
}
if (!disc.isEmpty()) {
const qint64 i = disc.indexOf('/');
const qint64 i = disc.indexOf(QLatin1Char('/'));
if (i != -1) {
// disc.right( i ).toInt() is total number of discs, we don't use this at the moment
song->set_disc(disc.left(i).toInt());
@ -677,7 +554,7 @@ bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta
// well, it wasn't set, but if the artist is VA assume it's a compilation
const QString albumartist = QString::fromUtf8(song->albumartist().data(), static_cast<qint64>(song->albumartist().size()));
const QString artist = QString::fromUtf8(song->artist().data(), static_cast<qint64>(song->artist().size()));
if (artist.compare("various artists") == 0 || albumartist.compare("various artists") == 0) {
if (artist.compare(QLatin1String("various artists")) == 0 || albumartist.compare(QLatin1String("various artists")) == 0) {
song->set_compilation(true);
}
}
@ -710,6 +587,139 @@ void TagReaderTagLib::TStringToStdString(const TagLib::String &tag, std::string
}
void TagReaderTagLib::ParseID3v2Tag(TagLib::ID3v2::Tag *tag, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const {
TagLib::ID3v2::FrameListMap map = tag->frameListMap();
if (map.contains("TPOS")) *disc = TStringToQString(map["TPOS"].front()->toString()).trimmed();
if (map.contains("TCOM")) TStringToStdString(map["TCOM"].front()->toString(), song->mutable_composer());
// content group
if (map.contains("TIT1")) TStringToStdString(map["TIT1"].front()->toString(), song->mutable_grouping());
// original artist/performer
if (map.contains("TOPE")) TStringToStdString(map["TOPE"].front()->toString(), song->mutable_performer());
// Skip TPE1 (which is the artist) here because we already fetched it
// non-standard: Apple, Microsoft
if (map.contains("TPE2")) TStringToStdString(map["TPE2"].front()->toString(), song->mutable_albumartist());
if (map.contains("TCMP")) *compilation = TStringToQString(map["TCMP"].front()->toString()).trimmed();
if (map.contains("TDOR")) {
song->set_originalyear(map["TDOR"].front()->toString().substr(0, 4).toInt());
}
else if (map.contains("TORY")) {
song->set_originalyear(map["TORY"].front()->toString().substr(0, 4).toInt());
}
if (map.contains("USLT")) {
TStringToStdString(map["USLT"].front()->toString(), song->mutable_lyrics());
}
else if (map.contains("SYLT")) {
TStringToStdString(map["SYLT"].front()->toString(), song->mutable_lyrics());
}
if (map.contains("APIC")) song->set_art_embedded(true);
// Find a suitable comment tag. For now we ignore iTunNORM comments.
for (uint i = 0; i < map["COMM"].size(); ++i) {
const TagLib::ID3v2::CommentsFrame *frame = dynamic_cast<const TagLib::ID3v2::CommentsFrame*>(map["COMM"][i]);
if (frame && TStringToQString(frame->description()) != QLatin1String("iTunNORM")) {
TStringToStdString(frame->text(), song->mutable_comment());
break;
}
}
if (TagLib::ID3v2::UserTextIdentificationFrame *frame_fmps_playcount = TagLib::ID3v2::UserTextIdentificationFrame::find(tag, "FMPS_Playcount")) {
TagLib::StringList frame_field_list = frame_fmps_playcount->fieldList();
if (frame_field_list.size() > 1) {
int playcount = TStringToQString(frame_field_list[1]).toInt();
if (song->playcount() <= 0 && playcount > 0) {
song->set_playcount(playcount);
}
}
}
if (TagLib::ID3v2::UserTextIdentificationFrame *frame_fmps_rating = TagLib::ID3v2::UserTextIdentificationFrame::find(tag, "FMPS_Rating")) {
TagLib::StringList frame_field_list = frame_fmps_rating->fieldList();
if (frame_field_list.size() > 1) {
float rating = TStringToQString(frame_field_list[1]).toFloat();
if (song->rating() <= 0 && rating > 0 && rating <= 1.0) {
song->set_rating(rating);
}
}
}
if (map.contains("POPM")) {
const TagLib::ID3v2::PopularimeterFrame *frame = dynamic_cast<const TagLib::ID3v2::PopularimeterFrame*>(map["POPM"].front());
if (frame) {
if (song->playcount() <= 0 && frame->counter() > 0) {
song->set_playcount(frame->counter());
}
if (song->rating() <= 0 && frame->rating() > 0) {
song->set_rating(ConvertPOPMRating(frame->rating()));
}
}
}
if (map.contains("UFID")) {
for (uint i = 0; i < map["UFID"].size(); ++i) {
if (TagLib::ID3v2::UniqueFileIdentifierFrame *frame = dynamic_cast<TagLib::ID3v2::UniqueFileIdentifierFrame*>(map["UFID"][i])) {
const TagLib::PropertyMap property_map = frame->asProperties();
if (property_map.contains(kID3v2_MusicBrainz_RecordingID)) {
TStringToStdString(property_map[kID3v2_MusicBrainz_RecordingID].toString(), song->mutable_musicbrainz_recording_id());
}
}
}
}
if (map.contains("TXXX")) {
for (uint i = 0; i < map["TXXX"].size(); ++i) {
if (TagLib::ID3v2::UserTextIdentificationFrame *frame = dynamic_cast<TagLib::ID3v2::UserTextIdentificationFrame*>(map["TXXX"][i])) {
const TagLib::StringList frame_field_list = frame->fieldList();
if (frame_field_list.size() != 2) continue;
if (frame->description() == kID3v2_AcoustID_ID) {
TStringToStdString(frame_field_list.back(), song->mutable_acoustid_id());
}
if (frame->description() == kID3v2_AcoustID_Fingerprint) {
TStringToStdString(frame_field_list.back(), song->mutable_acoustid_fingerprint());
}
if (frame->description() == kID3v2_MusicBrainz_AlbumArtistID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_album_artist_id());
}
if (frame->description() == kID3v2_MusicBrainz_ArtistID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_artist_id());
}
if (frame->description() == kID3v2_MusicBrainz_OriginalArtistID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_original_artist_id());
}
if (frame->description() == kID3v2_MusicBrainz_AlbumID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_album_id());
}
if (frame->description() == kID3v2_MusicBrainz_OriginalAlbumID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_original_album_id());
}
if (frame->description() == kID3v2_MusicBrainz_TrackID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_track_id());
}
if (frame->description() == kID3v2_MusicBrainz_DiscID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_disc_id());
}
if (frame->description() == kID3v2_MusicBrainz_ReleaseGroupID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_release_group_id());
}
if (frame->description() == kID3v2_MusicBrainz_WorkID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_work_id());
}
}
}
}
}
void TagReaderTagLib::ParseOggTag(const TagLib::Ogg::FieldListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const {
if (map.contains("COMPOSER")) TStringToStdString(map["COMPOSER"].front(), song->mutable_composer());
@ -824,7 +834,7 @@ void TagReaderTagLib::SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comment
vorbis_comment->addField("PERFORMER", StdStringToTaglibString(song.performer()), true);
vorbis_comment->addField("GROUPING", StdStringToTaglibString(song.grouping()), true);
vorbis_comment->addField("DISCNUMBER", QStringToTaglibString(song.disc() <= 0 ? QString() : QString::number(song.disc())), true);
vorbis_comment->addField("COMPILATION", QStringToTaglibString(song.compilation() ? "1" : QString()), true);
vorbis_comment->addField("COMPILATION", QStringToTaglibString(song.compilation() ? QStringLiteral("1") : QString()), true);
// Try to be coherent, the two forms are used but the first one is preferred
@ -849,19 +859,19 @@ bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) c
QStringList save_tags_options;
if (save_tags) {
save_tags_options << "tags";
save_tags_options << QStringLiteral("tags");
}
if (save_playcount) {
save_tags_options << "playcount";
save_tags_options << QStringLiteral("playcount");
}
if (save_rating) {
save_tags_options << "rating";
save_tags_options << QStringLiteral("rating");
}
if (save_cover) {
save_tags_options << "embedded cover";
save_tags_options << QStringLiteral("embedded cover");
}
qLog(Debug) << "Saving" << save_tags_options.join(", ") << "to" << filename;
qLog(Debug) << "Saving" << save_tags_options.join(QLatin1String(", ")) << "to" << filename;
const Cover cover = LoadCoverFromRequest(request);
@ -943,14 +953,7 @@ bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) c
TagLib::ID3v2::Tag *tag = file_mpeg->ID3v2Tag(true);
if (!tag) return false;
if (save_tags) {
SetTextFrame("TPOS", song.disc() <= 0 ? QString() : QString::number(song.disc()), tag);
SetTextFrame("TCOM", song.composer().empty() ? std::string() : song.composer(), tag);
SetTextFrame("TIT1", song.grouping().empty() ? std::string() : song.grouping(), tag);
SetTextFrame("TOPE", song.performer().empty() ? std::string() : song.performer(), tag);
// Skip TPE1 (which is the artist) here because we already set it
SetTextFrame("TPE2", song.albumartist().empty() ? std::string() : song.albumartist(), tag);
SetTextFrame("TCMP", song.compilation() ? QString::number(1) : QString(), tag);
SetUnsyncLyricsFrame(song.lyrics().empty() ? std::string() : song.lyrics(), tag);
SaveID3v2Tag(tag, song);
}
if (save_playcount) {
SetPlaycount(tag, song);
@ -959,7 +962,7 @@ bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) c
SetRating(tag, song);
}
if (save_cover) {
SetEmbeddedArt(file_mpeg, tag, cover.data, cover.mime_type);
SetEmbeddedArt(tag, cover.data, cover.mime_type);
}
}
@ -985,6 +988,22 @@ bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) c
}
}
else if (TagLib::RIFF::WAV::File *file_wav = dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file())) {
TagLib::ID3v2::Tag *tag = file_wav->ID3v2Tag();
if (save_tags) {
SaveID3v2Tag(tag, song);
}
if (save_playcount) {
SetPlaycount(tag, song);
}
if (save_rating) {
SetRating(tag, song);
}
if (save_cover) {
SetEmbeddedArt(tag, cover.data, cover.mime_type);
}
}
// Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same way;
// apart, so we keep specific behavior for some formats by adding another "else if" block above.
if (!is_flac) {
@ -1016,6 +1035,19 @@ bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) c
}
void TagReaderTagLib::SaveID3v2Tag(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const {
SetTextFrame("TPOS", song.disc() <= 0 ? QString() : QString::number(song.disc()), tag);
SetTextFrame("TCOM", song.composer().empty() ? std::string() : song.composer(), tag);
SetTextFrame("TIT1", song.grouping().empty() ? std::string() : song.grouping(), tag);
SetTextFrame("TOPE", song.performer().empty() ? std::string() : song.performer(), tag);
// Skip TPE1 (which is the artist) here because we already set it
SetTextFrame("TPE2", song.albumartist().empty() ? std::string() : song.albumartist(), tag);
SetTextFrame("TCMP", song.compilation() ? QString::number(1) : QString(), tag);
SetUnsyncLyricsFrame(song.lyrics().empty() ? std::string() : song.lyrics(), tag);
}
void TagReaderTagLib::SaveAPETag(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const {
tag->setItem("album artist", TagLib::APE::Item("album artist", TagLib::StringList(song.albumartist().c_str())));
@ -1278,9 +1310,7 @@ void TagReaderTagLib::SetEmbeddedArt(TagLib::Ogg::XiphComment *xiph_comment, con
}
void TagReaderTagLib::SetEmbeddedArt(TagLib::MPEG::File *file_mp3, TagLib::ID3v2::Tag *tag, const QByteArray &data, const QString &mime_type) const {
(void)file_mp3;
void TagReaderTagLib::SetEmbeddedArt(TagLib::ID3v2::Tag *tag, const QByteArray &data, const QString &mime_type) const {
// Remove existing covers
TagLib::ID3v2::FrameList apiclist = tag->frameListMap()["APIC"];
@ -1311,10 +1341,10 @@ void TagReaderTagLib::SetEmbeddedArt(TagLib::MP4::File *aac_file, TagLib::MP4::T
}
else {
TagLib::MP4::CoverArt::Format cover_format = TagLib::MP4::CoverArt::Format::JPEG;
if (mime_type == "image/jpeg") {
if (mime_type == QLatin1String("image/jpeg")) {
cover_format = TagLib::MP4::CoverArt::Format::JPEG;
}
else if (mime_type == "image/png") {
else if (mime_type == QLatin1String("image/png")) {
cover_format = TagLib::MP4::CoverArt::Format::PNG;
}
else {
@ -1360,7 +1390,7 @@ bool TagReaderTagLib::SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtReque
else if (TagLib::MPEG::File *file_mp3 = dynamic_cast<TagLib::MPEG::File*>(fileref.file())) {
TagLib::ID3v2::Tag *tag = file_mp3->ID3v2Tag();
if (!tag) return false;
SetEmbeddedArt(file_mp3, tag, cover.data, cover.mime_type);
SetEmbeddedArt(tag, cover.data, cover.mime_type);
}
// MP4/AAC
@ -1427,7 +1457,7 @@ void TagReaderTagLib::SetPlaycount(TagLib::APE::Tag *tag, const spb::tagreader::
void TagReaderTagLib::SetPlaycount(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const {
SetUserTextFrame("FMPS_Playcount", QString::number(song.playcount()), tag);
SetUserTextFrame(QStringLiteral("FMPS_Playcount"), QString::number(song.playcount()), tag);
TagLib::ID3v2::PopularimeterFrame *frame = GetPOPMFrameFromTag(tag);
if (frame) {
frame->setCounter(song.playcount());
@ -1546,7 +1576,7 @@ void TagReaderTagLib::SetRating(TagLib::APE::Tag *tag, const spb::tagreader::Son
void TagReaderTagLib::SetRating(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const {
SetUserTextFrame("FMPS_Rating", QString::number(song.rating()), tag);
SetUserTextFrame(QStringLiteral("FMPS_Rating"), QString::number(song.rating()), tag);
TagLib::ID3v2::PopularimeterFrame *frame = GetPOPMFrameFromTag(tag);
if (frame) {
frame->setRating(ConvertToPOPMRating(song.rating()));

View File

@ -68,10 +68,12 @@ class TagReaderTagLib : public TagReaderBase {
private:
spb::tagreader::SongMetadata_FileType GuessFileType(TagLib::FileRef *fileref) const;
void ParseID3v2Tag(TagLib::ID3v2::Tag *tag, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
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_comment, const spb::tagreader::SongMetadata &song) const;
void SaveID3v2Tag(TagLib::ID3v2::Tag *tag, 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;
@ -98,7 +100,7 @@ class TagReaderTagLib : public TagReaderBase {
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::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:

View File

@ -61,34 +61,34 @@ bool TagReaderWorker::HandleMessage(const spb::tagreader::Message &message, spb:
reply.mutable_is_media_file_response()->set_success(success);
return success;
}
else if (message.has_read_file_request()) {
const QString filename = QString::fromUtf8(message.read_file_request().filename().data(), static_cast<qint64>(static_cast<qint64>(message.read_file_request().filename().size())));
if (message.has_read_file_request()) {
const QString filename = QString::fromUtf8(message.read_file_request().filename().data(), 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()) {
if (message.has_save_file_request()) {
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()) {
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())));
if (message.has_load_embedded_art_request()) {
const QString filename = QString::fromUtf8(message.load_embedded_art_request().filename().data(), 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()) {
if (message.has_save_embedded_art_request()) {
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()) {
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())));
if (message.has_save_song_playcount_to_file_request()) {
const QString filename = QString::fromUtf8(message.save_song_playcount_to_file_request().filename().data(), 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()) {
if (message.has_save_song_rating_to_file_request()) {
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);

View File

@ -24,6 +24,7 @@ set(SOURCES
core/networktimeouts.cpp
core/networkproxyfactory.cpp
core/qtfslistener.cpp
core/settings.cpp
core/settingsprovider.cpp
core/signalchecker.cpp
core/song.cpp
@ -39,7 +40,7 @@ set(SOURCES
core/scopedtransaction.cpp
core/translations.cpp
core/systemtrayicon.cpp
core/localredirectserver.cpp
utilities/strutils.cpp
utilities/envutils.cpp
utilities/colorutils.cpp
@ -58,6 +59,7 @@ set(SOURCES
utilities/coverutils.cpp
utilities/screenutils.cpp
utilities/searchparserutils.cpp
utilities/textencodingutils.cpp
engine/enginebase.cpp
engine/enginedevice.cpp
@ -72,6 +74,7 @@ set(SOURCES
analyzer/boomanalyzer.cpp
analyzer/rainbowanalyzer.cpp
analyzer/sonogram.cpp
analyzer/waverubber.cpp
equalizer/equalizer.cpp
equalizer/equalizerslider.cpp
@ -89,12 +92,14 @@ set(SOURCES
collection/collectiondirectorymodel.cpp
collection/collectionfilteroptions.cpp
collection/collectionfilterwidget.cpp
collection/collectionfilter.cpp
collection/collectionplaylistitem.cpp
collection/collectionquery.cpp
collection/collectionqueryoptions.cpp
collection/savedgroupingmanager.cpp
collection/groupbydialog.cpp
collection/collectiontask.cpp
collection/collectionmodelupdate.cpp
playlist/playlist.cpp
playlist/playlistbackend.cpp
@ -113,6 +118,7 @@ set(SOURCES
playlist/playlisttabbar.cpp
playlist/playlistundocommands.cpp
playlist/playlistview.cpp
playlist/playlistproxystyle.cpp
playlist/songloaderinserter.cpp
playlist/songplaylistitem.cpp
playlist/dynamicplaylistcontrols.cpp
@ -169,7 +175,7 @@ set(SOURCES
covermanager/deezercoverprovider.cpp
covermanager/qobuzcoverprovider.cpp
covermanager/musixmatchcoverprovider.cpp
covermanager/spotifycoverprovider.cpp
covermanager/opentidalcoverprovider.cpp
lyrics/lyricsproviders.cpp
lyrics/lyricsprovider.cpp
@ -187,7 +193,7 @@ set(SOURCES
lyrics/songlyricscomlyricsprovider.cpp
lyrics/azlyricscomlyricsprovider.cpp
lyrics/elyricsnetlyricsprovider.cpp
lyrics/lyricsmodecomlyricsprovider.cpp
lyrics/letraslyricsprovider.cpp
providers/musixmatchprovider.cpp
@ -195,6 +201,7 @@ set(SOURCES
settings/settingspage.cpp
settings/behavioursettingspage.cpp
settings/collectionsettingspage.cpp
settings/collectionsettingsdirectorymodel.cpp
settings/backendsettingspage.cpp
settings/playlistsettingspage.cpp
settings/scrobblersettingspage.cpp
@ -248,19 +255,18 @@ set(SOURCES
osd/osdbase.cpp
osd/osdpretty.cpp
internet/internetservices.cpp
internet/internetservice.cpp
internet/internetplaylistitem.cpp
internet/internetsearchview.cpp
internet/internetsearchmodel.cpp
internet/internetsearchsortmodel.cpp
internet/internetsearchitemdelegate.cpp
internet/localredirectserver.cpp
internet/internetsongsview.cpp
internet/internettabsview.cpp
internet/internetcollectionview.cpp
internet/internetcollectionviewcontainer.cpp
internet/internetsearchview.cpp
streaming/streamingservices.cpp
streaming/streamingservice.cpp
streaming/streamplaylistitem.cpp
streaming/streamingsearchview.cpp
streaming/streamingsearchmodel.cpp
streaming/streamingsearchsortmodel.cpp
streaming/streamingsearchitemdelegate.cpp
streaming/streamingsongsview.cpp
streaming/streamingtabsview.cpp
streaming/streamingcollectionview.cpp
streaming/streamingcollectionviewcontainer.cpp
streaming/streamingsearchview.cpp
radios/radioservices.cpp
radios/radiobackend.cpp
@ -305,6 +311,7 @@ set(HEADERS
core/threadsafenetworkdiskcache.h
core/networktimeouts.h
core/qtfslistener.h
core/settings.h
core/songloader.h
core/tagreaderclient.h
core/taskmanager.h
@ -315,6 +322,7 @@ set(HEADERS
core/potranslator.h
core/mimedata.h
core/stylesheetloader.h
core/localredirectserver.h
engine/enginebase.h
engine/devicefinders.h
@ -325,6 +333,7 @@ set(HEADERS
analyzer/boomanalyzer.h
analyzer/rainbowanalyzer.h
analyzer/sonogram.h
analyzer/waverubber.h
equalizer/equalizer.h
equalizer/equalizerslider.h
@ -341,6 +350,7 @@ set(HEADERS
collection/collectionviewcontainer.h
collection/collectiondirectorymodel.h
collection/collectionfilterwidget.h
collection/collectionfilter.h
collection/savedgroupingmanager.h
collection/groupbydialog.h
@ -359,6 +369,7 @@ set(HEADERS
playlist/playlistsequence.h
playlist/playlisttabbar.h
playlist/playlistview.h
playlist/playlistproxystyle.h
playlist/playlistitemmimedata.h
playlist/songloaderinserter.h
playlist/songmimedata.h
@ -413,7 +424,7 @@ set(HEADERS
covermanager/deezercoverprovider.h
covermanager/qobuzcoverprovider.h
covermanager/musixmatchcoverprovider.h
covermanager/spotifycoverprovider.h
covermanager/opentidalcoverprovider.h
lyrics/lyricsproviders.h
lyrics/lyricsprovider.h
@ -429,12 +440,13 @@ set(HEADERS
lyrics/songlyricscomlyricsprovider.h
lyrics/azlyricscomlyricsprovider.h
lyrics/elyricsnetlyricsprovider.h
lyrics/lyricsmodecomlyricsprovider.h
lyrics/letraslyricsprovider.h
settings/settingsdialog.h
settings/settingspage.h
settings/behavioursettingspage.h
settings/collectionsettingspage.h
settings/collectionsettingsdirectorymodel.h
settings/backendsettingspage.h
settings/playlistsettingspage.h
settings/scrobblersettingspage.h
@ -484,22 +496,22 @@ set(HEADERS
widgets/qsearchfield.h
widgets/ratingwidget.h
widgets/forcescrollperpixel.h
widgets/resizabletextedit.h
osd/osdbase.h
osd/osdpretty.h
internet/internetservices.h
internet/internetservice.h
internet/internetsongmimedata.h
internet/internetsearchmodel.h
internet/internetsearchsortmodel.h
internet/internetsearchitemdelegate.h
internet/internetsearchview.h
internet/localredirectserver.h
internet/internetsongsview.h
internet/internettabsview.h
internet/internetcollectionview.h
internet/internetcollectionviewcontainer.h
streaming/streamingservices.h
streaming/streamingservice.h
streaming/streamsongmimedata.h
streaming/streamingsearchmodel.h
streaming/streamingsearchsortmodel.h
streaming/streamingsearchitemdelegate.h
streaming/streamingsearchview.h
streaming/streamingsongsview.h
streaming/streamingtabsview.h
streaming/streamingcollectionview.h
streaming/streamingcollectionviewcontainer.h
radios/radioservices.h
radios/radiobackend.h
@ -590,9 +602,9 @@ set(UI
osd/osdpretty.ui
internet/internettabsview.ui
internet/internetcollectionviewcontainer.ui
internet/internetsearchview.ui
streaming/streamingtabsview.ui
streaming/streamingcollectionviewcontainer.ui
streaming/streamingsearchview.ui
radios/radioviewcontainer.ui
@ -848,7 +860,7 @@ optional_source(WIN32
HEADERS
core/windows7thumbbar.h
)
optional_source(MSVC SOURCES engine/uwpdevicefinder.cpp)
optional_source(MSVC SOURCES engine/uwpdevicefinder.cpp engine/asiodevicefinder.cpp)
optional_source(HAVE_SUBSONIC
SOURCES
@ -894,6 +906,25 @@ optional_source(HAVE_TIDAL
settings/tidalsettingspage.ui
)
optional_source(HAVE_SPOTIFY
SOURCES
spotify/spotifyservice.cpp
spotify/spotifybaserequest.cpp
spotify/spotifyrequest.cpp
spotify/spotifyfavoriterequest.cpp
settings/spotifysettingspage.cpp
covermanager/spotifycoverprovider.cpp
HEADERS
spotify/spotifyservice.h
spotify/spotifybaserequest.h
spotify/spotifyrequest.h
spotify/spotifyfavoriterequest.h
settings/spotifysettingspage.h
covermanager/spotifycoverprovider.h
UI
settings/spotifysettingspage.ui
)
optional_source(HAVE_QOBUZ
SOURCES
qobuz/qobuzservice.cpp
@ -986,14 +1017,9 @@ link_directories(
${SQLITE_LIBRARY_DIRS}
${PROTOBUF_LIBRARY_DIRS}
${SINGLEAPPLICATION_LIBRARY_DIRS}
${ICU_LIBRARY_DIRS}
)
if(HAVE_ICU)
link_directories(${ICU_LIBRARY_DIRS})
else()
link_directories(${Iconv_LIBRARY_DIRS})
endif()
if(HAVE_ALSA)
link_directories(${ALSA_LIBRARY_DIRS})
endif()
@ -1077,6 +1103,7 @@ target_include_directories(strawberry_lib SYSTEM PUBLIC
${GOBJECT_INCLUDE_DIRS}
${SQLITE_INCLUDE_DIRS}
${PROTOBUF_INCLUDE_DIRS}
${ICU_INCLUDE_DIRS}
)
if(HAVE_QPA_QPLATFORMNATIVEINTERFACE_H)
@ -1099,6 +1126,7 @@ target_link_libraries(strawberry_lib PUBLIC
${GLIB_LIBRARIES}
${GOBJECT_LIBRARIES}
${SQLITE_LIBRARIES}
${ICU_LIBRARIES}
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Concurrent
Qt${QT_VERSION_MAJOR}::Gui
@ -1119,17 +1147,6 @@ if(HAVE_X11_GLOBALSHORTCUTS AND HAVE_X11EXTRAS)
target_link_libraries(strawberry_lib PUBLIC Qt${QT_VERSION_MAJOR}::X11Extras)
endif()
if(HAVE_ICU)
target_include_directories(strawberry_lib SYSTEM PRIVATE ${ICU_INCLUDE_DIRS})
target_link_libraries(strawberry_lib PRIVATE ${ICU_LIBRARIES})
else()
if(FREEBSD AND NOT Iconv_LIBRARIES)
set(Iconv_LIBRARIES iconv)
endif()
target_include_directories(strawberry_lib SYSTEM PRIVATE ${Iconv_INCLUDE_DIRS})
target_link_libraries(strawberry_lib PRIVATE ${Iconv_LIBRARIES})
endif()
if(HAVE_ALSA)
target_include_directories(strawberry_lib SYSTEM PRIVATE ${ALSA_INCLUDE_DIRS})
target_link_libraries(strawberry_lib PRIVATE ${ALSA_LIBRARIES})

View File

@ -88,7 +88,7 @@ void AnalyzerBase::ChangeTimeout(const int timeout) {
void AnalyzerBase::transform(Scope &scope) {
QVector<float> aux(fht_->size());
if (static_cast<unsigned long int>(aux.size()) >= scope.size()) {
if (static_cast<quint64>(aux.size()) >= scope.size()) {
std::copy(scope.begin(), scope.end(), aux.begin());
}
else {
@ -108,7 +108,7 @@ void AnalyzerBase::paintEvent(QPaintEvent *e) {
p.fillRect(e->rect(), palette().color(QPalette::Window));
switch (engine_->state()) {
case EngineBase::State::Playing: {
case EngineBase::State::Playing:{
const EngineBase::Scope &thescope = engine_->scope(timeout_);
int i = 0;

View File

@ -41,9 +41,11 @@
#include "boomanalyzer.h"
#include "rainbowanalyzer.h"
#include "sonogram.h"
#include "waverubber.h"
#include "core/logging.h"
#include "core/shared_ptr.h"
#include "core/settings.h"
#include "engine/enginebase.h"
using namespace std::chrono_literals;
@ -52,10 +54,12 @@ const char *AnalyzerContainer::kSettingsGroup = "Analyzer";
const char *AnalyzerContainer::kSettingsFramerate = "framerate";
// Framerates
const int AnalyzerContainer::kLowFramerate = 20;
const int AnalyzerContainer::kMediumFramerate = 25;
const int AnalyzerContainer::kHighFramerate = 30;
const int AnalyzerContainer::kSuperHighFramerate = 60;
namespace {
constexpr int kLowFramerate = 20;
constexpr int kMediumFramerate = 25;
constexpr int kHighFramerate = 30;
constexpr int kSuperHighFramerate = 60;
} // namespace
AnalyzerContainer::AnalyzerContainer(QWidget *parent)
: QWidget(parent),
@ -87,6 +91,7 @@ AnalyzerContainer::AnalyzerContainer(QWidget *parent)
AddAnalyzerType<NyanCatAnalyzer>();
AddAnalyzerType<RainbowDashAnalyzer>();
AddAnalyzerType<Sonogram>();
AddAnalyzerType<WaveRubber>();
disable_action_ = context_menu_->addAction(tr("No analyzer"), this, &AnalyzerContainer::DisableAnalyzer);
disable_action_->setCheckable(true);
@ -178,9 +183,9 @@ void AnalyzerContainer::ChangeFramerate(int new_framerate) {
void AnalyzerContainer::Load() {
QSettings s;
Settings s;
s.beginGroup(kSettingsGroup);
QString type = s.value("type", "BlockAnalyzer").toString();
QString type = s.value("type", QStringLiteral("BlockAnalyzer")).toString();
current_framerate_ = s.value(kSettingsFramerate, kMediumFramerate).toInt();
s.endGroup();
@ -191,7 +196,7 @@ void AnalyzerContainer::Load() {
}
else {
for (int i = 0; i < analyzer_types_.count(); ++i) {
if (type == analyzer_types_[i]->className()) {
if (type == QString::fromLatin1(analyzer_types_[i]->className())) {
ChangeAnalyzer(i);
actions_[i]->setChecked(true);
break;
@ -215,7 +220,7 @@ void AnalyzerContainer::SaveFramerate(const int framerate) {
// For now, framerate is common for all analyzers. Maybe each analyzer should have its own framerate?
current_framerate_ = framerate;
QSettings s;
Settings s;
s.beginGroup(kSettingsGroup);
s.setValue(kSettingsFramerate, current_framerate_);
s.endGroup();
@ -224,9 +229,9 @@ void AnalyzerContainer::SaveFramerate(const int framerate) {
void AnalyzerContainer::Save() {
QSettings s;
Settings s;
s.beginGroup(kSettingsGroup);
s.setValue("type", current_analyzer_ ? current_analyzer_->metaObject()->className() : QVariant());
s.setValue("type", current_analyzer_ ? QString::fromLatin1(current_analyzer_->metaObject()->className()) : QVariant());
s.endGroup();
}

View File

@ -46,7 +46,6 @@ class AnalyzerContainer : public QWidget {
explicit AnalyzerContainer(QWidget *parent);
void SetEngine(SharedPtr<EngineBase> engine);
void SetActions(QAction *visualisation);
static const char *kSettingsGroup;
static const char *kSettingsFramerate;
@ -55,7 +54,7 @@ class AnalyzerContainer : public QWidget {
void WheelEvent(const int delta);
protected:
void mouseReleaseEvent(QMouseEvent*) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void wheelEvent(QWheelEvent *e) override;
private slots:
@ -65,11 +64,6 @@ class AnalyzerContainer : public QWidget {
void ShowPopupMenu();
private:
static const int kLowFramerate;
static const int kMediumFramerate;
static const int kHighFramerate;
static const int kSuperHighFramerate;
void Load();
void Save();
void SaveFramerate(const int framerate);

View File

@ -266,12 +266,12 @@ QColor ensureContrast(const QColor &bg, const QColor &fg, int amount) {
// value is the best measure of contrast
// if there is enough difference in value already, return fg unchanged
if (dv > static_cast<int>(amount)) return fg;
if (dv > amount) return fg;
int ds = abs(bs - fs);
// saturation is good enough too. But not as good. TODO adapt this a little
if (ds > static_cast<int>(amount)) return fg;
if (ds > amount) return fg;
int dh = abs(bh - fh);
@ -285,7 +285,7 @@ QColor ensureContrast(const QColor &bg, const QColor &fg, int amount) {
if (ds > amount / 2 && (bs > 125 && fs > 125)) {
return fg;
}
else if (dv > amount / 2 && (bv > 125 && fv > 125)) {
if (dv > amount / 2 && (bv > 125 && fv > 125)) {
return fg;
}
}
@ -294,7 +294,7 @@ QColor ensureContrast(const QColor &bg, const QColor &fg, int amount) {
// low saturation on a low saturation is sad
const int tmp = 50 - fs;
fs = 50;
if (static_cast<int>(amount) > tmp) {
if (amount > tmp) {
amount -= tmp;
}
else {
@ -310,25 +310,25 @@ QColor ensureContrast(const QColor &bg, const QColor &fg, int amount) {
if (amount > 0) adjustToLimits(bs, fs, amount);
// see if we need to adjust the hue
if (static_cast<int>(amount) > 0)
fh += static_cast<int>(amount); // cycles around;
if (amount > 0)
fh += amount; // cycles around;
return QColor::fromHsv(fh, fs, fv);
}
if (fv > bv && bv > static_cast<int>(amount)) {
return QColor::fromHsv(fh, fs, bv - static_cast<int>(amount));
if (fv > bv && bv > amount) {
return QColor::fromHsv(fh, fs, bv - amount);
}
if (fv < bv && fv > static_cast<int>(amount)) {
if (fv < bv && fv > amount) {
return QColor::fromHsv(fh, fs, fv - amount);
}
if (fv > bv && (255 - fv > static_cast<int>(amount))) {
if (fv > bv && (255 - fv > amount)) {
return QColor::fromHsv(fh, fs, fv + amount);
}
if (fv < bv && (255 - bv > static_cast<int>(amount))) {
if (fv < bv && (255 - bv > amount)) {
return QColor::fromHsv(fh, fs, bv + amount);
}

View File

@ -78,7 +78,7 @@ void BoomAnalyzer::resizeEvent(QResizeEvent *e) {
bands_ = qMin(static_cast<int>(static_cast<double>(width() + 1) / (kColumnWidth + 1)) + 1, kMaxBandCount);
scope_.resize(bands_);
F_ = static_cast<double>(HEIGHT) / (log10(256) * static_cast<double>(1.1) /*<- max. amplitude*/);
F_ = static_cast<double>(HEIGHT) / (log10(256) * 1.1 /*<- max. amplitude*/);
barPixmap_ = QPixmap(kColumnWidth - 2, HEIGHT);
canvas_ = QPixmap(size());

View File

@ -28,7 +28,7 @@
#include <QVector>
#include <QtMath>
FHT::FHT(uint n) : num_((n < 3) ? 0 : 1 << n), exp2_((n < 3) ? static_cast<int>(-1) : static_cast<int>(n)) {
FHT::FHT(uint n) : num_((n < 3) ? 0 : 1 << n), exp2_((n < 3) ? -1 : static_cast<int>(n)) {
if (n > 3) {
buf_vector_.resize(num_);
@ -47,7 +47,7 @@ float *FHT::buf_() { return buf_vector_.data(); }
float *FHT::tab_() { return tab_vector_.data(); }
int *FHT::log_() { return log_vector_.data(); }
void FHT::makeCasTable(void) {
void FHT::makeCasTable() {
float *costab = tab_();
float *sintab = tab_() + num_ / 2 + 1;

View File

@ -65,8 +65,8 @@ RainbowAnalyzer::RainbowAnalyzer(const RainbowType rbtype, QWidget *parent)
background_brush_(QColor(0x0f, 0x43, 0x73)) {
rainbowtype = rbtype;
cat_dash_[0] = QPixmap(":/pictures/nyancat.png");
cat_dash_[1] = QPixmap(":/pictures/rainbowdash.png");
cat_dash_[0] = QPixmap(QStringLiteral(":/pictures/nyancat.png"));
cat_dash_[1] = QPixmap(QStringLiteral(":/pictures/rainbowdash.png"));
memset(history_, 0, sizeof(history_));
for (int i = 0; i < kRainbowBands; ++i) {

View File

@ -0,0 +1,92 @@
/*
Strawberry Music Player
Copyright 2024, Gustavo L Conte <suporte@gu.pro.br>
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 "waverubber.h"
const char *WaveRubber::kName = QT_TRANSLATE_NOOP("AnalyzerContainer", "WaveRubber");
WaveRubber::WaveRubber(QWidget *parent)
: AnalyzerBase(parent, 9) {}
void WaveRubber::resizeEvent(QResizeEvent *e) {
Q_UNUSED(e)
canvas_ = QPixmap(size());
canvas_.fill(palette().color(QPalette::AlternateBase));
}
void WaveRubber::analyze(QPainter &p, const Scope &s, const bool new_frame) {
if (!new_frame || engine_->state() == EngineBase::State::Paused) {
p.drawPixmap(0, 0, canvas_);
return;
}
// Clear the canvas
canvas_ = QPixmap(size());
canvas_.fill(palette().color(QPalette::Window));
QPainter canvas_painter(&canvas_);
// Set the pen color to the QT palette highlight color
canvas_painter.setPen(palette().color(QPalette::Highlight));
// Get pointer to amplitude data
const float *amplitude_data = s.data();
const int mid_y = height() / 4;
const int num_samples = static_cast<int>(s.size());
const float x_scale = static_cast<float>(width()) / static_cast<float>(num_samples);
float prev_y = static_cast<float>(mid_y);
// Draw the waveform
for (int i = 0; i < num_samples; ++i) {
// Normalize amplitude to 0-1 range
const float color_factor = amplitude_data[i] / 2.0F + 0.5F;
const int rgb_value = static_cast<int>(255 - color_factor * 255);
QColor highlight_color = palette().color(QPalette::Highlight);
// Blend blue and green with highlight color from QT palette based on amplitude
QColor blended_color = QColor(rgb_value, highlight_color.green(), highlight_color.blue());
canvas_painter.setPen(blended_color);
const int x = static_cast<int>(static_cast<float>(i) * x_scale);
const int y = static_cast<int>(static_cast<float>(mid_y) - (s[i] * static_cast<float>(mid_y)));
canvas_painter.drawLine(x, static_cast<int>(prev_y + static_cast<float>(mid_y)), static_cast<int>(static_cast<float>(x) + x_scale), static_cast<int>(static_cast<float>(y + mid_y))); // Draw
prev_y = static_cast<float>(y);
}
canvas_painter.end();
p.drawPixmap(0, 0, canvas_);
}
void WaveRubber::transform(Scope &s) {
// No need transformation for waveform analyzer
Q_UNUSED(s);
}
void WaveRubber::demo(QPainter &p) {
analyze(p, Scope(fht_->size(), 0), new_frame_);
}

41
src/analyzer/waverubber.h Normal file
View File

@ -0,0 +1,41 @@
/*
Strawberry Music Player
Copyright 2024, Gustavo L Conte <suporte@gu.pro.br>
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 <QPixmap>
#include <QPainter>
#include "analyzerbase.h"
class WaveRubber : public AnalyzerBase {
Q_OBJECT
public:
Q_INVOKABLE explicit WaveRubber(QWidget *parent);
static const char *kName;
protected:
void resizeEvent(QResizeEvent *e) override;
void analyze(QPainter &p, const Scope &s, const bool new_frame) override;
void transform(Scope &scope) override;
void demo(QPainter &p) override;
private:
QPixmap canvas_;
};

View File

@ -37,6 +37,7 @@
#include "core/thread.h"
#include "core/song.h"
#include "core/logging.h"
#include "core/settings.h"
#include "utilities/threadutils.h"
#include "collection.h"
#include "collectionwatcher.h"
@ -45,11 +46,9 @@
#include "scrobbler/lastfmimport.h"
#include "settings/collectionsettingspage.h"
using std::make_unique;
using std::make_shared;
const char *SCollection::kSongsTable = "songs";
const char *SCollection::kFtsTable = "songs_fts";
const char *SCollection::kDirsTable = "directories";
const char *SCollection::kSubdirsTable = "subdirectories";
@ -70,7 +69,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, QLatin1String(kSongsTable), QLatin1String(kDirsTable), QLatin1String(kSubdirsTable));
model_ = new CollectionModel(backend_, app_, this);
@ -93,14 +92,14 @@ SCollection::~SCollection() {
void SCollection::Init() {
watcher_ = make_unique<CollectionWatcher>(Song::Source::Collection);
watcher_ = new CollectionWatcher(Song::Source::Collection);
watcher_thread_ = new Thread(this);
watcher_thread_->SetIoPriority(Utilities::IoPriority::IOPRIO_CLASS_IDLE);
watcher_->moveToThread(watcher_thread_);
qLog(Debug) << &*watcher_ << "moved to thread" << watcher_thread_;
qLog(Debug) << watcher_ << "moved to thread" << watcher_thread_;
watcher_thread_->start(QThread::IdlePriority);
@ -108,20 +107,20 @@ void SCollection::Init() {
watcher_->set_task_manager(app_->task_manager());
QObject::connect(&*backend_, &CollectionBackend::Error, this, &SCollection::Error);
QObject::connect(&*backend_, &CollectionBackend::DirectoryDiscovered, &*watcher_, &CollectionWatcher::AddDirectory);
QObject::connect(&*backend_, &CollectionBackend::DirectoryDeleted, &*watcher_, &CollectionWatcher::RemoveDirectory);
QObject::connect(&*backend_, &CollectionBackend::DirectoryAdded, watcher_, &CollectionWatcher::AddDirectory);
QObject::connect(&*backend_, &CollectionBackend::DirectoryDeleted, watcher_, &CollectionWatcher::RemoveDirectory);
QObject::connect(&*backend_, &CollectionBackend::SongsRatingChanged, this, &SCollection::SongsRatingChanged);
QObject::connect(&*backend_, &CollectionBackend::SongsStatisticsChanged, this, &SCollection::SongsPlaycountChanged);
QObject::connect(&*watcher_, &CollectionWatcher::NewOrUpdatedSongs, &*backend_, &CollectionBackend::AddOrUpdateSongs);
QObject::connect(&*watcher_, &CollectionWatcher::SongsMTimeUpdated, &*backend_, &CollectionBackend::UpdateMTimesOnly);
QObject::connect(&*watcher_, &CollectionWatcher::SongsDeleted, &*backend_, &CollectionBackend::DeleteSongs);
QObject::connect(&*watcher_, &CollectionWatcher::SongsUnavailable, &*backend_, &CollectionBackend::MarkSongsUnavailable);
QObject::connect(&*watcher_, &CollectionWatcher::SongsReadded, &*backend_, &CollectionBackend::MarkSongsUnavailable);
QObject::connect(&*watcher_, &CollectionWatcher::SubdirsDiscovered, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
QObject::connect(&*watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
QObject::connect(&*watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*backend_, &CollectionBackend::CompilationsNeedUpdating);
QObject::connect(&*watcher_, &CollectionWatcher::UpdateLastSeen, &*backend_, &CollectionBackend::UpdateLastSeen);
QObject::connect(watcher_, &CollectionWatcher::NewOrUpdatedSongs, &*backend_, &CollectionBackend::AddOrUpdateSongs);
QObject::connect(watcher_, &CollectionWatcher::SongsMTimeUpdated, &*backend_, &CollectionBackend::UpdateMTimesOnly);
QObject::connect(watcher_, &CollectionWatcher::SongsDeleted, &*backend_, &CollectionBackend::DeleteSongs);
QObject::connect(watcher_, &CollectionWatcher::SongsUnavailable, &*backend_, &CollectionBackend::MarkSongsUnavailable);
QObject::connect(watcher_, &CollectionWatcher::SongsReadded, &*backend_, &CollectionBackend::MarkSongsUnavailable);
QObject::connect(watcher_, &CollectionWatcher::SubdirsDiscovered, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
QObject::connect(watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
QObject::connect(watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*backend_, &CollectionBackend::CompilationsNeedUpdating);
QObject::connect(watcher_, &CollectionWatcher::UpdateLastSeen, &*backend_, &CollectionBackend::UpdateLastSeen);
QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdateLastPlayed, &*backend_, &CollectionBackend::UpdateLastPlayed);
QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdatePlayCount, &*backend_, &CollectionBackend::UpdatePlayCount);
@ -133,13 +132,13 @@ void SCollection::Init() {
void SCollection::Exit() {
wait_for_exit_ << &*backend_ << &*watcher_;
wait_for_exit_ << &*backend_ << watcher_;
QObject::disconnect(&*backend_, nullptr, &*watcher_, nullptr);
QObject::disconnect(&*watcher_, nullptr, &*backend_, nullptr);
QObject::disconnect(&*backend_, nullptr, watcher_, nullptr);
QObject::disconnect(watcher_, nullptr, &*backend_, nullptr);
QObject::connect(&*backend_, &CollectionBackend::ExitFinished, this, &SCollection::ExitReceived);
QObject::connect(&*watcher_, &CollectionWatcher::ExitFinished, this, &SCollection::ExitReceived);
QObject::connect(watcher_, &CollectionWatcher::ExitFinished, this, &SCollection::ExitReceived);
backend_->ExitAsync();
watcher_->Abort();
watcher_->ExitAsync();
@ -180,7 +179,7 @@ void SCollection::ReloadSettings() {
watcher_->ReloadSettingsAsync();
model_->ReloadSettings();
QSettings s;
Settings s;
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
save_playcounts_to_files_ = s.value("save_playcounts", false).toBool();
save_ratings_to_files_ = s.value("save_ratings", false).toBool();

View File

@ -29,7 +29,6 @@
#include <QHash>
#include <QString>
#include "core/scoped_ptr.h"
#include "core/shared_ptr.h"
#include "core/song.h"
@ -91,7 +90,7 @@ class SCollection : public QObject {
SharedPtr<CollectionBackend> backend_;
CollectionModel *model_;
ScopedPtr<CollectionWatcher> watcher_;
CollectionWatcher *watcher_;
Thread *watcher_thread_;
QThread *original_thread_;

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-2023, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2024, 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
@ -80,12 +80,13 @@ class CollectionBackendInterface : public QObject {
using AlbumList = QList<Album>;
virtual QString songs_table() const = 0;
virtual QString fts_table() const = 0;
virtual Song::Source source() const = 0;
virtual SharedPtr<Database> db() const = 0;
virtual void GetAllSongsAsync(const int id = 0) = 0;
// Get a list of directories in the collection. Emits DirectoriesDiscovered.
virtual void LoadDirectoriesAsync() = 0;
@ -130,9 +131,10 @@ class CollectionBackendInterface : public QObject {
// Returns a section of a song with the given filename and beginning. If the section is not present in collection, returns invalid song.
// Using default beginning value is suitable when searching for single-section songs.
virtual Song GetSongByUrl(const QUrl &url, const qint64 beginning = 0) = 0;
virtual Song GetSongByUrlAndTrack(const QUrl &url, const int track) = 0;
virtual void AddDirectory(const QString &path) = 0;
virtual void RemoveDirectory(const CollectionDirectory &dir) = 0;
virtual void AddDirectoryAsync(const QString &path) = 0;
virtual void RemoveDirectoryAsync(const CollectionDirectory &dir) = 0;
};
class CollectionBackend : public CollectionBackendInterface {
@ -144,7 +146,8 @@ class CollectionBackend : public CollectionBackendInterface {
~CollectionBackend();
void Init(SharedPtr<Database> db, SharedPtr<TaskManager> task_manager, const Song::Source source, const QString &songs_table, const QString &fts_table, const QString &dirs_table = QString(), const QString &subdirs_table = QString());
void Init(SharedPtr<Database> db, SharedPtr<TaskManager> task_manager, const Song::Source source, const QString &songs_table, const QString &dirs_table = QString(), const QString &subdirs_table = QString());
void Close();
void ExitAsync();
@ -156,10 +159,11 @@ class CollectionBackend : public CollectionBackendInterface {
SharedPtr<Database> db() const override { return db_; }
QString songs_table() const override { return songs_table_; }
QString fts_table() const override { return fts_table_; }
QString dirs_table() const { return dirs_table_; }
QString subdirs_table() const { return subdirs_table_; }
void GetAllSongsAsync(const int id = 0) override;
// Get a list of directories in the collection. Emits DirectoriesDiscovered.
void LoadDirectoriesAsync() override;
@ -203,9 +207,10 @@ class CollectionBackend : public CollectionBackendInterface {
SongList GetSongsByUrl(const QUrl &url, const bool unavailable = false) override;
Song GetSongByUrl(const QUrl &url, qint64 beginning = 0) override;
Song GetSongByUrlAndTrack(const QUrl &url, const int track) override;
void AddDirectory(const QString &path) override;
void RemoveDirectory(const CollectionDirectory &dir) override;
void AddDirectoryAsync(const QString &path) override;
void RemoveDirectoryAsync(const CollectionDirectory &dir) override;
bool ExecCollectionQuery(CollectionQuery *query, SongList &songs);
bool ExecCollectionQuery(CollectionQuery *query, SongMap &songs);
@ -233,10 +238,13 @@ class CollectionBackend : public CollectionBackendInterface {
public slots:
void Exit();
void GetAllSongs(const int id);
void LoadDirectories();
void UpdateTotalSongCount();
void UpdateTotalArtistCount();
void UpdateTotalAlbumCount();
void AddDirectory(const QString &path);
void RemoveDirectory(const CollectionDirectory &dir);
void AddOrUpdateSongs(const SongList &songs);
void UpdateSongsBySongID(const SongMap &new_songs);
void UpdateMTimesOnly(const SongList &songs);
@ -248,7 +256,7 @@ class CollectionBackend : public CollectionBackendInterface {
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 ForceCompilation(const QString &album, const QStringList &artists, const bool on);
void IncrementPlayCount(const int id);
void IncrementSkipCount(const int id, const float progress);
void ResetPlayStatistics(const int id, const bool save_tags = false);
@ -268,11 +276,13 @@ class CollectionBackend : public CollectionBackendInterface {
void ExpireSongs(const int directory_id, const int expire_unavailable_songs_days);
signals:
void DirectoryDiscovered(const CollectionDirectory &dir, const CollectionSubdirectoryList &subdir);
void DirectoryAdded(const CollectionDirectory &dir, const CollectionSubdirectoryList &subdir);
void DirectoryDeleted(const CollectionDirectory &dir);
void SongsDiscovered(const SongList &songs);
void GotSongs(const SongList &songs, const int id);
void SongsAdded(const SongList &songs);
void SongsDeleted(const SongList &songs);
void SongsChanged(const SongList &songs);
void SongsStatisticsChanged(const SongList &songs, const bool save_tags = false);
void DatabaseReset();
@ -297,7 +307,7 @@ class CollectionBackend : public CollectionBackendInterface {
int has_not_compilation_detected;
};
bool UpdateCompilations(const QSqlDatabase &db, SongList &deleted_songs, SongList &added_songs, const QUrl &url, const bool compilation_detected);
bool UpdateCompilations(const QSqlDatabase &db, SongList &changed_songs, const QUrl &url, const bool compilation_detected);
AlbumList GetAlbums(const QString &artist, const QString &album_artist, const bool compilation_required = false, const CollectionFilterOptions &opt = CollectionFilterOptions());
AlbumList GetAlbums(const QString &artist, const bool compilation_required, const CollectionFilterOptions &opt = CollectionFilterOptions());
CollectionSubdirectoryList SubdirsInDirectory(const int id, QSqlDatabase &db);
@ -315,7 +325,6 @@ class CollectionBackend : public CollectionBackendInterface {
QString songs_table_;
QString dirs_table_;
QString subdirs_table_;
QString fts_table_;
QThread *original_thread_;
};

View File

@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, 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
@ -41,15 +42,18 @@ using std::make_shared;
CollectionDirectoryModel::CollectionDirectoryModel(SharedPtr<CollectionBackend> backend, QObject *parent)
: QStandardItemModel(parent),
dir_icon_(IconLoader::Load("document-open-folder")),
dir_icon_(IconLoader::Load(QStringLiteral("document-open-folder"))),
backend_(backend) {
QObject::connect(&*backend_, &CollectionBackend::DirectoryDiscovered, this, &CollectionDirectoryModel::DirectoryDiscovered);
QObject::connect(&*backend_, &CollectionBackend::DirectoryDeleted, this, &CollectionDirectoryModel::DirectoryDeleted);
QObject::connect(&*backend_, &CollectionBackend::DirectoryAdded, this, &CollectionDirectoryModel::AddDirectory);
QObject::connect(&*backend_, &CollectionBackend::DirectoryDeleted, this, &CollectionDirectoryModel::RemoveDirectory);
}
void CollectionDirectoryModel::DirectoryDiscovered(const CollectionDirectory &dir) {
void CollectionDirectoryModel::AddDirectory(const CollectionDirectory &dir) {
directories_.insert(dir.id, dir);
paths_.append(dir.path);
QStandardItem *item = new QStandardItem(dir.path);
item->setData(dir.id, kIdRole);
@ -59,7 +63,10 @@ void CollectionDirectoryModel::DirectoryDiscovered(const CollectionDirectory &di
}
void CollectionDirectoryModel::DirectoryDeleted(const CollectionDirectory &dir) {
void CollectionDirectoryModel::RemoveDirectory(const CollectionDirectory &dir) {
directories_.remove(dir.id);
paths_.removeAll(dir.path);
for (int i = 0; i < rowCount(); ++i) {
if (item(i, 0)->data(kIdRole).toInt() == dir.id) {
@ -71,26 +78,6 @@ void CollectionDirectoryModel::DirectoryDeleted(const CollectionDirectory &dir)
}
void CollectionDirectoryModel::AddDirectory(const QString &path) {
if (!backend_) return;
backend_->AddDirectory(path);
}
void CollectionDirectoryModel::RemoveDirectory(const QModelIndex &idx) {
if (!backend_ || !idx.isValid()) return;
CollectionDirectory dir;
dir.path = idx.data().toString();
dir.id = idx.data(kIdRole).toInt();
backend_->RemoveDirectory(dir);
}
QVariant CollectionDirectoryModel::data(const QModelIndex &idx, int role) const {
switch (role) {

View File

@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, 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
@ -26,15 +27,17 @@
#include <QObject>
#include <QStandardItemModel>
#include <QList>
#include <QMap>
#include <QVariant>
#include <QString>
#include <QStringList>
#include <QIcon>
#include "core/shared_ptr.h"
#include "collectiondirectory.h"
class QModelIndex;
struct CollectionDirectory;
class CollectionBackend;
class MusicStorage;
@ -44,22 +47,24 @@ class CollectionDirectoryModel : public QStandardItemModel {
public:
explicit CollectionDirectoryModel(SharedPtr<CollectionBackend> collection_backend, QObject *parent = nullptr);
// To be called by GUIs
void AddDirectory(const QString &path);
void RemoveDirectory(const QModelIndex &idx);
QVariant data(const QModelIndex &idx, int role) const override;
SharedPtr<CollectionBackend> backend() const { return backend_; }
QMap<int, CollectionDirectory> directories() const { return directories_; }
QStringList paths() const { return paths_; }
private slots:
// To be called by the backend
void DirectoryDiscovered(const CollectionDirectory &directories);
void DirectoryDeleted(const CollectionDirectory &directories);
void AddDirectory(const CollectionDirectory &directory);
void RemoveDirectory(const CollectionDirectory &directory);
private:
static const int kIdRole = Qt::UserRole + 1;
QIcon dir_icon_;
SharedPtr<CollectionBackend> backend_;
QMap<int, CollectionDirectory> directories_;
QStringList paths_;
QList<SharedPtr<MusicStorage>> storage_;
};

View File

@ -0,0 +1,335 @@
/*
* Strawberry Music Player
* Copyright 2021-2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QSortFilterProxyModel>
#include <QVariant>
#include <QString>
#include <QStringList>
#include "core/logging.h"
#include "utilities/timeconstants.h"
#include "utilities/searchparserutils.h"
#include "collectionfilter.h"
#include "collectionmodel.h"
#include "collectionitem.h"
const QStringList CollectionFilter::Operators = QStringList() << QStringLiteral(":")
<< QStringLiteral("=")
<< QStringLiteral("==")
<< QStringLiteral("<>")
<< QStringLiteral("<")
<< QStringLiteral("<=")
<< QStringLiteral(">")
<< QStringLiteral(">=");
CollectionFilter::CollectionFilter(QObject *parent) : QSortFilterProxyModel(parent) {}
bool CollectionFilter::filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const {
CollectionModel *model = qobject_cast<CollectionModel*>(sourceModel());
if (!model) return false;
const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
if (!idx.isValid()) return false;
CollectionItem *item = model->IndexToItem(idx);
if (!item) return false;
if (item->type == CollectionItem::Type::LoadingIndicator) return true;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QString filter_text = filterRegularExpression().pattern().remove(QLatin1Char('\\'));
#else
QString filter_text = filterRegExp().pattern();
#endif
if (filter_text.isEmpty()) return true;
filter_text = filter_text.replace(QRegularExpression(QStringLiteral("\\s*:\\s*")), QStringLiteral(":"))
.replace(QRegularExpression(QStringLiteral("\\s*=\\s*")), QStringLiteral("="))
.replace(QRegularExpression(QStringLiteral("\\s*==\\s*")), QStringLiteral("=="))
.replace(QRegularExpression(QStringLiteral("\\s*<>\\s*")), QStringLiteral("<>"))
.replace(QRegularExpression(QStringLiteral("\\s*<\\s*")), QStringLiteral("<"))
.replace(QRegularExpression(QStringLiteral("\\s*>\\s*")), QStringLiteral(">"))
.replace(QRegularExpression(QStringLiteral("\\s*<=\\s*")), QStringLiteral("<="))
.replace(QRegularExpression(QStringLiteral("\\s*>=\\s*")), QStringLiteral(">="));
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
const QStringList tokens = filter_text.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
#else
const QStringList tokens = filter_text.split(QRegularExpression(QStringLiteral("\\s+")), QString::SkipEmptyParts);
#endif
filter_text.clear();
FilterList filters;
static QRegularExpression operator_regex(QStringLiteral("(=|<[>=]?|>=?|!=)"));
for (int i = 0; i < tokens.count(); ++i) {
const QString &token = tokens[i];
if (token.contains(QLatin1Char(':'))) {
QString field = token.section(QLatin1Char(':'), 0, 0).remove(QLatin1Char(':')).trimmed();
QString value = token.section(QLatin1Char(':'), 1, -1).remove(QLatin1Char(':')).trimmed();
if (field.isEmpty() || value.isEmpty()) continue;
if (Song::kTextSearchColumns.contains(field, Qt::CaseInsensitive) && value.count(QLatin1Char('"')) <= 2) {
bool quotation_mark_start = false;
bool quotation_mark_end = false;
if (value.left(1) == QLatin1Char('"')) {
value.remove(0, 1);
quotation_mark_start = true;
if (value.length() >= 1 && value.count(QLatin1Char('"')) == 1) {
value = value.section(QLatin1Char(QLatin1Char('"')), 0, 0).remove(QLatin1Char('"')).trimmed();
quotation_mark_end = true;
}
}
for (int y = i + 1; y < tokens.count() && !quotation_mark_end; ++y) {
QString next_value = tokens[y];
if (!quotation_mark_start && ContainsOperators(next_value)) {
break;
}
if (quotation_mark_start && next_value.contains(QLatin1Char('"'))) {
next_value = next_value.section(QLatin1Char(QLatin1Char('"')), 0, 0).remove(QLatin1Char('"')).trimmed();
quotation_mark_end = true;
}
value.append(QLatin1Char(' ') + next_value);
i = y;
}
if (!field.isEmpty() && !value.isEmpty()) {
filters.insert(field, Filter(field, value));
}
continue;
}
}
else if (token.contains(operator_regex)) {
QRegularExpressionMatch re_match = operator_regex.match(token);
if (re_match.hasMatch()) {
const QString foperator = re_match.captured(0);
const QString field = token.section(foperator, 0, 0).remove(foperator).trimmed();
const QString value = token.section(foperator, 1, -1).remove(foperator).trimmed();
if (value.isEmpty()) continue;
if (Song::kNumericalSearchColumns.contains(field, Qt::CaseInsensitive)) {
if (Song::kIntSearchColumns.contains(field, Qt::CaseInsensitive)) {
bool ok = false;
const int value_int = value.toInt(&ok);
if (ok) {
filters.insert(field, Filter(field, value_int, foperator));
continue;
}
}
else if (Song::kUIntSearchColumns.contains(field, Qt::CaseInsensitive)) {
bool ok = false;
const uint value_uint = value.toUInt(&ok);
if (ok) {
filters.insert(field, Filter(field, value_uint, foperator));
continue;
}
}
else if (field.compare(QLatin1String("length"), Qt::CaseInsensitive) == 0) {
filters.insert(field, Filter(field, static_cast<qint64>(Utilities::ParseSearchTime(value)) * kNsecPerSec, foperator));
continue;
}
else if (field.compare(QLatin1String("rating"), Qt::CaseInsensitive) == 0) {
filters.insert(field, Filter(field, Utilities::ParseSearchRating(value), foperator));
}
}
}
}
if (!filter_text.isEmpty()) filter_text.append(QLatin1Char(' '));
filter_text += token;
}
if (filter_text.isEmpty() && filters.isEmpty()) return true;
return ItemMatchesFilters(item, filters, filter_text);
}
bool CollectionFilter::ItemMatchesFilters(CollectionItem *item, const FilterList &filters, const QString &filter_text) {
if (item->type == CollectionItem::Type::Song &&
item->metadata.is_valid() &&
ItemMetadataMatchesFilters(item->metadata, filters, filter_text)) {
return true;
}
for (CollectionItem *child : std::as_const(item->children)) {
if (ItemMatchesFilters(child, filters, filter_text)) return true;
}
return false;
}
bool CollectionFilter::ItemMetadataMatchesFilters(const Song &metadata, const FilterList &filters, const QString &filter_text) {
for (FilterList::const_iterator it = filters.begin() ; it != filters.end() ; ++it) {
const QString &field = it.key();
const Filter &filter = it.value();
const QVariant &value = filter.value;
const QString &foperator = filter.foperator;
if (field.isEmpty() || !value.isValid()) {
continue;
}
const QVariant data = DataFromField(field, metadata);
if (
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
value.metaType() != data.metaType()
#else
value.type() != data.type()
#endif
|| !FieldValueMatchesData(value, data, foperator)) {
return false;
}
}
return filter_text.isEmpty() || ItemMetadataMatchesFilterText(metadata, filter_text);
}
bool CollectionFilter::ItemMetadataMatchesFilterText(const Song &metadata, const QString &filter_text) {
return metadata.effective_albumartist().contains(filter_text, Qt::CaseInsensitive) ||
metadata.artist().contains(filter_text, Qt::CaseInsensitive) ||
metadata.album().contains(filter_text, Qt::CaseInsensitive) ||
metadata.title().contains(filter_text, Qt::CaseInsensitive) ||
metadata.composer().contains(filter_text, Qt::CaseInsensitive) ||
metadata.performer().contains(filter_text, Qt::CaseInsensitive) ||
metadata.grouping().contains(filter_text, Qt::CaseInsensitive) ||
metadata.genre().contains(filter_text, Qt::CaseInsensitive) ||
metadata.comment().contains(filter_text, Qt::CaseInsensitive);
}
QVariant CollectionFilter::DataFromField(const QString &field, const Song &metadata) {
if (field == QLatin1String("albumartist")) return metadata.effective_albumartist();
if (field == QLatin1String("artist")) return metadata.artist();
if (field == QLatin1String("album")) return metadata.album();
if (field == QLatin1String("title")) return metadata.title();
if (field == QLatin1String("composer")) return metadata.composer();
if (field == QLatin1String("performer")) return metadata.performer();
if (field == QLatin1String("grouping")) return metadata.grouping();
if (field == QLatin1String("genre")) return metadata.genre();
if (field == QLatin1String("comment")) return metadata.comment();
if (field == QLatin1String("track")) return metadata.track();
if (field == QLatin1String("year")) return metadata.year();
if (field == QLatin1String("length")) return metadata.length_nanosec();
if (field == QLatin1String("samplerate")) return metadata.samplerate();
if (field == QLatin1String("bitdepth")) return metadata.bitdepth();
if (field == QLatin1String("bitrate")) return metadata.bitrate();
if (field == QLatin1String("rating")) return metadata.rating();
if (field == QLatin1String("playcount")) return metadata.playcount();
if (field == QLatin1String("skipcount")) return metadata.skipcount();
return QVariant();
}
bool CollectionFilter::FieldValueMatchesData(const QVariant &value, const QVariant &data, const QString &foperator) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
switch (value.metaType().id()) {
#else
switch (value.userType()) {
#endif
case QMetaType::QString:{
const QString str_value = value.toString();
const QString str_data = data.toString();
return str_data.contains(str_value, Qt::CaseInsensitive);
}
case QMetaType::Int:{
return FieldIntValueMatchesData(value.toInt(), foperator, data.toInt());
}
case QMetaType::UInt:{
return FieldUIntValueMatchesData(value.toUInt(), foperator, data.toUInt());
}
case QMetaType::LongLong:{
return FieldLongLongValueMatchesData(value.toLongLong(), foperator, data.toLongLong());
}
case QMetaType::Float:{
return FieldFloatValueMatchesData(value.toFloat(), foperator, data.toFloat());
}
default:{
return false;
}
}
return false;
}
template<typename T>
bool CollectionFilter::FieldNumericalValueMatchesData(const T value, const QString &foperator, const T data) {
if (foperator == QLatin1Char('=') || foperator == QLatin1String("==")) {
return data == value;
}
if (foperator == QLatin1String("!=") || foperator == QLatin1String("<>")) {
return data != value;
}
if (foperator == QLatin1Char('<')) {
return data < value;
}
if (foperator == QLatin1Char('>')) {
return data > value;
}
if (foperator == QLatin1String(">=")) {
return data >= value;
}
if (foperator == QLatin1String("<=")) {
return data <= value;
}
return false;
}
bool CollectionFilter::FieldIntValueMatchesData(const int value, const QString &foperator, const int data) {
return FieldNumericalValueMatchesData(value, foperator, data);
}
bool CollectionFilter::FieldUIntValueMatchesData(const uint value, const QString &foperator, const uint data) {
return FieldNumericalValueMatchesData(value, foperator, data);
}
bool CollectionFilter::FieldLongLongValueMatchesData(const qint64 value, const QString &foperator, const qint64 data) {
return FieldNumericalValueMatchesData(value, foperator, data);
}
bool CollectionFilter::FieldFloatValueMatchesData(const float value, const QString &foperator, const float data) {
return FieldNumericalValueMatchesData(value, foperator, data);
}
bool CollectionFilter::ContainsOperators(const QString &token) {
for (const QString &foperator : Operators) {
if (token.contains(foperator, Qt::CaseInsensitive)) return true;
}
return false;
}

View File

@ -0,0 +1,69 @@
/*
* Strawberry Music Player
* Copyright 2021-2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef COLLECTIONFILTER_H
#define COLLECTIONFILTER_H
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QSortFilterProxyModel>
#include <QVariant>
#include <QString>
#include <QStringList>
#include "core/song.h"
class CollectionItem;
class CollectionFilter : public QSortFilterProxyModel {
Q_OBJECT
public:
explicit CollectionFilter(QObject *parent = nullptr);
protected:
bool filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const override;
private:
static const QStringList Operators;
struct Filter {
public:
Filter(const QString &_field = QString(), const QVariant &_value = QVariant(), const QString &_foperator = QString()) : field(_field), value(_value), foperator(_foperator) {}
QString field;
QVariant value;
QString foperator;
};
using FilterList = QMap<QString, Filter>;
static bool ItemMatchesFilters(CollectionItem *item, const FilterList &filters, const QString &filter_text);
static bool ItemMetadataMatchesFilters(const Song &metadata, const FilterList &filters, const QString &filter_text);
static bool ItemMetadataMatchesFilterText(const Song &metadata, const QString &filter_text);
static QVariant DataFromField(const QString &field, const Song &metadata);
static bool FieldValueMatchesData(const QVariant &value, const QVariant &data, const QString &foperator);
template<typename T>
static bool FieldNumericalValueMatchesData(const T value, const QString &foperator, const T data);
static bool FieldIntValueMatchesData(const int value, const QString &foperator, const int data);
static bool FieldUIntValueMatchesData(const uint value, const QString &foperator, const uint data);
static bool FieldLongLongValueMatchesData(const qint64 value, const QString &foperator, const qint64 data);
static bool FieldFloatValueMatchesData(const float value, const QString &foperator, const float data);
static bool ContainsOperators(const QString &token);
};
#endif // COLLECTIONFILTER_H

View File

@ -29,7 +29,7 @@ CollectionFilterOptions::CollectionFilterOptions() : filter_mode_(FilterMode::Al
bool CollectionFilterOptions::Matches(const Song &song) const {
if (max_age_ != -1) {
const qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - max_age_;
const qint64 cutoff = QDateTime::currentSecsSinceEpoch() - max_age_;
if (song.ctime() <= cutoff) return false;
}

View File

@ -21,6 +21,7 @@
#include "config.h"
#include <utility>
#include <memory>
#include <QApplication>
@ -46,8 +47,11 @@
#include "core/iconloader.h"
#include "core/song.h"
#include "core/logging.h"
#include "core/settings.h"
#include "collectionfilteroptions.h"
#include "collectionmodel.h"
#include "collectionfilter.h"
#include "collectionquery.h"
#include "savedgroupingmanager.h"
#include "collectionfilterwidget.h"
#include "groupbydialog.h"
@ -60,6 +64,7 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
: QWidget(parent),
ui_(new Ui_CollectionFilterWidget),
model_(nullptr),
filter_(nullptr),
group_by_dialog_(new GroupByDialog(this)),
groupings_manager_(nullptr),
filter_age_menu_(nullptr),
@ -72,34 +77,34 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
ui_->setupUi(this);
QString available_fields = Song::kFtsColumns.join(", ").replace(QRegularExpression("\\bfts"), "");
available_fields += QString(", ") + Song::kNumericalColumns.join(", ");
QString available_fields = Song::kTextSearchColumns.join(QLatin1String(", "));
available_fields += QLatin1String(", ") + Song::kNumericalSearchColumns.join(QLatin1String(", "));
ui_->search_field->setToolTip(
QString("<html><head/><body><p>") +
QLatin1String("<html><head/><body><p>") +
tr("Prefix a word with a field name to limit the search to that field, e.g.:") +
QString(" ") +
QString("<span style=\"font-weight:600;\">") +
QLatin1Char(' ') +
QLatin1String("<span style=\"font-weight:600;\">") +
tr("artist") +
QString(":</span><span style=\"font-style:italic;\">Strawbs</span> ") +
tr("searches the collection for all artists that contain the word %1. ").arg("Strawbs") +
QString("</p><p>") +
QLatin1String(":</span><span style=\"font-style:italic;\">Strawbs</span> ") +
tr("searches the collection for all artists that contain the word %1. ").arg(QLatin1String("Strawbs")) +
QLatin1String("</p><p>") +
tr("Search terms for numerical fields can be prefixed with %1 or %2 to refine the search, e.g.: ")
.arg(" =, !=, &lt;, &gt;, &lt;=", "&gt;=") +
QString("<span style=\"font-weight:600;\">") +
.arg(QLatin1String(" =, !=, &lt;, &gt;, &lt;="), QLatin1String("&gt;=")) +
QLatin1String("<span style=\"font-weight:600;\">") +
tr("rating") +
QString("</span>") +
QString(":>=") +
QString("<span style=\"font-weight:italic;\">4</span>") +
QLatin1String("</span>") +
QLatin1String(":>=") +
QLatin1String("<span style=\"font-weight:italic;\">4</span>") +
QString("</p><p><span style=\"font-weight:600;\">") +
QLatin1String("</p><p><span style=\"font-weight:600;\">") +
tr("Available fields") +
QString(": ") +
QString("</span>") +
QString("<span style=\"font-style:italic;\">") +
QLatin1String(": ") +
QLatin1String("</span>") +
QLatin1String("<span style=\"font-style:italic;\">") +
available_fields +
QString("</span>.") +
QString("</p></body></html>")
QLatin1String("</span>.") +
QLatin1String("</p></body></html>")
);
QObject::connect(ui_->search_field, &QSearchField::returnPressed, this, &CollectionFilterWidget::ReturnPressed);
@ -109,7 +114,7 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
filter_delay_->setSingleShot(true);
// Icons
ui_->options->setIcon(IconLoader::Load("configure"));
ui_->options->setIcon(IconLoader::Load(QStringLiteral("configure")));
// Filter by age
QActionGroup *filter_age_group = new QActionGroup(this);
@ -123,12 +128,12 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
filter_age_menu_ = new QMenu(tr("Show"), this);
filter_age_menu_->addActions(filter_age_group->actions());
filter_ages_[ui_->filter_age_all] = -1;
filter_ages_[ui_->filter_age_today] = 60 * 60 * 24;
filter_ages_[ui_->filter_age_week] = 60 * 60 * 24 * 7;
filter_ages_[ui_->filter_age_month] = 60 * 60 * 24 * 30;
filter_ages_[ui_->filter_age_three_months] = 60 * 60 * 24 * 30 * 3;
filter_ages_[ui_->filter_age_year] = 60 * 60 * 24 * 365;
filter_max_ages_[ui_->filter_age_all] = -1;
filter_max_ages_[ui_->filter_age_today] = 60 * 60 * 24;
filter_max_ages_[ui_->filter_age_week] = 60 * 60 * 24 * 7;
filter_max_ages_[ui_->filter_age_month] = 60 * 60 * 24 * 30;
filter_max_ages_[ui_->filter_age_three_months] = 60 * 60 * 24 * 30 * 3;
filter_max_ages_[ui_->filter_age_year] = 60 * 60 * 24 * 365;
group_by_menu_ = new QMenu(tr("Group by"), this);
@ -154,34 +159,35 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
CollectionFilterWidget::~CollectionFilterWidget() { delete ui_; }
void CollectionFilterWidget::Init(CollectionModel *model) {
void CollectionFilterWidget::Init(CollectionModel *model, CollectionFilter *filter) {
if (model_) {
QObject::disconnect(model_, nullptr, this, nullptr);
QObject::disconnect(model_, nullptr, group_by_dialog_, nullptr);
QObject::disconnect(group_by_dialog_, nullptr, model_, nullptr);
QList<QAction*> filter_ages = filter_ages_.keys();
for (QAction *action : filter_ages) {
const QList<QAction*> actions = filter_max_ages_.keys();
for (QAction *action : actions) {
QObject::disconnect(action, &QAction::triggered, model_, nullptr);
}
}
model_ = model;
filter_ = filter;
// Connect signals
QObject::connect(model_, &CollectionModel::GroupingChanged, group_by_dialog_, &GroupByDialog::CollectionGroupingChanged);
QObject::connect(model_, &CollectionModel::GroupingChanged, this, &CollectionFilterWidget::GroupingChanged);
QObject::connect(group_by_dialog_, &GroupByDialog::Accepted, model_, &CollectionModel::SetGroupBy);
QList<QAction*> filter_ages = filter_ages_.keys();
for (QAction *action : filter_ages) {
int age = filter_ages_[action];
QObject::connect(action, &QAction::triggered, this, [this, age]() { model_->SetFilterAge(age); } );
const QList<QAction*> actions = filter_max_ages_.keys();
for (QAction *action : actions) {
int filter_max_age = filter_max_ages_[action];
QObject::connect(action, &QAction::triggered, this, [this, filter_max_age]() { model_->SetFilterMaxAge(filter_max_age); } );
}
// Load settings
if (!settings_group_.isEmpty()) {
QSettings s;
Settings s;
s.beginGroup(settings_group_);
int version = 0;
if (s.contains(group_by_version())) version = s.value(group_by_version(), 0).toInt();
@ -215,9 +221,13 @@ void CollectionFilterWidget::SetSettingsPrefix(const QString &prefix) {
}
void CollectionFilterWidget::setFilter(CollectionFilter *filter) {
filter_ = filter;
}
void CollectionFilterWidget::ReloadSettings() {
QSettings s;
Settings s;
s.beginGroup(AppearanceSettingsPage::kSettingsGroup);
int iconsize = s.value(AppearanceSettingsPage::kIconSizeConfigureButtons, 20).toInt();
s.endGroup();
@ -229,23 +239,21 @@ void CollectionFilterWidget::ReloadSettings() {
QString CollectionFilterWidget::group_by_version() const {
if (settings_prefix_.isEmpty()) {
return "group_by_version";
}
else {
return QString("%1_group_by_version").arg(settings_prefix_);
return QStringLiteral("group_by_version");
}
return QStringLiteral("%1_group_by_version").arg(settings_prefix_);
}
QString CollectionFilterWidget::group_by_key() const {
if (settings_prefix_.isEmpty()) {
return "group_by";
}
else {
return QString("%1_group_by").arg(settings_prefix_);
return QStringLiteral("group_by");
}
return QStringLiteral("%1_group_by").arg(settings_prefix_);
}
QString CollectionFilterWidget::group_by_key(const int number) const { return group_by_key() + QString::number(number); }
@ -253,12 +261,11 @@ QString CollectionFilterWidget::group_by_key(const int number) const { return gr
QString CollectionFilterWidget::separate_albums_by_grouping_key() const {
if (settings_prefix_.isEmpty()) {
return "separate_albums_by_grouping";
}
else {
return QString("%1_separate_albums_by_grouping").arg(settings_prefix_);
return QStringLiteral("separate_albums_by_grouping");
}
return QStringLiteral("%1_separate_albums_by_grouping").arg(settings_prefix_);
}
void CollectionFilterWidget::UpdateGroupByActions() {
@ -306,13 +313,13 @@ QActionGroup *CollectionFilterWidget::CreateGroupByActions(const QString &saved_
ret->addAction(sep1);
// Read saved groupings
QSettings s;
Settings s;
s.beginGroup(saved_groupings_settings_group);
int version = s.value("version").toInt();
if (version == 1) {
QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) {
if (saved.at(i) == "version") continue;
if (saved.at(i) == QLatin1String("version")) continue;
QByteArray bytes = s.value(saved.at(i)).toByteArray();
QDataStream ds(&bytes, QIODevice::ReadOnly);
CollectionModel::Grouping g;
@ -323,7 +330,7 @@ QActionGroup *CollectionFilterWidget::CreateGroupByActions(const QString &saved_
else {
QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) {
if (saved.at(i) == "version") continue;
if (saved.at(i) == QLatin1String("version")) continue;
s.remove(saved.at(i));
}
}
@ -361,17 +368,17 @@ void CollectionFilterWidget::SaveGroupBy() {
qLog(Debug) << "Saving current grouping to" << name;
QSettings s;
if (settings_group_.isEmpty() || settings_group_ == CollectionSettingsPage::kSettingsGroup) {
Settings s;
if (settings_group_.isEmpty() || settings_group_ == QLatin1String(CollectionSettingsPage::kSettingsGroup)) {
s.beginGroup(SavedGroupingManager::kSavedGroupingsSettingsGroup);
}
else {
s.beginGroup(QString(SavedGroupingManager::kSavedGroupingsSettingsGroup) + "_" + settings_group_);
s.beginGroup(QLatin1String(SavedGroupingManager::kSavedGroupingsSettingsGroup) + QLatin1Char('_') + settings_group_);
}
QByteArray buffer;
QDataStream datastream(&buffer, QIODevice::WriteOnly);
datastream << model_->GetGroupBy();
s.setValue("version", "1");
s.setValue("version", QStringLiteral("1"));
s.setValue(name, buffer);
s.endGroup();
@ -425,7 +432,7 @@ void CollectionFilterWidget::GroupByClicked(QAction *action) {
void CollectionFilterWidget::GroupingChanged(const CollectionModel::Grouping g, const bool separate_albums_by_grouping) {
if (!settings_group_.isEmpty()) {
QSettings s;
Settings s;
s.beginGroup(settings_group_);
s.setValue(group_by_version(), 1);
s.setValue(group_by_key(1), static_cast<int>(g[0]));
@ -446,7 +453,8 @@ void CollectionFilterWidget::CheckCurrentGrouping(const CollectionModel::Groupin
UpdateGroupByActions();
}
for (QAction *action : group_by_group_->actions()) {
const QList<QAction*> actions = group_by_group_->actions();
for (QAction *action : actions) {
if (action->property("group_by").isNull()) continue;
if (g == action->property("group_by").value<CollectionModel::Grouping>()) {
@ -456,7 +464,6 @@ void CollectionFilterWidget::CheckCurrentGrouping(const CollectionModel::Groupin
}
// Check the advanced action
QList<QAction*> actions = group_by_group_->actions();
QAction *action = actions.last();
action->setChecked(true);
@ -508,6 +515,9 @@ void CollectionFilterWidget::keyReleaseEvent(QKeyEvent *e) {
ui_->search_field->clear();
e->accept();
break;
default:
break;
}
QWidget::keyReleaseEvent(e);
@ -516,9 +526,6 @@ void CollectionFilterWidget::keyReleaseEvent(QKeyEvent *e) {
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_ == DelayBehaviour::AlwaysDelayed) || (delay_behaviour_ == DelayBehaviour::DelayedOnLargeLibraries && !text.isEmpty() && text.length() < 3 && model_->total_song_count() >= 100000);
if (delay) {
@ -533,9 +540,8 @@ void CollectionFilterWidget::FilterTextChanged(const QString &text) {
void CollectionFilterWidget::FilterDelayTimeout() {
emit Filter(ui_->search_field->text());
if (filter_applies_to_model_) {
model_->SetFilterText(ui_->search_field->text());
filter_->setFilterFixedString(ui_->search_field->text());
}
}

View File

@ -41,6 +41,7 @@ class QKeyEvent;
class GroupByDialog;
class SavedGroupingManager;
class CollectionFilter;
class Ui_CollectionFilterWidget;
class CollectionFilterWidget : public QWidget {
@ -58,7 +59,9 @@ class CollectionFilterWidget : public QWidget {
AlwaysDelayed,
};
void Init(CollectionModel *model);
void Init(CollectionModel *model, CollectionFilter *filter);
void setFilter(CollectionFilter *filter);
static QActionGroup *CreateGroupByActions(const QString &saved_groupings_settings_group, QObject *parent);
@ -94,7 +97,6 @@ class CollectionFilterWidget : public QWidget {
void UpPressed();
void DownPressed();
void ReturnPressed();
void Filter(const QString &text);
protected:
void keyReleaseEvent(QKeyEvent *e) override;
@ -115,6 +117,7 @@ class CollectionFilterWidget : public QWidget {
private:
Ui_CollectionFilterWidget *ui_;
CollectionModel *model_;
CollectionFilter *filter_;
GroupByDialog *group_by_dialog_;
SavedGroupingManager *groupings_manager_;
@ -123,7 +126,7 @@ class CollectionFilterWidget : public QWidget {
QMenu *group_by_menu_;
QMenu *collection_menu_;
QActionGroup *group_by_group_;
QHash<QAction*, int> filter_ages_;
QHash<QAction*, int> filter_max_ages_;
QTimer *filter_delay_;

View File

@ -38,6 +38,9 @@
</item>
<item>
<widget class="QToolButton" name="options">
<property name="accessibleName">
<string>MenuPopupToolButton</string>
</property>
<property name="iconSize">
<size>
<width>16</width>

View File

@ -29,24 +29,27 @@
class CollectionItem : public SimpleTreeItem<CollectionItem> {
public:
enum Type {
Type_Root,
Type_Divider,
Type_Container,
Type_Song,
Type_LoadingIndicator,
enum class Type {
Root,
Divider,
Container,
Song,
LoadingIndicator,
};
explicit CollectionItem(SimpleTreeModel<CollectionItem> *_model)
: SimpleTreeItem<CollectionItem>(Type_Root, _model),
: SimpleTreeItem<CollectionItem>(_model),
type(Type::Root),
container_level(-1),
compilation_artist_node_(nullptr) {}
explicit CollectionItem(Type _type, CollectionItem *_parent = nullptr)
: SimpleTreeItem<CollectionItem>(_type, _parent),
explicit CollectionItem(const Type _type, CollectionItem *_parent = nullptr)
: SimpleTreeItem<CollectionItem>(_parent),
type(_type),
container_level(-1),
compilation_artist_node_(nullptr) {}
Type type;
int container_level;
Song metadata;
CollectionItem *compilation_artist_node_;
@ -55,4 +58,6 @@ class CollectionItem : public SimpleTreeItem<CollectionItem> {
Q_DISABLE_COPY(CollectionItem)
};
Q_DECLARE_METATYPE(CollectionItem::Type)
#endif // COLLECTIONITEM_H

View File

@ -130,7 +130,7 @@ bool CollectionItemDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *vie
if (text.isEmpty()) return false;
switch (event->type()) {
case QEvent::ToolTip: {
case QEvent::ToolTip:{
QSize real_text = sizeHint(option, idx);
QRect displayed_text = view->visualRect(idx);

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,6 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2024, 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
@ -44,6 +42,7 @@
#include <QIcon>
#include <QPixmap>
#include <QNetworkDiskCache>
#include <QQueue>
#include "core/shared_ptr.h"
#include "core/simpletreemodel.h"
@ -51,15 +50,17 @@
#include "core/sqlrow.h"
#include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h"
#include "collectionmodelupdate.h"
#include "collectionfilteroptions.h"
#include "collectionqueryoptions.h"
#include "collectionitem.h"
class QSettings;
class QTimer;
class Settings;
class Application;
class CollectionBackend;
class CollectionDirectoryModel;
class CollectionFilter;
class CollectionModel : public SimpleTreeModel<CollectionItem> {
Q_OBJECT
@ -69,20 +70,19 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
~CollectionModel() override;
static const int kPrettyCoverSize;
static const char *kPixmapDiskCacheDir;
enum Role {
Role_Type = Qt::UserRole + 1,
Role_ContainerType,
Role_SortText,
Role_Key,
Role_ContainerKey,
Role_Artist,
Role_IsDivider,
Role_Editable,
LastRole
};
// These values get saved in QSettings - don't change them
// These values get saved in Settings - don't change them
enum class GroupBy {
None = 0,
AlbumArtist = 1,
@ -125,167 +125,176 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
bool operator!=(const Grouping other) const { return !(*this == other); }
};
struct QueryResult {
QueryResult() : create_va(false) {}
struct Options {
Options() : group_by(GroupBy::AlbumArtist, GroupBy::AlbumDisc, GroupBy::None),
show_dividers(true),
show_pretty_covers(true),
show_various_artists(true),
sort_skips_articles(true),
separate_albums_by_grouping(false) {}
SqlRowList rows;
bool create_va;
Grouping group_by;
bool show_dividers;
bool show_pretty_covers;
bool show_various_artists;
bool sort_skips_articles;
bool separate_albums_by_grouping;
CollectionFilterOptions filter_options;
};
SharedPtr<CollectionBackend> backend() const { return backend_; }
CollectionFilter *filter() const { return filter_; }
void Init();
void Reset();
void ReloadSettings();
CollectionDirectoryModel *directory_model() const { return dir_model_; }
// Call before Init()
void set_show_various_artists(const bool show_various_artists) { show_various_artists_ = show_various_artists; }
// Get information about the collection
void GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) const;
SongList GetChildSongs(const QModelIndex &idx) const;
SongList GetChildSongs(const QModelIndexList &indexes) const;
// Might be accurate
int total_song_count() const { return total_song_count_; }
int total_artist_count() const { return total_artist_count_; }
int total_album_count() const { return total_album_count_; }
// QAbstractItemModel
QVariant data(const QModelIndex &idx, const int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &idx) const override;
QStringList mimeTypes() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
bool canFetchMore(const QModelIndex &parent) const override;
// Whether or not to use album cover art, if it exists, in the collection view
void set_pretty_covers(const bool use_pretty_covers);
bool use_pretty_covers() const { return use_pretty_covers_; }
// Whether or not to show letters heading in the collection view
void set_show_dividers(const bool show_dividers);
// Reload settings.
void ReloadSettings();
// Utility functions for manipulating text
static QString TextOrUnknown(const QString &text);
static QString PrettyYearAlbum(const int year, const QString &album);
static QString PrettyAlbumDisc(const QString &album, const int disc);
static QString PrettyYearAlbumDisc(const int year, const QString &album, const int disc);
static QString PrettyDisc(const int disc);
static QString SortText(QString text);
static QString SortTextForNumber(const int number);
static QString SortTextForArtist(QString artist);
static QString SortTextForSong(const Song &song);
static QString SortTextForYear(const int year);
static QString SortTextForBitrate(const int bitrate);
quint64 icon_cache_disk_size() { return sIconCache->cacheSize(); }
const CollectionModel::Grouping GetGroupBy() const { return options_current_.group_by; }
void SetGroupBy(const CollectionModel::Grouping g, const std::optional<bool> separate_albums_by_grouping = std::optional<bool>());
static bool IsArtistGroupBy(const GroupBy group_by) {
return group_by == CollectionModel::GroupBy::Artist || group_by == CollectionModel::GroupBy::AlbumArtist;
}
static bool IsAlbumGroupBy(const GroupBy group_by) { return group_by == GroupBy::Album || group_by == GroupBy::YearAlbum || group_by == GroupBy::AlbumDisc || group_by == GroupBy::YearAlbumDisc || group_by == GroupBy::OriginalYearAlbum || group_by == GroupBy::OriginalYearAlbumDisc; }
void set_use_lazy_loading(const bool value) { use_lazy_loading_ = value; }
QMap<QString, CollectionItem*> container_nodes(const int i) { return container_nodes_[i]; }
QList<CollectionItem*> song_nodes() const { return song_nodes_.values(); }
int divider_nodes_count() const { return divider_nodes_.count(); }
// QAbstractItemModel
QVariant data(const QModelIndex &idx, const int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &idx) const override;
QStringList mimeTypes() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
// Utility functions for manipulating text
static QString DisplayText(const GroupBy group_by, const Song &song);
static QString TextOrUnknown(const QString &text);
static QString PrettyYearAlbum(const int year, const QString &album);
static QString PrettyAlbumDisc(const QString &album, const int disc);
static QString PrettyYearAlbumDisc(const int year, const QString &album, const int disc);
static QString PrettyDisc(const int disc);
static QString PrettyFormat(const Song &song);
QString SortText(const GroupBy group_by, const int container_level, const Song &song, const bool sort_skips_articles);
static QString SortText(QString text);
static QString SortTextForNumber(const int number);
static QString SortTextForArtist(QString artist, const bool skip_articles);
static QString SortTextForSong(const Song &song);
static QString SortTextForYear(const int year);
static QString SortTextForBitrate(const int bitrate);
static bool IsSongTitleDataChanged(const Song &song1, const Song &song2);
QString ContainerKey(const GroupBy group_by, const Song &song, bool &has_unique_album_identifier) const;
// Get information about the collection
void GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) const;
SongList GetChildSongs(const QModelIndex &idx) const;
SongList GetChildSongs(const QModelIndexList &indexes) const;
void ExpandAll(CollectionItem *item = nullptr) const;
const CollectionModel::Grouping GetGroupBy() const { return group_by_; }
void SetGroupBy(const CollectionModel::Grouping g, const std::optional<bool> separate_albums_by_grouping = std::optional<bool>());
static QString ContainerKey(const GroupBy group_by, const bool separate_albums_by_grouping, const Song &song);
signals:
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);
void SongsAdded(const SongList &songs);
void SongsRemoved(const SongList &songs);
public slots:
void SetFilterMode(CollectionFilterOptions::FilterMode filter_mode);
void SetFilterAge(const int filter_age);
void SetFilterText(const QString &filter_text);
void SetFilterMode(const CollectionFilterOptions::FilterMode filter_mode);
void SetFilterMaxAge(const int filter_max_age);
void Init(const bool async = true);
void Reset();
void ResetAsync();
void SongsDiscovered(const SongList &songs);
protected:
void LazyPopulate(CollectionItem *item) override { LazyPopulate(item, true); }
void LazyPopulate(CollectionItem *parent, const bool signal);
private slots:
// From CollectionBackend
void SongsDeleted(const SongList &songs);
void SongsSlightlyChanged(const SongList &songs);
void TotalSongCountUpdatedSlot(const int count);
void TotalArtistCountUpdatedSlot(const int count);
void TotalAlbumCountUpdatedSlot(const int count);
static void ClearDiskCache();
// Called after ResetAsync
void ResetAsyncQueryFinished();
void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
void AddReAddOrUpdate(const SongList &songs);
void RemoveSongs(const SongList &songs);
private:
// Provides some optimizations for loading the list of items in the root.
// This gets called a lot when filtering the playlist, so it's nice to be able to do it in a background thread.
CollectionQueryOptions PrepareQuery(CollectionItem *parent);
QueryResult RunQuery(const CollectionFilterOptions &filter_options = CollectionFilterOptions(), const CollectionQueryOptions &query_options = CollectionQueryOptions());
void PostQuery(CollectionItem *parent, const QueryResult &result, const bool signal);
bool HasCompilations(const QSqlDatabase &db, const CollectionFilterOptions &filter_options, const CollectionQueryOptions &query_options);
void Clear();
void BeginReset();
void EndReset();
// Functions for working with queries and creating items.
// When the model is reset or when a node is lazy-loaded the Collection constructs a database query to populate the items.
// Filters are added for each parent item, restricting the songs returned to a particular album or artist for example.
static void SetQueryColumnSpec(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionQueryOptions *query_options);
static void AddQueryWhere(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionItem *item, CollectionQueryOptions *query_options);
QVariant data(const CollectionItem *item, const int role) const;
// Items can be created either from a query that's been run to populate a node, or by a spontaneous SongsDiscovered emission from the backend.
CollectionItem *ItemFromQuery(const GroupBy group_by, const bool separate_albums_by_grouping, const bool signal, const bool create_divider, CollectionItem *parent, const SqlRow &row, const int container_level);
CollectionItem *ItemFromSong(const GroupBy group_by, const bool separate_albums_by_grouping, const bool signal, const bool create_divider, CollectionItem *parent, const Song &s, const int container_level);
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs);
void ScheduleAddSongs(const SongList &songs);
void ScheduleUpdateSongs(const SongList &songs);
void ScheduleRemoveSongs(const SongList &songs);
// The "Various Artists" node is an annoying special case.
CollectionItem *CreateCompilationArtistNode(const bool signal, CollectionItem *parent);
void AddReAddOrUpdateSongsInternal(const SongList &songs);
void AddSongsInternal(const SongList &songs);
void UpdateSongsInternal(const SongList &songs);
void RemoveSongsInternal(const SongList &songs);
// Helpers for ItemFromQuery and ItemFromSong
CollectionItem *InitItem(const GroupBy group_by, const bool signal, CollectionItem *parent, const int container_level);
void FinishItem(const GroupBy group_by, const bool signal, const bool create_divider, CollectionItem *parent, CollectionItem *item);
void CreateDividerItem(const QString &divider_key, const QString &display_text, CollectionItem *parent);
CollectionItem *CreateContainerItem(const GroupBy group_by, const int container_level, const QString &container_key, const Song &song, CollectionItem *parent);
void CreateSongItem(const Song &song, CollectionItem *parent);
void SetSongItemData(CollectionItem *item, const Song &song);
CollectionItem *CreateCompilationArtistNode(CollectionItem *parent);
static QString DividerKey(const GroupBy group_by, CollectionItem *item);
void LoadSongsFromSqlAsync();
SongList LoadSongsFromSql(const CollectionFilterOptions &filter_options = CollectionFilterOptions());
static QString DividerKey(const GroupBy group_by, const Song &song, const QString &sort_text);
static QString DividerDisplayText(const GroupBy group_by, const QString &key);
// 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;
static QUrl AlbumIconPixmapDiskCacheKey(const QString &cache_key);
QVariant AlbumIcon(const QModelIndex &idx);
QVariant data(const CollectionItem *item, const int role) const;
void ClearItemPixmapCache(CollectionItem *item);
bool CompareItems(const CollectionItem *a, const CollectionItem *b) const;
static qint64 MaximumCacheSize(QSettings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default);
static qint64 MaximumCacheSize(Settings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default);
private slots:
void Reload();
void ScheduleReset();
void ProcessUpdate();
void LoadSongsFromSqlAsyncFinished();
void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
// From CollectionBackend
void TotalSongCountUpdatedSlot(const int count);
void TotalArtistCountUpdatedSlot(const int count);
void TotalAlbumCountUpdatedSlot(const int count);
static void ClearDiskCache();
void RowsInserted(const QModelIndex &parent, const int first, const int last);
void RowsRemoved(const QModelIndex &parent, const int first, const int last);
private:
static QNetworkDiskCache *sIconCache;
SharedPtr<CollectionBackend> backend_;
Application *app_;
CollectionDirectoryModel *dir_model_;
bool show_various_artists_;
CollectionFilter *filter_;
QTimer *timer_reload_;
QTimer *timer_update_;
QPixmap pixmap_no_cover_;
QIcon icon_artist_;
Options options_current_;
Options options_active_;
bool use_disk_cache_;
AlbumCoverLoaderOptions::Types cover_types_;
int total_song_count_;
int total_artist_count_;
int total_album_count_;
CollectionFilterOptions filter_options_;
Grouping group_by_;
bool separate_albums_by_grouping_;
bool loading_;
QQueue<CollectionModelUpdate> updates_;
// Keyed on database ID
QMap<int, CollectionItem*> song_nodes_;
@ -296,22 +305,6 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
// Keyed on a letter, a year, a century, etc.
QMap<QString, CollectionItem*> divider_nodes_;
QIcon artist_icon_;
QIcon album_icon_;
// Used as a generic icon to show when no cover art is found, fixed to the same size as the artwork (32x32)
QPixmap no_cover_icon_;
static QNetworkDiskCache *sIconCache;
int init_task_id_;
bool use_pretty_covers_;
bool show_dividers_;
bool use_disk_cache_;
bool use_lazy_loading_;
AlbumCoverLoaderOptions::Types cover_types_;
using ItemAndCacheKey = QPair<CollectionItem*, QString>;
QMap<quint64, ItemAndCacheKey> pending_art_;
QSet<QString> pending_cache_keys_;

View File

@ -0,0 +1,23 @@
/*
* Strawberry Music Player
* Copyright 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
* 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 "collectionmodelupdate.h"
CollectionModelUpdate::CollectionModelUpdate(const Type &_type, const SongList &_songs)
: type(_type), songs(_songs) {}

View File

@ -0,0 +1,38 @@
/*
* Strawberry Music Player
* Copyright 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
* 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 COLLECTIONMODELUPDATE_H
#define COLLECTIONMODELUPDATE_H
#include "core/song.h"
class CollectionModelUpdate {
public:
enum class Type {
AddReAddOrUpdate,
Add,
Update,
Remove,
};
explicit CollectionModelUpdate(const Type &_type, const SongList &_songs);
Type type;
SongList songs;
};
#endif // COLLECTIONMODELUPDATE_H

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-2024, 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,151 +27,51 @@
#include <QVariant>
#include <QString>
#include <QStringList>
#include <QStringBuilder>
#include <QRegularExpression>
#include <QSqlDatabase>
#include <QSqlQuery>
#include "core/sqlquery.h"
#include "core/song.h"
#include "collectionquery.h"
#include "collectionfilteroptions.h"
#include "utilities/searchparserutils.h"
CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const CollectionFilterOptions &filter_options)
: QSqlQuery(db),
CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const CollectionFilterOptions &filter_options)
: SqlQuery(db),
songs_table_(songs_table),
fts_table_(fts_table),
include_unavailable_(false),
join_with_fts_(false),
duplicates_only_(false),
limit_(-1) {
if (!filter_options.filter_text().isEmpty()) {
// We need to munge the filter text a little bit to get it to work as expected with sqlite's FTS5:
// 1) Append * to all tokens.
// 2) Prefix "fts" to column names.
// 3) Remove colons which don't correspond to column names.
// Split on whitespace
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QStringList tokens(filter_options.filter_text().split(QRegularExpression("\\s+"), Qt::SkipEmptyParts));
#else
QStringList tokens(filter_options.filter_text().split(QRegularExpression("\\s+"), QString::SkipEmptyParts));
#endif
QString query;
for (QString token : tokens) {
token.remove('(');
token.remove(')');
token.remove('"');
token.replace('-', ' ');
if (token.contains(':')) {
// Only prefix fts if the token is a valid column name.
if (Song::kFtsColumns.contains("fts" + token.section(':', 0, 0), Qt::CaseInsensitive)) {
// Account for multiple colons.
QString columntoken = token.section(':', 0, 0, QString::SectionIncludeTrailingSep);
QString subtoken = token.section(':', 1, -1);
subtoken.replace(":", " ");
subtoken = subtoken.trimmed();
if (!subtoken.isEmpty()) {
if (!query.isEmpty()) query.append(" ");
query += "fts" + columntoken + "\"" + subtoken + "\"*";
}
}
else if (Song::kNumericalColumns.contains(token.section(':', 0, 0), Qt::CaseInsensitive)) {
// Account for multiple colons.
QString columntoken = token.section(':', 0, 0);
QString subtoken = token.section(':', 1, -1);
subtoken = subtoken.trimmed();
if (!subtoken.isEmpty()) {
QString comparator = RemoveSqlOperator(subtoken);
if (columntoken.compare("rating", Qt::CaseInsensitive) == 0) {
subtoken.replace(":", " ");
AddWhereRating(subtoken, comparator);
}
else if (columntoken.compare("length", Qt::CaseInsensitive) == 0) {
// time is saved in nanoseconds, so add 9 0's
QString parsedTime = QString::number(Utilities::ParseSearchTime(subtoken)) + "000000000";
AddWhere(columntoken, parsedTime, comparator);
}
else {
subtoken.replace(":", " ");
AddWhere(columntoken, subtoken, comparator);
}
}
}
// not a valid filter, remove
else {
token.replace(":", " ");
token = token.trimmed();
if (!query.isEmpty()) query.append(" ");
query += "\"" + token + "\"*";
}
}
else {
if (!query.isEmpty()) query.append(" ");
query += "\"" + token + "\"*";
}
}
if (!query.isEmpty()) {
where_clauses_ << "fts.%fts_table_noprefix MATCH ?";
bound_values_ << query;
join_with_fts_ = true;
}
}
if (filter_options.max_age() != -1) {
qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - filter_options.max_age();
where_clauses_ << "ctime > ?";
where_clauses_ << QStringLiteral("ctime > ?");
bound_values_ << cutoff;
}
// TODO: Currently you cannot use any FilterMode other than All and FTS at the same time.
// Joining songs, duplicated_songs and songs_fts all together takes a huge amount of time.
// The query takes about 20 seconds on my machine then. Why?
// 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;
if (filter_options.filter_mode() == CollectionFilterOptions::FilterMode::Untagged) {
where_clauses_ << "(artist = '' OR album = '' OR title ='')";
where_clauses_ << QStringLiteral("(artist = '' OR album = '' OR title ='')");
}
}
QString CollectionQuery::RemoveSqlOperator(QString &token) {
QString op = "=";
static QRegularExpression rxOp("^(=|<[>=]?|>=?|!=)");
QRegularExpressionMatch match = rxOp.match(token);
if (match.hasMatch()) {
op = match.captured(0);
}
token.remove(rxOp);
if (op == "!=") {
op = "<>";
}
return op;
}
void CollectionQuery::AddWhere(const QString &column, const QVariant &value, const QString &op) {
// Ignore 'literal' for IN
if (op.compare("IN", Qt::CaseInsensitive) == 0) {
if (op.compare(QLatin1String("IN"), Qt::CaseInsensitive) == 0) {
QStringList values = value.toStringList();
QStringList final_values;
final_values.reserve(values.count());
for (const QString &single_value : values) {
final_values.append("?");
final_values.append(QStringLiteral("?"));
bound_values_ << single_value;
}
where_clauses_ << QString("%1 IN (" + final_values.join(",") + ")").arg(column);
where_clauses_ << QStringLiteral("%1 IN (%2)").arg(column, final_values.join(QLatin1Char(',')));
}
else {
// Do integers inline - sqlite seems to get confused when you pass integers to bound parameters
@ -180,7 +80,7 @@ void CollectionQuery::AddWhere(const QString &column, const QVariant &value, con
#else
if (value.type() == QVariant::Int) {
#endif
where_clauses_ << QString("%1 %2 %3").arg(column, op, value.toString());
where_clauses_ << QStringLiteral("%1 %2 %3").arg(column, op, value.toString());
}
else if (
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
@ -189,67 +89,26 @@ void CollectionQuery::AddWhere(const QString &column, const QVariant &value, con
value.type() == QVariant::String
#endif
&& value.toString().isNull()) {
where_clauses_ << QString("%1 %2 ?").arg(column, op);
bound_values_ << QString("");
where_clauses_ << QStringLiteral("%1 %2 ?").arg(column, op);
bound_values_ << QLatin1String("");
}
else {
where_clauses_ << QString("%1 %2 ?").arg(column, op);
where_clauses_ << QStringLiteral("%1 %2 ?").arg(column, op);
bound_values_ << value;
}
}
}
void CollectionQuery::AddWhereArtist(const QVariant &value) {
where_clauses_ << QString("((artist = ? AND albumartist = '') OR albumartist = ?)");
bound_values_ << value;
bound_values_ << value;
}
void CollectionQuery::AddWhereRating(const QVariant &value, const QString &op) {
float parsed_rating = Utilities::ParseSearchRating(value.toString());
// You can't query the database for a float, due to float precision errors,
// So we have to use a certain tolerance, so that the searched value is definetly included.
const float tolerance = 0.001;
if (op == "<") {
AddWhere("rating", parsed_rating-tolerance, "<");
}
else if (op == ">") {
AddWhere("rating", parsed_rating+tolerance, ">");
}
else if (op == "<=") {
AddWhere("rating", parsed_rating+tolerance, "<=");
}
else if (op == ">=") {
AddWhere("rating", parsed_rating-tolerance, ">=");
}
else if (op == "<>") {
where_clauses_ << QString("(rating<? OR rating>?)");
bound_values_ << parsed_rating - tolerance;
bound_values_ << parsed_rating + tolerance;
}
else /* (op == "=") */ {
AddWhere("rating", parsed_rating+tolerance, "<");
AddWhere("rating", parsed_rating-tolerance, ">");
}
}
void CollectionQuery::AddCompilationRequirement(const bool compilation) {
// The unary + is added to prevent sqlite from using the index idx_comp_artist.
// When joining with fts, sqlite 3.8 has a tendency to use this index and thereby nesting the tables in an order which gives very poor performance
where_clauses_ << QString("+compilation_effective = %1").arg(compilation ? 1 : 0);
where_clauses_ << QStringLiteral("+compilation_effective = %1").arg(compilation ? 1 : 0);
}
QString CollectionQuery::GetInnerQuery() const {
return duplicates_only_
? QString(" INNER JOIN (select * from duplicated_songs) dsongs "
? QStringLiteral(" INNER JOIN (select * from duplicated_songs) dsongs "
"ON (%songs_table.artist = dsongs.dup_artist "
"AND %songs_table.album = dsongs.dup_album "
"AND %songs_table.title = dsongs.dup_title) ")
@ -258,29 +117,20 @@ QString CollectionQuery::GetInnerQuery() const {
bool CollectionQuery::Exec() {
QString sql;
if (join_with_fts_) {
sql = QString("SELECT %1 FROM %2 INNER JOIN %3 AS fts ON %2.ROWID = fts.ROWID").arg(column_spec_, songs_table_, fts_table_);
}
else {
sql = QString("SELECT %1 FROM %2 %3").arg(column_spec_, songs_table_, GetInnerQuery());
}
QString sql = QStringLiteral("SELECT %1 FROM %2 %3").arg(column_spec_, songs_table_, GetInnerQuery());
QStringList where_clauses(where_clauses_);
if (!include_unavailable_) {
where_clauses << "unavailable = 0";
where_clauses << QStringLiteral("unavailable = 0");
}
if (!where_clauses.isEmpty()) sql += " WHERE " + where_clauses.join(" AND ");
if (!where_clauses.isEmpty()) sql += QLatin1String(" WHERE ") + where_clauses.join(QLatin1String(" AND "));
if (!order_by_.isEmpty()) sql += " ORDER BY " + order_by_;
if (!order_by_.isEmpty()) sql += QLatin1String(" ORDER BY ") + order_by_;
if (limit_ != -1) sql += " LIMIT " + QString::number(limit_);
if (limit_ != -1) sql += QLatin1String(" LIMIT ") + QString::number(limit_);
sql.replace("%songs_table", songs_table_);
sql.replace("%fts_table_noprefix", fts_table_.section('.', -1, -1));
sql.replace("%fts_table", fts_table_);
sql.replace(QLatin1String("%songs_table"), songs_table_);
if (!QSqlQuery::prepare(sql)) return false;

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-2024, 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,19 +29,20 @@
#include <QString>
#include <QStringList>
#include <QSqlDatabase>
#include <QSqlQuery>
#include "core/sqlquery.h"
#include "collectionfilteroptions.h"
class CollectionQuery : public QSqlQuery {
class CollectionQuery : public SqlQuery {
public:
explicit CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const CollectionFilterOptions &filter_options = CollectionFilterOptions());
explicit CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const CollectionFilterOptions &filter_options = CollectionFilterOptions());
QVariant Value(const int column) const;
QVariant value(const int column) const { return Value(column); }
bool Exec();
bool exec() { return QSqlQuery::exec(); }
bool exec() { return SqlQuery::exec(); }
bool Next();
@ -50,7 +51,6 @@ class CollectionQuery : public QSqlQuery {
QStringList where_clauses() const { return where_clauses_; }
QVariantList bound_values() const { return bound_values_; }
bool include_unavailable() const { return include_unavailable_; }
bool join_with_fts() const { return join_with_fts_; }
bool duplicates_only() const { return duplicates_only_; }
int limit() const { return limit_; }
@ -62,14 +62,9 @@ class CollectionQuery : public QSqlQuery {
void SetWhereClauses(const QStringList &where_clauses) { where_clauses_ = where_clauses; }
// Removes = < > <= >= <> from the beginning of the input string and returns the operator
// If the input String has no operator, returns "="
QString RemoveSqlOperator(QString &token);
// Adds a fragment of WHERE clause. When executed, this Query will connect all the fragments with AND operator.
// Please note that IN operator expects a QStringList as value.
void AddWhere(const QString &column, const QVariant &value, const QString &op = "=");
void AddWhereArtist(const QVariant &value);
void AddWhereRating(const QVariant &value, const QString &op = "=");
void AddWhere(const QString &column, const QVariant &value, const QString &op = QStringLiteral("="));
void SetBoundValues(const QVariantList &bound_values) { bound_values_ = bound_values; }
void SetDuplicatesOnly(const bool duplicates_only) { duplicates_only_ = duplicates_only; }
@ -82,7 +77,6 @@ class CollectionQuery : public QSqlQuery {
QSqlDatabase db_;
QString songs_table_;
QString fts_table_;
QString column_spec_;
QString order_by_;
@ -90,7 +84,6 @@ class CollectionQuery : public QSqlQuery {
QVariantList bound_values_;
bool include_unavailable_;
bool join_with_fts_;
bool duplicates_only_;
int limit_;
};

View File

@ -30,7 +30,7 @@ class CollectionQueryOptions {
explicit CollectionQueryOptions();
struct Where {
explicit Where(const QString _column = QString(), const QVariant _value = QString(), const QString _op = QString()) : column(_column), value(_value), op(_op) {}
explicit Where(const QString &_column = QString(), const QVariant &_value = QString(), const QString &_op = QString()) : column(_column), value(_value), op(_op) {}
QString column;
QVariant value;
QString op;
@ -51,7 +51,7 @@ class CollectionQueryOptions {
void set_query_have_compilations(const bool query_have_compilations) { query_have_compilations_ = query_have_compilations; }
QList<Where> where_clauses() const { return where_clauses_; }
void AddWhere(const QString &column, const QVariant &value, const QString &op = "=");
void AddWhere(const QString &column, const QVariant &value, const QString &op = QStringLiteral("="));
private:
QString column_spec_;

View File

@ -21,6 +21,7 @@
#include "config.h"
#include <utility>
#include <memory>
#include <QtGlobal>
@ -52,6 +53,7 @@
#include "core/mimedata.h"
#include "core/musicstorage.h"
#include "core/deletefiles.h"
#include "core/settings.h"
#include "utilities/filemanagerutils.h"
#include "collection.h"
#include "collectionbackend.h"
@ -80,7 +82,7 @@ CollectionView::CollectionView(QWidget *parent)
total_song_count_(-1),
total_artist_count_(-1),
total_album_count_(-1),
nomusic_(":/pictures/nomusic.png"),
nomusic_(QStringLiteral(":/pictures/nomusic.png")),
context_menu_(nullptr),
action_load_(nullptr),
action_add_to_playlist_(nullptr),
@ -88,6 +90,7 @@ CollectionView::CollectionView(QWidget *parent)
action_add_to_playlist_enqueue_next_(nullptr),
action_open_in_new_playlist_(nullptr),
action_organize_(nullptr),
action_search_for_this_(nullptr),
#ifndef Q_OS_WIN
action_copy_to_device_(nullptr),
#endif
@ -109,7 +112,7 @@ CollectionView::CollectionView(QWidget *parent)
setDragDropMode(QAbstractItemView::DragOnly);
setSelectionMode(QAbstractItemView::ExtendedSelection);
setStyleSheet("QTreeView::item{padding-top:1px;}");
setStyleSheet(QStringLiteral("QTreeView::item{padding-top:1px;}"));
}
@ -117,9 +120,14 @@ CollectionView::~CollectionView() = default;
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)) {
const QModelIndex current = currentIndex();
const QVariant role_type = model()->data(current, CollectionModel::Role_Type);
if (!role_type.isValid()) {
return;
}
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
if (item_type != CollectionItem::Type::Song && item_type != CollectionItem::Type::Container && item_type != CollectionItem::Type::Divider) {
return;
}
@ -127,8 +135,8 @@ void CollectionView::SaveFocus() {
last_selected_song_ = Song();
last_selected_container_ = QString();
switch (type.toInt()) {
case CollectionItem::Type_Song: {
switch (item_type) {
case CollectionItem::Type::Song:{
QModelIndex index = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
SongList songs = app_->collection_model()->GetChildSongs(index);
if (!songs.isEmpty()) {
@ -137,8 +145,8 @@ void CollectionView::SaveFocus() {
break;
}
case CollectionItem::Type_Container:
case CollectionItem::Type_Divider: {
case CollectionItem::Type::Container:
case CollectionItem::Type::Divider:{
QString text = model()->data(current, CollectionModel::Role_SortText).toString();
last_selected_container_ = text;
break;
@ -154,9 +162,14 @@ void CollectionView::SaveFocus() {
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)) {
const QModelIndex current = model()->parent(child);
const QVariant role_type = model()->data(current, CollectionModel::Role_Type);
if (!role_type.isValid()) {
return;
}
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
if (item_type != CollectionItem::Type::Container && item_type != CollectionItem::Type::Divider) {
return;
}
@ -180,12 +193,17 @@ bool CollectionView::RestoreLevelFocus(const QModelIndex &parent) {
if (model()->canFetchMore(parent)) {
model()->fetchMore(parent);
}
int rows = model()->rowCount(parent);
const int rows = model()->rowCount(parent);
for (int i = 0; i < rows; i++) {
QModelIndex current = model()->index(i, 0, parent);
QVariant type = model()->data(current, CollectionModel::Role_Type);
switch (type.toInt()) {
case CollectionItem::Type_Song:
const QVariant role_type = model()->data(current, CollectionModel::Role_Type);
if (!role_type.isValid()) return false;
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
switch (item_type) {
case CollectionItem::Type::Root:
case CollectionItem::Type::LoadingIndicator:
break;
case CollectionItem::Type::Song:
if (!last_selected_song_.url().isEmpty()) {
QModelIndex index = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
const SongList songs = app_->collection_model()->GetChildSongs(index);
@ -196,8 +214,8 @@ bool CollectionView::RestoreLevelFocus(const QModelIndex &parent) {
}
break;
case CollectionItem::Type_Container:
case CollectionItem::Type_Divider: {
case CollectionItem::Type::Container:
case CollectionItem::Type::Divider:{
QString text = model()->data(current, CollectionModel::Role_SortText).toString();
if (!last_selected_container_.isEmpty() && last_selected_container_ == text) {
expand(current);
@ -224,18 +242,10 @@ bool CollectionView::RestoreLevelFocus(const QModelIndex &parent) {
void CollectionView::ReloadSettings() {
QSettings settings;
Settings settings;
settings.beginGroup(CollectionSettingsPage::kSettingsGroup);
SetAutoOpen(settings.value("auto_open", false).toBool());
if (app_) {
app_->collection_model()->set_pretty_covers(settings.value("pretty_covers", true).toBool());
app_->collection_model()->set_show_dividers(settings.value("show_dividers", true).toBool());
}
delete_files_ = settings.value("delete_files", false).toBool();
settings.endGroup();
}
@ -343,29 +353,51 @@ void CollectionView::mouseReleaseEvent(QMouseEvent *e) {
}
void CollectionView::keyPressEvent(QKeyEvent *e) {
switch (e->key()) {
case Qt::Key_Enter:
case Qt::Key_Return:
if (currentIndex().isValid()) {
AddToPlaylist();
}
e->accept();
break;
default:
break;
}
AutoExpandingTreeView::keyPressEvent(e);
}
void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
if (!context_menu_) {
context_menu_ = new QMenu(this);
action_add_to_playlist_ = context_menu_->addAction(IconLoader::Load("media-playback-start"), tr("Append to current playlist"), this, &CollectionView::AddToPlaylist);
action_load_ = context_menu_->addAction(IconLoader::Load("media-playback-start"), tr("Replace current playlist"), this, &CollectionView::Load);
action_open_in_new_playlist_ = context_menu_->addAction(IconLoader::Load("document-new"), tr("Open in new playlist"), this, &CollectionView::OpenInNewPlaylist);
action_add_to_playlist_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("media-playback-start")), tr("Append to current playlist"), this, &CollectionView::AddToPlaylist);
action_load_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("media-playback-start")), tr("Replace current playlist"), this, &CollectionView::Load);
action_open_in_new_playlist_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("document-new")), tr("Open in new playlist"), this, &CollectionView::OpenInNewPlaylist);
context_menu_->addSeparator();
action_add_to_playlist_enqueue_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue track"), this, &CollectionView::AddToPlaylistEnqueue);
action_add_to_playlist_enqueue_next_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue to play next"), this, &CollectionView::AddToPlaylistEnqueueNext);
action_add_to_playlist_enqueue_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("go-next")), tr("Queue track"), this, &CollectionView::AddToPlaylistEnqueue);
action_add_to_playlist_enqueue_next_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("go-next")), tr("Queue to play next"), this, &CollectionView::AddToPlaylistEnqueueNext);
context_menu_->addSeparator();
action_organize_ = context_menu_->addAction(IconLoader::Load("edit-copy"), tr("Organize files..."), this, &CollectionView::Organize);
action_search_for_this_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("edit-find")), tr("Search for this"), this, &CollectionView::SearchForThis);
context_menu_->addSeparator();
action_organize_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("edit-copy")), tr("Organize files..."), this, &CollectionView::Organize);
#ifndef Q_OS_WIN
action_copy_to_device_ = context_menu_->addAction(IconLoader::Load("device"), tr("Copy to device..."), this, &CollectionView::CopyToDevice);
action_copy_to_device_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("device")), tr("Copy to device..."), this, &CollectionView::CopyToDevice);
#endif
action_delete_files_ = context_menu_->addAction(IconLoader::Load("edit-delete"), tr("Delete from disk..."), this, &CollectionView::Delete);
action_delete_files_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("edit-delete")), tr("Delete from disk..."), this, &CollectionView::Delete);
context_menu_->addSeparator();
action_edit_track_ = context_menu_->addAction(IconLoader::Load("edit-rename"), tr("Edit track information..."), this, &CollectionView::EditTracks);
action_edit_tracks_ = context_menu_->addAction(IconLoader::Load("edit-rename"), tr("Edit tracks information..."), this, &CollectionView::EditTracks);
action_show_in_browser_ = context_menu_->addAction(IconLoader::Load("document-open-folder"), tr("Show in file browser..."), this, &CollectionView::ShowInBrowser);
action_edit_track_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("edit-rename")), tr("Edit track information..."), this, &CollectionView::EditTracks);
action_edit_tracks_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("edit-rename")), tr("Edit tracks information..."), this, &CollectionView::EditTracks);
action_show_in_browser_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("document-open-folder")), tr("Show in file browser..."), this, &CollectionView::ShowInBrowser);
context_menu_->addSeparator();
@ -391,7 +423,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
context_menu_index_ = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(context_menu_index_);
QModelIndexList selected_indexes = qobject_cast<QSortFilterProxyModel*>(model())->mapSelectionToSource(selectionModel()->selection()).indexes();
const QModelIndexList selected_indexes = qobject_cast<QSortFilterProxyModel*>(model())->mapSelectionToSource(selectionModel()->selection()).indexes();
int regular_elements = 0;
int regular_editable = 0;
@ -462,7 +494,8 @@ void CollectionView::SetShowInVarious(const bool on) {
// We put through "Various Artists" changes one album at a time,
// to make sure the old album node gets removed (due to all children removed), before the new one gets added
QMultiMap<QString, QString> albums;
for (const Song &song : GetSelectedSongs()) {
const SongList songs = GetSelectedSongs();
for (const Song &song : songs) {
if (albums.find(song.album(), song.artist()) == albums.end())
albums.insert(song.album(), song.artist());
}
@ -472,7 +505,7 @@ void CollectionView::SetShowInVarious(const bool on) {
if (on && albums.keys().count() == 1) {
const QStringList albums_list = albums.keys();
const QString album = albums_list.first();
SongList all_of_album = app_->collection_backend()->GetSongsByAlbum(album);
const SongList all_of_album = app_->collection_backend()->GetSongsByAlbum(album);
QSet<QString> other_artists;
for (const Song &s : all_of_album) {
if (!albums.contains(album, s.artist()) && !other_artists.contains(s.artist())) {
@ -489,9 +522,9 @@ void CollectionView::SetShowInVarious(const bool on) {
}
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
QSet<QString> albums_set = QSet<QString>(albums.keyBegin(), albums.keyEnd());
const QSet<QString> albums_set = QSet<QString>(albums.keyBegin(), albums.keyEnd());
#else
QSet<QString> albums_set = QSet<QString>::fromList(albums.keys());
const QSet<QString> albums_set = QSet<QString>::fromList(albums.keys());
#endif
for (const QString &album : albums_set) {
app_->collection_backend()->ForceCompilation(album, albums.values(album), on);
@ -545,6 +578,101 @@ void CollectionView::OpenInNewPlaylist() {
}
void CollectionView::SearchForThis() {
QModelIndex current = currentIndex();
const QVariant role_type = model()->data(current, CollectionModel::Role_Type);
if (!role_type.isValid()) {
return;
}
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
if (item_type != CollectionItem::Type::Song && item_type != CollectionItem::Type::Container && item_type != CollectionItem::Type::Divider) {
return;
}
QString search;
QModelIndex index = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
switch (item_type) {
case CollectionItem::Type::Song:{
SongList songs = app_->collection_model()->GetChildSongs(index);
if (!songs.isEmpty()) {
last_selected_song_ = songs.last();
}
search = QStringLiteral("title:%1").arg(last_selected_song_.title());
break;
}
case CollectionItem::Type::Divider:{
break;
}
case CollectionItem::Type::Container:{
CollectionItem *item = app_->collection_model()->IndexToItem(index);
int container_level = item->container_level;
CollectionModel::GroupBy container_group_by = app_->collection_model()->GetGroupBy()[container_level];
switch (container_group_by) {
case CollectionModel::GroupBy::AlbumArtist:
search = QStringLiteral("albumartist:%1").arg(item->metadata.effective_albumartist());
break;
case CollectionModel::GroupBy::Artist:
search = QStringLiteral("artist:%1").arg(item->metadata.artist());
break;
case CollectionModel::GroupBy::Album:
case CollectionModel::GroupBy::AlbumDisc:
search = QStringLiteral("album:%1").arg(item->metadata.album());
break;
case CollectionModel::GroupBy::YearAlbum:
case CollectionModel::GroupBy::YearAlbumDisc:
search = QStringLiteral("year:%1 album:%2").arg(item->metadata.year()).arg(item->metadata.album());
break;
case CollectionModel::GroupBy::OriginalYearAlbum:
case CollectionModel::GroupBy::OriginalYearAlbumDisc:
search = QStringLiteral("year:%1 album:%2").arg(item->metadata.effective_originalyear()).arg(item->metadata.album());
break;
case CollectionModel::GroupBy::Year:
search = QStringLiteral("year:%1").arg(item->metadata.year());
break;
case CollectionModel::GroupBy::OriginalYear:
search = QStringLiteral("year:%1").arg(item->metadata.effective_originalyear());
break;
case CollectionModel::GroupBy::Genre:
search = QStringLiteral("genre:%1").arg(item->metadata.genre());
break;
case CollectionModel::GroupBy::Composer:
search = QStringLiteral("composer:%1").arg(item->metadata.composer());
break;
case CollectionModel::GroupBy::Performer:
search = QStringLiteral("performer:%1").arg(item->metadata.performer());
break;
case CollectionModel::GroupBy::Grouping:
search = QStringLiteral("grouping:%1").arg(item->metadata.grouping());
break;
case CollectionModel::GroupBy::Samplerate:
search = QStringLiteral("samplerate:%1").arg(item->metadata.samplerate());
break;
case CollectionModel::GroupBy::Bitdepth:
search = QStringLiteral("bitdepth:%1").arg(item->metadata.bitdepth());
break;
case CollectionModel::GroupBy::Bitrate:
search = QStringLiteral("bitrate:%1").arg(item->metadata.bitrate());
break;
default:
search = model()->data(current, Qt::DisplayRole).toString();
}
break;
}
default:
return;
}
filter_->ShowInCollection(search);
}
void CollectionView::keyboardSearch(const QString &search) {
is_in_keyboard_search_ = true;
@ -631,8 +759,11 @@ void CollectionView::FilterReturnPressed() {
if (!currentIndex().isValid()) {
// Pick the first thing that isn't a divider
for (int row = 0; row < model()->rowCount(); ++row) {
QModelIndex idx(model()->index(row, 0));
if (idx.data(CollectionModel::Role_Type) != CollectionItem::Type_Divider) {
const QModelIndex idx = model()->index(row, 0);
const QVariant role_type = idx.data(CollectionModel::Role_Type);
if (!role_type.isValid()) continue;
const CollectionItem::Type item_type = role_type.value<CollectionItem::Type>();
if (item_type != CollectionItem::Type::Divider) {
setCurrentIndex(idx);
break;
}
@ -646,7 +777,7 @@ void CollectionView::FilterReturnPressed() {
void CollectionView::ShowInBrowser() const {
SongList songs = GetSelectedSongs();
const SongList songs = GetSelectedSongs();
QList<QUrl> urls;
urls.reserve(songs.count());
for (const Song &song : songs) {
@ -671,7 +802,7 @@ void CollectionView::Delete() {
if (!delete_files_) return;
SongList selected_songs = GetSelectedSongs();
const SongList selected_songs = GetSelectedSongs();
SongList songs;
QStringList files;

View File

@ -93,6 +93,7 @@ class CollectionView : public AutoExpandingTreeView {
protected:
// QWidget
void paintEvent(QPaintEvent *event) override;
void keyPressEvent(QKeyEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
@ -102,6 +103,7 @@ class CollectionView : public AutoExpandingTreeView {
void AddToPlaylistEnqueue();
void AddToPlaylistEnqueueNext();
void OpenInNewPlaylist();
void SearchForThis();
void Organize();
void CopyToDevice();
void EditTracks();
@ -136,6 +138,8 @@ class CollectionView : public AutoExpandingTreeView {
QAction *action_add_to_playlist_enqueue_next_;
QAction *action_open_in_new_playlist_;
QAction *action_organize_;
QAction *action_search_for_this_;
#ifndef Q_OS_WIN
QAction *action_copy_to_device_;
#endif

View File

@ -48,6 +48,7 @@
#include "core/logging.h"
#include "core/tagreaderclient.h"
#include "core/taskmanager.h"
#include "core/settings.h"
#include "utilities/imageutils.h"
#include "utilities/timeconstants.h"
#include "collectiondirectory.h"
@ -70,7 +71,8 @@
using namespace std::chrono_literals;
QStringList CollectionWatcher::sValidImages = QStringList() << "jpg" << "png" << "gif" << "jpeg";
QStringList CollectionWatcher::sValidImages = QStringList() << QStringLiteral("jpg") << QStringLiteral("png") << QStringLiteral("gif") << QStringLiteral("jpeg");
QStringList CollectionWatcher::kIgnoredExtensions = QStringList() << QStringLiteral("tmp") << QStringLiteral("tar") << QStringLiteral("gz") << QStringLiteral("bz2") << QStringLiteral("xz") << QStringLiteral("tbz") << QStringLiteral("tgz") << QStringLiteral("z") << QStringLiteral("zip") << QStringLiteral("rar");
CollectionWatcher::CollectionWatcher(Song::Source source, QObject *parent)
: QObject(parent),
@ -104,7 +106,7 @@ CollectionWatcher::CollectionWatcher(Song::Source source, QObject *parent)
periodic_scan_timer_->setInterval(86400 * kMsecPerSec);
periodic_scan_timer_->setSingleShot(false);
QStringList image_formats = ImageUtils::SupportedImageFormats();
const QStringList image_formats = ImageUtils::SupportedImageFormats();
for (const QString &format : image_formats) {
if (!sValidImages.contains(format)) {
sValidImages.append(format);
@ -149,11 +151,11 @@ void CollectionWatcher::ReloadSettingsAsync() {
void CollectionWatcher::ReloadSettings() {
const bool was_monitoring_before = monitor_;
QSettings s;
Settings s;
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
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();
const QStringList filters = s.value("cover_art_patterns", QStringList() << QStringLiteral("front") << QStringLiteral("cover")).toStringList();
if (source_ == Song::Source::Collection) {
song_tracking_ = s.value("song_tracking", false).toBool();
song_ebur128_loudness_analysis_ = s.value("song_ebur128_loudness_analysis", false).toBool();
@ -181,7 +183,7 @@ void CollectionWatcher::ReloadSettings() {
else if (monitor_ && !was_monitoring_before) {
// Add all directories to all QFileSystemWatchers again
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
CollectionSubdirectoryList subdirs = backend_->SubdirsInDirectory(dir.id);
const CollectionSubdirectoryList subdirs = backend_->SubdirsInDirectory(dir.id);
for (const CollectionSubdirectory &subdir : subdirs) {
AddWatch(dir, subdir.path);
}
@ -191,7 +193,7 @@ void CollectionWatcher::ReloadSettings() {
if (monitor_ && scan_on_startup_ && mark_songs_unavailable_ && !periodic_scan_timer_->isActive()) {
periodic_scan_timer_->start();
}
else if (!mark_songs_unavailable_ && periodic_scan_timer_->isActive()) {
else if ((!monitor_ || !scan_on_startup_ || !mark_songs_unavailable_) && periodic_scan_timer_->isActive()) {
periodic_scan_timer_->stop();
}
@ -286,7 +288,7 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
touched_subdirs.clear();
}
for (const CollectionSubdirectory &subdir : deleted_subdirs) {
for (const CollectionSubdirectory &subdir : std::as_const(deleted_subdirs)) {
if (watcher_->watched_dirs_.contains(dir_)) {
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_], subdir);
}
@ -295,7 +297,7 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
if (watcher_->monitor_) {
// Watch the new subdirectories
for (const CollectionSubdirectory &subdir : new_subdirs) {
for (const CollectionSubdirectory &subdir : std::as_const(new_subdirs)) {
if (watcher_->watched_dirs_.contains(dir_)) {
watcher_->AddWatch(watcher_->watched_dirs_[dir_], subdir.path);
}
@ -315,7 +317,7 @@ SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QStri
if (cached_songs_dirty_) {
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_);
for (const Song &song : songs) {
const QString p = song.url().toLocalFile().section('/', 0, -2);
const QString p = song.url().toLocalFile().section(QLatin1Char('/'), 0, -2);
cached_songs_.insert(p, song);
}
cached_songs_dirty_ = false;
@ -324,7 +326,8 @@ SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QStri
if (cached_songs_.contains(path)) {
return cached_songs_.values(path);
}
else return SongList();
return SongList();
}
@ -333,7 +336,7 @@ bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QS
if (cached_songs_missing_fingerprint_dirty_) {
const SongList songs = watcher_->backend_->SongsWithMissingFingerprint(dir_);
for (const Song &song : songs) {
const QString p = song.url().toLocalFile().section('/', 0, -2);
const QString p = song.url().toLocalFile().section(QLatin1Char('/'), 0, -2);
cached_songs_missing_fingerprint_.insert(p, song);
}
cached_songs_missing_fingerprint_dirty_ = false;
@ -348,7 +351,7 @@ bool CollectionWatcher::ScanTransaction::HasSongsWithMissingLoudnessCharacterist
if (cached_songs_missing_loudness_characteristics_dirty_) {
const SongList songs = watcher_->backend_->SongsWithMissingLoudnessCharacteristics(dir_);
for (const Song &song : songs) {
const QString p = song.url().toLocalFile().section('/', 0, -2);
const QString p = song.url().toLocalFile().section(QLatin1Char('/'), 0, -2);
cached_songs_missing_loudness_characteristics_.insert(p, song);
}
cached_songs_missing_loudness_characteristics_dirty_ = false;
@ -382,7 +385,7 @@ CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdi
}
CollectionSubdirectoryList ret;
for (const CollectionSubdirectory &subdir : known_subdirs_) {
for (const CollectionSubdirectory &subdir : std::as_const(known_subdirs_)) {
if (subdir.path.left(subdir.path.lastIndexOf(QDir::separator())) == path && subdir.mtime != 0) {
ret << subdir;
}
@ -415,7 +418,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
transaction.SetKnownSubdirs(subdirs);
transaction.AddToProgressMax(files_count);
ScanSubdirectory(dir.path, CollectionSubdirectory(), files_count, &transaction);
last_scan_time_ = QDateTime::currentDateTime().toSecsSinceEpoch();
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
}
else {
// We can do an incremental scan - looking at the mtimes of each subdirectory and only rescan if the directory has changed.
@ -432,7 +435,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
if (monitor_) AddWatch(dir, subdir.path);
}
last_scan_time_ = QDateTime::currentDateTime().toSecsSinceEpoch();
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
}
@ -479,7 +482,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
// If a directory is moved then only its parent gets a changed notification, so we need to look and see if any of our children don't exist anymore.
// If one has been removed, "rescan" it to get the deleted songs
CollectionSubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
const CollectionSubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
for (const CollectionSubdirectory &prev_subdir : previous_subdirs) {
if (!QFile::exists(prev_subdir.path) && prev_subdir.path != path) {
ScanSubdirectory(prev_subdir.path, prev_subdir, 0, t, true);
@ -509,7 +512,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
else {
QString ext_part(ExtensionPart(child));
QString dir_part(DirectoryPart(child));
if (child_info.suffix() == "tmp" || child_info.baseName() == "qt_temp") {
if (kIgnoredExtensions.contains(child_info.suffix(), Qt::CaseInsensitive) || child_info.baseName() == QLatin1String("qt_temp")) {
t->AddToProgress(1);
}
else if (sValidImages.contains(ext_part)) {
@ -611,7 +614,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
Chromaprinter chromaprinter(file);
fingerprint = chromaprinter.CreateFingerprint();
if (fingerprint.isEmpty()) {
fingerprint = "NONE";
fingerprint = QLatin1String("NONE");
}
}
#endif
@ -626,6 +629,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
// Nothing has changed - mark the song available without re-scanning
else if (matching_song.unavailable()) {
qLog(Debug) << "Unavailable song" << file << "restored.";
t->readded_songs << matching_songs;
}
@ -637,11 +641,11 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
Chromaprinter chromaprinter(file);
fingerprint = chromaprinter.CreateFingerprint();
if (fingerprint.isEmpty()) {
fingerprint = "NONE";
fingerprint = QLatin1String("NONE");
}
}
#endif
if (song_tracking_ && !fingerprint.isEmpty() && fingerprint != "NONE" && FindSongsByFingerprint(file, fingerprint, &matching_songs)) {
if (song_tracking_ && !fingerprint.isEmpty() && fingerprint != QLatin1String("NONE") && FindSongsByFingerprint(file, fingerprint, &matching_songs)) {
// The song is in the database and still on disk.
// Check the mtime to see if it's been changed since it was added.
@ -655,7 +659,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
// Make sure the songs aren't deleted, as they still exist elsewhere with a different file path.
bool matching_songs_has_cue = false;
for (const Song &matching_song : matching_songs) {
for (const Song &matching_song : std::as_const(matching_songs)) {
QString matching_filename = matching_song.url().toLocalFile();
if (!t->files_changed_path_.contains(matching_filename)) {
t->files_changed_path_ << matching_filename;
@ -688,7 +692,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
}
else { // The song is on disk but not in the DB
SongList songs = ScanNewFile(file, path, fingerprint, new_cue, &cues_processed);
const SongList songs = ScanNewFile(file, path, fingerprint, new_cue, &cues_processed);
if (songs.isEmpty()) {
t->AddToProgress(1);
continue;
@ -710,7 +714,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
}
// Look for deleted songs
for (const Song &song : songs_in_db) {
for (const Song &song : std::as_const(songs_in_db)) {
QString file = song.url().toLocalFile();
if (!song.unavailable() && !files_on_disk.contains(file) && !t->files_changed_path_.contains(file)) {
qLog(Debug) << "Song deleted from disk:" << file;
@ -736,7 +740,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
}
// Recurse into the new subdirs that we found
for (const CollectionSubdirectory &my_new_subdir : my_new_subdirs) {
for (const CollectionSubdirectory &my_new_subdir : std::as_const(my_new_subdirs)) {
if (stop_requested_ || abort_requested_) return;
ScanSubdirectory(my_new_subdir.path, my_new_subdir, 0, t, true);
}
@ -885,55 +889,55 @@ void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching
QStringList changes;
if (matching_song.unavailable()) {
qLog(Debug) << "unavailable song" << file << "restored.";
qLog(Debug) << "Unavailable song" << file << "restored.";
notify_new = true;
}
else {
if (matching_song.url() != new_song.url()) {
changes << "file path";
changes << QStringLiteral("file path");
notify_new = true;
}
if (matching_song.fingerprint() != new_song.fingerprint()) {
changes << "fingerprint";
changes << QStringLiteral("fingerprint");
notify_new = true;
}
if (!matching_song.IsMetadataEqual(new_song)) {
changes << "metadata";
changes << QStringLiteral("metadata");
notify_new = true;
}
if (!matching_song.IsPlayStatisticsEqual(new_song)) {
changes << "play statistics";
changes << QStringLiteral("play statistics");
notify_new = true;
}
if (!matching_song.IsRatingEqual(new_song)) {
changes << "rating";
changes << QStringLiteral("rating");
notify_new = true;
}
if (!matching_song.IsArtEqual(new_song)) {
changes << "album art";
changes << QStringLiteral("album art");
notify_new = true;
}
if (!matching_song.IsAcoustIdEqual(new_song)) {
changes << "acoustid";
changes << QStringLiteral("acoustid");
notify_new = true;
}
if (!matching_song.IsMusicBrainzEqual(new_song)) {
changes << "musicbrainz";
changes << QStringLiteral("musicbrainz");
notify_new = true;
}
if (!matching_song.IsEBUR128Equal(new_song)) {
changes << "ebur128 loudness characteristics";
changes << QStringLiteral("ebur128 loudness characteristics");
notify_new = true;
}
if (matching_song.mtime() != new_song.mtime()) {
changes << "mtime";
changes << QStringLiteral("mtime");
}
if (changes.isEmpty()) {
qLog(Debug) << "Song" << file << "unchanged.";
}
else {
qLog(Debug) << "Song" << file << changes.join(", ") << "changed.";
qLog(Debug) << "Song" << file << changes.join(QLatin1String(", ")) << "changed.";
}
}
@ -988,7 +992,7 @@ void CollectionWatcher::AddWatch(const CollectionDirectory &dir, const QString &
void CollectionWatcher::RemoveWatch(const CollectionDirectory &dir, const CollectionSubdirectory &subdir) {
QStringList subdir_paths = subdir_mapping_.keys(dir);
const QStringList subdir_paths = subdir_mapping_.keys(dir);
for (const QString &subdir_path : subdir_paths) {
if (subdir_path != subdir.path) continue;
fs_watcher_->RemovePath(subdir_path);
@ -1005,7 +1009,7 @@ void CollectionWatcher::RemoveDirectory(const CollectionDirectory &dir) {
// Stop watching the directory's subdirectories
QStringList subdir_paths = subdir_mapping_.keys(dir);
for (const QString &subdir_path : subdir_paths) {
for (const QString &subdir_path : std::as_const(subdir_paths)) {
fs_watcher_->RemovePath(subdir_path);
subdir_mapping_.remove(subdir_path);
}
@ -1027,7 +1031,7 @@ bool CollectionWatcher::FindSongsByPath(const SongList &songs, const QString &pa
bool CollectionWatcher::FindSongsByFingerprint(const QString &file, const QString &fingerprint, SongList *out) {
SongList songs = backend_->GetSongsByFingerprint(fingerprint);
for (const Song &song : songs) {
for (const Song &song : std::as_const(songs)) {
QString filename = song.url().toLocalFile();
QFileInfo info(filename);
// Allow mulitiple songs in different directories with the same fingerprint.
@ -1075,19 +1079,21 @@ void CollectionWatcher::DirectoryChanged(const QString &subdir) {
void CollectionWatcher::RescanPathsNow() {
QList<int> dirs = rescan_queue_.keys();
const QList<int> dirs = rescan_queue_.keys();
for (const int dir : dirs) {
if (stop_requested_ || abort_requested_) break;
ScanTransaction transaction(this, dir, false, false, mark_songs_unavailable_);
const QStringList paths = rescan_queue_[dir];
QMap<QString, quint64> subdir_files_count;
for (const QString &path : rescan_queue_[dir]) {
for (const QString &path : paths) {
quint64 files_count = FilesCountForPath(&transaction, path);
subdir_files_count[path] = files_count;
transaction.AddToProgressMax(files_count);
}
for (const QString &path : rescan_queue_[dir]) {
for (const QString &path : paths) {
if (stop_requested_ || abort_requested_) break;
CollectionSubdirectory subdir;
subdir.directory_id = dir;
@ -1110,7 +1116,7 @@ QString CollectionWatcher::PickBestArt(const QStringList &art_automatic_list) {
QStringList filtered;
for (const QString &filter_text : best_art_filters_) {
for (const QString &filter_text : std::as_const(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 &art_automatic : art_automatic_list) {
QFileInfo fileinfo(art_automatic);
@ -1132,7 +1138,7 @@ QString CollectionWatcher::PickBestArt(const QStringList &art_automatic_list) {
int biggest_size = 0;
QString biggest_path;
for (const QString &path : filtered) {
for (const QString &path : std::as_const(filtered)) {
if (stop_requested_ || abort_requested_) break;
QImage image(path);
@ -1195,7 +1201,7 @@ void CollectionWatcher::FullScanAsync() {
void CollectionWatcher::IncrementalScanCheck() {
qint64 duration = QDateTime::currentDateTime().toSecsSinceEpoch() - last_scan_time_;
qint64 duration = QDateTime::currentSecsSinceEpoch() - last_scan_time_;
if (duration >= 86400) {
qLog(Debug) << "Performing periodic incremental scan.";
IncrementalScanNow();
@ -1230,14 +1236,14 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt
quint64 files_count = FilesCountForSubdirs(&transaction, subdirs, subdir_files_count);
transaction.AddToProgressMax(files_count);
for (const CollectionSubdirectory &subdir : subdirs) {
for (const CollectionSubdirectory &subdir : std::as_const(subdirs)) {
if (stop_requested_ || abort_requested_) break;
ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
}
}
last_scan_time_ = QDateTime::currentDateTime().toSecsSinceEpoch();
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
emit CompilationsNeedUpdating();
@ -1306,10 +1312,10 @@ void CollectionWatcher::RescanSongs(const SongList &songs) {
QStringList scanned_paths;
for (const Song &song : songs) {
if (stop_requested_ || abort_requested_) break;
const QString song_path = song.url().toLocalFile().section('/', 0, -2);
const QString song_path = song.url().toLocalFile().section(QLatin1Char('/'), 0, -2);
if (scanned_paths.contains(song_path)) continue;
ScanTransaction transaction(this, song.directory_id(), false, true, mark_songs_unavailable_);
CollectionSubdirectoryList subdirs(transaction.GetAllSubdirs());
const CollectionSubdirectoryList subdirs = transaction.GetAllSubdirs();
for (const CollectionSubdirectory &subdir : subdirs) {
if (stop_requested_ || abort_requested_) break;
if (subdir.path != song_path) continue;

View File

@ -245,20 +245,21 @@ class CollectionWatcher : public QObject {
CueParser *cue_parser_;
static QStringList sValidImages;
static QStringList kIgnoredExtensions;
qint64 last_scan_time_;
};
inline QString CollectionWatcher::NoExtensionPart(const QString &fileName) {
return fileName.contains('.') ? fileName.section('.', 0, -2) : "";
return fileName.contains(QLatin1Char('.')) ? fileName.section(QLatin1Char('.'), 0, -2) : QLatin1String("");
}
// Thanks Amarok
inline QString CollectionWatcher::ExtensionPart(const QString &fileName) {
return fileName.contains( '.' ) ? fileName.mid( fileName.lastIndexOf('.') + 1 ).toLower() : "";
return fileName.contains(QLatin1Char('.')) ? fileName.mid(fileName.lastIndexOf(QLatin1Char('.')) + 1).toLower() : QLatin1String("");
}
inline QString CollectionWatcher::DirectoryPart(const QString &fileName) {
return fileName.section('/', 0, -2);
return fileName.section(QLatin1Char('/'), 0, -2);
}
#endif // COLLECTIONWATCHER_H

View File

@ -21,6 +21,8 @@
#include "config.h"
#include <utility>
#include <QDialog>
#include <QStandardItemModel>
#include <QItemSelectionModel>
@ -36,6 +38,7 @@
#include "core/logging.h"
#include "core/iconloader.h"
#include "core/settings.h"
#include "settings/collectionsettingspage.h"
#include "collectionmodel.h"
#include "savedgroupingmanager.h"
@ -56,7 +59,7 @@ SavedGroupingManager::SavedGroupingManager(const QString &saved_groupings_settin
model_->setHorizontalHeaderItem(2, new QStandardItem(tr("Second Level")));
model_->setHorizontalHeaderItem(3, new QStandardItem(tr("Third Level")));
ui_->list->setModel(model_);
ui_->remove->setIcon(IconLoader::Load("edit-delete"));
ui_->remove->setIcon(IconLoader::Load(QStringLiteral("edit-delete")));
ui_->remove->setEnabled(false);
ui_->remove->setShortcut(QKeySequence::Delete);
@ -72,80 +75,79 @@ SavedGroupingManager::~SavedGroupingManager() {
QString SavedGroupingManager::GetSavedGroupingsSettingsGroup(const QString &settings_group) {
if (settings_group.isEmpty() || settings_group == CollectionSettingsPage::kSettingsGroup) {
return kSavedGroupingsSettingsGroup;
}
else {
return QString(kSavedGroupingsSettingsGroup) + "_" + settings_group;
if (settings_group.isEmpty() || settings_group == QLatin1String(CollectionSettingsPage::kSettingsGroup)) {
return QLatin1String(kSavedGroupingsSettingsGroup);
}
return QLatin1String(kSavedGroupingsSettingsGroup) + QLatin1Char('_') + settings_group;
}
QString SavedGroupingManager::GroupByToString(const CollectionModel::GroupBy g) {
switch (g) {
case CollectionModel::GroupBy::None:
case CollectionModel::GroupBy::GroupByCount: {
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");
}
}
@ -157,13 +159,13 @@ QString SavedGroupingManager::GroupByToString(const CollectionModel::GroupBy g)
void SavedGroupingManager::UpdateModel() {
model_->setRowCount(0); // don't use clear, it deletes headers
QSettings s;
Settings s;
s.beginGroup(saved_groupings_settings_group_);
int version = s.value("version").toInt();
if (version == 1) {
QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) {
if (saved.at(i) == "version") continue;
if (saved.at(i) == QLatin1String("version")) continue;
QByteArray bytes = s.value(saved.at(i)).toByteArray();
QDataStream ds(&bytes, QIODevice::ReadOnly);
CollectionModel::Grouping g;
@ -181,7 +183,7 @@ void SavedGroupingManager::UpdateModel() {
else {
QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) {
if (saved.at(i) == "version") continue;
if (saved.at(i) == QLatin1String("version")) continue;
s.remove(saved.at(i));
}
}
@ -192,9 +194,10 @@ void SavedGroupingManager::UpdateModel() {
void SavedGroupingManager::Remove() {
if (ui_->list->selectionModel()->hasSelection()) {
QSettings s;
Settings s;
s.beginGroup(saved_groupings_settings_group_);
for (const QModelIndex &idx : ui_->list->selectionModel()->selectedRows()) {
const QModelIndexList indexes = ui_->list->selectionModel()->selectedRows();
for (const QModelIndex &idx : indexes) {
if (idx.isValid()) {
qLog(Debug) << "Remove saved grouping: " << model_->item(idx.row(), 0)->text();
s.remove(model_->item(idx.row(), 0)->text());

View File

@ -21,7 +21,6 @@
#cmakedefine HAVE_MUSICBRAINZ
#cmakedefine HAVE_GLOBALSHORTCUTS
#cmakedefine HAVE_X11_GLOBALSHORTCUTS
#cmakedefine HAVE_ICU
#cmakedefine USE_INSTALL_PREFIX
@ -30,6 +29,7 @@
#cmakedefine HAVE_SUBSONIC
#cmakedefine HAVE_TIDAL
#cmakedefine HAVE_SPOTIFY
#cmakedefine HAVE_QOBUZ
#cmakedefine HAVE_MOODBAR
@ -41,7 +41,6 @@
#cmakedefine HAVE_TAGLIB_DSDIFFFILE
#cmakedefine USE_BUNDLE
#define USE_BUNDLE_DIR "${USE_BUNDLE_DIR}"
#cmakedefine HAVE_TRANSLATIONS
#cmakedefine INSTALL_TRANSLATIONS

View File

@ -47,7 +47,9 @@
using std::make_unique;
using std::make_shared;
const int ContextAlbum::kFadeTimeLineMs = 1000;
namespace {
constexpr int kFadeTimeLineMs = 1000;
}
ContextAlbum::ContextAlbum(QWidget *parent)
: QWidget(parent),
@ -56,12 +58,12 @@ ContextAlbum::ContextAlbum(QWidget *parent)
album_cover_choice_controller_(nullptr),
downloading_covers_(false),
timeline_fade_(new QTimeLine(kFadeTimeLineMs, this)),
image_strawberry_(":/pictures/strawberry.png"),
image_strawberry_(QStringLiteral(":/pictures/strawberry.png")),
image_original_(image_strawberry_),
pixmap_current_opacity_(1.0),
desired_height_(width()) {
setObjectName("context-widget-album");
setObjectName(QStringLiteral("context-widget-album"));
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
@ -139,11 +141,7 @@ void ContextAlbum::UpdateWidth(const int new_width) {
}
void ContextAlbum::SetImage(QImage image) {
if (image.isNull()) {
image = image_strawberry_;
}
void ContextAlbum::SetImage(const QImage &image) {
if (downloading_covers_) {
downloading_covers_ = false;
@ -154,14 +152,20 @@ void ContextAlbum::SetImage(QImage image) {
QPixmap pixmap_previous = pixmap_current_;
qreal opacity_previous = pixmap_current_opacity_;
image_original_ = image;
if (image.isNull()) {
image_original_ = image_strawberry_;
}
else {
image_original_ = image;
}
pixmap_current_opacity_ = 0.0;
ScaleCover();
if (!pixmap_previous.isNull()) {
SharedPtr<PreviousCover> previous_cover = make_shared<PreviousCover>();
previous_cover->image = image_previous;
previous_cover->pixmap = pixmap_previous;
previous_cover->pixmap = pixmap_previous;
previous_cover->opacity = opacity_previous;
previous_cover->timeline.reset(new QTimeLine(kFadeTimeLineMs), [](QTimeLine *timeline) { timeline->deleteLater(); });
previous_cover->timeline->setDirection(QTimeLine::Backward);
@ -231,6 +235,7 @@ void ContextAlbum::FadePreviousCover(SharedPtr<PreviousCover> previous_cover) {
void ContextAlbum::FadePreviousCoverFinished(SharedPtr<PreviousCover> previous_cover) {
previous_cover->timeline.reset();
previous_covers_.removeAll(previous_cover);
}
@ -266,7 +271,7 @@ void ContextAlbum::SearchCoverInProgress() {
downloading_covers_ = true;
// Show a spinner animation
spinner_animation_ = make_unique<QMovie>(":/pictures/spinner.gif", QByteArray(), this);
spinner_animation_ = make_unique<QMovie>(QStringLiteral(":/pictures/spinner.gif"), QByteArray(), this);
QObject::connect(&*spinner_animation_, &QMovie::updated, this, &ContextAlbum::Update);
spinner_animation_->start();
update();

View File

@ -51,7 +51,7 @@ class ContextAlbum : public QWidget {
explicit ContextAlbum(QWidget *parent = nullptr);
void Init(ContextView *context_view, AlbumCoverChoiceController *album_cover_choice_controller);
void SetImage(QImage image = QImage());
void SetImage(const QImage &image = QImage());
void UpdateWidth(const int width);
protected:
@ -63,7 +63,7 @@ class ContextAlbum : public QWidget {
private:
struct PreviousCover {
PreviousCover() : opacity(0.0) {}
explicit PreviousCover() : opacity(0.0) {}
QImage image;
QPixmap pixmap;
qreal opacity;
@ -77,7 +77,6 @@ class ContextAlbum : public QWidget {
void DrawPreviousCovers(QPainter *p);
void ScaleCover();
void ScalePreviousCovers();
void GetCoverAutomatically();
signals:
void FadeStopFinished();
@ -87,15 +86,12 @@ class ContextAlbum : public QWidget {
void AutomaticCoverSearchDone();
void FadeCurrentCover(const qreal value);
void FadeCurrentCoverFinished();
void FadePreviousCover(SharedPtr<PreviousCover> previouscover);
void FadePreviousCoverFinished(SharedPtr<PreviousCover> previouscover);
void FadePreviousCover(SharedPtr<PreviousCover> previous_cover);
void FadePreviousCoverFinished(SharedPtr<PreviousCover> previous_cover);
public slots:
void SearchCoverInProgress();
private:
static const int kFadeTimeLineMs;
private:
QMenu *menu_;
ContextView *context_view_;

View File

@ -51,6 +51,7 @@
#include "core/application.h"
#include "core/player.h"
#include "core/song.h"
#include "core/settings.h"
#include "utilities/strutils.h"
#include "utilities/timeutils.h"
#include "widgets/resizabletextedit.h"
@ -112,25 +113,25 @@ ContextView::ContextView(QWidget *parent)
setLayout(layout_container_);
layout_container_->setObjectName("context-layout-container");
layout_container_->setObjectName(QStringLiteral("context-layout-container"));
layout_container_->setContentsMargins(0, 0, 0, 0);
layout_container_->addWidget(scrollarea_);
scrollarea_->setObjectName("context-scrollarea");
scrollarea_->setObjectName(QStringLiteral("context-scrollarea"));
scrollarea_->setWidgetResizable(true);
scrollarea_->setWidget(widget_scrollarea_);
scrollarea_->setContentsMargins(0, 0, 0, 0);
scrollarea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollarea_->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
widget_scrollarea_->setObjectName("context-widget-scrollarea");
widget_scrollarea_->setObjectName(QStringLiteral("context-widget-scrollarea"));
widget_scrollarea_->setLayout(layout_scrollarea_);
widget_scrollarea_->setContentsMargins(0, 0, 0, 0);
textedit_top_->setReadOnly(true);
textedit_top_->setFrameShape(QFrame::NoFrame);
layout_scrollarea_->setObjectName("context-layout-scrollarea");
layout_scrollarea_->setObjectName(QStringLiteral("context-layout-scrollarea"));
layout_scrollarea_->setContentsMargins(15, 15, 15, 15);
layout_scrollarea_->addWidget(textedit_top_);
layout_scrollarea_->addWidget(widget_album_);
@ -291,20 +292,20 @@ void ContextView::ReloadSettings() {
QString default_font;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
if (QFontDatabase::families().contains(ContextSettingsPage::kDefaultFontFamily)) {
if (QFontDatabase::families().contains(QLatin1String(ContextSettingsPage::kDefaultFontFamily))) {
#else
if (QFontDatabase().families().contains(ContextSettingsPage::kDefaultFontFamily)) {
if (QFontDatabase().families().contains(QLatin1String(ContextSettingsPage::kDefaultFontFamily))) {
#endif
default_font = ContextSettingsPage::kDefaultFontFamily;
default_font = QLatin1String(ContextSettingsPage::kDefaultFontFamily);
}
else {
default_font = font().family();
}
QSettings s;
Settings s;
s.beginGroup(ContextSettingsPage::kSettingsGroup);
title_fmt_ = s.value(ContextSettingsPage::kSettingsTitleFmt, "%title% - %artist%").toString();
summary_fmt_ = s.value(ContextSettingsPage::kSettingsSummaryFmt, "%album%").toString();
title_fmt_ = s.value(ContextSettingsPage::kSettingsTitleFmt, QStringLiteral("%title% - %artist%")).toString();
summary_fmt_ = s.value(ContextSettingsPage::kSettingsSummaryFmt, QStringLiteral("%album%")).toString();
action_show_album_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::ALBUM)], true).toBool());
action_show_data_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::TECHNICAL_DATA)], false).toBool());
action_show_lyrics_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::SONG_LYRICS)], true).toBool());
@ -390,7 +391,7 @@ void ContextView::FadeStopFinished() {
}
void ContextView::SetLabelText(QLabel *label, int value, const QString &suffix, const QString &def) {
label->setText(value <= 0 ? def : (QString::number(value) + " " + suffix));
label->setText(value <= 0 ? def : (QString::number(value) + QLatin1Char(' ') + suffix));
}
void ContextView::UpdateNoSong() {
@ -409,15 +410,15 @@ void ContextView::NoSong() {
QString html;
if (collectionview_->TotalSongs() == 1) html += tr("%1 song").arg(collectionview_->TotalSongs());
else html += tr("%1 songs").arg(collectionview_->TotalSongs());
html += "<br />";
html += QLatin1String("<br />");
if (collectionview_->TotalArtists() == 1) html += tr("%1 artist").arg(collectionview_->TotalArtists());
else html += tr("%1 artists").arg(collectionview_->TotalArtists());
html += "<br />";
html += QLatin1String("<br />");
if (collectionview_->TotalAlbums() == 1) html += tr("%1 album").arg(collectionview_->TotalAlbums());
else html += tr("%1 albums").arg(collectionview_->TotalAlbums());
html += "<br />";
html += QLatin1String("<br />");
label_stop_summary_->setFont(font_normal_);
label_stop_summary_->setText(html);
@ -438,7 +439,7 @@ void ContextView::UpdateFonts() {
void ContextView::SetSong() {
textedit_top_->setFont(font_headline_);
textedit_top_->SetText(QString("<b>%1</b><br />%2").arg(Utilities::ReplaceMessage(title_fmt_, song_playing_, "<br />", true), Utilities::ReplaceMessage(summary_fmt_, song_playing_, "<br />", true)));
textedit_top_->SetText(QStringLiteral("<b>%1</b><br />%2").arg(Utilities::ReplaceMessage(title_fmt_, song_playing_, QStringLiteral("<br />"), true), Utilities::ReplaceMessage(summary_fmt_, song_playing_, QStringLiteral("<br />"), true)));
label_stop_summary_->clear();
@ -474,7 +475,7 @@ void ContextView::SetSong() {
else {
label_samplerate_title_->show();
label_samplerate_->show();
SetLabelText(label_samplerate_, song_playing_.samplerate(), "Hz");
SetLabelText(label_samplerate_, song_playing_.samplerate(), QStringLiteral("Hz"));
}
if (song_playing_.bitdepth() <= 0) {
label_bitdepth_title_->hide();
@ -484,7 +485,7 @@ void ContextView::SetSong() {
else {
label_bitdepth_title_->show();
label_bitdepth_->show();
SetLabelText(label_bitdepth_, song_playing_.bitdepth(), "Bit");
SetLabelText(label_bitdepth_, song_playing_.bitdepth(), QStringLiteral("Bit"));
}
if (song_playing_.bitrate() <= 0) {
label_bitrate_title_->hide();
@ -546,7 +547,7 @@ void ContextView::SetSong() {
void ContextView::UpdateSong(const Song &song) {
textedit_top_->SetText(QString("<b>%1</b><br />%2").arg(Utilities::ReplaceMessage(title_fmt_, song, "<br />", true), Utilities::ReplaceMessage(summary_fmt_, song, "<br />", true)));
textedit_top_->SetText(QStringLiteral("<b>%1</b><br />%2").arg(Utilities::ReplaceMessage(title_fmt_, song, QStringLiteral("<br />"), true), Utilities::ReplaceMessage(summary_fmt_, song, QStringLiteral("<br />"), true)));
if (action_show_data_->isChecked()) {
if (song.filetype() != song_playing_.filetype()) label_filetype_->setText(song.TextForFiletype());
@ -571,7 +572,7 @@ void ContextView::UpdateSong(const Song &song) {
else {
label_samplerate_title_->show();
label_samplerate_->show();
SetLabelText(label_samplerate_, song.samplerate(), "Hz");
SetLabelText(label_samplerate_, song.samplerate(), QStringLiteral("Hz"));
}
}
if (song.bitdepth() != song_playing_.bitdepth()) {
@ -583,7 +584,7 @@ void ContextView::UpdateSong(const Song &song) {
else {
label_bitdepth_title_->show();
label_bitdepth_->show();
SetLabelText(label_bitdepth_, song.bitdepth(), "Bit");
SetLabelText(label_bitdepth_, song.bitdepth(), QStringLiteral("Bit"));
}
}
if (song.bitrate() != song_playing_.bitrate()) {
@ -631,7 +632,12 @@ void ContextView::UpdateLyrics(const quint64 id, const QString &provider, const
if (static_cast<qint64>(id) != lyrics_id_) return;
lyrics_ = lyrics + "\n\n(Lyrics from " + provider + ")\n";
if (lyrics.isEmpty()) {
lyrics_ = QLatin1String("No lyrics found.\n");
}
else {
lyrics_ = lyrics + QLatin1String("\n\n(Lyrics from ") + provider + QLatin1String(")\n");
}
lyrics_id_ = -1;
if (action_show_lyrics_->isChecked() && !lyrics_.isEmpty()) {
@ -687,7 +693,7 @@ void ContextView::AlbumCoverLoaded(const Song &song, const QImage &image) {
void ContextView::ActionShowAlbum() {
QSettings s;
Settings s;
s.beginGroup(ContextSettingsPage::kSettingsGroup);
s.setValue(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::ALBUM)], action_show_album_->isChecked());
s.endGroup();
@ -697,7 +703,7 @@ void ContextView::ActionShowAlbum() {
void ContextView::ActionShowData() {
QSettings s;
Settings s;
s.beginGroup(ContextSettingsPage::kSettingsGroup);
s.setValue(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::TECHNICAL_DATA)], action_show_data_->isChecked());
s.endGroup();
@ -707,7 +713,7 @@ void ContextView::ActionShowData() {
void ContextView::ActionShowLyrics() {
QSettings s;
Settings s;
s.beginGroup(ContextSettingsPage::kSettingsGroup);
s.setValue(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::SONG_LYRICS)], action_show_lyrics_->isChecked());
s.endGroup();
@ -720,7 +726,7 @@ void ContextView::ActionShowLyrics() {
void ContextView::ActionSearchLyrics() {
QSettings s;
Settings s;
s.beginGroup(ContextSettingsPage::kSettingsGroup);
s.setValue(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::SEARCH_LYRICS)], action_search_lyrics_->isChecked());
s.endGroup();

View File

@ -56,7 +56,7 @@
#include "covermanager/musicbrainzcoverprovider.h"
#include "covermanager/deezercoverprovider.h"
#include "covermanager/musixmatchcoverprovider.h"
#include "covermanager/spotifycoverprovider.h"
#include "covermanager/opentidalcoverprovider.h"
#include "lyrics/lyricsproviders.h"
#include "lyrics/geniuslyricsprovider.h"
@ -67,7 +67,7 @@
#include "lyrics/songlyricscomlyricsprovider.h"
#include "lyrics/azlyricscomlyricsprovider.h"
#include "lyrics/elyricsnetlyricsprovider.h"
#include "lyrics/lyricsmodecomlyricsprovider.h"
#include "lyrics/letraslyricsprovider.h"
#include "scrobbler/audioscrobbler.h"
#include "scrobbler/lastfmscrobbler.h"
@ -78,7 +78,7 @@
# include "scrobbler/subsonicscrobbler.h"
#endif
#include "internet/internetservices.h"
#include "streaming/streamingservices.h"
#ifdef HAVE_SUBSONIC
# include "subsonic/subsonicservice.h"
@ -89,6 +89,11 @@
# include "covermanager/tidalcoverprovider.h"
#endif
#ifdef HAVE_SPOTIFY
# include "spotify/spotifyservice.h"
# include "covermanager/spotifycoverprovider.h"
#endif
#ifdef HAVE_QOBUZ
# include "qobuz/qobuzservice.h"
# include "covermanager/qobuzcoverprovider.h"
@ -142,10 +147,13 @@ class ApplicationImpl {
cover_providers->AddProvider(new DiscogsCoverProvider(app, app->network()));
cover_providers->AddProvider(new DeezerCoverProvider(app, app->network()));
cover_providers->AddProvider(new MusixmatchCoverProvider(app, app->network()));
cover_providers->AddProvider(new SpotifyCoverProvider(app, app->network()));
cover_providers->AddProvider(new OpenTidalCoverProvider(app, app->network()));
#ifdef HAVE_TIDAL
cover_providers->AddProvider(new TidalCoverProvider(app, app->network()));
#endif
#ifdef HAVE_SPOTIFY
cover_providers->AddProvider(new SpotifyCoverProvider(app, app->network()));
#endif
#ifdef HAVE_QOBUZ
cover_providers->AddProvider(new QobuzCoverProvider(app, app->network()));
#endif
@ -169,22 +177,25 @@ class ApplicationImpl {
lyrics_providers->AddProvider(new SongLyricsComLyricsProvider(app->network()));
lyrics_providers->AddProvider(new AzLyricsComLyricsProvider(app->network()));
lyrics_providers->AddProvider(new ElyricsNetLyricsProvider(app->network()));
lyrics_providers->AddProvider(new LyricsModeComLyricsProvider(app->network()));
lyrics_providers->AddProvider(new LetrasLyricsProvider(app->network()));
lyrics_providers->ReloadSettings();
return lyrics_providers;
}),
internet_services_([app]() {
InternetServices *internet_services = new InternetServices();
streaming_services_([app]() {
StreamingServices *streaming_services = new StreamingServices();
#ifdef HAVE_SUBSONIC
internet_services->AddService(make_shared<SubsonicService>(app));
streaming_services->AddService(make_shared<SubsonicService>(app));
#endif
#ifdef HAVE_TIDAL
internet_services->AddService(make_shared<TidalService>(app));
streaming_services->AddService(make_shared<TidalService>(app));
#endif
#ifdef HAVE_SPOTIFY
streaming_services->AddService(make_shared<SpotifyService>(app));
#endif
#ifdef HAVE_QOBUZ
internet_services->AddService(make_shared<QobuzService>(app));
streaming_services->AddService(make_shared<QobuzService>(app));
#endif
return internet_services;
return streaming_services;
}),
radio_services_([app]() { return new RadioServices(app); }),
scrobbler_([app]() {
@ -220,7 +231,7 @@ class ApplicationImpl {
Lazy<AlbumCoverLoader> album_cover_loader_;
Lazy<CurrentAlbumCoverLoader> current_albumcover_loader_;
Lazy<LyricsProviders> lyrics_providers_;
Lazy<InternetServices> internet_services_;
Lazy<StreamingServices> streaming_services_;
Lazy<RadioServices> radio_services_;
Lazy<AudioScrobbler> scrobbler_;
#ifdef HAVE_MOODBAR
@ -285,7 +296,7 @@ void Application::Exit() {
#ifndef Q_OS_WIN
<< &*device_manager()
#endif
<< &*internet_services()
<< &*streaming_services()
<< &*radio_services()->radio_backend();
QObject::connect(&*tag_reader_client(), &TagReaderClient::ExitFinished, this, &Application::ExitReceived);
@ -305,8 +316,8 @@ void Application::Exit() {
device_manager()->Exit();
#endif
QObject::connect(&*internet_services(), &InternetServices::ExitFinished, this, &Application::ExitReceived);
internet_services()->Exit();
QObject::connect(&*streaming_services(), &StreamingServices::ExitFinished, this, &Application::ExitReceived);
streaming_services()->Exit();
QObject::connect(&*radio_services()->radio_backend(), &RadioBackend::ExitFinished, this, &Application::ExitReceived);
radio_services()->radio_backend()->ExitAsync();
@ -351,7 +362,7 @@ SharedPtr<CurrentAlbumCoverLoader> Application::current_albumcover_loader() cons
SharedPtr<LyricsProviders> Application::lyrics_providers() const { return p_->lyrics_providers_.ptr(); }
SharedPtr<PlaylistBackend> Application::playlist_backend() const { return p_->playlist_backend_.ptr(); }
SharedPtr<PlaylistManager> Application::playlist_manager() const { return p_->playlist_manager_.ptr(); }
SharedPtr<InternetServices> Application::internet_services() const { return p_->internet_services_.ptr(); }
SharedPtr<StreamingServices> Application::streaming_services() const { return p_->streaming_services_.ptr(); }
SharedPtr<RadioServices> Application::radio_services() const { return p_->radio_services_.ptr(); }
SharedPtr<AudioScrobbler> Application::scrobbler() const { return p_->scrobbler_.ptr(); }
SharedPtr<LastFMImport> Application::lastfm_import() const { return p_->lastfm_import_.ptr(); }

View File

@ -58,7 +58,7 @@ class CoverProviders;
class LyricsProviders;
class AudioScrobbler;
class LastFMImport;
class InternetServices;
class StreamingServices;
class RadioServices;
#ifdef HAVE_MOODBAR
class MoodbarController;
@ -97,7 +97,7 @@ class Application : public QObject {
SharedPtr<AudioScrobbler> scrobbler() const;
SharedPtr<InternetServices> internet_services() const;
SharedPtr<StreamingServices> streaming_services() const;
SharedPtr<RadioServices> radio_services() const;
#ifdef HAVE_MOODBAR

View File

@ -34,6 +34,7 @@
#include <QIODevice>
#include <QDataStream>
#include <QBuffer>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QByteArray>
@ -100,7 +101,7 @@ CommandlineOptions::CommandlineOptions(int argc, char **argv)
play_track_at_(-1),
show_osd_(false),
toggle_pretty_osd_(false),
log_levels_(logging::kDefaultLogLevels) {
log_levels_(QLatin1String(logging::kDefaultLogLevels)) {
#ifdef Q_OS_WIN32
Q_UNUSED(argv);
@ -108,11 +109,11 @@ CommandlineOptions::CommandlineOptions(int argc, char **argv)
#ifdef Q_OS_MACOS
// Remove -psn_xxx option that Mac passes when opened from Finder.
RemoveArg("-psn", 1);
RemoveArg(QStringLiteral("-psn"), 1);
#endif
// Remove the -session option that KDE passes
RemoveArg("-session", 2);
RemoveArg(QStringLiteral("-session"), 2);
}
@ -212,9 +213,9 @@ bool CommandlineOptions::Parse() {
if (c == -1) break;
switch (c) {
case 'h': {
case 'h':{
QString translated_help_text =
QString(kHelpText)
QString::fromUtf8(kHelpText)
.arg(QObject::tr("Usage"), QObject::tr("options"), QObject::tr("URL(s)"),
QObject::tr("Player options"),
QObject::tr("Start the playlist currently playing"),
@ -301,16 +302,16 @@ bool CommandlineOptions::Parse() {
volume_modifier_ = -4;
break;
case LongOptions::Quiet:
log_levels_ = "1";
log_levels_ = QStringLiteral("1");
break;
case LongOptions::Verbose:
log_levels_ = "3";
log_levels_ = QStringLiteral("3");
break;
case LongOptions::LogLevels:
log_levels_ = OptArgToString(optarg);
break;
case LongOptions::Version: {
QString version_text = QString(kVersionText).arg(STRAWBERRY_VERSION_DISPLAY);
case LongOptions::Version:{
QString version_text = QString::fromUtf8(kVersionText).arg(QLatin1String(STRAWBERRY_VERSION_DISPLAY));
std::cout << version_text.toLocal8Bit().constData() << std::endl;
std::exit(0);
}
@ -364,7 +365,7 @@ bool CommandlineOptions::Parse() {
const QString value = DecodeName(argv_[i]);
QFileInfo fileinfo(value);
if (fileinfo.exists()) {
urls_ << QUrl::fromLocalFile(fileinfo.canonicalFilePath());
urls_ << QUrl::fromLocalFile(QDir::cleanPath(fileinfo.filePath()));
}
else {
urls_ << QUrl::fromUserInput(value);
@ -416,7 +417,7 @@ void CommandlineOptions::Load(const QByteArray &serialized) {
}
#ifdef Q_OS_WIN32
QString CommandlineOptions::OptArgToString(wchar_t *opt) {
QString CommandlineOptions::OptArgToString(const wchar_t *opt) {
return QString::fromWCharArray(opt);
@ -427,9 +428,9 @@ QString CommandlineOptions::DecodeName(wchar_t *opt) {
return QString::fromWCharArray(opt);
}
#else
QString CommandlineOptions::OptArgToString(char *opt) {
QString CommandlineOptions::OptArgToString(const char *opt) {
return QString(opt);
return QString::fromUtf8(opt);
}
QString CommandlineOptions::DecodeName(char *opt) {

View File

@ -108,10 +108,10 @@ class CommandlineOptions {
void RemoveArg(const QString &starts_with, int count);
#ifdef Q_OS_WIN32
static QString OptArgToString(wchar_t *opt);
static QString OptArgToString(const wchar_t *opt);
static QString DecodeName(wchar_t *opt);
#else
static QString OptArgToString(char *opt);
static QString OptArgToString(const char *opt);
static QString DecodeName(char *opt);
#endif

View File

@ -21,6 +21,8 @@
#include "config.h"
#include <utility>
#include <sqlite3.h>
#include <QObject>
@ -47,10 +49,13 @@
#include "sqlquery.h"
#include "scopedtransaction.h"
const char *Database::kDatabaseFilename = "strawberry.db";
const int Database::kSchemaVersion = 18;
const int Database::kMinSupportedSchemaVersion = 10;
const char *Database::kMagicAllSongsTables = "%allsongstables";
const int Database::kSchemaVersion = 20;
namespace {
constexpr char kDatabaseFilename[] = "strawberry.db";
constexpr int kMinSupportedSchemaVersion = 10;
constexpr char kMagicAllSongsTables[] = "%allsongstables";
} // namespace
int Database::sNextConnectionId = 1;
QMutex Database::sNextConnectionIdMutex;
@ -84,7 +89,8 @@ Database::~Database() {
QMutexLocker l(&connect_mutex_);
for (const QString &connection_id : QSqlDatabase::connectionNames()) {
const QStringList connection_names = QSqlDatabase::connectionNames();
for (const QString &connection_id : connection_names) {
qLog(Error) << "Connection" << connection_id << "is still open!";
}
@ -114,7 +120,7 @@ QSqlDatabase Database::Connect() {
}
}
const QString connection_id = QString("%1_thread_%2").arg(connection_id_).arg(reinterpret_cast<quint64>(QThread::currentThread()));
const QString connection_id = QStringLiteral("%1_thread_%2").arg(connection_id_).arg(reinterpret_cast<quint64>(QThread::currentThread()));
// Try to find an existing connection for this thread
QSqlDatabase db;
@ -122,23 +128,23 @@ QSqlDatabase Database::Connect() {
db = QSqlDatabase::database(connection_id);
}
else {
db = QSqlDatabase::addDatabase("QSQLITE", connection_id);
db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), connection_id);
}
if (db.isOpen()) {
return db;
}
db.setConnectOptions("QSQLITE_BUSY_TIMEOUT=30000");
db.setConnectOptions(QStringLiteral("QSQLITE_BUSY_TIMEOUT=30000"));
//qLog(Debug) << "Opened database with connection id" << connection_id;
if (injected_database_name_.isNull()) {
db.setDatabaseName(directory_ + "/" + kDatabaseFilename);
db.setDatabaseName(directory_ + QLatin1Char('/') + QLatin1String(kDatabaseFilename));
}
else {
db.setDatabaseName(injected_database_name_);
}
if (!db.open()) {
app_->AddError("Database: " + db.lastError().text());
app_->AddError(QStringLiteral("Database: ") + db.lastError().text());
return db;
}
@ -154,16 +160,16 @@ QSqlDatabase Database::Connect() {
// Attach external databases
QStringList keys = attached_databases_.keys();
for (const QString &key : keys) {
for (const QString &key : std::as_const(keys)) {
QString filename = attached_databases_[key].filename_;
if (!injected_database_name_.isNull()) filename = injected_database_name_;
// Attach the db
SqlQuery q(db);
q.prepare("ATTACH DATABASE :filename AS :alias");
q.BindValue(":filename", filename);
q.BindValue(":alias", key);
q.prepare(QStringLiteral("ATTACH DATABASE :filename AS :alias"));
q.BindValue(QStringLiteral(":filename"), filename);
q.BindValue(QStringLiteral(":alias"), key);
if (!q.Exec()) {
qFatal("Couldn't attach external database '%s'", key.toLatin1().constData());
}
@ -175,13 +181,13 @@ QSqlDatabase Database::Connect() {
// We might have to initialize the schema in some attached databases now, if they were deleted and don't match up with the main schema version.
keys = attached_databases_.keys();
for (const QString &key : keys) {
for (const QString &key : std::as_const(keys)) {
if (attached_databases_[key].is_temporary_ && attached_databases_[key].schema_.isEmpty()) {
continue;
}
// Find out if there are any tables in this database
SqlQuery q(db);
q.prepare(QString("SELECT ROWID FROM %1.sqlite_master WHERE type='table'").arg(key));
q.prepare(QStringLiteral("SELECT ROWID FROM %1.sqlite_master WHERE type='table'").arg(key));
if (!q.Exec() || !q.next()) {
q.finish();
ExecSchemaCommandsFromFile(db, attached_databases_[key].schema_, 0);
@ -196,7 +202,7 @@ void Database::Close() {
QMutexLocker l(&connect_mutex_);
const QString connection_id = QString("%1_thread_%2").arg(connection_id_).arg(reinterpret_cast<quint64>(QThread::currentThread()));
const QString connection_id = QStringLiteral("%1_thread_%2").arg(connection_id_).arg(reinterpret_cast<quint64>(QThread::currentThread()));
// Try to find an existing connection for this thread
if (QSqlDatabase::connectionNames().contains(connection_id)) {
@ -218,7 +224,7 @@ int Database::SchemaVersion(QSqlDatabase *db) {
int schema_version = 0;
{
SqlQuery q(*db);
q.prepare("SELECT version FROM schema_version");
q.prepare(QStringLiteral("SELECT version FROM schema_version"));
if (q.Exec() && q.next()) {
schema_version = q.value(0).toInt();
}
@ -259,8 +265,8 @@ void Database::RecreateAttachedDb(const QString &database_name) {
QSqlDatabase db(Connect());
SqlQuery q(db);
q.prepare("DETACH DATABASE :alias");
q.BindValue(":alias", database_name);
q.prepare(QStringLiteral("DETACH DATABASE :alias"));
q.BindValue(QStringLiteral(":alias"), database_name);
if (!q.Exec()) {
qLog(Warning) << "Failed to detach database" << database_name;
return;
@ -273,7 +279,8 @@ void Database::RecreateAttachedDb(const QString &database_name) {
// We can't just re-attach the database now because it needs to be done for each thread.
// Close all the database connections, so each thread will re-attach it when they next connect.
for (const QString &name : QSqlDatabase::connectionNames()) {
const QStringList connection_names = QSqlDatabase::connectionNames();
for (const QString &name : connection_names) {
QSqlDatabase::removeDatabase(name);
}
@ -289,9 +296,9 @@ void Database::AttachDatabaseOnDbConnection(const QString &database_name, const
// Attach the db
SqlQuery q(db);
q.prepare("ATTACH DATABASE :filename AS :alias");
q.BindValue(":filename", database.filename_);
q.BindValue(":alias", database_name);
q.prepare(QStringLiteral("ATTACH DATABASE :filename AS :alias"));
q.BindValue(QStringLiteral(":filename"), database.filename_);
q.BindValue(QStringLiteral(":alias"), database_name);
if (!q.Exec()) {
qFatal("Couldn't attach external database '%s'", database_name.toLatin1().constData());
}
@ -305,8 +312,8 @@ void Database::DetachDatabase(const QString &database_name) {
QSqlDatabase db(Connect());
SqlQuery q(db);
q.prepare("DETACH DATABASE :alias");
q.BindValue(":alias", database_name);
q.prepare(QStringLiteral("DETACH DATABASE :alias"));
q.BindValue(QStringLiteral(":alias"), database_name);
if (!q.Exec()) {
qLog(Warning) << "Failed to detach database" << database_name;
return;
@ -321,10 +328,10 @@ void Database::UpdateDatabaseSchema(int version, QSqlDatabase &db) {
QString filename;
if (version == 0) {
filename = ":/schema/schema.sql";
filename = QStringLiteral(":/schema/schema.sql");
}
else {
filename = QString(":/schema/schema-%1.sql").arg(version);
filename = QStringLiteral(":/schema/schema-%1.sql").arg(version);
qLog(Debug) << "Applying database schema update" << version << "from" << filename;
}
@ -335,9 +342,9 @@ void Database::UpdateDatabaseSchema(int version, QSqlDatabase &db) {
void Database::UrlEncodeFilenameColumn(const QString &table, QSqlDatabase &db) {
SqlQuery select(db);
select.prepare(QString("SELECT ROWID, filename FROM %1").arg(table));
select.prepare(QStringLiteral("SELECT ROWID, filename FROM %1").arg(table));
SqlQuery update(db);
update.prepare(QString("UPDATE %1 SET filename=:filename WHERE ROWID=:id").arg(table));
update.prepare(QStringLiteral("UPDATE %1 SET filename=:filename WHERE ROWID=:id").arg(table));
if (!select.Exec()) {
ReportErrors(select);
}
@ -346,14 +353,14 @@ void Database::UrlEncodeFilenameColumn(const QString &table, QSqlDatabase &db) {
const int rowid = select.value(0).toInt();
const QString filename = select.value(1).toString();
if (filename.isEmpty() || filename.contains("://")) {
if (filename.isEmpty() || filename.contains(QLatin1String("://"))) {
continue;
}
const QUrl url = QUrl::fromLocalFile(filename);
update.BindValue(":filename", url.toEncoded());
update.BindValue(":id", rowid);
update.BindValue(QStringLiteral(":filename"), url.toEncoded());
update.BindValue(QStringLiteral(":id"), rowid);
if (!update.Exec()) {
ReportErrors(update);
}
@ -370,8 +377,8 @@ void Database::ExecSchemaCommandsFromFile(QSqlDatabase &db, const QString &filen
}
QByteArray data = schema_file.readAll();
QString schema = QString::fromUtf8(data);
if (schema.contains("\r\n")) {
schema = schema.replace("\r\n", "\n");
if (schema.contains(QLatin1String("\r\n"))) {
schema = schema.replace(QLatin1String("\r\n"), QLatin1String("\n"));
}
schema_file.close();
ExecSchemaCommands(db, schema, schema_version, in_transaction);
@ -381,8 +388,7 @@ void Database::ExecSchemaCommandsFromFile(QSqlDatabase &db, const QString &filen
void Database::ExecSchemaCommands(QSqlDatabase &db, const QString &schema, int schema_version, bool in_transaction) {
// Run each command
QStringList commands;
commands = schema.split(QRegularExpression("; *\n\n"));
QStringList commands = schema.split(QRegularExpression(QStringLiteral("; *\n\n")));
// We don't want this list to reflect possible DB schema changes, so we initialize it before executing any statements.
// If no outer transaction is provided the song tables need to be queried before beginning an inner transaction!
@ -405,16 +411,16 @@ void Database::ExecSongTablesCommands(QSqlDatabase &db, const QStringList &song_
for (const QString &command : commands) {
// There are now lots of "songs" tables that need to have the same schema: songs and device_*_songs.
// We allow a magic value in the schema files to update all songs tables at once.
if (command.contains(kMagicAllSongsTables)) {
if (command.contains(QLatin1String(kMagicAllSongsTables))) {
for (const QString &table : song_tables) {
// Another horrible hack: device songs tables don't have matching _fts tables, so if this command tries to touch one, ignore it.
if (table.startsWith("device_") && command.contains(QString(kMagicAllSongsTables) + "_fts")) {
if (table.startsWith(QLatin1String("device_")) && command.contains(QLatin1String(kMagicAllSongsTables) + QLatin1String("_fts"))) {
continue;
}
qLog(Info) << "Updating" << table << "for" << kMagicAllSongsTables;
QString new_command(command);
new_command.replace(kMagicAllSongsTables, table);
new_command.replace(QLatin1String(kMagicAllSongsTables), table);
SqlQuery query(db);
query.prepare(new_command);
if (!query.Exec()) {
@ -442,18 +448,19 @@ QStringList Database::SongsTables(QSqlDatabase &db, const int schema_version) {
QStringList ret;
// look for the tables in the main db
for (const QString &table : db.tables()) {
if (table == "songs" || table.endsWith("_songs")) ret << table;
const QStringList &tables = db.tables();
for (const QString &table : tables) {
if (table == QLatin1String("songs") || table.endsWith(QLatin1String("_songs"))) ret << table;
}
// look for the tables in attached dbs
QStringList keys = attached_databases_.keys();
const QStringList keys = attached_databases_.keys();
for (const QString &key : keys) {
SqlQuery q(db);
q.prepare(QString("SELECT NAME FROM %1.sqlite_master WHERE type='table' AND name='songs' OR name LIKE '%songs'").arg(key));
q.prepare(QStringLiteral("SELECT NAME FROM %1.sqlite_master WHERE type='table' AND name='songs' OR name LIKE '%songs'").arg(key));
if (q.Exec()) {
while (q.next()) {
QString tab_name = key + "." + q.value(0).toString();
QString tab_name = key + QLatin1Char('.') + q.value(0).toString();
ret << tab_name;
}
}
@ -462,7 +469,7 @@ QStringList Database::SongsTables(QSqlDatabase &db, const int schema_version) {
}
}
ret << "playlist_items";
ret << QStringLiteral("playlist_items");
return ret;
@ -488,20 +495,20 @@ bool Database::IntegrityCheck(const QSqlDatabase &db) {
bool ok = false;
// Ask for 10 error messages at most.
SqlQuery q(db);
q.prepare("PRAGMA integrity_check(10)");
q.prepare(QStringLiteral("PRAGMA integrity_check(10)"));
if (q.Exec()) {
bool error_reported = false;
while (q.next()) {
QString message = q.value(0).toString();
// If no errors are found, a single row with the value "ok" is returned
if (message == "ok") {
if (message == QLatin1String("ok")) {
ok = true;
break;
}
else {
if (!error_reported) { app_->AddError(tr("Database corruption detected.")); }
app_->AddError("Database: " + message);
app_->AddError(QStringLiteral("Database: ") + message);
error_reported = true;
}
}
@ -553,7 +560,7 @@ bool Database::OpenDatabase(const QString &filename, sqlite3 **connection) {
void Database::BackupFile(const QString &filename) {
qLog(Debug) << "Starting database backup";
QString dest_filename = QString("%1.bak").arg(filename);
QString dest_filename = QStringLiteral("%1.bak").arg(filename);
const int task_id = app_->task_manager()->StartTask(tr("Backing up database"));
sqlite3 *source_connection = nullptr;

View File

@ -50,6 +50,8 @@ class Database : public QObject {
explicit Database(Application *app, QObject *parent = nullptr, const QString &database_name = QString());
~Database() override;
static const int kSchemaVersion;
struct AttachedDatabase {
AttachedDatabase() {}
AttachedDatabase(const QString &filename, const QString &schema, bool is_temporary)
@ -60,11 +62,6 @@ class Database : public QObject {
bool is_temporary_;
};
static const int kSchemaVersion;
static const int kMinSupportedSchemaVersion;
static const char *kDatabaseFilename;
static const char *kMagicAllSongsTables;
void ExitAsync();
QSqlDatabase Connect();
void Close();
@ -148,7 +145,7 @@ class MemoryDatabase : public Database {
public:
explicit MemoryDatabase(Application *app, QObject *parent = nullptr)
: Database(app, parent, ":memory:") {}
: Database(app, parent, QStringLiteral(":memory:")) {}
~MemoryDatabase() override {
// Make sure Qt doesn't reuse the same database
QSqlDatabase::removeDatabase(Connect().connectionName());

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