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();