Compare commits

..

137 Commits

Author SHA1 Message Date
Jonas Kvinge
c658a77b05 Release 1.2.13 2025-08-31 22:33:48 +02:00
Jonas Kvinge
1880dc8153 Update Changelog 2025-08-31 22:27:00 +02:00
7xnl
b5fd3d5717 Add settings customize Discord status text
The new settings let you customize the "Listening to" status text, according to the [status display types](https://discord.com/developers/docs/events/gateway-events#activity-object).

Fixes #1796.
2025-08-31 22:11:59 +02:00
Jonas Kvinge
3c3480fb84 SystemTrayIcon: Respect device aspect ratio
Fixes #1782
2025-08-31 02:34:13 +02:00
Jonas Kvinge
f628914173 MainWindow: Rename systemtrayicon 2025-08-31 00:37:09 +02:00
Jonas Kvinge
c100fb1bb8 TagReaderTagLib: Fallback to "Other" cover type
Fixes #1793
2025-08-31 00:20:00 +02:00
Jonas Kvinge
8c804c4fba Refactor CDDA loading signal/slots
Fixes #1803
2025-08-31 00:01:55 +02:00
Jonas Kvinge
912a7c7da9 MusicBrainzClient: Fix typo 2025-08-30 23:55:27 +02:00
Jonas Kvinge
0a5815c82e StyleSheetLoader: Set alpha on other platforms than macOS
Fixes #1806
2025-08-26 22:48:58 +02:00
Jonas Kvinge
6513b3032b CMake: Check additional names for getopt 2025-08-24 22:36:13 +02:00
Jonas Kvinge
8c51401bdc MacOsDeviceLister: Fix build without MTP
Fixes #1804
2025-08-24 01:28:22 +02:00
dependabot[bot]
45fc9c83d4 Bump vmactions/openbsd-vm from 1.1.8 to 1.2.0
Bumps [vmactions/openbsd-vm](https://github.com/vmactions/openbsd-vm) from 1.1.8 to 1.2.0.
- [Release notes](https://github.com/vmactions/openbsd-vm/releases)
- [Commits](https://github.com/vmactions/openbsd-vm/compare/v1.1.8...v1.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-20 23:57:47 +02:00
dependabot[bot]
be57d8147a Bump vmactions/freebsd-vm from 1.2.1 to 1.2.3
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.2.1 to 1.2.3.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.2.1...v1.2.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-19 18:25:13 +02:00
Jonas Kvinge
a97908fb6b CI: Bump msvc sdk 2025-08-17 13:44:57 +02:00
Lars Wendler
c0417d4bb3 cdda: fix build without musicbrainz
With -DENABLE_MUSICBRAINZ=NO the following build error occurs since 1.2.12:

/var/tmp/portage/media-sound/strawberry-1.2.12_pre/work/strawberry-1.2.12/src/de
vice/cddasongloader.cpp:58:91: error: ‘LoadMusicBrainzCDTags’ is not a member of
 ‘CDDASongLoader’
   58 |   QObject::connect(this, &CDDASongLoader::MusicBrainzDiscIdLoaded, this,
 &CDDASongLoader::LoadMusicBrainzCDTags);
      |
                  ^~~~~~~~~~~~~~~~~~~~~
2025-08-13 19:49:41 +02:00
Jonas Kvinge
062e2cfb84 Turn on git revision 2025-08-13 00:20:05 +02:00
Jonas Kvinge
700f7dbe36 Release 1.2.12 2025-08-12 22:57:10 +02:00
Jonas Kvinge
0487118dad Update Changelog 2025-08-12 22:54:54 +02:00
Jonas Kvinge
f3d088e48b Rename sort functions 2025-08-12 22:14:22 +02:00
Jonas Kvinge
f8afd49fcf Update Changelog 2025-08-12 01:46:46 +02:00
dependabot[bot]
363fcb5aba Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-11 22:52:06 +02:00
Jonas Kvinge
183aba4181 main: Workaround crash on exit on mingw with win32 threads 2025-08-10 22:41:38 +02:00
Jonas Kvinge
742be01aa6 nsi: Add /norestart to vc redist install 2025-08-10 18:34:12 +02:00
Jonas Kvinge
38c8054873 nsi: Only include gstwinrt-1.0-0.dll on arm64 2025-08-10 02:13:44 +02:00
Jonas Kvinge
da9e9840b8 Add BPM, mood and initial key support 2025-08-10 01:34:44 +02:00
Jonas Kvinge
c4646531b0 Refactor use of sort tags 2025-08-10 00:11:28 +02:00
dependabot[bot]
65d9b6a9e9 Bump actions/download-artifact from 4 to 5
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-06 23:31:07 +02:00
Jonas Kvinge
046f40fbca CollectionModel: Remove const reference on SortBehaviour enum 2025-08-04 22:54:18 +02:00
Strawberry Bot
a0ca50ac30 New translations 2025-08-04 22:43:33 +02:00
Jonas Kvinge
d939733675 CI: Remove Ubuntu oracular 2025-08-04 22:42:13 +02:00
Mark
61a8a3a84a SmartPlaylists: Add sort fields 2025-08-04 22:24:50 +02:00
Mark
d4858a338c Propose collection rescan on upgrade 2025-08-04 22:24:21 +02:00
Mark
31380a5bd4 CDDASongLoader: Add sort tags 2025-08-04 22:24:12 +02:00
Mark
e45b9aabeb Add sort tags to context view 2025-08-04 22:23:52 +02:00
Mark
27e782d8cf Allow editing new sort tags 2025-08-04 22:23:33 +02:00
Mark
0bfc2ee198 Add sort columns to playlists
Increment playlist state version from 1 to 2 to get sort columns next to
their "original" column. Discard old stored playlist state in config file.
2025-08-04 22:23:19 +02:00
Mark
e7fc4b1706 Collection: Use sort tags and add sort behaviour 2025-08-04 22:21:49 +02:00
Mark
6dea1a2149 Add support for sort tags 2025-08-04 22:21:33 +02:00
Jonas Kvinge
7844a2b932 Update Spotify access token
Fixes #1769
2025-08-04 22:11:44 +02:00
Jonas Kvinge
96a53bfbe5 SavedGroupingManager: Fix removing saved grouping 2025-07-30 00:47:54 +02:00
Jonas Kvinge
fe5fbae4b4 Use percent encoding for saved groupings
Fixes #1758
2025-07-30 00:41:12 +02:00
Jonas Kvinge
a9140232e5 Add workaround for QTBUG-135641
Fixes #1594
2025-07-29 23:42:38 +02:00
Jonas Kvinge
835090dd96 RichPresence: Disable Discord desktop file creation
Fixes #1771
2025-07-29 00:45:36 +02:00
Jonas Kvinge
af5590dcb1 NetworkAccessManager: Fix setting prefer cache setting 2025-07-28 22:40:26 +02:00
Jonas Kvinge
26b5588d7d NetworkAccessManager: Rename variables 2025-07-28 22:39:32 +02:00
Jonas Kvinge
390bf049f2 Don't set window icon on Wayland
Fixes #1753
2025-07-27 14:40:01 +02:00
Jonas Kvinge
321272b695 MainWindow: Remove hard-coded icon 2025-07-27 14:39:22 +02:00
Jonas Kvinge
342805e0f3 MainWindow: Automatically added UI changes 2025-07-27 14:39:05 +02:00
Jonas Kvinge
e614626913 TidalStreamURLRequest: Fix parsing manifest urls 2025-07-19 21:38:55 +02:00
Mark
2ddacf2f98 Database: Add *sort fields, bpm, mood, initial_key
Upgrade from schema version 20 to 21. This includes:

- six fields for sort tags
- new fields bpm, mood, initial_key

See https://github.com/strawberrymusicplayer/strawberry/pull/1779#pullrequestreview-3003042802
2025-07-12 18:27:32 +02:00
Jonas Kvinge
a47531d4ce Database: Remove FTS hack 2025-07-09 22:45:52 +02:00
Jonas Kvinge
84b758e395 README: Fix broken md link 2025-07-09 22:37:52 +02:00
Jonas Kvinge
51b69a85c4 GeniusLyricsProvider: Remove unused includes 2025-07-09 22:35:47 +02:00
Jonas Kvinge
52774a3222 ChartLyricsProvider: Fix empty results 2025-07-09 22:34:35 +02:00
gitlost
9030b2567b GeniusLyrics: update to parse latest HTML of returned lyrics,
devolving the removal of various crud to `HtmlLyricsProvider`;
  log initial query and use new `StartsOrEndsMatch()` static to
  match JSON replies, log each request, and break if full match;
  `StartsOrEndsMatch()` ignores some common punctuation variations
   & normalizes single quotes and allows match at beginning or end
HtmlLyricsProvider: fix `multiple` mode not to terminate on first
  batch, and defer processing till have whole HTML (avoids issues
  with tags spanning batches);
  add param to take list of regular expressions to remove from HTML
  prior to general processing (used only by `GeniusLyrics` for now)
README.md etc: update list of lyrics providers supported
2025-07-09 22:32:17 +02:00
gitlost
ee7bb449a5 Revert: Remove Genius lyrics [d9e38fb] 2025-07-09 22:32:17 +02:00
Madeline Schreiber
d901258f11 GstEnginePipeline: Ignore about-to-finish when position is 0 2025-07-07 01:05:47 +02:00
Madeline Schreiber
6372c5ee7d TagReaderClient: Call TagReaderGME when reading files 2025-07-07 01:05:47 +02:00
Madeline Schreiber
75f0402793 Add space to fix broken file filters 2025-07-04 17:25:34 -04:00
Ty
20e5c014ef PlaylistView: support alpha channel in background images 2025-07-04 20:42:32 +02:00
Strawberry Bot
1ebc32c3aa New translations 2025-07-03 21:06:02 +02:00
Piper McCorkle
a5f94b608b ListenBrainzScrobbler: Report more info to ListenBrainz
Report music service, URL, and Spotify ID to ListenBrainz.
ListenBrainz accepts the music service in listen reports, in both a canonical domain format and a human-readable display name format. This commit makes Strawberry report both, for maximum flexibility. I've also set it up to report a shareable track URL for supported streaming services. I am already using this data in my homepage's "Now Playing" widget.

Fixes #1768
2025-06-30 22:54:51 +02:00
Jonas Kvinge
e0d61223a4 CDDASongLoader: Fix freeing tag 2025-06-30 20:04:39 +02:00
Jonas Kvinge
459eea5bc4 FreeSpaceBar: Make sure bar size isn't negative
Fixes crash with CD drives.
2025-06-28 19:33:04 +02:00
Jonas Kvinge
09d02c53a3 StyleSheetLoader: Add back macOS hack 2025-06-23 21:12:54 +02:00
Jonas Kvinge
61a701554e style: Add back customized playlist background style 2025-06-23 20:44:00 +02:00
Jonas Kvinge
d280e6426f StyleSheetLoader: Add back alternate base color handling 2025-06-23 20:43:12 +02:00
Jonas Kvinge
5b9bb3efa7 Update Changelog 2025-06-23 20:06:35 +02:00
Jonas Kvinge
b8cbe49f8c StyleSheetLoader: Remove alternate base color handling 2025-06-23 20:05:35 +02:00
Jonas Kvinge
633e5707ef style: Remove customized playlist background style 2025-06-23 20:04:23 +02:00
Jonas Kvinge
d54290c3a7 Update Changelog 2025-06-23 19:01:55 +02:00
Jonas Kvinge
3ef2b53e46 Add back device view on Windows 2025-06-22 20:40:43 +02:00
Jonas Kvinge
d3a4dd6da6 CollectionView: Remove unused declaration 2025-06-22 20:36:57 +02:00
Jonas Kvinge
0158f7f08a Port DeviceManager to enum class 2025-06-22 17:35:19 +02:00
Jonas Kvinge
8cea020fac DeviceManager: Move creating device info to main thread 2025-06-22 17:21:12 +02:00
Jonas Kvinge
f6b38fecb0 DeviceManager: Set database ID when existing device info is found 2025-06-22 16:30:28 +02:00
Jonas Kvinge
5e2729fafe DeviceManager: Formatting 2025-06-22 16:29:27 +02:00
Jonas Kvinge
19dce1c25d DeviceInfo: Rename variables 2025-06-22 16:27:04 +02:00
Jonas Kvinge
00bb722e25 CDDALister: Trim friendly name 2025-06-22 16:26:35 +02:00
Jonas Kvinge
cbaf4d3121 DeviceManager: Rename variables 2025-06-22 00:49:01 +02:00
Jonas Kvinge
4b5370044b CDDASongLoader: Use cdiocddasrc 2025-06-22 00:39:09 +02:00
Jonas Kvinge
ffbe1ec9fd CDDASongLoader: Load tags from CD 2025-06-22 00:27:23 +02:00
Jonas Kvinge
53e43db91b CI: Add Fedora 43 2025-06-19 01:18:56 +02:00
dependabot[bot]
2858cdabc2 Bump vmactions/freebsd-vm from 1.2.0 to 1.2.1
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.2.0...v1.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-18 00:04:29 +02:00
Jonas Kvinge
cf74eeb120 CollectionSettingsPage: Remove edit triggers
Fixes #1767
2025-06-17 23:54:41 +02:00
Jonas Kvinge
790a1b4dbf ListenBrainzScrobbler: Use std::max 2025-06-17 23:47:46 +02:00
Jonas Kvinge
ee6332af1e ScrobblingAPI20: Replace std::min with std::max
Mistakenly written std::min instead of std::max here causing streams to never be scrobbled.
2025-06-17 23:47:37 +02:00
Jonas Kvinge
bf0704f6b2 Rename Cdda to CDDA 2025-06-09 04:21:17 +02:00
Jonas Kvinge
ae13fe7f52 Fix loading CD tracks in devices
Fixes #1676
2025-06-09 04:16:07 +02:00
Jonas Kvinge
90678e72ac DeviceManager: Remove device refresh 2025-06-09 04:12:23 +02:00
Jonas Kvinge
a0ec244008 CddaSongLoader: Fix some leaks 2025-06-09 02:27:11 +02:00
Jonas Kvinge
fba4f84fb6 CollectionModel: Move model reset to regular model updates 2025-06-09 02:24:53 +02:00
gitlost
950774f1c8 ExtendedEditor: padding for TextEdit & RTL LineEdit
`UpdateButtonGeometry()`: specify "QPlainTextEdit" for `TextEdit`
  padding (Comment and Lyrics) and invert left/right padding for
  `LineEdit` if layout direction RTL
2025-06-03 22:34:22 +02:00
gitlost
340bc21537 EditTagDialog: Make reset feedback work by calling
`set_reset_button()` in `UpdateModifiedField()` and catering for
  non-text in `IsValueModified()` (-1 original being same as 0);
  use new `ExtendedEditor::set_font()`;
  connect reset for "rating".
  Make "comment" `tabChangesFocus` to keep tab chain.
ExtendedEditor: New `set_font()` to get emboldened font to work and
  make reset feedback work for `CheckBox` and `RatingBox` by
  overriding `Resize()`.
RatingWidget: Allow tabbed focus and implement keyboard input.
2025-05-25 03:20:18 +02:00
Paper
a86ba4dffc GPodDevice: Add ALAC to supported file types for iPods
There are some iPods which do not support ALAC, but they are quite rare. Anything 3rd gen
and newer, which most people are likely to be using, will work if upgraded to the latest
firmware (they probably are already on it...)
2025-05-20 22:13:12 +02:00
Paper
d6bc6e33c0 Transcoder: Allow transcoding to ALAC 2025-05-20 22:13:12 +02:00
Paper
7e128a9af5 Song: Add ALAC song type 2025-05-20 22:13:12 +02:00
Jonas Kvinge
0f0746be9d CI: Remove Fedora 39 and 40 2025-05-15 22:39:15 +02:00
Jonas Kvinge
bec3fe9fd5 Turn on git revision 2025-05-15 22:38:32 +02:00
Jonas Kvinge
83c666baf9 Release 1.2.11 2025-05-15 21:09:19 +02:00
Strawberry Bot
b9b54e6e96 New translations 2025-05-13 22:11:19 +02:00
Jonas Kvinge
b2ff6240eb Update Changelog 2025-05-13 22:10:30 +02:00
Jonas Kvinge
26a7c74a24 nsi: Remove gioopenssl, except for msvc arm64 2025-05-13 22:10:25 +02:00
Jonas Kvinge
a34954ec4a PlaylistListContainer: Always check that playlist is open
Fixes #1741
2025-05-13 19:48:01 +02:00
Jonas Kvinge
349ab62e75 PlaylistListView: Check for valid current index 2025-05-13 19:42:25 +02:00
Jonas Kvinge
65e960f2c5 Update Changelog 2025-05-12 22:21:27 +02:00
Jonas Kvinge
e22fef8ca4 ContextView: Fix album cover visible check
Fixes #1744
2025-05-12 18:52:12 +02:00
Jonas Kvinge
3e99045e2c nsi: Update sqlite3 dll name 2025-05-08 22:31:43 +02:00
Jonas Kvinge
4fcade273e Update Changelog 2025-05-08 21:20:53 +02:00
Strawberry Bot
5eaff0d26e New translations 2025-05-08 21:17:36 +02:00
Strawberry Bot
5b22f12b4a New translations 2025-05-01 23:50:07 +02:00
OlegAckbar
5f85c2e7a5 Linux: enable startup notify
It was very odd for me why Strawberry doesn't have any feedback when launching from application menu. Turns out its desktop file had "StartupNotify=false" for some reason?
2025-05-01 23:41:35 +02:00
dependabot[bot]
4fb5a7b6bc Bump vmactions/openbsd-vm from 1.1.7 to 1.1.8
Bumps [vmactions/openbsd-vm](https://github.com/vmactions/openbsd-vm) from 1.1.7 to 1.1.8.
- [Release notes](https://github.com/vmactions/openbsd-vm/releases)
- [Commits](https://github.com/vmactions/openbsd-vm/compare/v1.1.7...v1.1.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-30 18:59:21 +02:00
Jonas Kvinge
04c6c862c4 Refactor playlist items
Fix a bug where playlist items cover is not updated
2025-04-27 03:03:58 +02:00
Jonas Kvinge
baec45f742 CollectionBackend: Add updating collection database task 2025-04-27 02:27:46 +02:00
Jonas Kvinge
9efdbd2c10 CollectionWatcher: Add missing updates 2025-04-27 02:25:42 +02:00
Jonas Kvinge
d8800b80d5 CMake: Move discord-rpc to same target_link_libraries 2025-04-23 19:23:01 +02:00
Jonas Kvinge
ec715abb0d CI: Use macOS 12 SDK when available 2025-04-21 14:40:12 +02:00
Jonas Kvinge
1485801efb CI: Add support for Windows arm64 2025-04-20 02:14:42 +02:00
Jonas Kvinge
4f9ac3d33a nsi: Add support for arm64 2025-04-20 02:13:58 +02:00
Jonas Kvinge
1577ce4d67 Turn on git revision 2025-04-18 21:59:18 +02:00
Jonas Kvinge
7eee74a2e9 Release 1.2.10 2025-04-18 20:04:22 +02:00
Jonas Kvinge
d9e38fb3be Remove Genius lyrics
No longer working properly because of website changes.
2025-04-18 15:56:30 +02:00
Jonas Kvinge
81cc90e54a Update Changelog 2025-04-18 02:38:37 +02:00
Jonas Kvinge
bd9771a88f TagReaderTagLib: Use TagLib::Tag::comment
Makes it use only commercial frames without description for comments, reading other commercial frames picks different iTunes tags we don't want.
2025-04-18 02:15:17 +02:00
Jonas Kvinge
f5cd81fe09 nsi: Re-enable Spotify 2025-04-16 23:25:03 +02:00
Gregor Santner
277e2cff59 Linux: Add Clementine search keyword to .desktop shortcut 2025-04-15 21:46:15 +02:00
Jonas Kvinge
6fa9514059 RichPresence: Only initialize discord when enabled 2025-04-13 21:45:55 +02:00
Jonas Kvinge
c5e38b71f7 discord_rpc: Use anonymous namespace 2025-04-13 21:34:40 +02:00
Jonas Kvinge
3746915ae7 RichPresence: Always include album 2025-04-13 19:19:53 +02:00
Jonas Kvinge
21bdf88d09 RichPresence: Remove unused variable 2025-04-13 12:16:57 +02:00
Jonas Kvinge
ff032c3cd7 RichPresence: Remove rate limit 2025-04-13 12:01:56 +02:00
Jonas Kvinge
c083110051 RichPresence: Move variable declaration
Fixes #1718
2025-04-13 11:52:16 +02:00
Jonas Kvinge
a7dbeb5d76 discord-rpc: Add copyright 2025-04-12 13:17:13 +02:00
Jonas Kvinge
634f6ea9f5 discord-rpc: Formatting 2025-04-12 13:17:00 +02:00
Jonas Kvinge
f9e4f9a09a discord-rpc: Formatting 2025-04-11 22:50:14 +02:00
Jonas Kvinge
aab9889174 Turn on git revision 2025-04-09 19:59:48 +02:00
176 changed files with 6055 additions and 3753 deletions

View File

@@ -97,7 +97,7 @@ jobs:
cmake --build build --config Release --parallel 4 cmake --build build --config Release --parallel 4
cmake --install build cmake --install build
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
@@ -156,7 +156,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
fedora_version: [ '39', '40', '41', '42' ] fedora_version: [ '41', '42', '43' ]
container: container:
image: fedora:${{matrix.fedora_version}} image: fedora:${{matrix.fedora_version}}
steps: steps:
@@ -209,7 +209,7 @@ jobs:
sparsehash-devel sparsehash-devel
rapidjson-devel rapidjson-devel
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
@@ -307,7 +307,7 @@ jobs:
- name: Remove files - name: Remove files
run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*} run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*}
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
@@ -409,7 +409,7 @@ jobs:
cmake --build build --config Release --parallel 4 cmake --build build --config Release --parallel 4
cmake --install build cmake --install build
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
@@ -507,7 +507,7 @@ jobs:
cmake --build build --config Release --parallel 4 cmake --build build --config Release --parallel 4
cmake --install build cmake --install build
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
@@ -538,7 +538,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
ubuntu_version: [ 'noble', 'oracular', 'plucky' ] ubuntu_version: [ 'noble', 'plucky' ]
container: container:
image: ubuntu:${{matrix.ubuntu_version}} image: ubuntu:${{matrix.ubuntu_version}}
steps: steps:
@@ -599,7 +599,7 @@ jobs:
cmake --build build --config Release --parallel 4 cmake --build build --config Release --parallel 4
cmake --install build cmake --install build
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
@@ -631,7 +631,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
ubuntu_version: [ 'noble', 'oracular', 'plucky' ] ubuntu_version: [ 'noble', 'plucky' ]
container: container:
image: ubuntu:${{matrix.ubuntu_version}} image: ubuntu:${{matrix.ubuntu_version}}
steps: steps:
@@ -691,7 +691,7 @@ jobs:
DEBIAN_FRONTEND: noninteractive DEBIAN_FRONTEND: noninteractive
run: apt install -y keyboxd run: apt install -y keyboxd
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
@@ -727,13 +727,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
- name: Build FreeBSD - name: Build FreeBSD
id: build-freebsd id: build-freebsd
uses: vmactions/freebsd-vm@v1.2.0 uses: vmactions/freebsd-vm@v1.2.3
with: with:
usesh: true usesh: true
mem: 4096 mem: 4096
@@ -752,13 +752,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
- name: Build OpenBSD - name: Build OpenBSD
id: build-openbsd id: build-openbsd
uses: vmactions/openbsd-vm@v1.1.7 uses: vmactions/openbsd-vm@v1.2.0
with: with:
usesh: true usesh: true
mem: 4096 mem: 4096
@@ -788,7 +788,7 @@ jobs:
- name: Set MACOSX_DEPLOYMENT_TARGET - name: Set MACOSX_DEPLOYMENT_TARGET
run: | run: |
for i in 13 14 15; do for i in 12 13 14 15; do
if [ -d "/Library/Developer/CommandLineTools/SDKs/MacOSX${i}.sdk" ]; then if [ -d "/Library/Developer/CommandLineTools/SDKs/MacOSX${i}.sdk" ]; then
echo "Using macOS SDK ${i}" echo "Using macOS SDK ${i}"
echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV
@@ -818,7 +818,7 @@ jobs:
rm -f uninstall.sh rm -f uninstall.sh
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
@@ -946,7 +946,7 @@ jobs:
- name: Set MACOSX_DEPLOYMENT_TARGET - name: Set MACOSX_DEPLOYMENT_TARGET
run: | run: |
for i in 13 14 15; do for i in 12 13 14 15; do
if [ -d "/Library/Developer/CommandLineTools/SDKs/MacOSX${i}.sdk" ]; then if [ -d "/Library/Developer/CommandLineTools/SDKs/MacOSX${i}.sdk" ]; then
echo "Using macOS SDK ${i}" echo "Using macOS SDK ${i}"
echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV
@@ -969,7 +969,7 @@ jobs:
run: echo "cmake_buildtype=$(echo ${{env.buildtype}} | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}')" >> $GITHUB_ENV run: echo "cmake_buildtype=$(echo ${{env.buildtype}} | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}')" >> $GITHUB_ENV
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
@@ -1072,7 +1072,7 @@ jobs:
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
@@ -1246,12 +1246,42 @@ jobs:
build-windows-msvc: build-windows-msvc:
name: Build Windows MSVC name: Build Windows MSVC
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master' if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: windows-2022
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
arch: [ 'x86', 'x86_64' ] include:
buildtype: [ 'release' ] - name: "x86_64 debug"
runner: windows-2022
arch: x86_64
buildtype: debug
- name: "x86_64 release"
runner: windows-2022
arch: x86_64
buildtype: release
- name: "x86 debug"
runner: windows-2022
arch: x86
buildtype: debug
- name: "x86 release"
runner: windows-2022
arch: x86
buildtype: release
- name: "arm64 debug"
runner: windows-11-arm
arch: arm64
buildtype: debug
- name: "arm64 release"
runner: windows-11-arm
arch: arm64
buildtype: release
runs-on: ${{matrix.runner}}
steps: steps:
- name: Set prefix path - name: Set prefix path
@@ -1265,6 +1295,20 @@ jobs:
shell: bash shell: bash
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
- name: Show SDK versions
shell: bash
run: ls -la "c:/Program Files (x86)/Windows Kits/10/include"
- name: Set SDK version
if: matrix.arch != 'arm64'
shell: bash
run: echo "sdk_version=10.0.19041.0" >> $GITHUB_ENV
- name: Set SDK version
if: matrix.arch == 'arm64'
shell: bash
run: echo "sdk_version=10.0.26100.0" >> $GITHUB_ENV
- name: Install rsync - name: Install rsync
shell: cmd shell: cmd
run: choco install --no-progress rsync run: choco install --no-progress rsync
@@ -1347,11 +1391,11 @@ jobs:
uses: ilammy/msvc-dev-cmd@v1 uses: ilammy/msvc-dev-cmd@v1
with: with:
arch: ${{matrix.arch}} arch: ${{matrix.arch}}
sdk: 10.0.20348.0 sdk: ${{env.sdk_version}}
vsversion: 2022 vsversion: 2022
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
@@ -1364,15 +1408,18 @@ jobs:
shell: cmd shell: cmd
run: cmake -E make_directory build run: cmake -E make_directory build
- name: Set ENABLE_WIN32_CONSOLE (debug) - name: Set ENABLE_WIN32_CONSOLE
if: matrix.buildtype == 'debug'
shell: bash shell: bash
run: echo "win32_console=ON" >> $GITHUB_ENV run: echo "enable_win32_console=$(test "${{matrix.buildtype}}" = "debug" && echo "ON" || echo "OFF")" >> $GITHUB_ENV
- name: Set ENABLE_WIN32_CONSOLE (release) - name: Set ENABLE_SPOTIFY
if: matrix.buildtype == 'release'
shell: bash shell: bash
run: echo "win32_console=OFF" >> $GITHUB_ENV run: echo "enable_spotify=$(test -f "${{env.prefix_path_unix}}/lib/gstreamer-1.0/gstspotify.dll" && echo "ON" || echo "OFF")" >> $GITHUB_ENV
- name: Remove -lm from .pc files
if: matrix.arch == 'arm64'
shell: bash
run: sed -i 's/\-lm$//g' ${{env.prefix_path_unix}}/lib/pkgconfig/*.pc
- name: Run CMake - name: Run CMake
shell: cmd shell: cmd
@@ -1384,14 +1431,14 @@ jobs:
-DCMAKE_BUILD_TYPE="${{env.cmake_buildtype}}" -DCMAKE_BUILD_TYPE="${{env.cmake_buildtype}}"
-DCMAKE_PREFIX_PATH="${{env.prefix_path_forwardslash}}/lib/cmake" -DCMAKE_PREFIX_PATH="${{env.prefix_path_forwardslash}}/lib/cmake"
-DARCH="${{matrix.arch}}" -DARCH="${{matrix.arch}}"
-DENABLE_WIN32_CONSOLE=${{env.win32_console}} -DENABLE_WIN32_CONSOLE=${{env.enable_win32_console}}
-DPKG_CONFIG_EXECUTABLE="${{env.prefix_path_forwardslash}}/bin/pkg-config.exe" -DPKG_CONFIG_EXECUTABLE="${{env.prefix_path_forwardslash}}/bin/pkg-config.exe"
-DICU_ROOT="${{env.prefix_path_forwardslash}}" -DICU_ROOT="${{env.prefix_path_forwardslash}}"
-DENABLE_GIO=OFF -DENABLE_GIO=OFF
-DENABLE_AUDIOCD=OFF -DENABLE_AUDIOCD=OFF
-DENABLE_MTP=OFF -DENABLE_MTP=OFF
-DENABLE_GPOD=OFF -DENABLE_GPOD=OFF
-DENABLE_SPOTIFY=ON -DENABLE_SPOTIFY=${{env.enable_spotify}}
- name: Run Make - name: Run Make
shell: cmd shell: cmd
@@ -1460,11 +1507,14 @@ jobs:
run: copy ${{env.prefix_path_backslash}}\lib\gstreamer-1.0\*.dll .\gstreamer-plugins\ run: copy ${{env.prefix_path_backslash}}\lib\gstreamer-1.0\*.dll .\gstreamer-plugins\
- name: Download copydlldeps.sh - name: Download copydlldeps.sh
if: matrix.arch != 'arm64'
shell: bash shell: bash
working-directory: build working-directory: build
run: curl -f -O -L https://raw.githubusercontent.com/strawberrymusicplayer/strawberry-mxe/master/tools/copydlldeps.sh run: curl -f -O -L https://raw.githubusercontent.com/strawberrymusicplayer/strawberry-mxe/master/tools/copydlldeps.sh
- name: Copy dependencies - name: Copy dependencies
# copydlldeps.sh doesn't work with arm64 binaries.
if: matrix.arch != 'arm64'
shell: bash shell: bash
working-directory: build working-directory: build
run: > run: >
@@ -1481,6 +1531,12 @@ jobs:
-F ./gstreamer-plugins -F ./gstreamer-plugins
-R ${{env.prefix_path_unix}}/bin -R ${{env.prefix_path_unix}}/bin
- name: Copy dependencies
if: matrix.arch == 'arm64'
shell: bash
working-directory: build
run: cp -v ${{env.prefix_path_unix}}/bin/{avcodec*.dll,avfilter*.dll,avformat*.dll,avutil*.dll,brotlicommon.dll,brotlidec.dll,chromaprint.dll,ebur128.dll,faad-2.dll,fdk-aac.dll,ffi-7.dll,FLAC.dll,freetype*.dll,getopt.dll,gio-2.0-0.dll,glib-2.0-0.dll,gme.dll,gmodule-2.0-0.dll,gobject-2.0-0.dll,gst-discoverer-1.0.exe,gst-launch-1.0.exe,gst-play-1.0.exe,gstadaptivedemux-1.0-0.dll,gstapp-1.0-0.dll,gstaudio-1.0-0.dll,gstbadaudio-1.0-0.dll,gstbase-1.0-0.dll,gstcodecparsers-1.0-0.dll,gstfft-1.0-0.dll,gstisoff-1.0-0.dll,gstmpegts-1.0-0.dll,gstnet-1.0-0.dll,gstpbutils-1.0-0.dll,gstreamer-1.0-0.dll,gstriff-1.0-0.dll,gstrtp-1.0-0.dll,gstrtsp-1.0-0.dll,gstsdp-1.0-0.dll,gsttag-1.0-0.dll,gsturidownloader-1.0-0.dll,gstvideo-1.0-0.dll,gstwinrt-1.0-0.dll,harfbuzz.dll,icudt*.dll,icuin*.dll,icuuc*.dll,intl-8.dll,jpeg62.dll,kdsingleapplication*.dll,libbs2b.dll,libcrypto-3-*.dll,fftw3.dll,libiconv*.dll,liblzma.dll,libmp3lame.dll,libopenmpt.dll,libpng16*.dll,libspeex*.dll,libssl-3-*.dll,libxml2*.dll,mpcdec.dll,mpg123.dll,nghttp2.dll,ogg.dll,opus.dll,orc-0.4-0.dll,pcre2-16*.dll,pcre2-8*.dll,postproc*.dll,psl-5.dll,Qt6Concurrent*.dll,Qt6Core*.dll,Qt6Gui*.dll,Qt6Network*.dll,Qt6Sql*.dll,Qt6Widgets*.dll,qtsparkle-qt6.dll,soup-3.0-0.dll,sqlite3.dll,sqlite3.exe,swresample*.dll,swscale*.dll,tag.dll,vorbis.dll,vorbisfile.dll,wavpackdll.dll,zlib*.dll} .
- name: Copy nsis files - name: Copy nsis files
shell: cmd shell: cmd
working-directory: build working-directory: build
@@ -1581,11 +1637,11 @@ jobs:
DEBIAN_FRONTEND: noninteractive DEBIAN_FRONTEND: noninteractive
run: sudo apt install -y git rsync run: sudo apt install -y git rsync
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v5
with: with:
path: artifacts path: artifacts
- name: SSH Setup - name: SSH Setup
@@ -1629,7 +1685,7 @@ jobs:
DEBIAN_FRONTEND: noninteractive DEBIAN_FRONTEND: noninteractive
run: sudo apt install -y git jq gh run: sudo apt install -y git jq gh
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Show release assets - name: Show release assets
@@ -1637,7 +1693,7 @@ jobs:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
run: gh release view "${{github.event.release.tag_name}}" --json assets | jq -r '.assets[].name' run: gh release view "${{github.event.release.tag_name}}" --json assets | jq -r '.assets[].name'
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v5
with: with:
path: artifacts path: artifacts
- name: Add artifacts to release - name: Add artifacts to release

View File

@@ -1 +1,41 @@
add_subdirectory(src) set(DISCORD_RPC_SOURCES
discord_rpc.h
discord_register.h
discord_rpc.cpp
discord_rpc_connection.h
discord_rpc_connection.cpp
discord_serialization.h
discord_serialization.cpp
discord_connection.h
discord_backoff.h
discord_msg_queue.h
)
if(UNIX)
list(APPEND DISCORD_RPC_SOURCES discord_connection_unix.cpp)
if(APPLE)
list(APPEND DISCORD_RPC_SOURCES discord_register_osx.m)
add_definitions(-DDISCORD_OSX)
else()
list(APPEND DISCORD_RPC_SOURCES discord_register_linux.cpp)
add_definitions(-DDISCORD_LINUX)
endif()
endif()
if(WIN32)
list(APPEND DISCORD_RPC_SOURCES discord_connection_win.cpp discord_register_win.cpp)
add_definitions(-DDISCORD_WINDOWS)
endif()
add_library(discord-rpc STATIC ${DISCORD_RPC_SOURCES})
if(APPLE)
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
endif()
if(WIN32)
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
endif()
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

63
3rdparty/discord-rpc/discord_backoff.h vendored Normal file
View File

@@ -0,0 +1,63 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#ifndef DISCORD_BACKOFF_H
#define DISCORD_BACKOFF_H
#include <algorithm>
#include <random>
#include <cstdint>
#include <ctime>
namespace discord_rpc {
struct Backoff {
int64_t minAmount;
int64_t maxAmount;
int64_t current;
int fails;
std::mt19937_64 randGenerator;
std::uniform_real_distribution<> randDistribution;
double rand01() { return randDistribution(randGenerator); }
Backoff(int64_t min, int64_t max)
: minAmount(min), maxAmount(max), current(min), fails(0), randGenerator(static_cast<uint64_t>(time(0))) {
}
void reset() {
fails = 0;
current = minAmount;
}
int64_t nextDelay() {
++fails;
int64_t delay = static_cast<int64_t>(static_cast<double>(current) * 2.0 * rand01());
current = std::min(current + delay, maxAmount);
return current;
}
};
} // namespace discord_rpc
#endif // DISCORD_BACKOFF_H

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#ifndef DISCORD_CONNECTION_H
#define DISCORD_CONNECTION_H
// This is to wrap the platform specific kinds of connect/read/write.
#include <cstdlib>
namespace discord_rpc {
// not really connectiony, but need per-platform
int GetProcessId();
struct BaseConnection {
static BaseConnection *Create();
static void Destroy(BaseConnection *&);
bool isOpen = false;
bool Open();
bool Close();
bool Write(const void *data, size_t length);
bool Read(void *data, size_t length);
};
} // namespace discord_rpc
#endif // DISCORD_CONNECTION_H

View File

@@ -1,4 +1,27 @@
#include "connection.h" /*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#include "discord_connection.h"
#include <cerrno> #include <cerrno>
#include <fcntl.h> #include <fcntl.h>
@@ -28,28 +51,34 @@ static int MsgFlags = 0;
#endif #endif
static const char *GetTempPath() { static const char *GetTempPath() {
const char *temp = getenv("XDG_RUNTIME_DIR"); const char *temp = getenv("XDG_RUNTIME_DIR");
temp = temp ? temp : getenv("TMPDIR"); temp = temp ? temp : getenv("TMPDIR");
temp = temp ? temp : getenv("TMP"); temp = temp ? temp : getenv("TMP");
temp = temp ? temp : getenv("TEMP"); temp = temp ? temp : getenv("TEMP");
temp = temp ? temp : "/tmp"; temp = temp ? temp : "/tmp";
return temp; return temp;
} }
/*static*/ BaseConnection *BaseConnection::Create() { BaseConnection *BaseConnection::Create() {
PipeAddr.sun_family = AF_UNIX; PipeAddr.sun_family = AF_UNIX;
return &Connection; return &Connection;
} }
/*static*/ void BaseConnection::Destroy(BaseConnection *&c) { void BaseConnection::Destroy(BaseConnection *&c) {
auto self = reinterpret_cast<BaseConnectionUnix *>(c);
auto self = reinterpret_cast<BaseConnectionUnix*>(c);
self->Close(); self->Close();
c = nullptr; c = nullptr;
} }
bool BaseConnection::Open() { bool BaseConnection::Open() {
const char *tempPath = GetTempPath(); const char *tempPath = GetTempPath();
auto self = reinterpret_cast<BaseConnectionUnix *>(this); auto self = reinterpret_cast<BaseConnectionUnix*>(this);
self->sock = socket(AF_UNIX, SOCK_STREAM, 0); self->sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (self->sock == -1) { if (self->sock == -1) {
return false; return false;
@@ -61,8 +90,7 @@ bool BaseConnection::Open() {
#endif #endif
for (int pipeNum = 0; pipeNum < 10; ++pipeNum) { for (int pipeNum = 0; pipeNum < 10; ++pipeNum) {
snprintf( snprintf(PipeAddr.sun_path, sizeof(PipeAddr.sun_path), "%s/discord-ipc-%d", tempPath, pipeNum);
PipeAddr.sun_path, sizeof(PipeAddr.sun_path), "%s/discord-ipc-%d", tempPath, pipeNum);
int err = connect(self->sock, reinterpret_cast<const sockaddr*>(&PipeAddr), sizeof(PipeAddr)); int err = connect(self->sock, reinterpret_cast<const sockaddr*>(&PipeAddr), sizeof(PipeAddr));
if (err == 0) { if (err == 0) {
self->isOpen = true; self->isOpen = true;
@@ -70,10 +98,13 @@ bool BaseConnection::Open() {
} }
} }
self->Close(); self->Close();
return false; return false;
} }
bool BaseConnection::Close() { bool BaseConnection::Close() {
auto self = reinterpret_cast<BaseConnectionUnix *>(this); auto self = reinterpret_cast<BaseConnectionUnix *>(this);
if (self->sock == -1) { if (self->sock == -1) {
return false; return false;
@@ -81,11 +112,14 @@ bool BaseConnection::Close() {
close(self->sock); close(self->sock);
self->sock = -1; self->sock = -1;
self->isOpen = false; self->isOpen = false;
return true; return true;
} }
bool BaseConnection::Write(const void *data, size_t length) { bool BaseConnection::Write(const void *data, size_t length) {
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
if (self->sock == -1) { if (self->sock == -1) {
return false; return false;
@@ -95,11 +129,14 @@ bool BaseConnection::Write(const void *data, size_t length) {
if (sentBytes < 0) { if (sentBytes < 0) {
Close(); Close();
} }
return sentBytes == static_cast<ssize_t>(length); return sentBytes == static_cast<ssize_t>(length);
} }
bool BaseConnection::Read(void *data, size_t length) { bool BaseConnection::Read(void *data, size_t length) {
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
if (self->sock == -1) { if (self->sock == -1) {
return false; return false;
@@ -115,8 +152,9 @@ bool BaseConnection::Read(void *data, size_t length) {
else if (res == 0) { else if (res == 0) {
Close(); Close();
} }
return static_cast<size_t>(res) == length; return static_cast<size_t>(res) == length;
} }
} // namespace discord_rpc } // namespace discord_rpc

View File

@@ -1,9 +1,33 @@
#include "connection.h" /*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#include "discord_connection.h"
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#define NOMCX #define NOMCX
#define NOSERVICE #define NOSERVICE
#define NOIME #define NOIME
#include <cassert> #include <cassert>
#include <windows.h> #include <windows.h>
@@ -19,24 +43,26 @@ struct BaseConnectionWin : public BaseConnection {
static BaseConnectionWin Connection; static BaseConnectionWin Connection;
/*static*/ BaseConnection *BaseConnection::Create() { BaseConnection *BaseConnection::Create() {
return &Connection; return &Connection;
} }
/*static*/ void BaseConnection::Destroy(BaseConnection *&c) { void BaseConnection::Destroy(BaseConnection *&c) {
auto self = reinterpret_cast<BaseConnectionWin*>(c); auto self = reinterpret_cast<BaseConnectionWin*>(c);
self->Close(); self->Close();
c = nullptr; c = nullptr;
} }
bool BaseConnection::Open() { bool BaseConnection::Open() {
wchar_t pipeName[] { L"\\\\?\\pipe\\discord-ipc-0" }; wchar_t pipeName[] { L"\\\\?\\pipe\\discord-ipc-0" };
const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2; const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
pipeName[pipeDigit] = L'0'; pipeName[pipeDigit] = L'0';
auto self = reinterpret_cast<BaseConnectionWin *>(this); auto self = reinterpret_cast<BaseConnectionWin *>(this);
for (;;) { for (;;) {
self->pipe = ::CreateFileW( self->pipe = ::CreateFileW(pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (self->pipe != INVALID_HANDLE_VALUE) { if (self->pipe != INVALID_HANDLE_VALUE) {
self->isOpen = true; self->isOpen = true;
return true; return true;
@@ -57,17 +83,22 @@ bool BaseConnection::Open() {
} }
return false; return false;
} }
} }
bool BaseConnection::Close() { bool BaseConnection::Close() {
auto self = reinterpret_cast<BaseConnectionWin *>(this); auto self = reinterpret_cast<BaseConnectionWin *>(this);
::CloseHandle(self->pipe); ::CloseHandle(self->pipe);
self->pipe = INVALID_HANDLE_VALUE; self->pipe = INVALID_HANDLE_VALUE;
self->isOpen = false; self->isOpen = false;
return true; return true;
} }
bool BaseConnection::Write(const void *data, size_t length) { bool BaseConnection::Write(const void *data, size_t length) {
if (length == 0) { if (length == 0) {
return true; return true;
} }
@@ -85,11 +116,13 @@ bool BaseConnection::Write(const void *data, size_t length) {
} }
const DWORD bytesLength = static_cast<DWORD>(length); const DWORD bytesLength = static_cast<DWORD>(length);
DWORD bytesWritten = 0; DWORD bytesWritten = 0;
return ::WriteFile(self->pipe, data, bytesLength, &bytesWritten, nullptr) == TRUE &&
bytesWritten == bytesLength; return ::WriteFile(self->pipe, data, bytesLength, &bytesWritten, nullptr) == TRUE && bytesWritten == bytesLength;
} }
bool BaseConnection::Read(void *data, size_t length) { bool BaseConnection::Read(void *data, size_t length) {
assert(data); assert(data);
if (!data) { if (!data) {
return false; return false;
@@ -119,8 +152,9 @@ bool BaseConnection::Read(void *data, size_t length) {
else { else {
Close(); Close();
} }
return false; return false;
} }
} // namespace discord_rpc } // namespace discord_rpc

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#ifndef DISCORD_MSG_QUEUE_H
#define DISCORD_MSG_QUEUE_H
#include <atomic>
// A simple queue. No locks, but only works with a single thread as producer and a single thread as
// a consumer. Mutex up as needed.
namespace discord_rpc {
template<typename ElementType, std::size_t QueueSize>
class MsgQueue {
ElementType queue_[QueueSize];
std::atomic_uint nextAdd_ { 0 };
std::atomic_uint nextSend_ { 0 };
std::atomic_uint pendingSends_ { 0 };
public:
MsgQueue() {}
ElementType *GetNextAddMessage() {
// if we are falling behind, bail
if (pendingSends_.load() >= QueueSize) {
return nullptr;
}
auto index = (nextAdd_++) % QueueSize;
return &queue_[index];
}
void CommitAdd() { ++pendingSends_; }
bool HavePendingSends() const { return pendingSends_.load() != 0; }
ElementType *GetNextSendMessage() {
auto index = (nextSend_++) % QueueSize;
return &queue_[index];
}
void CommitSend() { --pendingSends_; }
};
} // namespace discord_rpc
#endif // DISCORD_MSG_QUEUE_H

37
3rdparty/discord-rpc/discord_register.h vendored Normal file
View File

@@ -0,0 +1,37 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#ifndef DISCORD_REGISTER_H
#define DISCORD_REGISTER_H
#ifdef __cplusplus
extern "C" {
#endif
void Discord_Register(const char *applicationId, const char *command);
#ifdef __cplusplus
}
#endif
#endif // DISCORD_REGISTER_H

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#include "discord_rpc.h"
#include "discord_register.h"
#include <cstdio>
#include <errno.h>
#include <cstdlib>
#include <cstring>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
namespace {
static bool Mkdir(const char *path) {
int result = mkdir(path, 0755);
if (result == 0) {
return true;
}
if (errno == EEXIST) {
return true;
}
return false;
}
} // namespace
// We want to register games so we can run them from Discord client as discord-<appid>://
extern "C" void Discord_Register(const char *applicationId, const char *command) {
// Add a desktop file and update some mime handlers so that xdg-open does the right thing.
const char *home = getenv("HOME");
if (!home) {
return;
}
char exePath[1024]{};
if (!command || !command[0]) {
const ssize_t size = readlink("/proc/self/exe", exePath, sizeof(exePath));
if (size <= 0 || size >= static_cast<ssize_t>(sizeof(exePath))) {
return;
}
exePath[size] = '\0';
command = exePath;
}
constexpr char desktopFileFormat[] = "[Desktop Entry]\n"
"Name=Game %s\n"
"Exec=%s %%u\n" // note: it really wants that %u in there
"Type=Application\n"
"NoDisplay=true\n"
"Categories=Discord;Games;\n"
"MimeType=x-scheme-handler/discord-%s;\n";
char desktopFile[2048]{};
int fileLen = snprintf(desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId, command, applicationId);
if (fileLen <= 0) {
return;
}
char desktopFilename[256]{};
(void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId);
char desktopFilePath[1024]{};
(void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home);
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, "/share");
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, "/applications");
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, desktopFilename);
FILE *fp = fopen(desktopFilePath, "w");
if (fp) {
fwrite(desktopFile, 1, fileLen, fp);
fclose(fp);
}
else {
return;
}
char xdgMimeCommand[1024]{};
snprintf(xdgMimeCommand,
sizeof(xdgMimeCommand),
"xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s",
applicationId,
applicationId);
if (system(xdgMimeCommand) < 0) {
fprintf(stderr, "Failed to register mime handler\n");
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#include <stdio.h>
#include <sys/stat.h>
#import <AppKit/AppKit.h>
#include "discord_register.h"
static void RegisterCommand(const char *applicationId, const char *command) {
// There does not appear to be a way to register arbitrary commands on OSX, so instead we'll save the command
// to a file in the Discord config path, and when it is needed, Discord can try to load the file there, open
// the command therein (will pass to js's window.open, so requires a url-like thing)
// Note: will not work for sandboxed apps
NSString *home = NSHomeDirectory();
if (!home) {
return;
}
NSString *path = [[[[[[home stringByAppendingPathComponent:@"Library"]
stringByAppendingPathComponent:@"Application Support"]
stringByAppendingPathComponent:@"discord"]
stringByAppendingPathComponent:@"games"]
stringByAppendingPathComponent:[NSString stringWithUTF8String:applicationId]]
stringByAppendingPathExtension:@"json"];
[[NSFileManager defaultManager] createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
NSString *jsonBuffer = [NSString stringWithFormat:@"{\"command\": \"%s\"}", command];
[jsonBuffer writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:nil];
}
static void RegisterURL(const char *applicationId) {
char url[256];
snprintf(url, sizeof(url), "discord-%s", applicationId);
CFStringRef cfURL = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
NSString* myBundleId = [[NSBundle mainBundle] bundleIdentifier];
if (!myBundleId) {
fprintf(stderr, "No bundle id found\n");
return;
}
NSURL* myURL = [[NSBundle mainBundle] bundleURL];
if (!myURL) {
fprintf(stderr, "No bundle url found\n");
return;
}
OSStatus status = LSSetDefaultHandlerForURLScheme(cfURL, (__bridge CFStringRef)myBundleId);
if (status != noErr) {
fprintf(stderr, "Error in LSSetDefaultHandlerForURLScheme: %d\n", (int)status);
return;
}
status = LSRegisterURL((__bridge CFURLRef)myURL, true);
if (status != noErr) {
fprintf(stderr, "Error in LSRegisterURL: %d\n", (int)status);
}
}
void Discord_Register(const char *applicationId, const char *command) {
if (command) {
RegisterCommand(applicationId, command);
}
else {
// raii lite
@autoreleasepool {
RegisterURL(applicationId);
}
}
}

View File

@@ -1,3 +1,26 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#include "discord_rpc.h" #include "discord_rpc.h"
#include "discord_register.h" #include "discord_register.h"
@@ -5,6 +28,7 @@
#define NOMCX #define NOMCX
#define NOSERVICE #define NOSERVICE
#define NOIME #define NOIME
#include <windows.h> #include <windows.h>
#include <psapi.h> #include <psapi.h>
#include <cstdio> #include <cstdio>
@@ -46,12 +70,8 @@ static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat,
#endif #endif
#define RegSetKeyValueW regset #define RegSetKeyValueW regset
static LSTATUS regset(HKEY hkey, static LSTATUS regset(HKEY hkey, LPCWSTR subkey, LPCWSTR name, DWORD type, const void *data, DWORD len) {
LPCWSTR subkey,
LPCWSTR name,
DWORD type,
const void *data,
DWORD len) {
HKEY htkey = hkey, hsubkey = nullptr; HKEY htkey = hkey, hsubkey = nullptr;
LSTATUS ret; LSTATUS ret;
if (subkey && subkey[0]) { if (subkey && subkey[0]) {
@@ -64,16 +84,18 @@ static LSTATUS regset(HKEY hkey,
if (hsubkey && hsubkey != hkey) if (hsubkey && hsubkey != hkey)
RegCloseKey(hsubkey); RegCloseKey(hsubkey);
return ret; return ret;
} }
static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *command) { static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *command) {
// https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx // https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx
// we want to register games so we can run them as discord-<appid>:// // we want to register games so we can run them as discord-<appid>://
// Update the HKEY_CURRENT_USER, because it doesn't seem to require special permissions. // Update the HKEY_CURRENT_USER, because it doesn't seem to require special permissions.
wchar_t exeFilePath[MAX_PATH]; wchar_t exeFilePath[MAX_PATH]{};
DWORD exeLen = GetModuleFileNameW(nullptr, exeFilePath, MAX_PATH); DWORD exeLen = GetModuleFileNameW(nullptr, exeFilePath, MAX_PATH);
wchar_t openCommand[1024]; wchar_t openCommand[1024]{};
if (command && command[0]) { if (command && command[0]) {
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command); StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command);
@@ -83,18 +105,16 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath); StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath);
} }
wchar_t protocolName[64]; wchar_t protocolName[64]{};
StringCbPrintfW(protocolName, sizeof(protocolName), L"discord-%s", applicationId); StringCbPrintfW(protocolName, sizeof(protocolName), L"discord-%s", applicationId);
wchar_t protocolDescription[128]; wchar_t protocolDescription[128]{};
StringCbPrintfW( StringCbPrintfW(protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
wchar_t urlProtocol = 0; wchar_t urlProtocol = 0;
wchar_t keyName[256]; wchar_t keyName[256]{};
StringCbPrintfW(keyName, sizeof(keyName), L"Software\\Classes\\%s", protocolName); StringCbPrintfW(keyName, sizeof(keyName), L"Software\\Classes\\%s", protocolName);
HKEY key; HKEY key;
auto status = auto status = RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);
RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);
if (status != ERROR_SUCCESS) { if (status != ERROR_SUCCESS) {
fprintf(stderr, "Error creating key\n"); fprintf(stderr, "Error creating key\n");
return; return;
@@ -102,8 +122,7 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
DWORD len; DWORD len;
LSTATUS result; LSTATUS result;
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1); len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
result = result = RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
if (FAILED(result)) { if (FAILED(result)) {
fprintf(stderr, "Error writing description\n"); fprintf(stderr, "Error writing description\n");
} }
@@ -114,26 +133,26 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
fprintf(stderr, "Error writing description\n"); fprintf(stderr, "Error writing description\n");
} }
result = RegSetKeyValueW( result = RegSetKeyValueW(key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
if (FAILED(result)) { if (FAILED(result)) {
fprintf(stderr, "Error writing icon\n"); fprintf(stderr, "Error writing icon\n");
} }
len = static_cast<DWORD>(lstrlenW(openCommand) + 1); len = static_cast<DWORD>(lstrlenW(openCommand) + 1);
result = RegSetKeyValueW( result = RegSetKeyValueW(key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
if (FAILED(result)) { if (FAILED(result)) {
fprintf(stderr, "Error writing command\n"); fprintf(stderr, "Error writing command\n");
} }
RegCloseKey(key); RegCloseKey(key);
} }
extern "C" void Discord_Register(const char *applicationId, const char *command) { extern "C" void Discord_Register(const char *applicationId, const char *command) {
wchar_t appId[32];
wchar_t appId[32]{};
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32); MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
wchar_t openCommand[1024]; wchar_t openCommand[1024]{};
const wchar_t *wcommand = nullptr; const wchar_t *wcommand = nullptr;
if (command && command[0]) { if (command && command[0]) {
const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand); const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand);
@@ -142,42 +161,6 @@ extern "C" void Discord_Register(const char *applicationId, const char *command)
} }
Discord_RegisterW(appId, wcommand); Discord_RegisterW(appId, wcommand);
}
extern "C" void Discord_RegisterSteamGame(const char *applicationId,
const char *steamId) {
wchar_t appId[32];
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
wchar_t wSteamId[32];
MultiByteToWideChar(CP_UTF8, 0, steamId, -1, wSteamId, 32);
HKEY key;
auto status = RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam", 0, KEY_READ, &key);
if (status != ERROR_SUCCESS) {
fprintf(stderr, "Error opening Steam key\n");
return;
}
wchar_t steamPath[MAX_PATH];
DWORD pathBytes = sizeof(steamPath);
status = RegQueryValueExW(key, L"SteamExe", nullptr, nullptr, (BYTE *)steamPath, &pathBytes);
RegCloseKey(key);
if (status != ERROR_SUCCESS || pathBytes < 1) {
fprintf(stderr, "Error reading SteamExe key\n");
return;
}
DWORD pathChars = pathBytes / sizeof(wchar_t);
for (DWORD i = 0; i < pathChars; ++i) {
if (steamPath[i] == L'/') {
steamPath[i] = L'\\';
}
}
wchar_t command[1024];
StringCbPrintfW(command, sizeof(command), L"\"%s\" steam://rungameid/%s", steamPath, wSteamId);
Discord_RegisterW(appId, command);
} }

View File

@@ -1,19 +1,44 @@
#include "discord_rpc.h" /*
* Copyright 2017 Discord, Inc.
#include "backoff.h" *
#include "discord_register.h" * Permission is hereby granted, free of charge, to any person obtaining a copy of
#include "msg_queue.h" * this software and associated documentation files (the "Software"), to deal in
#include "rpc_connection.h" * the Software without restriction, including without limitation the rights to
#include "serialization.h" * 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.
*
*/
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <mutex> #include <mutex>
#include <condition_variable> #include <condition_variable>
#include <thread> #include <thread>
namespace discord_rpc { #include "discord_rpc.h"
#include "discord_backoff.h"
#include "discord_register.h"
#include "discord_msg_queue.h"
#include "discord_rpc_connection.h"
#include "discord_serialization.h"
using namespace discord_rpc;
static void Discord_UpdateConnection();
namespace {
constexpr size_t MaxMessageSize { 16 * 1024 }; constexpr size_t MaxMessageSize { 16 * 1024 };
constexpr size_t MessageQueueSize { 8 }; constexpr size_t MessageQueueSize { 8 };
@@ -67,14 +92,12 @@ static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
static MsgQueue<User, JoinQueueSize> JoinAskQueue; static MsgQueue<User, JoinQueueSize> JoinAskQueue;
static User connectedUser; static User connectedUser;
// We want to auto connect, and retry on failure, but not as fast as possible. This does expoential // We want to auto connect, and retry on failure, but not as fast as possible. This does expoential backoff from 0.5 seconds to 1 minute
// backoff from 0.5 seconds to 1 minute
static Backoff ReconnectTimeMs(500, 60 * 1000); static Backoff ReconnectTimeMs(500, 60 * 1000);
static auto NextConnect = std::chrono::system_clock::now(); static auto NextConnect = std::chrono::system_clock::now();
static int Pid { 0 }; static int Pid { 0 };
static int Nonce { 1 }; static int Nonce { 1 };
static void Discord_UpdateConnection(void);
class IoThreadHolder { class IoThreadHolder {
private: private:
std::atomic_bool keepRunning { true }; std::atomic_bool keepRunning { true };
@@ -108,14 +131,55 @@ class IoThreadHolder {
~IoThreadHolder() { Stop(); } ~IoThreadHolder() { Stop(); }
}; };
static IoThreadHolder *IoThread { nullptr }; static IoThreadHolder *IoThread { nullptr };
static void UpdateReconnectTime() { static void UpdateReconnectTime() {
NextConnect = std::chrono::system_clock::now() +
std::chrono::duration<int64_t, std::milli> { ReconnectTimeMs.nextDelay() }; NextConnect = std::chrono::system_clock::now() + std::chrono::duration<int64_t, std::milli> { ReconnectTimeMs.nextDelay() };
} }
static void Discord_UpdateConnection(void) { static void SignalIOActivity() {
if (IoThread != nullptr) {
IoThread->Notify();
}
}
static bool RegisterForEvent(const char *evtName) {
auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) {
qmessage->length = JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
SendQueue.CommitAdd();
SignalIOActivity();
return true;
}
return false;
}
static bool DeregisterForEvent(const char *evtName) {
auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) {
qmessage->length = JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
SendQueue.CommitAdd();
SignalIOActivity();
return true;
}
return false;
}
} // namespace
static void Discord_UpdateConnection() {
if (!Connection) { if (!Connection) {
return; return;
} }
@@ -217,54 +281,18 @@ static void Discord_UpdateConnection(void) {
SendQueue.CommitSend(); SendQueue.CommitSend();
} }
} }
} }
static void SignalIOActivity() { extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
if (IoThread != nullptr) {
IoThread->Notify();
}
}
static bool RegisterForEvent(const char *evtName) {
auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) {
qmessage->length =
JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
SendQueue.CommitAdd();
SignalIOActivity();
return true;
}
return false;
}
static bool DeregisterForEvent(const char *evtName) {
auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) {
qmessage->length =
JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
SendQueue.CommitAdd();
SignalIOActivity();
return true;
}
return false;
}
extern "C" void Discord_Initialize(const char *applicationId,
DiscordEventHandlers *handlers,
int autoRegister,
const char *optionalSteamId) {
IoThread = new (std::nothrow) IoThreadHolder(); IoThread = new (std::nothrow) IoThreadHolder();
if (IoThread == nullptr) { if (IoThread == nullptr) {
return; return;
} }
if (autoRegister) { if (autoRegister) {
if (optionalSteamId && optionalSteamId[0]) { Discord_Register(applicationId, nullptr);
Discord_RegisterSteamGame(applicationId, optionalSteamId);
}
else {
Discord_Register(applicationId, nullptr);
}
} }
Pid = GetProcessId(); Pid = GetProcessId();
@@ -323,9 +351,11 @@ extern "C" void Discord_Initialize(const char *applicationId,
}; };
IoThread->Start(); IoThread->Start();
} }
extern "C" void Discord_Shutdown(void) { extern "C" void Discord_Shutdown() {
if (!Connection) { if (!Connection) {
return; return;
} }
@@ -341,16 +371,19 @@ extern "C" void Discord_Shutdown(void) {
} }
RpcConnection::Destroy(Connection); RpcConnection::Destroy(Connection);
} }
extern "C" void Discord_UpdatePresence(const DiscordRichPresence *presence) { extern "C" void Discord_UpdatePresence(const DiscordRichPresence *presence) {
{ {
std::lock_guard<std::mutex> guard(PresenceMutex); std::lock_guard<std::mutex> guard(PresenceMutex);
QueuedPresence.length = JsonWriteRichPresenceObj( QueuedPresence.length = JsonWriteRichPresenceObj(QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
UpdatePresence.exchange(true); UpdatePresence.exchange(true);
} }
SignalIOActivity(); SignalIOActivity();
} }
extern "C" void Discord_ClearPresence(void) { extern "C" void Discord_ClearPresence(void) {
@@ -358,20 +391,22 @@ extern "C" void Discord_ClearPresence(void) {
} }
extern "C" void Discord_Respond(const char *userId, /* DISCORD_REPLY_ */ int reply) { extern "C" void Discord_Respond(const char *userId, /* DISCORD_REPLY_ */ int reply) {
// if we are not connected, let's not batch up stale messages for later // if we are not connected, let's not batch up stale messages for later
if (!Connection || !Connection->IsOpen()) { if (!Connection || !Connection->IsOpen()) {
return; return;
} }
auto qmessage = SendQueue.GetNextAddMessage(); auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) { if (qmessage) {
qmessage->length = qmessage->length = JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
SendQueue.CommitAdd(); SendQueue.CommitAdd();
SignalIOActivity(); SignalIOActivity();
} }
} }
extern "C" void Discord_RunCallbacks(void) { extern "C" void Discord_RunCallbacks() {
// Note on some weirdness: internally we might connect, get other signals, disconnect any number // Note on some weirdness: internally we might connect, get other signals, disconnect any number
// of times inbetween calls here. Externally, we want the sequence to seem sane, so any other // of times inbetween calls here. Externally, we want the sequence to seem sane, so any other
// signals are book-ended by calls to ready and disconnect. // signals are book-ended by calls to ready and disconnect.
@@ -380,8 +415,8 @@ extern "C" void Discord_RunCallbacks(void) {
return; return;
} }
bool wasDisconnected = WasJustDisconnected.exchange(false); const bool wasDisconnected = WasJustDisconnected.exchange(false);
bool isConnected = Connection->IsOpen(); const bool isConnected = Connection->IsOpen();
if (isConnected) { if (isConnected) {
// if we are connected, disconnect cb first // if we are connected, disconnect cb first
@@ -394,10 +429,7 @@ extern "C" void Discord_RunCallbacks(void) {
if (WasJustConnected.exchange(false)) { if (WasJustConnected.exchange(false)) {
std::lock_guard<std::mutex> guard(HandlerMutex); std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.ready) { if (Handlers.ready) {
DiscordUser du { connectedUser.userId, DiscordUser du { connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
connectedUser.username,
connectedUser.discriminator,
connectedUser.avatar };
Handlers.ready(&du); Handlers.ready(&du);
} }
} }
@@ -429,7 +461,7 @@ extern "C" void Discord_RunCallbacks(void) {
// maybe show them in one common dialog and/or start fetching the avatars in parallel, and if // maybe show them in one common dialog and/or start fetching the avatars in parallel, and if
// not it should be trivial for the implementer to make a queue themselves. // not it should be trivial for the implementer to make a queue themselves.
while (JoinAskQueue.HavePendingSends()) { while (JoinAskQueue.HavePendingSends()) {
auto req = JoinAskQueue.GetNextSendMessage(); const auto req = JoinAskQueue.GetNextSendMessage();
{ {
std::lock_guard<std::mutex> guard(HandlerMutex); std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.joinRequest) { if (Handlers.joinRequest) {
@@ -447,9 +479,11 @@ extern "C" void Discord_RunCallbacks(void) {
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage); Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
} }
} }
} }
extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) { extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
if (newHandlers) { if (newHandlers) {
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \ #define HANDLE_EVENT_REGISTRATION(handler_name, event) \
if (!Handlers.handler_name && newHandlers->handler_name) { \ if (!Handlers.handler_name && newHandlers->handler_name) { \
@@ -472,8 +506,5 @@ extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
std::lock_guard<std::mutex> guard(HandlerMutex); std::lock_guard<std::mutex> guard(HandlerMutex);
Handlers = {}; Handlers = {};
} }
return;
} }
} // namespace discord_rpc

94
3rdparty/discord-rpc/discord_rpc.h vendored Normal file
View File

@@ -0,0 +1,94 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#ifndef DISCORD_RPC_H
#define DISCORD_RPC_H
#include <cstdint>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct DiscordRichPresence {
int type;
int status_display_type;
const char *name; /* max 128 bytes */
const char *state; /* max 128 bytes */
const char *details; /* max 128 bytes */
int64_t startTimestamp;
int64_t endTimestamp;
const char *largeImageKey; /* max 32 bytes */
const char *largeImageText; /* max 128 bytes */
const char *smallImageKey; /* max 32 bytes */
const char *smallImageText; /* max 128 bytes */
const char *partyId; /* max 128 bytes */
int partySize;
int partyMax;
int partyPrivacy;
const char *matchSecret; /* max 128 bytes */
const char *joinSecret; /* max 128 bytes */
const char *spectateSecret; /* max 128 bytes */
int8_t instance;
} DiscordRichPresence;
typedef struct DiscordUser {
const char *userId;
const char *username;
const char *discriminator;
const char *avatar;
} DiscordUser;
typedef struct DiscordEventHandlers {
void (*ready)(const DiscordUser *request);
void (*disconnected)(int errorCode, const char *message);
void (*errored)(int errorCode, const char *message);
void (*joinGame)(const char *joinSecret);
void (*spectateGame)(const char *spectateSecret);
void (*joinRequest)(const DiscordUser *request);
} DiscordEventHandlers;
#define DISCORD_REPLY_NO 0
#define DISCORD_REPLY_YES 1
#define DISCORD_REPLY_IGNORE 2
#define DISCORD_PARTY_PRIVATE 0
#define DISCORD_PARTY_PUBLIC 1
void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister);
void Discord_Shutdown(void);
// checks for incoming messages, dispatches callbacks
void Discord_RunCallbacks(void);
void Discord_UpdatePresence(const DiscordRichPresence *presence);
void Discord_ClearPresence(void);
void Discord_Respond(const char *userid, /* DISCORD_REPLY_ */ int reply);
void Discord_UpdateHandlers(DiscordEventHandlers *handlers);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif // DISCORD_RPC_H

View File

@@ -1,24 +1,52 @@
#include "rpc_connection.h" /*
#include "serialization.h" * Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#include "discord_rpc_connection.h"
#include "discord_serialization.h"
namespace discord_rpc { namespace discord_rpc {
static const int RpcVersion = 1; static const int RpcVersion = 1;
static RpcConnection Instance; static RpcConnection Instance;
/*static*/ RpcConnection *RpcConnection::Create(const char *applicationId) { RpcConnection *RpcConnection::Create(const char *applicationId) {
Instance.connection = BaseConnection::Create(); Instance.connection = BaseConnection::Create();
StringCopy(Instance.appId, applicationId); StringCopy(Instance.appId, applicationId);
return &Instance; return &Instance;
} }
/*static*/ void RpcConnection::Destroy(RpcConnection *&c) { void RpcConnection::Destroy(RpcConnection *&c) {
c->Close(); c->Close();
BaseConnection::Destroy(c->connection); BaseConnection::Destroy(c->connection);
c = nullptr; c = nullptr;
} }
void RpcConnection::Open() { void RpcConnection::Open() {
if (state == State::Connected) { if (state == State::Connected) {
return; return;
} }
@@ -51,17 +79,21 @@ void RpcConnection::Open() {
Close(); Close();
} }
} }
} }
void RpcConnection::Close() { void RpcConnection::Close() {
if (onDisconnect && (state == State::Connected || state == State::SentHandshake)) { if (onDisconnect && (state == State::Connected || state == State::SentHandshake)) {
onDisconnect(lastErrorCode, lastErrorMessage); onDisconnect(lastErrorCode, lastErrorMessage);
} }
connection->Close(); connection->Close();
state = State::Disconnected; state = State::Disconnected;
} }
bool RpcConnection::Write(const void *data, size_t length) { bool RpcConnection::Write(const void *data, size_t length) {
sendFrame.opcode = Opcode::Frame; sendFrame.opcode = Opcode::Frame;
memcpy(sendFrame.message, data, length); memcpy(sendFrame.message, data, length);
sendFrame.length = static_cast<uint32_t>(length); sendFrame.length = static_cast<uint32_t>(length);
@@ -69,14 +101,17 @@ bool RpcConnection::Write(const void *data, size_t length) {
Close(); Close();
return false; return false;
} }
return true; return true;
} }
bool RpcConnection::Read(JsonDocument &message) { bool RpcConnection::Read(JsonDocument &message) {
if (state != State::Connected && state != State::SentHandshake) { if (state != State::Connected && state != State::SentHandshake) {
return false; return false;
} }
MessageFrame readFrame; MessageFrame readFrame{};
for (;;) { for (;;) {
bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader)); bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader));
if (!didRead) { if (!didRead) {
@@ -127,7 +162,7 @@ bool RpcConnection::Read(JsonDocument &message) {
return false; return false;
} }
} }
} }
} // namespace discord_rpc } // namespace discord_rpc

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#ifndef DISCORD_RPC_CONNECTION_H
#define DISCORD_RPC_CONNECTION_H
#include "discord_connection.h"
#include "discord_serialization.h"
namespace discord_rpc {
// I took this from the buffer size libuv uses for named pipes; I suspect ours would usually be much smaller.
constexpr size_t MaxRpcFrameSize = 64 * 1024;
struct RpcConnection {
enum class ErrorCode : int {
Success = 0,
PipeClosed = 1,
ReadCorrupt = 2,
};
enum class Opcode : uint32_t {
Handshake = 0,
Frame = 1,
Close = 2,
Ping = 3,
Pong = 4,
};
struct MessageFrameHeader {
Opcode opcode;
uint32_t length;
};
struct MessageFrame : public MessageFrameHeader {
char message[MaxRpcFrameSize - sizeof(MessageFrameHeader)];
};
enum class State : uint32_t {
Disconnected,
SentHandshake,
AwaitingResponse,
Connected,
};
BaseConnection *connection { nullptr };
State state { State::Disconnected };
void (*onConnect)(JsonDocument &message) { nullptr };
void (*onDisconnect)(int errorCode, const char *message) { nullptr };
char appId[64] {};
int lastErrorCode { 0 };
char lastErrorMessage[256] {};
RpcConnection::MessageFrame sendFrame;
static RpcConnection *Create(const char *applicationId);
static void Destroy(RpcConnection *&);
inline bool IsOpen() const { return state == State::Connected; }
void Open();
void Close();
bool Write(const void *data, size_t length);
bool Read(JsonDocument &message);
};
} // namespace discord_rpc
#endif // DISCORD_RPC_CONNECTION_H

View File

@@ -1,11 +1,35 @@
#include "serialization.h" /*
#include "connection.h" * Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#include "discord_serialization.h"
#include "discord_connection.h"
#include "discord_rpc.h" #include "discord_rpc.h"
namespace discord_rpc { namespace discord_rpc {
template<typename T> template<typename T>
void NumberToString(char *dest, T number) { void NumberToString(char *dest, T number) {
if (!number) { if (!number) {
*dest++ = '0'; *dest++ = '0';
*dest++ = 0; *dest++ = 0;
@@ -26,6 +50,7 @@ void NumberToString(char *dest, T number) {
*dest++ = temp[place]; *dest++ = temp[place];
} }
*dest = 0; *dest = 0;
} }
// it's ever so slightly faster to not have to strlen the key // it's ever so slightly faster to not have to strlen the key
@@ -62,24 +87,25 @@ struct WriteArray {
template<typename T> template<typename T>
void WriteOptionalString(JsonWriter &w, T &k, const char *value) { void WriteOptionalString(JsonWriter &w, T &k, const char *value) {
if (value && value[0]) { if (value && value[0]) {
w.Key(k, sizeof(T) - 1); w.Key(k, sizeof(T) - 1);
w.String(value); w.String(value);
} }
} }
static void JsonWriteNonce(JsonWriter &writer, int nonce) { static void JsonWriteNonce(JsonWriter &writer, const int nonce) {
WriteKey(writer, "nonce"); WriteKey(writer, "nonce");
char nonceBuffer[32]; char nonceBuffer[32];
NumberToString(nonceBuffer, nonce); NumberToString(nonceBuffer, nonce);
writer.String(nonceBuffer); writer.String(nonceBuffer);
} }
size_t JsonWriteRichPresenceObj(char *dest, size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence) {
size_t maxLen,
int nonce,
int pid,
const DiscordRichPresence *presence) {
JsonWriter writer(dest, maxLen); JsonWriter writer(dest, maxLen);
{ {
@@ -102,6 +128,9 @@ size_t JsonWriteRichPresenceObj(char *dest,
if (presence->type >= 0 && presence->type <= 5) { if (presence->type >= 0 && presence->type <= 5) {
WriteKey(writer, "type"); WriteKey(writer, "type");
writer.Int(presence->type); writer.Int(presence->type);
WriteKey(writer, "status_display_type");
writer.Int(presence->status_display_type);
} }
WriteOptionalString(writer, "name", presence->name); WriteOptionalString(writer, "name", presence->name);
@@ -168,6 +197,7 @@ size_t JsonWriteRichPresenceObj(char *dest,
} }
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId) { size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId) {
JsonWriter writer(dest, maxLen); JsonWriter writer(dest, maxLen);
{ {
@@ -179,9 +209,11 @@ size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char
} }
return writer.Size(); return writer.Size();
} }
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) { size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
JsonWriter writer(dest, maxLen); JsonWriter writer(dest, maxLen);
{ {
@@ -197,9 +229,11 @@ size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const cha
} }
return writer.Size(); return writer.Size();
} }
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) { size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
JsonWriter writer(dest, maxLen); JsonWriter writer(dest, maxLen);
{ {
@@ -215,9 +249,11 @@ size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const c
} }
return writer.Size(); return writer.Size();
} }
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int reply, int nonce) { size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, const int reply, const int nonce) {
JsonWriter writer(dest, maxLen); JsonWriter writer(dest, maxLen);
{ {
@@ -243,7 +279,7 @@ size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int rep
} }
return writer.Size(); return writer.Size();
} }
} // namespace discord_rpc } // namespace discord_rpc

View File

@@ -1,9 +1,35 @@
#pragma once /*
* Copyright 2017 Discord, Inc.
*
* 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.
*
*/
#ifndef DISCORD_SERIALIZATION_H
#define DISCORD_SERIALIZATION_H
#include <rapidjson/document.h> #include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h> #include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h> #include <rapidjson/writer.h>
struct DiscordRichPresence;
namespace discord_rpc { namespace discord_rpc {
// if only there was a standard library function for this // if only there was a standard library function for this
@@ -24,12 +50,7 @@ inline size_t StringCopy(char (&dest)[Len], const char *src) {
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId); size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId);
// Commands // Commands
struct DiscordRichPresence; size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence);
size_t JsonWriteRichPresenceObj(char *dest,
size_t maxLen,
int nonce,
int pid,
const DiscordRichPresence *presence);
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName); size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName); size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
@@ -149,35 +170,44 @@ class JsonDocument : public JsonDocumentBase {
using JsonValue = rapidjson::GenericValue<UTF8, PoolAllocator>; using JsonValue = rapidjson::GenericValue<UTF8, PoolAllocator>;
inline JsonValue *GetObjMember(JsonValue *obj, const char *name) { inline JsonValue *GetObjMember(JsonValue *obj, const char *name) {
if (obj) { if (obj) {
auto member = obj->FindMember(name); auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsObject()) { if (member != obj->MemberEnd() && member->value.IsObject()) {
return &member->value; return &member->value;
} }
} }
return nullptr; return nullptr;
} }
inline int GetIntMember(JsonValue *obj, const char *name, int notFoundDefault = 0) { inline int GetIntMember(JsonValue *obj, const char *name, int notFoundDefault = 0) {
if (obj) { if (obj) {
auto member = obj->FindMember(name); auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsInt()) { if (member != obj->MemberEnd() && member->value.IsInt()) {
return member->value.GetInt(); return member->value.GetInt();
} }
} }
return notFoundDefault; return notFoundDefault;
} }
inline const char *GetStrMember(JsonValue *obj, inline const char *GetStrMember(JsonValue *obj, const char *name, const char *notFoundDefault = nullptr) {
const char *name,
const char *notFoundDefault = nullptr) {
if (obj) { if (obj) {
auto member = obj->FindMember(name); auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsString()) { if (member != obj->MemberEnd() && member->value.IsString()) {
return member->value.GetString(); return member->value.GetString();
} }
} }
return notFoundDefault; return notFoundDefault;
} }
} // namespace discord_rpc } // namespace discord_rpc
#endif // DISCORD_SERIALIZATION_H

View File

@@ -1,12 +0,0 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
void Discord_Register(const char* applicationId, const char* command);
void Discord_RegisterSteamGame(const char* applicationId, const char* steamId);
#ifdef __cplusplus
}
#endif

View File

@@ -1,77 +0,0 @@
#pragma once
#include <stdint.h>
// clang-format off
// clang-format on
namespace discord_rpc {
#ifdef __cplusplus
extern "C" {
#endif
typedef struct DiscordRichPresence {
int type;
const char* name; /* max 128 bytes */
const char* state; /* max 128 bytes */
const char* details; /* max 128 bytes */
int64_t startTimestamp;
int64_t endTimestamp;
const char* largeImageKey; /* max 32 bytes */
const char* largeImageText; /* max 128 bytes */
const char* smallImageKey; /* max 32 bytes */
const char* smallImageText; /* max 128 bytes */
const char* partyId; /* max 128 bytes */
int partySize;
int partyMax;
int partyPrivacy;
const char* matchSecret; /* max 128 bytes */
const char* joinSecret; /* max 128 bytes */
const char* spectateSecret; /* max 128 bytes */
int8_t instance;
} DiscordRichPresence;
typedef struct DiscordUser {
const char* userId;
const char* username;
const char* discriminator;
const char* avatar;
} DiscordUser;
typedef struct DiscordEventHandlers {
void (*ready)(const DiscordUser* request);
void (*disconnected)(int errorCode, const char* message);
void (*errored)(int errorCode, const char* message);
void (*joinGame)(const char* joinSecret);
void (*spectateGame)(const char* spectateSecret);
void (*joinRequest)(const DiscordUser* request);
} DiscordEventHandlers;
#define DISCORD_REPLY_NO 0
#define DISCORD_REPLY_YES 1
#define DISCORD_REPLY_IGNORE 2
#define DISCORD_PARTY_PRIVATE 0
#define DISCORD_PARTY_PUBLIC 1
void Discord_Initialize(const char* applicationId,
DiscordEventHandlers* handlers,
int autoRegister,
const char* optionalSteamId);
void Discord_Shutdown(void);
/* checks for incoming messages, dispatches callbacks */
void Discord_RunCallbacks(void);
void Discord_UpdatePresence(const DiscordRichPresence* presence);
void Discord_ClearPresence(void);
void Discord_Respond(const char* userid, /* DISCORD_REPLY_ */ int reply);
void Discord_UpdateHandlers(DiscordEventHandlers* handlers);
#ifdef __cplusplus
} /* extern "C" */
} // namespace discord_rpc
#endif

View File

@@ -1,41 +0,0 @@
set(DISCORD_RPC_SOURCES
../include/discord_rpc.h
../include/discord_register.h
discord_rpc.cpp
rpc_connection.h
rpc_connection.cpp
serialization.h
serialization.cpp
connection.h
backoff.h
msg_queue.h
)
if(UNIX)
list(APPEND DISCORD_RPC_SOURCES connection_unix.cpp)
if(APPLE)
list(APPEND DISCORD_RPC_SOURCES discord_register_osx.m)
add_definitions(-DDISCORD_OSX)
else()
list(APPEND DISCORD_RPC_SOURCES discord_register_linux.cpp)
add_definitions(-DDISCORD_LINUX)
endif()
endif()
if(WIN32)
list(APPEND DISCORD_RPC_SOURCES connection_win.cpp discord_register_win.cpp)
add_definitions(-DDISCORD_WINDOWS)
endif()
add_library(discord-rpc STATIC ${DISCORD_RPC_SOURCES})
if(APPLE)
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
endif()
if(WIN32)
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
endif()
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include)

View File

@@ -1,44 +0,0 @@
#pragma once
#include <algorithm>
#include <random>
#include <cstdint>
#include <ctime>
namespace discord_rpc {
struct Backoff {
int64_t minAmount;
int64_t maxAmount;
int64_t current;
int fails;
std::mt19937_64 randGenerator;
std::uniform_real_distribution<> randDistribution;
double rand01() { return randDistribution(randGenerator); }
Backoff(int64_t min, int64_t max)
: minAmount(min)
, maxAmount(max)
, current(min)
, fails(0)
, randGenerator(static_cast<uint64_t>(time(0)))
{
}
void reset()
{
fails = 0;
current = minAmount;
}
int64_t nextDelay()
{
++fails;
int64_t delay = static_cast<int64_t>(static_cast<double>(current) * 2.0 * rand01());
current = std::min(current + delay, maxAmount);
return current;
}
};
} // namespace discord_rpc

View File

@@ -1,22 +0,0 @@
#pragma once
// This is to wrap the platform specific kinds of connect/read/write.
#include <cstdlib>
namespace discord_rpc {
// not really connectiony, but need per-platform
int GetProcessId();
struct BaseConnection {
static BaseConnection *Create();
static void Destroy(BaseConnection *&);
bool isOpen { false };
bool Open();
bool Close();
bool Write(const void *data, size_t length);
bool Read(void *data, size_t length);
};
} // namespace discord_rpc

View File

@@ -1,104 +0,0 @@
#include "discord_rpc.h"
#include "discord_register.h"
#include <cstdio>
#include <errno.h>
#include <cstdlib>
#include <cstring>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
namespace {
static bool Mkdir(const char *path) {
int result = mkdir(path, 0755);
if (result == 0) {
return true;
}
if (errno == EEXIST) {
return true;
}
return false;
}
} // namespace
// we want to register games so we can run them from Discord client as discord-<appid>://
extern "C" void Discord_Register(const char *applicationId, const char *command) {
// Add a desktop file and update some mime handlers so that xdg-open does the right thing.
const char *home = getenv("HOME");
if (!home) {
return;
}
char exePath[1024];
if (!command || !command[0]) {
ssize_t size = readlink("/proc/self/exe", exePath, sizeof(exePath));
if (size <= 0 || size >= static_cast<ssize_t>(sizeof(exePath))) {
return;
}
exePath[size] = '\0';
command = exePath;
}
constexpr char desktopFileFormat[] = "[Desktop Entry]\n"
"Name=Game %s\n"
"Exec=%s %%u\n" // note: it really wants that %u in there
"Type=Application\n"
"NoDisplay=true\n"
"Categories=Discord;Games;\n"
"MimeType=x-scheme-handler/discord-%s;\n";
char desktopFile[2048];
int fileLen = snprintf(
desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId, command, applicationId);
if (fileLen <= 0) {
return;
}
char desktopFilename[256];
(void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId);
char desktopFilePath[1024];
(void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home);
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, "/share");
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, "/applications");
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, desktopFilename);
FILE *fp = fopen(desktopFilePath, "w");
if (fp) {
fwrite(desktopFile, 1, fileLen, fp);
fclose(fp);
}
else {
return;
}
char xdgMimeCommand[1024];
snprintf(xdgMimeCommand,
sizeof(xdgMimeCommand),
"xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s",
applicationId,
applicationId);
if (system(xdgMimeCommand) < 0) {
fprintf(stderr, "Failed to register mime handler\n");
}
}
extern "C" void Discord_RegisterSteamGame(const char *applicationId,
const char *steamId) {
char command[256];
sprintf(command, "xdg-open steam://rungameid/%s", steamId);
Discord_Register(applicationId, command);
}

View File

@@ -1,80 +0,0 @@
#include <stdio.h>
#include <sys/stat.h>
#import <AppKit/AppKit.h>
#include "discord_register.h"
static void RegisterCommand(const char* applicationId, const char* command)
{
// There does not appear to be a way to register arbitrary commands on OSX, so instead we'll save the command
// to a file in the Discord config path, and when it is needed, Discord can try to load the file there, open
// the command therein (will pass to js's window.open, so requires a url-like thing)
// Note: will not work for sandboxed apps
NSString *home = NSHomeDirectory();
if (!home) {
return;
}
NSString *path = [[[[[[home stringByAppendingPathComponent:@"Library"]
stringByAppendingPathComponent:@"Application Support"]
stringByAppendingPathComponent:@"discord"]
stringByAppendingPathComponent:@"games"]
stringByAppendingPathComponent:[NSString stringWithUTF8String:applicationId]]
stringByAppendingPathExtension:@"json"];
[[NSFileManager defaultManager] createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
NSString *jsonBuffer = [NSString stringWithFormat:@"{\"command\": \"%s\"}", command];
[jsonBuffer writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:nil];
}
static void RegisterURL(const char* applicationId)
{
char url[256];
snprintf(url, sizeof(url), "discord-%s", applicationId);
CFStringRef cfURL = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
NSString* myBundleId = [[NSBundle mainBundle] bundleIdentifier];
if (!myBundleId) {
fprintf(stderr, "No bundle id found\n");
return;
}
NSURL* myURL = [[NSBundle mainBundle] bundleURL];
if (!myURL) {
fprintf(stderr, "No bundle url found\n");
return;
}
OSStatus status = LSSetDefaultHandlerForURLScheme(cfURL, (__bridge CFStringRef)myBundleId);
if (status != noErr) {
fprintf(stderr, "Error in LSSetDefaultHandlerForURLScheme: %d\n", (int)status);
return;
}
status = LSRegisterURL((__bridge CFURLRef)myURL, true);
if (status != noErr) {
fprintf(stderr, "Error in LSRegisterURL: %d\n", (int)status);
}
}
void Discord_Register(const char* applicationId, const char* command)
{
if (command) {
RegisterCommand(applicationId, command);
}
else {
// raii lite
@autoreleasepool {
RegisterURL(applicationId);
}
}
}
void Discord_RegisterSteamGame(const char* applicationId, const char* steamId)
{
char command[256];
snprintf(command, 256, "steam://rungameid/%s", steamId);
Discord_Register(applicationId, command);
}

View File

@@ -1,40 +0,0 @@
#pragma once
#include <atomic>
// A simple queue. No locks, but only works with a single thread as producer and a single thread as
// a consumer. Mutex up as needed.
namespace discord_rpc {
template <typename ElementType, std::size_t QueueSize>
class MsgQueue {
ElementType queue_[QueueSize];
std::atomic_uint nextAdd_{0};
std::atomic_uint nextSend_{0};
std::atomic_uint pendingSends_{0};
public:
MsgQueue() {}
ElementType* GetNextAddMessage()
{
// if we are falling behind, bail
if (pendingSends_.load() >= QueueSize) {
return nullptr;
}
auto index = (nextAdd_++) % QueueSize;
return &queue_[index];
}
void CommitAdd() { ++pendingSends_; }
bool HavePendingSends() const { return pendingSends_.load() != 0; }
ElementType* GetNextSendMessage()
{
auto index = (nextSend_++) % QueueSize;
return &queue_[index];
}
void CommitSend() { --pendingSends_; }
};
} // namespace discord_rpc

View File

@@ -1,64 +0,0 @@
#pragma once
#include "connection.h"
#include "serialization.h"
namespace discord_rpc {
// I took this from the buffer size libuv uses for named pipes; I suspect ours would usually be much
// smaller.
constexpr size_t MaxRpcFrameSize = 64 * 1024;
struct RpcConnection {
enum class ErrorCode : int {
Success = 0,
PipeClosed = 1,
ReadCorrupt = 2,
};
enum class Opcode : uint32_t {
Handshake = 0,
Frame = 1,
Close = 2,
Ping = 3,
Pong = 4,
};
struct MessageFrameHeader {
Opcode opcode;
uint32_t length;
};
struct MessageFrame : public MessageFrameHeader {
char message[MaxRpcFrameSize - sizeof(MessageFrameHeader)];
};
enum class State : uint32_t {
Disconnected,
SentHandshake,
AwaitingResponse,
Connected,
};
BaseConnection *connection { nullptr };
State state { State::Disconnected };
void (*onConnect)(JsonDocument &message) { nullptr };
void (*onDisconnect)(int errorCode, const char *message) { nullptr };
char appId[64] {};
int lastErrorCode { 0 };
char lastErrorMessage[256] {};
RpcConnection::MessageFrame sendFrame;
static RpcConnection *Create(const char *applicationId);
static void Destroy(RpcConnection *&);
inline bool IsOpen() const { return state == State::Connected; }
void Open();
void Close();
bool Write(const void *data, size_t length);
bool Read(JsonDocument &message);
};
} // namespace discord_rpc

View File

@@ -259,7 +259,16 @@ if(APPLE)
endif() endif()
if(WIN32) if(WIN32)
find_package(getopt-win REQUIRED) find_package(getopt NAMES getopt getopt-win unofficial-getopt-win32 REQUIRED)
if(TARGET getopt::getopt)
set(GETOPT_LIBRARIES getopt::getopt)
elseif(TARGET getopt-win::getopt)
set(GETOPT_LIBRARIES getopt-win::getopt)
elseif(TARGET getopt::getopt_shared)
set(GETOPT_LIBRARIES getopt::getopt_shared)
else()
message(FATAL_ERROR "Missing getopt")
endif()
endif() endif()
if(APPLE OR WIN32) if(APPLE OR WIN32)
@@ -1494,7 +1503,7 @@ endif()
if(HAVE_DISCORD_RPC) if(HAVE_DISCORD_RPC)
add_subdirectory(3rdparty/discord-rpc) add_subdirectory(3rdparty/discord-rpc)
target_include_directories(strawberry_lib PUBLIC 3rdparty/discord-rpc/include) target_include_directories(strawberry_lib PUBLIC 3rdparty/discord-rpc)
endif() endif()
if(HAVE_TRANSLATIONS) if(HAVE_TRANSLATIONS)
@@ -1554,9 +1563,10 @@ target_link_libraries(strawberry_lib PUBLIC
$<$<BOOL:${HAVE_MTP}>:PkgConfig::LIBMTP> $<$<BOOL:${HAVE_MTP}>:PkgConfig::LIBMTP>
$<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF> $<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF>
$<$<BOOL:${HAVE_QTSPARKLE}>:qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle> $<$<BOOL:${HAVE_QTSPARKLE}>:qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle>
$<$<BOOL:${WIN32}>:dsound dwmapi getopt-win::getopt> $<$<BOOL:${WIN32}>:dsound dwmapi ${GETOPT_LIBRARIES}>
$<$<BOOL:${MSVC}>:WindowsApp> $<$<BOOL:${MSVC}>:WindowsApp>
KDAB::kdsingleapplication KDAB::kdsingleapplication
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
) )
if(APPLE) if(APPLE)
@@ -1575,10 +1585,6 @@ if(APPLE)
endif() endif()
endif() endif()
if(HAVE_DISCORD_RPC)
target_link_libraries(strawberry_lib PRIVATE discord-rpc)
endif()
target_link_libraries(strawberry PUBLIC strawberry_lib) target_link_libraries(strawberry PUBLIC strawberry_lib)
if(NOT APPLE) if(NOT APPLE)

View File

@@ -2,6 +2,68 @@ Strawberry Music Player
======================= =======================
ChangeLog ChangeLog
Version 1.2.13 (2025.08.31):
Bugfixes:
* Fixed playlist alternating row colors no longer working with some styles (#1806)
* Fixed "Open Audio CD" no longer working (#1803)
* Fixed systemtray icon playback status not working with scaling (#1782)
* Fixed build without MusicBrainz (#1799)
* Fixed build without MTP (#1804)
Enhancements:
* Added Discord status text option (#1796)
* Read Vorbis/FLAC "Other" embedded covers if front cover is not available (#1793)
Version 1.2.12 (2025.08.12):
Bugfixes:
* Fixed scrobbling for radio streams.
* Fixed CDDA memory leaks.
* Fixed device view CDDA loading (#1676).
* Fixed collection directory editing (#1767).
* Fixed devices sometimes being duplicated in the database.
* Fixed alternating playlist row colors with Windows 11 style.
* Fixed broken file filter for GME formats.
* Fixed collection scanning for GME formats.
* Fixed Chartlyrics.
* Fixed network cache file descriptor leak on lyrics search with workaround for QTBUG-135641.
* Fixed parsing Tidal urls with certain stream URL replies.
* Fixed pixelated window icon on Wayland (#1753).
* Fixed saving collection grouping with special characters in the name (#1758).
* Fixed Spotify token not automatically updated on renewal when playing (#1769).
* (macOS/Windows) Fixed network cache file descriptor leak with patch for QTBUG-135641.
* (Windows|MSVC) Fixed installer to not restart the computer after installing Visual C++ Redistributable.
Enhancements:
* Implemented edit tag dialog reset for year, track, disc and rating.
* Added ALAC to supported filetypes for iPods.
* Added CD-TEXT support.
* Added back Genius lyrics.
* Added support for reporting more info to ListenBrainz.
* Added support for BPM, mood and initial key tags.
* Added support for sort tags to collection, playlists and smart playlists.
Version 1.2.11 (2025.05.15):
* Fixed playlist songs sometimes not updated with new cover.
* Fixed context album cover showing even when it's disabled in the setting (#1744).
* Fixed crash when dragging songs to a closed playlist (#1741).
* Enable startup notify in desktop file.
* (Windows|MSVC) Add experimental support for native ARM64 builds.
* (Windows|MinGW) Fixed crash on exit.
Version 1.2.10 (2025.04.18):
Bugfixes:
* Fixed Discord rich presence showing bogus artist and album.
* Fixed incorrect ID3v2 comment tag.
* (macOS|Windows MSVC) Fixed stuck playback of some streams.
Enhancements:
* Removed Genius lyrics (longer working properly because of website changes).
* (macOS|Windows MSVC) Added back Spotify
Version 1.2.9 (2025.04.08): Version 1.2.9 (2025.04.08):
Bugfixes: Bugfixes:

View File

@@ -53,7 +53,7 @@ Funding developers is a way to contribute to open source projects you appreciate
* Edit tags on audio files * Edit tags on audio files
* Fetch tags from MusicBrainz * 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/) * 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/) and [elyrics.net](https://www.elyrics.net/) * 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/), [letras.mus.br](https://www.letras.mus.br) and [LyricFind](https://lyrics.lyricfind.com)
* Support for multiple backends * Support for multiple backends
* Audio analyzer * Audio analyzer
* Audio equalizer * Audio equalizer

View File

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

View File

@@ -12,6 +12,7 @@
<file>schema/schema-18.sql</file> <file>schema/schema-18.sql</file>
<file>schema/schema-19.sql</file> <file>schema/schema-19.sql</file>
<file>schema/schema-20.sql</file> <file>schema/schema-20.sql</file>
<file>schema/schema-21.sql</file>
<file>schema/device-schema.sql</file> <file>schema/device-schema.sql</file>
<file>style/strawberry.css</file> <file>style/strawberry.css</file>
<file>style/smartplaylistsearchterm.css</file> <file>style/smartplaylistsearchterm.css</file>

View File

@@ -12,9 +12,13 @@ CREATE TABLE device_%deviceid_subdirectories (
CREATE TABLE device_%deviceid_songs ( CREATE TABLE device_%deviceid_songs (
title TEXT, title TEXT,
titlesort TEXT,
album TEXT, album TEXT,
albumsort TEXT,
artist TEXT, artist TEXT,
artistsort TEXT,
albumartist TEXT, albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1, track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1, disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1, year INTEGER NOT NULL DEFAULT -1,
@@ -22,7 +26,9 @@ CREATE TABLE device_%deviceid_songs (
genre TEXT, genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0, compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT, composer TEXT,
composersort TEXT,
performer TEXT, performer TEXT,
performersort TEXT,
grouping TEXT, grouping TEXT,
comment TEXT, comment TEXT,
lyrics TEXT, lyrics TEXT,
@@ -86,7 +92,11 @@ CREATE TABLE device_%deviceid_songs (
musicbrainz_work_id TEXT, musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL, ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
); );
@@ -94,4 +104,4 @@ CREATE INDEX idx_device_%deviceid_songs_album ON device_%deviceid_songs (album);
CREATE INDEX idx_device_%deviceid_songs_comp_artist ON device_%deviceid_songs (compilation_effective, artist); CREATE INDEX idx_device_%deviceid_songs_comp_artist ON device_%deviceid_songs (compilation_effective, artist);
UPDATE devices SET schema_version=5 WHERE ROWID=%deviceid; UPDATE devices SET schema_version=6 WHERE ROWID=%deviceid;

43
data/schema/schema-21.sql Normal file
View File

@@ -0,0 +1,43 @@
DROP INDEX IF EXISTS idx_albumartistsort;
DROP INDEX IF EXISTS idx_albumsort;
DROP INDEX IF EXISTS idx_artistsort;
DROP INDEX IF EXISTS idx_composersort;
DROP INDEX IF EXISTS idx_performersort;
DROP INDEX IF EXISTS idx_titlesort;
ALTER TABLE %allsongstables ADD COLUMN albumartistsort TEXT;
ALTER TABLE %allsongstables ADD COLUMN albumsort TEXT;
ALTER TABLE %allsongstables ADD COLUMN artistsort TEXT;
ALTER TABLE %allsongstables ADD COLUMN composersort TEXT;
ALTER TABLE %allsongstables ADD COLUMN performersort TEXT;
ALTER TABLE %allsongstables ADD COLUMN titlesort TEXT;
ALTER TABLE %allsongstables ADD COLUMN bpm REAL;
ALTER TABLE %allsongstables ADD COLUMN mood TEXT;
ALTER TABLE %allsongstables ADD COLUMN initial_key TEXT;
CREATE INDEX IF NOT EXISTS idx_albumartistsort ON songs (albumartistsort);
CREATE INDEX IF NOT EXISTS idx_albumsort ON songs (album);
CREATE INDEX IF NOT EXISTS idx_artistsort ON songs (artistsort);
CREATE INDEX IF NOT EXISTS idx_composersort ON songs (title);
CREATE INDEX IF NOT EXISTS idx_performersort ON songs (title);
CREATE INDEX IF NOT EXISTS idx_titlesort ON songs (title);
UPDATE schema_version SET version=21;

View File

@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
DELETE FROM schema_version; DELETE FROM schema_version;
INSERT INTO schema_version (version) VALUES (20); INSERT INTO schema_version (version) VALUES (21);
CREATE TABLE IF NOT EXISTS directories ( CREATE TABLE IF NOT EXISTS directories (
path TEXT NOT NULL, path TEXT NOT NULL,
@@ -20,9 +20,13 @@ CREATE TABLE IF NOT EXISTS subdirectories (
CREATE TABLE IF NOT EXISTS songs ( CREATE TABLE IF NOT EXISTS songs (
title TEXT, title TEXT,
titlesort TEXT,
album TEXT, album TEXT,
albumsort TEXT,
artist TEXT, artist TEXT,
artistsort TEXT,
albumartist TEXT, albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1, track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1, disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1, year INTEGER NOT NULL DEFAULT -1,
@@ -30,7 +34,9 @@ CREATE TABLE IF NOT EXISTS songs (
genre TEXT, genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0, compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT, composer TEXT,
composersort TEXT,
performer TEXT, performer TEXT,
performersort TEXT,
grouping TEXT, grouping TEXT,
comment TEXT, comment TEXT,
lyrics TEXT, lyrics TEXT,
@@ -94,16 +100,24 @@ CREATE TABLE IF NOT EXISTS songs (
musicbrainz_work_id TEXT, musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL, ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
); );
CREATE TABLE IF NOT EXISTS subsonic_songs ( CREATE TABLE IF NOT EXISTS subsonic_songs (
title TEXT, title TEXT,
titlesort TEXT,
album TEXT, album TEXT,
albumsort TEXT,
artist TEXT, artist TEXT,
artistsort TEXT,
albumartist TEXT, albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1, track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1, disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1, year INTEGER NOT NULL DEFAULT -1,
@@ -111,7 +125,9 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
genre TEXT, genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0, compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT, composer TEXT,
composersort TEXT,
performer TEXT, performer TEXT,
performersort TEXT,
grouping TEXT, grouping TEXT,
comment TEXT, comment TEXT,
lyrics TEXT, lyrics TEXT,
@@ -175,16 +191,24 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
musicbrainz_work_id TEXT, musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL, ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
); );
CREATE TABLE IF NOT EXISTS tidal_artists_songs ( CREATE TABLE IF NOT EXISTS tidal_artists_songs (
title TEXT, title TEXT,
titlesort TEXT,
album TEXT, album TEXT,
albumsort TEXT,
artist TEXT, artist TEXT,
artistsort TEXT,
albumartist TEXT, albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1, track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1, disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1, year INTEGER NOT NULL DEFAULT -1,
@@ -192,7 +216,9 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
genre TEXT, genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0, compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT, composer TEXT,
composersort TEXT,
performer TEXT, performer TEXT,
performersort TEXT,
grouping TEXT, grouping TEXT,
comment TEXT, comment TEXT,
lyrics TEXT, lyrics TEXT,
@@ -256,16 +282,24 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
musicbrainz_work_id TEXT, musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL, ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
); );
CREATE TABLE IF NOT EXISTS tidal_albums_songs ( CREATE TABLE IF NOT EXISTS tidal_albums_songs (
title TEXT, title TEXT,
titlesort TEXT,
album TEXT, album TEXT,
albumsort TEXT,
artist TEXT, artist TEXT,
artistsort TEXT,
albumartist TEXT, albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1, track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1, disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1, year INTEGER NOT NULL DEFAULT -1,
@@ -273,7 +307,9 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
genre TEXT, genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0, compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT, composer TEXT,
composersort TEXT,
performer TEXT, performer TEXT,
performersort TEXT,
grouping TEXT, grouping TEXT,
comment TEXT, comment TEXT,
lyrics TEXT, lyrics TEXT,
@@ -337,16 +373,24 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
musicbrainz_work_id TEXT, musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL, ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
); );
CREATE TABLE IF NOT EXISTS tidal_songs ( CREATE TABLE IF NOT EXISTS tidal_songs (
title TEXT, title TEXT,
titlesort TEXT,
album TEXT, album TEXT,
albumsort TEXT,
artist TEXT, artist TEXT,
artistsort TEXT,
albumartist TEXT, albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1, track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1, disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1, year INTEGER NOT NULL DEFAULT -1,
@@ -354,7 +398,9 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
genre TEXT, genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0, compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT, composer TEXT,
composersort TEXT,
performer TEXT, performer TEXT,
performersort TEXT,
grouping TEXT, grouping TEXT,
comment TEXT, comment TEXT,
lyrics TEXT, lyrics TEXT,
@@ -418,16 +464,24 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
musicbrainz_work_id TEXT, musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL, ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
); );
CREATE TABLE IF NOT EXISTS spotify_artists_songs ( CREATE TABLE IF NOT EXISTS spotify_artists_songs (
title TEXT, title TEXT,
titlesort TEXT,
album TEXT, album TEXT,
albumsort TEXT,
artist TEXT, artist TEXT,
artistsort TEXT,
albumartist TEXT, albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1, track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1, disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1, year INTEGER NOT NULL DEFAULT -1,
@@ -435,7 +489,9 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
genre TEXT, genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0, compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT, composer TEXT,
composersort TEXT,
performer TEXT, performer TEXT,
performersort TEXT,
grouping TEXT, grouping TEXT,
comment TEXT, comment TEXT,
lyrics TEXT, lyrics TEXT,
@@ -499,16 +555,24 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
musicbrainz_work_id TEXT, musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL, ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
); );
CREATE TABLE IF NOT EXISTS spotify_albums_songs ( CREATE TABLE IF NOT EXISTS spotify_albums_songs (
title TEXT, title TEXT,
titlesort TEXT,
album TEXT, album TEXT,
albumsort TEXT,
artist TEXT, artist TEXT,
artistsort TEXT,
albumartist TEXT, albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1, track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1, disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1, year INTEGER NOT NULL DEFAULT -1,
@@ -516,7 +580,9 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
genre TEXT, genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0, compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT, composer TEXT,
composersort TEXT,
performer TEXT, performer TEXT,
performersort TEXT,
grouping TEXT, grouping TEXT,
comment TEXT, comment TEXT,
lyrics TEXT, lyrics TEXT,
@@ -580,16 +646,24 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
musicbrainz_work_id TEXT, musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL, ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
); );
CREATE TABLE IF NOT EXISTS spotify_songs ( CREATE TABLE IF NOT EXISTS spotify_songs (
title TEXT, title TEXT,
titlesort TEXT,
album TEXT, album TEXT,
albumsort TEXT,
artist TEXT, artist TEXT,
artistsort TEXT,
albumartist TEXT, albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1, track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1, disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1, year INTEGER NOT NULL DEFAULT -1,
@@ -597,7 +671,9 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
genre TEXT, genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0, compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT, composer TEXT,
composersort TEXT,
performer TEXT, performer TEXT,
performersort TEXT,
grouping TEXT, grouping TEXT,
comment TEXT, comment TEXT,
lyrics TEXT, lyrics TEXT,
@@ -661,16 +737,24 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
musicbrainz_work_id TEXT, musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL, ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
); );
CREATE TABLE IF NOT EXISTS qobuz_artists_songs ( CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
title TEXT, title TEXT,
titlesort TEXT,
album TEXT, album TEXT,
albumsort TEXT,
artist TEXT, artist TEXT,
artistsort TEXT,
albumartist TEXT, albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1, track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1, disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1, year INTEGER NOT NULL DEFAULT -1,
@@ -678,7 +762,9 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
genre TEXT, genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0, compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT, composer TEXT,
composersort TEXT,
performer TEXT, performer TEXT,
performersort TEXT,
grouping TEXT, grouping TEXT,
comment TEXT, comment TEXT,
lyrics TEXT, lyrics TEXT,
@@ -742,16 +828,24 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
musicbrainz_work_id TEXT, musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL, ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
); );
CREATE TABLE IF NOT EXISTS qobuz_albums_songs ( CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
title TEXT, title TEXT,
titlesort TEXT,
album TEXT, album TEXT,
albumsort TEXT,
artist TEXT, artist TEXT,
artistsort TEXT,
albumartist TEXT, albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1, track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1, disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1, year INTEGER NOT NULL DEFAULT -1,
@@ -759,7 +853,9 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
genre TEXT, genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0, compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT, composer TEXT,
composersort TEXT,
performer TEXT, performer TEXT,
performersort TEXT,
grouping TEXT, grouping TEXT,
comment TEXT, comment TEXT,
lyrics TEXT, lyrics TEXT,
@@ -823,16 +919,24 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
musicbrainz_work_id TEXT, musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL, ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
); );
CREATE TABLE IF NOT EXISTS qobuz_songs ( CREATE TABLE IF NOT EXISTS qobuz_songs (
title TEXT, title TEXT,
titlesort TEXT,
album TEXT, album TEXT,
albumsort TEXT,
artist TEXT, artist TEXT,
artistsort TEXT,
albumartist TEXT, albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1, track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1, disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1, year INTEGER NOT NULL DEFAULT -1,
@@ -840,7 +944,9 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
genre TEXT, genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0, compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT, composer TEXT,
composersort TEXT,
performer TEXT, performer TEXT,
performersort TEXT,
grouping TEXT, grouping TEXT,
comment TEXT, comment TEXT,
lyrics TEXT, lyrics TEXT,
@@ -904,7 +1010,11 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
musicbrainz_work_id TEXT, musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL, ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
); );
@@ -931,9 +1041,13 @@ CREATE TABLE IF NOT EXISTS playlist_items (
playlist_url TEXT, playlist_url TEXT,
title TEXT, title TEXT,
titlesort TEXT,
album TEXT, album TEXT,
albumsort TEXT,
artist TEXT, artist TEXT,
artistsort TEXT,
albumartist TEXT, albumartist TEXT,
albumartistsort TEXT,
track INTEGER, track INTEGER,
disc INTEGER, disc INTEGER,
year INTEGER, year INTEGER,
@@ -941,7 +1055,9 @@ CREATE TABLE IF NOT EXISTS playlist_items (
genre TEXT, genre TEXT,
compilation INTEGER DEFAULT 0, compilation INTEGER DEFAULT 0,
composer TEXT, composer TEXT,
composersort TEXT,
performer TEXT, performer TEXT,
performersort TEXT,
grouping TEXT, grouping TEXT,
comment TEXT, comment TEXT,
lyrics TEXT, lyrics TEXT,
@@ -1005,7 +1121,11 @@ CREATE TABLE IF NOT EXISTS playlist_items (
musicbrainz_work_id TEXT, musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL, ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
); );
@@ -1032,10 +1152,22 @@ CREATE INDEX IF NOT EXISTS idx_comp_artist ON songs (compilation_effective, arti
CREATE INDEX IF NOT EXISTS idx_albumartist ON songs (albumartist); CREATE INDEX IF NOT EXISTS idx_albumartist ON songs (albumartist);
CREATE INDEX IF NOT EXISTS idx_albumartistsort ON songs (albumartistsort);
CREATE INDEX IF NOT EXISTS idx_artist ON songs (artist); CREATE INDEX IF NOT EXISTS idx_artist ON songs (artist);
CREATE INDEX IF NOT EXISTS idx_artistsort ON songs (artistsort);
CREATE INDEX IF NOT EXISTS idx_album ON songs (album); CREATE INDEX IF NOT EXISTS idx_album ON songs (album);
CREATE INDEX IF NOT EXISTS idx_albumsort ON songs (album);
CREATE INDEX IF NOT EXISTS idx_title ON songs (title); CREATE INDEX IF NOT EXISTS idx_title ON songs (title);
CREATE INDEX IF NOT EXISTS idx_titlesort ON songs (title);
CREATE INDEX IF NOT EXISTS idx_composersort ON songs (title);
CREATE INDEX IF NOT EXISTS idx_performersort 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 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;

2
debian/control vendored
View File

@@ -60,7 +60,7 @@ Description: music player and music collection organizer
- Edit tags on audio files - Edit tags on audio files
- Automatically retrieve tags from MusicBrainz - Automatically retrieve tags from MusicBrainz
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify - 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 and elyrics.net - Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind
- Audio analyzer - Audio analyzer
- Audio equalizer - Audio equalizer
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic - Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic

View File

@@ -31,7 +31,7 @@
<li>Edit tags on audio files</li> <li>Edit tags on audio files</li>
<li>Automatically retrieve tags from MusicBrainz</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>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 and elyrics.net</li> <li>Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind</li>
<li>Audio analyzer and equalizer</li> <li>Audio analyzer and equalizer</li>
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li> <li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
<li>Scrobbler with support for Last.fm, Libre.fm and ListenBrainz</li> <li>Scrobbler with support for Last.fm, Libre.fm and ListenBrainz</li>
@@ -51,6 +51,10 @@
</screenshots> </screenshots>
<update_contact>eclipseo@fedoraproject.org</update_contact> <update_contact>eclipseo@fedoraproject.org</update_contact>
<releases> <releases>
<release version="1.2.13" date="2025-08-31"/>
<release version="1.2.12" date="2025-08-12"/>
<release version="1.2.11" date="2025-05-15"/>
<release version="1.2.10" date="2025-04-18"/>
<release version="1.2.9" date="2025-04-08"/> <release version="1.2.9" date="2025-04-08"/>
<release version="1.2.8" date="2025-04-05"/> <release version="1.2.8" date="2025-04-05"/>
<release version="1.2.7" date="2025-01-31"/> <release version="1.2.7" date="2025-01-31"/>

View File

@@ -13,8 +13,7 @@ TryExec=strawberry
Icon=strawberry Icon=strawberry
Terminal=false Terminal=false
Categories=AudioVideo;Player;Qt;Audio; Categories=AudioVideo;Player;Qt;Audio;
Keywords=Audio;Player; Keywords=Audio;Player;Clementine;
StartupNotify=false
MimeType=x-content/audio-player;application/ogg;application/x-ogg;application/x-ogm-audio;audio/flac;audio/ogg;audio/vorbis;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/vnd.rn-realaudio;audio/x-flac;audio/x-oggflac;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-speex;audio/x-wav;audio/x-wavpack;audio/x-ape;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-pn-realaudio;audio/x-scpls;video/x-ms-asf;x-scheme-handler/tidal; MimeType=x-content/audio-player;application/ogg;application/x-ogg;application/x-ogm-audio;audio/flac;audio/ogg;audio/vorbis;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/vnd.rn-realaudio;audio/x-flac;audio/x-oggflac;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-speex;audio/x-wav;audio/x-wavpack;audio/x-ape;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-pn-realaudio;audio/x-scpls;video/x-ms-asf;x-scheme-handler/tidal;
StartupWMClass=strawberry StartupWMClass=strawberry
Actions=Play-Pause;Stop;StopAfterCurrent;Previous;Next; Actions=Play-Pause;Stop;StopAfterCurrent;Previous;Next;

View File

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

View File

@@ -93,7 +93,7 @@ Features:
- Edit tags on audio files - Edit tags on audio files
- Automatically retrieve tags from MusicBrainz - Automatically retrieve tags from MusicBrainz
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify - 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 and elyrics.net - Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind
- Support for multiple backends - Support for multiple backends
- Audio analyzer - Audio analyzer
- Audio equalizer - Audio equalizer

View File

@@ -21,6 +21,10 @@
!define arch_x64 !define arch_x64
!else if "@ARCH@" == "x86_64-w64-mingw32.shared" !else if "@ARCH@" == "x86_64-w64-mingw32.shared"
!define arch_x64 !define arch_x64
!else if "@ARCH@" == "arm64"
!define arch_arm64
!else
!error "Missing ARCH"
!endif !endif
!ifdef arch_x86 !ifdef arch_x86
@@ -31,6 +35,10 @@
!define arch "x64" !define arch "x64"
!endif !endif
!ifdef arch_arm64
!define arch "arm64"
!endif
!if "@CMAKE_BUILD_TYPE@" == "Release" !if "@CMAKE_BUILD_TYPE@" == "Release"
!define release !define release
@@ -38,6 +46,8 @@
!define release !define release
!else if "@CMAKE_BUILD_TYPE@" == "Debug" !else if "@CMAKE_BUILD_TYPE@" == "Debug"
!define debug !define debug
!else
!error "Missing CMAKE_BUILD_TYPE"
!endif !endif
!ifdef release !ifdef release
@@ -70,7 +80,7 @@
!ifdef arch_x86 !ifdef arch_x86
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player Debug" !define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player Debug"
!endif !endif
!ifdef arch_x64 !ifdef arch_x64 || arch_arm64
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player Debug" !define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player Debug"
!endif !endif
!else !else
@@ -80,7 +90,7 @@
!ifdef arch_x86 !ifdef arch_x86
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player" !define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player"
!endif !endif
!ifdef arch_x64 !ifdef arch_x64 || arch_arm64
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player" !define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player"
!endif !endif
!endif !endif
@@ -214,7 +224,7 @@ Function InstallMSVCRuntime
; ${If} $R0 == "" ; ${If} $R0 == ""
SetDetailsView hide SetDetailsView hide
; inetc::get /caption "Downloading..." "https://aka.ms/vs/17/release/${vc_redist_file}" "$TEMP\${vc_redist_file}" /end ; inetc::get /caption "Downloading..." "https://aka.ms/vs/17/release/${vc_redist_file}" "$TEMP\${vc_redist_file}" /end
ExecWait '"$TEMP\${vc_redist_file}" /install /passive' ExecWait '"$TEMP\${vc_redist_file}" /install /passive /norestart'
Delete "$TEMP\${vc_redist_file}" Delete "$TEMP\${vc_redist_file}"
SetDetailsView show SetDetailsView show
; ${EndIf} ; ${EndIf}
@@ -324,7 +334,7 @@ Section "Strawberry" Strawberry
File "libqtsparkle-qt6.dll" File "libqtsparkle-qt6.dll"
File "libsoup-3.0-0.dll" File "libsoup-3.0-0.dll"
File "libspeex-1.dll" File "libspeex-1.dll"
File "libsqlite3.dll" File "libsqlite3-0.dll"
File "libssp-0.dll" File "libssp-0.dll"
File "libstdc++-6.dll" File "libstdc++-6.dll"
File "libtag.dll" File "libtag.dll"
@@ -367,6 +377,10 @@ Section "Strawberry" Strawberry
File "libcrypto-3-x64.dll" File "libcrypto-3-x64.dll"
File "libssl-3-x64.dll" File "libssl-3-x64.dll"
!endif !endif
!ifdef arch_arm64
File "libcrypto-3-arm64.dll"
File "libssl-3-arm64.dll"
!endif
File "FLAC.dll" File "FLAC.dll"
File "brotlicommon.dll" File "brotlicommon.dll"
@@ -381,7 +395,9 @@ Section "Strawberry" Strawberry
File "glib-2.0-0.dll" File "glib-2.0-0.dll"
File "gme.dll" File "gme.dll"
File "gmodule-2.0-0.dll" File "gmodule-2.0-0.dll"
!ifndef arch_arm64
File "gnutls.dll" File "gnutls.dll"
!endif
File "gobject-2.0-0.dll" File "gobject-2.0-0.dll"
File "gstadaptivedemux-1.0-0.dll" File "gstadaptivedemux-1.0-0.dll"
File "gstapp-1.0-0.dll" File "gstapp-1.0-0.dll"
@@ -402,13 +418,17 @@ Section "Strawberry" Strawberry
File "gsttag-1.0-0.dll" File "gsttag-1.0-0.dll"
File "gsturidownloader-1.0-0.dll" File "gsturidownloader-1.0-0.dll"
File "gstvideo-1.0-0.dll" File "gstvideo-1.0-0.dll"
!ifdef arch_arm64
File "gstwinrt-1.0-0.dll" File "gstwinrt-1.0-0.dll"
!endif
File "harfbuzz.dll" File "harfbuzz.dll"
File "intl-8.dll" File "intl-8.dll"
File "jpeg62.dll" File "jpeg62.dll"
File "kdsingleapplication-qt6.dll" File "kdsingleapplication-qt6.dll"
File "libbs2b.dll" File "libbs2b.dll"
!ifndef arch_arm64
File "libfaac_dll.dll" File "libfaac_dll.dll"
!endif
File "liblzma.dll" File "liblzma.dll"
File "libmp3lame.dll" File "libmp3lame.dll"
File "libopenmpt.dll" File "libopenmpt.dll"
@@ -434,8 +454,10 @@ Section "Strawberry" Strawberry
File "libspeex.dll" File "libspeex.dll"
File "pcre2-8.dll" File "pcre2-8.dll"
File "pcre2-16.dll" File "pcre2-16.dll"
!ifndef arch_arm64
File "twolame.dll" File "twolame.dll"
File "zlib.dll" !endif
File "zlib1.dll"
!endif !endif
!ifdef debug !ifdef debug
File "freetyped.dll" File "freetyped.dll"
@@ -444,8 +466,10 @@ Section "Strawberry" Strawberry
File "libspeexd.dll" File "libspeexd.dll"
File "pcre2-8d.dll" File "pcre2-8d.dll"
File "pcre2-16d.dll" File "pcre2-16d.dll"
!ifndef arch_arm64
File "twolamed.dll" File "twolamed.dll"
File "zlibd.dll" !endif
File "zlibd1.dll"
!endif !endif
; Used by libfftw3-3.dll because fftw is compiled with MinGW. ; Used by libfftw3-3.dll because fftw is compiled with MinGW.
@@ -459,7 +483,11 @@ Section "Strawberry" Strawberry
; Common files ; Common files
File "icudt77.dll" File "icudt77.dll"
!ifdef msvc && arch_arm64
File "fftw3.dll"
!else
File "libfftw3-3.dll" File "libfftw3-3.dll"
!endif
!ifdef msvc && debug !ifdef msvc && debug
File "icuin77d.dll" File "icuin77d.dll"
File "icuuc77d.dll" File "icuuc77d.dll"
@@ -526,11 +554,13 @@ Section "GIO modules" gio-modules
SetOutPath "$INSTDIR\gio-modules" SetOutPath "$INSTDIR\gio-modules"
!ifdef mingw !ifdef mingw
File "/oname=libgiognutls.dll" "gio-modules\libgiognutls.dll" File "/oname=libgiognutls.dll" "gio-modules\libgiognutls.dll"
File "/oname=libgioopenssl.dll" "gio-modules\libgioopenssl.dll"
!endif !endif
!ifdef msvc !ifdef msvc
File "/oname=giognutls.dll" "gio-modules\giognutls.dll" !ifdef arch_arm64
File "/oname=gioopenssl.dll" "gio-modules\gioopenssl.dll" File "/oname=gioopenssl.dll" "gio-modules\gioopenssl.dll"
!else
File "/oname=giognutls.dll" "gio-modules\giognutls.dll"
!endif
!endif !endif
SectionEnd SectionEnd
@@ -674,7 +704,6 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=gstdirectsound.dll" "gstreamer-plugins\gstdirectsound.dll" File "/oname=gstdirectsound.dll" "gstreamer-plugins\gstdirectsound.dll"
File "/oname=gstdsd.dll" "gstreamer-plugins\gstdsd.dll" File "/oname=gstdsd.dll" "gstreamer-plugins\gstdsd.dll"
File "/oname=gstequalizer.dll" "gstreamer-plugins\gstequalizer.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" File "/oname=gstfaad.dll" "gstreamer-plugins\gstfaad.dll"
File "/oname=gstfdkaac.dll" "gstreamer-plugins\gstfdkaac.dll" File "/oname=gstfdkaac.dll" "gstreamer-plugins\gstfdkaac.dll"
File "/oname=gstflac.dll" "gstreamer-plugins\gstflac.dll" File "/oname=gstflac.dll" "gstreamer-plugins\gstflac.dll"
@@ -707,7 +736,6 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=gstspeex.dll" "gstreamer-plugins\gstspeex.dll" File "/oname=gstspeex.dll" "gstreamer-plugins\gstspeex.dll"
File "/oname=gsttaglib.dll" "gstreamer-plugins\gsttaglib.dll" File "/oname=gsttaglib.dll" "gstreamer-plugins\gsttaglib.dll"
File "/oname=gsttcp.dll" "gstreamer-plugins\gsttcp.dll" File "/oname=gsttcp.dll" "gstreamer-plugins\gsttcp.dll"
File "/oname=gsttwolame.dll" "gstreamer-plugins\gsttwolame.dll"
File "/oname=gsttypefindfunctions.dll" "gstreamer-plugins\gsttypefindfunctions.dll" File "/oname=gsttypefindfunctions.dll" "gstreamer-plugins\gsttypefindfunctions.dll"
File "/oname=gstudp.dll" "gstreamer-plugins\gstudp.dll" File "/oname=gstudp.dll" "gstreamer-plugins\gstudp.dll"
File "/oname=gstvolume.dll" "gstreamer-plugins\gstvolume.dll" File "/oname=gstvolume.dll" "gstreamer-plugins\gstvolume.dll"
@@ -719,8 +747,12 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=gstwavpack.dll" "gstreamer-plugins\gstwavpack.dll" File "/oname=gstwavpack.dll" "gstreamer-plugins\gstwavpack.dll"
File "/oname=gstwavparse.dll" "gstreamer-plugins\gstwavparse.dll" File "/oname=gstwavparse.dll" "gstreamer-plugins\gstwavparse.dll"
File "/oname=gstxingmux.dll" "gstreamer-plugins\gstxingmux.dll" File "/oname=gstxingmux.dll" "gstreamer-plugins\gstxingmux.dll"
!ifndef arch_arm64
File "/oname=gstfaac.dll" "gstreamer-plugins\gstfaac.dll"
File "/oname=gsttwolame.dll" "gstreamer-plugins\gsttwolame.dll"
!endif
!ifdef arch_x64 !ifdef arch_x64
;File "/oname=gstspotify.dll" "gstreamer-plugins\gstspotify.dll" File "/oname=gstspotify.dll" "gstreamer-plugins\gstspotify.dll"
!endif !endif
!endif ; MSVC !endif ; MSVC
@@ -849,7 +881,7 @@ Section "Uninstall"
Delete "$INSTDIR\libqtsparkle-qt6.dll" Delete "$INSTDIR\libqtsparkle-qt6.dll"
Delete "$INSTDIR\libsoup-3.0-0.dll" Delete "$INSTDIR\libsoup-3.0-0.dll"
Delete "$INSTDIR\libspeex-1.dll" Delete "$INSTDIR\libspeex-1.dll"
Delete "$INSTDIR\libsqlite3.dll" Delete "$INSTDIR\libsqlite3-0.dll"
Delete "$INSTDIR\libssp-0.dll" Delete "$INSTDIR\libssp-0.dll"
Delete "$INSTDIR\libstdc++-6.dll" Delete "$INSTDIR\libstdc++-6.dll"
Delete "$INSTDIR\libtag.dll" Delete "$INSTDIR\libtag.dll"
@@ -892,6 +924,10 @@ Section "Uninstall"
Delete "$INSTDIR\libcrypto-3-x64.dll" Delete "$INSTDIR\libcrypto-3-x64.dll"
Delete "$INSTDIR\libssl-3-x64.dll" Delete "$INSTDIR\libssl-3-x64.dll"
!endif !endif
!ifdef arch_arm64
Delete "$INSTDIR\libcrypto-3-arm64.dll"
Delete "$INSTDIR\libssl-3-arm64.dll"
!endif
Delete "$INSTDIR\FLAC.dll" Delete "$INSTDIR\FLAC.dll"
Delete "$INSTDIR\brotlicommon.dll" Delete "$INSTDIR\brotlicommon.dll"
@@ -906,7 +942,9 @@ Section "Uninstall"
Delete "$INSTDIR\glib-2.0-0.dll" Delete "$INSTDIR\glib-2.0-0.dll"
Delete "$INSTDIR\gme.dll" Delete "$INSTDIR\gme.dll"
Delete "$INSTDIR\gmodule-2.0-0.dll" Delete "$INSTDIR\gmodule-2.0-0.dll"
!ifndef arch_arm64
Delete "$INSTDIR\gnutls.dll" Delete "$INSTDIR\gnutls.dll"
!endif
Delete "$INSTDIR\gobject-2.0-0.dll" Delete "$INSTDIR\gobject-2.0-0.dll"
Delete "$INSTDIR\gstadaptivedemux-1.0-0.dll" Delete "$INSTDIR\gstadaptivedemux-1.0-0.dll"
Delete "$INSTDIR\gstapp-1.0-0.dll" Delete "$INSTDIR\gstapp-1.0-0.dll"
@@ -927,13 +965,17 @@ Section "Uninstall"
Delete "$INSTDIR\gsttag-1.0-0.dll" Delete "$INSTDIR\gsttag-1.0-0.dll"
Delete "$INSTDIR\gsturidownloader-1.0-0.dll" Delete "$INSTDIR\gsturidownloader-1.0-0.dll"
Delete "$INSTDIR\gstvideo-1.0-0.dll" Delete "$INSTDIR\gstvideo-1.0-0.dll"
!ifdef arch_arm64
Delete "$INSTDIR\gstwinrt-1.0-0.dll" Delete "$INSTDIR\gstwinrt-1.0-0.dll"
!endif
Delete "$INSTDIR\harfbuzz.dll" Delete "$INSTDIR\harfbuzz.dll"
Delete "$INSTDIR\intl-8.dll" Delete "$INSTDIR\intl-8.dll"
Delete "$INSTDIR\jpeg62.dll" Delete "$INSTDIR\jpeg62.dll"
Delete "$INSTDIR\kdsingleapplication-qt6.dll" Delete "$INSTDIR\kdsingleapplication-qt6.dll"
Delete "$INSTDIR\libbs2b.dll" Delete "$INSTDIR\libbs2b.dll"
!ifndef arch_arm64
Delete "$INSTDIR\libfaac_dll.dll" Delete "$INSTDIR\libfaac_dll.dll"
!endif
Delete "$INSTDIR\liblzma.dll" Delete "$INSTDIR\liblzma.dll"
Delete "$INSTDIR\libmp3lame.dll" Delete "$INSTDIR\libmp3lame.dll"
Delete "$INSTDIR\libopenmpt.dll" Delete "$INSTDIR\libopenmpt.dll"
@@ -959,8 +1001,10 @@ Section "Uninstall"
Delete "$INSTDIR\libspeex.dll" Delete "$INSTDIR\libspeex.dll"
Delete "$INSTDIR\pcre2-8.dll" Delete "$INSTDIR\pcre2-8.dll"
Delete "$INSTDIR\pcre2-16.dll" Delete "$INSTDIR\pcre2-16.dll"
!ifndef arch_arm64
Delete "$INSTDIR\twolame.dll" Delete "$INSTDIR\twolame.dll"
Delete "$INSTDIR\zlib.dll" !endif
Delete "$INSTDIR\zlib1.dll"
!endif !endif
!ifdef debug !ifdef debug
Delete "$INSTDIR\freetyped.dll" Delete "$INSTDIR\freetyped.dll"
@@ -969,8 +1013,10 @@ Section "Uninstall"
Delete "$INSTDIR\libspeexd.dll" Delete "$INSTDIR\libspeexd.dll"
Delete "$INSTDIR\pcre2-8d.dll" Delete "$INSTDIR\pcre2-8d.dll"
Delete "$INSTDIR\pcre2-16d.dll" Delete "$INSTDIR\pcre2-16d.dll"
!ifndef arch_arm64
Delete "$INSTDIR\twolamed.dll" Delete "$INSTDIR\twolamed.dll"
Delete "$INSTDIR\zlibd.dll" !endif
Delete "$INSTDIR\zlibd1.dll"
!endif !endif
!ifdef arch_x86 !ifdef arch_x86
@@ -983,7 +1029,11 @@ Section "Uninstall"
; Common files ; Common files
Delete "$INSTDIR\icudt77.dll" Delete "$INSTDIR\icudt77.dll"
!ifdef msvc && arch_arm64
Delete "$INSTDIR\fftw3.dll"
!else
Delete "$INSTDIR\libfftw3-3.dll" Delete "$INSTDIR\libfftw3-3.dll"
!endif
!ifdef msvc && debug !ifdef msvc && debug
Delete "$INSTDIR\icuin77d.dll" Delete "$INSTDIR\icuin77d.dll"
Delete "$INSTDIR\icuuc77d.dll" Delete "$INSTDIR\icuuc77d.dll"
@@ -1016,11 +1066,13 @@ Section "Uninstall"
!ifdef mingw !ifdef mingw
Delete "$INSTDIR\gio-modules\libgiognutls.dll" Delete "$INSTDIR\gio-modules\libgiognutls.dll"
Delete "$INSTDIR\gio-modules\libgioopenssl.dll"
!endif !endif
!ifdef msvc !ifdef msvc
Delete "$INSTDIR\gio-modules\giognutls.dll" !ifdef arch_arm64
Delete "$INSTDIR\gio-modules\gioopenssl.dll" Delete "$INSTDIR\gio-modules\gioopenssl.dll"
!else
Delete "$INSTDIR\gio-modules\giognutls.dll"
!endif
!endif !endif
!ifdef msvc && debug !ifdef msvc && debug
@@ -1133,7 +1185,6 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\gstdirectsound.dll" Delete "$INSTDIR\gstreamer-plugins\gstdirectsound.dll"
Delete "$INSTDIR\gstreamer-plugins\gstdsd.dll" Delete "$INSTDIR\gstreamer-plugins\gstdsd.dll"
Delete "$INSTDIR\gstreamer-plugins\gstequalizer.dll" Delete "$INSTDIR\gstreamer-plugins\gstequalizer.dll"
Delete "$INSTDIR\gstreamer-plugins\gstfaac.dll"
Delete "$INSTDIR\gstreamer-plugins\gstfaad.dll" Delete "$INSTDIR\gstreamer-plugins\gstfaad.dll"
Delete "$INSTDIR\gstreamer-plugins\gstfdkaac.dll" Delete "$INSTDIR\gstreamer-plugins\gstfdkaac.dll"
Delete "$INSTDIR\gstreamer-plugins\gstflac.dll" Delete "$INSTDIR\gstreamer-plugins\gstflac.dll"
@@ -1166,7 +1217,6 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\gstspeex.dll" Delete "$INSTDIR\gstreamer-plugins\gstspeex.dll"
Delete "$INSTDIR\gstreamer-plugins\gsttaglib.dll" Delete "$INSTDIR\gstreamer-plugins\gsttaglib.dll"
Delete "$INSTDIR\gstreamer-plugins\gsttcp.dll" Delete "$INSTDIR\gstreamer-plugins\gsttcp.dll"
Delete "$INSTDIR\gstreamer-plugins\gsttwolame.dll"
Delete "$INSTDIR\gstreamer-plugins\gsttypefindfunctions.dll" Delete "$INSTDIR\gstreamer-plugins\gsttypefindfunctions.dll"
Delete "$INSTDIR\gstreamer-plugins\gstudp.dll" Delete "$INSTDIR\gstreamer-plugins\gstudp.dll"
Delete "$INSTDIR\gstreamer-plugins\gstvolume.dll" Delete "$INSTDIR\gstreamer-plugins\gstvolume.dll"
@@ -1178,9 +1228,14 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\gstwavpack.dll" Delete "$INSTDIR\gstreamer-plugins\gstwavpack.dll"
Delete "$INSTDIR\gstreamer-plugins\gstwavparse.dll" Delete "$INSTDIR\gstreamer-plugins\gstwavparse.dll"
Delete "$INSTDIR\gstreamer-plugins\gstxingmux.dll" Delete "$INSTDIR\gstreamer-plugins\gstxingmux.dll"
!ifdef arch_x64 !ifndef arch_arm64
;Delete "$INSTDIR\gstreamer-plugins\gstspotify.dll" Delete "$INSTDIR\gstreamer-plugins\gstfaac.dll"
Delete "$INSTDIR\gstreamer-plugins\gsttwolame.dll"
!endif !endif
!ifdef arch_x64
Delete "$INSTDIR\gstreamer-plugins\gstspotify.dll"
!endif
!endif ; msvc !endif ; msvc
Delete "$INSTDIR\Uninstall.exe" Delete "$INSTDIR\Uninstall.exe"

View File

@@ -623,6 +623,7 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
QMutexLocker l(db_->Mutex()); QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect()); QSqlDatabase db(db_->Connect());
CollectionTask task(task_manager_, tr("Updating %1 database.").arg(Song::TextForSource(source_)));
ScopedTransaction transaction(&db); ScopedTransaction transaction(&db);
SongList added_songs; SongList added_songs;

View File

@@ -34,6 +34,7 @@
#include <QVariant> #include <QVariant>
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
#include <QUrl>
#include <QRegularExpression> #include <QRegularExpression>
#include <QInputDialog> #include <QInputDialog>
#include <QList> #include <QList>
@@ -295,19 +296,21 @@ QActionGroup *CollectionFilterWidget::CreateGroupByActions(const QString &saved_
if (version == 1) { if (version == 1) {
QStringList saved = s.childKeys(); QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) { for (int i = 0; i < saved.size(); ++i) {
if (saved.at(i) == "version"_L1) continue; const QString &name = saved.at(i);
QByteArray bytes = s.value(saved.at(i)).toByteArray(); if (name == "version"_L1) continue;
QByteArray bytes = s.value(name).toByteArray();
QDataStream ds(&bytes, QIODevice::ReadOnly); QDataStream ds(&bytes, QIODevice::ReadOnly);
CollectionModel::Grouping g; CollectionModel::Grouping g;
ds >> g; ds >> g;
ret->addAction(CreateGroupByAction(saved.at(i), parent, g)); ret->addAction(CreateGroupByAction(QUrl::fromPercentEncoding(name.toUtf8()), parent, g));
} }
} }
else { else {
QStringList saved = s.childKeys(); QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) { for (int i = 0; i < saved.size(); ++i) {
if (saved.at(i) == "version"_L1) continue; const QString &name = saved.at(i);
s.remove(saved.at(i)); if (name == "version"_L1) continue;
s.remove(name);
} }
} }
s.endGroup(); s.endGroup();
@@ -339,7 +342,7 @@ void CollectionFilterWidget::SaveGroupBy() {
if (!model_) return; if (!model_) return;
QString name = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:")); const QString name = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
if (name.isEmpty()) return; if (name.isEmpty()) return;
qLog(Debug) << "Saving current grouping to" << name; qLog(Debug) << "Saving current grouping to" << name;
@@ -355,7 +358,7 @@ void CollectionFilterWidget::SaveGroupBy() {
QDataStream datastream(&buffer, QIODevice::WriteOnly); QDataStream datastream(&buffer, QIODevice::WriteOnly);
datastream << model_->GetGroupBy(); datastream << model_->GetGroupBy();
s.setValue("version", u"1"_s); s.setValue("version", u"1"_s);
s.setValue(name, buffer); s.setValue(QUrl::toPercentEncoding(name), buffer);
s.endGroup(); s.endGroup();
UpdateGroupByActions(); UpdateGroupByActions();

View File

@@ -78,6 +78,8 @@ CollectionLibrary::CollectionLibrary(const SharedPtr<Database> database,
model_ = new CollectionModel(backend_, albumcover_loader, this); model_ = new CollectionModel(backend_, albumcover_loader, this);
full_rescan_revisions_[21] = tr("Support for sort tags artist, album, album artist, title, composer, and performer");
ReloadSettings(); ReloadSettings();
} }

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -54,6 +54,7 @@
#include "includes/scoped_ptr.h" #include "includes/scoped_ptr.h"
#include "includes/shared_ptr.h" #include "includes/shared_ptr.h"
#include "constants/collectionsettings.h"
#include "core/logging.h" #include "core/logging.h"
#include "core/standardpaths.h" #include "core/standardpaths.h"
#include "core/database.h" #include "core/database.h"
@@ -71,12 +72,12 @@
#include "covermanager/albumcoverloaderoptions.h" #include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h" #include "covermanager/albumcoverloaderresult.h"
#include "covermanager/albumcoverloader.h" #include "covermanager/albumcoverloader.h"
#include "constants/collectionsettings.h"
using namespace std::chrono_literals; using namespace std::chrono_literals;
using namespace Qt::Literals::StringLiterals; using namespace Qt::Literals::StringLiterals;
const int CollectionModel::kPrettyCoverSize = 32; const int CollectionModel::kPrettyCoverSize = 32;
namespace { namespace {
constexpr char kPixmapDiskCacheDir[] = "pixmapcache"; constexpr char kPixmapDiskCacheDir[] = "pixmapcache";
constexpr char kVariousArtists[] = QT_TR_NOOP("Various artists"); constexpr char kVariousArtists[] = QT_TR_NOOP("Various artists");
@@ -88,7 +89,6 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
albumcover_loader_(albumcover_loader), albumcover_loader_(albumcover_loader),
dir_model_(new CollectionDirectoryModel(backend, this)), dir_model_(new CollectionDirectoryModel(backend, this)),
filter_(new CollectionFilter(this)), filter_(new CollectionFilter(this)),
timer_reload_(new QTimer(this)),
timer_update_(new QTimer(this)), timer_update_(new QTimer(this)),
icon_artist_(IconLoader::Load(u"folder-sound"_s)), icon_artist_(IconLoader::Load(u"folder-sound"_s)),
use_disk_cache_(false), use_disk_cache_(false),
@@ -130,10 +130,6 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
backend_->UpdateTotalArtistCountAsync(); backend_->UpdateTotalArtistCountAsync();
backend_->UpdateTotalAlbumCountAsync(); backend_->UpdateTotalAlbumCountAsync();
timer_reload_->setSingleShot(true);
timer_reload_->setInterval(300ms);
QObject::connect(timer_reload_, &QTimer::timeout, this, &CollectionModel::Reload);
timer_update_->setSingleShot(false); timer_update_->setSingleShot(false);
timer_update_->setInterval(20ms); timer_update_->setInterval(20ms);
QObject::connect(timer_update_, &QTimer::timeout, this, &CollectionModel::ProcessUpdate); QObject::connect(timer_update_, &QTimer::timeout, this, &CollectionModel::ProcessUpdate);
@@ -191,13 +187,9 @@ void CollectionModel::EndReset() {
} }
void CollectionModel::Reload() { void CollectionModel::ResetInternal() {
loading_ = true; loading_ = true;
if (timer_reload_->isActive()) {
timer_reload_->stop();
}
updates_.clear();
options_active_ = options_current_; options_active_ = options_current_;
@@ -211,22 +203,15 @@ void CollectionModel::Reload() {
} }
void CollectionModel::ScheduleReset() {
if (!timer_reload_->isActive()) {
timer_reload_->start();
}
}
void CollectionModel::ReloadSettings() { void CollectionModel::ReloadSettings() {
Settings settings; Settings settings;
settings.beginGroup(CollectionSettings::kSettingsGroup); settings.beginGroup(CollectionSettings::kSettingsGroup);
const bool show_pretty_covers = settings.value(CollectionSettings::kPrettyCovers, true).toBool(); const bool show_pretty_covers = settings.value(CollectionSettings::kPrettyCovers, true).toBool();
const bool show_dividers= settings.value(CollectionSettings::kShowDividers, true).toBool(); const bool show_dividers = settings.value(CollectionSettings::kShowDividers, true).toBool();
const bool show_various_artists = settings.value(CollectionSettings::kVariousArtists, true).toBool(); const bool show_various_artists = settings.value(CollectionSettings::kVariousArtists, true).toBool();
const bool sort_skips_articles = settings.value(CollectionSettings::kSortSkipsArticles, true).toBool(); const bool sort_skip_articles_for_artists = settings.value(CollectionSettings::kSkipArticlesForArtists, true).toBool();
const bool sort_skip_articles_for_albums = settings.value(CollectionSettings::kSkipArticlesForAlbums, false).toBool();
use_disk_cache_ = settings.value(CollectionSettings::kSettingsDiskCacheEnable, false).toBool(); use_disk_cache_ = settings.value(CollectionSettings::kSettingsDiskCacheEnable, false).toBool();
QPixmapCache::setCacheLimit(static_cast<int>(MaximumCacheSize(&settings, CollectionSettings::kSettingsCacheSize, CollectionSettings::kSettingsCacheSizeUnit, CollectionSettings::kSettingsCacheSizeDefault) / 1024)); QPixmapCache::setCacheLimit(static_cast<int>(MaximumCacheSize(&settings, CollectionSettings::kSettingsCacheSize, CollectionSettings::kSettingsCacheSizeUnit, CollectionSettings::kSettingsCacheSizeDefault) / 1024));
@@ -241,11 +226,13 @@ void CollectionModel::ReloadSettings() {
if (show_pretty_covers != options_current_.show_pretty_covers || if (show_pretty_covers != options_current_.show_pretty_covers ||
show_dividers != options_current_.show_dividers || show_dividers != options_current_.show_dividers ||
show_various_artists != options_current_.show_various_artists || show_various_artists != options_current_.show_various_artists ||
sort_skips_articles != options_current_.sort_skips_articles) { sort_skip_articles_for_artists != options_current_.sort_skip_articles_for_artists ||
sort_skip_articles_for_albums != options_current_.sort_skip_articles_for_albums) {
options_current_.show_pretty_covers = show_pretty_covers; options_current_.show_pretty_covers = show_pretty_covers;
options_current_.show_dividers = show_dividers; options_current_.show_dividers = show_dividers;
options_current_.show_various_artists = show_various_artists; options_current_.show_various_artists = show_various_artists;
options_current_.sort_skips_articles = sort_skips_articles; options_current_.sort_skip_articles_for_artists = sort_skip_articles_for_artists;
options_current_.sort_skip_articles_for_albums = sort_skip_articles_for_albums;
ScheduleReset(); ScheduleReset();
} }
@@ -421,10 +408,15 @@ void CollectionModel::RemoveSongs(const SongList &songs) {
void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs) { void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs) {
for (qint64 i = 0; i < songs.count(); i += 400LL) { if (type == CollectionModelUpdate::Type::Reset) {
const qint64 number = std::min(songs.count() - i, 400LL); updates_.enqueue(CollectionModelUpdate(type));
const SongList songs_to_queue = songs.mid(i, number); }
updates_.enqueue(CollectionModelUpdate(type, songs_to_queue)); else {
for (qint64 i = 0; i < songs.count(); i += 400LL) {
const qint64 number = std::min(songs.count() - i, 400LL);
const SongList songs_to_queue = songs.mid(i, number);
updates_.enqueue(CollectionModelUpdate(type, songs_to_queue));
}
} }
if (!timer_update_->isActive()) { if (!timer_update_->isActive()) {
@@ -433,6 +425,12 @@ void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, con
} }
void CollectionModel::ScheduleReset() {
ScheduleUpdate(CollectionModelUpdate::Type::Reset);
}
void CollectionModel::ScheduleAddSongs(const SongList &songs) { void CollectionModel::ScheduleAddSongs(const SongList &songs) {
ScheduleUpdate(CollectionModelUpdate::Type::Add, songs); ScheduleUpdate(CollectionModelUpdate::Type::Add, songs);
@@ -465,6 +463,9 @@ void CollectionModel::ProcessUpdate() {
} }
switch (update.type) { switch (update.type) {
case CollectionModelUpdate::Type::Reset:
ResetInternal();
break;
case CollectionModelUpdate::Type::AddReAddOrUpdate: case CollectionModelUpdate::Type::AddReAddOrUpdate:
AddReAddOrUpdateSongsInternal(update.songs); AddReAddOrUpdateSongsInternal(update.songs);
break; break;
@@ -699,7 +700,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
QString divider_key; QString divider_key;
if (options_active_.show_dividers && container_level == 0) { if (options_active_.show_dividers && container_level == 0) {
divider_key = DividerKey(group_by, song, SortText(group_by, song, options_active_.sort_skips_articles)); divider_key = DividerKey(group_by, song, SortText(group_by, song, options_active_.sort_skip_articles_for_artists, options_active_.sort_skip_articles_for_albums));
if (!divider_key.isEmpty()) { if (!divider_key.isEmpty()) {
if (!divider_nodes_.contains(divider_key)) { if (!divider_nodes_.contains(divider_key)) {
CreateDividerItem(divider_key, DividerDisplayText(group_by, divider_key), parent); CreateDividerItem(divider_key, DividerDisplayText(group_by, divider_key), parent);
@@ -713,7 +714,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
item->container_level = container_level; item->container_level = container_level;
item->container_key = container_key; item->container_key = container_key;
item->display_text = DisplayText(group_by, song); item->display_text = DisplayText(group_by, song);
item->sort_text = SortText(group_by, song, options_active_.sort_skips_articles); item->sort_text = SortText(group_by, song, options_active_.sort_skip_articles_for_artists, options_active_.sort_skip_articles_for_albums);
if (!divider_key.isEmpty()) { if (!divider_key.isEmpty()) {
item->sort_text.prepend(divider_key + QLatin1Char(' ')); item->sort_text.prepend(divider_key + QLatin1Char(' '));
} }
@@ -1068,25 +1069,25 @@ QString CollectionModel::PrettyFormat(const Song &song) {
} }
QString CollectionModel::SortText(const GroupBy group_by, const Song &song, const bool sort_skips_articles) { QString CollectionModel::SortText(const GroupBy group_by, const Song &song, const bool sort_skip_articles_for_artists, const bool sort_skip_articles_for_albums) {
switch (group_by) { switch (group_by) {
case GroupBy::AlbumArtist: case GroupBy::AlbumArtist:
return SortTextForArtist(song.effective_albumartist(), sort_skips_articles); return SortTextForName(song.effective_albumartistsort(), sort_skip_articles_for_artists);
case GroupBy::Artist: case GroupBy::Artist:
return SortTextForArtist(song.artist(), sort_skips_articles); return SortTextForName(song.effective_artistsort(), sort_skip_articles_for_artists);
case GroupBy::Album: case GroupBy::Album:
return SortText(song.album()); return SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums);
case GroupBy::AlbumDisc: case GroupBy::AlbumDisc:
return song.album() + SortTextForNumber(std::max(0, song.disc())); return SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
case GroupBy::YearAlbum: case GroupBy::YearAlbum:
return SortTextForNumber(std::max(0, song.year())) + song.grouping() + song.album(); return SortTextForNumber(std::max(0, song.year())) + song.grouping() + SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums);
case GroupBy::YearAlbumDisc: case GroupBy::YearAlbumDisc:
return SortTextForNumber(std::max(0, song.year())) + song.album() + SortTextForNumber(std::max(0, song.disc())); return SortTextForNumber(std::max(0, song.year())) + SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
case GroupBy::OriginalYearAlbum: case GroupBy::OriginalYearAlbum:
return SortTextForNumber(std::max(0, song.effective_originalyear())) + song.grouping() + song.album(); return SortTextForNumber(std::max(0, song.effective_originalyear())) + song.grouping() + SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums);
case GroupBy::OriginalYearAlbumDisc: case GroupBy::OriginalYearAlbumDisc:
return SortTextForNumber(std::max(0, song.effective_originalyear())) + song.album() + SortTextForNumber(std::max(0, song.disc())); return SortTextForNumber(std::max(0, song.effective_originalyear())) + SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
case GroupBy::Disc: case GroupBy::Disc:
return SortTextForNumber(std::max(0, song.disc())); return SortTextForNumber(std::max(0, song.disc()));
case GroupBy::Year: case GroupBy::Year:
@@ -1094,13 +1095,13 @@ QString CollectionModel::SortText(const GroupBy group_by, const Song &song, cons
case GroupBy::OriginalYear: case GroupBy::OriginalYear:
return SortTextForNumber(std::max(0, song.effective_originalyear())) + QLatin1Char(' '); return SortTextForNumber(std::max(0, song.effective_originalyear())) + QLatin1Char(' ');
case GroupBy::Genre: case GroupBy::Genre:
return SortTextForArtist(song.genre(), sort_skips_articles); return SortText(song.genre());
case GroupBy::Composer: case GroupBy::Composer:
return SortTextForArtist(song.composer(), sort_skips_articles); return SortTextForName(song.effective_composersort(), sort_skip_articles_for_artists);
case GroupBy::Performer: case GroupBy::Performer:
return SortTextForArtist(song.performer(), sort_skips_articles); return SortTextForName(song.effective_performersort(), sort_skip_articles_for_artists);
case GroupBy::Grouping: case GroupBy::Grouping:
return SortTextForArtist(song.grouping(), sort_skips_articles); return SortText(song.grouping());
case GroupBy::FileType: case GroupBy::FileType:
return song.TextForFiletype(); return song.TextForFiletype();
case GroupBy::Format: case GroupBy::Format:
@@ -1135,21 +1136,9 @@ QString CollectionModel::SortText(QString text) {
} }
QString CollectionModel::SortTextForArtist(QString artist, const bool skip_articles) { QString CollectionModel::SortTextForName(const QString &name, const bool sort_skip_articles) {
artist = SortText(artist); return sort_skip_articles ? SkipArticles(SortText(name)) : SortText(name);
if (skip_articles) {
for (const auto &i : Song::kArticles) {
if (artist.startsWith(i)) {
qint64 ilen = i.length();
artist = artist.right(artist.length() - ilen) + ", "_L1 + i.left(ilen - 1);
break;
}
}
}
return artist;
} }
@@ -1180,6 +1169,20 @@ QString CollectionModel::SortTextForBitrate(const int bitrate) {
} }
QString CollectionModel::SkipArticles(QString name) {
for (const auto &i : Song::kArticles) {
if (name.startsWith(i)) {
qint64 ilen = i.length();
name = name.right(name.length() - ilen) + ", "_L1 + i.left(ilen - 1);
break;
}
}
return name;
}
bool CollectionModel::IsSongTitleDataChanged(const Song &song1, const Song &song2) { bool CollectionModel::IsSongTitleDataChanged(const Song &song1, const Song &song2) {
return song1.url() != song2.url() || return song1.url() != song2.url() ||

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -129,14 +129,16 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
show_dividers(true), show_dividers(true),
show_pretty_covers(true), show_pretty_covers(true),
show_various_artists(true), show_various_artists(true),
sort_skips_articles(true), sort_skip_articles_for_artists(false),
sort_skip_articles_for_albums(false),
separate_albums_by_grouping(false) {} separate_albums_by_grouping(false) {}
Grouping group_by; Grouping group_by;
bool show_dividers; bool show_dividers;
bool show_pretty_covers; bool show_pretty_covers;
bool show_various_artists; bool show_various_artists;
bool sort_skips_articles; bool sort_skip_articles_for_artists;
bool sort_skip_articles_for_albums;
bool separate_albums_by_grouping; bool separate_albums_by_grouping;
CollectionFilterOptions filter_options; CollectionFilterOptions filter_options;
}; };
@@ -176,20 +178,21 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
QMimeData *mimeData(const QModelIndexList &indexes) const override; QMimeData *mimeData(const QModelIndexList &indexes) const override;
// Utility functions for manipulating text // Utility functions for manipulating text
static QString DisplayText(const GroupBy group_by, const Song &song); QString DisplayText(const GroupBy group_by, const Song &song);
static QString TextOrUnknown(const QString &text); static QString TextOrUnknown(const QString &text);
static QString PrettyYearAlbum(const int year, const QString &album); static QString PrettyYearAlbum(const int year, const QString &album);
static QString PrettyAlbumDisc(const QString &album, const int disc); static QString PrettyAlbumDisc(const QString &album, const int disc);
static QString PrettyYearAlbumDisc(const int year, 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 PrettyDisc(const int disc);
static QString PrettyFormat(const Song &song); static QString PrettyFormat(const Song &song);
QString SortText(const GroupBy group_by, const Song &song, const bool sort_skips_articles); QString SortText(const GroupBy group_by, const Song &song, const bool sort_skip_articles_for_artists, const bool sort_skip_articles_for_albums);
static QString SortText(QString text); static QString SortText(QString text);
static QString SortTextForName(const QString &name, const bool sort_skip_articles);
static QString SortTextForNumber(const int number); static QString SortTextForNumber(const int number);
static QString SortTextForArtist(QString artist, const bool skip_articles);
static QString SortTextForSong(const Song &song); static QString SortTextForSong(const Song &song);
static QString SortTextForYear(const int year); static QString SortTextForYear(const int year);
static QString SortTextForBitrate(const int bitrate); static QString SortTextForBitrate(const int bitrate);
static QString SkipArticles(QString name);
static bool IsSongTitleDataChanged(const Song &song1, const Song &song2); static bool IsSongTitleDataChanged(const Song &song1, const Song &song2);
QString ContainerKey(const GroupBy group_by, const Song &song, bool &has_unique_album_identifier) const; QString ContainerKey(const GroupBy group_by, const Song &song, bool &has_unique_album_identifier) const;
@@ -228,7 +231,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
QVariant data(CollectionItem *item, const int role) const; QVariant data(CollectionItem *item, const int role) const;
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs); void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs = SongList());
void ScheduleAddSongs(const SongList &songs); void ScheduleAddSongs(const SongList &songs);
void ScheduleUpdateSongs(const SongList &songs); void ScheduleUpdateSongs(const SongList &songs);
void ScheduleRemoveSongs(const SongList &songs); void ScheduleRemoveSongs(const SongList &songs);
@@ -259,7 +262,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
static qint64 MaximumCacheSize(Settings *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 Q_SLOTS: private Q_SLOTS:
void Reload(); void ResetInternal();
void ScheduleReset(); void ScheduleReset();
void ProcessUpdate(); void ProcessUpdate();
void LoadSongsFromSqlAsyncFinished(); void LoadSongsFromSqlAsyncFinished();
@@ -278,7 +281,6 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
const SharedPtr<AlbumCoverLoader> albumcover_loader_; const SharedPtr<AlbumCoverLoader> albumcover_loader_;
CollectionDirectoryModel *dir_model_; CollectionDirectoryModel *dir_model_;
CollectionFilter *filter_; CollectionFilter *filter_;
QTimer *timer_reload_;
QTimer *timer_update_; QTimer *timer_update_;
QPixmap pixmap_no_cover_; QPixmap pixmap_no_cover_;

View File

@@ -25,12 +25,13 @@
class CollectionModelUpdate { class CollectionModelUpdate {
public: public:
enum class Type { enum class Type {
Reset,
AddReAddOrUpdate, AddReAddOrUpdate,
Add, Add,
Update, Update,
Remove, Remove,
}; };
explicit CollectionModelUpdate(const Type _type, const SongList &_songs); explicit CollectionModelUpdate(const Type _type, const SongList &_songs = SongList());
Type type; Type type;
SongList songs; SongList songs;
}; };

View File

@@ -34,8 +34,6 @@ CollectionPlaylistItem::CollectionPlaylistItem(const Song::Source source) : Play
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(song.source()), song_(song) {} CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(song.source()), song_(song) {}
QUrl CollectionPlaylistItem::Url() const { return song_.url(); }
bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) { bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) {
int col = 0; int col = 0;
@@ -62,7 +60,7 @@ void CollectionPlaylistItem::Reload() {
qLog(Error) << "Could not reload file" << song_.url() << result.error_string(); qLog(Error) << "Could not reload file" << song_.url() << result.error_string();
return; return;
} }
UpdateTemporaryMetadata(song_); UpdateStreamMetadata(song_);
} }
} }
@@ -78,16 +76,9 @@ QVariant CollectionPlaylistItem::DatabaseValue(const DatabaseColumn database_col
} }
Song CollectionPlaylistItem::Metadata() const {
if (HasTemporaryMetadata()) return temp_metadata_;
return song_;
}
void CollectionPlaylistItem::SetArtManual(const QUrl &cover_url) { void CollectionPlaylistItem::SetArtManual(const QUrl &cover_url) {
song_.set_art_manual(cover_url); song_.set_art_manual(cover_url);
if (HasTemporaryMetadata()) temp_metadata_.set_art_manual(cover_url); if (HasStreamMetadata()) stream_song_.set_art_manual(cover_url);
} }

View File

@@ -35,19 +35,17 @@ class CollectionPlaylistItem : public PlaylistItem {
explicit CollectionPlaylistItem(const Song::Source source); explicit CollectionPlaylistItem(const Song::Source source);
explicit CollectionPlaylistItem(const Song &song); explicit CollectionPlaylistItem(const Song &song);
QUrl Url() const override; Song OriginalMetadata() const override { return song_; }
void SetOriginalMetadata(const Song &song) override { song_ = song; }
QUrl OriginalUrl() const override { return song_.url(); }
bool IsLocalCollectionItem() const override { return song_.source() == Song::Source::Collection; }
bool InitFromQuery(const SqlRow &query) override; bool InitFromQuery(const SqlRow &query) override;
void Reload() override; void Reload() override;
Song Metadata() const override;
Song OriginalMetadata() const override { return song_; }
void SetMetadata(const Song &song) override { song_ = song; }
void SetArtManual(const QUrl &cover_url) override; void SetArtManual(const QUrl &cover_url) override;
bool IsLocalCollectionItem() const override { return song_.source() == Song::Source::Collection; }
protected: protected:
QVariant DatabaseValue(const DatabaseColumn database_column) const override; QVariant DatabaseValue(const DatabaseColumn database_column) const override;
Song DatabaseSongMetadata() const override { return Song(source_); } Song DatabaseSongMetadata() const override { return Song(source_); }

View File

@@ -65,10 +65,8 @@
#include "collectionitem.h" #include "collectionitem.h"
#include "collectionitemdelegate.h" #include "collectionitemdelegate.h"
#include "collectionview.h" #include "collectionview.h"
#ifndef Q_OS_WIN32 #include "device/devicemanager.h"
# include "device/devicemanager.h" #include "device/devicestatefiltermodel.h"
# include "device/devicestatefiltermodel.h"
#endif
#include "dialogs/edittagdialog.h" #include "dialogs/edittagdialog.h"
#include "dialogs/deleteconfirmationdialog.h" #include "dialogs/deleteconfirmationdialog.h"
#include "organize/organizedialog.h" #include "organize/organizedialog.h"
@@ -95,9 +93,7 @@ CollectionView::CollectionView(QWidget *parent)
action_open_in_new_playlist_(nullptr), action_open_in_new_playlist_(nullptr),
action_organize_(nullptr), action_organize_(nullptr),
action_search_for_this_(nullptr), action_search_for_this_(nullptr),
#ifndef Q_OS_WIN32
action_copy_to_device_(nullptr), action_copy_to_device_(nullptr),
#endif
action_edit_track_(nullptr), action_edit_track_(nullptr),
action_edit_tracks_(nullptr), action_edit_tracks_(nullptr),
action_rescan_songs_(nullptr), action_rescan_songs_(nullptr),
@@ -417,9 +413,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
context_menu_->addSeparator(); context_menu_->addSeparator();
action_organize_ = context_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Organize files..."), this, &CollectionView::Organize); action_organize_ = context_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Organize files..."), this, &CollectionView::Organize);
#ifndef Q_OS_WIN32
action_copy_to_device_ = context_menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &CollectionView::CopyToDevice); action_copy_to_device_ = context_menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &CollectionView::CopyToDevice);
#endif
action_delete_files_ = context_menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &CollectionView::Delete); action_delete_files_ = context_menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &CollectionView::Delete);
context_menu_->addSeparator(); context_menu_->addSeparator();
@@ -439,10 +433,8 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
context_menu_->addMenu(filter_widget_->menu()); context_menu_->addMenu(filter_widget_->menu());
#ifndef Q_OS_WIN32
action_copy_to_device_->setDisabled(device_manager_->connected_devices_model()->rowCount() == 0); action_copy_to_device_->setDisabled(device_manager_->connected_devices_model()->rowCount() == 0);
QObject::connect(device_manager_->connected_devices_model(), &DeviceStateFilterModel::IsEmptyChanged, action_copy_to_device_, &QAction::setDisabled); QObject::connect(device_manager_->connected_devices_model(), &DeviceStateFilterModel::IsEmptyChanged, action_copy_to_device_, &QAction::setDisabled);
#endif
} }
@@ -481,9 +473,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
action_rescan_songs_->setEnabled(regular_editable > 0); action_rescan_songs_->setEnabled(regular_editable > 0);
action_organize_->setVisible(regular_elements == regular_editable); action_organize_->setVisible(regular_elements == regular_editable);
#ifndef Q_OS_WIN32
action_copy_to_device_->setVisible(regular_elements == regular_editable); action_copy_to_device_->setVisible(regular_elements == regular_editable);
#endif
action_delete_files_->setVisible(delete_files_); action_delete_files_->setVisible(delete_files_);
@@ -492,9 +482,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
// only when all selected items are editable // only when all selected items are editable
action_organize_->setEnabled(regular_elements == regular_editable); action_organize_->setEnabled(regular_elements == regular_editable);
#ifndef Q_OS_WIN32
action_copy_to_device_->setEnabled(regular_elements == regular_editable); action_copy_to_device_->setEnabled(regular_elements == regular_editable);
#endif
action_delete_files_->setEnabled(delete_files_); action_delete_files_->setEnabled(delete_files_);
@@ -759,7 +747,6 @@ void CollectionView::RescanSongs() {
void CollectionView::CopyToDevice() { void CollectionView::CopyToDevice() {
#ifndef Q_OS_WIN32
if (!organize_dialog_) { if (!organize_dialog_) {
organize_dialog_ = make_unique<OrganizeDialog>(task_manager_, tagreader_client_, nullptr, this); organize_dialog_ = make_unique<OrganizeDialog>(task_manager_, tagreader_client_, nullptr, this);
} }
@@ -768,7 +755,6 @@ void CollectionView::CopyToDevice() {
organize_dialog_->SetCopy(true); organize_dialog_->SetCopy(true);
organize_dialog_->SetSongs(GetSelectedSongs()); organize_dialog_->SetSongs(GetSelectedSongs());
organize_dialog_->show(); organize_dialog_->show();
#endif
} }

View File

@@ -138,7 +138,6 @@ class CollectionView : public AutoExpandingTreeView {
void DeleteFilesFinished(const SongList &songs_with_errors); void DeleteFilesFinished(const SongList &songs_with_errors);
private: private:
void RecheckIsEmpty();
void SetShowInVarious(const bool on); void SetShowInVarious(const bool on);
bool RestoreLevelFocus(const QModelIndex &parent = QModelIndex()); bool RestoreLevelFocus(const QModelIndex &parent = QModelIndex());
void SaveContainerPath(const QModelIndex &child); void SaveContainerPath(const QModelIndex &child);
@@ -176,9 +175,7 @@ class CollectionView : public AutoExpandingTreeView {
QAction *action_organize_; QAction *action_organize_;
QAction *action_search_for_this_; QAction *action_search_for_this_;
#ifndef Q_OS_WIN32
QAction *action_copy_to_device_; QAction *action_copy_to_device_;
#endif
QAction *action_edit_track_; QAction *action_edit_track_;
QAction *action_edit_tracks_; QAction *action_edit_tracks_;
QAction *action_rescan_songs_; QAction *action_rescan_songs_;

View File

@@ -999,6 +999,18 @@ void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching
changes << u"file path"_s; changes << u"file path"_s;
notify_new = true; notify_new = true;
} }
if (matching_song.filetype() != new_song.filetype()) {
changes << u"filetype"_s;
notify_new = true;
}
if (matching_song.filesize() != new_song.filesize()) {
changes << u"filesize"_s;
notify_new = true;
}
if (matching_song.length_nanosec() != new_song.length_nanosec()) {
changes << u"length"_s;
notify_new = true;
}
if (matching_song.fingerprint() != new_song.fingerprint()) { if (matching_song.fingerprint() != new_song.fingerprint()) {
changes << u"fingerprint"_s; changes << u"fingerprint"_s;
notify_new = true; notify_new = true;
@@ -1034,6 +1046,9 @@ void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching
if (matching_song.mtime() != new_song.mtime()) { if (matching_song.mtime() != new_song.mtime()) {
changes << u"mtime"_s; changes << u"mtime"_s;
} }
if (matching_song.ctime() != new_song.ctime()) {
changes << u"ctime"_s;
}
if (changes.isEmpty()) { if (changes.isEmpty()) {
qLog(Debug) << "Song" << file << "unchanged."; qLog(Debug) << "Song" << file << "unchanged.";

View File

@@ -30,6 +30,7 @@
#include <QByteArray> #include <QByteArray>
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
#include <QUrl>
#include <QIODevice> #include <QIODevice>
#include <QDataStream> #include <QDataStream>
#include <QKeySequence> #include <QKeySequence>
@@ -167,14 +168,20 @@ void SavedGroupingManager::UpdateModel() {
if (version == 1) { if (version == 1) {
QStringList saved = s.childKeys(); QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) { for (int i = 0; i < saved.size(); ++i) {
if (saved.at(i) == "version"_L1) continue; const QString &name = saved.at(i);
QByteArray bytes = s.value(saved.at(i)).toByteArray(); if (name == "version"_L1) continue;
QByteArray bytes = s.value(name).toByteArray();
QDataStream ds(&bytes, QIODevice::ReadOnly); QDataStream ds(&bytes, QIODevice::ReadOnly);
CollectionModel::Grouping g; CollectionModel::Grouping g;
ds >> g; ds >> g;
QList<QStandardItem*> list; QList<QStandardItem*> list;
list << new QStandardItem(saved.at(i))
QStandardItem *item = new QStandardItem();
item->setText(QUrl::fromPercentEncoding(name.toUtf8()));
item->setData(name);
list << item
<< new QStandardItem(GroupByToString(g.first)) << new QStandardItem(GroupByToString(g.first))
<< new QStandardItem(GroupByToString(g.second)) << new QStandardItem(GroupByToString(g.second))
<< new QStandardItem(GroupByToString(g.third)); << new QStandardItem(GroupByToString(g.third));
@@ -185,8 +192,9 @@ void SavedGroupingManager::UpdateModel() {
else { else {
QStringList saved = s.childKeys(); QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) { for (int i = 0; i < saved.size(); ++i) {
if (saved.at(i) == "version"_L1) continue; const QString &name = saved.at(i);
s.remove(saved.at(i)); if (name == "version"_L1) continue;
s.remove(name);
} }
} }
s.endGroup(); s.endGroup();
@@ -202,7 +210,7 @@ void SavedGroupingManager::Remove() {
for (const QModelIndex &idx : indexes) { for (const QModelIndex &idx : indexes) {
if (idx.isValid()) { if (idx.isValid()) {
qLog(Debug) << "Remove saved grouping: " << model_->item(idx.row(), 0)->text(); qLog(Debug) << "Remove saved grouping: " << model_->item(idx.row(), 0)->text();
s.remove(model_->item(idx.row(), 0)->text()); s.remove(model_->item(idx.row(), 0)->data().toString());
} }
} }
s.endGroup(); s.endGroup();

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2024-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -24,18 +24,20 @@ namespace CollectionSettings {
constexpr char kSettingsGroup[] = "Collection"; constexpr char kSettingsGroup[] = "Collection";
constexpr char kStartupScan[] = "startup_scan";
constexpr char kMonitor[] = "monitor";
constexpr char kSongTracking[] = "song_tracking";
constexpr char kMarkSongsUnavailable[] = "mark_songs_unavailable";
constexpr char kSongENUR128LoudnessAnalysis[] = "song_ebur128_loudness_analysis";
constexpr char kExpireUnavailableSongs[] = "expire_unavailable_songs";
constexpr char kCoverArtPatterns[] = "cover_art_patterns";
constexpr char kAutoOpen[] = "auto_open"; constexpr char kAutoOpen[] = "auto_open";
constexpr char kShowDividers[] = "show_dividers"; constexpr char kShowDividers[] = "show_dividers";
constexpr char kPrettyCovers[] = "pretty_covers"; constexpr char kPrettyCovers[] = "pretty_covers";
constexpr char kVariousArtists[] = "various_artists"; constexpr char kVariousArtists[] = "various_artists";
constexpr char kSortSkipsArticles[] = "sort_skips_articles"; constexpr char kSkipArticlesForArtists[] = "skip_articles_for_artists";
constexpr char kStartupScan[] = "startup_scan"; constexpr char kSkipArticlesForAlbums[] = "skip_articles_for_albums";
constexpr char kMonitor[] = "monitor"; constexpr char kShowSortText[] = "show_sort_text";
constexpr char kSongTracking[] = "song_tracking";
constexpr char kSongENUR128LoudnessAnalysis[] = "song_ebur128_loudness_analysis";
constexpr char kMarkSongsUnavailable[] = "mark_songs_unavailable";
constexpr char kExpireUnavailableSongs[] = "expire_unavailable_songs";
constexpr char kCoverArtPatterns[] = "cover_art_patterns";
constexpr char kSettingsCacheSize[] = "cache_size"; constexpr char kSettingsCacheSize[] = "cache_size";
constexpr char kSettingsCacheSizeUnit[] = "cache_size_unit"; constexpr char kSettingsCacheSizeUnit[] = "cache_size_unit";
constexpr char kSettingsDiskCacheEnable[] = "disk_cache_enable"; constexpr char kSettingsDiskCacheEnable[] = "disk_cache_enable";

View File

@@ -30,7 +30,7 @@ constexpr char kFileFilter[] =
"*.aif *.aiff *.mka *.tta *.dsf *.dsd " "*.aif *.aiff *.mka *.tta *.dsf *.dsd "
"*.cue *.m3u *.m3u8 *.pls *.xspf *.asxini " "*.cue *.m3u *.m3u8 *.pls *.xspf *.asxini "
"*.ac3 *.dts " "*.ac3 *.dts "
"*.mod *.s3m *.xm *.it" "*.mod *.s3m *.xm *.it "
"*.spc *.vgm"; "*.spc *.vgm";
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)"); constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)");

View File

@@ -71,6 +71,14 @@ constexpr char kSettingsGroup[] = "DiscordRPC";
constexpr char kEnabled[] = "enabled"; constexpr char kEnabled[] = "enabled";
constexpr char kStatusDisplayType[] = "StatusDisplayType";
enum class StatusDisplayType {
App = 0,
Artist,
Song
};
} // namespace } // namespace
#endif // NOTIFICATIONSSETTINGS_H #endif // NOTIFICATIONSSETTINGS_H

View File

@@ -396,7 +396,7 @@ void ContextView::UpdateNoSong() {
void ContextView::NoSong() { void ContextView::NoSong() {
if (!widget_album_->isVisible()) { if (!widget_album_->isVisibleTo(this)) {
widget_album_->show(); widget_album_->show();
} }
@@ -440,11 +440,11 @@ void ContextView::SetSong() {
label_stop_summary_->clear(); label_stop_summary_->clear();
bool widget_album_changed = !song_prev_.is_valid(); bool widget_album_changed = !song_prev_.is_valid();
if (action_show_album_->isChecked() && !widget_album_->isVisible()) { if (action_show_album_->isChecked() && !widget_album_->isVisibleTo(this)) {
widget_album_->show(); widget_album_->show();
widget_album_changed = true; widget_album_changed = true;
} }
else if (!action_show_album_->isChecked() && widget_album_->isVisible()) { else if (!action_show_album_->isChecked() && widget_album_->isVisibleTo(this)) {
widget_album_->hide(); widget_album_->hide();
widget_album_changed = true; widget_album_changed = true;
} }

View File

@@ -50,7 +50,7 @@
using namespace Qt::Literals::StringLiterals; using namespace Qt::Literals::StringLiterals;
const int Database::kSchemaVersion = 20; const int Database::kSchemaVersion = 21;
namespace { namespace {
constexpr char kDatabaseFilename[] = "strawberry.db"; constexpr char kDatabaseFilename[] = "strawberry.db";
@@ -414,11 +414,6 @@ void Database::ExecSongTablesCommands(QSqlDatabase &db, const QStringList &song_
// We allow a magic value in the schema files to update all songs tables at once. // We allow a magic value in the schema files to update all songs tables at once.
if (command.contains(QLatin1String(kMagicAllSongsTables))) { if (command.contains(QLatin1String(kMagicAllSongsTables))) {
for (const QString &table : song_tables) { 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_"_L1) && command.contains(QLatin1String(kMagicAllSongsTables) + "_fts"_L1)) {
continue;
}
qLog(Info) << "Updating" << table << "for" << kMagicAllSongsTables; qLog(Info) << "Updating" << table << "for" << kMagicAllSongsTables;
QString new_command(command); QString new_command(command);
new_command.replace(QLatin1String(kMagicAllSongsTables), table); new_command.replace(QLatin1String(kMagicAllSongsTables), table);

View File

@@ -157,12 +157,16 @@ void HttpBaseRequest::HandleSSLErrors(const QList<QSslError> &ssl_errors) {
HttpBaseRequest::ReplyDataResult HttpBaseRequest::GetReplyData(QNetworkReply *reply) { HttpBaseRequest::ReplyDataResult HttpBaseRequest::GetReplyData(QNetworkReply *reply) {
if (reply->error() != QNetworkReply::NoError) { if (reply->error() != QNetworkReply::NoError) {
if (reply->error() >= 200) {
reply->readAll(); // QTBUG-135641
}
return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
} }
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) { if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) {
const int http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); const int http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (http_status_code < 200 || http_status_code > 207) { if (http_status_code < 200 || http_status_code > 207) {
reply->readAll(); // QTBUG-135641
return ReplyDataResult(ErrorCode::HttpError, QStringLiteral("Received HTTP code %1").arg(http_status_code)); return ReplyDataResult(ErrorCode::HttpError, QStringLiteral("Received HTTP code %1").arg(http_status_code));
} }
} }

View File

@@ -156,10 +156,8 @@
#include "lyrics/lyricsproviders.h" #include "lyrics/lyricsproviders.h"
#include "device/devicemanager.h" #include "device/devicemanager.h"
#include "device/devicestatefiltermodel.h" #include "device/devicestatefiltermodel.h"
#ifndef Q_OS_WIN32 #include "device/deviceview.h"
# include "device/deviceview.h" #include "device/deviceviewcontainer.h"
# include "device/deviceviewcontainer.h"
#endif
#include "transcoder/transcodedialog.h" #include "transcoder/transcodedialog.h"
#include "settings/settingsdialog.h" #include "settings/settingsdialog.h"
#include "constants/behavioursettings.h" #include "constants/behavioursettings.h"
@@ -175,6 +173,7 @@
# include "constants/tidalsettings.h" # include "constants/tidalsettings.h"
#endif #endif
#ifdef HAVE_SPOTIFY #ifdef HAVE_SPOTIFY
# include "spotify/spotifyservice.h"
# include "constants/spotifysettings.h" # include "constants/spotifysettings.h"
#endif #endif
#ifdef HAVE_QOBUZ #ifdef HAVE_QOBUZ
@@ -280,7 +279,7 @@ constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-
#endif // HAVE_QTSPARKLE #endif // HAVE_QTSPARKLE
MainWindow::MainWindow(Application *app, MainWindow::MainWindow(Application *app,
SharedPtr<SystemTrayIcon> tray_icon, OSDBase *osd, SharedPtr<SystemTrayIcon> systemtrayicon, OSDBase *osd,
#ifdef HAVE_DISCORD_RPC #ifdef HAVE_DISCORD_RPC
discord::RichPresence *discord_rich_presence, discord::RichPresence *discord_rich_presence,
#endif #endif
@@ -292,7 +291,7 @@ MainWindow::MainWindow(Application *app,
thumbbar_(new Windows7ThumbBar(this)), thumbbar_(new Windows7ThumbBar(this)),
#endif #endif
app_(app), app_(app),
tray_icon_(tray_icon), systemtrayicon_(systemtrayicon),
osd_(osd), osd_(osd),
#ifdef HAVE_DISCORD_RPC #ifdef HAVE_DISCORD_RPC
discord_rich_presence_(discord_rich_presence), discord_rich_presence_(discord_rich_presence),
@@ -310,9 +309,7 @@ MainWindow::MainWindow(Application *app,
context_view_(new ContextView(this)), context_view_(new ContextView(this)),
collection_view_(new CollectionViewContainer(this)), collection_view_(new CollectionViewContainer(this)),
file_view_(new FileView(this)), file_view_(new FileView(this)),
#ifndef Q_OS_WIN32
device_view_(new DeviceViewContainer(this)), device_view_(new DeviceViewContainer(this)),
#endif
playlist_list_(new PlaylistListContainer(this)), playlist_list_(new PlaylistListContainer(this)),
queue_view_(new QueueView(this)), queue_view_(new QueueView(this)),
settings_dialog_(std::bind(&MainWindow::CreateSettingsDialog, this)), settings_dialog_(std::bind(&MainWindow::CreateSettingsDialog, this)),
@@ -375,9 +372,7 @@ MainWindow::MainWindow(Application *app,
playlist_move_to_collection_(nullptr), playlist_move_to_collection_(nullptr),
playlist_open_in_browser_(nullptr), playlist_open_in_browser_(nullptr),
playlist_organize_(nullptr), playlist_organize_(nullptr),
#ifndef Q_OS_WIN32
playlist_copy_to_device_(nullptr), playlist_copy_to_device_(nullptr),
#endif
playlist_delete_(nullptr), playlist_delete_(nullptr),
playlist_queue_(nullptr), playlist_queue_(nullptr),
playlist_queue_play_next_(nullptr), playlist_queue_play_next_(nullptr),
@@ -409,7 +404,11 @@ MainWindow::MainWindow(Application *app,
// Initialize the UI // Initialize the UI
ui_->setupUi(this); ui_->setupUi(this);
setWindowIcon(IconLoader::Load(u"strawberry"_s)); if (QGuiApplication::platformName() != "wayland"_L1) {
setWindowIcon(IconLoader::Load(u"strawberry"_s));
}
systemtrayicon_->SetDevicePixelRatioF(devicePixelRatioF());
QObject::connect(&*app->database(), &Database::Error, this, &MainWindow::ShowErrorDialog); QObject::connect(&*app->database(), &Database::Error, this, &MainWindow::ShowErrorDialog);
@@ -430,9 +429,7 @@ MainWindow::MainWindow(Application *app,
ui_->tabs->AddTab(smartplaylists_view_, u"smartplaylists"_s, IconLoader::Load(u"view-media-playlist"_s, true, 0, 32), tr("Smart playlists")); ui_->tabs->AddTab(smartplaylists_view_, u"smartplaylists"_s, IconLoader::Load(u"view-media-playlist"_s, true, 0, 32), tr("Smart playlists"));
ui_->tabs->AddTab(file_view_, u"files"_s, IconLoader::Load(u"document-open"_s, true, 0, 32), tr("Files")); ui_->tabs->AddTab(file_view_, u"files"_s, IconLoader::Load(u"document-open"_s, true, 0, 32), tr("Files"));
ui_->tabs->AddTab(radio_view_, u"radios"_s, IconLoader::Load(u"radio"_s, true, 0, 32), tr("Radios")); ui_->tabs->AddTab(radio_view_, u"radios"_s, IconLoader::Load(u"radio"_s, true, 0, 32), tr("Radios"));
#ifndef Q_OS_WIN32
ui_->tabs->AddTab(device_view_, u"devices"_s, IconLoader::Load(u"device"_s, true, 0, 32), tr("Devices")); ui_->tabs->AddTab(device_view_, u"devices"_s, IconLoader::Load(u"device"_s, true, 0, 32), tr("Devices"));
#endif
#ifdef HAVE_SUBSONIC #ifdef HAVE_SUBSONIC
ui_->tabs->AddTab(subsonic_view_, u"subsonic"_s, IconLoader::Load(u"subsonic"_s, true, 0, 32), tr("Subsonic")); ui_->tabs->AddTab(subsonic_view_, u"subsonic"_s, IconLoader::Load(u"subsonic"_s, true, 0, 32), tr("Subsonic"));
#endif #endif
@@ -480,9 +477,7 @@ MainWindow::MainWindow(Application *app,
collection_view_->view()->setModel(app_->collection()->model()->filter()); collection_view_->view()->setModel(app_->collection()->model()->filter());
collection_view_->view()->Init(app->task_manager(), app->tagreader_client(), app->network(), app->albumcover_loader(), app->current_albumcover_loader(), app->cover_providers(), app->lyrics_providers(), app->collection(), app->device_manager(), app->streaming_services()); collection_view_->view()->Init(app->task_manager(), app->tagreader_client(), app->network(), app->albumcover_loader(), app->current_albumcover_loader(), app->cover_providers(), app->lyrics_providers(), app->collection(), app->device_manager(), app->streaming_services());
#ifndef Q_OS_WIN32
device_view_->view()->Init(app->task_manager(), app->tagreader_client(), app->device_manager(), app->collection_model()->directory_model()); device_view_->view()->Init(app->task_manager(), app->tagreader_client(), app->device_manager(), app->collection_model()->directory_model());
#endif
playlist_list_->Init(app_->task_manager(), app->tagreader_client(), app_->playlist_manager(), app_->playlist_backend(), app_->device_manager()); playlist_list_->Init(app_->task_manager(), app->tagreader_client(), app_->playlist_manager(), app_->playlist_backend(), app_->device_manager());
organize_dialog_->SetDestinationModel(app_->collection()->model()->directory_model()); organize_dialog_->SetDestinationModel(app_->collection()->model()->directory_model());
@@ -554,9 +549,7 @@ MainWindow::MainWindow(Application *app,
QObject::connect(file_view_, &FileView::CopyToCollection, this, &MainWindow::CopyFilesToCollection); QObject::connect(file_view_, &FileView::CopyToCollection, this, &MainWindow::CopyFilesToCollection);
QObject::connect(file_view_, &FileView::MoveToCollection, this, &MainWindow::MoveFilesToCollection); QObject::connect(file_view_, &FileView::MoveToCollection, this, &MainWindow::MoveFilesToCollection);
QObject::connect(file_view_, &FileView::EditTags, this, &MainWindow::EditFileTags); QObject::connect(file_view_, &FileView::EditTags, this, &MainWindow::EditFileTags);
#ifndef Q_OS_WIN32
QObject::connect(file_view_, &FileView::CopyToDevice, this, &MainWindow::CopyFilesToDevice); QObject::connect(file_view_, &FileView::CopyToDevice, this, &MainWindow::CopyFilesToDevice);
#endif
file_view_->SetTaskManager(app_->task_manager()); file_view_->SetTaskManager(app_->task_manager());
// Action connections // Action connections
@@ -718,10 +711,8 @@ MainWindow::MainWindow(Application *app,
QObject::connect(album_cover_choice_controller_->search_cover_auto_action(), &QAction::triggered, this, &MainWindow::SearchCoverAutomatically); QObject::connect(album_cover_choice_controller_->search_cover_auto_action(), &QAction::triggered, this, &MainWindow::SearchCoverAutomatically);
QObject::connect(album_cover_choice_controller_->search_cover_auto_action(), &QAction::toggled, this, &MainWindow::ToggleSearchCoverAuto); QObject::connect(album_cover_choice_controller_->search_cover_auto_action(), &QAction::toggled, this, &MainWindow::ToggleSearchCoverAuto);
#ifndef Q_OS_WIN32
// Devices connections // Devices connections
QObject::connect(device_view_->view(), &DeviceView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist); QObject::connect(device_view_->view(), &DeviceView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
#endif
// Collection filter widget // Collection filter widget
QActionGroup *collection_view_group = new QActionGroup(this); QActionGroup *collection_view_group = new QActionGroup(this);
@@ -784,6 +775,9 @@ MainWindow::MainWindow(Application *app,
QObject::connect(spotify_view_->songs_collection_view(), &StreamingCollectionView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist); QObject::connect(spotify_view_->songs_collection_view(), &StreamingCollectionView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
QObject::connect(spotify_view_->search_view(), &StreamingSearchView::OpenSettingsDialog, this, &MainWindow::OpenServiceSettingsDialog); QObject::connect(spotify_view_->search_view(), &StreamingSearchView::OpenSettingsDialog, this, &MainWindow::OpenServiceSettingsDialog);
QObject::connect(spotify_view_->search_view(), &StreamingSearchView::AddToPlaylist, this, &MainWindow::AddToPlaylist); QObject::connect(spotify_view_->search_view(), &StreamingSearchView::AddToPlaylist, this, &MainWindow::AddToPlaylist);
if (SpotifyServicePtr spotifyservice = app_->streaming_services()->Service<SpotifyService>()) {
QObject::connect(&*spotifyservice, &SpotifyService::UpdateSpotifyAccessToken, &*app_->player()->engine(), &EngineBase::UpdateSpotifyAccessToken);
}
#endif #endif
QObject::connect(radio_view_, &RadioViewContainer::Refresh, &*app_->radio_services(), &RadioServices::RefreshChannels); QObject::connect(radio_view_, &RadioViewContainer::Refresh, &*app_->radio_services(), &RadioServices::RefreshChannels);
@@ -824,9 +818,7 @@ MainWindow::MainWindow(Application *app,
playlist_organize_ = playlist_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Organize files..."), this, &MainWindow::PlaylistMoveToCollection); playlist_organize_ = playlist_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Organize files..."), this, &MainWindow::PlaylistMoveToCollection);
playlist_copy_to_collection_ = playlist_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Copy to collection..."), this, &MainWindow::PlaylistCopyToCollection); playlist_copy_to_collection_ = playlist_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Copy to collection..."), this, &MainWindow::PlaylistCopyToCollection);
playlist_move_to_collection_ = playlist_menu_->addAction(IconLoader::Load(u"go-jump"_s), tr("Move to collection..."), this, &MainWindow::PlaylistMoveToCollection); playlist_move_to_collection_ = playlist_menu_->addAction(IconLoader::Load(u"go-jump"_s), tr("Move to collection..."), this, &MainWindow::PlaylistMoveToCollection);
#ifndef Q_OS_WIN32
playlist_copy_to_device_ = playlist_menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &MainWindow::PlaylistCopyToDevice); playlist_copy_to_device_ = playlist_menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &MainWindow::PlaylistCopyToDevice);
#endif
playlist_delete_ = playlist_menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &MainWindow::PlaylistDelete); playlist_delete_ = playlist_menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &MainWindow::PlaylistDelete);
playlist_menu_->addSeparator(); playlist_menu_->addSeparator();
playlistitem_actions_separator_ = playlist_menu_->addSeparator(); playlistitem_actions_separator_ = playlist_menu_->addSeparator();
@@ -845,10 +837,8 @@ MainWindow::MainWindow(Application *app,
QObject::connect(ui_->playlist, &PlaylistContainer::UndoRedoActionsChanged, this, &MainWindow::PlaylistUndoRedoChanged); QObject::connect(ui_->playlist, &PlaylistContainer::UndoRedoActionsChanged, this, &MainWindow::PlaylistUndoRedoChanged);
QObject::connect(&*app_->device_manager(), &DeviceManager::DeviceError, this, &MainWindow::ShowErrorDialog); QObject::connect(&*app_->device_manager(), &DeviceManager::DeviceError, this, &MainWindow::ShowErrorDialog);
#ifndef WIN32
QObject::connect(app_->device_manager()->connected_devices_model(), &DeviceStateFilterModel::IsEmptyChanged, playlist_copy_to_device_, &QAction::setDisabled); QObject::connect(app_->device_manager()->connected_devices_model(), &DeviceStateFilterModel::IsEmptyChanged, playlist_copy_to_device_, &QAction::setDisabled);
playlist_copy_to_device_->setDisabled(app_->device_manager()->connected_devices_model()->rowCount() == 0); playlist_copy_to_device_->setDisabled(app_->device_manager()->connected_devices_model()->rowCount() == 0);
#endif
QObject::connect(&*app_->scrobbler()->settings(), &ScrobblerSettingsService::ScrobblingEnabledChanged, this, &MainWindow::ScrobblingEnabledChanged); QObject::connect(&*app_->scrobbler()->settings(), &ScrobblerSettingsService::ScrobblingEnabledChanged, this, &MainWindow::ScrobblingEnabledChanged);
QObject::connect(&*app_->scrobbler()->settings(), &ScrobblerSettingsService::ScrobbleButtonVisibilityChanged, this, &MainWindow::ScrobbleButtonVisibilityChanged); QObject::connect(&*app_->scrobbler()->settings(), &ScrobblerSettingsService::ScrobbleButtonVisibilityChanged, this, &MainWindow::ScrobbleButtonVisibilityChanged);
@@ -858,14 +848,14 @@ MainWindow::MainWindow(Application *app,
mac::SetApplicationHandler(this); mac::SetApplicationHandler(this);
#endif #endif
// Tray icon // Tray icon
tray_icon_->SetupMenu(ui_->action_previous_track, ui_->action_play_pause, ui_->action_stop, ui_->action_stop_after_this_track, ui_->action_next_track, ui_->action_mute, ui_->action_love, ui_->action_quit); systemtrayicon_->SetupMenu(ui_->action_previous_track, ui_->action_play_pause, ui_->action_stop, ui_->action_stop_after_this_track, ui_->action_next_track, ui_->action_mute, ui_->action_love, ui_->action_quit);
QObject::connect(&*tray_icon_, &SystemTrayIcon::PlayPause, &*app_->player(), &Player::PlayPauseHelper); QObject::connect(&*systemtrayicon_, &SystemTrayIcon::PlayPause, &*app_->player(), &Player::PlayPauseHelper);
QObject::connect(&*tray_icon_, &SystemTrayIcon::SeekForward, &*app_->player(), &Player::SeekForward); QObject::connect(&*systemtrayicon_, &SystemTrayIcon::SeekForward, &*app_->player(), &Player::SeekForward);
QObject::connect(&*tray_icon_, &SystemTrayIcon::SeekBackward, &*app_->player(), &Player::SeekBackward); QObject::connect(&*systemtrayicon_, &SystemTrayIcon::SeekBackward, &*app_->player(), &Player::SeekBackward);
QObject::connect(&*tray_icon_, &SystemTrayIcon::NextTrack, &*app_->player(), &Player::Next); QObject::connect(&*systemtrayicon_, &SystemTrayIcon::NextTrack, &*app_->player(), &Player::Next);
QObject::connect(&*tray_icon_, &SystemTrayIcon::PreviousTrack, &*app_->player(), &Player::Previous); QObject::connect(&*systemtrayicon_, &SystemTrayIcon::PreviousTrack, &*app_->player(), &Player::Previous);
QObject::connect(&*tray_icon_, &SystemTrayIcon::ShowHide, this, &MainWindow::ToggleShowHide); QObject::connect(&*systemtrayicon_, &SystemTrayIcon::ShowHide, this, &MainWindow::ToggleShowHide);
QObject::connect(&*tray_icon_, &SystemTrayIcon::ChangeVolume, this, &MainWindow::VolumeWheelEvent); QObject::connect(&*systemtrayicon_, &SystemTrayIcon::ChangeVolume, this, &MainWindow::VolumeWheelEvent);
// Windows 7 thumbbar buttons // Windows 7 thumbbar buttons
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
@@ -980,7 +970,7 @@ MainWindow::MainWindow(Application *app,
QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdateLastPlayed, &*app_->collection_backend(), &CollectionBackend::UpdateLastPlayed); QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdateLastPlayed, &*app_->collection_backend(), &CollectionBackend::UpdateLastPlayed);
QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdatePlayCount, &*app_->collection_backend(), &CollectionBackend::UpdatePlayCount); QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdatePlayCount, &*app_->collection_backend(), &CollectionBackend::UpdatePlayCount);
#if !defined(HAVE_AUDIOCD) || defined(Q_OS_WIN32) #if !defined(HAVE_AUDIOCD)
ui_->action_open_cd->setEnabled(false); ui_->action_open_cd->setEnabled(false);
ui_->action_open_cd->setVisible(false); ui_->action_open_cd->setVisible(false);
#endif #endif
@@ -1043,7 +1033,7 @@ MainWindow::MainWindow(Application *app,
show(); show();
break; break;
case BehaviourSettings::StartupBehaviour::Hide: case BehaviourSettings::StartupBehaviour::Hide:
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible()) { if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible()) {
break; break;
} }
[[fallthrough]]; [[fallthrough]];
@@ -1056,7 +1046,7 @@ MainWindow::MainWindow(Application *app,
was_minimized_ = settings_.value(MainWindowSettings::kMinimized, false).toBool(); was_minimized_ = settings_.value(MainWindowSettings::kMinimized, false).toBool();
if (was_minimized_) setWindowState(windowState() | Qt::WindowMinimized); if (was_minimized_) setWindowState(windowState() | Qt::WindowMinimized);
if (!tray_icon_->IsSystemTrayAvailable() || !tray_icon_->isVisible() || !settings_.value(MainWindowSettings::kHidden, false).toBool()) { if (!systemtrayicon_->IsSystemTrayAvailable() || !systemtrayicon_->isVisible() || !settings_.value(MainWindowSettings::kHidden, false).toBool()) {
show(); show();
} }
break; break;
@@ -1168,13 +1158,13 @@ void MainWindow::ReloadSettings() {
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
constexpr bool keeprunning_available = true; constexpr bool keeprunning_available = true;
#else #else
const bool systemtray_available = tray_icon_->IsSystemTrayAvailable(); const bool systemtray_available = systemtrayicon_->IsSystemTrayAvailable();
s.beginGroup(BehaviourSettings::kSettingsGroup); s.beginGroup(BehaviourSettings::kSettingsGroup);
const bool showtrayicon = s.value(BehaviourSettings::kShowTrayIcon, systemtray_available).toBool(); const bool showtrayicon = s.value(BehaviourSettings::kShowTrayIcon, systemtray_available).toBool();
s.endGroup(); s.endGroup();
const bool keeprunning_available = systemtray_available && showtrayicon; const bool keeprunning_available = systemtray_available && showtrayicon;
if (systemtray_available) { if (systemtray_available) {
tray_icon_->setVisible(showtrayicon); systemtrayicon_->setVisible(showtrayicon);
} }
if ((!showtrayicon || !systemtray_available) && !isVisible()) { if ((!showtrayicon || !systemtray_available) && !isVisible()) {
show(); show();
@@ -1199,7 +1189,7 @@ void MainWindow::ReloadSettings() {
int iconsize = s.value(AppearanceSettings::kIconSizePlayControlButtons, 32).toInt(); int iconsize = s.value(AppearanceSettings::kIconSizePlayControlButtons, 32).toInt();
s.endGroup(); s.endGroup();
tray_icon_->SetTrayiconProgress(trayicon_progress); systemtrayicon_->SetTrayiconProgress(trayicon_progress);
#ifdef HAVE_DBUS #ifdef HAVE_DBUS
if (taskbar_progress_ && !taskbar_progress) { if (taskbar_progress_ && !taskbar_progress) {
@@ -1221,11 +1211,11 @@ void MainWindow::ReloadSettings() {
ui_->volume->SetEnabled(volume_control); ui_->volume->SetEnabled(volume_control);
if (volume_control) { if (volume_control) {
if (!ui_->action_mute->isVisible()) ui_->action_mute->setVisible(true); if (!ui_->action_mute->isVisible()) ui_->action_mute->setVisible(true);
if (!tray_icon_->MuteEnabled()) tray_icon_->SetMuteEnabled(true); if (!systemtrayicon_->MuteEnabled()) systemtrayicon_->SetMuteEnabled(true);
} }
else { else {
if (ui_->action_mute->isVisible()) ui_->action_mute->setVisible(false); if (ui_->action_mute->isVisible()) ui_->action_mute->setVisible(false);
if (tray_icon_->MuteEnabled()) tray_icon_->SetMuteEnabled(false); if (systemtrayicon_->MuteEnabled()) systemtrayicon_->SetMuteEnabled(false);
} }
} }
@@ -1377,8 +1367,8 @@ void MainWindow::Exit() {
if (app_->player()->GetState() == EngineBase::State::Playing) { if (app_->player()->GetState() == EngineBase::State::Playing) {
app_->player()->Stop(); app_->player()->Stop();
hide(); hide();
if (tray_icon_->IsSystemTrayAvailable()) { if (systemtrayicon_->IsSystemTrayAvailable()) {
tray_icon_->setVisible(false); systemtrayicon_->setVisible(false);
} }
return; // Don't quit the application now: wait for the fadeout finished signal return; // Don't quit the application now: wait for the fadeout finished signal
} }
@@ -1435,7 +1425,7 @@ void MainWindow::MediaStopped() {
ui_->action_love->setEnabled(false); ui_->action_love->setEnabled(false);
ui_->button_love->setEnabled(false); ui_->button_love->setEnabled(false);
tray_icon_->LoveStateChanged(false); systemtrayicon_->LoveStateChanged(false);
if (track_position_timer_->isActive()) { if (track_position_timer_->isActive()) {
track_position_timer_->stop(); track_position_timer_->stop();
@@ -1444,8 +1434,8 @@ void MainWindow::MediaStopped() {
track_slider_timer_->stop(); track_slider_timer_->stop();
} }
ui_->track_slider->SetStopped(); ui_->track_slider->SetStopped();
tray_icon_->SetProgress(0); systemtrayicon_->SetProgress(0);
tray_icon_->SetStopped(); systemtrayicon_->SetStopped();
#ifdef HAVE_DBUS #ifdef HAVE_DBUS
if (taskbar_progress_) { if (taskbar_progress_) {
@@ -1477,7 +1467,7 @@ void MainWindow::MediaPaused() {
track_slider_timer_->start(); track_slider_timer_->start();
} }
tray_icon_->SetPaused(); systemtrayicon_->SetPaused();
} }
@@ -1498,7 +1488,7 @@ void MainWindow::MediaPlaying() {
} }
ui_->action_play_pause->setEnabled(enable_play_pause); ui_->action_play_pause->setEnabled(enable_play_pause);
ui_->track_slider->SetCanSeek(can_seek); ui_->track_slider->SetCanSeek(can_seek);
tray_icon_->SetPlaying(enable_play_pause); systemtrayicon_->SetPlaying(enable_play_pause);
if (!track_position_timer_->isActive()) { if (!track_position_timer_->isActive()) {
track_position_timer_->start(); track_position_timer_->start();
@@ -1515,18 +1505,18 @@ void MainWindow::SendNowPlaying() {
// Send now playing to scrobble services // Send now playing to scrobble services
Playlist *playlist = app_->playlist_manager()->active(); Playlist *playlist = app_->playlist_manager()->active();
if (app_->scrobbler()->enabled() && playlist && playlist->current_item() && playlist->current_item()->Metadata().is_metadata_good()) { if (app_->scrobbler()->enabled() && playlist && playlist->current_item() && playlist->current_item()->EffectiveMetadata().is_metadata_good()) {
app_->scrobbler()->UpdateNowPlaying(playlist->current_item()->Metadata()); app_->scrobbler()->UpdateNowPlaying(playlist->current_item()->EffectiveMetadata());
ui_->action_love->setEnabled(true); ui_->action_love->setEnabled(true);
ui_->button_love->setEnabled(true); ui_->button_love->setEnabled(true);
tray_icon_->LoveStateChanged(true); systemtrayicon_->LoveStateChanged(true);
} }
} }
void MainWindow::VolumeChanged(const uint volume) { void MainWindow::VolumeChanged(const uint volume) {
ui_->action_mute->setChecked(volume == 0); ui_->action_mute->setChecked(volume == 0);
tray_icon_->MuteButtonStateChanged(volume == 0); systemtrayicon_->MuteButtonStateChanged(volume == 0);
} }
void MainWindow::SongChanged(const Song &song) { void MainWindow::SongChanged(const Song &song) {
@@ -1536,7 +1526,7 @@ void MainWindow::SongChanged(const Song &song) {
song_playing_ = song; song_playing_ = song;
song_ = song; song_ = song;
setWindowTitle(song.PrettyTitleWithArtist()); setWindowTitle(song.PrettyTitleWithArtist());
tray_icon_->SetProgress(0); systemtrayicon_->SetProgress(0);
#ifdef HAVE_DBUS #ifdef HAVE_DBUS
if (taskbar_progress_) { if (taskbar_progress_) {
@@ -1562,9 +1552,9 @@ void MainWindow::TrackSkipped(PlaylistItemPtr item) {
// If it was a collection item then we have to increment its skipped count in the database. // If it was a collection item then we have to increment its skipped count in the database.
if (item && item->IsLocalCollectionItem() && item->Metadata().id() != -1) { if (item && item->IsLocalCollectionItem() && item->EffectiveMetadata().id() != -1) {
Song song = item->Metadata(); Song song = item->EffectiveMetadata();
const qint64 position = app_->player()->engine()->position_nanosec(); const qint64 position = app_->player()->engine()->position_nanosec();
const qint64 length = app_->player()->engine()->length_nanosec(); const qint64 length = app_->player()->engine()->length_nanosec();
const float percentage = (length == 0 ? 1 : static_cast<float>(position) / static_cast<float>(length)); const float percentage = (length == 0 ? 1 : static_cast<float>(position) / static_cast<float>(length));
@@ -1719,7 +1709,7 @@ void MainWindow::hideEvent(QHideEvent *e) {
void MainWindow::closeEvent(QCloseEvent *e) { void MainWindow::closeEvent(QCloseEvent *e) {
if (!exit_ && (!tray_icon_->IsSystemTrayAvailable() || !tray_icon_->isVisible() || !keep_running_)) { if (!exit_ && (!systemtrayicon_->IsSystemTrayAvailable() || !systemtrayicon_->isVisible() || !keep_running_)) {
Exit(); Exit();
} }
@@ -1730,7 +1720,7 @@ void MainWindow::closeEvent(QCloseEvent *e) {
void MainWindow::SetHiddenInTray(const bool hidden) { void MainWindow::SetHiddenInTray(const bool hidden) {
if (hidden && isVisible()) { if (hidden && isVisible()) {
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible() && keep_running_) { if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible() && keep_running_) {
close(); close();
} }
else { else {
@@ -1758,8 +1748,8 @@ void MainWindow::FilePathChanged(const QString &path) {
void MainWindow::Seeked(const qint64 microseconds) { void MainWindow::Seeked(const qint64 microseconds) {
const qint64 position = microseconds / kUsecPerSec; const qint64 position = microseconds / kUsecPerSec;
const qint64 length = app_->player()->GetCurrentItem()->Metadata().length_nanosec() / kNsecPerSec; const qint64 length = app_->player()->GetCurrentItem()->EffectiveMetadata().length_nanosec() / kNsecPerSec;
tray_icon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0)); systemtrayicon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
#ifdef HAVE_DBUS #ifdef HAVE_DBUS
if (taskbar_progress_) { if (taskbar_progress_) {
@@ -1774,12 +1764,12 @@ void MainWindow::UpdateTrackPosition() {
PlaylistItemPtr item(app_->player()->GetCurrentItem()); PlaylistItemPtr item(app_->player()->GetCurrentItem());
if (!item) return; if (!item) return;
const qint64 length = (item->Metadata().length_nanosec() / kNsecPerSec); const qint64 length = (item->EffectiveMetadata().length_nanosec() / kNsecPerSec);
if (length <= 0) return; if (length <= 0) return;
const int position = std::floor(static_cast<float>(app_->player()->engine()->position_nanosec()) / static_cast<float>(kNsecPerSec) + 0.5); const int position = std::floor(static_cast<float>(app_->player()->engine()->position_nanosec()) / static_cast<float>(kNsecPerSec) + 0.5);
// Update the tray icon every 10 seconds // Update the tray icon every 10 seconds
if (position % 10 == 0) tray_icon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0)); if (position % 10 == 0) systemtrayicon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
#ifdef HAVE_DBUS #ifdef HAVE_DBUS
if (taskbar_progress_) { if (taskbar_progress_) {
@@ -1788,12 +1778,12 @@ void MainWindow::UpdateTrackPosition() {
#endif #endif
// Send Scrobble // Send Scrobble
if (app_->scrobbler()->enabled() && item->Metadata().is_metadata_good()) { if (app_->scrobbler()->enabled() && item->EffectiveMetadata().is_metadata_good()) {
Playlist *playlist = app_->playlist_manager()->active(); Playlist *playlist = app_->playlist_manager()->active();
if (playlist && !playlist->scrobbled()) { if (playlist && !playlist->scrobbled()) {
const qint64 scrobble_point = (playlist->scrobble_point_nanosec() / kNsecPerSec); const qint64 scrobble_point = (playlist->scrobble_point_nanosec() / kNsecPerSec);
if (position >= scrobble_point) { if (position >= scrobble_point) {
app_->scrobbler()->Scrobble(item->Metadata(), scrobble_point); app_->scrobbler()->Scrobble(item->EffectiveMetadata(), scrobble_point);
playlist->set_scrobbled(true); playlist->set_scrobbled(true);
} }
} }
@@ -1910,7 +1900,7 @@ void MainWindow::AddToPlaylistFromAction(QAction *action) {
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row()); PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
if (!item) continue; if (!item) continue;
items << item; items << item;
songs << item->Metadata(); songs << item->EffectiveMetadata();
} }
// We're creating a new playlist // We're creating a new playlist
@@ -1989,12 +1979,12 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(src_idx.row()); PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(src_idx.row());
if (!item) continue; if (!item) continue;
if (item->Metadata().url().isLocalFile()) ++local_songs; if (item->EffectiveMetadata().url().isLocalFile()) ++local_songs;
if (item->Metadata().has_cue()) { if (item->EffectiveMetadata().has_cue()) {
cue_selected = true; cue_selected = true;
} }
else if (item->Metadata().IsEditable()) { else if (item->EffectiveMetadata().IsEditable()) {
++editable; ++editable;
} }
@@ -2032,9 +2022,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
playlist_show_in_collection_->setVisible(false); playlist_show_in_collection_->setVisible(false);
playlist_copy_to_collection_->setVisible(false); playlist_copy_to_collection_->setVisible(false);
playlist_move_to_collection_->setVisible(false); playlist_move_to_collection_->setVisible(false);
#ifndef Q_OS_WIN32
playlist_copy_to_device_->setVisible(false); playlist_copy_to_device_->setVisible(false);
#endif
playlist_organize_->setVisible(false); playlist_organize_->setVisible(false);
playlist_delete_->setVisible(false); playlist_delete_->setVisible(false);
@@ -2097,7 +2085,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
// Is it a collection item? // Is it a collection item?
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row()); PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
if (item && item->IsLocalCollectionItem() && item->Metadata().id() != -1) { if (item && item->IsLocalCollectionItem() && item->EffectiveMetadata().id() != -1) {
playlist_organize_->setVisible(local_songs > 0 && editable > 0 && !cue_selected); playlist_organize_->setVisible(local_songs > 0 && editable > 0 && !cue_selected);
playlist_show_in_collection_->setVisible(true); playlist_show_in_collection_->setVisible(true);
playlist_open_in_browser_->setVisible(true); playlist_open_in_browser_->setVisible(true);
@@ -2107,9 +2095,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
playlist_move_to_collection_->setVisible(local_songs > 0); playlist_move_to_collection_->setVisible(local_songs > 0);
} }
#ifndef Q_OS_WIN32
playlist_copy_to_device_->setVisible(local_songs > 0); playlist_copy_to_device_->setVisible(local_songs > 0);
#endif
playlist_delete_->setVisible(delete_files_ && local_songs > 0); playlist_delete_->setVisible(delete_files_ && local_songs > 0);
@@ -2189,9 +2175,9 @@ void MainWindow::RescanSongs() {
PlaylistItemPtr item(app_->playlist_manager()->current()->item_at(source_index.row())); PlaylistItemPtr item(app_->playlist_manager()->current()->item_at(source_index.row()));
if (!item) continue; if (!item) continue;
if (item->IsLocalCollectionItem()) { if (item->IsLocalCollectionItem()) {
songs << item->Metadata(); songs << item->EffectiveMetadata();
} }
else if (item->Metadata().source() == Song::Source::LocalFile) { else if (item->EffectiveMetadata().source() == Song::Source::LocalFile) {
QPersistentModelIndex persistent_index = QPersistentModelIndex(source_index); QPersistentModelIndex persistent_index = QPersistentModelIndex(source_index);
app_->playlist_manager()->current()->ItemReload(persistent_index, item->OriginalMetadata(), false); app_->playlist_manager()->current()->ItemReload(persistent_index, item->OriginalMetadata(), false);
} }
@@ -2751,7 +2737,6 @@ void MainWindow::MoveFilesToCollection(const QList<QUrl> &urls) {
void MainWindow::CopyFilesToDevice(const QList<QUrl> &urls) { void MainWindow::CopyFilesToDevice(const QList<QUrl> &urls) {
#ifndef Q_OS_WIN32
organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true); organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
organize_dialog_->SetCopy(true); organize_dialog_->SetCopy(true);
if (organize_dialog_->SetUrls(urls)) { if (organize_dialog_->SetUrls(urls)) {
@@ -2761,9 +2746,6 @@ void MainWindow::CopyFilesToDevice(const QList<QUrl> &urls) {
else { else {
QMessageBox::warning(this, tr("Error"), tr("None of the selected songs were suitable for copying to a device")); QMessageBox::warning(this, tr("Error"), tr("None of the selected songs were suitable for copying to a device"));
} }
#else
Q_UNUSED(urls);
#endif
} }
@@ -2823,7 +2805,7 @@ void MainWindow::PlaylistOpenInBrowser() {
for (const QModelIndex &proxy_index : proxy_indexes) { for (const QModelIndex &proxy_index : proxy_indexes) {
const QModelIndex source_index = app_->playlist_manager()->current()->filter()->mapToSource(proxy_index); const QModelIndex source_index = app_->playlist_manager()->current()->filter()->mapToSource(proxy_index);
if (!source_index.isValid()) continue; if (!source_index.isValid()) continue;
urls << QUrl(source_index.sibling(source_index.row(), static_cast<int>(Playlist::Column::Filename)).data().toString()); urls << QUrl(source_index.sibling(source_index.row(), static_cast<int>(Playlist::Column::URL)).data().toString());
} }
Utilities::OpenInFileBrowser(urls); Utilities::OpenInFileBrowser(urls);
@@ -2839,7 +2821,7 @@ void MainWindow::PlaylistCopyUrl() {
if (!source_index.isValid()) continue; if (!source_index.isValid()) continue;
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row()); PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
if (!item) continue; if (!item) continue;
urls << item->StreamUrl(); urls << item->EffectiveUrl();
} }
if (urls.count() > 0) { if (urls.count() > 0) {
@@ -2891,8 +2873,6 @@ void MainWindow::PlaylistSkip() {
void MainWindow::PlaylistCopyToDevice() { void MainWindow::PlaylistCopyToDevice() {
#ifndef Q_OS_WIN32
SongList songs; SongList songs;
const QModelIndexList proxy_indexes = ui_->playlist->view()->selectionModel()->selectedRows(); const QModelIndexList proxy_indexes = ui_->playlist->view()->selectionModel()->selectedRows();
@@ -2917,8 +2897,6 @@ void MainWindow::PlaylistCopyToDevice() {
QMessageBox::warning(this, tr("Error"), tr("None of the selected songs were suitable for copying to a device")); QMessageBox::warning(this, tr("Error"), tr("None of the selected songs were suitable for copying to a device"));
} }
#endif
} }
void MainWindow::ChangeCollectionFilterMode(QAction *action) { void MainWindow::ChangeCollectionFilterMode(QAction *action) {
@@ -3283,7 +3261,7 @@ void MainWindow::LoveButtonVisibilityChanged(const bool value) {
else else
ui_->widget_love->hide(); ui_->widget_love->hide();
tray_icon_->LoveVisibilityChanged(value); systemtrayicon_->LoveVisibilityChanged(value);
} }
@@ -3306,7 +3284,7 @@ void MainWindow::Love() {
app_->scrobbler()->Love(); app_->scrobbler()->Love();
ui_->button_love->setEnabled(false); ui_->button_love->setEnabled(false);
ui_->action_love->setEnabled(false); ui_->action_love->setEnabled(false);
tray_icon_->LoveStateChanged(false); systemtrayicon_->LoveStateChanged(false);
} }
@@ -3321,10 +3299,10 @@ void MainWindow::PlaylistDelete() {
for (const QModelIndex &proxy_idx : proxy_indexes) { for (const QModelIndex &proxy_idx : proxy_indexes) {
QModelIndex source_idx = app_->playlist_manager()->current()->filter()->mapToSource(proxy_idx); QModelIndex source_idx = app_->playlist_manager()->current()->filter()->mapToSource(proxy_idx);
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_idx.row()); PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_idx.row());
if (!item || !item->Metadata().url().isLocalFile()) continue; if (!item || !item->EffectiveMetadata().url().isLocalFile()) continue;
QString filename = item->Metadata().url().toLocalFile(); QString filename = item->EffectiveMetadata().url().toLocalFile();
if (files.contains(filename)) continue; if (files.contains(filename)) continue;
selected_songs << item->Metadata(); selected_songs << item->EffectiveMetadata();
files << filename; files << filename;
if (item == app_->player()->GetCurrentItem()) is_current_item = true; if (item == app_->player()->GetCurrentItem()) is_current_item = true;
} }

View File

@@ -73,9 +73,7 @@ class CollectionViewContainer;
class CollectionFilter; class CollectionFilter;
class AlbumCoverChoiceController; class AlbumCoverChoiceController;
class CommandlineOptions; class CommandlineOptions;
#ifndef Q_OS_WIN32
class DeviceViewContainer; class DeviceViewContainer;
#endif
class EditTagDialog; class EditTagDialog;
class Equalizer; class Equalizer;
class ErrorDialog; class ErrorDialog;
@@ -113,7 +111,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
public: public:
explicit MainWindow(Application *app, explicit MainWindow(Application *app,
SharedPtr<SystemTrayIcon> tray_icon, SharedPtr<SystemTrayIcon> systemtrayicon,
OSDBase *osd, OSDBase *osd,
#ifdef HAVE_DISCORD_RPC #ifdef HAVE_DISCORD_RPC
discord::RichPresence *discord_rich_presence, discord::RichPresence *discord_rich_presence,
@@ -312,7 +310,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
#endif #endif
Application *app_; Application *app_;
SharedPtr<SystemTrayIcon> tray_icon_; SharedPtr<SystemTrayIcon> systemtrayicon_;
OSDBase *osd_; OSDBase *osd_;
#ifdef HAVE_DISCORD_RPC #ifdef HAVE_DISCORD_RPC
discord::RichPresence *discord_rich_presence_; discord::RichPresence *discord_rich_presence_;
@@ -327,9 +325,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
ContextView *context_view_; ContextView *context_view_;
CollectionViewContainer *collection_view_; CollectionViewContainer *collection_view_;
FileView *file_view_; FileView *file_view_;
#ifndef Q_OS_WIN32
DeviceViewContainer *device_view_; DeviceViewContainer *device_view_;
#endif
PlaylistListContainer *playlist_list_; PlaylistListContainer *playlist_list_;
QueueView *queue_view_; QueueView *queue_view_;
@@ -380,9 +376,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
QAction *playlist_move_to_collection_; QAction *playlist_move_to_collection_;
QAction *playlist_open_in_browser_; QAction *playlist_open_in_browser_;
QAction *playlist_organize_; QAction *playlist_organize_;
#ifndef Q_OS_WIN32
QAction *playlist_copy_to_device_; QAction *playlist_copy_to_device_;
#endif
QAction *playlist_delete_; QAction *playlist_delete_;
QAction *playlist_queue_; QAction *playlist_queue_;
QAction *playlist_queue_play_next_; QAction *playlist_queue_play_next_;

View File

@@ -13,10 +13,6 @@
<property name="windowTitle"> <property name="windowTitle">
<string>Strawberry Music Player</string> <string>Strawberry Music Player</string>
</property> </property>
<property name="windowIcon">
<iconset resource="../../data/icons.qrc">
<normaloff>:/icons/128x128/strawberry.png</normaloff>:/icons/128x128/strawberry.png</iconset>
</property>
<widget class="QWidget" name="centralWidget"> <widget class="QWidget" name="centralWidget">
<layout class="QVBoxLayout" name="layout_centralWidget"> <layout class="QVBoxLayout" name="layout_centralWidget">
<property name="spacing"> <property name="spacing">
@@ -37,7 +33,7 @@
<item> <item>
<widget class="QSplitter" name="splitter"> <widget class="QSplitter" name="splitter">
<property name="orientation"> <property name="orientation">
<enum>Qt::Orientation::Horizontal</enum> <enum>Qt::Horizontal</enum>
</property> </property>
<widget class="QWidget" name="sidebar_layout"> <widget class="QWidget" name="sidebar_layout">
<layout class="QVBoxLayout" name="layout_left"> <layout class="QVBoxLayout" name="layout_left">
@@ -77,7 +73,7 @@
<item> <item>
<widget class="Line" name="line_6"> <widget class="Line" name="line_6">
<property name="orientation"> <property name="orientation">
<enum>Qt::Orientation::Vertical</enum> <enum>Qt::Vertical</enum>
</property> </property>
</widget> </widget>
</item> </item>
@@ -102,7 +98,7 @@
<item> <item>
<widget class="QFrame" name="player_controls"> <widget class="QFrame" name="player_controls">
<property name="frameShape"> <property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum> <enum>QFrame::NoFrame</enum>
</property> </property>
<layout class="QHBoxLayout" name="layout_player_controls"> <layout class="QHBoxLayout" name="layout_player_controls">
<property name="spacing"> <property name="spacing">
@@ -167,7 +163,7 @@
</size> </size>
</property> </property>
<property name="popupMode"> <property name="popupMode">
<enum>QToolButton::ToolButtonPopupMode::MenuButtonPopup</enum> <enum>QToolButton::MenuButtonPopup</enum>
</property> </property>
<property name="autoRaise"> <property name="autoRaise">
<bool>true</bool> <bool>true</bool>
@@ -211,7 +207,7 @@
<item> <item>
<widget class="Line" name="line_love"> <widget class="Line" name="line_love">
<property name="orientation"> <property name="orientation">
<enum>Qt::Orientation::Vertical</enum> <enum>Qt::Vertical</enum>
</property> </property>
</widget> </widget>
</item> </item>
@@ -237,7 +233,7 @@
<item> <item>
<widget class="Line" name="line_buttons"> <widget class="Line" name="line_buttons">
<property name="orientation"> <property name="orientation">
<enum>Qt::Orientation::Vertical</enum> <enum>Qt::Vertical</enum>
</property> </property>
</widget> </widget>
</item> </item>
@@ -260,10 +256,10 @@
<item> <item>
<spacer name="horizontalSpacer"> <spacer name="horizontalSpacer">
<property name="orientation"> <property name="orientation">
<enum>Qt::Orientation::Horizontal</enum> <enum>Qt::Horizontal</enum>
</property> </property>
<property name="sizeType"> <property name="sizeType">
<enum>QSizePolicy::Policy::Expanding</enum> <enum>QSizePolicy::Expanding</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
@@ -276,7 +272,7 @@
<item> <item>
<widget class="Line" name="line_volume"> <widget class="Line" name="line_volume">
<property name="orientation"> <property name="orientation">
<enum>Qt::Orientation::Vertical</enum> <enum>Qt::Vertical</enum>
</property> </property>
</widget> </widget>
</item> </item>
@@ -292,7 +288,7 @@
<number>100</number> <number>100</number>
</property> </property>
<property name="orientation"> <property name="orientation">
<enum>Qt::Orientation::Horizontal</enum> <enum>Qt::Horizontal</enum>
</property> </property>
</widget> </widget>
</item> </item>
@@ -326,7 +322,7 @@
<item> <item>
<widget class="Line" name="status_bar_line"> <widget class="Line" name="status_bar_line">
<property name="orientation"> <property name="orientation">
<enum>Qt::Orientation::Horizontal</enum> <enum>Qt::Horizontal</enum>
</property> </property>
</widget> </widget>
</item> </item>
@@ -380,7 +376,7 @@
<item> <item>
<widget class="QLabel" name="playlist_summary"> <widget class="QLabel" name="playlist_summary">
<property name="alignment"> <property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property> </property>
</widget> </widget>
</item> </item>
@@ -391,7 +387,7 @@
<item> <item>
<widget class="Line" name="line_5"> <widget class="Line" name="line_5">
<property name="orientation"> <property name="orientation">
<enum>Qt::Orientation::Vertical</enum> <enum>Qt::Vertical</enum>
</property> </property>
</widget> </widget>
</item> </item>
@@ -401,7 +397,7 @@
<item> <item>
<widget class="Line" name="line_2"> <widget class="Line" name="line_2">
<property name="orientation"> <property name="orientation">
<enum>Qt::Orientation::Vertical</enum> <enum>Qt::Vertical</enum>
</property> </property>
</widget> </widget>
</item> </item>
@@ -580,7 +576,7 @@
<string>Ctrl+Q</string> <string>Ctrl+Q</string>
</property> </property>
<property name="menuRole"> <property name="menuRole">
<enum>QAction::MenuRole::QuitRole</enum> <enum>QAction::QuitRole</enum>
</property> </property>
</action> </action>
<action name="action_stop_after_this_track"> <action name="action_stop_after_this_track">
@@ -644,7 +640,7 @@
<string>Ctrl+P</string> <string>Ctrl+P</string>
</property> </property>
<property name="menuRole"> <property name="menuRole">
<enum>QAction::MenuRole::PreferencesRole</enum> <enum>QAction::PreferencesRole</enum>
</property> </property>
</action> </action>
<action name="action_about_strawberry"> <action name="action_about_strawberry">
@@ -659,7 +655,7 @@
<string>F1</string> <string>F1</string>
</property> </property>
<property name="menuRole"> <property name="menuRole">
<enum>QAction::MenuRole::AboutRole</enum> <enum>QAction::AboutRole</enum>
</property> </property>
</action> </action>
<action name="action_shuffle"> <action name="action_shuffle">
@@ -785,7 +781,7 @@
<string>About &amp;Qt</string> <string>About &amp;Qt</string>
</property> </property>
<property name="menuRole"> <property name="menuRole">
<enum>QAction::MenuRole::AboutQtRole</enum> <enum>QAction::AboutQtRole</enum>
</property> </property>
</action> </action>
<action name="action_mute"> <action name="action_mute">

View File

@@ -43,29 +43,29 @@ NetworkAccessManager::NetworkAccessManager(QObject *parent)
} }
QNetworkReply *NetworkAccessManager::createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) { QNetworkReply *NetworkAccessManager::createRequest(Operation op, const QNetworkRequest &network_request, QIODevice *outgoing_data) {
QByteArray user_agent; QByteArray user_agent;
if (request.hasRawHeader("User-Agent")) { if (network_request.hasRawHeader("User-Agent")) {
user_agent = request.header(QNetworkRequest::UserAgentHeader).toByteArray(); user_agent = network_request.header(QNetworkRequest::UserAgentHeader).toByteArray();
} }
else { else {
user_agent = QStringLiteral("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8(); user_agent = QStringLiteral("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8();
} }
QNetworkRequest new_request(request); QNetworkRequest new_network_request(network_request);
new_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); new_network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
new_request.setHeader(QNetworkRequest::UserAgentHeader, user_agent); new_network_request.setHeader(QNetworkRequest::UserAgentHeader, user_agent);
if (op == QNetworkAccessManager::PostOperation && !new_request.header(QNetworkRequest::ContentTypeHeader).isValid()) { if (op == QNetworkAccessManager::PostOperation && !new_network_request.header(QNetworkRequest::ContentTypeHeader).isValid()) {
new_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); new_network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
} }
// Prefer the cache unless the caller has changed the setting already // Prefer the cache unless the caller has changed the setting already
if (request.attribute(QNetworkRequest::CacheLoadControlAttribute).toInt() == QNetworkRequest::PreferNetwork) { if (!network_request.attribute(QNetworkRequest::CacheLoadControlAttribute).isValid()) {
new_request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache); new_network_request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
} }
return QNetworkAccessManager::createRequest(op, new_request, outgoingData); return QNetworkAccessManager::createRequest(op, new_network_request, outgoing_data);
} }

View File

@@ -38,7 +38,7 @@ class NetworkAccessManager : public QNetworkAccessManager {
explicit NetworkAccessManager(QObject *parent = nullptr); explicit NetworkAccessManager(QObject *parent = nullptr);
protected: protected:
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) override; QNetworkReply *createRequest(Operation op, const QNetworkRequest &network_request, QIODevice *outgoing_data) override;
}; };
#endif // NETWORKACCESSMANAGER_H #endif // NETWORKACCESSMANAGER_H

View File

@@ -288,10 +288,10 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
bool is_current = false; bool is_current = false;
bool is_next = false; bool is_next = false;
if (result.media_url_ == current_item->Url()) { if (result.media_url_ == current_item->OriginalUrl()) {
is_current = true; is_current = true;
} }
else if (has_next_row && next_item->Url() == result.media_url_) { else if (has_next_row && next_item->OriginalUrl() == result.media_url_) {
is_next = true; is_next = true;
} }
else { else {
@@ -316,8 +316,8 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
qLog(Debug) << "URL handler for" << result.media_url_ << "returned" << result.stream_url_; qLog(Debug) << "URL handler for" << result.media_url_ << "returned" << result.stream_url_;
Song song; Song song;
if (is_current) song = current_item->Metadata(); if (is_current) song = current_item->EffectiveMetadata();
else if (is_next) song = next_item->Metadata(); else if (is_next) song = next_item->EffectiveMetadata();
bool update = false; bool update = false;
@@ -325,7 +325,7 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
if ( if (
(result.stream_url_.isValid()) (result.stream_url_.isValid())
&& &&
(result.stream_url_ != song.url()) (result.stream_url_ != song.effective_url())
) )
{ {
song.set_stream_url(result.stream_url_); song.set_stream_url(result.stream_url_);
@@ -371,14 +371,14 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
} }
if (is_current) { if (is_current) {
qLog(Debug) << "Playing song" << current_item->Metadata().title() << result.stream_url_ << "position" << play_offset_nanosec_; qLog(Debug) << "Playing song" << current_item->EffectiveMetadata().title() << result.stream_url_ << "position" << play_offset_nanosec_;
engine_->Play(result.media_url_, result.stream_url_, pause_, stream_change_type_, song.has_cue(), static_cast<quint64>(song.beginning_nanosec()), song.end_nanosec(), play_offset_nanosec_, song.ebur128_integrated_loudness_lufs()); engine_->Play(result.media_url_, result.stream_url_, pause_, stream_change_type_, song.has_cue(), static_cast<quint64>(song.beginning_nanosec()), song.end_nanosec(), play_offset_nanosec_, song.ebur128_integrated_loudness_lufs());
current_item_ = current_item; current_item_ = current_item;
play_offset_nanosec_ = 0; play_offset_nanosec_ = 0;
} }
else if (is_next && !current_item->Metadata().is_module_music()) { else if (is_next && !current_item->EffectiveMetadata().is_module_music()) {
qLog(Debug) << "Preloading next song" << next_item->Metadata().title() << result.stream_url_; qLog(Debug) << "Preloading next song" << next_item->EffectiveMetadata().title() << result.stream_url_;
engine_->StartPreloading(next_item->Url(), result.stream_url_, song.has_cue(), song.beginning_nanosec(), song.end_nanosec()); engine_->StartPreloading(next_item->OriginalUrl(), result.stream_url_, song.has_cue(), song.beginning_nanosec(), song.end_nanosec());
} }
break; break;
@@ -504,8 +504,8 @@ bool Player::HandleStopAfter(const Playlist::AutoScroll autoscroll) {
void Player::TrackEnded() { void Player::TrackEnded() {
if (current_item_ && current_item_->IsLocalCollectionItem() && current_item_->Metadata().id() != -1) { if (current_item_ && current_item_->IsLocalCollectionItem() && current_item_->EffectiveMetadata().id() != -1) {
playlist_manager_->collection_backend()->IncrementPlayCountAsync(current_item_->Metadata().id()); playlist_manager_->collection_backend()->IncrementPlayCountAsync(current_item_->EffectiveMetadata().id());
} }
if (HandleStopAfter(Playlist::AutoScroll::Maybe)) return; if (HandleStopAfter(Playlist::AutoScroll::Maybe)) return;
@@ -554,7 +554,7 @@ void Player::PlayPause(const quint64 offset_nanosec, const Playlist::AutoScroll
void Player::UnPause() { void Player::UnPause() {
if (current_item_ && pause_time_.isValid()) { if (current_item_ && pause_time_.isValid()) {
const Song &song = current_item_->Metadata(); const Song &song = current_item_->EffectiveMetadata();
if (url_handlers_->CanHandle(song.url()) && song.stream_url_can_expire()) { if (url_handlers_->CanHandle(song.url()) && song.stream_url_can_expire()) {
const qint64 time = QDateTime::currentSecsSinceEpoch() - pause_time_.toSecsSinceEpoch(); const qint64 time = QDateTime::currentSecsSinceEpoch() - pause_time_.toSecsSinceEpoch();
if (time >= 30) { // Stream URL might be expired. if (time >= 30) { // Stream URL might be expired.
@@ -745,7 +745,7 @@ void Player::PlayAt(const int index, const bool pause, const quint64 offset_nano
Q_EMIT TrackSkipped(current_item_); Q_EMIT TrackSkipped(current_item_);
} }
if (current_item_ && playlist_manager_->active()->has_item_at(index) && current_item_->Metadata().IsOnSameAlbum(playlist_manager_->active()->item_at(index)->Metadata())) { if (current_item_ && playlist_manager_->active()->has_item_at(index) && current_item_->EffectiveMetadata().IsOnSameAlbum(playlist_manager_->active()->item_at(index)->EffectiveMetadata())) {
change |= EngineBase::TrackChangeType::SameAlbum; change |= EngineBase::TrackChangeType::SameAlbum;
} }
@@ -758,7 +758,7 @@ void Player::PlayAt(const int index, const bool pause, const quint64 offset_nano
} }
current_item_ = playlist_manager_->active()->current_item(); current_item_ = playlist_manager_->active()->current_item();
const QUrl url = current_item_->StreamUrl(); const QUrl url = current_item_->EffectiveUrl();
if (url_handlers_->CanHandle(url)) { if (url_handlers_->CanHandle(url)) {
// It's already loading // It's already loading
@@ -773,8 +773,8 @@ void Player::PlayAt(const int index, const bool pause, const quint64 offset_nano
HandleLoadResult(url_handler->StartLoading(url)); HandleLoadResult(url_handler->StartLoading(url));
} }
else { else {
qLog(Debug) << "Playing song" << current_item_->Metadata().title() << url << "position" << offset_nanosec; qLog(Debug) << "Playing song" << current_item_->EffectiveMetadata().title() << url << "position" << offset_nanosec;
engine_->Play(current_item_->Url(), url, pause, change, current_item_->Metadata().has_cue(), static_cast<quint64>(current_item_->effective_beginning_nanosec()), current_item_->effective_end_nanosec(), offset_nanosec, current_item_->effective_ebur128_integrated_loudness_lufs()); engine_->Play(current_item_->OriginalUrl(), url, pause, change, current_item_->EffectiveMetadata().has_cue(), static_cast<quint64>(current_item_->effective_beginning_nanosec()), current_item_->effective_end_nanosec(), offset_nanosec, current_item_->EffectiveMetadata().ebur128_integrated_loudness_lufs());
} }
} }
@@ -823,8 +823,8 @@ void Player::EngineMetadataReceived(const EngineMetadata &engine_metadata) {
const int current_row = playlist_manager_->active()->current_row(); const int current_row = playlist_manager_->active()->current_row();
if (current_row != -1) { if (current_row != -1) {
PlaylistItemPtr item = playlist_manager_->active()->current_item(); PlaylistItemPtr item = playlist_manager_->active()->current_item();
if (item && engine_metadata.media_url == item->Url()) { if (item && engine_metadata.media_url == item->OriginalUrl()) {
Song song = item->Metadata(); Song song = item->EffectiveMetadata();
song.MergeFromEngineMetadata(engine_metadata); song.MergeFromEngineMetadata(engine_metadata);
playlist_manager_->active()->UpdateItemMetadata(current_row, item, song, true); playlist_manager_->active()->UpdateItemMetadata(current_row, item, song, true);
return; return;
@@ -836,8 +836,8 @@ void Player::EngineMetadataReceived(const EngineMetadata &engine_metadata) {
const int next_row = playlist_manager_->active()->next_row(); const int next_row = playlist_manager_->active()->next_row();
if (next_row != -1) { if (next_row != -1) {
PlaylistItemPtr next_item = playlist_manager_->active()->item_at(next_row); PlaylistItemPtr next_item = playlist_manager_->active()->item_at(next_row);
if (engine_metadata.media_url == next_item->Url()) { if (engine_metadata.media_url == next_item->OriginalUrl()) {
Song song = next_item->Metadata(); Song song = next_item->EffectiveMetadata();
song.MergeFromEngineMetadata(engine_metadata); song.MergeFromEngineMetadata(engine_metadata);
playlist_manager_->active()->UpdateItemMetadata(next_row, next_item, song, true); playlist_manager_->active()->UpdateItemMetadata(next_row, next_item, song, true);
} }
@@ -905,11 +905,11 @@ void Player::PlayWithPause(const quint64 offset_nanosec) {
} }
void Player::ShowOSD() { void Player::ShowOSD() {
if (current_item_) Q_EMIT ForceShowOSD(current_item_->Metadata(), false); if (current_item_) Q_EMIT ForceShowOSD(current_item_->EffectiveMetadata(), false);
} }
void Player::TogglePrettyOSD() { void Player::TogglePrettyOSD() {
if (current_item_) Q_EMIT ForceShowOSD(current_item_->Metadata(), true); if (current_item_) Q_EMIT ForceShowOSD(current_item_->EffectiveMetadata(), true);
} }
void Player::TrackAboutToEnd() { void Player::TrackAboutToEnd() {
@@ -932,7 +932,7 @@ void Player::TrackAboutToEnd() {
// If the next track is on the same album (or same cue file), // If the next track is on the same album (or same cue file),
// and the user doesn't want to crossfade between tracks on the same album, then don't do this automatic crossfading. // and the user doesn't want to crossfade between tracks on the same album, then don't do this automatic crossfading.
if (engine_->crossfade_same_album() || !has_next_row || !next_item || !current_item_->Metadata().IsOnSameAlbum(next_item->Metadata())) { if (engine_->crossfade_same_album() || !has_next_row || !next_item || !current_item_->EffectiveMetadata().IsOnSameAlbum(next_item->EffectiveMetadata())) {
TrackEnded(); TrackEnded();
return; return;
} }
@@ -941,7 +941,7 @@ void Player::TrackAboutToEnd() {
// Crossfade is off, so start preloading the next track, so we don't get a gap between songs. // Crossfade is off, so start preloading the next track, so we don't get a gap between songs.
if (!has_next_row || !next_item) return; if (!has_next_row || !next_item) return;
QUrl url = next_item->StreamUrl(); QUrl url = next_item->EffectiveUrl();
// Get the actual track URL rather than the stream URL. // Get the actual track URL rather than the stream URL.
if (url_handlers_->CanHandle(url)) { if (url_handlers_->CanHandle(url)) {
@@ -961,20 +961,20 @@ void Player::TrackAboutToEnd() {
case UrlHandler::LoadResult::Type::TrackAvailable: case UrlHandler::LoadResult::Type::TrackAvailable:
qLog(Debug) << "URL handler for" << result.media_url_ << "returned" << result.stream_url_; qLog(Debug) << "URL handler for" << result.media_url_ << "returned" << result.stream_url_;
url = result.stream_url_; url = result.stream_url_;
Song song = next_item->Metadata(); Song song = next_item->EffectiveMetadata();
song.set_stream_url(url); song.set_stream_url(url);
next_item->SetTemporaryMetadata(song); next_item->SetStreamMetadata(song);
break; break;
} }
} }
// Preloading any format while currently playing module music is broken in GStreamer. // Preloading any format while currently playing module music is broken in GStreamer.
// See: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/769 // See: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/769
if (current_item_ && current_item_->Metadata().is_module_music()) { if (current_item_ && current_item_->EffectiveMetadata().is_module_music()) {
return; return;
} }
engine_->StartPreloading(next_item->Url(), url, next_item->Metadata().has_cue(), next_item->effective_beginning_nanosec(), next_item->effective_end_nanosec()); engine_->StartPreloading(next_item->OriginalUrl(), url, next_item->EffectiveMetadata().has_cue(), next_item->effective_beginning_nanosec(), next_item->effective_end_nanosec());
} }

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player * Strawberry Music Player
* This file was part of Clementine. * This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com> * Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -68,9 +68,13 @@
using namespace Qt::Literals::StringLiterals; using namespace Qt::Literals::StringLiterals;
const QStringList Song::kColumns = QStringList() << u"title"_s const QStringList Song::kColumns = QStringList() << u"title"_s
<< u"titlesort"_s
<< u"album"_s << u"album"_s
<< u"albumsort"_s
<< u"artist"_s << u"artist"_s
<< u"artistsort"_s
<< u"albumartist"_s << u"albumartist"_s
<< u"albumartistsort"_s
<< u"track"_s << u"track"_s
<< u"disc"_s << u"disc"_s
<< u"year"_s << u"year"_s
@@ -78,7 +82,9 @@ const QStringList Song::kColumns = QStringList() << u"title"_s
<< u"genre"_s << u"genre"_s
<< u"compilation"_s << u"compilation"_s
<< u"composer"_s << u"composer"_s
<< u"composersort"_s
<< u"performer"_s << u"performer"_s
<< u"performersort"_s
<< u"grouping"_s << u"grouping"_s
<< u"comment"_s << u"comment"_s
<< u"lyrics"_s << u"lyrics"_s
@@ -126,6 +132,9 @@ const QStringList Song::kColumns = QStringList() << u"title"_s
<< u"cue_path"_s << u"cue_path"_s
<< u"rating"_s << u"rating"_s
<< u"bpm"_s
<< u"mood"_s
<< u"initial_key"_s
<< u"acoustid_id"_s << u"acoustid_id"_s
<< u"acoustid_fingerprint"_s << u"acoustid_fingerprint"_s
@@ -261,9 +270,13 @@ struct Song::Private : public QSharedData {
bool valid_; bool valid_;
QString title_; QString title_;
QString titlesort_;
QString album_; QString album_;
QString albumsort_;
QString artist_; QString artist_;
QString artistsort_;
QString albumartist_; QString albumartist_;
QString albumartistsort_;
int track_; int track_;
int disc_; int disc_;
int year_; int year_;
@@ -271,7 +284,9 @@ struct Song::Private : public QSharedData {
QString genre_; QString genre_;
bool compilation_; // From the file tag bool compilation_; // From the file tag
QString composer_; QString composer_;
QString composersort_;
QString performer_; QString performer_;
QString performersort_;
QString grouping_; QString grouping_;
QString comment_; QString comment_;
QString lyrics_; QString lyrics_;
@@ -316,6 +331,9 @@ struct Song::Private : public QSharedData {
QString cue_path_; // If the song has a CUE, this contains it's path. QString cue_path_; // If the song has a CUE, this contains it's path.
float rating_; // Database rating, initial rating read from tag. float rating_; // Database rating, initial rating read from tag.
float bpm_;
QString mood_;
QString initial_key_;
QString acoustid_id_; QString acoustid_id_;
QString acoustid_fingerprint_; QString acoustid_fingerprint_;
@@ -337,12 +355,7 @@ struct Song::Private : public QSharedData {
bool init_from_file_; // Whether this song was loaded from a file using taglib. bool init_from_file_; // Whether this song was loaded from a file using taglib.
bool suspicious_tags_; // Whether our encoding guesser thinks these tags might be incorrectly encoded. bool suspicious_tags_; // Whether our encoding guesser thinks these tags might be incorrectly encoded.
QString title_sortable_; QUrl stream_url_; // Temporary stream URL set by the URL handler.
QString album_sortable_;
QString artist_sortable_;
QString albumartist_sortable_;
QUrl stream_url_; // Temporary stream url set by the URL handler.
}; };
@@ -384,6 +397,7 @@ Song::Private::Private(const Source source)
art_unset_(false), art_unset_(false),
rating_(-1), rating_(-1),
bpm_(-1),
init_from_file_(false), init_from_file_(false),
suspicious_tags_(false) suspicious_tags_(false)
@@ -411,9 +425,13 @@ int Song::id() const { return d->id_; }
bool Song::is_valid() const { return d->valid_; } bool Song::is_valid() const { return d->valid_; }
const QString &Song::title() const { return d->title_; } const QString &Song::title() const { return d->title_; }
const QString &Song::titlesort() const { return d->titlesort_; }
const QString &Song::album() const { return d->album_; } const QString &Song::album() const { return d->album_; }
const QString &Song::albumsort() const { return d->albumsort_; }
const QString &Song::artist() const { return d->artist_; } const QString &Song::artist() const { return d->artist_; }
const QString &Song::artistsort() const { return d->artistsort_; }
const QString &Song::albumartist() const { return d->albumartist_; } const QString &Song::albumartist() const { return d->albumartist_; }
const QString &Song::albumartistsort() const { return d->albumartistsort_; }
int Song::track() const { return d->track_; } int Song::track() const { return d->track_; }
int Song::disc() const { return d->disc_; } int Song::disc() const { return d->disc_; }
int Song::year() const { return d->year_; } int Song::year() const { return d->year_; }
@@ -421,7 +439,9 @@ int Song::originalyear() const { return d->originalyear_; }
const QString &Song::genre() const { return d->genre_; } const QString &Song::genre() const { return d->genre_; }
bool Song::compilation() const { return d->compilation_; } bool Song::compilation() const { return d->compilation_; }
const QString &Song::composer() const { return d->composer_; } const QString &Song::composer() const { return d->composer_; }
const QString &Song::composersort() const { return d->composersort_; }
const QString &Song::performer() const { return d->performer_; } const QString &Song::performer() const { return d->performer_; }
const QString &Song::performersort() const { return d->performersort_; }
const QString &Song::grouping() const { return d->grouping_; } const QString &Song::grouping() const { return d->grouping_; }
const QString &Song::comment() const { return d->comment_; } const QString &Song::comment() const { return d->comment_; }
const QString &Song::lyrics() const { return d->lyrics_; } const QString &Song::lyrics() const { return d->lyrics_; }
@@ -468,6 +488,9 @@ bool Song::art_unset() const { return d->art_unset_; }
const QString &Song::cue_path() const { return d->cue_path_; } const QString &Song::cue_path() const { return d->cue_path_; }
float Song::rating() const { return d->rating_; } float Song::rating() const { return d->rating_; }
float Song::bpm() const { return d->bpm_; }
const QString &Song::mood() const { return d->mood_; }
const QString &Song::initial_key() const { return d->initial_key_; }
const QString &Song::acoustid_id() const { return d->acoustid_id_; } const QString &Song::acoustid_id() const { return d->acoustid_id_; }
const QString &Song::acoustid_fingerprint() const { return d->acoustid_fingerprint_; } const QString &Song::acoustid_fingerprint() const { return d->acoustid_fingerprint_; }
@@ -511,20 +534,19 @@ QString *Song::mutable_musicbrainz_work_id() { return &d->musicbrainz_work_id_;
bool Song::init_from_file() const { return d->init_from_file_; } bool Song::init_from_file() const { return d->init_from_file_; }
const QString &Song::title_sortable() const { return d->title_sortable_; }
const QString &Song::album_sortable() const { return d->album_sortable_; }
const QString &Song::artist_sortable() const { return d->artist_sortable_; }
const QString &Song::albumartist_sortable() const { return d->albumartist_sortable_; }
const QUrl &Song::stream_url() const { return d->stream_url_; } const QUrl &Song::stream_url() const { return d->stream_url_; }
void Song::set_id(const int id) { d->id_ = id; } void Song::set_id(const int id) { d->id_ = id; }
void Song::set_valid(const bool v) { d->valid_ = v; } void Song::set_valid(const bool v) { d->valid_ = v; }
void Song::set_title(const QString &v) { d->title_sortable_ = sortable(v); d->title_ = v; } void Song::set_title(const QString &v) { d->title_ = v; }
void Song::set_album(const QString &v) { d->album_sortable_ = sortable(v); d->album_ = v; } void Song::set_titlesort(const QString &v) { d->titlesort_ = v; }
void Song::set_artist(const QString &v) { d->artist_sortable_ = sortable(v); d->artist_ = v; } void Song::set_album(const QString &v) { d->album_ = v; }
void Song::set_albumartist(const QString &v) { d->albumartist_sortable_ = sortable(v); d->albumartist_ = v; } void Song::set_albumsort(const QString &v) { d->albumsort_ = v; }
void Song::set_artist(const QString &v) { d->artist_ = v; }
void Song::set_artistsort(const QString &v) { d->artistsort_ = v; }
void Song::set_albumartist(const QString &v) { d->albumartist_ = v; }
void Song::set_albumartistsort(const QString &v) { d->albumartistsort_ = v; }
void Song::set_track(const int v) { d->track_ = v; } void Song::set_track(const int v) { d->track_ = v; }
void Song::set_disc(const int v) { d->disc_ = v; } void Song::set_disc(const int v) { d->disc_ = v; }
void Song::set_year(const int v) { d->year_ = v; } void Song::set_year(const int v) { d->year_ = v; }
@@ -532,7 +554,9 @@ void Song::set_originalyear(const int v) { d->originalyear_ = v; }
void Song::set_genre(const QString &v) { d->genre_ = v; } void Song::set_genre(const QString &v) { d->genre_ = v; }
void Song::set_compilation(const bool v) { d->compilation_ = v; } void Song::set_compilation(const bool v) { d->compilation_ = v; }
void Song::set_composer(const QString &v) { d->composer_ = v; } void Song::set_composer(const QString &v) { d->composer_ = v; }
void Song::set_composersort(const QString &v) { d->composersort_ = v; }
void Song::set_performer(const QString &v) { d->performer_ = v; } void Song::set_performer(const QString &v) { d->performer_ = v; }
void Song::set_performersort(const QString &v) { d->performersort_ = v; }
void Song::set_grouping(const QString &v) { d->grouping_ = v; } void Song::set_grouping(const QString &v) { d->grouping_ = v; }
void Song::set_comment(const QString &v) { d->comment_ = v; } void Song::set_comment(const QString &v) { d->comment_ = v; }
void Song::set_lyrics(const QString &v) { d->lyrics_ = v; } void Song::set_lyrics(const QString &v) { d->lyrics_ = v; }
@@ -578,6 +602,9 @@ void Song::set_art_unset(const bool v) { d->art_unset_ = v; }
void Song::set_cue_path(const QString &v) { d->cue_path_ = v; } void Song::set_cue_path(const QString &v) { d->cue_path_ = v; }
void Song::set_rating(const float v) { d->rating_ = v; } void Song::set_rating(const float v) { d->rating_ = v; }
void Song::set_bpm(const float v) { d->bpm_ = v; }
void Song::set_mood(const QString &v) { d->mood_ = v; }
void Song::set_initial_key(const QString &v) { d->initial_key_ = v; }
void Song::set_acoustid_id(const QString &v) { d->acoustid_id_ = v; } void Song::set_acoustid_id(const QString &v) { d->acoustid_id_ = v; }
void Song::set_acoustid_fingerprint(const QString &v) { d->acoustid_fingerprint_ = v; } void Song::set_acoustid_fingerprint(const QString &v) { d->acoustid_fingerprint_ = v; }
@@ -600,40 +627,19 @@ void Song::set_init_from_file(const bool v) { d->init_from_file_ = v; }
void Song::set_stream_url(const QUrl &v) { d->stream_url_ = v; } void Song::set_stream_url(const QUrl &v) { d->stream_url_ = v; }
void Song::set_title(const TagLib::String &v) { void Song::set_title(const TagLib::String &v) { d->title_ = TagLibStringToQString(v); }
void Song::set_titlesort(const TagLib::String &v) { d->titlesort_ = TagLibStringToQString(v); }
const QString title = TagLibStringToQString(v); void Song::set_album(const TagLib::String &v) { d->album_ = TagLibStringToQString(v); }
d->title_sortable_ = sortable(title); void Song::set_albumsort(const TagLib::String &v) { d->albumsort_ = TagLibStringToQString(v); }
d->title_ = title; void Song::set_artist(const TagLib::String &v) { d->artist_ = TagLibStringToQString(v); }
void Song::set_artistsort(const TagLib::String &v) { d->artistsort_ = TagLibStringToQString(v); }
} void Song::set_albumartist(const TagLib::String &v) { d->albumartist_ = TagLibStringToQString(v); }
void Song::set_albumartistsort(const TagLib::String &v) { d->albumartistsort_ = TagLibStringToQString(v); }
void Song::set_album(const TagLib::String &v) {
const QString album = TagLibStringToQString(v);
d->album_sortable_ = sortable(album);
d->album_ = album;
}
void Song::set_artist(const TagLib::String &v) {
const QString artist = TagLibStringToQString(v);
d->artist_sortable_ = sortable(artist);
d->artist_ = artist;
}
void Song::set_albumartist(const TagLib::String &v) {
const QString albumartist = TagLibStringToQString(v);
d->albumartist_sortable_ = sortable(albumartist);
d->albumartist_ = albumartist;
}
void Song::set_genre(const TagLib::String &v) { d->genre_ = TagLibStringToQString(v); } void Song::set_genre(const TagLib::String &v) { d->genre_ = TagLibStringToQString(v); }
void Song::set_composer(const TagLib::String &v) { d->composer_ = TagLibStringToQString(v); } void Song::set_composer(const TagLib::String &v) { d->composer_ = TagLibStringToQString(v); }
void Song::set_composersort(const TagLib::String &v) { d->composersort_ = TagLibStringToQString(v); }
void Song::set_performer(const TagLib::String &v) { d->performer_ = TagLibStringToQString(v); } void Song::set_performer(const TagLib::String &v) { d->performer_ = TagLibStringToQString(v); }
void Song::set_performersort(const TagLib::String &v) { d->performersort_ = TagLibStringToQString(v); }
void Song::set_grouping(const TagLib::String &v) { d->grouping_ = TagLibStringToQString(v); } void Song::set_grouping(const TagLib::String &v) { d->grouping_ = TagLibStringToQString(v); }
void Song::set_comment(const TagLib::String &v) { d->comment_ = TagLibStringToQString(v); } void Song::set_comment(const TagLib::String &v) { d->comment_ = TagLibStringToQString(v); }
void Song::set_lyrics(const TagLib::String &v) { d->lyrics_ = TagLibStringToQString(v); } void Song::set_lyrics(const TagLib::String &v) { d->lyrics_ = TagLibStringToQString(v); }
@@ -652,14 +658,21 @@ void Song::set_musicbrainz_track_id(const TagLib::String &v) { d->musicbrainz_tr
void Song::set_musicbrainz_disc_id(const TagLib::String &v) { d->musicbrainz_disc_id_ = TagLibStringToQString(v).remove(u' ').replace(u';', u'/'); } void Song::set_musicbrainz_disc_id(const TagLib::String &v) { d->musicbrainz_disc_id_ = TagLibStringToQString(v).remove(u' ').replace(u';', u'/'); }
void Song::set_musicbrainz_release_group_id(const TagLib::String &v) { d->musicbrainz_release_group_id_ = TagLibStringToQString(v).remove(u' ').replace(u';', u'/'); } void Song::set_musicbrainz_release_group_id(const TagLib::String &v) { d->musicbrainz_release_group_id_ = TagLibStringToQString(v).remove(u' ').replace(u';', u'/'); }
void Song::set_musicbrainz_work_id(const TagLib::String &v) { d->musicbrainz_work_id_ = TagLibStringToQString(v).remove(u' ').replace(u';', u'/'); } void Song::set_musicbrainz_work_id(const TagLib::String &v) { d->musicbrainz_work_id_ = TagLibStringToQString(v).remove(u' ').replace(u';', u'/'); }
void Song::set_mood(const TagLib::String &v) { d->mood_ = TagLibStringToQString(v); }
void Song::set_initial_key(const TagLib::String &v) { d->initial_key_ = TagLibStringToQString(v); }
const QUrl &Song::effective_stream_url() const { return !d->stream_url_.isEmpty() && d->stream_url_.isValid() ? d->stream_url_ : d->url_; } const QUrl &Song::effective_url() const { return !d->stream_url_.isEmpty() && d->stream_url_.isValid() ? d->stream_url_ : d->url_; }
const QString &Song::effective_titlesort() const { return d->titlesort_.isEmpty() ? d->title_ : d->titlesort_; }
const QString &Song::effective_albumartist() const { return d->albumartist_.isEmpty() ? d->artist_ : d->albumartist_; } const QString &Song::effective_albumartist() const { return d->albumartist_.isEmpty() ? d->artist_ : d->albumartist_; }
const QString &Song::effective_albumartist_sortable() const { return d->albumartist_.isEmpty() ? d->artist_sortable_ : d->albumartist_sortable_; } const QString &Song::effective_albumartistsort() const { return !d->albumartistsort_.isEmpty() ? d->albumartistsort_ : !d->albumartist_.isEmpty() ? d->albumartist_ : effective_artistsort(); }
const QString &Song::effective_artistsort() const { return d->artistsort_.isEmpty() ? d->artist_ : d->artistsort_; }
const QString &Song::effective_album() const { return d->album_.isEmpty() ? d->title_ : d->album_; } const QString &Song::effective_album() const { return d->album_.isEmpty() ? d->title_ : d->album_; }
const QString &Song::effective_albumsort() const { return d->albumsort_.isEmpty() ? d->album_ : d->albumsort_; }
const QString &Song::effective_composersort() const { return d->composersort_.isEmpty() ? d->composer_ : d->composersort_; }
const QString &Song::effective_performersort() const { return d->performersort_.isEmpty() ? d->performer_ : d->performersort_; }
int Song::effective_originalyear() const { return d->originalyear_ < 0 ? d->year_ : d->originalyear_; } int Song::effective_originalyear() const { return d->originalyear_ < 0 ? d->year_ : d->originalyear_; }
const QString &Song::playlist_albumartist() const { return is_compilation() ? d->albumartist_ : effective_albumartist(); } const QString &Song::playlist_effective_albumartist() const { return is_compilation() ? d->albumartist_ : effective_albumartist(); }
const QString &Song::playlist_albumartist_sortable() const { return is_compilation() ? d->albumartist_sortable_ : effective_albumartist_sortable(); } const QString &Song::playlist_effective_albumartistsort() const { return is_compilation() ? (!d->albumartistsort_.isEmpty() ? d->albumartistsort_ : d->albumartist_) : effective_albumartistsort(); }
bool Song::is_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); } bool Song::is_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); }
bool Song::is_local_collection_song() const { return d->source_ == Source::Collection; } bool Song::is_local_collection_song() const { return d->source_ == Source::Collection; }
@@ -782,6 +795,31 @@ bool Song::lyrics_supported() const {
return additional_tags_supported() || d->filetype_ == FileType::ASF; return additional_tags_supported() || d->filetype_ == FileType::ASF;
} }
bool Song::albumartistsort_supported() const {
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::MPEG;
}
bool Song::albumsort_supported() const {
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::MPEG;
}
bool Song::artistsort_supported() const {
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::MPEG;
}
bool Song::composersort_supported() const {
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::MPEG;
}
bool Song::performersort_supported() const {
// Performer sort is a rare custom field even in vorbis comments, no write support in MPEG formats
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis;
}
bool Song::titlesort_supported() const {
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::MPEG;
}
bool Song::save_embedded_cover_supported(const FileType filetype) { bool Song::save_embedded_cover_supported(const FileType filetype) {
return filetype == FileType::FLAC || return filetype == FileType::FLAC ||
@@ -794,21 +832,6 @@ bool Song::save_embedded_cover_supported(const FileType filetype) {
} }
QString Song::sortable(const QString &v) {
QString copy = v.toLower();
for (const auto &i : kArticles) {
if (copy.startsWith(i)) {
qint64 ilen = i.length();
return copy.right(copy.length() - ilen) + u", "_s + copy.left(ilen - 1);
}
}
return copy;
}
int Song::ColumnIndex(const QString &field) { int Song::ColumnIndex(const QString &field) {
return static_cast<int>(kRowIdColumns.indexOf(field)); return static_cast<int>(kRowIdColumns.indexOf(field));
@@ -923,39 +946,63 @@ bool Song::IsEditable() const {
return d->valid_ && d->url_.isValid() && ((d->url_.isLocalFile() && write_tags_supported() && !has_cue()) || d->source_ == Source::Stream); return d->valid_ && d->url_.isValid() && ((d->url_.isLocalFile() && write_tags_supported() && !has_cue()) || d->source_ == Source::Stream);
} }
bool Song::IsFileInfoEqual(const Song &other) const {
return d->beginning_ == other.d->beginning_ &&
d->end_ == other.d->end_ &&
d->url_ == other.d->url_ &&
d->basefilename_ == other.d->basefilename_ &&
d->filetype_ == other.d->filetype_ &&
d->filesize_ == other.d->filesize_ &&
d->mtime_ == other.d->mtime_ &&
d->ctime_ == other.d->ctime_ &&
d->mtime_ == other.d->mtime_ &&
d->stream_url_ == other.d->stream_url_;
}
bool Song::IsMetadataEqual(const Song &other) const { bool Song::IsMetadataEqual(const Song &other) const {
return d->title_ == other.d->title_ && return d->title_ == other.d->title_ &&
d->album_ == other.d->album_ && d->titlesort_ == other.d->titlesort_ &&
d->artist_ == other.d->artist_ && d->album_ == other.d->album_ &&
d->albumartist_ == other.d->albumartist_ && d->albumsort_ == other.d->albumsort_ &&
d->track_ == other.d->track_ && d->artist_ == other.d->artist_ &&
d->disc_ == other.d->disc_ && d->artistsort_ == other.d->artistsort_ &&
d->year_ == other.d->year_ && d->albumartist_ == other.d->albumartist_ &&
d->originalyear_ == other.d->originalyear_ && d->albumartistsort_ == other.d->albumartistsort_ &&
d->genre_ == other.d->genre_ && d->track_ == other.d->track_ &&
d->compilation_ == other.d->compilation_ && d->disc_ == other.d->disc_ &&
d->composer_ == other.d->composer_ && d->year_ == other.d->year_ &&
d->performer_ == other.d->performer_ && d->originalyear_ == other.d->originalyear_ &&
d->grouping_ == other.d->grouping_ && d->genre_ == other.d->genre_ &&
d->comment_ == other.d->comment_ && d->compilation_ == other.d->compilation_ &&
d->lyrics_ == other.d->lyrics_ && d->composer_ == other.d->composer_ &&
d->artist_id_ == other.d->artist_id_ && d->composersort_ == other.d->composersort_ &&
d->album_id_ == other.d->album_id_ && d->performer_ == other.d->performer_ &&
d->song_id_ == other.d->song_id_ && d->performersort_ == other.d->performersort_ &&
d->beginning_ == other.d->beginning_ && d->grouping_ == other.d->grouping_ &&
length_nanosec() == other.length_nanosec() && d->comment_ == other.d->comment_ &&
d->bitrate_ == other.d->bitrate_ && d->lyrics_ == other.d->lyrics_ &&
d->samplerate_ == other.d->samplerate_ && d->artist_id_ == other.d->artist_id_ &&
d->bitdepth_ == other.d->bitdepth_ && d->album_id_ == other.d->album_id_ &&
d->cue_path_ == other.d->cue_path_; d->song_id_ == other.d->song_id_ &&
d->beginning_ == other.d->beginning_ &&
length_nanosec() == other.length_nanosec() &&
d->bitrate_ == other.d->bitrate_ &&
d->samplerate_ == other.d->samplerate_ &&
d->bitdepth_ == other.d->bitdepth_ &&
d->bpm_ == other.d->bpm_ &&
d->mood_ == other.d->mood_ &&
d->initial_key_ == other.d->initial_key_ &&
d->cue_path_ == other.d->cue_path_;
} }
bool Song::IsPlayStatisticsEqual(const Song &other) const { bool Song::IsPlayStatisticsEqual(const Song &other) const {
return d->playcount_ == other.d->playcount_ && return d->playcount_ == other.d->playcount_ &&
d->skipcount_ == other.d->skipcount_ && d->skipcount_ == other.d->skipcount_ &&
d->lastplayed_ == other.d->lastplayed_; d->lastplayed_ == other.d->lastplayed_;
} }
@@ -980,42 +1027,70 @@ bool Song::IsAcoustIdEqual(const Song &other) const {
bool Song::IsMusicBrainzEqual(const Song &other) const { bool Song::IsMusicBrainzEqual(const Song &other) const {
return d->musicbrainz_album_artist_id_ == other.d->musicbrainz_album_artist_id_ && return d->musicbrainz_album_artist_id_ == other.d->musicbrainz_album_artist_id_ &&
d->musicbrainz_artist_id_ == other.d->musicbrainz_artist_id_ && d->musicbrainz_artist_id_ == other.d->musicbrainz_artist_id_ &&
d->musicbrainz_original_artist_id_ == other.d->musicbrainz_original_artist_id_ && d->musicbrainz_original_artist_id_ == other.d->musicbrainz_original_artist_id_ &&
d->musicbrainz_album_id_ == other.d->musicbrainz_album_id_ && d->musicbrainz_album_id_ == other.d->musicbrainz_album_id_ &&
d->musicbrainz_original_album_id_ == other.d->musicbrainz_original_album_id_ && d->musicbrainz_original_album_id_ == other.d->musicbrainz_original_album_id_ &&
d->musicbrainz_recording_id_ == other.d->musicbrainz_recording_id_ && d->musicbrainz_recording_id_ == other.d->musicbrainz_recording_id_ &&
d->musicbrainz_track_id_ == other.d->musicbrainz_track_id_ && d->musicbrainz_track_id_ == other.d->musicbrainz_track_id_ &&
d->musicbrainz_disc_id_ == other.d->musicbrainz_disc_id_ && d->musicbrainz_disc_id_ == other.d->musicbrainz_disc_id_ &&
d->musicbrainz_release_group_id_ == other.d->musicbrainz_release_group_id_ && d->musicbrainz_release_group_id_ == other.d->musicbrainz_release_group_id_ &&
d->musicbrainz_work_id_ == other.d->musicbrainz_work_id_; d->musicbrainz_work_id_ == other.d->musicbrainz_work_id_;
} }
bool Song::IsEBUR128Equal(const Song &other) const { bool Song::IsEBUR128Equal(const Song &other) const {
return d->ebur128_integrated_loudness_lufs_ == other.d->ebur128_integrated_loudness_lufs_ && return d->ebur128_integrated_loudness_lufs_ == other.d->ebur128_integrated_loudness_lufs_ &&
d->ebur128_loudness_range_lu_ == other.d->ebur128_loudness_range_lu_; d->ebur128_loudness_range_lu_ == other.d->ebur128_loudness_range_lu_;
} }
bool Song::IsArtEqual(const Song &other) const { bool Song::IsArtEqual(const Song &other) const {
return d->art_embedded_ == other.d->art_embedded_ && return d->art_embedded_ == other.d->art_embedded_ &&
d->art_automatic_ == other.d->art_automatic_ && d->art_automatic_ == other.d->art_automatic_ &&
d->art_manual_ == other.d->art_manual_ && d->art_manual_ == other.d->art_manual_ &&
d->art_unset_ == other.d->art_unset_; d->art_unset_ == other.d->art_unset_;
}
bool Song::IsCompilationEqual(const Song &other) const {
return d->compilation_ == other.d->compilation_ &&
d->compilation_detected_ == other.d->compilation_detected_ &&
d->compilation_on_ == other.d->compilation_on_ &&
d->compilation_off_ == other.d->compilation_off_;
}
bool Song::IsSettingsEqual(const Song &other) const {
return d->source_ == other.d->source_ &&
d->directory_id_ == other.d->directory_id_ &&
d->unavailable_ == other.d->unavailable_;
} }
bool Song::IsAllMetadataEqual(const Song &other) const { bool Song::IsAllMetadataEqual(const Song &other) const {
return IsMetadataEqual(other) && return IsMetadataEqual(other) &&
IsPlayStatisticsEqual(other) && IsPlayStatisticsEqual(other) &&
IsRatingEqual(other) && IsRatingEqual(other) &&
IsAcoustIdEqual(other) && IsAcoustIdEqual(other) &&
IsMusicBrainzEqual(other) && IsMusicBrainzEqual(other) &&
IsArtEqual(other); IsArtEqual(other) &&
IsEBUR128Equal(other);
}
bool Song::IsEqual(const Song &other) const {
return IsFileInfoEqual(other) &&
IsSettingsEqual(other) &&
IsAllMetadataEqual(other) &&
IsFingerprintEqual(other) &&
IsCompilationEqual(other);
} }
@@ -1139,6 +1214,22 @@ QIcon Song::IconForSource(const Source source) {
} }
// Convert a source to a music service domain name, for ListenBrainz.
// See the "Music service names" note on https://listenbrainz.readthedocs.io/en/latest/users/json.html.
QString Song::DomainForSource(const Source source) {
switch (source) {
case Song::Source::Tidal: return u"tidal.com"_s;
case Song::Source::Qobuz: return u"qobuz.com"_s;
case Song::Source::SomaFM: return u"somafm.com"_s;
case Song::Source::RadioParadise: return u"radioparadise.com"_s;
case Song::Source::Spotify: return u"spotify.com"_s;
default: return QString();
}
}
QString Song::TextForFiletype(const FileType filetype) { QString Song::TextForFiletype(const FileType filetype) {
switch (filetype) { switch (filetype) {
@@ -1166,6 +1257,7 @@ QString Song::TextForFiletype(const FileType filetype) {
case FileType::CDDA: return u"CDDA"_s; case FileType::CDDA: return u"CDDA"_s;
case FileType::SPC: return u"SNES SPC700"_s; case FileType::SPC: return u"SNES SPC700"_s;
case FileType::VGM: return u"VGM"_s; case FileType::VGM: return u"VGM"_s;
case FileType::ALAC: return u"ALAC"_s;
case FileType::Stream: return u"Stream"_s; case FileType::Stream: return u"Stream"_s;
case FileType::Unknown: case FileType::Unknown:
default: return QObject::tr("Unknown"); default: return QObject::tr("Unknown");
@@ -1198,6 +1290,7 @@ QString Song::ExtensionForFiletype(const FileType filetype) {
case FileType::IT: return u"it"_s; case FileType::IT: return u"it"_s;
case FileType::SPC: return u"spc"_s; case FileType::SPC: return u"spc"_s;
case FileType::VGM: return u"vgm"_s; case FileType::VGM: return u"vgm"_s;
case FileType::ALAC: return u"m4a"_s;
case FileType::Unknown: case FileType::Unknown:
default: return u"dat"_s; default: return u"dat"_s;
} }
@@ -1230,12 +1323,30 @@ QIcon Song::IconForFiletype(const FileType filetype) {
case FileType::IT: return IconLoader::Load(u"it"_s); case FileType::IT: return IconLoader::Load(u"it"_s);
case FileType::CDDA: return IconLoader::Load(u"cd"_s); case FileType::CDDA: return IconLoader::Load(u"cd"_s);
case FileType::Stream: return IconLoader::Load(u"applications-internet"_s); case FileType::Stream: return IconLoader::Load(u"applications-internet"_s);
case FileType::ALAC: return IconLoader::Load(u"alac"_s);
case FileType::Unknown: case FileType::Unknown:
default: return IconLoader::Load(u"edit-delete"_s); default: return IconLoader::Load(u"edit-delete"_s);
} }
} }
// Get a URL usable for sharing this song with another user.
// This is only applicable when streaming from a streaming service, since we can't link to local content.
// Returns a web URL which points to the current streaming track or live stream, or an empty string if that is not applicable.
QString Song::ShareURL() const {
switch (source()) {
case Song::Source::Stream:
case Song::Source::SomaFM: return url().toString();
case Song::Source::Tidal: return "https://tidal.com/track/%1"_L1.arg(song_id());
case Song::Source::Qobuz: return "https://open.qobuz.com/track/%1"_L1.arg(song_id());
case Song::Source::Spotify: return "https://open.spotify.com/track/%1"_L1.arg(song_id());
default: return QString();
}
}
bool Song::IsFileLossless() const { bool Song::IsFileLossless() const {
switch (filetype()) { switch (filetype()) {
@@ -1250,6 +1361,7 @@ bool Song::IsFileLossless() const {
case FileType::TrueAudio: case FileType::TrueAudio:
case FileType::PCM: case FileType::PCM:
case FileType::CDDA: case FileType::CDDA:
case FileType::ALAC:
return true; return true;
default: default:
return false; return false;
@@ -1279,6 +1391,7 @@ Song::FileType Song::FiletypeByMimetype(const QString &mimetype) {
if (mimetype.compare("audio/x-s3m"_L1, Qt::CaseInsensitive) == 0) return FileType::S3M; if (mimetype.compare("audio/x-s3m"_L1, Qt::CaseInsensitive) == 0) return FileType::S3M;
if (mimetype.compare("audio/x-spc"_L1, Qt::CaseInsensitive) == 0) return FileType::SPC; if (mimetype.compare("audio/x-spc"_L1, Qt::CaseInsensitive) == 0) return FileType::SPC;
if (mimetype.compare("audio/x-vgm"_L1, Qt::CaseInsensitive) == 0) return FileType::VGM; if (mimetype.compare("audio/x-vgm"_L1, Qt::CaseInsensitive) == 0) return FileType::VGM;
if (mimetype.compare("audio/x-alac"_L1, Qt::CaseInsensitive) == 0) return FileType::ALAC;
return FileType::Unknown; return FileType::Unknown;
@@ -1306,6 +1419,7 @@ Song::FileType Song::FiletypeByDescription(const QString &text) {
if (text.compare("Module Music Format (MOD)"_L1, Qt::CaseInsensitive) == 0) return FileType::S3M; if (text.compare("Module Music Format (MOD)"_L1, Qt::CaseInsensitive) == 0) return FileType::S3M;
if (text.compare("SNES SPC700"_L1, Qt::CaseInsensitive) == 0) return FileType::SPC; if (text.compare("SNES SPC700"_L1, Qt::CaseInsensitive) == 0) return FileType::SPC;
if (text.compare("VGM"_L1, Qt::CaseInsensitive) == 0) return FileType::VGM; if (text.compare("VGM"_L1, Qt::CaseInsensitive) == 0) return FileType::VGM;
if (text.compare("Apple Lossless Audio Codec (ALAC)"_L1, Qt::CaseInsensitive) == 0) return FileType::ALAC;
return FileType::Unknown; return FileType::Unknown;
@@ -1416,9 +1530,13 @@ void Song::InitFromQuery(const QSqlRecord &r, const bool reliable_metadata, cons
d->id_ = SqlHelper::ValueToInt(r, ColumnIndex(u"ROWID"_s) + col); d->id_ = SqlHelper::ValueToInt(r, ColumnIndex(u"ROWID"_s) + col);
set_title(SqlHelper::ValueToString(r, ColumnIndex(u"title"_s) + col)); set_title(SqlHelper::ValueToString(r, ColumnIndex(u"title"_s) + col));
set_titlesort(SqlHelper::ValueToString(r, ColumnIndex(u"titlesort"_s) + col));
set_album(SqlHelper::ValueToString(r, ColumnIndex(u"album"_s) + col)); set_album(SqlHelper::ValueToString(r, ColumnIndex(u"album"_s) + col));
set_albumsort(SqlHelper::ValueToString(r, ColumnIndex(u"albumsort"_s) + col));
set_artist(SqlHelper::ValueToString(r, ColumnIndex(u"artist"_s) + col)); set_artist(SqlHelper::ValueToString(r, ColumnIndex(u"artist"_s) + col));
set_artistsort(SqlHelper::ValueToString(r, ColumnIndex(u"artistsort"_s) + col));
set_albumartist(SqlHelper::ValueToString(r, ColumnIndex(u"albumartist"_s) + col)); set_albumartist(SqlHelper::ValueToString(r, ColumnIndex(u"albumartist"_s) + col));
set_albumartistsort(SqlHelper::ValueToString(r, ColumnIndex(u"albumartistsort"_s) + col));
d->track_ = SqlHelper::ValueToInt(r, ColumnIndex(u"track"_s) + col); d->track_ = SqlHelper::ValueToInt(r, ColumnIndex(u"track"_s) + col);
d->disc_ = SqlHelper::ValueToInt(r, ColumnIndex(u"disc"_s) + col); d->disc_ = SqlHelper::ValueToInt(r, ColumnIndex(u"disc"_s) + col);
d->year_ = SqlHelper::ValueToInt(r, ColumnIndex(u"year"_s) + col); d->year_ = SqlHelper::ValueToInt(r, ColumnIndex(u"year"_s) + col);
@@ -1426,7 +1544,9 @@ void Song::InitFromQuery(const QSqlRecord &r, const bool reliable_metadata, cons
d->genre_ = SqlHelper::ValueToString(r, ColumnIndex(u"genre"_s) + col); d->genre_ = SqlHelper::ValueToString(r, ColumnIndex(u"genre"_s) + col);
d->compilation_ = r.value(ColumnIndex(u"compilation"_s) + col).toBool(); d->compilation_ = r.value(ColumnIndex(u"compilation"_s) + col).toBool();
d->composer_ = SqlHelper::ValueToString(r, ColumnIndex(u"composer"_s) + col); d->composer_ = SqlHelper::ValueToString(r, ColumnIndex(u"composer"_s) + col);
d->composersort_ = SqlHelper::ValueToString(r, ColumnIndex(u"composersort"_s) + col);
d->performer_ = SqlHelper::ValueToString(r, ColumnIndex(u"performer"_s) + col); d->performer_ = SqlHelper::ValueToString(r, ColumnIndex(u"performer"_s) + col);
d->performersort_ = SqlHelper::ValueToString(r, ColumnIndex(u"performersort"_s) + col);
d->grouping_ = SqlHelper::ValueToString(r, ColumnIndex(u"grouping"_s) + col); d->grouping_ = SqlHelper::ValueToString(r, ColumnIndex(u"grouping"_s) + col);
d->comment_ = SqlHelper::ValueToString(r, ColumnIndex(u"comment"_s) + col); d->comment_ = SqlHelper::ValueToString(r, ColumnIndex(u"comment"_s) + col);
d->lyrics_ = SqlHelper::ValueToString(r, ColumnIndex(u"lyrics"_s) + col); d->lyrics_ = SqlHelper::ValueToString(r, ColumnIndex(u"lyrics"_s) + col);
@@ -1468,7 +1588,11 @@ void Song::InitFromQuery(const QSqlRecord &r, const bool reliable_metadata, cons
d->art_unset_ = SqlHelper::ValueToBool(r, ColumnIndex(u"art_unset"_s) + col); d->art_unset_ = SqlHelper::ValueToBool(r, ColumnIndex(u"art_unset"_s) + col);
d->cue_path_ = SqlHelper::ValueToString(r, ColumnIndex(u"cue_path"_s) + col); d->cue_path_ = SqlHelper::ValueToString(r, ColumnIndex(u"cue_path"_s) + col);
d->rating_ = SqlHelper::ValueToFloat(r, ColumnIndex(u"rating"_s) + col); d->rating_ = SqlHelper::ValueToFloat(r, ColumnIndex(u"rating"_s) + col);
d->bpm_ = SqlHelper::ValueToFloat(r, ColumnIndex(u"bpm"_s) + col);
d->mood_ = SqlHelper::ValueToString(r, ColumnIndex(u"mood"_s) + col);
d->initial_key_ = SqlHelper::ValueToString(r, ColumnIndex(u"initial_key"_s) + col);
d->acoustid_id_ = SqlHelper::ValueToString(r, ColumnIndex(u"acoustid_id"_s) + col); d->acoustid_id_ = SqlHelper::ValueToString(r, ColumnIndex(u"acoustid_id"_s) + col);
d->acoustid_fingerprint_ = SqlHelper::ValueToString(r, ColumnIndex(u"acoustid_fingerprint"_s) + col); d->acoustid_fingerprint_ = SqlHelper::ValueToString(r, ColumnIndex(u"acoustid_fingerprint"_s) + col);
@@ -1734,9 +1858,13 @@ void Song::BindToQuery(SqlQuery *query) const {
// Remember to bind these in the same order as kBindSpec // Remember to bind these in the same order as kBindSpec
query->BindStringValue(u":title"_s, d->title_); query->BindStringValue(u":title"_s, d->title_);
query->BindStringValue(u":titlesort"_s, d->titlesort_);
query->BindStringValue(u":album"_s, d->album_); query->BindStringValue(u":album"_s, d->album_);
query->BindStringValue(u":albumsort"_s, d->albumsort_);
query->BindStringValue(u":artist"_s, d->artist_); query->BindStringValue(u":artist"_s, d->artist_);
query->BindStringValue(u":artistsort"_s, d->artistsort_);
query->BindStringValue(u":albumartist"_s, d->albumartist_); query->BindStringValue(u":albumartist"_s, d->albumartist_);
query->BindStringValue(u":albumartistsort"_s, d->albumartistsort_);
query->BindIntValue(u":track"_s, d->track_); query->BindIntValue(u":track"_s, d->track_);
query->BindIntValue(u":disc"_s, d->disc_); query->BindIntValue(u":disc"_s, d->disc_);
query->BindIntValue(u":year"_s, d->year_); query->BindIntValue(u":year"_s, d->year_);
@@ -1744,7 +1872,9 @@ void Song::BindToQuery(SqlQuery *query) const {
query->BindStringValue(u":genre"_s, d->genre_); query->BindStringValue(u":genre"_s, d->genre_);
query->BindBoolValue(u":compilation"_s, d->compilation_); query->BindBoolValue(u":compilation"_s, d->compilation_);
query->BindStringValue(u":composer"_s, d->composer_); query->BindStringValue(u":composer"_s, d->composer_);
query->BindStringValue(u":composersort"_s, d->composersort_);
query->BindStringValue(u":performer"_s, d->performer_); query->BindStringValue(u":performer"_s, d->performer_);
query->BindStringValue(u":performersort"_s, d->performersort_);
query->BindStringValue(u":grouping"_s, d->grouping_); query->BindStringValue(u":grouping"_s, d->grouping_);
query->BindStringValue(u":comment"_s, d->comment_); query->BindStringValue(u":comment"_s, d->comment_);
query->BindStringValue(u":lyrics"_s, d->lyrics_); query->BindStringValue(u":lyrics"_s, d->lyrics_);
@@ -1792,6 +1922,9 @@ void Song::BindToQuery(SqlQuery *query) const {
query->BindValue(u":cue_path"_s, d->cue_path_); query->BindValue(u":cue_path"_s, d->cue_path_);
query->BindFloatValue(u":rating"_s, d->rating_); query->BindFloatValue(u":rating"_s, d->rating_);
query->BindFloatValue(u":bpm"_s, d->bpm_);
query->BindStringValue(u":mood"_s, d->mood_);
query->BindStringValue(u":initial_key"_s, d->initial_key_);
query->BindStringValue(u":acoustid_id"_s, d->acoustid_id_); query->BindStringValue(u":acoustid_id"_s, d->acoustid_id_);
query->BindStringValue(u":acoustid_fingerprint"_s, d->acoustid_fingerprint_); query->BindStringValue(u":acoustid_fingerprint"_s, d->acoustid_fingerprint_);
@@ -1819,7 +1952,7 @@ void Song::ToXesam(QVariantMap *map) const {
using mpris::AddMetadataAsList; using mpris::AddMetadataAsList;
using mpris::AsMPRISDateTimeType; using mpris::AsMPRISDateTimeType;
AddMetadata(u"xesam:url"_s, effective_stream_url().toString(), map); AddMetadata(u"xesam:url"_s, effective_url().toString(), map);
AddMetadata(u"xesam:title"_s, PrettyTitle(), map); AddMetadata(u"xesam:title"_s, PrettyTitle(), map);
AddMetadataAsList(u"xesam:artist"_s, artist(), map); AddMetadataAsList(u"xesam:artist"_s, artist(), map);
AddMetadata(u"xesam:album"_s, album(), map); AddMetadata(u"xesam:album"_s, album(), map);

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player * Strawberry Music Player
* This file was part of Clementine. * This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com> * Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -105,6 +105,7 @@ class Song {
IT = 21, IT = 21,
SPC = 22, SPC = 22,
VGM = 23, VGM = 23,
ALAC = 24, // MP4, with ALAC codec
CDDA = 90, CDDA = 90,
Stream = 91 Stream = 91
}; };
@@ -149,9 +150,13 @@ class Song {
bool is_valid() const; bool is_valid() const;
const QString &title() const; const QString &title() const;
const QString &titlesort() const;
const QString &album() const; const QString &album() const;
const QString &albumsort() const;
const QString &artist() const; const QString &artist() const;
const QString &artistsort() const;
const QString &albumartist() const; const QString &albumartist() const;
const QString &albumartistsort() const;
int track() const; int track() const;
int disc() const; int disc() const;
int year() const; int year() const;
@@ -159,7 +164,9 @@ class Song {
const QString &genre() const; const QString &genre() const;
bool compilation() const; bool compilation() const;
const QString &composer() const; const QString &composer() const;
const QString &composersort() const;
const QString &performer() const; const QString &performer() const;
const QString &performersort() const;
const QString &grouping() const; const QString &grouping() const;
const QString &comment() const; const QString &comment() const;
const QString &lyrics() const; const QString &lyrics() const;
@@ -206,6 +213,9 @@ class Song {
const QString &cue_path() const; const QString &cue_path() const;
float rating() const; float rating() const;
float bpm() const;
const QString &mood() const;
const QString &initial_key() const;
const QString &acoustid_id() const; const QString &acoustid_id() const;
const QString &acoustid_fingerprint() const; const QString &acoustid_fingerprint() const;
@@ -249,11 +259,6 @@ class Song {
bool init_from_file() const; bool init_from_file() const;
const QString &title_sortable() const;
const QString &album_sortable() const;
const QString &artist_sortable() const;
const QString &albumartist_sortable() const;
const QUrl &stream_url() const; const QUrl &stream_url() const;
// Setters // Setters
@@ -261,9 +266,13 @@ class Song {
void set_valid(const bool v); void set_valid(const bool v);
void set_title(const QString &v); void set_title(const QString &v);
void set_titlesort(const QString &v);
void set_album(const QString &v); void set_album(const QString &v);
void set_albumsort(const QString &v);
void set_artist(const QString &v); void set_artist(const QString &v);
void set_artistsort(const QString &v);
void set_albumartist(const QString &v); void set_albumartist(const QString &v);
void set_albumartistsort(const QString &v);
void set_track(const int v); void set_track(const int v);
void set_disc(const int v); void set_disc(const int v);
void set_year(const int v); void set_year(const int v);
@@ -271,7 +280,9 @@ class Song {
void set_genre(const QString &v); void set_genre(const QString &v);
void set_compilation(bool v); void set_compilation(bool v);
void set_composer(const QString &v); void set_composer(const QString &v);
void set_composersort(const QString &v);
void set_performer(const QString &v); void set_performer(const QString &v);
void set_performersort(const QString &v);
void set_grouping(const QString &v); void set_grouping(const QString &v);
void set_comment(const QString &v); void set_comment(const QString &v);
void set_lyrics(const QString &v); void set_lyrics(const QString &v);
@@ -317,6 +328,9 @@ class Song {
void set_cue_path(const QString &v); void set_cue_path(const QString &v);
void set_rating(const float v); void set_rating(const float v);
void set_bpm(const float v);
void set_mood(const QString &v);
void set_initial_key(const QString &v);
void set_acoustid_id(const QString &v); void set_acoustid_id(const QString &v);
void set_acoustid_fingerprint(const QString &v); void set_acoustid_fingerprint(const QString &v);
@@ -340,12 +354,18 @@ class Song {
void set_stream_url(const QUrl &v); void set_stream_url(const QUrl &v);
void set_title(const TagLib::String &v); void set_title(const TagLib::String &v);
void set_titlesort(const TagLib::String &v);
void set_album(const TagLib::String &v); void set_album(const TagLib::String &v);
void set_albumsort(const TagLib::String &v);
void set_artist(const TagLib::String &v); void set_artist(const TagLib::String &v);
void set_artistsort(const TagLib::String &v);
void set_albumartist(const TagLib::String &v); void set_albumartist(const TagLib::String &v);
void set_albumartistsort(const TagLib::String &v);
void set_genre(const TagLib::String &v); void set_genre(const TagLib::String &v);
void set_composer(const TagLib::String &v); void set_composer(const TagLib::String &v);
void set_composersort(const TagLib::String &v);
void set_performer(const TagLib::String &v); void set_performer(const TagLib::String &v);
void set_performersort(const TagLib::String &v);
void set_grouping(const TagLib::String &v); void set_grouping(const TagLib::String &v);
void set_comment(const TagLib::String &v); void set_comment(const TagLib::String &v);
void set_lyrics(const TagLib::String &v); void set_lyrics(const TagLib::String &v);
@@ -364,14 +384,21 @@ class Song {
void set_musicbrainz_disc_id(const TagLib::String &v); void set_musicbrainz_disc_id(const TagLib::String &v);
void set_musicbrainz_release_group_id(const TagLib::String &v); void set_musicbrainz_release_group_id(const TagLib::String &v);
void set_musicbrainz_work_id(const TagLib::String &v); void set_musicbrainz_work_id(const TagLib::String &v);
void set_mood(const TagLib::String &v);
void set_initial_key(const TagLib::String &v);
const QUrl &effective_stream_url() const; const QUrl &effective_url() const;
const QString &effective_titlesort() const;
const QString &effective_albumartist() const; const QString &effective_albumartist() const;
const QString &effective_albumartist_sortable() const; const QString &effective_albumartistsort() const;
const QString &effective_artistsort() const;
const QString &effective_album() const; const QString &effective_album() const;
const QString &effective_albumsort() const;
const QString &effective_composersort() const;
const QString &effective_performersort() const;
int effective_originalyear() const; int effective_originalyear() const;
const QString &playlist_albumartist() const; const QString &playlist_effective_albumartist() const;
const QString &playlist_albumartist_sortable() const; const QString &playlist_effective_albumartistsort() const;
bool is_metadata_good() const; bool is_metadata_good() const;
bool is_local_collection_song() const; bool is_local_collection_song() const;
@@ -402,6 +429,13 @@ class Song {
bool comment_supported() const; bool comment_supported() const;
bool lyrics_supported() const; bool lyrics_supported() const;
bool albumartistsort_supported() const;
bool albumsort_supported() const;
bool artistsort_supported() const;
bool composersort_supported() const;
bool performersort_supported() const;
bool titlesort_supported() const;
static bool save_embedded_cover_supported(const FileType filetype); static bool save_embedded_cover_supported(const FileType filetype);
bool save_embedded_cover_supported() const { return url().isLocalFile() && save_embedded_cover_supported(filetype()) && !has_cue(); }; bool save_embedded_cover_supported() const { return url().isLocalFile() && save_embedded_cover_supported(filetype()) && !has_cue(); };
@@ -430,6 +464,7 @@ class Song {
bool IsEditable() const; bool IsEditable() const;
// Comparison functions // Comparison functions
bool IsFileInfoEqual(const Song &other) const;
bool IsMetadataEqual(const Song &other) const; bool IsMetadataEqual(const Song &other) const;
bool IsPlayStatisticsEqual(const Song &other) const; bool IsPlayStatisticsEqual(const Song &other) const;
bool IsRatingEqual(const Song &other) const; bool IsRatingEqual(const Song &other) const;
@@ -438,7 +473,10 @@ class Song {
bool IsMusicBrainzEqual(const Song &other) const; bool IsMusicBrainzEqual(const Song &other) const;
bool IsEBUR128Equal(const Song &other) const; bool IsEBUR128Equal(const Song &other) const;
bool IsArtEqual(const Song &other) const; bool IsArtEqual(const Song &other) const;
bool IsCompilationEqual(const Song &other) const;
bool IsSettingsEqual(const Song &other) const;
bool IsAllMetadataEqual(const Song &other) const; bool IsAllMetadataEqual(const Song &other) const;
bool IsEqual(const Song &other) const;
bool IsOnSameAlbum(const Song &other) const; bool IsOnSameAlbum(const Song &other) const;
bool IsSimilar(const Song &other) const; bool IsSimilar(const Song &other) const;
@@ -448,6 +486,7 @@ class Song {
static QString DescriptionForSource(const Source source); static QString DescriptionForSource(const Source source);
static Source SourceFromText(const QString &source); static Source SourceFromText(const QString &source);
static QIcon IconForSource(const Source source); static QIcon IconForSource(const Source source);
static QString DomainForSource(const Source source);
static QString TextForFiletype(const FileType filetype); static QString TextForFiletype(const FileType filetype);
static QString ExtensionForFiletype(const FileType filetype); static QString ExtensionForFiletype(const FileType filetype);
static QIcon IconForFiletype(const FileType filetype); static QIcon IconForFiletype(const FileType filetype);
@@ -455,9 +494,12 @@ class Song {
QString TextForSource() const { return TextForSource(source()); } QString TextForSource() const { return TextForSource(source()); }
QString DescriptionForSource() const { return DescriptionForSource(source()); } QString DescriptionForSource() const { return DescriptionForSource(source()); }
QIcon IconForSource() const { return IconForSource(source()); } QIcon IconForSource() const { return IconForSource(source()); }
QString DomainForSource() const { return DomainForSource(source()); }
QString TextForFiletype() const { return TextForFiletype(filetype()); } QString TextForFiletype() const { return TextForFiletype(filetype()); }
QIcon IconForFiletype() const { return IconForFiletype(filetype()); } QIcon IconForFiletype() const { return IconForFiletype(filetype()); }
QString ShareURL() const;
bool IsFileLossless() const; bool IsFileLossless() const;
static FileType FiletypeByMimetype(const QString &mimetype); static FileType FiletypeByMimetype(const QString &mimetype);
static FileType FiletypeByDescription(const QString &text); static FileType FiletypeByDescription(const QString &text);
@@ -521,9 +563,6 @@ class Song {
private: private:
struct Private; struct Private;
static QString sortable(const QString &v);
QSharedDataPointer<Private> d; QSharedDataPointer<Private> d;
}; };

View File

@@ -178,9 +178,11 @@ SongLoader::Result SongLoader::LoadLocalPartial(const QString &filename) {
SongLoader::Result SongLoader::LoadAudioCD() { SongLoader::Result SongLoader::LoadAudioCD() {
#ifdef HAVE_AUDIOCD #ifdef HAVE_AUDIOCD
CddaSongLoader *cdda_song_loader = new CddaSongLoader(QUrl(), this); CDDASongLoader *cdda_song_loader = new CDDASongLoader(QUrl(), this);
QObject::connect(cdda_song_loader, &CddaSongLoader::SongsDurationLoaded, this, &SongLoader::AudioCDTracksLoadFinishedSlot); QObject::connect(cdda_song_loader, &CDDASongLoader::LoadError, this, &SongLoader::AudioCDTracksLoadErrorSlot);
QObject::connect(cdda_song_loader, &CddaSongLoader::SongsMetadataLoaded, this, &SongLoader::AudioCDTracksTagsLoaded); QObject::connect(cdda_song_loader, &CDDASongLoader::SongsLoaded, this, &SongLoader::AudioCDTracksLoadedSlot);
QObject::connect(cdda_song_loader, &CDDASongLoader::SongsUpdated, this, &SongLoader::AudioCDTracksUpdatedSlot);
QObject::connect(cdda_song_loader, &CDDASongLoader::LoadingFinished, this, &SongLoader::AudioCDLoadingFinishedSlot);
cdda_song_loader->LoadSongs(); cdda_song_loader->LoadSongs();
return Result::Success; return Result::Success;
#else #else
@@ -192,23 +194,38 @@ SongLoader::Result SongLoader::LoadAudioCD() {
#ifdef HAVE_AUDIOCD #ifdef HAVE_AUDIOCD
void SongLoader::AudioCDTracksLoadFinishedSlot(const SongList &songs, const QString &error) { void SongLoader::AudioCDTracksLoadErrorSlot(const QString &error) {
songs_ = songs;
errors_ << error; errors_ << error;
Q_EMIT AudioCDTracksLoadFinished();
} }
void SongLoader::AudioCDTracksTagsLoaded(const SongList &songs) { void SongLoader::AudioCDTracksLoadedSlot(const SongList &songs) {
CddaSongLoader *cdda_song_loader = qobject_cast<CddaSongLoader*>(sender());
cdda_song_loader->deleteLater();
songs_ = songs; songs_ = songs;
Q_EMIT LoadAudioCDFinished(true);
Q_EMIT AudioCDTracksLoaded();
} }
#endif
void SongLoader::AudioCDTracksUpdatedSlot(const SongList &songs) {
songs_ = songs;
Q_EMIT AudioCDTracksUpdated();
}
void SongLoader::AudioCDLoadingFinishedSlot() {
CDDASongLoader *cdda_song_loader = qobject_cast<CDDASongLoader*>(sender());
cdda_song_loader->deleteLater();
Q_EMIT AudioCDLoadingFinished(true);
}
#endif // HAVE_AUDIOCD
SongLoader::Result SongLoader::LoadLocal(const QString &filename) { SongLoader::Result SongLoader::LoadLocal(const QString &filename) {

View File

@@ -50,7 +50,7 @@ class ParserBase;
class CueParser; class CueParser;
#ifdef HAVE_AUDIOCD #ifdef HAVE_AUDIOCD
class CddaSongLoader; class CDDASongLoader;
#endif #endif
class SongLoader : public QObject { class SongLoader : public QObject {
@@ -90,17 +90,21 @@ class SongLoader : public QObject {
QStringList errors() { return errors_; } QStringList errors() { return errors_; }
Q_SIGNALS: Q_SIGNALS:
void AudioCDTracksLoadFinished(); void AudioCDTracksLoaded();
void LoadAudioCDFinished(const bool success); void AudioCDTracksUpdated();
void AudioCDLoadingFinished(const bool success);
void LoadRemoteFinished(); void LoadRemoteFinished();
private Q_SLOTS: private Q_SLOTS:
void ScheduleTimeout(); void ScheduleTimeout();
void Timeout(); void Timeout();
void StopTypefind(); void StopTypefind();
#ifdef HAVE_AUDIOCD #ifdef HAVE_AUDIOCD
void AudioCDTracksLoadFinishedSlot(const SongList &songs, const QString &error); void AudioCDTracksLoadErrorSlot(const QString &error);
void AudioCDTracksTagsLoaded(const SongList &songs); void AudioCDTracksLoadedSlot(const SongList &songs);
void AudioCDTracksUpdatedSlot(const SongList &songs);
void AudioCDLoadingFinishedSlot();
#endif // HAVE_AUDIOCD #endif // HAVE_AUDIOCD
private: private:

View File

@@ -31,8 +31,8 @@
#include <QTextStream> #include <QTextStream>
#include <QFile> #include <QFile>
#include <QString> #include <QString>
#include <QColor>
#include <QPalette> #include <QPalette>
#include <QColor>
#include <QEvent> #include <QEvent>
#include "includes/shared_ptr.h" #include "includes/shared_ptr.h"
@@ -80,20 +80,13 @@ void StyleSheetLoader::UpdateStyleSheet(QWidget *widget, SharedPtr<StyleSheetDat
// Replace %palette-role with actual colours // Replace %palette-role with actual colours
QPalette p(widget->palette()); QPalette p(widget->palette());
{ QColor color_altbase = p.color(QPalette::AlternateBase);
QColor alt = p.color(QPalette::AlternateBase);
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
if (alt.lightness() > 180) { color_altbase.setAlpha(color_altbase.alpha() >= 180 ? (color_altbase.lightness() > 180 ? 130 : 16) : color_altbase.alpha());
alt.setAlpha(130);
}
else {
alt.setAlpha(16);
}
#else #else
alt.setAlpha(130); color_altbase.setAlpha(color_altbase.alpha() >= 180 ? 116 : color_altbase.alpha());
#endif #endif
stylesheet.replace("%palette-alternate-base"_L1, QStringLiteral("rgba(%1,%2,%3,%4)").arg(alt.red()).arg(alt.green()).arg(alt.blue()).arg(alt.alpha())); stylesheet.replace("%palette-alternate-base"_L1, QStringLiteral("rgba(%1,%2,%3,%4)").arg(color_altbase.red()).arg(color_altbase.green()).arg(color_altbase.blue()).arg(color_altbase.alpha()));
}
ReplaceColor(&stylesheet, u"Window"_s, p, QPalette::Window); ReplaceColor(&stylesheet, u"Window"_s, p, QPalette::Window);
ReplaceColor(&stylesheet, u"Background"_s, p, QPalette::Window); ReplaceColor(&stylesheet, u"Background"_s, p, QPalette::Window);

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player * Strawberry Music Player
* This file was part of Clementine. * This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com> * Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -21,10 +21,19 @@
#include "config.h" #include "config.h"
#include <cstddef>
#include <cdio/types.h>
#include <cdio/cdio.h>
#include <chrono>
#include <QString> #include <QString>
#include <QUrl> #include <QUrl>
#include <QTimer>
#include "includes/shared_ptr.h" #include "includes/shared_ptr.h"
#include "core/logging.h"
#include "collection/collectionmodel.h" #include "collection/collectionmodel.h"
#include "cddasongloader.h" #include "cddasongloader.h"
#include "connecteddevice.h" #include "connecteddevice.h"
@@ -33,7 +42,9 @@
class DeviceLister; class DeviceLister;
class DeviceManager; class DeviceManager;
CddaDevice::CddaDevice(const QUrl &url, using namespace std::chrono_literals;
CDDADevice::CDDADevice(const QUrl &url,
DeviceLister *lister, DeviceLister *lister,
const QString &unique_id, const QString &unique_id,
DeviceManager *device_manager, DeviceManager *device_manager,
@@ -45,36 +56,86 @@ CddaDevice::CddaDevice(const QUrl &url,
const bool first_time, const bool first_time,
QObject *parent) QObject *parent)
: ConnectedDevice(url, lister, unique_id, device_manager, task_manager, database, tagreader_client, albumcover_loader, database_id, first_time, parent), : ConnectedDevice(url, lister, unique_id, device_manager, task_manager, database, tagreader_client, albumcover_loader, database_id, first_time, parent),
cdda_song_loader_(url) { cdda_song_loader_(url),
cdio_(nullptr),
timer_disc_changed_(new QTimer(this)) {
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsLoaded, this, &CddaDevice::SongsLoaded); timer_disc_changed_->setInterval(1s);
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsDurationLoaded, this, &CddaDevice::SongsLoaded);
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsMetadataLoaded, this, &CddaDevice::SongsLoaded); QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongsLoaded, this, &CDDADevice::SongsLoaded);
QObject::connect(this, &CddaDevice::SongsDiscovered, collection_model_, &CollectionModel::AddReAddOrUpdate); QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongsUpdated, this, &CDDADevice::SongsLoaded);
QObject::connect(&cdda_song_loader_, &CDDASongLoader::LoadingFinished, this, &CDDADevice::SongLoadingFinished);
QObject::connect(this, &CDDADevice::SongsDiscovered, collection_model_, &CollectionModel::AddReAddOrUpdate);
QObject::connect(timer_disc_changed_, &QTimer::timeout, this, &CDDADevice::CheckDiscChanged);
} }
bool CddaDevice::Init() { CDDADevice::~CDDADevice() {
if (cdio_) {
cdio_destroy(cdio_);
cdio_ = nullptr;
}
}
bool CDDADevice::Init() {
if (!cdio_) {
cdio_ = cdio_open(url_.path().toLocal8Bit().constData(), DRIVER_DEVICE);
if (!cdio_) return false;
}
LoadSongs();
WatchForDiscChanges(true);
song_count_ = 0; // Reset song count, in case it was already set
cdda_song_loader_.LoadSongs();
return true; return true;
} }
void CddaDevice::Refresh() { void CDDADevice::WatchForDiscChanges(const bool watch) {
if (!cdda_song_loader_.HasChanged()) { if (watch && !timer_disc_changed_->isActive()) {
return; timer_disc_changed_->start();
}
else if (!watch && timer_disc_changed_->isActive()) {
timer_disc_changed_->stop();
} }
Init();
} }
void CddaDevice::SongsLoaded(const SongList &songs) { void CDDADevice::CheckDiscChanged() {
if (!cdio_ || cdda_song_loader_.IsActive()) return;
if (cdio_get_media_changed(cdio_) == 1) {
qLog(Debug) << "CD changed, reloading songs";
SongsLoaded();
LoadSongs();
}
}
void CDDADevice::LoadSongs() {
cdda_song_loader_.LoadSongs();
WatchForDiscChanges(false);
}
void CDDADevice::SongsLoaded(const SongList &songs) {
collection_model_->Reset(); collection_model_->Reset();
Q_EMIT SongsDiscovered(songs); Q_EMIT SongsDiscovered(songs);
song_count_ = songs.size(); song_count_ = songs.size();
(void)cdio_get_media_changed(cdio_);
} }
void CDDADevice::SongLoadingFinished() {
WatchForDiscChanges(true);
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player * Strawberry Music Player
* This file was part of Clementine. * This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com> * Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -24,6 +24,11 @@
#include "config.h" #include "config.h"
#include <cstddef>
#include <cdio/types.h>
#include <cdio/cdio.h>
#include <QObject> #include <QObject>
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
@@ -35,6 +40,8 @@
#include "cddasongloader.h" #include "cddasongloader.h"
#include "connecteddevice.h" #include "connecteddevice.h"
class QTimer;
class DeviceLister; class DeviceLister;
class DeviceManager; class DeviceManager;
class TaskManager; class TaskManager;
@@ -42,11 +49,11 @@ class Database;
class TagReaderClient; class TagReaderClient;
class AlbumCoverLoader; class AlbumCoverLoader;
class CddaDevice : public ConnectedDevice { class CDDADevice : public ConnectedDevice {
Q_OBJECT Q_OBJECT
public: public:
Q_INVOKABLE explicit CddaDevice(const QUrl &url, Q_INVOKABLE explicit CDDADevice(const QUrl &url,
DeviceLister *lister, DeviceLister *lister,
const QString &unique_id, const QString &unique_id,
DeviceManager *device_manager, DeviceManager *device_manager,
@@ -58,21 +65,29 @@ class CddaDevice : public ConnectedDevice {
const bool first_time, const bool first_time,
QObject *parent = nullptr); QObject *parent = nullptr);
~CDDADevice();
bool Init() override; bool Init() override;
void Refresh() override;
bool CopyToStorage(const CopyJob&, QString&) override { return false; } bool CopyToStorage(const CopyJob&, QString&) override { return false; }
bool DeleteFromStorage(const MusicStorage::DeleteJob&) override { return false; } bool DeleteFromStorage(const MusicStorage::DeleteJob&) override { return false; }
static QStringList url_schemes() { return QStringList() << QStringLiteral("cdda"); } static QStringList url_schemes() { return QStringList() << QStringLiteral("cdda"); }
void LoadSongs();
void WatchForDiscChanges(const bool watch);
Q_SIGNALS: Q_SIGNALS:
void SongsDiscovered(const SongList &songs); void SongsDiscovered(const SongList &songs);
private Q_SLOTS: private Q_SLOTS:
void SongsLoaded(const SongList &songs); void CheckDiscChanged();
void SongsLoaded(const SongList &songs = SongList());
void SongLoadingFinished();
private: private:
CddaSongLoader cdda_song_loader_; CDDASongLoader cdda_song_loader_;
CdIo_t *cdio_;
QTimer *timer_disc_changed_;
}; };
#endif // CDDADEVICE_H #endif // CDDADEVICE_H

View File

@@ -40,9 +40,9 @@
using namespace Qt::Literals::StringLiterals; using namespace Qt::Literals::StringLiterals;
QStringList CddaLister::DeviceUniqueIDs() { return devices_list_; } QStringList CDDALister::DeviceUniqueIDs() { return devices_list_; }
QVariantList CddaLister::DeviceIcons(const QString &id) { QVariantList CDDALister::DeviceIcons(const QString &id) {
Q_UNUSED(id) Q_UNUSED(id)
@@ -52,7 +52,7 @@ QVariantList CddaLister::DeviceIcons(const QString &id) {
} }
QString CddaLister::DeviceManufacturer(const QString &id) { QString CDDALister::DeviceManufacturer(const QString &id) {
CdIo_t *cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE); CdIo_t *cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE);
cdio_hwinfo_t cd_info; cdio_hwinfo_t cd_info;
@@ -65,7 +65,7 @@ QString CddaLister::DeviceManufacturer(const QString &id) {
} }
QString CddaLister::DeviceModel(const QString &id) { QString CDDALister::DeviceModel(const QString &id) {
CdIo_t *cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE); CdIo_t *cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE);
cdio_hwinfo_t cd_info; cdio_hwinfo_t cd_info;
@@ -78,7 +78,7 @@ QString CddaLister::DeviceModel(const QString &id) {
} }
quint64 CddaLister::DeviceCapacity(const QString &id) { quint64 CDDALister::DeviceCapacity(const QString &id) {
Q_UNUSED(id) Q_UNUSED(id)
@@ -86,7 +86,7 @@ quint64 CddaLister::DeviceCapacity(const QString &id) {
} }
quint64 CddaLister::DeviceFreeSpace(const QString &id) { quint64 CDDALister::DeviceFreeSpace(const QString &id) {
Q_UNUSED(id) Q_UNUSED(id)
@@ -94,37 +94,38 @@ quint64 CddaLister::DeviceFreeSpace(const QString &id) {
} }
QVariantMap CddaLister::DeviceHardwareInfo(const QString &id) { QVariantMap CDDALister::DeviceHardwareInfo(const QString &id) {
Q_UNUSED(id) Q_UNUSED(id)
return QVariantMap(); return QVariantMap();
} }
QString CddaLister::MakeFriendlyName(const QString &id) { QString CDDALister::MakeFriendlyName(const QString &id) {
CdIo_t *cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE); CdIo_t *cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE);
cdio_hwinfo_t cd_info; cdio_hwinfo_t cd_info;
if (cdio_get_hwinfo(cdio, &cd_info)) { if (cdio_get_hwinfo(cdio, &cd_info)) {
const QString friendly_name = QString::fromUtf8(cd_info.psz_model).trimmed();
cdio_destroy(cdio); cdio_destroy(cdio);
return QString::fromUtf8(cd_info.psz_model); return friendly_name;
} }
cdio_destroy(cdio); cdio_destroy(cdio);
return u"CD ("_s + id + QLatin1Char(')'); return u"CD ("_s + id + QLatin1Char(')');
} }
QList<QUrl> CddaLister::MakeDeviceUrls(const QString &id) { QList<QUrl> CDDALister::MakeDeviceUrls(const QString &id) {
return QList<QUrl>() << QUrl(u"cdda://"_s + id); return QList<QUrl>() << QUrl(u"cdda://"_s + id);
} }
void CddaLister::UnmountDevice(const QString &id) { void CDDALister::UnmountDevice(const QString &id) {
cdio_eject_media_drive(id.toLocal8Bit().constData()); cdio_eject_media_drive(id.toLocal8Bit().constData());
} }
void CddaLister::UpdateDeviceFreeSpace(const QString &id) { void CDDALister::UpdateDeviceFreeSpace(const QString &id) {
Q_UNUSED(id) Q_UNUSED(id)
} }
bool CddaLister::Init() { bool CDDALister::Init() {
cdio_init(); cdio_init();
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS

View File

@@ -34,11 +34,11 @@
#include "devicelister.h" #include "devicelister.h"
class CddaLister : public DeviceLister { class CDDALister : public DeviceLister {
Q_OBJECT Q_OBJECT
public: public:
explicit CddaLister(QObject *parent = nullptr) : DeviceLister(parent) {} explicit CDDALister(QObject *parent = nullptr) : DeviceLister(parent) {}
QStringList DeviceUniqueIDs() override; QStringList DeviceUniqueIDs() override;
QVariantList DeviceIcons(const QString &id) override; QVariantList DeviceIcons(const QString &id) override;

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player * Strawberry Music Player
* This file was part of Clementine. * This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com> * Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -21,25 +21,24 @@
#include "config.h" #include "config.h"
#include <memory> #include <QtGlobal>
#include <cstddef> #include <memory>
#include <glib.h> #include <glib.h>
#include <glib/gtypes.h> #include <glib/gtypes.h>
#include <glib-object.h> #include <glib-object.h>
#include <cdio/cdio.h>
#include <gst/gst.h> #include <gst/gst.h>
#include <gst/tag/tag.h> #include <gst/tag/tag.h>
#include <QtGlobal>
#include <QObject> #include <QObject>
#include <QMutex> #include <QMutex>
#include <QByteArray> #include <QByteArray>
#include <QString> #include <QString>
#include <QUrl> #include <QUrl>
#include <QtConcurrentRun>
#include <QScopeGuard>
#include "cddasongloader.h" #include "cddasongloader.h"
#include "includes/shared_ptr.h" #include "includes/shared_ptr.h"
@@ -51,18 +50,21 @@ using std::make_shared;
using namespace Qt::Literals::StringLiterals; using namespace Qt::Literals::StringLiterals;
CddaSongLoader::CddaSongLoader(const QUrl &url, QObject *parent) CDDASongLoader::CDDASongLoader(const QUrl &url, QObject *parent)
: QObject(parent), : QObject(parent),
url_(url), url_(url),
network_(make_shared<NetworkAccessManager>()), network_(make_shared<NetworkAccessManager>()) {
cdda_(nullptr),
cdio_(nullptr) {}
CddaSongLoader::~CddaSongLoader() { #ifdef HAVE_MUSICBRAINZ
if (cdio_) cdio_destroy(cdio_); QObject::connect(this, &CDDASongLoader::LoadTagsFromMusicBrainz, this, &CDDASongLoader::LoadTagsFromMusicBrainzSlot);
#endif // HAVE_MUSICBRAINZ
} }
QUrl CddaSongLoader::GetUrlFromTrack(int track_number) const { CDDASongLoader::~CDDASongLoader() {
loading_future_.waitForFinished();
}
QUrl CDDASongLoader::GetUrlFromTrack(int track_number) const {
if (url_.isEmpty()) { if (url_.isEmpty()) {
return QUrl(QStringLiteral("cdda://%1a").arg(track_number)); return QUrl(QStringLiteral("cdda://%1a").arg(track_number));
@@ -72,72 +74,77 @@ QUrl CddaSongLoader::GetUrlFromTrack(int track_number) const {
} }
void CddaSongLoader::LoadSongs() { void CDDASongLoader::LoadSongs() {
QMutexLocker locker(&mutex_load_); if (IsActive()) {
cdio_ = cdio_open(url_.path().toLocal8Bit().constData(), DRIVER_DEVICE);
if (cdio_ == nullptr) {
Error(u"Unable to open CDIO device."_s);
return; return;
} }
// Create gstreamer cdda element loading_future_ = QtConcurrent::run(&CDDASongLoader::LoadSongsFromCDDA, this);
}
void CDDASongLoader::LoadSongsFromCDDA() {
QMutexLocker l(&mutex_load_);
GError *error = nullptr; GError *error = nullptr;
cdda_ = gst_element_make_from_uri(GST_URI_SRC, "cdda://", nullptr, &error); GstElement *cdda = gst_element_factory_make("cdiocddasrc", nullptr);
if (error) { if (error) {
Error(QStringLiteral("%1: %2").arg(error->code).arg(QString::fromUtf8(error->message))); Error(QStringLiteral("%1: %2").arg(error->code).arg(QString::fromUtf8(error->message)));
} }
if (!cdda_) return; if (!cdda) {
Error(tr("Could not create cdiocddasrc"));
return;
}
if (!url_.isEmpty()) { if (!url_.isEmpty()) {
g_object_set(cdda_, "device", g_strdup(url_.path().toLocal8Bit().constData()), nullptr); g_object_set(cdda, "device", g_strdup(url_.path().toLocal8Bit().constData()), nullptr);
} }
if (g_object_class_find_property(G_OBJECT_GET_CLASS(cdda_), "paranoia-mode")) { if (g_object_class_find_property(G_OBJECT_GET_CLASS(cdda), "paranoia-mode")) {
g_object_set(cdda_, "paranoia-mode", 0, nullptr); g_object_set(cdda, "paranoia-mode", 0, nullptr);
} }
// Change the element's state to ready and paused, to be able to query it // Change the element's state to ready and paused, to be able to query it
if (gst_element_set_state(cdda_, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE) { if (gst_element_set_state(cdda, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE) {
gst_element_set_state(cdda_, GST_STATE_NULL); gst_element_set_state(cdda, GST_STATE_NULL);
gst_object_unref(GST_OBJECT(cdda_)); gst_object_unref(GST_OBJECT(cdda));
cdda_ = nullptr; cdda = nullptr;
Error(tr("Error while setting CDDA device to ready state.")); Error(tr("Error while setting CDDA device to ready state."));
return; return;
} }
if (gst_element_set_state(cdda_, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) { if (gst_element_set_state(cdda, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) {
gst_element_set_state(cdda_, GST_STATE_NULL); gst_element_set_state(cdda, GST_STATE_NULL);
gst_object_unref(GST_OBJECT(cdda_)); gst_object_unref(GST_OBJECT(cdda));
cdda_ = nullptr; cdda = nullptr;
Error(tr("Error while setting CDDA device to pause state.")); Error(tr("Error while setting CDDA device to pause state."));
return; return;
} }
// Get number of tracks // Get number of tracks
GstFormat fmt = gst_format_get_by_nick("track"); GstFormat format_track = gst_format_get_by_nick("track");
GstFormat out_fmt = fmt; GstFormat format_duration = format_track;
gint64 num_tracks = 0; gint64 total_tracks = 0;
if (!gst_element_query_duration(cdda_, out_fmt, &num_tracks)) { if (!gst_element_query_duration(cdda, format_duration, &total_tracks)) {
gst_element_set_state(cdda_, GST_STATE_NULL); gst_element_set_state(cdda, GST_STATE_NULL);
gst_object_unref(GST_OBJECT(cdda_)); gst_object_unref(GST_OBJECT(cdda));
cdda_ = nullptr; cdda = nullptr;
Error(tr("Error while querying CDDA tracks.")); Error(tr("Error while querying CDDA tracks."));
return; return;
} }
if (out_fmt != fmt) { if (format_duration != format_track) {
qLog(Error) << "Error while querying cdda GstElement (2)."; qLog(Error) << "Error while querying CDDA GstElement (2).";
gst_element_set_state(cdda_, GST_STATE_NULL); gst_element_set_state(cdda, GST_STATE_NULL);
gst_object_unref(GST_OBJECT(cdda_)); gst_object_unref(GST_OBJECT(cdda));
cdda_ = nullptr; cdda = nullptr;
Error(tr("Error while querying CDDA tracks.")); Error(tr("Error while querying CDDA tracks."));
return; return;
} }
SongList songs; QMap<int, Song> songs;
songs.reserve(num_tracks); for (int track_number = 1; track_number <= total_tracks; ++track_number) {
for (int track_number = 1; track_number <= num_tracks; ++track_number) {
// Init song
Song song(Song::Source::CDDA); Song song(Song::Source::CDDA);
song.set_id(track_number); song.set_id(track_number);
song.set_valid(true); song.set_valid(true);
@@ -145,129 +152,269 @@ void CddaSongLoader::LoadSongs() {
song.set_url(GetUrlFromTrack(track_number)); song.set_url(GetUrlFromTrack(track_number));
song.set_title(QStringLiteral("Track %1").arg(track_number)); song.set_title(QStringLiteral("Track %1").arg(track_number));
song.set_track(track_number); song.set_track(track_number);
songs << song; songs.insert(track_number, song);
} }
Q_EMIT SongsLoaded(songs);
Q_EMIT SongsLoaded(songs.values());
#ifdef HAVE_MUSICBRAINZ
gst_tag_register_musicbrainz_tags(); gst_tag_register_musicbrainz_tags();
#endif // HAVE_MUSICBRAINZ
GstElement *pipeline = gst_pipeline_new("pipeline"); GstElement *pipeline = gst_pipeline_new("pipeline");
GstElement *sink = gst_element_factory_make("fakesink", nullptr); GstElement *sink = gst_element_factory_make("fakesink", nullptr);
gst_bin_add_many(GST_BIN(pipeline), cdda_, sink, nullptr); gst_bin_add_many(GST_BIN(pipeline), cdda, sink, nullptr);
gst_element_link(cdda_, sink); gst_element_link(cdda, sink);
gst_element_set_state(pipeline, GST_STATE_READY); gst_element_set_state(pipeline, GST_STATE_READY);
gst_element_set_state(pipeline, GST_STATE_PAUSED); gst_element_set_state(pipeline, GST_STATE_PAUSED);
// Get TOC and TAG messages
GstMessage *msg = nullptr; GstMessage *msg = nullptr;
GstMessage *msg_toc = nullptr; int track_artist_tags = 0;
GstMessage *msg_tag = nullptr; int track_album_tags = 0;
while ((msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline), GST_SECOND, static_cast<GstMessageType>(GST_MESSAGE_TOC | GST_MESSAGE_TAG)))) { int track_title_tags = 0;
if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TOC) { #ifdef HAVE_MUSICBRAINZ
if (msg_toc) gst_message_unref(msg_toc); // Shouldn't happen, but just in case QString musicbrainz_discid;
msg_toc = msg; #endif // HAVE_MUSICBRAINZ
} GstMessageType msg_filter = static_cast<GstMessageType>(GST_MESSAGE_TOC|GST_MESSAGE_TAG);
else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TAG) { while (msg_filter != 0 && (msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline), GST_SECOND * 5, msg_filter))) {
if (msg_tag) gst_message_unref(msg_tag);
msg_tag = msg;
}
}
// Handle TOC message: get tracks duration const QScopeGuard scopeguard_msg = qScopeGuard([msg]() {
if (msg_toc) { gst_message_unref(msg);
GstToc *toc = nullptr; });
gst_message_parse_toc(msg_toc, &toc, nullptr);
if (toc) { if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TOC) {
GstToc *toc = nullptr;
gst_message_parse_toc(msg, &toc, nullptr);
const QScopeGuard scopeguard_toc = qScopeGuard([toc]() {
gst_toc_unref(toc);
});
GList *entries = gst_toc_get_entries(toc); GList *entries = gst_toc_get_entries(toc);
if (entries && static_cast<guint>(songs.size()) <= g_list_length(entries)) { int track_number = 0;
int i = 0; for (GList *entry_node = entries; entry_node != nullptr; entry_node = entry_node->next) {
for (GList *node = entries; node != nullptr; node = node->next) { ++track_number;
GstTocEntry *entry = static_cast<GstTocEntry*>(node->data); if (songs.contains(track_number)) {
qint64 duration = 0; Song &song = songs[track_number];
GstTocEntry *entry = static_cast<GstTocEntry*>(entry_node->data);
gint64 start = 0, stop = 0; gint64 start = 0, stop = 0;
if (gst_toc_entry_get_start_stop_times(entry, &start, &stop)) duration = stop - start; if (gst_toc_entry_get_start_stop_times(entry, &start, &stop)) {
songs[i++].set_length_nanosec(duration); song.set_length_nanosec(static_cast<qint64>(stop - start));
}
} }
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^ GST_MESSAGE_TOC);
} }
} }
gst_message_unref(msg_toc);
} else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TAG) {
Q_EMIT SongsDurationLoaded(songs);
GstTagList *tags = nullptr;
gst_message_parse_tag(msg, &tags);
const QScopeGuard scopeguard_tags = qScopeGuard([tags]() {
gst_tag_list_free(tags);
});
gint64 track_index = 0;
gst_element_query_position(cdda, format_track, &track_index);
char *tag = nullptr;
#ifdef HAVE_MUSICBRAINZ #ifdef HAVE_MUSICBRAINZ
// Handle TAG message: generate MusicBrainz DiscId if (musicbrainz_discid.isEmpty()) {
if (msg_tag) { if (gst_tag_list_get_string(tags, GST_TAG_CDDA_MUSICBRAINZ_DISCID, &tag)) {
GstTagList *tags = nullptr; musicbrainz_discid = QString::fromUtf8(tag);
gst_message_parse_tag(msg_tag, &tags); g_free(tag);
char *string_mb = nullptr; tag = nullptr;
if (gst_tag_list_get_string(tags, GST_TAG_CDDA_MUSICBRAINZ_DISCID, &string_mb)) { }
QString musicbrainz_discid = QString::fromUtf8(string_mb); }
qLog(Info) << "MusicBrainz discid: " << musicbrainz_discid;
MusicBrainzClient *musicbrainz_client = new MusicBrainzClient(network_);
QObject::connect(musicbrainz_client, &MusicBrainzClient::DiscIdFinished, this, &CddaSongLoader::AudioCDTagsLoaded);
musicbrainz_client->StartDiscIdRequest(musicbrainz_discid);
g_free(string_mb);
gst_message_unref(msg_tag);
gst_tag_list_unref(tags);
}
}
#endif #endif
guint track_number = 0;
if (!gst_tag_list_get_uint(tags, GST_TAG_TRACK_NUMBER, &track_number)) {
qLog(Error) << "Could not get track number";
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^GST_MESSAGE_TAG);
continue;
}
if (!songs.contains(track_number)) {
qLog(Error) << "Got invalid track number" << track_number;
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^GST_MESSAGE_TAG);
continue;
}
Song &song = songs[track_number];
guint64 duration = 0;
if (gst_tag_list_get_uint64(tags, GST_TAG_DURATION, &duration)) {
song.set_length_nanosec(static_cast<qint64>(duration));
}
if (gst_tag_list_get_string(tags, GST_TAG_ALBUM_ARTIST, &tag)) {
song.set_albumartist(QString::fromUtf8(tag));
g_free(tag);
tag = nullptr;
}
if (gst_tag_list_get_string(tags, GST_TAG_ALBUM_ARTIST_SORTNAME, &tag)) {
song.set_albumartistsort(QString::fromUtf8(tag));
g_free(tag);
tag = nullptr;
}
if (gst_tag_list_get_string(tags, GST_TAG_ARTIST, &tag)) {
song.set_artist(QString::fromUtf8(tag));
g_free(tag);
tag = nullptr;
++track_artist_tags;
}
if (gst_tag_list_get_string(tags, GST_TAG_ARTIST_SORTNAME, &tag)) {
song.set_artistsort(QString::fromUtf8(tag));
g_free(tag);
tag = nullptr;
}
if (gst_tag_list_get_string(tags, GST_TAG_ALBUM, &tag)) {
song.set_album(QString::fromUtf8(tag));
g_free(tag);
tag = nullptr;
++track_album_tags;
}
if (gst_tag_list_get_string(tags, GST_TAG_ALBUM_SORTNAME, &tag)) {
song.set_albumsort(QString::fromUtf8(tag));
g_free(tag);
tag = nullptr;
}
if (gst_tag_list_get_string(tags, GST_TAG_TITLE, &tag)) {
song.set_title(QString::fromUtf8(tag));
g_free(tag);
tag = nullptr;
++track_title_tags;
}
if (gst_tag_list_get_string(tags, GST_TAG_TITLE_SORTNAME, &tag)) {
song.set_titlesort(QString::fromUtf8(tag));
g_free(tag);
tag = nullptr;
}
if (gst_tag_list_get_string(tags, GST_TAG_GENRE, &tag)) {
song.set_genre(QString::fromUtf8(tag));
g_free(tag);
tag = nullptr;
}
if (gst_tag_list_get_string(tags, GST_TAG_COMPOSER, &tag)) {
song.set_composer(QString::fromUtf8(tag));
g_free(tag);
tag = nullptr;
}
if (gst_tag_list_get_string(tags, GST_TAG_COMPOSER_SORTNAME, &tag)) {
song.set_composersort(QString::fromUtf8(tag));
g_free(tag);
tag = nullptr;
}
if (gst_tag_list_get_string(tags, GST_TAG_PERFORMER, &tag)) {
song.set_performer(QString::fromUtf8(tag));
g_free(tag);
tag = nullptr;
}
if (gst_tag_list_get_string(tags, GST_TAG_COMMENT, &tag)) {
song.set_comment(QString::fromUtf8(tag));
g_free(tag);
tag = nullptr;
}
guint bitrate = 0;
if (gst_tag_list_get_uint(tags, GST_TAG_BITRATE, &bitrate)) {
song.set_bitrate(static_cast<int>(bitrate));
}
if (track_number >= total_tracks) {
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^GST_MESSAGE_TAG);
continue;
}
const gint64 next_track_index = track_index + 1;
if (!gst_element_seek_simple(pipeline, format_track, static_cast<GstSeekFlags>(GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_TRICKMODE), next_track_index)) {
qLog(Error) << "Failed to seek to next track index" << next_track_index;
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^GST_MESSAGE_TAG);
}
}
}
gst_element_set_state(pipeline, GST_STATE_NULL); gst_element_set_state(pipeline, GST_STATE_NULL);
// This will also cause cdda_ to be unref'd. // This will also cause cdda to be unref'd.
gst_object_unref(pipeline); gst_object_unref(pipeline);
if ((track_artist_tags >= total_tracks && track_album_tags >= total_tracks && track_title_tags >= total_tracks)) {
qLog(Info) << "Songs loaded from CD-Text";
Q_EMIT SongsUpdated(songs.values());
Q_EMIT LoadingFinished();
}
else {
#ifdef HAVE_MUSICBRAINZ
if (musicbrainz_discid.isEmpty()) {
qLog(Info) << "CD is missing tags";
Q_EMIT LoadingFinished();
}
else {
qLog(Info) << "MusicBrainz Disc ID:" << musicbrainz_discid;
Q_EMIT LoadTagsFromMusicBrainz(musicbrainz_discid);
}
#else
Q_EMIT LoadingFinished();
#endif // HAVE_MUSICBRAINZ
}
} }
#ifdef HAVE_MUSICBRAINZ #ifdef HAVE_MUSICBRAINZ
void CddaSongLoader::AudioCDTagsLoaded(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results) {
void CDDASongLoader::LoadTagsFromMusicBrainzSlot(const QString &musicbrainz_discid) const {
MusicBrainzClient *musicbrainz_client = new MusicBrainzClient(network_);
QObject::connect(musicbrainz_client, &MusicBrainzClient::DiscIdFinished, this, &CDDASongLoader::LoadTagsFromMusicBrainzFinished);
musicbrainz_client->StartDiscIdRequest(musicbrainz_discid);
}
void CDDASongLoader::LoadTagsFromMusicBrainzFinished(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results, const QString &error) {
MusicBrainzClient *musicbrainz_client = qobject_cast<MusicBrainzClient*>(sender()); MusicBrainzClient *musicbrainz_client = qobject_cast<MusicBrainzClient*>(sender());
musicbrainz_client->deleteLater(); musicbrainz_client->deleteLater();
if (results.empty()) return;
if (!error.isEmpty()) {
Error(error);
return;
}
if (results.empty()) {
Q_EMIT LoadingFinished();
return;
}
SongList songs; SongList songs;
songs.reserve(results.count()); songs.reserve(results.count());
int track_number = 1; int track_number = 0;
for (const MusicBrainzClient::Result &ret : results) { for (const MusicBrainzClient::Result &result : results) {
++track_number;
Song song(Song::Source::CDDA); Song song(Song::Source::CDDA);
song.set_artist(artist); song.set_artist(artist);
song.set_album(album); song.set_album(album);
song.set_title(ret.title_); song.set_title(result.title_);
song.set_length_nanosec(ret.duration_msec_ * kNsecPerMsec); song.set_length_nanosec(result.duration_msec_ * kNsecPerMsec);
song.set_track(track_number); song.set_track(track_number);
song.set_year(ret.year_); song.set_year(result.year_);
song.set_id(track_number); song.set_id(track_number);
song.set_filetype(Song::FileType::CDDA); song.set_filetype(Song::FileType::CDDA);
song.set_valid(true); song.set_valid(true);
// We need to set url: that's how playlist will find the correct item to update // We need to set URL, that's how playlist will find the correct item to update
song.set_url(GetUrlFromTrack(track_number++)); song.set_url(GetUrlFromTrack(track_number));
songs << song; songs << song;
} }
Q_EMIT SongsMetadataLoaded(songs);
} Q_EMIT SongsUpdated(songs);
#endif Q_EMIT LoadingFinished();
bool CddaSongLoader::HasChanged() {
if (cdio_ && cdio_get_media_changed(cdio_) != 1) {
return false;
}
// Check if mutex is already token (i.e. init is already taking place)
if (!mutex_load_.tryLock()) {
return false;
}
mutex_load_.unlock();
return true;
} }
void CddaSongLoader::Error(const QString &error) { #endif // HAVE_MUSICBRAINZ
void CDDASongLoader::Error(const QString &error) {
qLog(Error) << error; qLog(Error) << error;
Q_EMIT SongsDurationLoaded(SongList(), error);
Q_EMIT LoadError(error);
Q_EMIT LoadingFinished();
} }

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player * Strawberry Music Player
* This file was part of Clementine. * This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com> * Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -24,11 +24,6 @@
#include "config.h" #include "config.h"
#include <cstddef>
#include <cdio/types.h>
#include <cdio/cdio.h>
#include <gst/gstelement.h> #include <gst/gstelement.h>
#include <gst/audio/gstaudiocdsrc.h> #include <gst/audio/gstaudiocdsrc.h>
@@ -36,6 +31,7 @@
#include <QMutex> #include <QMutex>
#include <QString> #include <QString>
#include <QUrl> #include <QUrl>
#include <QFuture>
#include "includes/shared_ptr.h" #include "includes/shared_ptr.h"
#include "core/song.h" #include "core/song.h"
@@ -45,39 +41,40 @@
class NetworkAccessManager; class NetworkAccessManager;
// This class provides a (hopefully) nice, high level interface to get CD information and load tracks class CDDASongLoader : public QObject {
class CddaSongLoader : public QObject {
Q_OBJECT Q_OBJECT
public: public:
explicit CddaSongLoader(const QUrl &url, QObject *parent = nullptr); explicit CDDASongLoader(const QUrl &url, QObject *parent = nullptr);
~CddaSongLoader() override; ~CDDASongLoader() override;
// Load songs. Signals declared below will be emitted anytime new information will be available.
void LoadSongs(); void LoadSongs();
bool HasChanged();
bool IsActive() const { return loading_future_.isRunning(); }
private: private:
void LoadSongsFromCDDA();
void Error(const QString &error); void Error(const QString &error);
QUrl GetUrlFromTrack(const int track_number) const; QUrl GetUrlFromTrack(const int track_number) const;
Q_SIGNALS: Q_SIGNALS:
void SongsLoadError(const QString &error);
void SongsLoaded(const SongList &songs); void SongsLoaded(const SongList &songs);
void SongsDurationLoaded(const SongList &songs, const QString &error = QString()); void SongsUpdated(const SongList &songs);
void SongsMetadataLoaded(const SongList &songs); void LoadError(const QString &error);
void LoadingFinished();
void LoadTagsFromMusicBrainz(const QString &musicbrainz_discid);
private Q_SLOTS: private Q_SLOTS:
#ifdef HAVE_MUSICBRAINZ #ifdef HAVE_MUSICBRAINZ
void AudioCDTagsLoaded(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results); void LoadTagsFromMusicBrainzSlot(const QString &musicbrainz_discid) const;
void LoadTagsFromMusicBrainzFinished(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results, const QString &error);
#endif #endif
private: private:
const QUrl url_; const QUrl url_;
SharedPtr<NetworkAccessManager> network_; SharedPtr<NetworkAccessManager> network_;
GstElement *cdda_;
CdIo_t *cdio_;
QMutex mutex_load_; QMutex mutex_load_;
QFuture<void> loading_future_;
}; };
#endif // CDDASONGLOADER_H #endif // CDDASONGLOADER_H

View File

@@ -67,9 +67,6 @@ class ConnectedDevice : public QObject, public virtual MusicStorage, public enab
virtual bool IsLoading() { return false; } virtual bool IsLoading() { return false; }
virtual void NewConnection() {} virtual void NewConnection() {}
virtual void ConnectAsync(); virtual void ConnectAsync();
// For some devices (e.g. CD devices) we don't have callbacks to be notified when something change:
// we can call this method to refresh device's state
virtual void Refresh() {}
TranscodeMode GetTranscodeMode() const override; TranscodeMode GetTranscodeMode() const override;
Song::FileType GetTranscodeFormat() const override; Song::FileType GetTranscodeFormat() const override;

View File

@@ -41,7 +41,7 @@
using namespace Qt::Literals::StringLiterals; using namespace Qt::Literals::StringLiterals;
namespace { namespace {
constexpr int kDeviceSchemaVersion = 5; constexpr int kDeviceSchemaVersion = 6;
} }
DeviceDatabaseBackend::DeviceDatabaseBackend(QObject *parent) DeviceDatabaseBackend::DeviceDatabaseBackend(QObject *parent)

View File

@@ -36,61 +36,75 @@
using namespace Qt::Literals::StringLiterals; using namespace Qt::Literals::StringLiterals;
DeviceDatabaseBackend::Device DeviceInfo::SaveToDb() const { void DeviceInfo::InitFromDb(const DeviceDatabaseBackend::Device &device) {
DeviceDatabaseBackend::Device ret; database_id_ = device.id_;
ret.friendly_name_ = friendly_name_; friendly_name_ = device.friendly_name_;
ret.size_ = size_; size_ = device.size_;
ret.id_ = database_id_; transcode_mode_ = device.transcode_mode_;
ret.icon_name_ = icon_name_; transcode_format_ = device.transcode_format_;
ret.transcode_mode_ = transcode_mode_; icon_name_ = device.icon_name_;
ret.transcode_format_ = transcode_format_;
QStringList unique_ids; InitIcon();
unique_ids.reserve(backends_.count());
for (const Backend &backend : backends_) {
unique_ids << backend.unique_id_;
}
ret.unique_id_ = unique_ids.join(u',');
return ret; const QStringList unique_ids = device.unique_id_.split(u',');
}
void DeviceInfo::InitFromDb(const DeviceDatabaseBackend::Device &dev) {
database_id_ = dev.id_;
friendly_name_ = dev.friendly_name_;
size_ = dev.size_;
transcode_mode_ = dev.transcode_mode_;
transcode_format_ = dev.transcode_format_;
icon_name_ = dev.icon_name_;
const QStringList unique_ids = dev.unique_id_.split(u',');
for (const QString &id : unique_ids) { for (const QString &id : unique_ids) {
backends_ << Backend(nullptr, id); backends_ << Backend(nullptr, id);
} }
} }
DeviceDatabaseBackend::Device DeviceInfo::SaveToDb() const {
DeviceDatabaseBackend::Device device;
device.friendly_name_ = friendly_name_;
device.size_ = size_;
device.id_ = database_id_;
device.icon_name_ = icon_name_;
device.transcode_mode_ = transcode_mode_;
device.transcode_format_ = transcode_format_;
QStringList unique_ids;
unique_ids.reserve(backends_.count());
for (const Backend &backend : backends_) {
unique_ids << backend.unique_id_;
}
device.unique_id_ = unique_ids.join(u',');
return device;
}
const DeviceInfo::Backend *DeviceInfo::BestBackend() const { const DeviceInfo::Backend *DeviceInfo::BestBackend() const {
int best_priority = -1; int best_priority = -1;
const Backend *ret = nullptr; const Backend *backend = nullptr;
for (int i = 0; i < backends_.count(); ++i) { for (int i = 0; i < backends_.count(); ++i) {
if (backends_[i].lister_ && backends_[i].lister_->priority() > best_priority) { if (backends_[i].lister_ && backends_[i].lister_->priority() > best_priority) {
best_priority = backends_[i].lister_->priority(); best_priority = backends_[i].lister_->priority();
ret = &(backends_[i]); backend = &(backends_[i]);
} }
} }
if (!ret && !backends_.isEmpty()) return &(backends_[0]); if (!backend && !backends_.isEmpty()) return &(backends_[0]);
return ret; return backend;
} }
void DeviceInfo::SetIcon(const QVariantList &icons, const QString &name_hint) { void DeviceInfo::InitIcon() {
const QStringList icon_name_list = icon_name_.split(u',');
QVariantList icons;
icons.reserve(icon_name_list.count());
for (const QString &icon_name : icon_name_list) {
icons << icon_name;
}
LoadIcon(icons, friendly_name_);
}
void DeviceInfo::LoadIcon(const QVariantList &icons, const QString &name_hint) {
icon_name_ = "device"_L1; icon_name_ = "device"_L1;

View File

@@ -97,8 +97,9 @@ class DeviceInfo : public SimpleTreeItem<DeviceInfo> {
void InitFromDb(const DeviceDatabaseBackend::Device &dev); void InitFromDb(const DeviceDatabaseBackend::Device &dev);
DeviceDatabaseBackend::Device SaveToDb() const; DeviceDatabaseBackend::Device SaveToDb() const;
void InitIcon();
// Tries to load a good icon for the device. Sets icon_name_ and icon_. // Tries to load a good icon for the device. Sets icon_name_ and icon_.
void SetIcon(const QVariantList &icons, const QString &name_hint); void LoadIcon(const QVariantList &icons, const QString &name_hint);
// Gets the best backend available (the one with the highest priority) // Gets the best backend available (the one with the highest priority)
const Backend *BestBackend() const; const Backend *BestBackend() const;

View File

@@ -109,7 +109,7 @@ DeviceManager::DeviceManager(const SharedPtr<TaskManager> task_manager,
backend_->moveToThread(database->thread()); backend_->moveToThread(database->thread());
backend_->Init(database); backend_->Init(database);
QObject::connect(this, &DeviceManager::DeviceCreatedFromDB, this, &DeviceManager::AddDeviceFromDB); QObject::connect(this, &DeviceManager::DevicesLoaded, this, &DeviceManager::AddDevicesFromDB);
// This reads from the database and contents on the database mutex, which can be very slow on startup. // This reads from the database and contents on the database mutex, which can be very slow on startup.
(void)QtConcurrent::run(&thread_pool_, &DeviceManager::LoadAllDevices, this); (void)QtConcurrent::run(&thread_pool_, &DeviceManager::LoadAllDevices, this);
@@ -120,7 +120,7 @@ DeviceManager::DeviceManager(const SharedPtr<TaskManager> task_manager,
// CD devices are detected via the DiskArbitration framework instead on MacOs. // CD devices are detected via the DiskArbitration framework instead on MacOs.
#if defined(HAVE_AUDIOCD) && !defined(Q_OS_MACOS) #if defined(HAVE_AUDIOCD) && !defined(Q_OS_MACOS)
AddLister(new CddaLister); AddLister(new CDDALister);
#endif #endif
#ifdef HAVE_UDISKS2 #ifdef HAVE_UDISKS2
AddLister(new Udisks2Lister); AddLister(new Udisks2Lister);
@@ -133,7 +133,7 @@ DeviceManager::DeviceManager(const SharedPtr<TaskManager> task_manager,
#endif #endif
#ifdef HAVE_AUDIOCD #ifdef HAVE_AUDIOCD
AddDeviceClass<CddaDevice>(); AddDeviceClass<CDDADevice>();
#endif #endif
AddDeviceClass<FilesystemDevice>(); AddDeviceClass<FilesystemDevice>();
@@ -167,12 +167,12 @@ void DeviceManager::Exit() {
void DeviceManager::CloseDevices() { void DeviceManager::CloseDevices() {
for (DeviceInfo *info : std::as_const(devices_)) { for (DeviceInfo *device_info : std::as_const(devices_)) {
if (!info->device_) continue; if (!device_info->device_) continue;
if (wait_for_exit_.contains(&*info->device_)) continue; if (wait_for_exit_.contains(&*device_info->device_)) continue;
wait_for_exit_ << &*info->device_; wait_for_exit_ << &*device_info->device_;
QObject::connect(&*info->device_, &ConnectedDevice::destroyed, this, &DeviceManager::DeviceDestroyed); QObject::connect(&*device_info->device_, &ConnectedDevice::destroyed, this, &DeviceManager::DeviceDestroyed);
info->device_->Close(); device_info->device_->Close();
} }
if (wait_for_exit_.isEmpty()) CloseListers(); if (wait_for_exit_.isEmpty()) CloseListers();
@@ -224,10 +224,10 @@ void DeviceManager::ListerClosed() {
void DeviceManager::DeviceDestroyed() { void DeviceManager::DeviceDestroyed() {
ConnectedDevice *device = static_cast<ConnectedDevice*>(sender()); ConnectedDevice *connected_device = static_cast<ConnectedDevice*>(sender());
if (!wait_for_exit_.contains(device) || !backend_) return; if (!wait_for_exit_.contains(connected_device) || !backend_) return;
wait_for_exit_.removeAll(device); wait_for_exit_.removeAll(connected_device);
if (wait_for_exit_.isEmpty()) CloseListers(); if (wait_for_exit_.isEmpty()) CloseListers();
} }
@@ -237,41 +237,37 @@ void DeviceManager::LoadAllDevices() {
Q_ASSERT(QThread::currentThread() != qApp->thread()); Q_ASSERT(QThread::currentThread() != qApp->thread());
const DeviceDatabaseBackend::DeviceList devices = backend_->GetAllDevices(); const DeviceDatabaseBackend::DeviceList devices = backend_->GetAllDevices();
for (const DeviceDatabaseBackend::Device &device : devices) {
DeviceInfo *info = new DeviceInfo(DeviceInfo::Type::Device, root_); Q_EMIT DevicesLoaded(devices);
info->InitFromDb(device);
Q_EMIT DeviceCreatedFromDB(info);
}
// This is done in a concurrent thread so close the unique DB connection. // This is done in a concurrent thread so close the unique DB connection.
backend_->Close(); backend_->Close();
} }
void DeviceManager::AddDeviceFromDB(DeviceInfo *info) { void DeviceManager::AddDevicesFromDB(const DeviceDatabaseBackend::DeviceList &devices) {
const QStringList icon_names = info->icon_name_.split(u','); for (const DeviceDatabaseBackend::Device &device : devices) {
QVariantList icons; const QStringList unique_ids = device.unique_id_.split(u',');
icons.reserve(icon_names.count()); DeviceInfo *device_info = FindEquivalentDevice(unique_ids);
for (const QString &icon_name : icon_names) { if (device_info && device_info->database_id_ == -1) {
icons << icon_name; qLog(Info) << "Database device linked to physical device:" << device.friendly_name_;
} device_info->database_id_ = device.id_;
info->SetIcon(icons, info->friendly_name_); device_info->icon_name_ = device.icon_name_;
device_info->InitIcon();
DeviceInfo *existing = FindEquivalentDevice(info); const QModelIndex idx = ItemToIndex(device_info);
if (existing) { if (idx.isValid()) {
qLog(Info) << "Found existing device: " << info->friendly_name_; Q_EMIT dataChanged(idx, idx);
existing->icon_name_ = info->icon_name_; }
existing->icon_ = info->icon_; }
QModelIndex idx = ItemToIndex(existing); else {
if (idx.isValid()) Q_EMIT dataChanged(idx, idx); qLog(Info) << "Database device:" << device.friendly_name_;
root_->Delete(info->row); device_info = new DeviceInfo(DeviceInfo::Type::Device, root_);
} device_info->InitFromDb(device);
else { beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count()));
qLog(Info) << "Device added from database: " << info->friendly_name_; devices_ << device_info;
beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count())); endInsertRows();
devices_ << info; }
endInsertRows();
} }
} }
@@ -280,30 +276,29 @@ QVariant DeviceManager::data(const QModelIndex &idx, int role) const {
if (!idx.isValid() || idx.column() != 0) return QVariant(); if (!idx.isValid() || idx.column() != 0) return QVariant();
DeviceInfo *info = IndexToItem(idx); DeviceInfo *device_info = IndexToItem(idx);
if (!info) return QVariant(); if (!device_info) return QVariant();
switch (role) { switch (role) {
case Qt::DisplayRole:{ case Qt::DisplayRole:{
QString text; QString text;
if (!info->friendly_name_.isEmpty()) { if (!device_info->friendly_name_.isEmpty()) {
text = info->friendly_name_; text = device_info->friendly_name_;
} }
else if (info->BestBackend()) { else if (device_info->BestBackend()) {
text = info->BestBackend()->unique_id_; text = device_info->BestBackend()->unique_id_;
} }
if (info->size_ > 0) { if (device_info->size_ > 0) {
text = text + QStringLiteral(" (%1)").arg(Utilities::PrettySize(info->size_)); text = text + QStringLiteral(" (%1)").arg(Utilities::PrettySize(device_info->size_));
} }
if (info->device_) info->device_->Refresh();
return text; return text;
} }
case Qt::DecorationRole:{ case Qt::DecorationRole:{
QPixmap pixmap = info->icon_.pixmap(kDeviceIconSize); QPixmap pixmap = device_info->icon_.pixmap(kDeviceIconSize);
if (info->backends_.isEmpty() || !info->BestBackend() || !info->BestBackend()->lister_) { if (device_info->backends_.isEmpty() || !device_info->BestBackend() || !device_info->BestBackend()->lister_) {
// Disconnected but remembered // Disconnected but remembered
QPainter p(&pixmap); QPainter p(&pixmap);
p.drawPixmap(kDeviceIconSize - kDeviceIconOverlaySize, kDeviceIconSize - kDeviceIconOverlaySize, not_connected_overlay_.pixmap(kDeviceIconOverlaySize)); p.drawPixmap(kDeviceIconSize - kDeviceIconOverlaySize, kDeviceIconSize - kDeviceIconOverlaySize, not_connected_overlay_.pixmap(kDeviceIconOverlaySize));
@@ -313,62 +308,62 @@ QVariant DeviceManager::data(const QModelIndex &idx, int role) const {
} }
case Role_FriendlyName: case Role_FriendlyName:
return info->friendly_name_; return device_info->friendly_name_;
case Role_UniqueId: case Role_UniqueId:
if (!info->BestBackend()) return QString(); if (!device_info->BestBackend()) return QString();
return info->BestBackend()->unique_id_; return device_info->BestBackend()->unique_id_;
case Role_IconName: case Role_IconName:
return info->icon_name_; return device_info->icon_name_;
case Role_Capacity: case Role_Capacity:
case MusicStorage::Role_Capacity: case MusicStorage::Role_Capacity:
return info->size_; return device_info->size_;
case Role_FreeSpace: case Role_FreeSpace:
case MusicStorage::Role_FreeSpace: case MusicStorage::Role_FreeSpace:
return ((info->BestBackend() && info->BestBackend()->lister_) ? info->BestBackend()->lister_->DeviceFreeSpace(info->BestBackend()->unique_id_) : QVariant()); return ((device_info->BestBackend() && device_info->BestBackend()->lister_) ? device_info->BestBackend()->lister_->DeviceFreeSpace(device_info->BestBackend()->unique_id_) : QVariant());
case Role_State: case Role_State:
if (info->device_) return State_Connected; if (device_info->device_) return QVariant::fromValue(State::Connected);
if (info->BestBackend() && info->BestBackend()->lister_) { if (device_info->BestBackend() && device_info->BestBackend()->lister_) {
if (info->BestBackend()->lister_->DeviceNeedsMount(info->BestBackend()->unique_id_)) return State_NotMounted; if (device_info->BestBackend()->lister_->DeviceNeedsMount(device_info->BestBackend()->unique_id_)) return QVariant::fromValue(State::NotMounted);
return State_NotConnected; return QVariant::fromValue(State::NotConnected);
} }
return State_Remembered; return QVariant::fromValue(State::Remembered);
case Role_UpdatingPercentage: case Role_UpdatingPercentage:
if (info->task_percentage_ == -1) return QVariant(); if (device_info->task_percentage_ == -1) return QVariant();
return info->task_percentage_; return device_info->task_percentage_;
case MusicStorage::Role_Storage: case MusicStorage::Role_Storage:
if (!info->device_ && info->database_id_ != -1) { if (!device_info->device_ && device_info->database_id_ != -1) {
const_cast<DeviceManager*>(this)->Connect(info); const_cast<DeviceManager*>(this)->Connect(device_info);
} }
if (!info->device_) return QVariant(); if (!device_info->device_) return QVariant();
return QVariant::fromValue<SharedPtr<MusicStorage>>(info->device_); return QVariant::fromValue<SharedPtr<MusicStorage>>(device_info->device_);
case MusicStorage::Role_StorageForceConnect: case MusicStorage::Role_StorageForceConnect:
if (!info->BestBackend()) return QVariant(); if (!device_info->BestBackend()) return QVariant();
if (!info->device_) { if (!device_info->device_) {
if (info->database_id_ == -1 && !info->BestBackend()->lister_->DeviceNeedsMount(info->BestBackend()->unique_id_)) { if (device_info->database_id_ == -1 && !device_info->BestBackend()->lister_->DeviceNeedsMount(device_info->BestBackend()->unique_id_)) {
if (info->BestBackend()->lister_->AskForScan(info->BestBackend()->unique_id_)) { if (device_info->BestBackend()->lister_->AskForScan(device_info->BestBackend()->unique_id_)) {
ScopedPtr<QMessageBox> dialog(new QMessageBox(QMessageBox::Information, tr("Connect device"), tr("This is the first time you have connected this device. Strawberry will now scan the device to find music files - this may take some time."), QMessageBox::Cancel)); ScopedPtr<QMessageBox> dialog(new QMessageBox(QMessageBox::Information, tr("Connect device"), tr("This is the first time you have connected this device. Strawberry will now scan the device to find music files - this may take some time."), QMessageBox::Cancel));
QPushButton *pushbutton = dialog->addButton(tr("Connect device"), QMessageBox::AcceptRole); QPushButton *pushbutton = dialog->addButton(tr("Connect device"), QMessageBox::AcceptRole);
dialog->exec(); dialog->exec();
if (dialog->clickedButton() != pushbutton) return QVariant(); if (dialog->clickedButton() != pushbutton) return QVariant();
} }
} }
const_cast<DeviceManager*>(this)->Connect(info); const_cast<DeviceManager*>(this)->Connect(device_info);
} }
if (!info->device_) return QVariant(); if (!device_info->device_) return QVariant();
return QVariant::fromValue<SharedPtr<MusicStorage>>(info->device_); return QVariant::fromValue<SharedPtr<MusicStorage>>(device_info->device_);
case Role_MountPath:{ case Role_MountPath:{
if (!info->device_) return QVariant(); if (!device_info->device_) return QVariant();
QString ret = info->device_->url().path(); QString ret = device_info->device_->url().path();
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
if (ret.startsWith(u'/')) ret.remove(0, 1); if (ret.startsWith(u'/')) ret.remove(0, 1);
#endif #endif
@@ -376,17 +371,17 @@ QVariant DeviceManager::data(const QModelIndex &idx, int role) const {
} }
case Role_TranscodeMode: case Role_TranscodeMode:
return static_cast<int>(info->transcode_mode_); return static_cast<int>(device_info->transcode_mode_);
case Role_TranscodeFormat: case Role_TranscodeFormat:
return static_cast<int>(info->transcode_format_); return static_cast<int>(device_info->transcode_format_);
case Role_SongCount: case Role_SongCount:
if (!info->device_) return QVariant(); if (!device_info->device_) return QVariant();
return info->device_->song_count(); return device_info->device_->song_count();
case Role_CopyMusic: case Role_CopyMusic:
if (info->BestBackend() && info->BestBackend()->lister_) return info->BestBackend()->lister_->CopyMusic(); if (device_info->BestBackend() && device_info->BestBackend()->lister_) return device_info->BestBackend()->lister_->CopyMusic();
else return false; else return false;
default: default:
@@ -410,7 +405,9 @@ DeviceInfo *DeviceManager::FindDeviceById(const QString &id) const {
for (int i = 0; i < devices_.count(); ++i) { for (int i = 0; i < devices_.count(); ++i) {
for (const DeviceInfo::Backend &backend : std::as_const(devices_[i]->backends_)) { for (const DeviceInfo::Backend &backend : std::as_const(devices_[i]->backends_)) {
if (backend.unique_id_ == id) return devices_[i]; if (backend.unique_id_ == id) {
return devices_[i];
}
} }
} }
@@ -425,10 +422,11 @@ DeviceInfo *DeviceManager::FindDeviceByUrl(const QList<QUrl> &urls) const {
for (int i = 0; i < devices_.count(); ++i) { for (int i = 0; i < devices_.count(); ++i) {
for (const DeviceInfo::Backend &backend : std::as_const(devices_[i]->backends_)) { for (const DeviceInfo::Backend &backend : std::as_const(devices_[i]->backends_)) {
if (!backend.lister_) continue; if (!backend.lister_) continue;
const QList<QUrl> device_urls = backend.lister_->MakeDeviceUrls(backend.unique_id_); const QList<QUrl> device_urls = backend.lister_->MakeDeviceUrls(backend.unique_id_);
for (const QUrl &url : device_urls) { for (const QUrl &url : device_urls) {
if (urls.contains(url)) return devices_[i]; if (urls.contains(url)) {
return devices_[i];
}
} }
} }
} }
@@ -437,12 +435,15 @@ DeviceInfo *DeviceManager::FindDeviceByUrl(const QList<QUrl> &urls) const {
} }
DeviceInfo *DeviceManager::FindEquivalentDevice(DeviceInfo *info) const { DeviceInfo *DeviceManager::FindEquivalentDevice(const QStringList &unique_ids) const {
for (const DeviceInfo::Backend &backend : std::as_const(info->backends_)) { for (const QString &unique_id : unique_ids) {
DeviceInfo *match = FindDeviceById(backend.unique_id_); DeviceInfo *device_info_match = FindDeviceById(unique_id);
if (match) return match; if (device_info_match) {
return device_info_match;
}
} }
return nullptr; return nullptr;
} }
@@ -455,42 +456,42 @@ void DeviceManager::PhysicalDeviceAdded(const QString &id) {
qLog(Info) << "Device added:" << id << lister->DeviceUniqueIDs(); qLog(Info) << "Device added:" << id << lister->DeviceUniqueIDs();
// Do we have this device already? // Do we have this device already?
DeviceInfo *info = FindDeviceById(id); DeviceInfo *device_info = FindDeviceById(id);
if (info) { if (device_info) {
for (int backend_index = 0; backend_index < info->backends_.count(); ++backend_index) { for (int backend_index = 0; backend_index < device_info->backends_.count(); ++backend_index) {
if (info->backends_[backend_index].unique_id_ == id) { if (device_info->backends_[backend_index].unique_id_ == id) {
info->backends_[backend_index].lister_ = lister; device_info->backends_[backend_index].lister_ = lister;
break; break;
} }
} }
QModelIndex idx = ItemToIndex(info); QModelIndex idx = ItemToIndex(device_info);
if (idx.isValid()) Q_EMIT dataChanged(idx, idx); if (idx.isValid()) Q_EMIT dataChanged(idx, idx);
} }
else { else {
// Check if we have another device with the same URL // Check if we have another device with the same URL
info = FindDeviceByUrl(lister->MakeDeviceUrls(id)); device_info = FindDeviceByUrl(lister->MakeDeviceUrls(id));
if (info) { if (device_info) {
// Add this device's lister to the existing device // Add this device's lister to the existing device
info->backends_ << DeviceInfo::Backend(lister, id); device_info->backends_ << DeviceInfo::Backend(lister, id);
// If the user hasn't saved the device in the DB yet then overwrite the device's name and icon etc. // If the user hasn't saved the device in the DB yet then overwrite the device's name and icon etc.
if (info->database_id_ == -1 && info->BestBackend() && info->BestBackend()->lister_ == lister) { if (device_info->database_id_ == -1 && device_info->BestBackend() && device_info->BestBackend()->lister_ == lister) {
info->friendly_name_ = lister->MakeFriendlyName(id); device_info->friendly_name_ = lister->MakeFriendlyName(id);
info->size_ = lister->DeviceCapacity(id); device_info->size_ = lister->DeviceCapacity(id);
info->SetIcon(lister->DeviceIcons(id), info->friendly_name_); device_info->LoadIcon(lister->DeviceIcons(id), device_info->friendly_name_);
} }
QModelIndex idx = ItemToIndex(info); QModelIndex idx = ItemToIndex(device_info);
if (idx.isValid()) Q_EMIT dataChanged(idx, idx); if (idx.isValid()) Q_EMIT dataChanged(idx, idx);
} }
else { else {
// It's a completely new device // It's a completely new device
info = new DeviceInfo(DeviceInfo::Type::Device, root_); device_info = new DeviceInfo(DeviceInfo::Type::Device, root_);
info->backends_ << DeviceInfo::Backend(lister, id); device_info->backends_ << DeviceInfo::Backend(lister, id);
info->friendly_name_ = lister->MakeFriendlyName(id); device_info->friendly_name_ = lister->MakeFriendlyName(id);
info->size_ = lister->DeviceCapacity(id); device_info->size_ = lister->DeviceCapacity(id);
info->SetIcon(lister->DeviceIcons(id), info->friendly_name_); device_info->LoadIcon(lister->DeviceIcons(id), device_info->friendly_name_);
beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count())); beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count()));
devices_ << info; devices_ << device_info;
endInsertRows(); endInsertRows();
} }
} }
@@ -503,42 +504,42 @@ void DeviceManager::PhysicalDeviceRemoved(const QString &id) {
qLog(Info) << "Device removed:" << id; qLog(Info) << "Device removed:" << id;
DeviceInfo *info = FindDeviceById(id); DeviceInfo *device_info = FindDeviceById(id);
if (!info) return; if (!device_info) return;
QModelIndex idx = ItemToIndex(info); const QModelIndex idx = ItemToIndex(device_info);
if (!idx.isValid()) return; if (!idx.isValid()) return;
if (info->database_id_ != -1) { if (device_info->database_id_ != -1) {
// Keep the structure around, but just "disconnect" it // Keep the structure around, but just "disconnect" it
for (int backend_index = 0; backend_index < info->backends_.count(); ++backend_index) { for (int backend_index = 0; backend_index < device_info->backends_.count(); ++backend_index) {
if (info->backends_[backend_index].unique_id_ == id) { if (device_info->backends_[backend_index].unique_id_ == id) {
info->backends_[backend_index].lister_ = nullptr; device_info->backends_[backend_index].lister_ = nullptr;
break; break;
} }
} }
if (info->device_ && info->device_->lister() == lister) { if (device_info->device_ && device_info->device_->lister() == lister) {
info->device_->Close(); device_info->device_->Close();
} }
if (!info->device_) Q_EMIT DeviceDisconnected(idx); if (!device_info->device_) Q_EMIT DeviceDisconnected(idx);
Q_EMIT dataChanged(idx, idx); Q_EMIT dataChanged(idx, idx);
} }
else { else {
// If this was the last lister for the device then remove it from the model // If this was the last lister for the device then remove it from the model
for (int backend_index = 0; backend_index < info->backends_.count(); ++backend_index) { for (int backend_index = 0; backend_index < device_info->backends_.count(); ++backend_index) {
if (info->backends_[backend_index].unique_id_ == id) { if (device_info->backends_[backend_index].unique_id_ == id) {
info->backends_.removeAt(backend_index); device_info->backends_.removeAt(backend_index);
break; break;
} }
} }
if (info->backends_.isEmpty()) { if (device_info->backends_.isEmpty()) {
beginRemoveRows(ItemToIndex(root_), idx.row(), idx.row()); beginRemoveRows(ItemToIndex(root_), idx.row(), idx.row());
devices_.removeAll(info); devices_.removeAll(device_info);
root_->Delete(info->row); root_->Delete(device_info->row);
endRemoveRows(); endRemoveRows();
} }
} }
@@ -550,8 +551,8 @@ void DeviceManager::PhysicalDeviceChanged(const QString &id) {
DeviceLister *lister = qobject_cast<DeviceLister*>(sender()); DeviceLister *lister = qobject_cast<DeviceLister*>(sender());
Q_UNUSED(lister); Q_UNUSED(lister);
DeviceInfo *info = FindDeviceById(id); DeviceInfo *device_info = FindDeviceById(id);
if (!info) return; if (!device_info) return;
// TODO // TODO
@@ -561,40 +562,41 @@ SharedPtr<ConnectedDevice> DeviceManager::Connect(const QModelIndex &idx) {
SharedPtr<ConnectedDevice> ret; SharedPtr<ConnectedDevice> ret;
DeviceInfo *info = IndexToItem(idx); DeviceInfo *device_info = IndexToItem(idx);
if (!info) return ret; if (!device_info) return ret;
return Connect(info); return Connect(device_info);
} }
SharedPtr<ConnectedDevice> DeviceManager::Connect(DeviceInfo *info) { SharedPtr<ConnectedDevice> DeviceManager::Connect(DeviceInfo *device_info) {
SharedPtr<ConnectedDevice> ret; if (!device_info) {
return SharedPtr<ConnectedDevice>();
if (!info) return ret;
if (info->device_) { // Already connected
return info->device_;
} }
if (!info->BestBackend() || !info->BestBackend()->lister_) { // Not physically connected if (device_info->device_) { // Already connected
return ret; return device_info->device_;
} }
if (info->BestBackend()->lister_->DeviceNeedsMount(info->BestBackend()->unique_id_)) { // Mount the device if (!device_info->BestBackend() || !device_info->BestBackend()->lister_) { // Not physically connected
info->BestBackend()->lister_->MountDeviceAsync(info->BestBackend()->unique_id_); return SharedPtr<ConnectedDevice>();
return ret;
} }
bool first_time = (info->database_id_ == -1); if (device_info->BestBackend()->lister_->DeviceNeedsMount(device_info->BestBackend()->unique_id_)) { // Mount the device
device_info->BestBackend()->lister_->MountDeviceAsync(device_info->BestBackend()->unique_id_);
return SharedPtr<ConnectedDevice>();
}
const bool first_time = device_info->database_id_ == -1;
if (first_time) { if (first_time) {
// We haven't stored this device in the database before // We haven't stored this device in the database before
info->database_id_ = backend_->AddDevice(info->SaveToDb()); device_info->database_id_ = backend_->AddDevice(device_info->SaveToDb());
} }
// Get the device URLs // Get the device URLs
const QList<QUrl> urls = info->BestBackend()->lister_->MakeDeviceUrls(info->BestBackend()->unique_id_); const QList<QUrl> urls = device_info->BestBackend()->lister_->MakeDeviceUrls(device_info->BestBackend()->unique_id_);
if (urls.isEmpty()) return ret; if (urls.isEmpty()) return SharedPtr<ConnectedDevice>();
// Take the first URL that we have a handler for // Take the first URL that we have a handler for
QUrl device_url; QUrl device_url;
@@ -614,7 +616,7 @@ SharedPtr<ConnectedDevice> DeviceManager::Connect(DeviceInfo *info) {
tr("This is an MTP device, but you compiled Strawberry without libmtp support.") + u" "_s + tr("This is an MTP device, but you compiled Strawberry without libmtp support.") + u" "_s +
tr("If you continue, this device will work slowly and songs copied to it may not work."), tr("If you continue, this device will work slowly and songs copied to it may not work."),
QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort) QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort)
return ret; return SharedPtr<ConnectedDevice>();
} }
if (url.scheme() == "ipod"_L1) { if (url.scheme() == "ipod"_L1) {
@@ -622,7 +624,7 @@ SharedPtr<ConnectedDevice> DeviceManager::Connect(DeviceInfo *info) {
tr("This is an iPod, but you compiled Strawberry without libgpod support.") + " "_L1 + tr("This is an iPod, but you compiled Strawberry without libgpod support.") + " "_L1 +
tr("If you continue, this device will work slowly and songs copied to it may not work."), tr("If you continue, this device will work slowly and songs copied to it may not work."),
QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort) QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort)
return ret; return SharedPtr<ConnectedDevice>();
} }
} }
@@ -635,114 +637,114 @@ SharedPtr<ConnectedDevice> DeviceManager::Connect(DeviceInfo *info) {
} }
Q_EMIT DeviceError(tr("This type of device is not supported: %1").arg(url_strings.join(", "_L1))); Q_EMIT DeviceError(tr("This type of device is not supported: %1").arg(url_strings.join(", "_L1)));
return ret; return SharedPtr<ConnectedDevice>();
} }
QMetaObject meta_object = device_classes_.value(device_url.scheme()); QMetaObject meta_object = device_classes_.value(device_url.scheme());
QObject *instance = meta_object.newInstance( QObject *instance = meta_object.newInstance(
Q_ARG(QUrl, device_url), Q_ARG(QUrl, device_url),
Q_ARG(DeviceLister*, info->BestBackend()->lister_), Q_ARG(DeviceLister*, device_info->BestBackend()->lister_),
Q_ARG(QString, info->BestBackend()->unique_id_), Q_ARG(QString, device_info->BestBackend()->unique_id_),
Q_ARG(DeviceManager*, this), Q_ARG(DeviceManager*, this),
Q_ARG(SharedPtr<TaskManager>, task_manager_), Q_ARG(SharedPtr<TaskManager>, task_manager_),
Q_ARG(SharedPtr<Database>, database_), Q_ARG(SharedPtr<Database>, database_),
Q_ARG(SharedPtr<TagReaderClient>, tagreader_client_), Q_ARG(SharedPtr<TagReaderClient>, tagreader_client_),
Q_ARG(SharedPtr<AlbumCoverLoader>, albumcover_loader_), Q_ARG(SharedPtr<AlbumCoverLoader>, albumcover_loader_),
Q_ARG(int, info->database_id_), Q_ARG(int, device_info->database_id_),
Q_ARG(bool, first_time)); Q_ARG(bool, first_time));
ret.reset(qobject_cast<ConnectedDevice*>(instance)); SharedPtr<ConnectedDevice> connected_device = SharedPtr<ConnectedDevice>(qobject_cast<ConnectedDevice*>(instance));
if (!ret) { if (!connected_device) {
qLog(Warning) << "Could not create device for" << device_url.toString(); qLog(Warning) << "Could not create device for" << device_url.toString();
return ret; return connected_device;
} }
bool result = ret->Init(); bool result = connected_device->Init();
if (!result) { if (!result) {
qLog(Warning) << "Could not connect to device" << device_url.toString(); qLog(Warning) << "Could not connect to device" << device_url.toString();
return ret; return connected_device;
} }
info->device_ = ret; device_info->device_ = connected_device;
QModelIndex idx = ItemToIndex(info); QModelIndex idx = ItemToIndex(device_info);
if (!idx.isValid()) return ret; if (!idx.isValid()) return connected_device;
Q_EMIT dataChanged(idx, idx); Q_EMIT dataChanged(idx, idx);
QObject::connect(&*info->device_, &ConnectedDevice::TaskStarted, this, &DeviceManager::DeviceTaskStarted); QObject::connect(&*device_info->device_, &ConnectedDevice::TaskStarted, this, &DeviceManager::DeviceTaskStarted);
QObject::connect(&*info->device_, &ConnectedDevice::SongCountUpdated, this, &DeviceManager::DeviceSongCountUpdated); QObject::connect(&*device_info->device_, &ConnectedDevice::SongCountUpdated, this, &DeviceManager::DeviceSongCountUpdated);
QObject::connect(&*info->device_, &ConnectedDevice::DeviceConnectFinished, this, &DeviceManager::DeviceConnectFinished); QObject::connect(&*device_info->device_, &ConnectedDevice::DeviceConnectFinished, this, &DeviceManager::DeviceConnectFinished);
QObject::connect(&*info->device_, &ConnectedDevice::DeviceCloseFinished, this, &DeviceManager::DeviceCloseFinished); QObject::connect(&*device_info->device_, &ConnectedDevice::DeviceCloseFinished, this, &DeviceManager::DeviceCloseFinished);
QObject::connect(&*info->device_, &ConnectedDevice::Error, this, &DeviceManager::DeviceError); QObject::connect(&*device_info->device_, &ConnectedDevice::Error, this, &DeviceManager::DeviceError);
ret->ConnectAsync(); connected_device->ConnectAsync();
return ret; return connected_device;
} }
void DeviceManager::DeviceConnectFinished(const QString &id, const bool success) { void DeviceManager::DeviceConnectFinished(const QString &id, const bool success) {
DeviceInfo *info = FindDeviceById(id); DeviceInfo *device_info = FindDeviceById(id);
if (!info) return; if (!device_info) return;
QModelIndex idx = ItemToIndex(info); QModelIndex idx = ItemToIndex(device_info);
if (!idx.isValid()) return; if (!idx.isValid()) return;
if (success) { if (success) {
Q_EMIT DeviceConnected(idx); Q_EMIT DeviceConnected(idx);
} }
else { else {
info->device_->Close(); device_info->device_->Close();
} }
} }
void DeviceManager::DeviceCloseFinished(const QString &id) { void DeviceManager::DeviceCloseFinished(const QString &id) {
DeviceInfo *info = FindDeviceById(id); DeviceInfo *device_info = FindDeviceById(id);
if (!info) return; if (!device_info) return;
info->device_.reset(); device_info->device_.reset();
QModelIndex idx = ItemToIndex(info); QModelIndex idx = ItemToIndex(device_info);
if (!idx.isValid()) return; if (!idx.isValid()) return;
Q_EMIT DeviceDisconnected(idx); Q_EMIT DeviceDisconnected(idx);
Q_EMIT dataChanged(idx, idx); Q_EMIT dataChanged(idx, idx);
if (info->unmount_ && info->BestBackend() && info->BestBackend()->lister_) { if (device_info->unmount_ && device_info->BestBackend() && device_info->BestBackend()->lister_) {
info->BestBackend()->lister_->UnmountDeviceAsync(info->BestBackend()->unique_id_); device_info->BestBackend()->lister_->UnmountDeviceAsync(device_info->BestBackend()->unique_id_);
} }
if (info->forget_) { if (device_info->forget_) {
RemoveFromDB(info, idx); RemoveFromDB(device_info, idx);
} }
} }
DeviceInfo *DeviceManager::GetDevice(const QModelIndex &idx) const { DeviceInfo *DeviceManager::GetDevice(const QModelIndex &idx) const {
DeviceInfo *info = IndexToItem(idx); DeviceInfo *device_info = IndexToItem(idx);
return info; return device_info;
} }
SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(const QModelIndex &idx) const { SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(const QModelIndex &idx) const {
SharedPtr<ConnectedDevice> ret; SharedPtr<ConnectedDevice> connected_device;
DeviceInfo *info = IndexToItem(idx); DeviceInfo *device_info = IndexToItem(idx);
if (!info) return ret; if (!device_info) return connected_device;
return info->device_; return device_info->device_;
} }
SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(DeviceInfo *info) const { SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(DeviceInfo *device_info) const {
SharedPtr<ConnectedDevice> ret; SharedPtr<ConnectedDevice> connected_device;
if (!info) return ret; if (!device_info) return connected_device;
return info->device_; return device_info->device_;
} }
@@ -750,9 +752,9 @@ int DeviceManager::GetDatabaseId(const QModelIndex &idx) const {
if (!idx.isValid()) return -1; if (!idx.isValid()) return -1;
DeviceInfo *info = IndexToItem(idx); DeviceInfo *device_info = IndexToItem(idx);
if (!info) return -1; if (!device_info) return -1;
return info->database_id_; return device_info->database_id_;
} }
@@ -760,17 +762,17 @@ DeviceLister *DeviceManager::GetLister(const QModelIndex &idx) const {
if (!idx.isValid()) return nullptr; if (!idx.isValid()) return nullptr;
DeviceInfo *info = IndexToItem(idx); DeviceInfo *device_info = IndexToItem(idx);
if (!info || !info->BestBackend()) return nullptr; if (!device_info || !device_info->BestBackend()) return nullptr;
return info->BestBackend()->lister_; return device_info->BestBackend()->lister_;
} }
void DeviceManager::Disconnect(DeviceInfo *info, const QModelIndex &idx) { void DeviceManager::Disconnect(DeviceInfo *device_info, const QModelIndex &idx) {
Q_UNUSED(idx); Q_UNUSED(idx);
info->device_->Close(); device_info->device_->Close();
} }
@@ -778,37 +780,37 @@ void DeviceManager::Forget(const QModelIndex &idx) {
if (!idx.isValid()) return; if (!idx.isValid()) return;
DeviceInfo *info = IndexToItem(idx); DeviceInfo *device_info = IndexToItem(idx);
if (!info) return; if (!device_info) return;
if (info->database_id_ == -1) return; if (device_info->database_id_ == -1) return;
if (info->device_) { if (device_info->device_) {
info->forget_ = true; device_info->forget_ = true;
Disconnect(info, idx); Disconnect(device_info, idx);
} }
else { else {
RemoveFromDB(info, idx); RemoveFromDB(device_info, idx);
} }
} }
void DeviceManager::RemoveFromDB(DeviceInfo *info, const QModelIndex &idx) { void DeviceManager::RemoveFromDB(DeviceInfo *device_info, const QModelIndex &idx) {
backend_->RemoveDevice(info->database_id_); backend_->RemoveDevice(device_info->database_id_);
info->database_id_ = -1; device_info->database_id_ = -1;
if (!info->BestBackend() || !info->BestBackend()->lister_) { // It's not attached any more so remove it from the list if (!device_info->BestBackend() || !device_info->BestBackend()->lister_) { // It's not attached any more so remove it from the list
beginRemoveRows(ItemToIndex(root_), idx.row(), idx.row()); beginRemoveRows(ItemToIndex(root_), idx.row(), idx.row());
devices_.removeAll(info); devices_.removeAll(device_info);
root_->Delete(info->row); root_->Delete(device_info->row);
endRemoveRows(); endRemoveRows();
} }
else { // It's still attached, set the name and icon back to what they were originally else { // It's still attached, set the name and icon back to what they were originally
const QString id = info->BestBackend()->unique_id_; const QString id = device_info->BestBackend()->unique_id_;
info->friendly_name_ = info->BestBackend()->lister_->MakeFriendlyName(id); device_info->friendly_name_ = device_info->BestBackend()->lister_->MakeFriendlyName(id);
info->SetIcon(info->BestBackend()->lister_->DeviceIcons(id), info->friendly_name_); device_info->LoadIcon(device_info->BestBackend()->lister_->DeviceIcons(id), device_info->friendly_name_);
Q_EMIT dataChanged(idx, idx); Q_EMIT dataChanged(idx, idx);
} }
@@ -818,18 +820,18 @@ void DeviceManager::SetDeviceOptions(const QModelIndex &idx, const QString &frie
if (!idx.isValid()) return; if (!idx.isValid()) return;
DeviceInfo *info = IndexToItem(idx); DeviceInfo *device_info = IndexToItem(idx);
if (!info) return; if (!device_info) return;
info->friendly_name_ = friendly_name; device_info->friendly_name_ = friendly_name;
info->SetIcon(QVariantList() << icon_name, friendly_name); device_info->LoadIcon(QVariantList() << icon_name, friendly_name);
info->transcode_mode_ = mode; device_info->transcode_mode_ = mode;
info->transcode_format_ = format; device_info->transcode_format_ = format;
Q_EMIT dataChanged(idx, idx); Q_EMIT dataChanged(idx, idx);
if (info->database_id_ != -1) { if (device_info->database_id_ != -1) {
backend_->SetDeviceOptions(info->database_id_, friendly_name, icon_name, mode, format); backend_->SetDeviceOptions(device_info->database_id_, friendly_name, icon_name, mode, format);
} }
} }
@@ -840,12 +842,12 @@ void DeviceManager::DeviceTaskStarted(const int id) {
if (!device) return; if (!device) return;
for (int i = 0; i < devices_.count(); ++i) { for (int i = 0; i < devices_.count(); ++i) {
DeviceInfo *info = devices_.value(i); DeviceInfo *device_info = devices_.value(i);
if (info->device_ && &*info->device_ == device) { if (device_info->device_ && &*device_info->device_ == device) {
QModelIndex index = ItemToIndex(info); QModelIndex index = ItemToIndex(device_info);
if (!index.isValid()) continue; if (!index.isValid()) continue;
active_tasks_[id] = index; active_tasks_[id] = index;
info->task_percentage_ = 0; device_info->task_percentage_ = 0;
Q_EMIT dataChanged(index, index); Q_EMIT dataChanged(index, index);
return; return;
} }
@@ -864,12 +866,12 @@ void DeviceManager::TasksChanged() {
const QPersistentModelIndex idx = active_tasks_.value(task.id); const QPersistentModelIndex idx = active_tasks_.value(task.id);
if (!idx.isValid()) continue; if (!idx.isValid()) continue;
DeviceInfo *info = IndexToItem(idx); DeviceInfo *device_info = IndexToItem(idx);
if (task.progress_max) { if (task.progress_max) {
info->task_percentage_ = static_cast<int>(static_cast<float>(task.progress) / static_cast<float>(task.progress_max) * 100); device_info->task_percentage_ = static_cast<int>(static_cast<float>(task.progress) / static_cast<float>(task.progress_max) * 100);
} }
else { else {
info->task_percentage_ = 0; device_info->task_percentage_ = 0;
} }
Q_EMIT dataChanged(idx, idx); Q_EMIT dataChanged(idx, idx);
@@ -881,10 +883,10 @@ void DeviceManager::TasksChanged() {
if (!idx.isValid()) continue; if (!idx.isValid()) continue;
DeviceInfo *info = IndexToItem(idx); DeviceInfo *device_info = IndexToItem(idx);
if (!info) continue; if (!device_info) continue;
info->task_percentage_ = -1; device_info->task_percentage_ = -1;
Q_EMIT dataChanged(idx, idx); Q_EMIT dataChanged(idx, idx);
active_tasks_.remove(active_tasks_.key(idx)); active_tasks_.remove(active_tasks_.key(idx));
@@ -900,17 +902,17 @@ void DeviceManager::Unmount(const QModelIndex &idx) {
if (!idx.isValid()) return; if (!idx.isValid()) return;
DeviceInfo *info = IndexToItem(idx); DeviceInfo *device_info = IndexToItem(idx);
if (!info) return; if (!device_info) return;
if (info->database_id_ != -1 && !info->device_) return; if (device_info->database_id_ != -1 && !device_info->device_) return;
if (info->device_) { if (device_info->device_) {
info->unmount_ = true; device_info->unmount_ = true;
Disconnect(info, idx); Disconnect(device_info, idx);
} }
else if (info->BestBackend() && info->BestBackend()->lister_) { else if (device_info->BestBackend() && device_info->BestBackend()->lister_) {
info->BestBackend()->lister_->UnmountDeviceAsync(info->BestBackend()->unique_id_); device_info->BestBackend()->lister_->UnmountDeviceAsync(device_info->BestBackend()->unique_id_);
} }
} }
@@ -919,13 +921,13 @@ void DeviceManager::DeviceSongCountUpdated(const int count) {
Q_UNUSED(count); Q_UNUSED(count);
ConnectedDevice *device = qobject_cast<ConnectedDevice*>(sender()); ConnectedDevice *connected_device = qobject_cast<ConnectedDevice*>(sender());
if (!device) return; if (!connected_device) return;
DeviceInfo *info = FindDeviceById(device->unique_id()); DeviceInfo *device_info = FindDeviceById(connected_device->unique_id());
if (!info) return; if (!device_info) return;
QModelIndex idx = ItemToIndex(info); QModelIndex idx = ItemToIndex(device_info);
if (!idx.isValid()) return; if (!idx.isValid()) return;
Q_EMIT dataChanged(idx, idx); Q_EMIT dataChanged(idx, idx);
@@ -934,10 +936,10 @@ void DeviceManager::DeviceSongCountUpdated(const int count) {
QString DeviceManager::DeviceNameByID(const QString &unique_id) { QString DeviceManager::DeviceNameByID(const QString &unique_id) {
DeviceInfo *info = FindDeviceById(unique_id); DeviceInfo *device_info = FindDeviceById(unique_id);
if (!info) return QString(); if (!device_info) return QString();
QModelIndex idx = ItemToIndex(info); QModelIndex idx = ItemToIndex(device_info);
if (!idx.isValid()) return QString(); if (!idx.isValid()) return QString();
return data(idx, DeviceManager::Role_FriendlyName).toString(); return data(idx, DeviceManager::Role_FriendlyName).toString();

View File

@@ -85,11 +85,11 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
LastRole, LastRole,
}; };
enum State { enum class State {
State_Remembered, Remembered,
State_NotMounted, NotMounted,
State_NotConnected, NotConnected,
State_Connected, Connected,
}; };
static const int kDeviceIconSize; static const int kDeviceIconSize;
@@ -104,17 +104,17 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
DeviceLister *GetLister(const QModelIndex &idx) const; DeviceLister *GetLister(const QModelIndex &idx) const;
DeviceInfo *GetDevice(const QModelIndex &idx) const; DeviceInfo *GetDevice(const QModelIndex &idx) const;
SharedPtr<ConnectedDevice> GetConnectedDevice(const QModelIndex &idx) const; SharedPtr<ConnectedDevice> GetConnectedDevice(const QModelIndex &idx) const;
SharedPtr<ConnectedDevice> GetConnectedDevice(DeviceInfo *info) const; SharedPtr<ConnectedDevice> GetConnectedDevice(DeviceInfo *device_info) const;
DeviceInfo *FindDeviceById(const QString &id) const; DeviceInfo *FindDeviceById(const QString &id) const;
DeviceInfo *FindDeviceByUrl(const QList<QUrl> &url) const; DeviceInfo *FindDeviceByUrl(const QList<QUrl> &url) const;
QString DeviceNameByID(const QString &unique_id); QString DeviceNameByID(const QString &unique_id);
DeviceInfo *FindEquivalentDevice(DeviceInfo *info) const; DeviceInfo *FindEquivalentDevice(const QStringList &unique_ids) const;
// Actions on devices // Actions on devices
SharedPtr<ConnectedDevice> Connect(DeviceInfo *info); SharedPtr<ConnectedDevice> Connect(DeviceInfo *device_info);
SharedPtr<ConnectedDevice> Connect(const QModelIndex &idx); SharedPtr<ConnectedDevice> Connect(const QModelIndex &idx);
void Disconnect(DeviceInfo *info, const QModelIndex &idx); void Disconnect(DeviceInfo *device_info, const QModelIndex &idx);
void Forget(const QModelIndex &idx); void Forget(const QModelIndex &idx);
void UnmountAsync(const QModelIndex &idx); void UnmountAsync(const QModelIndex &idx);
@@ -128,9 +128,10 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
Q_SIGNALS: Q_SIGNALS:
void ExitFinished(); void ExitFinished();
void DevicesLoaded(const DeviceDatabaseBackend::DeviceList &devices);
void DeviceConnected(const QModelIndex idx); void DeviceConnected(const QModelIndex idx);
void DeviceDisconnected(const QModelIndex idx); void DeviceDisconnected(const QModelIndex idx);
void DeviceCreatedFromDB(DeviceInfo *info); void DeviceCreatedFromDB(DeviceInfo *device_info);
void DeviceError(const QString &error); void DeviceError(const QString &error);
private Q_SLOTS: private Q_SLOTS:
@@ -143,7 +144,7 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
void LoadAllDevices(); void LoadAllDevices();
void DeviceConnectFinished(const QString &id, bool success); void DeviceConnectFinished(const QString &id, bool success);
void DeviceCloseFinished(const QString &id); void DeviceCloseFinished(const QString &id);
void AddDeviceFromDB(DeviceInfo *info); void AddDevicesFromDB(const DeviceDatabaseBackend::DeviceList &devices);
void BackendClosed(); void BackendClosed();
void ListerClosed(); void ListerClosed();
void DeviceDestroyed(); void DeviceDestroyed();
@@ -154,7 +155,7 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
DeviceDatabaseBackend::Device InfoToDatabaseDevice(const DeviceInfo &info) const; DeviceDatabaseBackend::Device InfoToDatabaseDevice(const DeviceInfo &info) const;
void RemoveFromDB(DeviceInfo *info, const QModelIndex &idx); void RemoveFromDB(DeviceInfo *device_info, const QModelIndex &idx);
void CloseDevices(); void CloseDevices();
void CloseListers(); void CloseListers();

View File

@@ -28,7 +28,7 @@
#include "devicemanager.h" #include "devicemanager.h"
#include "devicestatefiltermodel.h" #include "devicestatefiltermodel.h"
DeviceStateFilterModel::DeviceStateFilterModel(QObject *parent, DeviceManager::State state) DeviceStateFilterModel::DeviceStateFilterModel(QObject *parent, const DeviceManager::State state)
: QSortFilterProxyModel(parent), : QSortFilterProxyModel(parent),
state_(state) { state_(state) {
@@ -40,7 +40,7 @@ DeviceStateFilterModel::DeviceStateFilterModel(QObject *parent, DeviceManager::S
bool DeviceStateFilterModel::filterAcceptsRow(const int row, const QModelIndex &parent) const { bool DeviceStateFilterModel::filterAcceptsRow(const int row, const QModelIndex &parent) const {
Q_UNUSED(parent) Q_UNUSED(parent)
return sourceModel()->index(row, 0).data(DeviceManager::Role_State).toInt() != state_ && sourceModel()->index(row, 0).data(DeviceManager::Role_CopyMusic).toBool(); return sourceModel()->index(row, 0).data(DeviceManager::Role_State).value<DeviceManager::State>() != state_ && sourceModel()->index(row, 0).data(DeviceManager::Role_CopyMusic).toBool();
} }
void DeviceStateFilterModel::ProxyRowCountChanged(const QModelIndex &idx, const int first, const int last) { void DeviceStateFilterModel::ProxyRowCountChanged(const QModelIndex &idx, const int first, const int last) {

View File

@@ -37,7 +37,7 @@ class DeviceStateFilterModel : public QSortFilterProxyModel {
Q_OBJECT Q_OBJECT
public: public:
explicit DeviceStateFilterModel(QObject *parent, DeviceManager::State state = DeviceManager::State_Remembered); explicit DeviceStateFilterModel(QObject *parent, const DeviceManager::State state = DeviceManager::State::Remembered);
void setSourceModel(QAbstractItemModel *sourceModel) override; void setSourceModel(QAbstractItemModel *sourceModel) override;
@@ -52,7 +52,7 @@ class DeviceStateFilterModel : public QSortFilterProxyModel {
void ProxyRowCountChanged(const QModelIndex &idx, const int first, const int last); void ProxyRowCountChanged(const QModelIndex &idx, const int first, const int last);
private: private:
DeviceManager::State state_; const DeviceManager::State state_;
}; };
#endif // DEVICESTATEFILTERMODEL_H #endif // DEVICESTATEFILTERMODEL_H

View File

@@ -128,19 +128,19 @@ void DeviceItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op
} }
else { else {
switch (state) { switch (state) {
case DeviceManager::State_Remembered: case DeviceManager::State::Remembered:
status_text = tr("Not connected"); status_text = tr("Not connected");
break; break;
case DeviceManager::State_NotMounted: case DeviceManager::State::NotMounted:
status_text = tr("Not mounted - double click to mount"); status_text = tr("Not mounted - double click to mount");
break; break;
case DeviceManager::State_NotConnected: case DeviceManager::State::NotConnected:
status_text = tr("Double click to open"); status_text = tr("Double click to open");
break; break;
case DeviceManager::State_Connected:{ case DeviceManager::State::Connected:{
QVariant song_count = idx.data(DeviceManager::Role_SongCount); QVariant song_count = idx.data(DeviceManager::Role_SongCount);
if (song_count.isValid()) { if (song_count.isValid()) {
int count = song_count.toInt(); int count = song_count.toInt();

View File

@@ -409,5 +409,6 @@ bool GPodDevice::FinishDelete(bool success, QString &error_text) {
bool GPodDevice::GetSupportedFiletypes(QList<Song::FileType> *ret) { bool GPodDevice::GetSupportedFiletypes(QList<Song::FileType> *ret) {
*ret << Song::FileType::MP4; *ret << Song::FileType::MP4;
*ret << Song::FileType::MPEG; *ret << Song::FileType::MPEG;
*ret << Song::FileType::ALAC;
return true; return true;
} }

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player * Strawberry Music Player
* This file was part of Clementine. * This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com> * Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -60,6 +60,7 @@ class MacOsDeviceLister : public DeviceLister {
void UpdateDeviceFreeSpace(const QString &id); void UpdateDeviceFreeSpace(const QString &id);
#ifdef HAVE_MTP
struct MTPDevice { struct MTPDevice {
MTPDevice() : capacity(0), free_space(0) {} MTPDevice() : capacity(0), free_space(0) {}
QString vendor; QString vendor;
@@ -74,6 +75,7 @@ class MacOsDeviceLister : public DeviceLister {
quint64 capacity; quint64 capacity;
quint64 free_space; quint64 free_space;
}; };
#endif // HAVE_MTP
void ExitAsync(); void ExitAsync();
@@ -91,11 +93,12 @@ class MacOsDeviceLister : public DeviceLister {
static void DiskUnmountCallback(DADiskRef disk, DADissenterRef dissenter, void *context); static void DiskUnmountCallback(DADiskRef disk, DADissenterRef dissenter, void *context);
void FoundMTPDevice(const MTPDevice &device, const QString &serial); #ifdef HAVE_MTP
void FoundMTPDevice(const MTPDevice &mtp_device, const QString &serial);
void RemovedMTPDevice(const QString &serial); void RemovedMTPDevice(const QString &serial);
quint64 GetFreeSpace(const QUrl &url); quint64 GetFreeSpace(const QUrl &url);
quint64 GetCapacity(const QUrl &url); quint64 GetCapacity(const QUrl &url);
#endif // HAVE_MTP
bool IsCDDevice(const QString &serial) const; bool IsCDDevice(const QString &serial) const;
@@ -103,18 +106,23 @@ class MacOsDeviceLister : public DeviceLister {
CFRunLoopRef run_loop_; CFRunLoopRef run_loop_;
QMap<QString, QString> current_devices_; QMap<QString, QString> current_devices_;
#ifdef HAVE_MTP
QMap<QString, MTPDevice> mtp_devices_; QMap<QString, MTPDevice> mtp_devices_;
#endif
QSet<QString> cd_devices_; QSet<QString> cd_devices_;
#ifdef HAVE_MTP
QMutex libmtp_mutex_; QMutex libmtp_mutex_;
static QSet<MTPDevice> sMTPDeviceList; static QSet<MTPDevice> sMTPDeviceList;
#endif
}; };
size_t qHash(const MacOsDeviceLister::MTPDevice &device); #ifdef HAVE_MTP
size_t qHash(const MacOsDeviceLister::MTPDevice &mtp_device);
inline bool operator==(const MacOsDeviceLister::MTPDevice &a, const MacOsDeviceLister::MTPDevice &b) { inline bool operator==(const MacOsDeviceLister::MTPDevice &a, const MacOsDeviceLister::MTPDevice &b) {
return (a.vendor_id == b.vendor_id) && (a.product_id == b.product_id); return (a.vendor_id == b.vendor_id) && (a.product_id == b.product_id);
} }
#endif // HAVE_MTP
#endif // MACDEVICELISTER_H #endif // MACDEVICELISTER_H

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player * Strawberry Music Player
* This file was part of Clementine. * This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com> * Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -21,7 +21,9 @@
#include "config.h" #include "config.h"
#include <libmtp.h> #ifdef HAVE_MTP
# include <libmtp.h>
#endif
#include <AvailabilityMacros.h> #include <AvailabilityMacros.h>
#include <CoreFoundation/CFRunLoop.h> #include <CoreFoundation/CFRunLoop.h>
@@ -41,12 +43,15 @@
#include <QScopeGuard> #include <QScopeGuard>
#include "macosdevicelister.h" #include "macosdevicelister.h"
#include "mtpconnection.h"
#include "includes/scoped_cftyperef.h" #include "includes/scoped_cftyperef.h"
#include "includes/scoped_nsobject.h" #include "includes/scoped_nsobject.h"
#include "core/logging.h" #include "core/logging.h"
#include "core/scoped_nsautorelease_pool.h" #include "core/scoped_nsautorelease_pool.h"
#ifdef HAVE_MTP
# include "mtpconnection.h"
#endif
#import <AppKit/NSWorkspace.h> #import <AppKit/NSWorkspace.h>
#import <Foundation/NSDictionary.h> #import <Foundation/NSDictionary.h>
#import <Foundation/NSNotification.h> #import <Foundation/NSNotification.h>
@@ -102,11 +107,15 @@ class ScopedIOObject {
// Libgphoto2 MTP detection code: // Libgphoto2 MTP detection code:
// http://www.sfr-fresh.com/unix/privat/libgphoto2-2.4.10.1.tar.gz:a/libgphoto2-2.4.10.1/libgphoto2_port/usb/check-mtp-device.c // http://www.sfr-fresh.com/unix/privat/libgphoto2-2.4.10.1.tar.gz:a/libgphoto2-2.4.10.1/libgphoto2_port/usb/check-mtp-device.c
#ifdef HAVE_MTP
QSet<MacOsDeviceLister::MTPDevice> MacOsDeviceLister::sMTPDeviceList; QSet<MacOsDeviceLister::MTPDevice> MacOsDeviceLister::sMTPDeviceList;
#endif
size_t qHash(const MacOsDeviceLister::MTPDevice &d) { #ifdef HAVE_MTP
return qHash(d.vendor_id) ^ qHash(d.product_id); size_t qHash(const MacOsDeviceLister::MTPDevice &mtp_device) {
return qHash(mtp_device.vendor_id) ^ qHash(mtp_device.product_id);
} }
#endif
MacOsDeviceLister::MacOsDeviceLister(QObject *parent) : DeviceLister(parent) {} MacOsDeviceLister::MacOsDeviceLister(QObject *parent) : DeviceLister(parent) {}
@@ -116,6 +125,7 @@ bool MacOsDeviceLister::Init() {
ScopedNSAutoreleasePool pool; ScopedNSAutoreleasePool pool;
#ifdef HAVE_MTP
// Populate MTP Device list. // Populate MTP Device list.
if (sMTPDeviceList.empty()) { if (sMTPDeviceList.empty()) {
LIBMTP_device_entry_t *devices = nullptr; LIBMTP_device_entry_t *devices = nullptr;
@@ -126,25 +136,26 @@ bool MacOsDeviceLister::Init() {
else { else {
for (int i = 0; i < num; ++i) { for (int i = 0; i < num; ++i) {
LIBMTP_device_entry_t device = devices[i]; LIBMTP_device_entry_t device = devices[i];
MTPDevice d; MTPDevice mtp_device;
d.vendor = QString::fromLatin1(device.vendor); mtp_device.vendor = QString::fromLatin1(device.vendor);
d.vendor_id = device.vendor_id; mtp_device.vendor_id = device.vendor_id;
d.product = QString::fromLatin1(device.product); mtp_device.product = QString::fromLatin1(device.product);
d.product_id = device.product_id; mtp_device.product_id = device.product_id;
d.quirks = device.device_flags; mtp_device.quirks = device.device_flags;
sMTPDeviceList << d; sMTPDeviceList << mtp_device;
} }
} }
MTPDevice d; MTPDevice mtp_device;
d.vendor = "SanDisk"_L1; mtp_device.vendor = "SanDisk"_L1;
d.vendor_id = 0x781; mtp_device.vendor_id = 0x781;
d.product = "Sansa Clip+"_L1; mtp_device.product = "Sansa Clip+"_L1;
d.product_id = 0x74d0; mtp_device.product_id = 0x74d0;
d.quirks = 0x2 | 0x4 | 0x40 | 0x4000; mtp_device.quirks = 0x2 | 0x4 | 0x40 | 0x4000;
sMTPDeviceList << d; sMTPDeviceList << mtp_device;
} }
#endif // HAVE_MTP
run_loop_ = CFRunLoopGetCurrent(); run_loop_ = CFRunLoopGetCurrent();
@@ -240,12 +251,14 @@ CFTypeRef GetUSBRegistryEntry(io_object_t device, CFStringRef key) {
} }
QString GetUSBRegistryEntryString(io_object_t device, CFStringRef key) { QString GetUSBRegistryEntryString(io_object_t device, CFStringRef key) {
ScopedCFTypeRef<CFStringRef> registry_string(reinterpret_cast<CFStringRef>(GetUSBRegistryEntry(device, key))); ScopedCFTypeRef<CFStringRef> registry_string(reinterpret_cast<CFStringRef>(GetUSBRegistryEntry(device, key)));
if (registry_string) { if (registry_string) {
return QString::fromUtf8([reinterpret_cast<NSString*>(registry_string.get()) UTF8String]); return QString::fromUtf8([reinterpret_cast<NSString*>(registry_string.get()) UTF8String]);
} }
return QString(); return QString();
} }
NSObject *GetPropertyForDevice(io_object_t device, CFStringRef key) { NSObject *GetPropertyForDevice(io_object_t device, CFStringRef key) {
@@ -277,17 +290,13 @@ NSObject *GetPropertyForDevice(io_object_t device, CFStringRef key) {
int GetUSBDeviceClass(io_object_t device) { int GetUSBDeviceClass(io_object_t device) {
ScopedCFTypeRef<CFTypeRef> interface_class(IORegistryEntrySearchCFProperty( ScopedCFTypeRef<CFTypeRef> interface_class(IORegistryEntrySearchCFProperty(device, kIOServicePlane, CFSTR(kUSBInterfaceClass), kCFAllocatorDefault, kIORegistryIterateRecursively));
device,
kIOServicePlane,
CFSTR(kUSBInterfaceClass),
kCFAllocatorDefault,
kIORegistryIterateRecursively));
NSNumber *number = reinterpret_cast<NSNumber*>(interface_class.get()); NSNumber *number = reinterpret_cast<NSNumber*>(interface_class.get());
if (number) { if (number) {
int ret = [number unsignedShortValue]; int ret = [number unsignedShortValue];
return ret; return ret;
} }
return 0; return 0;
} }
@@ -322,12 +331,14 @@ QString GetSerialForDevice(io_object_t device) {
} }
#ifdef HAVE_MTP
QString GetSerialForMTPDevice(io_object_t device) { QString GetSerialForMTPDevice(io_object_t device) {
scoped_nsobject<NSString> serial(reinterpret_cast<NSString*>(GetPropertyForDevice(device, CFSTR(kUSBSerialNumberString)))); scoped_nsobject<NSString> serial(reinterpret_cast<NSString*>(GetPropertyForDevice(device, CFSTR(kUSBSerialNumberString))));
return "MTP/"_L1 + QString::fromUtf8([serial UTF8String]); return "MTP/"_L1 + QString::fromUtf8([serial UTF8String]);
} }
#endif // HAVE_MTP
QString FindDeviceProperty(const QString &bsd_name, CFStringRef property) { QString FindDeviceProperty(const QString &bsd_name, CFStringRef property) {
@@ -343,6 +354,7 @@ QString FindDeviceProperty(const QString &bsd_name, CFStringRef property) {
} // namespace } // namespace
#ifdef HAVE_MTP
quint64 MacOsDeviceLister::GetFreeSpace(const QUrl &url) { quint64 MacOsDeviceLister::GetFreeSpace(const QUrl &url) {
QMutexLocker l(&libmtp_mutex_); QMutexLocker l(&libmtp_mutex_);
@@ -380,6 +392,8 @@ quint64 MacOsDeviceLister::GetCapacity(const QUrl &url) {
} }
#endif // HAVE_MTP
void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) { void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(context); MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(context);
@@ -390,12 +404,12 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
NSString *kind = [properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionMediaKindKey)]; NSString *kind = [properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionMediaKindKey)];
if (kind && strcmp([kind UTF8String], kIOCDMediaClass) == 0) { if (kind && strcmp([kind UTF8String], kIOCDMediaClass) == 0) {
// CD inserted. // CD inserted.
QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk)); const QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
me->cd_devices_ << bsd_name; me->cd_devices_ << bsd_name;
Q_EMIT me->DeviceAdded(bsd_name); Q_EMIT me->DeviceAdded(bsd_name);
return; return;
} }
#endif #endif // HAVE_AUDIOCD
NSURL *volume_path = [[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy]; NSURL *volume_path = [[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy];
@@ -403,8 +417,8 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
ScopedIOObject device(DADiskCopyIOMedia(disk)); ScopedIOObject device(DADiskCopyIOMedia(disk));
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(device.get())); ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(device.get()));
if (class_name && CFStringCompare(class_name.get(), CFSTR(kIOMediaClass), 0) == kCFCompareEqualTo) { if (class_name && CFStringCompare(class_name.get(), CFSTR(kIOMediaClass), 0) == kCFCompareEqualTo) {
QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString)); const QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString)); const QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
CFMutableDictionaryRef cf_properties; CFMutableDictionaryRef cf_properties;
kern_return_t ret = IORegistryEntryCreateCFProperties(device.get(), &cf_properties, kCFAllocatorDefault, 0); kern_return_t ret = IORegistryEntryCreateCFProperties(device.get(), &cf_properties, kCFAllocatorDefault, 0);
@@ -412,7 +426,7 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
if (ret == KERN_SUCCESS) { if (ret == KERN_SUCCESS) {
scoped_nsobject<NSDictionary> dict(reinterpret_cast<NSDictionary*>(cf_properties)); // Takes ownership. scoped_nsobject<NSDictionary> dict(reinterpret_cast<NSDictionary*>(cf_properties)); // Takes ownership.
if ([[dict objectForKey:@"Removable"] intValue] == 1) { if ([[dict objectForKey:@"Removable"] intValue] == 1) {
QString serial = GetSerialForDevice(device.get()); const QString serial = GetSerialForDevice(device.get());
if (!serial.isEmpty()) { if (!serial.isEmpty()) {
me->current_devices_[serial] = QString::fromLatin1(DADiskGetBSDName(disk)); me->current_devices_[serial] = QString::fromLatin1(DADiskGetBSDName(disk));
Q_EMIT me->DeviceAdded(serial); Q_EMIT me->DeviceAdded(serial);
@@ -427,10 +441,9 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
void MacOsDeviceLister::DiskRemovedCallback(DADiskRef disk, void *context) { void MacOsDeviceLister::DiskRemovedCallback(DADiskRef disk, void *context) {
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(context); MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(context);
// We cannot access the USB tree when the disk is removed but we still get // We cannot access the USB tree when the disk is removed but we still get the BSD disk name.
// the BSD disk name.
QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk)); const QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
if (me->cd_devices_.remove(bsd_name)) { if (me->cd_devices_.remove(bsd_name)) {
Q_EMIT me->DeviceRemoved(bsd_name); Q_EMIT me->DeviceRemoved(bsd_name);
return; return;
@@ -496,6 +509,7 @@ int GetBusNumber(io_object_t o) {
void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) { void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon); MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon);
Q_UNUSED(me)
io_object_t object; io_object_t object;
while ((object = IOIteratorNext(it))) { while ((object = IOIteratorNext(it))) {
@@ -503,30 +517,34 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
const QScopeGuard io_object_release = qScopeGuard([object]() { IOObjectRelease(object); }); const QScopeGuard io_object_release = qScopeGuard([object]() { IOObjectRelease(object); });
if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) { if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
const int interface_class = GetUSBDeviceClass(object);
qLog(Debug) << "Interface class:" << interface_class;
#ifdef HAVE_MTP
NSString *vendor = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBVendorString))); NSString *vendor = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBVendorString)));
NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString))); NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString)));
NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID))); NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID)));
NSNumber *product_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBProductID))); NSNumber *product_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBProductID)));
int interface_class = GetUSBDeviceClass(object);
qLog(Debug) << "Interface class:" << interface_class;
QString serial = GetSerialForMTPDevice(object); const QString serial = GetSerialForMTPDevice(object);
MTPDevice device; MTPDevice mtp_device;
device.vendor = QString::fromUtf8([vendor UTF8String]); mtp_device.vendor = QString::fromUtf8([vendor UTF8String]);
device.product = QString::fromUtf8([product UTF8String]); mtp_device.product = QString::fromUtf8([product UTF8String]);
device.vendor_id = [vendor_id unsignedShortValue]; mtp_device.vendor_id = [vendor_id unsignedShortValue];
device.product_id = [product_id unsignedShortValue]; mtp_device.product_id = [product_id unsignedShortValue];
device.quirks = 0; mtp_device.quirks = 0;
device.bus = -1; mtp_device.bus = -1;
device.address = -1; mtp_device.address = -1;
if (device.vendor_id == kAppleVendorID || // I think we can safely skip Apple products. if (mtp_device.vendor_id == kAppleVendorID || // I think we can safely skip Apple products.
// Blacklist ilok2 as this probe may be breaking it. // Blacklist ilok2 as this probe may be breaking it.
(device.vendor_id == 0x088e && device.product_id == 0x5036) || (mtp_device.vendor_id == 0x088e && mtp_device.product_id == 0x5036) ||
// Blacklist eLicenser // Blacklist eLicenser
(device.vendor_id == 0x0819 && device.product_id == 0x0101) || (mtp_device.vendor_id == 0x0819 && mtp_device.product_id == 0x0101) ||
// Skip HID devices, printers and hubs. // Skip HID devices, printers and hubs.
interface_class == kUSBHIDInterfaceClass || interface_class == kUSBHIDInterfaceClass ||
interface_class == kUSBPrintingInterfaceClass || interface_class == kUSBPrintingInterfaceClass ||
@@ -535,31 +553,28 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
} }
NSNumber *addr = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR("USB Address"))); NSNumber *addr = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR("USB Address")));
int bus = GetBusNumber(object); const int bus = GetBusNumber(object);
if (!addr || bus == -1) { if (!addr || bus == -1) {
// Failed to get bus or address number. // Failed to get bus or address number.
continue; continue;
} }
device.bus = bus; mtp_device.bus = bus;
device.address = [addr intValue]; mtp_device.address = [addr intValue];
// First check the libmtp device list. // First check the libmtp device list.
QSet<MTPDevice>::const_iterator it2 = sMTPDeviceList.find(device); QSet<MTPDevice>::const_iterator it2 = sMTPDeviceList.find(mtp_device);
if (it2 != sMTPDeviceList.end()) { if (it2 != sMTPDeviceList.end()) {
// Fill in quirks flags from libmtp. // Fill in quirks flags from libmtp.
device.quirks = it2->quirks; mtp_device.quirks = it2->quirks;
me->FoundMTPDevice(device, GetSerialForMTPDevice(object)); me->FoundMTPDevice(mtp_device, GetSerialForMTPDevice(object));
continue; continue;
} }
#endif // HAVE_MTP
IOCFPlugInInterface **plugin_interface = nullptr; IOCFPlugInInterface **plugin_interface = nullptr;
SInt32 score; SInt32 score;
kern_return_t err = IOCreatePlugInInterfaceForService( kern_return_t err = IOCreatePlugInInterfaceForService(object, kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin_interface, &score);
object,
kIOUSBDeviceUserClientTypeID,
kIOCFPlugInInterfaceID,
&plugin_interface,
&score);
if (err != KERN_SUCCESS) { if (err != KERN_SUCCESS) {
continue; continue;
} }
@@ -590,7 +605,7 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
bool ret = DeviceRequest(dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor, (kUSBStringDesc << 8) | 0xee, 0x0409, 2, &data); bool ret = DeviceRequest(dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor, (kUSBStringDesc << 8) | 0xee, 0x0409, 2, &data);
if (!ret) continue; if (!ret) continue;
UInt8 string_len = data[0]; const UInt8 string_len = data[0];
ret = DeviceRequest(dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor, (kUSBStringDesc << 8) | 0xee, 0x0409, string_len, &data); ret = DeviceRequest(dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor, (kUSBStringDesc << 8) | 0xee, 0x0409, string_len, &data);
if (!ret) continue; if (!ret) continue;
@@ -599,6 +614,7 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
// Because this was designed by MS, the characters are in UTF-16 (LE?). // Because this was designed by MS, the characters are in UTF-16 (LE?).
QString str = QString::fromUtf16(reinterpret_cast<char16_t*>(data.data() + 2), (data.size() / 2) - 2); QString str = QString::fromUtf16(reinterpret_cast<char16_t*>(data.data() + 2), (data.size() / 2) - 2);
#ifdef HAVE_MTP
if (str.startsWith("MSFT100"_L1)) { if (str.startsWith("MSFT100"_L1)) {
// We got the OS descriptor! // We got the OS descriptor!
char vendor_code = data[16]; char vendor_code = data[16];
@@ -621,8 +637,10 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
continue; continue;
} }
// Hurray! We made it! // Hurray! We made it!
me->FoundMTPDevice(device, serial); me->FoundMTPDevice(mtp_device, serial);
} }
#endif // HAVE_MTP
} }
} }
@@ -631,30 +649,39 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
void MacOsDeviceLister::USBDeviceRemovedCallback(void *refcon, io_iterator_t it) { void MacOsDeviceLister::USBDeviceRemovedCallback(void *refcon, io_iterator_t it) {
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon); MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon);
Q_UNUSED(me)
io_object_t object; io_object_t object;
while ((object = IOIteratorNext(it))) { while ((object = IOIteratorNext(it))) {
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(object)); ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(object));
const QScopeGuard io_object_release = qScopeGuard([object]() { IOObjectRelease(object); }); const QScopeGuard io_object_release = qScopeGuard([object]() { IOObjectRelease(object); });
if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) { if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
#ifdef HAVE_MTP
NSString *vendor = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBVendorString))); NSString *vendor = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBVendorString)));
NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString))); NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString)));
NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID))); NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID)));
NSNumber *product_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBProductID))); NSNumber *product_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBProductID)));
QString serial = GetSerialForMTPDevice(object);
MTPDevice device; const QString serial = GetSerialForMTPDevice(object);
device.vendor = QString::fromUtf8([vendor UTF8String]);
device.product = QString::fromUtf8([product UTF8String]); MTPDevice mtp_device;
device.vendor_id = [vendor_id unsignedShortValue]; mtp_device.vendor = QString::fromUtf8([vendor UTF8String]);
device.product_id = [product_id unsignedShortValue]; mtp_device.product = QString::fromUtf8([product UTF8String]);
mtp_device.vendor_id = [vendor_id unsignedShortValue];
mtp_device.product_id = [product_id unsignedShortValue];
me->RemovedMTPDevice(serial); me->RemovedMTPDevice(serial);
#endif // HAVE_MTP
} }
} }
} }
#ifdef HAVE_MTP
void MacOsDeviceLister::RemovedMTPDevice(const QString &serial) { void MacOsDeviceLister::RemovedMTPDevice(const QString &serial) {
int count = mtp_devices_.remove(serial); int count = mtp_devices_.remove(serial);
@@ -668,34 +695,40 @@ void MacOsDeviceLister::RemovedMTPDevice(const QString &serial) {
void MacOsDeviceLister::FoundMTPDevice(const MTPDevice &device, const QString &serial) { void MacOsDeviceLister::FoundMTPDevice(const MTPDevice &device, const QString &serial) {
qLog(Debug) << "New MTP device detected!" << device.bus << device.address; qLog(Debug) << "New MTP device detected!" << device.bus << device.address;
mtp_devices_[serial] = device; mtp_devices_[serial] = device;
QList<QUrl> urls = MakeDeviceUrls(serial); const QList<QUrl> urls = MakeDeviceUrls(serial);
MTPDevice *d = &mtp_devices_[serial]; MTPDevice *mtp_device = &mtp_devices_[serial];
d->capacity = GetCapacity(urls[0]); mtp_device->capacity = GetCapacity(urls[0]);
d->free_space = GetFreeSpace(urls[0]); mtp_device->free_space = GetFreeSpace(urls[0]);
Q_EMIT DeviceAdded(serial); Q_EMIT DeviceAdded(serial);
} }
bool IsMTPSerial(const QString &serial) { return serial.startsWith("MTP"_L1); } bool IsMTPSerial(const QString &serial) { return serial.startsWith("MTP"_L1); }
#endif // HAVE_MTP
bool MacOsDeviceLister::IsCDDevice(const QString &serial) const { bool MacOsDeviceLister::IsCDDevice(const QString &serial) const {
return cd_devices_.contains(serial); return cd_devices_.contains(serial);
} }
QString MacOsDeviceLister::MakeFriendlyName(const QString &serial) { QString MacOsDeviceLister::MakeFriendlyName(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) { if (IsMTPSerial(serial)) {
const MTPDevice &device = mtp_devices_[serial]; const MTPDevice &mtp_device = mtp_devices_[serial];
if (device.vendor.isEmpty()) { if (mtp_device.vendor.isEmpty()) {
return device.product; return mtp_device.product;
} }
else { else {
return device.vendor + QLatin1Char(' ') + device.product; return mtp_device.vendor + QLatin1Char(' ') + mtp_device.product;
} }
} }
#endif // HAVE_MTP
QString bsd_name = IsCDDevice(serial) ? *cd_devices_.find(serial) : current_devices_[serial]; const QString bsd_name = IsCDDevice(serial) ? *cd_devices_.find(serial) : current_devices_[serial];
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault)); ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData())); ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
@@ -708,75 +741,86 @@ QString MacOsDeviceLister::MakeFriendlyName(const QString &serial) {
ScopedIOObject device(DADiskCopyIOMedia(disk)); ScopedIOObject device(DADiskCopyIOMedia(disk));
QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString)); const QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString)); const QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
if (vendor.isEmpty()) { if (vendor.isEmpty()) {
return product; return product;
} }
return vendor + QLatin1Char(' ') + product; return vendor + QLatin1Char(' ') + product;
} }
QList<QUrl> MacOsDeviceLister::MakeDeviceUrls(const QString &serial) { QList<QUrl> MacOsDeviceLister::MakeDeviceUrls(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) { if (IsMTPSerial(serial)) {
const MTPDevice &device = mtp_devices_[serial]; const MTPDevice &mtp_device = mtp_devices_[serial];
QString str = QString::asprintf("gphoto2://usb-%d-%d/", device.bus, device.address); const QString str = QString::asprintf("gphoto2://usb-%d-%d/", mtp_device.bus, mtp_device.address);
QUrlQuery url_query; QUrlQuery url_query;
url_query.addQueryItem(u"vendor"_s, device.vendor); url_query.addQueryItem(u"vendor"_s, mtp_device.vendor);
url_query.addQueryItem(u"vendor_id"_s, QString::number(device.vendor_id)); url_query.addQueryItem(u"vendor_id"_s, QString::number(mtp_device.vendor_id));
url_query.addQueryItem(u"product"_s, device.product); url_query.addQueryItem(u"product"_s, mtp_device.product);
url_query.addQueryItem(u"product_id"_s, QString::number(device.product_id)); url_query.addQueryItem(u"product_id"_s, QString::number(mtp_device.product_id));
url_query.addQueryItem(u"quirks"_s, QString::number(device.quirks)); url_query.addQueryItem(u"quirks"_s, QString::number(mtp_device.quirks));
QUrl url(str); QUrl url(str);
url.setQuery(url_query); url.setQuery(url_query);
return QList<QUrl>() << url; return QList<QUrl>() << url;
} }
#endif // HAVE_MTP
if (IsCDDevice(serial)) { if (IsCDDevice(serial)) {
return QList<QUrl>() << QUrl(u"cdda:///dev/r"_s + serial); return QList<QUrl>() << QUrl(u"cdda:///dev/r"_s + serial);
} }
QString bsd_name = current_devices_[serial]; const QString bsd_name = current_devices_[serial];
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault)); ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData())); ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
scoped_nsobject<NSDictionary> properties(reinterpret_cast<NSDictionary*>(DADiskCopyDescription(disk.get()))); scoped_nsobject<NSDictionary> properties(reinterpret_cast<NSDictionary*>(DADiskCopyDescription(disk.get())));
scoped_nsobject<NSURL> volume_path([[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy]); scoped_nsobject<NSURL> volume_path([[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy]);
QString path = QString::fromUtf8([[volume_path path] UTF8String]); const QString path = QString::fromUtf8([[volume_path path] UTF8String]);
QUrl ret = MakeUrlFromLocalPath(path); const QUrl ret = MakeUrlFromLocalPath(path);
return QList<QUrl>() << ret; return QList<QUrl>() << ret;
} }
QStringList MacOsDeviceLister::DeviceUniqueIDs() { QStringList MacOsDeviceLister::DeviceUniqueIDs() {
#ifdef HAVE_MTP
return current_devices_.keys() + mtp_devices_.keys(); return current_devices_.keys() + mtp_devices_.keys();
#else
return current_devices_.keys();
#endif
} }
QVariantList MacOsDeviceLister::DeviceIcons(const QString &serial) { QVariantList MacOsDeviceLister::DeviceIcons(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) { if (IsMTPSerial(serial)) {
return QVariantList(); return QVariantList();
} }
#endif // HAVE_MTP
if (IsCDDevice(serial)) { if (IsCDDevice(serial)) {
return QVariantList() << u"media-optical"_s; return QVariantList() << u"media-optical"_s;
} }
QString bsd_name = current_devices_[serial]; const QString bsd_name = current_devices_[serial];
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault)); ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData())); ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
ScopedIOObject device(DADiskCopyIOMedia(disk.get())); ScopedIOObject device(DADiskCopyIOMedia(disk.get()));
QString icon = GetIconForDevice(device.get()); const QString icon = GetIconForDevice(device.get());
scoped_nsobject<NSDictionary> properties(reinterpret_cast<NSDictionary*>(DADiskCopyDescription(disk))); scoped_nsobject<NSDictionary> properties(reinterpret_cast<NSDictionary*>(DADiskCopyDescription(disk)));
scoped_nsobject<NSURL> volume_path([[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy]); scoped_nsobject<NSURL> volume_path([[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy]);
QString path = QString::fromUtf8([[volume_path path] UTF8String]); const QString path = QString::fromUtf8([[volume_path path] UTF8String]);
QVariantList ret; QVariantList ret;
ret << GuessIconForPath(path); ret << GuessIconForPath(path);
@@ -784,31 +828,45 @@ QVariantList MacOsDeviceLister::DeviceIcons(const QString &serial) {
if (!icon.isEmpty()) { if (!icon.isEmpty()) {
ret << icon; ret << icon;
} }
return ret; return ret;
} }
QString MacOsDeviceLister::DeviceManufacturer(const QString &serial) { QString MacOsDeviceLister::DeviceManufacturer(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) { if (IsMTPSerial(serial)) {
return mtp_devices_[serial].vendor; return mtp_devices_[serial].vendor;
} }
#endif // HAVE_MTP
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBVendorString)); return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBVendorString));
} }
QString MacOsDeviceLister::DeviceModel(const QString &serial) { QString MacOsDeviceLister::DeviceModel(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) { if (IsMTPSerial(serial)) {
return mtp_devices_[serial].product; return mtp_devices_[serial].product;
} }
#endif // HAVE_MTP
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBProductString)); return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBProductString));
} }
quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) { quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) { if (IsMTPSerial(serial)) {
QList<QUrl> urls = MakeDeviceUrls(serial); QList<QUrl> urls = MakeDeviceUrls(serial);
return mtp_devices_[serial].capacity; return mtp_devices_[serial].capacity;
} }
QString bsd_name = current_devices_[serial]; #endif // HAVE_MTP
const QString bsd_name = current_devices_[serial];
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault)); ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData())); ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
@@ -816,7 +874,7 @@ quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
NSNumber *capacity = reinterpret_cast<NSNumber*>(GetPropertyForDevice(device, CFSTR("Size"))); NSNumber *capacity = reinterpret_cast<NSNumber*>(GetPropertyForDevice(device, CFSTR("Size")));
quint64 ret = [capacity unsignedLongLongValue]; const quint64 ret = [capacity unsignedLongLongValue];
IOObjectRelease(device); IOObjectRelease(device);
@@ -826,10 +884,13 @@ quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
quint64 MacOsDeviceLister::DeviceFreeSpace(const QString &serial) { quint64 MacOsDeviceLister::DeviceFreeSpace(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) { if (IsMTPSerial(serial)) {
QList<QUrl> urls = MakeDeviceUrls(serial); QList<QUrl> urls = MakeDeviceUrls(serial);
return mtp_devices_[serial].free_space; return mtp_devices_[serial].free_space;
} }
#endif // HAVE_MTP
QString bsd_name = current_devices_[serial]; QString bsd_name = current_devices_[serial];
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault)); ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData())); ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
@@ -857,9 +918,11 @@ bool MacOsDeviceLister::AskForScan(const QString &serial) const {
void MacOsDeviceLister::UnmountDevice(const QString &serial) { void MacOsDeviceLister::UnmountDevice(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) return; if (IsMTPSerial(serial)) return;
#endif
QString bsd_name = current_devices_[serial]; const QString bsd_name = current_devices_[serial];
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, loop_session_, bsd_name.toLatin1().constData())); ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, loop_session_, bsd_name.toLatin1().constData()));
DADiskUnmount(disk, kDADiskUnmountOptionDefault, &DiskUnmountCallback, this); DADiskUnmount(disk, kDADiskUnmountOptionDefault, &DiskUnmountCallback, this);
@@ -879,13 +942,16 @@ void MacOsDeviceLister::DiskUnmountCallback(DADiskRef disk, DADissenterRef disse
void MacOsDeviceLister::UpdateDeviceFreeSpace(const QString &serial) { void MacOsDeviceLister::UpdateDeviceFreeSpace(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) { if (IsMTPSerial(serial)) {
if (mtp_devices_.contains(serial)) { if (mtp_devices_.contains(serial)) {
QList<QUrl> urls = MakeDeviceUrls(serial); QList<QUrl> urls = MakeDeviceUrls(serial);
MTPDevice *d = &mtp_devices_[serial]; MTPDevice *mtp_device = &mtp_devices_[serial];
d->free_space = GetFreeSpace(urls[0]); mtp_device->free_space = GetFreeSpace(urls[0]);
} }
} }
#endif
Q_EMIT DeviceChanged(serial); Q_EMIT DeviceChanged(serial);
} }

View File

@@ -199,6 +199,7 @@ EditTagDialog::EditTagDialog(const SharedPtr<NetworkAccessManager> network,
} }
else if (RatingBox *ratingbox = qobject_cast<RatingBox*>(widget)) { else if (RatingBox *ratingbox = qobject_cast<RatingBox*>(widget)) {
QObject::connect(ratingbox, &RatingWidget::RatingChanged, this, &EditTagDialog::FieldValueEdited); QObject::connect(ratingbox, &RatingWidget::RatingChanged, this, &EditTagDialog::FieldValueEdited);
QObject::connect(ratingbox, &RatingBox::Reset, this, &EditTagDialog::ResetField);
} }
} }
} }
@@ -273,12 +274,18 @@ EditTagDialog::EditTagDialog(const SharedPtr<NetworkAccessManager> network,
QKeySequence(QKeySequence::MoveToNextPage).toString(QKeySequence::NativeText))); QKeySequence(QKeySequence::MoveToNextPage).toString(QKeySequence::NativeText)));
new TagCompleter(collection_backend, Playlist::Column::Artist, ui_->artist); new TagCompleter(collection_backend, Playlist::Column::Artist, ui_->artist);
new TagCompleter(collection_backend, Playlist::Column::ArtistSort, ui_->artistsort);
new TagCompleter(collection_backend, Playlist::Column::Album, ui_->album); new TagCompleter(collection_backend, Playlist::Column::Album, ui_->album);
new TagCompleter(collection_backend, Playlist::Column::AlbumSort, ui_->albumsort);
new TagCompleter(collection_backend, Playlist::Column::AlbumArtist, ui_->albumartist); new TagCompleter(collection_backend, Playlist::Column::AlbumArtist, ui_->albumartist);
new TagCompleter(collection_backend, Playlist::Column::AlbumArtistSort, ui_->albumartistsort);
new TagCompleter(collection_backend, Playlist::Column::Genre, ui_->genre); new TagCompleter(collection_backend, Playlist::Column::Genre, ui_->genre);
new TagCompleter(collection_backend, Playlist::Column::Composer, ui_->composer); new TagCompleter(collection_backend, Playlist::Column::Composer, ui_->composer);
new TagCompleter(collection_backend, Playlist::Column::ComposerSort, ui_->composersort);
new TagCompleter(collection_backend, Playlist::Column::Performer, ui_->performer); new TagCompleter(collection_backend, Playlist::Column::Performer, ui_->performer);
new TagCompleter(collection_backend, Playlist::Column::PerformerSort, ui_->performersort);
new TagCompleter(collection_backend, Playlist::Column::Grouping, ui_->grouping); new TagCompleter(collection_backend, Playlist::Column::Grouping, ui_->grouping);
new TagCompleter(collection_backend, Playlist::Column::TitleSort, ui_->titlesort);
} }
@@ -492,11 +499,17 @@ void EditTagDialog::SetSongListVisibility(bool visible) {
QVariant EditTagDialog::Data::value(const Song &song, const QString &id) { QVariant EditTagDialog::Data::value(const Song &song, const QString &id) {
if (id == "title"_L1) return song.title(); if (id == "title"_L1) return song.title();
if (id == "titlesort"_L1) return song.titlesort();
if (id == "artist"_L1) return song.artist(); if (id == "artist"_L1) return song.artist();
if (id == "artistsort"_L1) return song.artistsort();
if (id == "album"_L1) return song.album(); if (id == "album"_L1) return song.album();
if (id == "albumsort"_L1) return song.albumsort();
if (id == "albumartist"_L1) return song.albumartist(); if (id == "albumartist"_L1) return song.albumartist();
if (id == "albumartistsort"_L1) return song.albumartistsort();
if (id == "composer"_L1) return song.composer(); if (id == "composer"_L1) return song.composer();
if (id == "composersort"_L1) return song.composersort();
if (id == "performer"_L1) return song.performer(); if (id == "performer"_L1) return song.performer();
if (id == "performersort"_L1) return song.performersort();
if (id == "grouping"_L1) return song.grouping(); if (id == "grouping"_L1) return song.grouping();
if (id == "genre"_L1) return song.genre(); if (id == "genre"_L1) return song.genre();
if (id == "comment"_L1) return song.comment(); if (id == "comment"_L1) return song.comment();
@@ -514,11 +527,17 @@ QVariant EditTagDialog::Data::value(const Song &song, const QString &id) {
void EditTagDialog::Data::set_value(const QString &id, const QVariant &value) { void EditTagDialog::Data::set_value(const QString &id, const QVariant &value) {
if (id == "title"_L1) current_.set_title(value.toString()); if (id == "title"_L1) current_.set_title(value.toString());
else if (id == "titlesort"_L1) current_.set_titlesort(value.toString());
else if (id == "artist"_L1) current_.set_artist(value.toString()); else if (id == "artist"_L1) current_.set_artist(value.toString());
else if (id == "artistsort"_L1) current_.set_artistsort(value.toString());
else if (id == "album"_L1) current_.set_album(value.toString()); else if (id == "album"_L1) current_.set_album(value.toString());
else if (id == "albumsort"_L1) current_.set_albumsort(value.toString());
else if (id == "albumartist"_L1) current_.set_albumartist(value.toString()); else if (id == "albumartist"_L1) current_.set_albumartist(value.toString());
else if (id == "albumartistsort"_L1) current_.set_albumartistsort(value.toString());
else if (id == "composer"_L1) current_.set_composer(value.toString()); else if (id == "composer"_L1) current_.set_composer(value.toString());
else if (id == "composersort"_L1) current_.set_composersort(value.toString());
else if (id == "performer"_L1) current_.set_performer(value.toString()); else if (id == "performer"_L1) current_.set_performer(value.toString());
else if (id == "performersort"_L1) current_.set_performersort(value.toString());
else if (id == "grouping"_L1) current_.set_grouping(value.toString()); else if (id == "grouping"_L1) current_.set_grouping(value.toString());
else if (id == "genre"_L1) current_.set_genre(value.toString()); else if (id == "genre"_L1) current_.set_genre(value.toString());
else if (id == "comment"_L1) current_.set_comment(value.toString()); else if (id == "comment"_L1) current_.set_comment(value.toString());
@@ -544,6 +563,20 @@ bool EditTagDialog::DoesValueVary(const QModelIndexList &sel, const QString &id)
bool EditTagDialog::IsValueModified(const QModelIndexList &sel, const QString &id) const { bool EditTagDialog::IsValueModified(const QModelIndexList &sel, const QString &id) const {
if (id == u"track"_s || id == u"disc"_s || id == u"year"_s) {
return std::any_of(sel.begin(), sel.end(), [this, id](const QModelIndex &i) {
const int original = data_[i.row()].original_value(id).toInt();
const int current = data_[i.row()].current_value(id).toInt();
return original != current && (original != -1 || current != 0);
});
}
else if (id == u"rating"_s) {
return std::any_of(sel.begin(), sel.end(), [this, id](const QModelIndex &i) {
const float original = data_[i.row()].original_value(id).toFloat();
const float current = data_[i.row()].current_value(id).toFloat();
return original != current && (original != -1 || current != 0);
});
}
return std::any_of(sel.begin(), sel.end(), [this, id](const QModelIndex &i) { return data_[i.row()].original_value(id) != data_[i.row()].current_value(id); }); return std::any_of(sel.begin(), sel.end(), [this, id](const QModelIndex &i) { return data_[i.row()].original_value(id) != data_[i.row()].current_value(id); });
} }
@@ -605,7 +638,15 @@ void EditTagDialog::UpdateModifiedField(const FieldData &field, const QModelInde
QFont new_font(font()); QFont new_font(font());
new_font.setBold(modified); new_font.setBold(modified);
field.label_->setFont(new_font); field.label_->setFont(new_font);
if (field.editor_) field.editor_->setFont(new_font); if (field.editor_) {
if (ExtendedEditor *editor = dynamic_cast<ExtendedEditor*>(field.editor_)) {
editor->set_font(new_font);
editor->set_reset_button(modified);
}
else {
field.editor_->setFont(new_font);
}
}
} }
@@ -652,14 +693,20 @@ void EditTagDialog::SelectionChanged() {
bool art_different = false; bool art_different = false;
bool action_different = false; bool action_different = false;
bool albumartist_enabled = false; bool albumartist_enabled = false;
bool albumartistsort_enabled = false;
bool composer_enabled = false; bool composer_enabled = false;
bool composersort_enabled = false;
bool performer_enabled = false; bool performer_enabled = false;
bool performersort_enabled = false;
bool grouping_enabled = false; bool grouping_enabled = false;
bool genre_enabled = false; bool genre_enabled = false;
bool compilation_enabled = false; bool compilation_enabled = false;
bool rating_enabled = false; bool rating_enabled = false;
bool comment_enabled = false; bool comment_enabled = false;
bool lyrics_enabled = false; bool lyrics_enabled = false;
bool titlesort_enabled = false;
bool artistsort_enabled = false;
bool albumsort_enabled = false;
for (const QModelIndex &idx : indexes) { for (const QModelIndex &idx : indexes) {
if (data_.value(idx.row()).cover_action_ == UpdateCoverAction::None) { if (data_.value(idx.row()).cover_action_ == UpdateCoverAction::None) {
data_[idx.row()].cover_result_ = AlbumCoverImageResult(); data_[idx.row()].cover_result_ = AlbumCoverImageResult();
@@ -679,12 +726,21 @@ void EditTagDialog::SelectionChanged() {
if (song.albumartist_supported()) { if (song.albumartist_supported()) {
albumartist_enabled = true; albumartist_enabled = true;
} }
if (song.albumartistsort_supported()) {
albumartistsort_enabled = true;
}
if (song.composer_supported()) { if (song.composer_supported()) {
composer_enabled = true; composer_enabled = true;
} }
if (song.composersort_supported()) {
composersort_enabled = true;
}
if (song.performer_supported()) { if (song.performer_supported()) {
performer_enabled = true; performer_enabled = true;
} }
if (song.performersort_supported()) {
performersort_enabled = true;
}
if (song.grouping_supported()) { if (song.grouping_supported()) {
grouping_enabled = true; grouping_enabled = true;
} }
@@ -703,6 +759,15 @@ void EditTagDialog::SelectionChanged() {
if (song.lyrics_supported()) { if (song.lyrics_supported()) {
lyrics_enabled = true; lyrics_enabled = true;
} }
if (song.titlesort_supported()) {
titlesort_enabled = true;
}
if (song.artistsort_supported()) {
artistsort_enabled = true;
}
if (song.albumsort_supported()) {
albumsort_enabled = true;
}
} }
QString summary; QString summary;
@@ -759,14 +824,20 @@ void EditTagDialog::SelectionChanged() {
album_cover_choice_controller_->set_save_embedded_cover_override(embedded_cover); album_cover_choice_controller_->set_save_embedded_cover_override(embedded_cover);
ui_->albumartist->setEnabled(albumartist_enabled); ui_->albumartist->setEnabled(albumartist_enabled);
ui_->albumartistsort->setEnabled(albumartistsort_enabled);
ui_->composer->setEnabled(composer_enabled); ui_->composer->setEnabled(composer_enabled);
ui_->composersort->setEnabled(composersort_enabled);
ui_->performer->setEnabled(performer_enabled); ui_->performer->setEnabled(performer_enabled);
ui_->performersort->setEnabled(performersort_enabled);
ui_->grouping->setEnabled(grouping_enabled); ui_->grouping->setEnabled(grouping_enabled);
ui_->genre->setEnabled(genre_enabled); ui_->genre->setEnabled(genre_enabled);
ui_->compilation->setEnabled(compilation_enabled); ui_->compilation->setEnabled(compilation_enabled);
ui_->rating->setEnabled(rating_enabled); ui_->rating->setEnabled(rating_enabled);
ui_->comment->setEnabled(comment_enabled); ui_->comment->setEnabled(comment_enabled);
ui_->lyrics->setEnabled(lyrics_enabled); ui_->lyrics->setEnabled(lyrics_enabled);
ui_->titlesort->setEnabled(titlesort_enabled);
ui_->artistsort->setEnabled(artistsort_enabled);
ui_->albumsort->setEnabled(albumsort_enabled);
} }

View File

@@ -31,8 +31,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>801</width> <width>781</width>
<height>918</height> <height>1047</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
@@ -700,54 +700,35 @@
<property name="sizeConstraint"> <property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum> <enum>QLayout::SetMinimumSize</enum>
</property> </property>
<item row="4" column="3">
<widget class="SpinBox" name="year">
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
</property>
<property name="maximum">
<number>9999</number>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="14" column="1">
<widget class="LineEdit" name="genre">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="2"> <item row="3" column="2">
<widget class="QLabel" name="label_disc"> <widget class="QLabel" name="label_year">
<property name="text"> <property name="text">
<string>Disc</string> <string>Year</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>disc</cstring> <cstring>year</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="1"> <item row="25" column="1">
<widget class="LineEdit" name="composer"> <widget class="QPushButton" name="fetch_tag">
<property name="has_reset_button" stdset="0"> <property name="text">
<bool>true</bool> <string>Complete tags automatically</string>
</property> </property>
<property name="has_clear_button" stdset="0"> <property name="icon">
<bool>false</bool> <iconset resource="../../data/data.qrc">
<normaloff>:/pictures/musicbrainz.png</normaloff>:/pictures/musicbrainz.png</iconset>
</property>
<property name="iconSize">
<size>
<width>38</width>
<height>22</height>
</size>
</property> </property>
</widget> </widget>
</item> </item>
<item row="13" column="0"> <item row="13" column="0">
<widget class="QLabel" name="label_grouping"> <widget class="QLabel" name="label_performersort">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>80</width> <width>80</width>
@@ -755,14 +736,56 @@
</size> </size>
</property> </property>
<property name="text"> <property name="text">
<string>Grouping</string> <string>Performer sort</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>grouping</cstring> <cstring>performersort</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0"> <item row="19" column="0">
<widget class="QLabel" name="label_compilation">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Compilation</string>
</property>
<property name="buddy">
<cstring>compilation</cstring>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="LineEdit" name="albumartistsort">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_titlesort">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Title sort</string>
</property>
<property name="buddy">
<cstring>titlesort</cstring>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_albumartist"> <widget class="QLabel" name="label_albumartist">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
@@ -778,7 +801,130 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="13" column="1"> <item row="0" column="1">
<widget class="LineEdit" name="title">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="12" column="0">
<widget class="QLabel" name="label_performer">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Performer</string>
</property>
<property name="buddy">
<cstring>performer</cstring>
</property>
</widget>
</item>
<item row="18" column="1">
<widget class="LineEdit" name="genre">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="SpinBox" name="year">
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
</property>
<property name="maximum">
<number>9999</number>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="20" column="1">
<widget class="RatingBox" name="rating" native="true">
<property name="maximumSize">
<size>
<width>140</width>
<height>16777215</height>
</size>
</property>
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="LineEdit" name="album">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="26" column="1">
<widget class="TextEdit" name="comment">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="tabChangesFocus">
<bool>true</bool>
</property>
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="LineEdit" name="artistsort">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_title">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Title</string>
</property>
<property name="buddy">
<cstring>title</cstring>
</property>
</widget>
</item>
<item row="17" column="1">
<widget class="LineEdit" name="grouping"> <widget class="LineEdit" name="grouping">
<property name="has_reset_button" stdset="0"> <property name="has_reset_button" stdset="0">
<bool>true</bool> <bool>true</bool>
@@ -788,7 +934,63 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="9" column="1"> <item row="1" column="3">
<widget class="SpinBox" name="disc">
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
</property>
<property name="maximum">
<number>9999</number>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="19" column="1">
<widget class="CheckBox" name="compilation">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="LineEdit" name="titlesort">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="20" column="0">
<widget class="QLabel" name="label_rating">
<property name="text">
<string>Rating</string>
</property>
<property name="buddy">
<cstring>rating</cstring>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="label_disc">
<property name="text">
<string>Disc</string>
</property>
<property name="buddy">
<cstring>disc</cstring>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="LineEdit" name="performer"> <widget class="LineEdit" name="performer">
<property name="has_reset_button" stdset="0"> <property name="has_reset_button" stdset="0">
<bool>true</bool> <bool>true</bool>
@@ -798,26 +1000,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1">
<widget class="LineEdit" name="artist">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="LineEdit" name="title">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="3"> <item row="0" column="3">
<widget class="SpinBox" name="track"> <widget class="SpinBox" name="track">
<property name="correctionMode"> <property name="correctionMode">
@@ -834,7 +1016,49 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="0">
<widget class="QLabel" name="label_albumartistsort">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Album artist sort</string>
</property>
<property name="buddy">
<cstring>albumartistsort</cstring>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="LineEdit" name="artist">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="4" column="0"> <item row="4" column="0">
<widget class="QLabel" name="label_artistsort">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Artist sort</string>
</property>
<property name="buddy">
<cstring>artistsort</cstring>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_album"> <widget class="QLabel" name="label_album">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
@@ -850,18 +1074,18 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="2"> <item row="0" column="2">
<widget class="QLabel" name="label_year"> <widget class="QLabel" name="label_track">
<property name="text"> <property name="text">
<string>Year</string> <string>Track</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>year</cstring> <cstring>track</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0"> <item row="10" column="0">
<widget class="QLabel" name="label_title"> <widget class="QLabel" name="label_composer">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>80</width> <width>80</width>
@@ -869,10 +1093,72 @@
</size> </size>
</property> </property>
<property name="text"> <property name="text">
<string>Title</string> <string>Composer</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>title</cstring> <cstring>composer</cstring>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="LineEdit" name="albumartist">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="LineEdit" name="composersort">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="13" column="1">
<widget class="LineEdit" name="performersort">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="17" column="0">
<widget class="QLabel" name="label_grouping">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Grouping</string>
</property>
<property name="buddy">
<cstring>grouping</cstring>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_albumsort">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Album sort</string>
</property>
<property name="buddy">
<cstring>albumsort</cstring>
</property> </property>
</widget> </widget>
</item> </item>
@@ -892,66 +1178,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="0"> <item row="18" column="0">
<widget class="QLabel" name="label_composer">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Composer</string>
</property>
<property name="buddy">
<cstring>composer</cstring>
</property>
</widget>
</item>
<item row="21" column="1">
<widget class="QPushButton" name="fetch_tag">
<property name="text">
<string>Complete tags automatically</string>
</property>
<property name="icon">
<iconset resource="../../data/data.qrc">
<normaloff>:/pictures/musicbrainz.png</normaloff>:/pictures/musicbrainz.png</iconset>
</property>
<property name="iconSize">
<size>
<width>38</width>
<height>22</height>
</size>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="SpinBox" name="disc">
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
</property>
<property name="maximum">
<number>9999</number>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="15" column="1">
<widget class="CheckBox" name="compilation">
<property name="has_reset_button" stdset="0">
<bool>false</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="14" column="0">
<widget class="QLabel" name="label_genre"> <widget class="QLabel" name="label_genre">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
@@ -967,7 +1194,17 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="22" column="0"> <item row="10" column="1">
<widget class="LineEdit" name="composer">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="26" column="0">
<widget class="QLabel" name="label_comment"> <widget class="QLabel" name="label_comment">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
@@ -983,44 +1220,8 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="11" column="0">
<widget class="LineEdit" name="album"> <widget class="QLabel" name="label_composersort">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="LineEdit" name="albumartist">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="22" column="1">
<widget class="TextEdit" name="comment">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_performer">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>80</width> <width>80</width>
@@ -1028,52 +1229,23 @@
</size> </size>
</property> </property>
<property name="text"> <property name="text">
<string>Performer</string> <string>Composer sort</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>performer</cstring> <cstring>composersort</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="15" column="0"> <item row="6" column="1">
<widget class="QLabel" name="label_compilation"> <widget class="LineEdit" name="albumsort">
<property name="minimumSize"> <property name="has_reset_button" stdset="0">
<size> <bool>true</bool>
<width>80</width>
<height>0</height>
</size>
</property> </property>
<property name="text"> <property name="has_clear_button" stdset="0">
<string>Compilation</string> <bool>false</bool>
</property>
<property name="buddy">
<cstring>compilation</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="2">
<widget class="QLabel" name="label_track">
<property name="text">
<string>Track</string>
</property>
<property name="buddy">
<cstring>track</cstring>
</property>
</widget>
</item>
<item row="16" column="0">
<widget class="QLabel" name="label_rating">
<property name="text">
<string>Rating</string>
</property>
<property name="buddy">
<cstring>rating</cstring>
</property>
</widget>
</item>
<item row="16" column="1">
<widget class="RatingBox" name="rating" native="true"/>
</item>
</layout> </layout>
</item> </item>
<item> <item>
@@ -1131,12 +1303,12 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="21" column="1"> <item>
<widget class="QPushButton" name="fetch_lyrics"> <widget class="QPushButton" name="fetch_lyrics">
<property name="text"> <property name="text">
<string>Complete lyrics automatically</string> <string>Complete lyrics automatically</string>
</property> </property>
</widget> </widget>
</item> </item>
</layout> </layout>
</widget> </widget>
@@ -1199,21 +1371,29 @@
<tabstop>summary</tabstop> <tabstop>summary</tabstop>
<tabstop>filename</tabstop> <tabstop>filename</tabstop>
<tabstop>path</tabstop> <tabstop>path</tabstop>
<tabstop>art_embedded</tabstop>
<tabstop>art_manual</tabstop> <tabstop>art_manual</tabstop>
<tabstop>art_automatic</tabstop> <tabstop>art_automatic</tabstop>
<tabstop>art_unset</tabstop>
<tabstop>playcount_reset</tabstop> <tabstop>playcount_reset</tabstop>
<tabstop>tags_summary</tabstop> <tabstop>tags_summary</tabstop>
<tabstop>tags_art_button</tabstop> <tabstop>tags_art_button</tabstop>
<tabstop>checkbox_embedded_cover</tabstop> <tabstop>checkbox_embedded_cover</tabstop>
<tabstop>title</tabstop> <tabstop>title</tabstop>
<tabstop>track</tabstop> <tabstop>track</tabstop>
<tabstop>artist</tabstop> <tabstop>titlesort</tabstop>
<tabstop>disc</tabstop> <tabstop>disc</tabstop>
<tabstop>album</tabstop> <tabstop>artist</tabstop>
<tabstop>year</tabstop> <tabstop>year</tabstop>
<tabstop>artistsort</tabstop>
<tabstop>album</tabstop>
<tabstop>albumsort</tabstop>
<tabstop>albumartist</tabstop> <tabstop>albumartist</tabstop>
<tabstop>albumartistsort</tabstop>
<tabstop>composer</tabstop> <tabstop>composer</tabstop>
<tabstop>composersort</tabstop>
<tabstop>performer</tabstop> <tabstop>performer</tabstop>
<tabstop>performersort</tabstop>
<tabstop>grouping</tabstop> <tabstop>grouping</tabstop>
<tabstop>genre</tabstop> <tabstop>genre</tabstop>
<tabstop>compilation</tabstop> <tabstop>compilation</tabstop>

View File

@@ -36,11 +36,8 @@ namespace {
constexpr char kDiscordApplicationId[] = "1352351827206733974"; constexpr char kDiscordApplicationId[] = "1352351827206733974";
constexpr char kStrawberryIconResourceName[] = "embedded_cover"; constexpr char kStrawberryIconResourceName[] = "embedded_cover";
constexpr char kStrawberryIconDescription[] = "Strawberry Music Player"; constexpr char kStrawberryIconDescription[] = "Strawberry Music Player";
constexpr qint64 kDiscordPresenceUpdateRateLimitMs = 2000;
} // namespace } // namespace
using namespace discord_rpc;
namespace discord { namespace discord {
RichPresence::RichPresence(const SharedPtr<Player> player, RichPresence::RichPresence(const SharedPtr<Player> player,
@@ -49,10 +46,8 @@ RichPresence::RichPresence(const SharedPtr<Player> player,
: QObject(parent), : QObject(parent),
player_(player), player_(player),
playlist_manager_(playlist_manager), playlist_manager_(playlist_manager),
send_presence_timestamp_(0), initialized_(false),
enabled_(false) { status_display_type_(0) {
Discord_Initialize(kDiscordApplicationId, nullptr, 1, nullptr);
QObject::connect(&*player_->engine(), &EngineBase::StateChanged, this, &RichPresence::EngineStateChanged); QObject::connect(&*player_->engine(), &EngineBase::StateChanged, this, &RichPresence::EngineStateChanged);
QObject::connect(&*playlist_manager_, &PlaylistManager::CurrentSongChanged, this, &RichPresence::CurrentSongChanged); QObject::connect(&*playlist_manager_, &PlaylistManager::CurrentSongChanged, this, &RichPresence::CurrentSongChanged);
@@ -63,7 +58,11 @@ RichPresence::RichPresence(const SharedPtr<Player> player,
} }
RichPresence::~RichPresence() { RichPresence::~RichPresence() {
Discord_Shutdown();
if (initialized_) {
Discord_Shutdown();
}
} }
void RichPresence::ReloadSettings() { void RichPresence::ReloadSettings() {
@@ -71,18 +70,25 @@ void RichPresence::ReloadSettings() {
Settings s; Settings s;
s.beginGroup(DiscordRPCSettings::kSettingsGroup); s.beginGroup(DiscordRPCSettings::kSettingsGroup);
const bool enabled = s.value(DiscordRPCSettings::kEnabled, false).toBool(); const bool enabled = s.value(DiscordRPCSettings::kEnabled, false).toBool();
status_display_type_ = s.value(DiscordRPCSettings::kStatusDisplayType, static_cast<int>(DiscordRPCSettings::StatusDisplayType::App)).toInt();
s.endGroup(); s.endGroup();
if (enabled_ && !enabled) { if (enabled && !initialized_) {
Discord_ClearPresence(); Discord_Initialize(kDiscordApplicationId, nullptr, 0);
initialized_ = true;
}
else if (!enabled && initialized_) {
Discord_ClearPresence();
Discord_Shutdown();
initialized_ = false;
} }
enabled_ = enabled;
} }
void RichPresence::EngineStateChanged(const EngineBase::State state) { void RichPresence::EngineStateChanged(const EngineBase::State state) {
if (!initialized_) return;
if (state == EngineBase::State::Playing) { if (state == EngineBase::State::Playing) {
SetTimestamp(player_->engine()->position_nanosec() / kNsecPerSec); SetTimestamp(player_->engine()->position_nanosec() / kNsecPerSec);
SendPresenceUpdate(); SendPresenceUpdate();
@@ -95,6 +101,8 @@ void RichPresence::EngineStateChanged(const EngineBase::State state) {
void RichPresence::CurrentSongChanged(const Song &song) { void RichPresence::CurrentSongChanged(const Song &song) {
if (!initialized_) return;
SetTimestamp(0LL); SetTimestamp(0LL);
activity_.length_secs = song.length_nanosec() / kNsecPerSec; activity_.length_secs = song.length_nanosec() / kNsecPerSec;
activity_.title = song.title(); activity_.title = song.title();
@@ -107,34 +115,32 @@ void RichPresence::CurrentSongChanged(const Song &song) {
void RichPresence::SendPresenceUpdate() { void RichPresence::SendPresenceUpdate() {
if (!enabled_) { if (!initialized_) return;
return;
}
const qint64 current_timestamp = QDateTime::currentMSecsSinceEpoch();
if (current_timestamp - send_presence_timestamp_ < kDiscordPresenceUpdateRateLimitMs) {
qLog(Info) << "Not sending rich presence due to rate limit of" << kDiscordPresenceUpdateRateLimitMs << "ms";
return;
}
send_presence_timestamp_ = current_timestamp;
::DiscordRichPresence presence_data{}; ::DiscordRichPresence presence_data{};
memset(&presence_data, 0, sizeof(presence_data)); memset(&presence_data, 0, sizeof(presence_data));
presence_data.type = 2; // Listening
// Listening to
presence_data.type = 2;
presence_data.status_display_type = status_display_type_;
presence_data.largeImageKey = kStrawberryIconResourceName; presence_data.largeImageKey = kStrawberryIconResourceName;
presence_data.smallImageKey = kStrawberryIconResourceName; presence_data.smallImageKey = kStrawberryIconResourceName;
presence_data.smallImageText = kStrawberryIconDescription; presence_data.smallImageText = kStrawberryIconDescription;
presence_data.instance = 0; presence_data.instance = 0;
QByteArray artist;
if (!activity_.artist.isEmpty()) { if (!activity_.artist.isEmpty()) {
QByteArray artist = activity_.artist.toUtf8(); artist = activity_.artist.toUtf8();
artist.prepend(tr("by ").toUtf8()); if (artist.size() < 2) { // Discord activity 2 char min. fix
artist.append(" ");
}
presence_data.state = artist.constData(); presence_data.state = artist.constData();
} }
if (!activity_.album.isEmpty() && activity_.album != activity_.title) { QByteArray album;
QByteArray album = activity_.album.toUtf8(); if (!activity_.album.isEmpty()) {
album = activity_.album.toUtf8();
album.prepend(tr("on ").toUtf8()); album.prepend(tr("on ").toUtf8());
presence_data.largeImageText = album.constData(); presence_data.largeImageText = album.constData();
} }
@@ -151,13 +157,19 @@ void RichPresence::SendPresenceUpdate() {
} }
void RichPresence::SetTimestamp(const qint64 seconds) { void RichPresence::SetTimestamp(const qint64 seconds) {
activity_.start_timestamp = QDateTime::currentSecsSinceEpoch(); activity_.start_timestamp = QDateTime::currentSecsSinceEpoch();
activity_.seek_secs = seconds; activity_.seek_secs = seconds;
} }
void RichPresence::Seeked(const qint64 seek_microseconds) { void RichPresence::Seeked(const qint64 seek_microseconds) {
if (!initialized_) return;
SetTimestamp(seek_microseconds / 1000LL); SetTimestamp(seek_microseconds / 1000LL);
SendPresenceUpdate(); SendPresenceUpdate();
} }
} // namespace discord } // namespace discord

View File

@@ -69,8 +69,8 @@ class RichPresence : public QObject {
qint64 seek_secs; qint64 seek_secs;
}; };
Activity activity_; Activity activity_;
qint64 send_presence_timestamp_; bool initialized_;
bool enabled_; int status_display_type_;
}; };
} // namespace discord } // namespace discord

View File

@@ -256,3 +256,19 @@ bool EngineBase::ValidOutput(const QString &output) {
return (true); return (true);
} }
void EngineBase::UpdateSpotifyAccessToken(const QString &spotify_access_token) {
#ifdef HAVE_SPOTIFY
spotify_access_token_ = spotify_access_token;
SetSpotifyAccessToken();
#else
Q_UNUSED(spotify_access_token)
#endif // HAVE_SPOTIFY
}

View File

@@ -127,6 +127,7 @@ class EngineBase : public QObject {
virtual void ReloadSettings(); virtual void ReloadSettings();
void UpdateVolume(const uint volume); void UpdateVolume(const uint volume);
void EmitAboutToFinish(); void EmitAboutToFinish();
void UpdateSpotifyAccessToken(const QString &spotify_access_token);
public: public:
// Simple accessors // Simple accessors
@@ -175,6 +176,11 @@ class EngineBase : public QObject {
void Finished(); void Finished();
private:
#ifdef HAVE_SPOTIFY
virtual void SetSpotifyAccessToken() {}
#endif
protected: protected:
bool playbin3_enabled_; bool playbin3_enabled_;
bool exclusive_mode_; bool exclusive_mode_;

View File

@@ -517,10 +517,20 @@ bool GstEngine::ExclusiveModeSupport(const QString &output) const {
void GstEngine::ReloadSettings() { void GstEngine::ReloadSettings() {
#ifdef HAVE_SPOTIFY
const QString old_spotify_access_token = spotify_access_token_;
#endif
EngineBase::ReloadSettings(); EngineBase::ReloadSettings();
if (output_.isEmpty()) output_ = QLatin1String(kAutoSink); if (output_.isEmpty()) output_ = QLatin1String(kAutoSink);
#ifdef HAVE_SPOTIFY
if (current_pipeline_ && old_spotify_access_token != spotify_access_token_) {
current_pipeline_->set_spotify_access_token(spotify_access_token_);
}
#endif
} }
void GstEngine::ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format) { void GstEngine::ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format) {
@@ -1199,3 +1209,13 @@ bool GstEngine::AnyExclusivePipelineActive() const {
return (current_pipeline_ && current_pipeline_->exclusive_mode()) || OldExclusivePipelineActive(); return (current_pipeline_ && current_pipeline_->exclusive_mode()) || OldExclusivePipelineActive();
} }
#ifdef HAVE_SPOTIFY
void GstEngine::SetSpotifyAccessToken() {
if (current_pipeline_) {
current_pipeline_->set_spotify_access_token(spotify_access_token_);
}
}
#endif // HAVE_SPOTIFY

View File

@@ -146,6 +146,10 @@ class GstEngine : public EngineBase, public GstBufferConsumer {
bool OldExclusivePipelineActive() const; bool OldExclusivePipelineActive() const;
bool AnyExclusivePipelineActive() const; bool AnyExclusivePipelineActive() const;
#ifdef HAVE_SPOTIFY
void SetSpotifyAccessToken() override;
#endif
private: private:
SharedPtr<TaskManager> task_manager_; SharedPtr<TaskManager> task_manager_;
GstDiscoverer *discoverer_; GstDiscoverer *discoverer_;

View File

@@ -1369,6 +1369,12 @@ void GstEnginePipeline::AboutToFinishCallback(GstPlayBin *playbin, gpointer self
qLog(Debug) << "Stream from URL" << instance->gst_url_ << "about to finish."; qLog(Debug) << "Stream from URL" << instance->gst_url_ << "about to finish.";
} }
// When playing GME files it seems playbin3 emits about-to-finish early
// This stops us from skipping when the song has just started.
if (instance->position() == 0) {
return;
}
instance->about_to_finish_ = true; instance->about_to_finish_ = true;
if (instance->HasNextUrl() && !instance->next_uri_set_.value()) { if (instance->HasNextUrl() && !instance->next_uri_set_.value()) {

View File

@@ -51,7 +51,7 @@ QVariant FilterTree::DataFromColumn(const QString &column, const Song &metadata)
if (column == "playcount"_L1) return metadata.playcount(); if (column == "playcount"_L1) return metadata.playcount();
if (column == "skipcount"_L1) return metadata.skipcount(); if (column == "skipcount"_L1) return metadata.skipcount();
if (column == "filename"_L1) return metadata.basefilename(); if (column == "filename"_L1) return metadata.basefilename();
if (column == "url"_L1) return metadata.effective_stream_url().toString(); if (column == "url"_L1) return metadata.effective_url().toString();
return QVariant(); return QVariant();

View File

@@ -66,7 +66,8 @@ void ChartLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id,
QObject::disconnect(reply, nullptr, this, nullptr); QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater(); reply->deleteLater();
const QScopeGuard search_finished = qScopeGuard([this, id]() { Q_EMIT SearchFinished(id); }); LyricsSearchResults results;
const QScopeGuard search_finished = qScopeGuard([this, id, &results]() { Q_EMIT SearchFinished(id, results); });
const ReplyDataResult reply_data_result = GetReplyData(reply); const ReplyDataResult reply_data_result = GetReplyData(reply);
if (!reply_data_result.success()) { if (!reply_data_result.success()) {
@@ -75,9 +76,7 @@ void ChartLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id,
} }
QXmlStreamReader reader(reply_data_result.data); QXmlStreamReader reader(reply_data_result.data);
LyricsSearchResults results;
LyricsSearchResult result; LyricsSearchResult result;
while (!reader.atEnd()) { while (!reader.atEnd()) {
const QXmlStreamReader::TokenType type = reader.readNext(); const QXmlStreamReader::TokenType type = reader.readNext();
const QString name = reader.name().toString(); const QString name = reader.name().toString();

View File

@@ -31,14 +31,12 @@
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QNetworkReply> #include <QNetworkReply>
#include <QRegularExpression> #include <QRegularExpression>
#include <QSettings>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonValue> #include <QJsonValue>
#include <QJsonParseError> #include <QJsonParseError>
#include <QMessageBox> #include <QMessageBox>
#include <QMutexLocker>
#include "includes/shared_ptr.h" #include "includes/shared_ptr.h"
#include "core/logging.h" #include "core/logging.h"
@@ -148,6 +146,8 @@ void GeniusLyricsProvider::StartSearch(const int id, const LyricsSearchRequest &
QNetworkReply *reply = CreateGetRequest(QUrl(QLatin1String(kUrlSearch)), url_query); QNetworkReply *reply = CreateGetRequest(QUrl(QLatin1String(kUrlSearch)), url_query);
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id]() { HandleSearchReply(reply, id); }); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id]() { HandleSearchReply(reply, id); });
qLog(Debug) << name_ << "Sending request for" << url_query.query();
} }
GeniusLyricsProvider::JsonObjectResult GeniusLyricsProvider::ParseJsonObject(QNetworkReply *reply) { GeniusLyricsProvider::JsonObjectResult GeniusLyricsProvider::ParseJsonObject(QNetworkReply *reply) {
@@ -302,10 +302,8 @@ void GeniusLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id)
const QString artist = primary_artist["name"_L1].toString(); const QString artist = primary_artist["name"_L1].toString();
const QString title = object_result["title"_L1].toString(); const QString title = object_result["title"_L1].toString();
// Ignore results where both the artist and title don't match. // Ignore results where the artist or title don't begin or end the same
if (!artist.startsWith(search->request.albumartist, Qt::CaseInsensitive) && if (!StartsOrEndsMatch(artist, search->request.artist) || !StartsOrEndsMatch(title, search->request.title)) {
!artist.startsWith(search->request.artist, Qt::CaseInsensitive) &&
!title.startsWith(search->request.title, Qt::CaseInsensitive)) {
continue; continue;
} }
@@ -323,6 +321,12 @@ void GeniusLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id)
QNetworkReply *new_reply = CreateGetRequest(url); QNetworkReply *new_reply = CreateGetRequest(url);
QObject::connect(new_reply, &QNetworkReply::finished, this, [this, new_reply, search, url]() { HandleLyricReply(new_reply, search->id, url); }); QObject::connect(new_reply, &QNetworkReply::finished, this, [this, new_reply, search, url]() { HandleLyricReply(new_reply, search->id, url); });
qLog(Debug) << name_ << "Sending request for" << url;
// If full match, don't bother iterating further
if (artist == search->request.albumartist && artist == search->request.artist && title == search->request.title) {
break;
}
} }
} }
@@ -363,12 +367,18 @@ void GeniusLyricsProvider::HandleLyricReply(QNetworkReply *reply, const int sear
return; return;
} }
const QString content = QString::fromUtf8(data); static const QRegularExpression start_tag(u"<div[^>]*>"_s);
QString lyrics = HtmlLyricsProvider::ParseLyricsFromHTML(content, QRegularExpression(u"<div[^>]*>"_s), QRegularExpression(u"<\\/div>"_s), QRegularExpression(u"<div data-lyrics-container=[^>]+>"_s), true); static const QRegularExpression end_tag(u"<\\/div>"_s);
if (lyrics.isEmpty()) { static const QRegularExpression lyrics_start(u"<div data-lyrics-container=[^>]+>"_s);
lyrics = HtmlLyricsProvider::ParseLyricsFromHTML(content, QRegularExpression(u"<div[^>]*>"_s), QRegularExpression(u"<\\/div>"_s), QRegularExpression(u"<div class=\"lyrics\">"_s), true);
}
static const QRegularExpression regex_html_tag_span_trans(u"<span class=\"LyricsHeader__Translations[^>]*>[^<]*</span>"_s);
static const QRegularExpression regex_html_tag_div_ellipsis(u"<div class=\"LyricsHeader__TextEllipsis[^>]*>[^<]*</div>"_s);
static const QRegularExpression regex_html_tag_span_contribs(u"<span class=\"ContributorsCreditSong__Contributors[^>]*>[^<]*</span>"_s);
static const QRegularExpression regex_html_tag_div_bio(u"<div class=\"SongBioPreview__Container[^>]*>.*?</div>"_s);
static const QRegularExpression regex_html_tag_h2(u"<h2 [^>]*>[^<]*</h2>"_s);
static const QList<QRegularExpression> regex_removes{ regex_html_tag_span_trans, regex_html_tag_div_ellipsis, regex_html_tag_span_contribs, regex_html_tag_div_bio, regex_html_tag_h2 };
const QString lyrics = HtmlLyricsProvider::ParseLyricsFromHTML(QString::fromUtf8(data), start_tag, end_tag, lyrics_start, true, regex_removes);
if (!lyrics.isEmpty()) { if (!lyrics.isEmpty()) {
LyricsSearchResult result(lyrics); LyricsSearchResult result(lyrics);
result.artist = lyric.artist; result.artist = lyric.artist;
@@ -404,3 +414,17 @@ void GeniusLyricsProvider::EndSearch(const int id, const LyricsSearchRequest &re
Q_EMIT SearchFinished(id, results); Q_EMIT SearchFinished(id, results);
} }
bool GeniusLyricsProvider::StartsOrEndsMatch(QString s, QString t) {
constexpr Qt::CaseSensitivity cs = Qt::CaseInsensitive;
static const QRegularExpression puncts_regex(u"[!,.:;]"_s);
static const QRegularExpression quotes_regex(u"[´`]"_s);
s.remove(puncts_regex).replace(quotes_regex, u"'"_s);
t.remove(puncts_regex).replace(quotes_regex, u"'"_s);
return (s.compare(t, cs) == 0 && !s.isEmpty()) || (!s.isEmpty() && !t.isEmpty() && (s.startsWith(t, cs) || t.startsWith(s, cs) || s.endsWith(t, cs) || t.endsWith(s, cs)));
}

View File

@@ -79,6 +79,9 @@ class GeniusLyricsProvider : public JsonLyricsProvider {
void HandleSearchReply(QNetworkReply *reply, const int id); void HandleSearchReply(QNetworkReply *reply, const int id);
void HandleLyricReply(QNetworkReply *reply, const int search_id, const QUrl &url); void HandleLyricReply(QNetworkReply *reply, const int search_id, const QUrl &url);
private:
static bool StartsOrEndsMatch(QString s, QString t);
private: private:
OAuthenticator *oauth_; OAuthenticator *oauth_;
mutable QMutex mutex_access_token_; mutable QMutex mutex_access_token_;

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