Compare commits

...

169 Commits

Author SHA1 Message Date
Jonas Kvinge
e2f5486987 Release 1.0.17 2023-03-29 18:54:51 +02:00
Jonas Kvinge
b0d390aaf1 Update Changelog 2023-03-29 18:49:35 +02:00
Jonas Kvinge
bae1b42394 OSDPretty: Respect device pixel ratio 2023-03-29 18:23:06 +02:00
Jonas Kvinge
a2533edd57 PlayingWidget: Make previous pixmap respect device pixel ratio 2023-03-29 00:24:38 +02:00
Jonas Kvinge
7b88c198fe SongSourceDelegate: Fix compile with Qt 5 2023-03-29 00:14:21 +02:00
Jonas Kvinge
c49cb0c119 ImageUtils: Formatting 2023-03-29 00:09:40 +02:00
Jonas Kvinge
03aabeb848 AlbumCoverManager: Respect device pixel ratio 2023-03-29 00:06:56 +02:00
Jonas Kvinge
1d3a837f7a SongSourceDelegate: Respect device pixel ratio 2023-03-29 00:04:34 +02:00
Jonas Kvinge
92adc18b8f ContextAlbum: Make size hint respect device pixel ratio 2023-03-28 18:21:19 +02:00
Fletcher Dostie
e4697c8ff1 Render context art at correct size 2023-03-28 17:54:56 +02:00
Jonas Kvinge
b741f1a580 Turn on git revision 2023-03-28 01:48:09 +02:00
Jonas Kvinge
8ee6de4162 Release 1.0.16 2023-03-27 22:28:02 +02:00
Jonas Kvinge
2b9e7db924 TagReaderTagLib: Touch file over saving cover art 2023-03-26 22:37:21 +02:00
Jonas Kvinge
49384ce294 TagReaderTagLib: Fix reading MusicBrainz from ID3v2 tags 2023-03-26 04:33:15 +02:00
Jonas Kvinge
ac59fff346 TagReaderTagLib: Formatting 2023-03-26 04:14:03 +02:00
Jonas Kvinge
a3e9f152d8 CollectionSettingsPage: Remove iopriority hide 2023-03-26 03:14:52 +02:00
Jonas Kvinge
ffe6a81b9a Collection: Always use Idle priority for watcher 2023-03-26 00:10:29 +01:00
Jonas Kvinge
9bb051b4eb CollectionSettingsPage: Remove settings for IO and thread priority and workers 2023-03-26 00:09:41 +01:00
Jonas Kvinge
c1465a890f TagReaderClient: Always use 1 tagreader worker 2023-03-26 00:08:55 +01:00
Jonas Kvinge
c3ee3318d7 WorkerPool: Default to 1 worker 2023-03-26 00:08:23 +01:00
Jonas Kvinge
272976f0b7 TagReaderClient: Fix default save options 2023-03-25 23:54:15 +01:00
Jonas Kvinge
4724b170b1 AuddLyricsProvider: Disable provider by default
Since we are currently missing API key.
2023-03-25 19:20:10 +01:00
Jonas Kvinge
337bd4bcef ListenBrainzScrobbler: Make sure we only add the same artist id once 2023-03-25 19:14:26 +01:00
Jonas Kvinge
0604c78453 CollectionBackend: Check for changed fingerprint 2023-03-25 18:35:14 +01:00
Jonas Kvinge
c8169adf7c CollectionModel: Guard against invalid disc and year 2023-03-25 18:32:41 +01:00
Jonas Kvinge
7b610d131c CollectionModel: Replace qMax with std::max 2023-03-25 18:31:21 +01:00
Jonas Kvinge
18da55453f EditTagDialog: Not set for track, disc, year, etc to -1 2023-03-25 18:29:12 +01:00
Jonas Kvinge
2aeab8b672 CollectionModel: Remove unused QByteArray include 2023-03-25 18:26:53 +01:00
Jonas Kvinge
9c21707a55 Update Changelog 2023-03-25 16:49:19 +01:00
Jonas Kvinge
b02ac833ad CollectionWatcher: Fix rescan songs feature 2023-03-25 16:38:03 +01:00
Jonas Kvinge
4de912cf41 Song: Fix missing colon in SQL bind value 2023-03-25 16:34:26 +01:00
Jonas Kvinge
ed260c6e20 CollectionWatcher: Check for changed AcoustID and MusicBrainz 2023-03-25 14:35:12 +01:00
Jonas Kvinge
fab38f693d Scrobbler: Refactor and add MusicBrainz integration 2023-03-25 14:25:21 +01:00
Jonas Kvinge
aedbd52e9d Add AcoustID 2023-03-24 22:48:22 +01:00
Jonas Kvinge
4cbcb9d99c Update list of lyrics providers in README.md and dist files 2023-03-24 21:23:10 +01:00
Jonas Kvinge
e967d15b4e Add AudD lyrics provider 2023-03-24 21:16:11 +01:00
Fletcher Dostie
bb43cc63ec Fix album art rendering on High DPI displays 2023-03-24 21:02:55 +01:00
Jonas Kvinge
ca176c319d Add Acoustid and MusicBrainz tags to database schema 2023-03-24 21:00:58 +01:00
Jonas Kvinge
17c960ecd4 Song: Add Acoustid fingerprint and MusicBrainz tags 2023-03-24 20:57:46 +01:00
Jonas Kvinge
b766ae3498 Song: Use static_cast from int to Source 2023-03-24 20:54:42 +01:00
Jonas Kvinge
faf4c817cd TagReaderTagLib: Read AcoustID fingerprint 2023-03-24 20:54:05 +01:00
Jonas Kvinge
b4e1f283c9 tagreadermessages: Add AcoustID fingerprint tags to protobuf 2023-03-24 19:52:45 +01:00
Jonas Kvinge
52c83d592c Rename Stands4 to Lyrics.com 2023-03-22 23:36:15 +01:00
Jonas Kvinge
c8caea0d30 GstEnginePipeline: Use constexpr 2023-03-22 23:29:56 +01:00
Jonas Kvinge
7a6c54d8e7 GstEnginePipeline: Remove hard-coded num-bands 2023-03-22 23:29:24 +01:00
Strawbs Bot
7082e52a4f Update translations 2023-03-21 01:01:49 +01:00
Strawbs Bot
6cdca617e0 Update translations 2023-03-20 01:14:38 +01:00
Jonas Kvinge
a1adc1a75a Add option for strict SSL mode in backend settings 2023-03-19 23:02:17 +01:00
Jonas Kvinge
b16bec704a GstEnginePipeline: Use "source-setup" instead of "notify::source" signal
This works with both playbin 2 and 3.

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

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

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

Fixes #1133 and #1123
2023-02-10 22:42:37 +01:00
Strawbs Bot
d02de72830 Update translations 2023-02-06 01:01:22 +01:00
Strawbs Bot
08f5172028 Update translations 2023-01-23 01:01:39 +01:00
4fury-c3440d8
04f062547d TagReaderTagParser: Fix timeconstants.h include 2023-01-22 13:22:02 +01:00
Jonas Kvinge
4717d783dc Stands4LyricsProvider: Finish search when no lyrics are found 2023-01-21 15:55:47 +01:00
Strawbs Bot
93af064b36 Update translations 2023-01-21 01:02:43 +01:00
Jonas Kvinge
c78d73d727 LastPlayedItemDelegate: Show blank for zero or invalid time 2023-01-20 23:45:33 +01:00
Jonas Kvinge
b69b3228be DateItemDelegate: Show blank for zero or invalid time 2023-01-20 23:45:19 +01:00
Jonas Kvinge
377f54700d TidalRequest: Make cover optional 2023-01-20 23:35:47 +01:00
Jonas Kvinge
d276339c80 Add lyrics from stands4 (lyrics.com) 2023-01-20 22:48:52 +01:00
Jonas Kvinge
b982a6a762 LyricsProvider: Improve parsing from HTML function 2023-01-20 22:45:05 +01:00
Jonas Kvinge
536fe637aa About: Remove e-mail addresses 2023-01-20 19:32:49 +01:00
Strawbs Bot
69f36eaa25 Update translations 2023-01-15 01:01:48 +01:00
Strawbs Bot
d6927a70bb Update translations 2023-01-14 01:01:51 +01:00
Jonas Kvinge
1b1892a187 Sonogram: Fix parameter mismatch in header 2023-01-13 23:46:38 +01:00
Sungrak Choi
5bea71cd5c Add Sonogram analyzer
Co-Authored-By: Jonas Kvinge <jonas@jkvinge.net>
2023-01-13 23:41:43 +01:00
Jonas Kvinge
bb8d4e70ae Turn on git revision 2023-01-13 22:53:39 +01:00
383 changed files with 15855 additions and 13593 deletions

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

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

View File

@@ -1,4 +1,4 @@
name: build
name: Build
on: [push, pull_request]
jobs:
@@ -139,12 +139,12 @@ jobs:
image: fedora:${{matrix.fedora_version}}
steps:
- name: Update repositories
run: yum update --assumeyes
run: dnf update -y
- name: Upgrade packages
run: yum upgrade --assumeyes
run: dnf upgrade -y
- name: Install dependencies
run: >
dnf install --assumeyes
dnf install -y
@development-tools
redhat-lsb-core
which
@@ -223,12 +223,12 @@ jobs:
image: openmandriva/${{matrix.openmandriva_version}}
steps:
- name: Update repositories
run: dnf update --assumeyes
run: dnf update -y
- name: Upgrade packages
run: dnf upgrade --assumeyes
run: dnf upgrade -y
- name: Install dependencies
run: >
dnf install --assumeyes
dnf install -y
glibc
gcc-c++
git
@@ -310,7 +310,7 @@ jobs:
image: debian:${{matrix.debian_version}}
steps:
- name: Update repositories
run: apt update
run: apt update -y
- name: Install packages
env:
DEBIAN_FRONTEND: noninteractive
@@ -353,24 +353,12 @@ jobs:
if: matrix.debian_version == 'buster' || matrix.debian_version == 'bullseye'
env:
DEBIAN_FRONTEND: noninteractive
run: >
apt install -y
qtbase5-dev
qtbase5-dev-tools
qttools5-dev
qttools5-dev-tools
libqt5x11extras5-dev
run: apt install -y qtbase5-dev qtbase5-dev-tools qttools5-dev qttools5-dev-tools libqt5x11extras5-dev
- name: Install Qt 6
if: matrix.debian_version != 'buster' && matrix.debian_version != 'bullseye'
env:
DEBIAN_FRONTEND: noninteractive
run: >
apt install -y
qt6-base-dev
qt6-base-dev-tools
qt6-tools-dev
qt6-tools-dev-tools
qt6-l10n-tools
run: apt install -y qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools
- uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -391,15 +379,17 @@ jobs:
strategy:
fail-fast: false
matrix:
ubuntu_version: [ 'bionic', 'focal', 'jammy', 'kinetic' ]
ubuntu_version: [ 'bionic', 'focal', 'jammy', 'kinetic', 'lunar' ]
container:
image: ubuntu:${{matrix.ubuntu_version}}
steps:
- name: Update repositories
run: apt update -y
- name: Install packages
env:
DEBIAN_FRONTEND: noninteractive
run: >
apt-get update && apt-get install -y
apt install -y
build-essential
dh-make
ssh
@@ -440,12 +430,12 @@ jobs:
if: matrix.ubuntu_version == 'bionic' || matrix.ubuntu_version == 'focal'
env:
DEBIAN_FRONTEND: noninteractive
run: apt-get update && apt-get install -y qtbase5-dev qtbase5-dev-tools qttools5-dev qttools5-dev-tools libqt5x11extras5-dev
run: apt install -y qtbase5-dev qtbase5-dev-tools qttools5-dev qttools5-dev-tools libqt5x11extras5-dev
- name: Install Qt 6
if: matrix.ubuntu_version != 'bionic' && matrix.ubuntu_version != 'focal'
env:
DEBIAN_FRONTEND: noninteractive
run: apt-get update && apt-get install -y qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools
run: apt install -y qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools
- uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -469,7 +459,7 @@ jobs:
fetch-depth: 0
- name: Build FreeBSD
id: build-freebsd
uses: vmactions/freebsd-vm@v0.2.9
uses: vmactions/freebsd-vm@v0.3.0
with:
usesh: true
mem: 4096
@@ -490,7 +480,7 @@ jobs:
steps:
- name: Remove packages
run: brew remove aliyun-cli ant aws-sam-cli azure-cli bazelisk bicep composer geckodriver ghostscript go@1.17 helm httpd imagemagick kotlin maven mongodb-community@5.0 mongodb-database-tools mongosh nginx node@16 php postgresql@14 ruby@2.7 rustup-init sbt selenium-server swiftformat switchaudio-osx chromedriver firefox google-chrome graalvm-ce-java11 julia microsoft-edge r session-manager-plugin
run: brew remove aliyun-cli ant aws-sam-cli azure-cli bazelisk bicep composer geckodriver ghostscript go@1.17 helm httpd imagemagick kotlin maven mongodb-community@5.0 mongodb-database-tools mongosh nginx php postgresql@14 ruby@2.7 rustup-init sbt selenium-server swiftformat switchaudio-osx chromedriver firefox google-chrome graalvm-ce-java11 julia microsoft-edge r session-manager-plugin
- name: Update packages
run: brew update
@@ -517,21 +507,33 @@ jobs:
mv gstreamer.rb gst-plugins-{base,good,bad,ugly}.rb gst-libav.rb /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/
- name: Build and install gstreamer
env:
HOMEBREW_NO_INSTALL_FROM_API: 1
run: brew reinstall --build-from-source gstreamer
- name: Build and install gst-plugins-base
env:
HOMEBREW_NO_INSTALL_FROM_API: 1
run: brew reinstall --build-from-source gst-plugins-base
- name: Build and install gst-plugins-good
env:
HOMEBREW_NO_INSTALL_FROM_API: 1
run: brew reinstall --build-from-source gst-plugins-good
- name: Build and install gst-plugins-bad
env:
HOMEBREW_NO_INSTALL_FROM_API: 1
run: brew reinstall --build-from-source gst-plugins-bad
- name: Build and install gst-plugins-ugly
env:
HOMEBREW_NO_INSTALL_FROM_API: 1
run: brew reinstall --build-from-source gst-plugins-ugly
- name: Build and install gst-libav
env:
HOMEBREW_NO_INSTALL_FROM_API: 1
run: brew reinstall --build-from-source gst-libav
- name: Build libgpod
@@ -633,7 +635,7 @@ jobs:
arch: [ 'i686', 'x86_64' ]
build_type: [ 'debug', 'release' ]
container:
image: jonaski/strawberry-mxe-${{matrix.arch}}
image: jonaski/strawberry-mxe-${{matrix.arch}}-${{matrix.build_type}}
steps:
- uses: actions/checkout@v3
with:
@@ -663,12 +665,12 @@ jobs:
run: >
cmake ..
-DCMAKE_TOOLCHAIN_FILE="../cmake/Toolchain-${{matrix.arch}}-w64-mingw32-shared.cmake"
-DCMAKE_BUILD_TYPE="${{env.build_type}}"
-DCMAKE_BUILD_TYPE="${{matrix.build_type}}"
-DCMAKE_PREFIX_PATH="/strawberry-mxe/usr/${{matrix.arch}}-w64-mingw32.shared/qt6"
-DBUILD_WITH_QT6=ON
-DBUILD_WERROR=OFF
-DARCH="${{matrix.arch}}"
-DENABLE_WIN32_CONSOLE=${{env.build_type}}
-DENABLE_WIN32_CONSOLE=${{env.win32_console}}
-DENABLE_DBUS=OFF
-DENABLE_LIBGPOD=OFF
-DENABLE_LIBMTP=OFF
@@ -715,6 +717,11 @@ jobs:
working-directory: build
run: cp /strawberry-mxe/usr/${{matrix.arch}}-w64-mingw32.shared/bin/{sqlite3.exe,gst-launch-1.0.exe,gst-discoverer-1.0.exe,libsoup-3.0-0.dll,libnghttp2.dll} .
- name: Copy extra binaries (debug)
if: matrix.build_type == 'debug'
working-directory: build
run: cp /strawberry-mxe/usr/${{matrix.arch}}-w64-mingw32.shared/bin/{gdb.exe,libreadline8.dll} .
- name: Copy dependencies
working-directory: build
run: >
@@ -750,7 +757,7 @@ jobs:
build-windows-msvc:
name: Build Windows MSVC
runs-on: windows-2019
runs-on: windows-2022
strategy:
fail-fast: false
matrix:
@@ -761,7 +768,9 @@ jobs:
- uses: ilammy/msvc-dev-cmd@v1
with:
arch: ${{matrix.arch}}
toolset: 14.29
sdk: 10.0.20348.0
vsversion: 17
toolset: 14.3
- uses: actions/checkout@v3
with:
@@ -770,7 +779,7 @@ jobs:
- name: Delete conflicting files
shell: bash
run: |
rm -f /c/programdata/chocolatey/bin/{addr2line.exe,ar.exe,as.exe,c++.exe,c++filt.exe,cc1.exe,cc1plus.exe,cpp.exe,g++.exe,gcc-ar.exe,gcc-nm.exe,gcc-ranlib.exe,gcc.exe,gdb.exe,gfortran.exe,ld.bfd.exe,ld.exe,ld.gold.exe,nm.exe,ranlib.exe,readelf.exe,windres.exe,x86_64-w64-mingw32-c++.exe,x86_64-w64-mingw32-g++.exe,x86_64-w64-mingw32-gcc-8.1.0.exe,x86_64-w64-mingw32-gcc-ar.exe,x86_64-w64-mingw32-gcc-nm.exe,x86_64-w64-mingw32-gcc-ranlib.exe,x86_64-w64-mingw32-gcc.exe,x86_64-w64-mingw32-gfortran.exe}
rm -f /c/programdata/chocolatey/bin/{addr2line.exe,ar.exe,as.exe,c++.exe,c++filt.exe,cc1.exe,cc1plus.exe,cpp.exe,g++.exe,gcc-ar.exe,gcc-nm.exe,gcc-ranlib.exe,gcc.exe,gdb.exe,gfortran.exe,ld.bfd.exe,ld.exe,ld.gold.exe,nm.exe,ranlib.exe,readelf.exe,windres.exe,x86_64-w64-mingw32-c++.exe,x86_64-w64-mingw32-g++.exe,x86_64-w64-mingw32-gcc-8.1.0.exe,x86_64-w64-mingw32-gcc-ar.exe,x86_64-w64-mingw32-gcc-nm.exe,x86_64-w64-mingw32-gcc-ranlib.exe,x86_64-w64-mingw32-gcc.exe,x86_64-w64-mingw32-gfortran.exe,ccache.exe}
rm -f /c/strawberry/c/bin/{addr2line.exe,ar.exe,as.exe,c++.exe,c++filt.exe,cpp.exe,g++.exe,gcc-ar.exe,gcc-nm.exe,gcc-ranlib.exe,gcc.exe,ld.exe,nm.exe,ranlib.exe,readelf.exe,widl.exe,windmc.exe,windres.exe,x86_64-w64-mingw32-c++.exe,x86_64-w64-mingw32-g++.exe,x86_64-w64-mingw32-gcc-8.3.0.exe,x86_64-w64-mingw32-gcc-ar.exe,x86_64-w64-mingw32-gcc-nm.exe,x86_64-w64-mingw32-gcc-ranlib.exe,x86_64-w64-mingw32-gcc.exe,x86_64-w64-mingw32-gfortran.exe}
- name: Get latest MSVC dependencies
@@ -1033,7 +1042,7 @@ jobs:
upload-windows-msvc:
name: Upload Windows MSVC Setup
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/msvc'
needs:
- build-windows-msvc
steps:

88
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,88 @@
name: CodeQL
on: [push, pull_request]
jobs:
codeql:
name: CodeQL Analyze
runs-on: ubuntu-latest
container:
image: opensuse/tumbleweed
steps:
- name: Refresh repositories
run: zypper -n --gpg-auto-import-keys ref
- name: Upgrade packages
run: zypper -n --gpg-auto-import-keys dup
- name: Install packages
run: >
zypper -n --gpg-auto-import-keys in
lsb-release
rpm-build
git
tar
gcc
gcc-c++
make
cmake
gettext-tools
glibc-devel
libboost_headers-devel
boost-devel
glib2-devel
glib2-tools
dbus-1-devel
alsa-devel
libnotify-devel
libgnutls-devel
protobuf-devel
sqlite3-devel
libpulse-devel
gstreamer-devel
gstreamer-plugins-base-devel
vlc-devel
taglib-devel
libicu-devel
libcdio-devel
libgpod-devel
libmtp-devel
libchromaprint-devel
qt6-core-devel
qt6-gui-devel
qt6-widgets-devel
qt6-concurrent-devel
qt6-network-devel
qt6-sql-devel
qt6-dbus-devel
qt6-test-devel
qt6-base-common-devel
qt6-sql-sqlite
qt6-linguist-devel
desktop-file-utils
update-desktop-files
appstream-glib
hicolor-icon-theme
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Add safe git directory
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: cpp
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:cpp"

View File

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

View File

@@ -1,266 +0,0 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2020
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// W A R N I N G !!!
// -----------------
//
// This is a modified version of SingleApplication,
// The original version is at:
//
// https://github.com/itay-grudev/SingleApplication
//
//
#include <cstdlib>
#include <limits>
#include <QtGlobal>
#include <QApplication>
#include <QThread>
#include <QSharedMemory>
#include <QLocalSocket>
#include <QByteArray>
#include <QElapsedTimer>
#include <QtDebug>
#include "singleapplication.h"
#include "singleapplication_p.h"
/**
* @brief Constructor. Checks and fires up LocalServer or closes the program if another instance already exists
* @param argc
* @param argv
* @param allowSecondary Whether to enable secondary instance support
* @param options Optional flags to toggle specific behaviour
* @param timeout Maximum time blocking functions are allowed during app load
*/
SingleApplication::SingleApplication(int &argc, char *argv[], const bool allowSecondary, const Options options, const int timeout)
: app_t(argc, argv),
d_ptr(new SingleApplicationPrivate(this)) {
Q_D(SingleApplication);
// Store the current mode of the program
d->options_ = options;
// Generating an application ID used for identifying the shared memory block and QLocalServer
d->genBlockServerName();
// To mitigate QSharedMemory issues with large amount of processes attempting to attach at the same time
SingleApplicationPrivate::randomSleep();
#ifdef Q_OS_UNIX
// By explicitly attaching it and then deleting it we make sure that the memory is deleted even after the process has crashed on Unix.
d->memory_ = new QSharedMemory(d->blockServerName_);
d->memory_->attach();
delete d->memory_;
#endif
// Guarantee thread safe behaviour with a shared memory block.
d->memory_ = new QSharedMemory(d->blockServerName_);
// Create a shared memory block
if (d->memory_->create(sizeof(InstancesInfo))) {
// Initialize the shared memory block
if (!d->memory_->lock()) {
qCritical() << "SingleApplication: Unable to lock memory block after create.";
abortSafely();
}
d->initializeMemoryBlock();
}
else {
if (d->memory_->error() == QSharedMemory::AlreadyExists) {
// Attempt to attach to the memory segment
if (!d->memory_->attach()) {
qCritical() << "SingleApplication: Unable to attach to shared memory block.";
abortSafely();
}
if (!d->memory_->lock()) {
qCritical() << "SingleApplication: Unable to lock memory block after attach.";
abortSafely();
}
}
else {
qCritical() << "SingleApplication: Unable to create block.";
abortSafely();
}
}
InstancesInfo *instance = static_cast<InstancesInfo*>(d->memory_->data());
QElapsedTimer time;
time.start();
// Make sure the shared memory block is initialised and in consistent state
forever {
// If the shared memory block's checksum is valid continue
if (d->blockChecksum() == instance->checksum) break;
// If more than 5s have elapsed, assume the primary instance crashed and assume it's position
if (time.elapsed() > 5000) {
qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5s. Assuming primary instance failure.";
d->initializeMemoryBlock();
}
// Otherwise wait for a random period and try again.
// The random sleep here limits the probability of a collision between two racing apps and allows the app to initialise faster
if (!d->memory_->unlock()) {
qDebug() << "SingleApplication: Unable to unlock memory for random wait.";
qDebug() << d->memory_->errorString();
}
SingleApplicationPrivate::randomSleep();
if (!d->memory_->lock()) {
qCritical() << "SingleApplication: Unable to lock memory after random wait.";
abortSafely();
}
}
if (!instance->primary) {
d->startPrimary();
if (!d->memory_->unlock()) {
qDebug() << "SingleApplication: Unable to unlock memory after primary start.";
qDebug() << d->memory_->errorString();
}
return;
}
// Check if another instance can be started
if (allowSecondary) {
d->startSecondary();
if (d->options_ & Mode::SecondaryNotification) {
d->connectToPrimary(timeout, SingleApplicationPrivate::SecondaryInstance);
}
if (!d->memory_->unlock()) {
qDebug() << "SingleApplication: Unable to unlock memory after secondary start.";
qDebug() << d->memory_->errorString();
}
return;
}
if (!d->memory_->unlock()) {
qDebug() << "SingleApplication: Unable to unlock memory at end of execution.";
qDebug() << d->memory_->errorString();
}
d->connectToPrimary(timeout, SingleApplicationPrivate::NewInstance);
delete d;
::exit(EXIT_SUCCESS);
}
SingleApplication::~SingleApplication() {
Q_D(SingleApplication);
delete d;
}
/**
* Checks if the current application instance is primary.
* @return Returns true if the instance is primary, false otherwise.
*/
bool SingleApplication::isPrimary() const {
Q_D(const SingleApplication);
return d->server_ != nullptr;
}
/**
* Checks if the current application instance is secondary.
* @return Returns true if the instance is secondary, false otherwise.
*/
bool SingleApplication::isSecondary() const {
Q_D(const SingleApplication);
return d->server_ == nullptr;
}
/**
* Allows you to identify an instance by returning unique consecutive instance ids.
* It is reset when the first (primary) instance of your app starts and only incremented afterwards.
* @return Returns a unique instance id.
*/
quint32 SingleApplication::instanceId() const {
Q_D(const SingleApplication);
return d->instanceNumber_;
}
/**
* Returns the OS PID (Process Identifier) of the process running the primary instance.
* Especially useful when SingleApplication is coupled with OS. specific APIs.
* @return Returns the primary instance PID.
*/
qint64 SingleApplication::primaryPid() const {
Q_D(const SingleApplication);
return d->primaryPid();
}
/**
* Returns the username the primary instance is running as.
* @return Returns the username the primary instance is running as.
*/
QString SingleApplication::primaryUser() const {
Q_D(const SingleApplication);
return d->primaryUser();
}
/**
* Returns the username the current instance is running as.
* @return Returns the username the current instance is running as.
*/
QString SingleApplication::currentUser() const {
return SingleApplicationPrivate::getUsername();
}
/**
* Sends message to the Primary Instance.
* @param message The message to send.
* @param timeout the maximum timeout in milliseconds for blocking functions.
* @return true if the message was sent successfully, false otherwise.
*/
bool SingleApplication::sendMessage(const QByteArray &message, const int timeout) {
Q_D(SingleApplication);
// Nobody to connect to
if (isPrimary()) return false;
// Make sure the socket is connected
if (!d->connectToPrimary(timeout, SingleApplicationPrivate::Reconnect)) {
return false;
}
return d->writeConfirmedMessage(timeout, message);
}
/**
* Cleans up the shared memory block and exits with a failure.
* This function halts program execution.
*/
void SingleApplication::abortSafely() {
Q_D(SingleApplication);
qCritical() << "SingleApplication: " << d->memory_->error() << d->memory_->errorString();
delete d;
::exit(EXIT_FAILURE);
}

View File

@@ -0,0 +1,18 @@
cmake_minimum_required(VERSION 3.7)
add_definitions(-DSINGLEAPPLICATION)
set(SOURCES ../singleapplication_t.cpp ../singleapplication_p.cpp)
set(HEADERS ../singleapplication_t.h ../singleapplication_p.h)
qt_wrap_cpp(MOC ${HEADERS})
add_library(singleapplication STATIC ${SOURCES} ${MOC})
target_include_directories(singleapplication PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/..
${CMAKE_CURRENT_BINARY_DIR}/..
${Boost_INCLUDE_DIRS}
)
target_link_libraries(singleapplication PUBLIC
${QtCore_LIBRARIES}
${QtWidgets_LIBRARIES}
${QtNetwork_LIBRARIES}
)

View File

@@ -0,0 +1,13 @@
#ifndef SINGLEAPPLICATION_H
#define SINGLEAPPLICATION_H
#ifdef SINGLEAPPLICATION
# error "SINGLEAPPLICATION already defined."
#endif
#define SINGLEAPPLICATION
#include "../singleapplication_t.h"
#undef SINGLEAPPLICATION_T_H
#undef SINGLEAPPLICATION
#endif // SINGLEAPPLICATION_H

View File

@@ -68,44 +68,41 @@
# include <QDateTime>
#endif
#include "singleapplication.h"
#include "singleapplication_t.h"
#include "singleapplication_p.h"
SingleApplicationPrivate::SingleApplicationPrivate(SingleApplication *ptr)
SingleApplicationPrivateClass::SingleApplicationPrivateClass(SingleApplicationClass *ptr)
: q_ptr(ptr),
memory_(nullptr),
socket_(nullptr),
server_(nullptr),
instanceNumber_(-1) {}
SingleApplicationPrivate::~SingleApplicationPrivate() {
SingleApplicationPrivateClass::~SingleApplicationPrivateClass() {
if (socket_ != nullptr) {
if (socket_ != nullptr && socket_->isOpen()) {
socket_->close();
delete socket_;
socket_ = nullptr;
}
if (memory_ != nullptr) {
memory_->lock();
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
if (server_ != nullptr) {
server_->close();
delete server_;
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
instance->primary = false;
instance->primaryPid = -1;
instance->primaryUser[0] = '\0';
instance->checksum = blockChecksum();
}
memory_->unlock();
delete memory_;
memory_ = nullptr;
if (memory_->isAttached()) {
memory_->detach();
}
}
}
QString SingleApplicationPrivate::getUsername() {
QString SingleApplicationPrivateClass::getUsername() {
#ifdef Q_OS_UNIX
QString username;
@@ -141,36 +138,36 @@ QString SingleApplicationPrivate::getUsername() {
}
void SingleApplicationPrivate::genBlockServerName() {
void SingleApplicationPrivateClass::genBlockServerName() {
QCryptographicHash appData(QCryptographicHash::Sha256);
appData.addData("SingleApplication");
appData.addData(SingleApplication::app_t::applicationName().toUtf8());
appData.addData(SingleApplication::app_t::organizationName().toUtf8());
appData.addData(SingleApplication::app_t::organizationDomain().toUtf8());
appData.addData(SingleApplicationClass::applicationName().toUtf8());
appData.addData(SingleApplicationClass::organizationName().toUtf8());
appData.addData(SingleApplicationClass::organizationDomain().toUtf8());
if (!(options_ & SingleApplication::Mode::ExcludeAppVersion)) {
appData.addData(SingleApplication::app_t::applicationVersion().toUtf8());
if (!(options_ & SingleApplicationClass::Mode::ExcludeAppVersion)) {
appData.addData(SingleApplicationClass::applicationVersion().toUtf8());
}
if (!(options_ & SingleApplication::Mode::ExcludeAppPath)) {
if (!(options_ & SingleApplicationClass::Mode::ExcludeAppPath)) {
#if defined(Q_OS_UNIX)
const QByteArray appImagePath = qgetenv("APPIMAGE");
if (appImagePath.isEmpty()) {
appData.addData(SingleApplication::app_t::applicationFilePath().toUtf8());
appData.addData(SingleApplicationClass::applicationFilePath().toUtf8());
}
else {
appData.addData(appImagePath);
};
}
#elif defined(Q_OS_WIN)
appData.addData(SingleApplication::app_t::applicationFilePath().toLower().toUtf8());
appData.addData(SingleApplicationClass::applicationFilePath().toLower().toUtf8());
#else
appData.addData(SingleApplication::app_t::applicationFilePath().toUtf8());
appData.addData(SingleApplicationClass::applicationFilePath().toUtf8());
#endif
}
// User level block requires a user specific data in the hash
if (options_ & SingleApplication::Mode::User) {
if (options_ & SingleApplicationClass::Mode::User) {
appData.addData(getUsername().toUtf8());
}
@@ -179,7 +176,7 @@ void SingleApplicationPrivate::genBlockServerName() {
}
void SingleApplicationPrivate::initializeMemoryBlock() const {
void SingleApplicationPrivateClass::initializeMemoryBlock() const {
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
instance->primary = false;
@@ -190,7 +187,7 @@ void SingleApplicationPrivate::initializeMemoryBlock() const {
}
void SingleApplicationPrivate::startPrimary() {
void SingleApplicationPrivateClass::startPrimary() {
// Reset the number of connections
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
@@ -203,10 +200,10 @@ void SingleApplicationPrivate::startPrimary() {
// Successful creation means that no main process exists
// So we start a QLocalServer to listen for connections
QLocalServer::removeServer(blockServerName_);
server_ = new QLocalServer();
server_ = new QLocalServer(this);
// Restrict access to the socket according to the SingleApplication::Mode::User flag on User level or no restrictions
if (options_ & SingleApplication::Mode::User) {
if (options_ & SingleApplicationClass::Mode::User) {
server_->setSocketOptions(QLocalServer::UserAccessOption);
}
else {
@@ -214,11 +211,11 @@ void SingleApplicationPrivate::startPrimary() {
}
server_->listen(blockServerName_);
QObject::connect(server_, &QLocalServer::newConnection, this, &SingleApplicationPrivate::slotConnectionEstablished);
QObject::connect(server_, &QLocalServer::newConnection, this, &SingleApplicationPrivateClass::slotConnectionEstablished);
}
void SingleApplicationPrivate::startSecondary() {
void SingleApplicationPrivateClass::startSecondary() {
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
@@ -228,18 +225,18 @@ void SingleApplicationPrivate::startSecondary() {
}
bool SingleApplicationPrivate::connectToPrimary(const int timeout, const ConnectionType connectionType) {
QElapsedTimer time;
time.start();
bool SingleApplicationPrivateClass::connectToPrimary(const int timeout, const ConnectionType connectionType) {
// Connect to the Local Server of the Primary Instance if not already connected.
if (socket_ == nullptr) {
socket_ = new QLocalSocket();
socket_ = new QLocalSocket(this);
}
if (socket_->state() == QLocalSocket::ConnectedState) return true;
QElapsedTimer time;
time.start();
if (socket_->state() != QLocalSocket::ConnectedState) {
forever {
@@ -261,7 +258,7 @@ bool SingleApplicationPrivate::connectToPrimary(const int timeout, const Connect
}
}
// Initialisation message according to the SingleApplication protocol
// Initialization message according to the SingleApplication protocol
QByteArray initMsg;
QDataStream writeStream(&initMsg, QIODevice::WriteOnly);
writeStream.setVersion(QDataStream::Qt_5_8);
@@ -282,11 +279,11 @@ bool SingleApplicationPrivate::connectToPrimary(const int timeout, const Connect
}
void SingleApplicationPrivate::writeAck(QLocalSocket *sock) {
void SingleApplicationPrivateClass::writeAck(QLocalSocket *sock) {
sock->putChar('\n');
}
bool SingleApplicationPrivate::writeConfirmedMessage(const int timeout, const QByteArray &msg) const {
bool SingleApplicationPrivateClass::writeConfirmedMessage(const int timeout, const QByteArray &msg) const {
QElapsedTimer time;
time.start();
@@ -306,7 +303,7 @@ bool SingleApplicationPrivate::writeConfirmedMessage(const int timeout, const QB
}
bool SingleApplicationPrivate::writeConfirmedFrame(const int timeout, const QByteArray &msg) const {
bool SingleApplicationPrivateClass::writeConfirmedFrame(const int timeout, const QByteArray &msg) const {
socket_->write(msg);
socket_->flush();
@@ -321,7 +318,7 @@ bool SingleApplicationPrivate::writeConfirmedFrame(const int timeout, const QByt
}
quint16 SingleApplicationPrivate::blockChecksum() const {
quint16 SingleApplicationPrivateClass::blockChecksum() const {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
quint16 checksum = qChecksum(QByteArray(static_cast<const char*>(memory_->constData()), offsetof(InstancesInfo, checksum)));
@@ -333,7 +330,7 @@ quint16 SingleApplicationPrivate::blockChecksum() const {
}
qint64 SingleApplicationPrivate::primaryPid() const {
qint64 SingleApplicationPrivateClass::primaryPid() const {
memory_->lock();
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
@@ -344,7 +341,7 @@ qint64 SingleApplicationPrivate::primaryPid() const {
}
QString SingleApplicationPrivate::primaryUser() const {
QString SingleApplicationPrivateClass::primaryUser() const {
memory_->lock();
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
@@ -358,23 +355,23 @@ QString SingleApplicationPrivate::primaryUser() const {
/**
* @brief Executed when a connection has been made to the LocalServer
*/
void SingleApplicationPrivate::slotConnectionEstablished() {
void SingleApplicationPrivateClass::slotConnectionEstablished() {
QLocalSocket *nextConnSocket = server_->nextPendingConnection();
connectionMap_.insert(nextConnSocket, ConnectionInfo());
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this, [nextConnSocket, this]() {
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this, [this, nextConnSocket]() {
const ConnectionInfo &info = connectionMap_[nextConnSocket];
slotClientConnectionClosed(nextConnSocket, info.instanceId);
});
QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater);
QObject::connect(nextConnSocket, &QLocalSocket::destroyed, this, [nextConnSocket, this]() {
QObject::connect(nextConnSocket, &QLocalSocket::destroyed, this, [this, nextConnSocket]() {
connectionMap_.remove(nextConnSocket);
});
QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this, [nextConnSocket, this]() {
QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this, [this, nextConnSocket]() {
const ConnectionInfo &info = connectionMap_[nextConnSocket];
switch (info.stage) {
case StageInitHeader:
@@ -387,16 +384,16 @@ void SingleApplicationPrivate::slotConnectionEstablished() {
readMessageHeader(nextConnSocket, StageConnectedBody);
break;
case StageConnectedBody:
this->slotDataAvailable(nextConnSocket, info.instanceId);
slotDataAvailable(nextConnSocket, info.instanceId);
break;
default:
break;
};
}
});
}
void SingleApplicationPrivate::readMessageHeader(QLocalSocket *sock, const SingleApplicationPrivate::ConnectionStage nextStage) {
void SingleApplicationPrivateClass::readMessageHeader(QLocalSocket *sock, const SingleApplicationPrivateClass::ConnectionStage nextStage) {
if (!connectionMap_.contains(sock)) {
return;
@@ -420,7 +417,7 @@ void SingleApplicationPrivate::readMessageHeader(QLocalSocket *sock, const Singl
}
bool SingleApplicationPrivate::isFrameComplete(QLocalSocket *sock) {
bool SingleApplicationPrivateClass::isFrameComplete(QLocalSocket *sock) {
if (!connectionMap_.contains(sock)) {
return false;
@@ -431,9 +428,9 @@ bool SingleApplicationPrivate::isFrameComplete(QLocalSocket *sock) {
}
void SingleApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
void SingleApplicationPrivateClass::readInitMessageBody(QLocalSocket *sock) {
Q_Q(SingleApplication);
Q_Q(SingleApplicationClass);
if (!isFrameComplete(sock)) {
return;
@@ -449,10 +446,9 @@ void SingleApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
readStream >> latin1Name;
// connection type
ConnectionType connectionType = InvalidConnection;
quint8 connTypeVal = InvalidConnection;
readStream >> connTypeVal;
connectionType = static_cast<ConnectionType>(connTypeVal);
const ConnectionType connectionType = static_cast<ConnectionType>(connTypeVal);
// instance id
quint32 instanceId = 0;
@@ -479,7 +475,7 @@ void SingleApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
info.instanceId = instanceId;
info.stage = StageConnectedHeader;
if (connectionType == NewInstance || (connectionType == SecondaryInstance && options_ & SingleApplication::Mode::SecondaryNotification)) {
if (connectionType == NewInstance || (connectionType == SecondaryInstance && options_ & SingleApplicationClass::Mode::SecondaryNotification)) {
emit q->instanceStarted();
}
@@ -487,9 +483,9 @@ void SingleApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
}
void SingleApplicationPrivate::slotDataAvailable(QLocalSocket *dataSocket, const quint32 instanceId) {
void SingleApplicationPrivateClass::slotDataAvailable(QLocalSocket *dataSocket, const quint32 instanceId) {
Q_Q(SingleApplication);
Q_Q(SingleApplicationClass);
if (!isFrameComplete(dataSocket)) {
return;
@@ -506,7 +502,7 @@ void SingleApplicationPrivate::slotDataAvailable(QLocalSocket *dataSocket, const
}
void SingleApplicationPrivate::slotClientConnectionClosed(QLocalSocket *closedSocket, const quint32 instanceId) {
void SingleApplicationPrivateClass::slotClientConnectionClosed(QLocalSocket *closedSocket, const quint32 instanceId) {
if (closedSocket->bytesAvailable() > 0) {
slotDataAvailable(closedSocket, instanceId);
@@ -514,7 +510,7 @@ void SingleApplicationPrivate::slotClientConnectionClosed(QLocalSocket *closedSo
}
void SingleApplicationPrivate::randomSleep() {
void SingleApplicationPrivateClass::randomSleep() {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
QThread::msleep(QRandomGenerator::global()->bounded(8U, 18U));

View File

@@ -39,31 +39,19 @@
#include <QString>
#include <QHash>
#include "singleapplication.h"
#include "singleapplication_t.h"
class QLocalServer;
class QLocalSocket;
class QSharedMemory;
struct InstancesInfo {
bool primary;
quint32 secondary;
qint64 primaryPid;
char primaryUser[128];
quint16 checksum;
};
struct ConnectionInfo {
explicit ConnectionInfo() : msgLen(0), instanceId(0), stage(0) {}
quint64 msgLen;
quint32 instanceId;
quint8 stage;
};
class SingleApplicationPrivate : public QObject {
class SingleApplicationPrivateClass : public QObject {
Q_OBJECT
public:
explicit SingleApplicationPrivateClass(SingleApplicationClass *ptr);
~SingleApplicationPrivateClass() override;
enum ConnectionType : quint8 {
InvalidConnection = 0,
NewInstance = 1,
@@ -74,12 +62,25 @@ class SingleApplicationPrivate : public QObject {
StageInitHeader = 0,
StageInitBody = 1,
StageConnectedHeader = 2,
StageConnectedBody = 3,
StageConnectedBody = 3
};
Q_DECLARE_PUBLIC(SingleApplication)
Q_DECLARE_PUBLIC(SingleApplicationClass)
explicit SingleApplicationPrivate(SingleApplication *ptr);
~SingleApplicationPrivate() override;
struct InstancesInfo {
explicit InstancesInfo() : primary(false), secondary(0), primaryPid(0), checksum(0) {}
bool primary;
quint32 secondary;
qint64 primaryPid;
char primaryUser[128];
quint16 checksum;
};
struct ConnectionInfo {
explicit ConnectionInfo() : msgLen(0), instanceId(0), stage(0) {}
quint64 msgLen;
quint32 instanceId;
quint8 stage;
};
static QString getUsername();
void genBlockServerName();
@@ -98,13 +99,13 @@ class SingleApplicationPrivate : public QObject {
bool writeConfirmedMessage(const int timeout, const QByteArray &msg) const;
static void randomSleep();
SingleApplication *q_ptr;
SingleApplicationClass *q_ptr;
QSharedMemory *memory_;
QLocalSocket *socket_;
QLocalServer *server_;
quint32 instanceNumber_;
QString blockServerName_;
SingleApplication::Options options_;
SingleApplicationClass::Options options_;
QHash<QLocalSocket*, ConnectionInfo> connectionMap_;
public slots:

View File

@@ -0,0 +1,330 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2020
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// W A R N I N G !!!
// -----------------
//
// This is a modified version of SingleApplication,
// The original version is at:
//
// https://github.com/itay-grudev/SingleApplication
//
//
#include <cstdlib>
#include <limits>
#include <memory>
#include <boost/scope_exit.hpp>
#include <QtGlobal>
#include <QThread>
#include <QSharedMemory>
#include <QLocalSocket>
#include <QByteArray>
#include <QElapsedTimer>
#include <QtDebug>
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
# include <QNativeIpcKey>
#endif
#include "singleapplication_t.h"
#include "singleapplication_p.h"
/**
* @brief Constructor. Checks and fires up LocalServer or closes the program if another instance already exists
* @param argc
* @param argv
* @param allowSecondary Whether to enable secondary instance support
* @param options Optional flags to toggle specific behaviour
* @param timeout Maximum time blocking functions are allowed during app load
*/
SingleApplicationClass::SingleApplicationClass(int &argc, char *argv[], const bool allowSecondary, const Options options, const int timeout)
: ApplicationClass(argc, argv),
d_ptr(new SingleApplicationPrivateClass(this)) {
#if defined(SINGLEAPPLICATION)
Q_D(SingleApplication);
#elif defined(SINGLECOREAPPLICATION)
Q_D(SingleCoreApplication);
#endif
// Store the current mode of the program
d->options_ = options;
// Generating an application ID used for identifying the shared memory block and QLocalServer
d->genBlockServerName();
// To mitigate QSharedMemory issues with large amount of processes attempting to attach at the same time
SingleApplicationPrivateClass::randomSleep();
#ifdef Q_OS_UNIX
// By explicitly attaching it and then deleting it we make sure that the memory is deleted even after the process has crashed on Unix.
{
# if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
std::unique_ptr<QSharedMemory> memory = std::make_unique<QSharedMemory>(QNativeIpcKey(d->blockServerName_));
# else
std::unique_ptr<QSharedMemory> memory = std::make_unique<QSharedMemory>(d->blockServerName_);
# endif
if (memory->attach()) {
memory->detach();
}
}
#endif
// Guarantee thread safe behaviour with a shared memory block.
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
QSharedMemory *memory = new QSharedMemory(QNativeIpcKey(d->blockServerName_), this);
#else
QSharedMemory *memory = new QSharedMemory(d->blockServerName_, this);
#endif
d->memory_ = memory;
bool primary = false;
// Create a shared memory block
if (d->memory_->create(sizeof(SingleApplicationPrivateClass::InstancesInfo))) {
primary = true;
}
else if (d->memory_->error() == QSharedMemory::AlreadyExists) {
if (!d->memory_->attach()) {
qCritical() << "SingleApplication: Unable to attach to shared memory block:" << d->memory_->error() << d->memory_->errorString();
return;
}
}
else {
qCritical() << "SingleApplication: Unable to create shared memory block:" << d->memory_->error() << d->memory_->errorString();
return;
}
bool locked = false;
BOOST_SCOPE_EXIT((memory)(&locked)) {
if (locked && !memory->unlock()) {
qWarning() << "SingleApplication: Unable to unlock shared memory block:" << memory->error() << memory->errorString();
return;
}
}BOOST_SCOPE_EXIT_END
if (!d->memory_->lock()) {
qCritical() << "SingleApplication: Unable to lock shared memory block:" << d->memory_->error() << d->memory_->errorString();
return;
}
locked = true;
if (primary) {
// Initialize the shared memory block
d->initializeMemoryBlock();
}
SingleApplicationPrivateClass::InstancesInfo *instance = static_cast<SingleApplicationPrivateClass::InstancesInfo*>(d->memory_->data());
QElapsedTimer time;
time.start();
// Make sure the shared memory block is initialized and in a consistent state
while (d->blockChecksum() != instance->checksum) {
// If more than 5 seconds have elapsed, assume the primary instance crashed and assume its position
if (time.elapsed() > 5000) {
qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5 seconds. Assuming primary instance failure.";
d->initializeMemoryBlock();
}
// Otherwise wait for a random period and try again.
// The random sleep here limits the probability of a collision between two racing apps and allows the app to initialize faster
if (locked) {
if (d->memory_->unlock()) {
locked = false;
}
else {
qCritical() << "SingleApplication: Unable to unlock shared memory block for random wait:" << memory->error() << memory->errorString();
return;
}
}
SingleApplicationPrivateClass::randomSleep();
if (!d->memory_->lock()) {
qCritical() << "SingleApplication: Unable to lock shared memory block after random wait:" << memory->error() << memory->errorString();
return;
}
locked = true;
}
if (instance->primary) {
// Check if another instance can be started
if (allowSecondary) {
d->startSecondary();
if (d->options_ & Mode::SecondaryNotification) {
d->connectToPrimary(timeout, SingleApplicationPrivateClass::SecondaryInstance);
}
}
}
else {
d->startPrimary();
primary = true;
}
if (locked) {
if (d->memory_->unlock()) {
locked = false;
}
else {
qWarning() << "SingleApplication: Unable to unlock shared memory block:" << memory->error() << memory->errorString();
}
}
if (!primary && !allowSecondary) {
d->connectToPrimary(timeout, SingleApplicationPrivateClass::NewInstance);
}
}
SingleApplicationClass::~SingleApplicationClass() {
#if defined(SINGLEAPPLICATION)
Q_D(SingleApplication);
#elif defined(SINGLECOREAPPLICATION)
Q_D(SingleCoreApplication);
#endif
delete d;
}
/**
* Checks if the current application instance is primary.
* @return Returns true if the instance is primary, false otherwise.
*/
bool SingleApplicationClass::isPrimary() const {
#if defined(SINGLEAPPLICATION)
Q_D(const SingleApplication);
#elif defined(SINGLECOREAPPLICATION)
Q_D(const SingleCoreApplication);
#endif
return d->server_ != nullptr;
}
/**
* Checks if the current application instance is secondary.
* @return Returns true if the instance is secondary, false otherwise.
*/
bool SingleApplicationClass::isSecondary() const {
#if defined(SINGLEAPPLICATION)
Q_D(const SingleApplication);
#elif defined(SINGLECOREAPPLICATION)
Q_D(const SingleCoreApplication);
#endif
return d->server_ == nullptr;
}
/**
* Allows you to identify an instance by returning unique consecutive instance ids.
* It is reset when the first (primary) instance of your app starts and only incremented afterwards.
* @return Returns a unique instance id.
*/
quint32 SingleApplicationClass::instanceId() const {
#if defined(SINGLEAPPLICATION)
Q_D(const SingleApplication);
#elif defined(SINGLECOREAPPLICATION)
Q_D(const SingleCoreApplication);
#endif
return d->instanceNumber_;
}
/**
* Returns the OS PID (Process Identifier) of the process running the primary instance.
* Especially useful when SingleApplication is coupled with OS. specific APIs.
* @return Returns the primary instance PID.
*/
qint64 SingleApplicationClass::primaryPid() const {
#if defined(SINGLEAPPLICATION)
Q_D(const SingleApplication);
#elif defined(SINGLECOREAPPLICATION)
Q_D(const SingleCoreApplication);
#endif
return d->primaryPid();
}
/**
* Returns the username the primary instance is running as.
* @return Returns the username the primary instance is running as.
*/
QString SingleApplicationClass::primaryUser() const {
#if defined(SINGLEAPPLICATION)
Q_D(const SingleApplication);
#elif defined(SINGLECOREAPPLICATION)
Q_D(const SingleCoreApplication);
#endif
return d->primaryUser();
}
/**
* Returns the username the current instance is running as.
* @return Returns the username the current instance is running as.
*/
QString SingleApplicationClass::currentUser() const {
return SingleApplicationPrivateClass::getUsername();
}
/**
* Sends message to the Primary Instance.
* @param message The message to send.
* @param timeout the maximum timeout in milliseconds for blocking functions.
* @return true if the message was sent successfully, false otherwise.
*/
bool SingleApplicationClass::sendMessage(const QByteArray &message, const int timeout) {
#if defined(SINGLEAPPLICATION)
Q_D(SingleApplication);
#elif defined(SINGLECOREAPPLICATION)
Q_D(SingleCoreApplication);
#endif
// Nobody to connect to
if (isPrimary()) return false;
// Make sure the socket is connected
if (!d->connectToPrimary(timeout, SingleApplicationPrivateClass::Reconnect)) {
return false;
}
return d->writeConfirmedMessage(timeout, message);
}

View File

@@ -31,25 +31,41 @@
//
//
#ifndef SINGLEAPPLICATION_H
#define SINGLEAPPLICATION_H
#ifndef SINGLEAPPLICATION_T_H
#define SINGLEAPPLICATION_T_H
#include <QtGlobal>
#include <QApplication>
#undef ApplicationClass
#undef SingleApplicationClass
#undef SingleApplicationPrivateClass
#if defined(SINGLEAPPLICATION)
# include <QApplication>
# define ApplicationClass QApplication
# define SingleApplicationClass SingleApplication
# define SingleApplicationPrivateClass SingleApplicationPrivate
#elif defined(SINGLECOREAPPLICATION)
# include <QCoreApplication>
# define ApplicationClass QCoreApplication
# define SingleApplicationClass SingleCoreApplication
# define SingleApplicationPrivateClass SingleCoreApplicationPrivate
#else
# error "Define SINGLEAPPLICATION or SINGLECOREAPPLICATION."
#endif
#include <QFlags>
#include <QByteArray>
class SingleApplicationPrivate;
class SingleApplicationPrivateClass;
/**
* @brief The SingleApplication class handles multiple instances of the same Application
* @see QApplication
*/
class SingleApplication : public QApplication { // clazy:exclude=ctor-missing-parent-argument
class SingleApplicationClass : public ApplicationClass { // clazy:exclude=ctor-missing-parent-argument
Q_OBJECT
using app_t = QApplication;
public:
/**
* @brief Mode of operation of SingleApplication.
@@ -61,7 +77,7 @@ class SingleApplication : public QApplication { // clazy:exclude=ctor-missing-p
* block will be user wide.
* @enum
*/
enum Mode {
enum class Mode {
User = 1 << 0,
System = 1 << 1,
SecondaryNotification = 1 << 2,
@@ -86,11 +102,11 @@ class SingleApplication : public QApplication { // clazy:exclude=ctor-missing-p
* instance and the secondary instance.
* @note The timeout is just a hint for the maximum time of blocking
* operations. It does not guarantee that the SingleApplication
* initialisation will be completed in given time, though is a good hint.
* initialization will be completed in given time, though is a good hint.
* Usually 4*timeout would be the worst case (fail) scenario.
*/
explicit SingleApplication(int &argc, char *argv[], const bool allowSecondary = false, const Options options = Mode::User, const int timeout = 1000);
~SingleApplication() override;
explicit SingleApplicationClass(int &argc, char *argv[], const bool allowSecondary = false, const Options options = Mode::User, const int timeout = 1000);
~SingleApplicationClass() override;
/**
* @brief Returns if the instance is the primary instance
@@ -142,11 +158,15 @@ class SingleApplication : public QApplication { // clazy:exclude=ctor-missing-p
void receivedMessage(quint32 instanceId, QByteArray message);
private:
SingleApplicationPrivate *d_ptr;
SingleApplicationPrivateClass *d_ptr;
#if defined(SINGLEAPPLICATION)
Q_DECLARE_PRIVATE(SingleApplication)
#elif defined(SINGLECOREAPPLICATION)
Q_DECLARE_PRIVATE(SingleCoreApplication)
#endif
void abortSafely();
};
Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplication::Options)
Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplicationClass::Options)
#endif // SINGLEAPPLICATION_H
#endif // SINGLEAPPLICATION_T_H

View File

@@ -1,266 +0,0 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2020
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// W A R N I N G !!!
// -----------------
//
// This is a modified version of SingleApplication,
// The original version is at:
//
// https://github.com/itay-grudev/SingleApplication
//
//
#include <cstdlib>
#include <limits>
#include <QtGlobal>
#include <QCoreApplication>
#include <QThread>
#include <QSharedMemory>
#include <QLocalSocket>
#include <QByteArray>
#include <QElapsedTimer>
#include <QtDebug>
#include "singlecoreapplication.h"
#include "singlecoreapplication_p.h"
/**
* @brief Constructor. Checks and fires up LocalServer or closes the program if another instance already exists
* @param argc
* @param argv
* @param allowSecondary Whether to enable secondary instance support
* @param options Optional flags to toggle specific behaviour
* @param timeout Maximum time blocking functions are allowed during app load
*/
SingleCoreApplication::SingleCoreApplication(int &argc, char *argv[], const bool allowSecondary, const Options options, const int timeout)
: app_t(argc, argv),
d_ptr(new SingleCoreApplicationPrivate(this)) {
Q_D(SingleCoreApplication);
// Store the current mode of the program
d->options_ = options;
// Generating an application ID used for identifying the shared memory block and QLocalServer
d->genBlockServerName();
// To mitigate QSharedMemory issues with large amount of processes attempting to attach at the same time
SingleCoreApplicationPrivate::randomSleep();
#ifdef Q_OS_UNIX
// By explicitly attaching it and then deleting it we make sure that the memory is deleted even after the process has crashed on Unix.
d->memory_ = new QSharedMemory(d->blockServerName_);
d->memory_->attach();
delete d->memory_;
#endif
// Guarantee thread safe behaviour with a shared memory block.
d->memory_ = new QSharedMemory(d->blockServerName_);
// Create a shared memory block
if (d->memory_->create(sizeof(InstancesInfo))) {
// Initialize the shared memory block
if (!d->memory_->lock()) {
qCritical() << "SingleCoreApplication: Unable to lock memory block after create.";
abortSafely();
}
d->initializeMemoryBlock();
}
else {
if (d->memory_->error() == QSharedMemory::AlreadyExists) {
// Attempt to attach to the memory segment
if (!d->memory_->attach()) {
qCritical() << "SingleCoreApplication: Unable to attach to shared memory block.";
abortSafely();
}
if (!d->memory_->lock()) {
qCritical() << "SingleCoreApplication: Unable to lock memory block after attach.";
abortSafely();
}
}
else {
qCritical() << "SingleCoreApplication: Unable to create block.";
abortSafely();
}
}
InstancesInfo *instance = static_cast<InstancesInfo*>(d->memory_->data());
QElapsedTimer time;
time.start();
// Make sure the shared memory block is initialised and in consistent state
forever {
// If the shared memory block's checksum is valid continue
if (d->blockChecksum() == instance->checksum) break;
// If more than 5s have elapsed, assume the primary instance crashed and assume it's position
if (time.elapsed() > 5000) {
qWarning() << "SingleCoreApplication: Shared memory block has been in an inconsistent state from more than 5s. Assuming primary instance failure.";
d->initializeMemoryBlock();
}
// Otherwise wait for a random period and try again.
// The random sleep here limits the probability of a collision between two racing apps and allows the app to initialise faster
if (!d->memory_->unlock()) {
qDebug() << "SingleCoreApplication: Unable to unlock memory for random wait.";
qDebug() << d->memory_->errorString();
}
SingleCoreApplicationPrivate::randomSleep();
if (!d->memory_->lock()) {
qCritical() << "SingleCoreApplication: Unable to lock memory after random wait.";
abortSafely();
}
}
if (!instance->primary) {
d->startPrimary();
if (!d->memory_->unlock()) {
qDebug() << "SingleCoreApplication: Unable to unlock memory after primary start.";
qDebug() << d->memory_->errorString();
}
return;
}
// Check if another instance can be started
if (allowSecondary) {
d->startSecondary();
if (d->options_ & Mode::SecondaryNotification) {
d->connectToPrimary(timeout, SingleCoreApplicationPrivate::SecondaryInstance);
}
if (!d->memory_->unlock()) {
qDebug() << "SingleCoreApplication: Unable to unlock memory after secondary start.";
qDebug() << d->memory_->errorString();
}
return;
}
if (!d->memory_->unlock()) {
qDebug() << "SingleCoreApplication: Unable to unlock memory at end of execution.";
qDebug() << d->memory_->errorString();
}
d->connectToPrimary(timeout, SingleCoreApplicationPrivate::NewInstance);
delete d;
::exit(EXIT_SUCCESS);
}
SingleCoreApplication::~SingleCoreApplication() {
Q_D(SingleCoreApplication);
delete d;
}
/**
* Checks if the current application instance is primary.
* @return Returns true if the instance is primary, false otherwise.
*/
bool SingleCoreApplication::isPrimary() const {
Q_D(const SingleCoreApplication);
return d->server_ != nullptr;
}
/**
* Checks if the current application instance is secondary.
* @return Returns true if the instance is secondary, false otherwise.
*/
bool SingleCoreApplication::isSecondary() const {
Q_D(const SingleCoreApplication);
return d->server_ == nullptr;
}
/**
* Allows you to identify an instance by returning unique consecutive instance ids.
* It is reset when the first (primary) instance of your app starts and only incremented afterwards.
* @return Returns a unique instance id.
*/
quint32 SingleCoreApplication::instanceId() const {
Q_D(const SingleCoreApplication);
return d->instanceNumber_;
}
/**
* Returns the OS PID (Process Identifier) of the process running the primary instance.
* Especially useful when SingleCoreApplication is coupled with OS. specific APIs.
* @return Returns the primary instance PID.
*/
qint64 SingleCoreApplication::primaryPid() const {
Q_D(const SingleCoreApplication);
return d->primaryPid();
}
/**
* Returns the username the primary instance is running as.
* @return Returns the username the primary instance is running as.
*/
QString SingleCoreApplication::primaryUser() const {
Q_D(const SingleCoreApplication);
return d->primaryUser();
}
/**
* Returns the username the current instance is running as.
* @return Returns the username the current instance is running as.
*/
QString SingleCoreApplication::currentUser() const {
return SingleCoreApplicationPrivate::getUsername();
}
/**
* Sends message to the Primary Instance.
* @param message The message to send.
* @param timeout the maximum timeout in milliseconds for blocking functions.
* @return true if the message was sent successfully, false otherwise.
*/
bool SingleCoreApplication::sendMessage(const QByteArray &message, const int timeout) {
Q_D(SingleCoreApplication);
// Nobody to connect to
if (isPrimary()) return false;
// Make sure the socket is connected
if (!d->connectToPrimary(timeout, SingleCoreApplicationPrivate::Reconnect)) {
return false;
}
return d->writeConfirmedMessage(timeout, message);
}
/**
* Cleans up the shared memory block and exits with a failure.
* This function halts program execution.
*/
void SingleCoreApplication::abortSafely() {
Q_D(SingleCoreApplication);
qCritical() << "SingleCoreApplication: " << d->memory_->error() << d->memory_->errorString();
delete d;
::exit(EXIT_FAILURE);
}

View File

@@ -1,152 +0,0 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2020
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// W A R N I N G !!!
// -----------------
//
// This is a modified version of SingleApplication,
// The original version is at:
//
// https://github.com/itay-grudev/SingleApplication
//
//
#ifndef SINGLECOREAPPLICATION_H
#define SINGLECOREAPPLICATION_H
#include <QtGlobal>
#include <QCoreApplication>
#include <QFlags>
#include <QByteArray>
class SingleCoreApplicationPrivate;
/**
* @brief The SingleCoreApplication class handles multiple instances of the same Application
* @see QCoreApplication
*/
class SingleCoreApplication : public QCoreApplication { // clazy:exclude=ctor-missing-parent-argument
Q_OBJECT
using app_t = QCoreApplication;
public:
/**
* @brief Mode of operation of SingleCoreApplication.
* Whether the block should be user-wide or system-wide and whether the
* primary instance should be notified when a secondary instance had been
* started.
* @note Operating system can restrict the shared memory blocks to the same
* user, in which case the User/System modes will have no effect and the
* block will be user wide.
* @enum
*/
enum Mode {
User = 1 << 0,
System = 1 << 1,
SecondaryNotification = 1 << 2,
ExcludeAppVersion = 1 << 3,
ExcludeAppPath = 1 << 4
};
Q_DECLARE_FLAGS(Options, Mode)
/**
* @brief Intitializes a SingleCoreApplication instance with argc command line
* arguments in argv
* @arg {int &} argc - Number of arguments in argv
* @arg {const char *[]} argv - Supplied command line arguments
* @arg {bool} allowSecondary - Whether to start the instance as secondary
* if there is already a primary instance.
* @arg {Mode} mode - Whether for the SingleCoreApplication block to be applied
* User wide or System wide.
* @arg {int} timeout - Timeout to wait in milliseconds.
* @note argc and argv may be changed as Qt removes arguments that it
* recognizes
* @note Mode::SecondaryNotification only works if set on both the primary
* instance and the secondary instance.
* @note The timeout is just a hint for the maximum time of blocking
* operations. It does not guarantee that the SingleCoreApplication
* initialisation will be completed in given time, though is a good hint.
* Usually 4*timeout would be the worst case (fail) scenario.
*/
explicit SingleCoreApplication(int &argc, char *argv[], const bool allowSecondary = false, const Options options = Mode::User, const int timeout = 1000);
~SingleCoreApplication() override;
/**
* @brief Returns if the instance is the primary instance
* @returns {bool}
*/
bool isPrimary() const;
/**
* @brief Returns if the instance is a secondary instance
* @returns {bool}
*/
bool isSecondary() const;
/**
* @brief Returns a unique identifier for the current instance
* @returns {qint32}
*/
quint32 instanceId() const;
/**
* @brief Returns the process ID (PID) of the primary instance
* @returns {qint64}
*/
qint64 primaryPid() const;
/**
* @brief Returns the username of the user running the primary instance
* @returns {QString}
*/
QString primaryUser() const;
/**
* @brief Returns the username of the current user
* @returns {QString}
*/
QString currentUser() const;
/**
* @brief Sends a message to the primary instance. Returns true on success.
* @param {int} timeout - Timeout for connecting
* @returns {bool}
* @note sendMessage() will return false if invoked from the primary
* instance.
*/
bool sendMessage(const QByteArray &message, const int timeout = 1000);
signals:
void instanceStarted();
void receivedMessage(quint32 instanceId, QByteArray message);
private:
SingleCoreApplicationPrivate *d_ptr;
Q_DECLARE_PRIVATE(SingleCoreApplication)
void abortSafely();
};
Q_DECLARE_OPERATORS_FOR_FLAGS(SingleCoreApplication::Options)
#endif // SINGLECOREAPPLICATION_H

View File

@@ -0,0 +1,17 @@
cmake_minimum_required(VERSION 3.7)
add_definitions(-DSINGLECOREAPPLICATION)
set(SOURCES ../singleapplication_t.cpp ../singleapplication_p.cpp)
set(HEADERS ../singleapplication_t.h ../singleapplication_p.h)
qt_wrap_cpp(MOC ${HEADERS})
add_library(singlecoreapplication STATIC ${SOURCES} ${MOC})
target_include_directories(singlecoreapplication PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/..
${CMAKE_CURRENT_BINARY_DIR}/..
${Boost_INCLUDE_DIRS}
)
target_link_libraries(singlecoreapplication PUBLIC
${QtCore_LIBRARIES}
${QtNetwork_LIBRARIES}
)

View File

@@ -0,0 +1,13 @@
#ifndef SINGLECOREAPPLICATION_H
#define SINGLECOREAPPLICATION_H
#ifdef SINGLECOREAPPLICATION
# error "SINGLECOREAPPLICATION already defined."
#endif
#define SINGLECOREAPPLICATION
#include "../singleapplication_t.h"
#undef SINGLEAPPLICATION_T_H
#undef SINGLECOREAPPLICATION
#endif // SINGLECOREAPPLICATION_H

View File

@@ -1,526 +0,0 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2020
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// W A R N I N G !!!
// -----------------
//
// This is a modified version of SingleApplication,
// The original version is at:
//
// https://github.com/itay-grudev/SingleApplication
//
//
#include "config.h"
#include <QtGlobal>
#include <cstdlib>
#include <cstddef>
#ifdef Q_OS_UNIX
# include <unistd.h>
# include <sys/types.h>
# include <pwd.h>
#endif
#ifdef Q_OS_WIN
# ifndef NOMINMAX
# define NOMINMAX 1
# endif
# include <windows.h>
# include <lmcons.h>
#endif
#include <QObject>
#include <QThread>
#include <QIODevice>
#include <QSharedMemory>
#include <QByteArray>
#include <QDataStream>
#include <QCryptographicHash>
#include <QLocalServer>
#include <QLocalSocket>
#include <QElapsedTimer>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
#else
# include <QDateTime>
#endif
#include "singlecoreapplication.h"
#include "singlecoreapplication_p.h"
SingleCoreApplicationPrivate::SingleCoreApplicationPrivate(SingleCoreApplication *ptr)
: q_ptr(ptr),
memory_(nullptr),
socket_(nullptr),
server_(nullptr),
instanceNumber_(-1) {}
SingleCoreApplicationPrivate::~SingleCoreApplicationPrivate() {
if (socket_ != nullptr) {
socket_->close();
delete socket_;
socket_ = nullptr;
}
if (memory_ != nullptr) {
memory_->lock();
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
if (server_ != nullptr) {
server_->close();
delete server_;
instance->primary = false;
instance->primaryPid = -1;
instance->primaryUser[0] = '\0';
instance->checksum = blockChecksum();
}
memory_->unlock();
delete memory_;
memory_ = nullptr;
}
}
QString SingleCoreApplicationPrivate::getUsername() {
#ifdef Q_OS_UNIX
QString username;
#if defined(HAVE_GETEUID) && defined(HAVE_GETPWUID)
struct passwd *pw = getpwuid(geteuid());
if (pw) {
username = QString::fromLocal8Bit(pw->pw_name);
}
#endif
if (username.isEmpty()) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
username = qEnvironmentVariable("USER");
#else
username = QString::fromLocal8Bit(qgetenv("USER"));
#endif
}
return username;
#endif
#ifdef Q_OS_WIN
wchar_t username[UNLEN + 1];
// Specifies size of the buffer on input
DWORD usernameLength = UNLEN + 1;
if (GetUserNameW(username, &usernameLength)) {
return QString::fromWCharArray(username);
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
return qEnvironmentVariable("USERNAME");
#else
return QString::fromLocal8Bit(qgetenv("USERNAME"));
#endif
#endif
}
void SingleCoreApplicationPrivate::genBlockServerName() {
QCryptographicHash appData(QCryptographicHash::Sha256);
appData.addData("SingleApplication");
appData.addData(SingleCoreApplication::app_t::applicationName().toUtf8());
appData.addData(SingleCoreApplication::app_t::organizationName().toUtf8());
appData.addData(SingleCoreApplication::app_t::organizationDomain().toUtf8());
if (!(options_ & SingleCoreApplication::Mode::ExcludeAppVersion)) {
appData.addData(SingleCoreApplication::app_t::applicationVersion().toUtf8());
}
if (!(options_ & SingleCoreApplication::Mode::ExcludeAppPath)) {
#if defined(Q_OS_UNIX)
const QByteArray appImagePath = qgetenv("APPIMAGE");
if (appImagePath.isEmpty()) {
appData.addData(SingleCoreApplication::app_t::applicationFilePath().toUtf8());
}
else {
appData.addData(appImagePath);
};
#elif defined(Q_OS_WIN)
appData.addData(SingleCoreApplication::app_t::applicationFilePath().toLower().toUtf8());
#else
appData.addData(SingleCoreApplication::app_t::applicationFilePath().toUtf8());
#endif
}
// User level block requires a user specific data in the hash
if (options_ & SingleCoreApplication::Mode::User) {
appData.addData(getUsername().toUtf8());
}
// Replace the backslash in RFC 2045 Base64 [a-zA-Z0-9+/=] to comply with server naming requirements.
blockServerName_ = appData.result().toBase64().replace("/", "_");
}
void SingleCoreApplicationPrivate::initializeMemoryBlock() const {
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
instance->primary = false;
instance->secondary = 0;
instance->primaryPid = -1;
instance->primaryUser[0] = '\0';
instance->checksum = blockChecksum();
}
void SingleCoreApplicationPrivate::startPrimary() {
// Reset the number of connections
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
instance->primary = true;
instance->primaryPid = QCoreApplication::applicationPid();
qstrncpy(instance->primaryUser, getUsername().toUtf8().data(), sizeof(instance->primaryUser));
instance->checksum = blockChecksum();
instanceNumber_ = 0;
// Successful creation means that no main process exists
// So we start a QLocalServer to listen for connections
QLocalServer::removeServer(blockServerName_);
server_ = new QLocalServer();
// Restrict access to the socket according to the SingleCoreApplication::Mode::User flag on User level or no restrictions
if (options_ & SingleCoreApplication::Mode::User) {
server_->setSocketOptions(QLocalServer::UserAccessOption);
}
else {
server_->setSocketOptions(QLocalServer::WorldAccessOption);
}
server_->listen(blockServerName_);
QObject::connect(server_, &QLocalServer::newConnection, this, &SingleCoreApplicationPrivate::slotConnectionEstablished);
}
void SingleCoreApplicationPrivate::startSecondary() {
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
instance->secondary += 1;
instance->checksum = blockChecksum();
instanceNumber_ = instance->secondary;
}
bool SingleCoreApplicationPrivate::connectToPrimary(const int timeout, const ConnectionType connectionType) {
QElapsedTimer time;
time.start();
// Connect to the Local Server of the Primary Instance if not already connected.
if (socket_ == nullptr) {
socket_ = new QLocalSocket();
}
if (socket_->state() == QLocalSocket::ConnectedState) return true;
if (socket_->state() != QLocalSocket::ConnectedState) {
forever {
randomSleep();
if (socket_->state() != QLocalSocket::ConnectingState) {
socket_->connectToServer(blockServerName_);
}
if (socket_->state() == QLocalSocket::ConnectingState) {
socket_->waitForConnected(static_cast<int>(timeout - time.elapsed()));
}
// If connected break out of the loop
if (socket_->state() == QLocalSocket::ConnectedState) break;
// If elapsed time since start is longer than the method timeout return
if (time.elapsed() >= timeout) return false;
}
}
// Initialisation message according to the SingleCoreApplication protocol
QByteArray initMsg;
QDataStream writeStream(&initMsg, QIODevice::WriteOnly);
writeStream.setVersion(QDataStream::Qt_5_8);
writeStream << blockServerName_.toLatin1();
writeStream << static_cast<quint8>(connectionType);
writeStream << instanceNumber_;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
quint16 checksum = qChecksum(QByteArray(initMsg, static_cast<quint32>(initMsg.length())));
#else
quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length()));
#endif
writeStream << checksum;
return writeConfirmedMessage(static_cast<int>(timeout - time.elapsed()), initMsg);
}
void SingleCoreApplicationPrivate::writeAck(QLocalSocket *sock) {
sock->putChar('\n');
}
bool SingleCoreApplicationPrivate::writeConfirmedMessage(const int timeout, const QByteArray &msg) const {
QElapsedTimer time;
time.start();
// Frame 1: The header indicates the message length that follows
QByteArray header;
QDataStream headerStream(&header, QIODevice::WriteOnly);
headerStream.setVersion(QDataStream::Qt_5_8);
headerStream << static_cast<quint64>(msg.length());
if (!writeConfirmedFrame(static_cast<int>(timeout - time.elapsed()), header)) {
return false;
}
// Frame 2: The message
return writeConfirmedFrame(static_cast<int>(timeout - time.elapsed()), msg);
}
bool SingleCoreApplicationPrivate::writeConfirmedFrame(const int timeout, const QByteArray &msg) const {
socket_->write(msg);
socket_->flush();
bool result = socket_->waitForReadyRead(timeout);
if (result) {
socket_->read(1);
return true;
}
return false;
}
quint16 SingleCoreApplicationPrivate::blockChecksum() const {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
quint16 checksum = qChecksum(QByteArray(static_cast<const char*>(memory_->constData()), offsetof(InstancesInfo, checksum)));
#else
quint16 checksum = qChecksum(static_cast<const char*>(memory_->constData()), offsetof(InstancesInfo, checksum));
#endif
return checksum;
}
qint64 SingleCoreApplicationPrivate::primaryPid() const {
memory_->lock();
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
qint64 pid = instance->primaryPid;
memory_->unlock();
return pid;
}
QString SingleCoreApplicationPrivate::primaryUser() const {
memory_->lock();
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
QByteArray username = instance->primaryUser;
memory_->unlock();
return QString::fromUtf8(username);
}
/**
* @brief Executed when a connection has been made to the LocalServer
*/
void SingleCoreApplicationPrivate::slotConnectionEstablished() {
QLocalSocket *nextConnSocket = server_->nextPendingConnection();
connectionMap_.insert(nextConnSocket, ConnectionInfo());
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this, [nextConnSocket, this]() {
const ConnectionInfo &info = connectionMap_[nextConnSocket];
slotClientConnectionClosed(nextConnSocket, info.instanceId);
});
QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater);
QObject::connect(nextConnSocket, &QLocalSocket::destroyed, this, [nextConnSocket, this]() {
connectionMap_.remove(nextConnSocket);
});
QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this, [nextConnSocket, this]() {
const ConnectionInfo &info = connectionMap_[nextConnSocket];
switch (info.stage) {
case StageInitHeader:
readMessageHeader(nextConnSocket, StageInitBody);
break;
case StageInitBody:
readInitMessageBody(nextConnSocket);
break;
case StageConnectedHeader:
readMessageHeader(nextConnSocket, StageConnectedBody);
break;
case StageConnectedBody:
this->slotDataAvailable(nextConnSocket, info.instanceId);
break;
default:
break;
};
});
}
void SingleCoreApplicationPrivate::readMessageHeader(QLocalSocket *sock, SingleCoreApplicationPrivate::ConnectionStage nextStage) {
if (!connectionMap_.contains(sock)) {
return;
}
if (sock->bytesAvailable() < static_cast<qint64>(sizeof(quint64))) {
return;
}
QDataStream headerStream(sock);
headerStream.setVersion(QDataStream::Qt_5_8);
// Read the header to know the message length
quint64 msgLen = 0;
headerStream >> msgLen;
ConnectionInfo &info = connectionMap_[sock];
info.stage = nextStage;
info.msgLen = msgLen;
writeAck(sock);
}
bool SingleCoreApplicationPrivate::isFrameComplete(QLocalSocket *sock) {
if (!connectionMap_.contains(sock)) {
return false;
}
ConnectionInfo &info = connectionMap_[sock];
return (sock->bytesAvailable() >= static_cast<qint64>(info.msgLen));
}
void SingleCoreApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
Q_Q(SingleCoreApplication);
if (!isFrameComplete(sock)) {
return;
}
// Read the message body
QByteArray msgBytes = sock->readAll();
QDataStream readStream(msgBytes);
readStream.setVersion(QDataStream::Qt_5_8);
// server name
QByteArray latin1Name;
readStream >> latin1Name;
// connection type
ConnectionType connectionType = InvalidConnection;
quint8 connTypeVal = InvalidConnection;
readStream >> connTypeVal;
connectionType = static_cast<ConnectionType>(connTypeVal);
// instance id
quint32 instanceId = 0;
readStream >> instanceId;
// checksum
quint16 msgChecksum = 0;
readStream >> msgChecksum;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
const quint16 actualChecksum = qChecksum(QByteArray(msgBytes, static_cast<quint32>(msgBytes.length() - sizeof(quint16))));
#else
const quint16 actualChecksum = qChecksum(msgBytes.constData(), static_cast<quint32>(msgBytes.length() - sizeof(quint16)));
#endif
bool isValid = readStream.status() == QDataStream::Ok && QLatin1String(latin1Name) == blockServerName_ && msgChecksum == actualChecksum;
if (!isValid) {
sock->close();
return;
}
ConnectionInfo &info = connectionMap_[sock];
info.instanceId = instanceId;
info.stage = StageConnectedHeader;
if (connectionType == NewInstance || (connectionType == SecondaryInstance && options_ & SingleCoreApplication::Mode::SecondaryNotification)) {
emit q->instanceStarted();
}
writeAck(sock);
}
void SingleCoreApplicationPrivate::slotDataAvailable(QLocalSocket *dataSocket, const quint32 instanceId) {
Q_Q(SingleCoreApplication);
if (!isFrameComplete(dataSocket)) {
return;
}
const QByteArray message = dataSocket->readAll();
writeAck(dataSocket);
ConnectionInfo &info = connectionMap_[dataSocket];
info.stage = StageConnectedHeader;
emit q->receivedMessage(instanceId, message);
}
void SingleCoreApplicationPrivate::slotClientConnectionClosed(QLocalSocket *closedSocket, const quint32 instanceId) {
if (closedSocket->bytesAvailable() > 0) {
slotDataAvailable(closedSocket, instanceId);
}
}
void SingleCoreApplicationPrivate::randomSleep() {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
QThread::msleep(QRandomGenerator::global()->bounded(8U, 18U));
#else
qsrand(QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max());
QThread::msleep(qrand() % 11 + 8);
#endif
}

View File

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

View File

@@ -2,8 +2,10 @@ cmake_minimum_required(VERSION 3.7)
project(strawberry)
cmake_policy(SET CMP0054 NEW)
if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.12)
if(POLICY CMP0054)
cmake_policy(SET CMP0054 NEW)
endif()
if(POLICY CMP0074)
cmake_policy(SET CMP0074 NEW)
endif()
@@ -309,7 +311,10 @@ endif()
# SingleApplication
add_subdirectory(3rdparty/singleapplication)
set(SINGLEAPPLICATION_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/singleapplication)
set(SINGLEAPPLICATION_INCLUDE_DIRS
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/singleapplication/singleapplication
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/singleapplication/singlecoreapplication
)
set(SINGLEAPPLICATION_LIBRARIES singleapplication)
set(SINGLECOREAPPLICATION_LIBRARIES singlecoreapplication)
@@ -376,12 +381,12 @@ optional_component(VLC ON "Engine: VLC backend"
optional_component(SONGFINGERPRINTING ON "Song fingerprinting and tracking"
DEPENDS "chromaprint" CHROMAPRINT_FOUND
DEPENDS "gstreamer" GSTREAMER_FOUND
DEPENDS "gstreamer" HAVE_GSTREAMER
)
optional_component(MUSICBRAINZ ON "MusicBrainz integration"
DEPENDS "chromaprint" CHROMAPRINT_FOUND
DEPENDS "gstreamer" GSTREAMER_FOUND
DEPENDS "gstreamer" HAVE_GSTREAMER
)
if(X11_FOUND OR HAVE_DBUS OR APPLE OR WIN32)
@@ -406,7 +411,7 @@ endif()
optional_component(AUDIOCD ON "Devices: Audio CD support"
DEPENDS "libcdio" LIBCDIO_FOUND
DEPENDS "gstreamer" GSTREAMER_FOUND
DEPENDS "gstreamer" HAVE_GSTREAMER
)
optional_component(UDISKS2 ON "Devices: UDisks2 backend"

112
CONTRIBUTING.md Normal file
View File

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

View File

@@ -2,6 +2,51 @@ Strawberry Music Player
=======================
ChangeLog
Version 1.0.17 (2023.03.29):
Bugfixes:
* Fixed over-sized context album cover with device pixel ratio higher than 1.0 (#1166).
* Fixed playing widget fading from a blurry previous cover with device pixel ratio higher than 1.0.
* Made playlist source icon, album cover manager and OSD pretty cover respect device pixel ratio.
Version 1.0.16 (2023.03.27):
Bugfixes:
* Fixed lyrics from Musixmatch.
* Fixed possible file corruption when saving both tags and embedded cover using the tag editor (#1158).
* Fixed compile without GStreamer.
* Fixed context and playing now album art rendering on High DPI displays (#1161).
* Fixed setting source properties (device, user-agent, ssl-strict) with GStreamer 1.22 (playbin3) and higher (#1148).
* Fixed rescan songs feature not ignoring mtime.
* Search lyrics by artist instead of album artist by default.
Code improvements:
* Replace use of deprecated QSqlDatabase::exec().
Added features:
* Added backend setting for strict SSL mode.
* Read AcoustID and MusicBrainz tags.
* Submit MusicBrainz tags with ListenBrainz.
Version 1.0.15 (2023.03.04):
Bugfixes:
* Fixed playlist column showing invalid last played date for streams.
* Fixed crash when the audio bin failed to initialize (#1123, #1133).
* Fixed duplicated filename when organizing files using dot in the filename (#1136).
* Fixed tag inline editing for streams (#1130).
* Fixed resetting play statistics using tag edit dialog (#1124).
* Fixed compilation songs not showing if group by was set to other than (Album) Artist / Album (#1140).
Enhancements:
* Added lyrics from stands4 (lyrics.com).
* Added Sonogram analyzer.
* Use GStreamer playbin3 with GStreamer 1.22.0 and higher.
Code improvements:
* Made use of C++11 enum class where possible.
* Use new QNativeIpcKey based QSharedMemory constructor with Qt 6.6 and higher.
Version 1.0.14 (2023.01.13):
Bugfixes:

View File

@@ -51,7 +51,7 @@ Funding developers is a way to contribute to open source projects you appreciate
* Edit tags on audio files
* Fetch tags from MusicBrainz
* Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/) and [Spotify](https://www.spotify.com/)
* Song lyrics from [AudD](https://audd.io/), [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/) and [lololyrics.com](https://www.lololyrics.com/)
* Song lyrics from [Lyrics.com](https://www.lyrics.com/), [AudD](https://audd.io/), [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/) and [lololyrics.com](https://www.lololyrics.com/)
* Support for multiple backends
* Audio analyzer
* Audio equalizer

View File

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

View File

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

View File

@@ -67,7 +67,21 @@ CREATE TABLE device_%deviceid_songs (
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -80,4 +94,4 @@ CREATE VIRTUAL TABLE device_%deviceid_fts USING fts5(
tokenize = "unicode61 remove_diacritics 1"
);
UPDATE devices SET schema_version=3 WHERE ROWID=%deviceid;
UPDATE devices SET schema_version=4 WHERE ROWID=%deviceid;

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

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

View File

@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
DELETE FROM schema_version;
INSERT INTO schema_version (version) VALUES (15);
INSERT INTO schema_version (version) VALUES (16);
CREATE TABLE IF NOT EXISTS directories (
path TEXT NOT NULL,
@@ -75,7 +75,21 @@ CREATE TABLE IF NOT EXISTS songs (
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -137,7 +151,21 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -199,7 +227,21 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -261,7 +303,21 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -323,7 +379,21 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -385,7 +455,21 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -447,7 +531,21 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -509,7 +607,21 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -591,7 +703,21 @@ CREATE TABLE IF NOT EXISTS playlist_items (
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);

2
debian/control.in vendored
View File

@@ -52,7 +52,7 @@ Description: music player and music collection organizer
- Edit tags on audio files
- Automatically retrieve tags from MusicBrainz
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
- Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
- Song lyrics from Lyrics.com, AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
- Audio analyzer
- Audio equalizer
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic

View File

@@ -29,7 +29,7 @@
<li>Edit tags on audio files</li>
<li>Automatically retrieve tags from MusicBrainz</li>
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
<li>Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com</li>
<li>Song lyrics from Lyrics.com, AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com</li>
<li>Support for multiple backends</li>
<li>Audio analyzer and equalizer</li>
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>

View File

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

View File

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

View File

@@ -1,57 +1,68 @@
!define build_type ""
!define compiler "unknown"
!define arch "unknown"
!if "@ARCH@" == "x86"
!define arch_x86
!undef arch
!define arch "x86"
!endif
!if "@ARCH@" == "i686-w64-mingw32.shared"
!define arch_x86
!undef arch
!define arch "x86"
!endif
!if "@ARCH@" == "x86_64"
!define arch_x64
!undef arch
!define arch "x64"
!endif
!if "@ARCH@" == "x86_64-w64-mingw32.shared"
!define arch_x64
!undef arch
!define arch "x64"
!endif
!if "@MINGW@" == "1"
!define mingw
!undef compiler
!define compiler "mingw"
!endif
!if "@MSVC@" == "1"
!define msvc
!undef compiler
!define compiler "msvc"
!endif
!if "@ARCH@" == "x86"
!define arch_x86
!else if "@ARCH@" == "i686"
!define arch_x86
!else if "@ARCH@" == "i686-w64-mingw32.shared"
!define arch_x86
!else if "@ARCH@" == "x64"
!define arch_x64
!else if "@ARCH@" == "x86_64"
!define arch_x64
!else if "@ARCH@" == "x86_64-w64-mingw32.shared"
!define arch_x64
!endif
!ifdef arch_x86
!define arch "x86"
!endif
!ifdef arch_x64
!define arch "x64"
!endif
!if "@CMAKE_BUILD_TYPE@" == "Release"
!define release
!endif
!if "@CMAKE_BUILD_TYPE@" == "RelWithDebInfo"
!else if "@CMAKE_BUILD_TYPE@" == "RelWithDebInfo"
!define release
!else if "@CMAKE_BUILD_TYPE@" == "Debug"
!define debug
!endif
!if "@CMAKE_BUILD_TYPE@" == "Debug"
!define debug
!undef build_type
!ifdef release
!define build_type ""
!endif
!ifdef debug
!define build_type "-Debug"
!endif
!ifndef compiler
!error "Missing compiler."
!endif
!ifndef build_type
!error "Missing build type."
!endif
!ifndef arch
!error "Missing arch."
!endif
!ifdef debug
!define PRODUCT_NAME "Strawberry Music Player Debug"
!define PRODUCT_NAME_SHORT "Strawberry"
@@ -241,10 +252,10 @@ Section "Strawberry" Strawberry
File "libssl-3-x64.dll"
!endif
File "avcodec-59.dll"
File "avfilter-8.dll"
File "avformat-59.dll"
File "avutil-57.dll"
File "avcodec-60.dll"
File "avfilter-9.dll"
File "avformat-60.dll"
File "avutil-58.dll"
File "libFLAC-12.dll"
File "libbrotlicommon.dll"
File "libbrotlidec.dll"
@@ -301,7 +312,6 @@ Section "Strawberry" Strawberry
File "libopus-0.dll"
File "liborc-0.4-0.dll"
File "libpng16-16.dll"
File "libprotobuf-32.dll"
File "libpsl-5.dll"
File "libqtsparkle-qt6.dll"
File "libsoup-3.0-0.dll"
@@ -320,11 +330,51 @@ Section "Strawberry" Strawberry
File "libwinpthread-1.dll"
File "libxml2-2.dll"
File "libzstd.dll"
File "postproc-56.dll"
File "postproc-57.dll"
File "swresample-4.dll"
File "swscale-6.dll"
File "swscale-7.dll"
File "zlib1.dll"
File "libabsl_base.dll"
File "libabsl_city.dll"
File "libabsl_cord.dll"
File "libabsl_cord_internal.dll"
File "libabsl_cordz_handle.dll"
File "libabsl_cordz_info.dll"
File "libabsl_crc32c.dll"
File "libabsl_crc_cord_state.dll"
File "libabsl_crc_internal.dll"
File "libabsl_die_if_null.dll"
File "libabsl_examine_stack.dll"
File "libabsl_hash.dll"
File "libabsl_int128.dll"
File "libabsl_log_globals.dll"
File "libabsl_log_internal_check_op.dll"
File "libabsl_log_internal_format.dll"
File "libabsl_log_internal_globals.dll"
File "libabsl_log_internal_log_sink_set.dll"
File "libabsl_log_internal_message.dll"
File "libabsl_log_internal_nullguard.dll"
File "libabsl_log_internal_proto.dll"
File "libabsl_log_sink.dll"
File "libabsl_low_level_hash.dll"
File "libabsl_malloc_internal.dll"
File "libabsl_raw_hash_set.dll"
File "libabsl_raw_logging_internal.dll"
File "libabsl_spinlock_wait.dll"
File "libabsl_stacktrace.dll"
File "libabsl_status.dll"
File "libabsl_statusor.dll"
File "libabsl_strerror.dll"
File "libabsl_str_format_internal.dll"
File "libabsl_strings.dll"
File "libabsl_strings_internal.dll"
File "libabsl_symbolize.dll"
File "libabsl_synchronization.dll"
File "libabsl_throw_delegate.dll"
File "libabsl_time.dll"
File "libabsl_time_zone.dll"
!ifdef debug
File "gdb.exe"
File "libexpat-1.dll"
@@ -334,6 +384,7 @@ Section "Strawberry" Strawberry
File "libpcre2-16d.dll"
File "libreadline8.dll"
File "libtermcap.dll"
File "libabsl_graphcycles_internal.dll"
!else
File "libpcre2-8.dll"
File "libpcre2-16.dll"
@@ -393,7 +444,6 @@ Section "Strawberry" Strawberry
File "jpeg62.dll"
File "libbs2b.dll"
File "libfaac_dll.dll"
File "libiconv.dll"
File "liblzma.dll"
File "libmp3lame.dll"
File "libopenmpt.dll"
@@ -415,11 +465,12 @@ Section "Strawberry" Strawberry
File "vorbis.dll"
File "vorbisfile.dll"
File "wavpackdll.dll"
File "abseil_dll.dll"
!ifdef release
File "freetype.dll"
File "libiconv.dll"
File "libpng16.dll"
File "libprotobuf.dll"
File "libxml2.dll"
File "pcre2-8.dll"
File "pcre2-16.dll"
@@ -428,8 +479,8 @@ Section "Strawberry" Strawberry
!endif
!ifdef debug
File "freetyped.dll"
File "libiconvd.dll"
File "libpng16d.dll"
File "libprotobufd.dll"
File "libxml2d.dll"
File "pcre2-8d.dll"
File "pcre2-16d.dll"
@@ -443,6 +494,11 @@ Section "Strawberry" Strawberry
File "icudt72.dll"
File "libfftw3-3.dll"
!ifdef debug
File "libprotobufd.dll"
!else
File "libprotobuf.dll"
!endif
!ifdef msvc && debug
File "icuin72d.dll"
File "icuuc72d.dll"
@@ -743,10 +799,10 @@ Section "Uninstall"
Delete "$INSTDIR\libssl-3-x64.dll"
!endif
Delete "$INSTDIR\avcodec-59.dll"
Delete "$INSTDIR\avfilter-8.dll"
Delete "$INSTDIR\avformat-59.dll"
Delete "$INSTDIR\avutil-57.dll"
Delete "$INSTDIR\avcodec-60.dll"
Delete "$INSTDIR\avfilter-9.dll"
Delete "$INSTDIR\avformat-60.dll"
Delete "$INSTDIR\avutil-58.dll"
Delete "$INSTDIR\libFLAC-12.dll"
Delete "$INSTDIR\libbrotlicommon.dll"
Delete "$INSTDIR\libbrotlidec.dll"
@@ -803,7 +859,6 @@ Section "Uninstall"
Delete "$INSTDIR\libopus-0.dll"
Delete "$INSTDIR\liborc-0.4-0.dll"
Delete "$INSTDIR\libpng16-16.dll"
Delete "$INSTDIR\libprotobuf-32.dll"
Delete "$INSTDIR\libpsl-5.dll"
Delete "$INSTDIR\libqtsparkle-qt6.dll"
Delete "$INSTDIR\libsoup-3.0-0.dll"
@@ -822,11 +877,51 @@ Section "Uninstall"
Delete "$INSTDIR\libwinpthread-1.dll"
Delete "$INSTDIR\libxml2-2.dll"
Delete "$INSTDIR\libzstd.dll"
Delete "$INSTDIR\postproc-56.dll"
Delete "$INSTDIR\postproc-57.dll"
Delete "$INSTDIR\swresample-4.dll"
Delete "$INSTDIR\swscale-6.dll"
Delete "$INSTDIR\swscale-7.dll"
Delete "$INSTDIR\zlib1.dll"
Delete "$INSTDIR\libabsl_base.dll"
Delete "$INSTDIR\libabsl_city.dll"
Delete "$INSTDIR\libabsl_cord.dll"
Delete "$INSTDIR\libabsl_cord_internal.dll"
Delete "$INSTDIR\libabsl_cordz_handle.dll"
Delete "$INSTDIR\libabsl_cordz_info.dll"
Delete "$INSTDIR\libabsl_crc32c.dll"
Delete "$INSTDIR\libabsl_crc_cord_state.dll"
Delete "$INSTDIR\libabsl_crc_internal.dll"
Delete "$INSTDIR\libabsl_die_if_null.dll"
Delete "$INSTDIR\libabsl_examine_stack.dll"
Delete "$INSTDIR\libabsl_hash.dll"
Delete "$INSTDIR\libabsl_int128.dll"
Delete "$INSTDIR\libabsl_log_globals.dll"
Delete "$INSTDIR\libabsl_log_internal_check_op.dll"
Delete "$INSTDIR\libabsl_log_internal_format.dll"
Delete "$INSTDIR\libabsl_log_internal_globals.dll"
Delete "$INSTDIR\libabsl_log_internal_log_sink_set.dll"
Delete "$INSTDIR\libabsl_log_internal_message.dll"
Delete "$INSTDIR\libabsl_log_internal_nullguard.dll"
Delete "$INSTDIR\libabsl_log_internal_proto.dll"
Delete "$INSTDIR\libabsl_log_sink.dll"
Delete "$INSTDIR\libabsl_low_level_hash.dll"
Delete "$INSTDIR\libabsl_malloc_internal.dll"
Delete "$INSTDIR\libabsl_raw_hash_set.dll"
Delete "$INSTDIR\libabsl_raw_logging_internal.dll"
Delete "$INSTDIR\libabsl_spinlock_wait.dll"
Delete "$INSTDIR\libabsl_stacktrace.dll"
Delete "$INSTDIR\libabsl_status.dll"
Delete "$INSTDIR\libabsl_statusor.dll"
Delete "$INSTDIR\libabsl_strerror.dll"
Delete "$INSTDIR\libabsl_str_format_internal.dll"
Delete "$INSTDIR\libabsl_strings.dll"
Delete "$INSTDIR\libabsl_strings_internal.dll"
Delete "$INSTDIR\libabsl_symbolize.dll"
Delete "$INSTDIR\libabsl_synchronization.dll"
Delete "$INSTDIR\libabsl_throw_delegate.dll"
Delete "$INSTDIR\libabsl_time.dll"
Delete "$INSTDIR\libabsl_time_zone.dll"
!ifdef debug
Delete "$INSTDIR\gdb.exe"
Delete "$INSTDIR\libexpat-1.dll"
@@ -836,6 +931,7 @@ Section "Uninstall"
Delete "$INSTDIR\libpcre2-16d.dll"
Delete "$INSTDIR\libreadline8.dll"
Delete "$INSTDIR\libtermcap.dll"
Delete "$INSTDIR\libabsl_graphcycles_internal.dll"
!else
Delete "$INSTDIR\libpcre2-8.dll"
Delete "$INSTDIR\libpcre2-16.dll"
@@ -895,7 +991,6 @@ Section "Uninstall"
Delete "$INSTDIR\jpeg62.dll"
Delete "$INSTDIR\libbs2b.dll"
Delete "$INSTDIR\libfaac_dll.dll"
Delete "$INSTDIR\libiconv.dll"
Delete "$INSTDIR\liblzma.dll"
Delete "$INSTDIR\libmp3lame.dll"
Delete "$INSTDIR\libopenmpt.dll"
@@ -917,11 +1012,12 @@ Section "Uninstall"
Delete "$INSTDIR\vorbis.dll"
Delete "$INSTDIR\vorbisfile.dll"
Delete "$INSTDIR\wavpackdll.dll"
Delete "$INSTDIR\abseil_dll.dll"
!ifdef release
Delete "$INSTDIR\freetype.dll"
Delete "$INSTDIR\libiconv.dll"
Delete "$INSTDIR\libpng16.dll"
Delete "$INSTDIR\libprotobuf.dll"
Delete "$INSTDIR\libxml2.dll"
Delete "$INSTDIR\pcre2-8.dll"
Delete "$INSTDIR\pcre2-16.dll"
@@ -930,8 +1026,8 @@ Section "Uninstall"
!endif
!ifdef debug
Delete "$INSTDIR\freetyped.dll"
Delete "$INSTDIR\libiconvd.dll"
Delete "$INSTDIR\libpng16d.dll"
Delete "$INSTDIR\libprotobufd.dll"
Delete "$INSTDIR\libxml2d.dll"
Delete "$INSTDIR\pcre2-8d.dll"
Delete "$INSTDIR\pcre2-16d.dll"
@@ -945,6 +1041,11 @@ Section "Uninstall"
Delete "$INSTDIR\icudt72.dll"
Delete "$INSTDIR\libfftw3-3.dll"
!ifdef debug
Delete "$INSTDIR\libprotobufd.dll"
!else
Delete "$INSTDIR\libprotobuf.dll"
!endif
!ifdef msvc && debug
Delete "$INSTDIR\icuin72d.dll"
Delete "$INSTDIR\icuuc72d.dll"

View File

@@ -165,10 +165,10 @@ class WorkerPool : public _WorkerPoolBase {
template<typename HandlerType>
WorkerPool<HandlerType>::WorkerPool(QObject *parent)
: _WorkerPoolBase(parent),
worker_count_(1),
next_worker_(0),
next_id_(0) {
worker_count_ = qBound(1, QThread::idealThreadCount() / 2, 4);
local_server_name_ = qApp->applicationName().toLower();
if (local_server_name_.isEmpty()) {

View File

@@ -43,12 +43,21 @@ target_include_directories(libstrawberry-tagreader PRIVATE
target_link_libraries(libstrawberry-tagreader PRIVATE
${GLIB_LIBRARIES}
${PROTOBUF_LIBRARY}
${Protobuf_LIBRARIES}
${QtCore_LIBRARIES}
${QtNetwork_LIBRARIES}
${QtGui_LIBRARIES}
libstrawberry-common
)
if(WIN32 AND Protobuf_VERSION VERSION_GREATER_EQUAL 4.22.0)
if (MSVC)
target_link_libraries(libstrawberry-tagreader PRIVATE abseil_dll)
else()
target_link_libraries(libstrawberry-tagreader PRIVATE absl_log_internal_message absl_log_internal_check_op)
endif()
endif()
if(USE_TAGLIB AND TAGLIB_FOUND)
target_include_directories(libstrawberry-tagreader SYSTEM PRIVATE ${TAGLIB_INCLUDE_DIRS})
target_link_libraries(libstrawberry-tagreader PRIVATE ${TAGLIB_LIBRARIES})

View File

@@ -19,6 +19,15 @@
#include <string>
#include <QByteArray>
#include <QString>
#include <QIODevice>
#include <QFile>
#include <QBuffer>
#include <QImage>
#include <QMimeDatabase>
#include "core/logging.h"
#include "tagreaderbase.h"
const std::string TagReaderBase::kEmbeddedCover = "(embedded)";
@@ -26,12 +35,6 @@ const std::string TagReaderBase::kEmbeddedCover = "(embedded)";
TagReaderBase::TagReaderBase() = default;
TagReaderBase::~TagReaderBase() = default;
void TagReaderBase::Decode(const QString &tag, std::string *output) {
output->assign(DataCommaSizeFromQString(tag));
}
float TagReaderBase::ConvertPOPMRating(const int POPM_rating) {
if (POPM_rating < 0x01) return 0.0F;
@@ -55,3 +58,86 @@ int TagReaderBase::ConvertToPOPMRating(const float rating) {
return 0xFF;
}
QByteArray TagReaderBase::LoadCoverDataFromRequest(const spb::tagreader::SaveFileRequest &request) {
if (!request.has_save_cover() || !request.save_cover()) {
return QByteArray();
}
const QString song_filename = QString::fromUtf8(request.filename().data(), request.filename().size());
QString cover_filename;
if (request.has_cover_filename()) {
cover_filename = QString::fromUtf8(request.cover_filename().data(), request.cover_filename().size());
}
QByteArray cover_data;
if (request.has_cover_data()) {
cover_data = QByteArray(request.cover_data().data(), request.cover_data().size());
}
bool cover_is_jpeg = false;
if (request.has_cover_is_jpeg()) {
cover_is_jpeg = request.cover_is_jpeg();
}
return LoadCoverDataFromRequest(song_filename, cover_filename, cover_data, cover_is_jpeg);
}
QByteArray TagReaderBase::LoadCoverDataFromRequest(const spb::tagreader::SaveEmbeddedArtRequest &request) {
const QString song_filename = QString::fromUtf8(request.filename().data(), request.filename().size());
QString cover_filename;
if (request.has_cover_filename()) {
cover_filename = QString::fromUtf8(request.cover_filename().data(), request.cover_filename().size());
}
QByteArray cover_data;
if (request.has_cover_data()) {
cover_data = QByteArray(request.cover_data().data(), request.cover_data().size());
}
bool cover_is_jpeg = false;
if (request.has_cover_is_jpeg()) {
cover_is_jpeg = request.cover_is_jpeg();
}
return LoadCoverDataFromRequest(song_filename, cover_filename, cover_data, cover_is_jpeg);
}
QByteArray TagReaderBase::LoadCoverDataFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, const bool cover_is_jpeg) {
if (!cover_data.isEmpty() && cover_is_jpeg) {
qLog(Debug) << "Using cover from JPEG data for" << song_filename;
return cover_data;
}
if (cover_data.isEmpty() && !cover_filename.isEmpty()) {
qLog(Debug) << "Loading cover from" << cover_filename << "for" << song_filename;
QFile file(cover_filename);
if (!file.open(QIODevice::ReadOnly)) {
qLog(Error) << "Failed to open file" << cover_filename << "for reading:" << file.errorString();
return QByteArray();
}
cover_data = file.readAll();
file.close();
}
if (!cover_data.isEmpty()) {
if (QMimeDatabase().mimeTypeForData(cover_data).name() == "image/jpeg") {
qLog(Debug) << "Using cover from JPEG data for" << song_filename;
return cover_data;
}
// Convert image to JPEG.
qLog(Debug) << "Converting cover to JPEG data for" << song_filename;
QImage cover_image(cover_data);
cover_data.clear();
QBuffer buffer(&cover_data);
if (buffer.open(QIODevice::WriteOnly)) {
cover_image.save(&buffer, "JPEG");
buffer.close();
}
return cover_data;
}
return QByteArray();
}

View File

@@ -27,9 +27,6 @@
#include "tagreadermessages.pb.h"
#define QStringFromStdString(x) QString::fromUtf8((x).data(), (x).size())
#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length()
/*
* This class holds all useful methods to read and write tags from/to files.
* You should not use it directly in the main process but rather use a TagReaderWorker process (using TagReaderClient)
@@ -42,19 +39,23 @@ class TagReaderBase {
virtual bool IsMediaFile(const QString &filename) const = 0;
virtual bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const = 0;
virtual bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
virtual bool SaveFile(const spb::tagreader::SaveFileRequest &request) const = 0;
virtual QByteArray LoadEmbeddedArt(const QString &filename) const = 0;
virtual bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) = 0;
virtual bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const = 0;
virtual bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
virtual bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
static void Decode(const QString &tag, std::string *output);
static float ConvertPOPMRating(const int POPM_rating);
static int ConvertToPOPMRating(const float rating);
static QByteArray LoadCoverDataFromRequest(const spb::tagreader::SaveFileRequest &request);
static QByteArray LoadCoverDataFromRequest(const spb::tagreader::SaveEmbeddedArtRequest &request);
private:
static QByteArray LoadCoverDataFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, const bool cover_is_jpeg);
protected:
static const std::string kEmbeddedCover;

View File

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

View File

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

View File

@@ -73,8 +73,30 @@ message SongMetadata {
optional float rating = 32;
optional bool suspicious_tags = 40;
optional string acoustid_id = 33;
optional string acoustid_fingerprint = 34;
optional string musicbrainz_album_artist_id = 35; // MusicBrainz Release Artist ID (MUSICBRAINZ_ALBUMARTISTID)
optional string musicbrainz_artist_id = 36; // MusicBrainz Artist ID (MUSICBRAINZ_ARTISTID)
optional string musicbrainz_original_artist_id = 37; // MusicBrainz Original Artist ID (MUSICBRAINZ_ORIGINALARTISTID)
optional string musicbrainz_album_id = 38; // MusicBrainz Release ID (MUSICBRAINZ_ALBUMID)
optional string musicbrainz_original_album_id = 39; // MusicBrainz Original Release ID (MUSICBRAINZ_ORIGINALALBUMID)
optional string musicbrainz_recording_id = 40; // MusicBrainz Recording ID (MUSICBRAINZ_TRACKID)
optional string musicbrainz_track_id = 41; // MusicBrainz Track ID (MUSICBRAINZ_RELEASETRACKID)
optional string musicbrainz_disc_id = 42; // MusicBrainz Disc ID (MUSICBRAINZ_DISCID)
optional string musicbrainz_release_group_id = 43; // MusicBrainz Release Group ID (MUSICBRAINZ_RELEASEGROUPID)
optional string musicbrainz_work_id = 44; // MusicBrainz Work ID (MUSICBRAINZ_WORKID)
optional bool suspicious_tags = 50;
}
message IsMediaFileRequest {
optional string filename = 1;
}
message IsMediaFileResponse {
optional bool success = 1;
}
message ReadFileRequest {
@@ -87,21 +109,20 @@ message ReadFileResponse {
message SaveFileRequest {
optional string filename = 1;
optional SongMetadata metadata = 2;
optional bool save_tags = 2;
optional bool save_playcount = 3;
optional bool save_rating = 4;
optional bool save_cover = 5;
optional SongMetadata metadata = 6;
optional string cover_filename = 7;
optional bytes cover_data = 8;
optional bool cover_is_jpeg = 9;
}
message SaveFileResponse {
optional bool success = 1;
}
message IsMediaFileRequest {
optional string filename = 1;
}
message IsMediaFileResponse {
optional bool success = 1;
}
message LoadEmbeddedArtRequest {
optional string filename = 1;
}
@@ -112,7 +133,9 @@ message LoadEmbeddedArtResponse {
message SaveEmbeddedArtRequest {
optional string filename = 1;
optional bytes data = 2;
optional string cover_filename = 2;
optional bytes cover_data = 3;
optional bool cover_is_jpeg = 4;
}
message SaveEmbeddedArtResponse {

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -55,6 +55,7 @@ set(SOURCES
utilities/transliterate.cpp
utilities/xmlutils.cpp
utilities/filemanagerutils.cpp
utilities/coverutils.cpp
engine/enginetype.cpp
engine/enginebase.cpp
@@ -67,6 +68,7 @@ set(SOURCES
analyzer/blockanalyzer.cpp
analyzer/boomanalyzer.cpp
analyzer/rainbowanalyzer.cpp
analyzer/sonogram.cpp
equalizer/equalizer.cpp
equalizer/equalizerslider.cpp
@@ -167,6 +169,8 @@ set(SOURCES
lyrics/lyricsproviders.cpp
lyrics/lyricsprovider.cpp
lyrics/lyricssearchrequest.h
lyrics/lyricssearchresult.h
lyrics/lyricsfetcher.cpp
lyrics/lyricsfetchersearch.cpp
lyrics/jsonlyricsprovider.cpp
@@ -176,6 +180,7 @@ set(SOURCES
lyrics/geniuslyricsprovider.cpp
lyrics/musixmatchlyricsprovider.cpp
lyrics/chartlyricsprovider.cpp
lyrics/lyricscomlyricsprovider.cpp
providers/musixmatchprovider.cpp
@@ -265,6 +270,7 @@ set(SOURCES
scrobbler/scrobblerservice.cpp
scrobbler/scrobblercache.cpp
scrobbler/scrobblercacheitem.cpp
scrobbler/scrobblemetadata.cpp
scrobbler/scrobblingapi20.cpp
scrobbler/lastfmscrobbler.cpp
scrobbler/librefmscrobbler.cpp
@@ -310,6 +316,7 @@ set(HEADERS
analyzer/blockanalyzer.h
analyzer/boomanalyzer.h
analyzer/rainbowanalyzer.h
analyzer/sonogram.h
equalizer/equalizer.h
equalizer/equalizerslider.h
@@ -411,6 +418,7 @@ set(HEADERS
lyrics/geniuslyricsprovider.h
lyrics/musixmatchlyricsprovider.h
lyrics/chartlyricsprovider.h
lyrics/lyricscomlyricsprovider.h
settings/settingsdialog.h
settings/settingspage.h
@@ -495,7 +503,6 @@ set(HEADERS
scrobbler/scrobblerservices.h
scrobbler/scrobblerservice.h
scrobbler/scrobblercache.h
scrobbler/scrobblercacheitem.h
scrobbler/scrobblingapi20.h
scrobbler/lastfmscrobbler.h
scrobbler/librefmscrobbler.h
@@ -962,6 +969,7 @@ link_directories(
${GOBJECT_LIBRARY_DIRS}
${GNUTLS_LIBRARY_DIRS}
${SQLITE_LIBRARY_DIRS}
${PROTOBUF_LIBRARY_DIRS}
${SINGLEAPPLICATION_LIBRARY_DIRS}
${SINGLECOREAPPLICATION_LIBRARY_DIRS}
)
@@ -1081,6 +1089,7 @@ target_link_libraries(strawberry_lib PUBLIC
${GNUTLS_LIBRARIES}
${SQLITE_LIBRARIES}
${QT_LIBRARIES}
${Protobuf_LIBRARIES}
${SINGLEAPPLICATION_LIBRARIES}
${SINGLECOREAPPLICATION_LIBRARIES}
libstrawberry-common

View File

@@ -108,7 +108,7 @@ void Analyzer::Base::paintEvent(QPaintEvent *e) {
p.fillRect(e->rect(), palette().color(QPalette::Window));
switch (engine_->state()) {
case Engine::Playing: {
case Engine::State::Playing: {
const Engine::Scope &thescope = engine_->scope(timeout_);
int i = 0;
@@ -126,7 +126,7 @@ void Analyzer::Base::paintEvent(QPaintEvent *e) {
break;
}
case Engine::Paused:
case Engine::State::Paused:
is_playing_ = false;
analyze(p, lastscope_, new_frame_);
break;

View File

@@ -40,6 +40,7 @@
#include "blockanalyzer.h"
#include "boomanalyzer.h"
#include "rainbowanalyzer.h"
#include "sonogram.h"
#include "core/logging.h"
#include "engine/enginebase.h"
@@ -85,6 +86,7 @@ AnalyzerContainer::AnalyzerContainer(QWidget *parent)
AddAnalyzerType<BoomAnalyzer>();
AddAnalyzerType<Rainbow::NyanCatAnalyzer>();
AddAnalyzerType<Rainbow::RainbowDashAnalyzer>();
AddAnalyzerType<Sonogram>();
disable_action_ = context_menu_->addAction(tr("No analyzer"), this, &AnalyzerContainer::DisableAnalyzer);
disable_action_->setCheckable(true);

View File

@@ -110,7 +110,7 @@ void BoomAnalyzer::transform(Scope &s) {
void BoomAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_frame) {
if (!new_frame || engine_->state() == Engine::Paused) {
if (!new_frame || engine_->state() == Engine::State::Paused) {
p.drawPixmap(0, 0, canvas_);
return;
}

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

@@ -0,0 +1,92 @@
/*
Strawberry Music Player
This file was part of Clementine.
Copyright 2004, Melchior FRANZ <mfranz@kde.org>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Strawberry is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Strawberry is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QPainter>
#include <QResizeEvent>
#include "sonogram.h"
const char *Sonogram::kName = QT_TRANSLATE_NOOP("AnalyzerContainer", "Sonogram");
Sonogram::Sonogram(QWidget *parent)
: Analyzer::Base(parent, 9) {}
void Sonogram::resizeEvent(QResizeEvent *e) {
Q_UNUSED(e)
canvas_ = QPixmap(size());
canvas_.fill(palette().color(QPalette::Window));
}
void Sonogram::analyze(QPainter &p, const Analyzer::Scope &s, bool new_frame) {
if (!new_frame || engine_->state() == Engine::State::Paused) {
p.drawPixmap(0, 0, canvas_);
return;
}
QPainter canvas_painter(&canvas_);
canvas_painter.drawPixmap(0, 0, canvas_, 1, 0, width() - 1, -1);
Analyzer::Scope::const_iterator it = s.begin(), end = s.end();
for (int y = height() - 1; y;) {
QColor c;
if (it >= end || *it < .005) {
c = palette().color(QPalette::Window);
}
else if (*it < .05) {
c.setHsv(95, 255, 255 - static_cast<int>(*it * 4000.0));
}
else if (*it < 1.0) {
c.setHsv(95 - static_cast<int>(*it * 90.0), 255, 255);
}
else {
c = Qt::red;
}
canvas_painter.setPen(c);
canvas_painter.drawPoint(width() - 1, y--);
if (it < end) ++it;
}
canvas_painter.end();
p.drawPixmap(0, 0, canvas_);
}
void Sonogram::transform(Analyzer::Scope &scope) {
fht_->power2(scope.data());
fht_->scale(scope.data(), 1.0 / 256);
scope.resize(fht_->size() / 2);
}
void Sonogram::demo(QPainter &p) {
analyze(p, Analyzer::Scope(fht_->size(), 0), new_frame_);
}

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

@@ -0,0 +1,49 @@
/*
Strawberry Music Player
This file was part of Clementine.
Copyright 2004, Melchior FRANZ <mfranz@kde.org>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Copyright 2015, Mark Furneaux <mark@furneaux.ca>
Strawberry is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Strawberry is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SONOGRAM_H
#define SONOGRAM_H
#include <QPixmap>
#include <QPainter>
#include "analyzerbase.h"
class Sonogram : public Analyzer::Base {
Q_OBJECT
public:
Q_INVOKABLE explicit Sonogram(QWidget *parent);
static const char *kName;
protected:
void resizeEvent(QResizeEvent *e) override;
void analyze(QPainter &p, const Analyzer::Scope &s, bool new_frame) override;
void transform(Analyzer::Scope &scope) override;
void demo(QPainter &p) override;
private:
QPixmap canvas_;
};
#endif // SONOGRAM_H

View File

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

View File

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

View File

@@ -60,7 +60,7 @@ CollectionBackend::CollectionBackend(QObject *parent)
: CollectionBackendInterface(parent),
db_(nullptr),
task_manager_(nullptr),
source_(Song::Source_Unknown),
source_(Song::Source::Unknown),
original_thread_(nullptr) {
original_thread_ = thread();
@@ -140,8 +140,12 @@ void CollectionBackend::IncrementSkipCountAsync(const int id, const float progre
QMetaObject::invokeMethod(this, "IncrementSkipCount", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(float, progress));
}
void CollectionBackend::ResetStatisticsAsync(const int id) {
QMetaObject::invokeMethod(this, "ResetStatistics", Qt::QueuedConnection, Q_ARG(int, id));
void CollectionBackend::ResetPlayStatisticsAsync(const int id, const bool save_tags) {
QMetaObject::invokeMethod(this, "ResetPlayStatistics", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(bool, save_tags));
}
void CollectionBackend::ResetPlayStatisticsAsync(const QList<int> &id_list, const bool save_tags) {
QMetaObject::invokeMethod(this, "ResetPlayStatistics", Qt::QueuedConnection, Q_ARG(QList<int>, id_list), Q_ARG(bool, save_tags));
}
void CollectionBackend::LoadDirectories() {
@@ -714,7 +718,7 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) {
Song old_song = old_songs[new_song.song_id()];
if (!new_song.IsMetadataEqual(old_song)) { // Update existing song.
if (!new_song.IsAllMetadataEqual(old_song) || !new_song.IsFingerprintEqual(old_song)) { // Update existing song.
{
SqlQuery q(db);
@@ -1776,23 +1780,48 @@ void CollectionBackend::IncrementSkipCount(const int id, const float progress) {
}
void CollectionBackend::ResetStatistics(const int id) {
void CollectionBackend::ResetPlayStatistics(const int id, const bool save_tags) {
if (id == -1) return;
ResetPlayStatistics(QList<int>() << id, save_tags);
}
void CollectionBackend::ResetPlayStatistics(const QList<int> &id_list, const bool save_tags) {
if (id_list.isEmpty()) return;
QStringList id_str_list;
id_str_list.reserve(id_list.count());
for (const int id : id_list) {
id_str_list << QString::number(id);
}
const bool success = ResetPlayStatistics(id_str_list);
if (success) {
const SongList songs = GetSongsById(id_list);
emit SongsStatisticsChanged(songs, save_tags);
}
}
bool CollectionBackend::ResetPlayStatistics(const QStringList &id_str_list) {
if (id_str_list.isEmpty()) return false;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
SqlQuery q(db);
q.prepare(QString("UPDATE %1 SET playcount = 0, skipcount = 0, lastplayed = -1 WHERE ROWID = :id").arg(songs_table_));
q.BindValue(":id", id);
q.prepare(QString("UPDATE %1 SET playcount = 0, skipcount = 0, lastplayed = -1 WHERE ROWID IN (:ids)").arg(songs_table_));
q.BindValue(":ids", id_str_list.join(","));
if (!q.Exec()) {
db_->ReportErrors(q);
return;
return false;
}
Song new_song = GetSongById(id, db);
emit SongsStatisticsChanged(SongList() << new_song);
return true;
}
@@ -1864,7 +1893,7 @@ SongList CollectionBackend::SmartPlaylistsFindSongs(const SmartPlaylistSearch &s
SongList CollectionBackend::SmartPlaylistsGetAllSongs() {
// Get all the songs!
return SmartPlaylistsFindSongs(SmartPlaylistSearch(SmartPlaylistSearch::Type_All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::Sort_FieldAsc, SmartPlaylistSearchTerm::Field_Artist, -1));
return SmartPlaylistsFindSongs(SmartPlaylistSearch(SmartPlaylistSearch::SearchType::All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::SortType::FieldAsc, SmartPlaylistSearchTerm::Field::Artist, -1));
}
@@ -1927,7 +1956,7 @@ void CollectionBackend::UpdateLastPlayed(const QString &artist, const QString &a
}
void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &title, const int playcount) {
void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &title, const int playcount, const bool save_tags) {
SongList songs = GetSongsBy(artist, QString(), title);
if (songs.isEmpty()) {
@@ -1949,7 +1978,7 @@ void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &ti
}
}
emit SongsStatisticsChanged(SongList() << songs);
emit SongsStatisticsChanged(SongList() << songs, save_tags);
}

View File

@@ -54,7 +54,7 @@ class CollectionBackendInterface : public QObject {
explicit CollectionBackendInterface(QObject *parent = nullptr) : QObject(parent) {}
struct Album {
Album() : filetype(Song::FileType_Unknown) {}
Album() : filetype(Song::FileType::Unknown) {}
Album(const QString &_album_artist, const QString &_album, const QUrl &_art_automatic, const QUrl &_art_manual, const QList<QUrl> &_urls, const Song::FileType _filetype, const QString &_cue_path)
: album_artist(_album_artist),
album(_album),
@@ -200,7 +200,8 @@ class CollectionBackend : public CollectionBackendInterface {
void IncrementPlayCountAsync(const int id);
void IncrementSkipCountAsync(const int id, const float progress);
void ResetStatisticsAsync(const int id);
void ResetPlayStatisticsAsync(const int id, const bool save_tags = false);
void ResetPlayStatisticsAsync(const QList<int> &id_list, const bool save_tags = false);
void DeleteAllAsync();
@@ -236,13 +237,15 @@ class CollectionBackend : public CollectionBackendInterface {
void ForceCompilation(const QString &album, const QList<QString> &artists, const bool on);
void IncrementPlayCount(const int id);
void IncrementSkipCount(const int id, const float progress);
void ResetStatistics(const int id);
void ResetPlayStatistics(const int id, const bool save_tags = false);
void ResetPlayStatistics(const QList<int> &id_list, const bool save_tags = false);
bool ResetPlayStatistics(const QStringList &id_str_list);
void DeleteAll();
void SongPathChanged(const Song &song, const QFileInfo &new_file, const std::optional<int> new_collection_directory_id);
SongList GetSongsBy(const QString &artist, const QString &album, const QString &title);
void UpdateLastPlayed(const QString &artist, const QString &album, const QString &title, const qint64 lastplayed);
void UpdatePlayCount(const QString &artist, const QString &title, const int playcount);
void UpdatePlayCount(const QString &artist, const QString &title, const int playcount, const bool save_tags = false);
void UpdateSongRating(const int id, const float rating, const bool save_tags = false);
void UpdateSongsRating(const QList<int> &id_list, const float rating, const bool save_tags = false);
@@ -256,7 +259,7 @@ class CollectionBackend : public CollectionBackendInterface {
void SongsDiscovered(SongList);
void SongsDeleted(SongList);
void SongsStatisticsChanged(SongList);
void SongsStatisticsChanged(SongList, bool = false);
void DatabaseReset();

View File

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

View File

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

View File

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

View File

@@ -54,7 +54,7 @@ class CollectionFilterWidget : public QWidget {
static const int kFilterDelay = 500; // msec
enum DelayBehaviour {
enum class DelayBehaviour {
AlwaysInstant,
DelayedOnLargeLibraries,
AlwaysDelayed,

File diff suppressed because it is too large Load Diff

View File

@@ -83,34 +83,34 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
};
// These values get saved in QSettings - don't change them
enum GroupBy {
GroupBy_None = 0,
GroupBy_AlbumArtist = 1,
GroupBy_Artist = 2,
GroupBy_Album = 3,
GroupBy_AlbumDisc = 4,
GroupBy_YearAlbum = 5,
GroupBy_YearAlbumDisc = 6,
GroupBy_OriginalYearAlbum = 7,
GroupBy_OriginalYearAlbumDisc = 8,
GroupBy_Disc = 9,
GroupBy_Year = 10,
GroupBy_OriginalYear = 11,
GroupBy_Genre = 12,
GroupBy_Composer = 13,
GroupBy_Performer = 14,
GroupBy_Grouping = 15,
GroupBy_FileType = 16,
GroupBy_Format = 17,
GroupBy_Samplerate = 18,
GroupBy_Bitdepth = 19,
GroupBy_Bitrate = 20,
enum class GroupBy {
None = 0,
AlbumArtist = 1,
Artist = 2,
Album = 3,
AlbumDisc = 4,
YearAlbum = 5,
YearAlbumDisc = 6,
OriginalYearAlbum = 7,
OriginalYearAlbumDisc = 8,
Disc = 9,
Year = 10,
OriginalYear = 11,
Genre = 12,
Composer = 13,
Performer = 14,
Grouping = 15,
FileType = 16,
Format = 17,
Samplerate = 18,
Bitdepth = 19,
Bitrate = 20,
GroupByCount = 21,
};
Q_ENUM(GroupBy)
struct Grouping {
explicit Grouping(GroupBy f = GroupBy_None, GroupBy s = GroupBy_None, GroupBy t = GroupBy_None)
explicit Grouping(GroupBy f = GroupBy::None, GroupBy s = GroupBy::None, GroupBy t = GroupBy::None)
: first(f), second(s), third(t) {}
GroupBy first;
@@ -181,9 +181,9 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
quint64 icon_cache_disk_size() { return sIconCache->cacheSize(); }
static bool IsArtistGroupBy(const GroupBy group_by) {
return group_by == CollectionModel::GroupBy_Artist || group_by == CollectionModel::GroupBy_AlbumArtist;
return group_by == CollectionModel::GroupBy::Artist || group_by == CollectionModel::GroupBy::AlbumArtist;
}
static bool IsAlbumGroupBy(const GroupBy group_by) { return group_by == GroupBy_Album || group_by == GroupBy_YearAlbum || group_by == GroupBy_AlbumDisc || group_by == GroupBy_YearAlbumDisc || group_by == GroupBy_OriginalYearAlbum || group_by == GroupBy_OriginalYearAlbumDisc; }
static bool IsAlbumGroupBy(const GroupBy group_by) { return group_by == GroupBy::Album || group_by == GroupBy::YearAlbum || group_by == GroupBy::AlbumDisc || group_by == GroupBy::YearAlbumDisc || group_by == GroupBy::OriginalYearAlbum || group_by == GroupBy::OriginalYearAlbumDisc; }
void set_use_lazy_loading(const bool value) { use_lazy_loading_ = value; }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -699,7 +699,7 @@ void CollectionView::DeleteFilesFinished(const SongList &songs_with_errors) {
if (songs_with_errors.isEmpty()) return;
OrganizeErrorDialog *dialog = new OrganizeErrorDialog(this);
dialog->Show(OrganizeErrorDialog::Type_Delete, songs_with_errors);
dialog->Show(OrganizeErrorDialog::OperationType::Delete, songs_with_errors);
// It deletes itself when the user closes it
}

View File

@@ -78,13 +78,12 @@ CollectionWatcher::CollectionWatcher(Song::Source source, QObject *parent)
scan_on_startup_(true),
monitor_(true),
song_tracking_(false),
mark_songs_unavailable_(source_ == Song::Source_Collection),
mark_songs_unavailable_(source_ == Song::Source::Collection),
expire_unavailable_songs_days_(60),
overwrite_playcount_(false),
overwrite_rating_(false),
stop_requested_(false),
abort_requested_(false),
rescan_in_progress_(false),
rescan_timer_(new QTimer(this)),
periodic_scan_timer_(new QTimer(this)),
rescan_paused_(false),
@@ -143,7 +142,7 @@ void CollectionWatcher::ReloadSettings() {
scan_on_startup_ = s.value("startup_scan", true).toBool();
monitor_ = s.value("monitor", true).toBool();
QStringList filters = s.value("cover_art_patterns", QStringList() << "front" << "cover").toStringList();
if (source_ == Song::Source_Collection) {
if (source_ == Song::Source::Collection) {
song_tracking_ = s.value("song_tracking", false).toBool();
mark_songs_unavailable_ = song_tracking_ ? true : s.value("mark_songs_unavailable", true).toBool();
}
@@ -239,7 +238,7 @@ void CollectionWatcher::ScanTransaction::AddToProgressMax(const quint64 n) {
void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
if (!deleted_songs.isEmpty()) {
if (mark_songs_unavailable_ && watcher_->source() == Song::Source_Collection) {
if (mark_songs_unavailable_ && watcher_->source() == Song::Source::Collection) {
emit watcher_->SongsUnavailable(deleted_songs);
}
else {
@@ -763,7 +762,7 @@ void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
const Song &matching_song = matching_songs.first();
if (cue_deleted) {
for (const Song &song : matching_songs) {
if (!song.IsMetadataAndMoreEqual(matching_song)) {
if (!song.IsAllMetadataEqual(matching_song)) {
t->deleted_songs << song;
}
}
@@ -855,10 +854,26 @@ void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching
changes << "metadata";
notify_new = true;
}
if (matching_song.art_automatic() != new_song.art_automatic() || matching_song.art_manual() != new_song.art_manual()) {
if (!matching_song.IsPlayStatisticsEqual(new_song)) {
changes << "play statistics";
notify_new = true;
}
if (!matching_song.IsRatingEqual(new_song)) {
changes << "rating";
notify_new = true;
}
if (!matching_song.IsArtEqual(new_song)) {
changes << "album art";
notify_new = true;
}
if (!matching_song.IsAcoustIdEqual(new_song)) {
changes << "acoustid";
notify_new = true;
}
if (!matching_song.IsMusicBrainzEqual(new_song)) {
changes << "musicbrainz";
notify_new = true;
}
if (matching_song.mtime() != new_song.mtime()) {
changes << "mtime";
}
@@ -1113,18 +1128,6 @@ void CollectionWatcher::FullScanAsync() {
}
void CollectionWatcher::RescanTracksAsync(const SongList &songs) {
// Is List thread safe? if not, this may crash.
song_rescan_queue_.append(songs);
// Call only if it's not already running
if (!rescan_in_progress_) {
QMetaObject::invokeMethod(this, "RescanTracksNow", Qt::QueuedConnection);
}
}
void CollectionWatcher::IncrementalScanCheck() {
qint64 duration = QDateTime::currentDateTime().toSecsSinceEpoch() - last_scan_time_;
@@ -1139,34 +1142,6 @@ void CollectionWatcher::IncrementalScanNow() { PerformScan(true, false); }
void CollectionWatcher::FullScanNow() { PerformScan(false, true); }
void CollectionWatcher::RescanTracksNow() {
Q_ASSERT(!rescan_in_progress_);
stop_requested_ = false;
// Currently we are too stupid to rescan one file at a time, so we'll just scan the full directories
QStringList scanned_dirs; // To avoid double scans
while (!song_rescan_queue_.isEmpty()) {
if (stop_requested_ || abort_requested_) break;
Song song = song_rescan_queue_.takeFirst();
QString songdir = song.url().toLocalFile().section('/', 0, -2);
if (!scanned_dirs.contains(songdir)) {
qLog(Debug) << "Song" << song.title() << "dir id" << song.directory_id() << "dir" << songdir;
ScanTransaction transaction(this, song.directory_id(), false, false, mark_songs_unavailable_);
quint64 files_count = FilesCountForPath(&transaction, songdir);
ScanSubdirectory(songdir, CollectionSubdirectory(), files_count, &transaction);
scanned_dirs << songdir;
emit CompilationsNeedUpdating();
}
else {
qLog(Debug) << "Directory" << songdir << "already scanned - skipping.";
}
}
Q_ASSERT(song_rescan_queue_.isEmpty());
rescan_in_progress_ = false;
}
void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mtimes) {
stop_requested_ = false;
@@ -1252,3 +1227,34 @@ quint64 CollectionWatcher::FilesCountForSubdirs(ScanTransaction *t, const Collec
return i;
}
void CollectionWatcher::RescanSongsAsync(const SongList &songs) {
QMetaObject::invokeMethod(this, "RescanSongs", Qt::QueuedConnection, Q_ARG(SongList, songs));
}
void CollectionWatcher::RescanSongs(const SongList &songs) {
stop_requested_ = false;
QStringList scanned_paths;
for (const Song &song : songs) {
if (stop_requested_ || abort_requested_) break;
const QString song_path = song.url().toLocalFile().section('/', 0, -2);
if (scanned_paths.contains(song_path)) continue;
ScanTransaction transaction(this, song.directory_id(), false, true, mark_songs_unavailable_);
CollectionSubdirectoryList subdirs(transaction.GetAllSubdirs());
for (const CollectionSubdirectory &subdir : subdirs) {
if (stop_requested_ || abort_requested_) break;
if (subdir.path != song_path) continue;
qLog(Debug) << "Rescan for directory ID" << song.directory_id() << "directory" << subdir.path;
quint64 files_count = FilesCountForPath(&transaction, subdir.path);
ScanSubdirectory(song_path, subdir, files_count, &transaction);
scanned_paths << subdir.path;
}
}
emit CompilationsNeedUpdating();
}

View File

@@ -59,7 +59,6 @@ class CollectionWatcher : public QObject {
void IncrementalScanAsync();
void FullScanAsync();
void RescanTracksAsync(const SongList &songs);
void SetRescanPausedAsync(const bool pause);
void ReloadSettingsAsync();
@@ -68,6 +67,8 @@ class CollectionWatcher : public QObject {
void ExitAsync();
void RescanSongsAsync(const SongList &songs);
signals:
void NewOrUpdatedSongs(SongList);
void SongsMTimeUpdated(SongList);
@@ -166,9 +167,9 @@ class CollectionWatcher : public QObject {
void IncrementalScanCheck();
void IncrementalScanNow();
void FullScanNow();
void RescanTracksNow();
void RescanPathsNow();
void ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
void RescanSongs(const SongList &songs);
private:
static bool FindSongsByPath(const SongList &songs, const QString &path, SongList *out);
@@ -223,7 +224,6 @@ class CollectionWatcher : public QObject {
bool stop_requested_;
bool abort_requested_;
bool rescan_in_progress_; // True if RescanTracksNow() has been called and is working.
QMap<int, CollectionDirectory> watched_dirs_;
QTimer *rescan_timer_;
@@ -237,8 +237,6 @@ class CollectionWatcher : public QObject {
static QStringList sValidImages;
SongList song_rescan_queue_; // Set by UI thread
qint64 last_scan_time_;
};

View File

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

View File

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

View File

@@ -63,7 +63,7 @@ ContextAlbum::ContextAlbum(QWidget *parent)
cover_loader_options_.desired_height_ = width();
cover_loader_options_.pad_output_image_ = true;
cover_loader_options_.scale_output_image_ = true;
QImage image = ImageUtils::ScaleAndPad(image_strawberry_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_);
QImage image = ImageUtils::ScaleAndPad(image_strawberry_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_, devicePixelRatioF());
if (!image.isNull()) {
pixmap_current_ = QPixmap::fromImage(image);
}
@@ -91,7 +91,7 @@ void ContextAlbum::Init(ContextView *context_view, AlbumCoverChoiceController *a
QSize ContextAlbum::sizeHint() const {
return QSize(pixmap_current_.width(), pixmap_current_.height());
return QSize(pixmap_current_.width() / devicePixelRatioF(), pixmap_current_.height() / devicePixelRatioF());
}
@@ -182,7 +182,7 @@ void ContextAlbum::DrawImage(QPainter *p, const QPixmap &pixmap, const qreal opa
if (qFuzzyCompare(opacity, static_cast<qreal>(0.0))) return;
p->setOpacity(opacity);
p->drawPixmap(0, 0, pixmap.width(), pixmap.height(), pixmap);
p->drawPixmap(0, 0, pixmap.width() / pixmap.devicePixelRatioF(), pixmap.height() / pixmap.devicePixelRatioF(), pixmap);
}
@@ -235,7 +235,7 @@ void ContextAlbum::FadePreviousCoverFinished(std::shared_ptr<PreviousCover> prev
void ContextAlbum::ScaleCover() {
QImage image = ImageUtils::ScaleAndPad(image_original_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_);
QImage image = ImageUtils::ScaleAndPad(image_original_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_, devicePixelRatioF());
if (image.isNull()) {
pixmap_current_ = QPixmap();
}
@@ -248,7 +248,7 @@ void ContextAlbum::ScaleCover() {
void ContextAlbum::ScalePreviousCovers() {
for (std::shared_ptr<PreviousCover> previous_cover : previous_covers_) {
QImage image = ImageUtils::ScaleAndPad(previous_cover->image, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_);
QImage image = ImageUtils::ScaleAndPad(previous_cover->image, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_, devicePixelRatioF());
if (image.isNull()) {
previous_cover->pixmap = QPixmap();
}

View File

@@ -338,11 +338,11 @@ void ContextView::ReloadSettings() {
s.beginGroup(ContextSettingsPage::kSettingsGroup);
title_fmt_ = s.value(ContextSettingsPage::kSettingsTitleFmt, "%title% - %artist%").toString();
summary_fmt_ = s.value(ContextSettingsPage::kSettingsSummaryFmt, "%album%").toString();
action_show_album_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::ALBUM], true).toBool());
action_show_data_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::TECHNICAL_DATA], false).toBool());
action_show_output_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::ENGINE_AND_DEVICE], false).toBool());
action_show_lyrics_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::SONG_LYRICS], true).toBool());
action_search_lyrics_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::SEARCH_LYRICS], true).toBool());
action_show_album_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::ALBUM)], true).toBool());
action_show_data_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::TECHNICAL_DATA)], false).toBool());
action_show_output_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::ENGINE_AND_DEVICE)], false).toBool());
action_show_lyrics_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::SONG_LYRICS)], true).toBool());
action_search_lyrics_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::SEARCH_LYRICS)], true).toBool());
font_headline_ = s.value("font_headline", font().family()).toString();
font_normal_ = s.value("font_normal", font().family()).toString();
font_size_headline_ = s.value("font_size_headline", ContextSettingsPage::kDefaultFontSizeHeadline).toReal();
@@ -407,7 +407,7 @@ void ContextView::SearchLyrics() {
if (lyrics_.isEmpty() && action_show_lyrics_->isChecked() && action_search_lyrics_->isChecked() && !song_playing_.artist().isEmpty() && !song_playing_.title().isEmpty() && !lyrics_tried_ && lyrics_id_ == -1) {
lyrics_fetcher_->Clear();
lyrics_tried_ = true;
lyrics_id_ = static_cast<qint64>(lyrics_fetcher_->Search(song_playing_.effective_albumartist(), song_playing_.album(), song_playing_.title()));
lyrics_id_ = static_cast<qint64>(lyrics_fetcher_->Search(song_playing_.effective_albumartist(), song_playing_.artist(), song_playing_.album(), song_playing_.title()));
}
}
@@ -435,7 +435,7 @@ void ContextView::NoSong() {
widget_album_->show();
}
textedit_top_->setFont(QFont(font_headline_, font_size_headline_ * 1.6));
textedit_top_->setFont(QFont(font_headline_, static_cast<int>(font_size_headline_ * 1.6)));
textedit_top_->SetText(tr("No song playing"));
QString html;
@@ -451,14 +451,14 @@ void ContextView::NoSong() {
else html += tr("%1 albums").arg(collectionview_->TotalAlbums());
html += "<br />";
label_stop_summary_->setFont(QFont(font_normal_, font_size_normal_));
label_stop_summary_->setFont(QFont(font_normal_, static_cast<int>(font_size_normal_)));
label_stop_summary_->setText(html);
}
void ContextView::UpdateFonts() {
QFont font(font_normal_, font_size_normal_);
QFont font(font_normal_, static_cast<int>(font_size_normal_));
font.setBold(false);
for (QLabel *l : labels_play_all_) {
l->setFont(font);
@@ -471,7 +471,7 @@ void ContextView::UpdateFonts() {
void ContextView::SetSong() {
textedit_top_->setFont(QFont(font_headline_, font_size_headline_));
textedit_top_->setFont(QFont(font_headline_, static_cast<int>(font_size_headline_)));
textedit_top_->SetText(QString("<b>%1</b><br />%2").arg(Utilities::ReplaceMessage(title_fmt_, song_playing_, "<br />", true), Utilities::ReplaceMessage(summary_fmt_, song_playing_, "<br />", true)));
label_stop_summary_->clear();
@@ -544,7 +544,7 @@ void ContextView::SetSong() {
if (action_show_output_->isChecked()) {
widget_play_output_->show();
Engine::EngineType enginetype(Engine::None);
Engine::EngineType enginetype(Engine::EngineType::None);
if (app_->player()->engine()) enginetype = app_->player()->engine()->type();
QIcon icon_engine = IconLoader::Load(EngineName(enginetype), true, 32);
@@ -739,7 +739,7 @@ void ContextView::ActionShowAlbum() {
QSettings s;
s.beginGroup(ContextSettingsPage::kSettingsGroup);
s.setValue(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::ALBUM], action_show_album_->isChecked());
s.setValue(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::ALBUM)], action_show_album_->isChecked());
s.endGroup();
if (song_playing_.is_valid()) SetSong();
@@ -749,7 +749,7 @@ void ContextView::ActionShowData() {
QSettings s;
s.beginGroup(ContextSettingsPage::kSettingsGroup);
s.setValue(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::TECHNICAL_DATA], action_show_data_->isChecked());
s.setValue(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::TECHNICAL_DATA)], action_show_data_->isChecked());
s.endGroup();
if (song_playing_.is_valid()) SetSong();
@@ -759,7 +759,7 @@ void ContextView::ActionShowOutput() {
QSettings s;
s.beginGroup(ContextSettingsPage::kSettingsGroup);
s.setValue(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::ENGINE_AND_DEVICE], action_show_output_->isChecked());
s.setValue(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::ENGINE_AND_DEVICE)], action_show_output_->isChecked());
s.endGroup();
if (song_playing_.is_valid()) SetSong();
@@ -769,7 +769,7 @@ void ContextView::ActionShowLyrics() {
QSettings s;
s.beginGroup(ContextSettingsPage::kSettingsGroup);
s.setValue(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::SONG_LYRICS], action_show_lyrics_->isChecked());
s.setValue(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::SONG_LYRICS)], action_show_lyrics_->isChecked());
s.endGroup();
if (song_playing_.is_valid()) SetSong();
@@ -782,7 +782,7 @@ void ContextView::ActionSearchLyrics() {
QSettings s;
s.beginGroup(ContextSettingsPage::kSettingsGroup);
s.setValue(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::SEARCH_LYRICS], action_search_lyrics_->isChecked());
s.setValue(ContextSettingsPage::kSettingsGroupEnable[static_cast<int>(ContextSettingsPage::ContextSettingsOrder::SEARCH_LYRICS)], action_search_lyrics_->isChecked());
s.endGroup();
if (song_playing_.is_valid()) SetSong();

View File

@@ -63,6 +63,7 @@
#include "lyrics/lololyricsprovider.h"
#include "lyrics/musixmatchlyricsprovider.h"
#include "lyrics/chartlyricsprovider.h"
#include "lyrics/lyricscomlyricsprovider.h"
#include "scrobbler/audioscrobbler.h"
#include "scrobbler/lastfmimport.h"
@@ -154,6 +155,7 @@ class ApplicationImpl {
lyrics_providers->AddProvider(new LoloLyricsProvider(lyrics_providers->network(), app));
lyrics_providers->AddProvider(new MusixmatchLyricsProvider(lyrics_providers->network(), app));
lyrics_providers->AddProvider(new ChartLyricsProvider(lyrics_providers->network(), app));
lyrics_providers->AddProvider(new LyricsComLyricsProvider(lyrics_providers->network(), app));
lyrics_providers->ReloadSettings();
return lyrics_providers;
}),

View File

@@ -81,8 +81,8 @@ const char *CommandlineOptions::kVersionText = "Strawberry %1";
CommandlineOptions::CommandlineOptions(int argc, char **argv)
: argc_(argc),
argv_(argv),
url_list_action_(UrlList_None),
player_action_(Player_None),
url_list_action_(UrlListAction::None),
player_action_(PlayerAction::None),
set_volume_(-1),
volume_modifier_(0),
seek_to_(-1),
@@ -128,13 +128,13 @@ bool CommandlineOptions::Parse() {
{"previous", no_argument, nullptr, 'r'},
{"next", no_argument, nullptr, 'f'},
{"volume", required_argument, nullptr, 'v'},
{"volume-up", no_argument, nullptr, VolumeUp},
{"volume-down", no_argument, nullptr, VolumeDown},
{"volume-increase-by", required_argument, nullptr, VolumeIncreaseBy},
{"volume-decrease-by", required_argument, nullptr, VolumeDecreaseBy},
{"seek-to", required_argument, nullptr, SeekTo},
{"seek-by", required_argument, nullptr, SeekBy},
{"restart-or-previous", no_argument, nullptr, RestartOrPrevious},
{"volume-up", no_argument, nullptr, LongOptions::VolumeUp},
{"volume-down", no_argument, nullptr, LongOptions::VolumeDown},
{"volume-increase-by", required_argument, nullptr, LongOptions::VolumeIncreaseBy},
{"volume-decrease-by", required_argument, nullptr, LongOptions::VolumeDecreaseBy},
{"seek-to", required_argument, nullptr, LongOptions::SeekTo},
{"seek-by", required_argument, nullptr, LongOptions::SeekBy},
{"restart-or-previous", no_argument, nullptr, LongOptions::RestartOrPrevious},
{"create", required_argument, nullptr, 'c'},
{"append", no_argument, nullptr, 'a'},
{"load", no_argument, nullptr, 'l'},
@@ -144,10 +144,10 @@ bool CommandlineOptions::Parse() {
{"toggle-pretty-osd", no_argument, nullptr, 'y'},
{"language", required_argument, nullptr, 'g'},
{"resize-window", required_argument, nullptr, 'w'},
{"quiet", no_argument, nullptr, Quiet},
{"verbose", no_argument, nullptr, Verbose},
{"log-levels", required_argument, nullptr, LogLevels},
{"version", no_argument, nullptr, Version},
{"quiet", no_argument, nullptr, LongOptions::Quiet},
{"verbose", no_argument, nullptr, LongOptions::Verbose},
{"log-levels", required_argument, nullptr, LongOptions::LogLevels},
{"version", no_argument, nullptr, LongOptions::Version},
{nullptr, 0, nullptr, 0}};
// Parse the arguments
@@ -198,39 +198,39 @@ bool CommandlineOptions::Parse() {
}
case 'p':
player_action_ = Player_Play;
player_action_ = PlayerAction::Play;
break;
case 't':
player_action_ = Player_PlayPause;
player_action_ = PlayerAction::PlayPause;
break;
case 'u':
player_action_ = Player_Pause;
player_action_ = PlayerAction::Pause;
break;
case 's':
player_action_ = Player_Stop;
player_action_ = PlayerAction::Stop;
break;
case 'q':
player_action_ = Player_StopAfterCurrent;
player_action_ = PlayerAction::StopAfterCurrent;
break;
case 'r':
player_action_ = Player_Previous;
player_action_ = PlayerAction::Previous;
break;
case 'f':
player_action_ = Player_Next;
player_action_ = PlayerAction::Next;
break;
case 'i':
player_action_ = Player_PlayPlaylist;
player_action_ = PlayerAction::PlayPlaylist;
playlist_name_ = QString(optarg);
break;
case 'c':
url_list_action_ = UrlList_CreateNew;
url_list_action_ = UrlListAction::CreateNew;
playlist_name_ = QString(optarg);
break;
case 'a':
url_list_action_ = UrlList_Append;
url_list_action_ = UrlListAction::Append;
break;
case 'l':
url_list_action_ = UrlList_Load;
url_list_action_ = UrlListAction::Load;
break;
case 'o':
show_osd_ = true;
@@ -241,22 +241,22 @@ bool CommandlineOptions::Parse() {
case 'g':
language_ = QString(optarg);
break;
case VolumeUp:
case LongOptions::VolumeUp:
volume_modifier_ = +4;
break;
case VolumeDown:
case LongOptions::VolumeDown:
volume_modifier_ = -4;
break;
case Quiet:
case LongOptions::Quiet:
log_levels_ = "1";
break;
case Verbose:
case LongOptions::Verbose:
log_levels_ = "3";
break;
case LogLevels:
case LongOptions::LogLevels:
log_levels_ = QString(optarg);
break;
case Version: {
case LongOptions::Version: {
QString version_text = QString(kVersionText).arg(STRAWBERRY_VERSION_DISPLAY);
std::cout << version_text.toLocal8Bit().constData() << std::endl;
std::exit(0);
@@ -266,28 +266,28 @@ bool CommandlineOptions::Parse() {
if (!ok) set_volume_ = -1;
break;
case VolumeIncreaseBy:
case LongOptions::VolumeIncreaseBy:
volume_modifier_ = QString(optarg).toInt(&ok);
if (!ok) volume_modifier_ = 0;
break;
case VolumeDecreaseBy:
case LongOptions::VolumeDecreaseBy:
volume_modifier_ = -QString(optarg).toInt(&ok);
if (!ok) volume_modifier_ = 0;
break;
case SeekTo:
case LongOptions::SeekTo:
seek_to_ = QString(optarg).toInt(&ok);
if (!ok) seek_to_ = -1;
break;
case SeekBy:
case LongOptions::SeekBy:
seek_by_ = QString(optarg).toInt(&ok);
if (!ok) seek_by_ = 0;
break;
case RestartOrPrevious:
player_action_ = Player_RestartOrPrevious;
case LongOptions::RestartOrPrevious:
player_action_ = PlayerAction::RestartOrPrevious;
break;
case 'k':
@@ -297,7 +297,7 @@ bool CommandlineOptions::Parse() {
case 'w':
window_size_ = QString(optarg);
player_action_ = Player_ResizeWindow;
player_action_ = PlayerAction::ResizeWindow;
break;
case '?':
@@ -323,7 +323,7 @@ bool CommandlineOptions::Parse() {
}
bool CommandlineOptions::is_empty() const {
return player_action_ == Player_None &&
return player_action_ == PlayerAction::None &&
set_volume_ == -1 &&
volume_modifier_ == 0 &&
seek_to_ == -1 &&
@@ -335,7 +335,7 @@ bool CommandlineOptions::is_empty() const {
}
bool CommandlineOptions::contains_play_options() const {
return player_action_ != Player_None || play_track_at_ != -1 || !urls_.isEmpty();
return player_action_ != PlayerAction::None || play_track_at_ != -1 || !urls_.isEmpty();
}
QByteArray CommandlineOptions::Serialize() const {

View File

@@ -41,24 +41,24 @@ class CommandlineOptions {
// Don't change the values or order, these get serialised and sent to
// possibly a different version of Strawberry
enum UrlListAction {
UrlList_Append = 0,
UrlList_Load = 1,
UrlList_None = 2,
UrlList_CreateNew = 3,
enum class UrlListAction {
Append = 0,
Load = 1,
None = 2,
CreateNew = 3
};
enum PlayerAction {
Player_None = 0,
Player_Play = 1,
Player_PlayPause = 2,
Player_Pause = 3,
Player_Stop = 4,
Player_Previous = 5,
Player_Next = 6,
Player_RestartOrPrevious = 7,
Player_StopAfterCurrent = 8,
Player_PlayPlaylist = 9,
Player_ResizeWindow = 10
enum class PlayerAction {
None = 0,
Play = 1,
PlayPause = 2,
Pause = 3,
Stop = 4,
Previous = 5,
Next = 6,
RestartOrPrevious = 7,
StopAfterCurrent = 8,
PlayPlaylist = 9,
ResizeWindow = 10
};
bool Parse();

View File

@@ -48,7 +48,7 @@
#include "scopedtransaction.h"
const char *Database::kDatabaseFilename = "strawberry.db";
const int Database::kSchemaVersion = 15;
const int Database::kSchemaVersion = 16;
const int Database::kMinSupportedSchemaVersion = 10;
const char *Database::kMagicAllSongsTables = "%allsongstables";

View File

@@ -59,6 +59,7 @@
#include <QActionGroup>
#include <QShortcut>
#include <QMessageBox>
#include <QErrorMessage>
#include <QtEvents>
#include <QSettings>
#include <QColor>
@@ -93,6 +94,7 @@
#ifdef Q_OS_MACOS
# include "mac_startup.h"
# include "macsystemtrayicon.h"
# include "utilities/macosutils.h"
#else
# include "qtsystemtrayicon.h"
#endif
@@ -293,13 +295,13 @@ MainWindow::MainWindow(Application *app, std::shared_ptr<SystemTrayIcon> tray_ic
}),
smartplaylists_view_(new SmartPlaylistsViewContainer(app, this)),
#ifdef HAVE_SUBSONIC
subsonic_view_(new InternetSongsView(app_, app->internet_services()->ServiceBySource(Song::Source_Subsonic), SubsonicSettingsPage::kSettingsGroup, SettingsDialog::Page_Subsonic, this)),
subsonic_view_(new InternetSongsView(app_, app->internet_services()->ServiceBySource(Song::Source::Subsonic), SubsonicSettingsPage::kSettingsGroup, SettingsDialog::Page::Subsonic, this)),
#endif
#ifdef HAVE_TIDAL
tidal_view_(new InternetTabsView(app_, app->internet_services()->ServiceBySource(Song::Source_Tidal), TidalSettingsPage::kSettingsGroup, SettingsDialog::Page_Tidal, this)),
tidal_view_(new InternetTabsView(app_, app->internet_services()->ServiceBySource(Song::Source::Tidal), TidalSettingsPage::kSettingsGroup, SettingsDialog::Page::Tidal, this)),
#endif
#ifdef HAVE_QOBUZ
qobuz_view_(new InternetTabsView(app_, app->internet_services()->ServiceBySource(Song::Source_Qobuz), QobuzSettingsPage::kSettingsGroup, SettingsDialog::Page_Qobuz, this)),
qobuz_view_(new InternetTabsView(app_, app->internet_services()->ServiceBySource(Song::Source::Qobuz), QobuzSettingsPage::kSettingsGroup, SettingsDialog::Page::Qobuz, this)),
#endif
radio_view_(new RadioViewContainer(this)),
lastfm_import_dialog_(new LastFMImportDialog(app_->lastfm_import(), this)),
@@ -331,10 +333,10 @@ MainWindow::MainWindow(Application *app, std::shared_ptr<SystemTrayIcon> tray_ic
track_slider_timer_(new QTimer(this)),
keep_running_(false),
playing_widget_(true),
doubleclick_addmode_(BehaviourSettingsPage::AddBehaviour_Append),
doubleclick_playmode_(BehaviourSettingsPage::PlayBehaviour_Never),
doubleclick_playlist_addmode_(BehaviourSettingsPage::PlaylistAddBehaviour_Play),
menu_playmode_(BehaviourSettingsPage::PlayBehaviour_Never),
doubleclick_addmode_(BehaviourSettingsPage::AddBehaviour::Append),
doubleclick_playmode_(BehaviourSettingsPage::PlayBehaviour::Never),
doubleclick_playlist_addmode_(BehaviourSettingsPage::PlaylistAddBehaviour::Play),
menu_playmode_(BehaviourSettingsPage::PlayBehaviour::Never),
initialized_(false),
was_maximized_(true),
was_minimized_(false),
@@ -700,7 +702,7 @@ MainWindow::MainWindow(Application *app, std::shared_ptr<SystemTrayIcon> tray_ic
QObject::connect(tidal_view_->albums_collection_view(), &InternetCollectionView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
QObject::connect(tidal_view_->songs_collection_view(), &InternetCollectionView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
QObject::connect(tidal_view_->search_view(), &InternetSearchView::AddToPlaylist, this, &MainWindow::AddToPlaylist);
if (TidalService *tidalservice = qobject_cast<TidalService*>(app_->internet_services()->ServiceBySource(Song::Source_Tidal))) {
if (TidalService *tidalservice = qobject_cast<TidalService*>(app_->internet_services()->ServiceBySource(Song::Source::Tidal))) {
QObject::connect(this, &MainWindow::AuthorizationUrlReceived, tidalservice, &TidalService::AuthorizationUrlReceived);
}
#endif
@@ -920,10 +922,9 @@ MainWindow::MainWindow(Application *app, std::shared_ptr<SystemTrayIcon> tray_ic
}
ui_->tabs->setCurrentIndex(settings_.value("current_tab", 1).toInt());
FancyTabWidget::Mode default_mode = FancyTabWidget::Mode_LargeSidebar;
int tab_mode_int = settings_.value("tab_mode", default_mode).toInt();
FancyTabWidget::Mode tab_mode = static_cast<FancyTabWidget::Mode>(tab_mode_int);
if (tab_mode == FancyTabWidget::Mode_None) tab_mode = default_mode;
FancyTabWidget::Mode default_mode = FancyTabWidget::Mode::LargeSidebar;
FancyTabWidget::Mode tab_mode = static_cast<FancyTabWidget::Mode>(settings_.value("tab_mode", static_cast<int>(default_mode)).toInt());
if (tab_mode == FancyTabWidget::Mode::None) tab_mode = default_mode;
ui_->tabs->SetMode(tab_mode);
TabSwitched();
@@ -946,26 +947,26 @@ MainWindow::MainWindow(Application *app, std::shared_ptr<SystemTrayIcon> tray_ic
#else
QSettings s;
s.beginGroup(BehaviourSettingsPage::kSettingsGroup);
BehaviourSettingsPage::StartupBehaviour behaviour = BehaviourSettingsPage::StartupBehaviour(s.value("startupbehaviour", BehaviourSettingsPage::Startup_Remember).toInt());
const BehaviourSettingsPage::StartupBehaviour startupbehaviour = static_cast<BehaviourSettingsPage::StartupBehaviour>(s.value("startupbehaviour", static_cast<int>(BehaviourSettingsPage::StartupBehaviour::Remember)).toInt());
s.endGroup();
switch (behaviour) {
case BehaviourSettingsPage::Startup_Show:
switch (startupbehaviour) {
case BehaviourSettingsPage::StartupBehaviour::Show:
show();
break;
case BehaviourSettingsPage::Startup_ShowMaximized:
case BehaviourSettingsPage::StartupBehaviour::ShowMaximized:
setWindowState(windowState() | Qt::WindowMaximized);
show();
break;
case BehaviourSettingsPage::Startup_ShowMinimized:
case BehaviourSettingsPage::StartupBehaviour::ShowMinimized:
setWindowState(windowState() | Qt::WindowMinimized);
show();
break;
case BehaviourSettingsPage::Startup_Hide:
case BehaviourSettingsPage::StartupBehaviour::Hide:
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible()) {
break;
}
[[fallthrough]];
case BehaviourSettingsPage::Startup_Remember:
case BehaviourSettingsPage::StartupBehaviour::Remember:
default: {
was_maximized_ = settings_.value("maximized", true).toBool();
@@ -1037,6 +1038,14 @@ MainWindow::MainWindow(Application *app, std::shared_ptr<SystemTrayIcon> tray_ic
}
#endif
#if defined(Q_OS_MACOS)
if (Utilities::ProcessTranslated()) {
QErrorMessage *error_message = new QErrorMessage;
error_message->setAttribute(Qt::WA_DeleteOnClose);
error_message->showMessage(tr("It is detected that Strawberry is running under Rosetta. Strawberry currently have limited macOS support, and running Strawberry under Rosetta is unsupported and known to have issues. If you want to use Strawberry on the current CPU, you should build Strawberry from source. For instructions see.: https://wiki.strawberrymusicplayer.org/wiki/Compile"));
}
#endif
qLog(Debug) << "Started" << QThread::currentThread();
initialized_ = true;
@@ -1067,10 +1076,10 @@ void MainWindow::ReloadSettings() {
playing_widget_ = s.value("playing_widget", true).toBool();
bool trayicon_progress = s.value("trayicon_progress", false).toBool();
if (playing_widget_ != ui_->widget_playing->IsEnabled()) TabSwitched();
doubleclick_addmode_ = BehaviourSettingsPage::AddBehaviour(s.value("doubleclick_addmode", BehaviourSettingsPage::AddBehaviour_Append).toInt());
doubleclick_playmode_ = BehaviourSettingsPage::PlayBehaviour(s.value("doubleclick_playmode", BehaviourSettingsPage::PlayBehaviour_Never).toInt());
doubleclick_playlist_addmode_ = BehaviourSettingsPage::PlaylistAddBehaviour(s.value("doubleclick_playlist_addmode", BehaviourSettingsPage::PlayBehaviour_Never).toInt());
menu_playmode_ = BehaviourSettingsPage::PlayBehaviour(s.value("menu_playmode", BehaviourSettingsPage::PlayBehaviour_Never).toInt());
doubleclick_addmode_ = static_cast<BehaviourSettingsPage::AddBehaviour>(s.value("doubleclick_addmode", static_cast<int>(BehaviourSettingsPage::AddBehaviour::Append)).toInt());
doubleclick_playmode_ = static_cast<BehaviourSettingsPage::PlayBehaviour>(s.value("doubleclick_playmode", static_cast<int>(BehaviourSettingsPage::PlayBehaviour::Never)).toInt());
doubleclick_playlist_addmode_ = static_cast<BehaviourSettingsPage::PlaylistAddBehaviour>(s.value("doubleclick_playlist_addmode", static_cast<int>(BehaviourSettingsPage::PlayBehaviour::Never)).toInt());
menu_playmode_ = static_cast<BehaviourSettingsPage::PlayBehaviour>(s.value("menu_playmode", static_cast<int>(BehaviourSettingsPage::PlayBehaviour::Never)).toInt());
s.endGroup();
s.beginGroup(AppearanceSettingsPage::kSettingsGroup);
@@ -1160,7 +1169,6 @@ void MainWindow::ReloadAllSettings() {
collection_view_->ReloadSettings();
ui_->playlist->view()->ReloadSettings();
app_->playlist_manager()->playlist_container()->ReloadSettings();
app_->album_cover_loader()->ReloadSettings();
album_cover_choice_controller_->ReloadSettings();
context_view_->ReloadSettings();
file_view_->ReloadSettings();
@@ -1224,7 +1232,7 @@ void MainWindow::Exit() {
if (app_->player()->engine()->is_fadeout_enabled()) {
// To shut down the application when fadeout will be finished
QObject::connect(app_->player()->engine(), &EngineBase::FadeoutFinishedSignal, this, &MainWindow::DoExit);
if (app_->player()->GetState() == Engine::Playing) {
if (app_->player()->GetState() == Engine::State::Playing) {
app_->player()->Stop();
ignore_close_ = true;
close();
@@ -1322,8 +1330,8 @@ void MainWindow::MediaPlaying() {
PlaylistItemPtr item(app_->player()->GetCurrentItem());
if (item) {
enable_play_pause = !(item->options() & PlaylistItem::PauseDisabled);
can_seek = !(item->options() & PlaylistItem::SeekDisabled);
enable_play_pause = !(item->options() & PlaylistItem::Option::PauseDisabled);
can_seek = !(item->options() & PlaylistItem::Option::SeekDisabled);
}
ui_->action_play_pause->setEnabled(enable_play_pause);
ui_->track_slider->SetCanSeek(can_seek);
@@ -1437,8 +1445,8 @@ void MainWindow::SavePlaybackStatus() {
QSettings s;
s.beginGroup(Player::kSettingsGroup);
s.setValue("playback_state", app_->player()->GetState());
if (app_->player()->GetState() == Engine::Playing || app_->player()->GetState() == Engine::Paused) {
s.setValue("playback_state", static_cast<int>(app_->player()->GetState()));
if (app_->player()->GetState() == Engine::State::Playing || app_->player()->GetState() == Engine::State::Paused) {
s.setValue("playback_playlist", app_->playlist_manager()->active()->id());
s.setValue("playback_position", app_->player()->engine()->position_nanosec() / kNsecPerSec);
}
@@ -1456,14 +1464,14 @@ void MainWindow::LoadPlaybackStatus() {
QSettings s;
s.beginGroup(BehaviourSettingsPage::kSettingsGroup);
bool resume_playback = s.value("resumeplayback", false).toBool();
const bool resume_playback = s.value("resumeplayback", false).toBool();
s.endGroup();
s.beginGroup(Player::kSettingsGroup);
Engine::State playback_state = static_cast<Engine::State>(s.value("playback_state", Engine::Empty).toInt());
const Engine::State playback_state = static_cast<Engine::State>(s.value("playback_state", static_cast<int>(Engine::State::Empty)).toInt());
s.endGroup();
if (resume_playback && playback_state != Engine::Empty && playback_state != Engine::Idle) {
if (resume_playback && playback_state != Engine::State::Empty && playback_state != Engine::State::Idle) {
std::shared_ptr<QMetaObject::Connection> connection = std::make_shared<QMetaObject::Connection>();
*connection = QObject::connect(app_->playlist_manager(), &PlaylistManager::AllPlaylistsLoaded, this, [this, connection]() {
QObject::disconnect(*connection);
@@ -1479,7 +1487,7 @@ void MainWindow::ResumePlayback() {
QSettings s;
s.beginGroup(Player::kSettingsGroup);
Engine::State playback_state = static_cast<Engine::State>(s.value("playback_state", Engine::Empty).toInt());
const Engine::State playback_state = static_cast<Engine::State>(s.value("playback_state", static_cast<int>(Engine::State::Empty)).toInt());
int playback_playlist = s.value("playback_playlist", -1).toInt();
int playback_position = s.value("playback_position", 0).toInt();
s.endGroup();
@@ -1487,7 +1495,7 @@ void MainWindow::ResumePlayback() {
if (playback_playlist == app_->playlist_manager()->current()->id()) {
// Set active to current to resume playback on correct playlist.
app_->playlist_manager()->SetActiveToCurrent();
if (playback_state == Engine::Paused) {
if (playback_state == Engine::State::Paused) {
std::shared_ptr<QMetaObject::Connection> connection = std::make_shared<QMetaObject::Connection>();
*connection = QObject::connect(app_->player(), &Player::Playing, app_->player(), [this, connection]() {
QObject::disconnect(*connection);
@@ -1499,7 +1507,7 @@ void MainWindow::ResumePlayback() {
// Reset saved playback status so we don't resume again from the same position.
s.beginGroup(Player::kSettingsGroup);
s.setValue("playback_state", Engine::Empty);
s.setValue("playback_state", static_cast<int>(Engine::State::Empty));
s.setValue("playback_playlist", -1);
s.setValue("playback_position", 0);
s.endGroup();
@@ -1517,7 +1525,7 @@ void MainWindow::PlayIndex(const QModelIndex &idx, Playlist::AutoScroll autoscro
}
app_->playlist_manager()->SetActiveToCurrent();
app_->player()->PlayAt(row, 0, Engine::Manual, autoscroll, true);
app_->player()->PlayAt(row, 0, Engine::TrackChangeType::Manual, autoscroll, true);
}
@@ -1532,16 +1540,16 @@ void MainWindow::PlaylistDoubleClick(const QModelIndex &idx) {
}
switch (doubleclick_playlist_addmode_) {
case BehaviourSettingsPage::PlaylistAddBehaviour_Play:
case BehaviourSettingsPage::PlaylistAddBehaviour::Play:
app_->playlist_manager()->SetActiveToCurrent();
app_->player()->PlayAt(source_idx.row(), 0, Engine::Manual, Playlist::AutoScroll_Never, true, true);
app_->player()->PlayAt(source_idx.row(), 0, Engine::TrackChangeType::Manual, Playlist::AutoScroll::Never, true, true);
break;
case BehaviourSettingsPage::PlaylistAddBehaviour_Enqueue:
case BehaviourSettingsPage::PlaylistAddBehaviour::Enqueue:
app_->playlist_manager()->current()->queue()->ToggleTracks(QModelIndexList() << source_idx);
if (app_->player()->GetState() != Engine::Playing) {
if (app_->player()->GetState() != Engine::State::Playing) {
app_->playlist_manager()->SetActiveToCurrent();
app_->player()->PlayAt(app_->playlist_manager()->current()->queue()->TakeNext(), 0, Engine::Manual, Playlist::AutoScroll_Never, true);
app_->player()->PlayAt(app_->playlist_manager()->current()->queue()->TakeNext(), 0, Engine::TrackChangeType::Manual, Playlist::AutoScroll::Never, true);
}
break;
}
@@ -1694,22 +1702,22 @@ void MainWindow::UpdateTrackSliderPosition() {
void MainWindow::ApplyAddBehaviour(const BehaviourSettingsPage::AddBehaviour b, MimeData *mimedata) {
switch (b) {
case BehaviourSettingsPage::AddBehaviour_Append:
case BehaviourSettingsPage::AddBehaviour::Append:
mimedata->clear_first_ = false;
mimedata->enqueue_now_ = false;
break;
case BehaviourSettingsPage::AddBehaviour_Enqueue:
case BehaviourSettingsPage::AddBehaviour::Enqueue:
mimedata->clear_first_ = false;
mimedata->enqueue_now_ = true;
break;
case BehaviourSettingsPage::AddBehaviour_Load:
case BehaviourSettingsPage::AddBehaviour::Load:
mimedata->clear_first_ = true;
mimedata->enqueue_now_ = false;
break;
case BehaviourSettingsPage::AddBehaviour_OpenInNew:
case BehaviourSettingsPage::AddBehaviour::OpenInNew:
mimedata->open_in_new_playlist_ = true;
break;
}
@@ -1718,16 +1726,16 @@ void MainWindow::ApplyAddBehaviour(const BehaviourSettingsPage::AddBehaviour b,
void MainWindow::ApplyPlayBehaviour(const BehaviourSettingsPage::PlayBehaviour b, MimeData *mimedata) const {
switch (b) {
case BehaviourSettingsPage::PlayBehaviour_Always:
case BehaviourSettingsPage::PlayBehaviour::Always:
mimedata->play_now_ = true;
break;
case BehaviourSettingsPage::PlayBehaviour_Never:
case BehaviourSettingsPage::PlayBehaviour::Never:
mimedata->play_now_ = false;
break;
case BehaviourSettingsPage::PlayBehaviour_IfStopped:
mimedata->play_now_ = !(app_->player()->GetState() == Engine::Playing);
case BehaviourSettingsPage::PlayBehaviour::IfStopped:
mimedata->play_now_ = !(app_->player()->GetState() == Engine::State::Playing);
break;
}
}
@@ -1762,7 +1770,7 @@ void MainWindow::AddToPlaylist(QMimeData *q_mimedata) {
void MainWindow::AddToPlaylistFromAction(QAction *action) {
const int destination = action->data().toInt();
PlaylistItemList items;
PlaylistItemPtrList items;
SongList songs;
// Get the selected playlist items
@@ -1813,7 +1821,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
playlist_menu_index_ = source_index;
// Is this song currently playing?
if (app_->playlist_manager()->current()->current_row() == source_index.row() && app_->player()->GetState() == Engine::Playing) {
if (app_->playlist_manager()->current()->current_row() == source_index.row() && app_->player()->GetState() == Engine::State::Playing) {
playlist_play_pause_->setText(tr("Pause"));
playlist_play_pause_->setIcon(IconLoader::Load("media-playback-pause"));
}
@@ -1824,7 +1832,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
// Are we allowed to pause?
if (source_index.isValid()) {
playlist_play_pause_->setEnabled(app_->playlist_manager()->current()->current_row() != source_index.row() || !(app_->playlist_manager()->current()->item_at(source_index.row())->options() & PlaylistItem::PauseDisabled));
playlist_play_pause_->setEnabled(app_->playlist_manager()->current()->current_row() != source_index.row() || !(app_->playlist_manager()->current()->item_at(source_index.row())->options() & PlaylistItem::Option::PauseDisabled));
}
else {
playlist_play_pause_->setEnabled(false);
@@ -1853,7 +1861,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
if (!item) continue;
if (item->Metadata().url().isLocalFile()) ++local_songs;
if (item->Metadata().source() == Song::Source_Collection) ++collection_songs;
if (item->Metadata().source() == Song::Source::Collection) ++collection_songs;
if (item->Metadata().has_cue()) {
cue_selected = true;
@@ -2033,10 +2041,10 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
void MainWindow::PlaylistPlay() {
if (app_->playlist_manager()->current()->current_row() == playlist_menu_index_.row()) {
app_->player()->PlayPause(Playlist::AutoScroll_Never);
app_->player()->PlayPause(0, Playlist::AutoScroll::Never);
}
else {
PlayIndex(playlist_menu_index_, Playlist::AutoScroll_Never);
PlayIndex(playlist_menu_index_, Playlist::AutoScroll::Never);
}
}
@@ -2057,7 +2065,7 @@ void MainWindow::RescanSongs() {
if (item->IsLocalCollectionItem()) {
songs << item->Metadata();
}
else if (item->Metadata().source() == Song::Source_LocalFile) {
else if (item->Metadata().source() == Song::Source::LocalFile) {
QPersistentModelIndex persistent_index = QPersistentModelIndex(source_index);
app_->playlist_manager()->current()->ItemReload(persistent_index, item->OriginalMetadata(), false);
}
@@ -2072,7 +2080,7 @@ void MainWindow::RescanSongs() {
void MainWindow::EditTracks() {
SongList songs;
PlaylistItemList items;
PlaylistItemPtrList items;
for (const QModelIndex &proxy_index : ui_->playlist->view()->selectionModel()->selectedRows()) {
const QModelIndex source_index = app_->playlist_manager()->current()->filter()->mapToSource(proxy_index);
@@ -2143,7 +2151,7 @@ void MainWindow::SongSaveComplete(TagReaderReply *reply, const QPersistentModelI
if (reply->is_successful() && idx.isValid()) {
app_->playlist_manager()->current()->ReloadItems(QList<int>() << idx.row());
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
reply->deleteLater();
}
@@ -2164,7 +2172,7 @@ void MainWindow::SelectionSetValue() {
QPersistentModelIndex persistent_index = QPersistentModelIndex(source_index);
QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, persistent_index]() { SongSaveComplete(reply, persistent_index); }, Qt::QueuedConnection);
}
else if (song.source() == Song::Source_Stream) {
else if (song.source() == Song::Source::Stream) {
app_->playlist_manager()->current()->setData(source_index, column_value, 0);
}
}
@@ -2199,7 +2207,7 @@ void MainWindow::AddFile() {
PlaylistParser parser(app_->collection_backend());
// Show dialog
QStringList file_names = QFileDialog::getOpenFileNames(this, tr("Add file"), directory, QString("%1 (%2);;%3;;%4").arg(tr("Music"), FileView::kFileFilter, parser.filters(PlaylistParser::Type_Load), tr(kAllFilesFilterSpec)));
QStringList file_names = QFileDialog::getOpenFileNames(this, tr("Add file"), directory, QString("%1 (%2);;%3;;%4").arg(tr("Music"), FileView::kFileFilter, parser.filters(PlaylistParser::Type::Load), tr(kAllFilesFilterSpec)));
if (file_names.isEmpty()) return;
@@ -2335,30 +2343,30 @@ void MainWindow::CommandlineOptionsReceived(const quint32 instanceId, const QByt
void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) {
switch (options.player_action()) {
case CommandlineOptions::Player_Play:
case CommandlineOptions::PlayerAction::Play:
if (options.urls().empty()) {
app_->player()->Play();
}
break;
case CommandlineOptions::Player_PlayPause:
app_->player()->PlayPause(Playlist::AutoScroll_Maybe);
case CommandlineOptions::PlayerAction::PlayPause:
app_->player()->PlayPause(0, Playlist::AutoScroll::Maybe);
break;
case CommandlineOptions::Player_Pause:
case CommandlineOptions::PlayerAction::Pause:
app_->player()->Pause();
break;
case CommandlineOptions::Player_Stop:
case CommandlineOptions::PlayerAction::Stop:
app_->player()->Stop();
break;
case CommandlineOptions::Player_StopAfterCurrent:
case CommandlineOptions::PlayerAction::StopAfterCurrent:
app_->player()->StopAfterCurrent();
break;
case CommandlineOptions::Player_Previous:
case CommandlineOptions::PlayerAction::Previous:
app_->player()->Previous();
break;
case CommandlineOptions::Player_Next:
case CommandlineOptions::PlayerAction::Next:
app_->player()->Next();
break;
case CommandlineOptions::Player_PlayPlaylist:
case CommandlineOptions::PlayerAction::PlayPlaylist:
if (options.playlist_name().isEmpty()) {
qLog(Error) << "ERROR: playlist name missing";
}
@@ -2366,11 +2374,11 @@ void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) {
app_->player()->PlayPlaylist(options.playlist_name());
}
break;
case CommandlineOptions::Player_RestartOrPrevious:
case CommandlineOptions::PlayerAction::RestartOrPrevious:
app_->player()->RestartOrPrevious();
break;
case CommandlineOptions::Player_ResizeWindow:{
case CommandlineOptions::PlayerAction::ResizeWindow:{
if (options.window_size().contains('x') && options.window_size().length() >= 4) {
QString str_w = options.window_size().left(options.window_size().indexOf('x'));
QString str_h = options.window_size().right(options.window_size().length() - options.window_size().indexOf('x') - 1);
@@ -2407,7 +2415,7 @@ void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) {
break;
}
case CommandlineOptions::Player_None:
case CommandlineOptions::PlayerAction::None:
break;
}
@@ -2427,22 +2435,22 @@ void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) {
// Behaviour depends on command line options, so set it here
mimedata->override_user_settings_ = true;
if (options.player_action() == CommandlineOptions::Player_Play) mimedata->play_now_ = true;
if (options.player_action() == CommandlineOptions::PlayerAction::Play) mimedata->play_now_ = true;
else ApplyPlayBehaviour(doubleclick_playmode_, mimedata);
switch (options.url_list_action()) {
case CommandlineOptions::UrlList_Load:
case CommandlineOptions::UrlListAction::Load:
mimedata->clear_first_ = true;
break;
case CommandlineOptions::UrlList_Append:
case CommandlineOptions::UrlListAction::Append:
// Nothing to do
break;
case CommandlineOptions::UrlList_None:
case CommandlineOptions::UrlListAction::None:
ApplyAddBehaviour(doubleclick_addmode_, mimedata);
break;
case CommandlineOptions::UrlList_CreateNew:
case CommandlineOptions::UrlListAction::CreateNew:
mimedata->name_for_new_playlist_ = options.playlist_name();
ApplyAddBehaviour(BehaviourSettingsPage::AddBehaviour_OpenInNew, mimedata);
ApplyAddBehaviour(BehaviourSettingsPage::AddBehaviour::OpenInNew, mimedata);
break;
}
@@ -2462,7 +2470,7 @@ void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) {
app_->player()->SeekTo(app_->player()->engine()->position_nanosec() / kNsecPerSec + options.seek_by());
}
if (options.play_track_at() != -1) app_->player()->PlayAt(options.play_track_at(), 0, Engine::Manual, Playlist::AutoScroll_Maybe, true);
if (options.play_track_at() != -1) app_->player()->PlayAt(options.play_track_at(), 0, Engine::TrackChangeType::Manual, Playlist::AutoScroll::Maybe, true);
if (options.show_osd()) app_->player()->ShowOSD();
@@ -2540,7 +2548,7 @@ void MainWindow::AddFilesToTranscoder() {
}
void MainWindow::ShowCollectionConfig() {
settings_dialog_->OpenAtPage(SettingsDialog::Page_Collection);
settings_dialog_->OpenAtPage(SettingsDialog::Page::Collection);
}
void MainWindow::TaskCountChanged(const int count) {
@@ -2610,7 +2618,7 @@ void MainWindow::EditFileTags(const QList<QUrl> &urls) {
Song song;
song.set_url(url);
song.set_valid(true);
song.set_filetype(Song::FileType_MPEG);
song.set_filetype(Song::FileType::MPEG);
songs << song;
}
@@ -2755,13 +2763,13 @@ void MainWindow::PlaylistCopyToDevice() {
void MainWindow::ChangeCollectionFilterMode(QAction *action) {
if (action == collection_show_duplicates_) {
collection_view_->filter_widget()->SetFilterMode(CollectionFilterOptions::FilterMode_Duplicates);
collection_view_->filter_widget()->SetFilterMode(CollectionFilterOptions::FilterMode::Duplicates);
}
else if (action == collection_show_untagged_) {
collection_view_->filter_widget()->SetFilterMode(CollectionFilterOptions::FilterMode_Untagged);
collection_view_->filter_widget()->SetFilterMode(CollectionFilterOptions::FilterMode::Untagged);
}
else {
collection_view_->filter_widget()->SetFilterMode(CollectionFilterOptions::FilterMode_All);
collection_view_->filter_widget()->SetFilterMode(CollectionFilterOptions::FilterMode::All);
}
}
@@ -2972,7 +2980,7 @@ void MainWindow::HandleNotificationPreview(const OSDBase::Behaviour type, const
else {
qLog(Debug) << "The current playlist is empty, showing a fake song";
// Create a fake song
Song fake(Song::Source_LocalFile);
Song fake(Song::Source::LocalFile);
fake.Init("Title", "Artist", "Album", 123);
fake.set_genre("Classical");
fake.set_composer("Anonymous");
@@ -2996,7 +3004,7 @@ void MainWindow::ShowConsole() {
void MainWindow::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Space) {
app_->player()->PlayPause(Playlist::AutoScroll_Never);
app_->player()->PlayPause(0, Playlist::AutoScroll::Never);
e->accept();
}
else if (e->key() == Qt::Key_Left) {
@@ -3159,17 +3167,17 @@ void MainWindow::PlaylistDelete() {
if (DeleteConfirmationDialog::warning(files) != QDialogButtonBox::Yes) return;
if (app_->player()->GetState() == Engine::Playing && app_->playlist_manager()->current()->rowCount() == selected_songs.count()) {
if (app_->player()->GetState() == Engine::State::Playing && app_->playlist_manager()->current()->rowCount() == selected_songs.count()) {
app_->player()->Stop();
}
ui_->playlist->view()->RemoveSelected();
if (app_->player()->GetState() == Engine::Playing && is_current_item) {
if (app_->player()->GetState() == Engine::State::Playing && is_current_item) {
app_->player()->Next();
}
std::shared_ptr<MusicStorage> storage = std::make_shared<FilesystemMusicStorage>(Song::Source_LocalFile, "/");
std::shared_ptr<MusicStorage> storage = std::make_shared<FilesystemMusicStorage>(Song::Source::LocalFile, "/");
DeleteFiles *delete_files = new DeleteFiles(app_->task_manager(), storage, true);
//QObject::connect(delete_files, &DeleteFiles::Finished, this, &MainWindow::DeleteFinished);
delete_files->Start(selected_songs);

View File

@@ -331,7 +331,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
std::unique_ptr<TagFetcher> tag_fetcher_;
#endif
std::unique_ptr<TrackSelectionDialog> track_selection_dialog_;
PlaylistItemList autocomplete_tag_items_;
PlaylistItemPtrList autocomplete_tag_items_;
SmartPlaylistsViewContainer *smartplaylists_view_;

View File

@@ -38,7 +38,6 @@
#include <QByteArray>
#include <QUrl>
#include <QImage>
#include <QNetworkCookie>
#include <QNetworkReply>
#include <QItemSelection>
#ifdef HAVE_DBUS
@@ -78,17 +77,18 @@
# include "device/mtpconnection.h"
#endif
#include "settings/playlistsettingspage.h"
#include "smartplaylists/smartplaylistsearchterm.h"
#include "smartplaylists/smartplaylistsitem.h"
void RegisterMetaTypes() {
qRegisterMetaType<const char*>("const char*");
qRegisterMetaType<QList<int>>("QList<int>");
qRegisterMetaType<QList<QUrl>>("QList<QUrl>");
qRegisterMetaType<QVector<int>>("QVector<int>");
qRegisterMetaType<QList<QUrl>>("QList<QUrl>");
qRegisterMetaType<QFileInfo>("QFileInfo");
qRegisterMetaType<QAbstractSocket::SocketState>();
qRegisterMetaType<QAbstractSocket::SocketState>("QAbstractSocket::SocketState");
qRegisterMetaType<QNetworkCookie>("QNetworkCookie");
qRegisterMetaType<QList<QNetworkCookie>>("QList<QNetworkCookie>");
qRegisterMetaType<QNetworkReply*>("QNetworkReply*");
qRegisterMetaType<QNetworkReply**>("QNetworkReply**");
qRegisterMetaType<QItemSelection>("QItemSelection");
@@ -98,16 +98,12 @@ void RegisterMetaTypes() {
qRegisterMetaTypeStreamOperators<QMap<int, Qt::Alignment>>("ColumnAlignmentMap");
qRegisterMetaTypeStreamOperators<QMap<int, int>>("ColumnAlignmentIntMap");
#endif
qRegisterMetaType<CollectionDirectory>("Directory");
qRegisterMetaType<CollectionDirectoryList>("DirectoryList");
qRegisterMetaType<CollectionSubdirectory>("Subdirectory");
qRegisterMetaType<CollectionSubdirectoryList>("SubdirectoryList");
qRegisterMetaType<Song>("Song");
qRegisterMetaType<SongList>("SongList");
qRegisterMetaType<SongMap>("SongMap");
qRegisterMetaType<QList<Song>>("QList<Song>");
qRegisterMetaType<QMap<QString, Song>>("QMap<QString, Song>");
qRegisterMetaType<Engine::EngineType>("EngineType");
qRegisterMetaType<Song::Source>("Song::Source");
qRegisterMetaType<Song::FileType>("Song::FileType");
qRegisterMetaType<Engine::EngineType>("Engine::EngineType");
qRegisterMetaType<Engine::SimpleMetaBundle>("Engine::SimpleMetaBundle");
qRegisterMetaType<Engine::State>("Engine::State");
qRegisterMetaType<Engine::TrackChangeFlags>("Engine::TrackChangeFlags");
@@ -117,23 +113,29 @@ void RegisterMetaTypes() {
qRegisterMetaType<GstElement*>("GstElement*");
qRegisterMetaType<GstEnginePipeline*>("GstEnginePipeline*");
#endif
qRegisterMetaType<CollectionDirectory>("CollectionDirectory");
qRegisterMetaType<CollectionDirectoryList>("CollectionDirectoryList");
qRegisterMetaType<CollectionSubdirectory>("CollectionSubdirectory");
qRegisterMetaType<CollectionSubdirectoryList>("CollectionSubdirectoryList");
qRegisterMetaType<CollectionModel::Grouping>("CollectionModel::Grouping");
qRegisterMetaType<PlaylistItemPtr>("PlaylistItemPtr");
qRegisterMetaType<PlaylistItemList>("PlaylistItemList");
qRegisterMetaType<QList<PlaylistItemPtr>>("QList<PlaylistItemPtr>");
qRegisterMetaType<PlaylistItemPtrList>("PlaylistItemPtrList");
qRegisterMetaType<PlaylistSequence::RepeatMode>("PlaylistSequence::RepeatMode");
qRegisterMetaType<PlaylistSequence::ShuffleMode>("PlaylistSequence::ShuffleMode");
qRegisterMetaType<AlbumCoverLoaderResult>("AlbumCoverLoaderResult");
qRegisterMetaType<AlbumCoverLoaderResult::Type>("AlbumCoverLoaderResult::Type");
qRegisterMetaType<CoverProviderSearchResult>("CoverProviderSearchResult");
qRegisterMetaType<CoverSearchStatistics>("CoverSearchStatistics");
qRegisterMetaType<QList<CoverProviderSearchResult>>("QList<CoverProviderSearchResult>");
qRegisterMetaType<CoverProviderSearchResults>("CoverProviderSearchResults");
qRegisterMetaType<CoverSearchStatistics>("CoverSearchStatistics");
qRegisterMetaType<Equalizer::Params>("Equalizer::Params");
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
qRegisterMetaTypeStreamOperators<Equalizer::Params>("Equalizer::Params");
#endif
#ifdef HAVE_DBUS
qDBusRegisterMetaType<QList<QByteArray>>();
qDBusRegisterMetaType<QByteArrayList>();
qDBusRegisterMetaType<QImage>();
qDBusRegisterMetaType<TrackMetadata>();
qDBusRegisterMetaType<Track_Ids>();
@@ -144,15 +146,21 @@ void RegisterMetaTypes() {
qDBusRegisterMetaType<ManagedObjectList>();
#endif
qRegisterMetaType<InternetSearchView::ResultList>("InternetSearchView::ResultList");
qRegisterMetaType<InternetSearchView::Result>("InternetSearchView::Result");
qRegisterMetaType<InternetSearchView::ResultList>("InternetSearchView::ResultList");
qRegisterMetaType<PlaylistGeneratorPtr>("PlaylistGeneratorPtr");
qRegisterMetaType<RadioChannel>("RadioChannel");
qRegisterMetaType<RadioChannelList>("RadioChannelList");
#ifdef HAVE_LIBMTP
qRegisterMetaType<MtpConnection*>("MtpConnection*");
#endif
qRegisterMetaType<PlaylistSettingsPage::PathType>("PlaylistSettingsPage::PathType");
qRegisterMetaType<PlaylistGeneratorPtr>("PlaylistGeneratorPtr");
qRegisterMetaType<SmartPlaylistSearchTerm::Operator>("SmartPlaylistSearchTerm::Operator");
qRegisterMetaType<SmartPlaylistSearchTerm::OperatorList>("SmartPlaylistSearchTerm::OperatorList");
qRegisterMetaType<SmartPlaylistsItem::Type>("SmartPlaylistsItem::Type");
}

View File

@@ -164,7 +164,7 @@ void Mpris2::PlaylistManagerInitialized() {
void Mpris2::EngineStateChanged(Engine::State newState) {
if (newState != Engine::Playing && newState != Engine::Paused) {
if (newState != Engine::State::Playing && newState != Engine::State::Paused) {
last_metadata_ = QVariantMap();
EmitNotification("Metadata");
}
@@ -172,7 +172,7 @@ void Mpris2::EngineStateChanged(Engine::State newState) {
EmitNotification("CanPlay");
EmitNotification("CanPause");
EmitNotification("PlaybackStatus", PlaybackStatus(newState));
if (newState == Engine::Playing) EmitNotification("CanSeek", CanSeek(newState));
if (newState == Engine::State::Playing) EmitNotification("CanSeek", CanSeek(newState));
}
@@ -307,8 +307,8 @@ QString Mpris2::PlaybackStatus() const {
QString Mpris2::PlaybackStatus(Engine::State state) const {
switch (state) {
case Engine::Playing: return "Playing";
case Engine::Paused: return "Paused";
case Engine::State::Playing: return "Playing";
case Engine::State::Paused: return "Paused";
default: return "Stopped";
}
@@ -321,9 +321,9 @@ QString Mpris2::LoopStatus() const {
}
switch (app_->playlist_manager()->sequence()->repeat_mode()) {
case PlaylistSequence::Repeat_Album:
case PlaylistSequence::Repeat_Playlist: return "Playlist";
case PlaylistSequence::Repeat_Track: return "Track";
case PlaylistSequence::RepeatMode::Album:
case PlaylistSequence::RepeatMode::Playlist: return "Playlist";
case PlaylistSequence::RepeatMode::Track: return "Track";
default: return "None";
}
@@ -331,16 +331,16 @@ QString Mpris2::LoopStatus() const {
void Mpris2::SetLoopStatus(const QString &value) {
PlaylistSequence::RepeatMode mode = PlaylistSequence::Repeat_Off;
PlaylistSequence::RepeatMode mode = PlaylistSequence::RepeatMode::Off;
if (value == "None") {
mode = PlaylistSequence::Repeat_Off;
mode = PlaylistSequence::RepeatMode::Off;
}
else if (value == "Track") {
mode = PlaylistSequence::Repeat_Track;
mode = PlaylistSequence::RepeatMode::Track;
}
else if (value == "Playlist") {
mode = PlaylistSequence::Repeat_Playlist;
mode = PlaylistSequence::RepeatMode::Playlist;
}
app_->playlist_manager()->active()->sequence()->SetRepeatMode(mode);
@@ -359,12 +359,12 @@ void Mpris2::SetRate(double rate) {
bool Mpris2::Shuffle() const {
return app_->playlist_manager()->sequence()->shuffle_mode() != PlaylistSequence::Shuffle_Off;
return app_->playlist_manager()->sequence()->shuffle_mode() != PlaylistSequence::ShuffleMode::Off;
}
void Mpris2::SetShuffle(bool enable) {
app_->playlist_manager()->active()->sequence()->SetShuffleMode(enable ? PlaylistSequence::Shuffle_All : PlaylistSequence::Shuffle_Off);
app_->playlist_manager()->active()->sequence()->SetShuffleMode(enable ? PlaylistSequence::ShuffleMode::All : PlaylistSequence::ShuffleMode::Off);
}
QVariantMap Mpris2::Metadata() const { return last_metadata_; }
@@ -447,13 +447,13 @@ bool Mpris2::CanPlay() const {
// This one's a bit different than MPRIS 1 - we want this to be true even when the song is already paused or stopped.
bool Mpris2::CanPause() const {
return (app_->player()->GetCurrentItem() && app_->player()->GetState() == Engine::Playing && !(app_->player()->GetCurrentItem()->options() & PlaylistItem::PauseDisabled)) || PlaybackStatus() == "Paused" || PlaybackStatus() == "Stopped";
return (app_->player()->GetCurrentItem() && app_->player()->GetState() == Engine::State::Playing && !(app_->player()->GetCurrentItem()->options() & PlaylistItem::Option::PauseDisabled)) || PlaybackStatus() == "Paused" || PlaybackStatus() == "Stopped";
}
bool Mpris2::CanSeek() const { return CanSeek(app_->player()->GetState()); }
bool Mpris2::CanSeek(Engine::State state) const {
return app_->player()->GetCurrentItem() && state != Engine::Empty && !app_->player()->GetCurrentItem()->Metadata().is_stream();
return app_->player()->GetCurrentItem() && state != Engine::State::Empty && !app_->player()->GetCurrentItem()->Metadata().is_stream();
}
bool Mpris2::CanControl() const { return true; }
@@ -471,7 +471,7 @@ void Mpris2::Previous() {
}
void Mpris2::Pause() {
if (CanPause() && app_->player()->GetState() != Engine::Paused) {
if (CanPause() && app_->player()->GetState() != Engine::State::Paused) {
app_->player()->Pause();
}
}

View File

@@ -50,7 +50,7 @@ class MusicStorage {
};
// Values are saved in the database - don't change
enum TranscodeMode {
enum class TranscodeMode {
Transcode_Always = 1,
Transcode_Never = 2,
Transcode_Unsupported = 3,
@@ -83,8 +83,8 @@ class MusicStorage {
virtual QString LocalPath() const { return QString(); }
virtual std::optional<int> collection_directory_id() const { return std::optional<int>(); }
virtual TranscodeMode GetTranscodeMode() const { return Transcode_Never; }
virtual Song::FileType GetTranscodeFormat() const { return Song::FileType_Unknown; }
virtual TranscodeMode GetTranscodeMode() const { return TranscodeMode::Transcode_Never; }
virtual Song::FileType GetTranscodeFormat() const { return Song::FileType::Unknown; }
virtual bool GetSupportedFiletypes(QList<Song::FileType> *ret) { Q_UNUSED(ret); return true; }
virtual bool StartCopy(QList<Song::FileType> *supported_types) { Q_UNUSED(supported_types); return true; }

View File

@@ -35,7 +35,7 @@ NetworkProxyFactory *NetworkProxyFactory::sInstance = nullptr;
const char *NetworkProxyFactory::kSettingsGroup = "NetworkProxy";
NetworkProxyFactory::NetworkProxyFactory()
: mode_(Mode_System),
: mode_(Mode::System),
type_(QNetworkProxy::HttpProxy),
port_(8080),
use_authentication_(false) {
@@ -81,7 +81,7 @@ void NetworkProxyFactory::ReloadSettings() {
QSettings s;
s.beginGroup(kSettingsGroup);
mode_ = Mode(s.value("mode", Mode_System).toInt());
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();
@@ -100,7 +100,7 @@ QList<QNetworkProxy> NetworkProxyFactory::queryProxy(const QNetworkProxyQuery &q
QNetworkProxy ret;
switch (mode_) {
case Mode_System:
case Mode::System:
#ifdef Q_OS_LINUX
Q_UNUSED(query);
@@ -125,11 +125,11 @@ QList<QNetworkProxy> NetworkProxyFactory::queryProxy(const QNetworkProxyQuery &q
return systemProxyForQuery(query);
#endif
case Mode_Direct:
case Mode::Direct:
ret.setType(QNetworkProxy::NoProxy);
break;
case Mode_Manual:
case Mode::Manual:
ret.setType(type_);
ret.setHostName(hostname_);
ret.setPort(port_);

View File

@@ -34,7 +34,11 @@
class NetworkProxyFactory : public QNetworkProxyFactory {
public:
// These values are persisted
enum Mode { Mode_System = 0, Mode_Direct = 1, Mode_Manual = 2, };
enum class Mode {
System = 0,
Direct = 1,
Manual = 2
};
static NetworkProxyFactory *Instance();
static const char *kSettingsGroup;

View File

@@ -79,22 +79,22 @@ Player::Player(Application *app, QObject *parent)
#endif
analyzer_(nullptr),
equalizer_(nullptr),
stream_change_type_(Engine::First),
autoscroll_(Playlist::AutoScroll_Maybe),
last_state_(Engine::Empty),
stream_change_type_(Engine::TrackChangeType::First),
autoscroll_(Playlist::AutoScroll::Maybe),
last_state_(Engine::State::Empty),
nb_errors_received_(0),
volume_(100),
volume_before_mute_(100),
last_pressed_previous_(QDateTime::currentDateTime()),
continue_on_error_(false),
greyout_(true),
menu_previousmode_(BehaviourSettingsPage::PreviousBehaviour_DontRestart),
menu_previousmode_(BehaviourSettingsPage::PreviousBehaviour::DontRestart),
seek_step_sec_(10),
play_offset_nanosec_(0) {
QSettings s;
s.beginGroup(BackendSettingsPage::kSettingsGroup);
Engine::EngineType enginetype = Engine::EngineTypeFromName(s.value("engine", EngineName(Engine::GStreamer)).toString().toLower());
Engine::EngineType enginetype = Engine::EngineTypeFromName(s.value("engine", EngineName(Engine::EngineType::GStreamer)).toString().toLower());
s.endGroup();
CreateEngine(enginetype);
@@ -103,14 +103,14 @@ Player::Player(Application *app, QObject *parent)
Engine::EngineType Player::CreateEngine(Engine::EngineType enginetype) {
Engine::EngineType use_enginetype(Engine::None);
Engine::EngineType use_enginetype(Engine::EngineType::None);
for (int i = 0; use_enginetype == Engine::None; i++) {
for (int i = 0; use_enginetype == Engine::EngineType::None; i++) {
switch (enginetype) {
case Engine::None:
case Engine::EngineType::None:
#ifdef HAVE_GSTREAMER
case Engine::GStreamer:{
use_enginetype=Engine::GStreamer;
case Engine::EngineType::GStreamer:{
use_enginetype=Engine::EngineType::GStreamer;
std::unique_ptr<GstEngine> gst_engine(new GstEngine(app_->task_manager()));
gst_engine->SetStartup(gst_startup_);
engine_.reset(gst_engine.release());
@@ -118,8 +118,8 @@ Engine::EngineType Player::CreateEngine(Engine::EngineType enginetype) {
}
#endif
#ifdef HAVE_VLC
case Engine::VLC:
use_enginetype = Engine::VLC;
case Engine::EngineType::VLC:
use_enginetype = Engine::EngineType::VLC;
engine_ = std::make_shared<VLCEngine>(app_->task_manager());
break;
#endif
@@ -127,7 +127,7 @@ Engine::EngineType Player::CreateEngine(Engine::EngineType enginetype) {
if (i > 0) {
qFatal("No engine available!");
}
enginetype = Engine::None;
enginetype = Engine::EngineType::None;
break;
}
}
@@ -157,7 +157,7 @@ void Player::Init() {
if (!engine_) {
s.beginGroup(BackendSettingsPage::kSettingsGroup);
Engine::EngineType enginetype = Engine::EngineTypeFromName(s.value("engine", EngineName(Engine::GStreamer)).toString().toLower());
Engine::EngineType enginetype = Engine::EngineTypeFromName(s.value("engine", EngineName(Engine::EngineType::GStreamer)).toString().toLower());
s.endGroup();
CreateEngine(enginetype);
}
@@ -205,7 +205,7 @@ void Player::ReloadSettings() {
s.endGroup();
s.beginGroup(BehaviourSettingsPage::kSettingsGroup);
menu_previousmode_ = BehaviourSettingsPage::PreviousBehaviour(s.value("menu_previousmode", BehaviourSettingsPage::PreviousBehaviour_DontRestart).toInt());
menu_previousmode_ = static_cast<BehaviourSettingsPage::PreviousBehaviour>(s.value("menu_previousmode", static_cast<int>(BehaviourSettingsPage::PreviousBehaviour::DontRestart)).toInt());
seek_step_sec_ = s.value("seek_step_sec", 10).toInt();
s.endGroup();
@@ -230,7 +230,7 @@ void Player::SaveVolume() {
s.beginGroup(kSettingsGroup);
s.setValue("volume", volume_);
s.endGroup();
}
void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
@@ -265,19 +265,19 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
}
switch (result.type_) {
case UrlHandler::LoadResult::Error:
case UrlHandler::LoadResult::Type::Error:
if (is_current) {
InvalidSongRequested(result.original_url_);
}
emit Error(result.error_);
break;
case UrlHandler::LoadResult::NoMoreTracks:
case UrlHandler::LoadResult::Type::NoMoreTracks:
qLog(Debug) << "URL handler for" << result.original_url_ << "said no more tracks" << is_current;
if (is_current) NextItem(stream_change_type_, autoscroll_);
break;
case UrlHandler::LoadResult::TrackAvailable: {
case UrlHandler::LoadResult::Type::TrackAvailable: {
qLog(Debug) << "URL handler for" << result.original_url_ << "returned" << result.stream_url_;
@@ -300,9 +300,9 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
// If there was no filetype in the song's metadata, use the one provided by URL handler, if there is one.
if (
(song.filetype() == Song::FileType_Unknown && result.filetype_ != Song::FileType_Unknown)
(song.filetype() == Song::FileType::Unknown && result.filetype_ != Song::FileType::Unknown)
||
(song.filetype() == Song::FileType_Stream && result.filetype_ != Song::FileType_Stream)
(song.filetype() == Song::FileType::Stream && result.filetype_ != Song::FileType::Stream)
)
{
song.set_filetype(result.filetype_);
@@ -353,7 +353,7 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
break;
}
case UrlHandler::LoadResult::WillLoadAsynchronously:
case UrlHandler::LoadResult::Type::WillLoadAsynchronously:
qLog(Debug) << "URL handler for" << result.original_url_ << "is loading asynchronously";
// We'll get called again later with either NoMoreTracks or TrackAvailable
@@ -363,7 +363,7 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
}
void Player::Next() { NextInternal(Engine::Manual, Playlist::AutoScroll_Always); }
void Player::Next() { NextInternal(Engine::TrackChangeType::Manual, Playlist::AutoScroll::Always); }
void Player::NextInternal(const Engine::TrackChangeFlags change, const Playlist::AutoScroll autoscroll) {
@@ -384,10 +384,10 @@ void Player::NextItem(const Engine::TrackChangeFlags change, const Playlist::Aut
Playlist *active_playlist = app_->playlist_manager()->active();
// If we received too many errors in auto change, with repeat enabled, we stop
if (change == Engine::Auto) {
if (change == Engine::TrackChangeType::Auto) {
const PlaylistSequence::RepeatMode repeat_mode = active_playlist->sequence()->repeat_mode();
if (repeat_mode != PlaylistSequence::Repeat_Off) {
if ((repeat_mode == PlaylistSequence::Repeat_Track && nb_errors_received_ >= 3) || (nb_errors_received_ >= app_->playlist_manager()->active()->filter()->rowCount())) {
if (repeat_mode != PlaylistSequence::RepeatMode::Off) {
if ((repeat_mode == PlaylistSequence::RepeatMode::Track && nb_errors_received_ >= 3) || (nb_errors_received_ >= app_->playlist_manager()->active()->filter()->rowCount())) {
// We received too many "Error" state changes: probably looping over a playlist which contains only unavailable elements: stop now.
nb_errors_received_ = 0;
Stop();
@@ -397,7 +397,7 @@ void Player::NextItem(const Engine::TrackChangeFlags change, const Playlist::Aut
}
// Manual track changes override "Repeat track"
const bool ignore_repeat_track = change & Engine::Manual;
const bool ignore_repeat_track = change & Engine::TrackChangeType::Manual;
int i = active_playlist->next_row(ignore_repeat_track);
if (i == -1) {
@@ -413,7 +413,7 @@ void Player::NextItem(const Engine::TrackChangeFlags change, const Playlist::Aut
}
void Player::PlayPlaylist(const QString &playlist_name) {
PlayPlaylistInternal(Engine::Manual, Playlist::AutoScroll_Always, playlist_name);
PlayPlaylistInternal(Engine::TrackChangeType::Manual, Playlist::AutoScroll::Always, playlist_name);
}
void Player::PlayPlaylistInternal(const Engine::TrackChangeFlags change, const Playlist::AutoScroll autoscroll, const QString &playlist_name) {
@@ -472,22 +472,22 @@ void Player::TrackEnded() {
app_->playlist_manager()->collection_backend()->IncrementPlayCountAsync(current_item_->Metadata().id());
}
if (HandleStopAfter(Playlist::AutoScroll_Maybe)) return;
if (HandleStopAfter(Playlist::AutoScroll::Maybe)) return;
NextInternal(Engine::Auto, Playlist::AutoScroll_Maybe);
NextInternal(Engine::TrackChangeType::Auto, Playlist::AutoScroll::Maybe);
}
void Player::PlayPause(const quint64 offset_nanosec, const Playlist::AutoScroll autoscroll) {
switch (engine_->state()) {
case Engine::Paused:
case Engine::State::Paused:
UnPause();
emit Resumed();
break;
case Engine::Playing: {
if (current_item_->options() & PlaylistItem::PauseDisabled) {
case Engine::State::Playing: {
if (current_item_->options() & PlaylistItem::Option::PauseDisabled) {
Stop();
}
else {
@@ -498,9 +498,9 @@ void Player::PlayPause(const quint64 offset_nanosec, const Playlist::AutoScroll
break;
}
case Engine::Empty:
case Engine::Error:
case Engine::Idle: {
case Engine::State::Empty:
case Engine::State::Error:
case Engine::State::Idle: {
pause_time_ = QDateTime();
play_offset_nanosec_ = offset_nanosec;
app_->playlist_manager()->SetActivePlaylist(app_->playlist_manager()->current_id());
@@ -508,7 +508,7 @@ void Player::PlayPause(const quint64 offset_nanosec, const Playlist::AutoScroll
int i = app_->playlist_manager()->active()->current_row();
if (i == -1) i = app_->playlist_manager()->active()->last_played_row();
if (i == -1) i = 0;
PlayAt(i, offset_nanosec, Engine::First, autoscroll, true);
PlayAt(i, offset_nanosec, Engine::TrackChangeType::First, autoscroll, true);
break;
}
}
@@ -568,45 +568,45 @@ void Player::StopAfterCurrent() {
bool Player::PreviousWouldRestartTrack() const {
// Check if it has been over two seconds since previous button was pressed
return menu_previousmode_ == BehaviourSettingsPage::PreviousBehaviour_Restart && last_pressed_previous_.isValid() && last_pressed_previous_.secsTo(QDateTime::currentDateTime()) >= 2;
return menu_previousmode_ == BehaviourSettingsPage::PreviousBehaviour::Restart && last_pressed_previous_.isValid() && last_pressed_previous_.secsTo(QDateTime::currentDateTime()) >= 2;
}
void Player::Previous() { PreviousItem(Engine::Manual); }
void Player::Previous() { PreviousItem(Engine::TrackChangeType::Manual); }
void Player::PreviousItem(const Engine::TrackChangeFlags change) {
pause_time_ = QDateTime();
play_offset_nanosec_ = 0;
const bool ignore_repeat_track = change & Engine::Manual;
const bool ignore_repeat_track = change & Engine::TrackChangeType::Manual;
if (menu_previousmode_ == BehaviourSettingsPage::PreviousBehaviour_Restart) {
if (menu_previousmode_ == BehaviourSettingsPage::PreviousBehaviour::Restart) {
// Check if it has been over two seconds since previous button was pressed
QDateTime now = QDateTime::currentDateTime();
if (last_pressed_previous_.isValid() && last_pressed_previous_.secsTo(now) >= 2) {
last_pressed_previous_ = now;
PlayAt(app_->playlist_manager()->active()->current_row(), 0, change, Playlist::AutoScroll_Always, false, true);
PlayAt(app_->playlist_manager()->active()->current_row(), 0, change, Playlist::AutoScroll::Always, false, true);
return;
}
last_pressed_previous_ = now;
}
int i = app_->playlist_manager()->active()->previous_row(ignore_repeat_track);
app_->playlist_manager()->active()->set_current_row(i, Playlist::AutoScroll_Always, false);
app_->playlist_manager()->active()->set_current_row(i, Playlist::AutoScroll::Always, false);
if (i == -1) {
Stop();
PlayAt(i, 0, change, Playlist::AutoScroll_Always, true);
PlayAt(i, 0, change, Playlist::AutoScroll::Always, true);
return;
}
PlayAt(i, 0, change, Playlist::AutoScroll_Always, false);
PlayAt(i, 0, change, Playlist::AutoScroll::Always, false);
}
void Player::EngineStateChanged(const Engine::State state) {
if (state == Engine::Error) {
if (state == Engine::State::Error) {
nb_errors_received_++;
}
else {
@@ -614,21 +614,21 @@ void Player::EngineStateChanged(const Engine::State state) {
}
switch (state) {
case Engine::Paused:
case Engine::State::Paused:
pause_time_ = QDateTime::currentDateTime();
play_offset_nanosec_ = engine_->position_nanosec();
emit Paused();
break;
case Engine::Playing:
case Engine::State::Playing:
pause_time_ = QDateTime();
play_offset_nanosec_ = 0;
emit Playing();
break;
case Engine::Error:
case Engine::State::Error:
emit Error();
[[fallthrough]];
case Engine::Empty:
case Engine::Idle:
case Engine::State::Empty:
case Engine::State::Idle:
pause_time_ = QDateTime();
play_offset_nanosec_ = 0;
emit Stopped();
@@ -700,12 +700,12 @@ void Player::PlayAt(const int index, const quint64 offset_nanosec, Engine::Track
pause_time_ = QDateTime();
play_offset_nanosec_ = offset_nanosec;
if (current_item_ && change == Engine::Manual && engine_->position_nanosec() != engine_->length_nanosec()) {
if (current_item_ && change == Engine::TrackChangeType::Manual && engine_->position_nanosec() != engine_->length_nanosec()) {
emit TrackSkipped(current_item_);
}
if (current_item_ && app_->playlist_manager()->active()->has_item_at(index) && current_item_->Metadata().IsOnSameAlbum(app_->playlist_manager()->active()->item_at(index)->Metadata())) {
change |= Engine::SameAlbum;
change |= Engine::TrackChangeType::SameAlbum;
}
if (reshuffle) app_->playlist_manager()->active()->ReshuffleIndices();
@@ -761,7 +761,7 @@ void Player::SeekTo(const quint64 seconds) {
emit Seeked(nanosec / 1000);
if (seconds == 0) {
app_->playlist_manager()->active()->InformOfCurrentSongChange(Playlist::AutoScroll_Maybe, false);
app_->playlist_manager()->active()->InformOfCurrentSongChange(Playlist::AutoScroll::Maybe, false);
}
}
@@ -776,7 +776,7 @@ void Player::SeekBackward() {
void Player::EngineMetadataReceived(const Engine::SimpleMetaBundle &bundle) {
if (bundle.type == Engine::SimpleMetaBundle::Type_Any || bundle.type == Engine::SimpleMetaBundle::Type_Current) {
if (bundle.type == Engine::SimpleMetaBundle::Type::Any || bundle.type == Engine::SimpleMetaBundle::Type::Current) {
PlaylistItemPtr item = app_->playlist_manager()->active()->current_item();
if (item && bundle.url == item->Url()) {
Song song = item->Metadata();
@@ -786,7 +786,7 @@ void Player::EngineMetadataReceived(const Engine::SimpleMetaBundle &bundle) {
}
}
if (bundle.type == Engine::SimpleMetaBundle::Type_Any || bundle.type == Engine::SimpleMetaBundle::Type_Next) {
if (bundle.type == Engine::SimpleMetaBundle::Type::Any || bundle.type == Engine::SimpleMetaBundle::Type::Next) {
int next_row = app_->playlist_manager()->active()->next_row();
if (next_row != -1) {
PlaylistItemPtr next_item = app_->playlist_manager()->active()->item_at(next_row);
@@ -828,10 +828,10 @@ void Player::Pause() { engine_->Pause(); }
void Player::Play(const quint64 offset_nanosec) {
switch (GetState()) {
case Engine::Playing:
case Engine::State::Playing:
SeekTo(offset_nanosec);
break;
case Engine::Paused:
case Engine::State::Paused:
UnPause();
break;
default:
@@ -880,18 +880,18 @@ void Player::TrackAboutToEnd() {
// Get the actual track URL rather than the stream URL.
if (url_handlers_.contains(url.scheme())) {
if (loading_async_.contains(url)) return;
autoscroll_ = Playlist::AutoScroll_Maybe;
autoscroll_ = Playlist::AutoScroll::Maybe;
UrlHandler::LoadResult result = url_handlers_[url.scheme()]->StartLoading(url);
switch (result.type_) {
case UrlHandler::LoadResult::Error:
case UrlHandler::LoadResult::Type::Error:
emit Error(result.error_);
return;
case UrlHandler::LoadResult::NoMoreTracks:
case UrlHandler::LoadResult::Type::NoMoreTracks:
return;
case UrlHandler::LoadResult::WillLoadAsynchronously:
case UrlHandler::LoadResult::Type::WillLoadAsynchronously:
loading_async_ << url;
return;
case UrlHandler::LoadResult::TrackAvailable:
case UrlHandler::LoadResult::Type::TrackAvailable:
qLog(Debug) << "URL handler for" << result.original_url_ << "returned" << result.stream_url_;
url = result.stream_url_;
Song song = next_item->Metadata();
@@ -929,7 +929,7 @@ void Player::InvalidSongRequested(const QUrl &url) {
return;
}
NextItem(Engine::Auto, Playlist::AutoScroll_Maybe);
NextItem(Engine::TrackChangeType::Auto, Playlist::AutoScroll::Maybe);
}

View File

@@ -77,7 +77,7 @@ class PlayerInterface : public QObject {
virtual void PlayAt(const int index, const quint64 offset_nanosec, Engine::TrackChangeFlags change, const Playlist::AutoScroll autoscroll, const bool reshuffle, const bool force_inform = false) = 0;
// If there's currently a song playing, pause it, otherwise play the track that was playing last, or the first one on the playlist
virtual void PlayPause(const quint64 offset_nanosec = 0, const Playlist::AutoScroll autoscroll = Playlist::AutoScroll_Always) = 0;
virtual void PlayPause(const quint64 offset_nanosec = 0, const Playlist::AutoScroll autoscroll = Playlist::AutoScroll::Always) = 0;
virtual void PlayPauseHelper() = 0;
virtual void RestartOrPrevious() = 0;
@@ -164,7 +164,7 @@ class Player : public PlayerInterface {
void SaveVolume() override;
void PlayAt(const int index, const quint64 offset_nanosec, Engine::TrackChangeFlags change, const Playlist::AutoScroll autoscroll, const bool reshuffle, const bool force_inform = false) override;
void PlayPause(const quint64 offset_nanosec = 0, const Playlist::AutoScroll autoscroll = Playlist::AutoScroll_Always) override;
void PlayPause(const quint64 offset_nanosec = 0, const Playlist::AutoScroll autoscroll = Playlist::AutoScroll::Always) override;
void PlayPauseHelper() override { PlayPause(play_offset_nanosec_); }
void RestartOrPrevious() override;
void Next() override;

View File

@@ -23,6 +23,7 @@
#include <QFileSystemWatcher>
#include <QString>
#include "core/logging.h"
#include "filesystemwatcherinterface.h"
#include "qtfslistener.h"
@@ -32,11 +33,25 @@ QtFSListener::QtFSListener(QObject *parent) : FileSystemWatcherInterface(parent)
}
void QtFSListener::AddPath(const QString &path) { watcher_.addPath(path); }
void QtFSListener::AddPath(const QString &path) {
void QtFSListener::RemovePath(const QString &path) { watcher_.removePath(path); }
if (!watcher_.addPath(path)) {
qLog(Error) << "Failed to add watch for path" << path;
}
}
void QtFSListener::RemovePath(const QString &path) {
if (!watcher_.removePath(path)) {
qLog(Error) << "Failed to remove watch for path" << path;
}
}
void QtFSListener::Clear() {
watcher_.removePaths(watcher_.directories());
watcher_.removePaths(watcher_.files());
}

File diff suppressed because it is too large Load Diff

View File

@@ -65,53 +65,53 @@ class Song {
public:
enum Source {
Source_Unknown = 0,
Source_LocalFile = 1,
Source_Collection = 2,
Source_CDDA = 3,
Source_Device = 4,
Source_Stream = 5,
Source_Tidal = 6,
Source_Subsonic = 7,
Source_Qobuz = 8,
Source_SomaFM = 9,
Source_RadioParadise = 10
enum class Source {
Unknown = 0,
LocalFile = 1,
Collection = 2,
CDDA = 3,
Device = 4,
Stream = 5,
Tidal = 6,
Subsonic = 7,
Qobuz = 8,
SomaFM = 9,
RadioParadise = 10
};
// Don't change these values - they're stored in the database, and defined in the tag reader protobuf.
// If a new lossless file is added, also add it to IsFileLossless().
enum FileType {
FileType_Unknown = 0,
FileType_WAV = 1,
FileType_FLAC = 2,
FileType_WavPack = 3,
FileType_OggFlac = 4,
FileType_OggVorbis = 5,
FileType_OggOpus = 6,
FileType_OggSpeex = 7,
FileType_MPEG = 8,
FileType_MP4 = 9,
FileType_ASF = 10,
FileType_AIFF = 11,
FileType_MPC = 12,
FileType_TrueAudio = 13,
FileType_DSF = 14,
FileType_DSDIFF = 15,
FileType_PCM = 16,
FileType_APE = 17,
FileType_MOD = 18,
FileType_S3M = 19,
FileType_XM = 20,
FileType_IT = 21,
FileType_SPC = 22,
FileType_VGM = 23,
FileType_CDDA = 90,
FileType_Stream = 91,
enum class FileType {
Unknown = 0,
WAV = 1,
FLAC = 2,
WavPack = 3,
OggFlac = 4,
OggVorbis = 5,
OggOpus = 6,
OggSpeex = 7,
MPEG = 8,
MP4 = 9,
ASF = 10,
AIFF = 11,
MPC = 12,
TrueAudio = 13,
DSF = 14,
DSDIFF = 15,
PCM = 16,
APE = 17,
MOD = 18,
S3M = 19,
XM = 20,
IT = 21,
SPC = 22,
VGM = 23,
CDDA = 90,
Stream = 91
};
Song(Song::Source source = Song::Source_Unknown);
Song(Source source = Source::Unknown);
Song(const Song &other);
~Song();
@@ -139,13 +139,13 @@ class Song {
static QString JoinSpec(const QString &table);
static Source SourceFromURL(const QUrl &url);
static QString TextForSource(Source source);
static QString DescriptionForSource(Source source);
static Song::Source SourceFromText(const QString &source);
static QIcon IconForSource(Source source);
static QString TextForFiletype(FileType filetype);
static QString ExtensionForFiletype(FileType filetype);
static QIcon IconForFiletype(FileType filetype);
static QString TextForSource(const Source source);
static QString DescriptionForSource(const Source source);
static Source SourceFromText(const QString &source);
static QIcon IconForSource(const Source source);
static QString TextForFiletype(const FileType filetype);
static QString ExtensionForFiletype(const FileType filetype);
static QIcon IconForFiletype(const FileType filetype);
QString TextForSource() const { return TextForSource(source()); }
QString DescriptionForSource() const { return DescriptionForSource(source()); }
@@ -157,7 +157,7 @@ class Song {
static FileType FiletypeByMimetype(const QString &mimetype);
static FileType FiletypeByDescription(const QString &text);
static FileType FiletypeByExtension(const QString &ext);
static QString ImageCacheDir(const Song::Source source);
static QString ImageCacheDir(const Source source);
// Sort songs alphabetically using their pretty title
static int CompareSongsName(const Song &song1, const Song &song2);
@@ -259,6 +259,20 @@ class Song {
float rating() const;
const QString &acoustid_id() const;
const QString &acoustid_fingerprint() const;
const QString &musicbrainz_album_artist_id() const;
const QString &musicbrainz_artist_id() const;
const QString &musicbrainz_original_artist_id() const;
const QString &musicbrainz_album_id() const;
const QString &musicbrainz_original_album_id() const;
const QString &musicbrainz_recording_id() const;
const QString &musicbrainz_track_id() const;
const QString &musicbrainz_disc_id() const;
const QString &musicbrainz_release_group_id() const;
const QString &musicbrainz_work_id() const;
const QString &effective_album() const;
int effective_originalyear() const;
const QString &effective_albumartist() const;
@@ -386,11 +400,32 @@ class Song {
void set_rating(const float v);
void set_acoustid_id(const QString &v);
void set_acoustid_fingerprint(const QString &v);
void set_musicbrainz_album_artist_id(const QString &v);
void set_musicbrainz_artist_id(const QString &v);
void set_musicbrainz_original_artist_id(const QString &v);
void set_musicbrainz_album_id(const QString &v);
void set_musicbrainz_original_album_id(const QString &v);
void set_musicbrainz_recording_id(const QString &v);
void set_musicbrainz_track_id(const QString &v);
void set_musicbrainz_disc_id(const QString &v);
void set_musicbrainz_release_group_id(const QString &v);
void set_musicbrainz_work_id(const QString &v);
void set_stream_url(const QUrl &v);
// Comparison functions
bool IsMetadataEqual(const Song &other) const;
bool IsMetadataAndMoreEqual(const Song &other) const;
bool IsPlayStatisticsEqual(const Song &other) const;
bool IsRatingEqual(const Song &other) const;
bool IsFingerprintEqual(const Song &other) const;
bool IsAcoustIdEqual(const Song &other) const;
bool IsMusicBrainzEqual(const Song &other) const;
bool IsArtEqual(const Song &other) const;
bool IsAllMetadataEqual(const Song &other) const;
bool IsOnSameAlbum(const Song &other) const;
bool IsSimilar(const Song &other) const;
@@ -417,6 +452,8 @@ using SongMap = QMap<QString, Song>;
Q_DECLARE_METATYPE(Song)
Q_DECLARE_METATYPE(SongList)
Q_DECLARE_METATYPE(SongMap)
Q_DECLARE_METATYPE(Song::Source)
Q_DECLARE_METATYPE(Song::FileType)
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
size_t qHash(const Song &song);

View File

@@ -68,15 +68,19 @@ const int SongLoader::kDefaultTimeout = 5000;
SongLoader::SongLoader(CollectionBackendInterface *collection, const Player *player, QObject *parent)
: QObject(parent),
player_(player),
collection_(collection),
timeout_timer_(new QTimer(this)),
playlist_parser_(new PlaylistParser(collection, this)),
cue_parser_(new CueParser(collection, this)),
timeout_(kDefaultTimeout),
state_(WaitingForType),
success_(false),
parser_(nullptr),
collection_(collection),
player_(player) {
state_(State::WaitingForType),
timeout_(kDefaultTimeout),
#ifdef HAVE_GSTREAMER
fakesink_(nullptr),
buffer_probe_cb_id_(0),
#endif
success_(false) {
if (sRawUriSchemes.isEmpty()) {
sRawUriSchemes << "udp"
@@ -99,17 +103,14 @@ SongLoader::SongLoader(CollectionBackendInterface *collection, const Player *pla
SongLoader::~SongLoader() {
#ifdef HAVE_GSTREAMER
if (pipeline_) {
state_ = Finished;
gst_element_set_state(pipeline_.get(), GST_STATE_NULL);
}
CleanupPipeline();
#endif
}
SongLoader::Result SongLoader::Load(const QUrl &url) {
if (url.isEmpty()) return Error;
if (url.isEmpty()) return Result::Error;
url_ = url;
@@ -121,24 +122,24 @@ SongLoader::Result SongLoader::Load(const QUrl &url) {
// The URI scheme indicates that it can't possibly be a playlist,
// or we have a custom handler for the URL, so add it as a raw stream.
AddAsRawStream();
return Success;
return Result::Success;
}
if (player_->engine()->type() == Engine::GStreamer) {
if (player_->engine()->type() == Engine::EngineType::GStreamer) {
#ifdef HAVE_GSTREAMER
preload_func_ = std::bind(&SongLoader::LoadRemote, this);
return BlockingLoadRequired;
return Result::BlockingLoadRequired;
#else
errors_ << tr("You need GStreamer for this URL.");
return Error;
return Result::Error;
#endif
}
else {
errors_ << tr("You need GStreamer for this URL.");
return Error;
return Result::Error;
}
return Success;
return Result::Success;
}
@@ -149,7 +150,7 @@ SongLoader::Result SongLoader::LoadFilenamesBlocking() {
}
else {
errors_ << tr("Preload function was not set for blocking operation.");
return Error;
return Result::Error;
}
}
@@ -162,44 +163,44 @@ SongLoader::Result SongLoader::LoadLocalPartial(const QString &filename) {
if (!fileinfo.exists()) {
errors_ << tr("File %1 does not exist.").arg(filename);
return Error;
return Result::Error;
}
// First check to see if it's a directory - if so we can load all the songs inside right away.
if (fileinfo.isDir()) {
LoadLocalDirectory(filename);
return Success;
return Result::Success;
}
// Assume it's just a normal file
if (TagReaderClient::Instance()->IsMediaFileBlocking(filename) || Song::kAcceptedExtensions.contains(fileinfo.suffix(), Qt::CaseInsensitive)) {
Song song(Song::Source_LocalFile);
Song song(Song::Source::LocalFile);
song.InitFromFilePartial(filename, fileinfo);
if (song.is_valid()) {
songs_ << song;
return Success;
return Result::Success;
}
}
errors_ << QObject::tr("File %1 is not recognized as a valid audio file.").arg(filename);
return Error;
return Result::Error;
}
SongLoader::Result SongLoader::LoadAudioCD() {
#if defined(HAVE_AUDIOCD) && defined(HAVE_GSTREAMER)
if (player_->engine()->type() == Engine::GStreamer) {
if (player_->engine()->type() == Engine::EngineType::GStreamer) {
CddaSongLoader *cdda_song_loader = new CddaSongLoader(QUrl(), this);
QObject::connect(cdda_song_loader, &CddaSongLoader::SongsDurationLoaded, this, &SongLoader::AudioCDTracksLoadFinishedSlot);
QObject::connect(cdda_song_loader, &CddaSongLoader::SongsMetadataLoaded, this, &SongLoader::AudioCDTracksTagsLoaded);
cdda_song_loader->LoadSongs();
return Success;
return Result::Success;
}
else {
#endif
errors_ << tr("CD playback is only available with the GStreamer engine.");
return Error;
return Result::Error;
#if defined(HAVE_AUDIOCD) && defined(HAVE_GSTREAMER)
}
#endif
@@ -243,7 +244,7 @@ SongLoader::Result SongLoader::LoadLocal(const QString &filename) {
if (query.Exec() && query.Next()) {
// We may have many results when the file has many sections
do {
Song song(Song::Source_Collection);
Song song(Song::Source::Collection);
song.InitFromQuery(query, true);
if (song.is_valid()) {
@@ -251,12 +252,12 @@ SongLoader::Result SongLoader::LoadLocal(const QString &filename) {
}
} while (query.Next());
return Success;
return Result::Success;
}
// It's not in the database, load it asynchronously.
preload_func_ = std::bind(&SongLoader::LoadLocalAsync, this, filename);
return BlockingLoadRequired;
return Result::BlockingLoadRequired;
}
@@ -266,20 +267,20 @@ SongLoader::Result SongLoader::LoadLocalAsync(const QString &filename) {
if (!fileinfo.exists()) {
errors_ << tr("File %1 does not exist.").arg(filename);
return Error;
return Result::Error;
}
// First check to see if it's a directory - if so we will load all the songs inside right away.
if (fileinfo.isDir()) {
LoadLocalDirectory(filename);
return Success;
return Result::Success;
}
// It's a local file, so check if it looks like a playlist. Read the first few bytes.
QFile file(filename);
if (!file.open(QIODevice::ReadOnly)) {
errors_ << tr("Could not open file %1 for reading: %2").arg(filename, file.errorString());
return Error;
return Result::Error;
}
QByteArray data(file.read(PlaylistParser::kMagicSize));
file.close();
@@ -287,13 +288,13 @@ SongLoader::Result SongLoader::LoadLocalAsync(const QString &filename) {
ParserBase *parser = playlist_parser_->ParserForMagic(data);
if (!parser) {
// Check the file extension as well, maybe the magic failed, or it was a basic M3U file which is just a plain list of filenames.
parser = playlist_parser_->ParserForExtension(PlaylistParser::Type_Load, fileinfo.suffix().toLower());
parser = playlist_parser_->ParserForExtension(PlaylistParser::Type::Load, fileinfo.suffix().toLower());
}
if (parser) { // It's a playlist!
qLog(Debug) << "Parsing using" << parser->name();
LoadPlaylist(parser, filename);
return Success;
return Result::Success;
}
// Check if it's a CUE file
@@ -307,26 +308,26 @@ SongLoader::Result SongLoader::LoadLocalAsync(const QString &filename) {
for (const Song &song : songs) {
if (song.is_valid()) songs_ << song;
}
return Success;
return Result::Success;
}
else {
errors_ << tr("Could not open CUE file %1 for reading: %2").arg(matching_cue, cue.errorString());
return Error;
return Result::Error;
}
}
// Assume it's just a normal file
if (TagReaderClient::Instance()->IsMediaFileBlocking(filename) || Song::kAcceptedExtensions.contains(fileinfo.suffix(), Qt::CaseInsensitive)) {
Song song(Song::Source_LocalFile);
Song song(Song::Source::LocalFile);
song.InitFromFilePartial(filename, fileinfo);
if (song.is_valid()) {
songs_ << song;
return Success;
return Result::Success;
}
}
errors_ << QObject::tr("File %1 is not recognized as a valid audio file.").arg(filename);
return Error;
return Result::Error;
}
@@ -342,7 +343,7 @@ void SongLoader::EffectiveSongLoad(Song *song) {
if (!song || !song->url().isLocalFile()) return;
if (song->init_from_file() && song->filetype() != Song::FileType_Unknown) {
if (song->init_from_file() && song->filetype() != Song::FileType::Unknown) {
// Maybe we loaded the metadata already, for example from a cuesheet.
return;
}
@@ -409,7 +410,7 @@ void SongLoader::AddAsRawStream() {
Song song(Song::SourceFromURL(url_));
song.set_valid(true);
song.set_filetype(Song::FileType_Stream);
song.set_filetype(Song::FileType::Stream);
song.set_url(url_);
song.set_title(url_.toString());
songs_ << song;
@@ -417,9 +418,11 @@ void SongLoader::AddAsRawStream() {
}
void SongLoader::Timeout() {
state_ = Finished;
state_ = State::Finished;
success_ = false;
StopTypefind();
}
void SongLoader::StopTypefind() {
@@ -428,7 +431,7 @@ void SongLoader::StopTypefind() {
// Destroy the pipeline
if (pipeline_) {
gst_element_set_state(pipeline_.get(), GST_STATE_NULL);
pipeline_.reset();
CleanupPipeline();
}
#endif
timeout_timer_->stop();
@@ -475,40 +478,59 @@ SongLoader::Result SongLoader::LoadRemote() {
GstElement *source = gst_element_make_from_uri(GST_URI_SRC, url_.toEncoded().constData(), nullptr, nullptr);
if (!source) {
errors_ << tr("Couldn't create GStreamer source element for %1").arg(url_.toString());
return Error;
return Result::Error;
}
gst_bin_add(GST_BIN(pipeline.get()), source);
g_object_set(source, "ssl-strict", FALSE, nullptr);
// Create the other elements and link them up
GstElement *typefind = gst_element_factory_make("typefind", nullptr);
GstElement *fakesink = gst_element_factory_make("fakesink", nullptr);
if (!typefind) {
errors_ << tr("Couldn't create GStreamer typefind element for %1").arg(url_.toString());
return Result::Error;
}
gst_bin_add(GST_BIN(pipeline.get()), typefind);
gst_bin_add_many(GST_BIN(pipeline.get()), source, typefind, fakesink, nullptr);
gst_element_link_many(source, typefind, fakesink, nullptr);
fakesink_ = gst_element_factory_make("fakesink", nullptr);
if (!fakesink_) {
errors_ << tr("Couldn't create GStreamer fakesink element for %1").arg(url_.toString());
return Result::Error;
}
gst_bin_add(GST_BIN(pipeline.get()), fakesink_);
if (!gst_element_link_many(source, typefind, fakesink_, nullptr)) {
errors_ << tr("Couldn't link GStreamer source, typefind and fakesink elements for %1").arg(url_.toString());
return Result::Error;
}
// Connect callbacks
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline.get()));
CHECKED_GCONNECT(typefind, "have-type", &TypeFound, this);
gst_bus_set_sync_handler(bus, BusCallbackSync, this, nullptr);
gst_bus_add_watch(bus, BusCallback, this);
gst_object_unref(bus);
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline.get()));
if (bus) {
gst_bus_set_sync_handler(bus, BusCallbackSync, this, nullptr);
gst_bus_add_watch(bus, BusWatchCallback, this);
gst_object_unref(bus);
}
// Add a probe to the sink so we can capture the data if it's a playlist
GstPad *pad = gst_element_get_static_pad(fakesink, "sink");
gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, &DataReady, this, nullptr);
gst_object_unref(pad);
GstPad *pad = gst_element_get_static_pad(fakesink_, "sink");
if (pad) {
buffer_probe_cb_id_ = gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, &DataReady, this, nullptr);
gst_object_unref(pad);
}
QEventLoop loop;
loop.connect(this, &SongLoader::LoadRemoteFinished, &loop, &QEventLoop::quit);
// Start "playing"
gst_element_set_state(pipeline.get(), GST_STATE_PLAYING);
pipeline_ = pipeline;
gst_element_set_state(pipeline.get(), GST_STATE_PLAYING);
// Wait until loading is finished
loop.exec();
return Success;
return Result::Success;
}
#endif
@@ -518,14 +540,14 @@ void SongLoader::TypeFound(GstElement*, uint, GstCaps *caps, void *self) {
SongLoader *instance = static_cast<SongLoader*>(self);
if (instance->state_ != WaitingForType) return;
if (instance->state_ != State::WaitingForType) return;
// Check the mimetype
instance->mime_type_ = gst_structure_get_name(gst_caps_get_structure(caps, 0));
qLog(Debug) << "Mime type is" << instance->mime_type_;
if (instance->mime_type_ == "text/plain" || instance->mime_type_ == "text/uri-list") {
// Yeah it might be a playlist, let's get some data and have a better look
instance->state_ = WaitingForMagic;
instance->state_ = State::WaitingForMagic;
return;
}
@@ -540,7 +562,7 @@ GstPadProbeReturn SongLoader::DataReady(GstPad*, GstPadProbeInfo *info, gpointer
SongLoader *instance = reinterpret_cast<SongLoader*>(self);
if (instance->state_ == Finished) {
if (instance->state_ == State::Finished) {
return GST_PAD_PROBE_OK;
}
@@ -553,7 +575,7 @@ GstPadProbeReturn SongLoader::DataReady(GstPad*, GstPadProbeInfo *info, gpointer
qLog(Debug) << "Received total" << instance->buffer_.size() << "bytes";
gst_buffer_unmap(buffer, &map);
if (instance->state_ == WaitingForMagic && (instance->buffer_.size() >= PlaylistParser::kMagicSize || !instance->IsPipelinePlaying())) {
if (instance->state_ == State::WaitingForMagic && (instance->buffer_.size() >= PlaylistParser::kMagicSize || !instance->IsPipelinePlaying())) {
// Got enough that we can test the magic
instance->MagicReady();
}
@@ -563,7 +585,7 @@ GstPadProbeReturn SongLoader::DataReady(GstPad*, GstPadProbeInfo *info, gpointer
#endif
#ifdef HAVE_GSTREAMER
gboolean SongLoader::BusCallback(GstBus*, GstMessage *msg, gpointer self) {
gboolean SongLoader::BusWatchCallback(GstBus*, GstMessage *msg, gpointer self) {
SongLoader *instance = reinterpret_cast<SongLoader*>(self);
@@ -604,7 +626,7 @@ GstBusSyncReply SongLoader::BusCallbackSync(GstBus*, GstMessage *msg, gpointer s
#ifdef HAVE_GSTREAMER
void SongLoader::ErrorMessageReceived(GstMessage *msg) {
if (state_ == Finished) return;
if (state_ == State::Finished) return;
GError *error = nullptr;
gchar *debugs = nullptr;
@@ -618,9 +640,9 @@ void SongLoader::ErrorMessageReceived(GstMessage *msg) {
g_error_free(error);
g_free(debugs);
if (state_ == WaitingForType && message_str == gst_error_get_message(GST_STREAM_ERROR, GST_STREAM_ERROR_TYPE_NOT_FOUND)) {
if (state_ == State::WaitingForType && message_str == gst_error_get_message(GST_STREAM_ERROR, GST_STREAM_ERROR_TYPE_NOT_FOUND)) {
// Don't give up - assume it's a playlist and see if one of our parsers can read it.
state_ = WaitingForMagic;
state_ = State::WaitingForMagic;
return;
}
@@ -632,24 +654,25 @@ void SongLoader::ErrorMessageReceived(GstMessage *msg) {
#ifdef HAVE_GSTREAMER
void SongLoader::EndOfStreamReached() {
qLog(Debug) << Q_FUNC_INFO << state_;
qLog(Debug) << Q_FUNC_INFO << static_cast<int>(state_);
switch (state_) {
case Finished:
case State::Finished:
break;
case WaitingForMagic:
case State::WaitingForMagic:
// Do the magic on the data we have already
MagicReady();
if (state_ == Finished) break;
if (state_ == State::Finished) break;
// It looks like a playlist, so parse it
[[fallthrough]];
case WaitingForData:
case State::WaitingForData:
// It's a playlist and we've got all the data - finish and parse it
StopTypefindAsync(true);
break;
case WaitingForType:
case State::WaitingForType:
StopTypefindAsync(false);
break;
}
@@ -661,6 +684,7 @@ void SongLoader::EndOfStreamReached() {
void SongLoader::MagicReady() {
qLog(Debug) << Q_FUNC_INFO;
parser_ = playlist_parser_->ParserForMagic(buffer_, mime_type_);
if (!parser_) {
@@ -673,6 +697,7 @@ void SongLoader::MagicReady() {
// We'll get more data and parse the whole thing in EndOfStreamReached
qLog(Debug) << "Magic says" << parser_->name();
if (parser_->name() == "ASX/INI" && url_.scheme() == "http") {
// This is actually a weird MS-WMSP stream. Changing the protocol to MMS from HTTP makes it playable.
parser_ = nullptr;
@@ -680,7 +705,7 @@ void SongLoader::MagicReady() {
StopTypefindAsync(true);
}
state_ = WaitingForData;
state_ = State::WaitingForData;
if (!IsPipelinePlaying()) {
EndOfStreamReached();
@@ -706,9 +731,9 @@ bool SongLoader::IsPipelinePlaying() {
#endif
#ifdef HAVE_GSTREAMER
void SongLoader::StopTypefindAsync(bool success) {
void SongLoader::StopTypefindAsync(const bool success) {
state_ = Finished;
state_ = State::Finished;
success_ = success;
QMetaObject::invokeMethod(this, "StopTypefind", Qt::QueuedConnection);
@@ -716,7 +741,6 @@ void SongLoader::StopTypefindAsync(bool success) {
}
#endif
void SongLoader::ScheduleTimeoutAsync() {
if (QThread::currentThread() == thread()) {
@@ -733,3 +757,38 @@ void SongLoader::ScheduleTimeout() {
timeout_timer_->start(timeout_);
}
#ifdef HAVE_GSTREAMER
void SongLoader::CleanupPipeline() {
if (pipeline_) {
gst_element_set_state(pipeline_.get(), GST_STATE_NULL);
if (fakesink_ && buffer_probe_cb_id_ != 0) {
GstPad *pad = gst_element_get_static_pad(fakesink_, "src");
if (pad) {
gst_pad_remove_probe(pad, buffer_probe_cb_id_);
gst_object_unref(pad);
}
}
{
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_.get()));
if (bus) {
gst_bus_remove_watch(bus);
gst_bus_set_sync_handler(bus, nullptr, nullptr, nullptr);
gst_object_unref(bus);
}
}
pipeline_.reset();
}
state_ = State::Finished;
}
#endif

View File

@@ -56,14 +56,15 @@ class CddaSongLoader;
class SongLoader : public QObject {
Q_OBJECT
public:
explicit SongLoader(CollectionBackendInterface *collection, const Player *player, QObject *parent = nullptr);
~SongLoader() override;
enum Result {
enum class Result {
Success,
Error,
BlockingLoadRequired,
BlockingLoadRequired
};
static const int kDefaultTimeout;
@@ -101,7 +102,12 @@ class SongLoader : public QObject {
#endif // HAVE_AUDIOCD && HAVE_GSTREAMER
private:
enum State { WaitingForType, WaitingForMagic, WaitingForData, Finished };
enum class State {
WaitingForType,
WaitingForMagic,
WaitingForData,
Finished
};
Result LoadLocal(const QString &filename);
SongLoader::Result LoadLocalAsync(const QString &filename);
@@ -119,13 +125,14 @@ class SongLoader : public QObject {
static void TypeFound(GstElement *typefind, uint probability, GstCaps *caps, void *self);
static GstPadProbeReturn DataReady(GstPad*, GstPadProbeInfo *info, gpointer self);
static GstBusSyncReply BusCallbackSync(GstBus*, GstMessage*, gpointer);
static gboolean BusCallback(GstBus*, GstMessage*, gpointer);
static gboolean BusWatchCallback(GstBus*, GstMessage*, gpointer);
void StopTypefindAsync(bool success);
void ErrorMessageReceived(GstMessage *msg);
void EndOfStreamReached();
void MagicReady();
bool IsPipelinePlaying();
void StopTypefindAsync(const bool success);
void CleanupPipeline();
#endif
void ScheduleTimeoutAsync();
@@ -136,29 +143,31 @@ class SongLoader : public QObject {
QUrl url_;
SongList songs_;
const Player *player_;
CollectionBackendInterface *collection_;
QTimer *timeout_timer_;
PlaylistParser *playlist_parser_;
CueParser *cue_parser_;
// For async loads
std::function<Result()> preload_func_;
int timeout_;
State state_;
bool success_;
ParserBase *parser_;
QString mime_type_;
QByteArray buffer_;
CollectionBackendInterface *collection_;
const Player *player_;
ParserBase *parser_;
State state_;
int timeout_;
#ifdef HAVE_GSTREAMER
std::shared_ptr<GstElement> pipeline_;
GstElement *fakesink_;
gulong buffer_probe_cb_id_;
#endif
QThreadPool thread_pool_;
QStringList errors_;
bool success_;
};
#endif // SONGLOADER_H

View File

@@ -38,8 +38,6 @@
#include "tagreaderclient.h"
#include "settings/collectionsettingspage.h"
#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length()
const char *TagReaderClient::kWorkerExecutableName = "strawberry-tagreader";
TagReaderClient *TagReaderClient::sInstance = nullptr;
@@ -48,15 +46,7 @@ TagReaderClient::TagReaderClient(QObject *parent) : QObject(parent), worker_pool
sInstance = this;
original_thread_ = thread();
QSettings s;
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
int workers = s.value("tagreader_workers", qBound(1, QThread::idealThreadCount() / 2, 4)).toInt();
s.endGroup();
qLog(Debug) << "Using" << workers << "tagreader workers.";
worker_pool_->SetExecutableName(kWorkerExecutableName);
worker_pool_->SetWorkerCount(workers);
QObject::connect(worker_pool_, &WorkerPool<HandlerType>::WorkerFailedToStart, this, &TagReaderClient::WorkerFailedToStart);
}
@@ -82,9 +72,10 @@ void TagReaderClient::WorkerFailedToStart() {
TagReaderReply *TagReaderClient::IsMediaFile(const QString &filename) {
spb::tagreader::Message message;
spb::tagreader::IsMediaFileRequest *req = message.mutable_is_media_file_request();
spb::tagreader::IsMediaFileRequest *request = message.mutable_is_media_file_request();
req->set_filename(DataCommaSizeFromQString(filename));
const QByteArray filename_data = filename.toUtf8();
request->set_filename(filename_data.constData(), filename_data.length());
return worker_pool_->SendMessageWithReply(&message);
@@ -93,21 +84,35 @@ TagReaderReply *TagReaderClient::IsMediaFile(const QString &filename) {
TagReaderReply *TagReaderClient::ReadFile(const QString &filename) {
spb::tagreader::Message message;
spb::tagreader::ReadFileRequest *req = message.mutable_read_file_request();
spb::tagreader::ReadFileRequest *request = message.mutable_read_file_request();
req->set_filename(DataCommaSizeFromQString(filename));
const QByteArray filename_data = filename.toUtf8();
request->set_filename(filename_data.constData(), filename_data.length());
return worker_pool_->SendMessageWithReply(&message);
}
TagReaderReply *TagReaderClient::SaveFile(const QString &filename, const Song &metadata) {
TagReaderReply *TagReaderClient::SaveFile(const QString &filename, const Song &metadata, const SaveTags save_tags, const SavePlaycount save_playcount, const SaveRating save_rating, const SaveCoverOptions &save_cover_options) {
spb::tagreader::Message message;
spb::tagreader::SaveFileRequest *req = message.mutable_save_file_request();
spb::tagreader::SaveFileRequest *request = message.mutable_save_file_request();
req->set_filename(DataCommaSizeFromQString(filename));
metadata.ToProtobuf(req->mutable_metadata());
const QByteArray filename_data = filename.toUtf8();
request->set_filename(filename_data.constData(), filename_data.length());
request->set_save_tags(save_tags == SaveTags::On);
request->set_save_playcount(save_playcount == SavePlaycount::On);
request->set_save_rating(save_rating == SaveRating::On);
request->set_save_cover(save_cover_options.enabled);
request->set_cover_is_jpeg(save_cover_options.is_jpeg);
if (save_cover_options.cover_filename.length() > 0) {
const QByteArray cover_filename = filename.toUtf8();
request->set_cover_filename(cover_filename.constData(), cover_filename.length());
}
if (save_cover_options.cover_data.length() > 0) {
request->set_cover_data(save_cover_options.cover_data.constData(), save_cover_options.cover_data.length());
}
metadata.ToProtobuf(request->mutable_metadata());
ReplyType *reply = worker_pool_->SendMessageWithReply(&message);
@@ -118,21 +123,31 @@ TagReaderReply *TagReaderClient::SaveFile(const QString &filename, const Song &m
TagReaderReply *TagReaderClient::LoadEmbeddedArt(const QString &filename) {
spb::tagreader::Message message;
spb::tagreader::LoadEmbeddedArtRequest *req = message.mutable_load_embedded_art_request();
spb::tagreader::LoadEmbeddedArtRequest *request = message.mutable_load_embedded_art_request();
req->set_filename(DataCommaSizeFromQString(filename));
const QByteArray filename_data = filename.toUtf8();
request->set_filename(filename_data.constData(), filename_data.length());
return worker_pool_->SendMessageWithReply(&message);
}
TagReaderReply *TagReaderClient::SaveEmbeddedArt(const QString &filename, const QByteArray &data) {
TagReaderReply *TagReaderClient::SaveEmbeddedArt(const QString &filename, const SaveCoverOptions &save_cover_options) {
spb::tagreader::Message message;
spb::tagreader::SaveEmbeddedArtRequest *req = message.mutable_save_embedded_art_request();
spb::tagreader::SaveEmbeddedArtRequest *request = message.mutable_save_embedded_art_request();
const QByteArray filename_data = filename.toUtf8();
request->set_filename(filename_data.constData(), filename_data.length());
request->set_cover_is_jpeg(save_cover_options.is_jpeg);
if (save_cover_options.cover_filename.length() > 0) {
const QByteArray cover_filename = filename.toUtf8();
request->set_cover_filename(cover_filename.constData(), cover_filename.length());
}
if (save_cover_options.cover_data.length() > 0) {
request->set_cover_data(save_cover_options.cover_data.constData(), save_cover_options.cover_data.length());
}
req->set_filename(DataCommaSizeFromQString(filename));
req->set_data(data.constData(), data.size());
return worker_pool_->SendMessageWithReply(&message);
@@ -141,10 +156,11 @@ TagReaderReply *TagReaderClient::SaveEmbeddedArt(const QString &filename, const
TagReaderReply *TagReaderClient::UpdateSongPlaycount(const Song &metadata) {
spb::tagreader::Message message;
spb::tagreader::SaveSongPlaycountToFileRequest *req = message.mutable_save_song_playcount_to_file_request();
spb::tagreader::SaveSongPlaycountToFileRequest *request = message.mutable_save_song_playcount_to_file_request();
req->set_filename(DataCommaSizeFromQString(metadata.url().toLocalFile()));
metadata.ToProtobuf(req->mutable_metadata());
const QByteArray filename_data = metadata.url().toLocalFile().toUtf8();
request->set_filename(filename_data.constData(), filename_data.length());
metadata.ToProtobuf(request->mutable_metadata());
return worker_pool_->SendMessageWithReply(&message);
@@ -162,10 +178,11 @@ void TagReaderClient::UpdateSongsPlaycount(const SongList &songs) {
TagReaderReply *TagReaderClient::UpdateSongRating(const Song &metadata) {
spb::tagreader::Message message;
spb::tagreader::SaveSongRatingToFileRequest *req = message.mutable_save_song_rating_to_file_request();
spb::tagreader::SaveSongRatingToFileRequest *request = message.mutable_save_song_rating_to_file_request();
req->set_filename(DataCommaSizeFromQString(metadata.url().toLocalFile()));
metadata.ToProtobuf(req->mutable_metadata());
const QByteArray filename_data = metadata.url().toLocalFile().toUtf8();
request->set_filename(filename_data.constData(), filename_data.length());
metadata.ToProtobuf(request->mutable_metadata());
return worker_pool_->SendMessageWithReply(&message);
@@ -190,7 +207,7 @@ bool TagReaderClient::IsMediaFileBlocking(const QString &filename) {
if (reply->WaitForFinished()) {
ret = reply->message().is_media_file_response().success();
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
reply->deleteLater();
return ret;
@@ -204,21 +221,21 @@ void TagReaderClient::ReadFileBlocking(const QString &filename, Song *song) {
if (reply->WaitForFinished()) {
song->InitFromProtobuf(reply->message().read_file_response().metadata());
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
reply->deleteLater();
}
bool TagReaderClient::SaveFileBlocking(const QString &filename, const Song &metadata) {
bool TagReaderClient::SaveFileBlocking(const QString &filename, const Song &metadata, const SaveTags save_tags, const SavePlaycount save_playcount, const SaveRating save_rating, const SaveCoverOptions &save_cover_options) {
Q_ASSERT(QThread::currentThread() != thread());
bool ret = false;
TagReaderReply *reply = SaveFile(filename, metadata);
TagReaderReply *reply = SaveFile(filename, metadata, save_tags, save_playcount, save_rating, save_cover_options);
if (reply->WaitForFinished()) {
ret = reply->message().save_file_response().success();
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
reply->deleteLater();
return ret;
@@ -235,7 +252,7 @@ QByteArray TagReaderClient::LoadEmbeddedArtBlocking(const QString &filename) {
const std::string &data_str = reply->message().load_embedded_art_response().data();
ret = QByteArray(data_str.data(), static_cast<qint64>(data_str.size()));
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
reply->deleteLater();
return ret;
@@ -252,23 +269,23 @@ QImage TagReaderClient::LoadEmbeddedArtAsImageBlocking(const QString &filename)
const std::string &data_str = reply->message().load_embedded_art_response().data();
ret.loadFromData(QByteArray(data_str.data(), static_cast<qint64>(data_str.size())));
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
reply->deleteLater();
return ret;
}
bool TagReaderClient::SaveEmbeddedArtBlocking(const QString &filename, const QByteArray &data) {
bool TagReaderClient::SaveEmbeddedArtBlocking(const QString &filename, const SaveCoverOptions &save_cover_options) {
Q_ASSERT(QThread::currentThread() != thread());
bool success = false;
TagReaderReply *reply = SaveEmbeddedArt(filename, data);
TagReaderReply *reply = SaveEmbeddedArt(filename, save_cover_options);
if (reply->WaitForFinished()) {
success = reply->message().save_embedded_art_response().success();
}
QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
reply->deleteLater();
return success;

View File

@@ -53,22 +53,48 @@ class TagReaderClient : public QObject {
void Start();
void ExitAsync();
ReplyType *ReadFile(const QString &filename);
ReplyType *SaveFile(const QString &filename, const Song &metadata);
enum class SaveTags {
Off,
On
};
enum class SavePlaycount {
Off,
On
};
enum class SaveRating {
Off,
On
};
class SaveCoverOptions {
public:
explicit SaveCoverOptions(const bool _enabled = false, const bool _is_jpeg = false, const QString &_cover_filename = QString(), const QByteArray &_cover_data = QByteArray()) : enabled(_enabled), is_jpeg(_is_jpeg), cover_filename(_cover_filename), cover_data(_cover_data) {}
explicit SaveCoverOptions(const QString &_cover_filename) : enabled(true), is_jpeg(false), cover_filename(_cover_filename) {}
explicit SaveCoverOptions(const QByteArray &_cover_data) : enabled(true), is_jpeg(false), cover_data(_cover_data) {}
bool enabled;
bool is_jpeg;
QString cover_filename;
QByteArray cover_data;
};
ReplyType *IsMediaFile(const QString &filename);
ReplyType *ReadFile(const QString &filename);
ReplyType *SaveFile(const QString &filename, const Song &metadata, const SaveTags save_tags = SaveTags::On, const SavePlaycount save_playcount = SavePlaycount::Off, const SaveRating save_rating = SaveRating::Off, const SaveCoverOptions &save_cover_options = SaveCoverOptions());
ReplyType *LoadEmbeddedArt(const QString &filename);
ReplyType *SaveEmbeddedArt(const QString &filename, const QByteArray &data);
ReplyType *SaveEmbeddedArt(const QString &filename, const SaveCoverOptions &save_cover_options);
ReplyType *UpdateSongPlaycount(const Song &metadata);
ReplyType *UpdateSongRating(const Song &metadata);
// Convenience functions that call the above functions and wait for a response.
// These block the calling thread with a semaphore, and must NOT be called from the TagReaderClient's thread.
void ReadFileBlocking(const QString &filename, Song *song);
bool SaveFileBlocking(const QString &filename, const Song &metadata);
bool SaveFileBlocking(const QString &filename, const Song &metadata, const SaveTags save_tags = SaveTags::On, const SavePlaycount save_playcount = SavePlaycount::Off, const SaveRating save_rating = SaveRating::Off, const SaveCoverOptions &save_cover_options = SaveCoverOptions());
bool IsMediaFileBlocking(const QString &filename);
QByteArray LoadEmbeddedArtBlocking(const QString &filename);
QImage LoadEmbeddedArtAsImageBlocking(const QString &filename);
bool SaveEmbeddedArtBlocking(const QString &filename, const QByteArray &data);
bool SaveEmbeddedArtBlocking(const QString &filename, const SaveCoverOptions &save_cover_options);
bool UpdateSongPlaycountBlocking(const Song &metadata);
bool UpdateSongRatingBlocking(const Song &metadata);

View File

@@ -25,7 +25,7 @@
void Thread::run() {
#ifndef Q_OS_WIN32
if (io_priority_ != Utilities::IOPRIO_CLASS_NONE) {
if (io_priority_ != Utilities::IoPriority::IOPRIO_CLASS_NONE) {
Utilities::SetThreadIOPriority(io_priority_);
}
#endif

View File

@@ -31,7 +31,7 @@ class Thread : public QThread {
Q_OBJECT
public:
explicit Thread(QObject *parent = nullptr) : QThread(parent), io_priority_(Utilities::IOPRIO_CLASS_NONE) {}
explicit Thread(QObject *parent = nullptr) : QThread(parent), io_priority_(Utilities::IoPriority::IOPRIO_CLASS_NONE) {}
void SetIoPriority(Utilities::IoPriority priority) {
io_priority_ = priority;

View File

@@ -44,7 +44,7 @@ UrlHandler::LoadResult::LoadResult(const QUrl &original_url, const Type type, co
UrlHandler::LoadResult::LoadResult(const QUrl &original_url, const Type type, const QString &error) :
original_url_(original_url),
type_(type),
filetype_(Song::FileType_Stream),
filetype_(Song::FileType::Stream),
samplerate_(-1),
bit_depth_(-1),
length_nanosec_(-1),

View File

@@ -42,7 +42,7 @@ class UrlHandler : public QObject {
// Returned by StartLoading() and LoadNext(), indicates what the player should do when it wants to load a URL.
struct LoadResult {
enum Type {
enum class Type {
// There wasn't a track available, and the player should move on to the next playlist item.
NoMoreTracks,
@@ -57,7 +57,7 @@ class UrlHandler : public QObject {
Error,
};
explicit LoadResult(const QUrl &original_url = QUrl(), const Type type = NoMoreTracks, const QUrl &stream_url = QUrl(), const Song::FileType filetype = Song::FileType_Stream, const int samplerate = -1, const int bit_depth = -1, const qint64 length_nanosec = -1, const QString &error = QString());
explicit LoadResult(const QUrl &original_url = QUrl(), const Type type = Type::NoMoreTracks, const QUrl &stream_url = QUrl(), const Song::FileType filetype = Song::FileType::Stream, const int samplerate = -1, const int bit_depth = -1, const qint64 length_nanosec = -1, const QString &error = QString());
explicit LoadResult(const QUrl &original_url, const Type type, const QString &error);

View File

@@ -53,9 +53,12 @@
#include <QSettings>
#include <QtEvents>
#include "utilities/filenameconstants.h"
#include "utilities/strutils.h"
#include "utilities/mimeutils.h"
#include "utilities/imageutils.h"
#include "utilities/coveroptions.h"
#include "utilities/coverutils.h"
#include "core/application.h"
#include "core/song.h"
#include "core/iconloader.h"
@@ -63,7 +66,6 @@
#include "collection/collectionfilteroptions.h"
#include "collection/collectionbackend.h"
#include "settings/collectionsettingspage.h"
#include "organize/organizeformat.h"
#include "internet/internetservices.h"
#include "internet/internetservice.h"
#include "albumcoverchoicecontroller.h"
@@ -98,11 +100,6 @@ AlbumCoverChoiceController::AlbumCoverChoiceController(QWidget *parent)
separator2_(nullptr),
show_cover_(nullptr),
search_cover_auto_(nullptr),
save_cover_type_(CollectionSettingsPage::SaveCoverType_Cache),
save_cover_filename_(CollectionSettingsPage::SaveCoverFilename_Pattern),
cover_overwrite_(false),
cover_lowercase_(true),
cover_replace_spaces_(true),
save_embedded_cover_override_(false) {
cover_from_file_ = new QAction(IconLoader::Load("document-open"), tr("Load cover from disk..."), this);
@@ -146,12 +143,12 @@ void AlbumCoverChoiceController::ReloadSettings() {
QSettings s;
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
save_cover_type_ = CollectionSettingsPage::SaveCoverType(s.value("save_cover_type", CollectionSettingsPage::SaveCoverType_Cache).toInt());
save_cover_filename_ = CollectionSettingsPage::SaveCoverFilename(s.value("save_cover_filename", CollectionSettingsPage::SaveCoverFilename_Pattern).toInt());
cover_pattern_ = s.value("cover_pattern", "%albumartist-%album").toString();
cover_overwrite_ = s.value("cover_overwrite", false).toBool();
cover_lowercase_ = s.value("cover_lowercase", false).toBool();
cover_replace_spaces_ = s.value("cover_replace_spaces", false).toBool();
cover_options_.cover_type = static_cast<CoverOptions::CoverType>(s.value("save_cover_type", static_cast<int>(CoverOptions::CoverType::Cache)).toInt());
cover_options_.cover_filename = static_cast<CoverOptions::CoverFilename>(s.value("save_cover_filename", static_cast<int>(CoverOptions::CoverFilename::Pattern)).toInt());
cover_options_.cover_pattern = s.value("cover_pattern", "%albumartist-%album").toString();
cover_options_.cover_overwrite = s.value("cover_overwrite", false).toBool();
cover_options_.cover_lowercase = s.value("cover_lowercase", false).toBool();
cover_options_.cover_replace_spaces = s.value("cover_replace_spaces", false).toBool();
s.endGroup();
}
@@ -214,14 +211,14 @@ QUrl AlbumCoverChoiceController::LoadCoverFromFile(Song *song) {
if (QImage(cover_file).isNull()) return QUrl();
switch (get_save_album_cover_type()) {
case CollectionSettingsPage::SaveCoverType_Embedded:
case CoverOptions::CoverType::Embedded:
if (song->save_embedded_cover_supported()) {
SaveCoverEmbeddedAutomatic(*song, cover_file);
return QUrl::fromLocalFile(Song::kEmbeddedCover);
}
[[fallthrough]];
case CollectionSettingsPage::SaveCoverType_Cache:
case CollectionSettingsPage::SaveCoverType_Album:{
case CoverOptions::CoverType::Cache:
case CoverOptions::CoverType::Album:{
QUrl cover_url = QUrl::fromLocalFile(cover_file);
SaveArtManualToSong(song, cover_url);
return cover_url;
@@ -242,7 +239,7 @@ void AlbumCoverChoiceController::SaveCoverToFileManual(const Song &song, const A
initial_file_name = initial_file_name + "-" + (song.effective_album().isEmpty() ? tr("unknown") : song.effective_album()) + ".jpg";
initial_file_name = initial_file_name.toLower();
initial_file_name.replace(QRegularExpression("\\s"), "-");
initial_file_name.remove(OrganizeFormat::kInvalidFatCharacters);
initial_file_name.remove(QRegularExpression(QString(kInvalidFatCharactersRegex), QRegularExpression::CaseInsensitiveOption));
QString save_filename = QFileDialog::getSaveFileName(this, tr("Save album cover"), GetInitialPathForFileDialog(song, initial_file_name), tr(kSaveImageFileFilter) + ";;" + tr(kAllFilesFilter));
@@ -445,12 +442,18 @@ void AlbumCoverChoiceController::ShowCover(const Song &song, const QImage &image
song.has_embedded_cover()
) {
QPixmap pixmap = ImageUtils::TryLoadPixmap(song.art_automatic(), song.art_manual(), song.url());
if (!pixmap.isNull()) ShowCover(song, pixmap);
if (!pixmap.isNull()) {
pixmap.setDevicePixelRatio(devicePixelRatioF());
ShowCover(song, pixmap);
}
}
}
else {
QPixmap pixmap = QPixmap::fromImage(image);
if (!pixmap.isNull()) ShowCover(song, pixmap);
if (!pixmap.isNull()) {
pixmap.setDevicePixelRatio(devicePixelRatioF());
ShowCover(song, pixmap);
}
}
}
@@ -484,21 +487,21 @@ void AlbumCoverChoiceController::ShowCover(const Song &song, const QPixmap &pixm
if (desktop_width < desktop_height) {
const int new_width = static_cast<int>(static_cast<double>(desktop_width) * 0.95);
if (new_width < pixmap.width()) {
label->setPixmap(pixmap.scaledToWidth(new_width, Qt::SmoothTransformation));
label->setPixmap(pixmap.scaledToWidth(new_width * pixmap.devicePixelRatioF(), Qt::SmoothTransformation));
}
}
else {
const int new_height = static_cast<int>(static_cast<double>(desktop_height) * 0.85);
if (new_height < pixmap.height()) {
label->setPixmap(pixmap.scaledToHeight(new_height, Qt::SmoothTransformation));
label->setPixmap(pixmap.scaledToHeight(new_height * pixmap.devicePixelRatioF(), Qt::SmoothTransformation));
}
}
dialog->setWindowTitle(title_text);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
dialog->setFixedSize(label->pixmap(Qt::ReturnByValue).size());
dialog->setFixedSize(label->pixmap(Qt::ReturnByValue).size() / pixmap.devicePixelRatioF());
#else
dialog->setFixedSize(label->pixmap()->size());
dialog->setFixedSize(label->pixmap()->size() / pixmap.devicePixelRatioF());
#endif
dialog->show();
@@ -540,7 +543,7 @@ void AlbumCoverChoiceController::SaveArtAutomaticToSong(Song *song, const QUrl &
song->clear_art_manual();
}
if (song->source() == Song::Source_Collection) {
if (song->source() == Song::Source::Collection) {
app_->collection_backend()->UpdateAutomaticAlbumArtAsync(song->effective_albumartist(), song->album(), art_automatic, song->has_embedded_cover());
}
@@ -559,20 +562,20 @@ void AlbumCoverChoiceController::SaveArtManualToSong(Song *song, const QUrl &art
// Update the backends.
switch (song->source()) {
case Song::Source_Collection:
case Song::Source::Collection:
app_->collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual, clear_art_automatic);
break;
case Song::Source_LocalFile:
case Song::Source_CDDA:
case Song::Source_Device:
case Song::Source_Stream:
case Song::Source_RadioParadise:
case Song::Source_SomaFM:
case Song::Source_Unknown:
case Song::Source::LocalFile:
case Song::Source::CDDA:
case Song::Source::Device:
case Song::Source::Stream:
case Song::Source::RadioParadise:
case Song::Source::SomaFM:
case Song::Source::Unknown:
break;
case Song::Source_Tidal:
case Song::Source_Qobuz:
case Song::Source_Subsonic:
case Song::Source::Tidal:
case Song::Source::Qobuz:
case Song::Source::Subsonic:
InternetService *service = app_->internet_services()->ServiceBySource(song->source());
if (!service) break;
if (service->artists_collection_backend()) {
@@ -613,12 +616,12 @@ QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song::Source sou
const AlbumCoverImageResult &result,
const bool force_overwrite) {
QString filepath = app_->album_cover_loader()->CoverFilePath(source, artist, album, album_id, album_dir, result.cover_url, "jpg");
QString filepath = CoverUtils::CoverFilePath(cover_options_, source, artist, album, album_id, album_dir, result.cover_url, "jpg");
if (filepath.isEmpty()) return QUrl();
QFile file(filepath);
// Don't overwrite when saving in album dir if the filename is set to pattern unless "force_overwrite" is set.
if (source == Song::Source_Collection && !cover_overwrite_ && !force_overwrite && get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType_Album && save_cover_filename_ == CollectionSettingsPage::SaveCoverFilename_Pattern && file.exists()) {
if (source == Song::Source::Collection && !cover_options_.cover_overwrite && !force_overwrite && get_save_album_cover_type() == CoverOptions::CoverType::Album && cover_options_.cover_filename == CoverOptions::CoverFilename::Pattern && file.exists()) {
while (file.exists()) {
QFileInfo fileinfo(file.fileName());
file.setFileName(fileinfo.path() + "/0" + fileinfo.fileName());
@@ -653,7 +656,7 @@ QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song::Source sou
void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const Song &song, const AlbumCoverImageResult &result) {
if (song.source() == Song::Source_Collection) {
if (song.source() == Song::Source::Collection) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QFuture<SongList> future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), song.effective_albumartist(), song.effective_album(), CollectionFilterOptions());
#else
@@ -698,7 +701,7 @@ void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const Song &song, co
void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const Song &song, const QString &cover_filename) {
if (song.source() == Song::Source_Collection) {
if (song.source() == Song::Source::Collection) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QFuture<SongList> future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), song.effective_albumartist(), song.effective_album(), CollectionFilterOptions());
#else
@@ -758,7 +761,7 @@ QUrl AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) {
const QString suffix = QFileInfo(filename).suffix().toLower();
if (IsKnownImageExtension(suffix)) {
if (get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType_Embedded && song->save_embedded_cover_supported()) {
if (get_save_album_cover_type() == CoverOptions::CoverType::Embedded && song->save_embedded_cover_supported()) {
SaveCoverEmbeddedAutomatic(*song, filename);
return QUrl::fromLocalFile(Song::kEmbeddedCover);
}
@@ -784,7 +787,7 @@ QUrl AlbumCoverChoiceController::SaveCoverAutomatic(Song *song, const AlbumCover
QUrl cover_url;
switch(get_save_album_cover_type()) {
case CollectionSettingsPage::SaveCoverType_Embedded:{
case CoverOptions::CoverType::Embedded:{
if (song->save_embedded_cover_supported()) {
SaveCoverEmbeddedAutomatic(*song, result);
cover_url = QUrl::fromLocalFile(Song::kEmbeddedCover);
@@ -792,8 +795,8 @@ QUrl AlbumCoverChoiceController::SaveCoverAutomatic(Song *song, const AlbumCover
}
}
[[fallthrough]];
case CollectionSettingsPage::SaveCoverType_Cache:
case CollectionSettingsPage::SaveCoverType_Album:{
case CoverOptions::CoverType::Cache:
case CoverOptions::CoverType::Album:{
cover_url = SaveCoverToFileAutomatic(song, result);
if (!cover_url.isEmpty()) SaveArtManualToSong(song, cover_url);
break;

View File

@@ -38,6 +38,7 @@
#include <QMutex>
#include "core/song.h"
#include "utilities/coveroptions.h"
#include "settings/collectionsettingspage.h"
#include "albumcoverimageresult.h"
@@ -69,8 +70,8 @@ class AlbumCoverChoiceController : public QWidget {
void Init(Application *app);
void ReloadSettings();
CollectionSettingsPage::SaveCoverType get_save_album_cover_type() const { return (save_embedded_cover_override_ ? CollectionSettingsPage::SaveCoverType_Embedded : save_cover_type_); }
CollectionSettingsPage::SaveCoverType get_collection_save_album_cover_type() const { return save_cover_type_; }
CoverOptions::CoverType get_save_album_cover_type() const { return (save_embedded_cover_override_ ? CoverOptions::CoverType::Embedded : cover_options_.cover_type); }
CoverOptions::CoverType get_collection_save_album_cover_type() const { return cover_options_.cover_type; }
// Getters for all QActions implemented by this controller.
@@ -189,12 +190,7 @@ class AlbumCoverChoiceController : public QWidget {
QMap<quint64, Song> cover_save_tasks_;
QMutex mutex_cover_save_tasks_;
CollectionSettingsPage::SaveCoverType save_cover_type_;
CollectionSettingsPage::SaveCoverFilename save_cover_filename_;
QString cover_pattern_;
bool cover_overwrite_;
bool cover_lowercase_;
bool cover_replace_spaces_;
CoverOptions cover_options_;
bool save_embedded_cover_override_;
};

View File

@@ -52,9 +52,9 @@ AlbumCoverExport::DialogResult AlbumCoverExport::Exec() {
// Restore last accepted settings
ui_->fileName->setText(s.value("fileName", "cover").toString());
ui_->doNotOverwrite->setChecked(s.value("overwrite", OverwriteMode_None).toInt() == OverwriteMode_None);
ui_->overwriteAll->setChecked(s.value("overwrite", OverwriteMode_All).toInt() == OverwriteMode_All);
ui_->overwriteSmaller->setChecked(s.value("overwrite", OverwriteMode_Smaller).toInt() == OverwriteMode_Smaller);
ui_->doNotOverwrite->setChecked(static_cast<OverwriteMode>(s.value("overwrite", static_cast<int>(OverwriteMode::None)).toInt()) == OverwriteMode::None);
ui_->overwriteAll->setChecked(static_cast<OverwriteMode>(s.value("overwrite", static_cast<int>(OverwriteMode::All)).toInt()) == OverwriteMode::All);
ui_->overwriteSmaller->setChecked(static_cast<OverwriteMode>(s.value("overwrite", static_cast<int>(OverwriteMode::Smaller)).toInt()) == OverwriteMode::Smaller);
ui_->forceSize->setChecked(s.value("forceSize", false).toBool());
ui_->width->setText(s.value("width", "").toString());
ui_->height->setText(s.value("height", "").toString());
@@ -71,13 +71,13 @@ AlbumCoverExport::DialogResult AlbumCoverExport::Exec() {
if (fileName.isEmpty()) {
fileName = "cover";
}
OverwriteMode overwrite = ui_->doNotOverwrite->isChecked() ? OverwriteMode_None : (ui_->overwriteAll->isChecked() ? OverwriteMode_All : OverwriteMode_Smaller);
OverwriteMode overwrite_mode = ui_->doNotOverwrite->isChecked() ? OverwriteMode::None : (ui_->overwriteAll->isChecked() ? OverwriteMode::All : OverwriteMode::Smaller);
bool forceSize = ui_->forceSize->isChecked();
QString width = ui_->width->text();
QString height = ui_->height->text();
s.setValue("fileName", fileName);
s.setValue("overwrite", overwrite);
s.setValue("overwrite", static_cast<int>(overwrite_mode));
s.setValue("forceSize", forceSize);
s.setValue("width", width);
s.setValue("height", height);
@@ -85,7 +85,7 @@ AlbumCoverExport::DialogResult AlbumCoverExport::Exec() {
s.setValue("export_embedded", ui_->export_embedded->isChecked());
result.filename_ = fileName;
result.overwrite_ = overwrite;
result.overwrite_ = overwrite_mode;
result.forcesize_ = forceSize;
result.width_ = width.toInt();
result.height_ = height.toInt();

View File

@@ -39,10 +39,10 @@ class AlbumCoverExport : public QDialog {
explicit AlbumCoverExport(QWidget *parent = nullptr);
~AlbumCoverExport() override;
enum OverwriteMode {
OverwriteMode_None = 0,
OverwriteMode_All = 1,
OverwriteMode_Smaller = 2
enum class OverwriteMode {
None = 0,
All = 1,
Smaller = 2
};
struct DialogResult {
@@ -63,7 +63,7 @@ class AlbumCoverExport : public QDialog {
}
bool RequiresCoverProcessing() const {
return IsSizeForced() || overwrite_ == OverwriteMode_Smaller;
return IsSizeForced() || overwrite_ == OverwriteMode::Smaller;
}
};

View File

@@ -100,7 +100,7 @@ Q_DECLARE_METATYPE(CoverProviderSearchResult)
// This is a complete result of a single search request (a list of results, each describing one image, actually).
using CoverProviderSearchResults = QList<CoverProviderSearchResult>;
Q_DECLARE_METATYPE(QList<CoverProviderSearchResult>)
Q_DECLARE_METATYPE(CoverProviderSearchResults)
// This class searches for album covers for a given query or artist/album and returns URLs. It's NOT thread-safe.
class AlbumCoverFetcher : public QObject {

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