diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9c95f811..5e723195 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -367,6 +367,7 @@ option(INSTALL_TRANSLATIONS "Install translations" OFF)
optional_component(SUBSONIC ON "Subsonic support")
optional_component(TIDAL ON "Tidal support")
+optional_component(QOBUZ ON "Qobuz support")
optional_component(MOODBAR ON "Moodbar"
DEPENDS "fftw3" FFTW3_FOUND
diff --git a/data/data.qrc b/data/data.qrc
index 3c6eb01f..828cb76b 100644
--- a/data/data.qrc
+++ b/data/data.qrc
@@ -13,8 +13,10 @@
schema/schema-10.sql
schema/schema-11.sql
schema/schema-12.sql
+ schema/schema-13.sql
schema/device-schema.sql
style/strawberry.css
+ style/smartplaylistsearchterm.css
html/playing-tooltip.html
html/oauthsuccess.html
pictures/strawberry.png
@@ -40,6 +42,8 @@
pictures/osd_shadow_edge.png
pictures/nyancat.png
pictures/rainbowdash.png
+ pictures/star-on.png
+ pictures/star-off.png
fonts/HumongousofEternitySt.ttf
mood/sample.mood
text/ghosts.txt
diff --git a/data/icons.qrc b/data/icons.qrc
index c7212d4c..21307b33 100644
--- a/data/icons.qrc
+++ b/data/icons.qrc
@@ -89,6 +89,7 @@
icons/128x128/love.png
icons/128x128/subsonic.png
icons/128x128/tidal.png
+ icons/128x128/qobuz.png
icons/128x128/multimedia-player-ipod-standard-black.png
icons/64x64/albums.png
icons/64x64/alsa.png
@@ -180,6 +181,7 @@
icons/64x64/love.png
icons/64x64/subsonic.png
icons/64x64/tidal.png
+ icons/64x64/qobuz.png
icons/64x64/multimedia-player-ipod-standard-black.png
icons/48x48/albums.png
icons/48x48/alsa.png
@@ -275,6 +277,7 @@
icons/48x48/love.png
icons/48x48/subsonic.png
icons/48x48/tidal.png
+ icons/48x48/qobuz.png
icons/48x48/multimedia-player-ipod-standard-black.png
icons/32x32/albums.png
icons/32x32/alsa.png
@@ -370,6 +373,7 @@
icons/32x32/love.png
icons/32x32/subsonic.png
icons/32x32/tidal.png
+ icons/32x32/qobuz.png
icons/32x32/multimedia-player-ipod-standard-black.png
icons/22x22/albums.png
icons/22x22/alsa.png
@@ -465,6 +469,7 @@
icons/22x22/love.png
icons/22x22/subsonic.png
icons/22x22/tidal.png
+ icons/22x22/qobuz.png
icons/22x22/multimedia-player-ipod-standard-black.png
diff --git a/data/icons/128x128/qobuz.png b/data/icons/128x128/qobuz.png
new file mode 100644
index 00000000..4b6b1775
Binary files /dev/null and b/data/icons/128x128/qobuz.png differ
diff --git a/data/icons/22x22/qobuz.png b/data/icons/22x22/qobuz.png
new file mode 100644
index 00000000..09834a5f
Binary files /dev/null and b/data/icons/22x22/qobuz.png differ
diff --git a/data/icons/32x32/qobuz.png b/data/icons/32x32/qobuz.png
new file mode 100644
index 00000000..c9c6b8a3
Binary files /dev/null and b/data/icons/32x32/qobuz.png differ
diff --git a/data/icons/48x48/qobuz.png b/data/icons/48x48/qobuz.png
new file mode 100644
index 00000000..08af18ce
Binary files /dev/null and b/data/icons/48x48/qobuz.png differ
diff --git a/data/icons/64x64/qobuz.png b/data/icons/64x64/qobuz.png
new file mode 100644
index 00000000..4c7f31b4
Binary files /dev/null and b/data/icons/64x64/qobuz.png differ
diff --git a/data/icons/full/qobuz.png b/data/icons/full/qobuz.png
new file mode 100644
index 00000000..c7ad97fd
Binary files /dev/null and b/data/icons/full/qobuz.png differ
diff --git a/data/pictures/star-off.png b/data/pictures/star-off.png
new file mode 100644
index 00000000..e6246317
Binary files /dev/null and b/data/pictures/star-off.png differ
diff --git a/data/pictures/star-on.png b/data/pictures/star-on.png
new file mode 100644
index 00000000..5d586b3c
Binary files /dev/null and b/data/pictures/star-on.png differ
diff --git a/data/schema/device-schema.sql b/data/schema/device-schema.sql
index 94853d0a..fbcd438a 100644
--- a/data/schema/device-schema.sql
+++ b/data/schema/device-schema.sql
@@ -62,7 +62,9 @@ CREATE TABLE device_%deviceid_songs (
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
- cue_path TEXT
+ cue_path TEXT,
+
+ rating INTEGER DEFAULT -1
);
@@ -75,4 +77,4 @@ CREATE VIRTUAL TABLE device_%deviceid_fts USING fts5(
tokenize = "unicode61 remove_diacritics 1"
);
-UPDATE devices SET schema_version=1 WHERE ROWID=%deviceid;
+UPDATE devices SET schema_version=2 WHERE ROWID=%deviceid;
diff --git a/data/schema/schema-13.sql b/data/schema/schema-13.sql
new file mode 100644
index 00000000..7720c0b3
--- /dev/null
+++ b/data/schema/schema-13.sql
@@ -0,0 +1,231 @@
+ALTER TABLE %allsongstables ADD COLUMN rating INTEGER DEFAULT -1;
+
+ALTER TABLE playlists ADD COLUMN dynamic_playlist_type INTEGER;
+
+ALTER TABLE playlists ADD COLUMN dynamic_playlist_backend TEXT;
+
+ALTER TABLE playlists ADD COLUMN dynamic_playlist_data BLOB;
+
+CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
+
+ title TEXT,
+ album TEXT,
+ artist TEXT,
+ albumartist TEXT,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT -1,
+ genre TEXT,
+ compilation INTEGER NOT NULL DEFAULT 0,
+ composer TEXT,
+ performer TEXT,
+ grouping TEXT,
+ comment TEXT,
+ lyrics TEXT,
+
+ artist_id TEXT,
+ album_id TEXT,
+ song_id TEXT,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT -1,
+ samplerate INTEGER NOT NULL DEFAULT -1,
+ bitdepth INTEGER NOT NULL DEFAULT -1,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL DEFAULT -1,
+ url TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT -1,
+ mtime INTEGER NOT NULL DEFAULT -1,
+ ctime INTEGER NOT NULL DEFAULT -1,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT -1,
+
+ compilation_detected INTEGER DEFAULT 0,
+ compilation_on INTEGER NOT NULL DEFAULT 0,
+ compilation_off INTEGER NOT NULL DEFAULT 0,
+ compilation_effective INTEGER NOT NULL DEFAULT 0,
+
+ art_automatic TEXT,
+ art_manual TEXT,
+
+ effective_albumartist TEXT,
+ effective_originalyear INTEGER NOT NULL DEFAULT 0,
+
+ cue_path TEXT,
+
+ rating INTEGER DEFAULT -1
+
+);
+
+CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
+
+ title TEXT,
+ album TEXT,
+ artist TEXT,
+ albumartist TEXT,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT -1,
+ genre TEXT,
+ compilation INTEGER NOT NULL DEFAULT 0,
+ composer TEXT,
+ performer TEXT,
+ grouping TEXT,
+ comment TEXT,
+ lyrics TEXT,
+
+ artist_id TEXT,
+ album_id TEXT,
+ song_id TEXT,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT -1,
+ samplerate INTEGER NOT NULL DEFAULT -1,
+ bitdepth INTEGER NOT NULL DEFAULT -1,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL DEFAULT -1,
+ url TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT -1,
+ mtime INTEGER NOT NULL DEFAULT -1,
+ ctime INTEGER NOT NULL DEFAULT -1,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT -1,
+
+ compilation_detected INTEGER DEFAULT 0,
+ compilation_on INTEGER NOT NULL DEFAULT 0,
+ compilation_off INTEGER NOT NULL DEFAULT 0,
+ compilation_effective INTEGER NOT NULL DEFAULT 0,
+
+ art_automatic TEXT,
+ art_manual TEXT,
+
+ effective_albumartist TEXT,
+ effective_originalyear INTEGER NOT NULL DEFAULT 0,
+
+ cue_path TEXT,
+
+ rating INTEGER DEFAULT -1
+
+);
+
+CREATE TABLE IF NOT EXISTS qobuz_songs (
+
+ title TEXT,
+ album TEXT,
+ artist TEXT,
+ albumartist TEXT,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT -1,
+ genre TEXT,
+ compilation INTEGER NOT NULL DEFAULT 0,
+ composer TEXT,
+ performer TEXT,
+ grouping TEXT,
+ comment TEXT,
+ lyrics TEXT,
+
+ artist_id TEXT,
+ album_id TEXT,
+ song_id TEXT,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT -1,
+ samplerate INTEGER NOT NULL DEFAULT -1,
+ bitdepth INTEGER NOT NULL DEFAULT -1,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL DEFAULT -1,
+ url TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT -1,
+ mtime INTEGER NOT NULL DEFAULT -1,
+ ctime INTEGER NOT NULL DEFAULT -1,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT -1,
+
+ compilation_detected INTEGER DEFAULT 0,
+ compilation_on INTEGER NOT NULL DEFAULT 0,
+ compilation_off INTEGER NOT NULL DEFAULT 0,
+ compilation_effective INTEGER NOT NULL DEFAULT 0,
+
+ art_automatic TEXT,
+ art_manual TEXT,
+
+ effective_albumartist TEXT,
+ effective_originalyear INTEGER NOT NULL DEFAULT 0,
+
+ cue_path TEXT,
+
+ rating INTEGER DEFAULT -1
+
+);
+
+CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_artists_songs_fts USING fts5(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize = "unicode61 remove_diacritics 1"
+
+);
+
+CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_albums_songs_fts USING fts5(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize = "unicode61 remove_diacritics 1"
+
+);
+
+CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_songs_fts USING fts5(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize = "unicode61 remove_diacritics 1"
+
+);
+
+UPDATE schema_version SET version=13;
diff --git a/data/schema/schema.sql b/data/schema/schema.sql
index ff485510..92dc3a4a 100644
--- a/data/schema/schema.sql
+++ b/data/schema/schema.sql
@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
DELETE FROM schema_version;
-INSERT INTO schema_version (version) VALUES (12);
+INSERT INTO schema_version (version) VALUES (13);
CREATE TABLE IF NOT EXISTS directories (
path TEXT NOT NULL,
@@ -70,178 +70,9 @@ CREATE TABLE IF NOT EXISTS songs (
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
- cue_path TEXT
+ cue_path TEXT,
-);
-
-CREATE TABLE IF NOT EXISTS tidal_artists_songs (
-
- title TEXT,
- album TEXT,
- artist TEXT,
- albumartist TEXT,
- track INTEGER NOT NULL DEFAULT -1,
- disc INTEGER NOT NULL DEFAULT -1,
- year INTEGER NOT NULL DEFAULT -1,
- originalyear INTEGER NOT NULL DEFAULT -1,
- genre TEXT,
- compilation INTEGER NOT NULL DEFAULT 0,
- composer TEXT,
- performer TEXT,
- grouping TEXT,
- comment TEXT,
- lyrics TEXT,
-
- artist_id TEXT,
- album_id TEXT,
- song_id TEXT,
-
- beginning INTEGER NOT NULL DEFAULT 0,
- length INTEGER NOT NULL DEFAULT 0,
-
- bitrate INTEGER NOT NULL DEFAULT -1,
- samplerate INTEGER NOT NULL DEFAULT -1,
- bitdepth INTEGER NOT NULL DEFAULT -1,
-
- source INTEGER NOT NULL DEFAULT 0,
- directory_id INTEGER NOT NULL DEFAULT -1,
- url TEXT NOT NULL,
- filetype INTEGER NOT NULL DEFAULT 0,
- filesize INTEGER NOT NULL DEFAULT -1,
- mtime INTEGER NOT NULL DEFAULT -1,
- ctime INTEGER NOT NULL DEFAULT -1,
- unavailable INTEGER DEFAULT 0,
-
- playcount INTEGER NOT NULL DEFAULT 0,
- skipcount INTEGER NOT NULL DEFAULT 0,
- lastplayed INTEGER NOT NULL DEFAULT -1,
-
- compilation_detected INTEGER DEFAULT 0,
- compilation_on INTEGER NOT NULL DEFAULT 0,
- compilation_off INTEGER NOT NULL DEFAULT 0,
- compilation_effective INTEGER NOT NULL DEFAULT 0,
-
- art_automatic TEXT,
- art_manual TEXT,
-
- effective_albumartist TEXT,
- effective_originalyear INTEGER NOT NULL DEFAULT 0,
-
- cue_path TEXT
-
-);
-
-CREATE TABLE IF NOT EXISTS tidal_albums_songs (
-
- title TEXT,
- album TEXT,
- artist TEXT,
- albumartist TEXT,
- track INTEGER NOT NULL DEFAULT -1,
- disc INTEGER NOT NULL DEFAULT -1,
- year INTEGER NOT NULL DEFAULT -1,
- originalyear INTEGER NOT NULL DEFAULT -1,
- genre TEXT,
- compilation INTEGER NOT NULL DEFAULT 0,
- composer TEXT,
- performer TEXT,
- grouping TEXT,
- comment TEXT,
- lyrics TEXT,
-
- artist_id TEXT,
- album_id TEXT,
- song_id TEXT,
-
- beginning INTEGER NOT NULL DEFAULT 0,
- length INTEGER NOT NULL DEFAULT 0,
-
- bitrate INTEGER NOT NULL DEFAULT -1,
- samplerate INTEGER NOT NULL DEFAULT -1,
- bitdepth INTEGER NOT NULL DEFAULT -1,
-
- source INTEGER NOT NULL DEFAULT 0,
- directory_id INTEGER NOT NULL DEFAULT -1,
- url TEXT NOT NULL,
- filetype INTEGER NOT NULL DEFAULT 0,
- filesize INTEGER NOT NULL DEFAULT -1,
- mtime INTEGER NOT NULL DEFAULT -1,
- ctime INTEGER NOT NULL DEFAULT -1,
- unavailable INTEGER DEFAULT 0,
-
- playcount INTEGER NOT NULL DEFAULT 0,
- skipcount INTEGER NOT NULL DEFAULT 0,
- lastplayed INTEGER NOT NULL DEFAULT -1,
-
- compilation_detected INTEGER DEFAULT 0,
- compilation_on INTEGER NOT NULL DEFAULT 0,
- compilation_off INTEGER NOT NULL DEFAULT 0,
- compilation_effective INTEGER NOT NULL DEFAULT 0,
-
- art_automatic TEXT,
- art_manual TEXT,
-
- effective_albumartist TEXT,
- effective_originalyear INTEGER NOT NULL DEFAULT 0,
-
- cue_path TEXT
-
-);
-
-CREATE TABLE IF NOT EXISTS tidal_songs (
-
- title TEXT,
- album TEXT,
- artist TEXT,
- albumartist TEXT,
- track INTEGER NOT NULL DEFAULT -1,
- disc INTEGER NOT NULL DEFAULT -1,
- year INTEGER NOT NULL DEFAULT -1,
- originalyear INTEGER NOT NULL DEFAULT -1,
- genre TEXT,
- compilation INTEGER NOT NULL DEFAULT 0,
- composer TEXT,
- performer TEXT,
- grouping TEXT,
- comment TEXT,
- lyrics TEXT,
-
- artist_id TEXT,
- album_id TEXT,
- song_id TEXT,
-
- beginning INTEGER NOT NULL DEFAULT 0,
- length INTEGER NOT NULL DEFAULT 0,
-
- bitrate INTEGER NOT NULL DEFAULT -1,
- samplerate INTEGER NOT NULL DEFAULT -1,
- bitdepth INTEGER NOT NULL DEFAULT -1,
-
- source INTEGER NOT NULL DEFAULT 0,
- directory_id INTEGER NOT NULL DEFAULT -1,
- url TEXT NOT NULL,
- filetype INTEGER NOT NULL DEFAULT 0,
- filesize INTEGER NOT NULL DEFAULT -1,
- mtime INTEGER NOT NULL DEFAULT -1,
- ctime INTEGER NOT NULL DEFAULT -1,
- unavailable INTEGER DEFAULT 0,
-
- playcount INTEGER NOT NULL DEFAULT 0,
- skipcount INTEGER NOT NULL DEFAULT 0,
- lastplayed INTEGER NOT NULL DEFAULT -1,
-
- compilation_detected INTEGER DEFAULT 0,
- compilation_on INTEGER NOT NULL DEFAULT 0,
- compilation_off INTEGER NOT NULL DEFAULT 0,
- compilation_effective INTEGER NOT NULL DEFAULT 0,
-
- art_automatic TEXT,
- art_manual TEXT,
-
- effective_albumartist TEXT,
- effective_originalyear INTEGER NOT NULL DEFAULT 0,
-
- cue_path TEXT
+ rating INTEGER DEFAULT -1
);
@@ -298,7 +129,363 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
- cue_path TEXT
+ cue_path TEXT,
+
+ rating INTEGER DEFAULT -1
+
+);
+
+CREATE TABLE IF NOT EXISTS tidal_artists_songs (
+
+ title TEXT,
+ album TEXT,
+ artist TEXT,
+ albumartist TEXT,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT -1,
+ genre TEXT,
+ compilation INTEGER NOT NULL DEFAULT 0,
+ composer TEXT,
+ performer TEXT,
+ grouping TEXT,
+ comment TEXT,
+ lyrics TEXT,
+
+ artist_id TEXT,
+ album_id TEXT,
+ song_id TEXT,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT -1,
+ samplerate INTEGER NOT NULL DEFAULT -1,
+ bitdepth INTEGER NOT NULL DEFAULT -1,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL DEFAULT -1,
+ url TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT -1,
+ mtime INTEGER NOT NULL DEFAULT -1,
+ ctime INTEGER NOT NULL DEFAULT -1,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT -1,
+
+ compilation_detected INTEGER DEFAULT 0,
+ compilation_on INTEGER NOT NULL DEFAULT 0,
+ compilation_off INTEGER NOT NULL DEFAULT 0,
+ compilation_effective INTEGER NOT NULL DEFAULT 0,
+
+ art_automatic TEXT,
+ art_manual TEXT,
+
+ effective_albumartist TEXT,
+ effective_originalyear INTEGER NOT NULL DEFAULT 0,
+
+ cue_path TEXT,
+
+ rating INTEGER DEFAULT -1
+
+);
+
+CREATE TABLE IF NOT EXISTS tidal_albums_songs (
+
+ title TEXT,
+ album TEXT,
+ artist TEXT,
+ albumartist TEXT,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT -1,
+ genre TEXT,
+ compilation INTEGER NOT NULL DEFAULT 0,
+ composer TEXT,
+ performer TEXT,
+ grouping TEXT,
+ comment TEXT,
+ lyrics TEXT,
+
+ artist_id TEXT,
+ album_id TEXT,
+ song_id TEXT,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT -1,
+ samplerate INTEGER NOT NULL DEFAULT -1,
+ bitdepth INTEGER NOT NULL DEFAULT -1,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL DEFAULT -1,
+ url TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT -1,
+ mtime INTEGER NOT NULL DEFAULT -1,
+ ctime INTEGER NOT NULL DEFAULT -1,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT -1,
+
+ compilation_detected INTEGER DEFAULT 0,
+ compilation_on INTEGER NOT NULL DEFAULT 0,
+ compilation_off INTEGER NOT NULL DEFAULT 0,
+ compilation_effective INTEGER NOT NULL DEFAULT 0,
+
+ art_automatic TEXT,
+ art_manual TEXT,
+
+ effective_albumartist TEXT,
+ effective_originalyear INTEGER NOT NULL DEFAULT 0,
+
+ cue_path TEXT,
+
+ rating INTEGER DEFAULT -1
+
+);
+
+CREATE TABLE IF NOT EXISTS tidal_songs (
+
+ title TEXT,
+ album TEXT,
+ artist TEXT,
+ albumartist TEXT,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT -1,
+ genre TEXT,
+ compilation INTEGER NOT NULL DEFAULT 0,
+ composer TEXT,
+ performer TEXT,
+ grouping TEXT,
+ comment TEXT,
+ lyrics TEXT,
+
+ artist_id TEXT,
+ album_id TEXT,
+ song_id TEXT,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT -1,
+ samplerate INTEGER NOT NULL DEFAULT -1,
+ bitdepth INTEGER NOT NULL DEFAULT -1,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL DEFAULT -1,
+ url TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT -1,
+ mtime INTEGER NOT NULL DEFAULT -1,
+ ctime INTEGER NOT NULL DEFAULT -1,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT -1,
+
+ compilation_detected INTEGER DEFAULT 0,
+ compilation_on INTEGER NOT NULL DEFAULT 0,
+ compilation_off INTEGER NOT NULL DEFAULT 0,
+ compilation_effective INTEGER NOT NULL DEFAULT 0,
+
+ art_automatic TEXT,
+ art_manual TEXT,
+
+ effective_albumartist TEXT,
+ effective_originalyear INTEGER NOT NULL DEFAULT 0,
+
+ cue_path TEXT,
+
+ rating INTEGER DEFAULT -1
+
+);
+
+CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
+
+ title TEXT,
+ album TEXT,
+ artist TEXT,
+ albumartist TEXT,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT -1,
+ genre TEXT,
+ compilation INTEGER NOT NULL DEFAULT 0,
+ composer TEXT,
+ performer TEXT,
+ grouping TEXT,
+ comment TEXT,
+ lyrics TEXT,
+
+ artist_id TEXT,
+ album_id TEXT,
+ song_id TEXT,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT -1,
+ samplerate INTEGER NOT NULL DEFAULT -1,
+ bitdepth INTEGER NOT NULL DEFAULT -1,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL DEFAULT -1,
+ url TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT -1,
+ mtime INTEGER NOT NULL DEFAULT -1,
+ ctime INTEGER NOT NULL DEFAULT -1,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT -1,
+
+ compilation_detected INTEGER DEFAULT 0,
+ compilation_on INTEGER NOT NULL DEFAULT 0,
+ compilation_off INTEGER NOT NULL DEFAULT 0,
+ compilation_effective INTEGER NOT NULL DEFAULT 0,
+
+ art_automatic TEXT,
+ art_manual TEXT,
+
+ effective_albumartist TEXT,
+ effective_originalyear INTEGER NOT NULL DEFAULT 0,
+
+ cue_path TEXT,
+
+ rating INTEGER DEFAULT -1
+
+);
+
+CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
+
+ title TEXT,
+ album TEXT,
+ artist TEXT,
+ albumartist TEXT,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT -1,
+ genre TEXT,
+ compilation INTEGER NOT NULL DEFAULT 0,
+ composer TEXT,
+ performer TEXT,
+ grouping TEXT,
+ comment TEXT,
+ lyrics TEXT,
+
+ artist_id TEXT,
+ album_id TEXT,
+ song_id TEXT,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT -1,
+ samplerate INTEGER NOT NULL DEFAULT -1,
+ bitdepth INTEGER NOT NULL DEFAULT -1,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL DEFAULT -1,
+ url TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT -1,
+ mtime INTEGER NOT NULL DEFAULT -1,
+ ctime INTEGER NOT NULL DEFAULT -1,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT -1,
+
+ compilation_detected INTEGER DEFAULT 0,
+ compilation_on INTEGER NOT NULL DEFAULT 0,
+ compilation_off INTEGER NOT NULL DEFAULT 0,
+ compilation_effective INTEGER NOT NULL DEFAULT 0,
+
+ art_automatic TEXT,
+ art_manual TEXT,
+
+ effective_albumartist TEXT,
+ effective_originalyear INTEGER NOT NULL DEFAULT 0,
+
+ cue_path TEXT,
+
+ rating INTEGER DEFAULT -1
+
+);
+
+CREATE TABLE IF NOT EXISTS qobuz_songs (
+
+ title TEXT,
+ album TEXT,
+ artist TEXT,
+ albumartist TEXT,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT -1,
+ genre TEXT,
+ compilation INTEGER NOT NULL DEFAULT 0,
+ composer TEXT,
+ performer TEXT,
+ grouping TEXT,
+ comment TEXT,
+ lyrics TEXT,
+
+ artist_id TEXT,
+ album_id TEXT,
+ song_id TEXT,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT -1,
+ samplerate INTEGER NOT NULL DEFAULT -1,
+ bitdepth INTEGER NOT NULL DEFAULT -1,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL DEFAULT -1,
+ url TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT -1,
+ mtime INTEGER NOT NULL DEFAULT -1,
+ ctime INTEGER NOT NULL DEFAULT -1,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT -1,
+
+ compilation_detected INTEGER DEFAULT 0,
+ compilation_on INTEGER NOT NULL DEFAULT 0,
+ compilation_off INTEGER NOT NULL DEFAULT 0,
+ compilation_effective INTEGER NOT NULL DEFAULT 0,
+
+ art_automatic TEXT,
+ art_manual TEXT,
+
+ effective_albumartist TEXT,
+ effective_originalyear INTEGER NOT NULL DEFAULT 0,
+
+ cue_path TEXT,
+
+ rating INTEGER DEFAULT -1
);
@@ -309,7 +496,11 @@ CREATE TABLE IF NOT EXISTS playlists (
ui_order INTEGER NOT NULL DEFAULT 0,
special_type TEXT,
ui_path TEXT,
- is_favorite INTEGER NOT NULL DEFAULT 0
+ is_favorite INTEGER NOT NULL DEFAULT 0,
+
+ dynamic_playlist_type INTEGER,
+ dynamic_playlist_backend TEXT,
+ dynamic_playlist_data BLOB
);
@@ -371,7 +562,9 @@ CREATE TABLE IF NOT EXISTS playlist_items (
effective_albumartist TEXT,
effective_originalyear INTEGER,
- cue_path TEXT
+ cue_path TEXT,
+
+ rating INTEGER DEFAULT -1
);
@@ -414,6 +607,21 @@ CREATE VIRTUAL TABLE IF NOT EXISTS songs_fts USING fts5(
);
+CREATE VIRTUAL TABLE IF NOT EXISTS subsonic_songs_fts USING fts5(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize = "unicode61 remove_diacritics 1"
+
+);
+
CREATE VIRTUAL TABLE IF NOT EXISTS tidal_artists_songs_fts USING fts5(
ftstitle,
@@ -459,7 +667,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS tidal_songs_fts USING fts5(
);
-CREATE VIRTUAL TABLE IF NOT EXISTS subsonic_songs_fts USING fts5(
+CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_artists_songs_fts USING fts5(
ftstitle,
ftsalbum,
@@ -474,7 +682,22 @@ CREATE VIRTUAL TABLE IF NOT EXISTS subsonic_songs_fts USING fts5(
);
-CREATE VIRTUAL TABLE IF NOT EXISTS playlist_items_fts_ USING fts5(
+CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_albums_songs_fts USING fts5(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize = "unicode61 remove_diacritics 1"
+
+);
+
+CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_songs_fts USING fts5(
ftstitle,
ftsalbum,
diff --git a/data/style/smartplaylistsearchterm.css b/data/style/smartplaylistsearchterm.css
new file mode 100644
index 00000000..39964869
--- /dev/null
+++ b/data/style/smartplaylistsearchterm.css
@@ -0,0 +1,43 @@
+#frame {
+ border: 1px solid palette(mid);
+ border-radius: 5px;
+}
+
+#container {
+ margin: 2px;
+}
+
+#remove {
+ border-top-left-radius: 0px;
+ border-top-right-radius: 5px;
+ border-bottom-left-radius: 0px;
+ border-bottom-right-radius: 5px;
+ border: 0px solid transparent;
+ background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
+ stop:0 %light,
+ stop:0.4 %light,
+ stop:0.6 %dark,
+ stop:1 %dark);
+
+ margin-left: 5px;
+ padding: 0px 5px;
+}
+
+#remove:hover {
+ background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
+ stop:0 %light2,
+ stop:0.4 %light2,
+ stop:0.6 %base,
+ stop:1 %base);
+ border: 0px solid transparent;
+}
+
+#remove:pressed {
+ background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
+ stop:0 %base,
+ stop:0.4 %base,
+ stop:0.6 %light2,
+ stop:1 %light2);
+ border: 0px solid transparent;
+}
+
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a89f8e11..887d2782 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -96,6 +96,7 @@ set(SOURCES
playlist/playlistview.cpp
playlist/songloaderinserter.cpp
playlist/songplaylistitem.cpp
+ playlist/dynamicplaylistcontrols.cpp
queue/queue.cpp
queue/queueview.cpp
@@ -111,6 +112,20 @@ set(SOURCES
playlistparsers/xmlparser.cpp
playlistparsers/xspfparser.cpp
+ smartplaylists/playlistgenerator.cpp
+ smartplaylists/playlistgeneratorinserter.cpp
+ smartplaylists/playlistquerygenerator.cpp
+ smartplaylists/smartplaylistquerywizardplugin.cpp
+ smartplaylists/smartplaylistsearch.cpp
+ smartplaylists/smartplaylistsearchpreview.cpp
+ smartplaylists/smartplaylistsearchterm.cpp
+ smartplaylists/smartplaylistsearchtermwidget.cpp
+ smartplaylists/smartplaylistsmodel.cpp
+ smartplaylists/smartplaylistsviewcontainer.cpp
+ smartplaylists/smartplaylistsview.cpp
+ smartplaylists/smartplaylistwizard.cpp
+ smartplaylists/smartplaylistwizardplugin.cpp
+
covermanager/albumcovermanager.cpp
covermanager/albumcovermanagerlist.cpp
covermanager/albumcoverloader.cpp
@@ -195,6 +210,7 @@ set(SOURCES
widgets/tracksliderpopup.cpp
widgets/tracksliderslider.cpp
widgets/loginstatewidget.cpp
+ widgets/ratingwidget.cpp
osd/osdbase.cpp
osd/osdpretty.cpp
@@ -301,6 +317,7 @@ set(HEADERS
playlist/playlistitemmimedata.h
playlist/songloaderinserter.h
playlist/songmimedata.h
+ playlist/dynamicplaylistcontrols.h
queue/queue.h
queue/queueview.h
@@ -314,6 +331,18 @@ set(HEADERS
playlistparsers/plsparser.h
playlistparsers/xspfparser.h
+ smartplaylists/playlistgenerator.h
+ smartplaylists/playlistgeneratorinserter.h
+ smartplaylists/playlistgeneratormimedata.h
+ smartplaylists/smartplaylistquerywizardplugin.h
+ smartplaylists/smartplaylistsearchpreview.h
+ smartplaylists/smartplaylistsearchtermwidget.h
+ smartplaylists/smartplaylistsmodel.h
+ smartplaylists/smartplaylistsviewcontainer.h
+ smartplaylists/smartplaylistsview.h
+ smartplaylists/smartplaylistwizard.h
+ smartplaylists/smartplaylistwizardplugin.h
+
covermanager/albumcovermanager.h
covermanager/albumcovermanagerlist.h
covermanager/albumcoverloader.h
@@ -397,6 +426,7 @@ set(HEADERS
widgets/tracksliderslider.h
widgets/loginstatewidget.h
widgets/qsearchfield.h
+ widgets/ratingwidget.h
osd/osdbase.h
osd/osdpretty.h
@@ -446,9 +476,17 @@ set(UI
playlist/playlistlistcontainer.ui
playlist/playlistsaveoptionsdialog.ui
playlist/playlistsequence.ui
+ playlist/dynamicplaylistcontrols.ui
queue/queueview.ui
+ smartplaylists/smartplaylistquerysearchpage.ui
+ smartplaylists/smartplaylistquerysortpage.ui
+ smartplaylists/smartplaylistsearchpreview.ui
+ smartplaylists/smartplaylistsearchtermwidget.ui
+ smartplaylists/smartplaylistsviewcontainer.ui
+ smartplaylists/smartplaylistwizardfinishpage.ui
+
covermanager/albumcoverexport.ui
covermanager/albumcovermanager.ui
covermanager/albumcoversearcher.ui
@@ -899,6 +937,27 @@ optional_source(HAVE_TIDAL
settings/tidalsettingspage.ui
)
+optional_source(HAVE_QOBUZ
+ SOURCES
+ qobuz/qobuzservice.cpp
+ qobuz/qobuzurlhandler.cpp
+ qobuz/qobuzbaserequest.cpp
+ qobuz/qobuzrequest.cpp
+ qobuz/qobuzstreamurlrequest.cpp
+ qobuz/qobuzfavoriterequest.cpp
+ settings/qobuzsettingspage.cpp
+ HEADERS
+ qobuz/qobuzservice.h
+ qobuz/qobuzurlhandler.h
+ qobuz/qobuzbaserequest.h
+ qobuz/qobuzrequest.h
+ qobuz/qobuzstreamurlrequest.h
+ qobuz/qobuzfavoriterequest.h
+ settings/qobuzsettingspage.h
+ UI
+ settings/qobuzsettingspage.ui
+)
+
# Moodbar
optional_source(HAVE_MOODBAR
SOURCES
diff --git a/src/collection/collectionbackend.cpp b/src/collection/collectionbackend.cpp
index df9e0b3f..ee23c1af 100644
--- a/src/collection/collectionbackend.cpp
+++ b/src/collection/collectionbackend.cpp
@@ -45,6 +45,7 @@
#include "core/logging.h"
#include "core/database.h"
#include "core/scopedtransaction.h"
+#include "smartplaylists/smartplaylistsearch.h"
#include "directory.h"
#include "collectionbackend.h"
@@ -1303,6 +1304,38 @@ void CollectionBackend::DeleteAll() {
}
+SongList CollectionBackend::FindSongs(const SmartPlaylistSearch &search) {
+
+ QMutexLocker l(db_->Mutex());
+ QSqlDatabase db(db_->Connect());
+
+ // Build the query
+ QString sql = search.ToSql(songs_table());
+
+ // Run the query
+ SongList ret;
+ QSqlQuery query(db);
+ query.prepare(sql);
+ query.exec();
+ if (db_->CheckErrors(query)) return ret;
+
+ // Read the results
+ while (query.next()) {
+ Song song;
+ song.InitFromQuery(query, true);
+ ret << song;
+ }
+ return ret;
+
+}
+
+SongList CollectionBackend::GetAllSongs() {
+
+ // Get all the songs!
+ return FindSongs(SmartPlaylistSearch(SmartPlaylistSearch::Type_All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::Sort_FieldAsc, SmartPlaylistSearchTerm::Field_Artist, -1));
+
+}
+
SongList CollectionBackend::GetSongsBy(const QString &artist, const QString &album, const QString &title) {
QMutexLocker l(db_->Mutex());
@@ -1378,3 +1411,44 @@ void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &ti
emit SongsStatisticsChanged(SongList() << songs);
}
+
+void CollectionBackend::UpdateSongRating(const int id, const float rating) {
+
+ if (id == -1) return;
+
+ QList id_list;
+ id_list << id;
+ UpdateSongsRating(id_list, rating);
+
+}
+
+void CollectionBackend::UpdateSongsRating(const QList &id_list, const float rating) {
+
+ if (id_list.isEmpty()) return;
+
+ QMutexLocker l(db_->Mutex());
+ QSqlDatabase db(db_->Connect());
+
+ QStringList id_str_list;
+ for (int i : id_list) {
+ id_str_list << QString::number(i);
+ }
+ QString ids = id_str_list.join(",");
+ QSqlQuery q(db);
+ q.prepare(QString("UPDATE %1 SET rating = :rating WHERE ROWID IN (%2)").arg(songs_table_, ids));
+ q.bindValue(":rating", rating);
+ q.exec();
+ if (db_->CheckErrors(q)) return;
+ SongList new_song_list = GetSongsById(id_str_list, db);
+ emit SongsRatingChanged(new_song_list);
+
+}
+
+void CollectionBackend::UpdateSongRatingAsync(const int id, const float rating) {
+ metaObject()->invokeMethod(this, "UpdateSongRating", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(float, rating));
+}
+
+void CollectionBackend::UpdateSongsRatingAsync(const QList& ids, const float rating) {
+ metaObject()->invokeMethod(this, "UpdateSongsRating", Qt::QueuedConnection, Q_ARG(QList, ids), Q_ARG(float, rating));
+}
+
diff --git a/src/collection/collectionbackend.h b/src/collection/collectionbackend.h
index c96e4cfd..6ba823da 100644
--- a/src/collection/collectionbackend.h
+++ b/src/collection/collectionbackend.h
@@ -40,6 +40,7 @@
class QThread;
class Database;
+class SmartPlaylistSearch;
class CollectionBackendInterface : public QObject {
Q_OBJECT
@@ -182,10 +183,16 @@ class CollectionBackend : public CollectionBackendInterface {
Song GetSongBySongId(const QString &song_id);
SongList GetSongsBySongId(const QStringList &song_ids);
+ SongList GetAllSongs();
+ SongList FindSongs(const SmartPlaylistSearch &search);
+
Song::Source Source() const;
void AddOrUpdateSongsAsync(const SongList &songs);
+ void UpdateSongRatingAsync(const int id, const float rating);
+ void UpdateSongsRatingAsync(const QList &ids, const float rating);
+
public slots:
void Exit();
void LoadDirectories();
@@ -209,19 +216,23 @@ class CollectionBackend : public CollectionBackendInterface {
void UpdateLastPlayed(const QString &artist, const QString &album, const QString &title, const int lastplayed);
void UpdatePlayCount(const QString &artist, const QString &title, const int playcount);
- signals:
- void DirectoryDiscovered(const Directory &dir, const SubdirectoryList &subdirs);
- void DirectoryDeleted(const Directory &dir);
+ void UpdateSongRating(const int id, const float rating);
+ void UpdateSongsRating(const QList &id_list, const float rating);
- void SongsDiscovered(const SongList &songs);
- void SongsDeleted(const SongList &songs);
- void SongsStatisticsChanged(const SongList& songs);
+ signals:
+ void DirectoryDiscovered(Directory, SubdirectoryList);
+ void DirectoryDeleted(Directory);
+
+ void SongsDiscovered(SongList);
+ void SongsDeleted(SongList);
+ void SongsStatisticsChanged(SongList);
void DatabaseReset();
- void TotalSongCountUpdated(const int total);
- void TotalArtistCountUpdated(const int total);
- void TotalAlbumCountUpdated(const int total);
+ void TotalSongCountUpdated(int);
+ void TotalArtistCountUpdated(int);
+ void TotalAlbumCountUpdated(int);
+ void SongsRatingChanged(SongList);
void ExitFinished();
diff --git a/src/collection/collectionmodel.cpp b/src/collection/collectionmodel.cpp
index 23b6b470..792e76f2 100644
--- a/src/collection/collectionmodel.cpp
+++ b/src/collection/collectionmodel.cpp
@@ -127,6 +127,7 @@ CollectionModel::CollectionModel(CollectionBackend *backend, Application *app, Q
connect(backend_, SIGNAL(TotalArtistCountUpdated(int)), SLOT(TotalArtistCountUpdatedSlot(int)));
connect(backend_, SIGNAL(TotalAlbumCountUpdated(int)), SLOT(TotalAlbumCountUpdatedSlot(int)));
connect(backend_, SIGNAL(SongsStatisticsChanged(SongList)), SLOT(SongsSlightlyChanged(SongList)));
+ connect(backend_, SIGNAL(SongsRatingChanged(SongList)), SLOT(SongsSlightlyChanged(SongList)));
backend_->UpdateTotalSongCountAsync();
backend_->UpdateTotalArtistCountAsync();
diff --git a/src/config.h.in b/src/config.h.in
index 90c2d21d..86cdafe7 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -46,6 +46,7 @@
#cmakedefine HAVE_SUBSONIC
#cmakedefine HAVE_TIDAL
+#cmakedefine HAVE_QOBUZ
#cmakedefine HAVE_MOODBAR
diff --git a/src/core/application.cpp b/src/core/application.cpp
index 70110b4e..22de1f03 100644
--- a/src/core/application.cpp
+++ b/src/core/application.cpp
@@ -56,7 +56,6 @@
#include "covermanager/discogscoverprovider.h"
#include "covermanager/musicbrainzcoverprovider.h"
#include "covermanager/deezercoverprovider.h"
-#include "covermanager/qobuzcoverprovider.h"
#include "covermanager/musixmatchcoverprovider.h"
#include "covermanager/spotifycoverprovider.h"
@@ -83,6 +82,11 @@
# include "covermanager/tidalcoverprovider.h"
#endif
+#ifdef HAVE_QOBUZ
+# include "qobuz/qobuzservice.h"
+# include "covermanager/qobuzcoverprovider.h"
+#endif
+
#ifdef HAVE_MOODBAR
# include "moodbar/moodbarcontroller.h"
# include "moodbar/moodbarloader.h"
@@ -124,11 +128,13 @@ class ApplicationImpl {
cover_providers->AddProvider(new MusicbrainzCoverProvider(app, app));
cover_providers->AddProvider(new DiscogsCoverProvider(app, app));
cover_providers->AddProvider(new DeezerCoverProvider(app, app));
- cover_providers->AddProvider(new QobuzCoverProvider(app, app));
cover_providers->AddProvider(new MusixmatchCoverProvider(app, app));
cover_providers->AddProvider(new SpotifyCoverProvider(app, app));
#ifdef HAVE_TIDAL
cover_providers->AddProvider(new TidalCoverProvider(app, app));
+#endif
+#ifdef HAVE_QOBUZ
+ cover_providers->AddProvider(new QobuzCoverProvider(app, app));
#endif
cover_providers->ReloadSettings();
return cover_providers;
@@ -159,6 +165,9 @@ class ApplicationImpl {
#endif
#ifdef HAVE_TIDAL
internet_services->AddService(new TidalService(app, internet_services));
+#endif
+#ifdef HAVE_QOBUZ
+ internet_services->AddService(new QobuzService(app, internet_services));
#endif
return internet_services;
}),
diff --git a/src/core/database.cpp b/src/core/database.cpp
index 5c31213e..6399bcd1 100644
--- a/src/core/database.cpp
+++ b/src/core/database.cpp
@@ -54,7 +54,7 @@
#include "scopedtransaction.h"
const char *Database::kDatabaseFilename = "strawberry.db";
-const int Database::kSchemaVersion = 12;
+const int Database::kSchemaVersion = 13;
const char *Database::kMagicAllSongsTables = "%allsongstables";
int Database::sNextConnectionId = 1;
diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp
index 99cec680..ecce3480 100644
--- a/src/core/mainwindow.cpp
+++ b/src/core/mainwindow.cpp
@@ -161,6 +161,9 @@
# include "tidal/tidalservice.h"
# include "settings/tidalsettingspage.h"
#endif
+#ifdef HAVE_QOBUZ
+# include "settings/qobuzsettingspage.h"
+#endif
#include "internet/internetservices.h"
#include "internet/internetservice.h"
@@ -185,6 +188,9 @@
# include "moodbar/moodbarproxystyle.h"
#endif
+#include "smartplaylists/smartplaylistsviewcontainer.h"
+#include "smartplaylists/smartplaylistsview.h"
+
#ifdef Q_OS_WIN
# include "windows7thumbbar.h"
#endif
@@ -257,11 +263,15 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
connect(add_stream_dialog, SIGNAL(accepted()), this, SLOT(AddStreamAccepted()));
return add_stream_dialog;
}),
+ 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)),
#endif
#ifdef HAVE_TIDAL
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)),
#endif
lastfm_import_dialog_(new LastFMImportDialog(app_->lastfm_import(), this)),
collection_show_all_(nullptr),
@@ -296,6 +306,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
playing_widget_(true),
doubleclick_addmode_(BehaviourSettingsPage::AddBehaviour_Append),
doubleclick_playmode_(BehaviourSettingsPage::PlayBehaviour_Never),
+ doubleclick_playlist_addmode_(BehaviourSettingsPage::PlaylistAddBehaviour_Play),
menu_playmode_(BehaviourSettingsPage::PlayBehaviour_Never),
exit_count_(0),
delete_files_(false)
@@ -321,9 +332,10 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
// Add tabs to the fancy tab widget
ui_->tabs->AddTab(context_view_, "context", IconLoader::Load("strawberry"), tr("Context"));
ui_->tabs->AddTab(collection_view_, "collection", IconLoader::Load("library-music"), tr("Collection"));
- ui_->tabs->AddTab(file_view_, "files", IconLoader::Load("document-open"), tr("Files"));
- ui_->tabs->AddTab(playlist_list_, "playlists", IconLoader::Load("view-media-playlist"), tr("Playlists"));
ui_->tabs->AddTab(queue_view_, "queue", IconLoader::Load("footsteps"), tr("Queue"));
+ ui_->tabs->AddTab(playlist_list_, "playlists", IconLoader::Load("view-media-playlist"), tr("Playlists"));
+ ui_->tabs->AddTab(smartplaylists_view_, "smartplaylists", IconLoader::Load("view-media-playlist"), tr("Smart playlists"));
+ ui_->tabs->AddTab(file_view_, "files", IconLoader::Load("document-open"), tr("Files"));
#ifndef Q_OS_WIN
ui_->tabs->AddTab(device_view_, "devices", IconLoader::Load("device"), tr("Devices"));
#endif
@@ -333,6 +345,9 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
#ifdef HAVE_TIDAL
ui_->tabs->AddTab(tidal_view_, "tidal", IconLoader::Load("tidal"), tr("Tidal"));
#endif
+#ifdef HAVE_QOBUZ
+ ui_->tabs->AddTab(qobuz_view_, "qobuz", IconLoader::Load("qobuz"), tr("Qobuz"));
+#endif
// Add the playing widget to the fancy tab widget
ui_->tabs->addBottomWidget(ui_->widget_playing);
@@ -644,6 +659,13 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
connect(this, SIGNAL(AuthorizationUrlReceived(QUrl)), tidalservice, SLOT(AuthorizationUrlReceived(QUrl)));
#endif
+#ifdef HAVE_QOBUZ
+ connect(qobuz_view_->artists_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
+ connect(qobuz_view_->albums_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
+ connect(qobuz_view_->songs_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
+ connect(qobuz_view_->search_view(), SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
+#endif
+
// Playlist menu
connect(playlist_menu_, SIGNAL(aboutToHide()), SLOT(PlaylistMenuHidden()));
playlist_play_pause_ = playlist_menu_->addAction(tr("Play"), this, SLOT(PlaylistPlay()));
@@ -829,6 +851,9 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
connect(app_->playlist_manager()->sequence(), SIGNAL(RepeatModeChanged(PlaylistSequence::RepeatMode)), osd_, SLOT(RepeatModeChanged(PlaylistSequence::RepeatMode)));
connect(app_->playlist_manager()->sequence(), SIGNAL(ShuffleModeChanged(PlaylistSequence::ShuffleMode)), osd_, SLOT(ShuffleModeChanged(PlaylistSequence::ShuffleMode)));
+ // Smart playlists
+ connect(smartplaylists_view_, SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
+
ScrobbleButtonVisibilityChanged(app_->scrobbler()->ScrobbleButton());
LoveButtonVisibilityChanged(app_->scrobbler()->LoveButton());
ScrobblingEnabledChanged(app_->scrobbler()->IsEnabled());
@@ -849,8 +874,8 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
restoreGeometry(settings_.value("geometry").toByteArray());
}
- if (!ui_->splitter->restoreState(settings_.value("splitter_state").toByteArray())) {
- ui_->splitter->setSizes(QList() << 250 << width() - 250);
+ if (!settings_.contains("splitter_state") || !ui_->splitter->restoreState(settings_.value("splitter_state").toByteArray())) {
+ ui_->splitter->setSizes(QList() << 20 << (width() - 20));
}
ui_->tabs->setCurrentIndex(settings_.value("current_tab", 1).toInt());
@@ -981,9 +1006,9 @@ void MainWindow::ReloadSettings() {
playing_widget_ = s.value("playing_widget", true).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_IfStopped).toInt());
- doubleclick_playlist_addmode_ = BehaviourSettingsPage::PlaylistAddBehaviour(s.value("doubleclick_playlist_addmode", BehaviourSettingsPage::PlaylistAddBehaviour_Play).toInt());
- menu_playmode_ = BehaviourSettingsPage::PlayBehaviour(s.value("menu_playmode", BehaviourSettingsPage::PlayBehaviour_IfStopped).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());
s.endGroup();
s.beginGroup(AppearanceSettingsPage::kSettingsGroup);
@@ -1039,6 +1064,16 @@ void MainWindow::ReloadSettings() {
ui_->tabs->DisableTab(tidal_view_);
#endif
+#ifdef HAVE_QOBUZ
+ s.beginGroup(QobuzSettingsPage::kSettingsGroup);
+ bool enable_qobuz = s.value("enabled", false).toBool();
+ s.endGroup();
+ if (enable_qobuz)
+ ui_->tabs->EnableTab(qobuz_view_);
+ else
+ ui_->tabs->DisableTab(qobuz_view_);
+#endif
+
ui_->tabs->ReloadSettings();
}
@@ -1061,6 +1096,7 @@ void MainWindow::ReloadAllSettings() {
file_view_->ReloadSettings();
queue_view_->ReloadSettings();
playlist_list_->ReloadSettings();
+ smartplaylists_view_->ReloadSettings();
app_->cover_providers()->ReloadSettings();
app_->lyrics_providers()->ReloadSettings();
#ifdef HAVE_SUBSONIC
@@ -1069,6 +1105,9 @@ void MainWindow::ReloadAllSettings() {
#ifdef HAVE_TIDAL
tidal_view_->ReloadSettings();
#endif
+#ifdef HAVE_QOBUZ
+ qobuz_view_->ReloadSettings();
+#endif
}
diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h
index 770b6d30..7398a412 100644
--- a/src/core/mainwindow.h
+++ b/src/core/mainwindow.h
@@ -93,6 +93,7 @@ class TranscodeDialog;
class Ui_MainWindow;
class InternetSongsView;
class InternetTabsView;
+class SmartPlaylistsViewContainer;
#ifdef Q_OS_WIN
class Windows7ThumbBar;
#endif
@@ -325,8 +326,11 @@ class MainWindow : public QMainWindow, public PlatformInterface {
std::unique_ptr track_selection_dialog_;
PlaylistItemList autocomplete_tag_items_;
+ SmartPlaylistsViewContainer *smartplaylists_view_;
+
InternetSongsView *subsonic_view_;
InternetTabsView *tidal_view_;
+ InternetTabsView *qobuz_view_;
LastFMImportDialog *lastfm_import_dialog_;
diff --git a/src/core/metatypes.cpp b/src/core/metatypes.cpp
index aaa54986..46a00ff2 100644
--- a/src/core/metatypes.cpp
+++ b/src/core/metatypes.cpp
@@ -69,6 +69,8 @@
#include "internet/internetsearchview.h"
+#include "smartplaylists/playlistgenerator_fwd.h"
+
void RegisterMetaTypes() {
qRegisterMetaType("const char*");
@@ -136,4 +138,6 @@ void RegisterMetaTypes() {
qRegisterMetaType("InternetSearchView::ResultList");
qRegisterMetaType("InternetSearchView::Result");
+ qRegisterMetaType("PlaylistGeneratorPtr");
+
}
diff --git a/src/core/song.cpp b/src/core/song.cpp
index 9b8c9a65..130c75d9 100644
--- a/src/core/song.cpp
+++ b/src/core/song.cpp
@@ -125,6 +125,8 @@ const QStringList Song::kColumns = QStringList() << "title"
<< "cue_path"
+ << "rating"
+
;
const QString Song::kColumnSpec = Song::kColumns.join(", ");
@@ -217,6 +219,8 @@ struct Song::Private : public QSharedData {
QString cue_path_; // If the song has a CUE, this contains it's path.
+ float rating_; // Database rating, not read from tags.
+
QUrl stream_url_; // Temporary stream url set by url handler.
QImage image_; // Album Cover image set by album cover loader.
bool init_from_file_; // Whether this song was loaded from a file using taglib.
@@ -259,6 +263,8 @@ Song::Private::Private(Song::Source source)
compilation_on_(false),
compilation_off_(false),
+ rating_(-1),
+
init_from_file_(false),
suspicious_tags_(false)
@@ -347,6 +353,8 @@ const QImage &Song::image() const { return d->image_; }
const QString &Song::cue_path() const { return d->cue_path_; }
bool Song::has_cue() const { return !d->cue_path_.isEmpty(); }
+float Song::rating() const { return d->rating_; }
+
bool Song::is_collection_song() const { return d->source_ == Source_Collection; }
bool Song::is_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); }
bool Song::is_stream() const { return d->source_ == Source_Stream || d->source_ == Source_Tidal || d->source_ == Source_Subsonic || d->source_ == Source_Qobuz; }
@@ -444,6 +452,8 @@ void Song::set_art_automatic(const QUrl &v) { d->art_automatic_ = v; }
void Song::set_art_manual(const QUrl &v) { d->art_manual_ = v; }
void Song::set_cue_path(const QString &v) { d->cue_path_ = v; }
+void Song::set_rating(float v) { d->rating_ = v; }
+
void Song::set_stream_url(const QUrl &v) { d->stream_url_ = v; }
void Song::set_image(const QImage &i) { d->image_ = i; }
@@ -1000,6 +1010,10 @@ void Song::InitFromQuery(const SqlRow &q, bool reliable_metadata, int col) {
d->cue_path_ = tostr(x);
}
+ else if (Song::kColumns.value(i) == "rating") {
+ d->rating_ = tofloat(x);
+ }
+
else {
qLog(Error) << "Forgot to handle" << Song::kColumns.value(i);
}
@@ -1363,6 +1377,8 @@ void Song::BindToQuery(QSqlQuery *query) const {
query->bindValue(":cue_path", d->cue_path_);
+ query->bindValue(":rating", intval(d->rating_));
+
#undef intval
#undef notnullintval
#undef strval
@@ -1441,6 +1457,16 @@ QString Song::SampleRateBitDepthToText() const {
}
+QString Song::PrettyRating() const {
+
+ float rating = d->rating_;
+
+ if (rating == -1.0f) return "0";
+
+ return QString::number(static_cast(rating * 100));
+
+}
+
bool Song::IsMetadataEqual(const Song &other) const {
return d->title_ == other.d->title_ &&
@@ -1547,5 +1573,6 @@ void Song::MergeUserSetData(const Song &other) {
set_art_manual(other.art_manual());
set_compilation_on(other.compilation_on());
set_compilation_off(other.compilation_off());
+ set_rating(other.rating());
}
diff --git a/src/core/song.h b/src/core/song.h
index 9ee81917..a34adbf7 100644
--- a/src/core/song.h
+++ b/src/core/song.h
@@ -245,6 +245,8 @@ class Song {
const QString &cue_path() const;
bool has_cue() const;
+ float rating() const;
+
const QString &effective_album() const;
int effective_originalyear() const;
const QString &effective_albumartist() const;
@@ -288,6 +290,8 @@ class Song {
QString SampleRateBitDepthToText() const;
+ QString PrettyRating() const;
+
// Setters
bool IsEditable() const;
@@ -346,6 +350,8 @@ class Song {
void set_cue_path(const QString &v);
+ void set_rating(const float v);
+
void set_stream_url(const QUrl &v);
void set_image(const QImage &i);
diff --git a/src/covermanager/qobuzcoverprovider.cpp b/src/covermanager/qobuzcoverprovider.cpp
index e9f7b65d..dc15d4a7 100644
--- a/src/covermanager/qobuzcoverprovider.cpp
+++ b/src/covermanager/qobuzcoverprovider.cpp
@@ -37,40 +37,24 @@
#include
#include
#include
-#include
#include
#include "core/application.h"
#include "core/network.h"
#include "core/logging.h"
#include "core/song.h"
-#include "core/utilities.h"
-#include "dialogs/userpassdialog.h"
+#include "internet/internetservices.h"
+#include "qobuz/qobuzservice.h"
+#include "qobuz/qobuzbaserequest.h"
#include "albumcoverfetcher.h"
#include "jsoncoverprovider.h"
#include "qobuzcoverprovider.h"
-const char *QobuzCoverProvider::kSettingsGroup = "Qobuz";
-const char *QobuzCoverProvider::kAuthUrl = "https://www.qobuz.com/api.json/0.2/user/login";
-const char *QobuzCoverProvider::kApiUrl = "https://www.qobuz.com/api.json/0.2";
-const char *QobuzCoverProvider::kAppID = "OTQyODUyNTY3";
const int QobuzCoverProvider::kLimit = 10;
-QobuzCoverProvider::QobuzCoverProvider(Application *app, QObject *parent) : JsonCoverProvider("Qobuz", true, true, 2.0, true, true, app, parent), network_(new NetworkAccessManager(this)) {
-
- QSettings s;
- s.beginGroup(kSettingsGroup);
- username_ = s.value("username").toString();
- QByteArray password = s.value("password").toByteArray();
- if (password.isEmpty()) password_.clear();
- else password_ = QString::fromUtf8(QByteArray::fromBase64(password));
- user_auth_token_ = s.value("user_auth_token").toString();
- user_id_ = s.value("user_id").toLongLong();
- credential_id_ = s.value("credential_id").toLongLong();
- device_id_ = s.value("device_id").toString();
- s.endGroup();
-
-}
+QobuzCoverProvider::QobuzCoverProvider(Application *app, QObject *parent) : JsonCoverProvider("Qobuz", true, true, 2.0, true, true, app, parent),
+ service_(app->internet_services()->Service()),
+ network_(new NetworkAccessManager(this)) {}
QobuzCoverProvider::~QobuzCoverProvider() {
@@ -83,221 +67,6 @@ QobuzCoverProvider::~QobuzCoverProvider() {
}
-void QobuzCoverProvider::Authenticate() {
-
- login_errors_.clear();
-
- if (username_.isEmpty() || password_.isEmpty()) {
- UserPassDialog dialog;
- if (dialog.exec() == QDialog::Rejected || dialog.username().isEmpty() || dialog.password().isEmpty()) {
- AuthError(tr("Missing username and password."));
- return;
- }
- username_ = dialog.username();
- password_ = dialog.password();
- }
-
- const ParamList params = ParamList() << Param("app_id", QString::fromUtf8(QByteArray::fromBase64(kAppID)))
- << Param("username", username_)
- << Param("password", password_)
- << Param("device_manufacturer_id", Utilities::MacAddress());
-
- QUrlQuery url_query;
- for (const Param ¶m : params) {
- url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
- }
-
- QUrl url(kAuthUrl);
- QNetworkRequest req(url);
-#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
- req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
-#else
- req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
-#endif
- req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
-
- QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
- QNetworkReply *reply = network_->post(req, query);
- replies_ << reply;
- connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleLoginSSLErrors(QList)));
- connect(reply, &QNetworkReply::finished, [=] { HandleAuthReply(reply); });
-
-}
-
-void QobuzCoverProvider::HandleAuthReply(QNetworkReply *reply) {
-
- if (!replies_.contains(reply)) return;
- replies_.removeAll(reply);
- disconnect(reply, nullptr, this, nullptr);
- reply->deleteLater();
-
- if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
- if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
- // This is a network error, there is nothing more to do.
- AuthError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
- return;
- }
- else {
- // See if there is Json data containing "status", "code" and "message" - then use that instead.
- QByteArray data(reply->readAll());
- QJsonParseError json_error;
- QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
- if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
- QJsonObject json_obj = json_doc.object();
- if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("code") && json_obj.contains("message")) {
- QString status = json_obj["status"].toString();
- int code = json_obj["code"].toInt();
- QString message = json_obj["message"].toString();
- login_errors_ << QString("%1 (%2)").arg(message).arg(code);
- if (code == 401) {
- username_.clear();
- password_.clear();
- }
- }
- }
- if (login_errors_.isEmpty()) {
- if (reply->error() != QNetworkReply::NoError) {
- login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
- if(reply->error() == QNetworkReply::AuthenticationRequiredError) {
- username_.clear();
- password_.clear();
- }
- }
- else {
- login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
- }
- }
- AuthError();
- return;
- }
- }
-
- login_errors_.clear();
-
- QByteArray data = reply->readAll();
- QJsonParseError json_error;
- QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
-
- if (json_error.error != QJsonParseError::NoError) {
- AuthError("Authentication reply from server missing Json data.");
- return;
- }
-
- if (json_doc.isEmpty()) {
- AuthError("Authentication reply from server has empty Json document.");
- return;
- }
-
- if (!json_doc.isObject()) {
- AuthError("Authentication reply from server has Json document that is not an object.", json_doc);
- return;
- }
-
- QJsonObject json_obj = json_doc.object();
- if (json_obj.isEmpty()) {
- AuthError("Authentication reply from server has empty Json object.", json_doc);
- return;
- }
-
- if (!json_obj.contains("user_auth_token")) {
- AuthError("Authentication reply from server is missing user_auth_token", json_obj);
- return;
- }
- user_auth_token_ = json_obj["user_auth_token"].toString();
-
- if (!json_obj.contains("user")) {
- AuthError("Authentication reply from server is missing user", json_obj);
- return;
- }
- QJsonValue json_user = json_obj["user"];
- if (!json_user.isObject()) {
- AuthError("Authentication reply user is not a object", json_obj);
- return;
- }
- QJsonObject json_obj_user = json_user.toObject();
-
- if (!json_obj_user.contains("id")) {
- AuthError("Authentication reply from server is missing user id", json_obj_user);
- return;
- }
- user_id_ = json_obj_user["id"].toVariant().toLongLong();
-
- if (!json_obj_user.contains("device")) {
- AuthError("Authentication reply from server is missing user device", json_obj_user);
- return;
- }
- QJsonValue json_device = json_obj_user["device"];
- if (!json_device.isObject()) {
- AuthError("Authentication reply from server user device is not a object", json_device);
- return;
- }
- QJsonObject json_obj_device = json_device.toObject();
-
- if (!json_obj_device.contains("device_manufacturer_id")) {
- AuthError("Authentication reply from server device is missing device_manufacturer_id", json_obj_device);
- return;
- }
- device_id_ = json_obj_device["device_manufacturer_id"].toString();
-
- if (!json_obj_user.contains("credential")) {
- AuthError("Authentication reply from server is missing user credential", json_obj_user);
- return;
- }
- QJsonValue json_credential = json_obj_user["credential"];
- if (!json_credential.isObject()) {
- AuthError("Authentication reply from serve userr credential is not a object", json_device);
- return;
- }
- QJsonObject json_obj_credential = json_credential.toObject();
-
- if (!json_obj_credential.contains("id")) {
- AuthError("Authentication reply user credential from server is missing user credential id", json_obj_device);
- return;
- }
- //credential_id_ = json_obj_credential["id"].toInt();
-
- QSettings s;
- s.beginGroup(kSettingsGroup);
- s.setValue("username", username_);
- s.setValue("password", password_.toUtf8().toBase64());
- s.setValue("user_auth_token", user_auth_token_);
- s.setValue("user_id", user_id_);
- s.setValue("credential_id", credential_id_);
- s.setValue("device_id", device_id_);
- s.endGroup();
-
- qLog(Debug) << "Qobuz: Login successful" << "user id" << user_id_ << "user auth token" << user_auth_token_ << "device id" << device_id_;
-
- emit AuthenticationComplete(true);
- emit AuthenticationSuccess();
-
-}
-
-void QobuzCoverProvider::Deauthenticate() {
-
- user_auth_token_.clear();
- user_id_ = 0;
- credential_id_ = 0;
- device_id_.clear();
-
- QSettings s;
- s.beginGroup(kSettingsGroup);
- s.remove("user_auth_token");
- s.remove("user_id");
- s.remove("credential_id");
- s.remove("device_id");
- s.endGroup();
-
-}
-
-void QobuzCoverProvider::HandleLoginSSLErrors(QList ssl_errors) {
-
- for (const QSslError &ssl_error : ssl_errors) {
- login_errors_ += ssl_error.errorString();
- }
-
-}
-
bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
if (artist.isEmpty() && album.isEmpty() && title.isEmpty()) return false;
@@ -319,7 +88,7 @@ bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album
ParamList params = ParamList() << Param("query", query)
<< Param("limit", QString::number(kLimit))
- << Param("app_id", QString::fromUtf8(QByteArray::fromBase64(kAppID)));
+ << Param("app_id", service_->app_id().toUtf8());
std::sort(params.begin(), params.end());
@@ -328,7 +97,7 @@ bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
}
- QUrl url(kApiUrl + QString("/") + resource);
+ QUrl url(QobuzBaseRequest::kApiUrl + QString("/") + resource);
url.setQuery(url_query);
QNetworkRequest req(url);
@@ -338,7 +107,7 @@ bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
#endif
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
- req.setRawHeader("X-App-Id", kAppID);
+ req.setRawHeader("X-App-Id", service_->app_id().toUtf8());
req.setRawHeader("X-User-Auth-Token", user_auth_token_.toUtf8());
QNetworkReply *reply = network_->get(req);
replies_ << reply;
@@ -517,20 +286,6 @@ void QobuzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
}
-void QobuzCoverProvider::AuthError(const QString &error, const QVariant &debug) {
-
- if (!error.isEmpty()) login_errors_ << error;
-
- for (const QString &e : login_errors_) Error(e);
- if (debug.isValid()) qLog(Debug) << debug;
-
- emit AuthenticationFailure(login_errors_);
- emit AuthenticationComplete(false, login_errors_);
-
- login_errors_.clear();
-
-}
-
void QobuzCoverProvider::Error(const QString &error, const QVariant &debug) {
qLog(Error) << "Qobuz:" << error;
diff --git a/src/covermanager/qobuzcoverprovider.h b/src/covermanager/qobuzcoverprovider.h
index a2e92e22..8b161539 100644
--- a/src/covermanager/qobuzcoverprovider.h
+++ b/src/covermanager/qobuzcoverprovider.h
@@ -31,10 +31,12 @@
#include
#include "jsoncoverprovider.h"
+#include "qobuz/qobuzservice.h"
class QNetworkAccessManager;
class QNetworkReply;
class Application;
+class QobuzService;
class QobuzCoverProvider : public JsonCoverProvider {
Q_OBJECT
@@ -46,32 +48,23 @@ class QobuzCoverProvider : public JsonCoverProvider {
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) override;
void CancelSearch(const int id) override;
- void Authenticate() override;
- void Deauthenticate() override;
- bool IsAuthenticated() const override { return !user_auth_token_.isEmpty(); }
-
- private slots:
- void HandleLoginSSLErrors(QList ssl_errors);
- void HandleAuthReply(QNetworkReply *reply);
+ bool IsAuthenticated() const override { return service_ && service_->authenticated(); }
+ void Deauthenticate() override { if (service_) service_->Logout(); }
private slots:
void HandleSearchReply(QNetworkReply *reply, const int id);
private:
QByteArray GetReplyData(QNetworkReply *reply);
- void AuthError(const QString &error = QString(), const QVariant &debug = QVariant());
void Error(const QString &error, const QVariant &debug = QVariant()) override;
private:
typedef QPair Param;
typedef QList ParamList;
- static const char *kSettingsGroup;
- static const char *kAuthUrl;
- static const char *kApiUrl;
- static const char *kAppID;
static const int kLimit;
+ QobuzService *service_;
QNetworkAccessManager *network_;
QList replies_;
diff --git a/src/device/devicedatabasebackend.cpp b/src/device/devicedatabasebackend.cpp
index abcb5663..0c689e43 100644
--- a/src/device/devicedatabasebackend.cpp
+++ b/src/device/devicedatabasebackend.cpp
@@ -37,7 +37,7 @@
#include "core/scopedtransaction.h"
#include "devicedatabasebackend.h"
-const int DeviceDatabaseBackend::kDeviceSchemaVersion = 1;
+const int DeviceDatabaseBackend::kDeviceSchemaVersion = 2;
DeviceDatabaseBackend::DeviceDatabaseBackend(QObject *parent) :
QObject(parent),
diff --git a/src/playlist/dynamicplaylistcontrols.cpp b/src/playlist/dynamicplaylistcontrols.cpp
new file mode 100644
index 00000000..ef0b9de6
--- /dev/null
+++ b/src/playlist/dynamicplaylistcontrols.cpp
@@ -0,0 +1,31 @@
+/* This file is part of Clementine.
+ Copyright 2010, David Sansome
+
+ Clementine 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.
+
+ Clementine 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 Clementine. If not, see .
+*/
+
+#include "dynamicplaylistcontrols.h"
+#include "ui_dynamicplaylistcontrols.h"
+
+DynamicPlaylistControls::DynamicPlaylistControls(QWidget *parent)
+ : QWidget(parent), ui_(new Ui_DynamicPlaylistControls) {
+
+ ui_->setupUi(this);
+
+ connect(ui_->expand, SIGNAL(clicked()), SIGNAL(Expand()));
+ connect(ui_->repopulate, SIGNAL(clicked()), SIGNAL(Repopulate()));
+ connect(ui_->off, SIGNAL(clicked()), SIGNAL(TurnOff()));
+}
+
+DynamicPlaylistControls::~DynamicPlaylistControls() { delete ui_; }
diff --git a/src/playlist/dynamicplaylistcontrols.h b/src/playlist/dynamicplaylistcontrols.h
new file mode 100644
index 00000000..45fa1d8e
--- /dev/null
+++ b/src/playlist/dynamicplaylistcontrols.h
@@ -0,0 +1,41 @@
+/* This file is part of Clementine.
+ Copyright 2010, David Sansome
+
+ Clementine 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.
+
+ Clementine 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 Clementine. If not, see .
+*/
+
+#ifndef DYNAMICPLAYLISTCONTROLS_H
+#define DYNAMICPLAYLISTCONTROLS_H
+
+#include
+
+class Ui_DynamicPlaylistControls;
+
+class DynamicPlaylistControls : public QWidget {
+ Q_OBJECT
+
+ public:
+ DynamicPlaylistControls(QWidget *parent = nullptr);
+ ~DynamicPlaylistControls();
+
+ signals:
+ void Expand();
+ void Repopulate();
+ void TurnOff();
+
+ private:
+ Ui_DynamicPlaylistControls* ui_;
+};
+
+#endif // DYNAMICPLAYLISTCONTROLS_H
diff --git a/src/playlist/dynamicplaylistcontrols.ui b/src/playlist/dynamicplaylistcontrols.ui
new file mode 100644
index 00000000..3ad3e8cb
--- /dev/null
+++ b/src/playlist/dynamicplaylistcontrols.ui
@@ -0,0 +1,99 @@
+
+
+ DynamicPlaylistControls
+
+
+
+ 0
+ 0
+ 483
+ 54
+
+
+
+ #container {
+ background: rgba(200, 200, 200, 50%);
+ border-radius: 10px;
+ border: 1px solid rgba(200, 200, 200, 75%);
+}
+
+#label1 {
+ font-weight: bold;
+}
+
+#label2 {
+ font-size: 7.5pt;
+}
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
-
+
+
+ 0
+
+
-
+
+
+ Dynamic mode is on
+
+
+
+ -
+
+
+ New tracks will be added automatically.
+
+
+
+
+
+ -
+
+
+ Expand
+
+
+
+ -
+
+
+ Repopulate
+
+
+
+ -
+
+
+ Turn off
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/playlist/playlist.cpp b/src/playlist/playlist.cpp
index cd4afed9..140728c8 100644
--- a/src/playlist/playlist.cpp
+++ b/src/playlist/playlist.cpp
@@ -84,6 +84,10 @@
#include "songplaylistitem.h"
#include "tagreadermessages.pb.h"
+#include "smartplaylists/playlistgenerator.h"
+#include "smartplaylists/playlistgeneratorinserter.h"
+#include "smartplaylists/playlistgeneratormimedata.h"
+
#include "internet/internetplaylistitem.h"
#include "internet/internetsongmimedata.h"
@@ -271,6 +275,9 @@ QVariant Playlist::data(const QModelIndex &idx, int role) const {
case Role_QueuePosition:
return queue_->PositionOf(idx);
+ case Role_CanSetRating:
+ return idx.column() == Column_Rating && items_[idx.row()]->IsLocalCollectionItem() && items_[idx.row()]->Metadata().id() != -1;
+
case Qt::EditRole:
case Qt::ToolTipRole:
case Qt::DisplayRole: {
@@ -314,6 +321,8 @@ QVariant Playlist::data(const QModelIndex &idx, int role) const {
case Column_Source: return song.source();
+ case Column_Rating: return song.rating();
+
}
return QVariant();
@@ -331,6 +340,9 @@ QVariant Playlist::data(const QModelIndex &idx, int role) const {
if (items_[idx.row()]->HasCurrentForegroundColor()) {
return QBrush(items_[idx.row()]->GetCurrentForegroundColor());
}
+ if (idx.row() < dynamic_history_length()) {
+ return QBrush(kDynamicHistoryColor);
+ }
return QVariant();
@@ -634,6 +646,35 @@ void Playlist::set_current_row(const int i, const AutoScroll autoscroll, const b
InformOfCurrentSongChange(autoscroll);
}
+ // The structure of a dynamic playlist is as follows:
+ // history - active song - future
+ // We have to ensure that this invariant is maintained.
+ if (dynamic_playlist_ && current_item_index_.isValid()) {
+
+ // When advancing to the next track
+ if (i > old_current_item_index.row()) {
+ // Move the new item one position ahead of the last item in the history.
+ MoveItemWithoutUndo(current_item_index_.row(), dynamic_history_length());
+
+ // Compute the number of new items that have to be inserted. This is not
+ // necessarily 1 because the user might have added or removed items
+ // manually. Note that the future excludes the current item.
+ const int count = dynamic_history_length() + 1 + dynamic_playlist_->GetDynamicFuture() - items_.count();
+ if (count > 0) {
+ InsertDynamicItems(count);
+ }
+
+ // Shrink the history, again this is not necessarily by 1, because the
+ // user might have moved items by hand.
+ const int remove_count = dynamic_history_length() - dynamic_playlist_->GetDynamicHistory();
+ if (0 < remove_count) RemoveItemsWithoutUndo(0, remove_count);
+ }
+
+ // the above actions make all commands on the undo stack invalid, so we
+ // better clear it.
+ undo_stack_->clear();
+ }
+
if (current_item_index_.isValid()) {
last_played_item_index_ = current_item_index_;
Save();
@@ -643,6 +684,16 @@ void Playlist::set_current_row(const int i, const AutoScroll autoscroll, const b
}
+void Playlist::InsertDynamicItems(const int count) {
+
+ PlaylistGeneratorInserter* inserter = new PlaylistGeneratorInserter(task_manager_, collection_, this);
+ connect(inserter, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
+ connect(inserter, SIGNAL(PlayRequested(QModelIndex)), SIGNAL(PlayRequested(QModelIndex)));
+
+ inserter->Load(this, -1, false, false, false, dynamic_playlist_, count);
+
+}
+
Qt::ItemFlags Playlist::flags(const QModelIndex &idx) const {
if (idx.isValid()) {
@@ -697,6 +748,9 @@ bool Playlist::dropMimeData(const QMimeData *data, Qt::DropAction action, int ro
else if (const InternetSongMimeData* internet_song_data = qobject_cast(data)) {
InsertInternetItems(internet_song_data->service, internet_song_data->songs, row, play_now, enqueue_now, enqueue_next_now);
}
+ else if (const PlaylistGeneratorMimeData *generator_data = qobject_cast(data)) {
+ InsertSmartPlaylist(generator_data->generator_, row, play_now, enqueue_now, enqueue_next_now);
+ }
else if (data->hasFormat(kRowsMimetype)) {
// Dragged from the playlist
// Rearranging it is tricky...
@@ -768,6 +822,34 @@ void Playlist::InsertUrls(const QList &urls, const int pos, const bool pla
}
+void Playlist::InsertSmartPlaylist(PlaylistGeneratorPtr generator, const int pos, const bool play_now, const bool enqueue, const bool enqueue_next) {
+
+ // Hack: If the generator hasn't got a collection set then use the main one
+ if (!generator->collection()) {
+ generator->set_collection(collection_);
+ }
+
+ PlaylistGeneratorInserter *inserter = new PlaylistGeneratorInserter(task_manager_, collection_, this);
+ connect(inserter, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
+
+ inserter->Load(this, pos, play_now, enqueue, enqueue_next, generator);
+
+ if (generator->is_dynamic()) {
+ TurnOnDynamicPlaylist(generator);
+ }
+
+}
+
+void Playlist::TurnOnDynamicPlaylist(PlaylistGeneratorPtr gen) {
+
+ dynamic_playlist_ = gen;
+ playlist_sequence_->SetUsingDynamicPlaylist(true);
+ ShuffleModeChanged(PlaylistSequence::Shuffle_Off);
+ emit DynamicModeChanged(true);
+ Save();
+
+}
+
void Playlist::MoveItemWithoutUndo(const int source, const int dest) {
MoveItemsWithoutUndo(QList() << source, dest);
}
@@ -1139,6 +1221,9 @@ bool Playlist::CompareItems(const int column, const Qt::SortOrder order, std::sh
case Column_Comment: strcmp(comment);
case Column_Source: cmp(source);
+
+ case Column_Rating: cmp(rating);
+
default: qLog(Error) << "No such column" << column;
}
@@ -1196,6 +1281,7 @@ QString Playlist::column_name(Column column) {
case Column_Comment: return tr("Comment");
case Column_Source: return tr("Source");
case Column_Mood: return tr("Mood");
+ case Column_Rating: return tr("Rating");
default: qLog(Error) << "No such column" << column;;
}
return "";
@@ -1226,6 +1312,9 @@ void Playlist::sort(int column, Qt::SortOrder order) {
PlaylistItemList new_items(items_);
PlaylistItemList::iterator begin = new_items.begin();
+ if (dynamic_playlist_ && current_item_index_.isValid())
+ begin += current_item_index_.row() + 1;
+
if (column == Column_Album) {
// When sorting by album, also take into account discs and tracks.
std::stable_sort(begin, new_items.end(), std::bind(&Playlist::CompareItems, Column_Track, order, _1, _2));
@@ -1291,7 +1380,7 @@ void Playlist::Save() const {
if (!backend_ || is_loading_) return;
- backend_->SavePlaylistAsync(id_, items_, last_played_row());
+ backend_->SavePlaylistAsync(id_, items_, last_played_row(), dynamic_playlist_);
}
@@ -1334,6 +1423,22 @@ void Playlist::ItemsLoaded(QFuture future) {
// The newly loaded list of items might be shorter than it was before so look out for a bad last_played index
last_played_item_index_ = p.last_played == -1 || p.last_played >= rowCount() ? QModelIndex() : index(p.last_played);
+ if (p.dynamic_type == PlaylistGenerator::Type_Query) {
+ PlaylistGeneratorPtr gen = PlaylistGenerator::Create(p.dynamic_type);
+ if (gen) {
+
+ CollectionBackend *backend = nullptr;
+ if (p.dynamic_backend == collection_->songs_table()) backend = collection_;
+
+ if (backend) {
+ gen->set_collection(backend);
+ gen->Load(p.dynamic_data);
+ TurnOnDynamicPlaylist(gen);
+ }
+
+ }
+ }
+
emit RestoreFinished();
QSettings s;
@@ -1572,10 +1677,29 @@ void Playlist::Clear() {
undo_stack_->push(new PlaylistUndoCommands::RemoveItems(this, 0, count));
}
+ TurnOffDynamicPlaylist();
+
Save();
}
+void Playlist::RepopulateDynamicPlaylist() {
+
+ if (!dynamic_playlist_) return;
+
+ RemoveItemsNotInQueue();
+ InsertSmartPlaylist(dynamic_playlist_);
+
+}
+
+void Playlist::ExpandDynamicPlaylist() {
+
+ if (!dynamic_playlist_) return;
+
+ InsertDynamicItems(5);
+
+}
+
void Playlist::RemoveItemsNotInQueue() {
if (queue_->is_empty() && !current_item_index_.isValid()) {
@@ -1650,6 +1774,9 @@ void Playlist::Shuffle() {
begin = 1;
}
+ if (dynamic_playlist_ && current_item_index_.isValid())
+ begin += current_item_index_.row() + 1;
+
const int count = items_.count();
for (int i = begin; i < count; ++i) {
int new_pos = i + (rand() % (count - i));
@@ -2036,3 +2163,48 @@ void Playlist::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &
}
}
+
+int Playlist::dynamic_history_length() const {
+ return dynamic_playlist_ && last_played_item_index_.isValid() ? last_played_item_index_.row() + 1 : 0;
+}
+
+void Playlist::TurnOffDynamicPlaylist() {
+
+ dynamic_playlist_.reset();
+
+ if (playlist_sequence_) {
+ playlist_sequence_->SetUsingDynamicPlaylist(false);
+ ShuffleModeChanged(playlist_sequence_->shuffle_mode());
+ }
+
+ emit DynamicModeChanged(false);
+ Save();
+
+}
+
+void Playlist::RateSong(const QModelIndex &idx, const double rating) {
+
+ if (has_item_at(idx.row())) {
+ PlaylistItemPtr item = item_at(idx.row());
+ if (item && item->IsLocalCollectionItem() && item->Metadata().id() != -1) {
+ collection_->UpdateSongRatingAsync(item->Metadata().id(), rating);
+ }
+ }
+
+}
+
+void Playlist::RateSongs(const QModelIndexList &index_list, const double rating) {
+
+ QList id_list;
+ for (const QModelIndex &idx : index_list) {
+ const int row = idx.row();
+ if (has_item_at(row)) {
+ PlaylistItemPtr item = item_at(row);
+ if (item && item->IsLocalCollectionItem() && item->Metadata().id() != -1) {
+ id_list << item->Metadata().id();
+ }
+ }
+ }
+ collection_->UpdateSongsRatingAsync(id_list, rating);
+
+}
diff --git a/src/playlist/playlist.h b/src/playlist/playlist.h
index 4c5286ef..6037b913 100644
--- a/src/playlist/playlist.h
+++ b/src/playlist/playlist.h
@@ -46,6 +46,7 @@
#include "covermanager/albumcoverloaderresult.h"
#include "playlistitem.h"
#include "playlistsequence.h"
+#include "smartplaylists/playlistgenerator_fwd.h"
class QMimeData;
class QSortFilterProxyModel;
@@ -128,6 +129,7 @@ class Playlist : public QAbstractListModel {
Column_Grouping,
Column_Source,
Column_Mood,
+ Column_Rating,
ColumnCount
};
@@ -135,7 +137,8 @@ class Playlist : public QAbstractListModel {
Role_IsCurrent = Qt::UserRole + 1,
Role_IsPaused,
Role_StopAfter,
- Role_QueuePosition
+ Role_QueuePosition,
+ Role_CanSetRating,
};
enum Path {
@@ -203,6 +206,8 @@ class Playlist : public QAbstractListModel {
const QModelIndex current_index() const;
bool stop_after_current() const;
+ bool is_dynamic() const { return static_cast(dynamic_playlist_); }
+ int dynamic_history_length() const;
QString special_type() const { return special_type_; }
void set_special_type(const QString &v) { special_type_ = v; }
@@ -240,6 +245,7 @@ class Playlist : public QAbstractListModel {
void InsertSongs(const SongList &songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
void InsertSongsOrCollectionItems(const SongList &songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
void InsertInternetItems(InternetService* service, const SongList& songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
+ void InsertSmartPlaylist(PlaylistGeneratorPtr gen, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
void ReshuffleIndices();
@@ -284,6 +290,10 @@ class Playlist : public QAbstractListModel {
static bool ComparePathDepths(Qt::SortOrder, PlaylistItemPtr, PlaylistItemPtr);
+ // Changes rating of a song to the given value asynchronously
+ void RateSong(const QModelIndex &idx, const double rating);
+ void RateSongs(const QModelIndexList &index_list, const double rating);
+
public slots:
void set_current_row(const int i, const AutoScroll autoscroll = AutoScroll_Maybe, const bool is_stopping = false);
void Paused();
@@ -309,6 +319,10 @@ class Playlist : public QAbstractListModel {
// Removes items with given indices from the playlist. This operation is not undoable.
void RemoveItemsWithoutUndo(const QList &indicesIn);
+ void ExpandDynamicPlaylist();
+ void RepopulateDynamicPlaylist();
+ void TurnOffDynamicPlaylist();
+
signals:
void RestoreFinished();
void PlaylistLoaded();
@@ -349,6 +363,9 @@ class Playlist : public QAbstractListModel {
// Removes rows with given indices from this playlist.
bool removeRows(QList &rows);
+ void TurnOnDynamicPlaylist(PlaylistGeneratorPtr gen);
+ void InsertDynamicItems(const int count);
+
private slots:
void TracksAboutToBeDequeued(const QModelIndex&, const int begin, const int end);
void TracksDequeued();
@@ -412,6 +429,8 @@ class Playlist : public QAbstractListModel {
int editing_;
+ PlaylistGeneratorPtr dynamic_playlist_;
+
};
#endif // PLAYLIST_H
diff --git a/src/playlist/playlistbackend.cpp b/src/playlist/playlistbackend.cpp
index bec60579..563d7810 100644
--- a/src/playlist/playlistbackend.cpp
+++ b/src/playlist/playlistbackend.cpp
@@ -55,6 +55,7 @@
#include "songplaylistitem.h"
#include "playlistbackend.h"
#include "playlistparsers/cueparser.h"
+#include "smartplaylists/playlistgenerator.h"
using std::placeholders::_1;
@@ -121,7 +122,7 @@ PlaylistBackend::PlaylistList PlaylistBackend::GetPlaylists(GetPlaylistsFlags fl
}
QSqlQuery q(db);
- q.prepare("SELECT ROWID, name, last_played, special_type, ui_path, is_favorite FROM playlists " + condition + " ORDER BY ui_order");
+ q.prepare("SELECT ROWID, name, last_played, special_type, ui_path, is_favorite, dynamic_playlist_type, dynamic_playlist_data, dynamic_playlist_backend FROM playlists " + condition + " ORDER BY ui_order");
q.exec();
if (db_->CheckErrors(q)) return ret;
@@ -133,6 +134,9 @@ PlaylistBackend::PlaylistList PlaylistBackend::GetPlaylists(GetPlaylistsFlags fl
p.special_type = q.value(3).toString();
p.ui_path = q.value(4).toString();
p.favorite = q.value(5).toBool();
+ p.dynamic_type = PlaylistGenerator::Type(q.value(6).toInt());
+ p.dynamic_data = q.value(7).toByteArray();
+ p.dynamic_backend = q.value(8).toString();
ret << p;
}
@@ -146,7 +150,7 @@ PlaylistBackend::Playlist PlaylistBackend::GetPlaylist(int id) {
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
- q.prepare("SELECT ROWID, name, last_played, special_type, ui_path, is_favorite FROM playlists WHERE ROWID=:id");
+ q.prepare("SELECT ROWID, name, last_played, special_type, ui_path, is_favorite, dynamic_playlist_type, dynamic_playlist_data, dynamic_playlist_backend FROM playlists WHERE ROWID=:id");
q.bindValue(":id", id);
q.exec();
@@ -161,6 +165,9 @@ PlaylistBackend::Playlist PlaylistBackend::GetPlaylist(int id) {
p.special_type = q.value(3).toString();
p.ui_path = q.value(4).toString();
p.favorite = q.value(5).toBool();
+ p.dynamic_type = PlaylistGenerator::Type(q.value(6).toInt());
+ p.dynamic_data = q.value(7).toByteArray();
+ p.dynamic_backend = q.value(8).toString();
return p;
@@ -315,13 +322,13 @@ PlaylistItemPtr PlaylistBackend::RestoreCueData(PlaylistItemPtr item, std::share
}
-void PlaylistBackend::SavePlaylistAsync(int playlist, const PlaylistItemList &items, int last_played) {
+void PlaylistBackend::SavePlaylistAsync(int playlist, const PlaylistItemList &items, int last_played, PlaylistGeneratorPtr dynamic) {
- metaObject()->invokeMethod(this, "SavePlaylist", Qt::QueuedConnection, Q_ARG(int, playlist), Q_ARG(PlaylistItemList, items), Q_ARG(int, last_played));
+ metaObject()->invokeMethod(this, "SavePlaylist", Qt::QueuedConnection, Q_ARG(int, playlist), Q_ARG(PlaylistItemList, items), Q_ARG(int, last_played), Q_ARG(PlaylistGeneratorPtr, dynamic));
}
-void PlaylistBackend::SavePlaylist(int playlist, const PlaylistItemList &items, int last_played) {
+void PlaylistBackend::SavePlaylist(int playlist, const PlaylistItemList &items, int last_played, PlaylistGeneratorPtr dynamic) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
@@ -333,7 +340,7 @@ void PlaylistBackend::SavePlaylist(int playlist, const PlaylistItemList &items,
QSqlQuery insert(db);
insert.prepare("INSERT INTO playlist_items (playlist, type, collection_id, " + Song::kColumnSpec + ") VALUES (:playlist, :type, :collection_id, " + Song::kBindSpec + ")");
QSqlQuery update(db);
- update.prepare("UPDATE playlists SET last_played=:last_played WHERE ROWID=:playlist");
+ update.prepare("UPDATE playlists SET last_played=:last_played, dynamic_playlist_type=:dynamic_type, dynamic_playlist_data=:dynamic_data, dynamic_playlist_backend=:dynamic_backend WHERE ROWID=:playlist");
ScopedTransaction transaction(&db);
@@ -353,6 +360,16 @@ void PlaylistBackend::SavePlaylist(int playlist, const PlaylistItemList &items,
// Update the last played track number
update.bindValue(":last_played", last_played);
+ if (dynamic) {
+ update.bindValue(":dynamic_type", dynamic->type());
+ update.bindValue(":dynamic_data", dynamic->Save());
+ update.bindValue(":dynamic_backend", dynamic->collection()->songs_table());
+ }
+ else {
+ update.bindValue(":dynamic_type", 0);
+ update.bindValue(":dynamic_data", QByteArray());
+ update.bindValue(":dynamic_backend", QString());
+ }
update.bindValue(":playlist", playlist);
update.exec();
if (db_->CheckErrors(update)) return;
diff --git a/src/playlist/playlistbackend.h b/src/playlist/playlistbackend.h
index 4959b996..e1ef4d10 100644
--- a/src/playlist/playlistbackend.h
+++ b/src/playlist/playlistbackend.h
@@ -37,6 +37,7 @@
#include "core/song.h"
#include "collection/sqlrow.h"
#include "playlistitem.h"
+#include "smartplaylists/playlistgenerator.h"
class QThread;
class Application;
@@ -57,6 +58,9 @@ class PlaylistBackend : public QObject {
bool favorite;
int last_played;
QString special_type;
+ PlaylistGenerator::Type dynamic_type;
+ QString dynamic_backend;
+ QByteArray dynamic_data;
};
typedef QList PlaylistList;
@@ -77,7 +81,7 @@ class PlaylistBackend : public QObject {
void SetPlaylistUiPath(int id, const QString &path);
int CreatePlaylist(const QString &name, const QString &special_type);
- void SavePlaylistAsync(int playlist, const PlaylistItemList &items, int last_played);
+ void SavePlaylistAsync(int playlist, const PlaylistItemList &items, int last_played, PlaylistGeneratorPtr dynamic);
void RenamePlaylist(int id, const QString &new_name);
void FavoritePlaylist(int id, bool is_favorite);
void RemovePlaylist(int id);
@@ -86,7 +90,7 @@ class PlaylistBackend : public QObject {
public slots:
void Exit();
- void SavePlaylist(int playlist, const PlaylistItemList &items, int last_played);
+ void SavePlaylist(int playlist, const PlaylistItemList &items, int last_played, PlaylistGeneratorPtr dynamic);
signals:
void ExitFinished();
diff --git a/src/playlist/playlistdelegates.cpp b/src/playlist/playlistdelegates.cpp
index c25dbc8d..0c980209 100644
--- a/src/playlist/playlistdelegates.cpp
+++ b/src/playlist/playlistdelegates.cpp
@@ -493,3 +493,40 @@ void SongSourceDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op
painter->drawPixmap(draw_rect, pixmap);
}
+
+RatingItemDelegate::RatingItemDelegate(QObject *parent) : PlaylistDelegateBase(parent) {}
+
+void RatingItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &idx) const {
+
+ // Draw the background
+ option.widget->style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, option.widget);
+
+ // Don't draw anything else if the user can't set the rating of this item
+ if (!idx.data(Playlist::Role_CanSetRating).toBool()) return;
+
+ const bool hover = mouse_over_index_.isValid() && (mouse_over_index_ == idx || (selected_indexes_.contains(mouse_over_index_) && selected_indexes_.contains(idx)));
+
+ const double rating = (hover ? RatingPainter::RatingForPos(mouse_over_pos_, option.rect) : idx.data().toDouble());
+
+ painter_.Paint(painter, option.rect, rating);
+
+}
+
+QSize RatingItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &idx) const {
+
+ QSize size = PlaylistDelegateBase::sizeHint(option, idx);
+ size.setWidth(size.height() * RatingPainter::kStarCount);
+ return size;
+
+}
+
+QString RatingItemDelegate::displayText(const QVariant &value, const QLocale&) const {
+
+ if (value.isNull() || value.toDouble() <= 0) return QString();
+
+ // Round to the nearest 0.5
+ const double rating = float(int(value.toDouble() * RatingPainter::kStarCount * 2 + 0.5)) / 2;
+
+ return QString::number(rating, 'f', 1);
+
+}
diff --git a/src/playlist/playlistdelegates.h b/src/playlist/playlistdelegates.h
index 123ca3a0..1d29ec83 100644
--- a/src/playlist/playlistdelegates.h
+++ b/src/playlist/playlistdelegates.h
@@ -51,6 +51,7 @@
#include "playlist.h"
#include "core/song.h"
+#include "widgets/ratingwidget.h"
class CollectionBackend;
class Player;
@@ -185,4 +186,29 @@ class SongSourceDelegate : public PlaylistDelegateBase {
mutable QPixmapCache pixmap_cache_;
};
+class RatingItemDelegate : public PlaylistDelegateBase {
+ public:
+ RatingItemDelegate(QObject *parent);
+ void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &idx) const override;
+ QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &idx) const override;
+ QString displayText(const QVariant &value, const QLocale &locale) const override;
+
+ void set_mouse_over(const QModelIndex &idx, const QModelIndexList &selected_indexes, const QPoint &pos) {
+ mouse_over_index_ = idx;
+ selected_indexes_ = selected_indexes;
+ mouse_over_pos_ = pos;
+ }
+
+ void set_mouse_out() { mouse_over_index_ = QModelIndex(); }
+ bool is_mouse_over() const { return mouse_over_index_.isValid(); }
+ QModelIndex mouse_over_index() const { return mouse_over_index_; }
+
+ private:
+ RatingPainter painter_;
+
+ QModelIndex mouse_over_index_;
+ QPoint mouse_over_pos_;
+ QModelIndexList selected_indexes_;
+};
+
#endif // PLAYLISTDELEGATES_H
diff --git a/src/playlist/playlistheader.cpp b/src/playlist/playlistheader.cpp
index 4c4cdaef..afe70332 100644
--- a/src/playlist/playlistheader.cpp
+++ b/src/playlist/playlistheader.cpp
@@ -29,6 +29,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -36,25 +37,37 @@
#include "playlistheader.h"
#include "playlistview.h"
+#include "settings/playlistsettingspage.h"
+
PlaylistHeader::PlaylistHeader(Qt::Orientation orientation, PlaylistView *view, QWidget *parent)
: StretchHeaderView(orientation, parent),
view_(view),
- menu_(new QMenu(this)) {
+ menu_(new QMenu(this)),
+ action_hide_(nullptr),
+ action_reset_(nullptr),
+ action_stretch_(nullptr),
+ action_rating_lock_(nullptr),
+ action_align_left_(nullptr),
+ action_align_center_(nullptr),
+ action_align_right_(nullptr)
+ {
- hide_action_ = menu_->addAction(tr("&Hide..."), this, SLOT(HideCurrent()));
- stretch_action_ = menu_->addAction(tr("&Stretch columns to fit window"), this, SLOT(ToggleStretchEnabled()));
- reset_action_ = menu_->addAction(tr("&Reset columns to default"), this, SLOT(ResetColumns()));
+ action_hide_ = menu_->addAction(tr("&Hide..."), this, SLOT(HideCurrent()));
+ action_stretch_ = menu_->addAction(tr("&Stretch columns to fit window"), this, SLOT(ToggleStretchEnabled()));
+ action_reset_ = menu_->addAction(tr("&Reset columns to default"), this, SLOT(ResetColumns()));
+ action_rating_lock_ = menu_->addAction(tr("&Lock rating"), this, SLOT(ToggleRatingEditStatus()));
+ action_rating_lock_->setCheckable(true);
menu_->addSeparator();
QMenu *align_menu = new QMenu(tr("&Align text"), this);
QActionGroup *align_group = new QActionGroup(this);
- align_left_action_ = new QAction(tr("&Left"), align_group);
- align_center_action_ = new QAction(tr("&Center"), align_group);
- align_right_action_ = new QAction(tr("&Right"), align_group);
+ action_align_left_ = new QAction(tr("&Left"), align_group);
+ action_align_center_ = new QAction(tr("&Center"), align_group);
+ action_align_right_ = new QAction(tr("&Right"), align_group);
- align_left_action_->setCheckable(true);
- align_center_action_->setCheckable(true);
- align_right_action_->setCheckable(true);
+ action_align_left_->setCheckable(true);
+ action_align_center_->setCheckable(true);
+ action_align_right_->setCheckable(true);
align_menu->addActions(align_group->actions());
connect(align_group, SIGNAL(triggered(QAction*)), SLOT(SetColumnAlignment(QAction*)));
@@ -62,10 +75,15 @@ PlaylistHeader::PlaylistHeader(Qt::Orientation orientation, PlaylistView *view,
menu_->addMenu(align_menu);
menu_->addSeparator();
- stretch_action_->setCheckable(true);
- stretch_action_->setChecked(is_stretch_enabled());
+ action_stretch_->setCheckable(true);
+ action_stretch_->setChecked(is_stretch_enabled());
- connect(this, SIGNAL(StretchEnabledChanged(bool)), stretch_action_, SLOT(setChecked(bool)));
+ connect(this, SIGNAL(StretchEnabledChanged(bool)), action_stretch_, SLOT(setChecked(bool)));
+
+ QSettings s;
+ s.beginGroup(PlaylistSettingsPage::kSettingsGroup);
+ action_rating_lock_->setChecked(s.value("rating_locked", false).toBool());
+ s.endGroup();
}
@@ -74,17 +92,21 @@ void PlaylistHeader::contextMenuEvent(QContextMenuEvent *e) {
menu_section_ = logicalIndexAt(e->pos());
if (menu_section_ == -1 || (menu_section_ == logicalIndex(0) && logicalIndex(1) == -1))
- hide_action_->setVisible(false);
+ action_hide_->setVisible(false);
else {
- hide_action_->setVisible(true);
+ action_hide_->setVisible(true);
QString title(model()->headerData(menu_section_, Qt::Horizontal).toString());
- hide_action_->setText(tr("&Hide %1").arg(title));
+ action_hide_->setText(tr("&Hide %1").arg(title));
Qt::Alignment alignment = view_->column_alignment(menu_section_);
- if (alignment & Qt::AlignLeft) align_left_action_->setChecked(true);
- else if (alignment & Qt::AlignHCenter) align_center_action_->setChecked(true);
- else if (alignment & Qt::AlignRight) align_right_action_->setChecked(true);
+ if (alignment & Qt::AlignLeft) action_align_left_->setChecked(true);
+ else if (alignment & Qt::AlignHCenter) action_align_center_->setChecked(true);
+ else if (alignment & Qt::AlignRight) action_align_right_->setChecked(true);
+
+ // Show rating lock action only for ratings section
+ action_rating_lock_->setVisible(menu_section_ == Playlist::Column_Rating);
+
}
qDeleteAll(show_actions_);
@@ -126,9 +148,9 @@ void PlaylistHeader::SetColumnAlignment(QAction *action) {
Qt::Alignment alignment = Qt::AlignVCenter;
- if (action == align_left_action_) alignment |= Qt::AlignLeft;
- if (action == align_center_action_) alignment |= Qt::AlignHCenter;
- if (action == align_right_action_) alignment |= Qt::AlignRight;
+ if (action == action_align_left_) alignment |= Qt::AlignLeft;
+ if (action == action_align_center_) alignment |= Qt::AlignHCenter;
+ if (action == action_align_right_) alignment |= Qt::AlignRight;
view_->SetColumnAlignment(menu_section_, alignment);
@@ -150,3 +172,8 @@ void PlaylistHeader::enterEvent(QEvent*) {
void PlaylistHeader::ResetColumns() {
view_->ResetHeaderState();
}
+
+void PlaylistHeader::ToggleRatingEditStatus() {
+ emit SectionRatingLockStatusChanged(action_rating_lock_->isChecked());
+}
+
diff --git a/src/playlist/playlistheader.h b/src/playlist/playlistheader.h
index e30bc6b6..6ba8fb37 100644
--- a/src/playlist/playlistheader.h
+++ b/src/playlist/playlistheader.h
@@ -54,12 +54,14 @@ class PlaylistHeader : public StretchHeaderView {
signals:
void SectionVisibilityChanged(int logical, bool visible);
void MouseEntered();
+ void SectionRatingLockStatusChanged(bool);
private slots:
void HideCurrent();
void ToggleVisible(int section);
void ResetColumns();
void SetColumnAlignment(QAction *action);
+ void ToggleRatingEditStatus();
private:
void AddColumnAction(int index);
@@ -69,12 +71,13 @@ class PlaylistHeader : public StretchHeaderView {
int menu_section_;
QMenu *menu_;
- QAction *hide_action_;
- QAction *stretch_action_;
- QAction *reset_action_;
- QAction *align_left_action_;
- QAction *align_center_action_;
- QAction *align_right_action_;
+ QAction *action_hide_;
+ QAction *action_reset_;
+ QAction *action_stretch_;
+ QAction *action_rating_lock_;
+ QAction *action_align_left_;
+ QAction *action_align_center_;
+ QAction *action_align_right_;
QList show_actions_;
};
diff --git a/src/playlist/playlistmanager.cpp b/src/playlist/playlistmanager.cpp
index b4aa2147..c67f1ece 100644
--- a/src/playlist/playlistmanager.cpp
+++ b/src/playlist/playlistmanager.cpp
@@ -96,6 +96,7 @@ void PlaylistManager::Init(CollectionBackend *collection_backend, PlaylistBacken
connect(collection_backend_, SIGNAL(SongsDiscovered(SongList)), SLOT(SongsDiscovered(SongList)));
connect(collection_backend_, SIGNAL(SongsStatisticsChanged(SongList)), SLOT(SongsDiscovered(SongList)));
+ connect(collection_backend_, SIGNAL(SongsRatingChanged(SongList)), SLOT(SongsDiscovered(SongList)));
for (const PlaylistBackend::Playlist &p : playlist_backend->GetAllOpenPlaylists()) {
++playlists_loading_;
@@ -602,3 +603,26 @@ void PlaylistManager::SetCurrentOrOpen(const int id) {
bool PlaylistManager::IsPlaylistOpen(const int id) {
return playlists_.contains(id);
}
+
+void PlaylistManager::PlaySmartPlaylist(PlaylistGeneratorPtr generator, bool as_new, bool clear) {
+
+ if (as_new) {
+ New(generator->name());
+ }
+
+ if (clear) {
+ current()->Clear();
+ }
+
+ current()->InsertSmartPlaylist(generator);
+
+}
+
+void PlaylistManager::RateCurrentSong(const double rating) {
+ active()->RateSong(active()->current_index(), rating);
+}
+
+void PlaylistManager::RateCurrentSong(const int rating) {
+ RateCurrentSong(rating / 5.0);
+}
+
diff --git a/src/playlist/playlistmanager.h b/src/playlist/playlistmanager.h
index e8a862e2..ad47ec51 100644
--- a/src/playlist/playlistmanager.h
+++ b/src/playlist/playlistmanager.h
@@ -34,6 +34,7 @@
#include "core/song.h"
#include "playlist.h"
+#include "smartplaylists/playlistgenerator.h"
class QModelIndex;
@@ -76,6 +77,8 @@ class PlaylistManagerInterface : public QObject {
virtual PlaylistParser *parser() const = 0;
virtual PlaylistContainer *playlist_container() const = 0;
+ virtual void PlaySmartPlaylist(PlaylistGeneratorPtr generator, const bool as_new, const bool clear) = 0;
+
public slots:
virtual void New(const QString &name, const SongList& songs = SongList(), const QString &special_type = QString()) = 0;
virtual void Load(const QString &filename) = 0;
@@ -103,6 +106,11 @@ class PlaylistManagerInterface : public QObject {
virtual void SetActivePaused() = 0;
virtual void SetActiveStopped() = 0;
+ // Rate current song using 0.0 - 1.0 scale.
+ virtual void RateCurrentSong(const double rating) = 0;
+ // Rate current song using 0 - 5 scale.
+ virtual void RateCurrentSong(const int rating) = 0;
+
signals:
void PlaylistManagerInitialized();
void AllPlaylistsLoaded();
@@ -196,7 +204,6 @@ class PlaylistManager : public PlaylistManagerInterface {
void ShuffleCurrent() override;
void RemoveDuplicatesCurrent() override;
void RemoveUnavailableCurrent() override;
- //void SetActiveStreamMetadata(const QUrl& url, const Song& song);
void SongChangeRequestProcessed(const QUrl& url, const bool valid) override;
@@ -207,6 +214,13 @@ class PlaylistManager : public PlaylistManagerInterface {
// Remove the current playing song
void RemoveCurrentSong();
+ void PlaySmartPlaylist(PlaylistGeneratorPtr generator, const bool as_new, const bool clear) override;
+
+ // Rate current song using 0.0 - 1.0 scale.
+ void RateCurrentSong(const double rating) override;
+ // Rate current song using 0 - 5 scale.
+ void RateCurrentSong(const int rating) override;
+
private slots:
void SetActivePlaying() override;
void SetActivePaused() override;
diff --git a/src/playlist/playlistsequence.cpp b/src/playlist/playlistsequence.cpp
index 01e099bb..3f86e8c3 100644
--- a/src/playlist/playlistsequence.cpp
+++ b/src/playlist/playlistsequence.cpp
@@ -47,7 +47,8 @@ PlaylistSequence::PlaylistSequence(QWidget *parent, SettingsProvider *settings)
shuffle_menu_(new QMenu(this)),
loading_(false),
repeat_mode_(Repeat_Off),
- shuffle_mode_(Shuffle_Off)
+ shuffle_mode_(Shuffle_Off),
+ dynamic_(false)
{
ui_->setupUi(this);
@@ -161,7 +162,7 @@ void PlaylistSequence::ShuffleActionTriggered(QAction *action) {
}
-void PlaylistSequence::SetRepeatMode(RepeatMode mode) {
+void PlaylistSequence::SetRepeatMode(const RepeatMode mode) {
ui_->repeat->setChecked(mode != Repeat_Off);
@@ -184,7 +185,7 @@ void PlaylistSequence::SetRepeatMode(RepeatMode mode) {
}
-void PlaylistSequence::SetShuffleMode(ShuffleMode mode) {
+void PlaylistSequence::SetShuffleMode(const ShuffleMode mode) {
ui_->shuffle->setChecked(mode != Shuffle_Off);
@@ -204,12 +205,23 @@ void PlaylistSequence::SetShuffleMode(ShuffleMode mode) {
}
+void PlaylistSequence::SetUsingDynamicPlaylist(const bool dynamic) {
+
+ dynamic_ = dynamic;
+ const QString not_available(tr("Not available while using a dynamic playlist"));
+
+ setEnabled(!dynamic);
+ ui_->shuffle->setToolTip(dynamic ? not_available : tr("Shuffle"));
+ ui_->repeat->setToolTip(dynamic ? not_available : tr("Repeat"));
+
+}
+
PlaylistSequence::ShuffleMode PlaylistSequence::shuffle_mode() const {
- return shuffle_mode_;
+ return dynamic_ ? Shuffle_Off : shuffle_mode_;
}
PlaylistSequence::RepeatMode PlaylistSequence::repeat_mode() const {
- return repeat_mode_;
+ return dynamic_ ? Repeat_Off : repeat_mode_;
}
//called from global shortcut
diff --git a/src/playlist/playlistsequence.h b/src/playlist/playlistsequence.h
index e0e355a9..3e701c7e 100644
--- a/src/playlist/playlistsequence.h
+++ b/src/playlist/playlistsequence.h
@@ -68,14 +68,15 @@ class PlaylistSequence : public QWidget {
QMenu *shuffle_menu() const { return shuffle_menu_; }
public slots:
- void SetRepeatMode(PlaylistSequence::RepeatMode mode);
- void SetShuffleMode(PlaylistSequence::ShuffleMode mode);
+ void SetRepeatMode(const PlaylistSequence::RepeatMode mode);
+ void SetShuffleMode(const PlaylistSequence::ShuffleMode mode);
void CycleShuffleMode();
void CycleRepeatMode();
+ void SetUsingDynamicPlaylist(const bool dynamic);
signals:
- void RepeatModeChanged(PlaylistSequence::RepeatMode mode);
- void ShuffleModeChanged(PlaylistSequence::ShuffleMode mode);
+ void RepeatModeChanged(const PlaylistSequence::RepeatMode mode);
+ void ShuffleModeChanged(const PlaylistSequence::ShuffleMode mode);
private slots:
void RepeatActionTriggered(QAction *);
@@ -97,7 +98,7 @@ class PlaylistSequence : public QWidget {
bool loading_;
RepeatMode repeat_mode_;
ShuffleMode shuffle_mode_;
-
+ bool dynamic_;
};
#endif // PLAYLISTSEQUENCE_H
diff --git a/src/playlist/playlistview.cpp b/src/playlist/playlistview.cpp
index 9766a7d9..750224b4 100644
--- a/src/playlist/playlistview.cpp
+++ b/src/playlist/playlistview.cpp
@@ -78,6 +78,7 @@
#include "covermanager/albumcoverloaderresult.h"
#include "settings/appearancesettingspage.h"
#include "settings/playlistsettingspage.h"
+#include "dynamicplaylistcontrols.h"
#ifdef HAVE_MOODBAR
# include "moodbar/moodbaritemdelegate.h"
@@ -168,7 +169,11 @@ PlaylistView::PlaylistView(QWidget *parent)
cached_current_row_row_(-1),
drop_indicator_row_(-1),
drag_over_(false),
- column_alignment_(DefaultColumnAlignment()) {
+ header_state_version_(1),
+ column_alignment_(DefaultColumnAlignment()),
+ rating_locked_(false),
+ dynamic_controls_(new DynamicPlaylistControls(this)),
+ rating_delegate_(nullptr) {
setHeader(header_);
header_->setSectionsMovable(true);
@@ -195,6 +200,9 @@ PlaylistView::PlaylistView(QWidget *parent)
connect(header_, SIGNAL(SectionVisibilityChanged(int, bool)), SLOT(InvalidateCachedCurrentPixmap()));
connect(header_, SIGNAL(StretchEnabledChanged(bool)), SLOT(StretchChanged(bool)));
+ connect(header_, SIGNAL(SectionRatingLockStatusChanged(bool)), SLOT(SetRatingLockStatus(bool)));
+ connect(header_, SIGNAL(MouseEntered()), SLOT(RatingHoverOut()));
+
inhibit_autoscroll_timer_->setInterval(kAutoscrollGraceTimeout * 1000);
inhibit_autoscroll_timer_->setSingleShot(true);
connect(inhibit_autoscroll_timer_, SIGNAL(timeout()), SLOT(InhibitAutoscrollTimeout()));
@@ -202,6 +210,8 @@ PlaylistView::PlaylistView(QWidget *parent)
horizontalScrollBar()->installEventFilter(this);
verticalScrollBar()->installEventFilter(this);
+ dynamic_controls_->hide();
+
// For fading
connect(fade_animation_, SIGNAL(valueChanged(qreal)), SLOT(FadePreviousBackgroundImage(qreal)));
fade_animation_->setDirection(QTimeLine::Backward); // 1.0 -> 0.0
@@ -258,12 +268,16 @@ void PlaylistView::SetItemDelegates() {
setItemDelegateForColumn(Playlist::Column_Mood, new MoodbarItemDelegate(app_, this, this));
#endif
+ rating_delegate_ = new RatingItemDelegate(this);
+ setItemDelegateForColumn(Playlist::Column_Rating, rating_delegate_);
+
}
void PlaylistView::setModel(QAbstractItemModel *m) {
if (model()) {
disconnect(model(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(InvalidateCachedCurrentPixmap()));
+ disconnect(model(), SIGNAL(layoutAboutToBeChanged()), this, SLOT(RatingHoverOut()));
// When changing the model, always invalidate the current pixmap.
// If a remote client uses "stop after", without invaliding the stop mark would not appear.
@@ -273,6 +287,7 @@ void PlaylistView::setModel(QAbstractItemModel *m) {
QTreeView::setModel(m);
connect(model(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(InvalidateCachedCurrentPixmap()));
+ connect(model(), SIGNAL(layoutAboutToBeChanged()), this, SLOT(RatingHoverOut()));
}
@@ -282,10 +297,16 @@ void PlaylistView::SetPlaylist(Playlist *playlist) {
disconnect(playlist_, SIGNAL(MaybeAutoscroll(Playlist::AutoScroll)), this, SLOT(MaybeAutoscroll(Playlist::AutoScroll)));
disconnect(playlist_, SIGNAL(destroyed()), this, SLOT(PlaylistDestroyed()));
disconnect(playlist_, SIGNAL(QueueChanged()), this, SLOT(update()));
+
+ disconnect(playlist_, SIGNAL(DynamicModeChanged(bool)), this, SLOT(DynamicModeChanged(bool)));
+ disconnect(dynamic_controls_, SIGNAL(Expand()), playlist_, SLOT(ExpandDynamicPlaylist()));
+ disconnect(dynamic_controls_, SIGNAL(Repopulate()), playlist_, SLOT(RepopulateDynamicPlaylist()));
+ disconnect(dynamic_controls_, SIGNAL(TurnOff()), playlist_, SLOT(TurnOffDynamicPlaylist()));
}
playlist_ = playlist;
RestoreHeaderState();
+ DynamicModeChanged(playlist->is_dynamic());
setFocus();
JumpToLastPlayedTrack();
@@ -294,13 +315,21 @@ void PlaylistView::SetPlaylist(Playlist *playlist) {
connect(playlist_, SIGNAL(destroyed()), SLOT(PlaylistDestroyed()));
connect(playlist_, SIGNAL(QueueChanged()), SLOT(update()));
+ connect(playlist_, SIGNAL(DynamicModeChanged(bool)), SLOT(DynamicModeChanged(bool)));
+ connect(dynamic_controls_, SIGNAL(Expand()), playlist_, SLOT(ExpandDynamicPlaylist()));
+ connect(dynamic_controls_, SIGNAL(Repopulate()), playlist_, SLOT(RepopulateDynamicPlaylist()));
+ connect(dynamic_controls_, SIGNAL(TurnOff()), playlist_, SLOT(TurnOffDynamicPlaylist()));
+
}
void PlaylistView::LoadHeaderState() {
QSettings s;
s.beginGroup(Playlist::kSettingsGroup);
- if (s.contains("state")) header_state_ = s.value("state").toByteArray();
+ if (s.contains("state")) {
+ header_state_version_ = s.value("state_version", 0).toInt();
+ header_state_ = s.value("state").toByteArray();
+ }
if (s.contains("column_alignments")) column_alignment_ = s.value("column_alignments").value();
s.endGroup();
@@ -322,6 +351,7 @@ void PlaylistView::SetHeaderState() {
void PlaylistView::ResetHeaderState() {
set_initial_header_layout_ = true;
+ header_state_version_ = 1;
header_state_ = header_->ResetState();
RestoreHeaderState();
@@ -355,6 +385,7 @@ void PlaylistView::RestoreHeaderState() {
header_->HideSection(Playlist::Column_Comment);
header_->HideSection(Playlist::Column_Grouping);
header_->HideSection(Playlist::Column_Mood);
+ header_->HideSection(Playlist::Column_Rating);
header_->moveSection(header_->visualIndex(Playlist::Column_Track), 0);
@@ -378,6 +409,11 @@ void PlaylistView::RestoreHeaderState() {
}
+ if (header_state_version_ < 1) {
+ header_->HideSection(Playlist::Column_Rating);
+ header_state_version_ = 1;
+ }
+
// Make sure at least one column is visible
bool all_hidden = true;
for (int i = 0; i < header_->count(); ++i) {
@@ -744,6 +780,17 @@ void PlaylistView::closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHi
void PlaylistView::mouseMoveEvent(QMouseEvent *event) {
+ // Check whether rating section is locked by user or not
+ if (!rating_locked_) {
+ QModelIndex idx = indexAt(event->pos());
+ if (idx.isValid() && idx.data(Playlist::Role_CanSetRating).toBool()) {
+ RatingHoverIn(idx, event->pos());
+ }
+ else if (rating_delegate_->is_mouse_over()) {
+ RatingHoverOut();
+ }
+ }
+
if (!drag_over_) {
QTreeView::mouseMoveEvent(event);
}
@@ -752,6 +799,10 @@ void PlaylistView::mouseMoveEvent(QMouseEvent *event) {
void PlaylistView::leaveEvent(QEvent *e) {
+ if (rating_delegate_->is_mouse_over() && !rating_locked_) {
+ RatingHoverOut();
+ }
+
QTreeView::leaveEvent(e);
}
@@ -764,19 +815,47 @@ void PlaylistView::mousePressEvent(QMouseEvent *event) {
}
QModelIndex idx = indexAt(event->pos());
- if (event->button() == Qt::XButton1 && idx.isValid()) {
- app_->player()->Previous();
- }
- else if (event->button() == Qt::XButton2 && idx.isValid()) {
- app_->player()->Next();
- }
- else {
- QTreeView::mousePressEvent(event);
+ if (idx.isValid()) {
+ switch (event->button()) {
+ case Qt::XButton1:
+ app_->player()->Previous();
+ break;
+ case Qt::XButton2:
+ app_->player()->Next();
+ break;
+ case Qt::LeftButton:{
+ if (idx.data(Playlist::Role_CanSetRating).toBool() && !rating_locked_) {
+ // Calculate which star was clicked
+ double new_rating = RatingPainter::RatingForPos(event->pos(), visualRect(idx));
+ if (selectedIndexes().contains(idx)) {
+ // Update all the selected item ratings
+ QModelIndexList src_index_list;
+ for (const QModelIndex &i : selectedIndexes()) {
+ if (i.data(Playlist::Role_CanSetRating).toBool()) {
+ src_index_list << playlist_->proxy()->mapToSource(i);
+ }
+ }
+ if (!src_index_list.isEmpty()) {
+ playlist_->RateSongs(src_index_list, new_rating);
+ }
+ }
+ else {
+ // Update only this item rating
+ playlist_->RateSong(playlist_->proxy()->mapToSource(idx), new_rating);
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
}
inhibit_autoscroll_ = true;
inhibit_autoscroll_timer_->start();
+ QTreeView::mousePressEvent(event);
+
}
void PlaylistView::scrollContentsBy(int dx, int dy) {
@@ -1150,8 +1229,10 @@ void PlaylistView::SaveSettings() {
QSettings s;
s.beginGroup(Playlist::kSettingsGroup);
+ s.setValue("state_version", header_state_version_);
s.setValue("state", header_->SaveState());
s.setValue("column_alignments", QVariant::fromValue(column_alignment_));
+ s.setValue("rating_locked", rating_locked_);
s.endGroup();
}
@@ -1165,6 +1246,15 @@ void PlaylistView::StretchChanged(const bool stretch) {
}
+void PlaylistView::resizeEvent(QResizeEvent *e) {
+
+ QTreeView::resizeEvent(e);
+ if (dynamic_controls_->isVisible()) {
+ RepositionDynamicControls();
+ }
+
+}
+
bool PlaylistView::eventFilter(QObject *object, QEvent *event) {
if (event->type() == QEvent::Enter && (object == horizontalScrollBar() || object == verticalScrollBar())) {
@@ -1357,3 +1447,75 @@ void PlaylistView::focusInEvent(QFocusEvent *event) {
}
}
+
+void PlaylistView::DynamicModeChanged(bool dynamic) {
+
+ if (!dynamic) {
+ dynamic_controls_->hide();
+ }
+ else {
+ RepositionDynamicControls();
+ dynamic_controls_->show();
+ }
+
+}
+
+void PlaylistView::RepositionDynamicControls() {
+
+ dynamic_controls_->resize(dynamic_controls_->sizeHint());
+ dynamic_controls_->move((width() - dynamic_controls_->width()) / 2, height() - dynamic_controls_->height() - 20);
+
+}
+
+void PlaylistView::SetRatingLockStatus(const bool state) {
+
+ if (!header_state_loaded_) return;
+
+ rating_locked_ = state;
+
+}
+
+void PlaylistView::RatingHoverIn(const QModelIndex &idx, const QPoint &pos) {
+
+ if (editTriggers() & QAbstractItemView::NoEditTriggers) {
+ return;
+ }
+
+ const QModelIndex old_index = rating_delegate_->mouse_over_index();
+ rating_delegate_->set_mouse_over(idx, selectedIndexes(), pos);
+ setCursor(Qt::PointingHandCursor);
+
+ update(idx);
+ update(old_index);
+ for (const QModelIndex &i : selectedIndexes()) {
+ if (i.column() == Playlist::Column_Rating) update(i);
+ }
+
+ if (idx.data(Playlist::Role_IsCurrent).toBool() || old_index.data(Playlist::Role_IsCurrent).toBool()) {
+ InvalidateCachedCurrentPixmap();
+ }
+
+}
+
+void PlaylistView::RatingHoverOut() {
+
+ if (editTriggers() & QAbstractItemView::NoEditTriggers) {
+ return;
+ }
+
+ const QModelIndex old_index = rating_delegate_->mouse_over_index();
+ rating_delegate_->set_mouse_out();
+ setCursor(QCursor());
+
+ update(old_index);
+ for (const QModelIndex &i : selectedIndexes()) {
+ if (i.column() == Playlist::Column_Rating) {
+ update(i);
+ }
+ }
+
+ if (old_index.data(Playlist::Role_IsCurrent).toBool()) {
+ InvalidateCachedCurrentPixmap();
+ }
+
+}
diff --git a/src/playlist/playlistview.h b/src/playlist/playlistview.h
index c42a6ba4..737b166a 100644
--- a/src/playlist/playlistview.h
+++ b/src/playlist/playlistview.h
@@ -72,6 +72,8 @@ class QTimerEvent;
class Application;
class CollectionBackend;
class PlaylistHeader;
+class DynamicPlaylistControls;
+class RatingItemDelegate;
// This proxy style works around a bug/feature introduced in Qt 4.7's QGtkStyle
// that uses Gtk to paint row backgrounds, ignoring any custom brush or palette the caller set in the QStyleOption.
@@ -148,6 +150,7 @@ class PlaylistView : public QTreeView {
void dropEvent(QDropEvent *event) override;
bool eventFilter(QObject *object, QEvent *event) override;
void focusInEvent(QFocusEvent *event) override;
+ void resizeEvent(QResizeEvent *event) override;
// QTreeView
void drawTree(QPainter *painter, const QRegion ®ion) const;
@@ -177,6 +180,10 @@ class PlaylistView : public QTreeView {
void Stopped();
void SongChanged(const Song &song);
void AlbumCoverLoaded(const Song &song, AlbumCoverLoaderResult result = AlbumCoverLoaderResult());
+ void DynamicModeChanged(const bool dynamic);
+ void SetRatingLockStatus(const bool state);
+ void RatingHoverIn(const QModelIndex &idx, const QPoint &pos);
+ void RatingHoverOut();
private:
void LoadHeaderState();
@@ -206,6 +213,8 @@ class PlaylistView : public QTreeView {
QModelIndex NextEditableIndex(const QModelIndex ¤t);
QModelIndex PrevEditableIndex(const QModelIndex ¤t);
+ void RepositionDynamicControls();
+
Application *app_;
PlaylistProxyStyle *style_;
Playlist *playlist_;
@@ -275,11 +284,16 @@ class PlaylistView : public QTreeView {
int drop_indicator_row_;
bool drag_over_;
+ int header_state_version_;
QByteArray header_state_;
ColumnAlignmentMap column_alignment_;
+ bool rating_locked_;
Song song_playing_;
+ DynamicPlaylistControls *dynamic_controls_;
+ RatingItemDelegate *rating_delegate_;
+
};
#endif // PLAYLISTVIEW_H
diff --git a/src/qobuz/qobuzbaserequest.cpp b/src/qobuz/qobuzbaserequest.cpp
new file mode 100644
index 00000000..727484b7
--- /dev/null
+++ b/src/qobuz/qobuzbaserequest.cpp
@@ -0,0 +1,196 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2019, Jonas Kvinge
+ *
+ * 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 .
+ *
+ */
+
+#include "config.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/logging.h"
+#include "core/network.h"
+#include "qobuzservice.h"
+#include "qobuzbaserequest.h"
+
+const char *QobuzBaseRequest::kApiUrl = "https://www.qobuz.com/api.json/0.2";
+
+QobuzBaseRequest::QobuzBaseRequest(QobuzService *service, NetworkAccessManager *network, QObject *parent) :
+ QObject(parent),
+ service_(service),
+ network_(network)
+ {}
+
+QobuzBaseRequest::~QobuzBaseRequest() {}
+
+QNetworkReply *QobuzBaseRequest::CreateRequest(const QString &ressource_name, const QList ¶ms_provided) {
+
+ ParamList params = ParamList() << params_provided
+ << Param("app_id", app_id());
+
+ std::sort(params.begin(), params.end());
+
+ QUrlQuery url_query;
+ for (const Param ¶m : params) {
+ url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
+ }
+
+ QUrl url(kApiUrl + QString("/") + ressource_name);
+ url.setQuery(url_query);
+ QNetworkRequest req(url);
+#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
+ req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
+#else
+ req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+#endif
+ req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+ req.setRawHeader("X-App-Id", app_id().toUtf8());
+ if (authenticated()) req.setRawHeader("X-User-Auth-Token", user_auth_token().toUtf8());
+
+ QNetworkReply *reply = network_->get(req);
+ connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleSSLErrors(QList)));
+
+ qLog(Debug) << "Qobuz: Sending request" << url;
+
+ return reply;
+
+}
+
+void QobuzBaseRequest::HandleSSLErrors(QList ssl_errors) {
+
+ for (QSslError &ssl_error : ssl_errors) {
+ Error(ssl_error.errorString());
+ }
+
+}
+
+QByteArray QobuzBaseRequest::GetReplyData(QNetworkReply *reply) {
+
+ QByteArray data;
+
+ if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
+ data = reply->readAll();
+ }
+ else {
+ if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
+ // This is a network error, there is nothing more to do.
+ Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
+ }
+ else {
+ // See if there is Json data containing "status", "code" and "message" - then use that instead.
+ data = reply->readAll();
+ QString error;
+ QJsonParseError parse_error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error);
+ if (parse_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
+ QJsonObject json_obj = json_doc.object();
+ if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("code") && json_obj.contains("message")) {
+ QString status = json_obj["status"].toString();
+ int code = json_obj["code"].toInt();
+ QString message = json_obj["message"].toString();
+ error = QString("%1 (%2)").arg(message).arg(code);
+ }
+ }
+ if (error.isEmpty()) {
+ if (reply->error() != QNetworkReply::NoError) {
+ error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
+ }
+ else {
+ error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
+ }
+ }
+ Error(error);
+ }
+ return QByteArray();
+ }
+
+ return data;
+
+}
+
+QJsonObject QobuzBaseRequest::ExtractJsonObj(QByteArray &data) {
+
+ QJsonParseError json_error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
+
+ if (json_error.error != QJsonParseError::NoError) {
+ Error("Reply from server missing Json data.", data);
+ return QJsonObject();
+ }
+
+ if (json_doc.isEmpty()) {
+ Error("Received empty Json document.", data);
+ return QJsonObject();
+ }
+
+ if (!json_doc.isObject()) {
+ Error("Json document is not an object.", json_doc);
+ return QJsonObject();
+ }
+
+ QJsonObject json_obj = json_doc.object();
+ if (json_obj.isEmpty()) {
+ Error("Received empty Json object.", json_doc);
+ return QJsonObject();
+ }
+
+ return json_obj;
+
+}
+
+QJsonValue QobuzBaseRequest::ExtractItems(QByteArray &data) {
+
+ QJsonObject json_obj = ExtractJsonObj(data);
+ if (json_obj.isEmpty()) return QJsonValue();
+ return ExtractItems(json_obj);
+
+}
+
+QJsonValue QobuzBaseRequest::ExtractItems(QJsonObject &json_obj) {
+
+ if (!json_obj.contains("items")) {
+ Error("Json reply is missing items.", json_obj);
+ return QJsonArray();
+ }
+ QJsonValue json_items = json_obj["items"];
+ return json_items;
+
+}
+
+QString QobuzBaseRequest::ErrorsToHTML(const QStringList &errors) {
+
+ QString error_html;
+ for (const QString &error : errors) {
+ error_html += error + "
";
+ }
+ return error_html;
+
+}
diff --git a/src/qobuz/qobuzbaserequest.h b/src/qobuz/qobuzbaserequest.h
new file mode 100644
index 00000000..c35cbff4
--- /dev/null
+++ b/src/qobuz/qobuzbaserequest.h
@@ -0,0 +1,105 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2019, Jonas Kvinge
+ *
+ * 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 .
+ *
+ */
+
+#ifndef QOBUZBASEREQUEST_H
+#define QOBUZBASEREQUEST_H
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/song.h"
+#include "qobuzservice.h"
+
+class QNetworkReply;
+class NetworkAccessManager;
+
+class QobuzBaseRequest : public QObject {
+ Q_OBJECT
+
+ public:
+
+ enum QueryType {
+ QueryType_None,
+ QueryType_Artists,
+ QueryType_Albums,
+ QueryType_Songs,
+ QueryType_SearchArtists,
+ QueryType_SearchAlbums,
+ QueryType_SearchSongs,
+ QueryType_StreamURL,
+ };
+
+ explicit QobuzBaseRequest(QobuzService *service, NetworkAccessManager *network, QObject *parent);
+ ~QobuzBaseRequest();
+
+ typedef QPair Param;
+ typedef QList ParamList;
+
+ static const char *kApiUrl;
+
+ QNetworkReply *CreateRequest(const QString &ressource_name, const QList ¶ms_provided);
+ QByteArray GetReplyData(QNetworkReply *reply);
+ QJsonObject ExtractJsonObj(QByteArray &data);
+ QJsonValue ExtractItems(QByteArray &data);
+ QJsonValue ExtractItems(QJsonObject &json_obj);
+
+ virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0;
+ QString ErrorsToHTML(const QStringList &errors);
+
+ QString api_url() { return QString(kApiUrl); }
+ QString app_id() { return service_->app_id(); }
+ QString app_secret() { return service_->app_secret(); }
+ QString username() { return service_->username(); }
+ QString password() { return service_->password(); }
+ int format() { return service_->format(); }
+ int artistssearchlimit() { return service_->artistssearchlimit(); }
+ int albumssearchlimit() { return service_->albumssearchlimit(); }
+ int songssearchlimit() { return service_->songssearchlimit(); }
+
+ qint64 user_id() { return service_->user_id(); }
+ QString user_auth_token() { return service_->user_auth_token(); }
+ QString device_id() { return service_->device_id(); }
+ qint64 credential_id() { return service_->credential_id(); }
+
+ bool authenticated() { return service_->authenticated(); }
+ bool login_sent() { return service_->login_sent(); }
+ int max_login_attempts() { return service_->max_login_attempts(); }
+ int login_attempts() { return service_->login_attempts(); }
+
+ private slots:
+ void HandleSSLErrors(QList ssl_errors);
+
+ private:
+ QobuzService *service_;
+ NetworkAccessManager *network_;
+
+};
+
+#endif // QOBUZBASEREQUEST_H
diff --git a/src/qobuz/qobuzfavoriterequest.cpp b/src/qobuz/qobuzfavoriterequest.cpp
new file mode 100644
index 00000000..78f7aa0c
--- /dev/null
+++ b/src/qobuz/qobuzfavoriterequest.cpp
@@ -0,0 +1,277 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2019, Jonas Kvinge
+ *
+ * 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 .
+ *
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/logging.h"
+#include "core/network.h"
+#include "core/song.h"
+#include "qobuzservice.h"
+#include "qobuzbaserequest.h"
+#include "qobuzfavoriterequest.h"
+
+QobuzFavoriteRequest::QobuzFavoriteRequest(QobuzService *service, NetworkAccessManager *network, QObject *parent)
+ : QobuzBaseRequest(service, network, parent),
+ service_(service),
+ network_(network) {}
+
+QobuzFavoriteRequest::~QobuzFavoriteRequest() {
+
+ while (!replies_.isEmpty()) {
+ QNetworkReply *reply = replies_.takeFirst();
+ disconnect(reply, nullptr, this, nullptr);
+ reply->abort();
+ reply->deleteLater();
+ }
+
+}
+
+QString QobuzFavoriteRequest::FavoriteText(const FavoriteType type) {
+
+ switch (type) {
+ case FavoriteType_Artists:
+ return "artists";
+ case FavoriteType_Albums:
+ return "albums";
+ case FavoriteType_Songs:
+ default:
+ return "tracks";
+ }
+
+}
+
+void QobuzFavoriteRequest::AddArtists(const SongList &songs) {
+ AddFavorites(FavoriteType_Artists, songs);
+}
+
+void QobuzFavoriteRequest::AddAlbums(const SongList &songs) {
+ AddFavorites(FavoriteType_Albums, songs);
+}
+
+void QobuzFavoriteRequest::AddSongs(const SongList &songs) {
+ AddFavorites(FavoriteType_Songs, songs);
+}
+
+void QobuzFavoriteRequest::AddFavorites(const FavoriteType type, const SongList &songs) {
+
+ if (songs.isEmpty()) return;
+
+ QString text;
+ switch (type) {
+ case FavoriteType_Artists:
+ text = "artist_ids";
+ break;
+ case FavoriteType_Albums:
+ text = "album_ids";
+ break;
+ case FavoriteType_Songs:
+ text = "track_ids";
+ break;
+ }
+
+ QStringList ids_list;
+ for (const Song &song : songs) {
+ QString id;
+ switch (type) {
+ case FavoriteType_Artists:
+ if (song.artist_id().isEmpty()) continue;
+ id = song.artist_id();
+ break;
+ case FavoriteType_Albums:
+ if (song.album_id().isEmpty()) continue;
+ id = song.album_id();
+ break;
+ case FavoriteType_Songs:
+ if (song.song_id().isEmpty()) continue;
+ id = song.song_id();
+ break;
+ }
+ if (id.isEmpty()) continue;
+ if (!ids_list.contains(id)) {
+ ids_list << id;
+ }
+ }
+ if (ids_list.isEmpty()) return;
+
+ QString ids = ids_list.join(',');
+
+ ParamList params = ParamList() << Param("app_id", app_id())
+ << Param("user_auth_token", user_auth_token())
+ << Param(text, ids);
+
+ QUrlQuery url_query;
+ for (const Param ¶m : params) {
+ url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
+ }
+
+ QNetworkReply *reply = CreateRequest("favorite/create", params);
+ connect(reply, &QNetworkReply::finished, [=] { AddFavoritesReply(reply, type, songs); });
+ replies_ << reply;
+
+}
+
+void QobuzFavoriteRequest::AddFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs) {
+
+ if (replies_.contains(reply)) {
+ replies_.removeAll(reply);
+ reply->deleteLater();
+ }
+ else {
+ return;
+ }
+
+ QByteArray data = GetReplyData(reply);
+
+ if (reply->error() != QNetworkReply::NoError) {
+ return;
+ }
+
+ qLog(Debug) << "Qobuz:" << songs.count() << "songs added to" << FavoriteText(type) << "favorites.";
+
+ switch (type) {
+ case FavoriteType_Artists:
+ emit ArtistsAdded(songs);
+ break;
+ case FavoriteType_Albums:
+ emit AlbumsAdded(songs);
+ break;
+ case FavoriteType_Songs:
+ emit SongsAdded(songs);
+ break;
+ }
+
+}
+
+void QobuzFavoriteRequest::RemoveArtists(const SongList &songs) {
+ RemoveFavorites(FavoriteType_Artists, songs);
+}
+
+void QobuzFavoriteRequest::RemoveAlbums(const SongList &songs) {
+ RemoveFavorites(FavoriteType_Albums, songs);
+}
+
+void QobuzFavoriteRequest::RemoveSongs(const SongList &songs) {
+ RemoveFavorites(FavoriteType_Songs, songs);
+}
+
+void QobuzFavoriteRequest::RemoveFavorites(const FavoriteType type, const SongList &songs) {
+
+ if (songs.isEmpty()) return;
+
+ QString text;
+ switch (type) {
+ case FavoriteType_Artists:
+ text = "artist_ids";
+ break;
+ case FavoriteType_Albums:
+ text = "album_ids";
+ break;
+ case FavoriteType_Songs:
+ text = "track_ids";
+ break;
+ }
+
+ QStringList ids_list;
+ for (const Song &song : songs) {
+ QString id;
+ switch (type) {
+ case FavoriteType_Artists:
+ if (song.artist_id().isEmpty()) continue;
+ id = song.artist_id();
+ break;
+ case FavoriteType_Albums:
+ if (song.album_id().isEmpty()) continue;
+ id = song.album_id();
+ break;
+ case FavoriteType_Songs:
+ if (song.song_id().isEmpty()) continue;
+ id = song.song_id();
+ break;
+ }
+ if (id.isEmpty()) continue;
+ if (!ids_list.contains(id)) {
+ ids_list << id;
+ }
+ }
+ if (ids_list.isEmpty()) return;
+
+ QString ids = ids_list.join(',');
+
+ ParamList params = ParamList() << Param("app_id", app_id())
+ << Param("user_auth_token", user_auth_token())
+ << Param(text, ids);
+
+ QUrlQuery url_query;
+ for (const Param ¶m : params) {
+ url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
+ }
+
+ QNetworkReply *reply = CreateRequest("favorite/delete", params);
+ connect(reply, &QNetworkReply::finished, [=] { RemoveFavoritesReply(reply, type, songs); });
+ replies_ << reply;
+
+}
+
+void QobuzFavoriteRequest::RemoveFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs) {
+
+ if (replies_.contains(reply)) {
+ replies_.removeAll(reply);
+ reply->deleteLater();
+ }
+ else {
+ return;
+ }
+
+ QByteArray data = GetReplyData(reply);
+ if (reply->error() != QNetworkReply::NoError) {
+ return;
+ }
+
+ qLog(Debug) << "Qobuz:" << songs.count() << "songs removed from" << FavoriteText(type) << "favorites.";
+
+ switch (type) {
+ case FavoriteType_Artists:
+ emit ArtistsRemoved(songs);
+ break;
+ case FavoriteType_Albums:
+ emit AlbumsRemoved(songs);
+ break;
+ case FavoriteType_Songs:
+ emit SongsRemoved(songs);
+ break;
+ }
+
+}
+
+void QobuzFavoriteRequest::Error(const QString &error, const QVariant &debug) {
+
+ qLog(Error) << "Qobuz:" << error;
+ if (debug.isValid()) qLog(Debug) << debug;
+
+}
diff --git a/src/qobuz/qobuzfavoriterequest.h b/src/qobuz/qobuzfavoriterequest.h
new file mode 100644
index 00000000..00306796
--- /dev/null
+++ b/src/qobuz/qobuzfavoriterequest.h
@@ -0,0 +1,82 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2019, Jonas Kvinge
+ *
+ * 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 .
+ *
+ */
+
+#ifndef QOBUZFAVORITEREQUEST_H
+#define QOBUZFAVORITEREQUEST_H
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+
+#include "qobuzbaserequest.h"
+#include "core/song.h"
+
+class QNetworkReply;
+class QobuzService;
+class NetworkAccessManager;
+
+class QobuzFavoriteRequest : public QobuzBaseRequest {
+ Q_OBJECT
+
+ public:
+ explicit QobuzFavoriteRequest(QobuzService *service, NetworkAccessManager *network, QObject *parent);
+ ~QobuzFavoriteRequest();
+
+ enum FavoriteType {
+ FavoriteType_Artists,
+ FavoriteType_Albums,
+ FavoriteType_Songs
+ };
+
+ signals:
+ void ArtistsAdded(SongList);
+ void AlbumsAdded(SongList);
+ void SongsAdded(SongList);
+ void ArtistsRemoved(SongList);
+ void AlbumsRemoved(SongList);
+ void SongsRemoved(SongList);
+
+ private slots:
+ void AddArtists(const SongList &songs);
+ void AddAlbums(const SongList &songs);
+ void AddSongs(const SongList &songs);
+
+ void RemoveArtists(const SongList &songs);
+ void RemoveAlbums(const SongList &songs);
+ void RemoveSongs(const SongList &songs);
+
+ void AddFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs);
+ void RemoveFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs);
+
+ private:
+ void Error(const QString &error, const QVariant &debug = QVariant());
+ QString FavoriteText(const FavoriteType type);
+ void AddFavorites(const FavoriteType type, const SongList &songs);
+ void RemoveFavorites(const FavoriteType type, const SongList &songs);
+
+ QobuzService *service_;
+ NetworkAccessManager *network_;
+ QList replies_;
+
+};
+
+#endif // QOBUZFAVORITEREQUEST_H
diff --git a/src/qobuz/qobuzrequest.cpp b/src/qobuz/qobuzrequest.cpp
new file mode 100644
index 00000000..76edc08b
--- /dev/null
+++ b/src/qobuz/qobuzrequest.cpp
@@ -0,0 +1,1343 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2019, Jonas Kvinge
+ *
+ * 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 .
+ *
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/logging.h"
+#include "core/network.h"
+#include "core/song.h"
+#include "core/timeconstants.h"
+#include "core/application.h"
+#include "covermanager/albumcoverloader.h"
+#include "qobuzservice.h"
+#include "qobuzurlhandler.h"
+#include "qobuzbaserequest.h"
+#include "qobuzrequest.h"
+
+const int QobuzRequest::kMaxConcurrentArtistsRequests = 3;
+const int QobuzRequest::kMaxConcurrentAlbumsRequests = 3;
+const int QobuzRequest::kMaxConcurrentSongsRequests = 3;
+const int QobuzRequest::kMaxConcurrentArtistAlbumsRequests = 3;
+const int QobuzRequest::kMaxConcurrentAlbumSongsRequests = 3;
+const int QobuzRequest::kMaxConcurrentAlbumCoverRequests = 1;
+
+QobuzRequest::QobuzRequest(QobuzService *service, QobuzUrlHandler *url_handler, Application *app, NetworkAccessManager *network, QueryType type, QObject *parent)
+ : QobuzBaseRequest(service, network, parent),
+ service_(service),
+ url_handler_(url_handler),
+ app_(app),
+ network_(network),
+ type_(type),
+ query_id_(-1),
+ finished_(false),
+ artists_requests_active_(0),
+ artists_total_(0),
+ artists_received_(0),
+ albums_requests_active_(0),
+ songs_requests_active_(0),
+ artist_albums_requests_active_(0),
+ artist_albums_requested_(0),
+ artist_albums_received_(0),
+ album_songs_requests_active_(0),
+ album_songs_requested_(0),
+ album_songs_received_(0),
+ album_covers_requests_active_(),
+ album_covers_requested_(0),
+ album_covers_received_(0),
+ no_results_(false) {}
+
+QobuzRequest::~QobuzRequest() {
+
+ while (!replies_.isEmpty()) {
+ QNetworkReply *reply = replies_.takeFirst();
+ disconnect(reply, nullptr, this, nullptr);
+ if (reply->isRunning()) reply->abort();
+ reply->deleteLater();
+ }
+
+ while (!album_cover_replies_.isEmpty()) {
+ QNetworkReply *reply = album_cover_replies_.takeFirst();
+ disconnect(reply, nullptr, this, nullptr);
+ if (reply->isRunning()) reply->abort();
+ reply->deleteLater();
+ }
+
+}
+
+void QobuzRequest::Process() {
+
+ switch (type_) {
+ case QueryType::QueryType_Artists:
+ GetArtists();
+ break;
+ case QueryType::QueryType_Albums:
+ GetAlbums();
+ break;
+ case QueryType::QueryType_Songs:
+ GetSongs();
+ break;
+ case QueryType::QueryType_SearchArtists:
+ ArtistsSearch();
+ break;
+ case QueryType::QueryType_SearchAlbums:
+ AlbumsSearch();
+ break;
+ case QueryType::QueryType_SearchSongs:
+ SongsSearch();
+ break;
+ default:
+ Error("Invalid query type.");
+ break;
+ }
+
+}
+
+void QobuzRequest::Search(const int query_id, const QString &search_text) {
+ query_id_ = query_id;
+ search_text_ = search_text;
+}
+
+void QobuzRequest::GetArtists() {
+
+ emit UpdateStatus(query_id_, tr("Retrieving artists..."));
+ emit UpdateProgress(query_id_, 0);
+ AddArtistsRequest();
+
+}
+
+void QobuzRequest::AddArtistsRequest(const int offset, const int limit) {
+
+ Request request;
+ request.limit = limit;
+ request.offset = offset;
+ artists_requests_queue_.enqueue(request);
+ if (artists_requests_active_ < kMaxConcurrentArtistsRequests) FlushArtistsRequests();
+
+}
+
+void QobuzRequest::FlushArtistsRequests() {
+
+ while (!artists_requests_queue_.isEmpty() && artists_requests_active_ < kMaxConcurrentArtistsRequests) {
+
+ Request request = artists_requests_queue_.dequeue();
+ ++artists_requests_active_;
+
+ ParamList params;
+ if (type_ == QueryType_Artists) {
+ params << Param("type", "artists");
+ params << Param("user_auth_token", user_auth_token());
+ }
+ else if (type_ == QueryType_SearchArtists) params << Param("query", search_text_);
+ if (request.limit > 0) params << Param("limit", QString::number(request.limit));
+ if (request.offset > 0) params << Param("offset", QString::number(request.offset));
+ QNetworkReply *reply = nullptr;
+ if (type_ == QueryType_Artists) {
+ reply = CreateRequest(QString("favorite/getUserFavorites"), params);
+ }
+ else if (type_ == QueryType_SearchArtists) {
+ reply = CreateRequest("artist/search", params);
+ }
+ if (!reply) continue;
+ replies_ << reply;
+ connect(reply, &QNetworkReply::finished, [=] { ArtistsReplyReceived(reply, request.limit, request.offset); });
+
+ }
+
+}
+
+void QobuzRequest::GetAlbums() {
+
+ emit UpdateStatus(query_id_, tr("Retrieving albums..."));
+ emit UpdateProgress(query_id_, 0);
+ AddAlbumsRequest();
+
+}
+
+void QobuzRequest::AddAlbumsRequest(const int offset, const int limit) {
+
+ Request request;
+ request.limit = limit;
+ request.offset = offset;
+ albums_requests_queue_.enqueue(request);
+ if (albums_requests_active_ < kMaxConcurrentAlbumsRequests) FlushAlbumsRequests();
+
+}
+
+void QobuzRequest::FlushAlbumsRequests() {
+
+ while (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) {
+
+ Request request = albums_requests_queue_.dequeue();
+ ++albums_requests_active_;
+
+ ParamList params;
+ if (type_ == QueryType_Albums) {
+ params << Param("type", "albums");
+ params << Param("user_auth_token", user_auth_token());
+ }
+ else if (type_ == QueryType_SearchAlbums) params << Param("query", search_text_);
+ if (request.limit > 0) params << Param("limit", QString::number(request.limit));
+ if (request.offset > 0) params << Param("offset", QString::number(request.offset));
+ QNetworkReply *reply = nullptr;
+ if (type_ == QueryType_Albums) {
+ reply = CreateRequest(QString("favorite/getUserFavorites"), params);
+ }
+ else if (type_ == QueryType_SearchAlbums) {
+ reply = CreateRequest("album/search", params);
+ }
+ if (!reply) continue;
+ replies_ << reply;
+ connect(reply, &QNetworkReply::finished, [=] { AlbumsReplyReceived(reply, request.limit, request.offset); });
+
+ }
+
+}
+
+void QobuzRequest::GetSongs() {
+
+ emit UpdateStatus(query_id_, tr("Retrieving songs..."));
+ emit UpdateProgress(query_id_, 0);
+ AddSongsRequest();
+
+}
+
+void QobuzRequest::AddSongsRequest(const int offset, const int limit) {
+
+ Request request;
+ request.limit = limit;
+ request.offset = offset;
+ songs_requests_queue_.enqueue(request);
+ if (songs_requests_active_ < kMaxConcurrentSongsRequests) FlushSongsRequests();
+
+}
+
+void QobuzRequest::FlushSongsRequests() {
+
+ while (!songs_requests_queue_.isEmpty() && songs_requests_active_ < kMaxConcurrentSongsRequests) {
+
+ Request request = songs_requests_queue_.dequeue();
+ ++songs_requests_active_;
+
+ ParamList params;
+ if (type_ == QueryType_Songs) {
+ params << Param("type", "tracks");
+ params << Param("user_auth_token", user_auth_token());
+ }
+ else if (type_ == QueryType_SearchSongs) params << Param("query", search_text_);
+ if (request.limit > 0) params << Param("limit", QString::number(request.limit));
+ if (request.offset > 0) params << Param("offset", QString::number(request.offset));
+ QNetworkReply *reply = nullptr;
+ if (type_ == QueryType_Songs) {
+ reply = CreateRequest(QString("favorite/getUserFavorites"), params);
+ }
+ else if (type_ == QueryType_SearchSongs) {
+ reply = CreateRequest("track/search", params);
+ }
+ if (!reply) continue;
+ replies_ << reply;
+ connect(reply, &QNetworkReply::finished, [=] { SongsReplyReceived(reply, request.limit, request.offset); });
+
+ }
+
+}
+
+void QobuzRequest::ArtistsSearch() {
+
+ emit UpdateStatus(query_id_, tr("Searching..."));
+ emit UpdateProgress(query_id_, 0);
+ AddArtistsSearchRequest();
+
+}
+
+void QobuzRequest::AddArtistsSearchRequest(const int offset) {
+
+ AddArtistsRequest(offset, service_->artistssearchlimit());
+
+}
+
+void QobuzRequest::AlbumsSearch() {
+
+ emit UpdateStatus(query_id_, tr("Searching..."));
+ emit UpdateProgress(query_id_, 0);
+ AddAlbumsSearchRequest();
+
+}
+
+void QobuzRequest::AddAlbumsSearchRequest(const int offset) {
+
+ AddAlbumsRequest(offset, service_->albumssearchlimit());
+
+}
+
+void QobuzRequest::SongsSearch() {
+
+ emit UpdateStatus(query_id_, tr("Searching..."));
+ emit UpdateProgress(query_id_, 0);
+ AddSongsSearchRequest();
+
+}
+
+void QobuzRequest::AddSongsSearchRequest(const int offset) {
+
+ AddSongsRequest(offset, service_->songssearchlimit());
+
+}
+
+void QobuzRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) {
+
+ if (!replies_.contains(reply)) return;
+ replies_.removeAll(reply);
+ disconnect(reply, nullptr, this, nullptr);
+ reply->deleteLater();
+
+ QByteArray data = GetReplyData(reply);
+
+ --artists_requests_active_;
+
+ if (finished_) return;
+
+ if (data.isEmpty()) {
+ ArtistsFinishCheck();
+ return;
+ }
+
+ QJsonObject json_obj = ExtractJsonObj(data);
+ if (json_obj.isEmpty()) {
+ ArtistsFinishCheck();
+ return;
+ }
+
+ if (!json_obj.contains("artists")) {
+ ArtistsFinishCheck();
+ Error("Json object is missing artists.", json_obj);
+ return;
+ }
+ QJsonValue value_artists = json_obj["artists"];
+ if (!value_artists.isObject()) {
+ Error("Json artists is not an object.", json_obj);
+ ArtistsFinishCheck();
+ return;
+ }
+ QJsonObject obj_artists = value_artists.toObject();
+
+ if (!obj_artists.contains("limit") ||
+ !obj_artists.contains("offset") ||
+ !obj_artists.contains("total") ||
+ !obj_artists.contains("items")) {
+ ArtistsFinishCheck();
+ Error("Json artists object is missing values.", json_obj);
+ return;
+ }
+ //int limit = obj_artists["limit"].toInt();
+ int offset = obj_artists["offset"].toInt();
+ int artists_total = obj_artists["total"].toInt();
+
+ if (offset_requested == 0) {
+ artists_total_ = artists_total;
+ }
+ else if (artists_total != artists_total_) {
+ Error(QString("total returned does not match previous total! %1 != %2").arg(artists_total).arg(artists_total_));
+ ArtistsFinishCheck();
+ return;
+ }
+
+ if (offset != offset_requested) {
+ Error(QString("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested));
+ ArtistsFinishCheck();
+ return;
+ }
+
+ if (offset_requested == 0) {
+ emit ProgressSetMaximum(query_id_, artists_total_);
+ emit UpdateProgress(query_id_, artists_received_);
+ }
+
+ QJsonValue value_items = ExtractItems(obj_artists);
+ if (!value_items.isArray()) {
+ ArtistsFinishCheck();
+ return;
+ }
+
+ QJsonArray array_items = value_items.toArray();
+ if (array_items.isEmpty()) { // Empty array means no results
+ if (offset_requested == 0) no_results_ = true;
+ ArtistsFinishCheck();
+ return;
+ }
+
+ int artists_received = 0;
+ for (const QJsonValue &value_item : array_items) {
+
+ ++artists_received;
+
+ if (!value_item.isObject()) {
+ Error("Invalid Json reply, item not a object.", value_item);
+ continue;
+ }
+ QJsonObject obj_item = value_item.toObject();
+
+ if (obj_item.contains("item")) {
+ QJsonValue json_item = obj_item["item"];
+ if (!json_item.isObject()) {
+ Error("Invalid Json reply, item not a object.", json_item);
+ continue;
+ }
+ obj_item = json_item.toObject();
+ }
+
+ if (!obj_item.contains("id") || !obj_item.contains("name")) {
+ Error("Invalid Json reply, item missing id or album.", obj_item);
+ continue;
+ }
+
+ QString artist_id;
+ if (obj_item["id"].isString()) {
+ artist_id = obj_item["id"].toString();
+ }
+ else {
+ artist_id = QString::number(obj_item["id"].toInt());
+ }
+ if (artist_albums_requests_pending_.contains(artist_id)) continue;
+ artist_albums_requests_pending_.append(artist_id);
+
+ }
+ artists_received_ += artists_received;
+
+ if (offset_requested != 0) emit UpdateProgress(query_id_, artists_received_);
+
+ ArtistsFinishCheck(limit_requested, offset, artists_received);
+
+}
+
+void QobuzRequest::ArtistsFinishCheck(const int limit, const int offset, const int artists_received) {
+
+ if (finished_) return;
+
+ if ((limit == 0 || limit > artists_received) && artists_received_ < artists_total_) {
+ int offset_next = offset + artists_received;
+ if (offset_next > 0 && offset_next < artists_total_) {
+ if (type_ == QueryType_Artists) AddArtistsRequest(offset_next);
+ else if (type_ == QueryType_SearchArtists) AddArtistsSearchRequest(offset_next);
+ }
+ }
+
+ if (!artists_requests_queue_.isEmpty() && artists_requests_active_ < kMaxConcurrentArtistsRequests) FlushArtistsRequests();
+
+ if (artists_requests_queue_.isEmpty() && artists_requests_active_ <= 0) { // Artist query is finished, get all albums for all artists.
+
+ // Get artist albums
+ for (const QString &artist_id : artist_albums_requests_pending_) {
+ AddArtistAlbumsRequest(artist_id);
+ ++artist_albums_requested_;
+ }
+ artist_albums_requests_pending_.clear();
+
+ if (artist_albums_requested_ > 0) {
+ if (artist_albums_requested_ == 1) emit UpdateStatus(query_id_, tr("Retrieving albums for %1 artist...").arg(artist_albums_requested_));
+ else emit UpdateStatus(query_id_, tr("Retrieving albums for %1 artists...").arg(artist_albums_requested_));
+ emit ProgressSetMaximum(query_id_, artist_albums_requested_);
+ emit UpdateProgress(query_id_, 0);
+ }
+
+ }
+
+ FinishCheck();
+
+}
+
+void QobuzRequest::AlbumsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) {
+ --albums_requests_active_;
+ AlbumsReceived(reply, QString(), limit_requested, offset_requested);
+ if (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) FlushAlbumsRequests();
+}
+
+void QobuzRequest::AddArtistAlbumsRequest(const QString &artist_id, const int offset) {
+
+ Request request;
+ request.artist_id = artist_id;
+ request.offset = offset;
+ artist_albums_requests_queue_.enqueue(request);
+ if (artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) FlushArtistAlbumsRequests();
+
+}
+
+void QobuzRequest::FlushArtistAlbumsRequests() {
+
+ while (!artist_albums_requests_queue_.isEmpty() && artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) {
+
+ Request request = artist_albums_requests_queue_.dequeue();
+ ++artist_albums_requests_active_;
+
+ ParamList params = ParamList() << Param("artist_id", request.artist_id)
+ << Param("extra", "albums");
+
+ if (request.offset > 0) params << Param("offset", QString::number(request.offset));
+ QNetworkReply *reply = CreateRequest(QString("artist/get"), params);
+ connect(reply, &QNetworkReply::finished, [=] { ArtistAlbumsReplyReceived(reply, request.artist_id, request.offset); });
+ replies_ << reply;
+
+ }
+
+}
+
+void QobuzRequest::ArtistAlbumsReplyReceived(QNetworkReply *reply, const QString artist_id, const int offset_requested) {
+
+ --artist_albums_requests_active_;
+ ++artist_albums_received_;
+ emit UpdateProgress(query_id_, artist_albums_received_);
+ AlbumsReceived(reply, artist_id, 0, offset_requested);
+ if (!artist_albums_requests_queue_.isEmpty() && artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) FlushArtistAlbumsRequests();
+
+}
+
+void QobuzRequest::AlbumsReceived(QNetworkReply *reply, const QString &artist_id_requested, const int limit_requested, const int offset_requested) {
+
+ if (!replies_.contains(reply)) return;
+ replies_.removeAll(reply);
+ disconnect(reply, nullptr, this, nullptr);
+ reply->deleteLater();
+
+ QByteArray data = GetReplyData(reply);
+
+ if (finished_) return;
+
+ if (data.isEmpty()) {
+ AlbumsFinishCheck(artist_id_requested);
+ return;
+ }
+
+ QJsonObject json_obj = ExtractJsonObj(data);
+ if (json_obj.isEmpty()) {
+ AlbumsFinishCheck(artist_id_requested);
+ return;
+ }
+
+ QString album_artist_id = artist_id_requested;
+ if (json_obj.contains("id")) {
+ if (json_obj["id"].isString()) {
+ album_artist_id = json_obj["id"].toString();
+ }
+ else {
+ album_artist_id = QString::number(json_obj["id"].toInt());
+ }
+ }
+ QString album_artist;
+ if (json_obj.contains("name")) {
+ album_artist = json_obj["name"].toString();
+ }
+
+ if (album_artist_id != artist_id_requested) {
+ AlbumsFinishCheck(artist_id_requested);
+ Error("Artist id returned does not match artist id requested.", json_obj);
+ return;
+ }
+
+ if (!json_obj.contains("albums")) {
+ AlbumsFinishCheck(artist_id_requested);
+ Error("Json object is missing albums.", json_obj);
+ return;
+ }
+ QJsonValue value_albums = json_obj["albums"];
+ if (!value_albums.isObject()) {
+ Error("Json albums is not an object.", json_obj);
+ AlbumsFinishCheck(artist_id_requested);
+ return;
+ }
+ QJsonObject obj_albums = value_albums.toObject();
+
+ if (!obj_albums.contains("limit") ||
+ !obj_albums.contains("offset") ||
+ !obj_albums.contains("total") ||
+ !obj_albums.contains("items")) {
+ AlbumsFinishCheck(artist_id_requested);
+ Error("Json albums object is missing values.", json_obj);
+ return;
+ }
+
+ //int limit = json_obj["limit"].toInt();
+ int offset = json_obj["offset"].toInt();
+ int albums_total = json_obj["total"].toInt();
+
+ if (offset != offset_requested) {
+ Error(QString("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested));
+ AlbumsFinishCheck(artist_id_requested);
+ return;
+ }
+
+ QJsonValue value_items = ExtractItems(obj_albums);
+ if (!value_items.isArray()) {
+ AlbumsFinishCheck(artist_id_requested);
+ return;
+ }
+ QJsonArray array_items = value_items.toArray();
+ if (array_items.isEmpty()) {
+ if ((type_ == QueryType_Albums || type_ == QueryType_SearchAlbums) && offset_requested == 0) {
+ no_results_ = true;
+ }
+ AlbumsFinishCheck(artist_id_requested);
+ return;
+ }
+
+ int albums_received = 0;
+ for (const QJsonValue &value : array_items) {
+
+ ++albums_received;
+
+ if (!value.isObject()) {
+ Error("Invalid Json reply, item not a object.", value);
+ continue;
+ }
+ QJsonObject obj_item = value.toObject();
+
+ if (!obj_item.contains("artist") || !obj_item.contains("title") || !obj_item.contains("id")) {
+ Error("Invalid Json reply, item missing artist, title or id.", obj_item);
+ continue;
+ }
+
+ QString album_id;
+ if (obj_item["id"].isString()) {
+ album_id = obj_item["id"].toString();
+ }
+ else {
+ album_id = QString::number(obj_item["id"].toInt());
+ }
+
+ if (album_songs_requests_pending_.contains(album_id)) continue;
+
+ QString album = obj_item["title"].toString();
+
+ QJsonValue value_artist = obj_item["artist"];
+ if (!value_artist.isObject()) {
+ Error("Invalid Json reply, item artist is not a object.", value_artist);
+ continue;
+ }
+ QJsonObject obj_artist = value_artist.toObject();
+ if (!obj_artist.contains("id") || !obj_artist.contains("name")) {
+ Error("Invalid Json reply, item artist missing id or name.", obj_artist);
+ continue;
+ }
+
+ QString artist_id;
+ if (obj_artist["id"].isString()) {
+ artist_id = obj_artist["id"].toString();
+ }
+ else {
+ artist_id = QString::number(obj_artist["id"].toInt());
+ }
+
+ QString artist = obj_artist["name"].toString();
+ if (artist_id_requested != 0 && artist_id != artist_id_requested) {
+ qLog(Debug) << "Skipping artist" << "artist" << artist << artist_id << "does not match album artist" << album_artist_id << album_artist;
+ continue;
+ }
+
+ Request request;
+ request.artist_id = artist_id;
+ request.album_id = album_id;
+ request.album_artist = artist;
+ request.album = album;
+ album_songs_requests_pending_.insert(album_id, request);
+
+ }
+
+ AlbumsFinishCheck(artist_id_requested, limit_requested, offset, albums_total, albums_received);
+
+}
+
+void QobuzRequest::AlbumsFinishCheck(const QString &artist_id, const int limit, const int offset, const int albums_total, const int albums_received) {
+
+ if (finished_) return;
+
+ if (limit == 0 || limit > albums_received) {
+ int offset_next = offset + albums_received;
+ if (offset_next > 0 && offset_next < albums_total) {
+ switch (type_) {
+ case QueryType_Albums:
+ AddAlbumsRequest(offset_next);
+ break;
+ case QueryType_SearchAlbums:
+ AddAlbumsSearchRequest(offset_next);
+ break;
+ case QueryType_Artists:
+ case QueryType_SearchArtists:
+ AddArtistAlbumsRequest(artist_id, offset_next);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ if (
+ albums_requests_queue_.isEmpty() &&
+ albums_requests_active_ <= 0 &&
+ artist_albums_requests_queue_.isEmpty() &&
+ artist_albums_requests_active_ <= 0
+ ) { // Artist albums query is finished, get all songs for all albums.
+
+ // Get songs for all the albums.
+
+ QHash ::iterator it;
+ for (it = album_songs_requests_pending_.begin() ; it != album_songs_requests_pending_.end() ; ++it) {
+ Request request = it.value();
+ AddAlbumSongsRequest(request.artist_id, request.album_id, request.album_artist, request.album);
+ }
+ album_songs_requests_pending_.clear();
+
+ if (album_songs_requested_ > 0) {
+ if (album_songs_requested_ == 1) emit UpdateStatus(query_id_, tr("Retrieving songs for %1 album...").arg(album_songs_requested_));
+ else emit UpdateStatus(query_id_, tr("Retrieving songs for %1 albums...").arg(album_songs_requested_));
+ emit ProgressSetMaximum(query_id_, album_songs_requested_);
+ emit UpdateProgress(query_id_, 0);
+ }
+ }
+
+ FinishCheck();
+
+}
+
+void QobuzRequest::SongsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) {
+
+ --songs_requests_active_;
+ SongsReceived(reply, QString(), QString(), limit_requested, offset_requested);
+
+}
+
+void QobuzRequest::AddAlbumSongsRequest(const QString &artist_id, const QString &album_id, const QString &album_artist, const QString &album, const int offset) {
+
+ Request request;
+ request.artist_id = artist_id;
+ request.album_id = album_id;
+ request.album_artist = album_artist;
+ request.album = album;
+ request.offset = offset;
+ album_songs_requests_queue_.enqueue(request);
+ ++album_songs_requested_;
+ if (album_songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) FlushAlbumSongsRequests();
+
+}
+
+void QobuzRequest::FlushAlbumSongsRequests() {
+
+ while (!album_songs_requests_queue_.isEmpty() && album_songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) {
+
+ Request request = album_songs_requests_queue_.dequeue();
+ ++album_songs_requests_active_;
+ ParamList params = ParamList() << Param("album_id", request.album_id);
+ if (request.offset > 0) params << Param("offset", QString::number(request.offset));
+ QNetworkReply *reply = CreateRequest(QString("album/get"), params);
+ replies_ << reply;
+ connect(reply, &QNetworkReply::finished, [=] { AlbumSongsReplyReceived(reply, request.artist_id, request.album_id, request.offset, request.album_artist, request.album); });
+
+ }
+
+}
+
+void QobuzRequest::AlbumSongsReplyReceived(QNetworkReply *reply, const QString &artist_id, const QString &album_id, const int offset_requested, const QString &album_artist, const QString &album) {
+
+ --album_songs_requests_active_;
+ ++album_songs_received_;
+ if (offset_requested == 0) {
+ emit UpdateProgress(query_id_, album_songs_received_);
+ }
+ SongsReceived(reply, artist_id, album_id, 0, offset_requested, album_artist, album);
+
+}
+
+void QobuzRequest::SongsReceived(QNetworkReply *reply, const QString &artist_id_requested, const QString &album_id_requested, const int limit_requested, const int offset_requested, const QString &album_artist_requested, const QString &album_requested) {
+
+ if (!replies_.contains(reply)) return;
+ replies_.removeAll(reply);
+ reply->deleteLater();
+
+ QByteArray data = GetReplyData(reply);
+
+ if (finished_) return;
+
+ if (data.isEmpty()) {
+ SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested, album_requested);
+ return;
+ }
+
+ QJsonObject json_obj = ExtractJsonObj(data);
+ if (json_obj.isEmpty()) {
+ SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested, album_requested);
+ return;
+ }
+
+ if (!json_obj.contains("tracks")) {
+ Error("Json object is missing tracks.", json_obj);
+ SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested, album_requested);
+ return;
+ }
+
+ QString artist_id = artist_id_requested;
+ QString album_artist = album_artist_requested;
+ QString album_id = album_id_requested;
+ QString album = album_requested;
+ QUrl cover_url;
+
+ if (json_obj.contains("id")) {
+ if (json_obj["id"].isString()) {
+ album_id = json_obj["id"].toString();
+ }
+ else {
+ album_id = QString::number(json_obj["id"].toInt());
+ }
+ }
+
+ if (json_obj.contains("title")) {
+ album = json_obj["title"].toString();
+ }
+
+ if (json_obj.contains("artist")) {
+ QJsonValue value_artist = json_obj["artist"];
+ if (!value_artist.isObject()) {
+ Error("Invalid Json reply, album artist is not a object.", value_artist);
+ SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album);
+ return;
+ }
+ QJsonObject obj_artist = value_artist.toObject();
+ if (!obj_artist.contains("id") || !obj_artist.contains("name")) {
+ Error("Invalid Json reply, album artist is missing id or name.", obj_artist);
+ SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album);
+ return;
+ }
+ if (obj_artist["id"].isString()) {
+ artist_id = obj_artist["id"].toString();
+ }
+ else {
+ artist_id = QString::number(obj_artist["id"].toInt());
+ }
+ album_artist = obj_artist["name"].toString();
+ }
+
+ if (json_obj.contains("image")) {
+ QJsonValue value_image = json_obj["image"];
+ if (!value_image.isObject()) {
+ Error("Invalid Json reply, album image is not a object.", value_image);
+ SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album);
+ return;
+ }
+ QJsonObject obj_image = value_image.toObject();
+ if (!obj_image.contains("large")) {
+ Error("Invalid Json reply, album image is missing large.", obj_image);
+ SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album);
+ return;
+ }
+ QString album_image = obj_image["large"].toString();
+ if (!album_image.isEmpty()) {
+ cover_url = QUrl(album_image);
+ }
+ }
+
+ QJsonValue value_tracks = json_obj["tracks"];
+ if (!value_tracks.isObject()) {
+ Error("Json tracks is not an object.", json_obj);
+ SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album);
+ return;
+ }
+ QJsonObject obj_tracks = value_tracks.toObject();
+
+ if (!obj_tracks.contains("limit") ||
+ !obj_tracks.contains("offset") ||
+ !obj_tracks.contains("total") ||
+ !obj_tracks.contains("items")) {
+ SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album);
+ Error("Json songs object is missing values.", json_obj);
+ return;
+ }
+
+ //int limit = obj_tracks["limit"].toInt();
+ int offset = obj_tracks["offset"].toInt();
+ int songs_total = obj_tracks["total"].toInt();
+
+ if (offset != offset_requested) {
+ Error(QString("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested));
+ SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist, album);
+ return;
+ }
+
+ QJsonValue value_items = ExtractItems(obj_tracks);
+ if (!value_items.isArray()) {
+ SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist, album);
+ return;
+ }
+
+ QJsonArray array_items = value_items.toArray();
+ if (array_items.isEmpty()) {
+ if ((type_ == QueryType_Songs || type_ == QueryType_SearchSongs) && offset_requested == 0) {
+ no_results_ = true;
+ }
+ SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist, album);
+ return;
+ }
+
+ bool compilation = false;
+ //bool multidisc = false;
+ SongList songs;
+ int songs_received = 0;
+ for (const QJsonValue &value_item : array_items) {
+
+ if (!value_item.isObject()) {
+ Error("Invalid Json reply, track is not a object.", value_item);
+ continue;
+ }
+ QJsonObject obj_item = value_item.toObject();
+
+ ++songs_received;
+ Song song(Song::Source_Qobuz);
+ ParseSong(song, obj_item, artist_id, album_id, album_artist, album, cover_url);
+ if (!song.is_valid()) continue;
+ //if (song.disc() >= 2) multidisc = true;
+ if (song.is_compilation()) compilation = true;
+ songs << song;
+ }
+
+ for (Song &song : songs) {
+ if (compilation) song.set_compilation_detected(true);
+ //if (multidisc) {
+ //QString album_full(QString("%1 - (Disc %2)").arg(song.album()).arg(song.disc()));
+ //song.set_album(album_full);
+ //}
+ songs_ << song;
+ }
+
+ SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, songs_received, album_artist, album);
+
+}
+
+void QobuzRequest::SongsFinishCheck(const QString &artist_id, const QString &album_id, const int limit, const int offset, const int songs_total, const int songs_received, const QString &album_artist, const QString &album) {
+
+ if (finished_) return;
+
+ if (limit == 0 || limit > songs_received) {
+ int offset_next = offset + songs_received;
+ if (offset_next > 0 && offset_next < songs_total) {
+ switch (type_) {
+ case QueryType_Songs:
+ AddSongsRequest(offset_next);
+ break;
+ case QueryType_SearchSongs:
+ AddSongsSearchRequest(offset_next);
+ break;
+ case QueryType_Artists:
+ case QueryType_SearchArtists:
+ case QueryType_Albums:
+ case QueryType_SearchAlbums:
+ AddAlbumSongsRequest(artist_id, album_id, album_artist, album, offset_next);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ if (!songs_requests_queue_.isEmpty() && songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) FlushAlbumSongsRequests();
+ if (!album_songs_requests_queue_.isEmpty() && album_songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) FlushAlbumSongsRequests();
+
+ if (
+ service_->download_album_covers() &&
+ IsQuery() &&
+ songs_requests_queue_.isEmpty() &&
+ songs_requests_active_ <= 0 &&
+ album_songs_requests_queue_.isEmpty() &&
+ album_songs_requests_active_ <= 0 &&
+ album_cover_requests_queue_.isEmpty() &&
+ album_covers_received_ <= 0 &&
+ album_covers_requests_sent_.isEmpty() &&
+ album_songs_received_ >= album_songs_requested_
+ ) {
+ GetAlbumCovers();
+ }
+
+ FinishCheck();
+
+}
+
+QString QobuzRequest::ParseSong(Song &song, const QJsonObject &json_obj, QString artist_id, QString album_id, QString album_artist, QString album, QUrl cover_url) {
+
+ if (
+ !json_obj.contains("id") ||
+ !json_obj.contains("title") ||
+ !json_obj.contains("track_number") ||
+ !json_obj.contains("duration") ||
+ !json_obj.contains("copyright") ||
+ !json_obj.contains("streamable")
+ ) {
+ Error("Invalid Json reply, track is missing one or more values.", json_obj);
+ return QString();
+ }
+
+ QString song_id;
+ if (json_obj["id"].isString()) {
+ song_id = json_obj["id"].toString();
+ }
+ else {
+ song_id = QString::number(json_obj["id"].toInt());
+ }
+
+ QString title = json_obj["title"].toString();
+ int track = json_obj["track_number"].toInt();
+ QString copyright = json_obj["copyright"].toString();
+ quint64 duration = json_obj["duration"].toInt() * kNsecPerSec;
+ //bool streamable = json_obj["streamable"].toBool();
+ QString composer;
+ QString performer;
+
+ if (json_obj.contains("album")) {
+
+ QJsonValue value_album = json_obj["album"];
+ if (!value_album.isObject()) {
+ Error("Invalid Json reply, album is not an object.", value_album);
+ return QString();
+ }
+ QJsonObject obj_album = value_album.toObject();
+
+ if (obj_album.contains("id")) {
+ if (obj_album["id"].isString()) {
+ album_id = obj_album["id"].toString();
+ }
+ else {
+ album_id = QString::number(obj_album["id"].toInt());
+ }
+ }
+
+ if (obj_album.contains("title")) {
+ album = obj_album["title"].toString();
+ }
+
+ if (obj_album.contains("artist")) {
+ QJsonValue value_artist = obj_album["artist"];
+ if (!value_artist.isObject()) {
+ Error("Invalid Json reply, album artist is not a object.", value_artist);
+ return QString();
+ }
+ QJsonObject obj_artist = value_artist.toObject();
+ if (!obj_artist.contains("id") || !obj_artist.contains("name")) {
+ Error("Invalid Json reply, album artist is missing id or name.", obj_artist);
+ return QString();
+ }
+ if (obj_artist["id"].isString()) {
+ artist_id = obj_artist["id"].toString();
+ }
+ else {
+ artist_id = QString::number(obj_artist["id"].toInt());
+ }
+ album_artist = obj_artist["name"].toString();
+ }
+
+ if (obj_album.contains("image")) {
+ QJsonValue value_image = obj_album["image"];
+ if (!value_image.isObject()) {
+ Error("Invalid Json reply, album image is not a object.", value_image);
+ return QString();
+ }
+ QJsonObject obj_image = value_image.toObject();
+ if (!obj_image.contains("large")) {
+ Error("Invalid Json reply, album image is missing large.", obj_image);
+ return QString();
+ }
+ QString album_image = obj_image["large"].toString();
+ if (!album_image.isEmpty()) {
+ cover_url = QUrl(album_image);
+ }
+ }
+ }
+
+ if (json_obj.contains("composer")) {
+ QJsonValue value_composer = json_obj["composer"];
+ if (!value_composer.isObject()) {
+ Error("Invalid Json reply, track composer is not a object.", value_composer);
+ return QString();
+ }
+ QJsonObject obj_composer = value_composer.toObject();
+ if (!obj_composer.contains("id") || !obj_composer.contains("name")) {
+ Error("Invalid Json reply, track composer is missing id or name.", obj_composer);
+ return QString();
+ }
+ composer = obj_composer["name"].toString();
+ }
+
+ if (json_obj.contains("performer")) {
+ QJsonValue value_performer = json_obj["performer"];
+ if (!value_performer.isObject()) {
+ Error("Invalid Json reply, track performer is not a object.", value_performer);
+ return QString();
+ }
+ QJsonObject obj_performer = value_performer.toObject();
+ if (!obj_performer.contains("id") || !obj_performer.contains("name")) {
+ Error("Invalid Json reply, track performer is missing id or name.", obj_performer);
+ return QString();
+ }
+ performer = obj_performer["name"].toString();
+ }
+
+ //if (!streamable) {
+ //Warn(QString("Song %1 %2 %3 is not streamable").arg(album_artist).arg(album).arg(title));
+ //}
+
+ QUrl url;
+ url.setScheme(url_handler_->scheme());
+ url.setPath(song_id);
+
+ title.remove(Song::kTitleRemoveMisc);
+
+ //qLog(Debug) << "id" << song_id << "track" << track << "title" << title << "album" << album << "album artist" << album_artist << cover_url << streamable << url;
+
+ song.set_source(Song::Source_Qobuz);
+ song.set_song_id(song_id);
+ song.set_album_id(album_id);
+ song.set_artist_id(artist_id);
+ song.set_album(album);
+ song.set_artist(album_artist);
+ song.set_title(title);
+ song.set_track(track);
+ song.set_url(url);
+ song.set_length_nanosec(duration);
+ song.set_art_automatic(cover_url);
+ song.set_comment(copyright);
+ song.set_directory_id(0);
+ song.set_filetype(Song::FileType_Stream);
+ song.set_filesize(0);
+ song.set_mtime(0);
+ song.set_ctime(0);
+ song.set_valid(true);
+
+ return song_id;
+
+}
+
+void QobuzRequest::GetAlbumCovers() {
+
+ for (Song &song : songs_) {
+ AddAlbumCoverRequest(song);
+ }
+ FlushAlbumCoverRequests();
+
+ if (album_covers_requested_ == 1) emit UpdateStatus(query_id_, tr("Retrieving album cover for %1 album...").arg(album_covers_requested_));
+ else emit UpdateStatus(query_id_, tr("Retrieving album covers for %1 albums...").arg(album_covers_requested_));
+ emit ProgressSetMaximum(query_id_, album_covers_requested_);
+ emit UpdateProgress(query_id_, 0);
+
+}
+
+void QobuzRequest::AddAlbumCoverRequest(Song &song) {
+
+ QUrl cover_url(song.art_automatic());
+ if (!cover_url.isValid()) return;
+
+ if (album_covers_requests_sent_.contains(cover_url)) {
+ album_covers_requests_sent_.insert(cover_url, &song);
+ return;
+ }
+
+ AlbumCoverRequest request;
+ request.url = cover_url;
+ request.filename = app_->album_cover_loader()->CoverFilePath(song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), cover_url);
+ if (request.filename.isEmpty()) return;
+
+ album_covers_requests_sent_.insert(cover_url, &song);
+ ++album_covers_requested_;
+
+ album_cover_requests_queue_.enqueue(request);
+
+}
+
+void QobuzRequest::FlushAlbumCoverRequests() {
+
+ while (!album_cover_requests_queue_.isEmpty() && album_covers_requests_active_ < kMaxConcurrentAlbumCoverRequests) {
+
+ AlbumCoverRequest request = album_cover_requests_queue_.dequeue();
+ ++album_covers_requests_active_;
+
+ QNetworkRequest req(request.url);
+#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
+ req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
+#else
+ req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+#endif
+ QNetworkReply *reply = network_->get(req);
+ album_cover_replies_ << reply;
+ connect(reply, &QNetworkReply::finished, [=] { AlbumCoverReceived(reply, request.url, request.filename); });
+
+ }
+
+}
+
+void QobuzRequest::AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_url, const QString &filename) {
+
+ if (album_cover_replies_.contains(reply)) {
+ album_cover_replies_.removeAll(reply);
+ reply->deleteLater();
+ }
+ else {
+ AlbumCoverFinishCheck();
+ return;
+ }
+
+ --album_covers_requests_active_;
+ ++album_covers_received_;
+
+ if (finished_) return;
+
+ emit UpdateProgress(query_id_, album_covers_received_);
+
+ if (!album_covers_requests_sent_.contains(cover_url)) {
+ AlbumCoverFinishCheck();
+ return;
+ }
+
+ if (reply->error() != QNetworkReply::NoError) {
+ Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
+ album_covers_requests_sent_.remove(cover_url);
+ AlbumCoverFinishCheck();
+ return;
+ }
+
+ if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
+ Error(QString("Received HTTP code %1 for %2.").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()).arg(cover_url.toString()));
+ if (album_covers_requests_sent_.contains(cover_url)) album_covers_requests_sent_.remove(cover_url);
+ AlbumCoverFinishCheck();
+ return;
+ }
+
+ QString mimetype = reply->header(QNetworkRequest::ContentTypeHeader).toString();
+ if (!QImageReader::supportedMimeTypes().contains(mimetype.toUtf8())) {
+ Error(QString("Unsupported mimetype for image reader %1 for %2").arg(mimetype).arg(cover_url.toString()));
+ if (album_covers_requests_sent_.contains(cover_url)) album_covers_requests_sent_.remove(cover_url);
+ AlbumCoverFinishCheck();
+ return;
+ }
+
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
+ QList format_list = QImageReader::imageFormatsForMimeType(mimetype.toUtf8());
+#else
+ QList format_list = Utilities::ImageFormatsForMimeType(mimetype.toUtf8());
+#endif
+
+ QByteArray data = reply->readAll();
+ if (format_list.isEmpty() || data.isEmpty()) {
+ Error(QString("Received empty image data for %1").arg(cover_url.toString()));
+ album_covers_requests_sent_.remove(cover_url);
+ AlbumCoverFinishCheck();
+ return;
+ }
+ QByteArray format = format_list.first();
+
+ QImage image;
+ if (image.loadFromData(data, format)) {
+ if (image.save(filename, format)) {
+ while (album_covers_requests_sent_.contains(cover_url)) {
+ Song *song = album_covers_requests_sent_.take(cover_url);
+ song->set_art_automatic(QUrl::fromLocalFile(filename));
+ }
+ }
+
+ }
+ else {
+ album_covers_requests_sent_.remove(cover_url);
+ Error(QString("Error decoding image data from %1").arg(cover_url.toString()));
+ }
+
+ AlbumCoverFinishCheck();
+
+}
+
+void QobuzRequest::AlbumCoverFinishCheck() {
+
+ if (!album_cover_requests_queue_.isEmpty() && album_covers_requests_active_ < kMaxConcurrentAlbumCoverRequests)
+ FlushAlbumCoverRequests();
+
+ FinishCheck();
+
+}
+
+void QobuzRequest::FinishCheck() {
+
+ if (
+ !finished_ &&
+ albums_requests_queue_.isEmpty() &&
+ artists_requests_queue_.isEmpty() &&
+ songs_requests_queue_.isEmpty() &&
+ artist_albums_requests_queue_.isEmpty() &&
+ album_songs_requests_queue_.isEmpty() &&
+ album_cover_requests_queue_.isEmpty() &&
+ artist_albums_requests_pending_.isEmpty() &&
+ album_songs_requests_pending_.isEmpty() &&
+ album_covers_requests_sent_.isEmpty() &&
+ artists_requests_active_ <= 0 &&
+ albums_requests_active_ <= 0 &&
+ songs_requests_active_ <= 0 &&
+ artist_albums_requests_active_ <= 0 &&
+ artist_albums_received_ >= artist_albums_requested_ &&
+ album_songs_requests_active_ <= 0 &&
+ album_songs_received_ >= album_songs_requested_ &&
+ album_covers_requested_ <= album_covers_received_ &&
+ album_covers_requests_active_ <= 0 &&
+ album_covers_received_ >= album_covers_requested_
+ ) {
+ finished_ = true;
+ if (no_results_ && songs_.isEmpty()) {
+ if (IsSearch())
+ emit Results(query_id_, SongList(), tr("No match."));
+ else
+ emit Results(query_id_, SongList(), QString());
+ }
+ else {
+ if (songs_.isEmpty() && errors_.isEmpty())
+ emit Results(query_id_, songs_, tr("Unknown error"));
+ else
+ emit Results(query_id_, songs_, ErrorsToHTML(errors_));
+ }
+ }
+
+}
+
+void QobuzRequest::Error(const QString &error, const QVariant &debug) {
+
+ if (!error.isEmpty()) {
+ errors_ << error;
+ qLog(Error) << "Qobuz:" << error;
+ }
+ if (debug.isValid()) qLog(Debug) << debug;
+ FinishCheck();
+
+}
+
+void QobuzRequest::Warn(const QString &error, const QVariant &debug) {
+
+ qLog(Error) << "Qobuz:" << error;
+ if (debug.isValid()) qLog(Debug) << debug;
+
+}
+
diff --git a/src/qobuz/qobuzrequest.h b/src/qobuz/qobuzrequest.h
new file mode 100644
index 00000000..1aeff0b1
--- /dev/null
+++ b/src/qobuz/qobuzrequest.h
@@ -0,0 +1,205 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2019, Jonas Kvinge
+ *
+ * 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 .
+ *
+ */
+
+#ifndef QOBUZREQUEST_H
+#define QOBUZREQUEST_H
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/song.h"
+#include "qobuzbaserequest.h"
+
+class QNetworkReply;
+class Application;
+class NetworkAccessManager;
+class QobuzService;
+class QobuzUrlHandler;
+
+class QobuzRequest : public QobuzBaseRequest {
+ Q_OBJECT
+
+ public:
+
+ explicit QobuzRequest(QobuzService *service, QobuzUrlHandler *url_handler, Application *app, NetworkAccessManager *network, QueryType type, QObject *parent);
+ ~QobuzRequest();
+
+ void ReloadSettings();
+
+ void Process();
+ void Search(const int search_id, const QString &search_text);
+
+ signals:
+ void Login();
+ void Login(const QString &username, const QString &password, const QString &token);
+ void LoginSuccess();
+ void LoginFailure(QString failure_reason);
+ void Results(const int id, const SongList &songs, const QString &error);
+ void UpdateStatus(const int id, const QString &text);
+ void ProgressSetMaximum(const int id, const int max);
+ void UpdateProgress(const int id, const int max);
+ void StreamURLFinished(const QUrl original_url, const QUrl url, const Song::FileType, QString error = QString());
+
+ private slots:
+
+ void ArtistsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested);
+
+ void AlbumsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested);
+ void AlbumsReceived(QNetworkReply *reply, const QString &artist_id_requested, const int limit_requested, const int offset_requested);
+
+ void SongsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested);
+ void SongsReceived(QNetworkReply *reply, const QString &artist_id_requested, const QString &album_id_requested, const int limit_requested, const int offset_requested, const QString &album_artist_requested = QString(), const QString &album_requested = QString());
+
+ void ArtistAlbumsReplyReceived(QNetworkReply *reply, const QString artist_id, const int offset_requested);
+ void AlbumSongsReplyReceived(QNetworkReply *reply, const QString &artist_id, const QString &album_id, const int offset_requested, const QString &album_artist, const QString &album);
+ void AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_url, const QString &filename);
+
+ private:
+
+ struct Request {
+ Request() : offset(0), limit(0) {}
+ QString artist_id;
+ QString album_id;
+ QString song_id;
+ int offset;
+ int limit;
+ QString album_artist;
+ QString album;
+ };
+ struct AlbumCoverRequest {
+ QUrl url;
+ QString filename;
+ };
+
+ bool IsQuery() { return (type_ == QueryType_Artists || type_ == QueryType_Albums || type_ == QueryType_Songs); }
+ bool IsSearch() { return (type_ == QueryType_SearchArtists || type_ == QueryType_SearchAlbums || type_ == QueryType_SearchSongs); }
+
+ void GetArtists();
+ void GetAlbums();
+ void GetSongs();
+
+ void ArtistsSearch();
+ void AlbumsSearch();
+ void SongsSearch();
+
+ void AddArtistsRequest(const int offset = 0, const int limit = 0);
+ void AddArtistsSearchRequest(const int offset = 0);
+ void FlushArtistsRequests();
+ void AddAlbumsRequest(const int offset = 0, const int limit = 0);
+ void AddAlbumsSearchRequest(const int offset = 0);
+ void FlushAlbumsRequests();
+ void AddSongsRequest(const int offset = 0, const int limit = 0);
+ void AddSongsSearchRequest(const int offset = 0);
+ void FlushSongsRequests();
+
+ void ArtistsFinishCheck(const int limit = 0, const int offset = 0, const int artists_received = 0);
+ void AlbumsFinishCheck(const QString &artist_id, const int limit = 0, const int offset = 0, const int albums_total = 0, const int albums_received = 0);
+ void SongsFinishCheck(const QString &artist_id, const QString &album_id, const int limit, const int offset, const int songs_total, const int songs_received, const QString &album_artist, const QString &album);
+
+ void AddArtistAlbumsRequest(const QString &artist_id, const int offset = 0);
+ void FlushArtistAlbumsRequests();
+
+ void AddAlbumSongsRequest(const QString &artist_id, const QString &album_id, const QString &album_artist, const QString &album, const int offset = 0);
+ void FlushAlbumSongsRequests();
+
+ QString ParseSong(Song &song, const QJsonObject &json_obj, QString artist_id, QString album_id, QString album_artist, QString album, QUrl cover_url);
+
+ QString AlbumCoverFileName(const Song &song);
+
+ void GetAlbumCovers();
+ void AddAlbumCoverRequest(Song &song);
+ void FlushAlbumCoverRequests();
+ void AlbumCoverFinishCheck();
+
+ void FinishCheck();
+ void Warn(const QString &error, const QVariant &debug = QVariant());
+ void Error(const QString &error, const QVariant &debug = QVariant()) override;
+
+ static const int kMaxConcurrentArtistsRequests;
+ static const int kMaxConcurrentAlbumsRequests;
+ static const int kMaxConcurrentSongsRequests;
+ static const int kMaxConcurrentArtistAlbumsRequests;
+ static const int kMaxConcurrentAlbumSongsRequests;
+ static const int kMaxConcurrentAlbumCoverRequests;
+
+ QobuzService *service_;
+ QobuzUrlHandler *url_handler_;
+ Application *app_;
+ NetworkAccessManager *network_;
+
+ QueryType type_;
+ int query_id_;
+ QString search_text_;
+
+ bool finished_;
+
+ QQueue artists_requests_queue_;
+ QQueue albums_requests_queue_;
+ QQueue songs_requests_queue_;
+
+ QQueue artist_albums_requests_queue_;
+ QQueue album_songs_requests_queue_;
+ QQueue album_cover_requests_queue_;
+
+ QList artist_albums_requests_pending_;
+ QHash album_songs_requests_pending_;
+ QMultiMap album_covers_requests_sent_;
+
+ int artists_requests_active_;
+ int artists_total_;
+ int artists_received_;
+
+ int albums_requests_active_;
+ int songs_requests_active_;
+
+ int artist_albums_requests_active_;
+ int artist_albums_requested_;
+ int artist_albums_received_;
+
+ int album_songs_requests_active_;
+ int album_songs_requested_;
+ int album_songs_received_;
+
+ int album_covers_requests_active_;
+ int album_covers_requested_;
+ int album_covers_received_;
+
+ SongList songs_;
+ QStringList errors_;
+ bool no_results_;
+ QList replies_;
+ QList album_cover_replies_;
+
+};
+
+#endif // QOBUZREQUEST_H
diff --git a/src/qobuz/qobuzservice.cpp b/src/qobuz/qobuzservice.cpp
new file mode 100644
index 00000000..81af2b98
--- /dev/null
+++ b/src/qobuz/qobuzservice.cpp
@@ -0,0 +1,761 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2019, Jonas Kvinge
+ *
+ * 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 .
+ *
+ */
+
+#include "config.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/application.h"
+#include "core/player.h"
+#include "core/logging.h"
+#include "core/network.h"
+#include "core/database.h"
+#include "core/song.h"
+#include "core/utilities.h"
+#include "internet/internetsearchview.h"
+#include "collection/collectionbackend.h"
+#include "collection/collectionmodel.h"
+#include "qobuzservice.h"
+#include "qobuzurlhandler.h"
+#include "qobuzbaserequest.h"
+#include "qobuzrequest.h"
+#include "qobuzfavoriterequest.h"
+#include "qobuzstreamurlrequest.h"
+#include "settings/settingsdialog.h"
+#include "settings/qobuzsettingspage.h"
+
+using std::shared_ptr;
+
+const Song::Source QobuzService::kSource = Song::Source_Qobuz;
+const char *QobuzService::kAuthUrl = "https://www.qobuz.com/api.json/0.2/user/login";
+const int QobuzService::kLoginAttempts = 2;
+const int QobuzService::kTimeResetLoginAttempts = 60000;
+
+const char *QobuzService::kArtistsSongsTable = "qobuz_artists_songs";
+const char *QobuzService::kAlbumsSongsTable = "qobuz_albums_songs";
+const char *QobuzService::kSongsTable = "qobuz_songs";
+
+const char *QobuzService::kArtistsSongsFtsTable = "qobuz_artists_songs_fts";
+const char *QobuzService::kAlbumsSongsFtsTable = "qobuz_albums_songs_fts";
+const char *QobuzService::kSongsFtsTable = "qobuz_songs_fts";
+
+QobuzService::QobuzService(Application *app, QObject *parent)
+ : InternetService(Song::Source_Qobuz, "Qobuz", "qobuz", QobuzSettingsPage::kSettingsGroup, SettingsDialog::Page_Qobuz, app, parent),
+ app_(app),
+ network_(new NetworkAccessManager(this)),
+ url_handler_(new QobuzUrlHandler(app, this)),
+ artists_collection_backend_(nullptr),
+ albums_collection_backend_(nullptr),
+ songs_collection_backend_(nullptr),
+ artists_collection_model_(nullptr),
+ albums_collection_model_(nullptr),
+ songs_collection_model_(nullptr),
+ artists_collection_sort_model_(new QSortFilterProxyModel(this)),
+ albums_collection_sort_model_(new QSortFilterProxyModel(this)),
+ songs_collection_sort_model_(new QSortFilterProxyModel(this)),
+ timer_search_delay_(new QTimer(this)),
+ timer_login_attempt_(new QTimer(this)),
+ favorite_request_(new QobuzFavoriteRequest(this, network_, this)),
+ format_(0),
+ search_delay_(1500),
+ artistssearchlimit_(1),
+ albumssearchlimit_(1),
+ songssearchlimit_(1),
+ download_album_covers_(true),
+ user_id_(-1),
+ credential_id_(-1),
+ pending_search_id_(0),
+ next_pending_search_id_(1),
+ search_id_(0),
+ login_sent_(false),
+ login_attempts_(0)
+ {
+
+ app->player()->RegisterUrlHandler(url_handler_);
+
+ // Backends
+
+ artists_collection_backend_ = new CollectionBackend();
+ artists_collection_backend_->moveToThread(app_->database()->thread());
+ artists_collection_backend_->Init(app_->database(), Song::Source_Qobuz, kArtistsSongsTable, QString(), QString(), kArtistsSongsFtsTable);
+
+ albums_collection_backend_ = new CollectionBackend();
+ albums_collection_backend_->moveToThread(app_->database()->thread());
+ albums_collection_backend_->Init(app_->database(), Song::Source_Qobuz, kAlbumsSongsTable, QString(), QString(), kAlbumsSongsFtsTable);
+
+ songs_collection_backend_ = new CollectionBackend();
+ songs_collection_backend_->moveToThread(app_->database()->thread());
+ songs_collection_backend_->Init(app_->database(), Song::Source_Qobuz, kSongsTable, QString(), QString(), kSongsFtsTable);
+
+ artists_collection_model_ = new CollectionModel(artists_collection_backend_, app_, this);
+ albums_collection_model_ = new CollectionModel(albums_collection_backend_, app_, this);
+ songs_collection_model_ = new CollectionModel(songs_collection_backend_, app_, this);
+
+ artists_collection_sort_model_->setSourceModel(artists_collection_model_);
+ artists_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
+ artists_collection_sort_model_->setDynamicSortFilter(true);
+ artists_collection_sort_model_->setSortLocaleAware(true);
+ artists_collection_sort_model_->sort(0);
+
+ albums_collection_sort_model_->setSourceModel(albums_collection_model_);
+ albums_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
+ albums_collection_sort_model_->setDynamicSortFilter(true);
+ albums_collection_sort_model_->setSortLocaleAware(true);
+ albums_collection_sort_model_->sort(0);
+
+ songs_collection_sort_model_->setSourceModel(songs_collection_model_);
+ songs_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
+ songs_collection_sort_model_->setDynamicSortFilter(true);
+ songs_collection_sort_model_->setSortLocaleAware(true);
+ songs_collection_sort_model_->sort(0);
+
+ // Search
+
+ timer_search_delay_->setSingleShot(true);
+ connect(timer_search_delay_, SIGNAL(timeout()), SLOT(StartSearch()));
+
+ timer_login_attempt_->setSingleShot(true);
+ connect(timer_login_attempt_, SIGNAL(timeout()), SLOT(ResetLoginAttempts()));
+
+ connect(this, SIGNAL(Login()), SLOT(SendLogin()));
+ connect(this, SIGNAL(Login(QString, QString, QString)), SLOT(SendLogin(QString, QString, QString)));
+
+ connect(this, SIGNAL(AddArtists(SongList)), favorite_request_, SLOT(AddArtists(SongList)));
+ connect(this, SIGNAL(AddAlbums(SongList)), favorite_request_, SLOT(AddAlbums(SongList)));
+ connect(this, SIGNAL(AddSongs(SongList)), favorite_request_, SLOT(AddSongs(SongList)));
+
+ connect(this, SIGNAL(RemoveArtists(SongList)), favorite_request_, SLOT(RemoveArtists(SongList)));
+ connect(this, SIGNAL(RemoveAlbums(SongList)), favorite_request_, SLOT(RemoveAlbums(SongList)));
+ connect(this, SIGNAL(RemoveSongs(SongList)), favorite_request_, SLOT(RemoveSongs(SongList)));
+
+ connect(favorite_request_, SIGNAL(ArtistsAdded(SongList)), artists_collection_backend_, SLOT(AddOrUpdateSongs(SongList)));
+ connect(favorite_request_, SIGNAL(AlbumsAdded(SongList)), albums_collection_backend_, SLOT(AddOrUpdateSongs(SongList)));
+ connect(favorite_request_, SIGNAL(SongsAdded(SongList)), songs_collection_backend_, SLOT(AddOrUpdateSongs(SongList)));
+
+ connect(favorite_request_, SIGNAL(ArtistsRemoved(SongList)), artists_collection_backend_, SLOT(DeleteSongs(SongList)));
+ connect(favorite_request_, SIGNAL(AlbumsRemoved(SongList)), albums_collection_backend_, SLOT(DeleteSongs(SongList)));
+ connect(favorite_request_, SIGNAL(SongsRemoved(SongList)), songs_collection_backend_, SLOT(DeleteSongs(SongList)));
+
+ ReloadSettings();
+
+}
+
+QobuzService::~QobuzService() {
+
+ while (!stream_url_requests_.isEmpty()) {
+ QobuzStreamURLRequest *stream_url_req = stream_url_requests_.takeFirst();
+ disconnect(stream_url_req, 0, this, 0);
+ stream_url_req->deleteLater();
+ }
+
+ artists_collection_backend_->deleteLater();
+ albums_collection_backend_->deleteLater();
+ songs_collection_backend_->deleteLater();
+
+}
+
+void QobuzService::Exit() {
+
+ wait_for_exit_ << artists_collection_backend_ << albums_collection_backend_ << songs_collection_backend_;
+
+ connect(artists_collection_backend_, SIGNAL(ExitFinished()), this, SLOT(ExitReceived()));
+ connect(albums_collection_backend_, SIGNAL(ExitFinished()), this, SLOT(ExitReceived()));
+ connect(songs_collection_backend_, SIGNAL(ExitFinished()), this, SLOT(ExitReceived()));
+
+ artists_collection_backend_->ExitAsync();
+ albums_collection_backend_->ExitAsync();
+ songs_collection_backend_->ExitAsync();
+
+}
+
+void QobuzService::ExitReceived() {
+
+ QObject *obj = qobject_cast(sender());
+ disconnect(obj, nullptr, this, nullptr);
+ qLog(Debug) << obj << "successfully exited.";
+ wait_for_exit_.removeAll(obj);
+ if (wait_for_exit_.isEmpty()) emit ExitFinished();
+
+}
+
+void QobuzService::ShowConfig() {
+ app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Qobuz);
+}
+
+void QobuzService::ReloadSettings() {
+
+ QSettings s;
+ s.beginGroup(QobuzSettingsPage::kSettingsGroup);
+
+ app_id_ = s.value("app_id").toString();
+ app_secret_ = s.value("app_secret").toString();
+
+ username_ = s.value("username").toString();
+ QByteArray password = s.value("password").toByteArray();
+ if (password.isEmpty()) password_.clear();
+ else password_ = QString::fromUtf8(QByteArray::fromBase64(password));
+
+ format_ = s.value("format", 27).toInt();
+ search_delay_ = s.value("searchdelay", 1500).toInt();
+ artistssearchlimit_ = s.value("artistssearchlimit", 4).toInt();
+ albumssearchlimit_ = s.value("albumssearchlimit", 10).toInt();
+ songssearchlimit_ = s.value("songssearchlimit", 10).toInt();
+ download_album_covers_ = s.value("downloadalbumcovers", true).toBool();
+
+ user_id_ = s.value("user_id").toInt();
+ device_id_ = s.value("device_id").toString();
+ user_auth_token_ = s.value("user_auth_token").toString();
+
+ s.endGroup();
+
+}
+
+void QobuzService::SendLogin() {
+ SendLogin(app_id_, username_, password_);
+}
+
+void QobuzService::SendLogin(const QString &app_id, const QString &username, const QString &password) {
+
+ emit UpdateStatus(tr("Authenticating..."));
+ login_errors_.clear();
+
+ login_sent_ = true;
+ ++login_attempts_;
+ if (timer_login_attempt_->isActive()) timer_login_attempt_->stop();
+ timer_login_attempt_->setInterval(kTimeResetLoginAttempts);
+ timer_login_attempt_->start();
+
+ const ParamList params = ParamList() << Param("app_id", app_id)
+ << Param("username", username)
+ << Param("password", password)
+ << Param("device_manufacturer_id", Utilities::MacAddress());
+
+ QUrlQuery url_query;
+ for (const Param ¶m : params) {
+ url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
+ }
+
+ QUrl url(kAuthUrl);
+ QNetworkRequest req(url);
+#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
+ req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
+#else
+ req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+#endif
+
+ req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+
+ QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
+ QNetworkReply *reply = network_->post(req, query);
+ replies_ << reply;
+ connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleLoginSSLErrors(QList)));
+ connect(reply, &QNetworkReply::finished, [=] { HandleAuthReply(reply); });
+
+ qLog(Debug) << "Qobuz: Sending request" << url << query;
+
+}
+
+void QobuzService::HandleLoginSSLErrors(QList ssl_errors) {
+
+ for (QSslError &ssl_error : ssl_errors) {
+ login_errors_ += ssl_error.errorString();
+ }
+
+}
+
+void QobuzService::HandleAuthReply(QNetworkReply *reply) {
+
+ reply->deleteLater();
+
+ login_sent_ = false;
+
+ if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
+ if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
+ // This is a network error, there is nothing more to do.
+ LoginError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
+ return;
+ }
+ else {
+ // See if there is Json data containing "status", "code" and "message" - then use that instead.
+ QByteArray data(reply->readAll());
+ QJsonParseError json_error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
+ if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
+ QJsonObject json_obj = json_doc.object();
+ if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("code") && json_obj.contains("message")) {
+ QString status = json_obj["status"].toString();
+ int code = json_obj["code"].toInt();
+ QString message = json_obj["message"].toString();
+ login_errors_ << QString("%1 (%2)").arg(message).arg(code);
+ }
+ }
+ if (login_errors_.isEmpty()) {
+ if (reply->error() != QNetworkReply::NoError) {
+ login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
+ }
+ else {
+ login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
+ }
+ }
+ LoginError();
+ return;
+ }
+ }
+
+ login_errors_.clear();
+
+ QByteArray data = reply->readAll();
+ QJsonParseError json_error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
+
+ if (json_error.error != QJsonParseError::NoError) {
+ LoginError("Authentication reply from server missing Json data.");
+ return;
+ }
+
+ if (json_doc.isEmpty()) {
+ LoginError("Authentication reply from server has empty Json document.");
+ return;
+ }
+
+ if (!json_doc.isObject()) {
+ LoginError("Authentication reply from server has Json document that is not an object.", json_doc);
+ return;
+ }
+
+ QJsonObject json_obj = json_doc.object();
+ if (json_obj.isEmpty()) {
+ LoginError("Authentication reply from server has empty Json object.", json_doc);
+ return;
+ }
+
+ if (!json_obj.contains("user_auth_token")) {
+ LoginError("Authentication reply from server is missing user_auth_token", json_obj);
+ return;
+ }
+ user_auth_token_ = json_obj["user_auth_token"].toString();
+
+ if (!json_obj.contains("user")) {
+ LoginError("Authentication reply from server is missing user", json_obj);
+ return;
+ }
+ QJsonValue value_user = json_obj["user"];
+ if (!value_user.isObject()) {
+ LoginError("Authentication reply user is not a object", json_obj);
+ return;
+ }
+ QJsonObject obj_user = value_user.toObject();
+
+ if (!obj_user.contains("id")) {
+ LoginError("Authentication reply from server is missing user id", obj_user);
+ return;
+ }
+ user_id_ = obj_user["id"].toInt();
+
+ if (!obj_user.contains("device")) {
+ LoginError("Authentication reply from server is missing user device", obj_user);
+ return;
+ }
+ QJsonValue value_device = obj_user["device"];
+ if (!value_device.isObject()) {
+ LoginError("Authentication reply from server user device is not a object", value_device);
+ return;
+ }
+ QJsonObject obj_device = value_device.toObject();
+
+ if (!obj_device.contains("device_manufacturer_id")) {
+ LoginError("Authentication reply from server device is missing device_manufacturer_id", obj_device);
+ return;
+ }
+ device_id_ = obj_device["device_manufacturer_id"].toString();
+
+ if (!obj_user.contains("credential")) {
+ LoginError("Authentication reply from server is missing user credential", obj_user);
+ return;
+ }
+ QJsonValue value_credential = obj_user["credential"];
+ if (!value_credential.isObject()) {
+ LoginError("Authentication reply from serve userr credential is not a object", value_device);
+ return;
+ }
+ QJsonObject obj_credential = value_credential.toObject();
+
+ if (!obj_credential.contains("id")) {
+ LoginError("Authentication reply user credential from server is missing user credential id", obj_credential);
+ return;
+ }
+ credential_id_ = obj_credential["id"].toInt();
+
+ QSettings s;
+ s.beginGroup(QobuzSettingsPage::kSettingsGroup);
+ s.setValue("user_auth_token", user_auth_token_);
+ s.setValue("user_id", user_id_);
+ s.setValue("credential_id", credential_id_);
+ s.setValue("device_id", device_id_);
+ s.endGroup();
+
+ qLog(Debug) << "Qobuz: Login successful" << "user id" << user_id_ << "device id" << device_id_;
+
+ login_attempts_ = 0;
+ if (timer_login_attempt_->isActive()) timer_login_attempt_->stop();
+
+ emit LoginComplete(true);
+ emit LoginSuccess();
+
+}
+
+void QobuzService::Logout() {
+
+ user_auth_token_.clear();
+ device_id_.clear();
+ user_id_ = -1;
+ credential_id_ = -1;
+
+ QSettings s;
+ s.beginGroup(QobuzSettingsPage::kSettingsGroup);
+ s.remove("user_id");
+ s.remove("credential_id");
+ s.remove("device_id");
+ s.remove("user_auth_token");
+ s.endGroup();
+
+}
+
+void QobuzService::ResetLoginAttempts() {
+ login_attempts_ = 0;
+}
+
+void QobuzService::TryLogin() {
+
+ if (authenticated() || login_sent_) return;
+
+ if (login_attempts_ >= kLoginAttempts) {
+ emit LoginComplete(false, tr("Maximum number of login attempts reached."));
+ return;
+ }
+ if (app_id_.isEmpty()) {
+ emit LoginComplete(false, tr("Missing Qobuz app ID."));
+ return;
+ }
+ if (username_.isEmpty()) {
+ emit LoginComplete(false, tr("Missing Qobuz username."));
+ return;
+ }
+ if (password_.isEmpty()) {
+ emit LoginComplete(false, tr("Missing Qobuz password."));
+ return;
+ }
+
+ emit Login();
+
+}
+
+void QobuzService::ResetArtistsRequest() {
+
+ if (artists_request_.get()) {
+ disconnect(artists_request_.get(), 0, this, 0);
+ disconnect(this, 0, artists_request_.get(), 0);
+ artists_request_.reset();
+ }
+
+}
+
+void QobuzService::GetArtists() {
+
+ if (app_id().isEmpty()) {
+ emit ArtistsResults(SongList(), tr("Missing Qobuz app ID."));
+ return;
+ }
+
+ if (!authenticated()) {
+ emit ArtistsResults(SongList(), tr("Not authenticated with Qobuz."));
+ return;
+ }
+
+ ResetArtistsRequest();
+
+ artists_request_.reset(new QobuzRequest(this, url_handler_, app_, network_, QobuzBaseRequest::QueryType_Artists, this));
+
+ connect(artists_request_.get(), SIGNAL(Results(int, SongList, QString)), SLOT(ArtistsResultsReceived(int, SongList, QString)));
+ connect(artists_request_.get(), SIGNAL(UpdateStatus(int, QString)), SLOT(ArtistsUpdateStatusReceived(int, QString)));
+ connect(artists_request_.get(), SIGNAL(ProgressSetMaximum(int, int)), SLOT(ArtistsProgressSetMaximumReceived(int, int)));
+ connect(artists_request_.get(), SIGNAL(UpdateProgress(int, int)), SLOT(ArtistsUpdateProgressReceived(int, int)));
+
+ artists_request_->Process();
+
+}
+
+void QobuzService::ArtistsResultsReceived(const int id, const SongList &songs, const QString &error) {
+ Q_UNUSED(id);
+ emit ArtistsResults(songs, error);
+}
+
+void QobuzService::ArtistsUpdateStatusReceived(const int id, const QString &text) {
+ Q_UNUSED(id);
+ emit ArtistsUpdateStatus(text);
+}
+
+void QobuzService::ArtistsProgressSetMaximumReceived(const int id, const int max) {
+ Q_UNUSED(id);
+ emit ArtistsProgressSetMaximum(max);
+}
+
+void QobuzService::ArtistsUpdateProgressReceived(const int id, const int progress) {
+ Q_UNUSED(id);
+ emit ArtistsUpdateProgress(progress);
+}
+
+void QobuzService::ResetAlbumsRequest() {
+
+ if (albums_request_.get()) {
+ disconnect(albums_request_.get(), 0, this, 0);
+ disconnect(this, 0, albums_request_.get(), 0);
+ albums_request_.reset();
+ }
+
+}
+
+void QobuzService::GetAlbums() {
+
+ if (app_id().isEmpty()) {
+ emit AlbumsResults(SongList(), tr("Missing Qobuz app ID."));
+ return;
+ }
+
+ if (!authenticated()) {
+ emit AlbumsResults(SongList(), tr("Not authenticated with Qobuz."));
+ return;
+ }
+
+ ResetAlbumsRequest();
+ albums_request_.reset(new QobuzRequest(this, url_handler_, app_, network_, QobuzBaseRequest::QueryType_Albums, this));
+ connect(albums_request_.get(), SIGNAL(Results(int, SongList, QString)), SLOT(AlbumsResultsReceived(int, SongList, QString)));
+ connect(albums_request_.get(), SIGNAL(UpdateStatus(int, QString)), SLOT(AlbumsUpdateStatusReceived(int, QString)));
+ connect(albums_request_.get(), SIGNAL(ProgressSetMaximum(int, int)), SLOT(AlbumsProgressSetMaximumReceived(int, int)));
+ connect(albums_request_.get(), SIGNAL(UpdateProgress(int, int)), SLOT(AlbumsUpdateProgressReceived(int, int)));
+
+ albums_request_->Process();
+
+}
+
+void QobuzService::AlbumsResultsReceived(const int id, const SongList &songs, const QString &error) {
+ Q_UNUSED(id);
+ emit AlbumsResults(songs, error);
+}
+
+void QobuzService::AlbumsUpdateStatusReceived(const int id, const QString &text) {
+ Q_UNUSED(id);
+ emit AlbumsUpdateStatus(text);
+}
+
+void QobuzService::AlbumsProgressSetMaximumReceived(const int id, const int max) {
+ Q_UNUSED(id);
+ emit AlbumsProgressSetMaximum(max);
+}
+
+void QobuzService::AlbumsUpdateProgressReceived(const int id, const int progress) {
+ Q_UNUSED(id);
+ emit AlbumsUpdateProgress(progress);
+}
+
+void QobuzService::ResetSongsRequest() {
+
+ if (songs_request_.get()) {
+ disconnect(songs_request_.get(), 0, this, 0);
+ disconnect(this, 0, songs_request_.get(), 0);
+ songs_request_.reset();
+ }
+
+}
+
+void QobuzService::GetSongs() {
+
+ if (app_id().isEmpty()) {
+ emit SongsResults(SongList(), tr("Missing Qobuz app ID."));
+ return;
+ }
+
+ if (!authenticated()) {
+ emit SongsResults(SongList(), tr("Not authenticated with Qobuz."));
+ return;
+ }
+
+ ResetSongsRequest();
+ songs_request_.reset(new QobuzRequest(this, url_handler_, app_, network_, QobuzBaseRequest::QueryType_Songs, this));
+ connect(songs_request_.get(), SIGNAL(Results(int, SongList, QString)), SLOT(SongsResultsReceived(int, SongList, QString)));
+ connect(songs_request_.get(), SIGNAL(UpdateStatus(int, QString)), SLOT(SongsUpdateStatusReceived(int, QString)));
+ connect(songs_request_.get(), SIGNAL(ProgressSetMaximum(int, int)), SLOT(SongsProgressSetMaximumReceived(int, int)));
+ connect(songs_request_.get(), SIGNAL(UpdateProgress(int, int)), SLOT(SongsUpdateProgressReceived(int, int)));
+
+ songs_request_->Process();
+
+}
+
+void QobuzService::SongsResultsReceived(const int id, const SongList &songs, const QString &error) {
+ Q_UNUSED(id);
+ emit SongsResults(songs, error);
+}
+
+void QobuzService::SongsUpdateStatusReceived(const int id, const QString &text) {
+ Q_UNUSED(id);
+ emit SongsUpdateStatus(text);
+}
+
+void QobuzService::SongsProgressSetMaximumReceived(const int id, const int max) {
+ Q_UNUSED(id);
+ emit SongsProgressSetMaximum(max);
+}
+
+void QobuzService::SongsUpdateProgressReceived(const int id, const int progress) {
+ Q_UNUSED(id);
+ emit SongsUpdateProgress(progress);
+}
+
+int QobuzService::Search(const QString &text, InternetSearchView::SearchType type) {
+
+ pending_search_id_ = next_pending_search_id_;
+ pending_search_text_ = text;
+ pending_search_type_ = type;
+
+ next_pending_search_id_++;
+
+ if (text.isEmpty()) {
+ timer_search_delay_->stop();
+ return pending_search_id_;
+ }
+ timer_search_delay_->setInterval(search_delay_);
+ timer_search_delay_->start();
+
+ return pending_search_id_;
+
+}
+
+void QobuzService::StartSearch() {
+
+ search_id_ = pending_search_id_;
+ search_text_ = pending_search_text_;
+
+ if (app_id_.isEmpty()) { // App ID is the only thing needed to search.
+ emit SearchResults(search_id_, SongList(), tr("Missing Qobuz app ID."));
+ return;
+ }
+
+ SendSearch();
+
+}
+
+void QobuzService::CancelSearch() {
+}
+
+void QobuzService::SendSearch() {
+
+ QobuzBaseRequest::QueryType type;
+
+ switch (pending_search_type_) {
+ case InternetSearchView::SearchType_Artists:
+ type = QobuzBaseRequest::QueryType_SearchArtists;
+ break;
+ case InternetSearchView::SearchType_Albums:
+ type = QobuzBaseRequest::QueryType_SearchAlbums;
+ break;
+ case InternetSearchView::SearchType_Songs:
+ type = QobuzBaseRequest::QueryType_SearchSongs;
+ break;
+ }
+
+ search_request_.reset(new QobuzRequest(this, url_handler_, app_, network_, type, this));
+
+ connect(search_request_.get(), SIGNAL(Results(int, SongList, QString)), SLOT(SearchResultsReceived(int, SongList, QString)));
+ connect(search_request_.get(), SIGNAL(UpdateStatus(int, QString)), SIGNAL(SearchUpdateStatus(int, QString)));
+ connect(search_request_.get(), SIGNAL(ProgressSetMaximum(int, int)), SIGNAL(SearchProgressSetMaximum(int, int)));
+ connect(search_request_.get(), SIGNAL(UpdateProgress(int, int)), SIGNAL(SearchUpdateProgress(int, int)));
+
+ search_request_->Search(search_id_, search_text_);
+ search_request_->Process();
+
+}
+
+void QobuzService::SearchResultsReceived(const int id, const SongList &songs, const QString &error) {
+ emit SearchResults(id, songs, error);
+}
+
+void QobuzService::GetStreamURL(const QUrl &url) {
+
+ if (app_id().isEmpty() || app_secret().isEmpty()) { // Don't check for login here, because we allow automatic login.
+ emit StreamURLFinished(url, url, Song::FileType_Stream, -1, -1, -1, tr("Missing Qobuz app ID or secret."));
+ return;
+ }
+
+ QobuzStreamURLRequest *stream_url_req = new QobuzStreamURLRequest(this, network_, url, this);
+ stream_url_requests_ << stream_url_req;
+
+ connect(stream_url_req, SIGNAL(TryLogin()), this, SLOT(TryLogin()));
+ connect(stream_url_req, SIGNAL(StreamURLFinished(QUrl, QUrl, Song::FileType, int, int, qint64, QString)), this, SLOT(HandleStreamURLFinished(QUrl, QUrl, Song::FileType, int, int, qint64, QString)));
+ connect(this, SIGNAL(LoginComplete(bool, QString)), stream_url_req, SLOT(LoginComplete(bool, QString)));
+
+ stream_url_req->Process();
+
+}
+
+void QobuzService::HandleStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error) {
+
+ QobuzStreamURLRequest *stream_url_req = qobject_cast(sender());
+ if (!stream_url_req || !stream_url_requests_.contains(stream_url_req)) return;
+ stream_url_req->deleteLater();
+ stream_url_requests_.removeAll(stream_url_req);
+
+ emit StreamURLFinished(original_url, stream_url, filetype, samplerate, bit_depth, duration, error);
+
+}
+
+void QobuzService::LoginError(const QString &error, const QVariant &debug) {
+
+ if (!error.isEmpty()) login_errors_ << error;
+
+ QString error_html;
+ for (const QString &e : login_errors_) {
+ qLog(Error) << "Qobuz:" << e;
+ error_html += e + "
";
+ }
+ if (debug.isValid()) qLog(Debug) << debug;
+
+ emit LoginFailure(error_html);
+ emit LoginComplete(false, error_html);
+
+ login_errors_.clear();
+
+}
diff --git a/src/qobuz/qobuzservice.h b/src/qobuz/qobuzservice.h
new file mode 100644
index 00000000..b2ac1b86
--- /dev/null
+++ b/src/qobuz/qobuzservice.h
@@ -0,0 +1,231 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2019, Jonas Kvinge
+ *
+ * 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 .
+ *
+ */
+
+#ifndef QOBUZSERVICE_H
+#define QOBUZSERVICE_H
+
+#include "config.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/song.h"
+#include "internet/internetservice.h"
+#include "internet/internetsearchview.h"
+
+class QTimer;
+class QNetworkReply;
+class QSortFilterProxyModel;
+class Application;
+class NetworkAccessManager;
+class QobuzUrlHandler;
+class QobuzRequest;
+class QobuzFavoriteRequest;
+class QobuzStreamURLRequest;
+class CollectionBackend;
+class CollectionModel;
+
+using std::shared_ptr;
+
+class QobuzService : public InternetService {
+ Q_OBJECT
+
+ public:
+ explicit QobuzService(Application *app, QObject *parent);
+ ~QobuzService();
+
+ static const Song::Source kSource;
+
+ void Exit() override;
+ void ReloadSettings() override;
+
+ void Logout();
+ int Search(const QString &text, InternetSearchView::SearchType type) override;
+ void CancelSearch() override;
+
+ int max_login_attempts() { return kLoginAttempts; }
+
+ Application *app() { return app_; }
+ QString app_id() { return app_id_; }
+ QString app_secret() { return app_secret_; }
+ QString username() { return username_; }
+ QString password() { return password_; }
+ int format() { return format_; }
+ int search_delay() { return search_delay_; }
+ int artistssearchlimit() { return artistssearchlimit_; }
+ int albumssearchlimit() { return albumssearchlimit_; }
+ int songssearchlimit() { return songssearchlimit_; }
+ bool download_album_covers() { return download_album_covers_; }
+
+ QString user_auth_token() { return user_auth_token_; }
+ qint64 user_id() { return user_id_; }
+ QString device_id() { return device_id_; }
+ qint64 credential_id() { return credential_id_; }
+
+ bool authenticated() override { return (!app_id_.isEmpty() && !app_secret_.isEmpty() && !user_auth_token_.isEmpty()); }
+ bool login_sent() { return login_sent_; }
+ bool login_attempts() { return login_attempts_; }
+
+ void GetStreamURL(const QUrl &url);
+
+ CollectionBackend *artists_collection_backend() override { return artists_collection_backend_; }
+ CollectionBackend *albums_collection_backend() override { return albums_collection_backend_; }
+ CollectionBackend *songs_collection_backend() override { return songs_collection_backend_; }
+
+ CollectionModel *artists_collection_model() override { return artists_collection_model_; }
+ CollectionModel *albums_collection_model() override { return albums_collection_model_; }
+ CollectionModel *songs_collection_model() override { return songs_collection_model_; }
+
+ QSortFilterProxyModel *artists_collection_sort_model() override { return artists_collection_sort_model_; }
+ QSortFilterProxyModel *albums_collection_sort_model() override { return albums_collection_sort_model_; }
+ QSortFilterProxyModel *songs_collection_sort_model() override { return songs_collection_sort_model_; }
+
+ enum QueryType {
+ QueryType_Artists,
+ QueryType_Albums,
+ QueryType_Songs,
+ QueryType_SearchArtists,
+ QueryType_SearchAlbums,
+ QueryType_SearchSongs,
+ };
+
+ public slots:
+ void ShowConfig() override;
+ void TryLogin();
+ void SendLogin(const QString &app_id, const QString &username, const QString &password);
+ void GetArtists() override;
+ void GetAlbums() override;
+ void GetSongs() override;
+ void ResetArtistsRequest() override;
+ void ResetAlbumsRequest() override;
+ void ResetSongsRequest() override;
+
+ private slots:
+ void ExitReceived();
+ void SendLogin();
+ void HandleLoginSSLErrors(QList ssl_errors);
+ void HandleAuthReply(QNetworkReply *reply);
+ void ResetLoginAttempts();
+ void StartSearch();
+ void ArtistsResultsReceived(const int id, const SongList &songs, const QString &error);
+ void AlbumsResultsReceived(const int id, const SongList &songs, const QString &error);
+ void SongsResultsReceived(const int id, const SongList &songs, const QString &error);
+ void SearchResultsReceived(const int id, const SongList &songs, const QString &error);
+ void ArtistsUpdateStatusReceived(const int id, const QString &text);
+ void AlbumsUpdateStatusReceived(const int id, const QString &text);
+ void SongsUpdateStatusReceived(const int id, const QString &text);
+ void ArtistsProgressSetMaximumReceived(const int id, const int max);
+ void AlbumsProgressSetMaximumReceived(const int id, const int max);
+ void SongsProgressSetMaximumReceived(const int id, const int max);
+ void ArtistsUpdateProgressReceived(const int id, const int progress);
+ void AlbumsUpdateProgressReceived(const int id, const int progress);
+ void SongsUpdateProgressReceived(const int id, const int progress);
+ void HandleStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error);
+
+ private:
+ typedef QPair Param;
+ typedef QList ParamList;
+
+ void SendSearch();
+ void LoginError(const QString &error = QString(), const QVariant &debug = QVariant());
+
+ static const char *kAuthUrl;
+ static const int kLoginAttempts;
+ static const int kTimeResetLoginAttempts;
+
+ static const char *kArtistsSongsTable;
+ static const char *kAlbumsSongsTable;
+ static const char *kSongsTable;
+
+ static const char *kArtistsSongsFtsTable;
+ static const char *kAlbumsSongsFtsTable;
+ static const char *kSongsFtsTable;
+
+ Application *app_;
+ NetworkAccessManager *network_;
+ QobuzUrlHandler *url_handler_;
+
+ CollectionBackend *artists_collection_backend_;
+ CollectionBackend *albums_collection_backend_;
+ CollectionBackend *songs_collection_backend_;
+
+ CollectionModel *artists_collection_model_;
+ CollectionModel *albums_collection_model_;
+ CollectionModel *songs_collection_model_;
+
+ QSortFilterProxyModel *artists_collection_sort_model_;
+ QSortFilterProxyModel *albums_collection_sort_model_;
+ QSortFilterProxyModel *songs_collection_sort_model_;
+
+ QTimer *timer_search_delay_;
+ QTimer *timer_login_attempt_;
+
+ std::shared_ptr artists_request_;
+ std::shared_ptr albums_request_;
+ std::shared_ptr songs_request_;
+ std::shared_ptr search_request_;
+ QobuzFavoriteRequest *favorite_request_;
+
+ QString app_id_;
+ QString app_secret_;
+ QString username_;
+ QString password_;
+ int format_;
+ int search_delay_;
+ int artistssearchlimit_;
+ int albumssearchlimit_;
+ int songssearchlimit_;
+ bool download_album_covers_;
+
+ qint64 user_id_;
+ QString user_auth_token_;
+ QString device_id_;
+ qint64 credential_id_;
+
+ int pending_search_id_;
+ int next_pending_search_id_;
+ QString pending_search_text_;
+ InternetSearchView::SearchType pending_search_type_;
+
+ int search_id_;
+ QString search_text_;
+ bool login_sent_;
+ int login_attempts_;
+
+ QList stream_url_requests_;
+
+ QStringList login_errors_;
+
+ QList wait_for_exit_;
+ QList replies_;
+
+};
+
+#endif // QOBUZSERVICE_H
diff --git a/src/qobuz/qobuzstreamurlrequest.cpp b/src/qobuz/qobuzstreamurlrequest.cpp
new file mode 100644
index 00000000..931070b5
--- /dev/null
+++ b/src/qobuz/qobuzstreamurlrequest.cpp
@@ -0,0 +1,249 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2019, Jonas Kvinge
+ *
+ * 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 .
+ *
+ */
+
+#include "config.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/logging.h"
+#include "core/network.h"
+#include "core/song.h"
+#include "core/timeconstants.h"
+#include "qobuzservice.h"
+#include "qobuzbaserequest.h"
+#include "qobuzstreamurlrequest.h"
+
+QobuzStreamURLRequest::QobuzStreamURLRequest(QobuzService *service, NetworkAccessManager *network, const QUrl &original_url, QObject *parent)
+ : QobuzBaseRequest(service, network, parent),
+ service_(service),
+ reply_(nullptr),
+ original_url_(original_url),
+ song_id_(original_url.path().toInt()),
+ tries_(0),
+ need_login_(false) {}
+
+QobuzStreamURLRequest::~QobuzStreamURLRequest() {
+
+ if (reply_) {
+ disconnect(reply_, 0, this, 0);
+ if (reply_->isRunning()) reply_->abort();
+ reply_->deleteLater();
+ }
+
+}
+
+void QobuzStreamURLRequest::LoginComplete(const bool success, const QString &error) {
+
+ if (!need_login_) return;
+ need_login_ = false;
+
+ if (!success) {
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error);
+ return;
+ }
+
+ Process();
+
+}
+
+void QobuzStreamURLRequest::Process() {
+
+ if (app_id().isEmpty() || app_secret().isEmpty()) {
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, tr("Missing Qobuz app ID or secret."));
+ return;
+ }
+
+ if (!authenticated()) {
+ need_login_ = true;
+ emit TryLogin();
+ return;
+ }
+ GetStreamURL();
+
+}
+
+void QobuzStreamURLRequest::Cancel() {
+
+ if (reply_ && reply_->isRunning()) {
+ reply_->abort();
+ }
+ else {
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, tr("Cancelled."));
+ }
+
+}
+
+void QobuzStreamURLRequest::GetStreamURL() {
+
+ ++tries_;
+
+ if (reply_) {
+ disconnect(reply_, 0, this, 0);
+ if (reply_->isRunning()) reply_->abort();
+ reply_->deleteLater();
+ }
+
+#if 0
+ QByteArray appid = app_id().toUtf8();
+ QByteArray secret_decoded = QByteArray::fromBase64(app_secret().toUtf8());
+ QString secret;
+ for (int x = 0, y = 0; x < secret_decoded.length(); ++x , ++y) {
+ if (y == appid.length()) y = 0;
+ secret.append(QChar(secret_decoded[x] ^ appid[y]));
+ }
+#endif
+
+ QString secret = app_secret();
+ quint64 timestamp = QDateTime::currentDateTime().toSecsSinceEpoch();
+
+ ParamList params_to_sign = ParamList() << Param("format_id", QString::number(format()))
+ << Param("track_id", QString::number(song_id_));
+
+ std::sort(params_to_sign.begin(), params_to_sign.end());
+
+ QString data_to_sign;
+ data_to_sign += "trackgetFileUrl";
+ for (const Param ¶m : params_to_sign) {
+ data_to_sign += param.first + param.second;
+ }
+ data_to_sign += QString::number(timestamp);
+ data_to_sign += secret.toUtf8();
+
+ QByteArray const digest = QCryptographicHash::hash(data_to_sign.toUtf8(), QCryptographicHash::Md5);
+ QString signature = QString::fromLatin1(digest.toHex()).rightJustified(32, '0').toLower();
+
+ ParamList params = params_to_sign;
+ params << Param("request_ts", QString::number(timestamp));
+ params << Param("request_sig", signature);
+ params << Param("user_auth_token", user_auth_token());
+
+ std::sort(params.begin(), params.end());
+
+ reply_ = CreateRequest(QString("track/getFileUrl"), params);
+ connect(reply_, SIGNAL(finished()), this, SLOT(StreamURLReceived()));
+
+}
+
+void QobuzStreamURLRequest::StreamURLReceived() {
+
+ if (!reply_) return;
+
+ QByteArray data = GetReplyData(reply_);
+
+ disconnect(reply_, 0, this, 0);
+ reply_->deleteLater();
+ reply_ = nullptr;
+
+ if (data.isEmpty()) {
+ if (!authenticated() && login_sent() && tries_ <= 1) {
+ need_login_ = true;
+ return;
+ }
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
+ return;
+ }
+
+ QJsonObject json_obj = ExtractJsonObj(data);
+ if (json_obj.isEmpty()) {
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
+ return;
+ }
+
+ if (!json_obj.contains("track_id")) {
+ Error("Invalid Json reply, stream url is missing track_id.", json_obj);
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
+ return;
+ }
+
+ int track_id = json_obj["track_id"].toInt();
+ if (track_id != song_id_) {
+ Error("Incorrect track ID returned.", json_obj);
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
+ return;
+ }
+
+ if (!json_obj.contains("mime_type") || !json_obj.contains("url")) {
+ Error("Invalid Json reply, stream url is missing url or mime_type.", json_obj);
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
+ return;
+ }
+
+ QUrl url(json_obj["url"].toString());
+ QString mimetype = json_obj["mime_type"].toString();
+
+ Song::FileType filetype(Song::FileType_Unknown);
+ QMimeDatabase mimedb;
+ for (QString suffix : mimedb.mimeTypeForName(mimetype.toUtf8()).suffixes()) {
+ filetype = Song::FiletypeByExtension(suffix);
+ if (filetype != Song::FileType_Unknown) break;
+ }
+ if (filetype == Song::FileType_Unknown) {
+ qLog(Debug) << "Qobuz: Unknown mimetype" << mimetype;
+ filetype = Song::FileType_Stream;
+ }
+
+ if (!url.isValid()) {
+ Error("Returned stream url is invalid.", json_obj);
+ emit StreamURLFinished(original_url_, original_url_, filetype, -1, -1, -1, errors_.first());
+ return;
+ }
+
+ qint64 duration = -1;
+ if (json_obj.contains("duration")) {
+ duration = json_obj["duration"].toDouble() * kNsecPerSec;
+ }
+ int samplerate = -1;
+ if (json_obj.contains("sampling_rate")) {
+ samplerate = json_obj["sampling_rate"].toDouble() * 1000;
+ }
+ int bit_depth = -1;
+ if (json_obj.contains("bit_depth")) {
+ bit_depth = json_obj["bit_depth"].toDouble();
+ }
+
+ emit StreamURLFinished(original_url_, url, filetype, samplerate, bit_depth, duration);
+
+}
+
+void QobuzStreamURLRequest::Error(const QString &error, const QVariant &debug) {
+
+ if (!error.isEmpty()) {
+ qLog(Error) << "Qobuz:" << error;
+ errors_ << error;
+ }
+ if (debug.isValid()) qLog(Debug) << debug;
+
+}
+
diff --git a/src/qobuz/qobuzstreamurlrequest.h b/src/qobuz/qobuzstreamurlrequest.h
new file mode 100644
index 00000000..14166cb2
--- /dev/null
+++ b/src/qobuz/qobuzstreamurlrequest.h
@@ -0,0 +1,76 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2019, Jonas Kvinge
+ *
+ * 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 .
+ *
+ */
+
+#ifndef QOBUZSTREAMURLREQUEST_H
+#define QOBUZSTREAMURLREQUEST_H
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/song.h"
+#include "qobuzbaserequest.h"
+
+class QNetworkReply;
+class NetworkAccessManager;
+class QobuzService;
+
+class QobuzStreamURLRequest : public QobuzBaseRequest {
+ Q_OBJECT
+
+ public:
+ explicit QobuzStreamURLRequest(QobuzService *service, NetworkAccessManager *network, const QUrl &original_url, QObject *parent);
+ ~QobuzStreamURLRequest();
+
+ void GetStreamURL();
+ void Process();
+ void NeedLogin() { need_login_ = true; }
+ void Cancel();
+
+ QUrl original_url() { return original_url_; }
+ int song_id() { return song_id_; }
+ bool need_login() { return need_login_; }
+
+ signals:
+ void TryLogin();
+ void StreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error = QString());
+
+ private slots:
+ void LoginComplete(const bool success, const QString &error = QString());
+ void StreamURLReceived();
+
+ private:
+ void Error(const QString &error, const QVariant &debug = QVariant());
+
+ QobuzService *service_;
+ QNetworkReply *reply_;
+ QUrl original_url_;
+ int song_id_;
+ int tries_;
+ bool need_login_;
+ QStringList errors_;
+
+};
+
+#endif // QOBUZSTREAMURLREQUEST_H
diff --git a/src/qobuz/qobuzurlhandler.cpp b/src/qobuz/qobuzurlhandler.cpp
new file mode 100644
index 00000000..c9086aea
--- /dev/null
+++ b/src/qobuz/qobuzurlhandler.cpp
@@ -0,0 +1,68 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2018, Jonas Kvinge
+ *
+ * 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 .
+ *
+ */
+
+#include
+#include
+#include
+
+#include "core/application.h"
+#include "core/taskmanager.h"
+#include "core/song.h"
+#include "qobuz/qobuzservice.h"
+#include "qobuzurlhandler.h"
+
+QobuzUrlHandler::QobuzUrlHandler(Application *app, QobuzService *service) :
+ UrlHandler(service),
+ app_(app),
+ service_(service),
+ task_id_(-1)
+ {
+
+ connect(service, SIGNAL(StreamURLFinished(QUrl, QUrl, Song::FileType, int, int, qint64, QString)), this, SLOT(GetStreamURLFinished(QUrl, QUrl, Song::FileType, int, int, qint64, QString)));
+
+}
+
+UrlHandler::LoadResult QobuzUrlHandler::StartLoading(const QUrl &url) {
+
+ LoadResult ret(url);
+ if (task_id_ != -1) return ret;
+ task_id_ = app_->task_manager()->StartTask(QString("Loading %1 stream...").arg(url.scheme()));
+ service_->GetStreamURL(url);
+ ret.type_ = LoadResult::WillLoadAsynchronously;
+ return ret;
+
+}
+
+void QobuzUrlHandler::GetStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error) {
+
+ if (task_id_ == -1) return;
+ CancelTask();
+ if (error.isEmpty()) {
+ emit AsyncLoadComplete(LoadResult(original_url, LoadResult::TrackAvailable, stream_url, filetype, samplerate, bit_depth, duration));
+ }
+ else {
+ emit AsyncLoadComplete(LoadResult(original_url, LoadResult::Error, error));
+ }
+
+}
+
+void QobuzUrlHandler::CancelTask() {
+ app_->task_manager()->SetTaskFinished(task_id_);
+ task_id_ = -1;
+}
diff --git a/src/qobuz/qobuzurlhandler.h b/src/qobuz/qobuzurlhandler.h
new file mode 100644
index 00000000..1e0b34ff
--- /dev/null
+++ b/src/qobuz/qobuzurlhandler.h
@@ -0,0 +1,55 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2018, Jonas Kvinge
+ *
+ * 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 .
+ *
+ */
+
+#ifndef QOBUZURLHANDLER_H
+#define QOBUZURLHANDLER_H
+
+#include
+#include
+#include
+#include
+
+#include "core/urlhandler.h"
+#include "core/song.h"
+#include "qobuz/qobuzservice.h"
+
+class Application;
+
+class QobuzUrlHandler : public UrlHandler {
+ Q_OBJECT
+
+ public:
+ explicit QobuzUrlHandler(Application *app, QobuzService *service);
+
+ QString scheme() const { return service_->url_scheme(); }
+ LoadResult StartLoading(const QUrl &url);
+
+ void CancelTask();
+
+ private slots:
+ void GetStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error = QString());
+
+ private:
+ Application *app_;
+ QobuzService *service_;
+ int task_id_;
+
+};
+
+#endif
diff --git a/src/settings/coverssettingspage.cpp b/src/settings/coverssettingspage.cpp
index 0cb5f08f..1f712463 100644
--- a/src/settings/coverssettingspage.cpp
+++ b/src/settings/coverssettingspage.cpp
@@ -121,6 +121,10 @@ void CoversSettingsPage::CurrentItemChanged(QListWidgetItem *item_current, QList
DisableAuthentication();
ui_->label_auth_info->setText(tr("Use Tidal settings to authenticate."));
}
+ else if (provider->name() == "Qobuz" && !provider->IsAuthenticated()) {
+ DisableAuthentication();
+ ui_->label_auth_info->setText(tr("Use Qobuz settings to authenticate."));
+ }
else {
ui_->login_state->SetLoggedIn(provider->IsAuthenticated() ? LoginStateWidget::LoggedIn : LoginStateWidget::LoggedOut);
ui_->button_authenticate->setEnabled(true);
@@ -229,6 +233,10 @@ void CoversSettingsPage::LogoutClicked() {
DisableAuthentication();
ui_->label_auth_info->setText(tr("Use Tidal settings to authenticate."));
}
+ else if (provider->name() == "Qobuz") {
+ DisableAuthentication();
+ ui_->label_auth_info->setText(tr("Use Qobuz settings to authenticate."));
+ }
else {
ui_->button_authenticate->setEnabled(true);
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
diff --git a/src/settings/qobuzsettingspage.cpp b/src/settings/qobuzsettingspage.cpp
new file mode 100644
index 00000000..db2e422c
--- /dev/null
+++ b/src/settings/qobuzsettingspage.cpp
@@ -0,0 +1,171 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2019, Jonas Kvinge
+ *
+ * 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 .
+ *
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "settingsdialog.h"
+#include "qobuzsettingspage.h"
+#include "ui_qobuzsettingspage.h"
+#include "core/application.h"
+#include "core/iconloader.h"
+#include "widgets/loginstatewidget.h"
+#include "internet/internetservices.h"
+#include "qobuz/qobuzservice.h"
+
+const char *QobuzSettingsPage::kSettingsGroup = "Qobuz";
+
+QobuzSettingsPage::QobuzSettingsPage(SettingsDialog *parent)
+ : SettingsPage(parent),
+ ui_(new Ui::QobuzSettingsPage),
+ service_(dialog()->app()->internet_services()->Service()) {
+
+ ui_->setupUi(this);
+ setWindowIcon(IconLoader::Load("qobuz"));
+
+ connect(ui_->button_login, SIGNAL(clicked()), SLOT(LoginClicked()));
+ connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked()));
+
+ connect(this, SIGNAL(Login(QString, QString, QString)), service_, SLOT(SendLogin(QString, QString, QString)));
+
+ connect(service_, SIGNAL(LoginFailure(QString)), SLOT(LoginFailure(QString)));
+ connect(service_, SIGNAL(LoginSuccess()), SLOT(LoginSuccess()));
+
+ dialog()->installEventFilter(this);
+
+ ui_->format->addItem("MP3 320", 5);
+ ui_->format->addItem("FLAC Lossless", 6);
+ ui_->format->addItem("FLAC Hi-Res <= 96kHz", 7);
+ ui_->format->addItem("FLAC Hi-Res > 96kHz", 27);
+
+}
+
+QobuzSettingsPage::~QobuzSettingsPage() { delete ui_; }
+
+void QobuzSettingsPage::Load() {
+
+ QSettings s;
+
+ s.beginGroup(kSettingsGroup);
+ ui_->enable->setChecked(s.value("enabled", false).toBool());
+ ui_->app_id->setText(s.value("app_id").toString());
+ ui_->app_secret->setText(s.value("app_secret").toString());
+
+ ui_->username->setText(s.value("username").toString());
+ QByteArray password = s.value("password").toByteArray();
+ if (password.isEmpty()) ui_->password->clear();
+ else ui_->password->setText(QString::fromUtf8(QByteArray::fromBase64(password)));
+
+ dialog()->ComboBoxLoadFromSettings(s, ui_->format, "format", 27);
+ ui_->searchdelay->setValue(s.value("searchdelay", 1500).toInt());
+ ui_->artistssearchlimit->setValue(s.value("artistssearchlimit", 4).toInt());
+ ui_->albumssearchlimit->setValue(s.value("albumssearchlimit", 10).toInt());
+ ui_->songssearchlimit->setValue(s.value("songssearchlimit", 10).toInt());
+ ui_->checkbox_download_album_covers->setChecked(s.value("downloadalbumcovers", true).toBool());
+
+ s.endGroup();
+
+ if (service_->authenticated()) ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn);
+
+ Init(ui_->layout_qobuzsettingspage->parentWidget());
+
+}
+
+void QobuzSettingsPage::Save() {
+
+ QSettings s;
+ s.beginGroup(kSettingsGroup);
+ s.setValue("enabled", ui_->enable->isChecked());
+ s.setValue("app_id", ui_->app_id->text());
+ s.setValue("app_secret", ui_->app_secret->text());
+
+ s.setValue("username", ui_->username->text());
+ s.setValue("password", QString::fromUtf8(ui_->password->text().toUtf8().toBase64()));
+
+ s.setValue("format", ui_->format->itemData(ui_->format->currentIndex()));
+ s.setValue("searchdelay", ui_->searchdelay->value());
+ s.setValue("artistssearchlimit", ui_->artistssearchlimit->value());
+ s.setValue("albumssearchlimit", ui_->albumssearchlimit->value());
+ s.setValue("songssearchlimit", ui_->songssearchlimit->value());
+ s.setValue("downloadalbumcovers", ui_->checkbox_download_album_covers->isChecked());
+ s.endGroup();
+
+ service_->ReloadSettings();
+
+}
+
+void QobuzSettingsPage::LoginClicked() {
+
+ if (ui_->app_id->text().isEmpty()) {
+ QMessageBox::critical(this, tr("Configuration incomplete"), tr("Missing app id."));
+ return;
+ }
+ if (ui_->username->text().isEmpty()) {
+ QMessageBox::critical(this, tr("Configuration incomplete"), tr("Missing username."));
+ return;
+ }
+ if (ui_->password->text().isEmpty()) {
+ QMessageBox::critical(this, tr("Configuration incomplete"), tr("Missing password."));
+ return;
+ }
+
+ emit Login(ui_->app_id->text(), ui_->username->text(), ui_->password->text());
+ ui_->button_login->setEnabled(false);
+
+}
+
+bool QobuzSettingsPage::eventFilter(QObject *object, QEvent *event) {
+
+ if (object == dialog() && event->type() == QEvent::Enter) {
+ ui_->button_login->setEnabled(true);
+ return false;
+ }
+
+ return SettingsPage::eventFilter(object, event);
+
+}
+
+void QobuzSettingsPage::LogoutClicked() {
+ service_->Logout();
+ ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
+ ui_->button_login->setEnabled(true);
+}
+
+void QobuzSettingsPage::LoginSuccess() {
+ if (!this->isVisible()) return;
+ ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn);
+ ui_->button_login->setEnabled(true);
+}
+
+void QobuzSettingsPage::LoginFailure(QString failure_reason) {
+ if (!this->isVisible()) return;
+ QMessageBox::warning(this, tr("Authentication failed"), failure_reason);
+}
diff --git a/src/settings/qobuzsettingspage.h b/src/settings/qobuzsettingspage.h
new file mode 100644
index 00000000..21db0a4f
--- /dev/null
+++ b/src/settings/qobuzsettingspage.h
@@ -0,0 +1,62 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2019, Jonas Kvinge
+ *
+ * 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 .
+ *
+ */
+
+#ifndef QOBUZSETTINGSPAGE_H
+#define QOBUZSETTINGSPAGE_H
+
+#include
+#include
+
+#include "settings/settingspage.h"
+
+class QEvent;
+class SettingsDialog;
+class QobuzService;
+class Ui_QobuzSettingsPage;
+
+class QobuzSettingsPage : public SettingsPage {
+ Q_OBJECT
+
+ public:
+ explicit QobuzSettingsPage(SettingsDialog* parent = nullptr);
+ ~QobuzSettingsPage();
+
+ static const char *kSettingsGroup;
+
+ void Load();
+ void Save();
+
+ bool eventFilter(QObject *object, QEvent *event);
+
+ signals:
+ void Login();
+ void Login(const QString &username, const QString &password, const QString &token);
+
+ private slots:
+ void LoginClicked();
+ void LogoutClicked();
+ void LoginSuccess();
+ void LoginFailure(QString failure_reason);
+
+ private:
+ Ui_QobuzSettingsPage* ui_;
+ QobuzService *service_;
+};
+
+#endif
diff --git a/src/settings/qobuzsettingspage.ui b/src/settings/qobuzsettingspage.ui
new file mode 100644
index 00000000..7c7b5571
--- /dev/null
+++ b/src/settings/qobuzsettingspage.ui
@@ -0,0 +1,306 @@
+
+
+ QobuzSettingsPage
+
+
+
+ 0
+ 0
+ 472
+ 697
+
+
+
+ Qobuz
+
+
+ -
+
+
+ Enable
+
+
+
+ -
+
+
+ Qobuz support is not official and requires an API app ID and secret from a registered application to work. We can't help you getting these.
+
+
+ true
+
+
+ 10
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Authentication
+
+
+
-
+
+
+
+ 150
+ 0
+
+
+
+ App ID
+
+
+
+ -
+
+
+ -
+
+
+ Username
+
+
+
+ -
+
+
-
+
+
+
+
+ -
+
+
+ Password
+
+
+
+ -
+
+
+ QLineEdit::Password
+
+
+
+ -
+
+
+ App Secret
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+ Login
+
+
+
+ -
+
+
+ -
+
+
+ Preferences
+
+
+
-
+
+
+ Audio format
+
+
+
+ -
+
+
+ -
+
+
+ Search delay
+
+
+
+ -
+
+
+ ms
+
+
+ 0
+
+
+ 10000
+
+
+ 50
+
+
+ 1500
+
+
+
+ -
+
+
+ Artists search limit
+
+
+
+ -
+
+
+ 1
+
+
+ 100
+
+
+ 50
+
+
+
+ -
+
+
+ Albums search limit
+
+
+
+ -
+
+
+ 1
+
+
+ 1000
+
+
+ 50
+
+
+
+ -
+
+
+ Songs search limit
+
+
+
+ -
+
+
+ 1
+
+
+ 1000
+
+
+ 50
+
+
+
+ -
+
+
+ Download album covers
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 30
+
+
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 64
+ 64
+
+
+
+
+ 64
+ 64
+
+
+
+ :/icons/64x64/qobuz.png
+
+
+
+
+
+
+
+
+
+ LoginStateWidget
+ QWidget
+ widgets/loginstatewidget.h
+ 1
+
+
+
+ enable
+ app_id
+ app_secret
+ username
+ password
+ button_login
+ format
+ searchdelay
+ artistssearchlimit
+ albumssearchlimit
+ songssearchlimit
+ checkbox_download_album_covers
+
+
+
+
+
+
+
diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp
index 935c051e..3deb582d 100644
--- a/src/settings/settingsdialog.cpp
+++ b/src/settings/settingsdialog.cpp
@@ -78,6 +78,9 @@
#ifdef HAVE_TIDAL
# include "tidalsettingspage.h"
#endif
+#ifdef HAVE_QOBUZ
+# include "qobuzsettingspage.h"
+#endif
#include "ui_settingsdialog.h"
@@ -153,7 +156,7 @@ SettingsDialog::SettingsDialog(Application *app, OSDBase *osd, QMainWindow *main
AddPage(Page_Moodbar, new MoodbarSettingsPage(this), iface);
#endif
-#if defined(HAVE_SUBSONIC) || defined(HAVE_TIDAL)
+#if defined(HAVE_SUBSONIC) || defined(HAVE_TIDAL) || defined(HAVE_QOBUZ)
QTreeWidgetItem *streaming = AddCategory(tr("Streaming"));
#endif
@@ -163,6 +166,9 @@ SettingsDialog::SettingsDialog(Application *app, OSDBase *osd, QMainWindow *main
#ifdef HAVE_TIDAL
AddPage(Page_Tidal, new TidalSettingsPage(this), streaming);
#endif
+#ifdef HAVE_QOBUZ
+ AddPage(Page_Qobuz, new QobuzSettingsPage(this), streaming);
+#endif
// List box
connect(ui_->list, SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)), SLOT(CurrentItemChanged(QTreeWidgetItem*)));
diff --git a/src/settings/settingsdialog.h b/src/settings/settingsdialog.h
index 8b937d02..4892c6f7 100644
--- a/src/settings/settingsdialog.h
+++ b/src/settings/settingsdialog.h
@@ -90,6 +90,7 @@ class SettingsDialog : public QDialog {
Page_Moodbar,
Page_Subsonic,
Page_Tidal,
+ Page_Qobuz,
};
enum Role {
diff --git a/src/smartplaylists/playlistgenerator.cpp b/src/smartplaylists/playlistgenerator.cpp
new file mode 100644
index 00000000..e3e9f38a
--- /dev/null
+++ b/src/smartplaylists/playlistgenerator.cpp
@@ -0,0 +1,43 @@
+/*
+ * Strawberry Music Player
+ * This file was part of Clementine.
+ * Copyright 2010, David Sansome
+ *
+ * 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 .
+ *
+ */
+
+#include "config.h"
+
+#include
+#include
+
+#include "core/logging.h"
+
+#include "playlistgenerator.h"
+#include "playlistquerygenerator.h"
+
+const int PlaylistGenerator::kDefaultLimit = 20;
+const int PlaylistGenerator::kDefaultDynamicHistory = 5;
+const int PlaylistGenerator::kDefaultDynamicFuture = 15;
+
+PlaylistGenerator::PlaylistGenerator() : QObject(nullptr) {}
+
+PlaylistGeneratorPtr PlaylistGenerator::Create(const Type type) {
+
+ Q_UNUSED(type)
+
+ return PlaylistGeneratorPtr(new PlaylistQueryGenerator);
+
+}
diff --git a/src/smartplaylists/playlistgenerator.h b/src/smartplaylists/playlistgenerator.h
new file mode 100644
index 00000000..a637a6bc
--- /dev/null
+++ b/src/smartplaylists/playlistgenerator.h
@@ -0,0 +1,100 @@
+/*
+ * Strawberry Music Player
+ * This file was part of Clementine.
+ * Copyright 2010, David Sansome
+ *
+ * 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 .
+ *
+ */
+
+#ifndef PLAYLISTGENERATOR_H
+#define PLAYLISTGENERATOR_H
+
+#include "config.h"
+
+#include
+
+#include
+#include
+#include
+
+#include "playlist/playlistitem.h"
+
+class CollectionBackend;
+
+class PlaylistGenerator : public QObject, public std::enable_shared_from_this {
+ Q_OBJECT
+
+ public:
+ explicit PlaylistGenerator();
+
+ static const int kDefaultLimit;
+ static const int kDefaultDynamicHistory;
+ static const int kDefaultDynamicFuture;
+
+ enum Type {
+ Type_None = 0,
+ Type_Query = 1
+ };
+
+ // Creates a new PlaylistGenerator of the given type
+ static std::shared_ptr Create(const Type type = Type_Query);
+
+ // Should be called before Load on a new PlaylistGenerator
+ void set_collection(CollectionBackend *backend) { backend_ = backend; }
+ void set_name(const QString &name) { name_ = name; }
+ CollectionBackend *collection() const { return backend_; }
+ QString name() const { return name_; }
+
+ // Name of the subclass
+ virtual Type type() const = 0;
+
+ // Serializes the PlaylistGenerator's settings
+ // Called on UI-thread.
+ virtual void Load(const QByteArray &data) = 0;
+ // Called on UI-thread.
+ virtual QByteArray Save() const = 0;
+
+ // Creates and returns a playlist
+ // Called from non-UI thread.
+ virtual PlaylistItemList Generate() = 0;
+
+ // If the generator can be used as a dynamic playlist then GenerateMore should return the next tracks in the sequence.
+ // The subclass should remember the last GetDynamicHistory() + GetDynamicFuture() tracks,
+ // and ensure that the tracks returned from this method are not in that set.
+ virtual bool is_dynamic() const { return false; }
+ virtual void set_dynamic(const bool dynamic) { Q_UNUSED(dynamic); }
+ // Called from non-UI thread.
+ virtual PlaylistItemList GenerateMore(int count) {
+ Q_UNUSED(count);
+ return PlaylistItemList();
+ }
+
+ virtual int GetDynamicHistory() { return kDefaultDynamicHistory; }
+ virtual int GetDynamicFuture() { return kDefaultDynamicFuture; }
+
+ signals:
+ void Error(const QString& message);
+
+ protected:
+ CollectionBackend *backend_;
+
+ private:
+ QString name_;
+
+};
+
+#include "playlistgenerator_fwd.h"
+
+#endif // PLAYLISTGENERATOR_H
diff --git a/src/smartplaylists/playlistgenerator_fwd.h b/src/smartplaylists/playlistgenerator_fwd.h
new file mode 100644
index 00000000..bb3d58b1
--- /dev/null
+++ b/src/smartplaylists/playlistgenerator_fwd.h
@@ -0,0 +1,32 @@
+/*
+ * Strawberry Music Player
+ * This file was part of Clementine.
+ * Copyright 2010, David Sansome
+ *
+ * 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 .
+ *
+ */
+
+#ifndef PLAYLISTGENERATOR_FWD_H
+#define PLAYLISTGENERATOR_FWD_H
+
+#include "config.h"
+
+#include
+
+class PlaylistGenerator;
+
+typedef std::shared_ptr PlaylistGeneratorPtr;
+
+#endif // PLAYLISTGENERATOR_FWD_H
diff --git a/src/smartplaylists/playlistgeneratorinserter.cpp b/src/smartplaylists/playlistgeneratorinserter.cpp
new file mode 100644
index 00000000..12edac2a
--- /dev/null
+++ b/src/smartplaylists/playlistgeneratorinserter.cpp
@@ -0,0 +1,90 @@
+/*
+ * Strawberry Music Player
+ * This file was part of Clementine.
+ * Copyright 2010, David Sansome
+ *
+ * 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 .
+ *
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+
+#include "core/closure.h"
+#include "core/taskmanager.h"
+
+#include "playlist/playlist.h"
+#include "playlistgenerator.h"
+#include "playlistgeneratorinserter.h"
+
+class CollectionBackend;
+
+PlaylistGeneratorInserter::PlaylistGeneratorInserter(TaskManager *task_manager, CollectionBackend *collection, QObject *parent)
+ : QObject(parent),
+ task_manager_(task_manager),
+ collection_(collection),
+ task_id_(-1),
+ is_dynamic_(false)
+ {}
+
+PlaylistItemList PlaylistGeneratorInserter::Generate(PlaylistGeneratorPtr generator, int dynamic_count) {
+
+ if (dynamic_count) {
+ return generator->GenerateMore(dynamic_count);
+ }
+ else {
+ return generator->Generate();
+ }
+
+}
+
+void PlaylistGeneratorInserter::Load(Playlist *destination, const int row, const bool play_now, const bool enqueue, const bool enqueue_next, PlaylistGeneratorPtr generator, const int dynamic_count) {
+
+ task_id_ = task_manager_->StartTask(tr("Loading smart playlist"));
+
+ destination_ = destination;
+ row_ = row;
+ play_now_ = play_now;
+ enqueue_ = enqueue;
+ enqueue_next_ = enqueue_next;
+ is_dynamic_ = generator->is_dynamic();
+
+ connect(generator.get(), SIGNAL(Error(QString)), SIGNAL(Error(QString)));
+
+ QFuture