Add artist search in internet view, and use album artist

This commit is contained in:
Jonas Kvinge 2019-02-12 21:58:03 +01:00
parent e1abd28a88
commit 35f448c34f
15 changed files with 446 additions and 186 deletions

View File

@ -144,7 +144,7 @@ const QString Song::kEmbeddedCover = "(embedded)";
const QRegExp Song::kAlbumRemoveDisc(" ?-? ((\\(|\\[)?)(Disc|CD) ?([0-9]{1,2})((\\)|\\])?)$"); const QRegExp Song::kAlbumRemoveDisc(" ?-? ((\\(|\\[)?)(Disc|CD) ?([0-9]{1,2})((\\)|\\])?)$");
const QRegExp Song::kAlbumRemoveMisc(" ?-? ((\\(|\\[)?)(Remastered) ?((\\)|\\])?)$"); const QRegExp Song::kAlbumRemoveMisc(" ?-? ((\\(|\\[)?)(Remastered) ?((\\)|\\])?)$");
const QRegExp Song::kTitleRemoveMisc(" ?-? ((\\(|\\[)?)(Remastered|Live) ?((\\)|\\])?)$"); const QRegExp Song::kTitleRemoveMisc(" ?-? ((\\(|\\[)?)(Remastered|Live|Remastered Version) ?((\\)|\\])?)$");
struct Song::Private : public QSharedData { struct Song::Private : public QSharedData {

View File

@ -350,8 +350,6 @@ QJsonObject DeezerService::ExtractJsonObj(QByteArray &data) {
QJsonParseError error; QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
//qLog(Debug) << json_doc;
if (error.error != QJsonParseError::NoError) { if (error.error != QJsonParseError::NoError) {
Error("Reply from server missing Json data.", data); Error("Reply from server missing Json data.", data);
return QJsonObject(); return QJsonObject();
@ -373,8 +371,6 @@ QJsonObject DeezerService::ExtractJsonObj(QByteArray &data) {
return QJsonObject(); return QJsonObject();
} }
//qLog(Debug) << json_obj;
return json_obj; return json_obj;
} }
@ -412,11 +408,11 @@ QJsonValue DeezerService::ExtractData(QByteArray &data) {
} }
int DeezerService::Search(const QString &text, InternetSearch::SearchBy searchby) { int DeezerService::Search(const QString &text, InternetSearch::SearchType searchby) {
pending_search_id_ = next_pending_search_id_; pending_search_id_ = next_pending_search_id_;
pending_search_text_ = text; pending_search_text_ = text;
pending_searchby_ = searchby; pending_search_type_ = searchby;
next_pending_search_id_++; next_pending_search_id_++;
@ -469,12 +465,13 @@ void DeezerService::SendSearch() {
QList<Param> parameters; QList<Param> parameters;
parameters << Param("q", search_text_); parameters << Param("q", search_text_);
QString searchparam; QString searchparam;
switch (pending_searchby_) { switch (pending_search_type_) {
case InternetSearch::SearchBy_Songs: case InternetSearch::SearchType_Songs:
searchparam = "search/track"; searchparam = "search/track";
parameters << Param("limit", QString::number(songssearchlimit_)); parameters << Param("limit", QString::number(songssearchlimit_));
break; break;
case InternetSearch::SearchBy_Albums: case InternetSearch::SearchType_Albums:
case InternetSearch::SearchType_Artists:
default: default:
searchparam = "search/album"; searchparam = "search/album";
parameters << Param("limit", QString::number(albumssearchlimit_)); parameters << Param("limit", QString::number(albumssearchlimit_));
@ -511,23 +508,19 @@ void DeezerService::SearchFinished(QNetworkReply *reply, int id) {
return; return;
} }
//qLog(Debug) << json_data;
for (const QJsonValue &value : json_data) { for (const QJsonValue &value : json_data) {
//qLog(Debug) << value;
if (!value.isObject()) { if (!value.isObject()) {
Error("Invalid Json reply, data is not an object.", value); Error("Invalid Json reply, data is not an object.", value);
continue; continue;
} }
QJsonObject json_obj = value.toObject(); QJsonObject json_obj = value.toObject();
//qLog(Debug) << json_obj;
if (!json_obj.contains("id") || !json_obj.contains("type")) { if (!json_obj.contains("id") || !json_obj.contains("type")) {
Error("Invalid Json reply, item is missing ID or type.", json_obj); Error("Invalid Json reply, item is missing ID or type.", json_obj);
continue; continue;
} }
//int id = json_obj["id"].toInt();
QString type = json_obj["type"].toString(); QString type = json_obj["type"].toString();
if (!json_obj.contains("artist")) { if (!json_obj.contains("artist")) {
@ -575,7 +568,7 @@ void DeezerService::SearchFinished(QNetworkReply *reply, int id) {
album = json_album["title"].toString(); album = json_album["title"].toString();
cover = json_album[coversize_].toString(); cover = json_album[coversize_].toString();
if (!fetchalbums_) { if (!fetchalbums_) {
Song song = ParseSong(album_id, album, cover, value); Song song = ParseSong(album_id, album, artist, cover, value);
songs_ << song; songs_ << song;
continue; continue;
} }
@ -668,14 +661,12 @@ void DeezerService::GetAlbumFinished(QNetworkReply *reply, int search_id, int al
bool compilation = false; bool compilation = false;
bool multidisc = false; bool multidisc = false;
Song first_song;
SongList songs; SongList songs;
for (const QJsonValue &value : json_data) { for (const QJsonValue &value : json_data) {
Song song = ParseSong(album_ctx->id, album_ctx->album, album_ctx->cover, value); Song song = ParseSong(album_ctx->id, album_ctx->album, album_ctx->artist, album_ctx->cover, value);
if (!song.is_valid()) continue; if (!song.is_valid()) continue;
if (song.disc() >= 2) multidisc = true; if (song.disc() >= 2) multidisc = true;
if (song.is_compilation() || (first_song.is_valid() && song.artist() != first_song.artist())) compilation = true; if (song.is_compilation()) compilation = true;
if (!first_song.is_valid()) first_song = song;
songs << song; songs << song;
} }
for (Song &song : songs) { for (Song &song : songs) {
@ -692,7 +683,7 @@ void DeezerService::GetAlbumFinished(QNetworkReply *reply, int search_id, int al
} }
Song DeezerService::ParseSong(const int album_id, const QString &album, const QString &album_cover, const QJsonValue &value) { Song DeezerService::ParseSong(const int album_id, const QString &album, const QString &album_artist, const QString &album_cover, const QJsonValue &value) {
if (!value.isObject()) { if (!value.isObject()) {
Error("Invalid Json reply, track is not an object.", value); Error("Invalid Json reply, track is not an object.", value);
@ -700,8 +691,6 @@ Song DeezerService::ParseSong(const int album_id, const QString &album, const QS
} }
QJsonObject json_obj = value.toObject(); QJsonObject json_obj = value.toObject();
//qLog(Debug) << json_obj;
if ( if (
!json_obj.contains("id") || !json_obj.contains("id") ||
!json_obj.contains("title") || !json_obj.contains("title") ||
@ -738,6 +727,7 @@ Song DeezerService::ParseSong(const int album_id, const QString &album, const QS
song.set_source(Song::Source_Deezer); song.set_source(Song::Source_Deezer);
song.set_id(song_id); song.set_id(song_id);
song.set_album_id(album_id); song.set_album_id(album_id);
if (artist != album_artist) song.set_albumartist(album_artist);
song.set_artist(artist); song.set_artist(artist);
song.set_album(album); song.set_album(album);
song.set_title(title); song.set_title(title);
@ -760,7 +750,6 @@ Song DeezerService::ParseSong(const int album_id, const QString &album, const QS
} }
} }
song.set_url(url); song.set_url(url);
song.set_valid(true); song.set_valid(true);
return song; return song;

View File

@ -70,7 +70,7 @@ class DeezerService : public InternetService {
void ReloadSettings(); void ReloadSettings();
void Logout(); void Logout();
int Search(const QString &query, InternetSearch::SearchBy searchby); int Search(const QString &query, InternetSearch::SearchType searchby);
void CancelSearch(); void CancelSearch();
const bool app_id() { return kAppID; } const bool app_id() { return kAppID; }
@ -116,7 +116,7 @@ class DeezerService : public InternetService {
void SendSearch(); void SendSearch();
DeezerAlbumContext *CreateAlbum(const int album_id, const QString &artist, const QString &album, const QString &cover); DeezerAlbumContext *CreateAlbum(const int album_id, const QString &artist, const QString &album, const QString &cover);
void GetAlbum(const DeezerAlbumContext *album_ctx); void GetAlbum(const DeezerAlbumContext *album_ctx);
Song ParseSong(const int album_id, const QString &album, const QString &album_cover, const QJsonValue &value); Song ParseSong(const int album_id, const QString &album, const QString &album_artist, const QString &album_cover, const QJsonValue &value);
void CheckFinish(); void CheckFinish();
void Error(QString error, QVariant debug = QString()); void Error(QString error, QVariant debug = QString());
@ -145,7 +145,7 @@ class DeezerService : public InternetService {
int pending_search_id_; int pending_search_id_;
int next_pending_search_id_; int next_pending_search_id_;
QString pending_search_text_; QString pending_search_text_;
InternetSearch::SearchBy pending_searchby_; InternetSearch::SearchType pending_search_type_;
int search_id_; int search_id_;
QString search_text_; QString search_text_;

View File

@ -69,7 +69,7 @@ InternetSearch::InternetSearch(Application *app, Song::Source source, QObject *p
cover_loader_options_.scale_output_image_ = true; cover_loader_options_.scale_output_image_ = true;
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(AlbumArtLoaded(quint64, QImage))); connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(AlbumArtLoaded(quint64, QImage)));
connect(this, SIGNAL(SearchAsyncSig(int, QString, SearchBy)), this, SLOT(DoSearchAsync(int, QString, SearchBy))); connect(this, SIGNAL(SearchAsyncSig(int, QString, SearchType)), this, SLOT(DoSearchAsync(int, QString, SearchType)));
connect(this, SIGNAL(ResultsAvailable(int, InternetSearch::ResultList)), SLOT(ResultsAvailableSlot(int, InternetSearch::ResultList))); connect(this, SIGNAL(ResultsAvailable(int, InternetSearch::ResultList)), SLOT(ResultsAvailableSlot(int, InternetSearch::ResultList)));
connect(this, SIGNAL(ArtLoaded(int, QImage)), SLOT(ArtLoadedSlot(int, QImage))); connect(this, SIGNAL(ArtLoaded(int, QImage)), SLOT(ArtLoadedSlot(int, QImage)));
connect(service_, SIGNAL(UpdateStatus(QString)), SLOT(UpdateStatusSlot(QString))); connect(service_, SIGNAL(UpdateStatus(QString)), SLOT(UpdateStatusSlot(QString)));
@ -113,29 +113,29 @@ bool InternetSearch::Matches(const QStringList &tokens, const QString &string) {
} }
int InternetSearch::SearchAsync(const QString &query, SearchBy searchby) { int InternetSearch::SearchAsync(const QString &query, SearchType type) {
const int id = searches_next_id_++; const int id = searches_next_id_++;
emit SearchAsyncSig(id, query, searchby); emit SearchAsyncSig(id, query, type);
return id; return id;
} }
void InternetSearch::SearchAsync(int id, const QString &query, SearchBy searchby) { void InternetSearch::SearchAsync(int id, const QString &query, SearchType type) {
const int service_id = service_->Search(query, searchby); const int service_id = service_->Search(query, type);
pending_searches_[service_id] = PendingState(id, TokenizeQuery(query)); pending_searches_[service_id] = PendingState(id, TokenizeQuery(query));
} }
void InternetSearch::DoSearchAsync(int id, const QString &query, SearchBy searchby) { void InternetSearch::DoSearchAsync(int id, const QString &query, SearchType type) {
int timer_id = startTimer(kDelayedSearchTimeoutMs); int timer_id = startTimer(kDelayedSearchTimeoutMs);
delayed_searches_[timer_id].id_ = id; delayed_searches_[timer_id].id_ = id;
delayed_searches_[timer_id].query_ = query; delayed_searches_[timer_id].query_ = query;
delayed_searches_[timer_id].searchby_ = searchby; delayed_searches_[timer_id].type_ = type;
} }
@ -186,7 +186,7 @@ void InternetSearch::CancelSearch(int id) {
void InternetSearch::timerEvent(QTimerEvent *e) { void InternetSearch::timerEvent(QTimerEvent *e) {
QMap<int, DelayedSearch>::iterator it = delayed_searches_.find(e->timerId()); QMap<int, DelayedSearch>::iterator it = delayed_searches_.find(e->timerId());
if (it != delayed_searches_.end()) { if (it != delayed_searches_.end()) {
SearchAsync(it.value().id_, it.value().query_, it.value().searchby_); SearchAsync(it.value().id_, it.value().query_, it.value().type_);
delayed_searches_.erase(it); delayed_searches_.erase(it);
return; return;
} }

View File

@ -45,9 +45,10 @@ class InternetSearch : public QObject {
InternetSearch(Application *app, Song::Source source, QObject *parent = nullptr); InternetSearch(Application *app, Song::Source source, QObject *parent = nullptr);
~InternetSearch(); ~InternetSearch();
enum SearchBy { enum SearchType {
SearchBy_Songs = 1, SearchType_Artists = 1,
SearchBy_Albums = 2, SearchType_Albums = 2,
SearchType_Songs = 3,
}; };
struct Result { struct Result {
@ -63,7 +64,7 @@ class InternetSearch : public QObject {
Song::Source source() const { return source_; } Song::Source source() const { return source_; }
InternetService *service() const { return service_; } InternetService *service() const { return service_; }
int SearchAsync(const QString &query, SearchBy searchby); int SearchAsync(const QString &query, SearchType type);
int LoadArtAsync(const InternetSearch::Result &result); int LoadArtAsync(const InternetSearch::Result &result);
void CancelSearch(int id); void CancelSearch(int id);
@ -74,7 +75,7 @@ class InternetSearch : public QObject {
MimeData *LoadTracks(const ResultList &results); MimeData *LoadTracks(const ResultList &results);
signals: signals:
void SearchAsyncSig(int id, const QString &query, SearchBy searchby); void SearchAsyncSig(int id, const QString &query, SearchType type);
void ResultsAvailable(int id, const InternetSearch::ResultList &results); void ResultsAvailable(int id, const InternetSearch::ResultList &results);
void AddResults(int id, const InternetSearch::ResultList &results); void AddResults(int id, const InternetSearch::ResultList &results);
void SearchError(const int id, const QString error); void SearchError(const int id, const QString error);
@ -112,7 +113,7 @@ class InternetSearch : public QObject {
static bool Matches(const QStringList &tokens, const QString &string); static bool Matches(const QStringList &tokens, const QString &string);
private slots: private slots:
void DoSearchAsync(int id, const QString &query, SearchBy searchby); void DoSearchAsync(int id, const QString &query, SearchType type);
void SearchDone(int id, const SongList &songs); void SearchDone(int id, const SongList &songs);
void HandleError(const int id, const QString error); void HandleError(const int id, const QString error);
void ResultsAvailableSlot(int id, InternetSearch::ResultList results); void ResultsAvailableSlot(int id, InternetSearch::ResultList results);
@ -125,7 +126,7 @@ class InternetSearch : public QObject {
void UpdateProgressSlot(int max); void UpdateProgressSlot(int max);
private: private:
void SearchAsync(int id, const QString &query, SearchBy searchby); void SearchAsync(int id, const QString &query, SearchType type);
void HandleLoadedArt(int id, const QImage &image); void HandleLoadedArt(int id, const QImage &image);
bool FindCachedPixmap(const InternetSearch::Result &result, QPixmap *pixmap) const; bool FindCachedPixmap(const InternetSearch::Result &result, QPixmap *pixmap) const;
QString PixmapCacheKey(const InternetSearch::Result &result) const; QString PixmapCacheKey(const InternetSearch::Result &result) const;
@ -137,7 +138,7 @@ class InternetSearch : public QObject {
struct DelayedSearch { struct DelayedSearch {
int id_; int id_;
QString query_; QString query_;
SearchBy searchby_; SearchType type_;
}; };
static const int kArtHeight; static const int kArtHeight;

View File

@ -41,17 +41,15 @@ InternetSearchModel::InternetSearchModel(InternetSearch *engine, QObject *parent
engine_(engine), engine_(engine),
proxy_(nullptr), proxy_(nullptr),
use_pretty_covers_(true), use_pretty_covers_(true),
artist_icon_(IconLoader::Load("folder-sound")) { artist_icon_(IconLoader::Load("folder-sound")),
album_icon_(IconLoader::Load("cdcase"))
{
group_by_[0] = CollectionModel::GroupBy_Artist; group_by_[0] = CollectionModel::GroupBy_Artist;
group_by_[1] = CollectionModel::GroupBy_Album; group_by_[1] = CollectionModel::GroupBy_Album;
group_by_[2] = CollectionModel::GroupBy_None; group_by_[2] = CollectionModel::GroupBy_None;
QIcon nocover = IconLoader::Load("cdcase"); no_cover_icon_ = album_icon_.pixmap(album_icon_.availableSizes().last()).scaled(CollectionModel::kPrettyCoverSize, CollectionModel::kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(CollectionModel::kPrettyCoverSize, CollectionModel::kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
//no_cover_icon_ = QPixmap(":/pictures/noalbumart.png").scaled(CollectionModel::kPrettyCoverSize, CollectionModel::kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
album_icon_ = no_cover_icon_;
} }
@ -93,6 +91,19 @@ QStandardItem *InternetSearchModel::BuildContainers(const Song &s, QStandardItem
int year = 0; int year = 0;
switch (group_by_[level]) { switch (group_by_[level]) {
case CollectionModel::GroupBy_AlbumArtist:
if (s.is_compilation()) {
display_text = tr("Various artists");
sort_text = "aaaaaa";
}
else {
display_text = CollectionModel::TextOrUnknown(s.effective_albumartist());
sort_text = CollectionModel::SortTextForArtist(s.effective_albumartist());
}
has_artist_icon = true;
break;
case CollectionModel::GroupBy_Artist: case CollectionModel::GroupBy_Artist:
if (s.is_compilation()) { if (s.is_compilation()) {
display_text = tr("Various artists"); display_text = tr("Various artists");
@ -134,28 +145,42 @@ QStandardItem *InternetSearchModel::BuildContainers(const Song &s, QStandardItem
break; break;
case CollectionModel::GroupBy_Composer: case CollectionModel::GroupBy_Composer:
display_text = s.composer(); display_text = CollectionModel::TextOrUnknown(s.composer());
sort_text = CollectionModel::SortTextForArtist(s.composer());
has_album_icon = true;
break;
case CollectionModel::GroupBy_Performer: case CollectionModel::GroupBy_Performer:
display_text = s.performer(); display_text = CollectionModel::TextOrUnknown(s.performer());
sort_text = CollectionModel::SortTextForArtist(s.performer());
has_album_icon = true;
break;
case CollectionModel::GroupBy_Disc: case CollectionModel::GroupBy_Disc:
display_text = s.disc(); display_text = s.disc();
case CollectionModel::GroupBy_Grouping:
display_text = s.grouping();
case CollectionModel::GroupBy_Genre:
if (display_text.isNull()) display_text = s.genre();
case CollectionModel::GroupBy_Album:
unique_tag = s.album_id();
if (display_text.isNull()) {
display_text = s.album();
}
// fallthrough
case CollectionModel::GroupBy_AlbumArtist:
if (display_text.isNull()) display_text = s.effective_albumartist();
display_text = CollectionModel::TextOrUnknown(display_text);
sort_text = CollectionModel::SortTextForArtist(display_text); sort_text = CollectionModel::SortTextForArtist(display_text);
has_album_icon = true; has_album_icon = true;
break; break;
case CollectionModel::GroupBy_Grouping:
display_text = CollectionModel::TextOrUnknown(s.grouping());
sort_text = CollectionModel::SortTextForArtist(s.grouping());
has_album_icon = true;
break;
case CollectionModel::GroupBy_Genre:
display_text = CollectionModel::TextOrUnknown(s.genre());
sort_text = CollectionModel::SortTextForArtist(s.genre());
has_album_icon = true;
break;
case CollectionModel::GroupBy_Album:
display_text = CollectionModel::TextOrUnknown(s.album());
sort_text = CollectionModel::SortTextForArtist(s.album());
unique_tag = s.album_id();
has_album_icon = true;
break;
case CollectionModel::GroupBy_FileType: case CollectionModel::GroupBy_FileType:
display_text = s.TextForFiletype(); display_text = s.TextForFiletype();
sort_text = display_text; sort_text = display_text;
@ -180,6 +205,9 @@ QStandardItem *InternetSearchModel::BuildContainers(const Song &s, QStandardItem
return parent; return parent;
} }
if (display_text.isEmpty()) display_text = "Unknown";
if (sort_text.isEmpty()) sort_text = "Unknown";
// Find a container for this level // Find a container for this level
key->group_[level] = display_text + QString::number(unique_tag); key->group_[level] = display_text + QString::number(unique_tag);
QStandardItem *container = containers_[*key]; QStandardItem *container = containers_[*key];

View File

@ -81,8 +81,8 @@ class InternetSearchModel : public QStandardItemModel {
QSortFilterProxyModel *proxy_; QSortFilterProxyModel *proxy_;
bool use_pretty_covers_; bool use_pretty_covers_;
QIcon artist_icon_; QIcon artist_icon_;
QPixmap no_cover_icon_;
QIcon album_icon_; QIcon album_icon_;
QPixmap no_cover_icon_;
CollectionModel::Grouping group_by_; CollectionModel::Grouping group_by_;
QMap<ContainerKey, QStandardItem*> containers_; QMap<ContainerKey, QStandardItem*> containers_;

View File

@ -144,8 +144,9 @@ InternetSearchView::InternetSearchView(Application *app, InternetSearch *engine,
settings_menu->addAction(IconLoader::Load("configure"), QString("Configure %1...").arg(Song::TextForSource(engine->source())), this, SLOT(OpenSettingsDialog())); settings_menu->addAction(IconLoader::Load("configure"), QString("Configure %1...").arg(Song::TextForSource(engine->source())), this, SLOT(OpenSettingsDialog()));
ui_->settings->setMenu(settings_menu); ui_->settings->setMenu(settings_menu);
connect(ui_->radiobutton_searchbyalbums, SIGNAL(clicked(bool)), SLOT(SearchByAlbumsClicked(bool))); connect(ui_->radiobutton_search_artists, SIGNAL(clicked(bool)), SLOT(SearchArtistsClicked(bool)));
connect(ui_->radiobutton_searchbysongs, SIGNAL(clicked(bool)), SLOT(SearchBySongsClicked(bool))); connect(ui_->radiobutton_search_albums, SIGNAL(clicked(bool)), SLOT(SearchAlbumsClicked(bool)));
connect(ui_->radiobutton_search_songs, SIGNAL(clicked(bool)), SLOT(SearchSongsClicked(bool)));
connect(group_by_actions_, SIGNAL(triggered(QAction*)), SLOT(GroupByClicked(QAction*))); connect(group_by_actions_, SIGNAL(triggered(QAction*)), SLOT(GroupByClicked(QAction*)));
@ -180,18 +181,21 @@ void InternetSearchView::ReloadSettings() {
// Internet search settings // Internet search settings
s.beginGroup(settings_group_); s.beginGroup(settings_group_);
searchby_ = InternetSearch::SearchBy(s.value("searchby", int(InternetSearch::SearchBy_Songs)).toInt()); search_type_ = InternetSearch::SearchType(s.value("type", int(InternetSearch::SearchType_Artists)).toInt());
switch (searchby_) { switch (search_type_) {
case InternetSearch::SearchBy_Songs: case InternetSearch::SearchType_Artists:
ui_->radiobutton_searchbysongs->setChecked(true); ui_->radiobutton_search_artists->setChecked(true);
break; break;
case InternetSearch::SearchBy_Albums: case InternetSearch::SearchType_Albums:
ui_->radiobutton_searchbyalbums->setChecked(true); ui_->radiobutton_search_albums->setChecked(true);
break;
case InternetSearch::SearchType_Songs:
ui_->radiobutton_search_songs->setChecked(true);
break; break;
} }
SetGroupBy(CollectionModel::Grouping( SetGroupBy(CollectionModel::Grouping(
CollectionModel::GroupBy(s.value("group_by1", int(CollectionModel::GroupBy_Artist)).toInt()), CollectionModel::GroupBy(s.value("group_by1", int(CollectionModel::GroupBy_AlbumArtist)).toInt()),
CollectionModel::GroupBy(s.value("group_by2", int(CollectionModel::GroupBy_Album)).toInt()), CollectionModel::GroupBy(s.value("group_by2", int(CollectionModel::GroupBy_Album)).toInt()),
CollectionModel::GroupBy(s.value("group_by3", int(CollectionModel::GroupBy_None)).toInt()))); CollectionModel::GroupBy(s.value("group_by3", int(CollectionModel::GroupBy_None)).toInt())));
s.endGroup(); s.endGroup();
@ -233,7 +237,7 @@ void InternetSearchView::TextEdited(const QString &text) {
} }
else { else {
ui_->progressbar->reset(); ui_->progressbar->reset();
last_search_id_ = engine_->SearchAsync(trimmed, searchby_); last_search_id_ = engine_->SearchAsync(trimmed, search_type_);
} }
} }
@ -293,7 +297,6 @@ void InternetSearchView::LazyLoadArt(const QModelIndex &proxy_index) {
// Is this an album? // Is this an album?
const CollectionModel::GroupBy container_type = CollectionModel::GroupBy(proxy_index.data(CollectionModel::Role_ContainerType).toInt()); const CollectionModel::GroupBy container_type = CollectionModel::GroupBy(proxy_index.data(CollectionModel::Role_ContainerType).toInt());
if (container_type != CollectionModel::GroupBy_Album && if (container_type != CollectionModel::GroupBy_Album &&
container_type != CollectionModel::GroupBy_AlbumArtist &&
container_type != CollectionModel::GroupBy_YearAlbum && container_type != CollectionModel::GroupBy_YearAlbum &&
container_type != CollectionModel::GroupBy_OriginalYearAlbum) { container_type != CollectionModel::GroupBy_OriginalYearAlbum) {
return; return;
@ -545,19 +548,23 @@ void InternetSearchView::SetGroupBy(const CollectionModel::Grouping &g) {
} }
void InternetSearchView::SearchBySongsClicked(bool checked) { void InternetSearchView::SearchArtistsClicked(bool checked) {
SetSearchBy(InternetSearch::SearchBy_Songs); SetSearchType(InternetSearch::SearchType_Artists);
} }
void InternetSearchView::SearchByAlbumsClicked(bool checked) { void InternetSearchView::SearchAlbumsClicked(bool checked) {
SetSearchBy(InternetSearch::SearchBy_Albums); SetSearchType(InternetSearch::SearchType_Albums);
} }
void InternetSearchView::SetSearchBy(InternetSearch::SearchBy searchby) { void InternetSearchView::SearchSongsClicked(bool checked) {
searchby_ = searchby; SetSearchType(InternetSearch::SearchType_Songs);
}
void InternetSearchView::SetSearchType(InternetSearch::SearchType type) {
search_type_ = type;
QSettings s; QSettings s;
s.beginGroup(settings_group_); s.beginGroup(settings_group_);
s.setValue("searchby", int(searchby)); s.setValue("type", int(search_type_));
s.endGroup(); s.endGroup();
TextEdited(ui_->search->text()); TextEdited(ui_->search->text());
} }

View File

@ -43,7 +43,6 @@
#include "settings/settingsdialog.h" #include "settings/settingsdialog.h"
#include "playlist/playlistmanager.h" #include "playlist/playlistmanager.h"
#include "internetsearch.h" #include "internetsearch.h"
//#include "settings/internetsettingspage.h"
class Application; class Application;
class GroupByDialog; class GroupByDialog;
@ -93,10 +92,11 @@ signals:
void SearchForThis(); void SearchForThis();
void SearchBySongsClicked(bool); void SearchArtistsClicked(bool);
void SearchByAlbumsClicked(bool); void SearchAlbumsClicked(bool);
void SearchSongsClicked(bool);
void GroupByClicked(QAction *action); void GroupByClicked(QAction *action);
void SetSearchBy(InternetSearch::SearchBy searchby); void SetSearchType(InternetSearch::SearchType type);
void SetGroupBy(const CollectionModel::Grouping &g); void SetGroupBy(const CollectionModel::Grouping &g);
private: private:
@ -133,7 +133,7 @@ signals:
QTimer *swap_models_timer_; QTimer *swap_models_timer_;
InternetSearch::SearchBy searchby_; InternetSearch::SearchType search_type_;
bool error_; bool error_;
}; };

View File

@ -72,7 +72,7 @@
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="text"> <property name="text">
<string>Search by</string> <string>Search for</string>
</property> </property>
<property name="margin"> <property name="margin">
<number>10</number> <number>10</number>
@ -80,14 +80,21 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QRadioButton" name="radiobutton_searchbyalbums"> <widget class="QRadioButton" name="radiobutton_search_artists">
<property name="text">
<string>ar&amp;tists</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radiobutton_search_albums">
<property name="text"> <property name="text">
<string>a&amp;lbums</string> <string>a&amp;lbums</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QRadioButton" name="radiobutton_searchbysongs"> <widget class="QRadioButton" name="radiobutton_search_songs">
<property name="text"> <property name="text">
<string>son&amp;gs</string> <string>son&amp;gs</string>
</property> </property>
@ -208,7 +215,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>398</width> <width>398</width>
<height>502</height> <height>521</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_5"> <layout class="QVBoxLayout" name="verticalLayout_5">

View File

@ -52,7 +52,7 @@ class InternetService : public QObject {
virtual void InitialLoadSettings() {} virtual void InitialLoadSettings() {}
virtual void ReloadSettings() {} virtual void ReloadSettings() {}
virtual QIcon Icon() { return Song::IconForSource(source_); } virtual QIcon Icon() { return Song::IconForSource(source_); }
virtual int Search(const QString &query, InternetSearch::SearchBy searchby) = 0; virtual int Search(const QString &query, InternetSearch::SearchType type) = 0;
virtual void CancelSearch() = 0; virtual void CancelSearch() = 0;
public slots: public slots:

View File

@ -81,6 +81,7 @@ void TidalSettingsPage::Load() {
else ui_->password->setText(QString::fromUtf8(QByteArray::fromBase64(password))); else ui_->password->setText(QString::fromUtf8(QByteArray::fromBase64(password)));
dialog()->ComboBoxLoadFromSettings(s, ui_->combobox_quality, "quality", "HIGH"); dialog()->ComboBoxLoadFromSettings(s, ui_->combobox_quality, "quality", "HIGH");
ui_->spinbox_searchdelay->setValue(s.value("searchdelay", 1500).toInt()); ui_->spinbox_searchdelay->setValue(s.value("searchdelay", 1500).toInt());
ui_->spinbox_artistssearchlimit->setValue(s.value("artistssearchlimit", 5).toInt());
ui_->spinbox_albumssearchlimit->setValue(s.value("albumssearchlimit", 100).toInt()); ui_->spinbox_albumssearchlimit->setValue(s.value("albumssearchlimit", 100).toInt());
ui_->spinbox_songssearchlimit->setValue(s.value("songssearchlimit", 100).toInt()); ui_->spinbox_songssearchlimit->setValue(s.value("songssearchlimit", 100).toInt());
ui_->checkbox_fetchalbums->setChecked(s.value("fetchalbums", false).toBool()); ui_->checkbox_fetchalbums->setChecked(s.value("fetchalbums", false).toBool());
@ -101,6 +102,7 @@ void TidalSettingsPage::Save() {
s.setValue("password", QString::fromUtf8(ui_->password->text().toUtf8().toBase64())); s.setValue("password", QString::fromUtf8(ui_->password->text().toUtf8().toBase64()));
s.setValue("quality", ui_->combobox_quality->itemData(ui_->combobox_quality->currentIndex())); s.setValue("quality", ui_->combobox_quality->itemData(ui_->combobox_quality->currentIndex()));
s.setValue("searchdelay", ui_->spinbox_searchdelay->value()); s.setValue("searchdelay", ui_->spinbox_searchdelay->value());
s.setValue("artistssearchlimit", ui_->spinbox_artistssearchlimit->value());
s.setValue("albumssearchlimit", ui_->spinbox_albumssearchlimit->value()); s.setValue("albumssearchlimit", ui_->spinbox_albumssearchlimit->value());
s.setValue("songssearchlimit", ui_->spinbox_songssearchlimit->value()); s.setValue("songssearchlimit", ui_->spinbox_songssearchlimit->value());
s.setValue("fetchalbums", ui_->checkbox_fetchalbums->isChecked()); s.setValue("fetchalbums", ui_->checkbox_fetchalbums->isChecked());

View File

@ -162,6 +162,49 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_artistssearchlimit">
<item>
<widget class="QLabel" name="label">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Artists search limit</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinbox_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>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_albumssearchlimit"> <layout class="QHBoxLayout" name="horizontalLayout_albumssearchlimit">
<item> <item>

View File

@ -63,15 +63,14 @@ const char *TidalService::kApiTokenB64 = "UDVYYmVvNUxGdkVTZUR5Ng==";
const int TidalService::kLoginAttempts = 1; const int TidalService::kLoginAttempts = 1;
const int TidalService::kTimeResetLoginAttempts = 60000; const int TidalService::kTimeResetLoginAttempts = 60000;
typedef QPair<QString, QString> Param;
TidalService::TidalService(Application *app, QObject *parent) TidalService::TidalService(Application *app, QObject *parent)
: InternetService(Song::Source_Tidal, "Tidal", "tidal", app, parent), : InternetService(Song::Source_Tidal, "Tidal", "tidal", app, parent),
network_(new NetworkAccessManager(this)), network_(new NetworkAccessManager(this)),
url_handler_(new TidalUrlHandler(app, this)), url_handler_(new TidalUrlHandler(app, this)),
timer_searchdelay_(new QTimer(this)), timer_search_delay_(new QTimer(this)),
timer_login_attempt_(new QTimer(this)), timer_login_attempt_(new QTimer(this)),
searchdelay_(1500), search_delay_(1500),
artistssearchlimit_(1),
albumssearchlimit_(1), albumssearchlimit_(1),
songssearchlimit_(1), songssearchlimit_(1),
fetchalbums_(false), fetchalbums_(false),
@ -79,14 +78,17 @@ TidalService::TidalService(Application *app, QObject *parent)
pending_search_id_(0), pending_search_id_(0),
next_pending_search_id_(1), next_pending_search_id_(1),
search_id_(0), search_id_(0),
albums_requested_(0), artist_search_(false),
albums_received_(0), artist_albums_requested_(0),
artist_albums_received_(0),
album_songs_requested_(0),
album_songs_received_(0),
login_sent_(false), login_sent_(false),
login_attempts_(0) login_attempts_(0)
{ {
timer_searchdelay_->setSingleShot(true); timer_search_delay_->setSingleShot(true);
connect(timer_searchdelay_, SIGNAL(timeout()), SLOT(StartSearch())); connect(timer_search_delay_, SIGNAL(timeout()), SLOT(StartSearch()));
timer_login_attempt_->setSingleShot(true); timer_login_attempt_->setSingleShot(true);
connect(timer_login_attempt_, SIGNAL(timeout()), SLOT(ResetLoginAttempts())); connect(timer_login_attempt_, SIGNAL(timeout()), SLOT(ResetLoginAttempts()));
@ -116,7 +118,8 @@ void TidalService::ReloadSettings() {
if (password.isEmpty()) password_.clear(); if (password.isEmpty()) password_.clear();
else password_ = QString::fromUtf8(QByteArray::fromBase64(password)); else password_ = QString::fromUtf8(QByteArray::fromBase64(password));
quality_ = s.value("quality").toString(); quality_ = s.value("quality").toString();
searchdelay_ = s.value("searchdelay", 1500).toInt(); search_delay_ = s.value("searchdelay", 1500).toInt();
artistssearchlimit_ = s.value("artistssearchlimit", 5).toInt();
albumssearchlimit_ = s.value("albumssearchlimit", 100).toInt(); albumssearchlimit_ = s.value("albumssearchlimit", 100).toInt();
songssearchlimit_ = s.value("songssearchlimit", 100).toInt(); songssearchlimit_ = s.value("songssearchlimit", 100).toInt();
fetchalbums_ = s.value("fetchalbums", false).toBool(); fetchalbums_ = s.value("fetchalbums", false).toBool();
@ -333,7 +336,7 @@ QNetworkReply *TidalService::CreateRequest(const QString &ressource_name, const
req.setRawHeader("X-Tidal-SessionId", session_id_.toUtf8()); req.setRawHeader("X-Tidal-SessionId", session_id_.toUtf8());
QNetworkReply *reply = network_->get(req); QNetworkReply *reply = network_->get(req);
//qLog(Debug) << "Tidal: Sending request" << url; qLog(Debug) << "Tidal: Sending request" << url;
return reply; return reply;
@ -402,8 +405,6 @@ QJsonObject TidalService::ExtractJsonObj(QByteArray &data) {
QJsonParseError error; QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
//qLog(Debug) << json_doc;
if (error.error != QJsonParseError::NoError) { if (error.error != QJsonParseError::NoError) {
Error("Reply from server missing Json data.", data); Error("Reply from server missing Json data.", data);
return QJsonObject(); return QJsonObject();
@ -425,8 +426,6 @@ QJsonObject TidalService::ExtractJsonObj(QByteArray &data) {
return QJsonObject(); return QJsonObject();
} }
//qLog(Debug) << json_obj;
return json_obj; return json_obj;
} }
@ -434,32 +433,36 @@ QJsonObject TidalService::ExtractJsonObj(QByteArray &data) {
QJsonValue TidalService::ExtractItems(QByteArray &data) { QJsonValue TidalService::ExtractItems(QByteArray &data) {
QJsonObject json_obj = ExtractJsonObj(data); QJsonObject json_obj = ExtractJsonObj(data);
if (json_obj.isEmpty()) return QJsonArray(); if (json_obj.isEmpty()) return QJsonValue();
return ExtractItems(json_obj);
}
QJsonValue TidalService::ExtractItems(QJsonObject &json_obj) {
if (!json_obj.contains("items")) { if (!json_obj.contains("items")) {
Error("Json reply is missing items.", json_obj); Error("Json reply is missing items.", json_obj);
return QJsonArray(); return QJsonArray();
} }
QJsonValue json_items = json_obj["items"]; QJsonValue json_items = json_obj["items"];
return json_items; return json_items;
} }
int TidalService::Search(const QString &text, InternetSearch::SearchBy searchby) { int TidalService::Search(const QString &text, InternetSearch::SearchType type) {
pending_search_id_ = next_pending_search_id_; pending_search_id_ = next_pending_search_id_;
pending_search_text_ = text; pending_search_text_ = text;
pending_searchby_ = searchby; pending_search_type_ = type;
next_pending_search_id_++; next_pending_search_id_++;
if (text.isEmpty()) { if (text.isEmpty()) {
timer_searchdelay_->stop(); timer_search_delay_->stop();
return pending_search_id_; return pending_search_id_;
} }
timer_searchdelay_->setInterval(searchdelay_); timer_search_delay_->setInterval(search_delay_);
timer_searchdelay_->start(); timer_search_delay_->start();
return pending_search_id_; return pending_search_id_;
@ -487,79 +490,215 @@ void TidalService::CancelSearch() {
} }
void TidalService::ClearSearch() { void TidalService::ClearSearch() {
search_id_ = 0; search_id_ = 0;
search_text_.clear(); search_text_.clear();
search_error_.clear(); search_error_.clear();
albums_requested_ = 0; artist_search_ = false;
albums_received_ = 0; artist_albums_requested_ = 0;
requests_album_.clear(); artist_albums_received_ = 0;
album_songs_requested_ = 0;
album_songs_received_ = 0;
requests_artist_albums_.clear();
requests_album_songs_.clear();
requests_song_.clear(); requests_song_.clear();
requests_artist_album_.clear();
songs_.clear(); songs_.clear();
} }
void TidalService::SendSearch() { void TidalService::SendSearch() {
emit UpdateStatus("Searching..."); emit UpdateStatus("Searching...");
QList<Param> parameters; switch (pending_search_type_) {
parameters << Param("query", search_text_); case InternetSearch::SearchType_Artists:
SendArtistsSearch();
QString searchparam; break;
switch (pending_searchby_) { case InternetSearch::SearchType_Albums:
case InternetSearch::SearchBy_Songs: SendAlbumsSearch();
searchparam = "search/tracks"; break;
parameters << Param("limit", QString::number(songssearchlimit_)); case InternetSearch::SearchType_Songs:
SendSongsSearch();
break; break;
case InternetSearch::SearchBy_Albums:
default: default:
searchparam = "search/albums"; Error("Invalid search type.");
parameters << Param("limit", QString::number(albumssearchlimit_));
break; break;
} }
QNetworkReply *reply = CreateRequest(searchparam, parameters);
NewClosure(reply, SIGNAL(finished()), this, SLOT(SearchFinished(QNetworkReply*, int)), reply, search_id_);
} }
void TidalService::SearchFinished(QNetworkReply *reply, int id) { void TidalService::SendArtistsSearch() {
artist_search_ = true;
QList<Param> parameters;
parameters << Param("query", search_text_);
parameters << Param("limit", QString::number(artistssearchlimit_));
QNetworkReply *reply = CreateRequest("search/artists", parameters);
NewClosure(reply, SIGNAL(finished()), this, SLOT(ArtistsReceived(QNetworkReply*, int)), reply, search_id_);
}
void TidalService::SendAlbumsSearch() {
QList<Param> parameters;
parameters << Param("query", search_text_);
parameters << Param("limit", QString::number(albumssearchlimit_));
QNetworkReply *reply = CreateRequest("search/albums", parameters);
NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumsReceived(QNetworkReply*, int, int)), reply, search_id_, 0);
}
void TidalService::SendSongsSearch() {
QList<Param> parameters;
parameters << Param("query", search_text_);
parameters << Param("limit", QString::number(songssearchlimit_));
QNetworkReply *reply = CreateRequest("search/tracks", parameters);
NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumsReceived(QNetworkReply*, int, int)), reply, search_id_, 0);
}
void TidalService::ArtistsReceived(QNetworkReply *reply, int search_id) {
reply->deleteLater(); reply->deleteLater();
if (id != search_id_) return; if (search_id != search_id_) return;
QByteArray data = GetReplyData(reply, true); QByteArray data = GetReplyData(reply, true);
if (data.isEmpty()) { if (data.isEmpty()) {
artist_search_ = false;
CheckFinish(); CheckFinish();
return; return;
} }
QJsonValue json_value = ExtractItems(data); QJsonValue json_value = ExtractItems(data);
if (!json_value.isArray()) { if (!json_value.isArray()) {
artist_search_ = false;
CheckFinish(); CheckFinish();
return; return;
} }
QJsonArray json_items = json_value.toArray(); QJsonArray json_items = json_value.toArray();
if (json_items.isEmpty()) { if (json_items.isEmpty()) {
artist_search_ = false;
Error("No match."); Error("No match.");
return; return;
} }
//qLog(Debug) << json_items;
QVector<QString> albums;
for (const QJsonValue &value : json_items) { for (const QJsonValue &value : json_items) {
//qLog(Debug) << value;
if (!value.isObject()) { if (!value.isObject()) {
qLog(Error) << "Tidal: Invalid Json reply, item not a object."; qLog(Error) << "Tidal: Invalid Json reply, item not a object.";
qLog(Debug) << value; qLog(Debug) << value;
continue; continue;
} }
QJsonObject json_obj = value.toObject(); QJsonObject json_obj = value.toObject();
//qLog(Debug) << json_obj;
int album_id(0); if (!json_obj.contains("id") || !json_obj.contains("name")) {
QString album(""); qLog(Error) << "Tidal: Invalid Json reply, item missing type or album.";
if (json_obj.contains("type")) { qLog(Debug) << json_obj;
// This was a albums search continue;
}
int artist_id = json_obj["id"].toInt();
if (requests_artist_albums_.contains(artist_id)) continue;
requests_artist_albums_.append(artist_id);
GetAlbums(artist_id);
artist_albums_requested_++;
if (artist_albums_requested_ >= artistssearchlimit_) break;
}
if (artist_albums_requested_ > 0) {
emit UpdateStatus(QString("Retrieving albums for %1 artist%2...").arg(artist_albums_requested_).arg(artist_albums_requested_ == 1 ? "" : "s"));
emit ProgressSetMaximum(artist_albums_requested_);
emit UpdateProgress(0);
}
CheckFinish();
}
void TidalService::GetAlbums(const int artist_id, const int offset) {
QList<Param> parameters;
if (offset > 0) parameters << Param("offset", QString::number(offset));
QNetworkReply *reply = CreateRequest(QString("artists/%1/albums").arg(artist_id), parameters);
NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumsReceived(QNetworkReply*, int, int, int)), reply, search_id_, artist_id, offset);
}
void TidalService::AlbumsReceived(QNetworkReply *reply, int search_id, int artist_id, int offset_requested) {
reply->deleteLater();
if (search_id != search_id_) return;
if (artist_search_) {
if (!requests_artist_albums_.contains(artist_id)) return;
artist_albums_received_++;
emit UpdateProgress(artist_albums_received_);
}
QByteArray data = GetReplyData(reply, true);
if (data.isEmpty()) {
AlbumsFinished(artist_id, offset_requested);
return;
}
QJsonObject json_obj = ExtractJsonObj(data);
if (json_obj.isEmpty()) {
AlbumsFinished(artist_id, offset_requested);
return;
}
int limit = 0;
int offset = 0;
int total_albums = 0;
if (artist_search_) { // This was a list of albums by artist
if (!json_obj.contains("limit") ||
!json_obj.contains("offset") ||
!json_obj.contains("totalNumberOfItems") ||
!json_obj.contains("items")) {
AlbumsFinished(artist_id, offset_requested);
Error("Json object missing values.", json_obj);
return;
}
limit = json_obj["limit"].toInt();
offset = json_obj["offset"].toInt();
total_albums = json_obj["totalNumberOfItems"].toInt();
if (offset != offset_requested) {
AlbumsFinished(artist_id, offset_requested, total_albums, limit);
Error(QString("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested));
return;
}
}
QJsonValue json_value = ExtractItems(json_obj);
if (!json_value.isArray()) {
AlbumsFinished(artist_id, offset_requested, total_albums, limit);
return;
}
QJsonArray json_items = json_value.toArray();
if (json_items.isEmpty()) {
if (!artist_search_) Error("No match.");
AlbumsFinished(artist_id, offset_requested, total_albums, limit);
return;
}
int albums = 0;
for (const QJsonValue &value : json_items) {
albums++;
if (!value.isObject()) {
qLog(Error) << "Tidal: Invalid Json reply, item not a object.";
qLog(Debug) << value;
continue;
}
QJsonObject json_obj = value.toObject();
int album_id = 0;
QString album;
if (json_obj.contains("type")) { // This was a albums search
if (!json_obj.contains("id") || !json_obj.contains("title")) { if (!json_obj.contains("id") || !json_obj.contains("title")) {
qLog(Error) << "Tidal: Invalid Json reply, item is missing ID or title."; qLog(Error) << "Tidal: Invalid Json reply, item is missing ID or title.";
qLog(Debug) << json_obj; qLog(Debug) << json_obj;
@ -568,8 +707,7 @@ void TidalService::SearchFinished(QNetworkReply *reply, int id) {
album_id = json_obj["id"].toInt(); album_id = json_obj["id"].toInt();
album = json_obj["title"].toString(); album = json_obj["title"].toString();
} }
else if (json_obj.contains("album")) { else if (json_obj.contains("album")) { // This was a tracks search
// This was a tracks search
if (!fetchalbums_) { if (!fetchalbums_) {
Song song = ParseSong(0, value); Song song = ParseSong(0, value);
songs_ << song; songs_ << song;
@ -589,6 +727,7 @@ void TidalService::SearchFinished(QNetworkReply *reply, int id) {
} }
album_id = json_album["id"].toInt(); album_id = json_album["id"].toInt();
album = json_album["title"].toString(); album = json_album["title"].toString();
} }
else { else {
qLog(Error) << "Tidal: Invalid Json reply, item missing type or album."; qLog(Error) << "Tidal: Invalid Json reply, item missing type or album.";
@ -596,7 +735,7 @@ void TidalService::SearchFinished(QNetworkReply *reply, int id) {
continue; continue;
} }
if (requests_album_.contains(album_id)) continue; if (requests_album_songs_.contains(album_id)) continue;
if (!json_obj.contains("artist") || !json_obj.contains("title") || !json_obj.contains("audioQuality")) { if (!json_obj.contains("artist") || !json_obj.contains("title") || !json_obj.contains("audioQuality")) {
qLog(Error) << "Tidal: Invalid Json reply, item missing artist, title or audioQuality."; qLog(Error) << "Tidal: Invalid Json reply, item missing artist, title or audioQuality.";
@ -622,45 +761,77 @@ void TidalService::SearchFinished(QNetworkReply *reply, int id) {
//qLog(Debug) << "Tidal:" << artist << album << quality << copyright; //qLog(Debug) << "Tidal:" << artist << album << quality << copyright;
QString artist_album(QString("%1-%2").arg(artist).arg(album)); QPair<QString,QString> artist_album(artist.toLower(), album.toLower());
if (albums.contains(artist_album)) { if (requests_artist_album_.contains(artist_album)) {
qLog(Debug) << "Tidal: Skipping duplicate album" << artist << album << quality << copyright; qLog(Debug) << "Tidal: Skipping duplicate album" << artist << album << quality << copyright;
continue; continue;
} }
albums.insert(0, artist_album); requests_artist_album_.append(artist_album);
requests_album_.insert(album_id, album_id); requests_album_songs_.insert(album_id, artist);
GetAlbum(album_id); album_songs_requested_++;
albums_requested_++; if (album_songs_requested_ >= albumssearchlimit_) break;
if (albums_requested_ >= albumssearchlimit_) break;
} }
if (albums_requested_ > 0) { AlbumsFinished(artist_id, offset_requested, total_albums, limit, albums);
emit UpdateStatus(QString("Retrieving %1 album%2...").arg(albums_requested_).arg(albums_requested_ == 1 ? "" : "s"));
emit ProgressSetMaximum(albums_requested_); }
emit UpdateProgress(0);
void TidalService::AlbumsFinished(const int artist_id, const int offset_requested, const int total_albums, const int limit, const int albums) {
if (artist_search_) { // This is a artist search.
if (albums > limit) {
Error("Albums returned does not match limit returned!");
}
int offset_next = offset_requested + albums;
if (album_songs_requested_ < albumssearchlimit_ && offset_next < total_albums) {
GetAlbums(artist_id, offset_next);
artist_albums_requested_++;
}
else if (artist_albums_received_ >= artist_albums_requested_) { // Artist search is finished.
artist_search_ = false;
}
}
if (!artist_search_) {
// Get songs for the albums.
QHashIterator<int, QString> i(requests_album_songs_);
while (i.hasNext()) {
i.next();
GetSongs(i.key());
}
if (album_songs_requested_ > 0) {
emit UpdateStatus(QString("Retrieving songs for %1 album%2...").arg(album_songs_requested_).arg(album_songs_requested_ == 1 ? "" : "s"));
emit ProgressSetMaximum(album_songs_requested_);
emit UpdateProgress(0);
}
} }
CheckFinish(); CheckFinish();
} }
void TidalService::GetAlbum(const int album_id) { void TidalService::GetSongs(const int album_id) {
QList<Param> parameters; QList<Param> parameters;
QNetworkReply *reply = CreateRequest(QString("albums/%1/tracks").arg(album_id), parameters); QNetworkReply *reply = CreateRequest(QString("albums/%1/tracks").arg(album_id), parameters);
NewClosure(reply, SIGNAL(finished()), this, SLOT(GetAlbumFinished(QNetworkReply*, int, int)), reply, search_id_, album_id); NewClosure(reply, SIGNAL(finished()), this, SLOT(SongsReceived(QNetworkReply*, int, int)), reply, search_id_, album_id);
} }
void TidalService::GetAlbumFinished(QNetworkReply *reply, int search_id, int album_id) { void TidalService::SongsReceived(QNetworkReply *reply, int search_id, int album_id) {
reply->deleteLater(); reply->deleteLater();
if (search_id != search_id_) return; if (search_id != search_id_) return;
if (!requests_album_.contains(album_id)) return; if (!requests_album_songs_.contains(album_id)) return;
albums_received_++; QString album_artist = requests_album_songs_[album_id];
emit UpdateProgress(albums_received_);
album_songs_received_++;
if (!artist_search_) {
emit UpdateProgress(album_songs_received_);
}
QByteArray data = GetReplyData(reply); QByteArray data = GetReplyData(reply);
if (data.isEmpty()) { if (data.isEmpty()) {
@ -682,14 +853,12 @@ void TidalService::GetAlbumFinished(QNetworkReply *reply, int search_id, int alb
bool compilation = false; bool compilation = false;
bool multidisc = false; bool multidisc = false;
Song first_song;
SongList songs; SongList songs;
for (const QJsonValue &value : json_items) { for (const QJsonValue &value : json_items) {
Song song = ParseSong(album_id, value); Song song = ParseSong(album_id, value, album_artist);
if (!song.is_valid()) continue; if (!song.is_valid()) continue;
if (song.disc() >= 2) multidisc = true; if (song.disc() >= 2) multidisc = true;
if (song.is_compilation() || (first_song.is_valid() && song.artist() != first_song.artist())) compilation = true; if (song.is_compilation()) compilation = true;
if (!first_song.is_valid()) first_song = song;
songs << song; songs << song;
} }
for (Song &song : songs) { for (Song &song : songs) {
@ -705,7 +874,7 @@ void TidalService::GetAlbumFinished(QNetworkReply *reply, int search_id, int alb
} }
Song TidalService::ParseSong(const int album_id_requested, const QJsonValue &value) { Song TidalService::ParseSong(const int album_id_requested, const QJsonValue &value, QString album_artist) {
Song song; Song song;
@ -716,8 +885,6 @@ Song TidalService::ParseSong(const int album_id_requested, const QJsonValue &val
} }
QJsonObject json_obj = value.toObject(); QJsonObject json_obj = value.toObject();
//qLog(Debug) << json_obj;
if ( if (
!json_obj.contains("album") || !json_obj.contains("album") ||
!json_obj.contains("allowStreaming") || !json_obj.contains("allowStreaming") ||
@ -786,7 +953,6 @@ Song TidalService::ParseSong(const int album_id_requested, const QJsonValue &val
if (!allow_streaming || !stream_ready) { if (!allow_streaming || !stream_ready) {
qLog(Error) << "Tidal: Skipping song" << artist << album << title << "because allowStreaming is false OR streamReady is false."; qLog(Error) << "Tidal: Skipping song" << artist << album << title << "because allowStreaming is false OR streamReady is false.";
//qLog(Debug) << json_obj;
return song; return song;
} }
@ -798,8 +964,9 @@ Song TidalService::ParseSong(const int album_id_requested, const QJsonValue &val
song.set_source(Song::Source_Tidal); song.set_source(Song::Source_Tidal);
song.set_id(song_id); song.set_id(song_id);
song.set_album_id(album_id); song.set_album_id(album_id);
song.set_artist(artist); if (album_artist != artist) song.set_albumartist(album_artist);
song.set_album(album); song.set_album(album);
song.set_artist(artist);
song.set_title(title); song.set_title(title);
song.set_track(track); song.set_track(track);
song.set_disc(disc); song.set_disc(disc);
@ -836,11 +1003,11 @@ void TidalService::GetStreamURL(const QUrl &url) {
QNetworkReply *reply = CreateRequest(QString("tracks/%1/streamUrl").arg(song_id), parameters); QNetworkReply *reply = CreateRequest(QString("tracks/%1/streamUrl").arg(song_id), parameters);
NewClosure(reply, SIGNAL(finished()), this, SLOT(GetStreamURLFinished(QNetworkReply*, int, QUrl)), reply, song_id, url); NewClosure(reply, SIGNAL(finished()), this, SLOT(StreamURLReceived(QNetworkReply*, int, QUrl)), reply, song_id, url);
} }
void TidalService::GetStreamURLFinished(QNetworkReply *reply, const int song_id, const QUrl original_url) { void TidalService::StreamURLReceived(QNetworkReply *reply, const int song_id, const QUrl original_url) {
reply->deleteLater(); reply->deleteLater();
if (requests_song_.contains(song_id)) requests_song_.remove(song_id); if (requests_song_.contains(song_id)) requests_song_.remove(song_id);
@ -892,7 +1059,7 @@ void TidalService::CheckFinish() {
if (search_id_ == 0) return; if (search_id_ == 0) return;
if (!login_sent_ && albums_requested_ <= albums_received_) { if (!login_sent_ && !artist_search_ && artist_albums_requested_ <= artist_albums_received_ && album_songs_requested_ <= album_songs_received_) {
if (songs_.isEmpty()) { if (songs_.isEmpty()) {
if (search_error_.isEmpty()) emit SearchError(search_id_, "Unknown error"); if (search_error_.isEmpty()) emit SearchError(search_id_, "Unknown error");
else emit SearchError(search_id_, search_error_); else emit SearchError(search_id_, search_error_);

View File

@ -24,6 +24,7 @@
#include <QtGlobal> #include <QtGlobal>
#include <QObject> #include <QObject>
#include <QList>
#include <QHash> #include <QHash>
#include <QString> #include <QString>
#include <QUrl> #include <QUrl>
@ -54,7 +55,7 @@ class TidalService : public InternetService {
void ReloadSettings(); void ReloadSettings();
void Logout(); void Logout();
int Search(const QString &query, InternetSearch::SearchBy searchby); int Search(const QString &query, InternetSearch::SearchType type);
void CancelSearch(); void CancelSearch();
const bool login_sent() { return login_sent_; } const bool login_sent() { return login_sent_; }
@ -84,20 +85,29 @@ class TidalService : public InternetService {
void HandleAuthReply(QNetworkReply *reply); void HandleAuthReply(QNetworkReply *reply);
void ResetLoginAttempts(); void ResetLoginAttempts();
void StartSearch(); void StartSearch();
void SearchFinished(QNetworkReply *reply, int search_id); void ArtistsReceived(QNetworkReply *reply, int search_id);
void GetAlbumFinished(QNetworkReply *reply, int search_id, int album_id); void AlbumsReceived(QNetworkReply *reply, int search_id, int artist_id, int offset_requested = 0);
void GetStreamURLFinished(QNetworkReply *reply, const int song_id, const QUrl original_url); void AlbumsFinished(const int artist_id, const int offset_requested, const int total_albums = 0, const int limit = 0, const int albums = 0);
void SongsReceived(QNetworkReply *reply, int search_id, int album_id);
void StreamURLReceived(QNetworkReply *reply, const int song_id, const QUrl original_url);
private: private:
typedef QPair<QString, QString> Param;
void ClearSearch(); void ClearSearch();
void LoadSessionID(); void LoadSessionID();
QNetworkReply *CreateRequest(const QString &ressource_name, const QList<QPair<QString, QString>> &params); QNetworkReply *CreateRequest(const QString &ressource_name, const QList<QPair<QString, QString>> &params);
QByteArray GetReplyData(QNetworkReply *reply, const bool sendlogin = false); QByteArray GetReplyData(QNetworkReply *reply, const bool sendlogin = false);
QJsonObject ExtractJsonObj(QByteArray &data); QJsonObject ExtractJsonObj(QByteArray &data);
QJsonValue ExtractItems(QByteArray &data); QJsonValue ExtractItems(QByteArray &data);
QJsonValue ExtractItems(QJsonObject &json_obj);
void SendSearch(); void SendSearch();
void GetAlbum(const int album_id); void SendArtistsSearch();
Song ParseSong(const int album_id_requested, const QJsonValue &value); void SendAlbumsSearch();
void SendSongsSearch();
void GetAlbums(const int artist_id, const int offset = 0);
void GetSongs(const int album_id);
Song ParseSong(const int album_id_requested, const QJsonValue &value, QString album_artist = QString());
void CheckFinish(); void CheckFinish();
void Error(QString error, QVariant debug = QVariant()); void Error(QString error, QVariant debug = QVariant());
@ -110,13 +120,14 @@ class TidalService : public InternetService {
NetworkAccessManager *network_; NetworkAccessManager *network_;
TidalUrlHandler *url_handler_; TidalUrlHandler *url_handler_;
QTimer *timer_searchdelay_; QTimer *timer_search_delay_;
QTimer *timer_login_attempt_; QTimer *timer_login_attempt_;
QString username_; QString username_;
QString password_; QString password_;
QString quality_; QString quality_;
int searchdelay_; int search_delay_;
int artistssearchlimit_;
int albumssearchlimit_; int albumssearchlimit_;
int songssearchlimit_; int songssearchlimit_;
bool fetchalbums_; bool fetchalbums_;
@ -130,14 +141,19 @@ class TidalService : public InternetService {
int pending_search_id_; int pending_search_id_;
int next_pending_search_id_; int next_pending_search_id_;
QString pending_search_text_; QString pending_search_text_;
InternetSearch::SearchBy pending_searchby_; InternetSearch::SearchType pending_search_type_;
int search_id_; int search_id_;
QString search_text_; QString search_text_;
QHash<int, int> requests_album_; bool artist_search_;
QList<int> requests_artist_albums_;
QHash<int, QString> requests_album_songs_;
QHash<int, QUrl> requests_song_; QHash<int, QUrl> requests_song_;
int albums_requested_; QList<QPair<QString, QString>> requests_artist_album_;
int albums_received_; int artist_albums_requested_;
int artist_albums_received_;
int album_songs_requested_;
int album_songs_received_;
SongList songs_; SongList songs_;
QString search_error_; QString search_error_;
bool login_sent_; bool login_sent_;