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 --install build
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
@@ -156,7 +156,7 @@ jobs:
strategy:
fail-fast: false
matrix:
fedora_version: [ '39', '40', '41', '42' ]
fedora_version: [ '41', '42', '43' ]
container:
image: fedora:${{matrix.fedora_version}}
steps:
@@ -209,7 +209,7 @@ jobs:
sparsehash-devel
rapidjson-devel
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
@@ -307,7 +307,7 @@ jobs:
- name: Remove files
run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*}
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
@@ -409,7 +409,7 @@ jobs:
cmake --build build --config Release --parallel 4
cmake --install build
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
@@ -507,7 +507,7 @@ jobs:
cmake --build build --config Release --parallel 4
cmake --install build
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
@@ -538,7 +538,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ubuntu_version: [ 'noble', 'oracular', 'plucky' ]
ubuntu_version: [ 'noble', 'plucky' ]
container:
image: ubuntu:${{matrix.ubuntu_version}}
steps:
@@ -599,7 +599,7 @@ jobs:
cmake --build build --config Release --parallel 4
cmake --install build
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
@@ -631,7 +631,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ubuntu_version: [ 'noble', 'oracular', 'plucky' ]
ubuntu_version: [ 'noble', 'plucky' ]
container:
image: ubuntu:${{matrix.ubuntu_version}}
steps:
@@ -691,7 +691,7 @@ jobs:
DEBIAN_FRONTEND: noninteractive
run: apt install -y keyboxd
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
@@ -727,13 +727,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
- name: Build FreeBSD
id: build-freebsd
uses: vmactions/freebsd-vm@v1.2.0
uses: vmactions/freebsd-vm@v1.2.3
with:
usesh: true
mem: 4096
@@ -752,13 +752,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
- name: Build OpenBSD
id: build-openbsd
uses: vmactions/openbsd-vm@v1.1.7
uses: vmactions/openbsd-vm@v1.2.0
with:
usesh: true
mem: 4096
@@ -788,7 +788,7 @@ jobs:
- name: Set MACOSX_DEPLOYMENT_TARGET
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
echo "Using macOS SDK ${i}"
echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV
@@ -818,7 +818,7 @@ jobs:
rm -f uninstall.sh
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
@@ -946,7 +946,7 @@ jobs:
- name: Set MACOSX_DEPLOYMENT_TARGET
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
echo "Using macOS SDK ${i}"
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
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
@@ -1072,7 +1072,7 @@ jobs:
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
@@ -1246,12 +1246,42 @@ jobs:
build-windows-msvc:
name: Build Windows MSVC
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: windows-2022
strategy:
fail-fast: false
matrix:
arch: [ 'x86', 'x86_64' ]
buildtype: [ 'release' ]
include:
- 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:
- name: Set prefix path
@@ -1265,6 +1295,20 @@ jobs:
shell: bash
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
shell: cmd
run: choco install --no-progress rsync
@@ -1347,11 +1391,11 @@ jobs:
uses: ilammy/msvc-dev-cmd@v1
with:
arch: ${{matrix.arch}}
sdk: 10.0.20348.0
sdk: ${{env.sdk_version}}
vsversion: 2022
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
@@ -1364,15 +1408,18 @@ jobs:
shell: cmd
run: cmake -E make_directory build
- name: Set ENABLE_WIN32_CONSOLE (debug)
if: matrix.buildtype == 'debug'
- name: Set ENABLE_WIN32_CONSOLE
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)
if: matrix.buildtype == 'release'
- name: Set ENABLE_SPOTIFY
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
shell: cmd
@@ -1384,14 +1431,14 @@ jobs:
-DCMAKE_BUILD_TYPE="${{env.cmake_buildtype}}"
-DCMAKE_PREFIX_PATH="${{env.prefix_path_forwardslash}}/lib/cmake"
-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"
-DICU_ROOT="${{env.prefix_path_forwardslash}}"
-DENABLE_GIO=OFF
-DENABLE_AUDIOCD=OFF
-DENABLE_MTP=OFF
-DENABLE_GPOD=OFF
-DENABLE_SPOTIFY=ON
-DENABLE_SPOTIFY=${{env.enable_spotify}}
- name: Run Make
shell: cmd
@@ -1460,11 +1507,14 @@ jobs:
run: copy ${{env.prefix_path_backslash}}\lib\gstreamer-1.0\*.dll .\gstreamer-plugins\
- name: Download copydlldeps.sh
if: matrix.arch != 'arm64'
shell: bash
working-directory: build
run: curl -f -O -L https://raw.githubusercontent.com/strawberrymusicplayer/strawberry-mxe/master/tools/copydlldeps.sh
- name: Copy dependencies
# copydlldeps.sh doesn't work with arm64 binaries.
if: matrix.arch != 'arm64'
shell: bash
working-directory: build
run: >
@@ -1481,6 +1531,12 @@ jobs:
-F ./gstreamer-plugins
-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
shell: cmd
working-directory: build
@@ -1581,11 +1637,11 @@ jobs:
DEBIAN_FRONTEND: noninteractive
run: sudo apt install -y git rsync
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
path: artifacts
- name: SSH Setup
@@ -1629,7 +1685,7 @@ jobs:
DEBIAN_FRONTEND: noninteractive
run: sudo apt install -y git jq gh
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Show release assets
@@ -1637,7 +1693,7 @@ jobs:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
run: gh release view "${{github.event.release.tag_name}}" --json assets | jq -r '.assets[].name'
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
path: artifacts
- 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 <fcntl.h>
@@ -28,28 +51,34 @@ static int MsgFlags = 0;
#endif
static const char *GetTempPath() {
const char *temp = getenv("XDG_RUNTIME_DIR");
temp = temp ? temp : getenv("TMPDIR");
temp = temp ? temp : getenv("TMP");
temp = temp ? temp : getenv("TEMP");
temp = temp ? temp : "/tmp";
return temp;
}
/*static*/ BaseConnection *BaseConnection::Create() {
BaseConnection *BaseConnection::Create() {
PipeAddr.sun_family = AF_UNIX;
return &Connection;
}
/*static*/ void BaseConnection::Destroy(BaseConnection *&c) {
auto self = reinterpret_cast<BaseConnectionUnix *>(c);
void BaseConnection::Destroy(BaseConnection *&c) {
auto self = reinterpret_cast<BaseConnectionUnix*>(c);
self->Close();
c = nullptr;
}
bool BaseConnection::Open() {
const char *tempPath = GetTempPath();
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
self->sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (self->sock == -1) {
return false;
@@ -61,8 +90,7 @@ bool BaseConnection::Open() {
#endif
for (int pipeNum = 0; pipeNum < 10; ++pipeNum) {
snprintf(
PipeAddr.sun_path, sizeof(PipeAddr.sun_path), "%s/discord-ipc-%d", tempPath, pipeNum);
snprintf(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));
if (err == 0) {
self->isOpen = true;
@@ -70,10 +98,13 @@ bool BaseConnection::Open() {
}
}
self->Close();
return false;
}
bool BaseConnection::Close() {
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
if (self->sock == -1) {
return false;
@@ -81,11 +112,14 @@ bool BaseConnection::Close() {
close(self->sock);
self->sock = -1;
self->isOpen = false;
return true;
}
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) {
return false;
@@ -95,11 +129,14 @@ bool BaseConnection::Write(const void *data, size_t length) {
if (sentBytes < 0) {
Close();
}
return sentBytes == static_cast<ssize_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) {
return false;
@@ -115,8 +152,9 @@ bool BaseConnection::Read(void *data, size_t length) {
else if (res == 0) {
Close();
}
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 NOMCX
#define NOSERVICE
#define NOIME
#include <cassert>
#include <windows.h>
@@ -19,24 +43,26 @@ struct BaseConnectionWin : public BaseConnection {
static BaseConnectionWin Connection;
/*static*/ BaseConnection *BaseConnection::Create() {
BaseConnection *BaseConnection::Create() {
return &Connection;
}
/*static*/ void BaseConnection::Destroy(BaseConnection *&c) {
void BaseConnection::Destroy(BaseConnection *&c) {
auto self = reinterpret_cast<BaseConnectionWin*>(c);
self->Close();
c = nullptr;
}
bool BaseConnection::Open() {
wchar_t pipeName[] { L"\\\\?\\pipe\\discord-ipc-0" };
const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
pipeName[pipeDigit] = L'0';
auto self = reinterpret_cast<BaseConnectionWin *>(this);
for (;;) {
self->pipe = ::CreateFileW(
pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
self->pipe = ::CreateFileW(pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (self->pipe != INVALID_HANDLE_VALUE) {
self->isOpen = true;
return true;
@@ -57,17 +83,22 @@ bool BaseConnection::Open() {
}
return false;
}
}
bool BaseConnection::Close() {
auto self = reinterpret_cast<BaseConnectionWin *>(this);
::CloseHandle(self->pipe);
self->pipe = INVALID_HANDLE_VALUE;
self->isOpen = false;
return true;
}
bool BaseConnection::Write(const void *data, size_t length) {
if (length == 0) {
return true;
}
@@ -85,11 +116,13 @@ bool BaseConnection::Write(const void *data, size_t length) {
}
const DWORD bytesLength = static_cast<DWORD>(length);
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) {
assert(data);
if (!data) {
return false;
@@ -119,8 +152,9 @@ bool BaseConnection::Read(void *data, size_t length) {
else {
Close();
}
return false;
}
} // 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_register.h"
@@ -5,6 +28,7 @@
#define NOMCX
#define NOSERVICE
#define NOIME
#include <windows.h>
#include <psapi.h>
#include <cstdio>
@@ -46,12 +70,8 @@ static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat,
#endif
#define RegSetKeyValueW regset
static LSTATUS regset(HKEY hkey,
LPCWSTR subkey,
LPCWSTR name,
DWORD type,
const void *data,
DWORD len) {
static LSTATUS regset(HKEY hkey, LPCWSTR subkey, LPCWSTR name, DWORD type, const void *data, DWORD len) {
HKEY htkey = hkey, hsubkey = nullptr;
LSTATUS ret;
if (subkey && subkey[0]) {
@@ -64,16 +84,18 @@ static LSTATUS regset(HKEY hkey,
if (hsubkey && hsubkey != hkey)
RegCloseKey(hsubkey);
return ret;
}
static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *command) {
// 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>://
// 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);
wchar_t openCommand[1024];
wchar_t openCommand[1024]{};
if (command && command[0]) {
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);
}
wchar_t protocolName[64];
wchar_t protocolName[64]{};
StringCbPrintfW(protocolName, sizeof(protocolName), L"discord-%s", applicationId);
wchar_t protocolDescription[128];
StringCbPrintfW(
protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
wchar_t protocolDescription[128]{};
StringCbPrintfW(protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
wchar_t urlProtocol = 0;
wchar_t keyName[256];
wchar_t keyName[256]{};
StringCbPrintfW(keyName, sizeof(keyName), L"Software\\Classes\\%s", protocolName);
HKEY key;
auto status =
RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);
auto status = RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);
if (status != ERROR_SUCCESS) {
fprintf(stderr, "Error creating key\n");
return;
@@ -102,8 +122,7 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
DWORD len;
LSTATUS result;
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
result =
RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
result = RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
if (FAILED(result)) {
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");
}
result = RegSetKeyValueW(
key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
result = RegSetKeyValueW(key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
if (FAILED(result)) {
fprintf(stderr, "Error writing icon\n");
}
len = static_cast<DWORD>(lstrlenW(openCommand) + 1);
result = RegSetKeyValueW(
key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
result = RegSetKeyValueW(key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
if (FAILED(result)) {
fprintf(stderr, "Error writing command\n");
}
RegCloseKey(key);
}
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);
wchar_t openCommand[1024];
wchar_t openCommand[1024]{};
const wchar_t *wcommand = nullptr;
if (command && command[0]) {
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);
}
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"
#include "backoff.h"
#include "discord_register.h"
#include "msg_queue.h"
#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 <atomic>
#include <chrono>
#include <mutex>
#include <condition_variable>
#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 MessageQueueSize { 8 };
@@ -67,14 +92,12 @@ static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
static MsgQueue<User, JoinQueueSize> JoinAskQueue;
static User connectedUser;
// 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
// 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
static Backoff ReconnectTimeMs(500, 60 * 1000);
static auto NextConnect = std::chrono::system_clock::now();
static int Pid { 0 };
static int Nonce { 1 };
static void Discord_UpdateConnection(void);
class IoThreadHolder {
private:
std::atomic_bool keepRunning { true };
@@ -108,14 +131,55 @@ class IoThreadHolder {
~IoThreadHolder() { Stop(); }
};
static IoThreadHolder *IoThread { nullptr };
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) {
return;
}
@@ -217,54 +281,18 @@ static void Discord_UpdateConnection(void) {
SendQueue.CommitSend();
}
}
}
static void SignalIOActivity() {
if (IoThread != nullptr) {
IoThread->Notify();
}
}
extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
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();
if (IoThread == nullptr) {
return;
}
if (autoRegister) {
if (optionalSteamId && optionalSteamId[0]) {
Discord_RegisterSteamGame(applicationId, optionalSteamId);
}
else {
Discord_Register(applicationId, nullptr);
}
Discord_Register(applicationId, nullptr);
}
Pid = GetProcessId();
@@ -323,9 +351,11 @@ extern "C" void Discord_Initialize(const char *applicationId,
};
IoThread->Start();
}
extern "C" void Discord_Shutdown(void) {
extern "C" void Discord_Shutdown() {
if (!Connection) {
return;
}
@@ -341,16 +371,19 @@ extern "C" void Discord_Shutdown(void) {
}
RpcConnection::Destroy(Connection);
}
extern "C" void Discord_UpdatePresence(const DiscordRichPresence *presence) {
{
std::lock_guard<std::mutex> guard(PresenceMutex);
QueuedPresence.length = JsonWriteRichPresenceObj(
QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
QueuedPresence.length = JsonWriteRichPresenceObj(QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
UpdatePresence.exchange(true);
}
SignalIOActivity();
}
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) {
// if we are not connected, let's not batch up stale messages for later
if (!Connection || !Connection->IsOpen()) {
return;
}
auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) {
qmessage->length =
JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
qmessage->length = JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
SendQueue.CommitAdd();
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
// 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.
@@ -380,8 +415,8 @@ extern "C" void Discord_RunCallbacks(void) {
return;
}
bool wasDisconnected = WasJustDisconnected.exchange(false);
bool isConnected = Connection->IsOpen();
const bool wasDisconnected = WasJustDisconnected.exchange(false);
const bool isConnected = Connection->IsOpen();
if (isConnected) {
// if we are connected, disconnect cb first
@@ -394,10 +429,7 @@ extern "C" void Discord_RunCallbacks(void) {
if (WasJustConnected.exchange(false)) {
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.ready) {
DiscordUser du { connectedUser.userId,
connectedUser.username,
connectedUser.discriminator,
connectedUser.avatar };
DiscordUser du { connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
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
// not it should be trivial for the implementer to make a queue themselves.
while (JoinAskQueue.HavePendingSends()) {
auto req = JoinAskQueue.GetNextSendMessage();
const auto req = JoinAskQueue.GetNextSendMessage();
{
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.joinRequest) {
@@ -447,9 +479,11 @@ extern "C" void Discord_RunCallbacks(void) {
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
}
}
}
extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
if (newHandlers) {
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \
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);
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 {
static const int RpcVersion = 1;
static RpcConnection Instance;
/*static*/ RpcConnection *RpcConnection::Create(const char *applicationId) {
RpcConnection *RpcConnection::Create(const char *applicationId) {
Instance.connection = BaseConnection::Create();
StringCopy(Instance.appId, applicationId);
return &Instance;
}
/*static*/ void RpcConnection::Destroy(RpcConnection *&c) {
void RpcConnection::Destroy(RpcConnection *&c) {
c->Close();
BaseConnection::Destroy(c->connection);
c = nullptr;
}
void RpcConnection::Open() {
if (state == State::Connected) {
return;
}
@@ -51,17 +79,21 @@ void RpcConnection::Open() {
Close();
}
}
}
void RpcConnection::Close() {
if (onDisconnect && (state == State::Connected || state == State::SentHandshake)) {
onDisconnect(lastErrorCode, lastErrorMessage);
}
connection->Close();
state = State::Disconnected;
}
bool RpcConnection::Write(const void *data, size_t length) {
sendFrame.opcode = Opcode::Frame;
memcpy(sendFrame.message, data, length);
sendFrame.length = static_cast<uint32_t>(length);
@@ -69,14 +101,17 @@ bool RpcConnection::Write(const void *data, size_t length) {
Close();
return false;
}
return true;
}
bool RpcConnection::Read(JsonDocument &message) {
if (state != State::Connected && state != State::SentHandshake) {
return false;
}
MessageFrame readFrame;
MessageFrame readFrame{};
for (;;) {
bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader));
if (!didRead) {
@@ -127,7 +162,7 @@ bool RpcConnection::Read(JsonDocument &message) {
return false;
}
}
}
} // 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"
namespace discord_rpc {
template<typename T>
void NumberToString(char *dest, T number) {
if (!number) {
*dest++ = '0';
*dest++ = 0;
@@ -26,6 +50,7 @@ void NumberToString(char *dest, T number) {
*dest++ = temp[place];
}
*dest = 0;
}
// it's ever so slightly faster to not have to strlen the key
@@ -62,24 +87,25 @@ struct WriteArray {
template<typename T>
void WriteOptionalString(JsonWriter &w, T &k, const char *value) {
if (value && value[0]) {
w.Key(k, sizeof(T) - 1);
w.String(value);
}
}
static void JsonWriteNonce(JsonWriter &writer, int nonce) {
static void JsonWriteNonce(JsonWriter &writer, const int nonce) {
WriteKey(writer, "nonce");
char nonceBuffer[32];
NumberToString(nonceBuffer, nonce);
writer.String(nonceBuffer);
}
size_t JsonWriteRichPresenceObj(char *dest,
size_t maxLen,
int nonce,
int pid,
const DiscordRichPresence *presence) {
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence) {
JsonWriter writer(dest, maxLen);
{
@@ -102,6 +128,9 @@ size_t JsonWriteRichPresenceObj(char *dest,
if (presence->type >= 0 && presence->type <= 5) {
WriteKey(writer, "type");
writer.Int(presence->type);
WriteKey(writer, "status_display_type");
writer.Int(presence->status_display_type);
}
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) {
JsonWriter writer(dest, maxLen);
{
@@ -179,9 +209,11 @@ size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char
}
return writer.Size();
}
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
JsonWriter writer(dest, maxLen);
{
@@ -197,9 +229,11 @@ size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const cha
}
return writer.Size();
}
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
JsonWriter writer(dest, maxLen);
{
@@ -215,9 +249,11 @@ size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const c
}
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);
{
@@ -243,7 +279,7 @@ size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int rep
}
return writer.Size();
}
} // 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/stringbuffer.h>
#include <rapidjson/writer.h>
struct DiscordRichPresence;
namespace discord_rpc {
// 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);
// Commands
struct DiscordRichPresence;
size_t JsonWriteRichPresenceObj(char *dest,
size_t maxLen,
int nonce,
int pid,
const DiscordRichPresence *presence);
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence);
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);
@@ -149,35 +170,44 @@ class JsonDocument : public JsonDocumentBase {
using JsonValue = rapidjson::GenericValue<UTF8, PoolAllocator>;
inline JsonValue *GetObjMember(JsonValue *obj, const char *name) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsObject()) {
return &member->value;
}
}
return nullptr;
}
inline int GetIntMember(JsonValue *obj, const char *name, int notFoundDefault = 0) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsInt()) {
return member->value.GetInt();
}
}
return notFoundDefault;
}
inline const char *GetStrMember(JsonValue *obj,
const char *name,
const char *notFoundDefault = nullptr) {
inline const char *GetStrMember(JsonValue *obj, const char *name, const char *notFoundDefault = nullptr) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsString()) {
return member->value.GetString();
}
}
return notFoundDefault;
}
} // 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()
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()
if(APPLE OR WIN32)
@@ -1494,7 +1503,7 @@ endif()
if(HAVE_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()
if(HAVE_TRANSLATIONS)
@@ -1554,9 +1563,10 @@ target_link_libraries(strawberry_lib PUBLIC
$<$<BOOL:${HAVE_MTP}>:PkgConfig::LIBMTP>
$<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF>
$<$<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>
KDAB::kdsingleapplication
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
)
if(APPLE)
@@ -1575,10 +1585,6 @@ if(APPLE)
endif()
endif()
if(HAVE_DISCORD_RPC)
target_link_libraries(strawberry_lib PRIVATE discord-rpc)
endif()
target_link_libraries(strawberry PUBLIC strawberry_lib)
if(NOT APPLE)

View File

@@ -2,6 +2,68 @@ Strawberry Music Player
=======================
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):
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
* Fetch tags from MusicBrainz
* Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/) and [Spotify](https://www.spotify.com/)
* Song lyrics from [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics.com](https://www.lololyrics.com/), [songlyrics.com](https://www.songlyrics.com/), [azlyrics.com](https://www.azlyrics.com/) 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
* Audio analyzer
* Audio equalizer

View File

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

View File

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

View File

@@ -12,9 +12,13 @@ CREATE TABLE device_%deviceid_subdirectories (
CREATE TABLE device_%deviceid_songs (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -22,7 +26,9 @@ CREATE TABLE device_%deviceid_songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -86,7 +92,11 @@ CREATE TABLE device_%deviceid_songs (
musicbrainz_work_id TEXT,
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);
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;
INSERT INTO schema_version (version) VALUES (20);
INSERT INTO schema_version (version) VALUES (21);
CREATE TABLE IF NOT EXISTS directories (
path TEXT NOT NULL,
@@ -20,9 +20,13 @@ CREATE TABLE IF NOT EXISTS subdirectories (
CREATE TABLE IF NOT EXISTS songs (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -30,7 +34,9 @@ CREATE TABLE IF NOT EXISTS songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -94,16 +100,24 @@ CREATE TABLE IF NOT EXISTS songs (
musicbrainz_work_id TEXT,
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 (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -111,7 +125,9 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -175,16 +191,24 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
musicbrainz_work_id TEXT,
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 (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc 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,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -256,16 +282,24 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
musicbrainz_work_id TEXT,
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 (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc 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,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -337,16 +373,24 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
musicbrainz_work_id TEXT,
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 (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -354,7 +398,9 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -418,16 +464,24 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
musicbrainz_work_id TEXT,
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 (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc 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,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -499,16 +555,24 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
musicbrainz_work_id TEXT,
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 (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc 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,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -580,16 +646,24 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
musicbrainz_work_id TEXT,
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 (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -597,7 +671,9 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -661,16 +737,24 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
musicbrainz_work_id TEXT,
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 (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc 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,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -742,16 +828,24 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
musicbrainz_work_id TEXT,
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 (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc 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,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -823,16 +919,24 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
musicbrainz_work_id TEXT,
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 (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -840,7 +944,9 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -904,7 +1010,11 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
musicbrainz_work_id TEXT,
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,
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER,
disc INTEGER,
year INTEGER,
@@ -941,7 +1055,9 @@ CREATE TABLE IF NOT EXISTS playlist_items (
genre TEXT,
compilation INTEGER DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -1005,7 +1121,11 @@ CREATE TABLE IF NOT EXISTS playlist_items (
musicbrainz_work_id TEXT,
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_albumartistsort ON songs (albumartistsort);
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_albumsort ON songs (album);
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;

2
debian/control vendored
View File

@@ -60,7 +60,7 @@ Description: music player and music collection organizer
- Edit tags on audio files
- Automatically retrieve tags from MusicBrainz
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com 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 equalizer
- 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>Automatically retrieve tags from MusicBrainz</li>
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
<li>Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com 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>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>
@@ -51,6 +51,10 @@
</screenshots>
<update_contact>eclipseo@fedoraproject.org</update_contact>
<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.8" date="2025-04-05"/>
<release version="1.2.7" date="2025-01-31"/>

View File

@@ -13,8 +13,7 @@ TryExec=strawberry
Icon=strawberry
Terminal=false
Categories=AudioVideo;Player;Qt;Audio;
Keywords=Audio;Player;
StartupNotify=false
Keywords=Audio;Player;Clementine;
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
Actions=Play-Pause;Stop;StopAfterCurrent;Previous;Next;

View File

@@ -29,7 +29,7 @@ Features:
.br
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
.br
- Song lyrics from 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
- Support for multiple backends
.br

View File

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

View File

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

View File

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

View File

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

View File

@@ -78,6 +78,8 @@ CollectionLibrary::CollectionLibrary(const SharedPtr<Database> database,
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();
}

View File

@@ -1,6 +1,6 @@
/*
* 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
* it under the terms of the GNU General Public License as published by
@@ -54,6 +54,7 @@
#include "includes/scoped_ptr.h"
#include "includes/shared_ptr.h"
#include "constants/collectionsettings.h"
#include "core/logging.h"
#include "core/standardpaths.h"
#include "core/database.h"
@@ -71,12 +72,12 @@
#include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h"
#include "covermanager/albumcoverloader.h"
#include "constants/collectionsettings.h"
using namespace std::chrono_literals;
using namespace Qt::Literals::StringLiterals;
const int CollectionModel::kPrettyCoverSize = 32;
namespace {
constexpr char kPixmapDiskCacheDir[] = "pixmapcache";
constexpr char kVariousArtists[] = QT_TR_NOOP("Various artists");
@@ -88,7 +89,6 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
albumcover_loader_(albumcover_loader),
dir_model_(new CollectionDirectoryModel(backend, this)),
filter_(new CollectionFilter(this)),
timer_reload_(new QTimer(this)),
timer_update_(new QTimer(this)),
icon_artist_(IconLoader::Load(u"folder-sound"_s)),
use_disk_cache_(false),
@@ -130,10 +130,6 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
backend_->UpdateTotalArtistCountAsync();
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_->setInterval(20ms);
QObject::connect(timer_update_, &QTimer::timeout, this, &CollectionModel::ProcessUpdate);
@@ -191,13 +187,9 @@ void CollectionModel::EndReset() {
}
void CollectionModel::Reload() {
void CollectionModel::ResetInternal() {
loading_ = true;
if (timer_reload_->isActive()) {
timer_reload_->stop();
}
updates_.clear();
options_active_ = options_current_;
@@ -211,22 +203,15 @@ void CollectionModel::Reload() {
}
void CollectionModel::ScheduleReset() {
if (!timer_reload_->isActive()) {
timer_reload_->start();
}
}
void CollectionModel::ReloadSettings() {
Settings settings;
settings.beginGroup(CollectionSettings::kSettingsGroup);
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 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();
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 ||
show_dividers != options_current_.show_dividers ||
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_dividers = show_dividers;
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();
}
@@ -421,10 +408,15 @@ void CollectionModel::RemoveSongs(const SongList &songs) {
void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs) {
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 (type == CollectionModelUpdate::Type::Reset) {
updates_.enqueue(CollectionModelUpdate(type));
}
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()) {
@@ -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) {
ScheduleUpdate(CollectionModelUpdate::Type::Add, songs);
@@ -465,6 +463,9 @@ void CollectionModel::ProcessUpdate() {
}
switch (update.type) {
case CollectionModelUpdate::Type::Reset:
ResetInternal();
break;
case CollectionModelUpdate::Type::AddReAddOrUpdate:
AddReAddOrUpdateSongsInternal(update.songs);
break;
@@ -699,7 +700,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
QString divider_key;
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_nodes_.contains(divider_key)) {
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_key = container_key;
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()) {
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) {
case GroupBy::AlbumArtist:
return SortTextForArtist(song.effective_albumartist(), sort_skips_articles);
return SortTextForName(song.effective_albumartistsort(), sort_skip_articles_for_artists);
case GroupBy::Artist:
return SortTextForArtist(song.artist(), sort_skips_articles);
return SortTextForName(song.effective_artistsort(), sort_skip_articles_for_artists);
case GroupBy::Album:
return SortText(song.album());
return SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums);
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:
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:
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:
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:
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:
return SortTextForNumber(std::max(0, song.disc()));
case GroupBy::Year:
@@ -1094,13 +1095,13 @@ QString CollectionModel::SortText(const GroupBy group_by, const Song &song, cons
case GroupBy::OriginalYear:
return SortTextForNumber(std::max(0, song.effective_originalyear())) + QLatin1Char(' ');
case GroupBy::Genre:
return SortTextForArtist(song.genre(), sort_skips_articles);
return SortText(song.genre());
case GroupBy::Composer:
return SortTextForArtist(song.composer(), sort_skips_articles);
return SortTextForName(song.effective_composersort(), sort_skip_articles_for_artists);
case GroupBy::Performer:
return SortTextForArtist(song.performer(), sort_skips_articles);
return SortTextForName(song.effective_performersort(), sort_skip_articles_for_artists);
case GroupBy::Grouping:
return SortTextForArtist(song.grouping(), sort_skips_articles);
return SortText(song.grouping());
case GroupBy::FileType:
return song.TextForFiletype();
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);
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;
return sort_skip_articles ? SkipArticles(SortText(name)) : SortText(name);
}
@@ -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) {
return song1.url() != song2.url() ||

View File

@@ -1,6 +1,6 @@
/*
* 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
* 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_pretty_covers(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) {}
Grouping group_by;
bool show_dividers;
bool show_pretty_covers;
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;
CollectionFilterOptions filter_options;
};
@@ -176,20 +178,21 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
QMimeData *mimeData(const QModelIndexList &indexes) const override;
// 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 PrettyYearAlbum(const int year, const QString &album);
static QString PrettyAlbumDisc(const QString &album, const int disc);
static QString PrettyYearAlbumDisc(const int year, const QString &album, const int disc);
static QString PrettyDisc(const int disc);
static QString PrettyFormat(const Song &song);
QString SortText(const GroupBy group_by, const 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 SortTextForName(const QString &name, const bool sort_skip_articles);
static QString SortTextForNumber(const int number);
static QString SortTextForArtist(QString artist, const bool skip_articles);
static QString SortTextForSong(const Song &song);
static QString SortTextForYear(const int year);
static QString SortTextForBitrate(const int bitrate);
static QString SkipArticles(QString name);
static bool IsSongTitleDataChanged(const Song &song1, const Song &song2);
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;
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 ScheduleUpdateSongs(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);
private Q_SLOTS:
void Reload();
void ResetInternal();
void ScheduleReset();
void ProcessUpdate();
void LoadSongsFromSqlAsyncFinished();
@@ -278,7 +281,6 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
const SharedPtr<AlbumCoverLoader> albumcover_loader_;
CollectionDirectoryModel *dir_model_;
CollectionFilter *filter_;
QTimer *timer_reload_;
QTimer *timer_update_;
QPixmap pixmap_no_cover_;

View File

@@ -25,12 +25,13 @@
class CollectionModelUpdate {
public:
enum class Type {
Reset,
AddReAddOrUpdate,
Add,
Update,
Remove,
};
explicit CollectionModelUpdate(const Type _type, const SongList &_songs);
explicit CollectionModelUpdate(const Type _type, const SongList &_songs = SongList());
Type type;
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) {}
QUrl CollectionPlaylistItem::Url() const { return song_.url(); }
bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) {
int col = 0;
@@ -62,7 +60,7 @@ void CollectionPlaylistItem::Reload() {
qLog(Error) << "Could not reload file" << song_.url() << result.error_string();
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) {
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 &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;
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;
bool IsLocalCollectionItem() const override { return song_.source() == Song::Source::Collection; }
protected:
QVariant DatabaseValue(const DatabaseColumn database_column) const override;
Song DatabaseSongMetadata() const override { return Song(source_); }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
/*
* 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
* 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 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 kShowDividers[] = "show_dividers";
constexpr char kPrettyCovers[] = "pretty_covers";
constexpr char kVariousArtists[] = "various_artists";
constexpr char kSortSkipsArticles[] = "sort_skips_articles";
constexpr char kStartupScan[] = "startup_scan";
constexpr char kMonitor[] = "monitor";
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 kSkipArticlesForArtists[] = "skip_articles_for_artists";
constexpr char kSkipArticlesForAlbums[] = "skip_articles_for_albums";
constexpr char kShowSortText[] = "show_sort_text";
constexpr char kSettingsCacheSize[] = "cache_size";
constexpr char kSettingsCacheSizeUnit[] = "cache_size_unit";
constexpr char kSettingsDiskCacheEnable[] = "disk_cache_enable";

View File

@@ -30,7 +30,7 @@ constexpr char kFileFilter[] =
"*.aif *.aiff *.mka *.tta *.dsf *.dsd "
"*.cue *.m3u *.m3u8 *.pls *.xspf *.asxini "
"*.ac3 *.dts "
"*.mod *.s3m *.xm *.it"
"*.mod *.s3m *.xm *.it "
"*.spc *.vgm";
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 kStatusDisplayType[] = "StatusDisplayType";
enum class StatusDisplayType {
App = 0,
Artist,
Song
};
} // namespace
#endif // NOTIFICATIONSSETTINGS_H

View File

@@ -396,7 +396,7 @@ void ContextView::UpdateNoSong() {
void ContextView::NoSong() {
if (!widget_album_->isVisible()) {
if (!widget_album_->isVisibleTo(this)) {
widget_album_->show();
}
@@ -440,11 +440,11 @@ void ContextView::SetSong() {
label_stop_summary_->clear();
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_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_changed = true;
}

View File

@@ -50,7 +50,7 @@
using namespace Qt::Literals::StringLiterals;
const int Database::kSchemaVersion = 20;
const int Database::kSchemaVersion = 21;
namespace {
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.
if (command.contains(QLatin1String(kMagicAllSongsTables))) {
for (const QString &table : song_tables) {
// Another horrible hack: device songs tables don't have matching _fts tables, so if this command tries to touch one, ignore it.
if (table.startsWith("device_"_L1) && command.contains(QLatin1String(kMagicAllSongsTables) + "_fts"_L1)) {
continue;
}
qLog(Info) << "Updating" << table << "for" << kMagicAllSongsTables;
QString new_command(command);
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) {
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()));
}
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) {
const int http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
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));
}
}

View File

@@ -156,10 +156,8 @@
#include "lyrics/lyricsproviders.h"
#include "device/devicemanager.h"
#include "device/devicestatefiltermodel.h"
#ifndef Q_OS_WIN32
# include "device/deviceview.h"
# include "device/deviceviewcontainer.h"
#endif
#include "device/deviceview.h"
#include "device/deviceviewcontainer.h"
#include "transcoder/transcodedialog.h"
#include "settings/settingsdialog.h"
#include "constants/behavioursettings.h"
@@ -175,6 +173,7 @@
# include "constants/tidalsettings.h"
#endif
#ifdef HAVE_SPOTIFY
# include "spotify/spotifyservice.h"
# include "constants/spotifysettings.h"
#endif
#ifdef HAVE_QOBUZ
@@ -280,7 +279,7 @@ constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-
#endif // HAVE_QTSPARKLE
MainWindow::MainWindow(Application *app,
SharedPtr<SystemTrayIcon> tray_icon, OSDBase *osd,
SharedPtr<SystemTrayIcon> systemtrayicon, OSDBase *osd,
#ifdef HAVE_DISCORD_RPC
discord::RichPresence *discord_rich_presence,
#endif
@@ -292,7 +291,7 @@ MainWindow::MainWindow(Application *app,
thumbbar_(new Windows7ThumbBar(this)),
#endif
app_(app),
tray_icon_(tray_icon),
systemtrayicon_(systemtrayicon),
osd_(osd),
#ifdef HAVE_DISCORD_RPC
discord_rich_presence_(discord_rich_presence),
@@ -310,9 +309,7 @@ MainWindow::MainWindow(Application *app,
context_view_(new ContextView(this)),
collection_view_(new CollectionViewContainer(this)),
file_view_(new FileView(this)),
#ifndef Q_OS_WIN32
device_view_(new DeviceViewContainer(this)),
#endif
playlist_list_(new PlaylistListContainer(this)),
queue_view_(new QueueView(this)),
settings_dialog_(std::bind(&MainWindow::CreateSettingsDialog, this)),
@@ -375,9 +372,7 @@ MainWindow::MainWindow(Application *app,
playlist_move_to_collection_(nullptr),
playlist_open_in_browser_(nullptr),
playlist_organize_(nullptr),
#ifndef Q_OS_WIN32
playlist_copy_to_device_(nullptr),
#endif
playlist_delete_(nullptr),
playlist_queue_(nullptr),
playlist_queue_play_next_(nullptr),
@@ -409,7 +404,11 @@ MainWindow::MainWindow(Application *app,
// Initialize the UI
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);
@@ -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(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"));
#ifndef Q_OS_WIN32
ui_->tabs->AddTab(device_view_, u"devices"_s, IconLoader::Load(u"device"_s, true, 0, 32), tr("Devices"));
#endif
#ifdef HAVE_SUBSONIC
ui_->tabs->AddTab(subsonic_view_, u"subsonic"_s, IconLoader::Load(u"subsonic"_s, true, 0, 32), tr("Subsonic"));
#endif
@@ -480,9 +477,7 @@ MainWindow::MainWindow(Application *app,
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());
#ifndef Q_OS_WIN32
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());
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::MoveToCollection, this, &MainWindow::MoveFilesToCollection);
QObject::connect(file_view_, &FileView::EditTags, this, &MainWindow::EditFileTags);
#ifndef Q_OS_WIN32
QObject::connect(file_view_, &FileView::CopyToDevice, this, &MainWindow::CopyFilesToDevice);
#endif
file_view_->SetTaskManager(app_->task_manager());
// 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::toggled, this, &MainWindow::ToggleSearchCoverAuto);
#ifndef Q_OS_WIN32
// Devices connections
QObject::connect(device_view_->view(), &DeviceView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
#endif
// Collection filter widget
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_->search_view(), &StreamingSearchView::OpenSettingsDialog, this, &MainWindow::OpenServiceSettingsDialog);
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
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_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);
#ifndef Q_OS_WIN32
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_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(&*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);
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::ScrobbleButtonVisibilityChanged, this, &MainWindow::ScrobbleButtonVisibilityChanged);
@@ -858,14 +848,14 @@ MainWindow::MainWindow(Application *app,
mac::SetApplicationHandler(this);
#endif
// 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);
QObject::connect(&*tray_icon_, &SystemTrayIcon::PlayPause, &*app_->player(), &Player::PlayPauseHelper);
QObject::connect(&*tray_icon_, &SystemTrayIcon::SeekForward, &*app_->player(), &Player::SeekForward);
QObject::connect(&*tray_icon_, &SystemTrayIcon::SeekBackward, &*app_->player(), &Player::SeekBackward);
QObject::connect(&*tray_icon_, &SystemTrayIcon::NextTrack, &*app_->player(), &Player::Next);
QObject::connect(&*tray_icon_, &SystemTrayIcon::PreviousTrack, &*app_->player(), &Player::Previous);
QObject::connect(&*tray_icon_, &SystemTrayIcon::ShowHide, this, &MainWindow::ToggleShowHide);
QObject::connect(&*tray_icon_, &SystemTrayIcon::ChangeVolume, this, &MainWindow::VolumeWheelEvent);
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(&*systemtrayicon_, &SystemTrayIcon::PlayPause, &*app_->player(), &Player::PlayPauseHelper);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::SeekForward, &*app_->player(), &Player::SeekForward);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::SeekBackward, &*app_->player(), &Player::SeekBackward);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::NextTrack, &*app_->player(), &Player::Next);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::PreviousTrack, &*app_->player(), &Player::Previous);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::ShowHide, this, &MainWindow::ToggleShowHide);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::ChangeVolume, this, &MainWindow::VolumeWheelEvent);
// Windows 7 thumbbar buttons
#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::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->setVisible(false);
#endif
@@ -1043,7 +1033,7 @@ MainWindow::MainWindow(Application *app,
show();
break;
case BehaviourSettings::StartupBehaviour::Hide:
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible()) {
if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible()) {
break;
}
[[fallthrough]];
@@ -1056,7 +1046,7 @@ MainWindow::MainWindow(Application *app,
was_minimized_ = settings_.value(MainWindowSettings::kMinimized, false).toBool();
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();
}
break;
@@ -1168,13 +1158,13 @@ void MainWindow::ReloadSettings() {
#ifdef Q_OS_MACOS
constexpr bool keeprunning_available = true;
#else
const bool systemtray_available = tray_icon_->IsSystemTrayAvailable();
const bool systemtray_available = systemtrayicon_->IsSystemTrayAvailable();
s.beginGroup(BehaviourSettings::kSettingsGroup);
const bool showtrayicon = s.value(BehaviourSettings::kShowTrayIcon, systemtray_available).toBool();
s.endGroup();
const bool keeprunning_available = systemtray_available && showtrayicon;
if (systemtray_available) {
tray_icon_->setVisible(showtrayicon);
systemtrayicon_->setVisible(showtrayicon);
}
if ((!showtrayicon || !systemtray_available) && !isVisible()) {
show();
@@ -1199,7 +1189,7 @@ void MainWindow::ReloadSettings() {
int iconsize = s.value(AppearanceSettings::kIconSizePlayControlButtons, 32).toInt();
s.endGroup();
tray_icon_->SetTrayiconProgress(trayicon_progress);
systemtrayicon_->SetTrayiconProgress(trayicon_progress);
#ifdef HAVE_DBUS
if (taskbar_progress_ && !taskbar_progress) {
@@ -1221,11 +1211,11 @@ void MainWindow::ReloadSettings() {
ui_->volume->SetEnabled(volume_control);
if (volume_control) {
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 {
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) {
app_->player()->Stop();
hide();
if (tray_icon_->IsSystemTrayAvailable()) {
tray_icon_->setVisible(false);
if (systemtrayicon_->IsSystemTrayAvailable()) {
systemtrayicon_->setVisible(false);
}
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_->button_love->setEnabled(false);
tray_icon_->LoveStateChanged(false);
systemtrayicon_->LoveStateChanged(false);
if (track_position_timer_->isActive()) {
track_position_timer_->stop();
@@ -1444,8 +1434,8 @@ void MainWindow::MediaStopped() {
track_slider_timer_->stop();
}
ui_->track_slider->SetStopped();
tray_icon_->SetProgress(0);
tray_icon_->SetStopped();
systemtrayicon_->SetProgress(0);
systemtrayicon_->SetStopped();
#ifdef HAVE_DBUS
if (taskbar_progress_) {
@@ -1477,7 +1467,7 @@ void MainWindow::MediaPaused() {
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_->track_slider->SetCanSeek(can_seek);
tray_icon_->SetPlaying(enable_play_pause);
systemtrayicon_->SetPlaying(enable_play_pause);
if (!track_position_timer_->isActive()) {
track_position_timer_->start();
@@ -1515,18 +1505,18 @@ void MainWindow::SendNowPlaying() {
// Send now playing to scrobble services
Playlist *playlist = app_->playlist_manager()->active();
if (app_->scrobbler()->enabled() && playlist && playlist->current_item() && playlist->current_item()->Metadata().is_metadata_good()) {
app_->scrobbler()->UpdateNowPlaying(playlist->current_item()->Metadata());
if (app_->scrobbler()->enabled() && playlist && playlist->current_item() && playlist->current_item()->EffectiveMetadata().is_metadata_good()) {
app_->scrobbler()->UpdateNowPlaying(playlist->current_item()->EffectiveMetadata());
ui_->action_love->setEnabled(true);
ui_->button_love->setEnabled(true);
tray_icon_->LoveStateChanged(true);
systemtrayicon_->LoveStateChanged(true);
}
}
void MainWindow::VolumeChanged(const uint volume) {
ui_->action_mute->setChecked(volume == 0);
tray_icon_->MuteButtonStateChanged(volume == 0);
systemtrayicon_->MuteButtonStateChanged(volume == 0);
}
void MainWindow::SongChanged(const Song &song) {
@@ -1536,7 +1526,7 @@ void MainWindow::SongChanged(const Song &song) {
song_playing_ = song;
song_ = song;
setWindowTitle(song.PrettyTitleWithArtist());
tray_icon_->SetProgress(0);
systemtrayicon_->SetProgress(0);
#ifdef HAVE_DBUS
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 (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 length = app_->player()->engine()->length_nanosec();
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) {
if (!exit_ && (!tray_icon_->IsSystemTrayAvailable() || !tray_icon_->isVisible() || !keep_running_)) {
if (!exit_ && (!systemtrayicon_->IsSystemTrayAvailable() || !systemtrayicon_->isVisible() || !keep_running_)) {
Exit();
}
@@ -1730,7 +1720,7 @@ void MainWindow::closeEvent(QCloseEvent *e) {
void MainWindow::SetHiddenInTray(const bool hidden) {
if (hidden && isVisible()) {
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible() && keep_running_) {
if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible() && keep_running_) {
close();
}
else {
@@ -1758,8 +1748,8 @@ void MainWindow::FilePathChanged(const QString &path) {
void MainWindow::Seeked(const qint64 microseconds) {
const qint64 position = microseconds / kUsecPerSec;
const qint64 length = app_->player()->GetCurrentItem()->Metadata().length_nanosec() / kNsecPerSec;
tray_icon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
const qint64 length = app_->player()->GetCurrentItem()->EffectiveMetadata().length_nanosec() / kNsecPerSec;
systemtrayicon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
#ifdef HAVE_DBUS
if (taskbar_progress_) {
@@ -1774,12 +1764,12 @@ void MainWindow::UpdateTrackPosition() {
PlaylistItemPtr item(app_->player()->GetCurrentItem());
if (!item) return;
const qint64 length = (item->Metadata().length_nanosec() / kNsecPerSec);
const qint64 length = (item->EffectiveMetadata().length_nanosec() / kNsecPerSec);
if (length <= 0) return;
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
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
if (taskbar_progress_) {
@@ -1788,12 +1778,12 @@ void MainWindow::UpdateTrackPosition() {
#endif
// 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();
if (playlist && !playlist->scrobbled()) {
const qint64 scrobble_point = (playlist->scrobble_point_nanosec() / kNsecPerSec);
if (position >= scrobble_point) {
app_->scrobbler()->Scrobble(item->Metadata(), scrobble_point);
app_->scrobbler()->Scrobble(item->EffectiveMetadata(), scrobble_point);
playlist->set_scrobbled(true);
}
}
@@ -1910,7 +1900,7 @@ void MainWindow::AddToPlaylistFromAction(QAction *action) {
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
if (!item) continue;
items << item;
songs << item->Metadata();
songs << item->EffectiveMetadata();
}
// 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());
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;
}
else if (item->Metadata().IsEditable()) {
else if (item->EffectiveMetadata().IsEditable()) {
++editable;
}
@@ -2032,9 +2022,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
playlist_show_in_collection_->setVisible(false);
playlist_copy_to_collection_->setVisible(false);
playlist_move_to_collection_->setVisible(false);
#ifndef Q_OS_WIN32
playlist_copy_to_device_->setVisible(false);
#endif
playlist_organize_->setVisible(false);
playlist_delete_->setVisible(false);
@@ -2097,7 +2085,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
// Is it a collection item?
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_show_in_collection_->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);
}
#ifndef Q_OS_WIN32
playlist_copy_to_device_->setVisible(local_songs > 0);
#endif
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()));
if (!item) continue;
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);
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) {
#ifndef Q_OS_WIN32
organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
organize_dialog_->SetCopy(true);
if (organize_dialog_->SetUrls(urls)) {
@@ -2761,9 +2746,6 @@ void MainWindow::CopyFilesToDevice(const QList<QUrl> &urls) {
else {
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) {
const QModelIndex source_index = app_->playlist_manager()->current()->filter()->mapToSource(proxy_index);
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);
@@ -2839,7 +2821,7 @@ void MainWindow::PlaylistCopyUrl() {
if (!source_index.isValid()) continue;
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
if (!item) continue;
urls << item->StreamUrl();
urls << item->EffectiveUrl();
}
if (urls.count() > 0) {
@@ -2891,8 +2873,6 @@ void MainWindow::PlaylistSkip() {
void MainWindow::PlaylistCopyToDevice() {
#ifndef Q_OS_WIN32
SongList songs;
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"));
}
#endif
}
void MainWindow::ChangeCollectionFilterMode(QAction *action) {
@@ -3283,7 +3261,7 @@ void MainWindow::LoveButtonVisibilityChanged(const bool value) {
else
ui_->widget_love->hide();
tray_icon_->LoveVisibilityChanged(value);
systemtrayicon_->LoveVisibilityChanged(value);
}
@@ -3306,7 +3284,7 @@ void MainWindow::Love() {
app_->scrobbler()->Love();
ui_->button_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) {
QModelIndex source_idx = app_->playlist_manager()->current()->filter()->mapToSource(proxy_idx);
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_idx.row());
if (!item || !item->Metadata().url().isLocalFile()) continue;
QString filename = item->Metadata().url().toLocalFile();
if (!item || !item->EffectiveMetadata().url().isLocalFile()) continue;
QString filename = item->EffectiveMetadata().url().toLocalFile();
if (files.contains(filename)) continue;
selected_songs << item->Metadata();
selected_songs << item->EffectiveMetadata();
files << filename;
if (item == app_->player()->GetCurrentItem()) is_current_item = true;
}

View File

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

View File

@@ -13,10 +13,6 @@
<property name="windowTitle">
<string>Strawberry Music Player</string>
</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">
<layout class="QVBoxLayout" name="layout_centralWidget">
<property name="spacing">
@@ -37,7 +33,7 @@
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="sidebar_layout">
<layout class="QVBoxLayout" name="layout_left">
@@ -77,7 +73,7 @@
<item>
<widget class="Line" name="line_6">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
@@ -102,7 +98,7 @@
<item>
<widget class="QFrame" name="player_controls">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
<enum>QFrame::NoFrame</enum>
</property>
<layout class="QHBoxLayout" name="layout_player_controls">
<property name="spacing">
@@ -167,7 +163,7 @@
</size>
</property>
<property name="popupMode">
<enum>QToolButton::ToolButtonPopupMode::MenuButtonPopup</enum>
<enum>QToolButton::MenuButtonPopup</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
@@ -211,7 +207,7 @@
<item>
<widget class="Line" name="line_love">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
@@ -237,7 +233,7 @@
<item>
<widget class="Line" name="line_buttons">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
@@ -260,10 +256,10 @@
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Policy::Expanding</enum>
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@@ -276,7 +272,7 @@
<item>
<widget class="Line" name="line_volume">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
@@ -292,7 +288,7 @@
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
@@ -326,7 +322,7 @@
<item>
<widget class="Line" name="status_bar_line">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
@@ -380,7 +376,7 @@
<item>
<widget class="QLabel" name="playlist_summary">
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
@@ -391,7 +387,7 @@
<item>
<widget class="Line" name="line_5">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
@@ -401,7 +397,7 @@
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
@@ -580,7 +576,7 @@
<string>Ctrl+Q</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::QuitRole</enum>
<enum>QAction::QuitRole</enum>
</property>
</action>
<action name="action_stop_after_this_track">
@@ -644,7 +640,7 @@
<string>Ctrl+P</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::PreferencesRole</enum>
<enum>QAction::PreferencesRole</enum>
</property>
</action>
<action name="action_about_strawberry">
@@ -659,7 +655,7 @@
<string>F1</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::AboutRole</enum>
<enum>QAction::AboutRole</enum>
</property>
</action>
<action name="action_shuffle">
@@ -785,7 +781,7 @@
<string>About &amp;Qt</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::AboutQtRole</enum>
<enum>QAction::AboutQtRole</enum>
</property>
</action>
<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;
if (request.hasRawHeader("User-Agent")) {
user_agent = request.header(QNetworkRequest::UserAgentHeader).toByteArray();
if (network_request.hasRawHeader("User-Agent")) {
user_agent = network_request.header(QNetworkRequest::UserAgentHeader).toByteArray();
}
else {
user_agent = QStringLiteral("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8();
}
QNetworkRequest new_request(request);
new_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
new_request.setHeader(QNetworkRequest::UserAgentHeader, user_agent);
QNetworkRequest new_network_request(network_request);
new_network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
new_network_request.setHeader(QNetworkRequest::UserAgentHeader, user_agent);
if (op == QNetworkAccessManager::PostOperation && !new_request.header(QNetworkRequest::ContentTypeHeader).isValid()) {
new_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
if (op == QNetworkAccessManager::PostOperation && !new_network_request.header(QNetworkRequest::ContentTypeHeader).isValid()) {
new_network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
}
// Prefer the cache unless the caller has changed the setting already
if (request.attribute(QNetworkRequest::CacheLoadControlAttribute).toInt() == QNetworkRequest::PreferNetwork) {
new_request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
if (!network_request.attribute(QNetworkRequest::CacheLoadControlAttribute).isValid()) {
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);
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

View File

@@ -288,10 +288,10 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
bool is_current = false;
bool is_next = false;
if (result.media_url_ == current_item->Url()) {
if (result.media_url_ == current_item->OriginalUrl()) {
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;
}
else {
@@ -316,8 +316,8 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
qLog(Debug) << "URL handler for" << result.media_url_ << "returned" << result.stream_url_;
Song song;
if (is_current) song = current_item->Metadata();
else if (is_next) song = next_item->Metadata();
if (is_current) song = current_item->EffectiveMetadata();
else if (is_next) song = next_item->EffectiveMetadata();
bool update = false;
@@ -325,7 +325,7 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
if (
(result.stream_url_.isValid())
&&
(result.stream_url_ != song.url())
(result.stream_url_ != song.effective_url())
)
{
song.set_stream_url(result.stream_url_);
@@ -371,14 +371,14 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
}
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());
current_item_ = current_item;
play_offset_nanosec_ = 0;
}
else if (is_next && !current_item->Metadata().is_module_music()) {
qLog(Debug) << "Preloading next song" << next_item->Metadata().title() << result.stream_url_;
engine_->StartPreloading(next_item->Url(), result.stream_url_, song.has_cue(), song.beginning_nanosec(), song.end_nanosec());
else if (is_next && !current_item->EffectiveMetadata().is_module_music()) {
qLog(Debug) << "Preloading next song" << next_item->EffectiveMetadata().title() << result.stream_url_;
engine_->StartPreloading(next_item->OriginalUrl(), result.stream_url_, song.has_cue(), song.beginning_nanosec(), song.end_nanosec());
}
break;
@@ -504,8 +504,8 @@ bool Player::HandleStopAfter(const Playlist::AutoScroll autoscroll) {
void Player::TrackEnded() {
if (current_item_ && current_item_->IsLocalCollectionItem() && current_item_->Metadata().id() != -1) {
playlist_manager_->collection_backend()->IncrementPlayCountAsync(current_item_->Metadata().id());
if (current_item_ && current_item_->IsLocalCollectionItem() && current_item_->EffectiveMetadata().id() != -1) {
playlist_manager_->collection_backend()->IncrementPlayCountAsync(current_item_->EffectiveMetadata().id());
}
if (HandleStopAfter(Playlist::AutoScroll::Maybe)) return;
@@ -554,7 +554,7 @@ void Player::PlayPause(const quint64 offset_nanosec, const Playlist::AutoScroll
void Player::UnPause() {
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()) {
const qint64 time = QDateTime::currentSecsSinceEpoch() - pause_time_.toSecsSinceEpoch();
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_);
}
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;
}
@@ -758,7 +758,7 @@ void Player::PlayAt(const int index, const bool pause, const quint64 offset_nano
}
current_item_ = playlist_manager_->active()->current_item();
const QUrl url = current_item_->StreamUrl();
const QUrl url = current_item_->EffectiveUrl();
if (url_handlers_->CanHandle(url)) {
// 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));
}
else {
qLog(Debug) << "Playing song" << current_item_->Metadata().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());
qLog(Debug) << "Playing song" << current_item_->EffectiveMetadata().title() << url << "position" << offset_nanosec;
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();
if (current_row != -1) {
PlaylistItemPtr item = playlist_manager_->active()->current_item();
if (item && engine_metadata.media_url == item->Url()) {
Song song = item->Metadata();
if (item && engine_metadata.media_url == item->OriginalUrl()) {
Song song = item->EffectiveMetadata();
song.MergeFromEngineMetadata(engine_metadata);
playlist_manager_->active()->UpdateItemMetadata(current_row, item, song, true);
return;
@@ -836,8 +836,8 @@ void Player::EngineMetadataReceived(const EngineMetadata &engine_metadata) {
const int next_row = playlist_manager_->active()->next_row();
if (next_row != -1) {
PlaylistItemPtr next_item = playlist_manager_->active()->item_at(next_row);
if (engine_metadata.media_url == next_item->Url()) {
Song song = next_item->Metadata();
if (engine_metadata.media_url == next_item->OriginalUrl()) {
Song song = next_item->EffectiveMetadata();
song.MergeFromEngineMetadata(engine_metadata);
playlist_manager_->active()->UpdateItemMetadata(next_row, next_item, song, true);
}
@@ -905,11 +905,11 @@ void Player::PlayWithPause(const quint64 offset_nanosec) {
}
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() {
if (current_item_) Q_EMIT ForceShowOSD(current_item_->Metadata(), true);
if (current_item_) Q_EMIT ForceShowOSD(current_item_->EffectiveMetadata(), true);
}
void Player::TrackAboutToEnd() {
@@ -932,7 +932,7 @@ void Player::TrackAboutToEnd() {
// 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.
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();
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.
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.
if (url_handlers_->CanHandle(url)) {
@@ -961,20 +961,20 @@ void Player::TrackAboutToEnd() {
case UrlHandler::LoadResult::Type::TrackAvailable:
qLog(Debug) << "URL handler for" << result.media_url_ << "returned" << result.stream_url_;
url = result.stream_url_;
Song song = next_item->Metadata();
Song song = next_item->EffectiveMetadata();
song.set_stream_url(url);
next_item->SetTemporaryMetadata(song);
next_item->SetStreamMetadata(song);
break;
}
}
// Preloading any format while currently playing module music is broken in GStreamer.
// 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;
}
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
* This file was part of Clementine.
* 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
* it under the terms of the GNU General Public License as published by
@@ -68,9 +68,13 @@
using namespace Qt::Literals::StringLiterals;
const QStringList Song::kColumns = QStringList() << u"title"_s
<< u"titlesort"_s
<< u"album"_s
<< u"albumsort"_s
<< u"artist"_s
<< u"artistsort"_s
<< u"albumartist"_s
<< u"albumartistsort"_s
<< u"track"_s
<< u"disc"_s
<< u"year"_s
@@ -78,7 +82,9 @@ const QStringList Song::kColumns = QStringList() << u"title"_s
<< u"genre"_s
<< u"compilation"_s
<< u"composer"_s
<< u"composersort"_s
<< u"performer"_s
<< u"performersort"_s
<< u"grouping"_s
<< u"comment"_s
<< u"lyrics"_s
@@ -126,6 +132,9 @@ const QStringList Song::kColumns = QStringList() << u"title"_s
<< u"cue_path"_s
<< u"rating"_s
<< u"bpm"_s
<< u"mood"_s
<< u"initial_key"_s
<< u"acoustid_id"_s
<< u"acoustid_fingerprint"_s
@@ -261,9 +270,13 @@ struct Song::Private : public QSharedData {
bool valid_;
QString title_;
QString titlesort_;
QString album_;
QString albumsort_;
QString artist_;
QString artistsort_;
QString albumartist_;
QString albumartistsort_;
int track_;
int disc_;
int year_;
@@ -271,7 +284,9 @@ struct Song::Private : public QSharedData {
QString genre_;
bool compilation_; // From the file tag
QString composer_;
QString composersort_;
QString performer_;
QString performersort_;
QString grouping_;
QString comment_;
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.
float rating_; // Database rating, initial rating read from tag.
float bpm_;
QString mood_;
QString initial_key_;
QString acoustid_id_;
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 suspicious_tags_; // Whether our encoding guesser thinks these tags might be incorrectly encoded.
QString title_sortable_;
QString album_sortable_;
QString artist_sortable_;
QString albumartist_sortable_;
QUrl stream_url_; // Temporary stream url set by the URL handler.
QUrl stream_url_; // Temporary stream URL set by the URL handler.
};
@@ -384,6 +397,7 @@ Song::Private::Private(const Source source)
art_unset_(false),
rating_(-1),
bpm_(-1),
init_from_file_(false),
suspicious_tags_(false)
@@ -411,9 +425,13 @@ int Song::id() const { return d->id_; }
bool Song::is_valid() const { return d->valid_; }
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::albumsort() const { return d->albumsort_; }
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::albumartistsort() const { return d->albumartistsort_; }
int Song::track() const { return d->track_; }
int Song::disc() const { return d->disc_; }
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_; }
bool Song::compilation() const { return d->compilation_; }
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::performersort() const { return d->performersort_; }
const QString &Song::grouping() const { return d->grouping_; }
const QString &Song::comment() const { return d->comment_; }
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_; }
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_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_; }
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_; }
void Song::set_id(const int id) { d->id_ = id; }
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_album(const QString &v) { d->album_sortable_ = sortable(v); d->album_ = v; }
void Song::set_artist(const QString &v) { d->artist_sortable_ = sortable(v); d->artist_ = v; }
void Song::set_albumartist(const QString &v) { d->albumartist_sortable_ = sortable(v); d->albumartist_ = v; }
void Song::set_title(const QString &v) { d->title_ = v; }
void Song::set_titlesort(const QString &v) { d->titlesort_ = v; }
void Song::set_album(const QString &v) { d->album_ = 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_disc(const int v) { d->disc_ = 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_compilation(const bool v) { d->compilation_ = 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_performersort(const QString &v) { d->performersort_ = v; }
void Song::set_grouping(const QString &v) { d->grouping_ = v; }
void Song::set_comment(const QString &v) { d->comment_ = 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_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_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_title(const TagLib::String &v) {
const QString title = TagLibStringToQString(v);
d->title_sortable_ = sortable(title);
d->title_ = title;
}
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_title(const TagLib::String &v) { d->title_ = TagLibStringToQString(v); }
void Song::set_titlesort(const TagLib::String &v) { d->titlesort_ = TagLibStringToQString(v); }
void Song::set_album(const TagLib::String &v) { d->album_ = TagLibStringToQString(v); }
void Song::set_albumsort(const TagLib::String &v) { d->albumsort_ = TagLibStringToQString(v); }
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_genre(const TagLib::String &v) { d->genre_ = 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_performersort(const TagLib::String &v) { d->performersort_ = 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_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_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_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_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_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_; }
const QString &Song::playlist_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_albumartist() const { return is_compilation() ? d->albumartist_ : effective_albumartist(); }
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_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;
}
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) {
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) {
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);
}
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 {
return d->title_ == other.d->title_ &&
d->album_ == other.d->album_ &&
d->artist_ == other.d->artist_ &&
d->albumartist_ == other.d->albumartist_ &&
d->track_ == other.d->track_ &&
d->disc_ == other.d->disc_ &&
d->year_ == other.d->year_ &&
d->originalyear_ == other.d->originalyear_ &&
d->genre_ == other.d->genre_ &&
d->compilation_ == other.d->compilation_ &&
d->composer_ == other.d->composer_ &&
d->performer_ == other.d->performer_ &&
d->grouping_ == other.d->grouping_ &&
d->comment_ == other.d->comment_ &&
d->lyrics_ == other.d->lyrics_ &&
d->artist_id_ == other.d->artist_id_ &&
d->album_id_ == other.d->album_id_ &&
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->cue_path_ == other.d->cue_path_;
d->titlesort_ == other.d->titlesort_ &&
d->album_ == other.d->album_ &&
d->albumsort_ == other.d->albumsort_ &&
d->artist_ == other.d->artist_ &&
d->artistsort_ == other.d->artistsort_ &&
d->albumartist_ == other.d->albumartist_ &&
d->albumartistsort_ == other.d->albumartistsort_ &&
d->track_ == other.d->track_ &&
d->disc_ == other.d->disc_ &&
d->year_ == other.d->year_ &&
d->originalyear_ == other.d->originalyear_ &&
d->genre_ == other.d->genre_ &&
d->compilation_ == other.d->compilation_ &&
d->composer_ == other.d->composer_ &&
d->composersort_ == other.d->composersort_ &&
d->performer_ == other.d->performer_ &&
d->performersort_ == other.d->performersort_ &&
d->grouping_ == other.d->grouping_ &&
d->comment_ == other.d->comment_ &&
d->lyrics_ == other.d->lyrics_ &&
d->artist_id_ == other.d->artist_id_ &&
d->album_id_ == other.d->album_id_ &&
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 {
return d->playcount_ == other.d->playcount_ &&
d->skipcount_ == other.d->skipcount_ &&
d->lastplayed_ == other.d->lastplayed_;
d->skipcount_ == other.d->skipcount_ &&
d->lastplayed_ == other.d->lastplayed_;
}
@@ -980,42 +1027,70 @@ bool Song::IsAcoustIdEqual(const Song &other) const {
bool Song::IsMusicBrainzEqual(const Song &other) const {
return d->musicbrainz_album_artist_id_ == other.d->musicbrainz_album_artist_id_ &&
d->musicbrainz_artist_id_ == other.d->musicbrainz_artist_id_ &&
d->musicbrainz_original_artist_id_ == other.d->musicbrainz_original_artist_id_ &&
d->musicbrainz_album_id_ == other.d->musicbrainz_album_id_ &&
d->musicbrainz_original_album_id_ == other.d->musicbrainz_original_album_id_ &&
d->musicbrainz_recording_id_ == other.d->musicbrainz_recording_id_ &&
d->musicbrainz_track_id_ == other.d->musicbrainz_track_id_ &&
d->musicbrainz_disc_id_ == other.d->musicbrainz_disc_id_ &&
d->musicbrainz_release_group_id_ == other.d->musicbrainz_release_group_id_ &&
d->musicbrainz_work_id_ == other.d->musicbrainz_work_id_;
d->musicbrainz_artist_id_ == other.d->musicbrainz_artist_id_ &&
d->musicbrainz_original_artist_id_ == other.d->musicbrainz_original_artist_id_ &&
d->musicbrainz_album_id_ == other.d->musicbrainz_album_id_ &&
d->musicbrainz_original_album_id_ == other.d->musicbrainz_original_album_id_ &&
d->musicbrainz_recording_id_ == other.d->musicbrainz_recording_id_ &&
d->musicbrainz_track_id_ == other.d->musicbrainz_track_id_ &&
d->musicbrainz_disc_id_ == other.d->musicbrainz_disc_id_ &&
d->musicbrainz_release_group_id_ == other.d->musicbrainz_release_group_id_ &&
d->musicbrainz_work_id_ == other.d->musicbrainz_work_id_;
}
bool Song::IsEBUR128Equal(const Song &other) const {
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 {
return d->art_embedded_ == other.d->art_embedded_ &&
d->art_automatic_ == other.d->art_automatic_ &&
d->art_manual_ == other.d->art_manual_ &&
d->art_unset_ == other.d->art_unset_;
d->art_automatic_ == other.d->art_automatic_ &&
d->art_manual_ == other.d->art_manual_ &&
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 {
return IsMetadataEqual(other) &&
IsPlayStatisticsEqual(other) &&
IsRatingEqual(other) &&
IsAcoustIdEqual(other) &&
IsMusicBrainzEqual(other) &&
IsArtEqual(other);
IsPlayStatisticsEqual(other) &&
IsRatingEqual(other) &&
IsAcoustIdEqual(other) &&
IsMusicBrainzEqual(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) {
switch (filetype) {
@@ -1166,6 +1257,7 @@ QString Song::TextForFiletype(const FileType filetype) {
case FileType::CDDA: return u"CDDA"_s;
case FileType::SPC: return u"SNES SPC700"_s;
case FileType::VGM: return u"VGM"_s;
case FileType::ALAC: return u"ALAC"_s;
case FileType::Stream: return u"Stream"_s;
case FileType::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::SPC: return u"spc"_s;
case FileType::VGM: return u"vgm"_s;
case FileType::ALAC: return u"m4a"_s;
case FileType::Unknown:
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::CDDA: return IconLoader::Load(u"cd"_s);
case FileType::Stream: return IconLoader::Load(u"applications-internet"_s);
case FileType::ALAC: return IconLoader::Load(u"alac"_s);
case FileType::Unknown:
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 {
switch (filetype()) {
@@ -1250,6 +1361,7 @@ bool Song::IsFileLossless() const {
case FileType::TrueAudio:
case FileType::PCM:
case FileType::CDDA:
case FileType::ALAC:
return true;
default:
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-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-alac"_L1, Qt::CaseInsensitive) == 0) return FileType::ALAC;
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("SNES SPC700"_L1, Qt::CaseInsensitive) == 0) return FileType::SPC;
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;
@@ -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);
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_albumsort(SqlHelper::ValueToString(r, ColumnIndex(u"albumsort"_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_albumartistsort(SqlHelper::ValueToString(r, ColumnIndex(u"albumartistsort"_s) + col));
d->track_ = SqlHelper::ValueToInt(r, ColumnIndex(u"track"_s) + col);
d->disc_ = SqlHelper::ValueToInt(r, ColumnIndex(u"disc"_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->compilation_ = r.value(ColumnIndex(u"compilation"_s) + col).toBool();
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->performersort_ = SqlHelper::ValueToString(r, ColumnIndex(u"performersort"_s) + col);
d->grouping_ = SqlHelper::ValueToString(r, ColumnIndex(u"grouping"_s) + col);
d->comment_ = SqlHelper::ValueToString(r, ColumnIndex(u"comment"_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->cue_path_ = SqlHelper::ValueToString(r, ColumnIndex(u"cue_path"_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_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
query->BindStringValue(u":title"_s, d->title_);
query->BindStringValue(u":titlesort"_s, d->titlesort_);
query->BindStringValue(u":album"_s, d->album_);
query->BindStringValue(u":albumsort"_s, d->albumsort_);
query->BindStringValue(u":artist"_s, d->artist_);
query->BindStringValue(u":artistsort"_s, d->artistsort_);
query->BindStringValue(u":albumartist"_s, d->albumartist_);
query->BindStringValue(u":albumartistsort"_s, d->albumartistsort_);
query->BindIntValue(u":track"_s, d->track_);
query->BindIntValue(u":disc"_s, d->disc_);
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->BindBoolValue(u":compilation"_s, d->compilation_);
query->BindStringValue(u":composer"_s, d->composer_);
query->BindStringValue(u":composersort"_s, d->composersort_);
query->BindStringValue(u":performer"_s, d->performer_);
query->BindStringValue(u":performersort"_s, d->performersort_);
query->BindStringValue(u":grouping"_s, d->grouping_);
query->BindStringValue(u":comment"_s, d->comment_);
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->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_fingerprint"_s, d->acoustid_fingerprint_);
@@ -1819,7 +1952,7 @@ void Song::ToXesam(QVariantMap *map) const {
using mpris::AddMetadataAsList;
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);
AddMetadataAsList(u"xesam:artist"_s, artist(), map);
AddMetadata(u"xesam:album"_s, album(), map);

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -105,6 +105,7 @@ class Song {
IT = 21,
SPC = 22,
VGM = 23,
ALAC = 24, // MP4, with ALAC codec
CDDA = 90,
Stream = 91
};
@@ -149,9 +150,13 @@ class Song {
bool is_valid() const;
const QString &title() const;
const QString &titlesort() const;
const QString &album() const;
const QString &albumsort() const;
const QString &artist() const;
const QString &artistsort() const;
const QString &albumartist() const;
const QString &albumartistsort() const;
int track() const;
int disc() const;
int year() const;
@@ -159,7 +164,9 @@ class Song {
const QString &genre() const;
bool compilation() const;
const QString &composer() const;
const QString &composersort() const;
const QString &performer() const;
const QString &performersort() const;
const QString &grouping() const;
const QString &comment() const;
const QString &lyrics() const;
@@ -206,6 +213,9 @@ class Song {
const QString &cue_path() const;
float rating() const;
float bpm() const;
const QString &mood() const;
const QString &initial_key() const;
const QString &acoustid_id() const;
const QString &acoustid_fingerprint() const;
@@ -249,11 +259,6 @@ class Song {
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;
// Setters
@@ -261,9 +266,13 @@ class Song {
void set_valid(const bool v);
void set_title(const QString &v);
void set_titlesort(const QString &v);
void set_album(const QString &v);
void set_albumsort(const QString &v);
void set_artist(const QString &v);
void set_artistsort(const QString &v);
void set_albumartist(const QString &v);
void set_albumartistsort(const QString &v);
void set_track(const int v);
void set_disc(const int v);
void set_year(const int v);
@@ -271,7 +280,9 @@ class Song {
void set_genre(const QString &v);
void set_compilation(bool v);
void set_composer(const QString &v);
void set_composersort(const QString &v);
void set_performer(const QString &v);
void set_performersort(const QString &v);
void set_grouping(const QString &v);
void set_comment(const QString &v);
void set_lyrics(const QString &v);
@@ -317,6 +328,9 @@ class Song {
void set_cue_path(const QString &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_fingerprint(const QString &v);
@@ -340,12 +354,18 @@ class Song {
void set_stream_url(const QUrl &v);
void set_title(const TagLib::String &v);
void set_titlesort(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_artistsort(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_composer(const TagLib::String &v);
void set_composersort(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_comment(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_release_group_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_sortable() const;
const QString &effective_albumartistsort() const;
const QString &effective_artistsort() 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;
const QString &playlist_albumartist() const;
const QString &playlist_albumartist_sortable() const;
const QString &playlist_effective_albumartist() const;
const QString &playlist_effective_albumartistsort() const;
bool is_metadata_good() const;
bool is_local_collection_song() const;
@@ -402,6 +429,13 @@ class Song {
bool comment_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);
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;
// Comparison functions
bool IsFileInfoEqual(const Song &other) const;
bool IsMetadataEqual(const Song &other) const;
bool IsPlayStatisticsEqual(const Song &other) const;
bool IsRatingEqual(const Song &other) const;
@@ -438,7 +473,10 @@ class Song {
bool IsMusicBrainzEqual(const Song &other) const;
bool IsEBUR128Equal(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 IsEqual(const Song &other) const;
bool IsOnSameAlbum(const Song &other) const;
bool IsSimilar(const Song &other) const;
@@ -448,6 +486,7 @@ class Song {
static QString DescriptionForSource(const Source source);
static Source SourceFromText(const QString &source);
static QIcon IconForSource(const Source source);
static QString DomainForSource(const Source source);
static QString TextForFiletype(const FileType filetype);
static QString ExtensionForFiletype(const FileType filetype);
static QIcon IconForFiletype(const FileType filetype);
@@ -455,9 +494,12 @@ class Song {
QString TextForSource() const { return TextForSource(source()); }
QString DescriptionForSource() const { return DescriptionForSource(source()); }
QIcon IconForSource() const { return IconForSource(source()); }
QString DomainForSource() const { return DomainForSource(source()); }
QString TextForFiletype() const { return TextForFiletype(filetype()); }
QIcon IconForFiletype() const { return IconForFiletype(filetype()); }
QString ShareURL() const;
bool IsFileLossless() const;
static FileType FiletypeByMimetype(const QString &mimetype);
static FileType FiletypeByDescription(const QString &text);
@@ -521,9 +563,6 @@ class Song {
private:
struct Private;
static QString sortable(const QString &v);
QSharedDataPointer<Private> d;
};

View File

@@ -178,9 +178,11 @@ SongLoader::Result SongLoader::LoadLocalPartial(const QString &filename) {
SongLoader::Result SongLoader::LoadAudioCD() {
#ifdef HAVE_AUDIOCD
CddaSongLoader *cdda_song_loader = new CddaSongLoader(QUrl(), this);
QObject::connect(cdda_song_loader, &CddaSongLoader::SongsDurationLoaded, this, &SongLoader::AudioCDTracksLoadFinishedSlot);
QObject::connect(cdda_song_loader, &CddaSongLoader::SongsMetadataLoaded, this, &SongLoader::AudioCDTracksTagsLoaded);
CDDASongLoader *cdda_song_loader = new CDDASongLoader(QUrl(), this);
QObject::connect(cdda_song_loader, &CDDASongLoader::LoadError, this, &SongLoader::AudioCDTracksLoadErrorSlot);
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();
return Result::Success;
#else
@@ -192,23 +194,38 @@ SongLoader::Result SongLoader::LoadAudioCD() {
#ifdef HAVE_AUDIOCD
void SongLoader::AudioCDTracksLoadFinishedSlot(const SongList &songs, const QString &error) {
void SongLoader::AudioCDTracksLoadErrorSlot(const QString &error) {
songs_ = songs;
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;
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) {

View File

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

View File

@@ -31,8 +31,8 @@
#include <QTextStream>
#include <QFile>
#include <QString>
#include <QColor>
#include <QPalette>
#include <QColor>
#include <QEvent>
#include "includes/shared_ptr.h"
@@ -80,20 +80,13 @@ void StyleSheetLoader::UpdateStyleSheet(QWidget *widget, SharedPtr<StyleSheetDat
// Replace %palette-role with actual colours
QPalette p(widget->palette());
{
QColor alt = p.color(QPalette::AlternateBase);
QColor color_altbase = p.color(QPalette::AlternateBase);
#ifdef Q_OS_MACOS
if (alt.lightness() > 180) {
alt.setAlpha(130);
}
else {
alt.setAlpha(16);
}
color_altbase.setAlpha(color_altbase.alpha() >= 180 ? (color_altbase.lightness() > 180 ? 130 : 16) : color_altbase.alpha());
#else
alt.setAlpha(130);
color_altbase.setAlpha(color_altbase.alpha() >= 180 ? 116 : color_altbase.alpha());
#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"Background"_s, p, QPalette::Window);

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,10 +21,19 @@
#include "config.h"
#include <cstddef>
#include <cdio/types.h>
#include <cdio/cdio.h>
#include <chrono>
#include <QString>
#include <QUrl>
#include <QTimer>
#include "includes/shared_ptr.h"
#include "core/logging.h"
#include "collection/collectionmodel.h"
#include "cddasongloader.h"
#include "connecteddevice.h"
@@ -33,7 +42,9 @@
class DeviceLister;
class DeviceManager;
CddaDevice::CddaDevice(const QUrl &url,
using namespace std::chrono_literals;
CDDADevice::CDDADevice(const QUrl &url,
DeviceLister *lister,
const QString &unique_id,
DeviceManager *device_manager,
@@ -45,36 +56,86 @@ CddaDevice::CddaDevice(const QUrl &url,
const bool first_time,
QObject *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);
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsDurationLoaded, this, &CddaDevice::SongsLoaded);
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsMetadataLoaded, this, &CddaDevice::SongsLoaded);
QObject::connect(this, &CddaDevice::SongsDiscovered, collection_model_, &CollectionModel::AddReAddOrUpdate);
timer_disc_changed_->setInterval(1s);
QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongsLoaded, this, &CDDADevice::SongsLoaded);
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;
}
void CddaDevice::Refresh() {
void CDDADevice::WatchForDiscChanges(const bool watch) {
if (!cdda_song_loader_.HasChanged()) {
return;
if (watch && !timer_disc_changed_->isActive()) {
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();
Q_EMIT SongsDiscovered(songs);
song_count_ = songs.size();
(void)cdio_get_media_changed(cdio_);
}
void CDDADevice::SongLoadingFinished() {
WatchForDiscChanges(true);
}

View File

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

View File

@@ -40,9 +40,9 @@
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)
@@ -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_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_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)
@@ -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)
@@ -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)
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_hwinfo_t cd_info;
if (cdio_get_hwinfo(cdio, &cd_info)) {
const QString friendly_name = QString::fromUtf8(cd_info.psz_model).trimmed();
cdio_destroy(cdio);
return QString::fromUtf8(cd_info.psz_model);
return friendly_name;
}
cdio_destroy(cdio);
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);
}
void CddaLister::UnmountDevice(const QString &id) {
void CDDALister::UnmountDevice(const QString &id) {
cdio_eject_media_drive(id.toLocal8Bit().constData());
}
void CddaLister::UpdateDeviceFreeSpace(const QString &id) {
void CDDALister::UpdateDeviceFreeSpace(const QString &id) {
Q_UNUSED(id)
}
bool CddaLister::Init() {
bool CDDALister::Init() {
cdio_init();
#ifdef Q_OS_MACOS

View File

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

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,25 +21,24 @@
#include "config.h"
#include <memory>
#include <QtGlobal>
#include <cstddef>
#include <memory>
#include <glib.h>
#include <glib/gtypes.h>
#include <glib-object.h>
#include <cdio/cdio.h>
#include <gst/gst.h>
#include <gst/tag/tag.h>
#include <QtGlobal>
#include <QObject>
#include <QMutex>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QtConcurrentRun>
#include <QScopeGuard>
#include "cddasongloader.h"
#include "includes/shared_ptr.h"
@@ -51,18 +50,21 @@ using std::make_shared;
using namespace Qt::Literals::StringLiterals;
CddaSongLoader::CddaSongLoader(const QUrl &url, QObject *parent)
CDDASongLoader::CDDASongLoader(const QUrl &url, QObject *parent)
: QObject(parent),
url_(url),
network_(make_shared<NetworkAccessManager>()),
cdda_(nullptr),
cdio_(nullptr) {}
network_(make_shared<NetworkAccessManager>()) {
CddaSongLoader::~CddaSongLoader() {
if (cdio_) cdio_destroy(cdio_);
#ifdef HAVE_MUSICBRAINZ
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()) {
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_);
cdio_ = cdio_open(url_.path().toLocal8Bit().constData(), DRIVER_DEVICE);
if (cdio_ == nullptr) {
Error(u"Unable to open CDIO device."_s);
if (IsActive()) {
return;
}
// Create gstreamer cdda element
loading_future_ = QtConcurrent::run(&CDDASongLoader::LoadSongsFromCDDA, this);
}
void CDDASongLoader::LoadSongsFromCDDA() {
QMutexLocker l(&mutex_load_);
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) {
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()) {
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")) {
g_object_set(cdda_, "paranoia-mode", 0, nullptr);
if (g_object_class_find_property(G_OBJECT_GET_CLASS(cdda), "paranoia-mode")) {
g_object_set(cdda, "paranoia-mode", 0, nullptr);
}
// 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) {
gst_element_set_state(cdda_, GST_STATE_NULL);
gst_object_unref(GST_OBJECT(cdda_));
cdda_ = nullptr;
if (gst_element_set_state(cdda, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE) {
gst_element_set_state(cdda, GST_STATE_NULL);
gst_object_unref(GST_OBJECT(cdda));
cdda = nullptr;
Error(tr("Error while setting CDDA device to ready state."));
return;
}
if (gst_element_set_state(cdda_, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) {
gst_element_set_state(cdda_, GST_STATE_NULL);
gst_object_unref(GST_OBJECT(cdda_));
cdda_ = nullptr;
if (gst_element_set_state(cdda, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) {
gst_element_set_state(cdda, GST_STATE_NULL);
gst_object_unref(GST_OBJECT(cdda));
cdda = nullptr;
Error(tr("Error while setting CDDA device to pause state."));
return;
}
// Get number of tracks
GstFormat fmt = gst_format_get_by_nick("track");
GstFormat out_fmt = fmt;
gint64 num_tracks = 0;
if (!gst_element_query_duration(cdda_, out_fmt, &num_tracks)) {
gst_element_set_state(cdda_, GST_STATE_NULL);
gst_object_unref(GST_OBJECT(cdda_));
cdda_ = nullptr;
GstFormat format_track = gst_format_get_by_nick("track");
GstFormat format_duration = format_track;
gint64 total_tracks = 0;
if (!gst_element_query_duration(cdda, format_duration, &total_tracks)) {
gst_element_set_state(cdda, GST_STATE_NULL);
gst_object_unref(GST_OBJECT(cdda));
cdda = nullptr;
Error(tr("Error while querying CDDA tracks."));
return;
}
if (out_fmt != fmt) {
qLog(Error) << "Error while querying cdda GstElement (2).";
gst_element_set_state(cdda_, GST_STATE_NULL);
gst_object_unref(GST_OBJECT(cdda_));
cdda_ = nullptr;
if (format_duration != format_track) {
qLog(Error) << "Error while querying CDDA GstElement (2).";
gst_element_set_state(cdda, GST_STATE_NULL);
gst_object_unref(GST_OBJECT(cdda));
cdda = nullptr;
Error(tr("Error while querying CDDA tracks."));
return;
}
SongList songs;
songs.reserve(num_tracks);
for (int track_number = 1; track_number <= num_tracks; ++track_number) {
// Init song
QMap<int, Song> songs;
for (int track_number = 1; track_number <= total_tracks; ++track_number) {
Song song(Song::Source::CDDA);
song.set_id(track_number);
song.set_valid(true);
@@ -145,129 +152,269 @@ void CddaSongLoader::LoadSongs() {
song.set_url(GetUrlFromTrack(track_number));
song.set_title(QStringLiteral("Track %1").arg(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();
#endif // HAVE_MUSICBRAINZ
GstElement *pipeline = gst_pipeline_new("pipeline");
GstElement *sink = gst_element_factory_make("fakesink", nullptr);
gst_bin_add_many(GST_BIN(pipeline), cdda_, sink, nullptr);
gst_element_link(cdda_, sink);
gst_bin_add_many(GST_BIN(pipeline), cdda, sink, nullptr);
gst_element_link(cdda, sink);
gst_element_set_state(pipeline, GST_STATE_READY);
gst_element_set_state(pipeline, GST_STATE_PAUSED);
// Get TOC and TAG messages
GstMessage *msg = nullptr;
GstMessage *msg_toc = nullptr;
GstMessage *msg_tag = nullptr;
while ((msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline), GST_SECOND, static_cast<GstMessageType>(GST_MESSAGE_TOC | GST_MESSAGE_TAG)))) {
if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TOC) {
if (msg_toc) gst_message_unref(msg_toc); // Shouldn't happen, but just in case
msg_toc = msg;
}
else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TAG) {
if (msg_tag) gst_message_unref(msg_tag);
msg_tag = msg;
}
}
int track_artist_tags = 0;
int track_album_tags = 0;
int track_title_tags = 0;
#ifdef HAVE_MUSICBRAINZ
QString musicbrainz_discid;
#endif // HAVE_MUSICBRAINZ
GstMessageType msg_filter = static_cast<GstMessageType>(GST_MESSAGE_TOC|GST_MESSAGE_TAG);
while (msg_filter != 0 && (msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline), GST_SECOND * 5, msg_filter))) {
// Handle TOC message: get tracks duration
if (msg_toc) {
GstToc *toc = nullptr;
gst_message_parse_toc(msg_toc, &toc, nullptr);
if (toc) {
const QScopeGuard scopeguard_msg = qScopeGuard([msg]() {
gst_message_unref(msg);
});
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);
if (entries && static_cast<guint>(songs.size()) <= g_list_length(entries)) {
int i = 0;
for (GList *node = entries; node != nullptr; node = node->next) {
GstTocEntry *entry = static_cast<GstTocEntry*>(node->data);
qint64 duration = 0;
int track_number = 0;
for (GList *entry_node = entries; entry_node != nullptr; entry_node = entry_node->next) {
++track_number;
if (songs.contains(track_number)) {
Song &song = songs[track_number];
GstTocEntry *entry = static_cast<GstTocEntry*>(entry_node->data);
gint64 start = 0, stop = 0;
if (gst_toc_entry_get_start_stop_times(entry, &start, &stop)) duration = stop - start;
songs[i++].set_length_nanosec(duration);
if (gst_toc_entry_get_start_stop_times(entry, &start, &stop)) {
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);
}
Q_EMIT SongsDurationLoaded(songs);
else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TAG) {
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
// Handle TAG message: generate MusicBrainz DiscId
if (msg_tag) {
GstTagList *tags = nullptr;
gst_message_parse_tag(msg_tag, &tags);
char *string_mb = 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);
}
}
if (musicbrainz_discid.isEmpty()) {
if (gst_tag_list_get_string(tags, GST_TAG_CDDA_MUSICBRAINZ_DISCID, &tag)) {
musicbrainz_discid = QString::fromUtf8(tag);
g_free(tag);
tag = nullptr;
}
}
#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);
// This will also cause cdda_ to be unref'd.
// This will also cause cdda to be unref'd.
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
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());
musicbrainz_client->deleteLater();
if (results.empty()) return;
if (!error.isEmpty()) {
Error(error);
return;
}
if (results.empty()) {
Q_EMIT LoadingFinished();
return;
}
SongList songs;
songs.reserve(results.count());
int track_number = 1;
for (const MusicBrainzClient::Result &ret : results) {
int track_number = 0;
for (const MusicBrainzClient::Result &result : results) {
++track_number;
Song song(Song::Source::CDDA);
song.set_artist(artist);
song.set_album(album);
song.set_title(ret.title_);
song.set_length_nanosec(ret.duration_msec_ * kNsecPerMsec);
song.set_title(result.title_);
song.set_length_nanosec(result.duration_msec_ * kNsecPerMsec);
song.set_track(track_number);
song.set_year(ret.year_);
song.set_year(result.year_);
song.set_id(track_number);
song.set_filetype(Song::FileType::CDDA);
song.set_valid(true);
// We need to set url: that's how playlist will find the correct item to update
song.set_url(GetUrlFromTrack(track_number++));
// We need to set URL, that's how playlist will find the correct item to update
song.set_url(GetUrlFromTrack(track_number));
songs << song;
}
Q_EMIT SongsMetadataLoaded(songs);
}
#endif
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;
Q_EMIT SongsUpdated(songs);
Q_EMIT LoadingFinished();
}
void CddaSongLoader::Error(const QString &error) {
#endif // HAVE_MUSICBRAINZ
void CDDASongLoader::Error(const QString &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
* This file was part of Clementine.
* 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
* it under the terms of the GNU General Public License as published by
@@ -24,11 +24,6 @@
#include "config.h"
#include <cstddef>
#include <cdio/types.h>
#include <cdio/cdio.h>
#include <gst/gstelement.h>
#include <gst/audio/gstaudiocdsrc.h>
@@ -36,6 +31,7 @@
#include <QMutex>
#include <QString>
#include <QUrl>
#include <QFuture>
#include "includes/shared_ptr.h"
#include "core/song.h"
@@ -45,39 +41,40 @@
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
public:
explicit CddaSongLoader(const QUrl &url, QObject *parent = nullptr);
~CddaSongLoader() override;
explicit CDDASongLoader(const QUrl &url, QObject *parent = nullptr);
~CDDASongLoader() override;
// Load songs. Signals declared below will be emitted anytime new information will be available.
void LoadSongs();
bool HasChanged();
bool IsActive() const { return loading_future_.isRunning(); }
private:
void LoadSongsFromCDDA();
void Error(const QString &error);
QUrl GetUrlFromTrack(const int track_number) const;
Q_SIGNALS:
void SongsLoadError(const QString &error);
void SongsLoaded(const SongList &songs);
void SongsDurationLoaded(const SongList &songs, const QString &error = QString());
void SongsMetadataLoaded(const SongList &songs);
void SongsUpdated(const SongList &songs);
void LoadError(const QString &error);
void LoadingFinished();
void LoadTagsFromMusicBrainz(const QString &musicbrainz_discid);
private Q_SLOTS:
#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
private:
const QUrl url_;
SharedPtr<NetworkAccessManager> network_;
GstElement *cdda_;
CdIo_t *cdio_;
QMutex mutex_load_;
QFuture<void> loading_future_;
};
#endif // CDDASONGLOADER_H

View File

@@ -67,9 +67,6 @@ class ConnectedDevice : public QObject, public virtual MusicStorage, public enab
virtual bool IsLoading() { return false; }
virtual void NewConnection() {}
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;
Song::FileType GetTranscodeFormat() const override;

View File

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

View File

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

View File

@@ -97,8 +97,9 @@ class DeviceInfo : public SimpleTreeItem<DeviceInfo> {
void InitFromDb(const DeviceDatabaseBackend::Device &dev);
DeviceDatabaseBackend::Device SaveToDb() const;
void InitIcon();
// 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)
const Backend *BestBackend() const;

View File

@@ -109,7 +109,7 @@ DeviceManager::DeviceManager(const SharedPtr<TaskManager> task_manager,
backend_->moveToThread(database->thread());
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.
(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.
#if defined(HAVE_AUDIOCD) && !defined(Q_OS_MACOS)
AddLister(new CddaLister);
AddLister(new CDDALister);
#endif
#ifdef HAVE_UDISKS2
AddLister(new Udisks2Lister);
@@ -133,7 +133,7 @@ DeviceManager::DeviceManager(const SharedPtr<TaskManager> task_manager,
#endif
#ifdef HAVE_AUDIOCD
AddDeviceClass<CddaDevice>();
AddDeviceClass<CDDADevice>();
#endif
AddDeviceClass<FilesystemDevice>();
@@ -167,12 +167,12 @@ void DeviceManager::Exit() {
void DeviceManager::CloseDevices() {
for (DeviceInfo *info : std::as_const(devices_)) {
if (!info->device_) continue;
if (wait_for_exit_.contains(&*info->device_)) continue;
wait_for_exit_ << &*info->device_;
QObject::connect(&*info->device_, &ConnectedDevice::destroyed, this, &DeviceManager::DeviceDestroyed);
info->device_->Close();
for (DeviceInfo *device_info : std::as_const(devices_)) {
if (!device_info->device_) continue;
if (wait_for_exit_.contains(&*device_info->device_)) continue;
wait_for_exit_ << &*device_info->device_;
QObject::connect(&*device_info->device_, &ConnectedDevice::destroyed, this, &DeviceManager::DeviceDestroyed);
device_info->device_->Close();
}
if (wait_for_exit_.isEmpty()) CloseListers();
@@ -224,10 +224,10 @@ void DeviceManager::ListerClosed() {
void DeviceManager::DeviceDestroyed() {
ConnectedDevice *device = static_cast<ConnectedDevice*>(sender());
if (!wait_for_exit_.contains(device) || !backend_) return;
ConnectedDevice *connected_device = static_cast<ConnectedDevice*>(sender());
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();
}
@@ -237,41 +237,37 @@ void DeviceManager::LoadAllDevices() {
Q_ASSERT(QThread::currentThread() != qApp->thread());
const DeviceDatabaseBackend::DeviceList devices = backend_->GetAllDevices();
for (const DeviceDatabaseBackend::Device &device : devices) {
DeviceInfo *info = new DeviceInfo(DeviceInfo::Type::Device, root_);
info->InitFromDb(device);
Q_EMIT DeviceCreatedFromDB(info);
}
Q_EMIT DevicesLoaded(devices);
// This is done in a concurrent thread so close the unique DB connection.
backend_->Close();
}
void DeviceManager::AddDeviceFromDB(DeviceInfo *info) {
void DeviceManager::AddDevicesFromDB(const DeviceDatabaseBackend::DeviceList &devices) {
const QStringList icon_names = info->icon_name_.split(u',');
QVariantList icons;
icons.reserve(icon_names.count());
for (const QString &icon_name : icon_names) {
icons << icon_name;
}
info->SetIcon(icons, info->friendly_name_);
DeviceInfo *existing = FindEquivalentDevice(info);
if (existing) {
qLog(Info) << "Found existing device: " << info->friendly_name_;
existing->icon_name_ = info->icon_name_;
existing->icon_ = info->icon_;
QModelIndex idx = ItemToIndex(existing);
if (idx.isValid()) Q_EMIT dataChanged(idx, idx);
root_->Delete(info->row);
}
else {
qLog(Info) << "Device added from database: " << info->friendly_name_;
beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count()));
devices_ << info;
endInsertRows();
for (const DeviceDatabaseBackend::Device &device : devices) {
const QStringList unique_ids = device.unique_id_.split(u',');
DeviceInfo *device_info = FindEquivalentDevice(unique_ids);
if (device_info && device_info->database_id_ == -1) {
qLog(Info) << "Database device linked to physical device:" << device.friendly_name_;
device_info->database_id_ = device.id_;
device_info->icon_name_ = device.icon_name_;
device_info->InitIcon();
const QModelIndex idx = ItemToIndex(device_info);
if (idx.isValid()) {
Q_EMIT dataChanged(idx, idx);
}
}
else {
qLog(Info) << "Database device:" << device.friendly_name_;
device_info = new DeviceInfo(DeviceInfo::Type::Device, root_);
device_info->InitFromDb(device);
beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count()));
devices_ << device_info;
endInsertRows();
}
}
}
@@ -280,30 +276,29 @@ QVariant DeviceManager::data(const QModelIndex &idx, int role) const {
if (!idx.isValid() || idx.column() != 0) return QVariant();
DeviceInfo *info = IndexToItem(idx);
if (!info) return QVariant();
DeviceInfo *device_info = IndexToItem(idx);
if (!device_info) return QVariant();
switch (role) {
case Qt::DisplayRole:{
QString text;
if (!info->friendly_name_.isEmpty()) {
text = info->friendly_name_;
if (!device_info->friendly_name_.isEmpty()) {
text = device_info->friendly_name_;
}
else if (info->BestBackend()) {
text = info->BestBackend()->unique_id_;
else if (device_info->BestBackend()) {
text = device_info->BestBackend()->unique_id_;
}
if (info->size_ > 0) {
text = text + QStringLiteral(" (%1)").arg(Utilities::PrettySize(info->size_));
if (device_info->size_ > 0) {
text = text + QStringLiteral(" (%1)").arg(Utilities::PrettySize(device_info->size_));
}
if (info->device_) info->device_->Refresh();
return text;
}
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
QPainter p(&pixmap);
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:
return info->friendly_name_;
return device_info->friendly_name_;
case Role_UniqueId:
if (!info->BestBackend()) return QString();
return info->BestBackend()->unique_id_;
if (!device_info->BestBackend()) return QString();
return device_info->BestBackend()->unique_id_;
case Role_IconName:
return info->icon_name_;
return device_info->icon_name_;
case Role_Capacity:
case MusicStorage::Role_Capacity:
return info->size_;
return device_info->size_;
case 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:
if (info->device_) return State_Connected;
if (info->BestBackend() && info->BestBackend()->lister_) {
if (info->BestBackend()->lister_->DeviceNeedsMount(info->BestBackend()->unique_id_)) return State_NotMounted;
return State_NotConnected;
if (device_info->device_) return QVariant::fromValue(State::Connected);
if (device_info->BestBackend() && device_info->BestBackend()->lister_) {
if (device_info->BestBackend()->lister_->DeviceNeedsMount(device_info->BestBackend()->unique_id_)) return QVariant::fromValue(State::NotMounted);
return QVariant::fromValue(State::NotConnected);
}
return State_Remembered;
return QVariant::fromValue(State::Remembered);
case Role_UpdatingPercentage:
if (info->task_percentage_ == -1) return QVariant();
return info->task_percentage_;
if (device_info->task_percentage_ == -1) return QVariant();
return device_info->task_percentage_;
case MusicStorage::Role_Storage:
if (!info->device_ && info->database_id_ != -1) {
const_cast<DeviceManager*>(this)->Connect(info);
if (!device_info->device_ && device_info->database_id_ != -1) {
const_cast<DeviceManager*>(this)->Connect(device_info);
}
if (!info->device_) return QVariant();
return QVariant::fromValue<SharedPtr<MusicStorage>>(info->device_);
if (!device_info->device_) return QVariant();
return QVariant::fromValue<SharedPtr<MusicStorage>>(device_info->device_);
case MusicStorage::Role_StorageForceConnect:
if (!info->BestBackend()) return QVariant();
if (!info->device_) {
if (info->database_id_ == -1 && !info->BestBackend()->lister_->DeviceNeedsMount(info->BestBackend()->unique_id_)) {
if (info->BestBackend()->lister_->AskForScan(info->BestBackend()->unique_id_)) {
if (!device_info->BestBackend()) return QVariant();
if (!device_info->device_) {
if (device_info->database_id_ == -1 && !device_info->BestBackend()->lister_->DeviceNeedsMount(device_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));
QPushButton *pushbutton = dialog->addButton(tr("Connect device"), QMessageBox::AcceptRole);
dialog->exec();
if (dialog->clickedButton() != pushbutton) return QVariant();
}
}
const_cast<DeviceManager*>(this)->Connect(info);
const_cast<DeviceManager*>(this)->Connect(device_info);
}
if (!info->device_) return QVariant();
return QVariant::fromValue<SharedPtr<MusicStorage>>(info->device_);
if (!device_info->device_) return QVariant();
return QVariant::fromValue<SharedPtr<MusicStorage>>(device_info->device_);
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
if (ret.startsWith(u'/')) ret.remove(0, 1);
#endif
@@ -376,17 +371,17 @@ QVariant DeviceManager::data(const QModelIndex &idx, int role) const {
}
case Role_TranscodeMode:
return static_cast<int>(info->transcode_mode_);
return static_cast<int>(device_info->transcode_mode_);
case Role_TranscodeFormat:
return static_cast<int>(info->transcode_format_);
return static_cast<int>(device_info->transcode_format_);
case Role_SongCount:
if (!info->device_) return QVariant();
return info->device_->song_count();
if (!device_info->device_) return QVariant();
return device_info->device_->song_count();
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;
default:
@@ -410,7 +405,9 @@ DeviceInfo *DeviceManager::FindDeviceById(const QString &id) const {
for (int i = 0; i < devices_.count(); ++i) {
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 (const DeviceInfo::Backend &backend : std::as_const(devices_[i]->backends_)) {
if (!backend.lister_) continue;
const QList<QUrl> device_urls = backend.lister_->MakeDeviceUrls(backend.unique_id_);
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_)) {
DeviceInfo *match = FindDeviceById(backend.unique_id_);
if (match) return match;
for (const QString &unique_id : unique_ids) {
DeviceInfo *device_info_match = FindDeviceById(unique_id);
if (device_info_match) {
return device_info_match;
}
}
return nullptr;
}
@@ -455,42 +456,42 @@ void DeviceManager::PhysicalDeviceAdded(const QString &id) {
qLog(Info) << "Device added:" << id << lister->DeviceUniqueIDs();
// Do we have this device already?
DeviceInfo *info = FindDeviceById(id);
if (info) {
for (int backend_index = 0; backend_index < info->backends_.count(); ++backend_index) {
if (info->backends_[backend_index].unique_id_ == id) {
info->backends_[backend_index].lister_ = lister;
DeviceInfo *device_info = FindDeviceById(id);
if (device_info) {
for (int backend_index = 0; backend_index < device_info->backends_.count(); ++backend_index) {
if (device_info->backends_[backend_index].unique_id_ == id) {
device_info->backends_[backend_index].lister_ = lister;
break;
}
}
QModelIndex idx = ItemToIndex(info);
QModelIndex idx = ItemToIndex(device_info);
if (idx.isValid()) Q_EMIT dataChanged(idx, idx);
}
else {
// Check if we have another device with the same URL
info = FindDeviceByUrl(lister->MakeDeviceUrls(id));
if (info) {
device_info = FindDeviceByUrl(lister->MakeDeviceUrls(id));
if (device_info) {
// 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 (info->database_id_ == -1 && info->BestBackend() && info->BestBackend()->lister_ == lister) {
info->friendly_name_ = lister->MakeFriendlyName(id);
info->size_ = lister->DeviceCapacity(id);
info->SetIcon(lister->DeviceIcons(id), info->friendly_name_);
if (device_info->database_id_ == -1 && device_info->BestBackend() && device_info->BestBackend()->lister_ == lister) {
device_info->friendly_name_ = lister->MakeFriendlyName(id);
device_info->size_ = lister->DeviceCapacity(id);
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);
}
else {
// It's a completely new device
info = new DeviceInfo(DeviceInfo::Type::Device, root_);
info->backends_ << DeviceInfo::Backend(lister, id);
info->friendly_name_ = lister->MakeFriendlyName(id);
info->size_ = lister->DeviceCapacity(id);
info->SetIcon(lister->DeviceIcons(id), info->friendly_name_);
device_info = new DeviceInfo(DeviceInfo::Type::Device, root_);
device_info->backends_ << DeviceInfo::Backend(lister, id);
device_info->friendly_name_ = lister->MakeFriendlyName(id);
device_info->size_ = lister->DeviceCapacity(id);
device_info->LoadIcon(lister->DeviceIcons(id), device_info->friendly_name_);
beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count()));
devices_ << info;
devices_ << device_info;
endInsertRows();
}
}
@@ -503,42 +504,42 @@ void DeviceManager::PhysicalDeviceRemoved(const QString &id) {
qLog(Info) << "Device removed:" << id;
DeviceInfo *info = FindDeviceById(id);
if (!info) return;
DeviceInfo *device_info = FindDeviceById(id);
if (!device_info) return;
QModelIndex idx = ItemToIndex(info);
const QModelIndex idx = ItemToIndex(device_info);
if (!idx.isValid()) return;
if (info->database_id_ != -1) {
if (device_info->database_id_ != -1) {
// Keep the structure around, but just "disconnect" it
for (int backend_index = 0; backend_index < info->backends_.count(); ++backend_index) {
if (info->backends_[backend_index].unique_id_ == id) {
info->backends_[backend_index].lister_ = nullptr;
for (int backend_index = 0; backend_index < device_info->backends_.count(); ++backend_index) {
if (device_info->backends_[backend_index].unique_id_ == id) {
device_info->backends_[backend_index].lister_ = nullptr;
break;
}
}
if (info->device_ && info->device_->lister() == lister) {
info->device_->Close();
if (device_info->device_ && device_info->device_->lister() == lister) {
device_info->device_->Close();
}
if (!info->device_) Q_EMIT DeviceDisconnected(idx);
if (!device_info->device_) Q_EMIT DeviceDisconnected(idx);
Q_EMIT dataChanged(idx, idx);
}
else {
// 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) {
if (info->backends_[backend_index].unique_id_ == id) {
info->backends_.removeAt(backend_index);
for (int backend_index = 0; backend_index < device_info->backends_.count(); ++backend_index) {
if (device_info->backends_[backend_index].unique_id_ == id) {
device_info->backends_.removeAt(backend_index);
break;
}
}
if (info->backends_.isEmpty()) {
if (device_info->backends_.isEmpty()) {
beginRemoveRows(ItemToIndex(root_), idx.row(), idx.row());
devices_.removeAll(info);
root_->Delete(info->row);
devices_.removeAll(device_info);
root_->Delete(device_info->row);
endRemoveRows();
}
}
@@ -550,8 +551,8 @@ void DeviceManager::PhysicalDeviceChanged(const QString &id) {
DeviceLister *lister = qobject_cast<DeviceLister*>(sender());
Q_UNUSED(lister);
DeviceInfo *info = FindDeviceById(id);
if (!info) return;
DeviceInfo *device_info = FindDeviceById(id);
if (!device_info) return;
// TODO
@@ -561,40 +562,41 @@ SharedPtr<ConnectedDevice> DeviceManager::Connect(const QModelIndex &idx) {
SharedPtr<ConnectedDevice> ret;
DeviceInfo *info = IndexToItem(idx);
if (!info) return ret;
DeviceInfo *device_info = IndexToItem(idx);
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 (!info) return ret;
if (info->device_) { // Already connected
return info->device_;
if (!device_info) {
return SharedPtr<ConnectedDevice>();
}
if (!info->BestBackend() || !info->BestBackend()->lister_) { // Not physically connected
return ret;
if (device_info->device_) { // Already connected
return device_info->device_;
}
if (info->BestBackend()->lister_->DeviceNeedsMount(info->BestBackend()->unique_id_)) { // Mount the device
info->BestBackend()->lister_->MountDeviceAsync(info->BestBackend()->unique_id_);
return ret;
if (!device_info->BestBackend() || !device_info->BestBackend()->lister_) { // Not physically connected
return SharedPtr<ConnectedDevice>();
}
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) {
// 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
const QList<QUrl> urls = info->BestBackend()->lister_->MakeDeviceUrls(info->BestBackend()->unique_id_);
if (urls.isEmpty()) return ret;
const QList<QUrl> urls = device_info->BestBackend()->lister_->MakeDeviceUrls(device_info->BestBackend()->unique_id_);
if (urls.isEmpty()) return SharedPtr<ConnectedDevice>();
// Take the first URL that we have a handler for
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("If you continue, this device will work slowly and songs copied to it may not work."),
QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort)
return ret;
return SharedPtr<ConnectedDevice>();
}
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("If you continue, this device will work slowly and songs copied to it may not work."),
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)));
return ret;
return SharedPtr<ConnectedDevice>();
}
QMetaObject meta_object = device_classes_.value(device_url.scheme());
QObject *instance = meta_object.newInstance(
Q_ARG(QUrl, device_url),
Q_ARG(DeviceLister*, info->BestBackend()->lister_),
Q_ARG(QString, info->BestBackend()->unique_id_),
Q_ARG(DeviceLister*, device_info->BestBackend()->lister_),
Q_ARG(QString, device_info->BestBackend()->unique_id_),
Q_ARG(DeviceManager*, this),
Q_ARG(SharedPtr<TaskManager>, task_manager_),
Q_ARG(SharedPtr<Database>, database_),
Q_ARG(SharedPtr<TagReaderClient>, tagreader_client_),
Q_ARG(SharedPtr<AlbumCoverLoader>, albumcover_loader_),
Q_ARG(int, info->database_id_),
Q_ARG(int, device_info->database_id_),
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();
return ret;
return connected_device;
}
bool result = ret->Init();
bool result = connected_device->Init();
if (!result) {
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);
if (!idx.isValid()) return ret;
QModelIndex idx = ItemToIndex(device_info);
if (!idx.isValid()) return connected_device;
Q_EMIT dataChanged(idx, idx);
QObject::connect(&*info->device_, &ConnectedDevice::TaskStarted, this, &DeviceManager::DeviceTaskStarted);
QObject::connect(&*info->device_, &ConnectedDevice::SongCountUpdated, this, &DeviceManager::DeviceSongCountUpdated);
QObject::connect(&*info->device_, &ConnectedDevice::DeviceConnectFinished, this, &DeviceManager::DeviceConnectFinished);
QObject::connect(&*info->device_, &ConnectedDevice::DeviceCloseFinished, this, &DeviceManager::DeviceCloseFinished);
QObject::connect(&*info->device_, &ConnectedDevice::Error, this, &DeviceManager::DeviceError);
QObject::connect(&*device_info->device_, &ConnectedDevice::TaskStarted, this, &DeviceManager::DeviceTaskStarted);
QObject::connect(&*device_info->device_, &ConnectedDevice::SongCountUpdated, this, &DeviceManager::DeviceSongCountUpdated);
QObject::connect(&*device_info->device_, &ConnectedDevice::DeviceConnectFinished, this, &DeviceManager::DeviceConnectFinished);
QObject::connect(&*device_info->device_, &ConnectedDevice::DeviceCloseFinished, this, &DeviceManager::DeviceCloseFinished);
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) {
DeviceInfo *info = FindDeviceById(id);
if (!info) return;
DeviceInfo *device_info = FindDeviceById(id);
if (!device_info) return;
QModelIndex idx = ItemToIndex(info);
QModelIndex idx = ItemToIndex(device_info);
if (!idx.isValid()) return;
if (success) {
Q_EMIT DeviceConnected(idx);
}
else {
info->device_->Close();
device_info->device_->Close();
}
}
void DeviceManager::DeviceCloseFinished(const QString &id) {
DeviceInfo *info = FindDeviceById(id);
if (!info) return;
DeviceInfo *device_info = FindDeviceById(id);
if (!device_info) return;
info->device_.reset();
device_info->device_.reset();
QModelIndex idx = ItemToIndex(info);
QModelIndex idx = ItemToIndex(device_info);
if (!idx.isValid()) return;
Q_EMIT DeviceDisconnected(idx);
Q_EMIT dataChanged(idx, idx);
if (info->unmount_ && info->BestBackend() && info->BestBackend()->lister_) {
info->BestBackend()->lister_->UnmountDeviceAsync(info->BestBackend()->unique_id_);
if (device_info->unmount_ && device_info->BestBackend() && device_info->BestBackend()->lister_) {
device_info->BestBackend()->lister_->UnmountDeviceAsync(device_info->BestBackend()->unique_id_);
}
if (info->forget_) {
RemoveFromDB(info, idx);
if (device_info->forget_) {
RemoveFromDB(device_info, idx);
}
}
DeviceInfo *DeviceManager::GetDevice(const QModelIndex &idx) const {
DeviceInfo *info = IndexToItem(idx);
return info;
DeviceInfo *device_info = IndexToItem(idx);
return device_info;
}
SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(const QModelIndex &idx) const {
SharedPtr<ConnectedDevice> ret;
DeviceInfo *info = IndexToItem(idx);
if (!info) return ret;
return info->device_;
SharedPtr<ConnectedDevice> connected_device;
DeviceInfo *device_info = IndexToItem(idx);
if (!device_info) return connected_device;
return device_info->device_;
}
SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(DeviceInfo *info) const {
SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(DeviceInfo *device_info) const {
SharedPtr<ConnectedDevice> ret;
if (!info) return ret;
return info->device_;
SharedPtr<ConnectedDevice> connected_device;
if (!device_info) return connected_device;
return device_info->device_;
}
@@ -750,9 +752,9 @@ int DeviceManager::GetDatabaseId(const QModelIndex &idx) const {
if (!idx.isValid()) return -1;
DeviceInfo *info = IndexToItem(idx);
if (!info) return -1;
return info->database_id_;
DeviceInfo *device_info = IndexToItem(idx);
if (!device_info) return -1;
return device_info->database_id_;
}
@@ -760,17 +762,17 @@ DeviceLister *DeviceManager::GetLister(const QModelIndex &idx) const {
if (!idx.isValid()) return nullptr;
DeviceInfo *info = IndexToItem(idx);
if (!info || !info->BestBackend()) return nullptr;
return info->BestBackend()->lister_;
DeviceInfo *device_info = IndexToItem(idx);
if (!device_info || !device_info->BestBackend()) return nullptr;
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);
info->device_->Close();
device_info->device_->Close();
}
@@ -778,37 +780,37 @@ void DeviceManager::Forget(const QModelIndex &idx) {
if (!idx.isValid()) return;
DeviceInfo *info = IndexToItem(idx);
if (!info) return;
DeviceInfo *device_info = IndexToItem(idx);
if (!device_info) return;
if (info->database_id_ == -1) return;
if (device_info->database_id_ == -1) return;
if (info->device_) {
info->forget_ = true;
Disconnect(info, idx);
if (device_info->device_) {
device_info->forget_ = true;
Disconnect(device_info, idx);
}
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_);
info->database_id_ = -1;
backend_->RemoveDevice(device_info->database_id_);
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());
devices_.removeAll(info);
root_->Delete(info->row);
devices_.removeAll(device_info);
root_->Delete(device_info->row);
endRemoveRows();
}
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);
info->SetIcon(info->BestBackend()->lister_->DeviceIcons(id), info->friendly_name_);
device_info->friendly_name_ = device_info->BestBackend()->lister_->MakeFriendlyName(id);
device_info->LoadIcon(device_info->BestBackend()->lister_->DeviceIcons(id), device_info->friendly_name_);
Q_EMIT dataChanged(idx, idx);
}
@@ -818,18 +820,18 @@ void DeviceManager::SetDeviceOptions(const QModelIndex &idx, const QString &frie
if (!idx.isValid()) return;
DeviceInfo *info = IndexToItem(idx);
if (!info) return;
DeviceInfo *device_info = IndexToItem(idx);
if (!device_info) return;
info->friendly_name_ = friendly_name;
info->SetIcon(QVariantList() << icon_name, friendly_name);
info->transcode_mode_ = mode;
info->transcode_format_ = format;
device_info->friendly_name_ = friendly_name;
device_info->LoadIcon(QVariantList() << icon_name, friendly_name);
device_info->transcode_mode_ = mode;
device_info->transcode_format_ = format;
Q_EMIT dataChanged(idx, idx);
if (info->database_id_ != -1) {
backend_->SetDeviceOptions(info->database_id_, friendly_name, icon_name, mode, format);
if (device_info->database_id_ != -1) {
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;
for (int i = 0; i < devices_.count(); ++i) {
DeviceInfo *info = devices_.value(i);
if (info->device_ && &*info->device_ == device) {
QModelIndex index = ItemToIndex(info);
DeviceInfo *device_info = devices_.value(i);
if (device_info->device_ && &*device_info->device_ == device) {
QModelIndex index = ItemToIndex(device_info);
if (!index.isValid()) continue;
active_tasks_[id] = index;
info->task_percentage_ = 0;
device_info->task_percentage_ = 0;
Q_EMIT dataChanged(index, index);
return;
}
@@ -864,12 +866,12 @@ void DeviceManager::TasksChanged() {
const QPersistentModelIndex idx = active_tasks_.value(task.id);
if (!idx.isValid()) continue;
DeviceInfo *info = IndexToItem(idx);
DeviceInfo *device_info = IndexToItem(idx);
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 {
info->task_percentage_ = 0;
device_info->task_percentage_ = 0;
}
Q_EMIT dataChanged(idx, idx);
@@ -881,10 +883,10 @@ void DeviceManager::TasksChanged() {
if (!idx.isValid()) continue;
DeviceInfo *info = IndexToItem(idx);
if (!info) continue;
DeviceInfo *device_info = IndexToItem(idx);
if (!device_info) continue;
info->task_percentage_ = -1;
device_info->task_percentage_ = -1;
Q_EMIT dataChanged(idx, idx);
active_tasks_.remove(active_tasks_.key(idx));
@@ -900,17 +902,17 @@ void DeviceManager::Unmount(const QModelIndex &idx) {
if (!idx.isValid()) return;
DeviceInfo *info = IndexToItem(idx);
if (!info) return;
DeviceInfo *device_info = IndexToItem(idx);
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_) {
info->unmount_ = true;
Disconnect(info, idx);
if (device_info->device_) {
device_info->unmount_ = true;
Disconnect(device_info, idx);
}
else if (info->BestBackend() && info->BestBackend()->lister_) {
info->BestBackend()->lister_->UnmountDeviceAsync(info->BestBackend()->unique_id_);
else if (device_info->BestBackend() && device_info->BestBackend()->lister_) {
device_info->BestBackend()->lister_->UnmountDeviceAsync(device_info->BestBackend()->unique_id_);
}
}
@@ -919,13 +921,13 @@ void DeviceManager::DeviceSongCountUpdated(const int count) {
Q_UNUSED(count);
ConnectedDevice *device = qobject_cast<ConnectedDevice*>(sender());
if (!device) return;
ConnectedDevice *connected_device = qobject_cast<ConnectedDevice*>(sender());
if (!connected_device) return;
DeviceInfo *info = FindDeviceById(device->unique_id());
if (!info) return;
DeviceInfo *device_info = FindDeviceById(connected_device->unique_id());
if (!device_info) return;
QModelIndex idx = ItemToIndex(info);
QModelIndex idx = ItemToIndex(device_info);
if (!idx.isValid()) return;
Q_EMIT dataChanged(idx, idx);
@@ -934,10 +936,10 @@ void DeviceManager::DeviceSongCountUpdated(const int count) {
QString DeviceManager::DeviceNameByID(const QString &unique_id) {
DeviceInfo *info = FindDeviceById(unique_id);
if (!info) return QString();
DeviceInfo *device_info = FindDeviceById(unique_id);
if (!device_info) return QString();
QModelIndex idx = ItemToIndex(info);
QModelIndex idx = ItemToIndex(device_info);
if (!idx.isValid()) return QString();
return data(idx, DeviceManager::Role_FriendlyName).toString();

View File

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

View File

@@ -28,7 +28,7 @@
#include "devicemanager.h"
#include "devicestatefiltermodel.h"
DeviceStateFilterModel::DeviceStateFilterModel(QObject *parent, DeviceManager::State state)
DeviceStateFilterModel::DeviceStateFilterModel(QObject *parent, const DeviceManager::State state)
: QSortFilterProxyModel(parent),
state_(state) {
@@ -40,7 +40,7 @@ DeviceStateFilterModel::DeviceStateFilterModel(QObject *parent, DeviceManager::S
bool DeviceStateFilterModel::filterAcceptsRow(const int row, const QModelIndex &parent) const {
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) {

View File

@@ -37,7 +37,7 @@ class DeviceStateFilterModel : public QSortFilterProxyModel {
Q_OBJECT
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;
@@ -52,7 +52,7 @@ class DeviceStateFilterModel : public QSortFilterProxyModel {
void ProxyRowCountChanged(const QModelIndex &idx, const int first, const int last);
private:
DeviceManager::State state_;
const DeviceManager::State state_;
};
#endif // DEVICESTATEFILTERMODEL_H

View File

@@ -128,19 +128,19 @@ void DeviceItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op
}
else {
switch (state) {
case DeviceManager::State_Remembered:
case DeviceManager::State::Remembered:
status_text = tr("Not connected");
break;
case DeviceManager::State_NotMounted:
case DeviceManager::State::NotMounted:
status_text = tr("Not mounted - double click to mount");
break;
case DeviceManager::State_NotConnected:
case DeviceManager::State::NotConnected:
status_text = tr("Double click to open");
break;
case DeviceManager::State_Connected:{
case DeviceManager::State::Connected:{
QVariant song_count = idx.data(DeviceManager::Role_SongCount);
if (song_count.isValid()) {
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) {
*ret << Song::FileType::MP4;
*ret << Song::FileType::MPEG;
*ret << Song::FileType::ALAC;
return true;
}

View File

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

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,7 +21,9 @@
#include "config.h"
#include <libmtp.h>
#ifdef HAVE_MTP
# include <libmtp.h>
#endif
#include <AvailabilityMacros.h>
#include <CoreFoundation/CFRunLoop.h>
@@ -41,12 +43,15 @@
#include <QScopeGuard>
#include "macosdevicelister.h"
#include "mtpconnection.h"
#include "includes/scoped_cftyperef.h"
#include "includes/scoped_nsobject.h"
#include "core/logging.h"
#include "core/scoped_nsautorelease_pool.h"
#ifdef HAVE_MTP
# include "mtpconnection.h"
#endif
#import <AppKit/NSWorkspace.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSNotification.h>
@@ -102,11 +107,15 @@ class ScopedIOObject {
// 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
#ifdef HAVE_MTP
QSet<MacOsDeviceLister::MTPDevice> MacOsDeviceLister::sMTPDeviceList;
#endif
size_t qHash(const MacOsDeviceLister::MTPDevice &d) {
return qHash(d.vendor_id) ^ qHash(d.product_id);
#ifdef HAVE_MTP
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) {}
@@ -116,6 +125,7 @@ bool MacOsDeviceLister::Init() {
ScopedNSAutoreleasePool pool;
#ifdef HAVE_MTP
// Populate MTP Device list.
if (sMTPDeviceList.empty()) {
LIBMTP_device_entry_t *devices = nullptr;
@@ -126,25 +136,26 @@ bool MacOsDeviceLister::Init() {
else {
for (int i = 0; i < num; ++i) {
LIBMTP_device_entry_t device = devices[i];
MTPDevice d;
d.vendor = QString::fromLatin1(device.vendor);
d.vendor_id = device.vendor_id;
d.product = QString::fromLatin1(device.product);
d.product_id = device.product_id;
d.quirks = device.device_flags;
sMTPDeviceList << d;
MTPDevice mtp_device;
mtp_device.vendor = QString::fromLatin1(device.vendor);
mtp_device.vendor_id = device.vendor_id;
mtp_device.product = QString::fromLatin1(device.product);
mtp_device.product_id = device.product_id;
mtp_device.quirks = device.device_flags;
sMTPDeviceList << mtp_device;
}
}
MTPDevice d;
d.vendor = "SanDisk"_L1;
d.vendor_id = 0x781;
d.product = "Sansa Clip+"_L1;
d.product_id = 0x74d0;
MTPDevice mtp_device;
mtp_device.vendor = "SanDisk"_L1;
mtp_device.vendor_id = 0x781;
mtp_device.product = "Sansa Clip+"_L1;
mtp_device.product_id = 0x74d0;
d.quirks = 0x2 | 0x4 | 0x40 | 0x4000;
sMTPDeviceList << d;
mtp_device.quirks = 0x2 | 0x4 | 0x40 | 0x4000;
sMTPDeviceList << mtp_device;
}
#endif // HAVE_MTP
run_loop_ = CFRunLoopGetCurrent();
@@ -240,12 +251,14 @@ CFTypeRef GetUSBRegistryEntry(io_object_t device, CFStringRef key) {
}
QString GetUSBRegistryEntryString(io_object_t device, CFStringRef key) {
ScopedCFTypeRef<CFStringRef> registry_string(reinterpret_cast<CFStringRef>(GetUSBRegistryEntry(device, key)));
if (registry_string) {
return QString::fromUtf8([reinterpret_cast<NSString*>(registry_string.get()) UTF8String]);
}
return QString();
}
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) {
ScopedCFTypeRef<CFTypeRef> interface_class(IORegistryEntrySearchCFProperty(
device,
kIOServicePlane,
CFSTR(kUSBInterfaceClass),
kCFAllocatorDefault,
kIORegistryIterateRecursively));
ScopedCFTypeRef<CFTypeRef> interface_class(IORegistryEntrySearchCFProperty(device, kIOServicePlane, CFSTR(kUSBInterfaceClass), kCFAllocatorDefault, kIORegistryIterateRecursively));
NSNumber *number = reinterpret_cast<NSNumber*>(interface_class.get());
if (number) {
int ret = [number unsignedShortValue];
return ret;
}
return 0;
}
@@ -322,12 +331,14 @@ QString GetSerialForDevice(io_object_t device) {
}
#ifdef HAVE_MTP
QString GetSerialForMTPDevice(io_object_t device) {
scoped_nsobject<NSString> serial(reinterpret_cast<NSString*>(GetPropertyForDevice(device, CFSTR(kUSBSerialNumberString))));
return "MTP/"_L1 + QString::fromUtf8([serial UTF8String]);
}
#endif // HAVE_MTP
QString FindDeviceProperty(const QString &bsd_name, CFStringRef property) {
@@ -343,6 +354,7 @@ QString FindDeviceProperty(const QString &bsd_name, CFStringRef property) {
} // namespace
#ifdef HAVE_MTP
quint64 MacOsDeviceLister::GetFreeSpace(const QUrl &url) {
QMutexLocker l(&libmtp_mutex_);
@@ -380,6 +392,8 @@ quint64 MacOsDeviceLister::GetCapacity(const QUrl &url) {
}
#endif // HAVE_MTP
void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *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)];
if (kind && strcmp([kind UTF8String], kIOCDMediaClass) == 0) {
// CD inserted.
QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
const QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
me->cd_devices_ << bsd_name;
Q_EMIT me->DeviceAdded(bsd_name);
return;
}
#endif
#endif // HAVE_AUDIOCD
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));
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(device.get()));
if (class_name && CFStringCompare(class_name.get(), CFSTR(kIOMediaClass), 0) == kCFCompareEqualTo) {
QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
const QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
const QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
CFMutableDictionaryRef cf_properties;
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) {
scoped_nsobject<NSDictionary> dict(reinterpret_cast<NSDictionary*>(cf_properties)); // Takes ownership.
if ([[dict objectForKey:@"Removable"] intValue] == 1) {
QString serial = GetSerialForDevice(device.get());
const QString serial = GetSerialForDevice(device.get());
if (!serial.isEmpty()) {
me->current_devices_[serial] = QString::fromLatin1(DADiskGetBSDName(disk));
Q_EMIT me->DeviceAdded(serial);
@@ -427,10 +441,9 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
void MacOsDeviceLister::DiskRemovedCallback(DADiskRef disk, void *context) {
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(context);
// We cannot access the USB tree when the disk is removed but we still get
// the BSD disk name.
// We cannot access the USB tree when the disk is removed but we still get 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)) {
Q_EMIT me->DeviceRemoved(bsd_name);
return;
@@ -496,6 +509,7 @@ int GetBusNumber(io_object_t o) {
void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon);
Q_UNUSED(me)
io_object_t object;
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); });
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 *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString)));
NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID)));
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;
device.vendor = QString::fromUtf8([vendor UTF8String]);
device.product = QString::fromUtf8([product UTF8String]);
device.vendor_id = [vendor_id unsignedShortValue];
device.product_id = [product_id unsignedShortValue];
device.quirks = 0;
MTPDevice mtp_device;
mtp_device.vendor = QString::fromUtf8([vendor UTF8String]);
mtp_device.product = QString::fromUtf8([product UTF8String]);
mtp_device.vendor_id = [vendor_id unsignedShortValue];
mtp_device.product_id = [product_id unsignedShortValue];
mtp_device.quirks = 0;
device.bus = -1;
device.address = -1;
mtp_device.bus = -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.
(device.vendor_id == 0x088e && device.product_id == 0x5036) ||
(mtp_device.vendor_id == 0x088e && mtp_device.product_id == 0x5036) ||
// 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.
interface_class == kUSBHIDInterfaceClass ||
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")));
int bus = GetBusNumber(object);
const int bus = GetBusNumber(object);
if (!addr || bus == -1) {
// Failed to get bus or address number.
continue;
}
device.bus = bus;
device.address = [addr intValue];
mtp_device.bus = bus;
mtp_device.address = [addr intValue];
// 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()) {
// Fill in quirks flags from libmtp.
device.quirks = it2->quirks;
me->FoundMTPDevice(device, GetSerialForMTPDevice(object));
mtp_device.quirks = it2->quirks;
me->FoundMTPDevice(mtp_device, GetSerialForMTPDevice(object));
continue;
}
#endif // HAVE_MTP
IOCFPlugInInterface **plugin_interface = nullptr;
SInt32 score;
kern_return_t err = IOCreatePlugInInterfaceForService(
object,
kIOUSBDeviceUserClientTypeID,
kIOCFPlugInInterfaceID,
&plugin_interface,
&score);
kern_return_t err = IOCreatePlugInInterfaceForService(object, kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin_interface, &score);
if (err != KERN_SUCCESS) {
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);
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);
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?).
QString str = QString::fromUtf16(reinterpret_cast<char16_t*>(data.data() + 2), (data.size() / 2) - 2);
#ifdef HAVE_MTP
if (str.startsWith("MSFT100"_L1)) {
// We got the OS descriptor!
char vendor_code = data[16];
@@ -621,8 +637,10 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
continue;
}
// 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) {
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon);
Q_UNUSED(me)
io_object_t object;
while ((object = IOIteratorNext(it))) {
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(object));
const QScopeGuard io_object_release = qScopeGuard([object]() { IOObjectRelease(object); });
if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
#ifdef HAVE_MTP
NSString *vendor = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBVendorString)));
NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString)));
NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID)));
NSNumber *product_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBProductID)));
QString serial = GetSerialForMTPDevice(object);
MTPDevice device;
device.vendor = QString::fromUtf8([vendor UTF8String]);
device.product = QString::fromUtf8([product UTF8String]);
device.vendor_id = [vendor_id unsignedShortValue];
device.product_id = [product_id unsignedShortValue];
const QString serial = GetSerialForMTPDevice(object);
MTPDevice mtp_device;
mtp_device.vendor = QString::fromUtf8([vendor UTF8String]);
mtp_device.product = QString::fromUtf8([product UTF8String]);
mtp_device.vendor_id = [vendor_id unsignedShortValue];
mtp_device.product_id = [product_id unsignedShortValue];
me->RemovedMTPDevice(serial);
#endif // HAVE_MTP
}
}
}
#ifdef HAVE_MTP
void MacOsDeviceLister::RemovedMTPDevice(const QString &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) {
qLog(Debug) << "New MTP device detected!" << device.bus << device.address;
mtp_devices_[serial] = device;
QList<QUrl> urls = MakeDeviceUrls(serial);
MTPDevice *d = &mtp_devices_[serial];
d->capacity = GetCapacity(urls[0]);
d->free_space = GetFreeSpace(urls[0]);
const QList<QUrl> urls = MakeDeviceUrls(serial);
MTPDevice *mtp_device = &mtp_devices_[serial];
mtp_device->capacity = GetCapacity(urls[0]);
mtp_device->free_space = GetFreeSpace(urls[0]);
Q_EMIT DeviceAdded(serial);
}
bool IsMTPSerial(const QString &serial) { return serial.startsWith("MTP"_L1); }
#endif // HAVE_MTP
bool MacOsDeviceLister::IsCDDevice(const QString &serial) const {
return cd_devices_.contains(serial);
}
QString MacOsDeviceLister::MakeFriendlyName(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) {
const MTPDevice &device = mtp_devices_[serial];
if (device.vendor.isEmpty()) {
return device.product;
const MTPDevice &mtp_device = mtp_devices_[serial];
if (mtp_device.vendor.isEmpty()) {
return mtp_device.product;
}
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<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
@@ -708,75 +741,86 @@ QString MacOsDeviceLister::MakeFriendlyName(const QString &serial) {
ScopedIOObject device(DADiskCopyIOMedia(disk));
QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
const QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
const QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
if (vendor.isEmpty()) {
return product;
}
return vendor + QLatin1Char(' ') + product;
}
QList<QUrl> MacOsDeviceLister::MakeDeviceUrls(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) {
const MTPDevice &device = mtp_devices_[serial];
QString str = QString::asprintf("gphoto2://usb-%d-%d/", device.bus, device.address);
const MTPDevice &mtp_device = mtp_devices_[serial];
const QString str = QString::asprintf("gphoto2://usb-%d-%d/", mtp_device.bus, mtp_device.address);
QUrlQuery url_query;
url_query.addQueryItem(u"vendor"_s, device.vendor);
url_query.addQueryItem(u"vendor_id"_s, QString::number(device.vendor_id));
url_query.addQueryItem(u"product"_s, device.product);
url_query.addQueryItem(u"product_id"_s, QString::number(device.product_id));
url_query.addQueryItem(u"quirks"_s, QString::number(device.quirks));
url_query.addQueryItem(u"vendor"_s, mtp_device.vendor);
url_query.addQueryItem(u"vendor_id"_s, QString::number(mtp_device.vendor_id));
url_query.addQueryItem(u"product"_s, mtp_device.product);
url_query.addQueryItem(u"product_id"_s, QString::number(mtp_device.product_id));
url_query.addQueryItem(u"quirks"_s, QString::number(mtp_device.quirks));
QUrl url(str);
url.setQuery(url_query);
return QList<QUrl>() << url;
}
#endif // HAVE_MTP
if (IsCDDevice(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<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
scoped_nsobject<NSDictionary> properties(reinterpret_cast<NSDictionary*>(DADiskCopyDescription(disk.get())));
scoped_nsobject<NSURL> volume_path([[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy]);
QString path = QString::fromUtf8([[volume_path path] UTF8String]);
QUrl ret = MakeUrlFromLocalPath(path);
const QString path = QString::fromUtf8([[volume_path path] UTF8String]);
const QUrl ret = MakeUrlFromLocalPath(path);
return QList<QUrl>() << ret;
}
QStringList MacOsDeviceLister::DeviceUniqueIDs() {
#ifdef HAVE_MTP
return current_devices_.keys() + mtp_devices_.keys();
#else
return current_devices_.keys();
#endif
}
QVariantList MacOsDeviceLister::DeviceIcons(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) {
return QVariantList();
}
#endif // HAVE_MTP
if (IsCDDevice(serial)) {
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<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
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<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;
ret << GuessIconForPath(path);
@@ -784,31 +828,45 @@ QVariantList MacOsDeviceLister::DeviceIcons(const QString &serial) {
if (!icon.isEmpty()) {
ret << icon;
}
return ret;
}
QString MacOsDeviceLister::DeviceManufacturer(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) {
return mtp_devices_[serial].vendor;
}
#endif // HAVE_MTP
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBVendorString));
}
QString MacOsDeviceLister::DeviceModel(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) {
return mtp_devices_[serial].product;
}
#endif // HAVE_MTP
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBProductString));
}
quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) {
QList<QUrl> urls = MakeDeviceUrls(serial);
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<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")));
quint64 ret = [capacity unsignedLongLongValue];
const quint64 ret = [capacity unsignedLongLongValue];
IOObjectRelease(device);
@@ -826,10 +884,13 @@ quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
quint64 MacOsDeviceLister::DeviceFreeSpace(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) {
QList<QUrl> urls = MakeDeviceUrls(serial);
return mtp_devices_[serial].free_space;
}
#endif // HAVE_MTP
QString bsd_name = current_devices_[serial];
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
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) {
#ifdef HAVE_MTP
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()));
DADiskUnmount(disk, kDADiskUnmountOptionDefault, &DiskUnmountCallback, this);
@@ -879,13 +942,16 @@ void MacOsDeviceLister::DiskUnmountCallback(DADiskRef disk, DADissenterRef disse
void MacOsDeviceLister::UpdateDeviceFreeSpace(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) {
if (mtp_devices_.contains(serial)) {
QList<QUrl> urls = MakeDeviceUrls(serial);
MTPDevice *d = &mtp_devices_[serial];
d->free_space = GetFreeSpace(urls[0]);
MTPDevice *mtp_device = &mtp_devices_[serial];
mtp_device->free_space = GetFreeSpace(urls[0]);
}
}
#endif
Q_EMIT DeviceChanged(serial);
}

View File

@@ -199,6 +199,7 @@ EditTagDialog::EditTagDialog(const SharedPtr<NetworkAccessManager> network,
}
else if (RatingBox *ratingbox = qobject_cast<RatingBox*>(widget)) {
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)));
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::AlbumSort, ui_->albumsort);
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::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::PerformerSort, ui_->performersort);
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) {
if (id == "title"_L1) return song.title();
if (id == "titlesort"_L1) return song.titlesort();
if (id == "artist"_L1) return song.artist();
if (id == "artistsort"_L1) return song.artistsort();
if (id == "album"_L1) return song.album();
if (id == "albumsort"_L1) return song.albumsort();
if (id == "albumartist"_L1) return song.albumartist();
if (id == "albumartistsort"_L1) return song.albumartistsort();
if (id == "composer"_L1) return song.composer();
if (id == "composersort"_L1) return song.composersort();
if (id == "performer"_L1) return song.performer();
if (id == "performersort"_L1) return song.performersort();
if (id == "grouping"_L1) return song.grouping();
if (id == "genre"_L1) return song.genre();
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) {
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 == "artistsort"_L1) current_.set_artistsort(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 == "albumartistsort"_L1) current_.set_albumartistsort(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 == "performersort"_L1) current_.set_performersort(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 == "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 {
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); });
}
@@ -605,7 +638,15 @@ void EditTagDialog::UpdateModifiedField(const FieldData &field, const QModelInde
QFont new_font(font());
new_font.setBold(modified);
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 action_different = false;
bool albumartist_enabled = false;
bool albumartistsort_enabled = false;
bool composer_enabled = false;
bool composersort_enabled = false;
bool performer_enabled = false;
bool performersort_enabled = false;
bool grouping_enabled = false;
bool genre_enabled = false;
bool compilation_enabled = false;
bool rating_enabled = false;
bool comment_enabled = false;
bool lyrics_enabled = false;
bool titlesort_enabled = false;
bool artistsort_enabled = false;
bool albumsort_enabled = false;
for (const QModelIndex &idx : indexes) {
if (data_.value(idx.row()).cover_action_ == UpdateCoverAction::None) {
data_[idx.row()].cover_result_ = AlbumCoverImageResult();
@@ -679,12 +726,21 @@ void EditTagDialog::SelectionChanged() {
if (song.albumartist_supported()) {
albumartist_enabled = true;
}
if (song.albumartistsort_supported()) {
albumartistsort_enabled = true;
}
if (song.composer_supported()) {
composer_enabled = true;
}
if (song.composersort_supported()) {
composersort_enabled = true;
}
if (song.performer_supported()) {
performer_enabled = true;
}
if (song.performersort_supported()) {
performersort_enabled = true;
}
if (song.grouping_supported()) {
grouping_enabled = true;
}
@@ -703,6 +759,15 @@ void EditTagDialog::SelectionChanged() {
if (song.lyrics_supported()) {
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;
@@ -759,14 +824,20 @@ void EditTagDialog::SelectionChanged() {
album_cover_choice_controller_->set_save_embedded_cover_override(embedded_cover);
ui_->albumartist->setEnabled(albumartist_enabled);
ui_->albumartistsort->setEnabled(albumartistsort_enabled);
ui_->composer->setEnabled(composer_enabled);
ui_->composersort->setEnabled(composersort_enabled);
ui_->performer->setEnabled(performer_enabled);
ui_->performersort->setEnabled(performersort_enabled);
ui_->grouping->setEnabled(grouping_enabled);
ui_->genre->setEnabled(genre_enabled);
ui_->compilation->setEnabled(compilation_enabled);
ui_->rating->setEnabled(rating_enabled);
ui_->comment->setEnabled(comment_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>
<x>0</x>
<y>0</y>
<width>801</width>
<height>918</height>
<width>781</width>
<height>1047</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@@ -700,54 +700,35 @@
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</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">
<widget class="QLabel" name="label_disc">
<widget class="QLabel" name="label_year">
<property name="text">
<string>Disc</string>
<string>Year</string>
</property>
<property name="buddy">
<cstring>disc</cstring>
<cstring>year</cstring>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="LineEdit" name="composer">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
<item row="25" column="1">
<widget class="QPushButton" name="fetch_tag">
<property name="text">
<string>Complete tags automatically</string>
</property>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
<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="13" column="0">
<widget class="QLabel" name="label_grouping">
<widget class="QLabel" name="label_performersort">
<property name="minimumSize">
<size>
<width>80</width>
@@ -755,14 +736,56 @@
</size>
</property>
<property name="text">
<string>Grouping</string>
<string>Performer sort</string>
</property>
<property name="buddy">
<cstring>grouping</cstring>
<cstring>performersort</cstring>
</property>
</widget>
</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">
<property name="minimumSize">
<size>
@@ -778,7 +801,130 @@
</property>
</widget>
</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">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
@@ -788,7 +934,63 @@
</property>
</widget>
</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">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
@@ -798,26 +1000,6 @@
</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="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">
<widget class="SpinBox" name="track">
<property name="correctionMode">
@@ -834,7 +1016,49 @@
</property>
</widget>
</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">
<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">
<property name="minimumSize">
<size>
@@ -850,18 +1074,18 @@
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QLabel" name="label_year">
<item row="0" column="2">
<widget class="QLabel" name="label_track">
<property name="text">
<string>Year</string>
<string>Track</string>
</property>
<property name="buddy">
<cstring>year</cstring>
<cstring>track</cstring>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_title">
<item row="10" column="0">
<widget class="QLabel" name="label_composer">
<property name="minimumSize">
<size>
<width>80</width>
@@ -869,10 +1093,72 @@
</size>
</property>
<property name="text">
<string>Title</string>
<string>Composer</string>
</property>
<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>
</widget>
</item>
@@ -892,66 +1178,7 @@
</property>
</widget>
</item>
<item row="8" 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">
<item row="18" column="0">
<widget class="QLabel" name="label_genre">
<property name="minimumSize">
<size>
@@ -967,7 +1194,17 @@
</property>
</widget>
</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">
<property name="minimumSize">
<size>
@@ -983,44 +1220,8 @@
</property>
</widget>
</item>
<item row="4" 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="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">
<item row="11" column="0">
<widget class="QLabel" name="label_composersort">
<property name="minimumSize">
<size>
<width>80</width>
@@ -1028,52 +1229,23 @@
</size>
</property>
<property name="text">
<string>Performer</string>
<string>Composer sort</string>
</property>
<property name="buddy">
<cstring>performer</cstring>
<cstring>composersort</cstring>
</property>
</widget>
</item>
<item row="15" column="0">
<widget class="QLabel" name="label_compilation">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
<item row="6" column="1">
<widget class="LineEdit" name="albumsort">
<property name="has_reset_button" stdset="0">
<bool>true</bool>
</property>
<property name="text">
<string>Compilation</string>
</property>
<property name="buddy">
<cstring>compilation</cstring>
<property name="has_clear_button" stdset="0">
<bool>false</bool>
</property>
</widget>
</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>
</item>
<item>
@@ -1131,12 +1303,12 @@
</property>
</widget>
</item>
<item row="21" column="1">
<widget class="QPushButton" name="fetch_lyrics">
<property name="text">
<string>Complete lyrics automatically</string>
</property>
</widget>
<item>
<widget class="QPushButton" name="fetch_lyrics">
<property name="text">
<string>Complete lyrics automatically</string>
</property>
</widget>
</item>
</layout>
</widget>
@@ -1199,21 +1371,29 @@
<tabstop>summary</tabstop>
<tabstop>filename</tabstop>
<tabstop>path</tabstop>
<tabstop>art_embedded</tabstop>
<tabstop>art_manual</tabstop>
<tabstop>art_automatic</tabstop>
<tabstop>art_unset</tabstop>
<tabstop>playcount_reset</tabstop>
<tabstop>tags_summary</tabstop>
<tabstop>tags_art_button</tabstop>
<tabstop>checkbox_embedded_cover</tabstop>
<tabstop>title</tabstop>
<tabstop>track</tabstop>
<tabstop>artist</tabstop>
<tabstop>titlesort</tabstop>
<tabstop>disc</tabstop>
<tabstop>album</tabstop>
<tabstop>artist</tabstop>
<tabstop>year</tabstop>
<tabstop>artistsort</tabstop>
<tabstop>album</tabstop>
<tabstop>albumsort</tabstop>
<tabstop>albumartist</tabstop>
<tabstop>albumartistsort</tabstop>
<tabstop>composer</tabstop>
<tabstop>composersort</tabstop>
<tabstop>performer</tabstop>
<tabstop>performersort</tabstop>
<tabstop>grouping</tabstop>
<tabstop>genre</tabstop>
<tabstop>compilation</tabstop>

View File

@@ -36,11 +36,8 @@ namespace {
constexpr char kDiscordApplicationId[] = "1352351827206733974";
constexpr char kStrawberryIconResourceName[] = "embedded_cover";
constexpr char kStrawberryIconDescription[] = "Strawberry Music Player";
constexpr qint64 kDiscordPresenceUpdateRateLimitMs = 2000;
} // namespace
using namespace discord_rpc;
namespace discord {
RichPresence::RichPresence(const SharedPtr<Player> player,
@@ -49,10 +46,8 @@ RichPresence::RichPresence(const SharedPtr<Player> player,
: QObject(parent),
player_(player),
playlist_manager_(playlist_manager),
send_presence_timestamp_(0),
enabled_(false) {
Discord_Initialize(kDiscordApplicationId, nullptr, 1, nullptr);
initialized_(false),
status_display_type_(0) {
QObject::connect(&*player_->engine(), &EngineBase::StateChanged, this, &RichPresence::EngineStateChanged);
QObject::connect(&*playlist_manager_, &PlaylistManager::CurrentSongChanged, this, &RichPresence::CurrentSongChanged);
@@ -63,7 +58,11 @@ RichPresence::RichPresence(const SharedPtr<Player> player,
}
RichPresence::~RichPresence() {
Discord_Shutdown();
if (initialized_) {
Discord_Shutdown();
}
}
void RichPresence::ReloadSettings() {
@@ -71,18 +70,25 @@ void RichPresence::ReloadSettings() {
Settings s;
s.beginGroup(DiscordRPCSettings::kSettingsGroup);
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();
if (enabled_ && !enabled) {
Discord_ClearPresence();
if (enabled && !initialized_) {
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) {
if (!initialized_) return;
if (state == EngineBase::State::Playing) {
SetTimestamp(player_->engine()->position_nanosec() / kNsecPerSec);
SendPresenceUpdate();
@@ -95,6 +101,8 @@ void RichPresence::EngineStateChanged(const EngineBase::State state) {
void RichPresence::CurrentSongChanged(const Song &song) {
if (!initialized_) return;
SetTimestamp(0LL);
activity_.length_secs = song.length_nanosec() / kNsecPerSec;
activity_.title = song.title();
@@ -107,34 +115,32 @@ void RichPresence::CurrentSongChanged(const Song &song) {
void RichPresence::SendPresenceUpdate() {
if (!enabled_) {
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;
if (!initialized_) return;
::DiscordRichPresence 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.smallImageKey = kStrawberryIconResourceName;
presence_data.smallImageText = kStrawberryIconDescription;
presence_data.instance = 0;
QByteArray artist;
if (!activity_.artist.isEmpty()) {
QByteArray artist = activity_.artist.toUtf8();
artist.prepend(tr("by ").toUtf8());
artist = activity_.artist.toUtf8();
if (artist.size() < 2) { // Discord activity 2 char min. fix
artist.append(" ");
}
presence_data.state = artist.constData();
}
if (!activity_.album.isEmpty() && activity_.album != activity_.title) {
QByteArray album = activity_.album.toUtf8();
QByteArray album;
if (!activity_.album.isEmpty()) {
album = activity_.album.toUtf8();
album.prepend(tr("on ").toUtf8());
presence_data.largeImageText = album.constData();
}
@@ -151,13 +157,19 @@ void RichPresence::SendPresenceUpdate() {
}
void RichPresence::SetTimestamp(const qint64 seconds) {
activity_.start_timestamp = QDateTime::currentSecsSinceEpoch();
activity_.seek_secs = seconds;
}
void RichPresence::Seeked(const qint64 seek_microseconds) {
if (!initialized_) return;
SetTimestamp(seek_microseconds / 1000LL);
SendPresenceUpdate();
}
} // namespace discord

View File

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

View File

@@ -256,3 +256,19 @@ bool EngineBase::ValidOutput(const QString &output) {
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();
void UpdateVolume(const uint volume);
void EmitAboutToFinish();
void UpdateSpotifyAccessToken(const QString &spotify_access_token);
public:
// Simple accessors
@@ -175,6 +176,11 @@ class EngineBase : public QObject {
void Finished();
private:
#ifdef HAVE_SPOTIFY
virtual void SetSpotifyAccessToken() {}
#endif
protected:
bool playbin3_enabled_;
bool exclusive_mode_;

View File

@@ -517,10 +517,20 @@ bool GstEngine::ExclusiveModeSupport(const QString &output) const {
void GstEngine::ReloadSettings() {
#ifdef HAVE_SPOTIFY
const QString old_spotify_access_token = spotify_access_token_;
#endif
EngineBase::ReloadSettings();
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) {
@@ -1199,3 +1209,13 @@ bool GstEngine::AnyExclusivePipelineActive() const {
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 AnyExclusivePipelineActive() const;
#ifdef HAVE_SPOTIFY
void SetSpotifyAccessToken() override;
#endif
private:
SharedPtr<TaskManager> task_manager_;
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.";
}
// 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;
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 == "skipcount"_L1) return metadata.skipcount();
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();

View File

@@ -66,7 +66,8 @@ void ChartLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id,
QObject::disconnect(reply, nullptr, this, nullptr);
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);
if (!reply_data_result.success()) {
@@ -75,9 +76,7 @@ void ChartLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id,
}
QXmlStreamReader reader(reply_data_result.data);
LyricsSearchResults results;
LyricsSearchResult result;
while (!reader.atEnd()) {
const QXmlStreamReader::TokenType type = reader.readNext();
const QString name = reader.name().toString();

View File

@@ -31,14 +31,12 @@
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QRegularExpression>
#include <QSettings>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include <QJsonParseError>
#include <QMessageBox>
#include <QMutexLocker>
#include "includes/shared_ptr.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);
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) {
@@ -302,10 +302,8 @@ void GeniusLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id)
const QString artist = primary_artist["name"_L1].toString();
const QString title = object_result["title"_L1].toString();
// Ignore results where both the artist and title don't match.
if (!artist.startsWith(search->request.albumartist, Qt::CaseInsensitive) &&
!artist.startsWith(search->request.artist, Qt::CaseInsensitive) &&
!title.startsWith(search->request.title, Qt::CaseInsensitive)) {
// Ignore results where the artist or title don't begin or end the same
if (!StartsOrEndsMatch(artist, search->request.artist) || !StartsOrEndsMatch(title, search->request.title)) {
continue;
}
@@ -323,6 +321,12 @@ void GeniusLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id)
QNetworkReply *new_reply = CreateGetRequest(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;
}
const QString content = QString::fromUtf8(data);
QString lyrics = HtmlLyricsProvider::ParseLyricsFromHTML(content, QRegularExpression(u"<div[^>]*>"_s), QRegularExpression(u"<\\/div>"_s), QRegularExpression(u"<div data-lyrics-container=[^>]+>"_s), true);
if (lyrics.isEmpty()) {
lyrics = HtmlLyricsProvider::ParseLyricsFromHTML(content, QRegularExpression(u"<div[^>]*>"_s), QRegularExpression(u"<\\/div>"_s), QRegularExpression(u"<div class=\"lyrics\">"_s), true);
}
static const QRegularExpression start_tag(u"<div[^>]*>"_s);
static const QRegularExpression end_tag(u"<\\/div>"_s);
static const QRegularExpression lyrics_start(u"<div data-lyrics-container=[^>]+>"_s);
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()) {
LyricsSearchResult result(lyrics);
result.artist = lyric.artist;
@@ -404,3 +414,17 @@ void GeniusLyricsProvider::EndSearch(const int id, const LyricsSearchRequest &re
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 HandleLyricReply(QNetworkReply *reply, const int search_id, const QUrl &url);
private:
static bool StartsOrEndsMatch(QString s, QString t);
private:
OAuthenticator *oauth_;
mutable QMutex mutex_access_token_;

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