Compare commits

...

382 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
Jonas Kvinge
3b560e4e4f Release 1.2.9 2025-04-08 23:52:00 +02:00
Jonas Kvinge
9e327c9556 Update Changelog 2025-04-08 23:52:00 +02:00
Jonas Kvinge
1ec640e088 LastFMImport: Fix progress 2025-04-08 23:05:44 +02:00
Jonas Kvinge
463aaf6942 Update Changelog 2025-04-08 21:19:29 +02:00
Jonas Kvinge
71287dd77e Add option to turn off playbin3 2025-04-08 21:19:29 +02:00
dependabot[bot]
b66c0f5573 Bump vmactions/openbsd-vm from 1.1.6 to 1.1.7
Bumps [vmactions/openbsd-vm](https://github.com/vmactions/openbsd-vm) from 1.1.6 to 1.1.7.
- [Release notes](https://github.com/vmactions/openbsd-vm/releases)
- [Commits](https://github.com/vmactions/openbsd-vm/compare/v1.1.6...v1.1.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-08 20:53:01 +02:00
Jonas Kvinge
a9f2c384fa RichPresence: Use class for activity 2025-04-08 20:52:34 +02:00
Jonas Kvinge
ae9584c213 Rename is_enabled to enabled 2025-04-08 20:33:54 +02:00
Jonas Kvinge
4db1c5ceb8 AlbumCoverFetcherSearch: Add debug for results 2025-04-08 20:33:27 +02:00
Jonas Kvinge
1738259467 SubsonicBaseRequest: Fix parsing
Fixes #1719
2025-04-08 20:18:54 +02:00
Jonas Kvinge
fcee02edc1 DeezerCoverProvider: Fix parsing
Fixes #1716
2025-04-08 20:04:02 +02:00
dependabot[bot]
4fff5820c5 Bump vmactions/freebsd-vm from 1.1.9 to 1.2.0
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.1.9 to 1.2.0.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.1.9...v1.2.0)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-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-04-07 19:32:24 +02:00
Jonas Kvinge
9aa6da2faf Mpris2: Ignore -Warray-bounds 2025-04-05 19:10:28 +02:00
Jonas Kvinge
279934411c Add Ubuntu Plucky 2025-04-05 18:31:08 +02:00
Jonas Kvinge
fd829551e8 Turn on git revision 2025-04-05 18:13:40 +02:00
Jonas Kvinge
be8f515388 Release 1.2.8 2025-04-05 12:38:22 +02:00
Jonas Kvinge
dd12462fbb Update Changelog 2025-04-05 12:38:22 +02:00
Strawberry Bot
5c3ded0099 New translations 2025-04-05 12:24:00 +02:00
Jonas Kvinge
2c9b14f5f2 GstEnginePipeline: Simplify version checks 2025-04-04 22:12:38 +02:00
Jonas Kvinge
8c195382c4 nsi: Disable spotify plugin 2025-04-03 22:51:12 +02:00
Jonas Kvinge
6e5198799c OAuthenticator: Rename function 2025-04-03 22:37:50 +02:00
Jonas Kvinge
389d04bbec OAuthenticator: Don't clear refresh token 2025-04-03 22:34:55 +02:00
Jonas Kvinge
6f0f88dd01 RichPresence: Simplify code 2025-04-03 22:21:51 +02:00
Jonas Kvinge
3dce84c73c MainWindow: Delay command line options until playlists are loaded
Fixes #1546
2025-03-30 16:45:34 +02:00
Jonas Kvinge
e798349aca Mpris2: Emit notifications when playlists are loaded
Fixes #1546
2025-03-30 16:43:29 +02:00
Jonas Kvinge
41ecf8e535 main: Add missing ifdef 2025-03-30 01:56:58 +01:00
Jonas Kvinge
70c96ded28 rpm: Add BuildRequires for cmake(RapidJSON) 2025-03-30 01:22:57 +01:00
Jonas Kvinge
d9cc6bf238 debian/control: Add rapidjson-dev 2025-03-30 01:22:57 +01:00
Jonas Kvinge
0a70ac1c95 Song: Update supported tags 2025-03-30 01:22:20 +01:00
Jonas Kvinge
e3a3f44513 TagReaderTagLib: Add full support for AIFF 2025-03-30 01:22:07 +01:00
Jonas Kvinge
c2e42ebf3a Song: Include fingerprints in IsSimilar
Fixes #1710
2025-03-30 00:34:43 +01:00
Jonas Kvinge
d71b344e77 MainWindow: Add missing ifdef 2025-03-30 00:07:22 +01:00
Jonas Kvinge
d5f7a4b883 RichPresence: Formatting and add settings reload 2025-03-30 00:06:05 +01:00
Jonas Kvinge
34fb289e33 serialization: Remove unneeded pragams 2025-03-29 23:01:41 +01:00
Jonas Kvinge
6d4d8251ad Update Changelog 2025-03-29 22:53:37 +01:00
Jonas Kvinge
fc69ef27e8 README: Add Discord rich presence 2025-03-29 22:46:50 +01:00
Jonas Kvinge
bbd8a24b75 discord: Add namespace and cleanup CMakeLists 2025-03-29 22:41:58 +01:00
fruityloops1
9fa9012c70 Discord RPC implementation 2025-03-29 22:41:58 +01:00
fruityloops1
2a4fc185ac Add discord-rpc library 2025-03-29 22:41:58 +01:00
Jonas Kvinge
fa0703246b Equalizer: Ignore -Warray-bounds 2025-03-28 23:09:49 +01:00
corubba
954c21e21e AlsaDeviceFinder: Use card id instead of card index
Like the card index, the card id is guaranteed to be unique. While card
index can easily change between reboots, the card id is based on the
actual audio hardware and does not change between reboots; or even
hardware changes. This makes using the card id preferable, because it
will "just work" 99% of the time, and removes the need to force cards to
have a specific index.

There is a corner case where card ids may change between reboots: If you
have two (or more) of the same audio hardware in the system. But that
should be rare enough, and requires explicit system configuration
anyway, so using the "custom" option should work here.

If there is an previously-saved index-based ALSA device in the config,
it will continue to work as-is and does not need to be migrated. There
is only a small UI side-effect: Because the index-based device will no
longer match any found id-based device, the settings window will show it
as "custom". Simply selecting the ALSA device from the drop-down again
will change it to the id-based device.
2025-03-28 19:54:32 +01:00
Jonas Kvinge
8954dfb0aa CMake: Add -W4 with MSVC 2025-03-25 18:05:41 +01:00
Jonas Kvinge
5e031be42c Fix cast warnings with MSVC 2025-03-25 18:05:41 +01:00
dependabot[bot]
d5281abb22 Bump apple-actions/import-codesign-certs from 3 to 5
Bumps [apple-actions/import-codesign-certs](https://github.com/apple-actions/import-codesign-certs) from 3 to 5.
- [Release notes](https://github.com/apple-actions/import-codesign-certs/releases)
- [Commits](https://github.com/apple-actions/import-codesign-certs/compare/v3...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-25 17:22:05 +01:00
Jonas Kvinge
f5368d8108 Revert "Bump apple-actions/import-codesign-certs from 3 to 4"
This reverts commit e2dc22c2c8.
2025-03-21 20:35:36 +01:00
dependabot[bot]
e2dc22c2c8 Bump apple-actions/import-codesign-certs from 3 to 4
Bumps [apple-actions/import-codesign-certs](https://github.com/apple-actions/import-codesign-certs) from 3 to 4.
- [Release notes](https://github.com/apple-actions/import-codesign-certs/releases)
- [Commits](https://github.com/apple-actions/import-codesign-certs/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-21 18:21:12 +01:00
Strawberry Bot
817ca828e6 New translations 2025-03-18 22:44:29 +01:00
dependabot[bot]
61dc2cc640 Bump vmactions/freebsd-vm from 1.1.8 to 1.1.9
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.1.8 to 1.1.9.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.1.8...v1.1.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-18 17:58:35 +01:00
Jonas Kvinge
cd4adf6f89 ebur128analysis: Handle extra enums with GStreamer 1.25 and higher 2025-03-17 22:57:36 +01:00
Jonas Kvinge
91ebcc948e debian/rules: Build with -DBUILD_WERROR=ON 2025-03-17 22:28:24 +01:00
Jonas Kvinge
e7e6d59172 Remove KDSingleApplication from 3rdparty 2025-03-17 21:38:11 +01:00
Jonas Kvinge
95b12a01a4 OAuthenticator: Fix logging 2025-03-17 19:42:59 +01:00
Jonas Kvinge
5947aeae24 nsi: Bump icu 2025-03-15 23:29:12 +01:00
Jonas Kvinge
0298fa0b73 TidalBaseRequest: Don't clear session 2025-03-14 20:18:53 +01:00
Jonas Kvinge
05d72c8bd6 nsi: Add asio for mingw 2025-03-13 00:10:37 +01:00
Roman Lebedev
70b7c4560d gst_channel_to_ebur_channel(): handle new top-surround channels
These seem to have appeared in gstreamer 1.26,
which is the version we need to use to guard the handling.

These are effectively geometrically located on the same azimuth,
but on the layer above than the non-top (i.e. middle layer)
surround channels. But they are still surround channels,
which ebur128 does not bias loudness-wise.

At least this is my understanding.
2025-03-12 22:20:56 +01:00
Roman Lebedev
2687dc31cc Support arbitrarily large EBU R 128 loudness normalization
While i have fixed gstreamer's `volume` in
https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/5063
i did not see anything that followed after it was merged, namely, in
https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/6222,
the feature was moved, `"volume"` was reverted to only handle `x10` gain,
and one needs to use `"volume-full-range"` instead to do arbitrary gain.
So let's do that.

This, of course, requires run-time detection of the version
of gstreamer base plugins that we are running with,
specifically, we need version `1.24`.
2025-03-12 22:20:56 +01:00
Jonas Kvinge
cabf1cb78d CI: Bump macOS runner 2025-03-11 22:54:34 +01:00
Jonas Kvinge
d9f68ab944 Application: Add QTimer include
Fixes #1695
2025-03-09 11:29:51 +01:00
Jonas Kvinge
8630c5329d LyricsFetcherSearch: Fix authentication check 2025-03-09 02:11:12 +01:00
Jonas Kvinge
66e175f6d1 PlaylistItem: Add dtor 2025-03-08 23:46:12 +01:00
Jonas Kvinge
1173d5f865 Lyrics: Refactor 2025-03-08 23:26:44 +01:00
Jonas Kvinge
b02b114caf Scrobbler: Refactor 2025-03-08 23:19:42 +01:00
Jonas Kvinge
cd516c37b9 Refactor Tidal, Spotify, Qobuz, Subsonic and cover providers
Use common HTTP, Json and OAuthenticator class
2025-03-08 23:11:07 +01:00
Jonas Kvinge
7de8a44709 Add OAuthenticator 2025-03-08 22:46:59 +01:00
Jonas Kvinge
bb345b14de Add base classes for HTTP and Json 2025-03-08 22:46:46 +01:00
Jonas Kvinge
baa82966d8 Move SearchType to StreamingService 2025-03-08 22:41:43 +01:00
Jonas Kvinge
f85d60f5cd Formatting 2025-03-08 22:31:00 +01:00
Jonas Kvinge
6f731fcf4a MainWindow: Add const 2025-03-08 22:30:27 +01:00
Jonas Kvinge
bdbe66b116 Support more collections 2025-03-08 22:24:28 +01:00
Jonas Kvinge
5ae0320911 CollectionBackend: Add delete songs by URLs function 2025-03-08 21:46:27 +01:00
Jonas Kvinge
31671af5f5 MultiLoadingIndicator: Only emit task count change when needed 2025-03-08 21:45:34 +01:00
Jonas Kvinge
27184eb001 Utilities: Add StringListToHTML 2025-03-08 21:45:00 +01:00
Jonas Kvinge
2e30f5c585 StreamTagReader: Rename variable 2025-03-08 21:44:33 +01:00
Jonas Kvinge
109d3f9ec3 PlaylistBackend: Move query to static function 2025-03-08 21:36:42 +01:00
Jonas Kvinge
e9d413c7dc QTcpServer: Add success and port 2025-03-08 21:26:47 +01:00
Jonas Kvinge
ee60191b6c MusicBrainzClient: Formatting 2025-03-08 21:24:59 +01:00
Jonas Kvinge
a6d8627129 AcoustidClient: Formatting 2025-03-08 21:24:29 +01:00
Jonas Kvinge
d317c9158b UrlHandler: Formatting 2025-03-08 21:23:02 +01:00
Jonas Kvinge
3716e8c3ef CollectionModel: Rename variable 2025-03-08 21:22:40 +01:00
Jonas Kvinge
ce0e1e900e Update README.md 2025-03-04 23:31:23 +01:00
Jonas Kvinge
f1aa92ec9f Update README.md 2025-03-04 21:25:35 +01:00
Jonas Kvinge
25bdfcdb76 nsi: Update sqlite3 dll name 2025-02-20 22:37:43 +01:00
Jonas Kvinge
6a3de3937a AppearanceSettingsPage: Add tooltip about restart
You need to restart Strawberry for this setting to take affect.
2025-02-20 16:11:24 +01:00
Jonas Kvinge
5f775e87ae BackendSettingsPage: Add tooltip for HTTP/2
You need to restart Strawberry for this setting to take affect
2025-02-20 16:10:21 +01:00
Jonas Kvinge
1fd83c55ee Equalizer: Add tooltip that playback must be restarted 2025-02-20 16:09:10 +01:00
Jonas Kvinge
e6e9edef7d tests: Remove unused TestObjectDecorators 2025-02-18 23:00:01 +01:00
Jonas Kvinge
1bae665f76 CI: Fix OpenMandriva build 2025-02-18 22:02:22 +01:00
Jonas Kvinge
5bfc8b74f5 CI: Fix Mageia build 2025-02-18 22:02:12 +01:00
Jonas Kvinge
e588896729 FilterParser: Update tooltip
Fixes #1680
2025-02-18 17:08:17 +01:00
Jonas Kvinge
0cd0f7f2e7 FilesystemMusicStorage: Use QFile::supportsMoveToTrash 2025-02-18 16:55:02 +01:00
Jonas Kvinge
6db540a3a7 nsi: Bump libFLAC version 2025-02-12 01:25:12 +01:00
Jonas Kvinge
82679e0cea Fix issue template 2025-02-12 01:07:04 +01:00
Jonas Kvinge
d571bc3305 TagReaderTagLib: Fix build without stream tagreader
Fixes #1672
2025-02-10 23:29:40 +01:00
Jonas Kvinge
2b52553864 Add stream tagreader 2025-02-08 02:53:10 +01:00
Jonas Kvinge
215627b0e4 TagReaderGME: Fix use of Qt::CaseInsensitive and length check 2025-02-08 01:17:44 +01:00
Jonas Kvinge
b17cae6ec7 rpm: Exclude %debug_package on tumbleweed 2025-02-07 22:23:04 +01:00
Jonas Kvinge
30ac9697ea BackendSettingsPage: Increase device lineedit height
Bottom of the text was cut off with the breeze style
2025-02-07 21:27:17 +01:00
Jonas Kvinge
61e3ea249d Turn off "Grey out unavailable songs in playlists on startup" by default 2025-02-02 23:48:05 +01:00
Jonas Kvinge
d1986eeae2 Tidal: Save token type 2025-02-01 22:25:53 +01:00
Jonas Kvinge
ba354207d2 Tidal: Remove deprecated username/password login 2025-02-01 22:10:53 +01:00
Jonas Kvinge
eac5674891 TidalService: Clear refresh token on sign out 2025-02-01 00:50:17 +01:00
Jonas Kvinge
8349a8b0ee Port back to "output" and "device" settings in lowercase
Was accidentally changed to capitalized.
2025-02-01 00:48:57 +01:00
Jonas Kvinge
b9b4e9f831 TidalSettingsPage: Add HI_RES_LOSSLESS 2025-01-31 23:17:19 +01:00
Jonas Kvinge
98ff2525f0 Turn on git revision 2025-01-31 18:26:03 +01:00
Jonas Kvinge
3fd29c6dcc Release 1.2.7 2025-01-31 16:35:19 +01:00
Jonas Kvinge
4429e9973f StandardItemIconLoader : Rename LoadIcon
Avoids conflict on Windows where windows headers define LoadIcon to LoadIconW with unicode.
2025-01-31 16:20:03 +01:00
Jonas Kvinge
1572d241d5 Replace Windows conflicting "LoadIcon" with "SetIcon"
Windows headers defines LoadIcon to LoadIconW when UNICODE is defined.
2025-01-31 16:10:23 +01:00
Jonas Kvinge
eae7e2a9e6 Update Changelog 2025-01-31 13:09:38 +01:00
Jonas Kvinge
251e5b379b Disable OSD Pretty on Wayland 2025-01-29 22:12:29 +01:00
Jonas Kvinge
0db082fca0 Replace Q_OS_WIN with Q_OS_WIN32 2025-01-28 20:30:43 +01:00
Jonas Kvinge
2799e55076 OSDPretty: Set window title 2025-01-28 20:24:27 +01:00
Jonas Kvinge
440ee43a91 Update Changelog 2025-01-27 14:56:06 +01:00
Jonas Kvinge
98c72ec1f8 import-from-clementine: Minor tidy 2025-01-27 14:55:51 +01:00
Jonas Kvinge
759488ae1a CMake: Reword QPlatformNativeInterface option 2025-01-27 14:55:30 +01:00
Jonas Kvinge
055cb413c9 import-from-clementine: Handle NULL filename and remove FTS
Fixes #1660
2025-01-27 14:41:14 +01:00
Jonas Kvinge
f760b87b58 CI: Add missing dependencies for PPA 2025-01-25 19:37:58 +01:00
Jonas Kvinge
39f228f862 CMake: Add QPA Platform interface as optional component 2025-01-25 18:31:22 +01:00
Jonas Kvinge
a683a279f5 CMake: Define NOMINMAX on Windows 2025-01-24 12:46:49 +01:00
Jonas Kvinge
e800926f57 Ignore -Wcpp for gtest.h/gmock.h 2025-01-24 12:46:48 +01:00
Strawberry Bot
dd9f80d539 New translations 2025-01-24 10:58:02 +01:00
Jonas Kvinge
8484cac4ed Add strawberry_fr_BE.ts 2025-01-24 10:42:40 +01:00
Strawberry Bot
e24097582f New translations 2025-01-24 10:40:12 +01:00
Jonas Kvinge
58fc8c82bb MainWindow: Maximize error dialog when window is shown 2025-01-22 17:51:16 +01:00
Jonas Kvinge
02bb875bb3 ErrorDialog: Only raise window if parent is maximized
Fixes #1627
2025-01-22 17:50:41 +01:00
Jonas Kvinge
5db01482eb Lazy: Fix bool 2025-01-22 17:49:52 +01:00
Jonas Kvinge
719fa6ffb3 MainWindow: Only hide window when system tray and keep running is enabled 2025-01-22 17:26:08 +01:00
Jonas Kvinge
159be5d79e MainWindow: Change close() to hide() in SetHiddenInTray
Otherwise close event is triggered causing Strawberry to quit.
2025-01-19 09:45:13 +01:00
Jonas Kvinge
911237e281 AnalyzerBase: Add missing parameter names 2025-01-19 09:42:00 +01:00
Jonas Kvinge
ae89ca8123 Turn on git revision 2025-01-17 12:34:43 +01:00
Jonas Kvinge
b832675893 Release 1.2.6 2025-01-17 10:27:48 +01:00
Jonas Kvinge
b4cfe636c9 TranscodeDialog: Fix mismatched definition 2025-01-17 09:15:55 +01:00
Jonas Kvinge
e6a0945dfa Call QObject::metaObject 2025-01-17 09:08:59 +01:00
Jonas Kvinge
726c105ed6 MoodbarItemDelegate: Remove delete of data
Memory is deleted in QCache::insert
2025-01-17 08:29:17 +01:00
Jonas Kvinge
d73cbc3a1d EngineBase: Fix mismatched definition 2025-01-17 08:26:11 +01:00
Jonas Kvinge
121f45d3b6 Playlist: Use sizeof playlist pointer to pointer 2025-01-17 07:22:49 +01:00
Jonas Kvinge
3a9ea81929 Queue: Fix sizeof, should be the pointer not the class 2025-01-17 07:12:25 +01:00
Jonas Kvinge
b919472241 Playlist: Correct sizeof 2025-01-17 06:56:08 +01:00
Jonas Kvinge
e8c8b39410 Turn on git revision 2025-01-17 04:29:05 +01:00
Jonas Kvinge
6904efef47 Release 1.2.5 2025-01-17 01:40:38 +01:00
Jonas Kvinge
fafa89baff CI: Disable GIO on Windows 2025-01-16 07:18:31 +01:00
Strawberry Bot
d9062446f5 New translations 2025-01-16 06:39:23 +01:00
Jonas Kvinge
9256b92d8f Update Changelog 2025-01-16 06:32:35 +01:00
Jonas Kvinge
f66459f3cb Make optional feature required unless disabled, add QtSparkle for macOS 2025-01-16 06:22:13 +01:00
Jonas Kvinge
f8ea9631ca Add sparkle 2025-01-15 23:03:40 +01:00
Jonas Kvinge
ab558f87b5 GstEnginePipeline: Use SetStateAsync in finish if needed 2025-01-15 07:01:43 +01:00
Jonas Kvinge
ab73eda2be Add mutex_protected_test 2025-01-15 07:00:28 +01:00
Jonas Kvinge
92f34ff36e mutex_protected: Add more operators 2025-01-15 07:00:16 +01:00
Strawberry Bot
d0bf2d7a9c New translations 2025-01-14 07:10:07 +01:00
Jonas Kvinge
b2cd3afe55 TagReaderClient: Add [[nodiscard]] 2025-01-14 06:36:15 +01:00
Jonas Kvinge
decd0a1dc6 TagReaderClient: Connect TagReaderReplyPtr
Makes sure TagReaderReplyPtr is not deleted too early.

Partial fix for #1633
2025-01-14 06:35:51 +01:00
Jonas Kvinge
3e0a9fa388 SettingsProvider: Use Settings
Fixes #1649
2025-01-13 12:22:31 +01:00
Jonas Kvinge
e5b6c5959f CMake: Use find_package for qtsparkle 2025-01-12 04:16:17 +01:00
Jonas Kvinge
8a9db5440d rpm: Enable unit tests on Fedora 2025-01-11 01:39:26 +01:00
Jonas Kvinge
28a25c5763 CMake: Fix add_test 2025-01-11 01:37:06 +01:00
dependabot[bot]
ea49fbcbee Bump vmactions/openbsd-vm from 1.1.5 to 1.1.6
Bumps [vmactions/openbsd-vm](https://github.com/vmactions/openbsd-vm) from 1.1.5 to 1.1.6.
- [Release notes](https://github.com/vmactions/openbsd-vm/releases)
- [Commits](https://github.com/vmactions/openbsd-vm/compare/v1.1.5...v1.1.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-11 00:37:52 +01:00
Jonas Kvinge
d9807b358e GioLister: Fix use of deprecated functions 2025-01-11 00:35:24 +01:00
Jonas Kvinge
3e53d2b237 CI: Add -Wno-maybe-uninitialized for Fedora 2025-01-11 00:35:24 +01:00
Jonas Kvinge
9122881f74 CI: Bump GCC on openSUSE Leap 2025-01-11 00:35:24 +01:00
Jonas Kvinge
9c36d7fd43 rpm: Add BUILD_WERROR 2025-01-11 00:35:24 +01:00
Jonas Kvinge
a4a365cbee CI: Set RPM_BUILD_NCPUS to 4 2025-01-11 00:35:24 +01:00
Jonas Kvinge
00f06b22b8 CMake: Check for gmock 2025-01-11 00:35:24 +01:00
Jonas Kvinge
e2d8838fca rpm: Run unit tests 2025-01-11 00:35:24 +01:00
Jonas Kvinge
9427691f39 CI: Install gtest and gmock for openSUSE, Fedora and Mageia 2025-01-11 00:35:24 +01:00
Jonas Kvinge
bebdcc4e7f CollectionModel: Simply data function 2025-01-10 17:47:30 +01:00
Jonas Kvinge
041f761921 test_utils: Fix Q_ASSERT 2025-01-10 15:35:15 +01:00
Jonas Kvinge
1435ae6dc0 Turn on git revision 2025-01-10 15:08:04 +01:00
Jonas Kvinge
33ae53a90f Release 1.2.4 2025-01-10 02:26:41 +01:00
Strawberry Bot
01c28867b7 New translations 2025-01-10 02:10:44 +01:00
Jonas Kvinge
08fb2ae331 Update Changelog 2025-01-10 01:58:19 +01:00
Jonas Kvinge
99970f9e52 Update strawberry_en_US.ts 2025-01-10 01:58:19 +01:00
Jonas Kvinge
bc206f43b4 TranscodeDialog: Minor adjustments 2025-01-10 01:58:19 +01:00
dependabot[bot]
018448159c Bump vmactions/freebsd-vm from 1.1.7 to 1.1.8
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.1.7 to 1.1.8.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.1.7...v1.1.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-10 01:27:57 +01:00
Jonas Kvinge
81fe90bdef Update Changelog 2025-01-10 01:07:32 +01:00
Jonas Kvinge
319558c535 tests: Fixed ignored return value 2025-01-10 00:58:41 +01:00
Jonas Kvinge
9095b0d6b2 Always run MSVC runtime installer 2025-01-08 19:34:27 +01:00
Jonas Kvinge
558eae1ca1 Remove unneeded slots 2025-01-08 04:57:17 +01:00
Jonas Kvinge
2c64f05cea MoodbarProxyStyle: Add const 2025-01-08 04:54:45 +01:00
Jonas Kvinge
c80e7071a1 StandardPaths: Fix namespace 2025-01-07 22:13:24 +01:00
Jonas Kvinge
038e679000 SmartPlaylistWizard: Apply dark mode workaround for all styles except vista
Fixes #1639
2025-01-07 22:04:28 +01:00
Jonas Kvinge
72447fecfb StandardPaths: Remove inheritance 2025-01-07 21:40:06 +01:00
Jonas Kvinge
c7830f6f05 MoodbarPipeline: Cleanup on finish 2025-01-07 01:09:49 +01:00
Jonas Kvinge
af525e42b6 MoodbarPipeline: Remove Q_ASSERT 2025-01-06 21:28:44 +01:00
Jonas Kvinge
eb83f23125 MoodbarPipeline: Use bytearray directly 2025-01-06 21:28:33 +01:00
Jonas Kvinge
7527d2ea9a MoodbarLoader: Set object name 2025-01-06 18:11:38 +01:00
Jonas Kvinge
69d38879d2 Use QCoreApplication::applicationName() directly 2025-01-06 00:38:51 +01:00
Jonas Kvinge
cbce9f7191 Override config, data and cache location 2025-01-05 23:45:29 +01:00
Jonas Kvinge
f938129d81 CI: Fix deb copy command 2025-01-05 21:22:08 +01:00
Jonas Kvinge
415a40ea04 FilterParser: Ignore space after operator 2025-01-05 20:25:48 +01:00
Jonas Kvinge
6e7aaed4ee Use QSharedPointer for GstEnginePipeline 2025-01-05 19:03:16 +01:00
Jonas Kvinge
7afae70bb0 GstEnginePipeline: Make sure all set states are finished before finishing pipeline 2025-01-05 18:58:03 +01:00
Jonas Kvinge
be8097919b mutex_protected: Add operator ++ and -- 2025-01-05 18:56:22 +01:00
Jonas Kvinge
1990a42e1d CI: Don't run build on l10n_master branch 2025-01-05 18:53:19 +01:00
Kyle Hopkins
8fcee4511d TranscodeDialog: update to optionally preserve directory structure 2025-01-05 18:34:14 +01:00
Leandro Matheus
24af1be666 Subsonic: Add support for using album id to retrieve album covers 2025-01-05 18:30:24 +01:00
Jonas Kvinge
8302a95bc1 Use shared pointers for moodbar pipelines
Possible fix for #1633
2025-01-05 18:28:41 +01:00
Jonas Kvinge
36a8ab49a0 MoodbarItemDelegate: Format comment 2025-01-05 03:51:00 +01:00
Jonas Kvinge
5c64dc9c4d MoodbarItemDelegate: Delete data if it fails to insert to cache 2025-01-05 03:50:51 +01:00
Strawberry Bot
c271743208 New translations 2025-01-05 00:22:50 +01:00
Jonas Kvinge
ee49b1ddc8 Update strawberry_en_US.ts 2025-01-04 04:56:08 +01:00
Jonas Kvinge
bf98633f16 Load XSPF title as playlist name 2025-01-04 04:52:17 +01:00
Jonas Kvinge
e2a928f2dc Save XSPF playlist with title
Fixes #1624
2025-01-04 03:48:53 +01:00
Jonas Kvinge
47d3312a6b PlaylistManager: Remove slash from playlist filename 2025-01-04 03:46:03 +01:00
Jonas Kvinge
4f97325953 CollectionSettingsPage: Fix string conversion 2025-01-04 03:32:50 +01:00
Jonas Kvinge
91e8fe0943 ScrobblerSettingsPage: Add tooltip 2025-01-04 03:15:38 +01:00
Jonas Kvinge
dc5894b38a CollectionWatcher: Ignore special filesystem paths
Fixes #1615
2025-01-04 03:06:46 +01:00
Jonas Kvinge
52ee50a2a4 CollectionSettingsPage: Add check for filesystem type 2025-01-04 02:58:59 +01:00
Jonas Kvinge
f545b028ee Add filesystem constants 2025-01-04 02:58:17 +01:00
Jonas Kvinge
a96627c5a9 Playlist: Invalidate deleted songs in main thread
Fixes #1625
2025-01-04 00:16:36 +01:00
Jonas Kvinge
a13fc31f83 PlaylistManager: Remove unused InvalidateDeletedSongs function 2025-01-04 00:09:26 +01:00
Jonas Kvinge
9ee5c8dc17 Playlist: Update copyright 2025-01-03 23:41:43 +01:00
Jonas Kvinge
82cd425ece Playlist: Add const 2025-01-03 23:41:30 +01:00
Jonas Kvinge
3b02d364ba Playlist: Rename some variables 2025-01-03 23:41:15 +01:00
dependabot[bot]
73e7947487 Bump vmactions/openbsd-vm from 1.1.4 to 1.1.5
Bumps [vmactions/openbsd-vm](https://github.com/vmactions/openbsd-vm) from 1.1.4 to 1.1.5.
- [Release notes](https://github.com/vmactions/openbsd-vm/releases)
- [Commits](https://github.com/vmactions/openbsd-vm/compare/v1.1.4...v1.1.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-30 17:38:02 +01:00
dependabot[bot]
cfc9a43b88 Bump vmactions/freebsd-vm from 1.1.6 to 1.1.7
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.1.6 to 1.1.7.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.1.6...v1.1.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-30 17:37:32 +01:00
dependabot[bot]
969500023a Bump vmactions/freebsd-vm from 1.1.5 to 1.1.6
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.1.5 to 1.1.6.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.1.5...v1.1.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-26 00:44:32 +01:00
Jonas Kvinge
53c72d4f8e CollectionModel: Don't use artist sort text for album 2024-12-17 22:38:04 +01:00
Jonas Kvinge
f971c92f32 Move debian back to source dir
PPA failed to build
2024-12-17 21:57:37 +01:00
Strawberry Bot
82156e8a13 New translations 2024-12-17 00:01:04 +01:00
dependabot[bot]
6a6285861e Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [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/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 23:59:25 +01:00
Jonas Kvinge
dae8c8730b debian: Remove whitespaces 2024-12-14 18:33:40 +01:00
Jonas Kvinge
20b47eae8f CI: Use -j 4 for Ubuntu 2024-12-14 18:33:29 +01:00
Jonas Kvinge
2ce8220d88 Move generated files to binary directory 2024-12-14 17:59:26 +01:00
Jonas Kvinge
35b0b5df57 CMake: Fix backtrace linking 2024-12-14 15:47:45 +01:00
Jonas Kvinge
63631d6b0c CMake: Fix backtrace linking 2024-12-14 05:30:31 +01:00
Jonas Kvinge
21ab2ef1a7 CI: Add FreeBSD and OpenBSD 2024-12-14 05:01:40 +01:00
Jonas Kvinge
a3f96d2b85 CMake: Link Backtrace 2024-12-14 05:01:14 +01:00
Jonas Kvinge
e2c1cb0116 Formatting 2024-12-14 00:55:53 +01:00
Jonas Kvinge
07e295776b Add Spotify to scrobbler 2024-12-10 01:29:57 +01:00
Jonas Kvinge
8604a39d94 Turn on git revision 2024-12-08 17:29:14 +01:00
Jonas Kvinge
315240fec7 Release 1.2.3 2024-12-08 16:21:28 +01:00
Jonas Kvinge
fbc6d326f8 Update Changelog 2024-12-08 16:15:24 +01:00
Strawberry Bot
c082377e7a New translations 2024-12-07 19:21:29 +01:00
Jonas Kvinge
448445a38a CI: Replace redhat-lsb-core with lsb_release 2024-12-07 14:42:15 +01:00
Jonas Kvinge
18f835d7e5 Mpris2: Check for valid current row 2024-12-07 14:36:44 +01:00
Jonas Kvinge
fd427dac29 Handle missing HTTP status code 2024-12-07 14:02:59 +01:00
Jonas Kvinge
e1afe03d51 Check for valid http status code 2024-12-07 00:32:06 +01:00
Jonas Kvinge
1b49653974 Update Changelog 2024-12-06 23:55:25 +01:00
Jonas Kvinge
d66126f998 GstEngine: Add missing seek
Fixes #1568
2024-12-06 23:44:27 +01:00
Jonas Kvinge
0fff5f672a Rename variables 2024-12-06 23:43:44 +01:00
Strawberry Bot
2726f01fb3 New translations 2024-12-04 22:41:27 +01:00
Jonas Kvinge
9a74fce53d DiscogsCoverProvider: Use anonymous namespace for constants 2024-12-02 22:01:00 +01:00
Jonas Kvinge
b3be8387f1 Song: Add .zst to rejected file extensions
Fixes #1612
2024-11-29 23:37:49 +01:00
Jonas Kvinge
d396cb515d Include cstddef before libcdio includes
Fixes #1610
2024-11-29 22:50:49 +01:00
Jonas Kvinge
2548b4648e Turn on git revision 2024-11-23 19:34:13 +01:00
504 changed files with 42675 additions and 15676 deletions

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -10,7 +10,7 @@ jobs:
build-opensuse:
name: Build openSUSE
if: github.repository != 'strawberrymusicplayer/strawberry-private' && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -21,18 +21,18 @@ jobs:
steps:
- name: Refresh repositories
run: zypper -n --gpg-auto-import-keys ref
- name: Upgrade packages
- name: Upgrade packages (Tumbleweed)
if: matrix.opensuse_version == 'tumbleweed'
run: zypper -n --gpg-auto-import-keys dup
- name: Upgrade packages
- name: Upgrade packages (Leap)
if: matrix.opensuse_version != 'tumbleweed'
run: zypper -n --gpg-auto-import-keys up
- name: Install gcc
- name: Install gcc (Tumbleweed)
if: matrix.opensuse_version == 'tumbleweed'
run: zypper -n --gpg-auto-import-keys in gcc gcc-c++
- name: Install gcc 13
- name: Install gcc (Leap)
if: matrix.opensuse_version != 'tumbleweed'
run: zypper -n --gpg-auto-import-keys in gcc13 gcc13-c++
run: zypper -n --gpg-auto-import-keys in gcc14 gcc14-c++
- name: Install packages
run: >
zypper -n --gpg-auto-import-keys in
@@ -68,6 +68,7 @@ jobs:
hicolor-icon-theme
qt6-core-devel
qt6-gui-devel
qt6-gui-private-devel
qt6-widgets-devel
qt6-concurrent-devel
qt6-network-devel
@@ -77,11 +78,26 @@ jobs:
qt6-base-common-devel
qt6-sql-sqlite
qt6-linguist-devel
gtest
gmock
sparsehash-devel
rapidjson-devel
- name: Install kdsingleapplication-qt6-devel
if: matrix.opensuse_version == 'tumbleweed'
run: zypper -n --gpg-auto-import-keys in kdsingleapplication-qt6-devel
- name: Build and install KDSingleApplication
if: matrix.opensuse_version == 'leap:15.6'
env:
CC: gcc-14
CXX: g++-14
run: |
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
cd KDSingleApplication
cmake -S . -B build -DCMAKE_BUILD_TYPE="Release" -DBUILD_SHARED_LIBS=OFF -DBUILD_STATIC_LIBS=ON -DKDSingleApplication_STATIC=ON
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
@@ -101,15 +117,18 @@ jobs:
run: cp strawberry-*.tar.xz /usr/src/packages/SOURCES/
- name: Build RPM (Tumbleweed)
if: matrix.opensuse_version == 'tumbleweed'
env:
RPM_BUILD_NCPUS: 4
working-directory: build
run: rpmbuild -ba ../dist/unix/strawberry.spec
run: rpmbuild -ba strawberry.spec
- name: Build RPM (Leap)
if: matrix.opensuse_version != 'tumbleweed'
working-directory: build
env:
CC: gcc-13
CXX: g++-13
run: rpmbuild -ba ../dist/unix/strawberry.spec
RPM_BUILD_NCPUS: 4
CC: gcc-14
CXX: g++-14
working-directory: build
run: rpmbuild -ba strawberry.spec
- name: Set subdir
id: set-subdir
run: echo "subdir=$(echo ${{matrix.opensuse_version}} | sed 's/leap:/lp/g' | sed 's/\.//g')" > $GITHUB_OUTPUT
@@ -132,12 +151,12 @@ jobs:
build-fedora:
name: Build Fedora
if: github.repository != 'strawberrymusicplayer/strawberry-private' && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
fedora_version: [ '39', '40', '41', '42' ]
fedora_version: [ '41', '42', '43' ]
container:
image: fedora:${{matrix.fedora_version}}
steps:
@@ -149,7 +168,7 @@ jobs:
run: >
dnf -y install
@development-tools
redhat-lsb-core
lsb_release
which
git
glibc
@@ -185,8 +204,12 @@ jobs:
libappstream-glib
hicolor-icon-theme
kdsingleapplication-qt6-devel
gtest-devel
gmock-devel
sparsehash-devel
rapidjson-devel
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
@@ -207,9 +230,9 @@ jobs:
run: cp strawberry-*.tar.xz ~/rpmbuild/SOURCES/
- name: Build RPM
env:
RPM_BUILD_NCPUS: "2"
RPM_BUILD_NCPUS: 4
working-directory: build
run: rpmbuild -ba ../dist/unix/strawberry.spec
run: rpmbuild -ba strawberry.spec
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
@@ -221,7 +244,7 @@ jobs:
build-openmandriva:
name: Build OpenMandriva
if: github.repository != 'strawberrymusicplayer/strawberry-private' && false
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -236,31 +259,30 @@ jobs:
run: >
dnf install -y
which
glibc
gcc-c++
git
gnutar
make
cmake
glib
lsb-release
rpmdevtools
rpm-build
glibc-devel
boost-devel
sqlite-devel
libasound-devel
pulseaudio-devel
libGL-devel
libgst-plugins-base1.0-devel
taglib-devel
chromaprint-devel
libebur128-devel
fftw-devel
icu-devel
libcdio-devel
libgpod-devel
libmtp-devel
lib64glib2.0-devel
lib64asound-devel
lib64pulseaudio-devel
lib64sqlite3-devel
lib64gstreamer-devel
lib64gst-plugins-base1.0-devel
lib64taglib-devel
lib64chromaprint-devel
lib64ebur128-devel
lib64fftw-devel
lib64icu-devel
lib64cdio-devel
lib64gpod-devel
lib64mtp-devel
lib64Qt6Core-devel
lib64Qt6Concurrent-devel
lib64Qt6Network-devel
@@ -269,6 +291,11 @@ jobs:
lib64Qt6Gui-devel
lib64Qt6Widgets-devel
lib64Qt6Test-devel
lib64kdsingleapplication-devel
lib64xkbcommon-devel
lib64gtest-devel
lib64gmock-devel
sparsehash-devel
qt6-cmake
qt6-qtbase-tools
qt6-qttools-linguist
@@ -276,10 +303,11 @@ jobs:
appstream
appstream-util
hicolor-icon-theme
rapidjson
- 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
@@ -300,9 +328,9 @@ jobs:
run: cp strawberry-*.tar.xz ~/rpmbuild/SOURCES/
- name: Build RPM
env:
RPM_BUILD_NCPUS: "2"
RPM_BUILD_NCPUS: 4
working-directory: build
run: rpmbuild -ba ../dist/unix/strawberry.spec
run: rpmbuild -ba strawberry.spec
- name: Upload artifacts
if: matrix.openmandriva_version != 'cooker'
uses: actions/upload-artifact@v4
@@ -315,7 +343,7 @@ jobs:
build-mageia:
name: Build Mageia
if: github.repository != 'strawberrymusicplayer/strawberry-private' && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -324,12 +352,6 @@ jobs:
container:
image: mageia:${{matrix.mageia_version}}
steps:
- name: Set media
run: |
urpmi.removemedia "Core Release"
urpmi.removemedia "Core Updates"
urpmi.addmedia "Core Release" "https://mirrors.kernel.org/mageia/distrib/${{matrix.mageia_version}}/x86_64/media/core/release/"
urpmi.addmedia "Core Updates" "https://mirrors.kernel.org/mageia/distrib/${{matrix.mageia_version}}/x86_64/media/core/updates/"
- name: Update repositories
run: urpmi.update -a
- name: Upgrade packages
@@ -371,11 +393,23 @@ jobs:
lib64qt6dbus-devel
lib64qt6help-devel
lib64qt6test-devel
lib64sparsehash-devel
lib64kdsingleapplication-devel
desktop-file-utils
appstream-util
hicolor-icon-theme
gtest
rapidjson
- name: Build and install KDSingleApplication
if: matrix.mageia_version == '9'
run: |
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
cd KDSingleApplication
cmake -S . -B build -DCMAKE_BUILD_TYPE="Release" -DBUILD_SHARED_LIBS=OFF -DBUILD_STATIC_LIBS=ON -DKDSingleApplication_STATIC=ON
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
@@ -396,9 +430,9 @@ jobs:
run: cp strawberry-*.tar.xz ~/rpmbuild/SOURCES/
- name: Build RPM
env:
RPM_BUILD_NCPUS: "2"
RPM_BUILD_NCPUS: 4
working-directory: build
run: rpmbuild -ba ../dist/unix/strawberry.spec
run: rpmbuild -ba strawberry.spec
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
@@ -410,7 +444,7 @@ jobs:
build-debian:
name: Build Debian
if: github.repository != 'strawberrymusicplayer/strawberry-private' && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -456,13 +490,24 @@ jobs:
libcdio-dev
libmtp-dev
libgpod-dev
libxkbcommon-dev
libsparsehash-dev
qt6-base-dev
qt6-base-private-dev
qt6-base-dev-tools
qt6-tools-dev
qt6-tools-dev-tools
qt6-l10n-tools
rapidjson-dev
- name: Build and install KDSingleApplication
run: |
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
cd KDSingleApplication
cmake -S . -B build -DCMAKE_BUILD_TYPE="Release" -DBUILD_SHARED_LIBS=OFF -DBUILD_STATIC_LIBS=ON -DKDSingleApplication_STATIC=ON
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
@@ -471,26 +516,29 @@ jobs:
- name: Create Build Environment
run: cmake -E make_directory build
- name: Configure CMake
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DBUILD_WERROR=ON
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON
- name: Delete build directory
run: rm -rf build
- name: make deb
run: dpkg-buildpackage -b -d -uc -us -nc -j2
run: dpkg-buildpackage -b -d -uc -us -nc -j4
- name: Copy deb
run: cp ../*.deb .
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: debian-${{matrix.debian_version}}
path: "*.deb"
path: |
*.deb
build-ubuntu:
name: Build Ubuntu
if: github.repository != 'strawberrymusicplayer/strawberry-private' && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ubuntu_version: [ 'noble', 'oracular' ]
ubuntu_version: [ 'noble', 'plucky' ]
container:
image: ubuntu:${{matrix.ubuntu_version}}
steps:
@@ -534,13 +582,24 @@ jobs:
libcdio-dev
libmtp-dev
libgpod-dev
libxkbcommon-dev
libsparsehash-dev
qt6-base-dev
qt6-base-private-dev
qt6-base-dev-tools
qt6-tools-dev
qt6-tools-dev-tools
qt6-l10n-tools
rapidjson-dev
- name: Build and install KDSingleApplication
run: |
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
cd KDSingleApplication
cmake -S . -B build -DCMAKE_BUILD_TYPE="Release" -DBUILD_SHARED_LIBS=OFF -DBUILD_STATIC_LIBS=ON -DKDSingleApplication_STATIC=ON
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
@@ -549,9 +608,11 @@ jobs:
- name: Create Build Environment
run: cmake -E make_directory build
- name: Configure CMake
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DBUILD_WERROR=ON
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON
- name: Delete build directory
run: rm -rf build
- name: make deb
run: dpkg-buildpackage -b -d -uc -us -nc -j2
run: dpkg-buildpackage -b -d -uc -us -nc -j4
- name: Copy deb
run: cp ../*.deb ../*.ddeb .
- name: Upload artifacts
@@ -565,12 +626,12 @@ jobs:
upload-ubuntu-ppa:
name: Upload Ubuntu PPA
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && (github.event_name == 'release' || (github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/ci' || github.ref == 'refs/heads/1.1'))) && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && (github.event_name == 'release' || (github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/ci' || github.ref == 'refs/heads/1.1')))
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ubuntu_version: [ 'noble', 'oracular' ]
ubuntu_version: [ 'noble', 'plucky' ]
container:
image: ubuntu:${{matrix.ubuntu_version}}
steps:
@@ -612,29 +673,38 @@ jobs:
libcdio-dev
libmtp-dev
libgpod-dev
libxkbcommon-dev
libsparsehash-dev
qt6-base-dev
qt6-base-private-dev
qt6-base-dev-tools
qt6-tools-dev
qt6-tools-dev-tools
qt6-l10n-tools
gstreamer1.0-alsa
gstreamer1.0-pulseaudio
libkdsingleapplication-qt6-dev
rapidjson-dev
- name: Install keyboxd
if: matrix.ubuntu_version == 'noble'
env:
DEBIAN_FRONTEND: noninteractive
run: apt install -y keyboxd
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
- name: Add safe git directory
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: Modify required KDSingleApplication version
run: |
sed -i 's/^find_package(KDSingleApplication-qt\${QT_VERSION_MAJOR} 1.1.0 REQUIRED)$/find_package(KDSingleApplication-qt\${QT_VERSION_MAJOR} 1.0.0 REQUIRED)/g' CMakeLists.txt
sed -i 's/, KDSingleApplication::Option::IncludeUsernameInSocketName);$/);/g' src/main.cpp
- name: Create Build Environment
run: cmake -E make_directory build
- name: Configure CMake
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DBUILD_WERROR=ON
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON
- name: Delete build directory
run: rm -rf build
- name: Import Ubuntu PPA GPG private key
@@ -651,14 +721,65 @@ jobs:
run: dput ppa:jonaski/strawberry ../*_source.changes
build-freebsd:
name: Build FreeBSD
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
- name: Build FreeBSD
id: build-freebsd
uses: vmactions/freebsd-vm@v1.2.3
with:
usesh: true
mem: 4096
prepare: pkg install -y git cmake pkgconf boost-libs alsa-lib glib qt6-base qt6-tools sqlite gstreamer1 gstreamer1-plugins chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf2 libgpod fftw3 icu kdsingleapplication googletest pulseaudio sparsehash rapidjson
run: |
set -e
git config --global --add safe.directory ${GITHUB_WORKSPACE}
cmake -E make_directory build
cmake -S . -B build -DCMAKE_BUILD_TYPE="Debug"
cmake --build build --config Debug --parallel 4
build-openbsd:
name: Build OpenBSD
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
- name: Build OpenBSD
id: build-openbsd
uses: vmactions/openbsd-vm@v1.2.0
with:
usesh: true
mem: 4096
prepare: pkg_add git cmake pkgconf boost glib2 qt6-qtbase qt6-qttools sqlite gstreamer1 gstreamer1-plugins-base chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf libgpod fftw3 icu4c kdsingleapplication pulseaudio sparsehash rapidjson
run: |
set -e
export LDFLAGS="-L/usr/local/lib"
git config --global --add safe.directory ${GITHUB_WORKSPACE}
cmake -E make_directory build
cmake -S . -B build -DCMAKE_BUILD_TYPE="Debug" -DENABLE_ALSA=OFF -DENABLE_DISCORD_RPC=OFF
cmake --build build --config Debug --parallel 4
build-macos-public:
name: Build macOS Public
if: github.repository != 'strawberrymusicplayer/strawberry-private' && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
strategy:
fail-fast: false
matrix:
runner: [ 'macos-13', 'macos-14' ]
runner: [ 'macos-13', 'macos-15' ]
buildtype: [ 'release' ]
runs-on: ${{ matrix.runner }}
@@ -697,14 +818,14 @@ jobs:
rm -f uninstall.sh
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
- name: Import certificate file
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false
uses: apple-actions/import-codesign-certs@v3
uses: apple-actions/import-codesign-certs@v5
with:
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE }}
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
@@ -737,10 +858,12 @@ jobs:
-DCMAKE_PREFIX_PATH="${{env.prefix_path}}/lib/cmake"
-DBUILD_WERROR=ON
-DUSE_BUNDLE=ON
-DENABLE_DBUS=OFF
-DICU_ROOT="${{env.prefix_path}}"
-DAPPLE_DEVELOPER_ID=$(test '${{github.repository}}' = 'strawberrymusicplayer/strawberry' && test '${{github.event.pull_request.base.repo.full_name}}' = '${{github.event.pull_request.head.repo.full_name}}' && echo "383J84DVB6" || echo "")
-DARCH="${{env.arch}}"
-DENABLE_SPOTIFY=$(test -f "${{env.prefix_path}}/lib/gstreamer-1.0/libgstspotify.dylib" && echo "ON" || echo "OFF")
-DENABLE_SPARKLE=ON
-DENABLE_QTSPARKLE=OFF
- name: Build
run: cmake --build build --config Release --parallel 4
@@ -764,7 +887,7 @@ jobs:
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
- name: Manually Codesign
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-14'
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15'
working-directory: build
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
@@ -846,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
@@ -876,10 +999,12 @@ jobs:
-DCMAKE_PREFIX_PATH="${{env.prefix_path}}/lib/cmake"
-DBUILD_WERROR=ON
-DUSE_BUNDLE=ON
-DENABLE_DBUS=OFF
-DICU_ROOT="${{env.prefix_path}}"
-DAPPLE_DEVELOPER_ID="383J84DVB6"
-DARCH="${{env.arch}}"
-DENABLE_SPOTIFY=$(test -f "${{env.prefix_path}}/lib/gstreamer-1.0/libgstspotify.dylib" && echo "ON" || echo "OFF")
-DENABLE_SPARKLE=ON
-DENABLE_QTSPARKLE=OFF
- name: Build
run: cmake --build build --config Release --parallel 4
@@ -931,7 +1056,7 @@ jobs:
build-windows-mingw:
name: Build Windows MinGW
if: github.repository != 'strawberrymusicplayer/strawberry-private' && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -947,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
@@ -974,7 +1099,7 @@ jobs:
-DBUILD_WERROR=ON
-DARCH="${{matrix.arch}}"
-DENABLE_WIN32_CONSOLE=$(test "${{matrix.buildtype}}" = "debug" && echo "ON" || echo "OFF")
-DENABLE_DBUS=OFF
-DENABLE_GIO=OFF
-DENABLE_AUDIOCD=OFF
-DENABLE_MTP=OFF
-DENABLE_GPOD=OFF
@@ -1120,13 +1245,43 @@ jobs:
build-windows-msvc:
name: Build Windows MSVC
if: github.repository != 'strawberrymusicplayer/strawberry-private' && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
runs-on: windows-2022
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
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
@@ -1140,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
@@ -1222,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
@@ -1239,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
@@ -1259,10 +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_SPOTIFY=ON
-DENABLE_GIO=OFF
-DENABLE_AUDIOCD=OFF
-DENABLE_MTP=OFF
-DENABLE_GPOD=OFF
-DENABLE_SPOTIFY=${{env.enable_spotify}}
- name: Run Make
shell: cmd
@@ -1331,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: >
@@ -1352,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
@@ -1396,6 +1581,11 @@ jobs:
exit 1
fi
- name: Download MSVC runtime
shell: bash
working-directory: build
run: curl -f -O -L https://aka.ms/vs/17/release/vc_redist.$(test "${{matrix.arch}}" = "x86_64" && echo "x64" || echo "${{matrix.arch}}").exe
- name: Create nsis installer
shell: cmd
working-directory: build
@@ -1447,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
@@ -1495,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
@@ -1503,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

2
.gitignore vendored
View File

@@ -11,6 +11,4 @@
/out
/CMakeSettings.json
/dist/scripts/maketarball.sh
/dist/unix/strawberry.spec
/debian/changelog
/dist/macos/Info.plist

4
.gitmodules vendored
View File

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

11
3rdparty/README.md vendored
View File

@@ -1,11 +0,0 @@
3rdparty libraries located in this directory
============================================
KDSingleApplication
-------------------
A small library used by Strawberry to prevent it from starting twice per user session.
If the user tries to start strawberry twice, the main window will maximize instead of starting another instance.
It is also used to pass command-line options through to the first instance.
This 3rdparty copy is used only if KDSingleApplication 1.1 or higher is not found on the system.
URL: https://github.com/KDAB/KDSingleApplication/

41
3rdparty/discord-rpc/CMakeLists.txt vendored Normal file
View File

@@ -0,0 +1,41 @@
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})

19
3rdparty/discord-rpc/LICENSE vendored Normal file
View File

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

162
3rdparty/discord-rpc/README.md vendored Normal file
View File

@@ -0,0 +1,162 @@
# Discord RPC
## Fork Notice
This library was slightly modified for Strawberry Music Player with some extra features from the new API and shared library support/more unnecessary components removed. The original repository is [here](https://github.com/discord/discord-rpc)
## Deprecation Notice
This library has been deprecated in favor of Discord's GameSDK. [Learn more here](https://discordapp.com/developers/docs/game-sdk/sdk-starter-guide)
---
This is a library for interfacing your game with a locally running Discord desktop client. It's known to work on Windows, macOS, and Linux. You can use the lib directly if you like, or use it as a guide to writing your own if it doesn't suit your game as is. PRs/feedback welcome if you have an improvement everyone might want, or can describe how this doesn't meet your needs.
Included here are some quick demos that implement the very minimal subset to show current status, and
have callbacks for where a more complete game would do more things (joining, spectating, etc).
## Documentation
The most up to date documentation for Rich Presence can always be found on our [developer site](https://discordapp.com/developers/docs/rich-presence/how-to)! If you're interested in rolling your own native implementation of Rich Presence via IPC sockets instead of using our SDK—hey, you've got free time, right?—check out the ["Hard Mode" documentation](https://github.com/discordapp/discord-rpc/blob/master/documentation/hard-mode.md).
## Basic Usage
Zeroith, you should be set up to build things because you are a game developer, right?
First, head on over to the [Discord developers site](https://discordapp.com/developers/applications/me) and make yourself an app. Keep track of `Client ID` -- you'll need it here to pass to the init function.
### Unreal Engine 4 Setup
To use the Rich Presense plugin with Unreal Engine Projects:
1. Download the latest [release](https://github.com/discordapp/discord-rpc/releases) for each operating system you are targeting and the zipped source code
2. In the source code zip, copy the UE plugin—`examples/unrealstatus/Plugins/discordrpc`—to your project's plugin directory
3. At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create an `Include` folder and copy `discord_rpc.h` and `discord_register.h` to it from the zip
4. Follow the steps below for each OS
5. Build your UE4 project
6. Launch the editor, and enable the Discord plugin.
#### Windows
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Win64` folder
- Copy `lib/discord-rpc.lib` and `bin/discord-rpc.dll` from `[RELEASE_ZIP]/win64-dynamic` to the `Win64` folder
#### Mac
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Mac` folder
- Copy `libdiscord-rpc.dylib` from `[RELEASE_ZIP]/osx-dynamic/lib` to the `Mac` folder
#### Linux
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Linux` folder
- Inside, create another folder `x86_64-unknown-linux-gnu`
- Copy `libdiscord-rpc.so` from `[RELEASE_ZIP]/linux-dynamic/lib` to `Linux/x86_64-unknown-linux-gnu`
### Unity Setup
If you're a Unity developer looking to integrate Rich Presence into your game, follow this simple guide to get started towards success:
1. Download the DLLs for any platform that you need from [our releases](https://github.com/discordapp/discord-rpc/releases)
2. In your Unity project, create a `Plugins` folder inside your `Assets` folder if you don't already have one
3. Copy the file `DiscordRpc.cs` from [here](https://github.com/discordapp/discord-rpc/blob/master/examples/button-clicker/Assets/DiscordRpc.cs) into your `Assets` folder. This is basically your header file for the SDK
We've got our `Plugins` folder ready, so let's get platform-specific!
#### Windows
4. Create `x86` and `x86_64` folders inside `Assets/Plugins/`
5. Copy `discord-rpc-win/win64-dynamic/bin/discord-rpc.dll` to `Assets/Plugins/x86_64/`
6. Copy `discord-rpc-win/win32-dynamic/bin/discord-rpc.dll` to `Assets/Plugins/x86/`
7. Click on both DLLs and make sure they are targetting the correct architectures in the Unity editor properties pane
8. Done!
#### MacOS
4. Copy `discord-rpc-osx/osx-dynamic/lib/libdiscord-rpc.dylib` to `Assets/Plugins/`
5. Rename `libdiscord-rpc.dylib` to `discord-rpc.bundle`
6. Done!
#### Linux
4. Copy `discord-rpc-linux/linux-dynamic-lib/libdiscord-rpc.so` to `Assets/Plugins/`
5. Done!
You're ready to roll! For code examples on how to interact with the SDK using the `DiscordRpc.cs` header file, check out [our example](https://github.com/discordapp/discord-rpc/blob/master/examples/button-clicker/Assets/DiscordController.cs)
### From package
Download a release package for your platform(s) -- they have subdirs with various prebuilt options, select the one you need add `/include` to your compile includes, `/lib` to your linker paths, and link with `discord-rpc`. For the dynamically linked builds, you'll need to ship the associated file along with your game.
### From repo
First-eth, you'll want `CMake`. There's a few different ways to install it on your system, and you should refer to [their website](https://cmake.org/install/). Many package managers provide ways of installing CMake as well.
To make sure it's installed correctly, type `cmake --version` into your flavor of terminal/cmd. If you get a response with a version number, you're good to go!
There's a [CMake](https://cmake.org/download/) file that should be able to generate the lib for you; Sometimes I use it like this:
```sh
cd <path to discord-rpc>
mkdir build
cd build
cmake .. -DCMAKE_INSTALL_PREFIX=<path to install discord-rpc to>
cmake --build . --config Release --target install
```
There is a wrapper build script `build.py` that runs `cmake` with a few different options.
Usually, I run `build.py` to get things started, then use the generated project files as I work on things. It does depend on `click` library, so do a quick `pip install click` to make sure you have it if you want to run `build.py`.
There are some CMake options you might care about:
| flag | default | does |
| ---------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ENABLE_IO_THREAD` | `ON` | When enabled, we start up a thread to do io processing, if disabled you should call `Discord_UpdateConnection` yourself. |
| `USE_STATIC_CRT` | `OFF` | (Windows) Enable to statically link the CRT, avoiding requiring users install the redistributable package. (The prebuilt binaries enable this option) |
| [`BUILD_SHARED_LIBS`](https://cmake.org/cmake/help/v3.7/variable/BUILD_SHARED_LIBS.html) | `OFF` | Build library as a DLL |
| `WARNINGS_AS_ERRORS` | `OFF` | When enabled, compiles with `-Werror` (on \*nix platforms). |
## Continuous Builds
Why do we have three of these? Three times the fun!
| CI | badge |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| TravisCI | [![Build status](https://travis-ci.org/discordapp/discord-rpc.svg?branch=master)](https://travis-ci.org/discordapp/discord-rpc) |
| AppVeyor | [![Build status](https://ci.appveyor.com/api/projects/status/qvkoc0w1c4f4b8tj?svg=true)](https://ci.appveyor.com/project/crmarsh/discord-rpc) |
| Buildkite (internal) | [![Build status](https://badge.buildkite.com/e103d79d247f6776605a15246352a04b8fd83d69211b836111.svg)](https://buildkite.com/discord/discord-rpc) |
## Sample: send-presence
This is a text adventure "game" that inits/deinits the connection to Discord, and sends a presence update on each command.
## Sample: button-clicker
This is a sample [Unity](https://unity3d.com/) project that wraps a DLL version of the library, and sends presence updates when you click on a button. Run `python build.py unity` in the root directory to build the correct library files and place them in their respective folders.
## Sample: unrealstatus
This is a sample [Unreal](https://www.unrealengine.com) project that wraps the DLL version of the library with an Unreal plugin, exposes a blueprint class for interacting with it, and uses that to make a very simple UI. Run `python build.py unreal` in the root directory to build the correct library files and place them in their respective folders.
## Wrappers and Implementations
Below is a table of unofficial, community-developed wrappers for and implementations of Rich Presence in various languages. If you would like to have yours added, please make a pull request adding your repository to the table. The repository should include:
- The code
- A brief ReadMe of how to use it
- A working example
###### Rich Presence Wrappers and Implementations
| Name | Language |
| ------------------------------------------------------------------------- | --------------------------------- |
| [Discord RPC C#](https://github.com/Lachee/discord-rpc-csharp) | C# |
| [Discord RPC D](https://github.com/voidblaster/discord-rpc-d) | [D](https://dlang.org/) |
| [discord-rpc.jar](https://github.com/Vatuu/discord-rpc 'Discord-RPC.jar') | Java |
| [java-discord-rpc](https://github.com/MinnDevelopment/java-discord-rpc) | Java |
| [Discord-IPC](https://github.com/jagrosh/DiscordIPC) | Java |
| [Discord Rich Presence](https://npmjs.org/discord-rich-presence) | JavaScript |
| [drpc4k](https://github.com/Bluexin/drpc4k) | [Kotlin](https://kotlinlang.org/) |
| [lua-discordRPC](https://github.com/pfirsich/lua-discordRPC) | LuaJIT (FFI) |
| [pypresence](https://github.com/qwertyquerty/pypresence) | [Python](https://python.org/) |
| [SwordRPC](https://github.com/Azoy/SwordRPC) | [Swift](https://swift.org) |

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

@@ -0,0 +1,160 @@
/*
* 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>
#include <cstdio>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
namespace discord_rpc {
int GetProcessId() {
return ::getpid();
}
struct BaseConnectionUnix : public BaseConnection {
int sock { -1 };
};
static BaseConnectionUnix Connection;
static sockaddr_un PipeAddr {};
#ifdef MSG_NOSIGNAL
static int MsgFlags = MSG_NOSIGNAL;
#else
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;
}
BaseConnection *BaseConnection::Create() {
PipeAddr.sun_family = AF_UNIX;
return &Connection;
}
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);
self->sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (self->sock == -1) {
return false;
}
fcntl(self->sock, F_SETFL, O_NONBLOCK);
#ifdef SO_NOSIGPIPE
int optval = 1;
setsockopt(self->sock, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval));
#endif
for (int pipeNum = 0; pipeNum < 10; ++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;
return true;
}
}
self->Close();
return false;
}
bool BaseConnection::Close() {
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
if (self->sock == -1) {
return false;
}
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);
if (self->sock == -1) {
return false;
}
ssize_t sentBytes = send(self->sock, data, length, MsgFlags);
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);
if (self->sock == -1) {
return false;
}
long res = recv(self->sock, data, length, MsgFlags);
if (res < 0) {
if (errno == EAGAIN) {
return false;
}
Close();
}
else if (res == 0) {
Close();
}
return static_cast<size_t>(res) == length;
}
} // namespace discord_rpc

View File

@@ -0,0 +1,160 @@
/*
* 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>
namespace discord_rpc {
int GetProcessId() {
return static_cast<int>(::GetCurrentProcessId());
}
struct BaseConnectionWin : public BaseConnection {
HANDLE pipe { INVALID_HANDLE_VALUE };
};
static BaseConnectionWin Connection;
BaseConnection *BaseConnection::Create() {
return &Connection;
}
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);
if (self->pipe != INVALID_HANDLE_VALUE) {
self->isOpen = true;
return true;
}
auto lastError = GetLastError();
if (lastError == ERROR_FILE_NOT_FOUND) {
if (pipeName[pipeDigit] < L'9') {
pipeName[pipeDigit]++;
continue;
}
}
else if (lastError == ERROR_PIPE_BUSY) {
if (!WaitNamedPipeW(pipeName, 10000)) {
return false;
}
continue;
}
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;
}
auto self = reinterpret_cast<BaseConnectionWin *>(this);
assert(self);
if (!self) {
return false;
}
if (self->pipe == INVALID_HANDLE_VALUE) {
return false;
}
assert(data);
if (!data) {
return false;
}
const DWORD bytesLength = static_cast<DWORD>(length);
DWORD bytesWritten = 0;
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;
}
auto self = reinterpret_cast<BaseConnectionWin *>(this);
assert(self);
if (!self) {
return false;
}
if (self->pipe == INVALID_HANDLE_VALUE) {
return false;
}
DWORD bytesAvailable = 0;
if (::PeekNamedPipe(self->pipe, nullptr, 0, nullptr, &bytesAvailable, nullptr)) {
if (bytesAvailable >= length) {
DWORD bytesToRead = static_cast<DWORD>(length);
DWORD bytesRead = 0;
if (::ReadFile(self->pipe, data, bytesToRead, &bytesRead, nullptr) == TRUE) {
assert(bytesToRead == bytesRead);
return true;
}
else {
Close();
}
}
}
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

@@ -0,0 +1,166 @@
/*
* 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"
#define WIN32_LEAN_AND_MEAN
#define NOMCX
#define NOSERVICE
#define NOIME
#include <windows.h>
#include <psapi.h>
#include <cstdio>
/**
* Updated fixes for MinGW and WinXP
* This block is written the way it does not involve changing the rest of the code
* Checked to be compiling
* 1) strsafe.h belongs to Windows SDK and cannot be added to MinGW
* #include guarded, functions redirected to <string.h> substitutes
* 2) RegSetKeyValueW and LSTATUS are not declared in <winreg.h>
* The entire function is rewritten
*/
#ifdef __MINGW32__
# include <wchar.h>
/// strsafe.h fixes
static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat, ...) {
HRESULT ret;
va_list va;
va_start(va, pszFormat);
cbDest /= 2; // Size is divided by 2 to convert from bytes to wide characters - causes segfault
// othervise
ret = vsnwprintf(pszDest, cbDest, pszFormat, va);
pszDest[cbDest - 1] = 0; // Terminate the string in case a buffer overflow; -1 will be returned
va_end(va);
return ret;
}
#else
# include <cwchar>
# include <strsafe.h>
#endif // __MINGW32__
/// winreg.h fixes
#ifndef LSTATUS
# define LSTATUS LONG
#endif
#ifdef RegSetKeyValueW
# undefine RegSetKeyValueW
#endif
#define RegSetKeyValueW regset
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]) {
if ((ret = RegCreateKeyExW(hkey, subkey, 0, 0, 0, KEY_ALL_ACCESS, 0, &hsubkey, 0)) !=
ERROR_SUCCESS)
return ret;
htkey = hsubkey;
}
ret = RegSetValueExW(htkey, name, 0, type, static_cast<const BYTE*>(data), len);
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]{};
DWORD exeLen = GetModuleFileNameW(nullptr, exeFilePath, MAX_PATH);
wchar_t openCommand[1024]{};
if (command && command[0]) {
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command);
}
else {
// StringCbCopyW(openCommand, sizeof(openCommand), exeFilePath);
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath);
}
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 urlProtocol = 0;
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);
if (status != ERROR_SUCCESS) {
fprintf(stderr, "Error creating key\n");
return;
}
DWORD len;
LSTATUS result;
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
result = RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
if (FAILED(result)) {
fprintf(stderr, "Error writing description\n");
}
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
result = RegSetKeyValueW(key, nullptr, L"URL Protocol", REG_SZ, &urlProtocol, sizeof(wchar_t));
if (FAILED(result)) {
fprintf(stderr, "Error writing description\n");
}
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));
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]{};
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
wchar_t openCommand[1024]{};
const wchar_t *wcommand = nullptr;
if (command && command[0]) {
const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand);
MultiByteToWideChar(CP_UTF8, 0, command, -1, openCommand, commandBufferLen);
wcommand = openCommand;
}
Discord_RegisterW(appId, wcommand);
}

510
3rdparty/discord-rpc/discord_rpc.cpp vendored Normal file
View File

@@ -0,0 +1,510 @@
/*
* 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>
#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 };
constexpr size_t JoinQueueSize { 8 };
struct QueuedMessage {
size_t length;
char buffer[MaxMessageSize];
void Copy(const QueuedMessage &other) {
length = other.length;
if (length) {
memcpy(buffer, other.buffer, length);
}
}
};
struct User {
// snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null
// terminator = 21
char userId[32];
// 32 unicode glyphs is max name size => 4 bytes per glyph in the worst case, +1 for null
// terminator = 129
char username[344];
// 4 decimal digits + 1 null terminator = 5
char discriminator[8];
// optional 'a_' + md5 hex digest (32 bytes) + null terminator = 35
char avatar[128];
// Rounded way up because I'm paranoid about games breaking from future changes in these sizes
};
static RpcConnection *Connection { nullptr };
static DiscordEventHandlers QueuedHandlers {};
static DiscordEventHandlers Handlers {};
static std::atomic_bool WasJustConnected { false };
static std::atomic_bool WasJustDisconnected { false };
static std::atomic_bool GotErrorMessage { false };
static std::atomic_bool WasJoinGame { false };
static std::atomic_bool WasSpectateGame { false };
static std::atomic_bool UpdatePresence { false };
static char JoinGameSecret[256];
static char SpectateGameSecret[256];
static int LastErrorCode { 0 };
static char LastErrorMessage[256];
static int LastDisconnectErrorCode { 0 };
static char LastDisconnectErrorMessage[256];
static std::mutex PresenceMutex;
static std::mutex HandlerMutex;
static QueuedMessage QueuedPresence {};
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
static Backoff ReconnectTimeMs(500, 60 * 1000);
static auto NextConnect = std::chrono::system_clock::now();
static int Pid { 0 };
static int Nonce { 1 };
class IoThreadHolder {
private:
std::atomic_bool keepRunning { true };
std::mutex waitForIOMutex;
std::condition_variable waitForIOActivity;
std::thread ioThread;
public:
void Start() {
keepRunning.store(true);
ioThread = std::thread([&]() {
const std::chrono::duration<int64_t, std::milli> maxWait { 500LL };
Discord_UpdateConnection();
while (keepRunning.load()) {
std::unique_lock<std::mutex> lock(waitForIOMutex);
waitForIOActivity.wait_for(lock, maxWait);
Discord_UpdateConnection();
}
});
}
void Notify() { waitForIOActivity.notify_all(); }
void Stop() {
keepRunning.exchange(false);
Notify();
if (ioThread.joinable()) {
ioThread.join();
}
}
~IoThreadHolder() { Stop(); }
};
static IoThreadHolder *IoThread { nullptr };
static void UpdateReconnectTime() {
NextConnect = std::chrono::system_clock::now() + std::chrono::duration<int64_t, std::milli> { ReconnectTimeMs.nextDelay() };
}
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;
}
if (!Connection->IsOpen()) {
if (std::chrono::system_clock::now() >= NextConnect) {
UpdateReconnectTime();
Connection->Open();
}
}
else {
// reads
for (;;) {
JsonDocument message;
if (!Connection->Read(message)) {
break;
}
const char *evtName = GetStrMember(&message, "evt");
const char *nonce = GetStrMember(&message, "nonce");
if (nonce) {
// in responses only -- should use to match up response when needed.
if (evtName && strcmp(evtName, "ERROR") == 0) {
auto data = GetObjMember(&message, "data");
LastErrorCode = GetIntMember(data, "code");
StringCopy(LastErrorMessage, GetStrMember(data, "message", ""));
GotErrorMessage.store(true);
}
}
else {
// should have evt == name of event, optional data
if (evtName == nullptr) {
continue;
}
auto data = GetObjMember(&message, "data");
if (strcmp(evtName, "ACTIVITY_JOIN") == 0) {
auto secret = GetStrMember(data, "secret");
if (secret) {
StringCopy(JoinGameSecret, secret);
WasJoinGame.store(true);
}
}
else if (strcmp(evtName, "ACTIVITY_SPECTATE") == 0) {
auto secret = GetStrMember(data, "secret");
if (secret) {
StringCopy(SpectateGameSecret, secret);
WasSpectateGame.store(true);
}
}
else if (strcmp(evtName, "ACTIVITY_JOIN_REQUEST") == 0) {
auto user = GetObjMember(data, "user");
auto userId = GetStrMember(user, "id");
auto username = GetStrMember(user, "username");
auto avatar = GetStrMember(user, "avatar");
auto joinReq = JoinAskQueue.GetNextAddMessage();
if (userId && username && joinReq) {
StringCopy(joinReq->userId, userId);
StringCopy(joinReq->username, username);
auto discriminator = GetStrMember(user, "discriminator");
if (discriminator) {
StringCopy(joinReq->discriminator, discriminator);
}
if (avatar) {
StringCopy(joinReq->avatar, avatar);
}
else {
joinReq->avatar[0] = 0;
}
JoinAskQueue.CommitAdd();
}
}
}
}
// writes
if (UpdatePresence.exchange(false) && QueuedPresence.length) {
QueuedMessage local;
{
std::lock_guard<std::mutex> guard(PresenceMutex);
local.Copy(QueuedPresence);
}
if (!Connection->Write(local.buffer, local.length)) {
// if we fail to send, requeue
std::lock_guard<std::mutex> guard(PresenceMutex);
QueuedPresence.Copy(local);
UpdatePresence.exchange(true);
}
}
while (SendQueue.HavePendingSends()) {
auto qmessage = SendQueue.GetNextSendMessage();
Connection->Write(qmessage->buffer, qmessage->length);
SendQueue.CommitSend();
}
}
}
extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
IoThread = new (std::nothrow) IoThreadHolder();
if (IoThread == nullptr) {
return;
}
if (autoRegister) {
Discord_Register(applicationId, nullptr);
}
Pid = GetProcessId();
{
std::lock_guard<std::mutex> guard(HandlerMutex);
if (handlers) {
QueuedHandlers = *handlers;
}
else {
QueuedHandlers = {};
}
Handlers = {};
}
if (Connection) {
return;
}
Connection = RpcConnection::Create(applicationId);
Connection->onConnect = [](JsonDocument &readyMessage) {
Discord_UpdateHandlers(&QueuedHandlers);
if (QueuedPresence.length > 0) {
UpdatePresence.exchange(true);
SignalIOActivity();
}
auto data = GetObjMember(&readyMessage, "data");
auto user = GetObjMember(data, "user");
auto userId = GetStrMember(user, "id");
auto username = GetStrMember(user, "username");
auto avatar = GetStrMember(user, "avatar");
if (userId && username) {
StringCopy(connectedUser.userId, userId);
StringCopy(connectedUser.username, username);
auto discriminator = GetStrMember(user, "discriminator");
if (discriminator) {
StringCopy(connectedUser.discriminator, discriminator);
}
if (avatar) {
StringCopy(connectedUser.avatar, avatar);
}
else {
connectedUser.avatar[0] = 0;
}
}
WasJustConnected.exchange(true);
ReconnectTimeMs.reset();
};
Connection->onDisconnect = [](int err, const char *message) {
LastDisconnectErrorCode = err;
StringCopy(LastDisconnectErrorMessage, message);
WasJustDisconnected.exchange(true);
UpdateReconnectTime();
};
IoThread->Start();
}
extern "C" void Discord_Shutdown() {
if (!Connection) {
return;
}
Connection->onConnect = nullptr;
Connection->onDisconnect = nullptr;
Handlers = {};
QueuedPresence.length = 0;
UpdatePresence.exchange(false);
if (IoThread != nullptr) {
IoThread->Stop();
delete IoThread;
IoThread = nullptr;
}
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);
UpdatePresence.exchange(true);
}
SignalIOActivity();
}
extern "C" void Discord_ClearPresence(void) {
Discord_UpdatePresence(nullptr);
}
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++);
SendQueue.CommitAdd();
SignalIOActivity();
}
}
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.
if (!Connection) {
return;
}
const bool wasDisconnected = WasJustDisconnected.exchange(false);
const bool isConnected = Connection->IsOpen();
if (isConnected) {
// if we are connected, disconnect cb first
std::lock_guard<std::mutex> guard(HandlerMutex);
if (wasDisconnected && Handlers.disconnected) {
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
}
}
if (WasJustConnected.exchange(false)) {
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.ready) {
DiscordUser du { connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
Handlers.ready(&du);
}
}
if (GotErrorMessage.exchange(false)) {
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.errored) {
Handlers.errored(LastErrorCode, LastErrorMessage);
}
}
if (WasJoinGame.exchange(false)) {
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.joinGame) {
Handlers.joinGame(JoinGameSecret);
}
}
if (WasSpectateGame.exchange(false)) {
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.spectateGame) {
Handlers.spectateGame(SpectateGameSecret);
}
}
// Right now this batches up any requests and sends them all in a burst; I could imagine a world
// where the implementer would rather sequentially accept/reject each one before the next invite
// is sent. I left it this way because I could also imagine wanting to process these all and
// 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()) {
const auto req = JoinAskQueue.GetNextSendMessage();
{
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.joinRequest) {
DiscordUser du { req->userId, req->username, req->discriminator, req->avatar };
Handlers.joinRequest(&du);
}
}
JoinAskQueue.CommitSend();
}
if (!isConnected) {
// if we are not connected, disconnect message last
std::lock_guard<std::mutex> guard(HandlerMutex);
if (wasDisconnected && Handlers.disconnected) {
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) { \
RegisterForEvent(event); \
} \
else if (Handlers.handler_name && !newHandlers->handler_name) { \
DeregisterForEvent(event); \
}
std::lock_guard<std::mutex> guard(HandlerMutex);
HANDLE_EVENT_REGISTRATION(joinGame, "ACTIVITY_JOIN")
HANDLE_EVENT_REGISTRATION(spectateGame, "ACTIVITY_SPECTATE")
HANDLE_EVENT_REGISTRATION(joinRequest, "ACTIVITY_JOIN_REQUEST")
#undef HANDLE_EVENT_REGISTRATION
Handlers = *newHandlers;
}
else {
std::lock_guard<std::mutex> guard(HandlerMutex);
Handlers = {};
}
}

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

@@ -0,0 +1,168 @@
/*
* 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;
RpcConnection *RpcConnection::Create(const char *applicationId) {
Instance.connection = BaseConnection::Create();
StringCopy(Instance.appId, applicationId);
return &Instance;
}
void RpcConnection::Destroy(RpcConnection *&c) {
c->Close();
BaseConnection::Destroy(c->connection);
c = nullptr;
}
void RpcConnection::Open() {
if (state == State::Connected) {
return;
}
if (state == State::Disconnected && !connection->Open()) {
return;
}
if (state == State::SentHandshake) {
JsonDocument message;
if (Read(message)) {
auto cmd = GetStrMember(&message, "cmd");
auto evt = GetStrMember(&message, "evt");
if (cmd && evt && !strcmp(cmd, "DISPATCH") && !strcmp(evt, "READY")) {
state = State::Connected;
if (onConnect) {
onConnect(message);
}
}
}
}
else {
sendFrame.opcode = Opcode::Handshake;
sendFrame.length = static_cast<uint32_t>(JsonWriteHandshakeObj(sendFrame.message, sizeof(sendFrame.message), RpcVersion, appId));
if (connection->Write(&sendFrame, sizeof(MessageFrameHeader) + sendFrame.length)) {
state = State::SentHandshake;
}
else {
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);
if (!connection->Write(&sendFrame, sizeof(MessageFrameHeader) + length)) {
Close();
return false;
}
return true;
}
bool RpcConnection::Read(JsonDocument &message) {
if (state != State::Connected && state != State::SentHandshake) {
return false;
}
MessageFrame readFrame{};
for (;;) {
bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader));
if (!didRead) {
if (!connection->isOpen) {
lastErrorCode = static_cast<int>(ErrorCode::PipeClosed);
StringCopy(lastErrorMessage, "Pipe closed");
Close();
}
return false;
}
if (readFrame.length > 0) {
didRead = connection->Read(readFrame.message, readFrame.length);
if (!didRead) {
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
StringCopy(lastErrorMessage, "Partial data in frame");
Close();
return false;
}
readFrame.message[readFrame.length] = 0;
}
switch (readFrame.opcode) {
case Opcode::Close: {
message.ParseInsitu(readFrame.message);
lastErrorCode = GetIntMember(&message, "code");
StringCopy(lastErrorMessage, GetStrMember(&message, "message", ""));
Close();
return false;
}
case Opcode::Frame:
message.ParseInsitu(readFrame.message);
return true;
case Opcode::Ping:
readFrame.opcode = Opcode::Pong;
if (!connection->Write(&readFrame, sizeof(MessageFrameHeader) + readFrame.length)) {
Close();
}
break;
case Opcode::Pong:
break;
case Opcode::Handshake:
default:
// something bad happened
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
StringCopy(lastErrorMessage, "Bad ipc frame");
Close();
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

@@ -0,0 +1,285 @@
/*
* 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;
return;
}
if (number < 0) {
*dest++ = '-';
number = -number;
}
char temp[32];
int place = 0;
while (number) {
auto digit = number % 10;
number = number / 10;
temp[place++] = '0' + static_cast<char>(digit);
}
for (--place; place >= 0; --place) {
*dest++ = temp[place];
}
*dest = 0;
}
// it's ever so slightly faster to not have to strlen the key
template<typename T>
void WriteKey(JsonWriter &w, T &k) {
w.Key(k, sizeof(T) - 1);
}
struct WriteObject {
JsonWriter &writer;
WriteObject(JsonWriter &w)
: writer(w) {
writer.StartObject();
}
template<typename T>
WriteObject(JsonWriter &w, T &name)
: writer(w) {
WriteKey(writer, name);
writer.StartObject();
}
~WriteObject() { writer.EndObject(); }
};
struct WriteArray {
JsonWriter &writer;
template<typename T>
WriteArray(JsonWriter &w, T &name)
: writer(w) {
WriteKey(writer, name);
writer.StartArray();
}
~WriteArray() { writer.EndArray(); }
};
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, const int nonce) {
WriteKey(writer, "nonce");
char nonceBuffer[32];
NumberToString(nonceBuffer, nonce);
writer.String(nonceBuffer);
}
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence) {
JsonWriter writer(dest, maxLen);
{
WriteObject top(writer);
JsonWriteNonce(writer, nonce);
WriteKey(writer, "cmd");
writer.String("SET_ACTIVITY");
{
WriteObject args(writer, "args");
WriteKey(writer, "pid");
writer.Int(pid);
if (presence != nullptr) {
WriteObject activity(writer, "activity");
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);
WriteOptionalString(writer, "state", presence->state);
WriteOptionalString(writer, "details", presence->details);
if (presence->startTimestamp || presence->endTimestamp) {
WriteObject timestamps(writer, "timestamps");
if (presence->startTimestamp) {
WriteKey(writer, "start");
writer.Int64(presence->startTimestamp);
}
if (presence->endTimestamp) {
WriteKey(writer, "end");
writer.Int64(presence->endTimestamp);
}
}
if ((presence->largeImageKey && presence->largeImageKey[0]) ||
(presence->largeImageText && presence->largeImageText[0]) ||
(presence->smallImageKey && presence->smallImageKey[0]) ||
(presence->smallImageText && presence->smallImageText[0])) {
WriteObject assets(writer, "assets");
WriteOptionalString(writer, "large_image", presence->largeImageKey);
WriteOptionalString(writer, "large_text", presence->largeImageText);
WriteOptionalString(writer, "small_image", presence->smallImageKey);
WriteOptionalString(writer, "small_text", presence->smallImageText);
}
if ((presence->partyId && presence->partyId[0]) || presence->partySize ||
presence->partyMax || presence->partyPrivacy) {
WriteObject party(writer, "party");
WriteOptionalString(writer, "id", presence->partyId);
if (presence->partySize && presence->partyMax) {
WriteArray size(writer, "size");
writer.Int(presence->partySize);
writer.Int(presence->partyMax);
}
if (presence->partyPrivacy) {
WriteKey(writer, "privacy");
writer.Int(presence->partyPrivacy);
}
}
if ((presence->matchSecret && presence->matchSecret[0]) ||
(presence->joinSecret && presence->joinSecret[0]) ||
(presence->spectateSecret && presence->spectateSecret[0])) {
WriteObject secrets(writer, "secrets");
WriteOptionalString(writer, "match", presence->matchSecret);
WriteOptionalString(writer, "join", presence->joinSecret);
WriteOptionalString(writer, "spectate", presence->spectateSecret);
}
writer.Key("instance");
writer.Bool(presence->instance != 0);
}
}
}
return writer.Size();
}
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId) {
JsonWriter writer(dest, maxLen);
{
WriteObject obj(writer);
WriteKey(writer, "v");
writer.Int(version);
WriteKey(writer, "client_id");
writer.String(applicationId);
}
return writer.Size();
}
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
JsonWriter writer(dest, maxLen);
{
WriteObject obj(writer);
JsonWriteNonce(writer, nonce);
WriteKey(writer, "cmd");
writer.String("SUBSCRIBE");
WriteKey(writer, "evt");
writer.String(evtName);
}
return writer.Size();
}
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
JsonWriter writer(dest, maxLen);
{
WriteObject obj(writer);
JsonWriteNonce(writer, nonce);
WriteKey(writer, "cmd");
writer.String("UNSUBSCRIBE");
WriteKey(writer, "evt");
writer.String(evtName);
}
return writer.Size();
}
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, const int reply, const int nonce) {
JsonWriter writer(dest, maxLen);
{
WriteObject obj(writer);
WriteKey(writer, "cmd");
if (reply == DISCORD_REPLY_YES) {
writer.String("SEND_ACTIVITY_JOIN_INVITE");
}
else {
writer.String("CLOSE_ACTIVITY_JOIN_REQUEST");
}
WriteKey(writer, "args");
{
WriteObject args(writer);
WriteKey(writer, "user_id");
writer.String(userId);
}
JsonWriteNonce(writer, nonce);
}
return writer.Size();
}
} // namespace discord_rpc

View File

@@ -0,0 +1,213 @@
/*
* 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
template<size_t Len>
inline size_t StringCopy(char (&dest)[Len], const char *src) {
if (!src || !Len) {
return 0;
}
size_t copied;
char *out = dest;
for (copied = 1; *src && copied < Len; ++copied) {
*out++ = *src++;
}
*out = 0;
return copied - 1;
}
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId);
// Commands
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);
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int reply, int nonce);
// I want to use as few allocations as I can get away with, and to do that with RapidJson, you need
// to supply some of your own allocators for stuff rather than use the defaults
class LinearAllocator {
public:
char *buffer_;
char *end_;
LinearAllocator() {
assert(0); // needed for some default case in rapidjson, should not use
}
LinearAllocator(char *buffer, size_t size)
: buffer_(buffer), end_(buffer + size) {
}
static const bool kNeedFree = false;
void *Malloc(size_t size) {
char *res = buffer_;
buffer_ += size;
if (buffer_ > end_) {
buffer_ = res;
return nullptr;
}
return res;
}
void *Realloc(void *originalPtr, size_t originalSize, size_t newSize) {
if (newSize == 0) {
return nullptr;
}
// allocate how much you need in the first place
assert(!originalPtr && !originalSize);
// unused parameter warning
(void)(originalPtr);
(void)(originalSize);
return Malloc(newSize);
}
static void Free(void *ptr) {
/* shrug */
(void)ptr;
}
};
template<size_t Size>
class FixedLinearAllocator : public LinearAllocator {
public:
char fixedBuffer_[Size];
FixedLinearAllocator()
: LinearAllocator(fixedBuffer_, Size) {
}
static const bool kNeedFree = false;
};
// wonder why this isn't a thing already, maybe I missed it
class DirectStringBuffer {
public:
using Ch = char;
char *buffer_;
char *end_;
char *current_;
DirectStringBuffer(char *buffer, size_t maxLen)
: buffer_(buffer), end_(buffer + maxLen), current_(buffer) {
}
void Put(char c) {
if (current_ < end_) {
*current_++ = c;
}
}
void Flush() {}
size_t GetSize() const { return static_cast<size_t>(current_ - buffer_); }
};
using MallocAllocator = rapidjson::CrtAllocator;
using PoolAllocator = rapidjson::MemoryPoolAllocator<MallocAllocator>;
using UTF8 = rapidjson::UTF8<char>;
// Writer appears to need about 16 bytes per nested object level (with 64bit size_t)
using StackAllocator = FixedLinearAllocator<2048>;
constexpr size_t WriterNestingLevels = 2048 / (2 * sizeof(size_t));
using JsonWriterBase =
rapidjson::Writer<DirectStringBuffer, UTF8, UTF8, StackAllocator, rapidjson::kWriteNoFlags>;
class JsonWriter : public JsonWriterBase {
public:
DirectStringBuffer stringBuffer_;
StackAllocator stackAlloc_;
JsonWriter(char *dest, size_t maxLen)
: JsonWriterBase(stringBuffer_, &stackAlloc_, WriterNestingLevels), stringBuffer_(dest, maxLen), stackAlloc_() {
}
size_t Size() const { return stringBuffer_.GetSize(); }
};
using JsonDocumentBase = rapidjson::GenericDocument<UTF8, PoolAllocator, StackAllocator>;
class JsonDocument : public JsonDocumentBase {
public:
static const int kDefaultChunkCapacity = 32 * 1024;
// json parser will use this buffer first, then allocate more if needed; I seriously doubt we
// send any messages that would use all of this, though.
char parseBuffer_[32 * 1024];
MallocAllocator mallocAllocator_;
PoolAllocator poolAllocator_;
StackAllocator stackAllocator_;
JsonDocument()
: JsonDocumentBase(rapidjson::kObjectType,
&poolAllocator_,
sizeof(stackAllocator_.fixedBuffer_),
&stackAllocator_),
poolAllocator_(parseBuffer_, sizeof(parseBuffer_), kDefaultChunkCapacity, &mallocAllocator_), stackAllocator_() {
}
};
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) {
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,11 +0,0 @@
cmake_minimum_required(VERSION 3.13)
set(SOURCES KDSingleApplication/src/kdsingleapplication.cpp KDSingleApplication/src/kdsingleapplication_localsocket.cpp)
set(HEADERS KDSingleApplication/src/kdsingleapplication.h KDSingleApplication/src/kdsingleapplication_localsocket_p.h)
qt_wrap_cpp(MOC ${HEADERS})
add_library(kdsingleapplication STATIC ${SOURCES} ${MOC})
if(NOT MSVC)
target_compile_options(kdsingleapplication PRIVATE -Wno-missing-declarations)
endif()
target_compile_definitions(kdsingleapplication PRIVATE -DKDSINGLEAPPLICATION_STATIC_BUILD)
target_include_directories(kdsingleapplication PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(kdsingleapplication PUBLIC Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network)

View File

@@ -18,24 +18,19 @@ include(CheckCXXSourceRuns)
include(CheckIncludeFiles)
include(FindPkgConfig)
include(cmake/Version.cmake)
include(cmake/Summary.cmake)
include(cmake/OptionalComponent.cmake)
include(cmake/OptionalSource.cmake)
include(cmake/ParseArguments.cmake)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(LINUX ON)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
set(FREEBSD ON)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "OpenBSD")
set(OPENBSD ON)
endif()
if(LINUX)
include(cmake/Rpm.cmake)
include(cmake/Deb.cmake)
endif()
if(APPLE)
include(cmake/Dmg.cmake)
endif()
@@ -72,7 +67,6 @@ option(USE_BUNDLE "Bundle dependencies" ${USE_BUNDLE_DEFAULT})
if(WIN32)
option(ENABLE_WIN32_CONSOLE "Show the windows console even outside Debug mode" ${ENABLE_WIN32_CONSOLE_DEFAULT})
option(USE_QTSPARKLE "Use Qt Sparkle updater" ON)
endif()
if(MSVC)
@@ -87,7 +81,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(MSVC)
list(APPEND COMPILE_OPTIONS /MP)
list(APPEND COMPILE_OPTIONS -MP -W4 -wd4702)
else()
list(APPEND COMPILE_OPTIONS
$<$<COMPILE_LANGUAGE:C>:-std=c11>
@@ -134,7 +128,10 @@ add_definitions(
)
if(WIN32)
add_definitions(-DUNICODE)
add_definitions(
-DUNICODE
-DNOMINMAX
)
endif()
if(BUILD_WERROR)
@@ -167,17 +164,17 @@ if(NOT Boost_FOUND)
find_package(Boost REQUIRED)
endif()
find_package(ICU COMPONENTS uc i18n REQUIRED)
if(LINUX)
find_package(ALSA REQUIRED)
else()
find_package(ALSA)
endif()
if(UNIX AND NOT APPLE)
if(LINUX)
find_package(ALSA REQUIRED)
else()
find_package(ALSA)
endif()
find_package(X11 COMPONENTS X11_xcb)
endif()
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GOBJECT REQUIRED IMPORTED_TARGET gobject-2.0)
if(UNIX AND NOT APPLE)
if(NOT APPLE)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
if(GIO_FOUND AND UNIX)
pkg_check_modules(GIO_UNIX IMPORTED_TARGET gio-unix-2.0)
@@ -191,7 +188,9 @@ pkg_check_modules(GSTREAMER_APP REQUIRED IMPORTED_TARGET gstreamer-app-1.0)
pkg_check_modules(GSTREAMER_TAG REQUIRED IMPORTED_TARGET gstreamer-tag-1.0)
pkg_check_modules(GSTREAMER_PBUTILS REQUIRED IMPORTED_TARGET gstreamer-pbutils-1.0)
pkg_check_modules(SQLITE REQUIRED IMPORTED_TARGET sqlite3>=3.9)
pkg_check_modules(LIBPULSE IMPORTED_TARGET libpulse)
if(UNIX AND NOT APPLE)
pkg_check_modules(LIBPULSE IMPORTED_TARGET libpulse)
endif()
pkg_check_modules(CHROMAPRINT IMPORTED_TARGET libchromaprint>=1.4)
pkg_check_modules(FFTW3 IMPORTED_TARGET fftw3)
pkg_check_modules(LIBEBUR128 IMPORTED_TARGET libebur128)
@@ -211,34 +210,31 @@ endif()
find_package(GTest)
pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash)
find_package(RapidJSON)
set(QT_VERSION_MAJOR 6)
set(QT_MIN_VERSION 6.4.0)
set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION_MAJOR})
set(QT_COMPONENTS Core Concurrent Gui Widgets Network Sql)
set(QT_OPTIONAL_COMPONENTS LinguistTools Test)
set(QT_OPTIONAL_COMPONENTS GuiPrivate LinguistTools Test)
if(UNIX AND NOT APPLE)
list(APPEND QT_OPTIONAL_COMPONENTS DBus)
endif()
set(QT_NO_PRIVATE_MODULE_WARNING ON)
find_package(Qt${QT_VERSION_MAJOR} ${QT_MIN_VERSION} COMPONENTS ${QT_COMPONENTS} REQUIRED OPTIONAL_COMPONENTS ${QT_OPTIONAL_COMPONENTS})
if(TARGET "Qt${QT_VERSION_MAJOR}::GuiPrivate")
set(QT_GUI_PRIVATE_FOUND ON)
endif()
if(Qt${QT_VERSION_MAJOR}DBus_FOUND)
set(DBUS_FOUND ON)
endif()
if(X11_FOUND)
find_path(QPA_QPLATFORMNATIVEINTERFACE_H qpa/qplatformnativeinterface.h PATHS ${Qt${QT_VERSION_MAJOR}Gui_INCLUDE_DIRS} ${Qt${QT_VERSION_MAJOR}Gui_PRIVATE_INCLUDE_DIRS})
if(NOT QPA_QPLATFORMNATIVEINTERFACE_H)
find_path(QPA_QPLATFORMNATIVEINTERFACE_H ${Qt${QT_VERSION_MAJOR}Gui_VERSION}/QtGui/qpa/qplatformnativeinterface.h PATHS ${Qt${QT_VERSION_MAJOR}Gui_INCLUDE_DIRS} ${Qt${QT_VERSION_MAJOR}Gui_PRIVATE_INCLUDE_DIRS})
endif()
if(QPA_QPLATFORMNATIVEINTERFACE_H)
set(HAVE_QPA_QPLATFORMNATIVEINTERFACE_H ON)
message(STATUS "Have qpa/qplatformnativeinterface.h header.")
else()
message(STATUS "Missing qpa/qplatformnativeinterface.h header.")
endif()
# Check for QX11Application (Qt 6 compiled with XCB).
set(CMAKE_REQUIRED_FLAGS "-std=c++17")
set(CMAKE_REQUIRED_LIBRARIES Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui)
@@ -253,51 +249,49 @@ if(X11_FOUND)
)
unset(CMAKE_REQUIRED_FLAGS)
unset(CMAKE_REQUIRED_LIBRARIES)
endif()
# SingleApplication
set(KDSINGLEAPPLICATION_NAME "KDSingleApplication-qt${QT_VERSION_MAJOR}")
find_package(${KDSINGLEAPPLICATION_NAME} 1.1.0)
if(TARGET KDAB::kdsingleapplication)
set(KDSINGLEAPPLICATION_VERSION "${KDSingleApplication-qt6_VERSION}")
message(STATUS "Using system KDSingleApplication (Version ${KDSINGLEAPPLICATION_VERSION})")
set(SINGLEAPPLICATION_LIBRARIES KDAB::kdsingleapplication)
else()
message(STATUS "Using 3rdparty KDSingleApplication")
add_subdirectory(3rdparty/kdsingleapplication)
set(SINGLEAPPLICATION_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/kdsingleapplication/KDSingleApplication/src)
set(SINGLEAPPLICATION_LIBRARIES kdsingleapplication)
add_definitions(-DKDSINGLEAPPLICATION_STATIC_BUILD)
endif()
find_package(KDSingleApplication-qt${QT_VERSION_MAJOR} 1.1.0 REQUIRED)
# if(APPLE)
# find_package(SPMediaKeyTap REQUIRED)
# endif()
if(APPLE)
find_library(SPARKLE Sparkle)
#find_package(SPMediaKeyTap REQUIRED)
endif()
if(WIN32)
find_package(getopt-win REQUIRED)
if(USE_QTSPARKLE)
pkg_check_modules(QTSPARKLE REQUIRED IMPORTED_TARGET qtsparkle-qt${QT_VERSION_MAJOR})
set(HAVE_QTSPARKLE ON)
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()
optional_component(ALSA ON "ALSA integration"
DEPENDS "alsa" ALSA_FOUND
)
if(APPLE OR WIN32)
find_package(qtsparkle-qt${QT_VERSION_MAJOR})
if(TARGET "qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle")
set(QTSPARKLE_FOUND ON)
endif()
endif()
optional_component(PULSE ON "PulseAudio integration"
DEPENDS "libpulse" LIBPULSE_FOUND
)
optional_component(DBUS ON "D-Bus support"
DEPENDS "Qt D-Bus" DBUS_FOUND
)
optional_component(MPRIS2 ON "MPRIS2 D-Bus Interface"
DEPENDS "D-Bus support" HAVE_DBUS
)
if(UNIX AND NOT APPLE)
optional_component(ALSA ON "ALSA integration"
DEPENDS "alsa" ALSA_FOUND
)
optional_component(PULSE ON "PulseAudio integration"
DEPENDS "libpulse" LIBPULSE_FOUND
)
optional_component(DBUS ON "D-Bus support"
DEPENDS "Qt D-Bus" DBUS_FOUND
)
optional_component(MPRIS2 ON "MPRIS2 D-Bus Interface"
DEPENDS "D-Bus support" HAVE_DBUS
)
endif()
optional_component(SONGFINGERPRINTING ON "Song fingerprinting and tracking"
DEPENDS "chromaprint" CHROMAPRINT_FOUND
@@ -307,26 +301,29 @@ optional_component(MUSICBRAINZ ON "MusicBrainz integration"
DEPENDS "chromaprint" CHROMAPRINT_FOUND
)
optional_component(X11_GLOBALSHORTCUTS ON "X11 global shortcuts"
DEPENDS "X11" X11_FOUND
DEPENDS "QX11Application" HAVE_QX11APPLICATION
)
if(UNIX AND NOT APPLE)
optional_component(X11_GLOBALSHORTCUTS ON "X11 global shortcuts"
DEPENDS "X11" X11_FOUND
DEPENDS "QX11Application" HAVE_QX11APPLICATION
)
optional_component(KGLOBALACCEL_GLOBALSHORTCUTS ON "KGlobalAccel global shortcuts"
DEPENDS "D-Bus support" HAVE_DBUS
)
optional_component(UDISKS2 ON "Devices: UDisks2 backend"
DEPENDS "D-Bus support" HAVE_DBUS
)
endif()
optional_component(KGLOBALACCEL_GLOBALSHORTCUTS ON "KGlobalAccel global shortcuts"
DEPENDS "D-Bus support" HAVE_DBUS
)
optional_component(UDISKS2 ON "Devices: UDisks2 backend"
DEPENDS "D-Bus support" HAVE_DBUS
)
optional_component(GIO ON "Devices: GIO device backend"
DEPENDS "libgio" GIO_FOUND
)
optional_component(GIO_UNIX ON "Devices: GIO device backend (Unix support)"
DEPENDS "libgio-unix" GIO_UNIX_FOUND
)
if(NOT APPLE)
optional_component(GIO ON "Devices: GIO device backend"
DEPENDS "libgio" GIO_FOUND
)
if(UNIX)
optional_component(GIO_UNIX ON "Devices: GIO device backend (Unix support)"
DEPENDS "libgio-unix" GIO_UNIX_FOUND
)
endif()
endif()
optional_component(AUDIOCD ON "Devices: Audio CD support"
DEPENDS "libcdio" LIBCDIO_FOUND
@@ -358,6 +355,30 @@ optional_component(EBUR128 ON "EBU R 128 loudness normalization"
DEPENDS "libebur128" LIBEBUR128_FOUND
)
if(APPLE)
optional_component(SPARKLE ON "Sparkle integration"
DEPENDS "Sparkle" SPARKLE
)
endif()
if(APPLE OR WIN32)
optional_component(QTSPARKLE ON "QtSparkle integration"
DEPENDS "QtSparkle" QTSPARKLE_FOUND
)
endif()
optional_component(QPA_QPLATFORMNATIVEINTERFACE ON "QPA Platform Native Interface"
DEPENDS "Qt Gui Private" QT_GUI_PRIVATE_FOUND
)
optional_component(STREAMTAGREADER ON "Stream tagreader"
DEPENDS "sparsehash" LIBSPARSEHASH_FOUND
)
optional_component(DISCORD_RPC ON "Discord Rich Presence"
DEPENDS "RapidJSON" RapidJSON_FOUND
)
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
set(HAVE_CHROMAPRINT ON)
endif()
@@ -392,7 +413,7 @@ add_executable(strawberry)
if(APPLE)
set_target_properties(strawberry PROPERTIES MACOSX_BUNDLE TRUE)
set_target_properties(strawberry PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/dist/macos/Info.plist")
set_target_properties(strawberry PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/dist/macos/Info.plist")
endif()
if(WIN32 AND NOT ENABLE_WIN32_CONSOLE)
@@ -443,6 +464,10 @@ set(SOURCES
src/core/enginemetadata.cpp
src/core/songmimedata.cpp
src/core/platforminterface.cpp
src/core/standardpaths.cpp
src/core/httpbaserequest.cpp
src/core/jsonbaserequest.cpp
src/core/oauthenticator.cpp
src/utilities/strutils.cpp
src/utilities/envutils.cpp
@@ -463,6 +488,7 @@ set(SOURCES
src/utilities/screenutils.cpp
src/utilities/textencodingutils.cpp
src/utilities/coveroptions.cpp
src/utilities/musixmatchprovider.cpp
src/tagreader/tagreaderclient.cpp
src/tagreader/tagreaderresult.cpp
@@ -572,6 +598,8 @@ set(SOURCES
src/playlist/playlistfilter.cpp
src/playlist/playlistheader.cpp
src/playlist/playlistitem.cpp
src/playlist/songplaylistitem.cpp
src/playlist/streamplaylistitem.cpp
src/playlist/playlistitemmimedata.cpp
src/playlist/playlistlistcontainer.cpp
src/playlist/playlistlistmodel.cpp
@@ -585,7 +613,6 @@ set(SOURCES
src/playlist/playlistview.cpp
src/playlist/playlistproxystyle.cpp
src/playlist/songloaderinserter.cpp
src/playlist/songplaylistitem.cpp
src/playlist/dynamicplaylistcontrols.cpp
src/playlist/playlistundocommandbase.cpp
src/playlist/playlistundocommandinsertitems.cpp
@@ -673,8 +700,6 @@ set(SOURCES
src/lyrics/letraslyricsprovider.cpp
src/lyrics/lyricfindlyricsprovider.cpp
src/providers/musixmatchprovider.cpp
src/settings/settingsdialog.cpp
src/settings/settingspage.cpp
src/settings/settingsitemdelegate.cpp
@@ -737,7 +762,7 @@ set(SOURCES
src/streaming/streamingservices.cpp
src/streaming/streamingservice.cpp
src/streaming/streamplaylistitem.cpp
src/streaming/streamserviceplaylistitem.cpp
src/streaming/streamingsearchview.cpp
src/streaming/streamingsearchmodel.cpp
src/streaming/streamingsearchsortmodel.cpp
@@ -755,7 +780,7 @@ set(SOURCES
src/radios/radioview.cpp
src/radios/radioviewcontainer.cpp
src/radios/radioservice.cpp
src/radios/radioplaylistitem.cpp
src/radios/radiostreamplaylistitem.cpp
src/radios/radiochannel.cpp
src/radios/somafmservice.cpp
src/radios/radioparadiseservice.cpp
@@ -837,6 +862,9 @@ set(HEADERS
src/core/stylesheetloader.h
src/core/localredirectserver.h
src/core/songmimedata.h
src/core/httpbaserequest.h
src/core/jsonbaserequest.h
src/core/oauthenticator.h
src/tagreader/tagreaderclient.h
src/tagreader/tagreaderreply.h
@@ -1202,6 +1230,7 @@ if(APPLE)
src/osd/osdmac.h
src/device/macosdevicelister.h
)
optional_source(HAVE_SPARKLE SOURCES src/core/sparkleupdater.mm HEADERS src/core/sparkleupdater.h)
else()
list(APPEND SOURCES src/systemtrayicon/qtsystemtrayicon.cpp src/widgets/searchfield_qt.cpp src/widgets/searchfield_qt_private.cpp)
list(APPEND HEADERS src/systemtrayicon/qtsystemtrayicon.h src/widgets/searchfield_qt_private.h)
@@ -1218,6 +1247,16 @@ optional_source(WIN32
src/core/windows7thumbbar.h
)
optional_source(HAVE_STREAMTAGREADER
SOURCES src/tagreader/streamtagreader.cpp src/tagreader/tagreaderreadstreamrequest.cpp src/tagreader/tagreaderreadstreamreply.cpp
HEADERS src/tagreader/tagreaderreadstreamreply.h
)
optional_source(HAVE_DISCORD_RPC
SOURCES src/discord/richpresence.cpp
HEADERS src/discord/richpresence.h
)
if(HAVE_GLOBALSHORTCUTS)
optional_source(HAVE_GLOBALSHORTCUTS
@@ -1306,9 +1345,7 @@ optional_source(HAVE_MOODBAR
src/settings/moodbarsettingspage.ui
)
if(UNIX)
optional_source(HAVE_GIO SOURCES src/device/giolister.cpp HEADERS src/device/giolister.h)
endif()
optional_source(HAVE_GIO SOURCES src/device/giolister.cpp HEADERS src/device/giolister.h)
if(HAVE_UDISKS2)
optional_source(HAVE_UDISKS2 SOURCES src/device/udisks2lister.cpp HEADERS src/device/udisks2lister.h)
@@ -1456,7 +1493,7 @@ endif()
add_subdirectory(src)
add_subdirectory(dist)
if(TARGET GTest::GTest AND Qt${QT_VERSION_MAJOR}Test_FOUND)
if(TARGET GTest::gtest AND TARGET GTest::gmock AND Qt${QT_VERSION_MAJOR}Test_FOUND)
add_subdirectory(tests)
endif()
@@ -1464,6 +1501,11 @@ if(LINUX AND LSB_RELEASE_EXEC AND DPKG_BUILDPACKAGE)
add_subdirectory(debian)
endif()
if(HAVE_DISCORD_RPC)
add_subdirectory(3rdparty/discord-rpc)
target_include_directories(strawberry_lib PUBLIC 3rdparty/discord-rpc)
endif()
if(HAVE_TRANSLATIONS)
qt_add_lupdate(strawberry_lib TS_FILES "${CMAKE_SOURCE_DIR}/src/translations/strawberry_en_US.ts" OPTIONS -locations none -no-ui-lines -no-obsolete)
file(GLOB_RECURSE ts_files ${CMAKE_SOURCE_DIR}/src/translations/*.ts)
@@ -1485,12 +1527,9 @@ if(SINGLEAPPLICATION_INCLUDE_DIRS)
target_include_directories(strawberry_lib SYSTEM PUBLIC ${SINGLEAPPLICATION_INCLUDE_DIRS})
endif()
if(HAVE_QPA_QPLATFORMNATIVEINTERFACE_H)
target_include_directories(strawberry_lib SYSTEM PUBLIC ${Qt${QT_VERSION_MAJOR}Gui_PRIVATE_INCLUDE_DIRS})
endif()
target_link_libraries(strawberry_lib PUBLIC
${CMAKE_THREAD_LIBS_INIT}
$<$<BOOL:${HAVE_BACKTRACE}>:${Backtrace_LIBRARIES}>
PkgConfig::GLIB
PkgConfig::GOBJECT
PkgConfig::SQLITE
@@ -1508,8 +1547,10 @@ target_link_libraries(strawberry_lib PUBLIC
Qt${QT_VERSION_MAJOR}::Network
Qt${QT_VERSION_MAJOR}::Sql
$<$<BOOL:${HAVE_DBUS}>:Qt${QT_VERSION_MAJOR}::DBus>
$<$<BOOL:${HAVE_QPA_QPLATFORMNATIVEINTERFACE}>:Qt${QT_VERSION_MAJOR}::GuiPrivate>
ICU::uc
ICU::i18n
$<$<BOOL:${HAVE_STREAMTAGREADER}>:PkgConfig::LIBSPARSEHASH>
$<$<BOOL:${HAVE_ALSA}>:ALSA::ALSA>
$<$<BOOL:${HAVE_PULSE}>:PkgConfig::LIBPULSE>
$<$<BOOL:${HAVE_CHROMAPRINT}>:PkgConfig::CHROMAPRINT>
@@ -1521,11 +1562,11 @@ target_link_libraries(strawberry_lib PUBLIC
$<$<BOOL:${HAVE_AUDIOCD}>:PkgConfig::LIBCDIO>
$<$<BOOL:${HAVE_MTP}>:PkgConfig::LIBMTP>
$<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF>
$<$<BOOL:${HAVE_QTSPARKLE}>:PkgConfig::QTSPARKLE>
$<$<BOOL:${FREEBSD}>:execinfo>
$<$<BOOL:${WIN32}>:dsound dwmapi getopt-win::getopt>
$<$<BOOL:${HAVE_QTSPARKLE}>:qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle>
$<$<BOOL:${WIN32}>:dsound dwmapi ${GETOPT_LIBRARIES}>
$<$<BOOL:${MSVC}>:WindowsApp>
${SINGLEAPPLICATION_LIBRARIES}
KDAB::kdsingleapplication
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
)
if(APPLE)
@@ -1538,6 +1579,10 @@ if(APPLE)
"-framework IOKit"
"-framework ScriptingBridge"
)
if(HAVE_SPARKLE)
target_include_directories(strawberry_lib SYSTEM PRIVATE ${SPARKLE}/Headers)
target_link_libraries(strawberry_lib PRIVATE ${SPARKLE})
endif()
endif()
target_link_libraries(strawberry PUBLIC strawberry_lib)
@@ -1553,7 +1598,7 @@ endif()
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" IMMEDIATE @ONLY)
add_custom_target(uninstall "${CMAKE_COMMAND}" -P "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake")
summary_show()
optional_component_summary_show()
if(NOT CMAKE_CROSSCOMPILING AND NOT QT_SQLITE_TEST)
message(WARNING "The Qt sqlite driver test failed.")

145
Changelog
View File

@@ -2,6 +2,151 @@ 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:
* Fixed subsonic parse error (#1719).
* Fixed Deezer cover provider parse error (#1716).
* Fixed last.fm import progress.
* (Windows|MinGW) Switched from winpthreads to win32 threads, winpthreads are no longer working with Qt as of version 6.9 (QTBUG-131892).
Enhancements:
* Added option to disable playbin3.
Version 1.2.8 (2025.04.05):
Bugfixes:
* Added "HI_RES_LOSSLESS" for Tidal quality setting.
* Increased backend settings device lineedit height.
* Possible fix for KGlobalAccel shortcuts sometimes not working.
Enhancements:
* Removed deprecated Tidal username/password login.
* Turned off "Grey out unavailable songs in playlists on startup" by default.
* Added support for reading tags from streams.
* Added tooltips in equalizer, backend and appearance settings.
* Added full tag support for AIFF including embedded covers.
* Use card ID instead of index for ALSA devices.
* Removed KDSingleApplication from 3rdparty.
* Support arbitrarily large EBU R 128 loudness normalization.
New features:
* Added Discord rich presence support.
Version 1.2.7 (2025.01.31):
Bugfixes:
* Fixed strawberry exiting when clicking tray icon.
* Fixed Clementine import script errors.
* Disabled OSD Pretty on Wayland since it's not working properly.
Enhancements:
* Only maximize error dialog if Strawberry is the active window (#1627).
* Added QPA Platform Native Interface as optional component.
Version 1.2.6 (2025.01.17):
Bugfixes:
* Fixed dragging songs from playlist to queue.
Version 1.2.5 (2025.01.17):
Bugfixes:
* Fixed crash when saving playcount or rating to file (#1633).
* Fixed QFile::open failing in unit tests.
* Fixed playlist sequence settings saved to wrong configuration file (#1649).
Enhancements:
* Fixed use of deprecated GIO functions with GLib 2.84 and newer.
* (macOS) Added back Sparkle updater to check for new releases.
Version 1.2.4 (2025.01.10):
Bugfixes:
* Fixed Spotify songs not being available for scrobbling.
* Fixed leading "A" and "The" articles being skipped for album sort text.
* Fixed thread safety issue when validating playlist songs on startup.
* Fixed filter search not ignoring space after colon when using column based search.
* Fixed KGlobalAccel to use capitalized application name.
* Fixed slash not properly handled when saving a playlist (#1624).
* (Unix) Fixed collection scanner so it ignores special filesystem paths (/sys, /proc, /run, etc) (#1615).
* (Windows) Fixed smart playlist wizard not respecting dark mode with Windows 11 style (#1639).
Enhancements:
* Use XSPF "title" as playlist name when loading and saving playlists (#1624).
* Added support for using album ID when receving album covers for Subsonic songs (#1636).
* Added option for preserving directory structure when trascoding songs (#1637).
* (Windows) Always run MSVC runtime installer to possible fix issues when there is an older runtime installed.
Version 1.2.3 (2024.12.08):
Bugfixes:
* Fixed libcdio NULL related compilation error on FreeBSD (#1610).
* Fixed missing seek when starting playback of a CUE song (#1568).
* Fixed "QDBusObjectPath: invalid path" error.
Version 1.2.2 (2024.11.23):
Bugfixes:

View File

@@ -4,7 +4,7 @@
[![Patreon](https://img.shields.io/badge/patreon-donate-green.svg)](https://patreon.com/jonaskvinge)
[![PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/jonaskvinge)
Strawberry is a music player and music collection organizer. It is a fork of Clementine released in 2018 aimed at music collectors and audiophiles. It's written in C++ using the Qt toolkit.
Strawberry is a music player and music collection organizer. It is a fork of Clementine released in 2018 aimed at music collectors and audiophiles. It's written in C++ using the Qt framework.
![Browse](https://raw.githubusercontent.com/strawberrymusicplayer/strawberry/master/data/screenshot/screenshot.png)
@@ -53,13 +53,15 @@ 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
* Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
* Scrobbler with support for [Last.fm](https://www.last.fm/), [Libre.fm](https://libre.fm/) and [ListenBrainz](https://listenbrainz.org/)
* Subsonic, Tidal, Spotify and Qobuz streaming support
* Streaming from Subsonic compatible servers
* Unofficial Tidal, Spotify and Qobuz integration
* Discord rich presence
It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows.
@@ -70,7 +72,7 @@ It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows.
To build Strawberry from source you need the following installed on your system with the additional development packages/headers:
* [CMake](https://cmake.org/)
* [CMake 3.13 or higher](https://cmake.org/)
* C/C++ compiler ([GCC](https://gcc.gnu.org/), [Clang](https://clang.llvm.org/) or [MSVC](https://visualstudio.microsoft.com/vs/features/cplusplus/))
* [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) or [pkgconf](https://github.com/pkgconf/pkgconf)
* [Boost](https://www.boost.org/)
@@ -81,7 +83,7 @@ To build Strawberry from source you need the following installed on your system
* [GStreamer](https://gstreamer.freedesktop.org/)
* [TagLib 1.12 or higher](https://www.taglib.org/)
* [ICU](https://unicode-org.github.io/icu/)
* [KDSingleApplication](https://github.com/KDAB/KDSingleApplication)
* [KDSingleApplication 1.1.0 or higher](https://github.com/KDAB/KDSingleApplication)
Optional dependencies:
@@ -92,25 +94,24 @@ Optional dependencies:
* MTP devices: [libmtp](http://libmtp.sourceforge.net/)
* iPod Classic devices: [libgpod](http://www.gtkpod.org/libgpod/)
* EBU R 128 loudness normalization [libebur128](https://github.com/jiixyj/libebur128)
* Discord rich presence [RapidJSON](https://rapidjson.org/)
You should also install the gstreamer plugins base and good, and optionally bad, ugly and libav to support all audio formats.
### :wrench: Compiling from source
### :wrench: Build from source
### Get the code:
git clone --recursive https://github.com/strawberrymusicplayer/strawberry
### Compile and install:
### Build and install:
cd strawberry
mkdir build
cd build
cmake ..
make -j $(nproc)
sudo make install
cmake -S . -B build
cmake --build build --parallel $(nproc)
sudo cmake --install build
To compile on Windows with Visual Studio 2019 or 2022, see https://github.com/strawberrymusicplayer/strawberry-msvc
To build on Windows with Visual Studio 2022, see https://github.com/strawberrymusicplayer/strawberry-msvc
### :penguin: Packaging status

View File

@@ -2,5 +2,5 @@ find_program(LSB_RELEASE_EXEC lsb_release)
find_program(DPKG_BUILDPACKAGE dpkg-buildpackage)
if (LSB_RELEASE_EXEC AND DPKG_BUILDPACKAGE)
add_custom_target(deb WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${DPKG_BUILDPACKAGE} -b -d -uc -us)
add_custom_target(deb WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${DPKG_BUILDPACKAGE} -b -d -uc -us -nc -j4)
endif()

View File

@@ -31,7 +31,7 @@ if(MACDEPLOYQT_EXECUTABLE)
add_custom_target(deploy
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/{Frameworks,Resources}
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/Info.plist ${CMAKE_BINARY_DIR}/strawberry.app/Contents/
COMMAND cp -v ${CMAKE_BINARY_DIR}/dist/macos/Info.plist ${CMAKE_BINARY_DIR}/strawberry.app/Contents/
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/strawberry.icns ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources/
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh ${CMAKE_BINARY_DIR}/strawberry.app
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner ${MACDEPLOYQT_CODESIGN}

View File

@@ -1,15 +1,15 @@
set(summary_willbuild "")
set(summary_willnotbuild "")
macro(summary_add name test)
macro(optional_component_summary_add name test)
if (${test})
list(APPEND summary_willbuild ${name})
else (${test})
list(APPEND summary_willnotbuild "${name}")
endif (${test})
endmacro(summary_add)
endmacro(optional_component_summary_add)
macro(summary_show_part variable title)
macro(optional_component_summary_show_part variable title)
list(LENGTH ${variable} _len)
if (_len)
message("")
@@ -18,19 +18,20 @@ macro(summary_show_part variable title)
message(" ${_item}")
endforeach (_item)
endif (_len)
endmacro(summary_show_part)
endmacro(optional_component_summary_show_part)
macro(summary_show)
macro(optional_component_summary_show)
list(SORT summary_willbuild)
list(SORT summary_willnotbuild)
message("")
message("Building strawberry version: ${STRAWBERRY_VERSION_DISPLAY}, Qt version ${Qt${QT_VERSION_MAJOR}Core_VERSION}")
summary_show_part(summary_willbuild "The following components will be built:")
summary_show_part(summary_willnotbuild "The following components WILL NOT be built:")
optional_component_summary_show_part(summary_willbuild "The following components will be built:")
optional_component_summary_show_part(summary_willnotbuild "The following components WILL NOT be built:")
message("")
endmacro(summary_show)
endmacro(optional_component_summary_show)
function(optional_component name default description)
set(option_variable "ENABLE_${name}")
set(have_variable "HAVE_${name}")
set(${have_variable} OFF)
@@ -79,6 +80,9 @@ function(optional_component name default description)
set(text "${description} (missing ${deplist_text})")
set(summary_willnotbuild "${summary_willnotbuild};${text}" PARENT_SCOPE)
message(FATAL_ERROR "${text}, to disable this optional feature, pass -D${option_variable}=OFF to CMake")
else()
set(${have_variable} ON PARENT_SCOPE)
set(summary_willbuild "${summary_willbuild};${description}" PARENT_SCOPE)

View File

@@ -64,7 +64,7 @@ if (LSB_RELEASE_EXEC AND RPMBUILD_EXEC)
add_custom_target(rpm
COMMAND ${CMAKE_SOURCE_DIR}/dist/scripts/maketarball.sh
COMMAND ${CMAKE_COMMAND} -E copy strawberry-${STRAWBERRY_VERSION_PACKAGE}.tar.xz ${RPMBUILD_DIR}/SOURCES/
COMMAND ${RPMBUILD_EXEC} -ba ${CMAKE_SOURCE_DIR}/dist/unix/strawberry.spec
COMMAND ${RPMBUILD_EXEC} -ba ${CMAKE_BINARY_DIR}/strawberry.spec
)
endif()

View File

@@ -1,6 +1,6 @@
set(STRAWBERRY_VERSION_MAJOR 1)
set(STRAWBERRY_VERSION_MINOR 2)
set(STRAWBERRY_VERSION_PATCH 2)
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;

3
debian/clean vendored Normal file
View File

@@ -0,0 +1,3 @@
dist/scripts/maketarball.sh
CMakeCache.txt
CMakeFiles/

1
debian/compat vendored
View File

@@ -1 +0,0 @@
11

13
debian/control vendored
View File

@@ -2,7 +2,7 @@ Source: strawberry
Section: sound
Priority: optional
Maintainer: Jonas Kvinge <jonas@jkvinge.net>
Build-Depends: debhelper (>= 11),
Build-Depends: debhelper-compat (= 12),
git,
make,
cmake,
@@ -15,11 +15,14 @@ Build-Depends: debhelper (>= 11),
libpulse-dev,
libtag1-dev,
libicu-dev,
libxkbcommon-dev,
qt6-base-dev,
qt6-base-private-dev,
qt6-base-dev-tools,
qt6-tools-dev,
qt6-tools-dev-tools,
qt6-l10n-tools,
libkdsingleapplication-qt6-dev,
libgstreamer1.0-dev,
libgstreamer-plugins-base1.0-dev,
libcdio-dev,
@@ -27,8 +30,10 @@ Build-Depends: debhelper (>= 11),
libmtp-dev,
libchromaprint-dev,
libfftw3-dev,
libebur128-dev
Standards-Version: 4.6.1
libebur128-dev,
libsparsehash-dev,
rapidjson-dev
Standards-Version: 4.7.0
Package: strawberry
Architecture: any
@@ -55,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

17
debian/rules vendored
View File

@@ -1,17 +1,14 @@
#!/usr/bin/make -f
%:
dh $@ --buildsystem=cmake -builddirectory=build
export DH_VERBOSE=1
export DEB_BUILD_MAINT_OPTIONS=hardening=+all
override_dh_auto_clean:
rm -f dist/macos/Info.plist
rm -f dist/unix/strawberry.spec
rm -f dist/scripts/maketarball.sh
rm -f dist/windows/strawberry.nsi
rm -f src/translations/translations.pot
dh_auto_clean
override_dh_auto_configure:
dh_auto_configure -- \
-DBUILD_WERROR=ON
override_dh_installchangelogs:
dh_installchangelogs Changelog
override_dh_auto_test:
%:
dh $@

14
dist/CMakeLists.txt vendored
View File

@@ -1,7 +1,7 @@
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/scripts/maketarball.sh.in ${CMAKE_CURRENT_SOURCE_DIR}/scripts/maketarball.sh @ONLY)
if(RPM_DISTRO AND RPM_DATE)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/unix/strawberry.spec.in ${CMAKE_CURRENT_SOURCE_DIR}/unix/strawberry.spec @ONLY)
endif(RPM_DISTRO AND RPM_DATE)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/unix/strawberry.spec.in ${CMAKE_BINARY_DIR}/strawberry.spec @ONLY)
endif()
if(APPLE)
if(DEFINED ENV{MACOSX_DEPLOYMENT_TARGET})
@@ -9,13 +9,13 @@ if(APPLE)
else()
set(LSMinimumSystemVersion 12.0)
endif()
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist)
endif(APPLE)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist)
endif()
if(WIN32)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/windres.rc.in ${CMAKE_BINARY_DIR}/windres.rc)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/strawberry.nsi.in ${CMAKE_BINARY_DIR}/strawberry.nsi @ONLY)
endif(WIN32)
endif()
if(UNIX AND NOT APPLE)
install(FILES ../data/icons/48x48/strawberry.png DESTINATION share/icons/hicolor/48x48/apps/)
@@ -24,9 +24,9 @@ if(UNIX AND NOT APPLE)
install(FILES unix/org.strawberrymusicplayer.strawberry.desktop DESTINATION share/applications)
install(FILES unix/org.strawberrymusicplayer.strawberry.appdata.xml DESTINATION share/metainfo)
install(FILES unix/strawberry.1 DESTINATION share/man/man1)
endif(UNIX AND NOT APPLE)
endif()
if(APPLE)
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist" DESTINATION "${CMAKE_BINARY_DIR}/strawberry.app/Contents")
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist" DESTINATION "${CMAKE_BINARY_DIR}/strawberry.app/Contents")
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/macos/strawberry.icns" DESTINATION "${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources")
endif()

View File

@@ -35,9 +35,9 @@
<key>LSMinimumSystemVersion</key>
<string>@LSMinimumSystemVersion@</string>
<key>SUFeedURL</key>
<string>https://www.strawberrymusicplayer.org/sparkle-macos</string>
<string>https://www.strawberrymusicplayer.org/sparkle-macos-@ARCH@</string>
<key>SUPublicEDKey</key>
<string>3IRScV8YtNVnx7zoeJAXvg28Kh1gN/Pyl2iPM467pG8=</string>
<string>/OydhYVfypuO2Mf7G6DUqVZWW9G19eFV74qaDCBTOUk=</string>
<key>CFBundleURLTypes</key>
<array>
<dict>

View File

@@ -2,7 +2,7 @@
# Strawberry Music Player
# Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
# 2021 Alexey Vazhnov
# Copyright 2021, Alexey Vazhnov
#
# Strawberry is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -19,7 +19,7 @@
# SPDX-License-Identifier: GPL-3.0-only
# Based on https://github.com/strawberrymusicplayer/strawberry/wiki/Import-collection-library-and-playlists-data-from-Clementine
# Based on https://wiki.strawberrymusicplayer.org/wiki/Import_collection_library_and_playlists_from_Clementine
set -o nounset
set -o errexit
@@ -35,8 +35,8 @@ test -f "$FILE_DST" || { echo "No such file: $FILE_DST"; exit 1; }
echo "Will try to copy information from $FILE_SRC to $FILE_DST."
echo
echo 'This script will **delete all information** from Strawberry database!'
read -r -p 'Do you want to continue? (the only YES is accepted) ' answer
echo 'This script will **delete all data** from the Strawberry database!'
read -r -p 'Do you want to continue? (Only YES is accepted) ' answer
if [ "$answer" != "YES" ]; then exit 1; fi
# 'heredoc' with substitution of variables, see `man bash`, "Here Documents":
@@ -62,9 +62,9 @@ INSERT INTO strawberry.subdirectories (directory_id, path, mtime) SELECT directo
INSERT INTO strawberry.songs (ROWID, title, album, artist, albumartist, track, disc, year, originalyear, genre, compilation, composer, performer, grouping, comment, lyrics, beginning, length, bitrate, samplerate, directory_id, url, filetype, filesize, mtime, ctime, unavailable, playcount, skipcount, lastplayed, compilation_detected, compilation_on, compilation_off, compilation_effective, art_automatic, art_manual, effective_albumartist, effective_originalyear, cue_path, rating)
SELECT ROWID, title, album, artist, albumartist, track, disc, year, originalyear, genre, compilation, composer, performer, grouping, comment, lyrics, beginning, length, bitrate, samplerate, directory, filename, filetype, filesize, mtime, ctime, unavailable, playcount, skipcount, lastplayed, sampler, forced_compilation_on, forced_compilation_off, effective_compilation, art_automatic, art_manual, effective_albumartist, effective_originalyear, cue_path, rating FROM clementine.songs WHERE unavailable = 0;
UPDATE strawberry.songs SET source = 2;
UPDATE strawberry.songs SET artist_id = "";
UPDATE strawberry.songs SET album_id = "";
UPDATE strawberry.songs SET song_id = "";
UPDATE strawberry.songs SET artist_id = '';
UPDATE strawberry.songs SET album_id = '';
UPDATE strawberry.songs SET song_id = '';
/* Import playlists */
@@ -140,7 +140,7 @@ SELECT ROWID,
bitrate,
samplerate,
directory,
filename,
CASE WHEN filename IS NULL THEN '' ELSE filename END,
filetype,
filesize,
mtime,
@@ -162,16 +162,9 @@ SELECT ROWID,
UPDATE strawberry.playlist_items SET source = 2;
UPDATE strawberry.playlist_items SET type = 2;
UPDATE strawberry.playlist_items SET artist_id = "";
UPDATE strawberry.playlist_items SET album_id = "";
UPDATE strawberry.playlist_items SET song_id = "";
/* Recreate the FTS tables */
DELETE FROM strawberry.songs_fts;
INSERT INTO strawberry.songs_fts (ROWID, ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment)
SELECT ROWID, title, album, artist, albumartist, composer, performer, grouping, genre, comment
FROM strawberry.songs;
UPDATE strawberry.playlist_items SET artist_id = '';
UPDATE strawberry.playlist_items SET album_id = '';
UPDATE strawberry.playlist_items SET song_id = '';
EOF

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,17 @@
</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"/>
<release version="1.2.6" date="2025-01-17"/>
<release version="1.2.5" date="2025-01-17"/>
<release version="1.2.4" date="2025-01-10"/>
<release version="1.2.3" date="2024-12-08"/>
<release version="1.2.2" date="2024-11-23"/>
<release version="1.2.1" date="2024-11-21"/>
<release version="1.1.3" date="2024-09-21"/>

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

@@ -63,6 +63,14 @@ BuildRequires: pkgconfig(libcdio)
BuildRequires: pkgconfig(libebur128)
BuildRequires: pkgconfig(libgpod-1.0)
BuildRequires: pkgconfig(libmtp)
BuildRequires: pkgconfig(libsparsehash)
BuildRequires: cmake(GTest)
BuildRequires: pkgconfig(gmock)
BuildRequires: cmake(RapidJSON)
%if 0%{?fedora} || (0%{?suse_version} && 0%{?suse_version} > 1600) || "%{?_vendor}" == "openmandriva"
BuildRequires: cmake(KDSingleApplication-qt6)
%endif
%if 0%{?suse_version}
Requires: qt6-sql-sqlite
@@ -85,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
@@ -94,7 +102,7 @@ Features:
- Streaming support for Subsonic-compatible servers
- Unofficial streaming support for Tidal and Qobuz
%if 0%{?suse_version}
%if 0%{?suse_version} && 0%{?suse_version} <= 1600
%debug_package
%endif
@@ -103,13 +111,13 @@ Features:
%build
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos}
export CXXFLAGS="-fPIC $RPM_OPT_FLAGS"
export CXXFLAGS="-fPIC -Wno-maybe-uninitialized $RPM_OPT_FLAGS"
%endif
%if "%{?_vendor}" == "openmandriva"
%{cmake} -DQT_VERSION_MAJOR=@QT_VERSION_MAJOR@ -DENABLE_TRANSLATIONS=OFF
%{cmake} -DBUILD_WERROR=ON
%make_build
%else
%{cmake} -DQT_VERSION_MAJOR=@QT_VERSION_MAJOR@
%{cmake} -DBUILD_WERROR=ON
%cmake_build
%endif
@@ -120,11 +128,13 @@ Features:
%cmake_install
%endif
%if 0%{?suse_version}
%suse_update_desktop_file org.strawberrymusicplayer.strawberry Qt AudioVideo Audio Player
%endif
%check
export QT_QPA_PLATFORM="offscreen"
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos}
%{cmake_build} -t strawberry_tests
%else
%{make_build} -j $(nproc) -C build strawberry_tests
%endif
desktop-file-validate %{buildroot}%{_datadir}/applications/org.strawberrymusicplayer.strawberry.desktop
%if 0%{?suse_version}
appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/org.strawberrymusicplayer.strawberry.appdata.xml

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
@@ -208,14 +218,16 @@ FunctionEnd
!ifdef msvc
!define vc_redist_file "vc_redist.${arch}.exe"
Function InstallMSVCRuntime
${registry::Read} "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\${arch}" "Version" $R0 $R1
${If} $R0 == ""
SetOutPath "$TEMP"
File "${vc_redist_file}"
; ${registry::Read} "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\${arch}" "Version" $R0 $R1
; ${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'
; 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 /norestart'
Delete "$TEMP\${vc_redist_file}"
SetDetailsView show
${EndIf}
; ${EndIf}
FunctionEnd
!endif
@@ -257,7 +269,7 @@ Section "Strawberry" Strawberry
File "libssl-3-x64.dll"
!endif
File "libFLAC-12.dll"
File "libFLAC-14.dll"
File "libbrotlicommon.dll"
File "libbrotlidec.dll"
File "libbrotlienc.dll"
@@ -365,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"
@@ -379,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"
@@ -400,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"
@@ -432,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"
@@ -442,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.
@@ -456,11 +482,15 @@ Section "Strawberry" Strawberry
; Common files
File "icudt76.dll"
File "icudt77.dll"
!ifdef msvc && arch_arm64
File "fftw3.dll"
!else
File "libfftw3-3.dll"
!endif
!ifdef msvc && debug
File "icuin76d.dll"
File "icuuc76d.dll"
File "icuin77d.dll"
File "icuuc77d.dll"
File "libxml2d.dll"
File "Qt6Concurrentd.dll"
File "Qt6Cored.dll"
@@ -469,8 +499,8 @@ Section "Strawberry" Strawberry
File "Qt6Sqld.dll"
File "Qt6Widgetsd.dll"
!else
File "icuin76.dll"
File "icuuc76.dll"
File "icuin77.dll"
File "icuuc77.dll"
File "libxml2.dll"
File "Qt6Concurrent.dll"
File "Qt6Core.dll"
@@ -524,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
@@ -594,6 +626,7 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=libgstapp.dll" "gstreamer-plugins\libgstapp.dll"
File "/oname=libgstasf.dll" "gstreamer-plugins\libgstasf.dll"
File "/oname=libgstasfmux.dll" "gstreamer-plugins\libgstasfmux.dll"
File "/oname=libgstasio.dll" "gstreamer-plugins\libgstasio.dll"
File "/oname=libgstaudioconvert.dll" "gstreamer-plugins\libgstaudioconvert.dll"
File "/oname=libgstaudiofx.dll" "gstreamer-plugins\libgstaudiofx.dll"
File "/oname=libgstaudioparsers.dll" "gstreamer-plugins\libgstaudioparsers.dll"
@@ -671,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"
@@ -704,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"
@@ -716,6 +747,10 @@ 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"
!endif
@@ -781,7 +816,7 @@ Section "Uninstall"
Delete "$INSTDIR\libssl-3-x64.dll"
!endif
Delete "$INSTDIR\libFLAC-12.dll"
Delete "$INSTDIR\libFLAC-14.dll"
Delete "$INSTDIR\libbrotlicommon.dll"
Delete "$INSTDIR\libbrotlidec.dll"
Delete "$INSTDIR\libbrotlienc.dll"
@@ -889,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"
@@ -903,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"
@@ -924,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"
@@ -956,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"
@@ -966,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
@@ -979,11 +1028,15 @@ Section "Uninstall"
; Common files
Delete "$INSTDIR\icudt76.dll"
Delete "$INSTDIR\icudt77.dll"
!ifdef msvc && arch_arm64
Delete "$INSTDIR\fftw3.dll"
!else
Delete "$INSTDIR\libfftw3-3.dll"
!endif
!ifdef msvc && debug
Delete "$INSTDIR\icuin76d.dll"
Delete "$INSTDIR\icuuc76d.dll"
Delete "$INSTDIR\icuin77d.dll"
Delete "$INSTDIR\icuuc77d.dll"
Delete "$INSTDIR\libxml2d.dll"
Delete "$INSTDIR\Qt6Concurrentd.dll"
Delete "$INSTDIR\Qt6Cored.dll"
@@ -992,8 +1045,8 @@ Section "Uninstall"
Delete "$INSTDIR\Qt6Sqld.dll"
Delete "$INSTDIR\Qt6Widgetsd.dll"
!else
Delete "$INSTDIR\icuin76.dll"
Delete "$INSTDIR\icuuc76.dll"
Delete "$INSTDIR\icuin77.dll"
Delete "$INSTDIR\icuuc77.dll"
Delete "$INSTDIR\libxml2.dll"
Delete "$INSTDIR\Qt6Concurrent.dll"
Delete "$INSTDIR\Qt6Core.dll"
@@ -1013,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
@@ -1050,6 +1105,7 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\libgstapp.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstasf.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstasfmux.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstasio.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstaudioconvert.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstaudiofx.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstaudioparsers.dll"
@@ -1129,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"
@@ -1162,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"
@@ -1174,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
!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

@@ -50,9 +50,9 @@
// Make an INSTRUCTIONS file
// can't mod scope in analyze you have to use transform for 2D use setErasePixmap Qt function insetead of m_background
AnalyzerBase::AnalyzerBase(QWidget *parent, const uint scopeSize)
AnalyzerBase::AnalyzerBase(QWidget *parent, const uint scope_size)
: QWidget(parent),
fht_(new FHT(scopeSize)),
fht_(new FHT(scope_size)),
engine_(nullptr),
lastscope_(512),
new_frame_(false),
@@ -100,7 +100,7 @@ void AnalyzerBase::transform(Scope &scope) {
fht_->logSpectrum(scope.data(), aux.data());
fht_->scale(scope.data(), 1.0F / 20);
scope.resize(fht_->size() / 2); // second half of values are rubbish
scope.resize(static_cast<size_t>(fht_->size() / 2)); // second half of values are rubbish
}
@@ -112,7 +112,7 @@ void AnalyzerBase::paintEvent(QPaintEvent *e) {
switch (engine_->state()) {
case EngineBase::State::Playing:{
const EngineBase::Scope &thescope = engine_->scope(timeout_);
int i = 0;
size_t i = 0;
// convert to mono here - our built in analyzers need mono, but the engines provide interleaved pcm
for (uint x = 0; static_cast<int>(x) < fht_->size(); ++x) {
@@ -124,7 +124,7 @@ void AnalyzerBase::paintEvent(QPaintEvent *e) {
transform(lastscope_);
analyze(p, lastscope_, new_frame_);
lastscope_.resize(fht_->size());
lastscope_.resize(static_cast<size_t>(fht_->size()));
break;
}
@@ -153,7 +153,7 @@ int AnalyzerBase::resizeExponent(int exp) {
if (exp != fht_->sizeExp()) {
delete fht_;
fht_ = new FHT(exp);
fht_ = new FHT(static_cast<uint>(exp));
}
return exp;
@@ -211,28 +211,28 @@ void AnalyzerBase::demo(QPainter &p) {
}
void AnalyzerBase::interpolate(const Scope &inVec, Scope &outVec) {
void AnalyzerBase::interpolate(const Scope &in_scope, Scope &out_scope) {
double pos = 0.0;
const double step = static_cast<double>(inVec.size()) / static_cast<double>(outVec.size());
const double step = static_cast<double>(in_scope.size()) / static_cast<double>(out_scope.size());
for (uint i = 0; i < outVec.size(); ++i, pos += step) {
for (uint i = 0; i < out_scope.size(); ++i, pos += step) {
const double error = pos - std::floor(pos);
const uint64_t offset = static_cast<uint64_t>(pos);
uint64_t indexLeft = offset + 0;
if (indexLeft >= inVec.size()) {
indexLeft = inVec.size() - 1;
if (indexLeft >= in_scope.size()) {
indexLeft = in_scope.size() - 1;
}
uint64_t indexRight = offset + 1;
if (indexRight >= inVec.size()) {
indexRight = inVec.size() - 1;
if (indexRight >= in_scope.size()) {
indexRight = in_scope.size() - 1;
}
outVec[i] = inVec[indexLeft] * (1.0F - static_cast<float>(error)) + inVec[indexRight] * static_cast<float>(error);
out_scope[i] = in_scope[indexLeft] * (1.0F - static_cast<float>(error)) + in_scope[indexRight] * static_cast<float>(error);
}
}

View File

@@ -61,7 +61,7 @@ class AnalyzerBase : public QWidget {
protected:
using Scope = std::vector<float>;
explicit AnalyzerBase(QWidget*, const uint scopeSize = 7);
explicit AnalyzerBase(QWidget *parent, const uint scope_size = 7);
void hideEvent(QHideEvent *e) override;
void showEvent(QShowEvent *e) override;
@@ -71,12 +71,12 @@ class AnalyzerBase : public QWidget {
int resizeExponent(int exp);
int resizeForBands(const int bands);
virtual void init() {}
virtual void transform(Scope&);
virtual void analyze(QPainter &p, const Scope&, const bool new_frame) = 0;
virtual void transform(Scope &scope);
virtual void analyze(QPainter &p, const Scope &s, const bool new_frame) = 0;
virtual void demo(QPainter &p);
void interpolate(const Scope&, Scope&);
void initSin(Scope&, const uint = 6000);
void interpolate(const Scope &in_scope, Scope &out_scope);
void initSin(Scope &v, const uint size = 6000);
protected:
QBasicTimer timer_;

View File

@@ -85,7 +85,7 @@ void BlockAnalyzer::resizeEvent(QResizeEvent *e) {
// this is the y-offset for drawing from the top of the widget
y_ = (height() - (rows_ * (kHeight + 1)) + 2) / 2;
scope_.resize(columns_);
scope_.resize(static_cast<size_t>(columns_));
if (rows_ != oldRows) {
barpixmap_ = QPixmap(kWidth, rows_ * (kHeight + 1));
@@ -165,9 +165,9 @@ void BlockAnalyzer::analyze(QPainter &p, const Scope &s, const bool new_frame) {
// Paint the background
canvas_painter.drawPixmap(0, 0, background_);
for (int x = 0, y = 0; x < static_cast<int>(scope_.size()); ++x) {
for (qint64 x = 0, y = 0; x < static_cast<qint64>(scope_.size()); ++x) {
// determine y
for (y = 0; scope_[x] < yscale_.at(y); ++y);
for (y = 0; scope_[static_cast<quint64>(x)] < yscale_.at(y); ++y);
// This is opposite to what you'd think, higher than y means the bar is lower than y (physically)
if (static_cast<double>(y) > store_.at(x)) {
@@ -175,13 +175,13 @@ void BlockAnalyzer::analyze(QPainter &p, const Scope &s, const bool new_frame) {
y = static_cast<int>(store_.value(x));
}
else {
store_[x] = y;
store_[x] = static_cast<double>(y);
}
// If y is lower than fade_pos_, then the bar has exceeded the height of the fadeout
// if the fadeout is quite faded now, then display the new one
if (y <= fade_pos_.at(x) /*|| fade_intensity_[x] < kFadeSize / 3*/) {
fade_pos_[x] = y;
fade_pos_[x] = static_cast<int>(y);
fade_intensity_[x] = kFadeSize;
}
@@ -189,13 +189,13 @@ void BlockAnalyzer::analyze(QPainter &p, const Scope &s, const bool new_frame) {
--fade_intensity_[x];
const int offset = fade_intensity_.value(x);
const int y2 = y_ + (fade_pos_.value(x) * (kHeight + 1));
canvas_painter.drawPixmap(x * (kWidth + 1), y2, fade_bars_[offset], 0, 0, kWidth, height() - y2);
canvas_painter.drawPixmap(static_cast<int>(x) * (kWidth + 1), y2, fade_bars_[offset], 0, 0, kWidth, height() - y2);
}
if (fade_intensity_.at(x) == 0) fade_pos_[x] = rows_;
// REMEMBER: y is a number from 0 to rows_, 0 means all blocks are glowing, rows_ means none are
canvas_painter.drawPixmap(x * (kWidth + 1), y * (kHeight + 1) + y_, *bar(), 0, y * (kHeight + 1), bar()->width(), bar()->height());
canvas_painter.drawPixmap(static_cast<int>(x) * (kWidth + 1), static_cast<int>(y) * (kHeight + 1) + y_, *bar(), 0, static_cast<int>(y) * (kHeight + 1), bar()->width(), bar()->height());
}
for (int x = 0; x < store_.size(); ++x) {

View File

@@ -76,7 +76,7 @@ void BoomAnalyzer::resizeEvent(QResizeEvent *e) {
const double h = 1.2 / HEIGHT;
bands_ = qMin(static_cast<int>(static_cast<double>(width() + 1) / (kColumnWidth + 1)) + 1, kMaxBandCount);
scope_.resize(bands_);
scope_.resize(static_cast<size_t>(bands_));
F_ = static_cast<double>(HEIGHT) / (log10(256) * 1.1 /*<- max. amplitude*/);
@@ -112,17 +112,19 @@ void BoomAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_frame
return;
}
const uint MAX_HEIGHT = height() - 1;
const uint MAX_HEIGHT = static_cast<uint>(height() - 1);
QPainter canvas_painter(&canvas_);
canvas_.fill(palette().color(QPalette::Window));
interpolate(scope, scope_);
for (int i = 0, x = 0, y = 0; i < bands_; ++i, x += kColumnWidth + 1) {
int x = 0;
int y = 0;
for (size_t i = 0; i < static_cast<size_t>(bands_); ++i, x += kColumnWidth + 1) {
double h = log10(scope_[i] * 256.0) * F_;
if (h > MAX_HEIGHT) h = MAX_HEIGHT;
h = std::min(h, static_cast<double>(MAX_HEIGHT));
if (h > bar_height_[i]) {
bar_height_[i] = h;
@@ -138,7 +140,7 @@ void BoomAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_frame
else {
if (bar_height_[i] > 0.0) {
bar_height_[i] -= K_barHeight_; // 1.4
if (bar_height_[i] < 0.0) bar_height_[i] = 0.0;
bar_height_[i] = std::max(0.0, bar_height_[i]);
}
peak_handling:
@@ -147,8 +149,8 @@ void BoomAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_frame
peak_height_[i] -= peak_speed_[i];
peak_speed_[i] *= F_peakSpeed_; // 1.12
if (peak_height_[i] < bar_height_[i]) peak_height_[i] = bar_height_[i];
if (peak_height_[i] < 0.0) peak_height_[i] = 0.0;
peak_height_[i] = std::max(bar_height_[i], bar_height_[i]);
peak_height_[i] = std::max(0.0, peak_height_[i]);
}
}

View File

@@ -129,7 +129,7 @@ void RainbowAnalyzer::analyze(QPainter &p, const Scope &s, const bool new_frame)
// of band pass filters for this, so bands can leak into neighbouring bands,
// but for now it's a series of separate square filters.
const int samples_per_band = scope_size / kRainbowBands;
int sample = 0;
size_t sample = 0;
for (int band = 0; band < kRainbowBands; ++band) {
float accumulator = 0.0;
for (int i = 0; i < samples_per_band; ++i) {

View File

@@ -85,10 +85,10 @@ void SonogramAnalyzer::transform(Scope &scope) {
fht_->power2(scope.data());
fht_->scale(scope.data(), 1.0 / 256);
scope.resize(fht_->size() / 2);
scope.resize(static_cast<size_t>(fht_->size() / 2));
}
void SonogramAnalyzer::demo(QPainter &p) {
analyze(p, Scope(fht_->size(), 0), new_frame_);
analyze(p, Scope(static_cast<size_t>(fht_->size()), 0), new_frame_);
}

View File

@@ -43,7 +43,7 @@ void TurbineAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_fr
return;
}
const uint hd2 = height() / 2;
const uint hd2 = static_cast<uint>(height() / 2);
const uint kMaxHeight = hd2 - 1;
QPainter canvas_painter(&canvas_);
@@ -67,7 +67,7 @@ void TurbineAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_fr
else {
if (bar_height_[i] > 0.0) {
bar_height_[i] -= K_barHeight_; // 1.4
if (bar_height_[i] < 0.0) bar_height_[i] = 0.0;
bar_height_[i] = std::max(0.0, bar_height_[i]);
}
peak_handling:

View File

@@ -54,13 +54,13 @@ void WaveRubberAnalyzer::analyze(QPainter &p, const Scope &s, const bool new_fra
const float *amplitude_data = s.data();
const int mid_y = height() / 4;
const int num_samples = static_cast<int>(s.size());
const size_t num_samples = static_cast<size_t>(s.size());
const float x_scale = static_cast<float>(width()) / static_cast<float>(num_samples);
float prev_y = static_cast<float>(mid_y);
// Draw the waveform
for (int i = 0; i < num_samples; ++i) {
for (size_t i = 0; i < num_samples; ++i) {
// Normalize amplitude to 0-1 range
const float color_factor = amplitude_data[i] / 2.0F + 0.5F;
@@ -88,5 +88,5 @@ void WaveRubberAnalyzer::transform(Scope &scope) {
}
void WaveRubberAnalyzer::demo(QPainter &p) {
analyze(p, Scope(fht_->size(), 0), new_frame_);
analyze(p, Scope(static_cast<size_t>(fht_->size()), 0), new_frame_);
}

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
@@ -78,7 +78,7 @@ CollectionBackend::~CollectionBackend() {
void CollectionBackend::Init(SharedPtr<Database> db, SharedPtr<TaskManager> task_manager, const Song::Source source, const QString &songs_table, const QString &dirs_table, const QString &subdirs_table) {
setObjectName(source == Song::Source::Collection ? QLatin1String(metaObject()->className()) : QStringLiteral("%1%2").arg(Song::DescriptionForSource(source), QLatin1String(metaObject()->className())));
setObjectName(source == Song::Source::Collection ? QLatin1String(QObject::metaObject()->className()) : QStringLiteral("%1%2").arg(Song::DescriptionForSource(source), QLatin1String(QObject::metaObject()->className())));
db_ = db;
task_manager_ = task_manager;
@@ -522,7 +522,7 @@ void CollectionBackend::SongPathChanged(const Song &song, const QFileInfo &new_f
updated_song.set_url(QUrl::fromLocalFile(QDir::cleanPath(new_file.filePath())));
updated_song.set_basefilename(new_file.fileName());
updated_song.InitArtManual();
if (updated_song.is_collection_song() && new_collection_directory_id) {
if (updated_song.is_linked_collection_song() && new_collection_directory_id) {
updated_song.set_directory_id(new_collection_directory_id.value());
}
@@ -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;
@@ -853,6 +854,10 @@ void CollectionBackend::UpdateMTimesOnly(const SongList &songs) {
}
void CollectionBackend::DeleteSongsAsync(const SongList &songs) {
QMetaObject::invokeMethod(this, "DeleteSongs", Qt::QueuedConnection, Q_ARG(SongList, songs));
}
void CollectionBackend::DeleteSongs(const SongList &songs) {
QMutexLocker l(db_->Mutex());
@@ -879,6 +884,24 @@ void CollectionBackend::DeleteSongs(const SongList &songs) {
}
void CollectionBackend::DeleteSongsByUrlsAsync(const QList<QUrl> &urls) {
QMetaObject::invokeMethod(this, "DeleteSongsByUrl", Qt::QueuedConnection, Q_ARG(QList<QUrl>, urls));
}
void CollectionBackend::DeleteSongsByUrls(const QList<QUrl> &urls) {
SongList songs;
songs.reserve(urls.count());
for (const QUrl &url : urls) {
songs << GetSongsByUrl(url);
}
if (!songs.isEmpty()) {
DeleteSongs(songs);
}
}
void CollectionBackend::MarkSongsUnavailable(const SongList &songs, const bool unavailable) {
QMutexLocker l(db_->Mutex());

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
@@ -234,6 +234,9 @@ class CollectionBackend : public CollectionBackendInterface {
void UpdateSongRatingAsync(const int id, const float rating, const bool save_tags = false);
void UpdateSongsRatingAsync(const QList<int> &ids, const float rating, const bool save_tags = false);
void DeleteSongsAsync(const SongList &songs);
void DeleteSongsByUrlsAsync(const QList<QUrl> &url);
public Q_SLOTS:
void Exit();
void GetAllSongs(const int id);
@@ -247,6 +250,7 @@ class CollectionBackend : public CollectionBackendInterface {
void UpdateSongsBySongID(const SongMap &new_songs);
void UpdateMTimesOnly(const SongList &songs);
void DeleteSongs(const SongList &songs);
void DeleteSongsByUrls(const QList<QUrl> &url);
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs);
void CompilationsNeedUpdating();

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

@@ -66,7 +66,7 @@ CollectionLibrary::CollectionLibrary(const SharedPtr<Database> database,
save_playcounts_to_files_(false),
save_ratings_to_files_(false) {
setObjectName(QLatin1String(metaObject()->className()));
setObjectName(QLatin1String(QObject::metaObject()->className()));
original_thread_ = thread();
@@ -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();
}
@@ -199,8 +201,8 @@ void CollectionLibrary::SyncPlaycountAndRatingToFiles() {
task_manager_->SetTaskBlocksCollectionScans(task_id);
const SongList songs = backend_->GetAllSongs();
const qint64 nb_songs = songs.size();
int i = 0;
const quint64 nb_songs = static_cast<quint64>(songs.size());
quint64 i = 0;
for (const Song &song : songs) {
(void)tagreader_client_->SaveSongPlaycountBlocking(song.url().toLocalFile(), song.playcount());
(void)tagreader_client_->SaveSongRatingBlocking(song.url().toLocalFile(), song.rating());

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
@@ -50,14 +50,15 @@
#include <QPixmapCache>
#include <QNetworkDiskCache>
#include <QSettings>
#include <QStandardPaths>
#include <QTimer>
#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"
#include "core/iconloader.h"
#include "core/logging.h"
#include "core/settings.h"
#include "core/songmimedata.h"
#include "collectionfilteroptions.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),
@@ -98,7 +98,7 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
loading_(false),
icon_disk_cache_(new QNetworkDiskCache(this)) {
setObjectName(backend_->source() == Song::Source::Collection ? QLatin1String(metaObject()->className()) : QStringLiteral("%1%2").arg(Song::DescriptionForSource(backend_->source()), QLatin1String(metaObject()->className())));
setObjectName(backend_->source() == Song::Source::Collection ? QLatin1String(QObject::metaObject()->className()) : QStringLiteral("%1%2").arg(Song::DescriptionForSource(backend_->source()), QLatin1String(QObject::metaObject()->className())));
filter_->setSourceModel(this);
filter_->setSortRole(Role_SortText);
@@ -114,7 +114,7 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
pixmap_no_cover_ = nocover.pixmap(nocover_sizes.last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
icon_disk_cache_->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/' + QLatin1String(kPixmapDiskCacheDir) + u'-' + Song::TextForSource(backend_->source()));
icon_disk_cache_->setCacheDirectory(StandardPaths::WritableLocation(StandardPaths::StandardLocation::CacheLocation) + u'/' + QLatin1String(kPixmapDiskCacheDir) + u'-' + Song::TextForSource(backend_->source()));
QObject::connect(&*backend_, &CollectionBackend::SongsAdded, this, &CollectionModel::AddReAddOrUpdate);
QObject::connect(&*backend_, &CollectionBackend::SongsChanged, this, &CollectionModel::AddReAddOrUpdate);
@@ -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();
}
@@ -288,29 +275,11 @@ void CollectionModel::SetFilterMaxAge(const int filter_max_age) {
QVariant CollectionModel::data(const QModelIndex &idx, const int role) const {
const CollectionItem *item = IndexToItem(idx);
// Handle a special case for returning album artwork instead of a generic CD icon.
// this is here instead of in the other data() function to let us use the
// QModelIndex& version of GetChildSongs, which satisfies const-ness, instead
// of the CollectionItem *version, which doesn't.
if (options_active_.show_pretty_covers) {
bool is_album_node = false;
if (role == Qt::DecorationRole && item->type == CollectionItem::Type::Container) {
GroupBy container_group_by = options_active_.group_by[item->container_level];
is_album_node = IsAlbumGroupBy(container_group_by);
}
if (is_album_node) {
// It has const behaviour some of the time - that's ok right?
return const_cast<CollectionModel*>(this)->AlbumIcon(idx);
}
}
return data(item, role);
return data(IndexToItem(idx), role);
}
QVariant CollectionModel::data(const CollectionItem *item, const int role) const {
QVariant CollectionModel::data(CollectionItem *item, const int role) const {
GroupBy container_group_by = item->type == CollectionItem::Type::Container ? options_active_.group_by[item->container_level] : GroupBy::None;
@@ -329,7 +298,7 @@ QVariant CollectionModel::data(const CollectionItem *item, const int role) const
case GroupBy::YearAlbumDisc:
case GroupBy::OriginalYearAlbum:
case GroupBy::OriginalYearAlbumDisc:
return QVariant();
return options_active_.show_pretty_covers ? const_cast<CollectionModel*>(this)->AlbumIcon(item) : QVariant();
case GroupBy::Artist:
case GroupBy::AlbumArtist:
return icon_artist_;
@@ -408,20 +377,20 @@ QMimeData *CollectionModel::mimeData(const QModelIndexList &indexes) const {
if (indexes.isEmpty()) return nullptr;
SongMimeData *data = new SongMimeData;
QList<QUrl> urls;
SongList songs;
QSet<int> song_ids;
data->backend = backend_;
QList<QUrl> urls;
for (const QModelIndex &idx : indexes) {
GetChildSongs(IndexToItem(idx), &urls, &data->songs, &song_ids);
GetChildSongs(IndexToItem(idx), songs, song_ids, urls);
}
data->setUrls(urls);
data->name_for_new_playlist_ = Song::GetNameForNewPlaylist(data->songs);
SongMimeData *song_mime_data = new SongMimeData;
song_mime_data->setUrls(urls);
song_mime_data->backend = backend_;
song_mime_data->songs = songs;
song_mime_data->name_for_new_playlist_ = Song::GetNameForNewPlaylist(songs);
return data;
return song_mime_data;
}
@@ -439,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()) {
@@ -451,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);
@@ -483,6 +463,9 @@ void CollectionModel::ProcessUpdate() {
}
switch (update.type) {
case CollectionModelUpdate::Type::Reset:
ResetInternal();
break;
case CollectionModelUpdate::Type::AddReAddOrUpdate:
AddReAddOrUpdateSongsInternal(update.songs);
break;
@@ -717,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);
@@ -731,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(' '));
}
@@ -854,9 +837,9 @@ void CollectionModel::LoadSongsFromSqlAsyncFinished() {
}
QString CollectionModel::AlbumIconPixmapCacheKey(const QModelIndex &idx) const {
QString CollectionModel::AlbumIconPixmapCacheKey(const CollectionItem *item) const {
return Song::TextForSource(backend_->source()) + QLatin1Char('/') + idx.data(Role_ContainerKey).toString();
return Song::TextForSource(backend_->source()) + QLatin1Char('/') + item->container_key;
}
@@ -869,7 +852,7 @@ QUrl CollectionModel::AlbumIconPixmapDiskCacheKey(const QString &cache_key) {
void CollectionModel::ClearItemPixmapCache(CollectionItem *item) {
// Remove from pixmap cache
const QString cache_key = AlbumIconPixmapCacheKey(ItemToIndex(item));
const QString cache_key = AlbumIconPixmapCacheKey(item);
QPixmapCache::remove(cache_key);
if (use_disk_cache_ && icon_disk_cache_) icon_disk_cache_->remove(AlbumIconPixmapDiskCacheKey(cache_key));
if (pending_cache_keys_.contains(cache_key)) {
@@ -888,13 +871,12 @@ void CollectionModel::ClearItemPixmapCache(CollectionItem *item) {
}
QVariant CollectionModel::AlbumIcon(const QModelIndex &idx) {
QVariant CollectionModel::AlbumIcon(CollectionItem *item) {
CollectionItem *item = IndexToItem(idx);
if (!item) return pixmap_no_cover_;
// Check the cache for a pixmap we already loaded.
const QString cache_key = AlbumIconPixmapCacheKey(idx);
const QString cache_key = AlbumIconPixmapCacheKey(item);
QPixmap cached_pixmap;
if (QPixmapCache::find(cache_key, &cached_pixmap)) {
@@ -919,7 +901,7 @@ QVariant CollectionModel::AlbumIcon(const QModelIndex &idx) {
}
// No art is cached and we're not loading it already. Load art for the first song in the album.
SongList songs = GetChildSongs(idx);
const SongList songs = GetChildSongs(item);
if (!songs.isEmpty()) {
AlbumCoverLoaderOptions cover_loader_options(AlbumCoverLoaderOptions::Option::ScaledImage | AlbumCoverLoaderOptions::Option::PadScaledImage);
cover_loader_options.desired_scaled_size = QSize(kPrettyCoverSize, kPrettyCoverSize);
@@ -1087,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 SortTextForArtist(song.album(), sort_skips_articles);
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:
@@ -1113,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:
@@ -1154,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);
}
@@ -1199,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() ||
@@ -1412,7 +1396,7 @@ QString CollectionModel::DividerDisplayText(const GroupBy group_by, const QStrin
}
bool CollectionModel::CompareItems(const CollectionItem *a, const CollectionItem *b) const {
bool CollectionModel::CompareItems(CollectionItem *a, CollectionItem *b) const {
QVariant left = data(a, CollectionModel::Role_SortText);
QVariant right = data(b, CollectionModel::Role_SortText);
@@ -1453,24 +1437,23 @@ qint64 CollectionModel::MaximumCacheSize(Settings *s, const char *size_id, const
}
void CollectionModel::GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) const {
void CollectionModel::GetChildSongs(CollectionItem *item, SongList &songs, QSet<int> &song_ids, QList<QUrl> &urls) const {
switch (item->type) {
case CollectionItem::Type::Container: {
QList<CollectionItem*> children = item->children;
std::sort(children.begin(), children.end(), std::bind(&CollectionModel::CompareItems, this, std::placeholders::_1, std::placeholders::_2));
for (CollectionItem *child : children) {
GetChildSongs(child, urls, songs, song_ids);
GetChildSongs(child, songs, song_ids, urls);
}
break;
}
case CollectionItem::Type::Song:
urls->append(item->metadata.url());
if (!song_ids->contains(item->metadata.id())) {
songs->append(item->metadata);
song_ids->insert(item->metadata.id());
urls << item->metadata.url();
if (!song_ids.contains(item->metadata.id())) {
songs << item->metadata;
song_ids << item->metadata.id();
}
break;
@@ -1480,16 +1463,33 @@ void CollectionModel::GetChildSongs(CollectionItem *item, QList<QUrl> *urls, Son
}
SongList CollectionModel::GetChildSongs(const QList<CollectionItem*> items) const {
SongList songs;
QSet<int> song_ids;
QList<QUrl> urls;
for (CollectionItem *item : items) {
GetChildSongs(item, songs, song_ids, urls);
}
return songs;
}
SongList CollectionModel::GetChildSongs(CollectionItem *item) const {
return GetChildSongs(QList<CollectionItem*>() << item);
}
SongList CollectionModel::GetChildSongs(const QModelIndexList &indexes) const {
QList<QUrl> dontcare;
SongList ret;
SongList songs;
QSet<int> song_ids;
QList<QUrl> urls;
for (const QModelIndex &idx : indexes) {
GetChildSongs(IndexToItem(idx), &dontcare, &ret, &song_ids);
GetChildSongs(IndexToItem(idx), songs, song_ids, urls);
}
return ret;
return songs;
}

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;
};
@@ -155,7 +157,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
int total_artist_count() const { return total_artist_count_; }
int total_album_count() const { return total_album_count_; }
quint64 icon_disk_cache_size() { return icon_disk_cache_->cacheSize(); }
quint64 icon_disk_cache_size() { return static_cast<quint64>(icon_disk_cache_->cacheSize()); }
const CollectionModel::Grouping GetGroupBy() const { return options_current_.group_by; }
void SetGroupBy(const CollectionModel::Grouping g, const std::optional<bool> separate_albums_by_grouping = std::optional<bool>());
@@ -176,29 +178,32 @@ 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;
// Get information about the collection
void GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) const;
void GetChildSongs(CollectionItem *item, SongList &songs, QSet<int> &song_ids, QList<QUrl> &urls) const;
SongList GetChildSongs(const QList<CollectionItem*> items) const;
SongList GetChildSongs(CollectionItem *item) const;
SongList GetChildSongs(const QModelIndex &idx) const;
SongList GetChildSongs(const QModelIndexList &indexes) const;
bool CompareItems(const CollectionItem *a, const CollectionItem *b) const;
bool CompareItems(CollectionItem *a, CollectionItem *b) const;
bool HasParentAlbumGroupBy(CollectionItem *item) const;
@@ -224,9 +229,9 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
void BeginReset();
void EndReset();
QVariant data(const CollectionItem *item, const int role) const;
QVariant data(CollectionItem *item, const int role) const;
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs);
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs = SongList());
void ScheduleAddSongs(const SongList &songs);
void ScheduleUpdateSongs(const SongList &songs);
void ScheduleRemoveSongs(const SongList &songs);
@@ -250,14 +255,14 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
// Helpers
static bool IsCompilationArtistNode(const CollectionItem *node) { return node == node->parent->compilation_artist_node_; }
QString AlbumIconPixmapCacheKey(const QModelIndex &idx) const;
QString AlbumIconPixmapCacheKey(const CollectionItem *item) const;
static QUrl AlbumIconPixmapDiskCacheKey(const QString &cache_key);
QVariant AlbumIcon(const QModelIndex &idx);
QVariant AlbumIcon(CollectionItem *item);
void ClearItemPixmapCache(CollectionItem *item);
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();
@@ -276,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

@@ -1,8 +1,6 @@
/*
* 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
@@ -30,55 +28,57 @@
class SqlRow;
CollectionPlaylistItem::CollectionPlaylistItem() : PlaylistItem(Song::Source::Collection) {
song_.set_source(Song::Source::Collection);
CollectionPlaylistItem::CollectionPlaylistItem(const Song::Source source) : PlaylistItem(source) {
song_.set_source(source);
}
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(Song::Source::Collection), song_(song) {
song_.set_source(Song::Source::Collection);
}
QUrl CollectionPlaylistItem::Url() const { return song_.url(); }
void CollectionPlaylistItem::Reload() {
const TagReaderResult result = TagReaderClient::Instance()->ReadFileBlocking(song_.url().toLocalFile(), &song_);
if (!result.success()) {
qLog(Error) << "Could not reload file" << song_.url() << result.error_string();
return;
}
UpdateTemporaryMetadata(song_);
}
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(song.source()), song_(song) {}
bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) {
// Rows from the songs tables come first
song_.InitFromQuery(query, true);
song_.set_source(Song::Source::Collection);
int col = 0;
switch (source_) {
case Song::Source::Collection:
col = 0;
break;
default:
col = static_cast<int>(Song::kRowIdColumns.count());
break;
}
song_.InitFromQuery(query, true, col);
return song_.is_valid();
}
QVariant CollectionPlaylistItem::DatabaseValue(DatabaseColumn column) const {
void CollectionPlaylistItem::Reload() {
switch (column) {
case Column_CollectionId: return song_.id();
default: return PlaylistItem::DatabaseValue(column);
if (song_.url().isLocalFile()) {
const TagReaderResult result = TagReaderClient::Instance()->ReadFileBlocking(song_.url().toLocalFile(), &song_);
if (!result.success()) {
qLog(Error) << "Could not reload file" << song_.url() << result.error_string();
return;
}
UpdateStreamMetadata(song_);
}
}
Song CollectionPlaylistItem::Metadata() const {
QVariant CollectionPlaylistItem::DatabaseValue(const DatabaseColumn database_column) const {
if (HasTemporaryMetadata()) return temp_metadata_;
return song_;
switch (database_column) {
case DatabaseColumn::CollectionId:
return song_.id();
default:
return PlaylistItem::DatabaseValue(database_column);
}
}
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

@@ -1,8 +1,6 @@
/*
* 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
@@ -34,25 +32,23 @@ class SqlRow;
class CollectionPlaylistItem : public PlaylistItem {
public:
explicit CollectionPlaylistItem();
explicit CollectionPlaylistItem(const Song::Source source);
explicit CollectionPlaylistItem(const Song &song);
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; }
QUrl Url() const override;
bool IsLocalCollectionItem() const override { return true; }
void SetArtManual(const QUrl &cover_url) override;
protected:
QVariant DatabaseValue(DatabaseColumn column) const override;
Song DatabaseSongMetadata() const override { return Song(Song::Source::Collection); }
QVariant DatabaseValue(const DatabaseColumn database_column) const override;
Song DatabaseSongMetadata() const override { return Song(source_); }
protected:
Song song_;

View File

@@ -65,10 +65,8 @@
#include "collectionitem.h"
#include "collectionitemdelegate.h"
#include "collectionview.h"
#ifndef Q_OS_WIN
# 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_WIN
action_copy_to_device_(nullptr),
#endif
action_edit_track_(nullptr),
action_edit_tracks_(nullptr),
action_rescan_songs_(nullptr),
@@ -108,7 +104,7 @@ CollectionView::CollectionView(QWidget *parent)
is_in_keyboard_search_(false),
delete_files_(false) {
setObjectName(QLatin1String(metaObject()->className()));
setObjectName(QLatin1String(QObject::metaObject()->className()));
setItemDelegate(new CollectionItemDelegate(this));
setAttribute(Qt::WA_MacShowFocusRect, false);
@@ -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_WIN
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_WIN
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_WIN
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_WIN
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_WIN
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_WIN
QAction *action_copy_to_device_;
#endif
QAction *action_edit_track_;
QAction *action_edit_tracks_;
QAction *action_rescan_songs_;

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-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
@@ -27,6 +27,7 @@
#include <QObject>
#include <QThread>
#include <QIODevice>
#include <QStorageInfo>
#include <QDir>
#include <QDirIterator>
#include <QFile>
@@ -51,6 +52,7 @@
#include "core/settings.h"
#include "utilities/imageutils.h"
#include "constants/timeconstants.h"
#include "constants/filesystemconstants.h"
#include "tagreader/tagreaderclient.h"
#include "collectiondirectory.h"
#include "collectionbackend.h"
@@ -104,7 +106,7 @@ CollectionWatcher::CollectionWatcher(const Song::Source source,
cue_parser_(new CueParser(tagreader_client, backend, this)),
last_scan_time_(0) {
setObjectName(source_ == Song::Source::Collection ? QLatin1String(metaObject()->className()) : QStringLiteral("%1%2").arg(Song::DescriptionForSource(source_), QLatin1String(metaObject()->className())));
setObjectName(source_ == Song::Source::Collection ? QLatin1String(QObject::metaObject()->className()) : QStringLiteral("%1%2").arg(Song::DescriptionForSource(source_), QLatin1String(QObject::metaObject()->className())));
original_thread_ = thread();
@@ -464,6 +466,24 @@ CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const CollectionSubdirectoryList &subdirs) {
{
const QFileInfo path_info(dir.path);
if (path_info.isSymbolicLink()) {
const QStorageInfo storage_info(path_info.symLinkTarget());
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
qLog(Warning) << "Ignoring collection directory path" << dir.path << "which is a symbolic link to path" << path_info.symLinkTarget() << "with rejected filesystem type" << storage_info.fileSystemType();
return;
}
}
else {
const QStorageInfo storage_info(dir.path);
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
qLog(Warning) << "Ignoring collection directory path" << dir.path << "with rejected filesystem type" << storage_info.fileSystemType();
return;
}
}
}
CancelStop();
watched_dirs_[dir.id] = dir;
@@ -506,17 +526,29 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
QFileInfo path_info(path);
const QFileInfo path_info(path);
// Do not scan symlinked dirs that are already in collection
if (path_info.isSymLink()) {
QString real_path = path_info.symLinkTarget();
const QString real_path = path_info.symLinkTarget();
const QStorageInfo storage_info(real_path);
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
qLog(Warning) << "Ignoring symbolic link" << path << "which links to" << real_path << "with rejected filesystem type" << storage_info.fileSystemType();
return;
}
// Do not scan symlinked dirs that are already in collection
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
if (real_path.startsWith(dir.path)) {
return;
}
}
}
else {
const QStorageInfo storage_info(path);
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
qLog(Warning) << "Ignoring path" << path << "with rejected filesystem type" << storage_info.fileSystemType();
return;
}
}
bool songs_missing_fingerprint = false;
bool songs_missing_loudness_characteristics = false;
@@ -556,32 +588,40 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
if (stop_or_abort_requested()) return;
QString child(it.next());
QFileInfo child_info(child);
const QString child_filepath = it.next();
const QFileInfo child_fileinfo(child_filepath);
if (child_info.isDir()) {
if (!t->HasSeenSubdir(child)) {
if (child_fileinfo.isSymLink()) {
QStorageInfo storage_info(child_fileinfo.symLinkTarget());
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
qLog(Warning) << "Ignoring symbolic link" << child_filepath << "which links to" << child_fileinfo.symLinkTarget() << "with rejected filesystem type" << storage_info.fileSystemType();
continue;
}
}
if (child_fileinfo.isDir()) {
if (!t->HasSeenSubdir(child_filepath)) {
// We haven't seen this subdirectory before - add it to a list, and later we'll tell the backend about it and scan it.
CollectionSubdirectory new_subdir;
new_subdir.directory_id = -1;
new_subdir.path = child;
new_subdir.mtime = child_info.lastModified().toSecsSinceEpoch();
new_subdir.path = child_filepath;
new_subdir.mtime = child_fileinfo.lastModified().toSecsSinceEpoch();
my_new_subdirs << new_subdir;
}
t->AddToProgress(1);
}
else {
QString ext_part(ExtensionPart(child));
QString dir_part(DirectoryPart(child));
if (Song::kRejectedExtensions.contains(child_info.suffix(), Qt::CaseInsensitive) || child_info.baseName() == "qt_temp"_L1) {
QString ext_part(ExtensionPart(child_filepath));
QString dir_part(DirectoryPart(child_filepath));
if (Song::kRejectedExtensions.contains(child_fileinfo.suffix(), Qt::CaseInsensitive) || child_fileinfo.baseName() == "qt_temp"_L1) {
t->AddToProgress(1);
}
else if (sValidImages.contains(ext_part)) {
album_art[dir_part] << child;
album_art[dir_part] << child_filepath;
t->AddToProgress(1);
}
else if (tagreader_client_->IsMediaFileBlocking(child)) {
files_on_disk << child;
else if (tagreader_client_->IsMediaFileBlocking(child_filepath)) {
files_on_disk << child_filepath;
}
else {
t->AddToProgress(1);
@@ -818,7 +858,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
QHash<quint64, Song> sections_map;
for (const Song &song : old_cue_songs) {
sections_map.insert(song.beginning_nanosec(), song);
sections_map.insert(static_cast<quint64>(song.beginning_nanosec()), song);
}
// Load new CUE songs
@@ -828,7 +868,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
qLog(Error) << "Could not open CUE file" << matching_cue << "for reading:" << cue_file.errorString();
return;
}
const SongList songs = cue_parser_->Load(&cue_file, matching_cue, path, false);
const SongList songs = cue_parser_->Load(&cue_file, matching_cue, path, false).songs;
cue_file.close();
// Update every song that's in the CUE and collection
@@ -839,8 +879,8 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
PerformEBUR128Analysis(new_cue_song);
new_cue_song.set_fingerprint(fingerprint);
if (sections_map.contains(new_cue_song.beginning_nanosec())) { // Changed section
const Song matching_cue_song = sections_map[new_cue_song.beginning_nanosec()];
if (sections_map.contains(static_cast<quint64>(new_cue_song.beginning_nanosec()))) { // Changed section
const Song matching_cue_song = sections_map[static_cast<quint64>(new_cue_song.beginning_nanosec())];
new_cue_song.set_id(matching_cue_song.id());
new_cue_song.set_art_automatic(art_automatic);
new_cue_song.MergeUserSetData(matching_cue_song, true, true);
@@ -915,7 +955,7 @@ SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path
// Also, watch out for incorrect media files.
// Playlist parser for CUEs considers every entry in sheet valid, and we don't want invalid media getting into collection!
QString file_nfd = file.normalized(QString::NormalizationForm_D);
SongList cue_songs = cue_parser_->Load(&cue_file, matching_cue, path, false);
SongList cue_songs = cue_parser_->Load(&cue_file, matching_cue, path, false).songs;
cue_file.close();
songs.reserve(cue_songs.count());
for (Song &cue_song : cue_songs) {
@@ -959,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;
@@ -994,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.";
@@ -1042,7 +1097,7 @@ quint64 CollectionWatcher::GetMtimeForCue(const QString &cue_path) {
const QDateTime cue_last_modified = fileinfo.lastModified();
return cue_last_modified.isValid() ? cue_last_modified.toSecsSinceEpoch() : 0;
return cue_last_modified.isValid() ? static_cast<quint64>(cue_last_modified.toSecsSinceEpoch()) : 0;
}
@@ -1153,7 +1208,7 @@ void CollectionWatcher::RescanPathsNow() {
QMap<QString, quint64> subdir_files_count;
for (const QString &path : paths) {
quint64 files_count = FilesCountForPath(&transaction, path);
const quint64 files_count = FilesCountForPath(&transaction, path);
subdir_files_count[path] = files_count;
transaction.AddToProgressMax(files_count);
}
@@ -1316,18 +1371,45 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt
quint64 CollectionWatcher::FilesCountForPath(ScanTransaction *t, const QString &path) {
const QFileInfo path_info(path);
if (path_info.isSymLink()) {
const QString real_path = path_info.symLinkTarget();
const QStorageInfo storage_info(real_path);
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
return 0;
}
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
if (real_path.startsWith(dir.path)) {
return 0;
}
}
}
else {
const QStorageInfo storage_info(path);
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
return 0;
}
}
quint64 i = 0;
QDirIterator it(path, QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
while (it.hasNext()) {
if (stop_or_abort_requested()) break;
QString child = it.next();
QFileInfo path_info(child);
const QString child_filepath = it.next();
const QFileInfo child_fileinfo(child_filepath);
if (child_fileinfo.isDir()) {
if (child_fileinfo.isSymLink()) {
const QString real_path = child_fileinfo.symLinkTarget();
QStorageInfo storage_info(real_path);
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
continue;
}
if (path_info.isDir()) {
if (path_info.isSymLink()) {
QString real_path = path_info.symLinkTarget();
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
if (real_path.startsWith(dir.path)) {
continue;
@@ -1335,9 +1417,9 @@ quint64 CollectionWatcher::FilesCountForPath(ScanTransaction *t, const QString &
}
}
if (!t->HasSeenSubdir(child) && !path_info.isHidden()) {
if (!t->HasSeenSubdir(child_filepath) && !child_fileinfo.isHidden()) {
// We haven't seen this subdirectory before, so we need to include the file count for this directory too.
i += FilesCountForPath(t, child);
i += FilesCountForPath(t, child_filepath);
}
}
@@ -1385,7 +1467,7 @@ void CollectionWatcher::RescanSongs(const SongList &songs) {
if (stop_or_abort_requested()) break;
if (subdir.path != song_path) continue;
qLog(Debug) << "Rescan for directory ID" << song.directory_id() << "directory" << subdir.path;
quint64 files_count = FilesCountForPath(&transaction, subdir.path);
const quint64 files_count = FilesCountForPath(&transaction, subdir.path);
ScanSubdirectory(song_path, subdir, files_count, &transaction);
scanned_paths << subdir.path;
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-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

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

@@ -18,6 +18,7 @@
#cmakedefine HAVE_AUDIOCD
#cmakedefine HAVE_MTP
#cmakedefine HAVE_GPOD
#cmakedefine HAVE_SPARKLE
#cmakedefine HAVE_QTSPARKLE
#cmakedefine HAVE_SONGFINGERPRINTING
#cmakedefine HAVE_MUSICBRAINZ
@@ -26,10 +27,12 @@
#cmakedefine HAVE_GLOBALSHORTCUTS
#cmakedefine HAVE_X11_GLOBALSHORTCUTS
#cmakedefine HAVE_KGLOBALACCEL_GLOBALSHORTCUTS
#cmakedefine HAVE_STREAMTAGREADER
#cmakedefine HAVE_SUBSONIC
#cmakedefine HAVE_TIDAL
#cmakedefine HAVE_SPOTIFY
#cmakedefine HAVE_QOBUZ
#cmakedefine HAVE_DISCORD_RPC
#cmakedefine HAVE_TAGLIB_DSFFILE
#cmakedefine HAVE_TAGLIB_DSDIFFFILE
@@ -40,8 +43,8 @@
#cmakedefine INSTALL_TRANSLATIONS
#define TRANSLATIONS_DIR "${CMAKE_INSTALL_PREFIX}/share/strawberry/translations"
#cmakedefine HAVE_QPA_QPLATFORMNATIVEINTERFACE
#cmakedefine HAVE_QX11APPLICATION
#cmakedefine HAVE_QPA_QPLATFORMNATIVEINTERFACE_H
#cmakedefine ENABLE_WIN32_CONSOLE

View File

@@ -26,10 +26,14 @@ namespace BackendSettings {
constexpr char kSettingsGroup[] = "Backend";
constexpr char kEngine[] = "Engine";
constexpr char kOutput[] = "Output";
constexpr char kDevice[] = "Device";
constexpr char kEngine[] = "engine";
constexpr char kEngineU[] = "Engine";
constexpr char kOutput[] = "output";
constexpr char kOutputU[] = "Output";
constexpr char kDevice[] = "device";
constexpr char kDeviceU[] = "Device";
constexpr char kALSAPlugin[] = "alsaplugin";
constexpr char kPlaybin3[] = "playbin3";
constexpr char kExclusiveMode[] = "exclusive_mode";
constexpr char kVolumeControl[] = "volume_control";
constexpr char kChannelsEnabled[] = "channels_enabled";

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

@@ -0,0 +1,31 @@
/*
* Strawberry Music Player
* Copyright 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef FILESYSTEMCONSTANTS_H
#define FILESYSTEMCONSTANTS_H
#include <QByteArrayList>
const QByteArrayList kRejectedFileSystems = QByteArrayList() << "proc"
<< "sysfs"
<< "tmpfs"
<< "devtmpfs";
#endif // FILESYSTEMCONSTANTS_H

View File

@@ -65,4 +65,20 @@ constexpr QRgb kPresetRed = qRgb(202, 22, 16);
} // namespace
namespace DiscordRPCSettings {
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

@@ -37,6 +37,7 @@ constexpr char kPassword[] = "password";
constexpr char kHTTP2[] = "http2";
constexpr char kVerifyCertificate[] = "verifycertificate";
constexpr char kDownloadAlbumCovers[] = "downloadalbumcovers";
constexpr char kUseAlbumIdForAlbumCovers[] = "usealbumidforalbumcovers";
constexpr char kServerSideScrobbling[] = "serversidescrobbling";
constexpr char kAuthMethod[] = "authmethod";

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

@@ -3,7 +3,7 @@
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.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
@@ -36,6 +36,7 @@
#include <QMetaObject>
#include <QCoreApplication>
#include <QAbstractEventDispatcher>
#include <QTimer>
#include "core/logging.h"
@@ -208,7 +209,7 @@ class ApplicationImpl {
scrobbler->AddService(make_shared<LibreFMScrobbler>(scrobbler->settings(), app->network()));
scrobbler->AddService(make_shared<ListenBrainzScrobbler>(scrobbler->settings(), app->network()));
#ifdef HAVE_SUBSONIC
scrobbler->AddService(make_shared<SubsonicScrobbler>(scrobbler->settings(), app->streaming_services()->Service<SubsonicService>(), app));
scrobbler->AddService(make_shared<SubsonicScrobbler>(scrobbler->settings(), app->network(), app->streaming_services()->Service<SubsonicService>(), app));
#endif
return scrobbler;
}),
@@ -250,7 +251,7 @@ Application::Application(QObject *parent)
p_(new ApplicationImpl(this)),
g_thread_(nullptr) {
setObjectName(QLatin1String(metaObject()->className()));
setObjectName(QLatin1String(QObject::metaObject()->className()));
const QMetaObject *mo = QAbstractEventDispatcher::instance(QCoreApplication::instance()->thread())->metaObject();
if (mo && strcmp(mo->className(), "QEventDispatcherGlib") != 0 && strcmp(mo->superClass()->className(), "QEventDispatcherGlib") != 0) {

View File

@@ -39,10 +39,10 @@
#include <QSqlDriver>
#include <QSqlDatabase>
#include <QSqlError>
#include <QStandardPaths>
#include <QScopeGuard>
#include "core/logging.h"
#include "logging.h"
#include "standardpaths.h"
#include "taskmanager.h"
#include "database.h"
#include "sqlquery.h"
@@ -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";
@@ -69,7 +69,7 @@ Database::Database(SharedPtr<TaskManager> task_manager, QObject *parent, const Q
startup_schema_version_(-1),
original_thread_(nullptr) {
setObjectName(QLatin1String(metaObject()->className()));
setObjectName(QLatin1String(QObject::metaObject()->className()));
original_thread_ = thread();
@@ -78,7 +78,7 @@ Database::Database(SharedPtr<TaskManager> task_manager, QObject *parent, const Q
connection_id_ = sNextConnectionId++;
}
directory_ = QDir::toNativeSeparators(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
directory_ = QDir::toNativeSeparators(StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation)).replace(u"Strawberry"_s, u"strawberry"_s);
QMutexLocker l(&mutex_);
Connect();
@@ -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);
@@ -598,7 +593,7 @@ void Database::BackupFile(const QString &filename) {
do {
ret = sqlite3_backup_step(backup, 16);
const int page_count = sqlite3_backup_pagecount(backup);
task_manager_->SetTaskProgress(task_id, page_count - sqlite3_backup_remaining(backup), page_count);
task_manager_->SetTaskProgress(task_id, static_cast<quint64>(page_count - sqlite3_backup_remaining(backup)), static_cast<quint64>(page_count));
}
while (ret == SQLITE_OK);

View File

@@ -92,7 +92,7 @@ void DeleteFiles::ProcessSomeFiles() {
// None left?
if (progress_ >= songs_.count()) {
task_manager_->SetTaskProgress(task_id_, progress_, songs_.count());
task_manager_->SetTaskProgress(task_id_, static_cast<quint64>(progress_), static_cast<quint64>(songs_.count()));
QString error_text;
storage_->FinishCopy(songs_with_errors_.isEmpty(), error_text);
@@ -114,7 +114,7 @@ void DeleteFiles::ProcessSomeFiles() {
const qint64 n = qMin(static_cast<qint64>(songs_.count()), static_cast<qint64>(progress_ + kBatchSize));
for (; progress_ < n; ++progress_) {
task_manager_->SetTaskProgress(task_id_, progress_, songs_.count());
task_manager_->SetTaskProgress(task_id_, static_cast<quint64>(progress_), static_cast<quint64>(songs_.count()));
const Song song = songs_.value(progress_);

View File

@@ -113,7 +113,11 @@ bool FilesystemMusicStorage::DeleteFromStorage(const DeleteJob &job) {
QString path = job.metadata_.url().toLocalFile();
QFileInfo fileInfo(path);
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
if (job.use_trash_ && QFile::supportsMoveToTrash()) {
#else
if (job.use_trash_) {
#endif
return QFile::moveToTrash(path);
}

View File

@@ -0,0 +1,185 @@
/*
* Strawberry Music Player
* Copyright 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QSslError>
#include <QJsonDocument>
#include <QJsonObject>
#include "core/logging.h"
#include "networkaccessmanager.h"
#include "httpbaserequest.h"
using namespace Qt::Literals::StringLiterals;
HttpBaseRequest::HttpBaseRequest(const SharedPtr<NetworkAccessManager> network, QObject *parent)
: QObject(parent),
network_(network) {}
HttpBaseRequest::~HttpBaseRequest() {
if (!replies_.isEmpty()) {
qLog(Debug) << "Aborting" << replies_.count() << "network replies";
while (!replies_.isEmpty()) {
QNetworkReply *reply = replies_.takeFirst();
QObject::disconnect(reply, nullptr, this, nullptr);
reply->abort();
reply->deleteLater();
}
}
}
QNetworkReply *HttpBaseRequest::CreateGetRequest(const QUrl &url, const bool fake_user_agent_header) {
return CreateGetRequest(url, QUrlQuery(), fake_user_agent_header);
}
QNetworkReply *HttpBaseRequest::CreateGetRequest(const QUrl &url, const ParamList &params, const bool fake_user_agent_header) {
QUrlQuery url_query;
if (!params.isEmpty()) {
ParamList sorted_params = params;
std::sort(sorted_params.begin(), sorted_params.end());
for (const Param &param : sorted_params) {
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
}
}
return CreateGetRequest(url, url_query, fake_user_agent_header);
}
QNetworkReply *HttpBaseRequest::CreateGetRequest(const QUrl &url, const QUrlQuery &url_query, const bool fake_user_agent_header) {
QUrl request_url(url);
if (!url_query.isEmpty()) {
request_url.setQuery(url_query);
}
QNetworkRequest network_request(request_url);
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
if (use_authorization_header() && authenticated()) {
network_request.setRawHeader("Authorization", authorization_header());
}
if (fake_user_agent_header) {
network_request.setHeader(QNetworkRequest::UserAgentHeader, u"Mozilla/5.0 (X11; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0"_s);
}
QNetworkReply *reply = network_->get(network_request);
QObject::connect(reply, &QNetworkReply::sslErrors, this, &HttpBaseRequest::HandleSSLErrors);
replies_ << reply;
//qLog(Debug) << service_name() << "Sending get request" << request_url;
return reply;
}
QNetworkReply *HttpBaseRequest::CreatePostRequest(const QUrl &url, const QByteArray &content_type_header, const QByteArray &data) {
QNetworkRequest network_request(url);
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
network_request.setHeader(QNetworkRequest::ContentTypeHeader, content_type_header);
if (use_authorization_header() && authenticated()) {
network_request.setRawHeader("Authorization", authorization_header());
}
QNetworkReply *reply = network_->post(network_request, data);
QObject::connect(reply, &QNetworkReply::sslErrors, this, &HttpBaseRequest::HandleSSLErrors);
replies_ << reply;
//qLog(Debug) << service_name() << "Sending post request" << url << data;
return reply;
}
QNetworkReply *HttpBaseRequest::CreatePostRequest(const QUrl &url, const QUrlQuery &url_query) {
return CreatePostRequest(url, "application/x-www-form-urlencoded", url_query.toString(QUrl::FullyEncoded).toUtf8());
}
QNetworkReply *HttpBaseRequest::CreatePostRequest(const QUrl &url, const ParamList &params) {
QUrlQuery url_query;
for (const Param &param : std::as_const(params)) {
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
}
return CreatePostRequest(url, url_query);
}
QNetworkReply *HttpBaseRequest::CreatePostRequest(const QUrl &url, const QJsonDocument &json_document) {
return CreatePostRequest(url, "application/json; charset=utf-8", json_document.toJson());
}
QNetworkReply *HttpBaseRequest::CreatePostRequest(const QUrl &url, const QJsonObject &json_object) {
return CreatePostRequest(url, QJsonDocument(json_object));
}
void HttpBaseRequest::HandleSSLErrors(const QList<QSslError> &ssl_errors) {
for (const QSslError &ssl_error : ssl_errors) {
Error(ssl_error.errorString());
}
}
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));
}
}
return reply->readAll();
}
void HttpBaseRequest::Error(const QString &error_message, const QVariant &debug_output) {
qLog(Error) << service_name() << error_message;
if (debug_output.isValid()) {
qLog(Debug) << debug_output;
}
}

110
src/core/httpbaserequest.h Normal file
View File

@@ -0,0 +1,110 @@
/*
* Strawberry Music Player
* Copyright 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef HTTPBASEREQUEST_H
#define HTTPBASEREQUEST_H
#include <QObject>
#include <QList>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QSslError>
#include <QJsonDocument>
#include <QJsonObject>
#include "includes/shared_ptr.h"
class NetworkAccessManager;
class HttpBaseRequest : public QObject {
Q_OBJECT
public:
explicit HttpBaseRequest(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
~HttpBaseRequest() override;
using Param = QPair<QString, QString>;
using ParamList = QList<Param>;
enum class ErrorCode {
Success,
NetworkError,
HttpError,
APIError,
ParseError,
};
class HttpBaseRequestResult {
public:
HttpBaseRequestResult(const ErrorCode _error_code, const QString &_error_message = QString())
: error_code(_error_code),
network_error(QNetworkReply::NetworkError::UnknownNetworkError),
http_status_code(200),
api_error(-1),
error_message(_error_message) {}
ErrorCode error_code;
QNetworkReply::NetworkError network_error;
int http_status_code;
int api_error;
QString error_message;
bool success() const { return error_code == ErrorCode::Success; }
};
class ReplyDataResult : public HttpBaseRequestResult {
public:
ReplyDataResult(const ErrorCode _error_code, const QString &_error_message = QString()) : HttpBaseRequestResult(_error_code, _error_message) {}
ReplyDataResult(const QByteArray &_data) : HttpBaseRequestResult(ErrorCode::Success), data(_data) {}
QByteArray data;
};
static ReplyDataResult GetReplyData(QNetworkReply *reply);
protected:
virtual QString service_name() const = 0;
virtual bool authentication_required() const = 0;
virtual bool authenticated() const = 0;
virtual bool use_authorization_header() const = 0;
virtual QByteArray authorization_header() const = 0;
virtual QNetworkReply *CreateGetRequest(const QUrl &url, const bool fake_user_agent_header);
virtual QNetworkReply *CreateGetRequest(const QUrl &url, const ParamList &params = ParamList(), const bool fake_user_agent_header = false);
virtual QNetworkReply *CreateGetRequest(const QUrl &url, const QUrlQuery &url_query, const bool fake_user_agent_header = false);
virtual QNetworkReply *CreatePostRequest(const QUrl &url, const QByteArray &content_type_header, const QByteArray &data);
virtual QNetworkReply *CreatePostRequest(const QUrl &url, const QUrlQuery &url_query);
virtual QNetworkReply *CreatePostRequest(const QUrl &url, const ParamList &params);
virtual QNetworkReply *CreatePostRequest(const QUrl &url, const QJsonDocument &json_document);
virtual QNetworkReply *CreatePostRequest(const QUrl &url, const QJsonObject &json_object);
virtual void Error(const QString &error_message, const QVariant &debug_output = QVariant());
public Q_SLOTS:
void HandleSSLErrors(const QList<QSslError> &ssl_errors);
Q_SIGNALS:
void ShowErrorDialog(const QString &error);
protected:
const SharedPtr<NetworkAccessManager> network_;
QList<QNetworkReply*> replies_;
};
#endif // HTTPBASEREQUEST_H

View File

@@ -27,10 +27,10 @@
#include <QString>
#include <QIcon>
#include <QSize>
#include <QStandardPaths>
#include <QSettings>
#include "logging.h"
#include "standardpaths.h"
#include "settings.h"
#include "includes/iconmapper.h"
#include "iconloader.h"
@@ -43,7 +43,7 @@ bool IconLoader::custom_icons_ = false;
void IconLoader::Init() {
#if !defined(Q_OS_MACOS) && !defined(Q_OS_WIN)
#if !defined(Q_OS_MACOS) && !defined(Q_OS_WIN32)
Settings s;
s.beginGroup(AppearanceSettings::kSettingsGroup);
system_icons_ = s.value("system_icons", false).toBool();
@@ -51,7 +51,7 @@ void IconLoader::Init() {
#endif
QDir dir;
if (dir.exists(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/icons"_s)) {
if (dir.exists(StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/icons"_s)) {
custom_icons_ = true;
}
@@ -125,7 +125,7 @@ QIcon IconLoader::Load(const QString &name, const bool system_icon, const int fi
}
if (custom_icons_) {
QString custom_icon_path = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/icons/%1x%2/%3.png"_s;
QString custom_icon_path = StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/icons/%1x%2/%3.png"_s;
for (int s : std::as_const(sizes)) {
QString filename(custom_icon_path.arg(s).arg(s).arg(name));
if (QFile::exists(filename)) ret.addFile(filename, QSize(s, s));

View File

@@ -0,0 +1,103 @@
/*
* Strawberry Music Player
* Copyright 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QByteArray>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonValue>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonParseError>
#include "networkaccessmanager.h"
#include "jsonbaserequest.h"
using namespace Qt::Literals::StringLiterals;
JsonBaseRequest::JsonBaseRequest(const SharedPtr<NetworkAccessManager> network, QObject *parent)
: HttpBaseRequest(network, parent) {}
JsonBaseRequest::JsonObjectResult JsonBaseRequest::GetJsonObject(const QByteArray &data) {
if (data.isEmpty()) {
return JsonObjectResult(ErrorCode::ParseError, "Empty data from server"_L1);
}
QJsonParseError json_error;
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_error);
if (json_error.error != QJsonParseError::NoError) {
return JsonObjectResult(ErrorCode::ParseError, json_error.errorString());
}
if (json_document.isEmpty()) {
return JsonObjectResult(ErrorCode::ParseError, "Received empty Json document."_L1);
}
if (!json_document.isObject()) {
return JsonObjectResult(ErrorCode::ParseError, "Json document is not an object."_L1);
}
const QJsonObject json_object = json_document.object();
if (json_object.isEmpty()) {
return JsonObjectResult(ErrorCode::ParseError, "Received empty Json object."_L1);
}
return json_object;
}
JsonBaseRequest::JsonValueResult JsonBaseRequest::GetJsonValue(const QJsonObject &json_object, const QString &name) {
if (!json_object.contains(name)) {
return JsonValueResult(ErrorCode::ParseError, QStringLiteral("Json object is missing value %1.").arg(name));
}
return json_object[name];
}
JsonBaseRequest::JsonObjectResult JsonBaseRequest::GetJsonObject(const QJsonObject &json_object, const QString &name) {
if (!json_object.contains(name)) {
return JsonValueResult(ErrorCode::ParseError, QStringLiteral("Json object is missing object %1.").arg(name));
}
const QJsonValue json_value = json_object[name];
if (!json_value.isObject()) {
return JsonValueResult(ErrorCode::ParseError, QStringLiteral("Json value %1 is not a object.").arg(name));
}
return json_value.toObject();
}
JsonBaseRequest::JsonArrayResult JsonBaseRequest::GetJsonArray(const QJsonObject &json_object, const QString &name) {
const JsonValueResult json_value_result = GetJsonValue(json_object, name);
if (!json_value_result.success()) {
return JsonArrayResult(ErrorCode::ParseError, json_value_result.error_message);
}
if (!json_value_result.json_value.isArray()) {
return JsonArrayResult(ErrorCode::ParseError, QStringLiteral("Json object value %1 is not a array.").arg(name));
}
return json_value_result.json_value.toArray();
}

View File

@@ -0,0 +1,68 @@
/*
* Strawberry Music Player
* Copyright 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef JSONBASEREQUEST_H
#define JSONBASEREQUEST_H
#include <QByteArray>
#include <QJsonObject>
#include <QJsonArray>
#include "includes/shared_ptr.h"
#include "httpbaserequest.h"
class NetworkAccessManager;
class JsonBaseRequest : public HttpBaseRequest {
Q_OBJECT
public:
explicit JsonBaseRequest(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
class JsonValueResult : public ReplyDataResult {
public:
JsonValueResult(const ReplyDataResult &reply_data_result) : ReplyDataResult(reply_data_result.error_code, reply_data_result.error_message) {}
JsonValueResult(const ErrorCode _error_code, const QString &_error_message = QString()) : ReplyDataResult(_error_code, _error_message) {}
JsonValueResult(const QJsonValue &_json_value) : ReplyDataResult(ErrorCode::Success), json_value(_json_value) {}
QJsonValue json_value;
};
class JsonObjectResult : public ReplyDataResult {
public:
JsonObjectResult(const ReplyDataResult &reply_data_result) : ReplyDataResult(reply_data_result.error_code, reply_data_result.error_message) {}
JsonObjectResult(const ErrorCode _error_code, const QString &_error_message = QString()) : ReplyDataResult(_error_code, _error_message) {}
JsonObjectResult(const QJsonObject &_json_object) : ReplyDataResult(ErrorCode::Success), json_object(_json_object) {}
QJsonObject json_object;
};
class JsonArrayResult : public ReplyDataResult {
public:
JsonArrayResult(const ReplyDataResult &reply_data_result) : ReplyDataResult(reply_data_result.error_code, reply_data_result.error_message) {}
JsonArrayResult(const ErrorCode _error_code, const QString &_error_message = QString()) : ReplyDataResult(_error_code, _error_message) {}
JsonArrayResult(const QJsonArray &_json_array) : ReplyDataResult(ErrorCode::Success), json_array(_json_array) {}
QJsonArray json_array;
};
static JsonObjectResult GetJsonObject(const QByteArray &data);
static JsonValueResult GetJsonValue(const QJsonObject &json_object, const QString &name);
static JsonObjectResult GetJsonObject(const QJsonObject &json_object, const QString &name);
static JsonArrayResult GetJsonArray(const QJsonObject &json_object, const QString &name);
};
#endif // JSONBASEREQUEST_H

View File

@@ -1,8 +1,7 @@
/*
* This file was part of Clementine.
* Strawberry Music Player
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.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
@@ -44,7 +43,8 @@ using namespace Qt::Literals::StringLiterals;
LocalRedirectServer::LocalRedirectServer(QObject *parent)
: QTcpServer(parent),
port_(0),
socket_(nullptr) {}
socket_(nullptr),
success_(false) {}
LocalRedirectServer::~LocalRedirectServer() {
if (isListening()) close();
@@ -52,7 +52,8 @@ LocalRedirectServer::~LocalRedirectServer() {
bool LocalRedirectServer::Listen() {
if (!listen(QHostAddress::LocalHost, port_)) {
if (!listen(QHostAddress::LocalHost, static_cast<quint64>(port_))) {
success_ = false;
error_ = errorString();
return false;
}
@@ -61,6 +62,7 @@ bool LocalRedirectServer::Listen() {
url_.setHost(u"localhost"_s);
url_.setPort(serverPort());
url_.setPath(u"/"_s);
port_ = serverPort();
QObject::connect(this, &QTcpServer::newConnection, this, &LocalRedirectServer::NewConnection);
return true;
@@ -77,7 +79,7 @@ void LocalRedirectServer::NewConnection() {
void LocalRedirectServer::incomingConnection(qintptr socket_descriptor) {
if (socket_) {
if (socket_ != nullptr) {
if (socket_->state() == QAbstractSocket::ConnectedState) socket_->close();
socket_->deleteLater();
socket_ = nullptr;
@@ -88,6 +90,7 @@ void LocalRedirectServer::incomingConnection(qintptr socket_descriptor) {
if (!tcp_socket->setSocketDescriptor(socket_descriptor)) {
delete tcp_socket;
close();
success_ = false;
error_ = "Unable to set socket descriptor"_L1;
Q_EMIT Finished();
return;
@@ -115,6 +118,10 @@ void LocalRedirectServer::ReadyRead() {
socket_->deleteLater();
socket_ = nullptr;
request_url_ = ParseUrlFromRequest(buffer_);
success_ = request_url_.isValid();
if (!request_url_.isValid()) {
error_ = "Invalid request URL"_L1;
}
close();
Q_EMIT Finished();
}
@@ -169,9 +176,9 @@ QUrl LocalRedirectServer::ParseUrlFromRequest(const QByteArray &request) const {
const QByteArrayList lines = request.split('\r');
const QByteArray &request_line = lines[0];
QByteArray path = request_line.split(' ')[1];
QUrl base_url = url_;
QUrl request_url(base_url.toString() + QString::fromLatin1(path.mid(1)), QUrl::StrictMode);
const QByteArray path = request_line.split(' ')[1];
const QUrl base_url = url_;
const QUrl request_url(base_url.toString() + QString::fromLatin1(path.mid(1)), QUrl::StrictMode);
return request_url;

View File

@@ -1,8 +1,7 @@
/*
* This file was part of Clementine.
* Strawberry Music Player
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.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
@@ -39,12 +38,16 @@ class LocalRedirectServer : public QTcpServer {
explicit LocalRedirectServer(QObject *parent = nullptr);
~LocalRedirectServer() override;
void set_port(const int port) { port_ = port; }
bool Listen();
const QUrl &url() const { return url_; }
const QUrl &request_url() const { return request_url_; }
bool success() const { return success_; }
const QString &error() const { return error_; }
int port() const { return port_; }
void set_port(const int port) { port_ = port; }
bool Listen();
Q_SIGNALS:
void Finished();
@@ -66,6 +69,7 @@ class LocalRedirectServer : public QTcpServer {
QUrl request_url_;
QAbstractSocket *socket_;
QByteArray buffer_;
bool success_;
QString error_;
};

View File

@@ -156,10 +156,8 @@
#include "lyrics/lyricsproviders.h"
#include "device/devicemanager.h"
#include "device/devicestatefiltermodel.h"
#ifndef Q_OS_WIN
# 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
@@ -208,7 +207,7 @@
#include "organize/organizeerrordialog.h"
#ifdef Q_OS_WIN
#ifdef Q_OS_WIN32
# include "core/windows7thumbbar.h"
#endif
@@ -220,10 +219,18 @@
# include "systemtrayicon/qtsystemtrayicon.h"
#endif
#ifdef HAVE_SPARKLE
#include "core/sparkleupdater.h"
#endif
#ifdef HAVE_QTSPARKLE
# include <qtsparkle-qt6/Updater>
#endif // HAVE_QTSPARKLE
#ifdef HAVE_DISCORD_RPC
#include "discord/richpresence.h"
#endif
using std::make_unique;
using std::make_shared;
using namespace std::chrono_literals;
@@ -235,30 +242,60 @@ const int kTrackPositionUpdateTimeMs = 1000;
} // namespace
#ifdef HAVE_QTSPARKLE
# ifdef _MSC_VER
# ifdef _M_X64
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-windows-msvc-x64";
# else
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-windows-msvc-x86";
# endif
# else
# ifdef __x86_64__
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-windows-mingw-x64";
# else
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-windows-mingw-x86";
# endif
# endif
#endif
namespace {
MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OSDBase *osd, const CommandlineOptions &options, QWidget *parent)
# if defined(__APPLE__)
# if defined(__x86_64__)
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-macos-x86_64";
# elif defined(__aarch64__)
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-macos-arm64";
# else
# error "Unsupported macOS arch for QtSparkle"
# endif
# elif defined(__MINGW32__)
# if defined(__x86_64__)
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-windows-mingw-x64";
# elif defined(__i686__)
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-windows-mingw-x86";
# else
# error "Unsupported MinGW arch for QtSparkle"
# endif
# elif defined(_MSC_VER)
# if defined(_WIN64)
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-windows-msvc-x64";
# elif defined(_WIN32)
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-windows-msvc-x86";
# else
# error "Unsupported MSVC arch for QtSparkle"
# endif
# else
# error "Unsupported OS for QtSparkle"
# endif // OS
} // namespace
#endif // HAVE_QTSPARKLE
MainWindow::MainWindow(Application *app,
SharedPtr<SystemTrayIcon> systemtrayicon, OSDBase *osd,
#ifdef HAVE_DISCORD_RPC
discord::RichPresence *discord_rich_presence,
#endif
const CommandlineOptions &options,
QWidget *parent)
: QMainWindow(parent),
ui_(new Ui_MainWindow),
#ifdef Q_OS_WIN
#ifdef Q_OS_WIN32
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),
#endif
console_([app, this]() {
Console *console = new Console(app->database());
QObject::connect(console, &Console::Error, this, &MainWindow::ShowErrorDialog);
@@ -272,9 +309,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
context_view_(new ContextView(this)),
collection_view_(new CollectionViewContainer(this)),
file_view_(new FileView(this)),
#ifndef Q_OS_WIN
device_view_(new DeviceViewContainer(this)),
#endif
playlist_list_(new PlaylistListContainer(this)),
queue_view_(new QueueView(this)),
settings_dialog_(std::bind(&MainWindow::CreateSettingsDialog, this)),
@@ -337,9 +372,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
playlist_move_to_collection_(nullptr),
playlist_open_in_browser_(nullptr),
playlist_organize_(nullptr),
#ifndef Q_OS_WIN
playlist_copy_to_device_(nullptr),
#endif
playlist_delete_(nullptr),
playlist_queue_(nullptr),
playlist_queue_play_next_(nullptr),
@@ -363,6 +396,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
was_minimized_(false),
exit_(false),
exit_count_(0),
playlists_loaded_(false),
delete_files_(false) {
qLog(Debug) << "Starting";
@@ -370,7 +404,11 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
// 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);
@@ -391,9 +429,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
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_WIN
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
@@ -441,9 +477,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
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_WIN
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());
@@ -515,9 +549,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
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_WIN
QObject::connect(file_view_, &FileView::CopyToDevice, this, &MainWindow::CopyFilesToDevice);
#endif
file_view_->SetTaskManager(app_->task_manager());
// Action connections
@@ -629,7 +661,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
QObject::connect(&*app_->player(), &Player::Playing, playlist_list_, &PlaylistListContainer::ActivePlaying);
QObject::connect(&*app_->player(), &Player::Stopped, playlist_list_, &PlaylistListContainer::ActiveStopped);
QObject::connect(&*app_->playlist_manager(), &PlaylistManager::AllPlaylistsLoaded, &*app->player(), &Player::PlaylistsLoaded);
QObject::connect(&*app_->playlist_manager(), &PlaylistManager::AllPlaylistsLoaded, this, &MainWindow::PlaylistsLoaded);
QObject::connect(&*app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, this, &MainWindow::SongChanged);
QObject::connect(&*app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, &*app_->player(), &Player::CurrentMetadataChanged);
QObject::connect(&*app_->playlist_manager(), &PlaylistManager::EditingFinished, this, &MainWindow::PlaylistEditFinished);
@@ -679,10 +711,8 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
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_WIN
// Devices connections
QObject::connect(device_view_->view(), &DeviceView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
#endif
// Collection filter widget
QActionGroup *collection_view_group = new QActionGroup(this);
@@ -745,6 +775,9 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
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);
@@ -785,9 +818,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
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_WIN
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();
@@ -806,10 +837,8 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
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);
@@ -819,23 +848,23 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
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_WIN
#ifdef Q_OS_WIN32
thumbbar_->SetActions(QList<QAction*>() << ui_->action_previous_track << ui_->action_play_pause << ui_->action_stop << ui_->action_next_track << nullptr << ui_->action_love);
#endif
#if defined(HAVE_QTSPARKLE)
QAction *check_updates = ui_->menu_tools->addAction(tr("Check for updates..."));
check_updates->setMenuRole(QAction::ApplicationSpecificRole);
#if defined(HAVE_SPARKLE) || defined(HAVE_QTSPARKLE)
QAction *action_check_updates = ui_->menu_tools->addAction(tr("Check for updates..."));
action_check_updates->setMenuRole(QAction::ApplicationSpecificRole);
#endif
#ifdef HAVE_GLOBALSHORTCUTS
@@ -941,7 +970,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
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_WIN)
#if !defined(HAVE_AUDIOCD)
ui_->action_open_cd->setEnabled(false);
ui_->action_open_cd->setVisible(false);
#endif
@@ -1004,7 +1033,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
show();
break;
case BehaviourSettings::StartupBehaviour::Hide:
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible()) {
if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible()) {
break;
}
[[fallthrough]];
@@ -1017,7 +1046,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
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;
@@ -1046,13 +1075,18 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
app_->scrobbler()->Submit();
}
#ifdef HAVE_SPARKLE
SparkleUpdater *sparkle_updater = new SparkleUpdater(action_check_updates, this);
QObject::connect(action_check_updates, &QAction::triggered, sparkle_updater, &SparkleUpdater::CheckForUpdates);
#endif
#ifdef HAVE_QTSPARKLE
QUrl sparkle_url(QString::fromLatin1(QTSPARKLE_URL));
if (!sparkle_url.isEmpty()) {
qLog(Debug) << "Creating Qt Sparkle updater";
qtsparkle::Updater *updater = new qtsparkle::Updater(sparkle_url, this);
updater->SetVersion(QStringLiteral(STRAWBERRY_VERSION_PACKAGE));
QObject::connect(check_updates, &QAction::triggered, updater, &qtsparkle::Updater::CheckNow);
QObject::connect(action_check_updates, &QAction::triggered, updater, &qtsparkle::Updater::CheckNow);
}
#endif
@@ -1124,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();
@@ -1155,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) {
@@ -1177,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);
}
}
@@ -1287,6 +1321,9 @@ void MainWindow::ReloadAllSettings() {
qobuz_view_->ReloadSettings();
qobuz_view_->search_view()->ReloadSettings();
#endif
#ifdef HAVE_DISCORD_RPC
discord_rich_presence_->ReloadSettings();
#endif
}
@@ -1330,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
}
@@ -1362,6 +1399,19 @@ void MainWindow::ExitFinished() {
}
void MainWindow::PlaylistsLoaded() {
playlists_loaded_ = true;
if (options_.has_value()) {
CommandlineOptionsReceived(options_.value());
options_.reset();
}
app_->player()->PlaylistsLoaded();
}
void MainWindow::MediaStopped() {
setWindowTitle(u"Strawberry Music Player"_s);
@@ -1375,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();
@@ -1384,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_) {
@@ -1417,7 +1467,7 @@ void MainWindow::MediaPaused() {
track_slider_timer_->start();
}
tray_icon_->SetPaused();
systemtrayicon_->SetPaused();
}
@@ -1438,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();
@@ -1455,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) {
@@ -1476,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_) {
@@ -1486,7 +1536,7 @@ void MainWindow::SongChanged(const Song &song) {
SendNowPlaying();
const bool enable_change_art = song.is_collection_song() && !song.effective_albumartist().isEmpty() && !song.album().isEmpty();
const bool enable_change_art = song.is_local_collection_song() && !song.effective_albumartist().isEmpty() && !song.album().isEmpty();
album_cover_choice_controller_->show_cover_action()->setEnabled(song.has_valid_art() && !song.art_unset());
album_cover_choice_controller_->cover_to_file_action()->setEnabled(song.has_valid_art() && !song.art_unset());
album_cover_choice_controller_->cover_from_file_action()->setEnabled(enable_change_art);
@@ -1502,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));
@@ -1636,6 +1686,11 @@ void MainWindow::StopAfterCurrent() {
void MainWindow::showEvent(QShowEvent *e) {
if (error_dialog_ && error_dialog_->isVisible() && error_dialog_->isMinimized()) {
error_dialog_->raise();
error_dialog_->activateWindow();
}
QMainWindow::showEvent(e);
}
@@ -1654,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();
}
@@ -1665,7 +1720,12 @@ void MainWindow::closeEvent(QCloseEvent *e) {
void MainWindow::SetHiddenInTray(const bool hidden) {
if (hidden && isVisible()) {
close();
if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible() && keep_running_) {
close();
}
else {
showMinimized();
}
}
else if (!hidden && isHidden()) {
if (was_minimized_) {
@@ -1688,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_) {
@@ -1704,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_) {
@@ -1718,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);
}
}
@@ -1840,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
@@ -1919,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;
}
@@ -1962,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_WIN
playlist_copy_to_device_->setVisible(false);
#endif
playlist_organize_->setVisible(false);
playlist_delete_->setVisible(false);
@@ -2027,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);
@@ -2037,9 +2095,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
playlist_move_to_collection_->setVisible(local_songs > 0);
}
#ifndef Q_OS_WIN
playlist_copy_to_device_->setVisible(local_songs > 0);
#endif
playlist_delete_->setVisible(delete_files_ && local_songs > 0);
@@ -2119,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);
}
@@ -2407,6 +2463,11 @@ void MainWindow::CommandlineOptionsReceived(const QByteArray &string_options) {
void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) {
if (!playlists_loaded_) {
options_ = options;
return;
}
switch (options.player_action()) {
case CommandlineOptions::PlayerAction::Play:
if (options.urls().empty()) {
@@ -2526,10 +2587,10 @@ void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) {
}
if (options.seek_to() != -1) {
app_->player()->SeekTo(options.seek_to());
app_->player()->SeekTo(static_cast<quint64>(options.seek_to()));
}
else if (options.seek_by() != 0) {
app_->player()->SeekTo(app_->player()->engine()->position_nanosec() / kNsecPerSec + options.seek_by());
app_->player()->SeekTo(static_cast<quint64>(app_->player()->engine()->position_nanosec() / kNsecPerSec + options.seek_by()));
}
if (options.play_track_at() != -1) app_->player()->PlayAt(options.play_track_at(), false, 0, EngineBase::TrackChangeType::Manual, Playlist::AutoScroll::Maybe, true);
@@ -2676,7 +2737,6 @@ void MainWindow::MoveFilesToCollection(const QList<QUrl> &urls) {
void MainWindow::CopyFilesToDevice(const QList<QUrl> &urls) {
#ifndef Q_OS_WIN
organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
organize_dialog_->SetCopy(true);
if (organize_dialog_->SetUrls(urls)) {
@@ -2686,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
}
@@ -2748,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);
@@ -2764,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) {
@@ -2816,8 +2873,6 @@ void MainWindow::PlaylistSkip() {
void MainWindow::PlaylistCopyToDevice() {
#ifndef Q_OS_WIN
SongList songs;
const QModelIndexList proxy_indexes = ui_->playlist->view()->selectionModel()->selectedRows();
@@ -2842,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) {
@@ -2906,7 +2959,7 @@ void MainWindow::OpenSettingsDialog() {
}
void MainWindow::OpenSettingsDialogAtPage(SettingsDialog::Page page) {
void MainWindow::OpenSettingsDialogAtPage(const SettingsDialog::Page page) {
settings_dialog_->OpenAtPage(page);
}
@@ -2992,7 +3045,7 @@ void MainWindow::Raise() {
}
#ifdef Q_OS_WIN
#ifdef Q_OS_WIN32
bool MainWindow::nativeEvent(const QByteArray &eventType, void *message, qintptr *result) {
if (exit_count_ == 0 && message) {
@@ -3001,7 +3054,7 @@ bool MainWindow::nativeEvent(const QByteArray &eventType, void *message, qintptr
}
return QMainWindow::nativeEvent(eventType, message, result);
}
#endif // Q_OS_WIN
#endif // Q_OS_WIN32
void MainWindow::AutoCompleteTags() {
@@ -3157,7 +3210,7 @@ void MainWindow::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult
Q_EMIT AlbumCoverReady(song, result.album_cover.image);
const bool enable_change_art = song.is_collection_song() && !song.effective_albumartist().isEmpty() && !song.album().isEmpty();
const bool enable_change_art = song.is_local_collection_song() && !song.effective_albumartist().isEmpty() && !song.album().isEmpty();
album_cover_choice_controller_->show_cover_action()->setEnabled(result.success && result.type != AlbumCoverLoaderResult::Type::Unset);
album_cover_choice_controller_->cover_to_file_action()->setEnabled(result.success && result.type != AlbumCoverLoaderResult::Type::Unset);
album_cover_choice_controller_->cover_from_file_action()->setEnabled(enable_change_art);
@@ -3208,7 +3261,7 @@ void MainWindow::LoveButtonVisibilityChanged(const bool value) {
else
ui_->widget_love->hide();
tray_icon_->LoveVisibilityChanged(value);
systemtrayicon_->LoveVisibilityChanged(value);
}
@@ -3231,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);
}
@@ -3246,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

@@ -24,6 +24,8 @@
#include "config.h"
#include <optional>
#include <QtGlobal>
#include <QObject>
#include <QWidget>
@@ -52,6 +54,7 @@
#include "core/platforminterface.h"
#include "core/song.h"
#include "core/settings.h"
#include "core/commandlineoptions.h"
#include "tagreader/tagreaderclient.h"
#include "osd/osdbase.h"
#include "playlist/playlist.h"
@@ -70,9 +73,7 @@ class CollectionViewContainer;
class CollectionFilter;
class AlbumCoverChoiceController;
class CommandlineOptions;
#ifndef Q_OS_WIN
class DeviceViewContainer;
#endif
class EditTagDialog;
class Equalizer;
class ErrorDialog;
@@ -92,18 +93,31 @@ class Ui_MainWindow;
class StreamingSongsView;
class StreamingTabsView;
class SmartPlaylistsViewContainer;
#ifdef Q_OS_WIN
#ifdef Q_OS_WIN32
class Windows7ThumbBar;
#endif
class AddStreamDialog;
class LastFMImportDialog;
class RadioViewContainer;
#ifdef HAVE_DISCORD_RPC
namespace discord {
class RichPresence;
}
#endif
class MainWindow : public QMainWindow, public PlatformInterface {
Q_OBJECT
public:
explicit MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OSDBase *osd, const CommandlineOptions &options, QWidget *parent = nullptr);
explicit MainWindow(Application *app,
SharedPtr<SystemTrayIcon> systemtrayicon,
OSDBase *osd,
#ifdef HAVE_DISCORD_RPC
discord::RichPresence *discord_rich_presence,
#endif
const CommandlineOptions &options,
QWidget *parent = nullptr);
~MainWindow() override;
void SetHiddenInTray(const bool hidden);
@@ -114,7 +128,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void hideEvent(QHideEvent *e) override;
void closeEvent(QCloseEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
#ifdef Q_OS_WIN
#ifdef Q_OS_WIN32
bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override;
#endif
@@ -131,6 +145,8 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void AuthorizationUrlReceived(const QUrl &url);
private Q_SLOTS:
void PlaylistsLoaded();
void FilePathChanged(const QString &path);
void MediaStopped();
@@ -224,7 +240,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
SettingsDialog *CreateSettingsDialog();
EditTagDialog *CreateEditTagDialog();
void OpenSettingsDialog();
void OpenSettingsDialogAtPage(SettingsDialog::Page page);
void OpenSettingsDialogAtPage(const SettingsDialog::Page page);
void TabSwitched();
void ToggleSidebar(const bool checked);
@@ -289,13 +305,16 @@ class MainWindow : public QMainWindow, public PlatformInterface {
private:
Ui_MainWindow *ui_;
#ifdef Q_OS_WIN
#ifdef Q_OS_WIN32
Windows7ThumbBar *thumbbar_;
#endif
Application *app_;
SharedPtr<SystemTrayIcon> tray_icon_;
SharedPtr<SystemTrayIcon> systemtrayicon_;
OSDBase *osd_;
#ifdef HAVE_DISCORD_RPC
discord::RichPresence *discord_rich_presence_;
#endif
Lazy<About> about_dialog_;
Lazy<Console> console_;
Lazy<EditTagDialog> edit_tag_dialog_;
@@ -306,9 +325,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
ContextView *context_view_;
CollectionViewContainer *collection_view_;
FileView *file_view_;
#ifndef Q_OS_WIN
DeviceViewContainer *device_view_;
#endif
PlaylistListContainer *playlist_list_;
QueueView *queue_view_;
@@ -359,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_WIN
QAction *playlist_copy_to_device_;
#endif
QAction *playlist_delete_;
QAction *playlist_queue_;
QAction *playlist_queue_play_next_;
@@ -396,7 +411,9 @@ class MainWindow : public QMainWindow, public PlatformInterface {
AlbumCoverImageResult album_cover_;
bool exit_;
int exit_count_;
bool playlists_loaded_;
bool delete_files_;
std::optional<CommandlineOptions> options_;
};

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

@@ -88,6 +88,4 @@ int MultiSortFilterProxy::Compare(const QVariant &left, const QVariant &right) c
}
}
return 0;
}

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

@@ -86,7 +86,7 @@ void NetworkProxyFactory::ReloadSettings() {
mode_ = static_cast<Mode>(s.value("mode", static_cast<int>(Mode::System)).toInt());
type_ = QNetworkProxy::ProxyType(s.value("type", QNetworkProxy::HttpProxy).toInt());
hostname_ = s.value("hostname").toString();
port_ = s.value("port", 8080).toInt();
port_ = s.value("port", 8080).toULongLong();
use_authentication_ = s.value("use_authentication", false).toBool();
username_ = s.value("username").toString();
password_ = s.value("password").toString();

View File

@@ -57,7 +57,7 @@ class NetworkProxyFactory : public QNetworkProxyFactory {
Mode mode_;
QNetworkProxy::ProxyType type_;
QString hostname_;
int port_;
quint64 port_;
bool use_authentication_;
QString username_;
QString password_;

607
src/core/oauthenticator.cpp Normal file
View File

@@ -0,0 +1,607 @@
/*
* Strawberry Music Player
* Copyright 2022-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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <algorithm>
#include <QObject>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QDateTime>
#include <QTimer>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QCryptographicHash>
#include <QJsonDocument>
#include <QJsonValue>
#include <QJsonObject>
#include <QJsonParseError>
#include <QDesktopServices>
#include <QMessageBox>
#include "constants/timeconstants.h"
#include "utilities/randutils.h"
#include "logging.h"
#include "settings.h"
#include "networkaccessmanager.h"
#include "localredirectserver.h"
#include "oauthenticator.h"
using namespace Qt::Literals::StringLiterals;
using std::make_shared;
using namespace std::chrono_literals;
namespace {
constexpr char kTokenType[] = "token_type";
constexpr char kAccessToken[] = "access_token";
constexpr char kRefreshToken[] = "refresh_token";
constexpr char kExpiresIn[] = "expires_in";
constexpr char kLoginTime[] = "login_time";
constexpr char kUserId[] = "user_id";
constexpr char kCountryCode[] = "country_code";
constexpr int kMaxPortInc = 20;
} // namespace
OAuthenticator::OAuthenticator(const SharedPtr<NetworkAccessManager> network, QObject *parent)
: QObject(parent),
network_(network),
timer_refresh_login_(new QTimer(this)),
type_(Type::Authorization_Code),
use_local_redirect_server_(true),
random_port_(true),
expires_in_(0LL),
login_time_(0LL),
user_id_(0) {
timer_refresh_login_->setSingleShot(true);
QObject::connect(timer_refresh_login_, &QTimer::timeout, this, &OAuthenticator::RerefreshAccessToken);
}
OAuthenticator::~OAuthenticator() {
while (!replies_.isEmpty()) {
QNetworkReply *reply = replies_.takeFirst();
QObject::disconnect(reply, nullptr, this, nullptr);
reply->abort();
reply->deleteLater();
}
}
void OAuthenticator::set_settings_group(const QString &settings_group) {
settings_group_ = settings_group;
}
void OAuthenticator::set_type(const Type type) {
type_ = type;
}
void OAuthenticator::set_authorize_url(const QUrl &authorize_url) {
authorize_url_ = authorize_url;
}
void OAuthenticator::set_redirect_url(const QUrl &redirect_url) {
redirect_url_ = redirect_url;
}
void OAuthenticator::set_access_token_url(const QUrl &access_token_url) {
access_token_url_ = access_token_url;
}
void OAuthenticator::set_client_id(const QString &client_id) {
client_id_ = client_id;
}
void OAuthenticator::set_client_secret(const QString &client_secret) {
client_secret_ = client_secret;
}
void OAuthenticator::set_scope(const QString &scope) {
scope_ = scope;
}
void OAuthenticator::set_use_local_redirect_server(const bool use_local_redirect_server) {
use_local_redirect_server_ = use_local_redirect_server;
}
void OAuthenticator::set_random_port(const bool random_port) {
random_port_ = random_port;
}
QByteArray OAuthenticator::authorization_header() const {
if (token_type_.isEmpty() || access_token_.isEmpty()) {
return QByteArray();
}
return token_type().toUtf8() + " " + access_token().toUtf8();
}
QString OAuthenticator::GrantType() const {
switch (type_) {
case Type::Authorization_Code:
return u"authorization_code"_s;
break;
case Type::Client_Credentials:
return u"client_credentials"_s;
break;
}
return QString();
}
void OAuthenticator::LoadSession() {
Settings s;
s.beginGroup(settings_group_);
token_type_ = s.value(kTokenType).toString();
access_token_ = s.value(kAccessToken).toString();
refresh_token_ = s.value(kRefreshToken).toString();
expires_in_ = s.value(kExpiresIn, 0LL).toLongLong();
login_time_ = s.value(kLoginTime, 0LL).toLongLong();
country_code_ = s.value(kCountryCode).toString();
user_id_ = s.value(kUserId).toULongLong();
s.endGroup();
StartRefreshLoginTimer();
}
void OAuthenticator::ClearSession() {
token_type_.clear();
access_token_.clear();
refresh_token_.clear();
expires_in_ = 0;
login_time_ = 0;
country_code_.clear();
user_id_ = 0;
Settings s;
s.beginGroup(settings_group_);
s.remove(kTokenType);
s.remove(kAccessToken);
s.remove(kRefreshToken);
s.remove(kExpiresIn);
s.remove(kLoginTime);
s.remove(kCountryCode);
s.remove(kUserId);
s.endGroup();
if (timer_refresh_login_->isActive()) {
timer_refresh_login_->stop();
}
}
void OAuthenticator::StartRefreshLoginTimer() {
if (login_time_ > 0 && !refresh_token_.isEmpty() && expires_in_ > 0) {
const qint64 time = std::max(1LL, expires_in_ - (QDateTime::currentSecsSinceEpoch() - login_time_));
qLog(Debug) << settings_group_ << "Refreshing login in" << time << "seconds";
timer_refresh_login_->setInterval(static_cast<int>(time * kMsecPerSec));
if (!timer_refresh_login_->isActive()) {
timer_refresh_login_->start();
}
}
}
void OAuthenticator::Authenticate() {
if (type_ == Type::Client_Credentials) {
RequestAccessToken();
return;
}
QUrl redirect_url(redirect_url_);
if (use_local_redirect_server_) {
local_redirect_server_.reset(new LocalRedirectServer(this));
bool success = false;
if (random_port_) {
success = local_redirect_server_->Listen();
}
else {
const int max_port = redirect_url.port() + kMaxPortInc;
for (int port = redirect_url.port(); port < max_port; ++port) {
local_redirect_server_->set_port(port);
if (local_redirect_server_->Listen()) {
success = true;
break;
}
}
}
if (!success) {
Q_EMIT AuthenticationFinished(false, local_redirect_server_->error());
local_redirect_server_.reset();
return;
}
QObject::connect(&*local_redirect_server_, &LocalRedirectServer::Finished, this, &OAuthenticator::RedirectArrived);
redirect_url.setPort(local_redirect_server_->port());
}
code_verifier_ = Utilities::CryptographicRandomString(44);
code_challenge_ = QString::fromLatin1(QCryptographicHash::hash(code_verifier_.toUtf8(), QCryptographicHash::Sha256).toBase64(QByteArray::Base64UrlEncoding));
if (code_challenge_.lastIndexOf(u'=') == code_challenge_.length() - 1) {
code_challenge_.chop(1);
}
ParamList params = ParamList() << Param(u"response_type"_s, u"code"_s)
<< Param(u"redirect_uri"_s, redirect_url.toString())
<< Param(u"state"_s, code_challenge_)
<< Param(u"code_challenge_method"_s, u"S256"_s)
<< Param(u"code_challenge"_s, code_challenge_);
if (!client_id_.isEmpty()) {
params << Param(u"client_id"_s, client_id_);
}
if (!scope_.isEmpty()) {
params << Param(u"scope"_s, scope_);
}
std::sort(params.begin(), params.end());
QUrlQuery url_query;
for (const Param &param : std::as_const(params)) {
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first, ";")), QString::fromLatin1(QUrl::toPercentEncoding(param.second, ";")));
}
QUrl url(authorize_url_);
url.setQuery(url_query);
const bool success = QDesktopServices::openUrl(url);
if (!success) {
QMessageBox messagebox(QMessageBox::Information, tr("Authentication"), tr("Please open this URL in your browser") + QStringLiteral(":<br /><a href=\"%1\">%1</a>").arg(url.toString()), QMessageBox::Ok);
messagebox.setTextFormat(Qt::RichText);
messagebox.exec();
}
}
void OAuthenticator::RedirectArrived() {
if (local_redirect_server_.isNull()) {
return;
}
if (local_redirect_server_->success()) {
QUrl redirect_url(redirect_url_);
redirect_url.setPort(local_redirect_server_->port());
AuthorizationUrlReceived(local_redirect_server_->request_url(), redirect_url);
}
else {
Q_EMIT AuthenticationFinished(false, local_redirect_server_->error());
}
local_redirect_server_.reset();
}
void OAuthenticator::ExternalAuthorizationUrlReceived(const QUrl &request_url) {
AuthorizationUrlReceived(request_url, redirect_url_);
}
void OAuthenticator::AuthorizationUrlReceived(const QUrl &request_url, const QUrl &redirect_url) {
if (!request_url.isValid()) {
Q_EMIT AuthenticationFinished(false, tr("Received invalid reply from web browser."));
return;
}
if (!request_url.hasQuery()) {
Q_EMIT AuthenticationFinished(false, tr("Redirect URL is missing query."));
return;
}
qLog(Debug) << settings_group_ << "Authorization URL Received" << request_url.toDisplayString();
QUrlQuery url_query(request_url);
if (url_query.hasQueryItem(u"error_description"_s)) {
Q_EMIT AuthenticationFinished(false, url_query.queryItemValue(u"error_description"_s, QUrl::FullyDecoded));
return;
}
if (url_query.hasQueryItem(u"error"_s)) {
Q_EMIT AuthenticationFinished(false, url_query.queryItemValue(u"error"_s));
return;
}
if (!url_query.hasQueryItem(u"code"_s)) {
Q_EMIT AuthenticationFinished(false, tr("Request URL is missing code!"));
return;
}
if (!url_query.hasQueryItem(u"state"_s)) {
Q_EMIT AuthenticationFinished(false, tr("Request URL is missing state!"));
return;
}
if (url_query.queryItemValue(u"state"_s) != code_challenge_) {
Q_EMIT AuthenticationFinished(false, tr("Request URL has wrong state %1 != %2").arg(url_query.queryItemValue(u"state"_s), code_challenge_));
return;
}
RequestAccessToken(url_query.queryItemValue(u"code"_s), redirect_url);
}
QNetworkReply *OAuthenticator::CreateAccessTokenRequest(const ParamList &params, const bool refresh_token) {
QUrlQuery url_query;
for (const Param &param : std::as_const(params)) {
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
}
QNetworkRequest network_request(access_token_url_);
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
if (type_ == Type::Client_Credentials && !client_id_.isEmpty() && !client_secret_.isEmpty()) {
const QString authorization_header = client_id_ + u':' + client_secret_;
network_request.setRawHeader("Authorization", "Basic " + authorization_header.toUtf8().toBase64());
}
QNetworkReply *reply = network_->post(network_request, url_query.toString(QUrl::FullyEncoded).toUtf8());
replies_ << reply;
QObject::connect(reply, &QNetworkReply::sslErrors, this, &OAuthenticator::HandleSSLErrors);
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, refresh_token]() { AccessTokenRequestFinished(reply, refresh_token); });
return reply;
}
void OAuthenticator::RequestAccessToken(const QString &code, const QUrl &redirect_url) {
if (timer_refresh_login_->isActive()) {
timer_refresh_login_->stop();
}
ParamList params = ParamList() << Param(u"grant_type"_s, GrantType());
if (!code.isEmpty()) {
params << Param(u"code"_s, code);
}
if (!code_verifier_.isEmpty()) {
params << Param(u"code_verifier"_s, code_verifier_);
}
if (!code.isEmpty()) {
params << Param(u"redirect_uri"_s, redirect_url.toString());
}
if (!client_id_.isEmpty()) {
params << Param(u"client_id"_s, client_id_);
}
if (!client_secret_.isEmpty()) {
params << Param(u"client_secret"_s, client_secret_);
}
std::sort(params.begin(), params.end());
CreateAccessTokenRequest(params, false);
}
void OAuthenticator::RerefreshAccessToken() {
if (timer_refresh_login_->isActive()) {
timer_refresh_login_->stop();
}
if (client_id_.isEmpty() || refresh_token_.isEmpty()) {
return;
}
ParamList params = ParamList() << Param(u"grant_type"_s, u"refresh_token"_s)
<< Param(u"client_id"_s, client_id_)
<< Param(u"refresh_token"_s, refresh_token_);
if (!client_secret_.isEmpty()) {
params << Param(u"client_secret"_s, client_secret_);
}
CreateAccessTokenRequest(params, true);
}
void OAuthenticator::HandleSSLErrors(const QList<QSslError> &ssl_errors) {
for (const QSslError &ssl_error : ssl_errors) {
qLog(Debug) << settings_group_ << ssl_error.errorString();
}
}
void OAuthenticator::AccessTokenRequestFinished(QNetworkReply *reply, const bool refresh_token) {
if (!replies_.contains(reply)) return;
replies_.removeAll(reply);
QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
const QString error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
Q_EMIT AuthenticationFinished(false, error_message);
}
if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
const QByteArray data = reply->readAll();
if (!data.isEmpty()) {
QJsonParseError json_error;
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_error);
if (json_error.error == QJsonParseError::NoError && !json_document.isEmpty() && json_document.isObject()) {
const QJsonObject json_object = json_document.object();
if (json_object.contains("error"_L1) && json_object.contains("error_description"_L1)) {
const QString error = json_object["error"_L1].toString();
const QString error_description = json_object["error_description"_L1].toString();
Q_EMIT AuthenticationFinished(false, QStringLiteral("%1 (%2)").arg(error, error_description));
return;
}
qLog(Debug) << settings_group_ << "Unknown Json reply" << json_object;
}
}
if (reply->error() == QNetworkReply::NoError) {
Q_EMIT AuthenticationFinished(false, QStringLiteral("Received HTTP status code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
}
else {
Q_EMIT AuthenticationFinished(false, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
}
return;
}
const QByteArray data = reply->readAll();
QJsonParseError json_error;
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_error);
if (json_error.error != QJsonParseError::NoError) {
Q_EMIT AuthenticationFinished(false, QStringLiteral("Failed to parse Json data in authentication reply: %1").arg(json_error.errorString()));
return;
}
if (json_document.isEmpty()) {
Q_EMIT AuthenticationFinished(false, u"Authentication reply from server has empty Json document."_s);
return;
}
if (!json_document.isObject()) {
Q_EMIT AuthenticationFinished(false, u"Authentication reply from server has Json document that is not an object."_s);
return;
}
const QJsonObject json_object = json_document.object();
if (json_object.isEmpty()) {
Q_EMIT AuthenticationFinished(false, u"Authentication reply from server has empty Json object."_s);
return;
}
if (!json_object.contains("token_type"_L1)) {
Q_EMIT AuthenticationFinished(false, u"Authentication reply from server is missing token type."_s);
return;
}
if (!json_object.contains("access_token"_L1)) {
Q_EMIT AuthenticationFinished(false, u"Authentication reply from server is missing access token."_s);
return;
}
token_type_ = json_object["token_type"_L1].toString();
access_token_ = json_object["access_token"_L1].toString();
if (json_object.contains("refresh_token"_L1)) {
refresh_token_ = json_object["refresh_token"_L1].toString();
}
else if (!refresh_token) {
refresh_token_.clear();
}
if (json_object.contains("expires_in"_L1)) {
expires_in_ = json_object["expires_in"_L1].toInt();
}
else {
expires_in_ = 0;
}
login_time_ = QDateTime::currentSecsSinceEpoch();
country_code_.clear();
user_id_ = 0;
if (json_object.contains("user"_L1) && json_object["user"_L1].isObject()) {
const QJsonObject object_user = json_object["user"_L1].toObject();
if (object_user.contains("countryCode"_L1) && object_user.contains("userId"_L1)) {
country_code_ = object_user["countryCode"_L1].toString();
user_id_ = static_cast<quint64>(object_user["userId"_L1].toInt());
}
}
Settings s;
s.beginGroup(settings_group_);
s.setValue(kTokenType, token_type_);
s.setValue(kAccessToken, access_token_);
s.setValue(kLoginTime, login_time_);
if (refresh_token_.isEmpty()) {
s.remove(kRefreshToken);
}
else {
s.setValue(kRefreshToken, refresh_token_);
}
if (expires_in_ == 0) {
s.remove(kExpiresIn);
}
else {
s.setValue(kExpiresIn, expires_in_);
}
if (country_code_.isEmpty()) {
s.remove(kCountryCode);
}
else {
s.setValue(kCountryCode, country_code_);
}
if (user_id_ == 0) {
s.remove(kUserId);
}
else {
s.setValue(kUserId, user_id_);
}
s.endGroup();
StartRefreshLoginTimer();
qLog(Debug) << settings_group_ << "Authentication was successful, login expires in" << expires_in_;
Q_EMIT AuthenticationFinished(true);
}

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