diff --git a/CMakeLists.txt b/CMakeLists.txt
index 28c42078d..4035c6a58 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -345,6 +345,7 @@ optional_component(TRANSLATIONS ON "Translations"
)
optional_component(TIDAL ON "Tidal support")
+optional_component(QOBUZ ON "Qobuz support")
optional_component(SUBSONIC ON "Subsonic support")
optional_component(MOODBAR ON "Moodbar"
diff --git a/README.md b/README.md
index a2dc00cca..137a3f28a 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ Strawberry is a music player and music collection organizer. It is a fork of Cle
* Audio analyzer
* Audio equalizer
* Transfer music to iPod, iPhone, MTP or mass-storage USB player
- * Streaming support for Tidal and Subsonic
+ * Streaming support for Tidal, Qobuz and Subsonic
* Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
It has so far been tested to work on Linux, OpenBSD, macOS and Windows.
diff --git a/data/data.qrc b/data/data.qrc
index a075d0c17..f85e63311 100644
--- a/data/data.qrc
+++ b/data/data.qrc
@@ -7,6 +7,7 @@
schema/schema-4.sql
schema/schema-5.sql
schema/schema-6.sql
+ schema/schema-7.sql
schema/device-schema.sql
style/strawberry.css
html/playing-tooltip-plain.html
diff --git a/data/icons.qrc b/data/icons.qrc
index b6d184914..9566d728f 100644
--- a/data/icons.qrc
+++ b/data/icons.qrc
@@ -83,12 +83,13 @@
icons/128x128/xine.png
icons/128x128/zoom-in.png
icons/128x128/zoom-out.png
- icons/128x128/tidal.png
icons/128x128/scrobble.png
icons/128x128/scrobble-disabled.png
icons/128x128/moodbar.png
icons/128x128/love.png
icons/128x128/subsonic.png
+ icons/128x128/tidal.png
+ icons/128x128/qobuz.png
icons/64x64/albums.png
icons/64x64/alsa.png
icons/64x64/application-exit.png
@@ -172,12 +173,13 @@
icons/64x64/xine.png
icons/64x64/zoom-in.png
icons/64x64/zoom-out.png
- icons/64x64/tidal.png
icons/64x64/scrobble.png
icons/64x64/scrobble-disabled.png
icons/64x64/moodbar.png
icons/64x64/love.png
icons/64x64/subsonic.png
+ icons/64x64/tidal.png
+ icons/64x64/qobuz.png
icons/48x48/albums.png
icons/48x48/alsa.png
icons/48x48/application-exit.png
@@ -264,12 +266,13 @@
icons/48x48/xine.png
icons/48x48/zoom-in.png
icons/48x48/zoom-out.png
- icons/48x48/tidal.png
icons/48x48/scrobble.png
icons/48x48/scrobble-disabled.png
icons/48x48/moodbar.png
icons/48x48/love.png
icons/48x48/subsonic.png
+ icons/48x48/tidal.png
+ icons/48x48/qobuz.png
icons/32x32/albums.png
icons/32x32/alsa.png
icons/32x32/application-exit.png
@@ -357,12 +360,13 @@
icons/32x32/xine.png
icons/32x32/zoom-in.png
icons/32x32/zoom-out.png
- icons/32x32/tidal.png
icons/32x32/scrobble.png
icons/32x32/scrobble-disabled.png
icons/32x32/moodbar.png
icons/32x32/love.png
icons/32x32/subsonic.png
+ icons/32x32/tidal.png
+ icons/32x32/qobuz.png
icons/22x22/albums.png
icons/22x22/alsa.png
icons/22x22/application-exit.png
@@ -450,11 +454,12 @@
icons/22x22/xine.png
icons/22x22/zoom-in.png
icons/22x22/zoom-out.png
- icons/22x22/tidal.png
icons/22x22/scrobble.png
icons/22x22/scrobble-disabled.png
icons/22x22/moodbar.png
icons/22x22/love.png
icons/22x22/subsonic.png
+ icons/22x22/tidal.png
+ icons/22x22/qobuz.png
diff --git a/data/icons/128x128/qobuz.png b/data/icons/128x128/qobuz.png
new file mode 100644
index 000000000..4b6b17756
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 000000000..09834a5f1
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 000000000..c9c6b8a32
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 000000000..08af18ceb
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 000000000..4c7f31b4e
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 000000000..c7ad97fdb
Binary files /dev/null and b/data/icons/full/qobuz.png differ
diff --git a/data/schema/device-schema.sql b/data/schema/device-schema.sql
index df18e7b77..886e706bc 100644
--- a/data/schema/device-schema.sql
+++ b/data/schema/device-schema.sql
@@ -76,4 +76,3 @@ CREATE VIRTUAL TABLE device_%deviceid_fts USING fts3(
);
UPDATE devices SET schema_version=0 WHERE ROWID=%deviceid;
-
diff --git a/data/schema/schema-7.sql b/data/schema/schema-7.sql
new file mode 100644
index 000000000..92a03b4a9
--- /dev/null
+++ b/data/schema/schema-7.sql
@@ -0,0 +1,217 @@
+CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
+
+ title TEXT NOT NULL,
+ album TEXT NOT NULL,
+ artist TEXT NOT NULL,
+ albumartist TEXT NOT NULL,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT 0,
+ genre TEXT NOT NULL,
+ compilation INTEGER NOT NULL DEFAULT -1,
+ composer TEXT NOT NULL,
+ performer TEXT NOT NULL,
+ grouping TEXT NOT NULL,
+ comment TEXT NOT NULL,
+ lyrics TEXT NOT NULL,
+
+ artist_id INTEGER NOT NULL DEFAULT -1,
+ album_id INTEGER NOT NULL DEFAULT -1,
+ song_id INTEGER NOT NULL DEFAULT -1,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT 0,
+ samplerate INTEGER NOT NULL DEFAULT 0,
+ bitdepth INTEGER NOT NULL DEFAULT 0,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL,
+ filename TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT 0,
+ mtime INTEGER NOT NULL DEFAULT 0,
+ ctime INTEGER NOT NULL DEFAULT 0,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT 0,
+
+ 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 qobuz_albums_songs (
+
+ title TEXT NOT NULL,
+ album TEXT NOT NULL,
+ artist TEXT NOT NULL,
+ albumartist TEXT NOT NULL,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT 0,
+ genre TEXT NOT NULL,
+ compilation INTEGER NOT NULL DEFAULT -1,
+ composer TEXT NOT NULL,
+ performer TEXT NOT NULL,
+ grouping TEXT NOT NULL,
+ comment TEXT NOT NULL,
+ lyrics TEXT NOT NULL,
+
+ artist_id INTEGER NOT NULL DEFAULT -1,
+ album_id INTEGER NOT NULL DEFAULT -1,
+ song_id INTEGER NOT NULL DEFAULT -1,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT 0,
+ samplerate INTEGER NOT NULL DEFAULT 0,
+ bitdepth INTEGER NOT NULL DEFAULT 0,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL,
+ filename TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT 0,
+ mtime INTEGER NOT NULL DEFAULT 0,
+ ctime INTEGER NOT NULL DEFAULT 0,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT 0,
+
+ 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 qobuz_songs (
+
+ title TEXT NOT NULL,
+ album TEXT NOT NULL,
+ artist TEXT NOT NULL,
+ albumartist TEXT NOT NULL,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT 0,
+ genre TEXT NOT NULL,
+ compilation INTEGER NOT NULL DEFAULT -1,
+ composer TEXT NOT NULL,
+ performer TEXT NOT NULL,
+ grouping TEXT NOT NULL,
+ comment TEXT NOT NULL,
+ lyrics TEXT NOT NULL,
+
+ artist_id INTEGER NOT NULL DEFAULT -1,
+ album_id INTEGER NOT NULL DEFAULT -1,
+ song_id INTEGER NOT NULL DEFAULT -1,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT 0,
+ samplerate INTEGER NOT NULL DEFAULT 0,
+ bitdepth INTEGER NOT NULL DEFAULT 0,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL,
+ filename TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT 0,
+ mtime INTEGER NOT NULL DEFAULT 0,
+ ctime INTEGER NOT NULL DEFAULT 0,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT 0,
+
+ 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 VIRTUAL TABLE IF NOT EXISTS qobuz_artists_songs_fts USING fts3(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize=unicode
+
+);
+
+CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_albums_songs_fts USING fts3(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize=unicode
+
+);
+
+CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_songs_fts USING fts3(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize=unicode
+
+);
+
+UPDATE schema_version SET version=7;
diff --git a/data/schema/schema.sql b/data/schema/schema.sql
index bcda861a9..67f0d0b87 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 (6);
+INSERT INTO schema_version (version) VALUES (7);
CREATE TABLE IF NOT EXISTS directories (
path TEXT NOT NULL,
@@ -302,6 +302,177 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
);
+CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
+
+ title TEXT NOT NULL,
+ album TEXT NOT NULL,
+ artist TEXT NOT NULL,
+ albumartist TEXT NOT NULL,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT 0,
+ genre TEXT NOT NULL,
+ compilation INTEGER NOT NULL DEFAULT -1,
+ composer TEXT NOT NULL,
+ performer TEXT NOT NULL,
+ grouping TEXT NOT NULL,
+ comment TEXT NOT NULL,
+ lyrics TEXT NOT NULL,
+
+ artist_id INTEGER NOT NULL DEFAULT -1,
+ album_id INTEGER NOT NULL DEFAULT -1,
+ song_id INTEGER NOT NULL DEFAULT -1,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT 0,
+ samplerate INTEGER NOT NULL DEFAULT 0,
+ bitdepth INTEGER NOT NULL DEFAULT 0,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL,
+ filename TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT 0,
+ mtime INTEGER NOT NULL DEFAULT 0,
+ ctime INTEGER NOT NULL DEFAULT 0,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT 0,
+
+ 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 qobuz_albums_songs (
+
+ title TEXT NOT NULL,
+ album TEXT NOT NULL,
+ artist TEXT NOT NULL,
+ albumartist TEXT NOT NULL,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT 0,
+ genre TEXT NOT NULL,
+ compilation INTEGER NOT NULL DEFAULT -1,
+ composer TEXT NOT NULL,
+ performer TEXT NOT NULL,
+ grouping TEXT NOT NULL,
+ comment TEXT NOT NULL,
+ lyrics TEXT NOT NULL,
+
+ artist_id INTEGER NOT NULL DEFAULT -1,
+ album_id INTEGER NOT NULL DEFAULT -1,
+ song_id INTEGER NOT NULL DEFAULT -1,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT 0,
+ samplerate INTEGER NOT NULL DEFAULT 0,
+ bitdepth INTEGER NOT NULL DEFAULT 0,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL,
+ filename TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT 0,
+ mtime INTEGER NOT NULL DEFAULT 0,
+ ctime INTEGER NOT NULL DEFAULT 0,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT 0,
+
+ 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 qobuz_songs (
+
+ title TEXT NOT NULL,
+ album TEXT NOT NULL,
+ artist TEXT NOT NULL,
+ albumartist TEXT NOT NULL,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT 0,
+ genre TEXT NOT NULL,
+ compilation INTEGER NOT NULL DEFAULT -1,
+ composer TEXT NOT NULL,
+ performer TEXT NOT NULL,
+ grouping TEXT NOT NULL,
+ comment TEXT NOT NULL,
+ lyrics TEXT NOT NULL,
+
+ artist_id INTEGER NOT NULL DEFAULT -1,
+ album_id INTEGER NOT NULL DEFAULT -1,
+ song_id INTEGER NOT NULL DEFAULT -1,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT 0,
+ samplerate INTEGER NOT NULL DEFAULT 0,
+ bitdepth INTEGER NOT NULL DEFAULT 0,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL,
+ filename TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT 0,
+ mtime INTEGER NOT NULL DEFAULT 0,
+ ctime INTEGER NOT NULL DEFAULT 0,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT 0,
+
+ 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 playlists (
name TEXT NOT NULL,
@@ -470,6 +641,51 @@ CREATE VIRTUAL TABLE IF NOT EXISTS subsonic_songs_fts USING fts3(
);
+CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_artists_songs_fts USING fts3(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize=unicode
+
+);
+
+CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_albums_songs_fts USING fts3(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize=unicode
+
+);
+
+CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_songs_fts USING fts3(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize=unicode
+
+);
+
CREATE VIRTUAL TABLE IF NOT EXISTS playlist_items_fts_ USING fts3(
ftstitle,
@@ -500,7 +716,6 @@ CREATE VIRTUAL TABLE IF NOT EXISTS %allsongstables_fts USING fts3(
);
-
INSERT INTO songs_fts (ROWID, ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment)
SELECT ROWID, title, album, artist, albumartist, composer, performer, grouping, genre, comment FROM songs;
diff --git a/debian/control b/debian/control
index 006283d45..234511613 100644
--- a/debian/control
+++ b/debian/control
@@ -66,7 +66,7 @@ Description: Audio player and music collection organizer
- Audio analyzer
- Audio equalizer
- Transfer music to iPod, iPhone, MTP or mass-storage USB player
- - Streaming support for Tidal and Subsonic
+ - Streaming support for Tidal, Qobuz and Subsonic
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
.
It is a fork of Clementine. The name is inspired by the band Strawbs.
diff --git a/debian/copyright b/debian/copyright
index 9bf3bb27e..0c18b14c0 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -58,9 +58,11 @@ Files: src/core/main.h
src/lyrics/*
src/scrobbler/*
src/tidal/*
+ src/qobuz/*
+ src/subsonic/*
src/transcoder/transcoderoptionswavpack.cpp
src/transcoder/transcoderoptionswavpack.h
-Copyright: 2012-2014, 2017-2018, Jonas Kvinge
+Copyright: 2012-2014, 2017-2019, Jonas Kvinge
License: GPL-3+
Files: src/core/main.cpp
diff --git a/dist/man/strawberry.1 b/dist/man/strawberry.1
index 3aac59771..0010b01e8 100644
--- a/dist/man/strawberry.1
+++ b/dist/man/strawberry.1
@@ -37,7 +37,7 @@ Features:
.br
- Transfer music to iPod, iPhone, MTP or mass-storage USB player
.br
-- Integrated Tidal support
+- Streaming from Tidal, Qobuz and Subsonic
.TP
It is a fork of Clementine. The name is inspired by the band Strawbs.
.SH OPTIONS
diff --git a/dist/rpm/strawberry.spec.in b/dist/rpm/strawberry.spec.in
index 49b509f63..430a4327b 100644
--- a/dist/rpm/strawberry.spec.in
+++ b/dist/rpm/strawberry.spec.in
@@ -106,7 +106,7 @@ Features:
- Audio analyzer
- Audio equalizer
- Transfer music to iPod, iPhone, MTP or mass-storage USB player
- - Streaming support for Tidal and Subsonic
+ - Streaming support for Tidal, Qobuz and Subsonic
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
%prep
diff --git a/dist/unix/org.strawbs.strawberry.appdata.xml b/dist/unix/org.strawbs.strawberry.appdata.xml
index 82e33102a..145c4b6bc 100644
--- a/dist/unix/org.strawbs.strawberry.appdata.xml
+++ b/dist/unix/org.strawbs.strawberry.appdata.xml
@@ -34,7 +34,7 @@
Audio analyzer
Audio equalizer
Transfer music to iPod, iPhone, MTP or mass-storage USB player
- Streaming support for Tidal and Subsonic
+ Streaming support for Tidal, Qobuz and Subsonic
Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 641ebcfe7..6bee17d3a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -911,6 +911,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
+)
+
optional_source(HAVE_SUBSONIC
SOURCES
subsonic/subsonicservice.cpp
diff --git a/src/config.h.in b/src/config.h.in
index c017d1554..d8d27c670 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -51,6 +51,7 @@
#cmakedefine HAVE_TIDAL
#cmakedefine HAVE_SUBSONIC
+#cmakedefine HAVE_QOBUZ
#cmakedefine HAVE_MOODBAR
diff --git a/src/core/application.cpp b/src/core/application.cpp
index 83fc8399e..cbd143a26 100644
--- a/src/core/application.cpp
+++ b/src/core/application.cpp
@@ -71,6 +71,10 @@
# include "covermanager/tidalcoverprovider.h"
#endif
+#ifdef HAVE_QOBUZ
+# include "qobuz/qobuzservice.h"
+#endif
+
#ifdef HAVE_SUBSONIC
# include "subsonic/subsonicservice.h"
#endif
@@ -140,6 +144,9 @@ class ApplicationImpl {
#ifdef HAVE_TIDAL
internet_services->AddService(new TidalService(app, internet_services));
#endif
+#ifdef HAVE_QOBUZ
+ internet_services->AddService(new QobuzService(app, internet_services));
+#endif
#ifdef HAVE_SUBSONIC
internet_services->AddService(new SubsonicService(app, internet_services));
#endif
@@ -147,6 +154,9 @@ class ApplicationImpl {
}),
#ifdef HAVE_TIDAL
tidal_search_([=]() { return new InternetSearch(app, Song::Source_Tidal, app); }),
+#endif
+#ifdef HAVE_QOBUZ
+ qobuz_search_([=]() { return new InternetSearch(app, Song::Source_Qobuz, app); }),
#endif
scrobbler_([=]() { return new AudioScrobbler(app, app); }),
@@ -177,6 +187,9 @@ class ApplicationImpl {
Lazy internet_services_;
#ifdef HAVE_TIDAL
Lazy tidal_search_;
+#endif
+#ifdef HAVE_QOBUZ
+ Lazy qobuz_search_;
#endif
Lazy scrobbler_;
#ifdef HAVE_MOODBAR
@@ -254,6 +267,9 @@ InternetServices *Application::internet_services() const { return p_->internet_s
#ifdef HAVE_TIDAL
InternetSearch *Application::tidal_search() const { return p_->tidal_search_.get(); }
#endif
+#ifdef HAVE_QOBUZ
+InternetSearch *Application::qobuz_search() const { return p_->qobuz_search_.get(); }
+#endif
AudioScrobbler *Application::scrobbler() const { return p_->scrobbler_.get(); }
#ifdef HAVE_MOODBAR
MoodbarController *Application::moodbar_controller() const { return p_->moodbar_controller_.get(); }
diff --git a/src/core/application.h b/src/core/application.h
index ade650653..2d39446c0 100644
--- a/src/core/application.h
+++ b/src/core/application.h
@@ -102,6 +102,9 @@ class Application : public QObject {
#ifdef HAVE_TIDAL
InternetSearch *tidal_search() const;
#endif
+#ifdef HAVE_QOBUZ
+ InternetSearch *qobuz_search() const;
+#endif
#ifdef HAVE_MOODBAR
MoodbarController *moodbar_controller() const;
diff --git a/src/core/database.cpp b/src/core/database.cpp
index 249596150..7364f5684 100644
--- a/src/core/database.cpp
+++ b/src/core/database.cpp
@@ -52,7 +52,7 @@
#include "scopedtransaction.h"
const char *Database::kDatabaseFilename = "strawberry.db";
-const int Database::kSchemaVersion = 6;
+const int Database::kSchemaVersion = 7;
const char *Database::kMagicAllSongsTables = "%allsongstables";
int Database::sNextConnectionId = 1;
diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp
index 64740a90d..b9009b212 100644
--- a/src/core/mainwindow.cpp
+++ b/src/core/mainwindow.cpp
@@ -138,6 +138,10 @@
# include "tidal/tidalservice.h"
# include "settings/tidalsettingspage.h"
#endif
+#ifdef HAVE_QOBUZ
+# include "qobuz/qobuzservice.h"
+# include "settings/qobuzsettingspage.h"
+#endif
#ifdef HAVE_SUBSONIC
# include "subsonic/subsonicservice.h"
# include "settings/subsonicsettingspage.h"
@@ -216,6 +220,9 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
#ifdef HAVE_TIDAL
tidal_view_(new InternetTabsView(app_, app->internet_services()->ServiceBySource(Song::Source_Tidal), app_->tidal_search(), TidalSettingsPage::kSettingsGroup, SettingsDialog::Page_Tidal, this)),
#endif
+#ifdef HAVE_QOBUZ
+ qobuz_view_(new InternetTabsView(app_, app->internet_services()->ServiceBySource(Song::Source_Qobuz), app_->qobuz_search(), QobuzSettingsPage::kSettingsGroup, SettingsDialog::Page_Qobuz, this)),
+#endif
#ifdef HAVE_SUBSONIC
subsonic_view_(new InternetSongsView(app_, app->internet_services()->ServiceBySource(Song::Source_Subsonic), SubsonicSettingsPage::kSettingsGroup, SettingsDialog::Page_Subsonic, this)),
#endif
@@ -273,6 +280,9 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
#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
#ifdef HAVE_SUBSONIC
ui_->tabs->AddTab(subsonic_view_, "subsonic", IconLoader::Load("subsonic"), tr("Subsonic"));
#endif
@@ -566,7 +576,13 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
TidalService *tidalservice = qobject_cast (app_->internet_services()->ServiceBySource(Song::Source_Tidal));
if (tidalservice)
connect(this, SIGNAL(AuthorisationUrlReceived(const QUrl&)), tidalservice, SLOT(AuthorisationUrlReceived(const 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
#ifdef HAVE_SUBSONIC
@@ -889,6 +905,16 @@ void MainWindow::ReloadSettings() {
ui_->tabs->DisableTab(tidal_view_);
#endif
+#ifdef HAVE_QOBUZ
+ settings.beginGroup(QobuzSettingsPage::kSettingsGroup);
+ bool enable_qobuz = settings.value("enabled", false).toBool();
+ settings.endGroup();
+ if (enable_qobuz)
+ ui_->tabs->EnableTab(qobuz_view_);
+ else
+ ui_->tabs->DisableTab(qobuz_view_);
+#endif
+
#ifdef HAVE_SUBSONIC
settings.beginGroup(SubsonicSettingsPage::kSettingsGroup);
bool enable_subsonic = settings.value("enabled", false).toBool();
@@ -917,6 +943,9 @@ void MainWindow::ReloadAllSettings() {
#ifdef HAVE_TIDAL
tidal_view_->ReloadSettings();
#endif
+#ifdef HAVE_QOBUZ
+ qobuz_view_->ReloadSettings();
+#endif
#ifdef HAVE_SUBSONIC
subsonic_view_->ReloadSettings();
#endif
diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h
index 416852f41..94ab4d503 100644
--- a/src/core/mainwindow.h
+++ b/src/core/mainwindow.h
@@ -314,6 +314,7 @@ signals:
#endif
InternetTabsView *tidal_view_;
+ InternetTabsView *qobuz_view_;
InternetSongsView *subsonic_view_;
QAction *collection_show_all_;
diff --git a/src/core/song.cpp b/src/core/song.cpp
index a2d0eb150..c8ea4fd0f 100644
--- a/src/core/song.cpp
+++ b/src/core/song.cpp
@@ -333,7 +333,7 @@ bool Song::has_cue() const { return !d->cue_path_.isEmpty(); }
bool Song::is_collection_song() const { return !is_cdda() && !is_stream() && id() != -1; }
bool Song::is_metadata_good() const { return !d->title_.isEmpty() && !d->album_.isEmpty() && !d->artist_.isEmpty() && !d->url_.isEmpty() && d->end_ > 0; }
-bool Song::is_stream() const { return d->source_ == Source_Stream || d->source_ == Source_Tidal || d->source_ == Source_Subsonic; }
+bool Song::is_stream() const { return d->source_ == Source_Stream || d->source_ == Source_Tidal || d->source_ == Source_Subsonic || d->source_ == Source_Qobuz; }
bool Song::is_cdda() const { return d->source_ == Source_CDDA; }
const QString &Song::error() const { return d->error_; }
@@ -411,6 +411,7 @@ Song::Source Song::SourceFromURL(const QUrl &url) {
else if (url.scheme() == "cdda") return Source_CDDA;
else if (url.scheme() == "tidal") return Source_Tidal;
else if (url.scheme() == "subsonic") return Source_Subsonic;
+ else if (url.scheme() == "qobuz") return Source_Qobuz;
else if (url.scheme() == "http" || url.scheme() == "https" || url.scheme() == "rtsp") return Source_Stream;
else return Source_Unknown;
@@ -426,6 +427,7 @@ QString Song::TextForSource(Source source) {
case Song::Source_Stream: return QObject::tr("Stream");
case Song::Source_Tidal: return QObject::tr("Tidal");
case Song::Source_Subsonic: return QObject::tr("subsonic");
+ case Song::Source_Qobuz: return QObject::tr("qobuz");
case Song::Source_Unknown: return QObject::tr("Unknown");
}
return QObject::tr("Unknown");
@@ -442,6 +444,7 @@ QIcon Song::IconForSource(Source source) {
case Song::Source_Stream: return IconLoader::Load("applications-internet");
case Song::Source_Tidal: return IconLoader::Load("tidal");
case Song::Source_Subsonic: return IconLoader::Load("subsonic");
+ case Song::Source_Qobuz: return IconLoader::Load("qobuz");
case Song::Source_Unknown: return IconLoader::Load("edit-delete");
}
return IconLoader::Load("edit-delete");
diff --git a/src/core/song.h b/src/core/song.h
index 0bd09e158..7958c3cdd 100644
--- a/src/core/song.h
+++ b/src/core/song.h
@@ -75,6 +75,7 @@ class Song {
Source_Stream = 5,
Source_Tidal = 6,
Source_Subsonic = 7,
+ Source_Qobuz = 8,
};
// Don't change these values - they're stored in the database, and defined in the tag reader protobuf.
diff --git a/src/qobuz/qobuzbaserequest.cpp b/src/qobuz/qobuzbaserequest.cpp
new file mode 100644
index 000000000..29b45c581
--- /dev/null
+++ b/src/qobuz/qobuzbaserequest.cpp
@@ -0,0 +1,194 @@
+/*
+ * 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 "core/logging.h"
+#include "core/network.h"
+#include "qobuzservice.h"
+#include "qobuzbaserequest.h"
+
+const char *QobuzBaseRequest::kApiUrl = "http://www.qobuz.com/api.json/0.2";
+
+QobuzBaseRequest::QobuzBaseRequest(QobuzService *service, NetworkAccessManager *network, QObject *parent) :
+ QObject(parent),
+ service_(service),
+ network_(network)
+ {}
+
+QobuzBaseRequest::~QobuzBaseRequest() {
+
+ while (!replies_.isEmpty()) {
+ QNetworkReply *reply = replies_.takeFirst();
+ disconnect(reply, 0, nullptr, 0);
+ if (reply->isRunning()) reply->abort();
+ reply->deleteLater();
+ }
+
+}
+
+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& param : params) {
+ EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
+ url_query.addQueryItem(encoded_param.first, encoded_param.second);
+ }
+
+ QUrl url(kApiUrl + QString("/") + ressource_name);
+ url.setQuery(url_query);
+ QNetworkRequest req(url);
+ req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+
+ QNetworkReply *reply = network_->get(req);
+ replies_ << reply;
+
+ //qLog(Debug) << "Qobuz: Sending request" << url;
+
+ return reply;
+
+}
+
+QByteArray QobuzBaseRequest::GetReplyData(QNetworkReply *reply, QString &error) {
+
+ if (replies_.contains(reply)) {
+ replies_.removeAll(reply);
+ reply->deleteLater();
+ }
+
+ QByteArray data;
+
+ if (reply->error() == QNetworkReply::NoError) {
+ int http_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ if (http_code == 200) {
+ data = reply->readAll();
+ }
+ else {
+ error = Error(QString("Received HTTP code %1").arg(http_code));
+ }
+ }
+ else {
+ if (reply->error() < 200) {
+ // This is a network error, there is nothing more to do.
+ error = 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();
+ QJsonParseError parse_error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error);
+ QString failure_reason;
+ if (parse_error.error == QJsonParseError::NoError && !json_doc.isNull() && !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();
+ failure_reason = QString("%1 (%2)").arg(message).arg(code);
+ }
+ }
+ if (failure_reason.isEmpty()) {
+ failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
+ }
+ error = Error(failure_reason);
+ }
+ return QByteArray();
+ }
+
+ return data;
+
+}
+
+QJsonObject QobuzBaseRequest::ExtractJsonObj(QByteArray &data, QString &error) {
+
+ QJsonParseError json_error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
+
+ if (json_error.error != QJsonParseError::NoError) {
+ error = Error("Reply from server missing Json data.", data);
+ return QJsonObject();
+ }
+
+ if (json_doc.isNull() || json_doc.isEmpty()) {
+ error = Error("Received empty Json document.", data);
+ return QJsonObject();
+ }
+
+ if (!json_doc.isObject()) {
+ error = Error("Json document is not an object.", json_doc);
+ return QJsonObject();
+ }
+
+ QJsonObject json_obj = json_doc.object();
+ if (json_obj.isEmpty()) {
+ error = Error("Received empty Json object.", json_doc);
+ return QJsonObject();
+ }
+
+ return json_obj;
+
+}
+
+QJsonValue QobuzBaseRequest::ExtractItems(QByteArray &data, QString &error) {
+
+ QJsonObject json_obj = ExtractJsonObj(data, error);
+ if (json_obj.isEmpty()) return QJsonValue();
+ return ExtractItems(json_obj, error);
+
+}
+
+QJsonValue QobuzBaseRequest::ExtractItems(QJsonObject &json_obj, QString &error) {
+
+ if (!json_obj.contains("items")) {
+ error = Error("Json reply is missing items.", json_obj);
+ return QJsonArray();
+ }
+ QJsonValue json_items = json_obj["items"];
+ return json_items;
+
+}
+
+QString QobuzBaseRequest::Error(QString error, QVariant debug) {
+
+ qLog(Error) << "Qobuz:" << error;
+ if (debug.isValid()) qLog(Debug) << debug;
+
+ return error;
+
+}
diff --git a/src/qobuz/qobuzbaserequest.h b/src/qobuz/qobuzbaserequest.h
new file mode 100644
index 000000000..e2089c208
--- /dev/null
+++ b/src/qobuz/qobuzbaserequest.h
@@ -0,0 +1,108 @@
+/*
+ * 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 "core/song.h"
+#include "internet/internetservices.h"
+#include "internet/internetservice.h"
+#include "internet/internetsearch.h"
+#include "qobuzservice.h"
+
+class Application;
+class NetworkAccessManager;
+class QobuzUrlHandler;
+class CollectionBackend;
+class CollectionModel;
+
+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,
+ };
+
+ QobuzBaseRequest(QobuzService *service, NetworkAccessManager *network, QObject *parent);
+ ~QobuzBaseRequest();
+
+ typedef QPair Param;
+ typedef QList ParamList;
+
+ typedef QPair EncodedParam;
+ typedef QList EncodedParamList;
+
+ QNetworkReply *CreateRequest(const QString &ressource_name, const QList ¶ms_provided);
+ QByteArray GetReplyData(QNetworkReply *reply, QString &error);
+ QJsonObject ExtractJsonObj(QByteArray &data, QString &error);
+ QJsonValue ExtractItems(QByteArray &data, QString &error);
+ QJsonValue ExtractItems(QJsonObject &json_obj, QString &error);
+
+ virtual QString Error(QString error, QVariant debug = QVariant());
+
+ 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(); }
+
+ QString access_token() { return service_->access_token(); }
+
+ 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:
+
+ static const char *kApiUrl;
+
+ QobuzService *service_;
+ NetworkAccessManager *network_;
+ QList replies_;
+
+};
+
+#endif // QOBUZBASEREQUEST_H
diff --git a/src/qobuz/qobuzfavoriterequest.cpp b/src/qobuz/qobuzfavoriterequest.cpp
new file mode 100644
index 000000000..7db24a487
--- /dev/null
+++ b/src/qobuz/qobuzfavoriterequest.cpp
@@ -0,0 +1,281 @@
+/*
+ * 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/closure.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, 0, nullptr, 0);
+ 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() <= 0) continue;
+ id = QString::number(song.artist_id());
+ break;
+ case FavoriteType_Albums:
+ if (song.album_id() <= 0) continue;
+ id = QString::number(song.album_id());
+ break;
+ case FavoriteType_Songs:
+ if (song.song_id() <= 0) continue;
+ id = QString::number(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(',');
+
+ typedef QPair EncodedParam;
+ typedef QList EncodedParamList;
+
+ ParamList params = ParamList() << Param("app_id", app_id())
+ << Param("user_auth_token", access_token())
+ << Param(text, ids);
+
+ QUrlQuery url_query;
+ for (const Param& param : params) {
+ EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
+ url_query.addQueryItem(encoded_param.first, encoded_param.second);
+ }
+
+ QNetworkReply *reply = CreateRequest("favorite/create", params);
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(AddFavoritesReply(QNetworkReply*, const FavoriteType, const SongList&)), 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;
+ }
+
+ QString error;
+ QByteArray data = GetReplyData(reply, error);
+
+ 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() <= 0) continue;
+ id = QString::number(song.artist_id());
+ break;
+ case FavoriteType_Albums:
+ if (song.album_id() <= 0) continue;
+ id = QString::number(song.album_id());
+ break;
+ case FavoriteType_Songs:
+ if (song.song_id() <= 0) continue;
+ id = QString::number(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", access_token())
+ << Param(text, ids);
+
+ QUrlQuery url_query;
+ for (const Param ¶m : params) {
+ EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
+ url_query.addQueryItem(encoded_param.first, encoded_param.second);
+ }
+
+ QNetworkReply *reply = CreateRequest("favorite/delete", params);
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(RemoveFavoritesReply(QNetworkReply*, const FavoriteType, const SongList&)), 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;
+ }
+
+ QString error;
+ QByteArray data = GetReplyData(reply, error);
+ 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;
+ }
+
+}
diff --git a/src/qobuz/qobuzfavoriterequest.h b/src/qobuz/qobuzfavoriterequest.h
new file mode 100644
index 000000000..a2d182a5e
--- /dev/null
+++ b/src/qobuz/qobuzfavoriterequest.h
@@ -0,0 +1,78 @@
+/*
+ * 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 "qobuzbaserequest.h"
+#include "core/song.h"
+
+class QNetworkReply;
+class QobuzService;
+class NetworkAccessManager;
+
+class QobuzFavoriteRequest : public QobuzBaseRequest {
+ Q_OBJECT
+
+ public:
+ QobuzFavoriteRequest(QobuzService *service, NetworkAccessManager *network, QObject *parent);
+ ~QobuzFavoriteRequest();
+
+ enum FavoriteType {
+ FavoriteType_Artists,
+ FavoriteType_Albums,
+ FavoriteType_Songs
+ };
+
+ signals:
+ void ArtistsAdded(const SongList &songs);
+ void AlbumsAdded(const SongList &songs);
+ void SongsAdded(const SongList &songs);
+ void ArtistsRemoved(const SongList &songs);
+ void AlbumsRemoved(const SongList &songs);
+ void SongsRemoved(const SongList &songs);
+
+ 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:
+ 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 000000000..f0ea5ac31
--- /dev/null
+++ b/src/qobuz/qobuzrequest.cpp
@@ -0,0 +1,1273 @@
+/*
+ * 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 "core/closure.h"
+#include "core/logging.h"
+#include "core/network.h"
+#include "core/song.h"
+#include "core/timeconstants.h"
+#include "organise/organiseformat.h"
+#include "qobuzservice.h"
+#include "qobuzurlhandler.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, NetworkAccessManager *network, QueryType type, QObject *parent)
+ : QobuzBaseRequest(service, network, parent),
+ service_(service),
+ url_handler_(url_handler),
+ network_(network),
+ type_(type),
+ search_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 (!album_cover_replies_.isEmpty()) {
+ QNetworkReply *reply = album_cover_replies_.takeFirst();
+ disconnect(reply, 0, nullptr, 0);
+ 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 search_id, const QString &search_text) {
+ search_id_ = search_id;
+ search_text_ = search_text;
+}
+
+void QobuzRequest::GetArtists() {
+
+ emit UpdateStatus(tr("Retrieving artists..."));
+ emit UpdateProgress(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", access_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;
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(ArtistsReplyReceived(QNetworkReply*, int, int)), reply, request.limit, request.offset);
+
+ }
+
+}
+
+void QobuzRequest::GetAlbums() {
+
+ emit UpdateStatus(tr("Retrieving albums..."));
+ emit UpdateProgress(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", access_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;
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumsReplyReceived(QNetworkReply*, int, int)), reply, request.limit, request.offset);
+
+ }
+
+}
+
+void QobuzRequest::GetSongs() {
+
+ emit UpdateStatus(tr("Retrieving songs..."));
+ emit UpdateProgress(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", access_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;
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(SongsReplyReceived(QNetworkReply*, int, int)), reply, request.limit, request.offset);
+
+ }
+
+}
+
+void QobuzRequest::ArtistsSearch() {
+
+ emit UpdateStatus(tr("Searching..."));
+ emit UpdateProgress(0);
+ AddArtistsSearchRequest();
+
+}
+
+void QobuzRequest::AddArtistsSearchRequest(const int offset) {
+
+ AddArtistsRequest(offset, service_->artistssearchlimit());
+
+}
+
+void QobuzRequest::AlbumsSearch() {
+
+ emit UpdateStatus(tr("Searching..."));
+ emit UpdateProgress(0);
+ AddAlbumsSearchRequest();
+
+}
+
+void QobuzRequest::AddAlbumsSearchRequest(const int offset) {
+
+ AddAlbumsRequest(offset, service_->albumssearchlimit());
+
+}
+
+void QobuzRequest::SongsSearch() {
+
+ emit UpdateStatus(tr("Searching..."));
+ emit UpdateProgress(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) {
+
+ QString error;
+ QByteArray data = GetReplyData(reply, error);
+
+ --artists_requests_active_;
+
+ if (finished_) return;
+
+ if (data.isEmpty()) {
+ ArtistsFinishCheck();
+ return;
+ }
+
+ QJsonObject json_obj = ExtractJsonObj(data, error);
+ if (json_obj.isEmpty()) {
+ ArtistsFinishCheck();
+ return;
+ }
+
+ if (!json_obj.contains("artists")) {
+ ArtistsFinishCheck();
+ Error("Json object is missing artists.", json_obj);
+ return;
+ }
+ QJsonValue json_artists = json_obj["artists"];
+ if (!json_artists.isObject()) {
+ Error("Json artists is not an object.", json_obj);
+ ArtistsFinishCheck();
+ return;
+ }
+ QJsonObject json_obj_artists = json_artists.toObject();
+
+ if (!json_obj_artists.contains("limit") ||
+ !json_obj_artists.contains("offset") ||
+ !json_obj_artists.contains("total") ||
+ !json_obj_artists.contains("items")) {
+ ArtistsFinishCheck();
+ Error("Json artists object is missing values.", json_obj);
+ return;
+ }
+ //int limit = json_obj_artists["limit"].toInt();
+ int offset = json_obj_artists["offset"].toInt();
+ int artists_total = json_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(artists_total_);
+ emit UpdateProgress(artists_received_);
+ }
+
+ QJsonValue json_value = ExtractItems(json_obj_artists, error);
+ if (!json_value.isArray()) {
+ ArtistsFinishCheck();
+ return;
+ }
+
+ QJsonArray json_items = json_value.toArray();
+ if (json_items.isEmpty()) { // Empty array means no results
+ no_results_ = true;
+ ArtistsFinishCheck();
+ return;
+ }
+
+ int artists_received = 0;
+ for (const QJsonValue &value : json_items) {
+
+ ++artists_received;
+
+ if (!value.isObject()) {
+ Error("Invalid Json reply, item not a object.", value);
+ continue;
+ }
+ QJsonObject json_obj = value.toObject();
+
+ if (json_obj.contains("item")) {
+ QJsonValue json_item = json_obj["item"];
+ if (!json_item.isObject()) {
+ Error("Invalid Json reply, item not a object.", json_item);
+ continue;
+ }
+ json_obj = json_item.toObject();
+ }
+
+ if (!json_obj.contains("id") || !json_obj.contains("name")) {
+ Error("Invalid Json reply, item missing id or album.", json_obj);
+ continue;
+ }
+
+ int artist_id = json_obj["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(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 (int 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(tr("Retrieving albums for %1 artist...").arg(artist_albums_requested_));
+ else emit UpdateStatus(tr("Retrieving albums for %1 artists...").arg(artist_albums_requested_));
+ emit ProgressSetMaximum(artist_albums_requested_);
+ emit UpdateProgress(0);
+ }
+
+ }
+
+ FinishCheck();
+
+}
+
+void QobuzRequest::AlbumsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) {
+ --albums_requests_active_;
+ AlbumsReceived(reply, 0, limit_requested, offset_requested);
+ if (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) FlushAlbumsRequests();
+}
+
+void QobuzRequest::AddArtistAlbumsRequest(const int 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", QString::number(request.artist_id))
+ << Param("extra", "albums");
+
+ if (request.offset > 0) params << Param("offset", QString::number(request.offset));
+ QNetworkReply *reply = CreateRequest(QString("artist/get"), params);
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(ArtistAlbumsReplyReceived(QNetworkReply*, int, int)), reply, request.artist_id, request.offset);
+
+ }
+
+}
+
+void QobuzRequest::ArtistAlbumsReplyReceived(QNetworkReply *reply, const int artist_id, const int offset_requested) {
+
+ --artist_albums_requests_active_;
+ ++artist_albums_received_;
+ emit UpdateProgress(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 int artist_id_requested, const int limit_requested, const int offset_requested) {
+
+ QString error;
+ QByteArray data = GetReplyData(reply, error);
+
+ if (finished_) return;
+
+ if (data.isEmpty()) {
+ AlbumsFinishCheck(artist_id_requested);
+ return;
+ }
+
+ QJsonObject json_obj = ExtractJsonObj(data, error);
+ if (json_obj.isEmpty()) {
+ AlbumsFinishCheck(artist_id_requested);
+ return;
+ }
+
+ int artist_id = 0;
+ if (json_obj.contains("id")) {
+ artist_id = json_obj["id"].toInt();
+ }
+
+ if (!json_obj.contains("albums")) {
+ AlbumsFinishCheck(artist_id_requested);
+ Error("Json object is missing albums.", json_obj);
+ return;
+ }
+ QJsonValue json_albums = json_obj["albums"];
+ if (!json_albums.isObject()) {
+ Error("Json albums is not an object.", json_obj);
+ AlbumsFinishCheck(artist_id_requested);
+ return;
+ }
+ QJsonObject json_obj_albums = json_albums.toObject();
+
+ if (!json_obj_albums.contains("limit") ||
+ !json_obj_albums.contains("offset") ||
+ !json_obj_albums.contains("total") ||
+ !json_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 json_value = ExtractItems(json_obj_albums, error);
+ if (!json_value.isArray()) {
+ AlbumsFinishCheck(artist_id_requested);
+ return;
+ }
+ QJsonArray json_items = json_value.toArray();
+ if (json_items.isEmpty()) {
+ no_results_ = true;
+ AlbumsFinishCheck(artist_id_requested);
+ return;
+ }
+
+ int albums_received = 0;
+ for (const QJsonValue &value : json_items) {
+
+ ++albums_received;
+
+ if (!value.isObject()) {
+ Error("Invalid Json reply, item not a object.", value);
+ continue;
+ }
+ QJsonObject json_obj = value.toObject();
+
+ if (!json_obj.contains("artist") || !json_obj.contains("title") || !json_obj.contains("id")) {
+ Error("Invalid Json reply, item missing artist, title or id.", json_obj);
+ continue;
+ }
+
+ QString album_id = json_obj["id"].toString();
+ if (album_songs_requests_pending_.contains(album_id)) continue;
+
+ QString album = json_obj["title"].toString();
+
+ QJsonValue json_value_artist = json_obj["artist"];
+ if (!json_value_artist.isObject()) {
+ Error("Invalid Json reply, item artist is not a object.", json_value_artist);
+ continue;
+ }
+ QJsonObject json_artist = json_value_artist.toObject();
+ if (!json_artist.contains("id") || !json_artist.contains("name")) {
+ Error("Invalid Json reply, item artist missing id or name.", json_artist);
+ continue;
+ }
+
+ artist_id = json_artist["id"].toInt();
+ QString artist = json_artist["name"].toString();
+
+ Request request;
+ if (artist_id_requested == 0) {
+ request.artist_id = artist_id;
+ }
+ else {
+ request.artist_id = artist_id_requested;
+ }
+ 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 int 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 i;
+ for (i = album_songs_requests_pending_.begin() ; i != album_songs_requests_pending_.end() ; ++i) {
+ Request request = i.value();
+ AddAlbumSongsRequest(request.artist_id, request.album_id, request.album_artist);
+ }
+ album_songs_requests_pending_.clear();
+
+ if (album_songs_requested_ > 0) {
+ if (album_songs_requested_ == 1) emit UpdateStatus(tr("Retrieving songs for %1 album...").arg(album_songs_requested_));
+ else emit UpdateStatus(tr("Retrieving songs for %1 albums...").arg(album_songs_requested_));
+ emit ProgressSetMaximum(album_songs_requested_);
+ emit UpdateProgress(0);
+ }
+ }
+
+ FinishCheck();
+
+}
+
+void QobuzRequest::SongsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) {
+
+ --songs_requests_active_;
+ SongsReceived(reply, 0, 0, limit_requested, offset_requested);
+
+}
+
+void QobuzRequest::AddAlbumSongsRequest(const int artist_id, const QString &album_id, const QString &album_artist, const int offset) {
+
+ Request request;
+ request.artist_id = artist_id;
+ request.album_id = album_id;
+ request.album_artist = album_artist;
+ 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);
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumSongsReplyReceived(QNetworkReply*, int, const QString&, int, const QString&)), reply, request.artist_id, request.album_id, request.offset, request.album_artist);
+
+ }
+
+}
+
+void QobuzRequest::AlbumSongsReplyReceived(QNetworkReply *reply, const int artist_id, const QString &album_id, const int offset_requested, const QString &album_artist) {
+
+ --album_songs_requests_active_;
+ ++album_songs_received_;
+ if (offset_requested == 0) {
+ emit UpdateProgress(album_songs_received_);
+ }
+ SongsReceived(reply, artist_id, album_id, 0, offset_requested, album_artist);
+
+}
+
+void QobuzRequest::SongsReceived(QNetworkReply *reply, const int artist_id_requested, const QString &album_id_requested, const int limit_requested, const int offset_requested, const QString &album_artist_requested) {
+
+ QString error;
+ QByteArray data = GetReplyData(reply, error);
+
+ if (finished_) return;
+
+ if (data.isEmpty()) {
+ SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested);
+ return;
+ }
+
+ QJsonObject json_obj = ExtractJsonObj(data, error);
+ if (json_obj.isEmpty()) {
+ SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested);
+ return;
+ }
+
+ //qLog(Debug) << json_obj;
+
+ 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);
+ return;
+ }
+
+ int artist_id = 0;
+ QString album_artist;
+ QString album_id;
+ QString album;
+ QUrl cover_url;
+
+ if (json_obj.contains("id")) {
+ album_id = json_obj["id"].toString();
+ }
+
+ if (json_obj.contains("title")) {
+ album = json_obj["title"].toString();
+ }
+
+ if (json_obj.contains("artist")) {
+ QJsonValue json_artist = json_obj["artist"];
+ if (!json_artist.isObject()) {
+ Error("Invalid Json reply, album artist is not a object.", json_artist);
+ SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested);
+ return;
+ }
+ QJsonObject json_obj_artist = json_artist.toObject();
+ if (!json_obj_artist.contains("id") || !json_obj_artist.contains("name")) {
+ Error("Invalid Json reply, album artist is missing id or name.", json_obj_artist);
+ SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested);
+ return;
+ }
+ artist_id = json_obj_artist["id"].toInt();
+ album_artist = json_obj_artist["name"].toString();
+ }
+
+ if (json_obj.contains("image")) {
+ QJsonValue json_image = json_obj["image"];
+ if (!json_image.isObject()) {
+ Error("Invalid Json reply, album image is not a object.", json_image);
+ SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested);
+ return;
+ }
+ QJsonObject json_obj_image = json_image.toObject();
+ if (!json_obj_image.contains("large")) {
+ Error("Invalid Json reply, album image is missing large.", json_obj_image);
+ SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested);
+ return;
+ }
+ QString album_image = json_obj_image["large"].toString();
+ if (!album_image.isEmpty()) {
+ cover_url = QUrl(album_image);
+ }
+ }
+
+ QJsonValue json_tracks = json_obj["tracks"];
+ if (!json_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_requested);
+ return;
+ }
+ QJsonObject json_obj_tracks = json_tracks.toObject();
+
+ if (!json_obj_tracks.contains("limit") ||
+ !json_obj_tracks.contains("offset") ||
+ !json_obj_tracks.contains("total") ||
+ !json_obj_tracks.contains("items")) {
+ SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested);
+ Error("Json songs object is missing values.", json_obj);
+ return;
+ }
+
+ //int limit = json_obj_tracks["limit"].toInt();
+ int offset = json_obj_tracks["offset"].toInt();
+ int songs_total = json_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);
+ return;
+ }
+
+ QJsonValue json_value = ExtractItems(json_obj_tracks, error);
+ if (!json_value.isArray()) {
+ SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist);
+ return;
+ }
+
+ QJsonArray json_items = json_value.toArray();
+ if (json_items.isEmpty()) {
+ no_results_ = true;
+ SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist);
+ return;
+ }
+
+ bool compilation = false;
+ bool multidisc = false;
+ SongList songs;
+ int songs_received = 0;
+ for (const QJsonValue &value : json_items) {
+
+ if (!value.isObject()) {
+ Error("Invalid Json reply, track is not a object.", value);
+ continue;
+ }
+ QJsonObject json_obj = value.toObject();
+
+ ++songs_received;
+ Song song;
+ ParseSong(song, json_obj, 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);
+
+}
+
+void QobuzRequest::SongsFinishCheck(const int artist_id, const QString &album_id, const int limit, const int offset, const int songs_total, const int songs_received, const QString &album_artist) {
+
+ 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:
+ // If artist_id and album_id isn't zero it means that it's a songs search where we fetch all albums too. So pass through.
+ if (artist_id == 0 && album_id == 0) {
+ AddSongsSearchRequest(offset_next);
+ break;
+ }
+ case QueryType_Artists:
+ case QueryType_SearchArtists:
+ case QueryType_Albums:
+ case QueryType_SearchAlbums:
+ AddAlbumSongsRequest(artist_id, album_id, album_artist, 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_->cache_album_covers() &&
+ 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();
+
+}
+
+int QobuzRequest::ParseSong(Song &song, const QJsonObject &json_obj, int 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 -1;
+ }
+
+ int song_id = 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 json_album = json_obj["album"];
+ if (!json_album.isObject()) {
+ Error("Invalid Json reply, album is not an object.", json_album);
+ return -1;
+ }
+ QJsonObject json_obj_album = json_album.toObject();
+
+ if (json_obj_album.contains("id")) {
+ album_id = json_obj_album["id"].toString();
+ }
+
+ if (json_obj_album.contains("title")) {
+ album = json_obj_album["title"].toString();
+ }
+
+ if (json_obj_album.contains("artist")) {
+ QJsonValue json_artist = json_obj_album["artist"];
+ if (!json_artist.isObject()) {
+ Error("Invalid Json reply, album artist is not a object.", json_artist);
+ return -1;
+ }
+ QJsonObject json_obj_artist = json_artist.toObject();
+ if (!json_obj_artist.contains("id") || !json_obj_artist.contains("name")) {
+ Error("Invalid Json reply, album artist is missing id or name.", json_obj_artist);
+ return -1;
+ }
+ artist_id = json_obj_artist["id"].toInt();
+ album_artist = json_obj_artist["name"].toString();
+ }
+
+ if (json_obj_album.contains("image")) {
+ QJsonValue json_image = json_obj_album["image"];
+ if (!json_image.isObject()) {
+ Error("Invalid Json reply, album image is not a object.", json_image);
+ return -1;
+ }
+ QJsonObject json_obj_image = json_image.toObject();
+ if (!json_obj_image.contains("large")) {
+ Error("Invalid Json reply, album image is missing large.", json_obj_image);
+ return -1;
+ }
+ QString album_image = json_obj_image["large"].toString();
+ if (!album_image.isEmpty()) {
+ cover_url = QUrl(album_image);
+ }
+ }
+ }
+
+ if (json_obj.contains("composer")) {
+ QJsonValue json_composer = json_obj["composer"];
+ if (!json_composer.isObject()) {
+ Error("Invalid Json reply, track composer is not a object.", json_composer);
+ return -1;
+ }
+ QJsonObject json_obj_composer = json_composer.toObject();
+ if (!json_obj_composer.contains("id") || !json_obj_composer.contains("name")) {
+ Error("Invalid Json reply, track composer is missing id or name.", json_obj_composer);
+ return -1;
+ }
+ QString composer = json_obj_composer["name"].toString();
+ }
+
+ if (json_obj.contains("performer")) {
+ QJsonValue json_performer = json_obj["performer"];
+ if (!json_performer.isObject()) {
+ Error("Invalid Json reply, track performer is not a object.", json_performer);
+ return -1;
+ }
+ QJsonObject json_obj_performer = json_performer.toObject();
+ if (!json_obj_performer.contains("id") || !json_obj_performer.contains("name")) {
+ Error("Invalid Json reply, track performer is missing id or name.", json_obj_performer);
+ return -1;
+ }
+ performer = json_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(QString::number(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_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.toEncoded());
+ 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(tr("Retrieving album cover for %1 album...").arg(album_covers_requested_));
+ else emit UpdateStatus(tr("Retrieving album covers for %1 albums...").arg(album_covers_requested_));
+ emit ProgressSetMaximum(album_covers_requested_);
+ emit UpdateProgress(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_.insertMulti(cover_url, &song);
+ return;
+ }
+
+ album_covers_requests_sent_.insertMulti(cover_url, &song);
+ ++album_covers_requested_;
+
+ AlbumCoverRequest request;
+ request.url = cover_url;
+ request.filename = AlbumCoverFileName(song);
+
+ 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);
+ QNetworkReply *reply = network_->get(req);
+ album_cover_replies_ << reply;
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumCoverReceived(QNetworkReply*, const QUrl&, const QString&)), 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(album_covers_received_);
+
+ if (!album_covers_requests_sent_.contains(cover_url)) {
+ AlbumCoverFinishCheck();
+ return;
+ }
+
+ QString error;
+ if (reply->error() != QNetworkReply::NoError) {
+ error = Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
+ album_covers_requests_sent_.remove(cover_url);
+ AlbumCoverFinishCheck();
+ return;
+ }
+
+ QByteArray data = reply->readAll();
+ if (data.isEmpty()) {
+ error = Error(QString("Received empty image data for %1").arg(cover_url.toString()));
+ album_covers_requests_sent_.remove(cover_url);
+ AlbumCoverFinishCheck();
+ return;
+ }
+
+ QImage image;
+ if (image.loadFromData(data)) {
+
+ QDir dir;
+ if (dir.mkpath(service_->CoverCacheDir())) {
+ QString filepath(service_->CoverCacheDir() + "/" + filename);
+ if (image.save(filepath, "JPG")) {
+ while (album_covers_requests_sent_.contains(cover_url)) {
+ Song *song = album_covers_requests_sent_.take(cover_url);
+ song->set_art_automatic(filepath);
+ }
+ }
+ }
+
+ }
+ else {
+ album_covers_requests_sent_.remove(cover_url);
+ error = Error(QString("Error decoding image data from %1").arg(cover_url.toString()));
+ }
+
+ AlbumCoverFinishCheck();
+
+}
+
+QString QobuzRequest::AlbumCoverFileName(const Song &song) {
+
+ QString artist = song.effective_albumartist();
+ QString album = song.effective_album();
+ QString title = song.title();
+
+ artist.remove('/');
+ album.remove('/');
+ title.remove('/');
+
+ QString filename = artist + "-" + album + ".jpg";
+ filename = filename.toLower();
+ filename.replace(' ', '-');
+ filename.replace("--", "-");
+ filename.replace(230, "ae");
+ filename.replace(198, "AE");
+ filename.replace(246, 'o');
+ filename.replace(248, 'o');
+ filename.replace(214, 'O');
+ filename.replace(216, 'O');
+ filename.replace(228, 'a');
+ filename.replace(229, 'a');
+ filename.replace(196, 'A');
+ filename.replace(197, 'A');
+ filename.remove(OrganiseFormat::kValidFatCharacters);
+
+ return filename;
+
+}
+
+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_requests_active_ <= 0 &&
+ album_covers_received_ >= album_covers_requested_
+ ) {
+ finished_ = true;
+ if (songs_.isEmpty()) {
+ if (IsSearch()) {
+ if (no_results_) emit ErrorSignal(search_id_, tr("No match"));
+ else if (errors_.isEmpty()) emit ErrorSignal(search_id_, tr("Unknown error"));
+ else emit ErrorSignal(search_id_, errors_);
+ }
+ else {
+ if (no_results_) emit Results(songs_);
+ else if (errors_.isEmpty()) emit ErrorSignal(tr("Unknown error"));
+ else emit ErrorSignal(errors_);
+ }
+ }
+ else {
+ if (IsSearch()) {
+ emit SearchResults(search_id_, songs_);
+ }
+ else {
+ emit Results(songs_);
+ }
+ }
+
+ }
+
+}
+
+QString QobuzRequest::Error(QString error, QVariant debug) {
+
+ qLog(Error) << "Qobuz:" << error;
+ if (debug.isValid()) qLog(Debug) << debug;
+
+ if (!error.isEmpty()) {
+ errors_ += error;
+ errors_ += "
";
+ }
+ FinishCheck();
+
+ return error;
+
+}
+
+void QobuzRequest::Warn(QString error, 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 000000000..f63b77a07
--- /dev/null
+++ b/src/qobuz/qobuzrequest.h
@@ -0,0 +1,208 @@
+/*
+ * 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 NetworkAccessManager;
+class QobuzService;
+class QobuzUrlHandler;
+
+class QobuzRequest : public QobuzBaseRequest {
+ Q_OBJECT
+
+ public:
+
+ QobuzRequest(QobuzService *service, QobuzUrlHandler *url_handler, 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(SongList songs);
+ void SearchResults(int id, SongList songs);
+ void ErrorSignal(QString message);
+ void ErrorSignal(int id, QString message);
+ void UpdateStatus(QString text);
+ void ProgressSetMaximum(int max);
+ void UpdateProgress(int max);
+ void StreamURLFinished(const QUrl original_url, const QUrl url, const Song::FileType, QString error = QString());
+
+ private slots:
+ //void LoginComplete(bool success, QString error = QString());
+
+ 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 int 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 int artist_id_requested, const QString &album_id_requested, const int limit_requested, const int offset_requested, const QString &album_artist_requested = QString());
+
+ void ArtistAlbumsReplyReceived(QNetworkReply *reply, const int artist_id, const int offset_requested);
+ void AlbumSongsReplyReceived(QNetworkReply *reply, const int artist_id, const QString &album_id, const int offset_requested, const QString &album_artist);
+ void AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_url, const QString &filename);
+
+ private:
+ typedef QPair Param;
+ typedef QList ParamList;
+
+ struct Request {
+ int artist_id = 0;
+ QString album_id = 0;
+ int song_id = 0;
+ int offset = 0;
+ int limit = 0;
+ QString album_artist;
+ QString album;
+ };
+ struct AlbumCoverRequest {
+ //int artist_id = 0;
+ QUrl url;
+ QString filename;
+ };
+
+ const bool IsQuery() { return (type_ == QueryType_Artists || type_ == QueryType_Albums || type_ == QueryType_Songs); }
+ const 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 int artist_id, const int limit = 0, const int offset = 0, const int albums_total = 0, const int albums_received = 0);
+ void SongsFinishCheck(const int artist_id, const QString &album_id, const int limit, const int offset, const int songs_total, const int songs_received, const QString &album_artist);
+
+ void AddArtistAlbumsRequest(const int artist_id, const int offset = 0);
+ void FlushArtistAlbumsRequests();
+
+ void AddAlbumSongsRequest(const int artist_id, const QString &album_id, const QString &album_artist, const int offset = 0);
+ void FlushAlbumSongsRequests();
+
+ int ParseSong(Song &song, const QJsonObject &json_obj, int 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(QString error, QVariant debug = QVariant());
+ QString Error(QString error, QVariant debug = QVariant());
+
+ 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_;
+ NetworkAccessManager *network_;
+
+ QueryType type_;
+
+ int search_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_;
+ QString errors_;
+ bool no_results_;
+ QList album_cover_replies_;
+
+};
+
+#endif // QOBUZREQUEST_H
diff --git a/src/qobuz/qobuzservice.cpp b/src/qobuz/qobuzservice.cpp
new file mode 100644
index 000000000..af34088c5
--- /dev/null
+++ b/src/qobuz/qobuzservice.cpp
@@ -0,0 +1,603 @@
+/*
+ * 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
+#include
+
+#include "core/application.h"
+#include "core/player.h"
+#include "core/closure.h"
+#include "core/logging.h"
+#include "core/network.h"
+#include "core/database.h"
+#include "core/song.h"
+#include "core/utilities.h"
+#include "internet/internetsearch.h"
+#include "collection/collectionbackend.h"
+#include "collection/collectionmodel.h"
+#include "qobuzservice.h"
+#include "qobuzurlhandler.h"
+#include "qobuzrequest.h"
+#include "qobuzfavoriterequest.h"
+#include "qobuzstreamurlrequest.h"
+#include "settings/qobuzsettingspage.h"
+
+using std::shared_ptr;
+
+const Song::Source QobuzService::kSource = Song::Source_Qobuz;
+const char *QobuzService::kAuthUrl = "http://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", 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),
+ cache_album_covers_(true),
+ 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(), kArtistsSongsTable, QString(), QString(), kArtistsSongsFtsTable);
+
+ albums_collection_backend_ = new CollectionBackend();
+ albums_collection_backend_->moveToThread(app_->database()->thread());
+ albums_collection_backend_->Init(app_->database(), kAlbumsSongsTable, QString(), QString(), kAlbumsSongsFtsTable);
+
+ songs_collection_backend_ = new CollectionBackend();
+ songs_collection_backend_->moveToThread(app_->database()->thread());
+ songs_collection_backend_->Init(app_->database(), 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(const SongList&)), favorite_request_, SLOT(AddArtists(const SongList&)));
+ connect(this, SIGNAL(AddAlbums(const SongList&)), favorite_request_, SLOT(AddAlbums(const SongList&)));
+ connect(this, SIGNAL(AddSongs(const SongList&)), favorite_request_, SLOT(AddSongs(const SongList&)));
+
+ connect(this, SIGNAL(RemoveArtists(const SongList&)), favorite_request_, SLOT(RemoveArtists(const SongList&)));
+ connect(this, SIGNAL(RemoveAlbums(const SongList&)), favorite_request_, SLOT(RemoveAlbums(const SongList&)));
+ connect(this, SIGNAL(RemoveSongs(const SongList&)), favorite_request_, SLOT(RemoveSongs(const SongList&)));
+
+ connect(favorite_request_, SIGNAL(ArtistsAdded(const SongList&)), artists_collection_backend_, SLOT(AddOrUpdateSongs(const SongList&)));
+ connect(favorite_request_, SIGNAL(AlbumsAdded(const SongList&)), albums_collection_backend_, SLOT(AddOrUpdateSongs(const SongList&)));
+ connect(favorite_request_, SIGNAL(SongsAdded(const SongList&)), songs_collection_backend_, SLOT(AddOrUpdateSongs(const SongList&)));
+
+ connect(favorite_request_, SIGNAL(ArtistsRemoved(const SongList&)), artists_collection_backend_, SLOT(DeleteSongs(const SongList&)));
+ connect(favorite_request_, SIGNAL(AlbumsRemoved(const SongList&)), albums_collection_backend_, SLOT(DeleteSongs(const SongList&)));
+ connect(favorite_request_, SIGNAL(SongsRemoved(const SongList&)), songs_collection_backend_, SLOT(DeleteSongs(const SongList&)));
+
+ ReloadSettings();
+
+}
+
+QobuzService::~QobuzService() {
+
+ while (!stream_url_requests_.isEmpty()) {
+ QobuzStreamURLRequest *stream_url_req = stream_url_requests_.takeFirst();
+ disconnect(stream_url_req, 0, nullptr, 0);
+ stream_url_req->deleteLater();
+ }
+
+}
+
+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", 5).toInt();
+ albumssearchlimit_ = s.value("albumssearchlimit", 100).toInt();
+ songssearchlimit_ = s.value("songssearchlimit", 100).toInt();
+ cache_album_covers_ = s.value("cachealbumcovers", true).toBool();
+
+ access_token_ = s.value("access_token").toString();
+
+ s.endGroup();
+
+}
+
+QString QobuzService::CoverCacheDir() {
+ return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/qobuzalbumcovers";
+}
+
+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_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);
+
+ QUrlQuery url_query;
+ for (const Param ¶m : params) {
+ EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
+ url_query.addQueryItem(encoded_param.first, encoded_param.second);
+ }
+
+ QUrl url(kAuthUrl);
+ QNetworkRequest req(url);
+
+ req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+
+ QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
+ QNetworkReply *reply = network_->post(req, query);
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleAuthReply(QNetworkReply*)), reply);
+
+ qLog(Debug) << "Qobuz: Sending request" << url << query;
+
+}
+
+void QobuzService::HandleAuthReply(QNetworkReply *reply) {
+
+ reply->deleteLater();
+
+ login_sent_ = false;
+
+ if (reply->error() != QNetworkReply::NoError) {
+ if (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);
+ QString failure_reason;
+ if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !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();
+ failure_reason = QString("%1 (%2)").arg(message).arg(code);
+ }
+ }
+ if (failure_reason.isEmpty()) {
+ failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
+ }
+ LoginError(failure_reason);
+ return;
+ }
+ }
+
+ int http_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ if (http_code != 200) {
+ LoginError(QString("Received HTTP code %1").arg(http_code));
+ return;
+ }
+
+ 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.isNull() || 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;
+ }
+
+ access_token_ = json_obj["user_auth_token"].toString();
+
+ QSettings s;
+ s.beginGroup(QobuzSettingsPage::kSettingsGroup);
+ s.setValue("access_token", access_token_);
+ s.endGroup();
+
+ qLog(Debug) << "Qobuz: Login successful" << "access token" << access_token_;
+
+ login_attempts_ = 0;
+ if (timer_login_attempt_->isActive()) timer_login_attempt_->stop();
+
+ emit LoginComplete(true);
+ emit LoginSuccess();
+
+}
+
+void QobuzService::Logout() {
+
+ access_token_.clear();
+
+ QSettings s;
+ s.beginGroup(QobuzSettingsPage::kSettingsGroup);
+ 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, "Maximum number of login attempts reached.");
+ return;
+ }
+ if (app_id_.isEmpty()) {
+ emit LoginComplete(false, "Missing Qobuz app ID.");
+ return;
+ }
+ if (username_.isEmpty()) {
+ emit LoginComplete(false, "Missing Qobuz username.");
+ return;
+ }
+ if (password_.isEmpty()) {
+ emit LoginComplete(false, "Missing Qobuz password.");
+ return;
+ }
+
+ emit Login();
+
+}
+
+void QobuzService::ResetArtistsRequest() {
+
+ if (artists_request_.get()) {
+ disconnect(artists_request_.get(), 0, nullptr, 0);
+ disconnect(this, 0, artists_request_.get(), 0);
+ artists_request_.reset();
+ }
+
+}
+
+void QobuzService::GetArtists() {
+
+ ResetArtistsRequest();
+
+ artists_request_.reset(new QobuzRequest(this, url_handler_, network_, QobuzBaseRequest::QueryType_Artists, this));
+
+ connect(artists_request_.get(), SIGNAL(ErrorSignal(QString)), SLOT(ArtistsErrorReceived(QString)));
+ connect(artists_request_.get(), SIGNAL(Results(SongList)), SLOT(ArtistsResultsReceived(SongList)));
+ connect(artists_request_.get(), SIGNAL(UpdateStatus(QString)), SIGNAL(ArtistsUpdateStatus(QString)));
+ connect(artists_request_.get(), SIGNAL(ProgressSetMaximum(int)), SIGNAL(ArtistsProgressSetMaximum(int)));
+ connect(artists_request_.get(), SIGNAL(UpdateProgress(int)), SIGNAL(ArtistsUpdateProgress(int)));
+ connect(this, SIGNAL(LoginComplete(bool, QString)), artists_request_.get(), SLOT(LoginComplete(bool, QString)));
+
+ artists_request_->Process();
+
+}
+
+void QobuzService::ArtistsResultsReceived(SongList songs) {
+
+ emit ArtistsResults(songs);
+
+}
+
+void QobuzService::ArtistsErrorReceived(QString error) {
+
+ emit ArtistsError(error);
+
+}
+
+void QobuzService::ResetAlbumsRequest() {
+
+ if (albums_request_.get()) {
+ disconnect(albums_request_.get(), 0, nullptr, 0);
+ disconnect(this, 0, albums_request_.get(), 0);
+ albums_request_.reset();
+ }
+
+}
+
+void QobuzService::GetAlbums() {
+
+ ResetAlbumsRequest();
+ albums_request_.reset(new QobuzRequest(this, url_handler_, network_, QobuzBaseRequest::QueryType_Albums, this));
+ connect(albums_request_.get(), SIGNAL(ErrorSignal(QString)), SLOT(AlbumsErrorReceived(QString)));
+ connect(albums_request_.get(), SIGNAL(Results(SongList)), SLOT(AlbumsResultsReceived(SongList)));
+ connect(albums_request_.get(), SIGNAL(UpdateStatus(QString)), SIGNAL(AlbumsUpdateStatus(QString)));
+ connect(albums_request_.get(), SIGNAL(ProgressSetMaximum(int)), SIGNAL(AlbumsProgressSetMaximum(int)));
+ connect(albums_request_.get(), SIGNAL(UpdateProgress(int)), SIGNAL(AlbumsUpdateProgress(int)));
+ connect(this, SIGNAL(LoginComplete(bool, QString)), albums_request_.get(), SLOT(LoginComplete(bool, QString)));
+
+ albums_request_->Process();
+
+}
+
+void QobuzService::AlbumsResultsReceived(SongList songs) {
+
+ emit AlbumsResults(songs);
+
+}
+
+void QobuzService::AlbumsErrorReceived(QString error) {
+
+ emit AlbumsError(error);
+
+}
+
+void QobuzService::ResetSongsRequest() {
+
+ if (songs_request_.get()) {
+ disconnect(songs_request_.get(), 0, nullptr, 0);
+ disconnect(this, 0, songs_request_.get(), 0);
+ songs_request_.reset();
+ }
+
+}
+
+void QobuzService::GetSongs() {
+
+ ResetSongsRequest();
+ songs_request_.reset(new QobuzRequest(this, url_handler_, network_, QobuzBaseRequest::QueryType_Songs, this));
+ connect(songs_request_.get(), SIGNAL(ErrorSignal(QString)), SLOT(SongsErrorReceived(QString)));
+ connect(songs_request_.get(), SIGNAL(Results(SongList)), SLOT(SongsResultsReceived(SongList)));
+ connect(songs_request_.get(), SIGNAL(UpdateStatus(QString)), SIGNAL(SongsUpdateStatus(QString)));
+ connect(songs_request_.get(), SIGNAL(ProgressSetMaximum(int)), SIGNAL(SongsProgressSetMaximum(int)));
+ connect(songs_request_.get(), SIGNAL(UpdateProgress(int)), SIGNAL(SongsUpdateProgress(int)));
+ connect(this, SIGNAL(LoginComplete(bool, QString)), songs_request_.get(), SLOT(LoginComplete(bool, QString)));
+
+ songs_request_->Process();
+
+}
+
+void QobuzService::SongsResultsReceived(SongList songs) {
+
+ emit SongsResults(songs);
+
+}
+
+void QobuzService::SongsErrorReceived(QString error) {
+
+ emit SongsError(error);
+
+}
+
+int QobuzService::Search(const QString &text, InternetSearch::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() {
+
+ if (app_id_.isEmpty() || username_.isEmpty() || password_.isEmpty()) {
+ emit SearchError(pending_search_id_, tr("Not authenticated."));
+ next_pending_search_id_ = 1;
+ ShowConfig();
+ return;
+ }
+
+ search_id_ = pending_search_id_;
+ search_text_ = pending_search_text_;
+
+ SendSearch();
+
+}
+
+void QobuzService::CancelSearch() {
+}
+
+void QobuzService::SendSearch() {
+
+ QobuzBaseRequest::QueryType type;
+
+ switch (pending_search_type_) {
+ case InternetSearch::SearchType_Artists:
+ type = QobuzBaseRequest::QueryType_SearchArtists;
+ break;
+ case InternetSearch::SearchType_Albums:
+ type = QobuzBaseRequest::QueryType_SearchAlbums;
+ break;
+ case InternetSearch::SearchType_Songs:
+ type = QobuzBaseRequest::QueryType_SearchSongs;
+ break;
+ default:
+ //Error("Invalid search type.");
+ return;
+ }
+
+ search_request_.reset(new QobuzRequest(this, url_handler_, network_, type, this));
+
+ connect(search_request_.get(), SIGNAL(SearchResults(int, SongList)), SIGNAL(SearchResults(int, SongList)));
+ connect(search_request_.get(), SIGNAL(ErrorSignal(int, QString)), SIGNAL(SearchError(int, QString)));
+ connect(search_request_.get(), SIGNAL(UpdateStatus(QString)), SIGNAL(SearchUpdateStatus(QString)));
+ connect(search_request_.get(), SIGNAL(ProgressSetMaximum(int)), SIGNAL(SearchProgressSetMaximum(int)));
+ connect(search_request_.get(), SIGNAL(UpdateProgress(int)), SIGNAL(SearchUpdateProgress(int)));
+ connect(this, SIGNAL(LoginComplete(bool, QString)), search_request_.get(), SLOT(LoginComplete(bool, QString)));
+
+ search_request_->Search(search_id_, search_text_);
+ search_request_->Process();
+
+}
+
+void QobuzService::GetStreamURL(const QUrl &url) {
+
+ 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, QString)), this, SLOT(HandleStreamURLFinished(QUrl, QUrl, Song::FileType, 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, 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, error);
+
+}
+
+QString QobuzService::LoginError(QString error, QVariant debug) {
+
+ qLog(Error) << "Qobuz:" << error;
+ if (debug.isValid()) qLog(Debug) << debug;
+
+ emit LoginFailure(error);
+ emit LoginComplete(false, error);
+
+ return error;
+
+}
diff --git a/src/qobuz/qobuzservice.h b/src/qobuz/qobuzservice.h
new file mode 100644
index 000000000..7f17f05a6
--- /dev/null
+++ b/src/qobuz/qobuzservice.h
@@ -0,0 +1,212 @@
+/*
+ * 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 "core/song.h"
+#include "internet/internetservice.h"
+#include "internet/internetsearch.h"
+#include "settings/qobuzsettingspage.h"
+
+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:
+ QobuzService(Application *app, QObject *parent);
+ ~QobuzService();
+
+ static const Song::Source kSource;
+
+ void ReloadSettings();
+ QString CoverCacheDir();
+
+ void Logout();
+ int Search(const QString &query, InternetSearch::SearchType type);
+ void CancelSearch();
+
+ const int max_login_attempts() { return kLoginAttempts; }
+
+ 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 cache_album_covers() { return cache_album_covers_; }
+
+ QString access_token() { return access_token_; }
+
+ const bool authenticated() { return (!app_id_.isEmpty() && !app_secret_.isEmpty() && !access_token_.isEmpty()); }
+ const bool login_sent() { return login_sent_; }
+ const bool login_attempts() { return login_attempts_; }
+
+ void GetStreamURL(const QUrl &url);
+
+ CollectionBackend *artists_collection_backend() { return artists_collection_backend_; }
+ CollectionBackend *albums_collection_backend() { return albums_collection_backend_; }
+ CollectionBackend *songs_collection_backend() { return songs_collection_backend_; }
+
+ CollectionModel *artists_collection_model() { return artists_collection_model_; }
+ CollectionModel *albums_collection_model() { return albums_collection_model_; }
+ CollectionModel *songs_collection_model() { return songs_collection_model_; }
+
+ QSortFilterProxyModel *artists_collection_sort_model() { return artists_collection_sort_model_; }
+ QSortFilterProxyModel *albums_collection_sort_model() { return albums_collection_sort_model_; }
+ QSortFilterProxyModel *songs_collection_sort_model() { return songs_collection_sort_model_; }
+
+ enum QueryType {
+ QueryType_Artists,
+ QueryType_Albums,
+ QueryType_Songs,
+ QueryType_SearchArtists,
+ QueryType_SearchAlbums,
+ QueryType_SearchSongs,
+ };
+
+ signals:
+
+ public slots:
+ void ShowConfig();
+ void TryLogin();
+ void SendLogin(const QString &username, const QString &password, const QString &token);
+ void GetArtists();
+ void GetAlbums();
+ void GetSongs();
+ void ResetArtistsRequest();
+ void ResetAlbumsRequest();
+ void ResetSongsRequest();
+
+ private slots:
+ void SendLogin();
+ void HandleAuthReply(QNetworkReply *reply);
+ void ResetLoginAttempts();
+ void StartSearch();
+ void ArtistsResultsReceived(SongList songs);
+ void ArtistsErrorReceived(QString error);
+ void AlbumsResultsReceived(SongList songs);
+ void AlbumsErrorReceived(QString error);
+ void SongsResultsReceived(SongList songs);
+ void SongsErrorReceived(QString error);
+ void HandleStreamURLFinished(const QUrl original_url, const QUrl stream_url, const Song::FileType filetype, QString error = QString());
+
+ private:
+ typedef QPair Param;
+ typedef QList ParamList;
+
+ typedef QPair EncodedParam;
+ typedef QList EncodedParamList;
+
+ void SendSearch();
+ QString LoginError(QString error, 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 cache_album_covers_;
+
+ QString access_token_;
+
+ int pending_search_id_;
+ int next_pending_search_id_;
+ QString pending_search_text_;
+ InternetSearch::SearchType pending_search_type_;
+
+ int search_id_;
+ QString search_text_;
+ bool login_sent_;
+ int login_attempts_;
+
+ QList stream_url_requests_;
+
+};
+
+#endif // QOBUZSERVICE_H
diff --git a/src/qobuz/qobuzstreamurlrequest.cpp b/src/qobuz/qobuzstreamurlrequest.cpp
new file mode 100644
index 000000000..ad55a9d11
--- /dev/null
+++ b/src/qobuz/qobuzstreamurlrequest.cpp
@@ -0,0 +1,213 @@
+/*
+ * 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 "core/logging.h"
+#include "core/network.h"
+#include "core/song.h"
+#include "settings/qobuzsettingspage.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, nullptr, 0);
+ if (reply_->isRunning()) reply_->abort();
+ reply_->deleteLater();
+ }
+
+}
+
+void QobuzStreamURLRequest::LoginComplete(bool success, QString error) {
+
+ if (!need_login_) return;
+ need_login_ = false;
+
+ if (!success) {
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
+ return;
+ }
+
+ Process();
+
+}
+
+void QobuzStreamURLRequest::Process() {
+
+ if (app_id().isEmpty() || app_secret().isEmpty()) {
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, tr("Missing 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, tr("Cancelled."));
+ }
+
+}
+
+void QobuzStreamURLRequest::GetStreamURL() {
+
+ ++tries_;
+
+ if (reply_) {
+ disconnect(reply_, 0, nullptr, 0);
+ if (reply_->isRunning()) reply_->abort();
+ reply_->deleteLater();
+ }
+
+ quint64 timestamp = QDateTime::currentDateTime().toTime_t();
+
+ 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) {
+ EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
+ data_to_sign += param.first;
+ data_to_sign += param.second;
+ }
+ data_to_sign += QString::number(timestamp);
+ data_to_sign += app_secret();
+
+ 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", access_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;
+ disconnect(reply_, 0, nullptr, 0);
+ reply_->deleteLater();
+
+ QString error;
+
+ QByteArray data = GetReplyData(reply_, error);
+ if (data.isEmpty()) {
+ reply_ = nullptr;
+ if (!authenticated() && login_sent() && tries_ <= 1) {
+ need_login_ = true;
+ return;
+ }
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
+ return;
+ }
+ reply_ = nullptr;
+
+ QJsonObject json_obj = ExtractJsonObj(data, error);
+ if (json_obj.isEmpty()) {
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
+ return;
+ }
+
+ if (!json_obj.contains("track_id")) {
+ error = Error("Invalid Json reply, stream url is missing track_id.", json_obj);
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
+ return;
+ }
+
+ int track_id = json_obj["track_id"].toInt();
+ if (track_id != song_id_) {
+ error = Error("Incorrect track ID returned.", json_obj);
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
+ return;
+ }
+
+ if (!json_obj.contains("mime_type") || !json_obj.contains("url")) {
+ error = Error("Invalid Json reply, stream url is missing url or mime_type.", json_obj);
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error);
+ 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 = Error("Returned stream url is invalid.", json_obj);
+ emit StreamURLFinished(original_url_, original_url_, filetype, error);
+ return;
+ }
+
+ emit StreamURLFinished(original_url_, url, filetype);
+
+}
diff --git a/src/qobuz/qobuzstreamurlrequest.h b/src/qobuz/qobuzstreamurlrequest.h
new file mode 100644
index 000000000..00b6d08bd
--- /dev/null
+++ b/src/qobuz/qobuzstreamurlrequest.h
@@ -0,0 +1,69 @@
+/*
+ * 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 "core/song.h"
+#include "qobuzbaserequest.h"
+
+class QNetworkReply;
+class NetworkAccessManager;
+class QobuzService;
+
+class QobuzStreamURLRequest : public QobuzBaseRequest {
+ Q_OBJECT
+
+ public:
+ 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, QString error = QString());
+
+ private slots:
+ void LoginComplete(bool success, QString error = QString());
+ void StreamURLReceived();
+
+ private:
+ QobuzService *service_;
+ QNetworkReply *reply_;
+ QUrl original_url_;
+ int song_id_;
+ int tries_;
+ bool need_login_;
+
+};
+
+#endif // QOBUZSTREAMURLREQUEST_H
diff --git a/src/qobuz/qobuzurlhandler.cpp b/src/qobuz/qobuzurlhandler.cpp
new file mode 100644
index 000000000..375ba249f
--- /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/iconloader.h"
+#include "core/logging.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, QString)), this, SLOT(GetStreamURLFinished(QUrl, QUrl, Song::FileType, 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(QUrl original_url, QUrl url, Song::FileType filetype, QString error) {
+
+ if (task_id_ == -1) return;
+ CancelTask();
+ if (error.isEmpty())
+ emit AsyncLoadComplete(LoadResult(original_url, LoadResult::TrackAvailable, url, filetype));
+ else
+ emit AsyncLoadComplete(LoadResult(original_url, LoadResult::Error, url, filetype, -1, 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 000000000..746ea4523
--- /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 "core/urlhandler.h"
+#include "core/song.h"
+#include "qobuz/qobuzservice.h"
+
+class Application;
+class QobuzService;
+
+class QobuzUrlHandler : public UrlHandler {
+ Q_OBJECT
+
+ public:
+ QobuzUrlHandler(Application *app, QobuzService *service);
+
+ QString scheme() const { return service_->url_scheme(); }
+ LoadResult StartLoading(const QUrl &url);
+
+ void CancelTask();
+
+ private slots:
+ void GetStreamURLFinished(QUrl original_url, QUrl url, Song::FileType filetype, QString error = QString());
+
+ private:
+ Application *app_;
+ QobuzService *service_;
+ int task_id_;
+
+};
+
+#endif
diff --git a/src/settings/qobuzsettingspage.cpp b/src/settings/qobuzsettingspage.cpp
new file mode 100644
index 000000000..698690830
--- /dev/null
+++ b/src/settings/qobuzsettingspage.cpp
@@ -0,0 +1,152 @@
+/*
+ * 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 "qobuzsettingspage.h"
+#include "ui_qobuzsettingspage.h"
+#include "core/application.h"
+#include "core/iconloader.h"
+#include "internet/internetservices.h"
+#include "qobuz/qobuzservice.h"
+#include "qobuz/qobuzstreamurlrequest.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", 5).toInt());
+ ui_->albumssearchlimit->setValue(s.value("albumssearchlimit", 100).toInt());
+ ui_->songssearchlimit->setValue(s.value("songssearchlimit", 100).toInt());
+ ui_->checkbox_cache_album_covers->setChecked(s.value("cachealbumcovers", true).toBool());
+
+ s.endGroup();
+
+ if (service_->authenticated()) ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn);
+
+}
+
+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("cachealbumcovers", ui_->checkbox_cache_album_covers->isChecked());
+ s.endGroup();
+
+ service_->ReloadSettings();
+
+}
+
+void QobuzSettingsPage::LoginClicked() {
+
+ if (ui_->app_id->text().isEmpty() || ui_->username->text().isEmpty() || ui_->password->text().isEmpty()) {
+ QMessageBox::critical(this, tr("Configuration incomplete"), tr("Missing app id, username or 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_->button_login->setEnabled(true);
+ ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
+}
+
+void QobuzSettingsPage::LoginSuccess() {
+ if (!this->isVisible()) return;
+ ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn);
+ ui_->button_login->setEnabled(false);
+}
+
+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 000000000..1b4baa926
--- /dev/null
+++ b/src/settings/qobuzsettingspage.h
@@ -0,0 +1,61 @@
+/*
+ * 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
+
+#include "settings/settingspage.h"
+
+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 000000000..546aeebd1
--- /dev/null
+++ b/src/settings/qobuzsettingspage.ui
@@ -0,0 +1,297 @@
+
+
+ QobuzSettingsPage
+
+
+
+ 0
+ 0
+ 715
+ 836
+
+
+
+ Qobuz
+
+
+ -
+
+
+ Enable
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Authentication
+
+
+
-
+
+
+
+ 150
+ 0
+
+
+
+ App ID
+
+
+
+ -
+
+
+ -
+
+
+ Username
+
+
+
+ -
+
+
-
+
+
+
+
+ -
+
+
+ Password
+
+
+
+ -
+
+
+ QLineEdit::Password
+
+
+
+ -
+
+
+ App Secret
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+ Login
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ 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
+
+
+
+ -
+
+
+ Cache album covers
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 30
+
+
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 64
+ 64
+
+
+
+
+ 64
+ 64
+
+
+
+ :/icons/64x64/qobuz.png
+
+
+
+
+
+
+
+
+
+ LoginStateWidget
+ QWidget
+ widgets/loginstatewidget.h
+ 1
+
+
+
+ username
+ password
+
+
+
+
+
+
+
diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp
index a7ac3aa66..32b2eceff 100644
--- a/src/settings/settingsdialog.cpp
+++ b/src/settings/settingsdialog.cpp
@@ -62,11 +62,14 @@
#include "transcodersettingspage.h"
#include "networkproxysettingspage.h"
#include "scrobblersettingspage.h"
+#ifdef HAVE_MOODBAR
+# include "moodbarsettingspage.h"
+#endif
#ifdef HAVE_TIDAL
# include "tidalsettingspage.h"
#endif
-#ifdef HAVE_MOODBAR
-# include "moodbarsettingspage.h"
+#ifdef HAVE_QOBUZ
+# include "qobuzsettingspage.h"
#endif
#ifdef HAVE_SUBSONIC
# include "subsonicsettingspage.h"
@@ -143,12 +146,15 @@ SettingsDialog::SettingsDialog(Application *app, QWidget *parent)
AddPage(Page_Moodbar, new MoodbarSettingsPage(this), iface);
#endif
-#if defined(HAVE_TIDAL) || defined(HAVE_SUBSONIC)
+#if defined(HAVE_TIDAL) || defined(HAVE_SUBSONIC) || defined(HAVE_QOBUZ)
QTreeWidgetItem *streaming = AddCategory(tr("Streaming"));
#endif
#ifdef HAVE_TIDAL
AddPage(Page_Tidal, new TidalSettingsPage(this), streaming);
#endif
+#ifdef HAVE_QOBUZ
+ AddPage(Page_Qobuz, new QobuzSettingsPage(this), streaming);
+#endif
#ifdef HAVE_SUBSONIC
AddPage(Page_Subsonic, new SubsonicSettingsPage(this), streaming);
#endif
@@ -320,9 +326,20 @@ void SettingsDialog::CurrentItemChanged(QTreeWidgetItem *item) {
}
-void SettingsDialog::ComboBoxLoadFromSettings(QSettings &s, QComboBox *combobox, QString setting, QString default_value) {
+void SettingsDialog::ComboBoxLoadFromSettings(const QSettings &s, QComboBox *combobox, const QString &setting, const QString &default_value) {
+
QString value = s.value(setting, default_value).toString();
int i = combobox->findData(value);
if (i == -1) i = combobox->findData(default_value);
combobox->setCurrentIndex(i);
+
+}
+
+void SettingsDialog::ComboBoxLoadFromSettings(const QSettings &s, QComboBox *combobox, const QString &setting, const int default_value) {
+
+ int value = s.value(setting, default_value).toInt();
+ int i = combobox->findData(value);
+ if (i == -1) i = combobox->findData(default_value);
+ combobox->setCurrentIndex(i);
+
}
diff --git a/src/settings/settingsdialog.h b/src/settings/settingsdialog.h
index 6eae7329f..1f25faf2b 100644
--- a/src/settings/settingsdialog.h
+++ b/src/settings/settingsdialog.h
@@ -82,9 +82,10 @@ class SettingsDialog : public QDialog {
Page_Transcoding,
Page_Proxy,
Page_Scrobbler,
+ Page_Moodbar,
Page_Tidal,
Page_Subsonic,
- Page_Moodbar,
+ Page_Qobuz,
};
enum Role {
@@ -111,7 +112,8 @@ class SettingsDialog : public QDialog {
// QWidget
void showEvent(QShowEvent *e);
- void ComboBoxLoadFromSettings(QSettings &s, QComboBox *combobox, QString setting, QString default_value);
+ void ComboBoxLoadFromSettings(const QSettings &s, QComboBox *combobox, const QString &setting, const QString &default_value);
+ void ComboBoxLoadFromSettings(const QSettings &s, QComboBox *combobox, const QString &setting, const int default_value);
signals:
void ReloadSettings();