diff --git a/README.md b/README.md index 22a125b7c..d40373d08 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Strawberry is a music player and music collection organizer. It is a fork of Cle * Advanced audio output and device configuration for bit-perfect playback on Linux * Edit tags on music files * Fetch tags from MusicBrainz - * Album cover art from Last.fm, Musicbrainz, Discogs and Deezer + * Album cover art from Last.fm, Musicbrainz, Discogs, Deezer and Tidal * Song lyrics from AudD * Support for multiple backends * Audio analyzer diff --git a/dist/debian/control b/dist/debian/control index f2907080a..c430a86bc 100644 --- a/dist/debian/control +++ b/dist/debian/control @@ -58,7 +58,7 @@ Description: Audio player and music collection organizer - Advanced audio output and device configuration for bit-perfect playback on Linux - Edit tags on music files - Fetch tags from MusicBrainz - - Album cover art from Lastfm, Musicbrainz, Discogs and Deezer + - Album cover art from Lastfm, Musicbrainz, Discogs, Deezer and Tidal - Song lyrics from AudD - Support for multiple backends - Audio analyzer diff --git a/dist/man/strawberry.1 b/dist/man/strawberry.1 index 6a3dba161..889968a77 100644 --- a/dist/man/strawberry.1 +++ b/dist/man/strawberry.1 @@ -25,7 +25,7 @@ Features: .br - Fetch tags from MusicBrainz .br -- Album cover art from Lastfm, Musicbrainz, Discogs and Deezer +- Album cover art from Lastfm, Musicbrainz, Discogs, Deezer and Tidal .br - Song lyrics from AudD .br diff --git a/dist/rpm/strawberry.spec.in b/dist/rpm/strawberry.spec.in index 007838672..7c921abee 100644 --- a/dist/rpm/strawberry.spec.in +++ b/dist/rpm/strawberry.spec.in @@ -96,7 +96,7 @@ Features: - Advanced audio output and device configuration for bit-perfect playback on Linux - Edit tags on music files - Fetch tags from MusicBrainz - - Album cover art from Last.fm, Musicbrainz, Discogs and Deezer + - Album cover art from Last.fm, Musicbrainz, Discogs, Deezer and Tidal - Song lyrics from AudD - Support for multiple backends - Audio analyzer diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index eb04938fa..9c2c57d06 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -199,6 +199,7 @@ set(SOURCES covermanager/musicbrainzcoverprovider.cpp covermanager/discogscoverprovider.cpp covermanager/deezercoverprovider.cpp + covermanager/tidalcoverprovider.cpp lyrics/lyricsproviders.cpp lyrics/lyricsprovider.cpp @@ -375,6 +376,7 @@ set(HEADERS covermanager/musicbrainzcoverprovider.h covermanager/discogscoverprovider.h covermanager/deezercoverprovider.h + covermanager/tidalcoverprovider.h lyrics/lyricsproviders.h lyrics/lyricsprovider.h diff --git a/src/core/application.cpp b/src/core/application.cpp index 9fd960abf..a29427f0d 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -55,6 +55,7 @@ #include "covermanager/discogscoverprovider.h" #include "covermanager/musicbrainzcoverprovider.h" #include "covermanager/deezercoverprovider.h" +#include "covermanager/tidalcoverprovider.h" #include "lyrics/lyricsproviders.h" #include "lyrics/lyricsprovider.h" @@ -103,10 +104,11 @@ class ApplicationImpl { cover_providers_([=]() { CoverProviders *cover_providers = new CoverProviders(app); // Initialize the repository of cover providers. - cover_providers->AddProvider(new LastFmCoverProvider(app)); - cover_providers->AddProvider(new DiscogsCoverProvider(app)); - cover_providers->AddProvider(new MusicbrainzCoverProvider(app)); - cover_providers->AddProvider(new DeezerCoverProvider(app)); + cover_providers->AddProvider(new LastFmCoverProvider(app, app)); + cover_providers->AddProvider(new DiscogsCoverProvider(app, app)); + cover_providers->AddProvider(new MusicbrainzCoverProvider(app, app)); + cover_providers->AddProvider(new DeezerCoverProvider(app, app)); + cover_providers->AddProvider(new TidalCoverProvider(app, app)); return cover_providers; }), album_cover_loader_([=]() { diff --git a/src/covermanager/coverprovider.cpp b/src/covermanager/coverprovider.cpp index d728f7551..56d2cfd30 100644 --- a/src/covermanager/coverprovider.cpp +++ b/src/covermanager/coverprovider.cpp @@ -23,7 +23,8 @@ #include #include +#include "core/application.h" #include "coverprovider.h" -CoverProvider::CoverProvider(const QString &name, const bool &fetchall, QObject *parent) - : QObject(parent), name_(name), fetchall_(fetchall) {} +CoverProvider::CoverProvider(const QString &name, const bool &fetchall, Application *app, QObject *parent) + : QObject(parent), app_(app), name_(name), fetchall_(fetchall) {} diff --git a/src/covermanager/coverprovider.h b/src/covermanager/coverprovider.h index d4e464322..ce3c5a276 100644 --- a/src/covermanager/coverprovider.h +++ b/src/covermanager/coverprovider.h @@ -29,6 +29,7 @@ #include #include +class Application; struct CoverSearchResult; // Each implementation of this interface downloads covers from one online service. @@ -36,8 +37,8 @@ struct CoverSearchResult; class CoverProvider : public QObject { Q_OBJECT -public: - explicit CoverProvider(const QString &name, const bool &fetchall, QObject *parent); + public: + explicit CoverProvider(const QString &name, const bool &fetchall, Application *app, QObject *parent); // A name (very short description) of this provider, like "last.fm". QString name() const { return name_; } @@ -50,10 +51,11 @@ public: virtual void CancelSearch(int id) {} -signals: + signals: void SearchFinished(int id, const QList& results); -private: + private: + Application *app_; QString name_; bool fetchall_; diff --git a/src/covermanager/coverproviders.h b/src/covermanager/coverproviders.h index e4c815067..2c3e4a551 100644 --- a/src/covermanager/coverproviders.h +++ b/src/covermanager/coverproviders.h @@ -61,7 +61,7 @@ class CoverProviders : public QObject { private: Q_DISABLE_COPY(CoverProviders); - QMap cover_providers_; + QMap cover_providers_; QMutex mutex_; QAtomicInt next_id_; diff --git a/src/covermanager/deezercoverprovider.cpp b/src/covermanager/deezercoverprovider.cpp index ab6139f57..0cb4ad7ec 100644 --- a/src/covermanager/deezercoverprovider.cpp +++ b/src/covermanager/deezercoverprovider.cpp @@ -37,6 +37,7 @@ #include #include +#include "core/application.h" #include "core/closure.h" #include "core/network.h" #include "core/logging.h" @@ -47,7 +48,7 @@ const char *DeezerCoverProvider::kApiUrl = "https://api.deezer.com"; const int DeezerCoverProvider::kLimit = 10; -DeezerCoverProvider::DeezerCoverProvider(QObject *parent): CoverProvider("Deezer", true, parent), network_(new NetworkAccessManager(this)) {} +DeezerCoverProvider::DeezerCoverProvider(Application *app, QObject *parent): CoverProvider("Deezer", true, app, parent), network_(new NetworkAccessManager(this)) {} bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &album, int id) { diff --git a/src/covermanager/deezercoverprovider.h b/src/covermanager/deezercoverprovider.h index 444471d29..2ffaa5fce 100644 --- a/src/covermanager/deezercoverprovider.h +++ b/src/covermanager/deezercoverprovider.h @@ -36,11 +36,13 @@ #include "coverprovider.h" +class Application; + class DeezerCoverProvider : public CoverProvider { Q_OBJECT public: - explicit DeezerCoverProvider(QObject *parent = nullptr); + explicit DeezerCoverProvider(Application *app, QObject *parent = nullptr); bool StartSearch(const QString &artist, const QString &album, int id); void CancelSearch(int id); diff --git a/src/covermanager/discogscoverprovider.cpp b/src/covermanager/discogscoverprovider.cpp index 533d06f7d..25a49f69d 100644 --- a/src/covermanager/discogscoverprovider.cpp +++ b/src/covermanager/discogscoverprovider.cpp @@ -39,6 +39,7 @@ #include #include +#include "core/application.h" #include "core/closure.h" #include "core/logging.h" #include "core/network.h" @@ -53,7 +54,7 @@ const char *DiscogsCoverProvider::kUrlReleases = "https://api.discogs.com/releas const char *DiscogsCoverProvider::kAccessKeyB64 = "dGh6ZnljUGJlZ1NEeXBuSFFxSVk="; const char *DiscogsCoverProvider::kSecretKeyB64 = "ZkFIcmlaSER4aHhRSlF2U3d0bm5ZVmdxeXFLWUl0UXI="; -DiscogsCoverProvider::DiscogsCoverProvider(QObject *parent) : CoverProvider("Discogs", false, parent), network_(new NetworkAccessManager(this)) {} +DiscogsCoverProvider::DiscogsCoverProvider(Application *app, QObject *parent) : CoverProvider("Discogs", false, app, parent), network_(new NetworkAccessManager(this)) {} bool DiscogsCoverProvider::StartSearch(const QString &artist, const QString &album, int s_id) { diff --git a/src/covermanager/discogscoverprovider.h b/src/covermanager/discogscoverprovider.h index 891a75b7a..270f49f5f 100644 --- a/src/covermanager/discogscoverprovider.h +++ b/src/covermanager/discogscoverprovider.h @@ -36,6 +36,8 @@ #include "coverprovider.h" #include "albumcoverfetcher.h" +class Application; + // This struct represents a single search-for-cover request. It identifies and describes the request. struct DiscogsCoverSearchContext { @@ -66,7 +68,7 @@ class DiscogsCoverProvider : public CoverProvider { Q_OBJECT public: - explicit DiscogsCoverProvider(QObject *parent = nullptr); + explicit DiscogsCoverProvider(Application *app, QObject *parent = nullptr); bool StartSearch(const QString &artist, const QString &album, int s_id); diff --git a/src/covermanager/lastfmcoverprovider.cpp b/src/covermanager/lastfmcoverprovider.cpp index e8bc75e71..5af34f8d2 100644 --- a/src/covermanager/lastfmcoverprovider.cpp +++ b/src/covermanager/lastfmcoverprovider.cpp @@ -35,6 +35,7 @@ #include #include +#include "core/application.h" #include "core/closure.h" #include "core/network.h" #include "core/logging.h" @@ -47,7 +48,7 @@ const char *LastFmCoverProvider::kUrl = "https://ws.audioscrobbler.com/2.0/"; const char *LastFmCoverProvider::kApiKey = "211990b4c96782c05d1536e7219eb56e"; const char *LastFmCoverProvider::kSecret = "80fd738f49596e9709b1bf9319c444a8"; -LastFmCoverProvider::LastFmCoverProvider(QObject *parent) : CoverProvider("last.fm", true, parent), network_(new NetworkAccessManager(this)) {} +LastFmCoverProvider::LastFmCoverProvider(Application *app, QObject *parent) : CoverProvider("last.fm", true, app, parent), network_(new NetworkAccessManager(this)) {} bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &album, int id) { diff --git a/src/covermanager/lastfmcoverprovider.h b/src/covermanager/lastfmcoverprovider.h index 1ac254d21..99ab4d76d 100644 --- a/src/covermanager/lastfmcoverprovider.h +++ b/src/covermanager/lastfmcoverprovider.h @@ -36,11 +36,13 @@ #include "coverprovider.h" +class Application; + class LastFmCoverProvider : public CoverProvider { Q_OBJECT public: - explicit LastFmCoverProvider(QObject *parent = nullptr); + explicit LastFmCoverProvider(Application *app, QObject *parent = nullptr); bool StartSearch(const QString &artist, const QString &album, int id); private slots: diff --git a/src/covermanager/musicbrainzcoverprovider.cpp b/src/covermanager/musicbrainzcoverprovider.cpp index ff3ea782e..d3476818f 100644 --- a/src/covermanager/musicbrainzcoverprovider.cpp +++ b/src/covermanager/musicbrainzcoverprovider.cpp @@ -37,6 +37,7 @@ #include #include +#include "core/application.h" #include "core/closure.h" #include "core/network.h" #include "core/logging.h" @@ -48,7 +49,7 @@ const char *MusicbrainzCoverProvider::kReleaseSearchUrl = "https://musicbrainz.o const char *MusicbrainzCoverProvider::kAlbumCoverUrl = "https://coverartarchive.org/release/%1/front"; const int MusicbrainzCoverProvider::kLimit = 8; -MusicbrainzCoverProvider::MusicbrainzCoverProvider(QObject *parent): CoverProvider("MusicBrainz", true, parent), network_(new NetworkAccessManager(this)) {} +MusicbrainzCoverProvider::MusicbrainzCoverProvider(Application *app, QObject *parent): CoverProvider("MusicBrainz", true, app, parent), network_(new NetworkAccessManager(this)) {} bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString &album, int id) { diff --git a/src/covermanager/musicbrainzcoverprovider.h b/src/covermanager/musicbrainzcoverprovider.h index 65b4a4b75..7ae0e0f7b 100644 --- a/src/covermanager/musicbrainzcoverprovider.h +++ b/src/covermanager/musicbrainzcoverprovider.h @@ -35,10 +35,12 @@ #include "coverprovider.h" #include "albumcoverfetcher.h" +class Application; + class MusicbrainzCoverProvider : public CoverProvider { Q_OBJECT public: - explicit MusicbrainzCoverProvider(QObject *parent = nullptr); + explicit MusicbrainzCoverProvider(Application *app, QObject *parent = nullptr); bool StartSearch(const QString &artist, const QString &album, int id); void CancelSearch(int id); diff --git a/src/covermanager/tidalcoverprovider.cpp b/src/covermanager/tidalcoverprovider.cpp new file mode 100644 index 000000000..934b6ff68 --- /dev/null +++ b/src/covermanager/tidalcoverprovider.cpp @@ -0,0 +1,276 @@ +/* + * Strawberry Music Player + * Copyright 2018, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/application.h" +#include "core/closure.h" +#include "core/network.h" +#include "core/logging.h" +#include "settings/tidalsettingspage.h" +#include "tidal/tidalservice.h" +#include "albumcoverfetcher.h" +#include "coverprovider.h" +#include "tidalcoverprovider.h" + +const char *TidalCoverProvider::kApiUrl = "https://listen.tidal.com/v1"; +const char *TidalCoverProvider::kResourcesUrl = "http://resources.tidal.com"; +const char *TidalCoverProvider::kApiTokenB64 = "UDVYYmVvNUxGdkVTZUR5Ng=="; +const int TidalCoverProvider::kLimit = 10; + +TidalCoverProvider::TidalCoverProvider(Application *app, QObject *parent) : + CoverProvider("Tidal", true, app, parent), + service_(app->internet_services()->Service()), + network_(new NetworkAccessManager(this)) { + +} + +bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album, int id) { + + if (!service_ || !service_->authenticated()) return false; + + QList parameters; + parameters << Param("query", QString(artist + " " + album)); + parameters << Param("limit", QString::number(kLimit)); + QNetworkReply *reply = CreateRequest("search/albums", parameters); + NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleSearchReply(QNetworkReply*, int)), reply, id); + + return true; + +} + +void TidalCoverProvider::CancelSearch(int id) {} + +QNetworkReply *TidalCoverProvider::CreateRequest(const QString &ressource_name, const QList ¶ms_supplied) { + + typedef QPair Param; + typedef QList ParamList; + typedef QPair EncodedParam; + typedef QList EncodedParamList; + + ParamList parameters = ParamList() + << params_supplied + << Param("sessionId", service_->session_id()) + << Param("countryCode", service_->country_code()); + + QStringList query_items; + QUrlQuery url_query; + for (const Param& param : parameters) { + EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); + query_items << QString(encoded_param.first + "=" + encoded_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.setRawHeader("Origin", "http://listen.tidal.com"); + 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) { + data = reply->readAll(); + } + else { + if (reply->error() < 200) { + // This is a network error, there is nothing more to do. + error = Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + } + else { + // See if there is Json data containing "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; + QString failure_reason; + if (parse_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { + QJsonObject json_obj = json_doc.object(); + if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("userMessage")) { + status = json_obj["status"].toInt(); + sub_status = json_obj["subStatus"].toInt(); + QString user_message = json_obj["userMessage"].toString(); + failure_reason = QString("%1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); + } + } + if (failure_reason.isEmpty()) { + failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + if (status == 401 && sub_status == 6001) { // User does not have a valid session + service_->Logout(); + } + error = Error(failure_reason); + } + 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.isNull() || json_doc.isEmpty()) { + error = Error("Received empty Json document.", data); + return QJsonObject(); + } + + if (!json_doc.isObject()) { + error = Error("Json document is not an object.", json_doc); + return QJsonObject(); + } + + QJsonObject json_obj = json_doc.object(); + if (json_obj.isEmpty()) { + error = Error("Received empty Json object.", json_doc); + return QJsonObject(); + } + + return json_obj; + +} + +QJsonValue 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, 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(); + + cover = cover.replace("-", "/"); + QUrl cover_url (QString("%1/images/%2/%3.jpg").arg(kResourcesUrl).arg(cover).arg("1280x1280")); + + CoverSearchResult cover_result; + cover_result.description = artist + " " + album; + cover_result.image_url = cover_url; + results << cover_result; + + } + emit SearchFinished(id, results); + +} + +QString TidalCoverProvider::Error(QString error, QVariant debug) { + qLog(Error) << "Tidal:" << error; + if (debug.isValid()) qLog(Debug) << debug; + return error; +} diff --git a/src/covermanager/tidalcoverprovider.h b/src/covermanager/tidalcoverprovider.h new file mode 100644 index 000000000..e33ed77d6 --- /dev/null +++ b/src/covermanager/tidalcoverprovider.h @@ -0,0 +1,83 @@ +/* + * Strawberry Music Player + * Copyright 2018, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef TIDALCOVERPROVIDER_H +#define TIDALCOVERPROVIDER_H + +#include "config.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "coverprovider.h" +#include "tidal/tidalservice.h" + +class Application; + +class TidalCoverProvider : public CoverProvider { + Q_OBJECT + + public: + explicit TidalCoverProvider(Application *app, QObject *parent = nullptr); + void SetService(TidalService *service); + void ReloadSettings(); + bool StartSearch(const QString &artist, const QString &album, int id); + void CancelSearch(int id); + + private slots: + void HandleSearchReply(QNetworkReply *reply, int id); + + private: + typedef QPair Param; + static const char *kApiUrl; + static const char *kResourcesUrl; + static const char *kApiTokenB64; + static const int kLimit; + + //QString username_; + //QString password_; + //QString session_id_; + //quint64 user_id_; + //QString country_code_; + +#if 0 + void LoadSessionID(); +#endif + QNetworkReply *CreateRequest(const QString &ressource_name, const QList ¶ms_supplied); + QByteArray GetReplyData(QNetworkReply *reply, QString &error); + QJsonObject ExtractJsonObj(QByteArray &data, QString &error); + QJsonValue ExtractItems(QByteArray &data, QString &error); + QJsonValue ExtractItems(QJsonObject &json_obj, QString &error); + QString Error(QString error, QVariant debug = QVariant()); + + TidalService *service_; + QNetworkAccessManager *network_; + +}; + +#endif // TIDALCOVERPROVIDER_H diff --git a/src/tidal/tidalservice.h b/src/tidal/tidalservice.h index 8e60db10f..3555e48d7 100644 --- a/src/tidal/tidalservice.h +++ b/src/tidal/tidalservice.h @@ -62,6 +62,9 @@ class TidalService : public InternetService { const bool login_sent() { return login_sent_; } const bool authenticated() { return (!session_id_.isEmpty() && !country_code_.isEmpty()); } + QString session_id() { return session_id_; } + QString country_code() { return country_code_; } + void GetStreamURL(const QUrl &url); signals: