diff --git a/CMakeLists.txt b/CMakeLists.txt
index 67a420314..0ccca7c50 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -330,6 +330,7 @@ optional_component(TRANSLATIONS ON "Translations"
)
optional_component(SUBSONIC ON "Subsonic support")
+optional_component(TIDAL ON "Tidal support")
optional_component(MOODBAR ON "Moodbar"
DEPENDS "fftw3" FFTW3_FOUND
diff --git a/data/data.qrc b/data/data.qrc
index 210f09b84..4dc238bbc 100644
--- a/data/data.qrc
+++ b/data/data.qrc
@@ -12,6 +12,7 @@
schema/schema-9.sql
schema/schema-10.sql
schema/schema-11.sql
+ schema/schema-12.sql
schema/device-schema.sql
style/strawberry.css
html/playing-tooltip-plain.html
diff --git a/data/icons.qrc b/data/icons.qrc
index ff491cc7a..9993765b1 100644
--- a/data/icons.qrc
+++ b/data/icons.qrc
@@ -89,6 +89,7 @@
icons/128x128/moodbar.png
icons/128x128/love.png
icons/128x128/subsonic.png
+ icons/128x128/tidal.png
icons/64x64/albums.png
icons/64x64/alsa.png
icons/64x64/application-exit.png
@@ -179,6 +180,7 @@
icons/64x64/moodbar.png
icons/64x64/love.png
icons/64x64/subsonic.png
+ icons/64x64/tidal.png
icons/48x48/albums.png
icons/48x48/alsa.png
icons/48x48/application-exit.png
@@ -272,6 +274,7 @@
icons/48x48/moodbar.png
icons/48x48/love.png
icons/48x48/subsonic.png
+ icons/48x48/tidal.png
icons/32x32/albums.png
icons/32x32/alsa.png
icons/32x32/application-exit.png
@@ -365,6 +368,7 @@
icons/32x32/moodbar.png
icons/32x32/love.png
icons/32x32/subsonic.png
+ icons/32x32/tidal.png
icons/22x22/albums.png
icons/22x22/alsa.png
icons/22x22/application-exit.png
@@ -458,5 +462,6 @@
icons/22x22/moodbar.png
icons/22x22/love.png
icons/22x22/subsonic.png
+ icons/22x22/tidal.png
diff --git a/data/icons/128x128/tidal.png b/data/icons/128x128/tidal.png
new file mode 100644
index 000000000..5b7e2f11e
Binary files /dev/null and b/data/icons/128x128/tidal.png differ
diff --git a/data/icons/22x22/tidal.png b/data/icons/22x22/tidal.png
new file mode 100644
index 000000000..496105541
Binary files /dev/null and b/data/icons/22x22/tidal.png differ
diff --git a/data/icons/32x32/tidal.png b/data/icons/32x32/tidal.png
new file mode 100644
index 000000000..c206ac0a3
Binary files /dev/null and b/data/icons/32x32/tidal.png differ
diff --git a/data/icons/48x48/tidal.png b/data/icons/48x48/tidal.png
new file mode 100644
index 000000000..fb0143432
Binary files /dev/null and b/data/icons/48x48/tidal.png differ
diff --git a/data/icons/64x64/tidal.png b/data/icons/64x64/tidal.png
new file mode 100644
index 000000000..cbd33703d
Binary files /dev/null and b/data/icons/64x64/tidal.png differ
diff --git a/data/icons/full/tidal.png b/data/icons/full/tidal.png
new file mode 100644
index 000000000..316831c69
Binary files /dev/null and b/data/icons/full/tidal.png differ
diff --git a/data/schema/schema-12.sql b/data/schema/schema-12.sql
new file mode 100644
index 000000000..4a5960c2e
--- /dev/null
+++ b/data/schema/schema-12.sql
@@ -0,0 +1,217 @@
+CREATE TABLE IF NOT EXISTS tidal_artists_songs (
+
+ title TEXT,
+ album TEXT,
+ artist TEXT,
+ albumartist TEXT,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT 0,
+ genre TEXT,
+ compilation INTEGER NOT NULL DEFAULT 0,
+ composer TEXT,
+ performer TEXT,
+ grouping TEXT,
+ comment TEXT,
+ lyrics TEXT,
+
+ artist_id TEXT,
+ album_id TEXT,
+ song_id TEXT,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT -1,
+ samplerate INTEGER NOT NULL DEFAULT -1,
+ bitdepth INTEGER NOT NULL DEFAULT -1,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL DEFAULT -1,
+ url TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT -1,
+ mtime INTEGER NOT NULL DEFAULT -1,
+ ctime INTEGER NOT NULL DEFAULT -1,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT -1,
+
+ compilation_detected INTEGER DEFAULT 0,
+ compilation_on INTEGER NOT NULL DEFAULT 0,
+ compilation_off INTEGER NOT NULL DEFAULT 0,
+ compilation_effective INTEGER NOT NULL DEFAULT 0,
+
+ art_automatic TEXT,
+ art_manual TEXT,
+
+ effective_albumartist TEXT,
+ effective_originalyear INTEGER NOT NULL DEFAULT 0,
+
+ cue_path TEXT
+
+);
+
+CREATE TABLE IF NOT EXISTS tidal_albums_songs (
+
+ title TEXT,
+ album TEXT,
+ artist TEXT,
+ albumartist TEXT,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT 0,
+ genre TEXT,
+ compilation INTEGER NOT NULL DEFAULT 0,
+ composer TEXT,
+ performer TEXT,
+ grouping TEXT,
+ comment TEXT,
+ lyrics TEXT,
+
+ artist_id TEXT,
+ album_id TEXT,
+ song_id TEXT,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT -1,
+ samplerate INTEGER NOT NULL DEFAULT -1,
+ bitdepth INTEGER NOT NULL DEFAULT -1,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL DEFAULT -1,
+ url TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT -1,
+ mtime INTEGER NOT NULL DEFAULT -1,
+ ctime INTEGER NOT NULL DEFAULT -1,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT -1,
+
+ compilation_detected INTEGER DEFAULT 0,
+ compilation_on INTEGER NOT NULL DEFAULT 0,
+ compilation_off INTEGER NOT NULL DEFAULT 0,
+ compilation_effective INTEGER NOT NULL DEFAULT 0,
+
+ art_automatic TEXT,
+ art_manual TEXT,
+
+ effective_albumartist TEXT,
+ effective_originalyear INTEGER NOT NULL DEFAULT 0,
+
+ cue_path TEXT
+
+);
+
+CREATE TABLE IF NOT EXISTS tidal_songs (
+
+ title TEXT,
+ album TEXT,
+ artist TEXT,
+ albumartist TEXT,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT 0,
+ genre TEXT,
+ compilation INTEGER NOT NULL DEFAULT 0,
+ composer TEXT,
+ performer TEXT,
+ grouping TEXT,
+ comment TEXT,
+ lyrics TEXT,
+
+ artist_id TEXT,
+ album_id TEXT,
+ song_id TEXT,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT -1,
+ samplerate INTEGER NOT NULL DEFAULT -1,
+ bitdepth INTEGER NOT NULL DEFAULT -1,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL DEFAULT -1,
+ url TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT -1,
+ mtime INTEGER NOT NULL DEFAULT -1,
+ ctime INTEGER NOT NULL DEFAULT -1,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT -1,
+
+ compilation_detected INTEGER DEFAULT 0,
+ compilation_on INTEGER NOT NULL DEFAULT 0,
+ compilation_off INTEGER NOT NULL DEFAULT 0,
+ compilation_effective INTEGER NOT NULL DEFAULT 0,
+
+ art_automatic TEXT,
+ art_manual TEXT,
+
+ effective_albumartist TEXT,
+ effective_originalyear INTEGER NOT NULL DEFAULT 0,
+
+ cue_path TEXT
+
+);
+
+CREATE VIRTUAL TABLE IF NOT EXISTS tidal_artists_songs_fts USING fts5(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize = "unicode61 remove_diacritics 0"
+
+);
+
+CREATE VIRTUAL TABLE IF NOT EXISTS tidal_albums_songs_fts USING fts5(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize = "unicode61 remove_diacritics 0"
+
+);
+
+CREATE VIRTUAL TABLE IF NOT EXISTS tidal_songs_fts USING fts5(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize = "unicode61 remove_diacritics 0"
+
+);
+
+UPDATE schema_version SET version=12;
diff --git a/data/schema/schema.sql b/data/schema/schema.sql
index 802fad5ad..c5cff9a7c 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 (11);
+INSERT INTO schema_version (version) VALUES (12);
CREATE TABLE IF NOT EXISTS directories (
path TEXT NOT NULL,
@@ -74,6 +74,177 @@ CREATE TABLE IF NOT EXISTS songs (
);
+CREATE TABLE IF NOT EXISTS tidal_artists_songs (
+
+ title TEXT,
+ album TEXT,
+ artist TEXT,
+ albumartist TEXT,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT 0,
+ genre TEXT,
+ compilation INTEGER NOT NULL DEFAULT 0,
+ composer TEXT,
+ performer TEXT,
+ grouping TEXT,
+ comment TEXT,
+ lyrics TEXT,
+
+ artist_id TEXT,
+ album_id TEXT,
+ song_id TEXT,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT -1,
+ samplerate INTEGER NOT NULL DEFAULT -1,
+ bitdepth INTEGER NOT NULL DEFAULT -1,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL DEFAULT -1,
+ url TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT -1,
+ mtime INTEGER NOT NULL DEFAULT -1,
+ ctime INTEGER NOT NULL DEFAULT -1,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT -1,
+
+ compilation_detected INTEGER DEFAULT 0,
+ compilation_on INTEGER NOT NULL DEFAULT 0,
+ compilation_off INTEGER NOT NULL DEFAULT 0,
+ compilation_effective INTEGER NOT NULL DEFAULT 0,
+
+ art_automatic TEXT,
+ art_manual TEXT,
+
+ effective_albumartist TEXT,
+ effective_originalyear INTEGER NOT NULL DEFAULT 0,
+
+ cue_path TEXT
+
+);
+
+CREATE TABLE IF NOT EXISTS tidal_albums_songs (
+
+ title TEXT,
+ album TEXT,
+ artist TEXT,
+ albumartist TEXT,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT 0,
+ genre TEXT,
+ compilation INTEGER NOT NULL DEFAULT 0,
+ composer TEXT,
+ performer TEXT,
+ grouping TEXT,
+ comment TEXT,
+ lyrics TEXT,
+
+ artist_id TEXT,
+ album_id TEXT,
+ song_id TEXT,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT -1,
+ samplerate INTEGER NOT NULL DEFAULT -1,
+ bitdepth INTEGER NOT NULL DEFAULT -1,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL DEFAULT -1,
+ url TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT -1,
+ mtime INTEGER NOT NULL DEFAULT -1,
+ ctime INTEGER NOT NULL DEFAULT -1,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT -1,
+
+ compilation_detected INTEGER DEFAULT 0,
+ compilation_on INTEGER NOT NULL DEFAULT 0,
+ compilation_off INTEGER NOT NULL DEFAULT 0,
+ compilation_effective INTEGER NOT NULL DEFAULT 0,
+
+ art_automatic TEXT,
+ art_manual TEXT,
+
+ effective_albumartist TEXT,
+ effective_originalyear INTEGER NOT NULL DEFAULT 0,
+
+ cue_path TEXT
+
+);
+
+CREATE TABLE IF NOT EXISTS tidal_songs (
+
+ title TEXT,
+ album TEXT,
+ artist TEXT,
+ albumartist TEXT,
+ track INTEGER NOT NULL DEFAULT -1,
+ disc INTEGER NOT NULL DEFAULT -1,
+ year INTEGER NOT NULL DEFAULT -1,
+ originalyear INTEGER NOT NULL DEFAULT 0,
+ genre TEXT,
+ compilation INTEGER NOT NULL DEFAULT 0,
+ composer TEXT,
+ performer TEXT,
+ grouping TEXT,
+ comment TEXT,
+ lyrics TEXT,
+
+ artist_id TEXT,
+ album_id TEXT,
+ song_id TEXT,
+
+ beginning INTEGER NOT NULL DEFAULT 0,
+ length INTEGER NOT NULL DEFAULT 0,
+
+ bitrate INTEGER NOT NULL DEFAULT -1,
+ samplerate INTEGER NOT NULL DEFAULT -1,
+ bitdepth INTEGER NOT NULL DEFAULT -1,
+
+ source INTEGER NOT NULL DEFAULT 0,
+ directory_id INTEGER NOT NULL DEFAULT -1,
+ url TEXT NOT NULL,
+ filetype INTEGER NOT NULL DEFAULT 0,
+ filesize INTEGER NOT NULL DEFAULT -1,
+ mtime INTEGER NOT NULL DEFAULT -1,
+ ctime INTEGER NOT NULL DEFAULT -1,
+ unavailable INTEGER DEFAULT 0,
+
+ playcount INTEGER NOT NULL DEFAULT 0,
+ skipcount INTEGER NOT NULL DEFAULT 0,
+ lastplayed INTEGER NOT NULL DEFAULT -1,
+
+ compilation_detected INTEGER DEFAULT 0,
+ compilation_on INTEGER NOT NULL DEFAULT 0,
+ compilation_off INTEGER NOT NULL DEFAULT 0,
+ compilation_effective INTEGER NOT NULL DEFAULT 0,
+
+ art_automatic TEXT,
+ art_manual TEXT,
+
+ effective_albumartist TEXT,
+ effective_originalyear INTEGER NOT NULL DEFAULT 0,
+
+ cue_path TEXT
+
+);
+
CREATE TABLE IF NOT EXISTS subsonic_songs (
title TEXT,
@@ -243,6 +414,51 @@ CREATE VIRTUAL TABLE IF NOT EXISTS songs_fts USING fts5(
);
+CREATE VIRTUAL TABLE IF NOT EXISTS tidal_artists_songs_fts USING fts5(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize = "unicode61 remove_diacritics 0"
+
+);
+
+CREATE VIRTUAL TABLE IF NOT EXISTS tidal_albums_songs_fts USING fts5(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize = "unicode61 remove_diacritics 0"
+
+);
+
+CREATE VIRTUAL TABLE IF NOT EXISTS tidal_songs_fts USING fts5(
+
+ ftstitle,
+ ftsalbum,
+ ftsartist,
+ ftsalbumartist,
+ ftscomposer,
+ ftsperformer,
+ ftsgrouping,
+ ftsgenre,
+ ftscomment,
+ tokenize = "unicode61 remove_diacritics 0"
+
+);
+
CREATE VIRTUAL TABLE IF NOT EXISTS subsonic_songs_fts USING fts5(
ftstitle,
diff --git a/dist/unix/org.strawberrymusicplayer.strawberry.desktop b/dist/unix/org.strawberrymusicplayer.strawberry.desktop
index a8959b21f..c687087af 100755
--- a/dist/unix/org.strawberrymusicplayer.strawberry.desktop
+++ b/dist/unix/org.strawberrymusicplayer.strawberry.desktop
@@ -10,5 +10,5 @@ Icon=strawberry
Terminal=false
Categories=AudioVideo;Player;Qt;Audio;
StartupNotify=false
-MimeType=x-content/audio-player;application/ogg;application/x-ogg;application/x-ogm-audio;audio/flac;audio/ogg;audio/vorbis;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/vnd.rn-realaudio;audio/x-flac;audio/x-oggflac;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-speex;audio/x-wav;audio/x-wavpack;audio/x-ape;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-pn-realaudio;audio/x-scpls;video/x-ms-asf;
+MimeType=x-content/audio-player;application/ogg;application/x-ogg;application/x-ogm-audio;audio/flac;audio/ogg;audio/vorbis;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/vnd.rn-realaudio;audio/x-flac;audio/x-oggflac;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-speex;audio/x-wav;audio/x-wavpack;audio/x-ape;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-pn-realaudio;audio/x-scpls;video/x-ms-asf;x-scheme-handler/tidal;
StartupWMClass=strawberry
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index f1b8f2372..623c46603 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -921,6 +921,29 @@ optional_source(HAVE_SUBSONIC
settings/subsonicsettingspage.ui
)
+optional_source(HAVE_TIDAL
+ SOURCES
+ tidal/tidalservice.cpp
+ tidal/tidalurlhandler.cpp
+ tidal/tidalbaserequest.cpp
+ tidal/tidalrequest.cpp
+ tidal/tidalstreamurlrequest.cpp
+ tidal/tidalfavoriterequest.cpp
+ settings/tidalsettingspage.cpp
+ covermanager/tidalcoverprovider.cpp
+ HEADERS
+ tidal/tidalservice.h
+ tidal/tidalurlhandler.h
+ tidal/tidalbaserequest.h
+ tidal/tidalrequest.h
+ tidal/tidalstreamurlrequest.h
+ tidal/tidalfavoriterequest.h
+ settings/tidalsettingspage.h
+ covermanager/tidalcoverprovider.h
+ UI
+ settings/tidalsettingspage.ui
+)
+
# Moodbar
optional_source(HAVE_MOODBAR
SOURCES
diff --git a/src/config.h.in b/src/config.h.in
index e8ee1b242..7ba2d3888 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -49,6 +49,7 @@
#cmakedefine XINE_ANALYZER
#cmakedefine HAVE_SUBSONIC
+#cmakedefine HAVE_TIDAL
#cmakedefine HAVE_MOODBAR
diff --git a/src/core/application.cpp b/src/core/application.cpp
index 84d016712..eb3b5350b 100644
--- a/src/core/application.cpp
+++ b/src/core/application.cpp
@@ -70,6 +70,11 @@
# include "subsonic/subsonicservice.h"
#endif
+#ifdef HAVE_TIDAL
+# include "tidal/tidalservice.h"
+# include "covermanager/tidalcoverprovider.h"
+#endif
+
#ifdef HAVE_MOODBAR
# include "moodbar/moodbarcontroller.h"
# include "moodbar/moodbarloader.h"
@@ -111,6 +116,9 @@ class ApplicationImpl {
cover_providers->AddProvider(new DiscogsCoverProvider(app, app));
cover_providers->AddProvider(new MusicbrainzCoverProvider(app, app));
cover_providers->AddProvider(new DeezerCoverProvider(app, app));
+#ifdef HAVE_TIDAL
+ cover_providers->AddProvider(new TidalCoverProvider(app, app));
+#endif
return cover_providers;
}),
album_cover_loader_([=]() {
@@ -130,6 +138,9 @@ class ApplicationImpl {
InternetServices *internet_services = new InternetServices(app);
#ifdef HAVE_SUBSONIC
internet_services->AddService(new SubsonicService(app, internet_services));
+#endif
+#ifdef HAVE_TIDAL
+ internet_services->AddService(new TidalService(app, internet_services));
#endif
return internet_services;
}),
diff --git a/src/core/database.cpp b/src/core/database.cpp
index 7ace63889..2ddee1674 100644
--- a/src/core/database.cpp
+++ b/src/core/database.cpp
@@ -54,7 +54,7 @@
#include "scopedtransaction.h"
const char *Database::kDatabaseFilename = "strawberry.db";
-const int Database::kSchemaVersion = 11;
+const int Database::kSchemaVersion = 12;
const char *Database::kMagicAllSongsTables = "%allsongstables";
int Database::sNextConnectionId = 1;
diff --git a/src/core/iconmapper.h b/src/core/iconmapper.h
index f111c8df3..e1aa8db61 100644
--- a/src/core/iconmapper.h
+++ b/src/core/iconmapper.h
@@ -115,6 +115,7 @@ static const QMap iconmapper_ = {
{ "star", { {}, 0, 0 } },
{ "strawberry", { {}, 0, 0 } },
{ "subsonic", { {}, 0, 0 } },
+ { "tidal", { {}, 0, 0 } },
{ "tools-wizard", { {}, 0, 0 } },
{ "view-choose", { {}, 0, 0 } },
{ "view-fullscreen", { {}, 0, 0 } },
diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp
index 9facf9358..449b96df1 100644
--- a/src/core/mainwindow.cpp
+++ b/src/core/mainwindow.cpp
@@ -147,6 +147,10 @@
#ifdef HAVE_SUBSONIC
# include "settings/subsonicsettingspage.h"
#endif
+#ifdef HAVE_TIDAL
+# include "tidal/tidalservice.h"
+# include "settings/tidalsettingspage.h"
+#endif
#include "internet/internetservices.h"
#include "internet/internetservice.h"
@@ -229,6 +233,9 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
}),
#ifdef HAVE_SUBSONIC
subsonic_view_(new InternetSongsView(app_, app->internet_services()->ServiceBySource(Song::Source_Subsonic), SubsonicSettingsPage::kSettingsGroup, SettingsDialog::Page_Subsonic, this)),
+#endif
+#ifdef HAVE_TIDAL
+ tidal_view_(new InternetTabsView(app_, app->internet_services()->ServiceBySource(Song::Source_Tidal), TidalSettingsPage::kSettingsGroup, SettingsDialog::Page_Tidal, this)),
#endif
playlist_menu_(new QMenu(this)),
playlist_add_to_another_(nullptr),
@@ -282,6 +289,9 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
#ifdef HAVE_SUBSONIC
ui_->tabs->AddTab(subsonic_view_, "subsonic", IconLoader::Load("subsonic"), tr("Subsonic"));
#endif
+#ifdef HAVE_TIDAL
+ ui_->tabs->AddTab(tidal_view_, "tidal", IconLoader::Load("tidal"), tr("Tidal"));
+#endif
// Add the playing widget to the fancy tab widget
ui_->tabs->addBottomWidget(ui_->widget_playing);
@@ -567,6 +577,15 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
connect(subsonic_view_->view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
#endif
+#ifdef HAVE_TIDAL
+ connect(tidal_view_->artists_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
+ connect(tidal_view_->albums_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
+ connect(tidal_view_->songs_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
+ connect(tidal_view_->search_view(), SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
+ if (TidalService *tidalservice = qobject_cast (app_->internet_services()->ServiceBySource(Song::Source_Tidal)))
+ connect(this, SIGNAL(AuthorisationUrlReceived(QUrl)), tidalservice, SLOT(AuthorisationUrlReceived(QUrl)));
+#endif
+
// Playlist menu
connect(playlist_menu_, SIGNAL(aboutToHide()), SLOT(PlaylistMenuHidden()));
playlist_play_pause_ = playlist_menu_->addAction(tr("Play"), this, SLOT(PlaylistPlay()));
@@ -896,6 +915,16 @@ void MainWindow::ReloadSettings() {
ui_->tabs->DisableTab(subsonic_view_);
#endif
+#ifdef HAVE_TIDAL
+ settings.beginGroup(TidalSettingsPage::kSettingsGroup);
+ bool enable_tidal = settings.value("enabled", false).toBool();
+ settings.endGroup();
+ if (enable_tidal)
+ ui_->tabs->EnableTab(tidal_view_);
+ else
+ ui_->tabs->DisableTab(tidal_view_);
+#endif
+
ui_->tabs->ReloadSettings();
}
@@ -919,6 +948,9 @@ void MainWindow::ReloadAllSettings() {
#ifdef HAVE_SUBSONIC
subsonic_view_->ReloadSettings();
#endif
+#ifdef HAVE_TIDAL
+ tidal_view_->ReloadSettings();
+#endif
}
@@ -1992,6 +2024,14 @@ void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) {
if (!options.urls().empty()) {
+#ifdef HAVE_TIDAL
+ for (const QUrl url : options.urls()) {
+ if (url.scheme() == "tidal" && url.host() == "login") {
+ emit AuthorisationUrlReceived(url);
+ return;
+ }
+ }
+#endif
MimeData *data = new MimeData;
data->setUrls(options.urls());
// Behaviour depends on command line options, so set it here
diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h
index 257b88859..8b68c6326 100644
--- a/src/core/mainwindow.h
+++ b/src/core/mainwindow.h
@@ -316,6 +316,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
PlaylistItemList autocomplete_tag_items_;
InternetSongsView *subsonic_view_;
+ InternetTabsView *tidal_view_;
QAction *collection_show_all_;
QAction *collection_show_duplicates_;
diff --git a/src/covermanager/tidalcoverprovider.cpp b/src/covermanager/tidalcoverprovider.cpp
new file mode 100644
index 000000000..7a6658d42
--- /dev/null
+++ b/src/covermanager/tidalcoverprovider.cpp
@@ -0,0 +1,275 @@
+/*
+ * 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 "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/application.h"
+#include "core/closure.h"
+#include "core/network.h"
+#include "core/logging.h"
+#include "core/song.h"
+#include "internet/internetservices.h"
+#include "tidal/tidalservice.h"
+#include "albumcoverfetcher.h"
+#include "coverprovider.h"
+#include "tidalcoverprovider.h"
+
+const char *TidalCoverProvider::kApiUrl = "https://api.tidalhifi.com/v1";
+const char *TidalCoverProvider::kResourcesUrl = "https://resources.tidal.com";
+const int TidalCoverProvider::kLimit = 10;
+
+TidalCoverProvider::TidalCoverProvider(Application *app, QObject *parent) :
+ CoverProvider("Tidal", 2.0, true, app, parent),
+ service_(app->internet_services()->Service()),
+ network_(new NetworkAccessManager(this)) {
+
+}
+
+bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album, const int id) {
+
+ if (!service_ || !service_->authenticated()) return false;
+
+ ParamList params = ParamList() << Param("query", QString(artist + " " + album))
+ << Param("limit", QString::number(kLimit));
+
+ QNetworkReply *reply = CreateRequest("search/albums", params);
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleSearchReply(QNetworkReply*, int)), reply, id);
+
+ return true;
+
+}
+
+void TidalCoverProvider::CancelSearch(int id) { Q_UNUSED(id); }
+
+QNetworkReply *TidalCoverProvider::CreateRequest(const QString &ressource_name, const ParamList ¶ms_supplied) {
+
+ const ParamList params = ParamList() << params_supplied
+ << Param("countryCode", service_->country_code());
+
+ 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(kApiUrl + QString("/") + ressource_name);
+ url.setQuery(url_query);
+ QNetworkRequest req(url);
+ req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+ req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+ if (!service_->access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + service_->access_token().toUtf8());
+ if (!service_->session_id().isEmpty()) req.setRawHeader("X-Tidal-SessionId", service_->session_id().toUtf8());
+ QNetworkReply *reply = network_->get(req);
+
+ return reply;
+
+}
+
+QByteArray TidalCoverProvider::GetReplyData(QNetworkReply *reply, QString &error) {
+
+ QByteArray data;
+
+ if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
+ data = reply->readAll();
+ }
+ else {
+ if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
+ // This is a network error, there is nothing more to do.
+ error = Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
+ }
+ else {
+ // See if there is Json data containing "status" and "userMessage" - then use that instead.
+ data = reply->readAll();
+ QJsonParseError parse_error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error);
+ int status = 0;
+ int sub_status = 0;
+ 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("userMessage")) {
+ status = json_obj["status"].toInt();
+ sub_status = json_obj["subStatus"].toInt();
+ QString user_message = json_obj["userMessage"].toString();
+ error = QString("%1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status);
+ }
+ }
+ if (error.isEmpty()) {
+ if (reply->error() != QNetworkReply::NoError) {
+ error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
+ }
+ else {
+ error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
+ }
+ }
+ if (status == 401 && sub_status == 6001) { // User does not have a valid session
+ service_->Logout();
+ }
+ error = Error(error);
+ }
+ return QByteArray();
+ }
+
+ return data;
+
+}
+
+QJsonObject TidalCoverProvider::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.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 TidalCoverProvider::ExtractItems(QByteArray &data, QString &error) {
+
+ QJsonObject json_obj = ExtractJsonObj(data, error);
+ if (json_obj.isEmpty()) return QJsonValue();
+ return ExtractItems(json_obj, error);
+
+}
+
+QJsonValue TidalCoverProvider::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;
+
+}
+
+void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
+
+ reply->deleteLater();
+
+ CoverSearchResults results;
+ QString error;
+
+ QByteArray data = GetReplyData(reply, error);
+ if (data.isEmpty()) {
+ emit SearchFinished(id, results);
+ return;
+ }
+
+ QJsonObject json_obj = ExtractJsonObj(data, error);
+ if (json_obj.isEmpty()) {
+ emit SearchFinished(id, results);
+ return;
+ }
+
+ QJsonValue json_value = ExtractItems(json_obj, error);
+ if (!json_value.isArray()) {
+ emit SearchFinished(id, results);
+ return;
+ }
+ QJsonArray json_items = json_value.toArray();
+ if (json_items.isEmpty()) {
+ emit SearchFinished(id, results);
+ return;
+ }
+
+ for (const QJsonValue &value : json_items) {
+ 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("type") || !json_obj.contains("id") || !json_obj.contains("title") || !json_obj.contains("cover")) {
+ Error("Invalid Json reply, item missing id, type, album or cover.", json_obj);
+ continue;
+ }
+ QString album = json_obj["title"].toString();
+ QString cover = json_obj["cover"].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("name")) {
+ Error("Invalid Json reply, item artist missing name.", json_artist);
+ continue;
+ }
+ QString artist = json_artist["name"].toString();
+
+ album.remove(Song::kAlbumRemoveDisc);
+ album.remove(Song::kAlbumRemoveMisc);
+
+ cover = cover.replace("-", "/");
+ QUrl cover_url (QString("%1/images/%2/%3.jpg").arg(kResourcesUrl).arg(cover).arg("1280x1280"));
+
+ CoverSearchResult cover_result;
+ cover_result.artist = artist;
+ cover_result.album = album;
+ cover_result.image_url = cover_url;
+ results << cover_result;
+
+ }
+ emit SearchFinished(id, results);
+
+}
+
+QString TidalCoverProvider::Error(QString error, QVariant debug) {
+ qLog(Error) << "Tidal:" << error;
+ if (debug.isValid()) qLog(Debug) << debug;
+ return error;
+}
diff --git a/src/covermanager/tidalcoverprovider.h b/src/covermanager/tidalcoverprovider.h
new file mode 100644
index 000000000..9eb3ee2e4
--- /dev/null
+++ b/src/covermanager/tidalcoverprovider.h
@@ -0,0 +1,73 @@
+/*
+ * 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 TIDALCOVERPROVIDER_H
+#define TIDALCOVERPROVIDER_H
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "coverprovider.h"
+
+class QNetworkAccessManager;
+class QNetworkReply;
+class Application;
+class TidalService;
+
+class TidalCoverProvider : public CoverProvider {
+ Q_OBJECT
+
+ public:
+ explicit TidalCoverProvider(Application *app, QObject *parent = nullptr);
+ bool StartSearch(const QString &artist, const QString &album, const int id);
+ void CancelSearch(int id);
+
+ private slots:
+ void HandleSearchReply(QNetworkReply *reply, const int id);
+
+ private:
+ typedef QPair Param;
+ typedef QList ParamList;
+ typedef QPair EncodedParam;
+ static const char *kApiUrl;
+ static const char *kResourcesUrl;
+ static const int kLimit;
+
+ QNetworkReply *CreateRequest(const QString &ressource_name, const ParamList ¶ms_supplied);
+ 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);
+ QString Error(QString error, QVariant debug = QVariant());
+
+ TidalService *service_;
+ QNetworkAccessManager *network_;
+
+};
+
+#endif // TIDALCOVERPROVIDER_H
diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp
index 2bf2bb06c..33c348d31 100644
--- a/src/settings/settingsdialog.cpp
+++ b/src/settings/settingsdialog.cpp
@@ -73,6 +73,9 @@
#ifdef HAVE_SUBSONIC
# include "subsonicsettingspage.h"
#endif
+#ifdef HAVE_TIDAL
+# include "tidalsettingspage.h"
+#endif
#include "ui_settingsdialog.h"
@@ -145,12 +148,16 @@ SettingsDialog::SettingsDialog(Application *app, QMainWindow *mainwindow, QWidge
AddPage(Page_Moodbar, new MoodbarSettingsPage(this), iface);
#endif
-#if defined(HAVE_SUBSONIC)
+#if defined(HAVE_SUBSONIC) || defined(HAVE_TIDAL)
QTreeWidgetItem *streaming = AddCategory(tr("Streaming"));
#endif
+
#ifdef HAVE_SUBSONIC
AddPage(Page_Subsonic, new SubsonicSettingsPage(this), streaming);
#endif
+#ifdef HAVE_TIDAL
+ AddPage(Page_Tidal, new TidalSettingsPage(this), streaming);
+#endif
// List box
connect(ui_->list, SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)), SLOT(CurrentItemChanged(QTreeWidgetItem*)));
diff --git a/src/settings/settingsdialog.h b/src/settings/settingsdialog.h
index 4d2dc2435..0b8575cca 100644
--- a/src/settings/settingsdialog.h
+++ b/src/settings/settingsdialog.h
@@ -86,7 +86,8 @@ class SettingsDialog : public QDialog {
Page_Proxy,
Page_Scrobbler,
Page_Moodbar,
- Page_Subsonic
+ Page_Subsonic,
+ Page_Tidal,
};
enum Role {
diff --git a/src/settings/tidalsettingspage.cpp b/src/settings/tidalsettingspage.cpp
new file mode 100644
index 000000000..90df87c46
--- /dev/null
+++ b/src/settings/tidalsettingspage.cpp
@@ -0,0 +1,205 @@
+/*
+ * 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 "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "settingsdialog.h"
+#include "tidalsettingspage.h"
+#include "ui_tidalsettingspage.h"
+#include "core/application.h"
+#include "core/iconloader.h"
+#include "internet/internetservices.h"
+#include "tidal/tidalservice.h"
+#include "widgets/loginstatewidget.h"
+
+const char *TidalSettingsPage::kSettingsGroup = "Tidal";
+
+TidalSettingsPage::TidalSettingsPage(SettingsDialog *parent)
+ : SettingsPage(parent),
+ ui_(new Ui::TidalSettingsPage),
+ service_(dialog()->app()->internet_services()->Service()) {
+
+ ui_->setupUi(this);
+ setWindowIcon(IconLoader::Load("tidal"));
+
+ connect(ui_->button_login, SIGNAL(clicked()), SLOT(LoginClicked()));
+ connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked()));
+ connect(ui_->oauth, SIGNAL(toggled(bool)), SLOT(OAuthClicked(bool)));
+
+ connect(this, SIGNAL(Login()), service_, SLOT(StartAuthorisation()));
+ 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_->quality->addItem("Low", "LOW");
+ ui_->quality->addItem("High", "HIGH");
+ ui_->quality->addItem("Lossless", "LOSSLESS");
+ ui_->quality->addItem("Hi resolution", "HI_RES");
+
+ ui_->coversize->addItem("160x160", "160x160");
+ ui_->coversize->addItem("320x320", "320x320");
+ ui_->coversize->addItem("640x640", "640x640");
+ ui_->coversize->addItem("750x750", "750x750");
+ ui_->coversize->addItem("1280x1280", "1280x1280");
+
+ ui_->streamurl->addItem("streamurl", StreamUrlMethod_StreamUrl);
+ ui_->streamurl->addItem("urlpostpaywall", StreamUrlMethod_UrlPostPaywall);
+ ui_->streamurl->addItem("playbackinfopostpaywall", StreamUrlMethod_PlaybackInfoPostPaywall);
+
+}
+
+TidalSettingsPage::~TidalSettingsPage() { delete ui_; }
+
+void TidalSettingsPage::Load() {
+
+ QSettings s;
+
+ s.beginGroup(kSettingsGroup);
+ ui_->enable->setChecked(s.value("enabled", false).toBool());
+ ui_->oauth->setChecked(s.value("oauth", false).toBool());
+
+ ui_->client_id->setText(s.value("client_id").toString());
+ ui_->api_token->setText(s.value("api_token").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_->quality, "quality", "HIGH");
+ ui_->searchdelay->setValue(s.value("searchdelay", 1500).toInt());
+ ui_->artistssearchlimit->setValue(s.value("artistssearchlimit", 4).toInt());
+ ui_->albumssearchlimit->setValue(s.value("albumssearchlimit", 10).toInt());
+ ui_->songssearchlimit->setValue(s.value("songssearchlimit", 10).toInt());
+ ui_->checkbox_fetchalbums->setChecked(s.value("fetchalbums", false).toBool());
+ ui_->checkbox_download_album_covers->setChecked(s.value("downloadalbumcovers", true).toBool());
+ dialog()->ComboBoxLoadFromSettings(s, ui_->coversize, "coversize", "320x320");
+
+ StreamUrlMethod stream_url = static_cast(s.value("streamurl").toInt());
+ int i = ui_->streamurl->findData(stream_url);
+ if (i == -1) i = ui_->streamurl->findData(StreamUrlMethod_StreamUrl);
+ ui_->streamurl->setCurrentIndex(i);
+
+ s.endGroup();
+
+ OAuthClicked(ui_->oauth->isChecked());
+ if (service_->authenticated()) ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn);
+
+}
+
+void TidalSettingsPage::Save() {
+
+ QSettings s;
+ s.beginGroup(kSettingsGroup);
+ s.setValue("enabled", ui_->enable->isChecked());
+ s.setValue("oauth", ui_->oauth->isChecked());
+ s.setValue("client_id", ui_->client_id->text());
+ s.setValue("api_token", ui_->api_token->text());
+
+ s.setValue("username", ui_->username->text());
+ s.setValue("password", QString::fromUtf8(ui_->password->text().toUtf8().toBase64()));
+
+ s.setValue("quality", ui_->quality->itemData(ui_->quality->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("fetchalbums", ui_->checkbox_fetchalbums->isChecked());
+ s.setValue("downloadalbumcovers", ui_->checkbox_download_album_covers->isChecked());
+ s.setValue("coversize", ui_->coversize->itemData(ui_->coversize->currentIndex()));
+ s.setValue("streamurl", ui_->streamurl->itemData(ui_->streamurl->currentIndex()));
+ s.endGroup();
+
+ service_->ReloadSettings();
+
+}
+
+void TidalSettingsPage::LoginClicked() {
+
+ if (ui_->oauth->isChecked()) {
+ if (ui_->client_id->text().isEmpty()) {
+ QMessageBox::critical(this, tr("Configuration incomplete"), tr("Missing Tidal client ID."));
+ return;
+ }
+ emit Login();
+ }
+ else {
+ if (ui_->api_token->text().isEmpty() || ui_->username->text().isEmpty() || ui_->password->text().isEmpty()) {
+ QMessageBox::critical(this, tr("Configuration incomplete"), tr("Missing API token, username or password."));
+ return;
+ }
+ emit Login(ui_->api_token->text(), ui_->username->text(), ui_->password->text());
+ }
+ ui_->button_login->setEnabled(false);
+
+}
+
+bool TidalSettingsPage::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 TidalSettingsPage::OAuthClicked(const bool enabled) {
+
+ ui_->client_id->setEnabled(enabled);
+ ui_->api_token->setEnabled(!enabled);
+ ui_->username->setEnabled(!enabled);
+ ui_->password->setEnabled(!enabled);
+
+}
+
+void TidalSettingsPage::LogoutClicked() {
+ service_->Logout();
+ ui_->button_login->setEnabled(true);
+ ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
+}
+
+void TidalSettingsPage::LoginSuccess() {
+ if (!this->isVisible()) return;
+ ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn);
+ ui_->button_login->setEnabled(true);
+}
+
+void TidalSettingsPage::LoginFailure(const QString &failure_reason) {
+ if (!this->isVisible()) return;
+ QMessageBox::warning(this, tr("Authentication failed"), failure_reason);
+ ui_->button_login->setEnabled(true);
+}
diff --git a/src/settings/tidalsettingspage.h b/src/settings/tidalsettingspage.h
new file mode 100644
index 000000000..87cd6a6cf
--- /dev/null
+++ b/src/settings/tidalsettingspage.h
@@ -0,0 +1,71 @@
+/*
+ * 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 TIDALSETTINGSPAGE_H
+#define TIDALSETTINGSPAGE_H
+
+#include "config.h"
+
+#include
+#include
+
+#include "settings/settingspage.h"
+
+class QEvent;
+class TidalService;
+class SettingsDialog;
+class Ui_TidalSettingsPage;
+
+class TidalSettingsPage : public SettingsPage {
+ Q_OBJECT
+
+ public:
+ explicit TidalSettingsPage(SettingsDialog* parent = nullptr);
+ ~TidalSettingsPage();
+
+ static const char *kSettingsGroup;
+
+ enum StreamUrlMethod {
+ StreamUrlMethod_StreamUrl,
+ StreamUrlMethod_UrlPostPaywall,
+ StreamUrlMethod_PlaybackInfoPostPaywall,
+ };
+
+ void Load();
+ void Save();
+
+ bool eventFilter(QObject *object, QEvent *event);
+
+ signals:
+ void Login();
+ void Login(const QString &api_token, const QString &username, const QString &password);
+
+ private slots:
+ void OAuthClicked(const bool enabled);
+ void LoginClicked();
+ void LogoutClicked();
+ void LoginSuccess();
+ void LoginFailure(const QString &failure_reason);
+
+ private:
+ Ui_TidalSettingsPage* ui_;
+ TidalService *service_;
+};
+
+#endif
diff --git a/src/settings/tidalsettingspage.ui b/src/settings/tidalsettingspage.ui
new file mode 100644
index 000000000..835c2bc7a
--- /dev/null
+++ b/src/settings/tidalsettingspage.ui
@@ -0,0 +1,357 @@
+
+
+ TidalSettingsPage
+
+
+
+ 0
+ 0
+ 715
+ 836
+
+
+
+ Tidal
+
+
+ -
+
+
+ Enable
+
+
+
+ -
+
+
+ Tidal support is not official and requires a API token from a registered application to work. We can't help you getting these.
+
+
+ true
+
+
+ 10
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Authentication
+
+
+
-
+
+
+ Use OAuth
+
+
+
+ -
+
+
+
+ 150
+ 0
+
+
+
+ Client ID
+
+
+
+ -
+
+
+ -
+
+
+
+ 150
+ 0
+
+
+
+ API Token
+
+
+
+ -
+
+
+
+ 200
+ 0
+
+
+
+
+ -
+
+
+ Username
+
+
+
+ -
+
+
-
+
+
+
+
+ -
+
+
+ Password
+
+
+
+ -
+
+
+ QLineEdit::Password
+
+
+
+
+
+
+ -
+
+
+ Login
+
+
+
+ -
+
+
+ -
+
+
+ Preferences
+
+
+
-
+
+
+ Audio quality
+
+
+
+ -
+
+
+ -
+
+
+ Search delay
+
+
+
+ -
+
+
+ ms
+
+
+ 0
+
+
+ 10000
+
+
+ 50
+
+
+ 1500
+
+
+
+ -
+
+
+ Artists search limit
+
+
+
+ -
+
+
+ 1
+
+
+ 100
+
+
+ 50
+
+
+
+ -
+
+
+ Albums search limit
+
+
+
+ -
+
+
+ 1
+
+
+ 1000
+
+
+ 50
+
+
+
+ -
+
+
+ Songs search limit
+
+
+
+ -
+
+
+ Download album covers
+
+
+
+ -
+
+
+ Fetch entire albums when searching songs
+
+
+
+ -
+
+
+ -
+
+
+ Album cover size
+
+
+
+ -
+
+
+ 1
+
+
+ 1000
+
+
+ 50
+
+
+
+ -
+
+
+ Stream URL method
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 30
+
+
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 64
+ 64
+
+
+
+
+ 64
+ 64
+
+
+
+ :/icons/64x64/tidal.png
+
+
+
+
+
+
+
+
+
+ LoginStateWidget
+ QWidget
+ widgets/loginstatewidget.h
+ 1
+
+
+
+ enable
+ oauth
+ client_id
+ api_token
+ username
+ password
+ button_login
+ quality
+ searchdelay
+ artistssearchlimit
+ albumssearchlimit
+ songssearchlimit
+ checkbox_download_album_covers
+ checkbox_fetchalbums
+ coversize
+ streamurl
+
+
+
+
+
+
+
diff --git a/src/tidal/tidalbaserequest.cpp b/src/tidal/tidalbaserequest.cpp
new file mode 100644
index 000000000..1adf5754b
--- /dev/null
+++ b/src/tidal/tidalbaserequest.cpp
@@ -0,0 +1,204 @@
+/*
+ * 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 "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/logging.h"
+#include "core/network.h"
+#include "tidalservice.h"
+#include "tidalbaserequest.h"
+
+const char *TidalBaseRequest::kApiUrl = "https://api.tidalhifi.com/v1";
+
+TidalBaseRequest::TidalBaseRequest(TidalService *service, NetworkAccessManager *network, QObject *parent) :
+ QObject(parent),
+ service_(service),
+ network_(network)
+ {}
+
+TidalBaseRequest::~TidalBaseRequest() {}
+
+QNetworkReply *TidalBaseRequest::CreateRequest(const QString &ressource_name, const QList ¶ms_provided) {
+
+ ParamList params = ParamList() << params_provided
+ << Param("countryCode", country_code());
+
+ 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.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+ req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+ if (!access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + access_token().toUtf8());
+ if (!session_id().isEmpty()) req.setRawHeader("X-Tidal-SessionId", session_id().toUtf8());
+
+ QNetworkReply *reply = network_->get(req);
+ connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleSSLErrors(QList)));
+
+ qLog(Debug) << "Tidal: Sending request" << url;
+
+ return reply;
+
+}
+
+void TidalBaseRequest::HandleSSLErrors(QList ssl_errors) {
+
+ for (QSslError &ssl_error : ssl_errors) {
+ Error(ssl_error.errorString());
+ }
+
+}
+
+QByteArray TidalBaseRequest::GetReplyData(QNetworkReply *reply, const bool send_login) {
+
+ QByteArray data;
+
+ if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
+ data = reply->readAll();
+ }
+ else {
+ if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
+ // This is a network error, there is nothing more to do.
+ Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
+ }
+ else {
+ // See if there is Json data containing "status" and "userMessage" - then use that instead.
+ data = reply->readAll();
+ QString error;
+ QJsonParseError json_error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
+ int status = 0;
+ int sub_status = 0;
+ 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("userMessage")) {
+ status = json_obj["status"].toInt();
+ sub_status = json_obj["subStatus"].toInt();
+ QString user_message = json_obj["userMessage"].toString();
+ error = QString("%1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status);
+ }
+ }
+ if (reply->error() != QNetworkReply::NoError) {
+ error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
+ }
+ else {
+ error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
+ }
+ if (status == 401 && sub_status == 6001) { // User does not have a valid session
+ emit service_->Logout();
+ if (!oauth() && send_login && login_attempts() < max_login_attempts() && !api_token().isEmpty() && !username().isEmpty() && !password().isEmpty()) {
+ qLog(Error) << "Tidal:" << error;
+ qLog(Info) << "Tidal:" << "Attempting to login.";
+ NeedLogin();
+ emit service_->Login();
+ }
+ else {
+ Error(error);
+ }
+ }
+ else {
+ Error(error);
+ }
+ }
+ return QByteArray();
+ }
+
+ return data;
+
+}
+
+QJsonObject TidalBaseRequest::ExtractJsonObj(QByteArray &data) {
+
+ QJsonParseError json_error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
+
+ if (json_error.error != QJsonParseError::NoError) {
+ Error("Reply from server missing Json data.", data);
+ return QJsonObject();
+ }
+
+ if (json_doc.isNull() || json_doc.isEmpty()) {
+ Error("Received empty Json document.", data);
+ return QJsonObject();
+ }
+
+ if (!json_doc.isObject()) {
+ Error("Json document is not an object.", json_doc);
+ return QJsonObject();
+ }
+
+ QJsonObject json_obj = json_doc.object();
+ if (json_obj.isEmpty()) {
+ Error("Received empty Json object.", json_doc);
+ return QJsonObject();
+ }
+
+ return json_obj;
+
+}
+
+QJsonValue TidalBaseRequest::ExtractItems(QByteArray &data) {
+
+ QJsonObject json_obj = ExtractJsonObj(data);
+ if (json_obj.isEmpty()) return QJsonValue();
+ return ExtractItems(json_obj);
+
+}
+
+QJsonValue TidalBaseRequest::ExtractItems(QJsonObject &json_obj) {
+
+ if (!json_obj.contains("items")) {
+ Error("Json reply is missing items.", json_obj);
+ return QJsonArray();
+ }
+ QJsonValue json_items = json_obj["items"];
+ return json_items;
+
+}
+
+QString TidalBaseRequest::ErrorsToHTML(const QStringList &errors) {
+
+ QString error_html;
+ for (const QString &error : errors) {
+ error_html += error + "
";
+ }
+ return error_html;
+
+}
diff --git a/src/tidal/tidalbaserequest.h b/src/tidal/tidalbaserequest.h
new file mode 100644
index 000000000..c18c8ddee
--- /dev/null
+++ b/src/tidal/tidalbaserequest.h
@@ -0,0 +1,113 @@
+/*
+ * 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 TIDALBASEREQUEST_H
+#define TIDALBASEREQUEST_H
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "tidalservice.h"
+
+class QNetworkReply;
+class NetworkAccessManager;
+
+class TidalBaseRequest : public QObject {
+ Q_OBJECT
+
+ public:
+
+ enum QueryType {
+ QueryType_None,
+ QueryType_Artists,
+ QueryType_Albums,
+ QueryType_Songs,
+ QueryType_SearchArtists,
+ QueryType_SearchAlbums,
+ QueryType_SearchSongs,
+ QueryType_StreamURL,
+ };
+
+ TidalBaseRequest(TidalService *service, NetworkAccessManager *network, QObject *parent);
+ ~TidalBaseRequest();
+
+ 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, const bool send_login);
+ QJsonObject ExtractJsonObj(QByteArray &data);
+ QJsonValue ExtractItems(QByteArray &data);
+ QJsonValue ExtractItems(QJsonObject &json_obj);
+
+ virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0;
+ QString ErrorsToHTML(const QStringList &errors);
+
+ QString api_url() { return QString(kApiUrl); }
+ bool oauth() { return service_->oauth(); }
+ QString client_id() { return service_->client_id(); }
+ QString api_token() { return service_->api_token(); }
+ quint64 user_id() { return service_->user_id(); }
+ QString country_code() { return service_->country_code(); }
+ QString username() { return service_->username(); }
+ QString password() { return service_->password(); }
+ QString quality() { return service_->quality(); }
+ int artistssearchlimit() { return service_->artistssearchlimit(); }
+ int albumssearchlimit() { return service_->albumssearchlimit(); }
+ int songssearchlimit() { return service_->songssearchlimit(); }
+
+ QString access_token() { return service_->access_token(); }
+ QString session_id() { return service_->session_id(); }
+
+ bool authenticated() { return service_->authenticated(); }
+ bool login_sent() { return service_->login_sent(); }
+ int max_login_attempts() { return service_->max_login_attempts(); }
+ int login_attempts() { return service_->login_attempts(); }
+
+ virtual void NeedLogin() = 0;
+
+ private slots:
+ void HandleSSLErrors(QList ssl_errors);
+
+ private:
+
+ static const char *kApiUrl;
+
+ TidalService *service_;
+ NetworkAccessManager *network_;
+
+};
+
+#endif // TIDALBASEREQUEST_H
diff --git a/src/tidal/tidalfavoriterequest.cpp b/src/tidal/tidalfavoriterequest.cpp
new file mode 100644
index 000000000..047625a04
--- /dev/null
+++ b/src/tidal/tidalfavoriterequest.cpp
@@ -0,0 +1,297 @@
+/*
+ * 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 "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 "core/closure.h"
+#include "core/song.h"
+#include "tidalservice.h"
+#include "tidalbaserequest.h"
+#include "tidalfavoriterequest.h"
+
+TidalFavoriteRequest::TidalFavoriteRequest(TidalService *service, NetworkAccessManager *network, QObject *parent)
+ : TidalBaseRequest(service, network, parent),
+ service_(service),
+ network_(network),
+ need_login_(false) {}
+
+TidalFavoriteRequest::~TidalFavoriteRequest() {
+
+ while (!replies_.isEmpty()) {
+ QNetworkReply *reply = replies_.takeFirst();
+ disconnect(reply, 0, this, 0);
+ reply->abort();
+ reply->deleteLater();
+ }
+
+}
+
+QString TidalFavoriteRequest::FavoriteText(const FavoriteType type) {
+
+ switch (type) {
+ case FavoriteType_Artists:
+ return "artists";
+ case FavoriteType_Albums:
+ return "albums";
+ case FavoriteType_Songs:
+ default:
+ return "tracks";
+ }
+
+}
+
+void TidalFavoriteRequest::AddArtists(const SongList &songs) {
+ AddFavorites(FavoriteType_Artists, songs);
+}
+
+void TidalFavoriteRequest::AddAlbums(const SongList &songs) {
+ AddFavorites(FavoriteType_Albums, songs);
+}
+
+void TidalFavoriteRequest::AddSongs(const SongList &songs) {
+ AddFavorites(FavoriteType_Songs, songs);
+}
+
+void TidalFavoriteRequest::AddFavorites(const FavoriteType type, const SongList &songs) {
+
+ if (songs.isEmpty()) return;
+
+ QString text;
+ switch (type) {
+ case FavoriteType_Artists:
+ text = "artistIds";
+ break;
+ case FavoriteType_Albums:
+ text = "albumIds";
+ break;
+ case FavoriteType_Songs:
+ text = "trackIds";
+ break;
+ }
+
+ QStringList ids_list;
+ for (const Song &song : songs) {
+ QString id;
+ switch (type) {
+ case FavoriteType_Artists:
+ if (song.artist_id().isEmpty()) continue;
+ id = song.artist_id();
+ break;
+ case FavoriteType_Albums:
+ if (song.album_id().isEmpty()) continue;
+ id = song.album_id();
+ break;
+ case FavoriteType_Songs:
+ if (song.song_id().isEmpty()) continue;
+ id = song.song_id();
+ break;
+ }
+ if (id.isEmpty()) continue;
+ if (!ids_list.contains(id)) {
+ ids_list << id;
+ }
+ }
+ if (ids_list.isEmpty()) return;
+
+ QString ids = ids_list.join(',');
+
+ typedef QPair EncodedParam;
+
+ ParamList params = ParamList() << Param("countryCode", country_code())
+ << 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);
+ }
+
+ QUrl url(api_url() + QString("/") + "users/" + QString::number(service_->user_id()) + "/favorites/" + FavoriteText(type));
+ QNetworkRequest req(url);
+ req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+ req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+ if (!access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + access_token().toUtf8());
+ if (!session_id().isEmpty()) req.setRawHeader("X-Tidal-SessionId", session_id().toUtf8());
+ QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
+ QNetworkReply *reply = network_->post(req, query);
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(AddFavoritesReply(QNetworkReply*, FavoriteType, SongList)), reply, type, songs);
+ replies_ << reply;
+
+ qLog(Debug) << "Tidal: Sending request" << url << query;
+
+}
+
+void TidalFavoriteRequest::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, false);
+
+ if (reply->error() != QNetworkReply::NoError) {
+ return;
+ }
+
+ qLog(Debug) << "Tidal:" << 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 TidalFavoriteRequest::RemoveArtists(const SongList &songs) {
+ RemoveFavorites(FavoriteType_Artists, songs);
+}
+
+void TidalFavoriteRequest::RemoveAlbums(const SongList &songs) {
+ RemoveFavorites(FavoriteType_Albums, songs);
+}
+
+void TidalFavoriteRequest::RemoveSongs(const SongList &songs) {
+ RemoveFavorites(FavoriteType_Songs, songs);
+}
+
+void TidalFavoriteRequest::RemoveFavorites(const FavoriteType type, const SongList songs) {
+
+ if (songs.isEmpty()) return;
+
+ QStringList ids;
+ QMultiMap songs_map;
+ for (const Song &song : songs) {
+ QString id;
+ switch (type) {
+ case FavoriteType_Artists:
+ if (song.artist_id() <= 0) continue;
+ id = song.artist_id();
+ break;
+ case FavoriteType_Albums:
+ if (song.album_id().isEmpty()) continue;
+ id = song.album_id().toLongLong();
+ break;
+ case FavoriteType_Songs:
+ if (song.song_id() <= 0) continue;
+ id = song.song_id();
+ break;
+ }
+ if (!ids.contains(id)) ids << id;
+ songs_map.insertMulti(id, song);
+ }
+
+ for (const QString &id : ids) {
+ SongList songs_list = songs_map.values(id);
+ RemoveFavorites(type, id, songs_list);
+ }
+
+}
+
+void TidalFavoriteRequest::RemoveFavorites(const FavoriteType type, const QString &id, const SongList &songs) {
+
+ ParamList params = ParamList() << Param("countryCode", country_code());
+
+ 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(api_url() + QString("/") + "users/" + QString::number(service_->user_id()) + "/favorites/" + FavoriteText(type) + QString("/") + id);
+ url.setQuery(url_query);
+ QNetworkRequest req(url);
+ req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+ req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+ if (!access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + access_token().toUtf8());
+ if (!session_id().isEmpty()) req.setRawHeader("X-Tidal-SessionId", session_id().toUtf8());
+ QNetworkReply *reply = network_->deleteResource(req);
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(RemoveFavoritesReply(QNetworkReply*, FavoriteType, SongList)), reply, type, songs);
+ replies_ << reply;
+
+ qLog(Debug) << "Tidal: Sending request" << url << "with" << songs.count() << "songs";
+
+}
+
+void TidalFavoriteRequest::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, false);
+ if (reply->error() != QNetworkReply::NoError) {
+ return;
+ }
+
+ qLog(Debug) << "Tidal:" << songs.count() << "songs removed from" << FavoriteText(type) << "favorites.";
+
+ switch (type) {
+ case FavoriteType_Artists:
+ emit ArtistsRemoved(songs);
+ break;
+ case FavoriteType_Albums:
+ emit AlbumsRemoved(songs);
+ break;
+ case FavoriteType_Songs:
+ emit SongsRemoved(songs);
+ break;
+ }
+
+}
+
+void TidalFavoriteRequest::Error(const QString &error, const QVariant &debug) {
+
+ qLog(Error) << "Tidal:" << error;
+ if (debug.isValid()) qLog(Debug) << debug;
+
+}
diff --git a/src/tidal/tidalfavoriterequest.h b/src/tidal/tidalfavoriterequest.h
new file mode 100644
index 000000000..c19a49b8d
--- /dev/null
+++ b/src/tidal/tidalfavoriterequest.h
@@ -0,0 +1,88 @@
+/*
+ * 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 TIDALFAVORITEREQUEST_H
+#define TIDALFAVORITEREQUEST_H
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+
+#include "tidalbaserequest.h"
+#include "core/song.h"
+
+class QNetworkReply;
+class TidalService;
+class NetworkAccessManager;
+
+class TidalFavoriteRequest : public TidalBaseRequest {
+ Q_OBJECT
+
+ public:
+ TidalFavoriteRequest(TidalService *service, NetworkAccessManager *network, QObject *parent);
+ ~TidalFavoriteRequest();
+
+ enum FavoriteType {
+ FavoriteType_Artists,
+ FavoriteType_Albums,
+ FavoriteType_Songs
+ };
+
+ bool need_login() { return need_login_; }
+
+ void NeedLogin() { need_login_ = true; }
+
+ 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:
+ void Error(const QString &error, const QVariant &debug = QVariant());
+ QString FavoriteText(const FavoriteType type);
+ void AddFavorites(const FavoriteType type, const SongList &songs);
+ void RemoveFavorites(const FavoriteType type, const SongList songs);
+ void RemoveFavorites(const FavoriteType type, const QString &id, const SongList &songs);
+
+ TidalService *service_;
+ NetworkAccessManager *network_;
+ QList replies_;
+ bool need_login_;
+
+};
+
+#endif // TIDALFAVORITEREQUEST_H
diff --git a/src/tidal/tidalrequest.cpp b/src/tidal/tidalrequest.cpp
new file mode 100644
index 000000000..f3699590b
--- /dev/null
+++ b/src/tidal/tidalrequest.cpp
@@ -0,0 +1,1253 @@
+/*
+ * 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 "config.h"
+
+#include
+#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 "core/application.h"
+#include "covermanager/albumcoverloader.h"
+#include "tidalservice.h"
+#include "tidalurlhandler.h"
+#include "tidalbaserequest.h"
+#include "tidalrequest.h"
+
+const char *TidalRequest::kResourcesUrl = "https://resources.tidal.com";
+const int TidalRequest::kMaxConcurrentArtistsRequests = 3;
+const int TidalRequest::kMaxConcurrentAlbumsRequests = 3;
+const int TidalRequest::kMaxConcurrentSongsRequests = 3;
+const int TidalRequest::kMaxConcurrentArtistAlbumsRequests = 3;
+const int TidalRequest::kMaxConcurrentAlbumSongsRequests = 3;
+const int TidalRequest::kMaxConcurrentAlbumCoverRequests = 1;
+
+TidalRequest::TidalRequest(TidalService *service, TidalUrlHandler *url_handler, Application *app, NetworkAccessManager *network, QueryType type, QObject *parent)
+ : TidalBaseRequest(service, network, parent),
+ service_(service),
+ url_handler_(url_handler),
+ app_(app),
+ network_(network),
+ type_(type),
+ fetchalbums_(service->fetchalbums()),
+ coversize_(service_->coversize()),
+ query_id_(-1),
+ finished_(false),
+ artists_requests_active_(0),
+ artists_total_(0),
+ artists_received_(0),
+ albums_requests_active_(0),
+ songs_requests_active_(0),
+ artist_albums_requests_active_(0),
+ artist_albums_requested_(0),
+ artist_albums_received_(0),
+ album_songs_requests_active_(0),
+ album_songs_requested_(0),
+ album_songs_received_(0),
+ album_covers_requests_active_(),
+ album_covers_requested_(0),
+ album_covers_received_(0),
+ need_login_(false),
+ no_results_(false) {}
+
+TidalRequest::~TidalRequest() {
+
+ while (!replies_.isEmpty()) {
+ QNetworkReply *reply = replies_.takeFirst();
+ disconnect(reply, 0, this, 0);
+ if (reply->isRunning()) reply->abort();
+ reply->deleteLater();
+ }
+
+ while (!album_cover_replies_.isEmpty()) {
+ QNetworkReply *reply = album_cover_replies_.takeFirst();
+ disconnect(reply, 0, this, 0);
+ if (reply->isRunning()) reply->abort();
+ reply->deleteLater();
+ }
+
+}
+
+void TidalRequest::LoginComplete(const bool success, QString error) {
+
+ if (!need_login_) return;
+ need_login_ = false;
+
+ if (!success) {
+ Error(error);
+ return;
+ }
+
+ Process();
+
+}
+
+void TidalRequest::Process() {
+
+ if (!service_->authenticated()) {
+ emit UpdateStatus(query_id_, tr("Authenticating..."));
+ need_login_ = true;
+ service_->TryLogin();
+ return;
+ }
+
+ 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 TidalRequest::Search(const int query_id, const QString &search_text) {
+ query_id_ = query_id;
+ search_text_ = search_text;
+}
+
+void TidalRequest::GetArtists() {
+
+ emit UpdateStatus(query_id_, tr("Retrieving artists..."));
+ emit UpdateProgress(query_id_, 0);
+ AddArtistsRequest();
+
+}
+
+void TidalRequest::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 TidalRequest::FlushArtistsRequests() {
+
+ while (!artists_requests_queue_.isEmpty() && artists_requests_active_ < kMaxConcurrentArtistsRequests) {
+
+ Request request = artists_requests_queue_.dequeue();
+ ++artists_requests_active_;
+
+ ParamList parameters;
+ if (type_ == QueryType_SearchArtists) parameters << Param("query", search_text_);
+ if (request.limit > 0) parameters << Param("limit", QString::number(request.limit));
+ if (request.offset > 0) parameters << Param("offset", QString::number(request.offset));
+ QNetworkReply *reply;
+ if (type_ == QueryType_Artists) {
+ reply = CreateRequest(QString("users/%1/favorites/artists").arg(service_->user_id()), parameters);
+ }
+ if (type_ == QueryType_SearchArtists) {
+ reply = CreateRequest("search/artists", parameters);
+ }
+ if (!reply) continue;
+ replies_ << reply;
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(ArtistsReplyReceived(QNetworkReply*, int, int)), reply, request.limit, request.offset);
+
+ }
+
+}
+
+void TidalRequest::GetAlbums() {
+
+ emit UpdateStatus(query_id_, tr("Retrieving albums..."));
+ emit UpdateProgress(query_id_, 0);
+ AddAlbumsRequest();
+
+}
+
+void TidalRequest::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 TidalRequest::FlushAlbumsRequests() {
+
+ while (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) {
+
+ Request request = albums_requests_queue_.dequeue();
+ ++albums_requests_active_;
+
+ ParamList parameters;
+ if (type_ == QueryType_SearchAlbums) parameters << Param("query", search_text_);
+ if (request.limit > 0) parameters << Param("limit", QString::number(request.limit));
+ if (request.offset > 0) parameters << Param("offset", QString::number(request.offset));
+ QNetworkReply *reply;
+ if (type_ == QueryType_Albums) {
+ reply = CreateRequest(QString("users/%1/favorites/albums").arg(service_->user_id()), parameters);
+ }
+ if (type_ == QueryType_SearchAlbums) {
+ reply = CreateRequest("search/albums", parameters);
+ }
+ if (!reply) continue;
+ replies_ << reply;
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumsReplyReceived(QNetworkReply*, int, int)), reply, request.limit, request.offset);
+
+ }
+
+}
+
+void TidalRequest::GetSongs() {
+
+ emit UpdateStatus(query_id_, tr("Retrieving songs..."));
+ emit UpdateProgress(query_id_, 0);
+ AddSongsRequest();
+
+}
+
+void TidalRequest::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 TidalRequest::FlushSongsRequests() {
+
+ while (!songs_requests_queue_.isEmpty() && songs_requests_active_ < kMaxConcurrentSongsRequests) {
+
+ Request request = songs_requests_queue_.dequeue();
+ ++songs_requests_active_;
+
+ ParamList parameters;
+ if (type_ == QueryType_SearchSongs) parameters << Param("query", search_text_);
+ if (request.limit > 0) parameters << Param("limit", QString::number(request.limit));
+ if (request.offset > 0) parameters << Param("offset", QString::number(request.offset));
+ QNetworkReply *reply;
+ if (type_ == QueryType_Songs) {
+ reply = CreateRequest(QString("users/%1/favorites/tracks").arg(service_->user_id()), parameters);
+ }
+ if (type_ == QueryType_SearchSongs) {
+ reply = CreateRequest("search/tracks", parameters);
+ }
+ if (!reply) continue;
+ replies_ << reply;
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(SongsReplyReceived(QNetworkReply*, int, int)), reply, request.limit, request.offset);
+
+ }
+
+}
+
+void TidalRequest::ArtistsSearch() {
+
+ emit UpdateStatus(query_id_, tr("Searching..."));
+ emit UpdateProgress(query_id_, 0);
+ AddArtistsSearchRequest();
+
+}
+
+void TidalRequest::AddArtistsSearchRequest(const int offset) {
+
+ AddArtistsRequest(offset, service_->artistssearchlimit());
+
+}
+
+void TidalRequest::AlbumsSearch() {
+
+ emit UpdateStatus(query_id_, tr("Searching..."));
+ emit UpdateProgress(query_id_, 0);
+ AddAlbumsSearchRequest();
+
+}
+
+void TidalRequest::AddAlbumsSearchRequest(const int offset) {
+
+ AddAlbumsRequest(offset, service_->albumssearchlimit());
+
+}
+
+void TidalRequest::SongsSearch() {
+
+ emit UpdateStatus(query_id_, tr("Searching..."));
+ emit UpdateProgress(query_id_, 0);
+ AddSongsSearchRequest();
+
+}
+
+void TidalRequest::AddSongsSearchRequest(const int offset) {
+
+ AddSongsRequest(offset, service_->songssearchlimit());
+
+}
+
+void TidalRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) {
+
+ if (!replies_.contains(reply)) return;
+ replies_.removeAll(reply);
+ reply->deleteLater();
+
+ QByteArray data = GetReplyData(reply, (offset_requested == 0));
+
+ --artists_requests_active_;
+
+ if (finished_) return;
+
+ if (data.isEmpty()) {
+ ArtistsFinishCheck();
+ return;
+ }
+
+ QJsonObject json_obj = ExtractJsonObj(data);
+ if (json_obj.isEmpty()) {
+ ArtistsFinishCheck();
+ return;
+ }
+
+ if (!json_obj.contains("limit") ||
+ !json_obj.contains("offset") ||
+ !json_obj.contains("totalNumberOfItems") ||
+ !json_obj.contains("items")) {
+ ArtistsFinishCheck();
+ Error("Json object missing values.", json_obj);
+ return;
+ }
+ //int limit = json_obj["limit"].toInt();
+ int offset = json_obj["offset"].toInt();
+ int artists_total = json_obj["totalNumberOfItems"].toInt();
+
+ if (offset_requested == 0) {
+ artists_total_ = artists_total;
+ }
+ else if (artists_total != artists_total_) {
+ Error(QString("totalNumberOfItems returned does not match previous totalNumberOfItems! %1 != %2").arg(artists_total).arg(artists_total_));
+ ArtistsFinishCheck();
+ return;
+ }
+
+ if (offset != offset_requested) {
+ Error(QString("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested));
+ ArtistsFinishCheck();
+ return;
+ }
+
+ if (offset_requested == 0) {
+ emit ProgressSetMaximum(query_id_, artists_total_);
+ emit UpdateProgress(query_id_, artists_received_);
+ }
+
+ QJsonValue json_value = ExtractItems(json_obj);
+ if (!json_value.isArray()) {
+ ArtistsFinishCheck();
+ return;
+ }
+
+ QJsonArray json_items = json_value.toArray();
+ if (json_items.isEmpty()) { // Empty array means no results
+ if (offset_requested == 0) 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;
+ }
+
+ QString artist_id;
+ if (json_obj["id"].isString()) {
+ artist_id = json_obj["id"].toString();
+ }
+ else {
+ artist_id = QString::number(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(query_id_, artists_received_);
+
+ ArtistsFinishCheck(limit_requested, offset, artists_received);
+
+}
+
+void TidalRequest::ArtistsFinishCheck(const int limit, const int offset, const int artists_received) {
+
+ if (finished_) return;
+
+ if ((limit == 0 || limit > artists_received) && artists_received_ < artists_total_) {
+ int offset_next = offset + artists_received;
+ if (offset_next > 0 && offset_next < artists_total_) {
+ if (type_ == QueryType_Artists) AddArtistsRequest(offset_next);
+ else if (type_ == QueryType_SearchArtists) AddArtistsSearchRequest(offset_next);
+ }
+ }
+
+ if (!artists_requests_queue_.isEmpty() && artists_requests_active_ < kMaxConcurrentArtistsRequests) FlushArtistsRequests();
+
+ if (artists_requests_queue_.isEmpty() && artists_requests_active_ <= 0) { // Artist query is finished, get all albums for all artists.
+
+ // Get artist albums
+ for (const QString &artist_id : artist_albums_requests_pending_) {
+ AddArtistAlbumsRequest(artist_id);
+ ++artist_albums_requested_;
+ }
+ artist_albums_requests_pending_.clear();
+
+ if (artist_albums_requested_ > 0) {
+ if (artist_albums_requested_ == 1) emit UpdateStatus(query_id_, tr("Retrieving albums for %1 artist...").arg(artist_albums_requested_));
+ else emit UpdateStatus(query_id_, tr("Retrieving albums for %1 artists...").arg(artist_albums_requested_));
+ emit ProgressSetMaximum(query_id_, artist_albums_requested_);
+ emit UpdateProgress(query_id_, 0);
+ }
+
+ }
+
+ FinishCheck();
+
+}
+
+void TidalRequest::AlbumsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) {
+ --albums_requests_active_;
+ AlbumsReceived(reply, QString(), limit_requested, offset_requested, (offset_requested == 0));
+ if (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) FlushAlbumsRequests();
+}
+
+void TidalRequest::AddArtistAlbumsRequest(const QString &artist_id, const int offset) {
+
+ Request request;
+ request.artist_id = artist_id;
+ request.offset = offset;
+ artist_albums_requests_queue_.enqueue(request);
+ if (artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) FlushArtistAlbumsRequests();
+
+}
+
+void TidalRequest::FlushArtistAlbumsRequests() {
+
+ while (!artist_albums_requests_queue_.isEmpty() && artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) {
+
+ Request request = artist_albums_requests_queue_.dequeue();
+ ++artist_albums_requests_active_;
+
+ ParamList parameters;
+ if (request.offset > 0) parameters << Param("offset", QString::number(request.offset));
+ QNetworkReply *reply = CreateRequest(QString("artists/%1/albums").arg(request.artist_id), parameters);
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(ArtistAlbumsReplyReceived(QNetworkReply*, QString, int)), reply, request.artist_id, request.offset);
+ replies_ << reply;
+
+ }
+
+}
+
+void TidalRequest::ArtistAlbumsReplyReceived(QNetworkReply *reply, const QString &artist_id, const int offset_requested) {
+
+ --artist_albums_requests_active_;
+ ++artist_albums_received_;
+ emit UpdateProgress(query_id_, artist_albums_received_);
+ AlbumsReceived(reply, artist_id, 0, offset_requested, false);
+ if (!artist_albums_requests_queue_.isEmpty() && artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) FlushArtistAlbumsRequests();
+
+}
+
+void TidalRequest::AlbumsReceived(QNetworkReply *reply, const QString &artist_id_requested, const int limit_requested, const int offset_requested, const bool auto_login) {
+
+ if (!replies_.contains(reply)) return;
+ replies_.removeAll(reply);
+ reply->deleteLater();
+
+ QByteArray data = GetReplyData(reply, auto_login);
+
+ if (finished_) return;
+
+ if (data.isEmpty()) {
+ AlbumsFinishCheck(artist_id_requested);
+ return;
+ }
+
+ QJsonObject json_obj = ExtractJsonObj(data);
+ if (json_obj.isEmpty()) {
+ AlbumsFinishCheck(artist_id_requested);
+ return;
+ }
+
+ if (!json_obj.contains("limit") ||
+ !json_obj.contains("offset") ||
+ !json_obj.contains("totalNumberOfItems") ||
+ !json_obj.contains("items")) {
+ Error("Json object missing values.", json_obj);
+ AlbumsFinishCheck(artist_id_requested);
+ return;
+ }
+
+ //int limit = json_obj["limit"].toInt();
+ int offset = json_obj["offset"].toInt();
+ int albums_total = json_obj["totalNumberOfItems"].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);
+ if (!json_value.isArray()) {
+ AlbumsFinishCheck(artist_id_requested);
+ return;
+ }
+ QJsonArray json_items = json_value.toArray();
+ if (json_items.isEmpty()) {
+ if ((type_ == QueryType_Albums || type_ == QueryType_SearchAlbums || (type_ == QueryType_SearchSongs && fetchalbums_)) && offset_requested == 0) {
+ 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("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();
+ }
+
+ QString album_id;
+ QString album;
+ if (json_obj.contains("type")) { // This was a albums request or search
+ if (!json_obj.contains("id") || !json_obj.contains("title")) {
+ Error("Invalid Json reply, item is missing ID or title.", json_obj);
+ continue;
+ }
+ if (json_obj["id"].isString()) {
+ album_id = json_obj["id"].toString();
+ }
+ else {
+ album_id = QString::number(json_obj["id"].toInt());
+ }
+ album = json_obj["title"].toString();
+ }
+ else if (json_obj.contains("album")) { // This was a tracks request or search
+ QJsonValue json_value_album = json_obj["album"];
+ if (!json_value_album.isObject()) {
+ Error("Invalid Json reply, item album is not a object.", json_value_album);
+ continue;
+ }
+ QJsonObject json_album = json_value_album.toObject();
+ if (!json_album.contains("id") || !json_album.contains("title")) {
+ Error("Invalid Json reply, item album is missing ID or title.", json_album);
+ continue;
+ }
+ if (json_album["id"].isString()) {
+ album_id = json_album["id"].toString();
+ }
+ else {
+ album_id = QString::number(json_album["id"].toInt());
+ }
+ album = json_album["title"].toString();
+
+ }
+ else {
+ Error("Invalid Json reply, item missing type or album.", json_obj);
+ continue;
+ }
+
+ if (album_songs_requests_pending_.contains(album_id)) continue;
+
+ if (!json_obj.contains("artist") || !json_obj.contains("title") || !json_obj.contains("audioQuality")) {
+ Error("Invalid Json reply, item missing artist, title or audioQuality.", json_obj);
+ continue;
+ }
+ 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;
+ }
+
+ QString artist_id;
+ if (json_artist["id"].isString()) {
+ artist_id = json_artist["id"].toString();
+ }
+ else {
+ artist_id = QString::number(json_artist["id"].toInt());
+ }
+ QString artist = json_artist["name"].toString();
+
+ QString quality = json_obj["audioQuality"].toString();
+ QString copyright = json_obj["copyright"].toString();
+
+ //qLog(Debug) << "Tidal:" << artist << album << quality << copyright;
+
+ Request request;
+ if (artist_id_requested.isEmpty()) {
+ request.artist_id = artist_id;
+ }
+ else {
+ request.artist_id = artist_id_requested;
+ }
+ request.album_id = album_id;
+ request.album_artist = artist;
+ album_songs_requests_pending_.insert(album_id, request);
+
+ }
+
+ AlbumsFinishCheck(artist_id_requested, limit_requested, offset, albums_total, albums_received);
+
+}
+
+void TidalRequest::AlbumsFinishCheck(const QString &artist_id, const int limit, const int offset, const int albums_total, const int albums_received) {
+
+ if (finished_) return;
+
+ if (limit == 0 || limit > albums_received) {
+ int offset_next = offset + albums_received;
+ if (offset_next > 0 && offset_next < albums_total) {
+ switch (type_) {
+ case QueryType_Albums:
+ AddAlbumsRequest(offset_next);
+ break;
+ case QueryType_SearchAlbums:
+ AddAlbumsSearchRequest(offset_next);
+ break;
+ case QueryType_Artists:
+ case QueryType_SearchArtists:
+ AddArtistAlbumsRequest(artist_id, offset_next);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ if (
+ albums_requests_queue_.isEmpty() &&
+ albums_requests_active_ <= 0 &&
+ artist_albums_requests_queue_.isEmpty() &&
+ artist_albums_requests_active_ <= 0
+ ) { // Artist albums query is finished, get all songs for all albums.
+
+ // Get songs for all the albums.
+
+ QHash ::iterator 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(query_id_, tr("Retrieving songs for %1 album...").arg(album_songs_requested_));
+ else emit UpdateStatus(query_id_, tr("Retrieving songs for %1 albums...").arg(album_songs_requested_));
+ emit ProgressSetMaximum(query_id_, album_songs_requested_);
+ emit UpdateProgress(query_id_, 0);
+ }
+ }
+
+ FinishCheck();
+
+}
+
+void TidalRequest::SongsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) {
+
+ --songs_requests_active_;
+ if (type_ == QueryType_SearchSongs && fetchalbums_) {
+ AlbumsReceived(reply, 0, limit_requested, offset_requested, (offset_requested == 0));
+ }
+ else {
+ SongsReceived(reply, 0, 0, limit_requested, offset_requested, (offset_requested == 0));
+ }
+
+}
+
+void TidalRequest::AddAlbumSongsRequest(const QString &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 TidalRequest::FlushAlbumSongsRequests() {
+
+ while (!album_songs_requests_queue_.isEmpty() && album_songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) {
+
+ Request request = album_songs_requests_queue_.dequeue();
+ ++album_songs_requests_active_;
+ ParamList parameters;
+ if (request.offset > 0) parameters << Param("offset", QString::number(request.offset));
+ QNetworkReply *reply = CreateRequest(QString("albums/%1/tracks").arg(request.album_id), parameters);
+ replies_ << reply;
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumSongsReplyReceived(QNetworkReply*, QString, QString, int, QString)), reply, request.artist_id, request.album_id, request.offset, request.album_artist);
+
+ }
+
+}
+
+void TidalRequest::AlbumSongsReplyReceived(QNetworkReply *reply, const QString &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(query_id_, album_songs_received_);
+ }
+ SongsReceived(reply, artist_id, album_id, 0, offset_requested, false, album_artist);
+
+}
+
+void TidalRequest::SongsReceived(QNetworkReply *reply, const QString &artist_id, const QString &album_id, const int limit_requested, const int offset_requested, const bool auto_login, const QString &album_artist) {
+
+ if (!replies_.contains(reply)) return;
+ replies_.removeAll(reply);
+ reply->deleteLater();
+
+ QByteArray data = GetReplyData(reply, auto_login);
+
+ if (finished_) return;
+
+ if (data.isEmpty()) {
+ SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, 0, 0, album_artist);
+ return;
+ }
+
+ QJsonObject json_obj = ExtractJsonObj(data);
+ if (json_obj.isEmpty()) {
+ SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, 0, 0, album_artist);
+ return;
+ }
+
+ if (!json_obj.contains("limit") ||
+ !json_obj.contains("offset") ||
+ !json_obj.contains("totalNumberOfItems") ||
+ !json_obj.contains("items")) {
+ Error("Json object missing values.", json_obj);
+ SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, 0, 0, album_artist);
+ return;
+ }
+
+ //int limit = json_obj["limit"].toInt();
+ int offset = json_obj["offset"].toInt();
+ int songs_total = json_obj["totalNumberOfItems"].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);
+ 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()) {
+ if ((type_ == QueryType_Songs || type_ == QueryType_SearchSongs) && offset_requested == 0) {
+ no_results_ = true;
+ }
+ SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist);
+ 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();
+
+ 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();
+ }
+
+ ++songs_received;
+ Song song(Song::Source_Tidal);
+ ParseSong(song, json_obj, artist_id, album_id, album_artist);
+ 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 TidalRequest::SongsFinishCheck(const QString &artist_id, const QString &album_id, const int limit, const int offset, const int songs_total, const int songs_received, const QString &album_artist) {
+
+ 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 fallthrough.
+ if (artist_id.isEmpty() && album_id.isEmpty()) {
+ AddSongsSearchRequest(offset_next);
+ break;
+ }
+ // fallthrough
+ 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_->download_album_covers() &&
+ IsQuery() &&
+ songs_requests_queue_.isEmpty() &&
+ songs_requests_active_ <= 0 &&
+ album_songs_requests_queue_.isEmpty() &&
+ album_songs_requests_active_ <= 0 &&
+ album_cover_requests_queue_.isEmpty() &&
+ album_covers_received_ <= 0 &&
+ album_covers_requests_sent_.isEmpty() &&
+ album_songs_received_ >= album_songs_requested_
+ ) {
+ GetAlbumCovers();
+ }
+
+ FinishCheck();
+
+}
+
+QString TidalRequest::ParseSong(Song &song, const QJsonObject &json_obj, const QString &artist_id_requested, const QString &album_id_requested, const QString &album_artist) {
+
+ Q_UNUSED(artist_id_requested);
+
+ if (
+ !json_obj.contains("album") ||
+ !json_obj.contains("allowStreaming") ||
+ !json_obj.contains("artist") ||
+ !json_obj.contains("artists") ||
+ !json_obj.contains("audioQuality") ||
+ !json_obj.contains("duration") ||
+ !json_obj.contains("id") ||
+ !json_obj.contains("streamReady") ||
+ !json_obj.contains("title") ||
+ !json_obj.contains("trackNumber") ||
+ !json_obj.contains("url") ||
+ !json_obj.contains("volumeNumber") ||
+ !json_obj.contains("copyright")
+ ) {
+ Error("Invalid Json reply, track is missing one or more values.", json_obj);
+ return QString();
+ }
+
+ QJsonValue json_value_artist = json_obj["artist"];
+ QJsonValue json_value_album = json_obj["album"];
+ QJsonValue json_duration = json_obj["duration"];
+ QJsonArray json_artists = json_obj["artists"].toArray();
+
+ QString song_id;
+ if (json_obj["id"].isString()) {
+ song_id = json_obj["id"].toString();
+ }
+ else {
+ song_id = QString::number(json_obj["id"].toInt());
+ }
+
+ QString title = json_obj["title"].toString();
+ QString urlstr = json_obj["url"].toString();
+ int track = json_obj["trackNumber"].toInt();
+ int disc = json_obj["volumeNumber"].toInt();
+ bool allow_streaming = json_obj["allowStreaming"].toBool();
+ bool stream_ready = json_obj["streamReady"].toBool();
+ QString copyright = json_obj["copyright"].toString();
+
+ if (!json_value_artist.isObject()) {
+ Error("Invalid Json reply, track artist is not a object.", json_value_artist);
+ return QString();
+ }
+ QJsonObject json_artist = json_value_artist.toObject();
+ if (!json_artist.contains("id") || !json_artist.contains("name")) {
+ Error("Invalid Json reply, track artist is missing id or name.", json_artist);
+ return QString();
+ }
+ QString artist_id;
+ if (json_artist["id"].isString()) {
+ artist_id = json_artist["id"].toString();
+ }
+ else {
+ artist_id = QString::number(json_artist["id"].toInt());
+ }
+ QString artist = json_artist["name"].toString();
+
+ if (!json_value_album.isObject()) {
+ Error("Invalid Json reply, track album is not a object.", json_value_album);
+ return QString();
+ }
+ QJsonObject json_album = json_value_album.toObject();
+ if (!json_album.contains("id") || !json_album.contains("title") || !json_album.contains("cover")) {
+ Error("Invalid Json reply, track album is missing id, title or cover.", json_album);
+ return QString();
+ }
+ QString album_id;
+ if (json_album["id"].isString()) {
+ album_id = json_album["id"].toString();
+ }
+ else {
+ album_id = QString::number(json_album["id"].toInt());
+ }
+ if (!album_id_requested.isEmpty() && album_id_requested != album_id) {
+ Error("Invalid Json reply, track album id is wrong.", json_album);
+ return QString();
+ }
+ QString album = json_album["title"].toString();
+ QString cover = json_album["cover"].toString();
+
+ if (!allow_streaming) {
+ Warn(QString("Song %1 %2 %3 is not allowStreaming").arg(artist).arg(album).arg(title));
+ }
+
+ if (!stream_ready) {
+ Warn(QString("Song %1 %2 %3 is not streamReady").arg(artist).arg(album).arg(title));
+ }
+
+ QUrl url;
+ url.setScheme(url_handler_->scheme());
+ url.setPath(song_id);
+
+ QVariant q_duration = json_duration.toVariant();
+ quint64 duration = 0;
+ if (q_duration.isValid() && (q_duration.type() == QVariant::Int || q_duration.type() == QVariant::Double)) {
+ duration = q_duration.toLongLong() * kNsecPerSec;
+ }
+ else {
+ Error("Invalid duration for song.", json_duration);
+ return QString();
+ }
+
+ cover = cover.replace("-", "/");
+ QUrl cover_url (QString("%1/images/%2/%3.jpg").arg(kResourcesUrl).arg(cover).arg(coversize_));
+
+ title.remove(Song::kTitleRemoveMisc);
+
+ //qLog(Debug) << "id" << song_id << "track" << track << "disc" << disc << "title" << title << "album" << album << "album artist" << album_artist << "artist" << artist << cover << allow_streaming << url;
+
+ song.set_source(Song::Source_Tidal);
+ song.set_song_id(song_id);
+ song.set_album_id(album_id);
+ song.set_artist_id(artist_id);
+ if (album_artist != artist) song.set_albumartist(album_artist);
+ song.set_album(album);
+ song.set_artist(artist);
+ song.set_title(title);
+ song.set_track(track);
+ song.set_disc(disc);
+ song.set_url(url);
+ song.set_length_nanosec(duration);
+ song.set_art_automatic(cover_url);
+ song.set_comment(copyright);
+ song.set_directory_id(0);
+ song.set_filetype(Song::FileType_Stream);
+ song.set_filesize(0);
+ song.set_mtime(0);
+ song.set_ctime(0);
+ song.set_valid(true);
+
+ return song_id;
+
+}
+
+void TidalRequest::GetAlbumCovers() {
+
+ for (Song &song : songs_) {
+ AddAlbumCoverRequest(song);
+ }
+ FlushAlbumCoverRequests();
+
+ if (album_covers_requested_ == 1) emit UpdateStatus(query_id_, tr("Retrieving album cover for %1 album...").arg(album_covers_requested_));
+ else emit UpdateStatus(query_id_, tr("Retrieving album covers for %1 albums...").arg(album_covers_requested_));
+ emit ProgressSetMaximum(query_id_, album_covers_requested_);
+ emit UpdateProgress(query_id_, 0);
+
+}
+
+void TidalRequest::AddAlbumCoverRequest(Song &song) {
+
+ if (album_covers_requests_sent_.contains(song.album_id())) {
+ album_covers_requests_sent_.insertMulti(song.album_id(), &song);
+ return;
+ }
+
+ AlbumCoverRequest request;
+ request.album_id = song.album_id();
+ request.url = QUrl(song.art_automatic());
+ request.filename = app_->album_cover_loader()->CoverFilePath(song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), request.url);
+ if (request.filename.isEmpty()) return;
+
+ album_covers_requests_sent_.insertMulti(song.album_id(), &song);
+ ++album_covers_requested_;
+
+ album_cover_requests_queue_.enqueue(request);
+
+}
+
+void TidalRequest::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);
+ req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+ QNetworkReply *reply = network_->get(req);
+ album_cover_replies_ << reply;
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumCoverReceived(QNetworkReply*, QString, QUrl, QString)), reply, request.album_id, request.url, request.filename);
+
+ }
+
+}
+
+void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &album_id, const QUrl &url, const QString &filename) {
+
+ if (album_cover_replies_.contains(reply)) {
+ album_cover_replies_.removeAll(reply);
+ reply->deleteLater();
+ }
+ else {
+ AlbumCoverFinishCheck();
+ return;
+ }
+
+ --album_covers_requests_active_;
+ ++album_covers_received_;
+
+ if (finished_) return;
+
+ emit UpdateProgress(query_id_, album_covers_received_);
+
+ if (!album_covers_requests_sent_.contains(album_id)) {
+ AlbumCoverFinishCheck();
+ return;
+ }
+
+ if (reply->error() != QNetworkReply::NoError) {
+ Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
+ album_covers_requests_sent_.remove(album_id);
+ AlbumCoverFinishCheck();
+ return;
+ }
+
+ QByteArray data = reply->readAll();
+ if (data.isEmpty()) {
+ Error(QString("Received empty image data for %1").arg(url.toString()));
+ album_covers_requests_sent_.remove(album_id);
+ AlbumCoverFinishCheck();
+ return;
+ }
+
+ QImage image;
+ if (image.loadFromData(data)) {
+
+ if (image.save(filename, "JPG")) {
+ while (album_covers_requests_sent_.contains(album_id)) {
+ Song *song = album_covers_requests_sent_.take(album_id);
+ song->set_art_automatic(QUrl::fromLocalFile(filename));
+ }
+ }
+
+ }
+ else {
+ album_covers_requests_sent_.remove(album_id);
+ Error(QString("Error decoding image data from %1").arg(url.toString()));
+ }
+
+ AlbumCoverFinishCheck();
+
+}
+
+void TidalRequest::AlbumCoverFinishCheck() {
+
+ if (!album_cover_requests_queue_.isEmpty() && album_covers_requests_active_ < kMaxConcurrentAlbumCoverRequests)
+ FlushAlbumCoverRequests();
+
+ FinishCheck();
+
+}
+
+void TidalRequest::FinishCheck() {
+
+ if (
+ !finished_ &&
+ !need_login_ &&
+ albums_requests_queue_.isEmpty() &&
+ artists_requests_queue_.isEmpty() &&
+ songs_requests_queue_.isEmpty() &&
+ artist_albums_requests_queue_.isEmpty() &&
+ album_songs_requests_queue_.isEmpty() &&
+ album_cover_requests_queue_.isEmpty() &&
+ artist_albums_requests_pending_.isEmpty() &&
+ album_songs_requests_pending_.isEmpty() &&
+ album_covers_requests_sent_.isEmpty() &&
+ artists_requests_active_ <= 0 &&
+ albums_requests_active_ <= 0 &&
+ songs_requests_active_ <= 0 &&
+ artist_albums_requests_active_ <= 0 &&
+ artist_albums_received_ >= artist_albums_requested_ &&
+ album_songs_requests_active_ <= 0 &&
+ album_songs_received_ >= album_songs_requested_ &&
+ album_covers_requested_ <= album_covers_received_ &&
+ album_covers_requests_active_ <= 0 &&
+ album_covers_received_ >= album_covers_requested_
+ ) {
+ finished_ = true;
+ if (no_results_ && songs_.isEmpty()) {
+ if (IsSearch())
+ emit Results(query_id_, SongList(), tr("No match."));
+ else
+ emit Results(query_id_, SongList(), QString());
+ }
+ else {
+ if (songs_.isEmpty() && errors_.isEmpty())
+ emit Results(query_id_, songs_, tr("Unknown error"));
+ else
+ emit Results(query_id_, songs_, ErrorsToHTML(errors_));
+ }
+ }
+
+}
+
+void TidalRequest::Error(const QString &error, const QVariant &debug) {
+
+ if (!error.isEmpty()) {
+ errors_ << error;
+ qLog(Error) << "Tidal:" << error;
+ }
+
+ if (debug.isValid()) qLog(Debug) << debug;
+
+ FinishCheck();
+
+}
+
+void TidalRequest::Warn(QString error, QVariant debug) {
+
+ qLog(Error) << "Tidal:" << error;
+ if (debug.isValid()) qLog(Debug) << debug;
+
+}
+
diff --git a/src/tidal/tidalrequest.h b/src/tidal/tidalrequest.h
new file mode 100644
index 000000000..320cf73c8
--- /dev/null
+++ b/src/tidal/tidalrequest.h
@@ -0,0 +1,212 @@
+/*
+ * 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 TIDALREQUEST_H
+#define TIDALREQUEST_H
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/song.h"
+#include "tidalbaserequest.h"
+
+class QNetworkReply;
+class Application;
+class NetworkAccessManager;
+class TidalService;
+class TidalUrlHandler;
+
+class TidalRequest : public TidalBaseRequest {
+ Q_OBJECT
+
+ public:
+
+ TidalRequest(TidalService *service, TidalUrlHandler *url_handler, Application *app, NetworkAccessManager *network, QueryType type, QObject *parent);
+ ~TidalRequest();
+
+ void ReloadSettings();
+
+ void Process();
+ void NeedLogin() { need_login_ = true; }
+ void Search(const int search_id, const QString &search_text);
+
+ signals:
+ void Login();
+ void Login(const QString &username, const QString &password, const QString &token);
+ void LoginSuccess();
+ void LoginFailure(QString failure_reason);
+ void Results(const int id, const SongList &songs, const QString &error);
+ void UpdateStatus(const int id, const QString &text);
+ void ProgressSetMaximum(const int id, const int max);
+ void UpdateProgress(const int id, const int max);
+ void StreamURLFinished(const QUrl original_url, const QUrl url, const Song::FileType, QString error = QString());
+
+ private slots:
+ void LoginComplete(const 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 QString &artist_id_requested, const int limit_requested, const int offset_requested, const bool auto_login);
+
+ void SongsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested);
+ void SongsReceived(QNetworkReply *reply, const QString &artist_id, const QString &album_id, const int limit_requested, const int offset_requested, const bool auto_login = false, const QString &album_artist = QString());
+
+ void ArtistAlbumsReplyReceived(QNetworkReply *reply, const QString &artist_id, const int offset_requested);
+ void AlbumSongsReplyReceived(QNetworkReply *reply, const QString &artist_id, const QString &album_id, const int offset_requested, const QString &album_artist);
+ void AlbumCoverReceived(QNetworkReply *reply, const QString &album_id, const QUrl &url, const QString &filename);
+
+ private:
+ typedef QPair Param;
+ typedef QList ParamList;
+
+ struct Request {
+ QString artist_id = 0;
+ QString album_id = 0;
+ QString song_id = 0;
+ int offset = 0;
+ int limit = 0;
+ QString album_artist;
+ };
+ struct AlbumCoverRequest {
+ qint64 artist_id = 0;
+ QString album_id = 0;
+ QUrl url;
+ QString filename;
+ };
+
+ bool IsQuery() { return (type_ == QueryType_Artists || type_ == QueryType_Albums || type_ == QueryType_Songs); }
+ bool IsSearch() { return (type_ == QueryType_SearchArtists || type_ == QueryType_SearchAlbums || type_ == QueryType_SearchSongs); }
+
+ void GetArtists();
+ void GetAlbums();
+ void GetSongs();
+
+ void ArtistsSearch();
+ void AlbumsSearch();
+ void SongsSearch();
+
+ void AddArtistsRequest(const int offset = 0, const int limit = 0);
+ void AddArtistsSearchRequest(const int offset = 0);
+ void FlushArtistsRequests();
+ void AddAlbumsRequest(const int offset = 0, const int limit = 0);
+ void AddAlbumsSearchRequest(const int offset = 0);
+ void FlushAlbumsRequests();
+ void AddSongsRequest(const int offset = 0, const int limit = 0);
+ void AddSongsSearchRequest(const int offset = 0);
+ void FlushSongsRequests();
+
+ void ArtistsFinishCheck(const int limit = 0, const int offset = 0, const int artists_received = 0);
+ void AlbumsFinishCheck(const QString &artist_id, const int limit = 0, const int offset = 0, const int albums_total = 0, const int albums_received = 0);
+ void SongsFinishCheck(const QString &artist_id, const QString &album_id, const int limit, const int offset, const int songs_total, const int songs_received, const QString &album_artist);
+
+ void AddArtistAlbumsRequest(const QString &artist_id, const int offset = 0);
+ void FlushArtistAlbumsRequests();
+
+ void AddAlbumSongsRequest(const QString &artist_id, const QString &album_id, const QString &album_artist, const int offset = 0);
+ void FlushAlbumSongsRequests();
+
+ QString ParseSong(Song &song, const QJsonObject &json_obj, const QString &artist_id_requested = QString(), const QString &album_id_requested = QString(), const QString &album_artist = QString());
+
+ void GetAlbumCovers();
+ void AddAlbumCoverRequest(Song &song);
+ void FlushAlbumCoverRequests();
+ void AlbumCoverFinishCheck();
+
+ void FinishCheck();
+ void Warn(QString error, QVariant debug = QVariant());
+ void Error(const QString &error, const QVariant &debug = QVariant());
+
+ static const char *kResourcesUrl;
+ static const int kMaxConcurrentArtistsRequests;
+ static const int kMaxConcurrentAlbumsRequests;
+ static const int kMaxConcurrentSongsRequests;
+ static const int kMaxConcurrentArtistAlbumsRequests;
+ static const int kMaxConcurrentAlbumSongsRequests;
+ static const int kMaxConcurrentAlbumCoverRequests;
+
+ TidalService *service_;
+ TidalUrlHandler *url_handler_;
+ Application *app_;
+ NetworkAccessManager *network_;
+
+ QueryType type_;
+ bool fetchalbums_;
+ QString coversize_;
+
+ int query_id_;
+ QString search_text_;
+
+ bool finished_;
+
+ QQueue artists_requests_queue_;
+ QQueue albums_requests_queue_;
+ QQueue songs_requests_queue_;
+
+ QQueue artist_albums_requests_queue_;
+ QQueue album_songs_requests_queue_;
+ QQueue album_cover_requests_queue_;
+
+ QList artist_albums_requests_pending_;
+ QHash album_songs_requests_pending_;
+ QMultiMap album_covers_requests_sent_;
+
+ int artists_requests_active_;
+ int artists_total_;
+ int artists_received_;
+
+ int albums_requests_active_;
+ int songs_requests_active_;
+
+ int artist_albums_requests_active_;
+ int artist_albums_requested_;
+ int artist_albums_received_;
+
+ int album_songs_requests_active_;
+ int album_songs_requested_;
+ int album_songs_received_;
+
+ int album_covers_requests_active_;
+ int album_covers_requested_;
+ int album_covers_received_;
+
+ SongList songs_;
+ QStringList errors_;
+ bool need_login_;
+ bool no_results_;
+ QList replies_;
+ QList album_cover_replies_;
+
+};
+
+#endif // TIDALREQUEST_H
diff --git a/src/tidal/tidalservice.cpp b/src/tidal/tidalservice.cpp
new file mode 100644
index 000000000..86fba2fdf
--- /dev/null
+++ b/src/tidal/tidalservice.cpp
@@ -0,0 +1,966 @@
+/*
+ * 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 "config.h"
+
+#include
+
+#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/internetsearchview.h"
+#include "collection/collectionbackend.h"
+#include "collection/collectionmodel.h"
+#include "tidalservice.h"
+#include "tidalurlhandler.h"
+#include "tidalbaserequest.h"
+#include "tidalrequest.h"
+#include "tidalfavoriterequest.h"
+#include "tidalstreamurlrequest.h"
+#include "settings/settingsdialog.h"
+#include "settings/tidalsettingspage.h"
+
+const Song::Source TidalService::kSource = Song::Source_Tidal;
+const char *TidalService::kOAuthUrl = "https://login.tidal.com/authorize";
+const char *TidalService::kOAuthAccessTokenUrl = "https://login.tidal.com/oauth2/token";
+const char *TidalService::kOAuthRedirectUrl = "tidal://login/auth";
+const char *TidalService::kAuthUrl = "https://api.tidalhifi.com/v1/login/username";
+const int TidalService::kLoginAttempts = 2;
+const int TidalService::kTimeResetLoginAttempts = 60000;
+
+const char *TidalService::kArtistsSongsTable = "tidal_artists_songs";
+const char *TidalService::kAlbumsSongsTable = "tidal_albums_songs";
+const char *TidalService::kSongsTable = "tidal_songs";
+
+const char *TidalService::kArtistsSongsFtsTable = "tidal_artists_songs_fts";
+const char *TidalService::kAlbumsSongsFtsTable = "tidal_albums_songs_fts";
+const char *TidalService::kSongsFtsTable = "tidal_songs_fts";
+
+TidalService::TidalService(Application *app, QObject *parent)
+ : InternetService(Song::Source_Tidal, "Tidal", "tidal", TidalSettingsPage::kSettingsGroup, SettingsDialog::Page_Tidal, app, parent),
+ app_(app),
+ network_(new NetworkAccessManager(this)),
+ url_handler_(new TidalUrlHandler(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 TidalFavoriteRequest(this, network_, this)),
+ user_id_(0),
+ search_delay_(1500),
+ artistssearchlimit_(1),
+ albumssearchlimit_(1),
+ songssearchlimit_(1),
+ fetchalbums_(true),
+ download_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(), Song::Source_Tidal, kArtistsSongsTable, QString(), QString(), kArtistsSongsFtsTable);
+
+ albums_collection_backend_ = new CollectionBackend();
+ albums_collection_backend_->moveToThread(app_->database()->thread());
+ albums_collection_backend_->Init(app_->database(), Song::Source_Tidal, kAlbumsSongsTable, QString(), QString(), kAlbumsSongsFtsTable);
+
+ songs_collection_backend_ = new CollectionBackend();
+ songs_collection_backend_->moveToThread(app_->database()->thread());
+ songs_collection_backend_->Init(app_->database(), Song::Source_Tidal, kSongsTable, QString(), QString(), kSongsFtsTable);
+
+ artists_collection_model_ = new CollectionModel(artists_collection_backend_, app_, this);
+ albums_collection_model_ = new CollectionModel(albums_collection_backend_, app_, this);
+ songs_collection_model_ = new CollectionModel(songs_collection_backend_, app_, this);
+
+ artists_collection_sort_model_->setSourceModel(artists_collection_model_);
+ artists_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
+ artists_collection_sort_model_->setDynamicSortFilter(true);
+ artists_collection_sort_model_->setSortLocaleAware(true);
+ artists_collection_sort_model_->sort(0);
+
+ albums_collection_sort_model_->setSourceModel(albums_collection_model_);
+ albums_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
+ albums_collection_sort_model_->setDynamicSortFilter(true);
+ albums_collection_sort_model_->setSortLocaleAware(true);
+ albums_collection_sort_model_->sort(0);
+
+ songs_collection_sort_model_->setSourceModel(songs_collection_model_);
+ songs_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
+ songs_collection_sort_model_->setDynamicSortFilter(true);
+ songs_collection_sort_model_->setSortLocaleAware(true);
+ songs_collection_sort_model_->sort(0);
+
+ // Search
+
+ timer_search_delay_->setSingleShot(true);
+ connect(timer_search_delay_, SIGNAL(timeout()), SLOT(StartSearch()));
+
+ timer_login_attempt_->setSingleShot(true);
+ connect(timer_login_attempt_, SIGNAL(timeout()), SLOT(ResetLoginAttempts()));
+
+ connect(this, SIGNAL(Login()), SLOT(SendLogin()));
+ connect(this, SIGNAL(Login(QString, QString, QString)), SLOT(SendLogin(QString, QString, QString)));
+
+ connect(this, SIGNAL(AddArtists(SongList)), favorite_request_, SLOT(AddArtists(SongList)));
+ connect(this, SIGNAL(AddAlbums(SongList)), favorite_request_, SLOT(AddAlbums(SongList)));
+ connect(this, SIGNAL(AddSongs(SongList)), favorite_request_, SLOT(AddSongs(SongList)));
+
+ connect(this, SIGNAL(RemoveArtists(SongList)), favorite_request_, SLOT(RemoveArtists(SongList)));
+ connect(this, SIGNAL(RemoveAlbums(SongList)), favorite_request_, SLOT(RemoveAlbums(SongList)));
+ connect(this, SIGNAL(RemoveSongs(SongList)), favorite_request_, SLOT(RemoveSongs(SongList)));
+
+ connect(favorite_request_, SIGNAL(ArtistsAdded(SongList)), artists_collection_backend_, SLOT(AddOrUpdateSongs(SongList)));
+ connect(favorite_request_, SIGNAL(AlbumsAdded(SongList)), albums_collection_backend_, SLOT(AddOrUpdateSongs(SongList)));
+ connect(favorite_request_, SIGNAL(SongsAdded(SongList)), songs_collection_backend_, SLOT(AddOrUpdateSongs(SongList)));
+
+ connect(favorite_request_, SIGNAL(ArtistsRemoved(SongList)), artists_collection_backend_, SLOT(DeleteSongs(SongList)));
+ connect(favorite_request_, SIGNAL(AlbumsRemoved(SongList)), albums_collection_backend_, SLOT(DeleteSongs(SongList)));
+ connect(favorite_request_, SIGNAL(SongsRemoved(SongList)), songs_collection_backend_, SLOT(DeleteSongs(SongList)));
+
+ ReloadSettings();
+
+}
+
+TidalService::~TidalService() {
+
+ while (!stream_url_requests_.isEmpty()) {
+ TidalStreamURLRequest *stream_url_req = stream_url_requests_.takeFirst();
+ disconnect(stream_url_req, 0, this, 0);
+ stream_url_req->deleteLater();
+ }
+
+ artists_collection_backend_->deleteLater();
+ albums_collection_backend_->deleteLater();
+ songs_collection_backend_->deleteLater();
+
+}
+
+void TidalService::Exit() {
+
+ wait_for_exit_ << artists_collection_backend_ << albums_collection_backend_ << songs_collection_backend_;
+
+ connect(artists_collection_backend_, SIGNAL(ExitFinished()), this, SLOT(ExitReceived()));
+ connect(albums_collection_backend_, SIGNAL(ExitFinished()), this, SLOT(ExitReceived()));
+ connect(songs_collection_backend_, SIGNAL(ExitFinished()), this, SLOT(ExitReceived()));
+
+ artists_collection_backend_->ExitAsync();
+ albums_collection_backend_->ExitAsync();
+ songs_collection_backend_->ExitAsync();
+
+}
+
+void TidalService::ExitReceived() {
+
+ QObject *obj = static_cast(sender());
+ disconnect(obj, 0, this, 0);
+ qLog(Debug) << obj << "successfully exited.";
+ wait_for_exit_.removeAll(obj);
+ if (wait_for_exit_.isEmpty()) emit ExitFinished();
+
+}
+
+void TidalService::ShowConfig() {
+ app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Tidal);
+}
+
+void TidalService::ReloadSettings() {
+
+ QSettings s;
+ s.beginGroup(TidalSettingsPage::kSettingsGroup);
+
+ oauth_ = s.value("oauth", false).toBool();
+ client_id_ = s.value("client_id").toString();
+ api_token_ = s.value("api_token").toString();
+
+ username_ = s.value("username").toString();
+ QByteArray password = s.value("password").toByteArray();
+ if (password.isEmpty()) password_.clear();
+ else password_ = QString::fromUtf8(QByteArray::fromBase64(password));
+
+ quality_ = s.value("quality", "LOSSLESS").toString();
+ search_delay_ = s.value("searchdelay", 1500).toInt();
+ artistssearchlimit_ = s.value("artistssearchlimit", 4).toInt();
+ albumssearchlimit_ = s.value("albumssearchlimit", 10).toInt();
+ songssearchlimit_ = s.value("songssearchlimit", 10).toInt();
+ fetchalbums_ = s.value("fetchalbums", false).toBool();
+ coversize_ = s.value("coversize", "320x320").toString();
+ download_album_covers_ = s.value("downloadalbumcovers", true).toBool();
+ stream_url_method_ = static_cast(s.value("streamurl").toInt());
+
+ user_id_ = s.value("user_id").toInt();
+ country_code_ = s.value("country_code", "US").toString();
+ access_token_ = s.value("access_token").toString();
+ refresh_token_ = s.value("refresh_token").toString();
+ session_id_ = s.value("session_id").toString();
+ expiry_time_ = s.value("expiry_time").toDateTime();
+
+ s.endGroup();
+
+}
+
+void TidalService::StartAuthorisation() {
+
+ login_sent_ = true;
+ ++login_attempts_;
+ if (timer_login_attempt_->isActive()) timer_login_attempt_->stop();
+ timer_login_attempt_->setInterval(kTimeResetLoginAttempts);
+ timer_login_attempt_->start();
+
+ code_verifier_ = Utilities::CryptographicRandomString(44);
+ code_challenge_ = QString(QCryptographicHash::hash(code_verifier_.toUtf8(), QCryptographicHash::Sha256).toBase64(QByteArray::Base64UrlEncoding));
+
+ if (code_challenge_.lastIndexOf(QChar('=')) == code_challenge_.length() - 1) {
+ code_challenge_.chop(1);
+ }
+
+ const ParamList params = ParamList() << Param("response_type", "code")
+ << Param("code_challenge", code_challenge_)
+ << Param("code_challenge_method", "S256")
+ << Param("redirect_uri", kOAuthRedirectUrl)
+ << Param("client_id", client_id_)
+ << Param("scope", "r_usr w_usr");
+
+ 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 = QUrl(kOAuthUrl);
+ url.setQuery(url_query);
+ QDesktopServices::openUrl(url);
+
+}
+
+void TidalService::AuthorisationUrlReceived(const QUrl &url) {
+
+ qLog(Debug) << "Tidal: Authorisation URL Received" << url;
+
+ QUrlQuery url_query(url);
+
+ if (url_query.hasQueryItem("token_type") && url_query.hasQueryItem("expires_in") && url_query.hasQueryItem("access_token")) {
+
+ access_token_ = url_query.queryItemValue("access_token").toUtf8();
+ int expires_in = url_query.queryItemValue("expires_in").toInt();
+ expiry_time_ = QDateTime::currentDateTime().addSecs(expires_in - 120);
+ session_id_.clear();
+
+ QSettings s;
+ s.beginGroup(TidalSettingsPage::kSettingsGroup);
+ s.setValue("access_token", access_token_);
+ s.setValue("expiry_time", expiry_time_);
+ s.remove("refresh_token");
+ s.remove("session_id");
+ s.endGroup();
+
+ login_attempts_ = 0;
+ if (timer_login_attempt_->isActive()) timer_login_attempt_->stop();
+
+ emit LoginComplete(true);
+ emit LoginSuccess();
+ }
+
+ else if (url_query.hasQueryItem("code") && url_query.hasQueryItem("state")) {
+
+ QString code = url_query.queryItemValue("code");
+ QString state = url_query.queryItemValue("state");
+
+ const ParamList params = ParamList() << Param("code", code)
+ << Param("client_id", client_id_)
+ << Param("grant_type", "authorization_code")
+ << Param("redirect_uri", kOAuthRedirectUrl)
+ << Param("scope", "r_usr w_usr")
+ << Param("code_verifier", code_verifier_);
+
+ 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(kOAuthAccessTokenUrl);
+ QNetworkRequest request = QNetworkRequest(url);
+ QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
+
+ login_errors_.clear();
+ QNetworkReply *reply = network_->post(request, query);
+ connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleLoginSSLErrors(QList)));
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(AccessTokenRequestFinished(QNetworkReply*)), reply);
+
+ }
+
+ else {
+
+ LoginError(tr("Reply from Tidal is missing query items."));
+ return;
+ }
+
+}
+
+void TidalService::HandleLoginSSLErrors(QList ssl_errors) {
+
+ for (QSslError &ssl_error : ssl_errors) {
+ login_errors_ += ssl_error.errorString();
+ }
+
+}
+
+void TidalService::AccessTokenRequestFinished(QNetworkReply *reply) {
+
+ reply->deleteLater();
+
+ login_sent_ = false;
+
+ if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
+ if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
+ // This is a network error, there is nothing more to do.
+ LoginError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
+ return;
+ }
+ else {
+ // See if there is Json data containing "status" and "userMessage" then use that instead.
+ QByteArray data(reply->readAll());
+ QJsonParseError json_error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
+ if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
+ QJsonObject json_obj = json_doc.object();
+ if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("userMessage")) {
+ int status = json_obj["status"].toInt();
+ int sub_status = json_obj["subStatus"].toInt();
+ QString user_message = json_obj["userMessage"].toString();
+ login_errors_ << QString("Authentication failure: %1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status);
+ }
+ }
+ if (login_errors_.isEmpty()) {
+ if (reply->error() != QNetworkReply::NoError) {
+ login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
+ }
+ else {
+ login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
+ }
+ }
+ LoginError();
+ return;
+ }
+ }
+
+ 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("access_token") ||
+ !json_obj.contains("refresh_token") ||
+ !json_obj.contains("expires_in") ||
+ !json_obj.contains("user")
+ ) {
+ LoginError("Authentication reply from server is missing access_token, refresh_token, expires_in or user", json_obj);
+ return;
+ }
+
+ access_token_ = json_obj["access_token"].toString();
+ refresh_token_ = json_obj["refresh_token"].toString();
+ int expires_in = json_obj["expires_in"].toInt();
+ expiry_time_ = QDateTime::currentDateTime().addSecs(expires_in - 120);
+
+ QJsonValue json_user = json_obj["user"];
+ if (!json_user.isObject()) {
+ LoginError("Authentication reply from server has Json user that is not an object.", json_doc);
+ return;
+ }
+ QJsonObject json_obj_user = json_user.toObject();
+ if (json_obj_user.isEmpty()) {
+ LoginError("Authentication reply from server has empty Json user object.", json_doc);
+ return;
+ }
+
+ country_code_ = json_obj_user["countryCode"].toString();
+ user_id_ = json_obj_user["userId"].toInt();
+ session_id_.clear();
+
+ QSettings s;
+ s.beginGroup(TidalSettingsPage::kSettingsGroup);
+ s.setValue("access_token", access_token_);
+ s.setValue("refresh_token", refresh_token_);
+ s.setValue("expiry_time", expiry_time_);
+ s.setValue("country_code", country_code_);
+ s.setValue("user_id", user_id_);
+ s.remove("session_id");
+ s.endGroup();
+
+ qLog(Debug) << "Tidal: Login successful" << "user id" << user_id_ << "access token" << access_token_;
+
+ login_attempts_ = 0;
+ if (timer_login_attempt_->isActive()) timer_login_attempt_->stop();
+
+ emit LoginComplete(true);
+ emit LoginSuccess();
+
+}
+
+void TidalService::SendLogin() {
+ SendLogin(api_token_, username_, password_);
+}
+
+void TidalService::SendLogin(const QString &api_token, const QString &username, const QString &password) {
+
+ 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("token", (api_token.isEmpty() ? api_token_ : api_token))
+ << Param("username", username)
+ << Param("password", password)
+ << Param("clientVersion", "2.2.1--7");
+
+ 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");
+ req.setRawHeader("X-Tidal-Token", (api_token.isEmpty() ? api_token_.toUtf8() : api_token.toUtf8()));
+
+ QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
+ QNetworkReply *reply = network_->post(req, query);
+ connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleLoginSSLErrors(QList)));
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleAuthReply(QNetworkReply*)), reply);
+
+ //qLog(Debug) << "Tidal: Sending request" << url << query;
+
+}
+
+void TidalService::HandleAuthReply(QNetworkReply *reply) {
+
+ reply->deleteLater();
+
+ login_sent_ = false;
+
+ if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
+ if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
+ // This is a network error, there is nothing more to do.
+ LoginError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
+ login_errors_.clear();
+ return;
+ }
+ else {
+ // See if there is Json data containing "status" and "userMessage" - then use that instead.
+ QByteArray data(reply->readAll());
+ QJsonParseError json_error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
+ if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
+ QJsonObject json_obj = json_doc.object();
+ if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("userMessage")) {
+ int status = json_obj["status"].toInt();
+ int sub_status = json_obj["subStatus"].toInt();
+ QString user_message = json_obj["userMessage"].toString();
+ login_errors_ << QString("Authentication failure: %1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status);
+ }
+ }
+ if (login_errors_.isEmpty()) {
+ if (reply->error() != QNetworkReply::NoError) {
+ login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
+ }
+ else {
+ login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
+ }
+ }
+ LoginError();
+ login_errors_.clear();
+ return;
+ }
+ }
+
+ login_errors_.clear();
+
+ QByteArray data(reply->readAll());
+ QJsonParseError json_error;
+ QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
+
+ if (json_error.error != QJsonParseError::NoError) {
+ LoginError("Authentication reply from server missing Json data.");
+ return;
+ }
+
+ if (json_doc.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("userId") || !json_obj.contains("sessionId") || !json_obj.contains("countryCode") ) {
+ LoginError("Authentication reply from server is missing userId, sessionId or countryCode", json_obj);
+ return;
+ }
+
+ country_code_ = json_obj["countryCode"].toString();
+ session_id_ = json_obj["sessionId"].toString();
+ user_id_ = json_obj["userId"].toInt();
+ access_token_.clear();
+ refresh_token_.clear();
+ expiry_time_ = QDateTime();
+
+ QSettings s;
+ s.beginGroup(TidalSettingsPage::kSettingsGroup);
+ s.remove("access_token");
+ s.remove("refresh_token");
+ s.remove("expiry_time");
+ s.setValue("user_id", user_id_);
+ s.setValue("session_id", session_id_);
+ s.setValue("country_code", country_code_);
+ s.endGroup();
+
+ qLog(Debug) << "Tidal: Login successful" << "user id" << user_id_ << "session id" << session_id_ << "country code" << country_code_;
+
+ login_attempts_ = 0;
+ if (timer_login_attempt_->isActive()) timer_login_attempt_->stop();
+
+ emit LoginComplete(true);
+ emit LoginSuccess();
+
+}
+
+void TidalService::Logout() {
+
+ access_token_.clear();
+ session_id_.clear();
+ expiry_time_ = QDateTime();
+
+ QSettings s;
+ s.beginGroup(TidalSettingsPage::kSettingsGroup);
+ s.remove("user_id");
+ s.remove("country_code");
+ s.remove("access_token");
+ s.remove("session_id");
+ s.remove("expiry_time");
+ s.endGroup();
+
+}
+
+void TidalService::ResetLoginAttempts() {
+ login_attempts_ = 0;
+}
+
+void TidalService::TryLogin() {
+
+ if (authenticated() || login_sent_) return;
+
+ if (api_token_.isEmpty()) {
+ emit LoginComplete(false, tr("Missing Tidal API token."));
+ return;
+ }
+ if (username_.isEmpty()) {
+ emit LoginComplete(false, tr("Missing Tidal username."));
+ return;
+ }
+ if (password_.isEmpty()) {
+ emit LoginComplete(false, tr("Missing Tidal password."));
+ return;
+ }
+ if (login_attempts_ >= kLoginAttempts) {
+ emit LoginComplete(false, tr("Not authenticated with Tidal and reached maximum number of login attempts."));
+ return;
+ }
+
+ emit Login();
+
+}
+
+void TidalService::ResetArtistsRequest() {
+
+ if (artists_request_.get()) {
+ disconnect(artists_request_.get(), 0, this, 0);
+ disconnect(this, 0, artists_request_.get(), 0);
+ artists_request_.reset();
+ }
+
+}
+
+void TidalService::GetArtists() {
+
+ if (!authenticated()) {
+ if (oauth_) {
+ emit ArtistsResults(SongList(), tr("Not authenticated with Tidal."));
+ ShowConfig();
+ return;
+ }
+ else if (api_token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) {
+ emit ArtistsResults(SongList(), tr("Missing Tidal API token, username or password."));
+ ShowConfig();
+ return;
+ }
+ }
+
+ ResetArtistsRequest();
+
+ artists_request_.reset(new TidalRequest(this, url_handler_, app_, network_, TidalBaseRequest::QueryType_Artists, this));
+
+ connect(artists_request_.get(), SIGNAL(Results(int, SongList, QString)), SLOT(ArtistsResultsReceived(int, SongList, QString)));
+ connect(artists_request_.get(), SIGNAL(UpdateStatus(int, QString)), SLOT(ArtistsUpdateStatusReceived(int, QString)));
+ connect(artists_request_.get(), SIGNAL(ProgressSetMaximum(int, int)), SLOT(ArtistsProgressSetMaximumReceived(int, int)));
+ connect(artists_request_.get(), SIGNAL(UpdateProgress(int, int)), SLOT(ArtistsUpdateProgressReceived(int, int)));
+ connect(this, SIGNAL(LoginComplete(bool, QString)), artists_request_.get(), SLOT(LoginComplete(bool, QString)));
+
+ artists_request_->Process();
+
+}
+
+void TidalService::ArtistsResultsReceived(const int id, const SongList &songs, const QString &error) {
+ Q_UNUSED(id);
+ emit ArtistsResults(songs, error);
+}
+
+void TidalService::ArtistsUpdateStatusReceived(const int id, const QString &text) {
+ Q_UNUSED(id);
+ emit ArtistsUpdateStatus(text);
+}
+
+void TidalService::ArtistsProgressSetMaximumReceived(const int id, const int max) {
+ Q_UNUSED(id);
+ emit ArtistsProgressSetMaximum(max);
+}
+
+void TidalService::ArtistsUpdateProgressReceived(const int id, const int progress) {
+ Q_UNUSED(id);
+ emit ArtistsUpdateProgress(progress);
+}
+
+void TidalService::ResetAlbumsRequest() {
+
+ if (albums_request_.get()) {
+ disconnect(albums_request_.get(), 0, this, 0);
+ disconnect(this, 0, albums_request_.get(), 0);
+ albums_request_.reset();
+ }
+
+}
+
+void TidalService::GetAlbums() {
+
+ if (!authenticated()) {
+ if (oauth_) {
+ emit AlbumsResults(SongList(), tr("Not authenticated with Tidal."));
+ ShowConfig();
+ return;
+ }
+ else if (api_token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) {
+ emit AlbumsResults(SongList(), tr("Missing Tidal API token, username or password."));
+ ShowConfig();
+ return;
+ }
+ }
+
+ ResetAlbumsRequest();
+ albums_request_.reset(new TidalRequest(this, url_handler_, app_, network_, TidalBaseRequest::QueryType_Albums, this));
+ connect(albums_request_.get(), SIGNAL(Results(int, SongList, QString)), SLOT(AlbumsResultsReceived(int, SongList, QString)));
+ connect(albums_request_.get(), SIGNAL(UpdateStatus(int, QString)), SLOT(AlbumsUpdateStatusReceived(int, QString)));
+ connect(albums_request_.get(), SIGNAL(ProgressSetMaximum(int, int)), SLOT(AlbumsProgressSetMaximumReceived(int, int)));
+ connect(albums_request_.get(), SIGNAL(UpdateProgress(int, int)), SLOT(AlbumsUpdateProgressReceived(int, int)));
+ connect(this, SIGNAL(LoginComplete(bool, QString)), albums_request_.get(), SLOT(LoginComplete(bool, QString)));
+
+ albums_request_->Process();
+
+}
+
+void TidalService::AlbumsResultsReceived(const int id, const SongList &songs, const QString &error) {
+ Q_UNUSED(id);
+ emit AlbumsResults(songs, error);
+}
+
+void TidalService::AlbumsUpdateStatusReceived(const int id, const QString &text) {
+ Q_UNUSED(id);
+ emit AlbumsUpdateStatus(text);
+}
+
+void TidalService::AlbumsProgressSetMaximumReceived(const int id, const int max) {
+ Q_UNUSED(id);
+ emit AlbumsProgressSetMaximum(max);
+}
+
+void TidalService::AlbumsUpdateProgressReceived(const int id, const int progress) {
+ Q_UNUSED(id);
+ emit AlbumsUpdateProgress(progress);
+}
+
+void TidalService::ResetSongsRequest() {
+
+ if (songs_request_.get()) {
+ disconnect(songs_request_.get(), 0, this, 0);
+ disconnect(this, 0, songs_request_.get(), 0);
+ songs_request_.reset();
+ }
+
+}
+
+void TidalService::GetSongs() {
+
+ if (!authenticated()) {
+ if (oauth_) {
+ emit SongsResults(SongList(), tr("Not authenticated with Tidal."));
+ ShowConfig();
+ return;
+ }
+ else if (api_token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) {
+ emit SongsResults(SongList(), tr("Missing Tidal API token, username or password."));
+ ShowConfig();
+ return;
+ }
+ }
+
+ ResetSongsRequest();
+ songs_request_.reset(new TidalRequest(this, url_handler_, app_, network_, TidalBaseRequest::QueryType_Songs, this));
+ connect(songs_request_.get(), SIGNAL(Results(int, SongList, QString)), SLOT(SongsResultsReceived(int, SongList, QString)));
+ connect(songs_request_.get(), SIGNAL(UpdateStatus(int, QString)), SLOT(SongsUpdateStatusReceived(int, QString)));
+ connect(songs_request_.get(), SIGNAL(ProgressSetMaximum(int, int)), SLOT(SongsProgressSetMaximumReceived(int, int)));
+ connect(songs_request_.get(), SIGNAL(UpdateProgress(int, int)), SLOT(SongsUpdateProgressReceived(int, int)));
+ connect(this, SIGNAL(LoginComplete(bool, QString)), songs_request_.get(), SLOT(LoginComplete(bool, QString)));
+
+ songs_request_->Process();
+
+}
+
+void TidalService::SongsResultsReceived(const int id, const SongList &songs, const QString &error) {
+ Q_UNUSED(id);
+ emit SongsResults(songs, error);
+}
+
+void TidalService::SongsUpdateStatusReceived(const int id, const QString &text) {
+ Q_UNUSED(id);
+ emit SongsUpdateStatus(text);
+}
+
+void TidalService::SongsProgressSetMaximumReceived(const int id, const int max) {
+ Q_UNUSED(id);
+ emit SongsProgressSetMaximum(max);
+}
+
+void TidalService::SongsUpdateProgressReceived(const int id, const int progress) {
+ Q_UNUSED(id);
+ emit SongsUpdateProgress(progress);
+}
+
+int TidalService::Search(const QString &text, InternetSearchView::SearchType type) {
+
+ pending_search_id_ = next_pending_search_id_;
+ pending_search_text_ = text;
+ pending_search_type_ = type;
+
+ next_pending_search_id_++;
+
+ if (text.isEmpty()) {
+ timer_search_delay_->stop();
+ return pending_search_id_;
+ }
+ timer_search_delay_->setInterval(search_delay_);
+ timer_search_delay_->start();
+
+ return pending_search_id_;
+
+}
+
+void TidalService::StartSearch() {
+
+ if (!authenticated()) {
+ if (oauth_) {
+ emit SearchResults(pending_search_id_, SongList(), tr("Not authenticated with Tidal."));
+ ShowConfig();
+ return;
+ }
+ else if (api_token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) {
+ emit SearchResults(pending_search_id_, SongList(), tr("Missing Tidal API token, username or password."));
+ ShowConfig();
+ return;
+ }
+ }
+
+ search_id_ = pending_search_id_;
+ search_text_ = pending_search_text_;
+
+ SendSearch();
+
+}
+
+void TidalService::CancelSearch() {
+}
+
+void TidalService::SendSearch() {
+
+ TidalBaseRequest::QueryType type;
+
+ switch (pending_search_type_) {
+ case InternetSearchView::SearchType_Artists:
+ type = TidalBaseRequest::QueryType_SearchArtists;
+ break;
+ case InternetSearchView::SearchType_Albums:
+ type = TidalBaseRequest::QueryType_SearchAlbums;
+ break;
+ case InternetSearchView::SearchType_Songs:
+ type = TidalBaseRequest::QueryType_SearchSongs;
+ break;
+ default:
+ //Error("Invalid search type.");
+ return;
+ }
+
+ search_request_.reset(new TidalRequest(this, url_handler_, app_, network_, type, this));
+
+ connect(search_request_.get(), SIGNAL(Results(int, SongList, QString)), SLOT(SearchResultsReceived(int, SongList, QString)));
+ connect(search_request_.get(), SIGNAL(UpdateStatus(int, QString)), SIGNAL(SearchUpdateStatus(int, QString)));
+ connect(search_request_.get(), SIGNAL(ProgressSetMaximum(int, int)), SIGNAL(SearchProgressSetMaximum(int, int)));
+ connect(search_request_.get(), SIGNAL(UpdateProgress(int, int)), SIGNAL(SearchUpdateProgress(int, int)));
+ connect(this, SIGNAL(LoginComplete(bool, QString)), search_request_.get(), SLOT(LoginComplete(bool, QString)));
+
+ search_request_->Search(search_id_, search_text_);
+ search_request_->Process();
+
+}
+
+void TidalService::SearchResultsReceived(const int id, const SongList &songs, const QString &error) {
+ emit SearchResults(id, songs, error);
+}
+
+void TidalService::GetStreamURL(const QUrl &url) {
+
+ if (!authenticated()) {
+ if (oauth_) {
+ emit StreamURLFinished(url, url, Song::FileType_Stream, -1, -1, -1, tr("Not authenticated with Tidal."));
+ return;
+ }
+ else if (api_token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) {
+ emit StreamURLFinished(url, url, Song::FileType_Stream, -1, -1, -1, tr("Missing Tidal API token, username or password."));
+ return;
+ }
+ }
+
+ TidalStreamURLRequest *stream_url_req = new TidalStreamURLRequest(this, network_, url, this);
+ stream_url_requests_ << stream_url_req;
+
+ connect(stream_url_req, SIGNAL(TryLogin()), this, SLOT(TryLogin()));
+ connect(stream_url_req, SIGNAL(StreamURLFinished(QUrl, QUrl, Song::FileType, int, int, qint64, QString)), this, SLOT(HandleStreamURLFinished(QUrl, QUrl, Song::FileType, int, int, qint64, QString)));
+ connect(this, SIGNAL(LoginComplete(bool, QString)), stream_url_req, SLOT(LoginComplete(bool, QString)));
+
+ stream_url_req->Process();
+
+}
+
+void TidalService::HandleStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error) {
+
+ TidalStreamURLRequest *stream_url_req = qobject_cast(sender());
+ if (!stream_url_req || !stream_url_requests_.contains(stream_url_req)) return;
+ stream_url_req->deleteLater();
+ stream_url_requests_.removeAll(stream_url_req);
+
+ emit StreamURLFinished(original_url, stream_url, filetype, samplerate, bit_depth, duration, error);
+
+}
+
+void TidalService::LoginError(const QString &error, const QVariant &debug) {
+
+ if (!error.isEmpty()) login_errors_ << error;
+
+ QString error_html;
+ for (const QString &error : login_errors_) {
+ qLog(Error) << "Tidal:" << error;
+ error_html += error + "
";
+ }
+ if (debug.isValid()) qLog(Debug) << debug;
+
+ emit LoginFailure(error_html);
+ emit LoginComplete(false, error_html);
+
+ login_errors_.clear();
+
+}
diff --git a/src/tidal/tidalservice.h b/src/tidal/tidalservice.h
new file mode 100644
index 000000000..9d19516e3
--- /dev/null
+++ b/src/tidal/tidalservice.h
@@ -0,0 +1,256 @@
+/*
+ * 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 TIDALSERVICE_H
+#define TIDALSERVICE_H
+
+#include "config.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/song.h"
+#include "internet/internetservice.h"
+#include "internet/internetsearchview.h"
+#include "settings/tidalsettingspage.h"
+
+class QSortFilterProxyModel;
+class QNetworkReply;
+class QTimer;
+
+class Application;
+class NetworkAccessManager;
+class TidalUrlHandler;
+class TidalRequest;
+class TidalFavoriteRequest;
+class TidalStreamURLRequest;
+class CollectionBackend;
+class CollectionModel;
+
+class TidalService : public InternetService {
+ Q_OBJECT
+
+ public:
+ TidalService(Application *app, QObject *parent);
+ ~TidalService();
+
+ static const Song::Source kSource;
+
+ void Exit();
+ void ReloadSettings();
+
+ void Logout();
+ int Search(const QString &query, InternetSearchView::SearchType type);
+ void CancelSearch();
+
+ int max_login_attempts() { return kLoginAttempts; }
+
+ Application *app() { return app_; }
+
+ bool oauth() { return oauth_; }
+ QString client_id() { return client_id_; }
+ QString api_token() { return api_token_; }
+ quint64 user_id() { return user_id_; }
+ QString country_code() { return country_code_; }
+ QString username() { return username_; }
+ QString password() { return password_; }
+ QString quality() { return quality_; }
+ int search_delay() { return search_delay_; }
+ int artistssearchlimit() { return artistssearchlimit_; }
+ int albumssearchlimit() { return albumssearchlimit_; }
+ int songssearchlimit() { return songssearchlimit_; }
+ bool fetchalbums() { return fetchalbums_; }
+ QString coversize() { return coversize_; }
+ bool download_album_covers() { return download_album_covers_; }
+ TidalSettingsPage::StreamUrlMethod stream_url_method() { return stream_url_method_; }
+
+ QString access_token() { return access_token_; }
+ QString session_id() { return session_id_; }
+
+ bool authenticated() { return (!access_token_.isEmpty() || !session_id_.isEmpty()); }
+ bool login_sent() { return login_sent_; }
+ 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 &api_token, const QString &username, const QString &password);
+ void GetArtists();
+ void GetAlbums();
+ void GetSongs();
+ void ResetArtistsRequest();
+ void ResetAlbumsRequest();
+ void ResetSongsRequest();
+
+ private slots:
+ void ExitReceived();
+ void StartAuthorisation();
+ void AuthorisationUrlReceived(const QUrl &url);
+ void HandleLoginSSLErrors(QList ssl_errors);
+ void AccessTokenRequestFinished(QNetworkReply *reply);
+ void SendLogin();
+ void HandleAuthReply(QNetworkReply *reply);
+ void ResetLoginAttempts();
+ void StartSearch();
+ void ArtistsResultsReceived(const int id, const SongList &songs, const QString &error);
+ void AlbumsResultsReceived(const int id, const SongList &songs, const QString &error);
+ void SongsResultsReceived(const int id, const SongList &songs, const QString &error);
+ void SearchResultsReceived(const int id, const SongList &songs, const QString &error);
+ void ArtistsUpdateStatusReceived(const int id, const QString &text);
+ void AlbumsUpdateStatusReceived(const int id, const QString &text);
+ void SongsUpdateStatusReceived(const int id, const QString &text);
+ void ArtistsProgressSetMaximumReceived(const int id, const int max);
+ void AlbumsProgressSetMaximumReceived(const int id, const int max);
+ void SongsProgressSetMaximumReceived(const int id, const int max);
+ void ArtistsUpdateProgressReceived(const int id, const int progress);
+ void AlbumsUpdateProgressReceived(const int id, const int progress);
+ void SongsUpdateProgressReceived(const int id, const int progress);
+ void HandleStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error = QString());
+
+ private:
+ typedef QPair Param;
+ typedef QList ParamList;
+
+ typedef QPair EncodedParam;
+ typedef QList EncodedParamList;
+
+ void SendSearch();
+ void LoginError(const QString &error = QString(), const QVariant &debug = QVariant());
+
+ static const char *kOAuthUrl;
+ static const char *kOAuthAccessTokenUrl;
+ static const char *kOAuthRedirectUrl;
+ 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_;
+ TidalUrlHandler *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_;
+ TidalFavoriteRequest *favorite_request_;
+
+ bool oauth_;
+ QString client_id_;
+ QString api_token_;
+ quint64 user_id_;
+ QString country_code_;
+ QString username_;
+ QString password_;
+ QString quality_;
+ int search_delay_;
+ int artistssearchlimit_;
+ int albumssearchlimit_;
+ int songssearchlimit_;
+ bool fetchalbums_;
+ QString coversize_;
+ bool download_album_covers_;
+ TidalSettingsPage::StreamUrlMethod stream_url_method_;
+
+ QString access_token_;
+ QString refresh_token_;
+ QString session_id_;
+ QDateTime expiry_time_;
+
+ int pending_search_id_;
+ int next_pending_search_id_;
+ QString pending_search_text_;
+ InternetSearchView::SearchType pending_search_type_;
+
+ int search_id_;
+ QString search_text_;
+ bool login_sent_;
+ int login_attempts_;
+
+ QString code_verifier_;
+ QString code_challenge_;
+
+ QList stream_url_requests_;
+
+ QStringList login_errors_;
+
+ QList wait_for_exit_;
+
+};
+
+#endif // TIDALSERVICE_H
diff --git a/src/tidal/tidalstreamurlrequest.cpp b/src/tidal/tidalstreamurlrequest.cpp
new file mode 100644
index 000000000..45cbca5df
--- /dev/null
+++ b/src/tidal/tidalstreamurlrequest.cpp
@@ -0,0 +1,299 @@
+/*
+ * 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 "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/logging.h"
+#include "core/network.h"
+#include "core/song.h"
+#include "settings/tidalsettingspage.h"
+#include "tidalservice.h"
+#include "tidalbaserequest.h"
+#include "tidalstreamurlrequest.h"
+
+TidalStreamURLRequest::TidalStreamURLRequest(TidalService *service, NetworkAccessManager *network, const QUrl &original_url, QObject *parent)
+ : TidalBaseRequest(service, network, parent),
+ service_(service),
+ reply_(nullptr),
+ original_url_(original_url),
+ song_id_(original_url.path().toInt()),
+ tries_(0),
+ need_login_(false) {}
+
+TidalStreamURLRequest::~TidalStreamURLRequest() {
+
+ if (reply_) {
+ disconnect(reply_, 0, this, 0);
+ if (reply_->isRunning()) reply_->abort();
+ reply_->deleteLater();
+ }
+
+}
+
+void TidalStreamURLRequest::LoginComplete(const bool success, QString error) {
+
+ if (!need_login_) return;
+ need_login_ = false;
+
+ if (!success) {
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error);
+ return;
+ }
+
+ Process();
+
+}
+
+void TidalStreamURLRequest::Process() {
+
+ if (!authenticated()) {
+ if (oauth()) {
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, tr("Not authenticated with Tidal."));
+ return;
+ }
+ else if (api_token().isEmpty() || username().isEmpty() || password().isEmpty()) {
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, tr("Missing Tidal API token, username or password."));
+ return;
+ }
+ need_login_ = true;
+ emit TryLogin();
+ return;
+ }
+
+ GetStreamURL();
+
+}
+
+void TidalStreamURLRequest::Cancel() {
+
+ if (reply_ && reply_->isRunning()) {
+ reply_->abort();
+ }
+ else {
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, tr("Cancelled."));
+ }
+
+}
+
+void TidalStreamURLRequest::GetStreamURL() {
+
+ ++tries_;
+
+ if (reply_) {
+ disconnect(reply_, 0, this, 0);
+ if (reply_->isRunning()) reply_->abort();
+ reply_->deleteLater();
+ }
+
+ ParamList params;
+
+ switch (stream_url_method()) {
+ case TidalSettingsPage::StreamUrlMethod_StreamUrl:
+ params << Param("soundQuality", quality());
+ reply_ = CreateRequest(QString("tracks/%1/streamUrl").arg(song_id_), params);
+ connect(reply_, SIGNAL(finished()), this, SLOT(StreamURLReceived()));
+ break;
+ case TidalSettingsPage::StreamUrlMethod_UrlPostPaywall:
+ params << Param("audioquality", quality());
+ params << Param("playbackmode", "STREAM");
+ params << Param("assetpresentation", "FULL");
+ params << Param("urlusagemode", "STREAM");
+ reply_ = CreateRequest(QString("tracks/%1/urlpostpaywall").arg(song_id_), params);
+ connect(reply_, SIGNAL(finished()), this, SLOT(StreamURLReceived()));
+ break;
+ case TidalSettingsPage::StreamUrlMethod_PlaybackInfoPostPaywall:
+ params << Param("audioquality", quality());
+ params << Param("playbackmode", "STREAM");
+ params << Param("assetpresentation", "FULL");
+ reply_ = CreateRequest(QString("tracks/%1/playbackinfopostpaywall").arg(song_id_), params);
+ connect(reply_, SIGNAL(finished()), this, SLOT(StreamURLReceived()));
+ break;
+ }
+
+}
+
+void TidalStreamURLRequest::StreamURLReceived() {
+
+ if (!reply_) return;
+ disconnect(reply_, 0, this, 0);
+ reply_->deleteLater();
+
+ QByteArray data = GetReplyData(reply_, true);
+ if (data.isEmpty()) {
+ reply_ = nullptr;
+ if (!authenticated() && login_sent() && tries_ <= 1) {
+ need_login_ = true;
+ return;
+ }
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
+ return;
+ }
+ reply_ = nullptr;
+
+ //qLog(Debug) << "Tidal:" << data;
+
+ QJsonObject json_obj = ExtractJsonObj(data);
+ if (json_obj.isEmpty()) {
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
+ return;
+ }
+
+ if (!json_obj.contains("trackId")) {
+ Error("Invalid Json reply, stream missing trackId.", json_obj);
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
+ return;
+ }
+ int track_id(json_obj["trackId"].toInt());
+ if (track_id != song_id_) {
+ Error("Incorrect track ID returned.", json_obj);
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
+ return;
+ }
+
+ Song::FileType filetype(Song::FileType_Unknown);
+
+ if (json_obj.contains("codec") || json_obj.contains("codecs")) {
+ QString codec;
+ if (json_obj.contains("codec")) codec = json_obj["codec"].toString().toLower();
+ if (json_obj.contains("codecs")) codec = json_obj["codecs"].toString().toLower();
+ filetype = Song::FiletypeByExtension(codec);
+ if (filetype == Song::FileType_Unknown) {
+ qLog(Debug) << "Tidal: Unknown codec" << codec;
+ filetype = Song::FileType_Stream;
+ }
+ }
+
+ QList urls;
+
+ if (json_obj.contains("manifest")) {
+
+ QString manifest(json_obj["manifest"].toString());
+ QByteArray data_manifest = QByteArray::fromBase64(manifest.toUtf8());
+
+ //qLog(Debug) << "Tidal:" << data_manifest;
+
+ QXmlStreamReader xml_reader(data_manifest);
+ if (xml_reader.readNextStartElement()) {
+
+ QString filepath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/tidalstreams";
+ QString filename = "tidal-" + QString::number(song_id_) + ".xml";
+ if (!QDir().mkpath(filepath)) {
+ Error(QString("Failed to create directory %1.").arg(filepath), json_obj);
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
+ return;
+ }
+ QUrl url("file://" + filepath + "/" + filename);
+ QFile file(url.toLocalFile());
+ if (file.exists())
+ file.remove();
+ if (!file.open(QIODevice::WriteOnly)) {
+ Error(QString("Failed to open file %1 for writing.").arg(url.toLocalFile()), json_obj);
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
+ return;
+ }
+ file.write(data_manifest);
+ file.close();
+
+ urls << url;
+
+ }
+
+ else {
+
+ json_obj = ExtractJsonObj(data_manifest);
+ if (json_obj.isEmpty()) {
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
+ return;
+ }
+
+ if (!json_obj.contains("mimeType")) {
+ Error("Invalid Json reply, stream url reply manifest is missing mimeType.", json_obj);
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
+ return;
+ }
+
+ QString mimetype = json_obj["mimeType"].toString();
+ 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) << "Tidal: Unknown mimetype" << mimetype;
+ filetype = Song::FileType_Stream;
+ }
+ }
+
+ }
+
+ if (json_obj.contains("urls")) {
+ QJsonValue json_urls = json_obj["urls"];
+ if (!json_urls.isArray()) {
+ Error("Invalid Json reply, urls is not an array.", json_urls);
+ emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
+ return;
+ }
+ QJsonArray json_array_urls = json_urls.toArray();
+ for (const QJsonValue &value : json_array_urls) {
+ urls << QUrl(value.toString());
+ }
+ }
+ else if (json_obj.contains("url")) {
+ QUrl new_url(json_obj["url"].toString());
+ urls << new_url;
+ }
+
+ if (urls.isEmpty()) {
+ Error("Missing stream urls.", json_obj);
+ emit StreamURLFinished(original_url_, original_url_, filetype, -1, -1, -1, errors_.first());
+ return;
+ }
+
+ emit StreamURLFinished(original_url_, urls.first(), filetype, -1, -1, -1);
+
+}
+
+void TidalStreamURLRequest::Error(const QString &error, const QVariant &debug) {
+
+ qLog(Error) << "Tidal:" << error;
+ if (debug.isValid()) qLog(Debug) << debug;
+
+ if (!error.isEmpty()) {
+ errors_ << error;
+ }
+
+}
diff --git a/src/tidal/tidalstreamurlrequest.h b/src/tidal/tidalstreamurlrequest.h
new file mode 100644
index 000000000..aacb86f0d
--- /dev/null
+++ b/src/tidal/tidalstreamurlrequest.h
@@ -0,0 +1,79 @@
+/*
+ * 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 TIDALSTREAMURLREQUEST_H
+#define TIDALSTREAMURLREQUEST_H
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/song.h"
+#include "tidalservice.h"
+#include "tidalbaserequest.h"
+#include "settings/tidalsettingspage.h"
+
+class QNetworkReply;
+class NetworkAccessManager;
+
+class TidalStreamURLRequest : public TidalBaseRequest {
+ Q_OBJECT
+
+ public:
+ TidalStreamURLRequest(TidalService *service, NetworkAccessManager *network, const QUrl &original_url, QObject *parent);
+ ~TidalStreamURLRequest();
+
+ void GetStreamURL();
+ void Process();
+ void NeedLogin() { need_login_ = true; }
+ void Cancel();
+
+ bool oauth() { return service_->oauth(); }
+ TidalSettingsPage::StreamUrlMethod stream_url_method() { return service_->stream_url_method(); }
+ QUrl original_url() { return original_url_; }
+ int song_id() { return song_id_; }
+ bool need_login() { return need_login_; }
+
+ signals:
+ void TryLogin();
+ void StreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error = QString());
+
+ private slots:
+ void LoginComplete(const bool success, QString error = QString());
+ void StreamURLReceived();
+
+ private:
+ void Error(const QString &error, const QVariant &debug = QVariant());
+
+ TidalService *service_;
+ QNetworkReply *reply_;
+ QUrl original_url_;
+ int song_id_;
+ int tries_;
+ bool need_login_;
+ QStringList errors_;
+
+};
+
+#endif // TIDALSTREAMURLREQUEST_H
diff --git a/src/tidal/tidalurlhandler.cpp b/src/tidal/tidalurlhandler.cpp
new file mode 100644
index 000000000..640896280
--- /dev/null
+++ b/src/tidal/tidalurlhandler.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 "config.h"
+
+#include
+#include
+#include
+
+#include "core/application.h"
+#include "core/taskmanager.h"
+#include "core/song.h"
+#include "tidal/tidalservice.h"
+#include "tidalurlhandler.h"
+
+TidalUrlHandler::TidalUrlHandler(Application *app, TidalService *service) :
+ UrlHandler(service),
+ app_(app),
+ service_(service),
+ task_id_(-1)
+ {
+
+ connect(service, SIGNAL(StreamURLFinished(QUrl, QUrl, Song::FileType, int, int, qint64, QString)), this, SLOT(GetStreamURLFinished(QUrl, QUrl, Song::FileType, int, int, qint64, QString)));
+
+}
+
+UrlHandler::LoadResult TidalUrlHandler::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 TidalUrlHandler::GetStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error) {
+
+ if (task_id_ == -1) return;
+ CancelTask();
+ if (error.isEmpty())
+ emit AsyncLoadComplete(LoadResult(original_url, LoadResult::TrackAvailable, stream_url, filetype, samplerate, bit_depth, duration));
+ else
+ emit AsyncLoadComplete(LoadResult(original_url, LoadResult::Error, stream_url, filetype, -1, -1, -1, error));
+
+}
+
+void TidalUrlHandler::CancelTask() {
+ app_->task_manager()->SetTaskFinished(task_id_);
+ task_id_ = -1;
+}
diff --git a/src/tidal/tidalurlhandler.h b/src/tidal/tidalurlhandler.h
new file mode 100644
index 000000000..519510a6b
--- /dev/null
+++ b/src/tidal/tidalurlhandler.h
@@ -0,0 +1,57 @@
+/*
+ * 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 TIDALURLHANDLER_H
+#define TIDALURLHANDLER_H
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+
+#include "core/urlhandler.h"
+#include "core/song.h"
+#include "tidal/tidalservice.h"
+
+class Application;
+
+class TidalUrlHandler : public UrlHandler {
+ Q_OBJECT
+
+ public:
+ TidalUrlHandler(Application *app, TidalService *service);
+
+ QString scheme() const { return service_->url_scheme(); }
+ LoadResult StartLoading(const QUrl &url);
+
+ void CancelTask();
+
+ private slots:
+ void GetStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error = QString());
+
+ private:
+ Application *app_;
+ TidalService *service_;
+ int task_id_;
+
+};
+
+#endif