Merge pull request #406 from strawberrymusicplayer/tidal
Add back Tidal support
This commit is contained in:
commit
a21855fa20
|
@ -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
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
<file>schema/schema-9.sql</file>
|
||||
<file>schema/schema-10.sql</file>
|
||||
<file>schema/schema-11.sql</file>
|
||||
<file>schema/schema-12.sql</file>
|
||||
<file>schema/device-schema.sql</file>
|
||||
<file>style/strawberry.css</file>
|
||||
<file>html/playing-tooltip-plain.html</file>
|
||||
|
|
|
@ -89,6 +89,7 @@
|
|||
<file>icons/128x128/moodbar.png</file>
|
||||
<file>icons/128x128/love.png</file>
|
||||
<file>icons/128x128/subsonic.png</file>
|
||||
<file>icons/128x128/tidal.png</file>
|
||||
<file>icons/64x64/albums.png</file>
|
||||
<file>icons/64x64/alsa.png</file>
|
||||
<file>icons/64x64/application-exit.png</file>
|
||||
|
@ -179,6 +180,7 @@
|
|||
<file>icons/64x64/moodbar.png</file>
|
||||
<file>icons/64x64/love.png</file>
|
||||
<file>icons/64x64/subsonic.png</file>
|
||||
<file>icons/64x64/tidal.png</file>
|
||||
<file>icons/48x48/albums.png</file>
|
||||
<file>icons/48x48/alsa.png</file>
|
||||
<file>icons/48x48/application-exit.png</file>
|
||||
|
@ -272,6 +274,7 @@
|
|||
<file>icons/48x48/moodbar.png</file>
|
||||
<file>icons/48x48/love.png</file>
|
||||
<file>icons/48x48/subsonic.png</file>
|
||||
<file>icons/48x48/tidal.png</file>
|
||||
<file>icons/32x32/albums.png</file>
|
||||
<file>icons/32x32/alsa.png</file>
|
||||
<file>icons/32x32/application-exit.png</file>
|
||||
|
@ -365,6 +368,7 @@
|
|||
<file>icons/32x32/moodbar.png</file>
|
||||
<file>icons/32x32/love.png</file>
|
||||
<file>icons/32x32/subsonic.png</file>
|
||||
<file>icons/32x32/tidal.png</file>
|
||||
<file>icons/22x22/albums.png</file>
|
||||
<file>icons/22x22/alsa.png</file>
|
||||
<file>icons/22x22/application-exit.png</file>
|
||||
|
@ -458,5 +462,6 @@
|
|||
<file>icons/22x22/moodbar.png</file>
|
||||
<file>icons/22x22/love.png</file>
|
||||
<file>icons/22x22/subsonic.png</file>
|
||||
<file>icons/22x22/tidal.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 933 B |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
|
@ -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;
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
#cmakedefine XINE_ANALYZER
|
||||
|
||||
#cmakedefine HAVE_SUBSONIC
|
||||
#cmakedefine HAVE_TIDAL
|
||||
|
||||
#cmakedefine HAVE_MOODBAR
|
||||
|
||||
|
|
|
@ -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;
|
||||
}),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -115,6 +115,7 @@ static const QMap<QString, IconProperties> 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 } },
|
||||
|
|
|
@ -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<TidalService*> (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
|
||||
|
|
|
@ -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_;
|
||||
|
|
|
@ -0,0 +1,275 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QtDebug>
|
||||
|
||||
#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<TidalService>()),
|
||||
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;
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALCOVERPROVIDER_H
|
||||
#define TIDALCOVERPROVIDER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
|
||||
#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<QString, QString> Param;
|
||||
typedef QList<Param> ParamList;
|
||||
typedef QPair<QByteArray, QByteArray> 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
|
|
@ -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*)));
|
||||
|
|
|
@ -86,7 +86,8 @@ class SettingsDialog : public QDialog {
|
|||
Page_Proxy,
|
||||
Page_Scrobbler,
|
||||
Page_Moodbar,
|
||||
Page_Subsonic
|
||||
Page_Subsonic,
|
||||
Page_Tidal,
|
||||
};
|
||||
|
||||
enum Role {
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QSettings>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QMessageBox>
|
||||
#include <QEvent>
|
||||
|
||||
#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<TidalService>()) {
|
||||
|
||||
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<StreamUrlMethod>(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);
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALSETTINGSPAGE_H
|
||||
#define TIDALSETTINGSPAGE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#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
|
|
@ -0,0 +1,357 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TidalSettingsPage</class>
|
||||
<widget class="QWidget" name="TidalSettingsPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>715</width>
|
||||
<height>836</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Tidal</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="layout_tidalsettingspage">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="enable">
|
||||
<property name="text">
|
||||
<string>Enable</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_warning">
|
||||
<property name="text">
|
||||
<string>Tidal support is not official and requires a API token from a registered application to work. We can't help you getting these.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="credential_group">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Authentication</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="layout_credential_group">
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="oauth">
|
||||
<property name="text">
|
||||
<string>Use OAuth</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_client_id">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Client ID</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="client_id"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_api_token">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>API Token</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="api_token">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_username">
|
||||
<property name="text">
|
||||
<string>Username</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="username"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_password">
|
||||
<property name="text">
|
||||
<string>Password</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLineEdit" name="password">
|
||||
<property name="echoMode">
|
||||
<enum>QLineEdit::Password</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_login">
|
||||
<property name="text">
|
||||
<string>Login</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="LoginStateWidget" name="login_state" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupbox_preferences">
|
||||
<property name="title">
|
||||
<string>Preferences</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="layout_preferences">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_quality">
|
||||
<property name="text">
|
||||
<string>Audio quality</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="quality"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_searchdelay">
|
||||
<property name="text">
|
||||
<string>Search delay</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="searchdelay">
|
||||
<property name="suffix">
|
||||
<string>ms</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>50</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>1500</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_artistssearchlimit">
|
||||
<property name="text">
|
||||
<string>Artists search limit</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QSpinBox" name="artistssearchlimit">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>50</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_albumssearchlimit">
|
||||
<property name="text">
|
||||
<string>Albums search limit</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QSpinBox" name="albumssearchlimit">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>1000</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>50</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_songssearchlimit">
|
||||
<property name="text">
|
||||
<string>Songs search limit</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QCheckBox" name="checkbox_download_album_covers">
|
||||
<property name="text">
|
||||
<string>Download album covers</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QCheckBox" name="checkbox_fetchalbums">
|
||||
<property name="text">
|
||||
<string>Fetch entire albums when searching songs</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QComboBox" name="coversize"/>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_coversize">
|
||||
<property name="text">
|
||||
<string>Album cover size</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QSpinBox" name="songssearchlimit">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>1000</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>50</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="label_streamurl">
|
||||
<property name="text">
|
||||
<string>Stream URL method</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QComboBox" name="streamurl"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="spacer_middle">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="layout_bottom">
|
||||
<item>
|
||||
<spacer name="spacer_bottom">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_tidal">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap resource="../../data/icons.qrc">:/icons/64x64/tidal.png</pixmap>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>LoginStateWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/loginstatewidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>enable</tabstop>
|
||||
<tabstop>oauth</tabstop>
|
||||
<tabstop>client_id</tabstop>
|
||||
<tabstop>api_token</tabstop>
|
||||
<tabstop>username</tabstop>
|
||||
<tabstop>password</tabstop>
|
||||
<tabstop>button_login</tabstop>
|
||||
<tabstop>quality</tabstop>
|
||||
<tabstop>searchdelay</tabstop>
|
||||
<tabstop>artistssearchlimit</tabstop>
|
||||
<tabstop>albumssearchlimit</tabstop>
|
||||
<tabstop>songssearchlimit</tabstop>
|
||||
<tabstop>checkbox_download_album_covers</tabstop>
|
||||
<tabstop>checkbox_fetchalbums</tabstop>
|
||||
<tabstop>coversize</tabstop>
|
||||
<tabstop>streamurl</tabstop>
|
||||
</tabstops>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
<include location="../../data/icons.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QPair>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QSslError>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
#include <QtDebug>
|
||||
|
||||
#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<Param> ¶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<QSslError>)), this, SLOT(HandleSSLErrors(QList<QSslError>)));
|
||||
|
||||
qLog(Debug) << "Tidal: Sending request" << url;
|
||||
|
||||
return reply;
|
||||
|
||||
}
|
||||
|
||||
void TidalBaseRequest::HandleSSLErrors(QList<QSslError> 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 + "<br />";
|
||||
}
|
||||
return error_html;
|
||||
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALBASEREQUEST_H
|
||||
#define TIDALBASEREQUEST_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QSslError>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
|
||||
#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<QString, QString> Param;
|
||||
typedef QList<Param> ParamList;
|
||||
|
||||
typedef QPair<QByteArray, QByteArray> EncodedParam;
|
||||
typedef QList<EncodedParam> EncodedParamList;
|
||||
|
||||
QNetworkReply *CreateRequest(const QString &ressource_name, const QList<Param> ¶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<QSslError> ssl_errors);
|
||||
|
||||
private:
|
||||
|
||||
static const char *kApiUrl;
|
||||
|
||||
TidalService *service_;
|
||||
NetworkAccessManager *network_;
|
||||
|
||||
};
|
||||
|
||||
#endif // TIDALBASEREQUEST_H
|
|
@ -0,0 +1,297 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QMultiMap>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QtDebug>
|
||||
|
||||
#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<QByteArray, QByteArray> 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<QString, Song> 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;
|
||||
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALFAVORITEREQUEST_H
|
||||
#define TIDALFAVORITEREQUEST_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
|
||||
#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 <QNetworkReply*> replies_;
|
||||
bool need_login_;
|
||||
|
||||
};
|
||||
|
||||
#endif // TIDALFAVORITEREQUEST_H
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALREQUEST_H
|
||||
#define TIDALREQUEST_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QHash>
|
||||
#include <QMap>
|
||||
#include <QMultiMap>
|
||||
#include <QQueue>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QJsonObject>
|
||||
|
||||
#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<QString, QString> Param;
|
||||
typedef QList<Param> 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<Request> artists_requests_queue_;
|
||||
QQueue<Request> albums_requests_queue_;
|
||||
QQueue<Request> songs_requests_queue_;
|
||||
|
||||
QQueue<Request> artist_albums_requests_queue_;
|
||||
QQueue<Request> album_songs_requests_queue_;
|
||||
QQueue<AlbumCoverRequest> album_cover_requests_queue_;
|
||||
|
||||
QList<QString> artist_albums_requests_pending_;
|
||||
QHash<QString, Request> album_songs_requests_pending_;
|
||||
QMultiMap<QString, Song*> 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<QNetworkReply*> replies_;
|
||||
QList<QNetworkReply*> album_cover_replies_;
|
||||
|
||||
};
|
||||
|
||||
#endif // TIDALREQUEST_H
|
|
@ -0,0 +1,966 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QDesktopServices>
|
||||
#include <QCryptographicHash>
|
||||
#include <QByteArray>
|
||||
#include <QPair>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QChar>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QSslError>
|
||||
#include <QTimer>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSettings>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QtDebug>
|
||||
|
||||
#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<QObject*>(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<TidalSettingsPage::StreamUrlMethod>(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<QSslError>)), this, SLOT(HandleLoginSSLErrors(QList<QSslError>)));
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(AccessTokenRequestFinished(QNetworkReply*)), reply);
|
||||
|
||||
}
|
||||
|
||||
else {
|
||||
|
||||
LoginError(tr("Reply from Tidal is missing query items."));
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void TidalService::HandleLoginSSLErrors(QList<QSslError> 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<QSslError>)), this, SLOT(HandleLoginSSLErrors(QList<QSslError>)));
|
||||
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<TidalStreamURLRequest*>(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 + "<br />";
|
||||
}
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
emit LoginFailure(error_html);
|
||||
emit LoginComplete(false, error_html);
|
||||
|
||||
login_errors_.clear();
|
||||
|
||||
}
|
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALSERVICE_H
|
||||
#define TIDALSERVICE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QDateTime>
|
||||
#include <QSslError>
|
||||
|
||||
#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<QSslError> 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<QString, QString> Param;
|
||||
typedef QList<Param> ParamList;
|
||||
|
||||
typedef QPair<QByteArray, QByteArray> EncodedParam;
|
||||
typedef QList<EncodedParam> 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<TidalRequest> artists_request_;
|
||||
std::shared_ptr<TidalRequest> albums_request_;
|
||||
std::shared_ptr<TidalRequest> songs_request_;
|
||||
std::shared_ptr<TidalRequest> 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<TidalStreamURLRequest*> stream_url_requests_;
|
||||
|
||||
QStringList login_errors_;
|
||||
|
||||
QList<QObject*> wait_for_exit_;
|
||||
|
||||
};
|
||||
|
||||
#endif // TIDALSERVICE_H
|
|
@ -0,0 +1,299 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QStandardPaths>
|
||||
#include <QMimeDatabase>
|
||||
#include <QMimeType>
|
||||
#include <QIODevice>
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include <QList>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QtDebug>
|
||||
|
||||
#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<QUrl> 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALSTREAMURLREQUEST_H
|
||||
#define TIDALSTREAMURLREQUEST_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
|
||||
#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
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#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;
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALURLHANDLER_H
|
||||
#define TIDALURLHANDLER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#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
|
Loading…
Reference in New Issue