diff --git a/data/data.qrc b/data/data.qrc index 4c858a706..f447b5c36 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -4,6 +4,7 @@ schema/schema-1.sql schema/schema-2.sql schema/schema-3.sql + schema/schema-4.sql schema/device-schema.sql style/strawberry.css html/playing-tooltip-plain.html diff --git a/data/schema/schema-4.sql b/data/schema/schema-4.sql new file mode 100644 index 000000000..3d8ff0f2e --- /dev/null +++ b/data/schema/schema-4.sql @@ -0,0 +1,205 @@ +CREATE TABLE IF NOT EXISTS tidal_artists_songs ( + + title TEXT NOT NULL, + album TEXT NOT NULL, + artist TEXT NOT NULL, + albumartist TEXT NOT NULL, + track INTEGER NOT NULL DEFAULT -1, + disc INTEGER NOT NULL DEFAULT -1, + year INTEGER NOT NULL DEFAULT -1, + originalyear INTEGER NOT NULL DEFAULT 0, + genre TEXT NOT NULL, + compilation INTEGER NOT NULL DEFAULT -1, + composer TEXT NOT NULL, + performer TEXT NOT NULL, + grouping TEXT NOT NULL, + comment TEXT NOT NULL, + lyrics TEXT NOT NULL, + + beginning INTEGER NOT NULL DEFAULT 0, + length INTEGER NOT NULL DEFAULT 0, + + bitrate INTEGER NOT NULL DEFAULT 0, + samplerate INTEGER NOT NULL DEFAULT 0, + bitdepth INTEGER NOT NULL DEFAULT 0, + + source INTEGER NOT NULL DEFAULT 0, + directory_id INTEGER NOT NULL, + filename TEXT NOT NULL, + filetype INTEGER NOT NULL DEFAULT 0, + filesize INTEGER NOT NULL DEFAULT 0, + mtime INTEGER NOT NULL DEFAULT 0, + ctime INTEGER NOT NULL DEFAULT 0, + unavailable INTEGER DEFAULT 0, + + playcount INTEGER NOT NULL DEFAULT 0, + skipcount INTEGER NOT NULL DEFAULT 0, + lastplayed INTEGER NOT NULL DEFAULT 0, + + compilation_detected INTEGER DEFAULT 0, + compilation_on INTEGER NOT NULL DEFAULT 0, + compilation_off INTEGER NOT NULL DEFAULT 0, + compilation_effective INTEGER NOT NULL DEFAULT 0, + + art_automatic TEXT, + art_manual TEXT, + + effective_albumartist TEXT, + effective_originalyear INTEGER NOT NULL DEFAULT 0, + + cue_path TEXT + +); + +CREATE TABLE IF NOT EXISTS tidal_albums_songs ( + + title TEXT NOT NULL, + album TEXT NOT NULL, + artist TEXT NOT NULL, + albumartist TEXT NOT NULL, + track INTEGER NOT NULL DEFAULT -1, + disc INTEGER NOT NULL DEFAULT -1, + year INTEGER NOT NULL DEFAULT -1, + originalyear INTEGER NOT NULL DEFAULT 0, + genre TEXT NOT NULL, + compilation INTEGER NOT NULL DEFAULT -1, + composer TEXT NOT NULL, + performer TEXT NOT NULL, + grouping TEXT NOT NULL, + comment TEXT NOT NULL, + lyrics TEXT NOT NULL, + + beginning INTEGER NOT NULL DEFAULT 0, + length INTEGER NOT NULL DEFAULT 0, + + bitrate INTEGER NOT NULL DEFAULT 0, + samplerate INTEGER NOT NULL DEFAULT 0, + bitdepth INTEGER NOT NULL DEFAULT 0, + + source INTEGER NOT NULL DEFAULT 0, + directory_id INTEGER NOT NULL, + filename TEXT NOT NULL, + filetype INTEGER NOT NULL DEFAULT 0, + filesize INTEGER NOT NULL DEFAULT 0, + mtime INTEGER NOT NULL DEFAULT 0, + ctime INTEGER NOT NULL DEFAULT 0, + unavailable INTEGER DEFAULT 0, + + playcount INTEGER NOT NULL DEFAULT 0, + skipcount INTEGER NOT NULL DEFAULT 0, + lastplayed INTEGER NOT NULL DEFAULT 0, + + compilation_detected INTEGER DEFAULT 0, + compilation_on INTEGER NOT NULL DEFAULT 0, + compilation_off INTEGER NOT NULL DEFAULT 0, + compilation_effective INTEGER NOT NULL DEFAULT 0, + + art_automatic TEXT, + art_manual TEXT, + + effective_albumartist TEXT, + effective_originalyear INTEGER NOT NULL DEFAULT 0, + + cue_path TEXT + +); + +CREATE TABLE IF NOT EXISTS tidal_songs ( + + title TEXT NOT NULL, + album TEXT NOT NULL, + artist TEXT NOT NULL, + albumartist TEXT NOT NULL, + track INTEGER NOT NULL DEFAULT -1, + disc INTEGER NOT NULL DEFAULT -1, + year INTEGER NOT NULL DEFAULT -1, + originalyear INTEGER NOT NULL DEFAULT 0, + genre TEXT NOT NULL, + compilation INTEGER NOT NULL DEFAULT -1, + composer TEXT NOT NULL, + performer TEXT NOT NULL, + grouping TEXT NOT NULL, + comment TEXT NOT NULL, + lyrics TEXT NOT NULL, + + beginning INTEGER NOT NULL DEFAULT 0, + length INTEGER NOT NULL DEFAULT 0, + + bitrate INTEGER NOT NULL DEFAULT 0, + samplerate INTEGER NOT NULL DEFAULT 0, + bitdepth INTEGER NOT NULL DEFAULT 0, + + source INTEGER NOT NULL DEFAULT 0, + directory_id INTEGER NOT NULL, + filename TEXT NOT NULL, + filetype INTEGER NOT NULL DEFAULT 0, + filesize INTEGER NOT NULL DEFAULT 0, + mtime INTEGER NOT NULL DEFAULT 0, + ctime INTEGER NOT NULL DEFAULT 0, + unavailable INTEGER DEFAULT 0, + + playcount INTEGER NOT NULL DEFAULT 0, + skipcount INTEGER NOT NULL DEFAULT 0, + lastplayed INTEGER NOT NULL DEFAULT 0, + + compilation_detected INTEGER DEFAULT 0, + compilation_on INTEGER NOT NULL DEFAULT 0, + compilation_off INTEGER NOT NULL DEFAULT 0, + compilation_effective INTEGER NOT NULL DEFAULT 0, + + art_automatic TEXT, + art_manual TEXT, + + effective_albumartist TEXT, + effective_originalyear INTEGER NOT NULL DEFAULT 0, + + cue_path TEXT + +); + +CREATE VIRTUAL TABLE IF NOT EXISTS tidal_artists_songs_fts USING fts3( + + ftstitle, + ftsalbum, + ftsartist, + ftsalbumartist, + ftscomposer, + ftsperformer, + ftsgrouping, + ftsgenre, + ftscomment, + tokenize=unicode + +); + +CREATE VIRTUAL TABLE IF NOT EXISTS tidal_albums_songs_fts USING fts3( + + ftstitle, + ftsalbum, + ftsartist, + ftsalbumartist, + ftscomposer, + ftsperformer, + ftsgrouping, + ftsgenre, + ftscomment, + tokenize=unicode + +); + +CREATE VIRTUAL TABLE IF NOT EXISTS tidal_songs_fts USING fts3( + + ftstitle, + ftsalbum, + ftsartist, + ftsalbumartist, + ftscomposer, + ftsperformer, + ftsgrouping, + ftsgenre, + ftscomment, + tokenize=unicode + +); + +UPDATE schema_version SET version=4; diff --git a/data/schema/schema.sql b/data/schema/schema.sql index 942e7cff6..d2a2db337 100644 --- a/data/schema/schema.sql +++ b/data/schema/schema.sql @@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version ( DELETE FROM schema_version; -INSERT INTO schema_version (version) VALUES (3); +INSERT INTO schema_version (version) VALUES (4); CREATE TABLE IF NOT EXISTS directories ( path TEXT NOT NULL, @@ -46,9 +46,168 @@ CREATE TABLE IF NOT EXISTS songs ( directory_id INTEGER NOT NULL, filename TEXT NOT NULL, filetype INTEGER NOT NULL DEFAULT 0, - filesize INTEGER NOT NULL, - mtime INTEGER NOT NULL, - ctime INTEGER NOT NULL, + filesize INTEGER NOT NULL DEFAULT 0, + mtime INTEGER NOT NULL DEFAULT 0, + ctime INTEGER NOT NULL DEFAULT 0, + unavailable INTEGER DEFAULT 0, + + playcount INTEGER NOT NULL DEFAULT 0, + skipcount INTEGER NOT NULL DEFAULT 0, + lastplayed INTEGER NOT NULL DEFAULT 0, + + compilation_detected INTEGER DEFAULT 0, + compilation_on INTEGER NOT NULL DEFAULT 0, + compilation_off INTEGER NOT NULL DEFAULT 0, + compilation_effective INTEGER NOT NULL DEFAULT 0, + + art_automatic TEXT, + art_manual TEXT, + + effective_albumartist TEXT, + effective_originalyear INTEGER NOT NULL DEFAULT 0, + + cue_path TEXT + +); + +CREATE TABLE IF NOT EXISTS tidal_artists_songs ( + + title TEXT NOT NULL, + album TEXT NOT NULL, + artist TEXT NOT NULL, + albumartist TEXT NOT NULL, + track INTEGER NOT NULL DEFAULT -1, + disc INTEGER NOT NULL DEFAULT -1, + year INTEGER NOT NULL DEFAULT -1, + originalyear INTEGER NOT NULL DEFAULT 0, + genre TEXT NOT NULL, + compilation INTEGER NOT NULL DEFAULT -1, + composer TEXT NOT NULL, + performer TEXT NOT NULL, + grouping TEXT NOT NULL, + comment TEXT NOT NULL, + lyrics TEXT NOT NULL, + + beginning INTEGER NOT NULL DEFAULT 0, + length INTEGER NOT NULL DEFAULT 0, + + bitrate INTEGER NOT NULL DEFAULT 0, + samplerate INTEGER NOT NULL DEFAULT 0, + bitdepth INTEGER NOT NULL DEFAULT 0, + + source INTEGER NOT NULL DEFAULT 0, + directory_id INTEGER NOT NULL, + filename TEXT NOT NULL, + filetype INTEGER NOT NULL DEFAULT 0, + filesize INTEGER NOT NULL DEFAULT 0, + mtime INTEGER NOT NULL DEFAULT 0, + ctime INTEGER NOT NULL DEFAULT 0, + unavailable INTEGER DEFAULT 0, + + playcount INTEGER NOT NULL DEFAULT 0, + skipcount INTEGER NOT NULL DEFAULT 0, + lastplayed INTEGER NOT NULL DEFAULT 0, + + compilation_detected INTEGER DEFAULT 0, + compilation_on INTEGER NOT NULL DEFAULT 0, + compilation_off INTEGER NOT NULL DEFAULT 0, + compilation_effective INTEGER NOT NULL DEFAULT 0, + + art_automatic TEXT, + art_manual TEXT, + + effective_albumartist TEXT, + effective_originalyear INTEGER NOT NULL DEFAULT 0, + + cue_path TEXT + +); + +CREATE TABLE IF NOT EXISTS tidal_albums_songs ( + + title TEXT NOT NULL, + album TEXT NOT NULL, + artist TEXT NOT NULL, + albumartist TEXT NOT NULL, + track INTEGER NOT NULL DEFAULT -1, + disc INTEGER NOT NULL DEFAULT -1, + year INTEGER NOT NULL DEFAULT -1, + originalyear INTEGER NOT NULL DEFAULT 0, + genre TEXT NOT NULL, + compilation INTEGER NOT NULL DEFAULT -1, + composer TEXT NOT NULL, + performer TEXT NOT NULL, + grouping TEXT NOT NULL, + comment TEXT NOT NULL, + lyrics TEXT NOT NULL, + + beginning INTEGER NOT NULL DEFAULT 0, + length INTEGER NOT NULL DEFAULT 0, + + bitrate INTEGER NOT NULL DEFAULT 0, + samplerate INTEGER NOT NULL DEFAULT 0, + bitdepth INTEGER NOT NULL DEFAULT 0, + + source INTEGER NOT NULL DEFAULT 0, + directory_id INTEGER NOT NULL, + filename TEXT NOT NULL, + filetype INTEGER NOT NULL DEFAULT 0, + filesize INTEGER NOT NULL DEFAULT 0, + mtime INTEGER NOT NULL DEFAULT 0, + ctime INTEGER NOT NULL DEFAULT 0, + unavailable INTEGER DEFAULT 0, + + playcount INTEGER NOT NULL DEFAULT 0, + skipcount INTEGER NOT NULL DEFAULT 0, + lastplayed INTEGER NOT NULL DEFAULT 0, + + compilation_detected INTEGER DEFAULT 0, + compilation_on INTEGER NOT NULL DEFAULT 0, + compilation_off INTEGER NOT NULL DEFAULT 0, + compilation_effective INTEGER NOT NULL DEFAULT 0, + + art_automatic TEXT, + art_manual TEXT, + + effective_albumartist TEXT, + effective_originalyear INTEGER NOT NULL DEFAULT 0, + + cue_path TEXT + +); + +CREATE TABLE IF NOT EXISTS tidal_songs ( + + title TEXT NOT NULL, + album TEXT NOT NULL, + artist TEXT NOT NULL, + albumartist TEXT NOT NULL, + track INTEGER NOT NULL DEFAULT -1, + disc INTEGER NOT NULL DEFAULT -1, + year INTEGER NOT NULL DEFAULT -1, + originalyear INTEGER NOT NULL DEFAULT 0, + genre TEXT NOT NULL, + compilation INTEGER NOT NULL DEFAULT -1, + composer TEXT NOT NULL, + performer TEXT NOT NULL, + grouping TEXT NOT NULL, + comment TEXT NOT NULL, + lyrics TEXT NOT NULL, + + beginning INTEGER NOT NULL DEFAULT 0, + length INTEGER NOT NULL DEFAULT 0, + + bitrate INTEGER NOT NULL DEFAULT 0, + samplerate INTEGER NOT NULL DEFAULT 0, + bitdepth INTEGER NOT NULL DEFAULT 0, + + source INTEGER NOT NULL DEFAULT 0, + directory_id INTEGER NOT NULL, + filename TEXT NOT NULL, + filetype INTEGER NOT NULL DEFAULT 0, + filesize INTEGER NOT NULL DEFAULT 0, + mtime INTEGER NOT NULL DEFAULT 0, + ctime INTEGER NOT NULL DEFAULT 0, unavailable INTEGER DEFAULT 0, playcount INTEGER NOT NULL DEFAULT 0, @@ -174,6 +333,51 @@ CREATE VIRTUAL TABLE IF NOT EXISTS songs_fts USING fts3( ); +CREATE VIRTUAL TABLE IF NOT EXISTS tidal_artists_songs_fts USING fts3( + + ftstitle, + ftsalbum, + ftsartist, + ftsalbumartist, + ftscomposer, + ftsperformer, + ftsgrouping, + ftsgenre, + ftscomment, + tokenize=unicode + +); + +CREATE VIRTUAL TABLE IF NOT EXISTS tidal_albums_songs_fts USING fts3( + + ftstitle, + ftsalbum, + ftsartist, + ftsalbumartist, + ftscomposer, + ftsperformer, + ftsgrouping, + ftsgenre, + ftscomment, + tokenize=unicode + +); + +CREATE VIRTUAL TABLE IF NOT EXISTS tidal_songs_fts USING fts3( + + ftstitle, + ftsalbum, + ftsartist, + ftsalbumartist, + ftscomposer, + ftsperformer, + ftsgrouping, + ftsgenre, + ftscomment, + tokenize=unicode + +); + CREATE VIRTUAL TABLE IF NOT EXISTS playlist_items_fts_ USING fts3( ftstitle, diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e78d84773..ca0218edf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -138,6 +138,7 @@ set(SOURCES collection/collectionbackend.cpp collection/collectionwatcher.cpp collection/collectionview.cpp + collection/collectionitemdelegate.cpp collection/collectionviewcontainer.cpp collection/collectiondirectorymodel.cpp collection/collectionfilterwidget.cpp @@ -263,6 +264,10 @@ set(SOURCES internet/internetsearchsortmodel.cpp internet/internetsearchitemdelegate.cpp internet/localredirectserver.cpp + internet/internettabsview.cpp + internet/internetcollectionview.cpp + internet/internetcollectionviewcontainer.cpp + internet/internetsearchview.cpp scrobbler/audioscrobbler.cpp scrobbler/scrobblerservices.cpp @@ -322,6 +327,7 @@ set(HEADERS collection/collectionbackend.h collection/collectionwatcher.h collection/collectionview.h + collection/collectionitemdelegate.h collection/collectionviewcontainer.h collection/collectiondirectorymodel.h collection/collectionfilterwidget.h @@ -437,6 +443,10 @@ set(HEADERS internet/internetsearchview.h internet/internetsearchmodel.h internet/localredirectserver.h + internet/internettabsview.h + internet/internetcollectionview.h + internet/internetcollectionviewcontainer.h + internet/internetsearchview.h scrobbler/audioscrobbler.h scrobbler/scrobblerservices.h @@ -502,6 +512,8 @@ set(UI widgets/fileview.ui widgets/loginstatewidget.ui + internet/internettabsview.ui + internet/internetcollectionviewcontainer.ui internet/internetsearchview.ui organise/organisedialog.ui @@ -875,11 +887,17 @@ optional_source(HAVE_TIDAL SOURCES tidal/tidalservice.cpp tidal/tidalurlhandler.cpp + tidal/tidalbaserequest.cpp + tidal/tidalrequest.cpp + tidal/tidalstreamurlrequest.cpp settings/tidalsettingspage.cpp covermanager/tidalcoverprovider.cpp HEADERS tidal/tidalservice.h tidal/tidalurlhandler.h + tidal/tidalbaserequest.h + tidal/tidalrequest.h + tidal/tidalstreamurlrequest.h settings/tidalsettingspage.h covermanager/tidalcoverprovider.h UI diff --git a/src/collection/collectionitemdelegate.cpp b/src/collection/collectionitemdelegate.cpp new file mode 100644 index 000000000..8b5b16955 --- /dev/null +++ b/src/collection/collectionitemdelegate.cpp @@ -0,0 +1,165 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "collectionitemdelegate.h" +#include "collectionmodel.h" + +CollectionItemDelegate::CollectionItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void CollectionItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt, const QModelIndex &index) const { + + const bool is_divider = index.data(CollectionModel::Role_IsDivider).toBool(); + + if (is_divider) { + QString text(index.data().toString()); + + painter->save(); + + QRect text_rect(opt.rect); + + // Does this item have an icon? + QPixmap pixmap; + QVariant decoration = index.data(Qt::DecorationRole); + if (!decoration.isNull()) { + if (decoration.canConvert()) { + pixmap = decoration.value(); + } + else if (decoration.canConvert()) { + pixmap = decoration.value().pixmap(opt.decorationSize); + } + } + + // Draw the icon at the left of the text rectangle + if (!pixmap.isNull()) { + QRect icon_rect(text_rect.topLeft(), opt.decorationSize); + const int padding = (text_rect.height() - icon_rect.height()) / 2; + icon_rect.adjust(padding, padding, padding, padding); + text_rect.moveLeft(icon_rect.right() + padding + 6); + + if (pixmap.size() != opt.decorationSize) { + pixmap = pixmap.scaled(opt.decorationSize, Qt::KeepAspectRatio); + } + + painter->drawPixmap(icon_rect, pixmap); + } + else { + text_rect.setLeft(text_rect.left() + 30); + } + + // Draw the text + QFont bold_font(opt.font); + bold_font.setBold(true); + + painter->setPen(opt.palette.color(QPalette::Text)); + painter->setFont(bold_font); + painter->drawText(text_rect, text); + + // Draw the line under the item + QColor line_color = opt.palette.color(QPalette::Text); + QLinearGradient grad_color(opt.rect.bottomLeft(), opt.rect.bottomRight()); + const double fade_start_end = (opt.rect.width()/3.0)/opt.rect.width(); + line_color.setAlphaF(0.0); + grad_color.setColorAt(0, line_color); + line_color.setAlphaF(0.5); + grad_color.setColorAt(fade_start_end, line_color); + grad_color.setColorAt(1.0 - fade_start_end, line_color); + line_color.setAlphaF(0.0); + grad_color.setColorAt(1, line_color); + painter->setPen(QPen(grad_color, 1)); + painter->drawLine(opt.rect.bottomLeft(), opt.rect.bottomRight()); + + painter->restore(); + } + else { + QStyledItemDelegate::paint(painter, opt, index); + } + +} + +bool CollectionItemDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) { + + Q_UNUSED(option); + + if (!event || !view) return false; + + QHelpEvent *he = static_cast(event); + QString text = displayText(index.data(), QLocale::system()); + + if (text.isEmpty() || !he) return false; + + switch (event->type()) { + case QEvent::ToolTip: { + + QSize real_text = sizeHint(option, index); + QRect displayed_text = view->visualRect(index); + bool is_elided = displayed_text.width() < real_text.width(); + + if (is_elided) { + QToolTip::showText(he->globalPos(), text, view); + } + else if (index.data(Qt::ToolTipRole).isValid()) { + // If the item has a tooltip text, display it + QString tooltip_text = index.data(Qt::ToolTipRole).toString(); + QToolTip::showText(he->globalPos(), tooltip_text, view); + } + else { + // in case that another text was previously displayed + QToolTip::hideText(); + } + return true; + } + + case QEvent::QueryWhatsThis: + return true; + + case QEvent::WhatsThis: + QWhatsThis::showText(he->globalPos(), text, view); + return true; + + default: + break; + } + return false; + +} + diff --git a/src/collection/collectionitemdelegate.h b/src/collection/collectionitemdelegate.h new file mode 100644 index 000000000..825e4a86f --- /dev/null +++ b/src/collection/collectionitemdelegate.h @@ -0,0 +1,46 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * 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 COLLECTIONITEMDELEGATE_H +#define COLLECTIONITEMDELEGATE_H + +#include "config.h" + +#include + +#include +#include +#include +#include + +class QHelpEvent; + +class CollectionItemDelegate : public QStyledItemDelegate { + Q_OBJECT + + public: + CollectionItemDelegate(QObject *parent); + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; + + public slots: + bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index); +}; + +#endif // COLLECTIONITEMDELEGATE_H diff --git a/src/collection/collectionmodel.cpp b/src/collection/collectionmodel.cpp index 74dd04062..2f4b30686 100644 --- a/src/collection/collectionmodel.cpp +++ b/src/collection/collectionmodel.cpp @@ -98,7 +98,7 @@ CollectionModel::CollectionModel(CollectionBackend *backend, Application *app, Q root_->lazy_loaded = true; - group_by_[0] = GroupBy_Artist; + group_by_[0] = GroupBy_AlbumArtist; group_by_[1] = GroupBy_Album; group_by_[2] = GroupBy_None; diff --git a/src/collection/collectionview.cpp b/src/collection/collectionview.cpp index 1d784dae7..676f95dba 100644 --- a/src/collection/collectionview.cpp +++ b/src/collection/collectionview.cpp @@ -20,8 +20,6 @@ #include "config.h" -#include - #include #include #include @@ -66,6 +64,7 @@ #include "collectiondirectorymodel.h" #include "collectionfilterwidget.h" #include "collectionitem.h" +#include "collectionitemdelegate.h" #include "collectionmodel.h" #include "collectionview.h" #ifndef Q_OS_WIN @@ -76,124 +75,7 @@ #include "organise/organisedialog.h" #include "settings/collectionsettingspage.h" -CollectionItemDelegate::CollectionItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {} - -void CollectionItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt, const QModelIndex &index) const { - - const bool is_divider = index.data(CollectionModel::Role_IsDivider).toBool(); - - if (is_divider) { - QString text(index.data().toString()); - - painter->save(); - - QRect text_rect(opt.rect); - - // Does this item have an icon? - QPixmap pixmap; - QVariant decoration = index.data(Qt::DecorationRole); - if (!decoration.isNull()) { - if (decoration.canConvert()) { - pixmap = decoration.value(); - } - else if (decoration.canConvert()) { - pixmap = decoration.value().pixmap(opt.decorationSize); - } - } - - // Draw the icon at the left of the text rectangle - if (!pixmap.isNull()) { - QRect icon_rect(text_rect.topLeft(), opt.decorationSize); - const int padding = (text_rect.height() - icon_rect.height()) / 2; - icon_rect.adjust(padding, padding, padding, padding); - text_rect.moveLeft(icon_rect.right() + padding + 6); - - if (pixmap.size() != opt.decorationSize) { - pixmap = pixmap.scaled(opt.decorationSize, Qt::KeepAspectRatio); - } - - painter->drawPixmap(icon_rect, pixmap); - } - else { - text_rect.setLeft(text_rect.left() + 30); - } - - // Draw the text - QFont bold_font(opt.font); - bold_font.setBold(true); - - painter->setPen(opt.palette.color(QPalette::Text)); - painter->setFont(bold_font); - painter->drawText(text_rect, text); - - // Draw the line under the item - QColor line_color = opt.palette.color(QPalette::Text); - QLinearGradient grad_color(opt.rect.bottomLeft(), opt.rect.bottomRight()); - const double fade_start_end = (opt.rect.width()/3.0)/opt.rect.width(); - line_color.setAlphaF(0.0); - grad_color.setColorAt(0, line_color); - line_color.setAlphaF(0.5); - grad_color.setColorAt(fade_start_end, line_color); - grad_color.setColorAt(1.0 - fade_start_end, line_color); - line_color.setAlphaF(0.0); - grad_color.setColorAt(1, line_color); - painter->setPen(QPen(grad_color, 1)); - painter->drawLine(opt.rect.bottomLeft(), opt.rect.bottomRight()); - - painter->restore(); - } - else { - QStyledItemDelegate::paint(painter, opt, index); - } - -} - -bool CollectionItemDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) { - - Q_UNUSED(option); - - if (!event || !view) return false; - - QHelpEvent *he = static_cast(event); - QString text = displayText(index.data(), QLocale::system()); - - if (text.isEmpty() || !he) return false; - - switch (event->type()) { - case QEvent::ToolTip: { - - QSize real_text = sizeHint(option, index); - QRect displayed_text = view->visualRect(index); - bool is_elided = displayed_text.width() < real_text.width(); - - if (is_elided) { - QToolTip::showText(he->globalPos(), text, view); - } - else if (index.data(Qt::ToolTipRole).isValid()) { - // If the item has a tooltip text, display it - QString tooltip_text = index.data(Qt::ToolTipRole).toString(); - QToolTip::showText(he->globalPos(), tooltip_text, view); - } - else { - // in case that another text was previously displayed - QToolTip::hideText(); - } - return true; - } - - case QEvent::QueryWhatsThis: - return true; - - case QEvent::WhatsThis: - QWhatsThis::showText(he->globalPos(), text, view); - return true; - - default: - break; - } - return false; - -} +using std::unique_ptr; CollectionView::CollectionView(QWidget *parent) : AutoExpandingTreeView(parent), diff --git a/src/collection/collectionview.h b/src/collection/collectionview.h index adfa3dc8b..d4b019dc7 100644 --- a/src/collection/collectionview.h +++ b/src/collection/collectionview.h @@ -54,16 +54,7 @@ class CollectionFilterWidget; class EditTagDialog; class OrganiseDialog; -class CollectionItemDelegate : public QStyledItemDelegate { - Q_OBJECT - - public: - CollectionItemDelegate(QObject *parent); - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; - - public slots: - bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index); -}; +using std::unique_ptr; class CollectionView : public AutoExpandingTreeView { Q_OBJECT @@ -173,4 +164,3 @@ class CollectionView : public AutoExpandingTreeView { }; #endif // COLLECTIONVIEW_H - diff --git a/src/core/database.cpp b/src/core/database.cpp index a59713a08..2981fb4f6 100644 --- a/src/core/database.cpp +++ b/src/core/database.cpp @@ -52,7 +52,7 @@ #include "scopedtransaction.h" const char *Database::kDatabaseFilename = "strawberry.db"; -const int Database::kSchemaVersion = 3; +const int Database::kSchemaVersion = 4; const char *Database::kMagicAllSongsTables = "%allsongstables"; int Database::sNextConnectionId = 1; diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index db8f1544f..437d8a6d6 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -140,7 +140,7 @@ #include "internet/internetservices.h" #include "internet/internetservice.h" -#include "internet/internetsearchview.h" +#include "internet/internettabsview.h" #include "scrobbler/audioscrobbler.h" @@ -208,7 +208,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co return dialog; }), #ifdef HAVE_TIDAL - tidal_search_view_(new InternetSearchView(app_, app_->tidal_search(), TidalSettingsPage::kSettingsGroup, SettingsDialog::Page_Tidal, this)), + tidal_view_(new InternetTabsView(app_, app->internet_services()->ServiceBySource(Song::Source_Tidal), app_->tidal_search(), TidalSettingsPage::kSettingsGroup, SettingsDialog::Page_Tidal, this)), #endif playlist_menu_(new QMenu(this)), playlist_add_to_another_(nullptr), @@ -262,7 +262,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co ui_->tabs->AddTab(device_view_, "devices", IconLoader::Load("device"), tr("Devices")); #endif #ifdef HAVE_TIDAL - ui_->tabs->AddTab(tidal_search_view_, "tidal", IconLoader::Load("tidal"), tr("Tidal")); + ui_->tabs->AddTab(tidal_view_, "tidal", IconLoader::Load("tidal"), tr("Tidal")); #endif // Add the playing widget to the fancy tab widget @@ -544,7 +544,10 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co collection_view_->filter()->AddMenuAction(collection_config_action); #ifdef HAVE_TIDAL - connect(tidal_search_view_, SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); + 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*))); #endif // Playlist menu @@ -855,9 +858,9 @@ void MainWindow::ReloadSettings() { bool enable_tidal = settings.value("enabled", false).toBool(); settings.endGroup(); if (enable_tidal) - ui_->tabs->EnableTab(tidal_search_view_); + ui_->tabs->EnableTab(tidal_view_); else - ui_->tabs->DisableTab(tidal_search_view_); + ui_->tabs->DisableTab(tidal_view_); #endif } @@ -876,7 +879,7 @@ void MainWindow::ReloadAllSettings() { album_cover_choice_controller_->ReloadSettings(); if (cover_manager_.get()) cover_manager_->ReloadSettings(); #ifdef HAVE_TIDAL - tidal_search_view_->ReloadSettings(); + tidal_view_->ReloadSettings(); #endif } diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 2b2aceb3e..8905ac6b6 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -91,7 +91,7 @@ class TranscodeDialog; #endif class Ui_MainWindow; class Windows7ThumbBar; -class InternetSearchView; +class InternetTabsView; class MainWindow : public QMainWindow, public PlatformInterface { Q_OBJECT @@ -308,7 +308,7 @@ signals: PlaylistItemList autocomplete_tag_items_; #endif - InternetSearchView *tidal_search_view_; + InternetTabsView *tidal_view_; QAction *collection_show_all_; QAction *collection_show_duplicates_; diff --git a/src/covermanager/tidalcoverprovider.cpp b/src/covermanager/tidalcoverprovider.cpp index f137db0a8..5976c343f 100644 --- a/src/covermanager/tidalcoverprovider.cpp +++ b/src/covermanager/tidalcoverprovider.cpp @@ -19,8 +19,7 @@ #include "config.h" -#include -#include +#include #include #include @@ -29,19 +28,18 @@ #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 "internet/internetservices.h" #include "settings/tidalsettingspage.h" #include "tidal/tidalservice.h" #include "albumcoverfetcher.h" diff --git a/src/covermanager/tidalcoverprovider.h b/src/covermanager/tidalcoverprovider.h index d77e89351..f5c2d0bca 100644 --- a/src/covermanager/tidalcoverprovider.h +++ b/src/covermanager/tidalcoverprovider.h @@ -32,7 +32,6 @@ #include #include #include -#include #include "coverprovider.h" diff --git a/src/device/deviceview.cpp b/src/device/deviceview.cpp index 51a35b3c8..b7bb11eb1 100644 --- a/src/device/deviceview.cpp +++ b/src/device/deviceview.cpp @@ -61,7 +61,7 @@ #include "organise/organiseerrordialog.h" #include "collection/collectiondirectorymodel.h" #include "collection/collectionmodel.h" -#include "collection/collectionview.h" +#include "collection/collectionitemdelegate.h" #include "connecteddevice.h" #include "devicelister.h" #include "devicemanager.h" diff --git a/src/device/deviceview.h b/src/device/deviceview.h index 5d17a936f..3de1f3e58 100644 --- a/src/device/deviceview.h +++ b/src/device/deviceview.h @@ -41,7 +41,7 @@ #include #include "core/song.h" -#include "collection/collectionview.h" +#include "collection/collectionitemdelegate.h" #include "widgets/autoexpandingtreeview.h" class Application; diff --git a/src/internet/internetcollectionview.cpp b/src/internet/internetcollectionview.cpp new file mode 100644 index 000000000..7d2e5505f --- /dev/null +++ b/src/internet/internetcollectionview.cpp @@ -0,0 +1,441 @@ +/* + * Strawberry Music Player + * This code was part of Clementine + * Copyright 2010, David Sansome + * 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 "core/application.h" +#include "core/iconloader.h" +#include "core/mimedata.h" +#include "collection/collectionbackend.h" +#include "collection/collectionmodel.h" +#include "collection/collectionfilterwidget.h" +#include "collection/collectionitem.h" +#include "collection/collectionitemdelegate.h" +#include "internetcollectionview.h" + +InternetCollectionView::InternetCollectionView(QWidget *parent) + : AutoExpandingTreeView(parent), + app_(nullptr), + collection_backend_(nullptr), + collection_model_(nullptr), + filter_(nullptr), + total_song_count_(0), + total_artist_count_(0), + total_album_count_(0), + nomusic_(":/pictures/nomusic.png"), + context_menu_(nullptr), + is_in_keyboard_search_(false) + { + + setItemDelegate(new CollectionItemDelegate(this)); + setAttribute(Qt::WA_MacShowFocusRect, false); + setHeaderHidden(true); + setAllColumnsShowFocus(true); + setDragEnabled(true); + setDragDropMode(QAbstractItemView::DragOnly); + setSelectionMode(QAbstractItemView::ExtendedSelection); + SetAutoOpen(false); + + setStyleSheet("QTreeView::item{padding-top:1px;}"); + +} + +InternetCollectionView::~InternetCollectionView() {} + +void InternetCollectionView::Init(Application *app, CollectionBackend *backend, CollectionModel *model) { + + app_ = app; + collection_backend_ = backend; + collection_model_ = model; + + collection_model_->set_pretty_covers(true); + collection_model_->set_show_dividers(true); + + ReloadSettings(); + +} + +void InternetCollectionView::SetFilter(CollectionFilterWidget *filter) { filter_ = filter; } + +void InternetCollectionView::ReloadSettings() {} + +void InternetCollectionView::SaveFocus() { + + QModelIndex current = currentIndex(); + QVariant type = model()->data(current, CollectionModel::Role_Type); + if (!type.isValid() || !(type.toInt() == CollectionItem::Type_Song || type.toInt() == CollectionItem::Type_Container || type.toInt() == CollectionItem::Type_Divider)) { + return; + } + + last_selected_path_.clear(); + last_selected_song_ = Song(); + last_selected_container_ = QString(); + + switch (type.toInt()) { + case CollectionItem::Type_Song: { + QModelIndex index = qobject_cast(model())->mapToSource(current); + SongList songs = collection_model_->GetChildSongs(index); + if (!songs.isEmpty()) { + last_selected_song_ = songs.last(); + } + break; + } + + case CollectionItem::Type_Container: + case CollectionItem::Type_Divider: { + QString text = model()->data(current, CollectionModel::Role_SortText).toString(); + last_selected_container_ = text; + break; + } + + default: + return; + } + + SaveContainerPath(current); + +} + +void InternetCollectionView::SaveContainerPath(const QModelIndex &child) { + + QModelIndex current = model()->parent(child); + QVariant type = model()->data(current, CollectionModel::Role_Type); + if (!type.isValid() || !(type.toInt() == CollectionItem::Type_Container || type.toInt() == CollectionItem::Type_Divider)) { + return; + } + + QString text = model()->data(current, CollectionModel::Role_SortText).toString(); + last_selected_path_ << text; + SaveContainerPath(current); + +} + +void InternetCollectionView::RestoreFocus() { + + if (last_selected_container_.isEmpty() && last_selected_song_.url().isEmpty()) { + return; + } + RestoreLevelFocus(); + +} + +bool InternetCollectionView::RestoreLevelFocus(const QModelIndex &parent) { + + if (model()->canFetchMore(parent)) { + model()->fetchMore(parent); + } + int rows = model()->rowCount(parent); + for (int i = 0; i < rows; i++) { + QModelIndex current = model()->index(i, 0, parent); + QVariant type = model()->data(current, CollectionModel::Role_Type); + switch (type.toInt()) { + case CollectionItem::Type_Song: + if (!last_selected_song_.url().isEmpty()) { + QModelIndex index = qobject_cast(model())->mapToSource(current); + SongList songs = collection_model_->GetChildSongs(index); + for (const Song &song : songs) { + if (song == last_selected_song_) { + setCurrentIndex(current); + return true; + } + } + } + break; + + case CollectionItem::Type_Container: + case CollectionItem::Type_Divider: { + QString text = model()->data(current, CollectionModel::Role_SortText).toString(); + if (!last_selected_container_.isEmpty() && last_selected_container_ == text) { + emit expand(current); + setCurrentIndex(current); + return true; + } + else if (last_selected_path_.contains(text)) { + emit expand(current); + // If a selected container or song were not found, we've got into a wrong subtree (happens with "unknown" all the time) + if (!RestoreLevelFocus(current)) { + emit collapse(current); + } + else { + return true; + } + } + break; + } + } + } + return false; + +} + +void InternetCollectionView::TotalSongCountUpdated(int count) { + + int old = total_song_count_; + total_song_count_ = count; + if (old != total_song_count_) update(); + + if (total_song_count_ == 0) + setCursor(Qt::PointingHandCursor); + else + unsetCursor(); + + emit TotalSongCountUpdated_(); + +} + +void InternetCollectionView::TotalArtistCountUpdated(int count) { + + int old = total_artist_count_; + total_artist_count_ = count; + if (old != total_artist_count_) update(); + + if (total_artist_count_ == 0) + setCursor(Qt::PointingHandCursor); + else + unsetCursor(); + + emit TotalArtistCountUpdated_(); + +} + +void InternetCollectionView::TotalAlbumCountUpdated(int count) { + + int old = total_album_count_; + total_album_count_ = count; + if (old != total_album_count_) update(); + + if (total_album_count_ == 0) + setCursor(Qt::PointingHandCursor); + else + unsetCursor(); + + emit TotalAlbumCountUpdated_(); + +} + +void InternetCollectionView::paintEvent(QPaintEvent *event) { + + if (total_song_count_ == 0) { + QPainter p(viewport()); + QRect rect(viewport()->rect()); + + // Draw the confused strawberry + QRect image_rect((rect.width() - nomusic_.width()) / 2, 50, nomusic_.width(), nomusic_.height()); + p.drawPixmap(image_rect, nomusic_); + + // Draw the title text + QFont bold_font; + bold_font.setBold(true); + p.setFont(bold_font); + + QFontMetrics metrics(bold_font); + + QRect title_rect(0, image_rect.bottom() + 20, rect.width(), metrics.height()); + p.drawText(title_rect, Qt::AlignHCenter, tr("The internet collection is empty!")); + + // Draw the other text + p.setFont(QFont()); + + QRect text_rect(0, title_rect.bottom() + 5, rect.width(), metrics.height()); + p.drawText(text_rect, Qt::AlignHCenter, tr("Click here to retrieve music")); + } + else { + QTreeView::paintEvent(event); + } + +} + +void InternetCollectionView::mouseReleaseEvent(QMouseEvent *e) { + + QTreeView::mouseReleaseEvent(e); + + if (total_song_count_ == 0) { + emit GetSongs(); + } + +} + +void InternetCollectionView::contextMenuEvent(QContextMenuEvent *e) { + + if (!context_menu_) { + context_menu_ = new QMenu(this); + add_to_playlist_ = context_menu_->addAction(IconLoader::Load("media-play"), tr("Append to current playlist"), this, SLOT(AddToPlaylist())); + load_ = context_menu_->addAction(IconLoader::Load("media-play"), tr("Replace current playlist"), this, SLOT(Load())); + open_in_new_playlist_ = context_menu_->addAction(IconLoader::Load("document-new"), tr("Open in new playlist"), this, SLOT(OpenInNewPlaylist())); + + context_menu_->addSeparator(); + add_to_playlist_enqueue_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue track"), this, SLOT(AddToPlaylistEnqueue())); + add_to_playlist_enqueue_next_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue to play next"), this, SLOT(AddToPlaylistEnqueueNext())); + + context_menu_->addSeparator(); + + //add_songs_ = context_menu_->addAction(IconLoader::Load("document-new"), tr("Add songs"), this, SLOT(AddSongs())); + //remove_songs_ = context_menu_->addAction(IconLoader::Load("document-new"), tr("Remove songs"), this, SLOT(RemoveSongs())); + //context_menu_->addSeparator(); + + if (filter_) context_menu_->addMenu(filter_->menu()); + + } + + context_menu_index_ = indexAt(e->pos()); + if (!context_menu_index_.isValid()) return; + + context_menu_index_ = qobject_cast(model())->mapToSource(context_menu_index_); + QModelIndexList selected_indexes = qobject_cast(model())->mapSelectionToSource(selectionModel()->selection()).indexes(); + int songs_selected = selected_indexes.count();; + + // In all modes + load_->setEnabled(songs_selected); + add_to_playlist_->setEnabled(songs_selected); + open_in_new_playlist_->setEnabled(songs_selected); + add_to_playlist_enqueue_->setEnabled(songs_selected); + //add_songs_->setEnabled(songs_selected); + //remove_songs_->setEnabled(songs_selected); + + context_menu_->popup(e->globalPos()); + +} + +void InternetCollectionView::Load() { + + QMimeData *data = model()->mimeData(selectedIndexes()); + if (MimeData *mime_data = qobject_cast(data)) { + mime_data->clear_first_ = true; + } + emit AddToPlaylistSignal(data); + +} + +void InternetCollectionView::AddToPlaylist() { + + emit AddToPlaylistSignal(model()->mimeData(selectedIndexes())); + +} + +void InternetCollectionView::AddToPlaylistEnqueue() { + + QMimeData *data = model()->mimeData(selectedIndexes()); + if (MimeData* mime_data = qobject_cast(data)) { + mime_data->enqueue_now_ = true; + } + emit AddToPlaylistSignal(data); + +} + +void InternetCollectionView::AddToPlaylistEnqueueNext() { + + QMimeData *data = model()->mimeData(selectedIndexes()); + if (MimeData *mime_data = qobject_cast(data)) { + mime_data->enqueue_next_now_ = true; + } + emit AddToPlaylistSignal(data); + +} + +void InternetCollectionView::OpenInNewPlaylist() { + + QMimeData *data = model()->mimeData(selectedIndexes()); + if (MimeData* mime_data = qobject_cast(data)) { + mime_data->open_in_new_playlist_ = true; + } + emit AddToPlaylistSignal(data); + +} + + +void InternetCollectionView::AddSongs() { + + emit AddSongs(GetSelectedSongs()); + +} + +void InternetCollectionView::RemoveSongs() { + + emit RemoveSongs(GetSelectedSongs()); + +} + +void InternetCollectionView::keyboardSearch(const QString &search) { + + is_in_keyboard_search_ = true; + QTreeView::keyboardSearch(search); + is_in_keyboard_search_ = false; + +} + +void InternetCollectionView::scrollTo(const QModelIndex &index, ScrollHint hint) { + + if (is_in_keyboard_search_) + QTreeView::scrollTo(index, QAbstractItemView::PositionAtTop); + else + QTreeView::scrollTo(index, hint); + +} + +SongList InternetCollectionView::GetSelectedSongs() const { + + QModelIndexList selected_indexes = qobject_cast(model())->mapSelectionToSource(selectionModel()->selection()).indexes(); + return collection_model_->GetChildSongs(selected_indexes); + +} + +void InternetCollectionView::FilterReturnPressed() { + + if (!currentIndex().isValid()) { + // Pick the first thing that isn't a divider + for (int row = 0; row < model()->rowCount(); ++row) { + QModelIndex idx(model()->index(row, 0)); + if (idx.data(CollectionModel::Role_Type) != CollectionItem::Type_Divider) { + setCurrentIndex(idx); + break; + } + } + } + + if (!currentIndex().isValid()) return; + + emit doubleClicked(currentIndex()); + +} + +int InternetCollectionView::TotalSongs() { + return total_song_count_; +} +int InternetCollectionView::TotalArtists() { + return total_artist_count_; +} +int InternetCollectionView::TotalAlbums() { + return total_album_count_; +} diff --git a/src/internet/internetcollectionview.h b/src/internet/internetcollectionview.h new file mode 100644 index 000000000..df33b6bac --- /dev/null +++ b/src/internet/internetcollectionview.h @@ -0,0 +1,146 @@ +/* + * Strawberry Music Player + * This code was part of Clementine + * Copyright 2010, David Sansome + * 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 INTERNETCOLLECTIONVIEW_H +#define INTERNETCOLLECTIONVIEW_H + +#include "config.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "widgets/autoexpandingtreeview.h" +#include "core/song.h" + +class QContextMenuEvent; +class QHelpEvent; +class QMouseEvent; +class QPaintEvent; + +class Application; +class CollectionBackend; +class CollectionModel; +class CollectionFilterWidget; + +class InternetCollectionView : public AutoExpandingTreeView { + Q_OBJECT + + public: + InternetCollectionView(QWidget *parent = nullptr); + ~InternetCollectionView(); + + void Init(Application *app, CollectionBackend *backend, CollectionModel *model); + + // Returns Songs currently selected in the collection view. + // Please note that the selection is recursive meaning that if for example an album is selected this will return all of it's songs. + SongList GetSelectedSongs() const; + + void SetApplication(Application *app); + void SetFilter(CollectionFilterWidget *filter); + + // QTreeView + void keyboardSearch(const QString &search); + void scrollTo(const QModelIndex &index, ScrollHint hint = EnsureVisible); + + int TotalSongs(); + int TotalArtists(); + int TotalAlbums(); + + public slots: + void TotalSongCountUpdated(int count); + void TotalArtistCountUpdated(int count); + void TotalAlbumCountUpdated(int count); + void ReloadSettings(); + + void FilterReturnPressed(); + + void SaveFocus(); + void RestoreFocus(); + + signals: + void GetSongs(); + void TotalSongCountUpdated_(); + void TotalArtistCountUpdated_(); + void TotalAlbumCountUpdated_(); + void Error(const QString &message); + void AddSongs(const SongList songs); + void RemoveSongs(const SongList songs); + + protected: + // QWidget + void paintEvent(QPaintEvent *event); + void mouseReleaseEvent(QMouseEvent *e); + void contextMenuEvent(QContextMenuEvent *e); + + private slots: + void Load(); + void AddToPlaylist(); + void AddToPlaylistEnqueue(); + void AddToPlaylistEnqueueNext(); + void OpenInNewPlaylist(); + void AddSongs(); + void RemoveSongs(); + + private: + void RecheckIsEmpty(); + bool RestoreLevelFocus(const QModelIndex &parent = QModelIndex()); + void SaveContainerPath(const QModelIndex &child); + + private: + Application *app_; + CollectionBackend *collection_backend_; + CollectionModel*collection_model_; + CollectionFilterWidget *filter_; + + int total_song_count_; + int total_artist_count_; + int total_album_count_; + + QPixmap nomusic_; + + QMenu *context_menu_; + QModelIndex context_menu_index_; + QAction *load_; + QAction *add_to_playlist_; + QAction *add_to_playlist_enqueue_; + QAction *add_to_playlist_enqueue_next_; + QAction *open_in_new_playlist_; + //QAction *add_songs_; + //QAction *remove_songs_; + + bool is_in_keyboard_search_; + + // Save focus + Song last_selected_song_; + QString last_selected_container_; + QSet last_selected_path_; + +}; + +#endif // INTERNETCOLLECTIONVIEW_H diff --git a/src/internet/internetcollectionviewcontainer.cpp b/src/internet/internetcollectionviewcontainer.cpp new file mode 100644 index 000000000..5774e86e7 --- /dev/null +++ b/src/internet/internetcollectionviewcontainer.cpp @@ -0,0 +1,58 @@ +/* + * 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 "core/application.h" +#include "internetcollectionview.h" +#include "internetcollectionviewcontainer.h" +#include "ui_internetcollectionviewcontainer.h" +#include "collection/collectionfilterwidget.h" +#include "internetservice.h" + +InternetCollectionViewContainer::InternetCollectionViewContainer(QWidget *parent) : + QWidget(parent), + ui_(new Ui_InternetCollectionViewContainer) + { + + ui_->setupUi(this); + view()->SetFilter(filter()); + + connect(filter(), SIGNAL(UpPressed()), view(), SLOT(UpAndFocus())); + connect(filter(), SIGNAL(DownPressed()), view(), SLOT(DownAndFocus())); + connect(filter(), SIGNAL(ReturnPressed()), view(), SLOT(FilterReturnPressed())); + connect(view(), SIGNAL(FocusOnFilterSignal(QKeyEvent*)), filter(), SLOT(FocusOnFilter(QKeyEvent*))); + + ui_->progressbar->hide(); + + ReloadSettings(); + +} + +InternetCollectionViewContainer::~InternetCollectionViewContainer() { delete ui_; } + +void InternetCollectionViewContainer::contextMenuEvent(QContextMenuEvent *e) { +} diff --git a/src/internet/internetcollectionviewcontainer.h b/src/internet/internetcollectionviewcontainer.h new file mode 100644 index 000000000..daf6b4140 --- /dev/null +++ b/src/internet/internetcollectionviewcontainer.h @@ -0,0 +1,66 @@ +/* + * 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 INTERNETCOLLECTIONVIEWCONTAINER_H +#define INTERNETCOLLECTIONVIEWCONTAINER_H + +#include "config.h" + +#include + +#include "ui_internetcollectionviewcontainer.h" + +class QStackedWidget; +class QPushButton; +class QLabel; +class QProgressBar; +class Application; +class InternetCollectionView; +class CollectionFilterWidget; +class InternetService; +class Ui_InternetCollectionViewContainer; + +class InternetCollectionViewContainer : public QWidget { + Q_OBJECT + + public: + InternetCollectionViewContainer(QWidget *parent = nullptr); + ~InternetCollectionViewContainer(); + + QStackedWidget *stacked() const { return ui_->stacked; } + QWidget *help_page() const { return ui_->help_page; } + QWidget *internetcollection_page() const { return ui_->internetcollection_page; } + InternetCollectionView *view() const { return ui_->view; } + CollectionFilterWidget *filter() const { return ui_->filter; } + QPushButton *refresh() const { return ui_->refresh; } + QLabel *status() const { return ui_->status; } + QProgressBar *progressbar() const { return ui_->progressbar; } + void ReloadSettings() { view()->ReloadSettings(); } + + private slots: + void contextMenuEvent(QContextMenuEvent *e); + + private: + Ui_InternetCollectionViewContainer *ui_; + Application *app_; + InternetService *service_; + +}; + +#endif // INTERNETCOLLECTIONVIEWCONTAINER_H diff --git a/src/internet/internetcollectionviewcontainer.ui b/src/internet/internetcollectionviewcontainer.ui new file mode 100644 index 000000000..a2821eef6 --- /dev/null +++ b/src/internet/internetcollectionviewcontainer.ui @@ -0,0 +1,131 @@ + + + InternetCollectionViewContainer + + + + 0 + 0 + 300 + 300 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Refresh catalogue + + + + + + + + + + + true + + + + 0 + 40 + + + + + + + true + + + + + + + true + + + 0 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + + + + CollectionFilterWidget + QWidget +
collection/collectionfilterwidget.h
+ 1 +
+ + InternetCollectionView + QTreeView +
internet/internetcollectionview.h
+
+
+ + +
diff --git a/src/internet/internetsearch.cpp b/src/internet/internetsearch.cpp index 1b7b82e3a..1c8a78331 100644 --- a/src/internet/internetsearch.cpp +++ b/src/internet/internetsearch.cpp @@ -21,8 +21,6 @@ #include "config.h" -#include - #include #include #include @@ -40,20 +38,15 @@ #include "core/application.h" #include "core/logging.h" -#include "core/closure.h" -#include "core/iconloader.h" #include "core/song.h" +#include "playlist/songmimedata.h" #include "covermanager/albumcoverloader.h" #include "internet/internetsongmimedata.h" -#include "playlist/songmimedata.h" #include "internetsearch.h" #include "internetservice.h" #include "internetservices.h" -using std::advance; - const int InternetSearch::kDelayedSearchTimeoutMs = 200; -const int InternetSearch::kMaxResultsPerEmission = 2000; const int InternetSearch::kArtHeight = 32; InternetSearch::InternetSearch(Application *app, Song::Source source, QObject *parent) @@ -70,11 +63,10 @@ InternetSearch::InternetSearch(Application *app, Song::Source source, QObject *p connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(AlbumArtLoaded(quint64, QImage))); 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(ArtLoaded(int, QImage)), SLOT(ArtLoadedSlot(int, QImage))); - connect(service_, SIGNAL(UpdateStatus(QString)), SLOT(UpdateStatusSlot(QString))); - connect(service_, SIGNAL(ProgressSetMaximum(int)), SLOT(ProgressSetMaximumSlot(int))); - connect(service_, SIGNAL(UpdateProgress(int)), SLOT(UpdateProgressSlot(int))); + + connect(service_, SIGNAL(SearchUpdateStatus(QString)), SLOT(UpdateStatusSlot(QString))); + connect(service_, SIGNAL(SearchProgressSetMaximum(int)), SLOT(ProgressSetMaximumSlot(int))); + connect(service_, SIGNAL(SearchUpdateProgress(int)), SLOT(UpdateProgressSlot(int))); connect(service_, SIGNAL(SearchResults(int, SongList)), SLOT(SearchDone(int, SongList))); connect(service_, SIGNAL(SearchError(int, QString)), SLOT(HandleError(int, QString))); @@ -145,14 +137,22 @@ void InternetSearch::SearchDone(int service_id, const SongList &songs) { const PendingState state = pending_searches_.take(service_id); const int search_id = state.orig_id_; - ResultList ret; + ResultList results; for (const Song &song : songs) { Result result; result.metadata_ = song; - ret << result; + results << result; } - emit ResultsAvailable(search_id, ret); + if (results.isEmpty()) return; + + // Load cached pixmaps into the results + for (InternetSearch::ResultList::iterator it = results.begin(); it != results.end(); ++it) { + it->pixmap_cache_key_ = PixmapCacheKey(*it); + } + + emit AddResults(search_id, results); + MaybeSearchFinished(search_id); } @@ -172,6 +172,7 @@ void InternetSearch::MaybeSearchFinished(int id) { } void InternetSearch::CancelSearch(int id) { + QMap::iterator it; for (it = delayed_searches_.begin(); it != delayed_searches_.end(); ++it) { if (it.value().id_ == id) { @@ -181,9 +182,11 @@ void InternetSearch::CancelSearch(int id) { } } service_->CancelSearch(); + } void InternetSearch::timerEvent(QTimerEvent *e) { + QMap::iterator it = delayed_searches_.find(e->timerId()); if (it != delayed_searches_.end()) { SearchAsync(it.value().id_, it.value().query_, it.value().type_); @@ -192,25 +195,6 @@ void InternetSearch::timerEvent(QTimerEvent *e) { } QObject::timerEvent(e); -} - -void InternetSearch::ResultsAvailableSlot(int id, InternetSearch::ResultList results) { - - if (results.isEmpty()) return; - - // Limit the number of results that are used from each emission. - if (results.count() > kMaxResultsPerEmission) { - InternetSearch::ResultList::iterator begin = results.begin(); - std::advance(begin, kMaxResultsPerEmission); - results.erase(begin, results.end()); - } - - // Load cached pixmaps into the results - for (InternetSearch::ResultList::iterator it = results.begin(); it != results.end(); ++it) { - it->pixmap_cache_key_ = PixmapCacheKey(*it); - } - - emit AddResults(id, results); } @@ -235,27 +219,17 @@ int InternetSearch::LoadArtAsync(const InternetSearch::Result &result) { } -void InternetSearch::ArtLoadedSlot(int id, const QImage &image) { - HandleLoadedArt(id, image); -} - void InternetSearch::AlbumArtLoaded(quint64 id, const QImage &image) { if (!cover_loader_tasks_.contains(id)) return; int orig_id = cover_loader_tasks_.take(id); - HandleLoadedArt(orig_id, image); - -} - -void InternetSearch::HandleLoadedArt(int id, const QImage &image) { - - const QString key = pending_art_searches_.take(id); + const QString key = pending_art_searches_.take(orig_id); QPixmap pixmap = QPixmap::fromImage(image); pixmap_cache_.insert(key, pixmap); - emit ArtLoaded(id, pixmap); + emit ArtLoaded(orig_id, pixmap); } diff --git a/src/internet/internetsearch.h b/src/internet/internetsearch.h index 47c4a7dea..dad4dbe38 100644 --- a/src/internet/internetsearch.h +++ b/src/internet/internetsearch.h @@ -24,10 +24,13 @@ #include "config.h" +#include #include -#include -#include -#include +#include +#include +#include +#include +#include #include #include "core/song.h" @@ -58,7 +61,6 @@ class InternetSearch : public QObject { typedef QList ResultList; static const int kDelayedSearchTimeoutMs; - static const int kMaxResultsPerEmission; Application *application() const { return app_; } Song::Source source() const { return source_; } @@ -85,7 +87,6 @@ class InternetSearch : public QObject { void UpdateProgress(int max); void ArtLoaded(int id, const QPixmap &pixmap); - void ArtLoaded(int id, const QImage &image); protected: @@ -107,7 +108,7 @@ class InternetSearch : public QObject { void timerEvent(QTimerEvent *e); - // These functions treat queries in the same way as LibraryQuery. + // These functions treat queries in the same way as CollectionQuery. // They're useful for figuring out whether you got a result because it matched in the song title or the artist/album name. static QStringList TokenizeQuery(const QString &query); static bool Matches(const QStringList &tokens, const QString &string); @@ -116,9 +117,7 @@ class InternetSearch : public QObject { void DoSearchAsync(int id, const QString &query, SearchType type); void SearchDone(int id, const SongList &songs); void HandleError(const int id, const QString error); - void ResultsAvailableSlot(int id, InternetSearch::ResultList results); - void ArtLoadedSlot(int id, const QImage &image); void AlbumArtLoaded(quint64 id, const QImage &image); void UpdateStatusSlot(QString text); diff --git a/src/internet/internetsearchitemdelegate.cpp b/src/internet/internetsearchitemdelegate.cpp index 20ce6c4f6..2916b212f 100644 --- a/src/internet/internetsearchitemdelegate.cpp +++ b/src/internet/internetsearchitemdelegate.cpp @@ -18,18 +18,20 @@ * */ -#include #include +#include #include "internetsearchitemdelegate.h" #include "internetsearchview.h" -InternetSearchItemDelegate::InternetSearchItemDelegate(InternetSearchView* view) +InternetSearchItemDelegate::InternetSearchItemDelegate(InternetSearchView *view) : CollectionItemDelegate(view), view_(view) {} void InternetSearchItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { + // Tell the view we painted this item so it can lazy load some art. const_cast(view_)->LazyLoadArt(index); CollectionItemDelegate::paint(painter, option, index); + } diff --git a/src/internet/internetsearchitemdelegate.h b/src/internet/internetsearchitemdelegate.h index 42e825370..1dc0b30cc 100644 --- a/src/internet/internetsearchitemdelegate.h +++ b/src/internet/internetsearchitemdelegate.h @@ -21,11 +21,11 @@ #ifndef INTERNETSEARCHITEMDELEGATE_H #define INTERNETSEARCHITEMDELEGATE_H -#include #include -#include "collection/collectionview.h" +#include "collection/collectionitemdelegate.h" +class QPainter; class InternetSearchView; class InternetSearchItemDelegate : public CollectionItemDelegate { @@ -35,7 +35,8 @@ class InternetSearchItemDelegate : public CollectionItemDelegate { void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; private: - InternetSearchView* view_; + InternetSearchView *view_; + }; #endif // INTERNETSEARCHITEMDELEGATE_H diff --git a/src/internet/internetsearchmodel.cpp b/src/internet/internetsearchmodel.cpp index af79bade9..cf3dd944b 100644 --- a/src/internet/internetsearchmodel.cpp +++ b/src/internet/internetsearchmodel.cpp @@ -20,15 +20,13 @@ #include "config.h" -#include -#include #include +#include #include #include #include #include #include -#include #include "core/mimedata.h" #include "core/iconloader.h" diff --git a/src/internet/internetsearchview.cpp b/src/internet/internetsearchview.cpp index 2486cc6b4..181c588dd 100644 --- a/src/internet/internetsearchview.cpp +++ b/src/internet/internetsearchview.cpp @@ -21,30 +21,24 @@ #include "config.h" -#include -#include - #include #include #include #include #include -#include #include #include #include #include -#include -#include #include -#include +#include #include +#include #include #include "core/application.h" #include "core/logging.h" #include "core/mimedata.h" -#include "core/timeconstants.h" #include "core/iconloader.h" #include "internet/internetsongmimedata.h" #include "collection/collectionfilterwidget.h" @@ -64,18 +58,16 @@ using std::swap; const int InternetSearchView::kSwapModelsTimeoutMsec = 250; -InternetSearchView::InternetSearchView(Application *app, InternetSearch *engine, QString settings_group, SettingsDialog::Page settings_page, QWidget *parent) +InternetSearchView::InternetSearchView(QWidget *parent) : QWidget(parent), - app_(app), - engine_(engine), - settings_group_(settings_group), - settings_page_(settings_page), + app_(nullptr), + engine_(nullptr), ui_(new Ui_InternetSearchView), context_menu_(nullptr), last_search_id_(0), - front_model_(new InternetSearchModel(engine_, this)), - back_model_(new InternetSearchModel(engine_, this)), - current_model_(front_model_), + front_model_(nullptr), + back_model_(nullptr), + current_model_(nullptr), front_proxy_(new InternetSearchSortModel(this)), back_proxy_(new InternetSearchSortModel(this)), current_proxy_(front_proxy_), @@ -84,24 +76,15 @@ InternetSearchView::InternetSearchView(Application *app, InternetSearch *engine, { ui_->setupUi(this); + ui_->progressbar->hide(); ui_->progressbar->reset(); - front_model_->set_proxy(front_proxy_); - back_model_->set_proxy(back_proxy_); - ui_->search->installEventFilter(this); ui_->results_stack->installEventFilter(this); ui_->settings->setIcon(IconLoader::Load("configure")); - // Must be a queued connection to ensure the InternetSearch handles it first. - connect(app_, SIGNAL(SettingsChanged()), SLOT(ReloadSettings()), Qt::QueuedConnection); - - connect(ui_->search, SIGNAL(textChanged(QString)), SLOT(TextEdited(QString))); - connect(ui_->results, SIGNAL(AddToPlaylistSignal(QMimeData*)), SIGNAL(AddToPlaylist(QMimeData*))); - connect(ui_->results, SIGNAL(FocusOnFilterSignal(QKeyEvent*)), SLOT(FocusOnFilter(QKeyEvent*))); - // Set the appearance of the results list ui_->results->setItemDelegate(new InternetSearchItemDelegate(this)); ui_->results->setAttribute(Qt::WA_MacShowFocusRect, false); @@ -123,6 +106,32 @@ InternetSearchView::InternetSearchView(Application *app, InternetSearch *engine, help_font.setBold(true); ui_->label_helptext->setFont(help_font); +} + +InternetSearchView::~InternetSearchView() { delete ui_; } + +void InternetSearchView::Init(Application *app, InternetSearch *engine, QString settings_group, SettingsDialog::Page settings_page) { + + app_ = app; + engine_ = engine; + settings_group_ = settings_group; + settings_page_ = settings_page; + + front_model_ = new InternetSearchModel(engine_, this); + back_model_ = new InternetSearchModel(engine_, this); + + front_proxy_ = new InternetSearchSortModel(this); + back_proxy_ = new InternetSearchSortModel(this); + + front_model_->set_proxy(front_proxy_); + back_model_->set_proxy(back_proxy_); + + current_model_ = front_model_; + current_proxy_ = front_proxy_; + + // Must be a queued connection to ensure the InternetSearch handles it first. + connect(app_, SIGNAL(SettingsChanged()), SLOT(ReloadSettings()), Qt::QueuedConnection); + // Set up the sorting proxy model front_proxy_->setSourceModel(front_model_); front_proxy_->setDynamicSortFilter(true); @@ -132,10 +141,6 @@ InternetSearchView::InternetSearchView(Application *app, InternetSearch *engine, back_proxy_->setDynamicSortFilter(true); back_proxy_->sort(0); - swap_models_timer_->setSingleShot(true); - swap_models_timer_->setInterval(kSwapModelsTimeoutMsec); - connect(swap_models_timer_, SIGNAL(timeout()), SLOT(SwapModels())); - // Add actions to the settings menu group_by_actions_ = CollectionFilterWidget::CreateGroupByActions(this); QMenu *settings_menu = new QMenu(this); @@ -144,12 +149,19 @@ InternetSearchView::InternetSearchView(Application *app, InternetSearch *engine, settings_menu->addAction(IconLoader::Load("configure"), tr("Configure %1...").arg(Song::TextForSource(engine->source())), this, SLOT(OpenSettingsDialog())); ui_->settings->setMenu(settings_menu); + swap_models_timer_->setSingleShot(true); + swap_models_timer_->setInterval(kSwapModelsTimeoutMsec); + connect(swap_models_timer_, SIGNAL(timeout()), SLOT(SwapModels())); + connect(ui_->radiobutton_search_artists, SIGNAL(clicked(bool)), SLOT(SearchArtistsClicked(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(ui_->search, SIGNAL(textChanged(QString)), SLOT(TextEdited(QString))); + connect(ui_->results, SIGNAL(AddToPlaylistSignal(QMimeData*)), SIGNAL(AddToPlaylist(QMimeData*))); + connect(ui_->results, SIGNAL(FocusOnFilterSignal(QKeyEvent*)), SLOT(FocusOnFilter(QKeyEvent*))); + // These have to be queued connections because they may get emitted before our call to Search() (or whatever) returns and we add the ID to the map. connect(engine_, SIGNAL(UpdateStatus(QString)), SLOT(UpdateStatus(QString))); @@ -164,8 +176,6 @@ InternetSearchView::InternetSearchView(Application *app, InternetSearch *engine, } -InternetSearchView::~InternetSearchView() { delete ui_; } - void InternetSearchView::ReloadSettings() { QSettings s; @@ -176,11 +186,9 @@ void InternetSearchView::ReloadSettings() { const bool pretty = s.value("pretty_covers", true).toBool(); front_model_->set_use_pretty_covers(pretty); back_model_->set_use_pretty_covers(pretty); - s.endGroup(); // Internet search settings - s.beginGroup(settings_group_); search_type_ = InternetSearch::SearchType(s.value("type", int(InternetSearch::SearchType_Artists)).toInt()); switch (search_type_) { case InternetSearch::SearchType_Artists: @@ -243,21 +251,25 @@ void InternetSearchView::TextEdited(const QString &text) { } void InternetSearchView::AddResults(int id, const InternetSearch::ResultList &results) { + if (id != last_search_id_) return; if (results.isEmpty()) return; ui_->label_status->clear(); ui_->progressbar->reset(); ui_->progressbar->hide(); current_model_->AddResults(results); + } void InternetSearchView::SearchError(const int id, const QString error) { + error_ = true; ui_->label_helptext->setText(error); ui_->label_status->clear(); ui_->progressbar->reset(); ui_->progressbar->hide(); ui_->results_stack->setCurrentWidget(ui_->help_page); + } void InternetSearchView::SwapModels() { diff --git a/src/internet/internetsearchview.h b/src/internet/internetsearchview.h index dc943bc7b..cf48b5ad7 100644 --- a/src/internet/internetsearchview.h +++ b/src/internet/internetsearchview.h @@ -25,25 +25,28 @@ #include "config.h" #include -#include -#include #include #include #include -#include #include -#include -#include -#include -#include -#include -#include #include "collection/collectionmodel.h" #include "settings/settingsdialog.h" #include "playlist/playlistmanager.h" #include "internetsearch.h" +class QSortFilterProxyModel; +class QMimeData; +class QTimer; +class QMenu; +class QAction; +class QActionGroup; +class QEvent; +class QKeyEvent; +class QShowEvent; +class QHideEvent; +class QContextMenuEvent; + class Application; class GroupByDialog; class InternetSearchModel; @@ -53,9 +56,11 @@ class InternetSearchView : public QWidget { Q_OBJECT public: - InternetSearchView(Application *app, InternetSearch *engine, QString settings_group, SettingsDialog::Page settings_page, QWidget *parent = nullptr); + InternetSearchView(QWidget *parent = nullptr); ~InternetSearchView(); + void Init(Application *app, InternetSearch *engine, QString settings_group, SettingsDialog::Page settings_page); + static const int kSwapModelsTimeoutMsec; void LazyLoadArt(const QModelIndex &index); diff --git a/src/internet/internetsearchview.ui b/src/internet/internetsearchview.ui index 427e002fc..bf5296fbe 100644 --- a/src/internet/internetsearchview.ui +++ b/src/internet/internetsearchview.ui @@ -72,7 +72,7 @@ true - Search for + Search type 10 @@ -215,7 +215,7 @@ 0 0 398 - 521 + 511 diff --git a/src/internet/internetservice.cpp b/src/internet/internetservice.cpp index 4a51d4dce..c36444d56 100644 --- a/src/internet/internetservice.cpp +++ b/src/internet/internetservice.cpp @@ -18,14 +18,12 @@ */ #include -#include -#include #include -#include "core/logging.h" -#include "core/mimedata.h" -#include "internetservices.h" #include "internetservice.h" +#include "core/song.h" + +class Application; InternetService::InternetService(Song::Source source, const QString &name, const QString &url_scheme, Application *app, QObject *parent) : QObject(parent), app_(app), source_(source), name_(name), url_scheme_(url_scheme) { diff --git a/src/internet/internetservice.h b/src/internet/internetservice.h index a61203c0c..70a291d48 100644 --- a/src/internet/internetservice.h +++ b/src/internet/internetservice.h @@ -21,23 +21,17 @@ #define INTERNETSERVICE_H #include -#include -#include -#include -#include #include #include #include #include "core/song.h" -#include "core/iconloader.h" -#include "playlist/playlistitem.h" -#include "settings/settingsdialog.h" #include "internetsearch.h" +class QSortFilterProxyModel; class Application; -class InternetServices; -class CollectionFilterWidget; +class CollectionBackend; +class CollectionModel; class InternetService : public QObject { Q_OBJECT @@ -45,6 +39,7 @@ class InternetService : public QObject { public: InternetService(Song::Source source, const QString &name, const QString &url_scheme, Application *app, QObject *parent = nullptr); virtual ~InternetService() {} + virtual Song::Source source() const { return source_; } virtual QString name() const { return name_; } virtual QString url_scheme() const { return url_scheme_; } @@ -55,8 +50,63 @@ class InternetService : public QObject { virtual int Search(const QString &query, InternetSearch::SearchType type) = 0; virtual void CancelSearch() = 0; + virtual CollectionBackend *artists_collection_backend() = 0; + virtual CollectionBackend *albums_collection_backend() = 0; + virtual CollectionBackend *songs_collection_backend() = 0; + + virtual CollectionModel *artists_collection_model() = 0; + virtual CollectionModel *albums_collection_model() = 0; + virtual CollectionModel *songs_collection_model() = 0; + + virtual QSortFilterProxyModel *artists_collection_sort_model() = 0; + virtual QSortFilterProxyModel *albums_collection_sort_model() = 0; + virtual QSortFilterProxyModel *songs_collection_sort_model() = 0; + public slots: virtual void ShowConfig() {} + virtual void GetArtists() = 0; + virtual void GetAlbums() = 0; + virtual void GetSongs() = 0; + + signals: + void Login(); + void Logout(); + void Login(const QString &username, const QString &password, const QString &token); + void LoginSuccess(); + void LoginFailure(QString failure_reason); + void LoginComplete(bool success, QString error = QString()); + + void Error(QString message); + void Results(SongList songs); + void UpdateStatus(QString text); + void ProgressSetMaximum(int max); + void UpdateProgress(int max); + + void ArtistsError(QString message); + void ArtistsResults(SongList songs); + void ArtistsUpdateStatus(QString text); + void ArtistsProgressSetMaximum(int max); + void ArtistsUpdateProgress(int max); + + void AlbumsError(QString message); + void AlbumsResults(SongList songs); + void AlbumsUpdateStatus(QString text); + void AlbumsProgressSetMaximum(int max); + void AlbumsUpdateProgress(int max); + + void SongsError(QString message); + void SongsResults(SongList songs); + void SongsUpdateStatus(QString text); + void SongsProgressSetMaximum(int max); + void SongsUpdateProgress(int max); + + void SearchResults(int id, SongList songs); + void SearchError(int id, QString message); + void SearchUpdateStatus(QString text); + void SearchProgressSetMaximum(int max); + void SearchUpdateProgress(int max); + + void StreamURLFinished(const QUrl original_url, const QUrl stream_url, const Song::FileType, QString error = QString()); protected: Application *app_; diff --git a/src/internet/internetservices.h b/src/internet/internetservices.h index d9edd6169..8f77bc087 100644 --- a/src/internet/internetservices.h +++ b/src/internet/internetservices.h @@ -24,19 +24,11 @@ #include "config.h" -#include #include -#include #include -#include #include "core/song.h" -#include "collection/collectionmodel.h" -#include "playlist/playlistitem.h" -#include "settings/settingsdialog.h" -#include "widgets/multiloadingindicator.h" -class Application; class InternetService; class InternetServices : public QObject { diff --git a/src/internet/internettabsview.cpp b/src/internet/internettabsview.cpp new file mode 100644 index 000000000..ba4e09f00 --- /dev/null +++ b/src/internet/internettabsview.cpp @@ -0,0 +1,213 @@ +/* + * 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 "core/application.h" +#include "collection/collectionbackend.h" +#include "collection/collectionfilterwidget.h" +#include "internetservice.h" +#include "internettabsview.h" +#include "ui_internettabsview.h" + +InternetTabsView::InternetTabsView(Application *app, InternetService *service, InternetSearch *engine, QString settings_group, SettingsDialog::Page settings_page, QWidget *parent) + : QWidget(parent), + app_(app), + service_(service), + engine_(engine), + settings_group_(settings_group), + settings_page_(settings_page), + ui_(new Ui_InternetTabsView) + { + + ui_->setupUi(this); + + ui_->search_view->Init(app, engine, settings_group, settings_page); + + if (service_->artists_collection_model()) { + ui_->artists_collection->stacked()->setCurrentWidget(ui_->artists_collection->internetcollection_page()); + ui_->artists_collection->view()->Init(app_, service_->artists_collection_backend(), service_->artists_collection_model()); + ui_->artists_collection->view()->setModel(service_->artists_collection_sort_model()); + ui_->artists_collection->view()->SetFilter(ui_->artists_collection->filter()); + ui_->artists_collection->filter()->SetCollectionModel(service_->artists_collection_model()); + + connect(ui_->artists_collection->view(), SIGNAL(GetSongs()), SLOT(GetArtists())); + connect(ui_->artists_collection->refresh(), SIGNAL(clicked()), SLOT(GetArtists())); + connect(service_, SIGNAL(ArtistsResults(SongList)), SLOT(ArtistsFinished(SongList))); + connect(service_, SIGNAL(ArtistsError(QString)), ui_->artists_collection->status(), SLOT(setText(QString))); + connect(service_, SIGNAL(ArtistsUpdateStatus(QString)), ui_->artists_collection->status(), SLOT(setText(QString))); + connect(service_, SIGNAL(ArtistsProgressSetMaximum(int)), ui_->artists_collection->progressbar(), SLOT(setMaximum(int))); + connect(service_, SIGNAL(ArtistsUpdateProgress(int)), ui_->artists_collection->progressbar(), SLOT(setValue(int))); + + connect(service_->artists_collection_model(), SIGNAL(TotalArtistCountUpdated(int)), ui_->artists_collection->view(), SLOT(TotalArtistCountUpdated(int))); + connect(service_->artists_collection_model(), SIGNAL(TotalAlbumCountUpdated(int)), ui_->artists_collection->view(), SLOT(TotalAlbumCountUpdated(int))); + connect(service_->artists_collection_model(), SIGNAL(TotalSongCountUpdated(int)), ui_->artists_collection->view(), SLOT(TotalSongCountUpdated(int))); + connect(service_->artists_collection_model(), SIGNAL(modelAboutToBeReset()), ui_->artists_collection->view(), SLOT(SaveFocus())); + connect(service_->artists_collection_model(), SIGNAL(modelReset()), ui_->artists_collection->view(), SLOT(RestoreFocus())); + + } + else { + ui_->tabs->removeTab(ui_->tabs->indexOf(ui_->artists)); + } + + if (service_->albums_collection_model()) { + ui_->albums_collection->stacked()->setCurrentWidget(ui_->albums_collection->internetcollection_page()); + ui_->albums_collection->view()->Init(app_, service_->albums_collection_backend(), service_->albums_collection_model()); + ui_->albums_collection->view()->setModel(service_->albums_collection_sort_model()); + ui_->albums_collection->view()->SetFilter(ui_->albums_collection->filter()); + ui_->albums_collection->filter()->SetCollectionModel(service_->albums_collection_model()); + + connect(ui_->albums_collection->view(), SIGNAL(GetSongs()), SLOT(GetAlbums())); + connect(ui_->albums_collection->refresh(), SIGNAL(clicked()), SLOT(GetAlbums())); + connect(service_, SIGNAL(AlbumsResults(SongList)), SLOT(AlbumsFinished(SongList))); + connect(service_, SIGNAL(AlbumsError(QString)), ui_->albums_collection->status(), SLOT(setText(QString))); + connect(service_, SIGNAL(AlbumsUpdateStatus(QString)), ui_->albums_collection->status(), SLOT(setText(QString))); + connect(service_, SIGNAL(AlbumsProgressSetMaximum(int)), ui_->albums_collection->progressbar(), SLOT(setMaximum(int))); + connect(service_, SIGNAL(AlbumsUpdateProgress(int)), ui_->albums_collection->progressbar(), SLOT(setValue(int))); + + connect(service_->albums_collection_model(), SIGNAL(TotalArtistCountUpdated(int)), ui_->albums_collection->view(), SLOT(TotalArtistCountUpdated(int))); + connect(service_->albums_collection_model(), SIGNAL(TotalAlbumCountUpdated(int)), ui_->albums_collection->view(), SLOT(TotalAlbumCountUpdated(int))); + connect(service_->albums_collection_model(), SIGNAL(TotalSongCountUpdated(int)), ui_->albums_collection->view(), SLOT(TotalSongCountUpdated(int))); + connect(service_->albums_collection_model(), SIGNAL(modelAboutToBeReset()), ui_->albums_collection->view(), SLOT(SaveFocus())); + connect(service_->albums_collection_model(), SIGNAL(modelReset()), ui_->albums_collection->view(), SLOT(RestoreFocus())); + + } + else { + ui_->tabs->removeTab(ui_->tabs->indexOf(ui_->albums)); + } + + if (service_->songs_collection_model()) { + ui_->songs_collection->stacked()->setCurrentWidget(ui_->songs_collection->internetcollection_page()); + ui_->songs_collection->view()->Init(app_, service_->songs_collection_backend(), service_->songs_collection_model()); + ui_->songs_collection->view()->setModel(service_->songs_collection_sort_model()); + ui_->songs_collection->view()->SetFilter(ui_->songs_collection->filter()); + ui_->songs_collection->filter()->SetCollectionModel(service_->songs_collection_model()); + + connect(ui_->songs_collection->view(), SIGNAL(GetSongs()), SLOT(GetSongs())); + connect(ui_->songs_collection->refresh(), SIGNAL(clicked()), SLOT(GetSongs())); + connect(service_, SIGNAL(SongsResults(SongList)), SLOT(SongsFinished(SongList))); + connect(service_, SIGNAL(SongsError(QString)), ui_->songs_collection->status(), SLOT(setText(QString))); + connect(service_, SIGNAL(SongsUpdateStatus(QString)), ui_->songs_collection->status(), SLOT(setText(QString))); + connect(service_, SIGNAL(SongsProgressSetMaximum(int)), ui_->songs_collection->progressbar(), SLOT(setMaximum(int))); + connect(service_, SIGNAL(SongsUpdateProgress(int)), ui_->songs_collection->progressbar(), SLOT(setValue(int))); + + connect(service_->songs_collection_model(), SIGNAL(TotalArtistCountUpdated(int)), ui_->songs_collection->view(), SLOT(TotalArtistCountUpdated(int))); + connect(service_->songs_collection_model(), SIGNAL(TotalAlbumCountUpdated(int)), ui_->songs_collection->view(), SLOT(TotalAlbumCountUpdated(int))); + connect(service_->songs_collection_model(), SIGNAL(TotalSongCountUpdated(int)), ui_->songs_collection->view(), SLOT(TotalSongCountUpdated(int))); + connect(service_->songs_collection_model(), SIGNAL(modelAboutToBeReset()), ui_->songs_collection->view(), SLOT(SaveFocus())); + connect(service_->songs_collection_model(), SIGNAL(modelReset()), ui_->songs_collection->view(), SLOT(RestoreFocus())); + + } + else { + ui_->tabs->removeTab(ui_->tabs->indexOf(ui_->songs)); + } + + QSettings s; + s.beginGroup(settings_group_); + QString tab = s.value("tab", "artists").toString().toLower(); + s.endGroup(); + qLog(Debug) << tab; + + if (tab == "artists") { + ui_->tabs->setCurrentWidget(ui_->artists); + } + else if (tab == "albums") { + ui_->tabs->setCurrentWidget(ui_->albums); + } + else if (tab == "songs") { + ui_->tabs->setCurrentWidget(ui_->songs); + } + else if (tab == "search") { + ui_->tabs->setCurrentWidget(ui_->search); + } + + ReloadSettings(); + +} + +InternetTabsView::~InternetTabsView() { + + QSettings s; + s.beginGroup(settings_group_); + s.setValue("tab", ui_->tabs->currentWidget()->objectName().toLower()); + s.endGroup(); + + delete ui_; +} + +void InternetTabsView::ReloadSettings() { ui_->search_view->ReloadSettings(); } + +void InternetTabsView::contextMenuEvent(QContextMenuEvent *e) { +} + +void InternetTabsView::GetArtists() { + + ui_->artists_collection->stacked()->setCurrentWidget(ui_->artists_collection->help_page()); + ui_->artists_collection->progressbar()->show(); + service_->GetArtists(); + +} + +void InternetTabsView::ArtistsFinished(SongList songs) { + + service_->artists_collection_backend()->DeleteAll(); + ui_->artists_collection->stacked()->setCurrentWidget(ui_->artists_collection->internetcollection_page()); + ui_->artists_collection->status()->clear(); + service_->artists_collection_backend()->AddOrUpdateSongs(songs); + +} + +void InternetTabsView::GetAlbums() { + + ui_->albums_collection->stacked()->setCurrentWidget(ui_->albums_collection->help_page()); + service_->GetAlbums(); + +} + +void InternetTabsView::AlbumsFinished(SongList songs) { + + service_->albums_collection_backend()->DeleteAll(); + ui_->albums_collection->stacked()->setCurrentWidget(ui_->albums_collection->internetcollection_page()); + ui_->albums_collection->status()->clear(); + service_->albums_collection_backend()->AddOrUpdateSongs(songs); + +} + +void InternetTabsView::GetSongs() { + + ui_->songs_collection->stacked()->setCurrentWidget(ui_->songs_collection->help_page()); + service_->GetSongs(); + +} + +void InternetTabsView::SongsFinished(SongList songs) { + + service_->songs_collection_backend()->DeleteAll(); + ui_->songs_collection->stacked()->setCurrentWidget(ui_->songs_collection->internetcollection_page()); + ui_->songs_collection->status()->clear(); + service_->songs_collection_backend()->AddOrUpdateSongs(songs); + +} diff --git a/src/internet/internettabsview.h b/src/internet/internettabsview.h new file mode 100644 index 000000000..76ee58c7e --- /dev/null +++ b/src/internet/internettabsview.h @@ -0,0 +1,76 @@ +/* + * 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 INTERNETTABSVIEW_H +#define INTERNETTABSVIEW_H + +#include "config.h" + +#include +#include + +#include "settings/settingsdialog.h" +#include "internetcollectionviewcontainer.h" +#include "internetcollectionview.h" +#include "ui_internettabsview.h" +#include "core/song.h" + +class QContextMenuEvent; + +class Application; +class InternetService; +class InternetSearch; +class Ui_InternetTabsView; +class InternetCollectionView; +class InternetSearchView; + +class InternetTabsView : public QWidget { + Q_OBJECT + + public: + InternetTabsView(Application *app, InternetService *service, InternetSearch *engine, QString settings_group, SettingsDialog::Page settings_page, QWidget *parent = nullptr); + ~InternetTabsView(); + + void ReloadSettings(); + + InternetCollectionView *artists_collection_view() const { return ui_->artists_collection->view(); } + InternetCollectionView *albums_collection_view() const { return ui_->albums_collection->view(); } + InternetCollectionView *songs_collection_view() const { return ui_->songs_collection->view(); } + InternetSearchView *search_view() const { return ui_->search_view; } + + private slots: + void contextMenuEvent(QContextMenuEvent *e); + void GetArtists(); + void GetAlbums(); + void GetSongs(); + void ArtistsFinished(SongList songs); + void AlbumsFinished(SongList songs); + void SongsFinished(SongList songs); + + private: + Application *app_; + InternetService *service_; + InternetSearch *engine_; + QString settings_group_; + SettingsDialog::Page settings_page_; + Ui_InternetTabsView *ui_; + +}; + +#endif // INTERNETTABSVIEW_H diff --git a/src/internet/internettabsview.ui b/src/internet/internettabsview.ui new file mode 100644 index 000000000..e8e39e0e0 --- /dev/null +++ b/src/internet/internettabsview.ui @@ -0,0 +1,130 @@ + + + InternetTabsView + + + + 0 + 0 + 400 + 660 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 3 + + + + Artists + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + Albums + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + Songs + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + Search + + + + + + + + + + + + + + InternetSearchView + QWidget +
internet/internetsearchview.h
+ 1 +
+ + InternetCollectionViewContainer + QWidget +
internet/internetcollectionviewcontainer.h
+ 1 +
+
+ + +
diff --git a/src/settings/tidalsettingspage.cpp b/src/settings/tidalsettingspage.cpp index cf94ab87f..21e1e6be2 100644 --- a/src/settings/tidalsettingspage.cpp +++ b/src/settings/tidalsettingspage.cpp @@ -84,6 +84,7 @@ void TidalSettingsPage::Load() { ui_->albumssearchlimit->setValue(s.value("albumssearchlimit", 100).toInt()); ui_->songssearchlimit->setValue(s.value("songssearchlimit", 100).toInt()); ui_->checkbox_fetchalbums->setChecked(s.value("fetchalbums", false).toBool()); + ui_->checkbox_cache_album_covers->setChecked(s.value("cachealbumcovers", true).toBool()); dialog()->ComboBoxLoadFromSettings(s, ui_->coversize, "coversize", "320x320"); s.endGroup(); @@ -105,6 +106,7 @@ void TidalSettingsPage::Save() { s.setValue("albumssearchlimit", ui_->albumssearchlimit->value()); s.setValue("songssearchlimit", ui_->songssearchlimit->value()); s.setValue("fetchalbums", ui_->checkbox_fetchalbums->isChecked()); + s.setValue("cachealbumcovers", ui_->checkbox_cache_album_covers->isChecked()); s.setValue("coversize", ui_->coversize->itemData(ui_->coversize->currentIndex())); s.endGroup(); diff --git a/src/settings/tidalsettingspage.ui b/src/settings/tidalsettingspage.ui index 491417bd6..29627fef9 100644 --- a/src/settings/tidalsettingspage.ui +++ b/src/settings/tidalsettingspage.ui @@ -352,6 +352,13 @@ + + + + Cache album covers + + + diff --git a/src/tidal/tidalbaserequest.cpp b/src/tidal/tidalbaserequest.cpp new file mode 100644 index 000000000..a1e35ee1f --- /dev/null +++ b/src/tidal/tidalbaserequest.cpp @@ -0,0 +1,212 @@ +/* + * Strawberry Music Player + * Copyright 2018, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/logging.h" +#include "core/network.h" +#include "tidalservice.h" +#include "tidalbaserequest.h" + +const char *TidalBaseRequest::kApiUrl = "https://api.tidalhifi.com/v1"; +const char *TidalBaseRequest::kApiTokenB64 = "UDVYYmVvNUxGdkVTZUR5Ng=="; + +TidalBaseRequest::TidalBaseRequest(TidalService *service, NetworkAccessManager *network, QObject *parent) : + QObject(parent), + service_(service), + network_(network) + {} + +TidalBaseRequest::~TidalBaseRequest() { + + while (!replies_.isEmpty()) { + QNetworkReply *reply = replies_.takeFirst(); + disconnect(reply, 0, nullptr, 0); + reply->abort(); + reply->deleteLater(); + } + +} + +QNetworkReply *TidalBaseRequest::CreateRequest(const QString &ressource_name, const QList ¶ms_provided) { + + typedef QPair EncodedParam; + typedef QList EncodedParamList; + + ParamList params = ParamList() << params_provided + << Param("sessionId", session_id()) + << Param("countryCode", country_code()); + + QStringList query_items; + QUrlQuery url_query; + for (const Param& param : params) { + 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.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + req.setRawHeader("X-Tidal-SessionId", session_id().toUtf8()); + QNetworkReply *reply = network_->get(req); + replies_ << reply; + + //qLog(Debug) << "Tidal: Sending request" << url; + + return reply; + +} + +QByteArray TidalBaseRequest::GetReplyData(QNetworkReply *reply, QString &error, const bool send_login) { + + if (replies_.contains(reply)) { + replies_.removeAll(reply); + reply->deleteLater(); + } + else { + return QByteArray(); + } + + 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 + emit service_->Logout(); + if (send_login && login_attempts() < max_login_attempts() && !token().isEmpty() && !username().isEmpty() && !password().isEmpty()) { + qLog(Error) << "Tidal:" << failure_reason; + qLog(Info) << "Tidal:" << "Attempting to login."; + NeedLogin(); + emit service_->Login(); + } + else { + error = Error(failure_reason); + } + } + else { // Fail + error = Error(failure_reason); + } + } + return QByteArray(); + } + + return data; + +} + +QJsonObject TidalBaseRequest::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 TidalBaseRequest::ExtractItems(QByteArray &data, QString &error) { + + QJsonObject json_obj = ExtractJsonObj(data, error); + if (json_obj.isEmpty()) return QJsonValue(); + return ExtractItems(json_obj, error); + +} + +QJsonValue TidalBaseRequest::ExtractItems(QJsonObject &json_obj, QString &error) { + + if (!json_obj.contains("items")) { + error = Error("Json reply is missing items.", json_obj); + return QJsonArray(); + } + QJsonValue json_items = json_obj["items"]; + return json_items; + +} + +QString TidalBaseRequest::Error(QString error, QVariant debug) { + + qLog(Error) << "Tidal:" << error; + if (debug.isValid()) qLog(Debug) << debug; + + return error; + +} diff --git a/src/tidal/tidalbaserequest.h b/src/tidal/tidalbaserequest.h new file mode 100644 index 000000000..0d695c4a7 --- /dev/null +++ b/src/tidal/tidalbaserequest.h @@ -0,0 +1,111 @@ +/* + * Strawberry Music Player + * Copyright 2018, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef TIDALBASEREQUEST_H +#define TIDALBASEREQUEST_H + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/song.h" +#include "internet/internetservices.h" +#include "internet/internetservice.h" +#include "internet/internetsearch.h" +#include "tidalservice.h" + +class Application; +class NetworkAccessManager; +class TidalUrlHandler; +class CollectionBackend; +class CollectionModel; + +class TidalBaseRequest : public QObject { + Q_OBJECT + + public: + + enum QueryType { + QueryType_None, + QueryType_Artists, + QueryType_Albums, + QueryType_Songs, + QueryType_SearchArtists, + QueryType_SearchAlbums, + QueryType_SearchSongs, + QueryType_StreamURL, + }; + + TidalBaseRequest(TidalService *service, NetworkAccessManager *network, QObject *parent); + ~TidalBaseRequest(); + + typedef QPair Param; + typedef QList ParamList; + + QNetworkReply *CreateRequest(const QString &ressource_name, const QList ¶ms_provided); + QByteArray GetReplyData(QNetworkReply *reply, QString &error, const bool send_login); + 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()); + + QString token() { return service_->token(); } + 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(); } + bool fetchalbums() { return service_->fetchalbums(); } + QString coversize() { return service_->coversize(); } + + QString session_id() { return service_->session_id(); } + quint64 user_id() { return service_->user_id(); } + QString country_code() { return service_->country_code(); } + + bool authenticated() { return service_->authenticated(); } + bool need_login() { return need_login(); } + 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: + + static const char *kApiUrl; + static const char *kApiTokenB64; + + TidalService *service_; + NetworkAccessManager *network_; + QList replies_; + +}; + +#endif // TIDALBASEREQUEST_H diff --git a/src/tidal/tidalrequest.cpp b/src/tidal/tidalrequest.cpp new file mode 100644 index 000000000..8ff1d4a0d --- /dev/null +++ b/src/tidal/tidalrequest.cpp @@ -0,0 +1,821 @@ +/* + * 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 "core/closure.h" +#include "core/logging.h" +#include "core/network.h" +#include "core/song.h" +#include "core/timeconstants.h" +#include "tidalservice.h" +#include "tidalurlhandler.h" +#include "tidalrequest.h" + +const char *TidalRequest::kResourcesUrl = "http://resources.tidal.com"; + +TidalRequest::TidalRequest(TidalService *service, TidalUrlHandler *url_handler, NetworkAccessManager *network, QueryType type, QObject *parent) + : TidalBaseRequest(service, network, parent), + service_(service), + url_handler_(url_handler), + network_(network), + type_(type), + artist_query_(false), + artist_albums_requested_(0), + artist_albums_received_(0), + album_songs_requested_(0), + album_songs_received_(0), + album_covers_requested_(0), + album_covers_received_(0), + need_login_(false), + no_match_(false) + { + +} + +TidalRequest::~TidalRequest() { + + while (!replies_.isEmpty()) { + QNetworkReply *reply = replies_.takeFirst(); + disconnect(reply, 0, nullptr, 0); + reply->abort(); + reply->deleteLater(); + } + +} + +void TidalRequest::LoginComplete(bool success, QString error) { + + if (!need_login_) return; + need_login_ = false; + + if (!success) { + Error(error); + return; + } + + Process(); + +} + +void TidalRequest::Process() { + + if (!service_->authenticated()) { + need_login_ = true; + service_->TryLogin(); + return; + } + + switch (type_) { + case QueryType::QueryType_Artists: + GetArtists(); + break; + case QueryType::QueryType_Albums: + GetAlbums(); + break; + case QueryType::QueryType_Songs: + GetSongs(); + break; + case QueryType::QueryType_SearchArtists: + SendArtistsSearch(); + break; + case QueryType::QueryType_SearchAlbums: + SendAlbumsSearch(); + break; + case QueryType::QueryType_SearchSongs: + SendSongsSearch(); + break; + default: + Error("Invalid query type."); + break; + } + +} + +void TidalRequest::Search(const int search_id, const QString &search_text) { + search_id_ = search_id; + search_text_ = search_text; +} + +void TidalRequest::GetArtists() { + + emit UpdateStatus(tr("Retrieving artists...")); + + artist_query_ = true; + + ParamList parameters; + QNetworkReply *reply = CreateRequest(QString("users/%1/favorites/artists").arg(service_->user_id()), parameters); + NewClosure(reply, SIGNAL(finished()), this, SLOT(ArtistsReceived(QNetworkReply*)), reply); + +} + +void TidalRequest::GetAlbums() { + + emit UpdateStatus(tr("Retrieving albums...")); + + type_ = QueryType_Albums; + + if (!service_->authenticated()) { + need_login_ = true; + return; + } + + ParamList parameters; + QNetworkReply *reply = CreateRequest(QString("users/%1/favorites/albums").arg(service_->user_id()), parameters); + NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumsReceived(QNetworkReply*, int, int)), reply, 0, 0); + +} + +void TidalRequest::GetSongs() { + + emit UpdateStatus(tr("Retrieving songs...")); + + type_ = QueryType_Songs; + + if (!service_->authenticated()) { + need_login_ = true; + return; + } + + ParamList parameters; + QNetworkReply *reply = CreateRequest(QString("users/%1/favorites/tracks").arg(service_->user_id()), parameters); + NewClosure(reply, SIGNAL(finished()), this, SLOT(SongsReceived(QNetworkReply*, int)), reply, 0); + +} + +void TidalRequest::SendArtistsSearch() { + + if (!service_->authenticated()) { + need_login_ = true; + return; + } + + artist_query_ = true; + + ParamList parameters; + parameters << Param("query", search_text_); + parameters << Param("limit", QString::number(service_->artistssearchlimit())); + QNetworkReply *reply = CreateRequest("search/artists", parameters); + NewClosure(reply, SIGNAL(finished()), this, SLOT(ArtistsReceived(QNetworkReply*)), reply); + +} + +void TidalRequest::SendAlbumsSearch() { + + if (!service_->authenticated()) { + need_login_ = true; + return; + } + + ParamList parameters; + parameters << Param("query", search_text_); + parameters << Param("limit", QString::number(service_->albumssearchlimit())); + QNetworkReply *reply = CreateRequest("search/albums", parameters); + NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumsReceived(QNetworkReply*, int, int)), reply, 0, 0); + +} + +void TidalRequest::SendSongsSearch() { + + if (!service_->authenticated()) { + need_login_ = true; + return; + } + + ParamList parameters; + parameters << Param("query", search_text_); + parameters << Param("limit", QString::number(service_->songssearchlimit())); + QNetworkReply *reply = CreateRequest("search/tracks", parameters); + NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumsReceived(QNetworkReply*, int, int)), reply, 0, 0); + +} + +void TidalRequest::ArtistsReceived(QNetworkReply *reply) { + + QString error; + QByteArray data = GetReplyData(reply, error, true); + if (data.isEmpty()) { + artist_query_ = false; + CheckFinish(); + return; + } + + QJsonValue json_value = ExtractItems(data, error); + if (!json_value.isArray()) { + artist_query_ = false; + CheckFinish(); + return; + } + QJsonArray json_items = json_value.toArray(); + if (json_items.isEmpty()) { // Empty array means no match + artist_query_ = false; + no_match_ = true; + CheckFinish(); + return; + } + + for (const QJsonValue &value : json_items) { + if (!value.isObject()) { + qLog(Error) << "Tidal: Invalid Json reply, item not a object."; + qLog(Debug) << value; + continue; + } + QJsonObject json_obj = value.toObject(); + + if (json_obj.contains("item")) { + QJsonValue json_item = json_obj["item"]; + if (!json_item.isObject()) { + qLog(Error) << "Tidal: Invalid Json reply, item not a object."; + qLog(Debug) << json_item; + continue; + } + json_obj = json_item.toObject(); + } + + if (!json_obj.contains("id") || !json_obj.contains("name")) { + qLog(Error) << "Tidal: Invalid Json reply, item missing id or album."; + qLog(Debug) << json_obj; + continue; + } + + int artist_id = json_obj["id"].toInt(); + if (requests_artist_albums_.contains(artist_id)) continue; + requests_artist_albums_.append(artist_id); + GetArtistAlbums(artist_id); + artist_albums_requested_++; + if (artist_albums_requested_ >= service_->artistssearchlimit()) break; + + } + + if (artist_albums_requested_ > 0) { + if (artist_albums_requested_ == 1) emit UpdateStatus(tr("Retrieving albums for %1 artist...").arg(artist_albums_requested_)); + else emit UpdateStatus(tr("Retrieving albums for %1 artists...").arg(artist_albums_requested_)); + emit ProgressSetMaximum(artist_albums_requested_); + emit UpdateProgress(0); + } + else { + artist_query_ = false; + } + + CheckFinish(); + +} + +void TidalRequest::GetArtistAlbums(const int artist_id, const int offset) { + + ParamList 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)), reply, artist_id, offset); + +} + +void TidalRequest::AlbumsReceived(QNetworkReply *reply, const int artist_id, const int offset_requested) { + + QString error; + QByteArray data = GetReplyData(reply, error, (artist_id == 0)); + + if (artist_query_) { + if (!requests_artist_albums_.contains(artist_id)) return; + artist_albums_received_++; + emit UpdateProgress(artist_albums_received_); + } + + if (data.isEmpty()) { + AlbumsFinished(artist_id, offset_requested); + return; + } + + QJsonObject json_obj = ExtractJsonObj(data, error); + if (json_obj.isEmpty()) { + AlbumsFinished(artist_id, offset_requested); + return; + } + + int limit = 0; + int total_albums = 0; + if (artist_query_) { // 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(); + int 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, error); + 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_query_) no_match_ = true; + 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(); + + if (json_obj.contains("item")) { + QJsonValue json_item = json_obj["item"]; + if (!json_item.isObject()) { + qLog(Error) << "Tidal: Invalid Json reply, item not a object."; + qLog(Debug) << json_item; + continue; + } + json_obj = json_item.toObject(); + } + + int album_id = 0; + QString album; + if (json_obj.contains("type")) { // This was a albums request or search + if (!json_obj.contains("id") || !json_obj.contains("title")) { + qLog(Error) << "Tidal: Invalid Json reply, item is missing ID or title."; + qLog(Debug) << json_obj; + continue; + } + album_id = json_obj["id"].toInt(); + album = json_obj["title"].toString(); + } + else if (json_obj.contains("album")) { // This was a tracks request or search + if (!service_->fetchalbums()) { + Song song; + ParseSong(song, 0, value); + songs_ << song; + continue; + } + QJsonValue json_value_album = json_obj["album"]; + if (!json_value_album.isObject()) { + qLog(Error) << "Tidal: Invalid Json reply, item album is not a object."; + qLog(Debug) << json_value_album; + continue; + } + QJsonObject json_album = json_value_album.toObject(); + if (!json_album.contains("id") || !json_album.contains("title")) { + qLog(Error) << "Tidal: Invalid Json reply, item album is missing ID or title."; + qLog(Debug) << json_album; + continue; + } + album_id = json_album["id"].toInt(); + album = json_album["title"].toString(); + + } + else { + qLog(Error) << "Tidal: Invalid Json reply, item missing type or album."; + qLog(Debug) << json_obj; + continue; + } + + if (requests_album_songs_.contains(album_id)) continue; + + 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(Debug) << json_obj; + continue; + } + QJsonValue json_value_artist = json_obj["artist"]; + if (!json_value_artist.isObject()) { + qLog(Error) << "Tidal: Invalid Json reply, item artist is not a object."; + qLog(Debug) << json_value_artist; + continue; + } + QJsonObject json_artist = json_value_artist.toObject(); + if (!json_artist.contains("name")) { + qLog(Error) << "Tidal: Invalid Json reply, item artist missing name."; + qLog(Debug) << json_artist; + continue; + } + QString artist = json_artist["name"].toString(); + + QString quality = json_obj["audioQuality"].toString(); + QString copyright = json_obj["copyright"].toString(); + + //qLog(Debug) << "Tidal:" << artist << album << quality << copyright; + + requests_album_songs_.insert(album_id, artist); + album_songs_requested_++; + if (album_songs_requested_ >= service_->albumssearchlimit()) break; + } + + AlbumsFinished(artist_id, offset_requested, total_albums, limit, albums); + +} + +void TidalRequest::AlbumsFinished(const int artist_id, const int offset_requested, const int total_albums, const int limit, const int albums) { + + if (artist_query_) { // 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_ < service_->albumssearchlimit() && offset_next < total_albums) { + GetArtistAlbums(artist_id, offset_next); + artist_albums_requested_++; + } + else if (artist_albums_received_ >= artist_albums_requested_) { // Artist search is finished. + artist_query_ = false; + } + } + + if (!artist_query_) { + // Get songs for the albums. + QHashIterator i(requests_album_songs_); + while (i.hasNext()) { + i.next(); + GetAlbumSongs(i.key()); + } + + if (album_songs_requested_ > 0) { + if (album_songs_requested_ == 1) emit UpdateStatus(tr("Retrieving songs for %1 album...").arg(album_songs_requested_)); + else emit UpdateStatus(tr("Retrieving songs for %1 albums...").arg(album_songs_requested_)); + emit ProgressSetMaximum(album_songs_requested_); + emit UpdateProgress(0); + } + } + + CheckFinish(); + +} + +void TidalRequest::GetAlbumSongs(const int album_id) { + + ParamList parameters; + QNetworkReply *reply = CreateRequest(QString("albums/%1/tracks").arg(album_id), parameters); + NewClosure(reply, SIGNAL(finished()), this, SLOT(SongsReceived(QNetworkReply*, int)), reply, album_id); + +} + +void TidalRequest::SongsReceived(QNetworkReply *reply, const int album_id) { + + QString error; + QByteArray data = GetReplyData(reply, error, false); + + QString album_artist; + if (album_id != 0) { + if (!requests_album_songs_.contains(album_id)) return; + album_artist = requests_album_songs_[album_id]; + } + + album_songs_received_++; + if (!artist_query_) { + emit UpdateProgress(album_songs_received_); + } + + if (data.isEmpty()) { + CheckFinish(); + return; + } + + QJsonValue json_value = ExtractItems(data, error); + if (!json_value.isArray()) { + CheckFinish(); + return; + } + + QJsonArray json_items = json_value.toArray(); + if (json_items.isEmpty()) { + no_match_ = true; + CheckFinish(); + return; + } + + bool compilation = false; + bool multidisc = false; + SongList songs; + for (const QJsonValue &value : json_items) { + Song song; + ParseSong(song, album_id, value, album_artist); + if (!song.is_valid()) continue; + if (song.disc() >= 2) multidisc = true; + if (song.is_compilation()) compilation = true; + songs << song; + } + + for (Song &song : songs) { + if (compilation) song.set_compilation_detected(true); + if (multidisc) { + QString album_full(QString("%1 - (Disc %2)").arg(song.album()).arg(song.disc())); + song.set_album(album_full); + } + songs_ << song; + + } + + if (service_->cache_album_covers() && artist_albums_requested_ <= artist_albums_received_ && album_songs_requested_ <= album_songs_received_) { + GetAlbumCovers(); + } + + CheckFinish(); + +} + +int TidalRequest::ParseSong(Song &song, const int album_id_requested, const QJsonValue &value, QString album_artist) { + + if (!value.isObject()) { + qLog(Error) << "Tidal: Invalid Json reply, track is not a object."; + qLog(Debug) << value; + return -1; + } + QJsonObject json_obj = value.toObject(); + + if ( + !json_obj.contains("album") || + !json_obj.contains("allowStreaming") || + !json_obj.contains("artist") || + !json_obj.contains("artists") || + !json_obj.contains("audioQuality") || + !json_obj.contains("duration") || + !json_obj.contains("id") || + !json_obj.contains("streamReady") || + !json_obj.contains("title") || + !json_obj.contains("trackNumber") || + !json_obj.contains("url") || + !json_obj.contains("volumeNumber") || + !json_obj.contains("copyright") + ) { + qLog(Error) << "Tidal: Invalid Json reply, track is missing one or more values."; + qLog(Debug) << json_obj; + return -1; + } + + QJsonValue json_value_artist = json_obj["artist"]; + QJsonValue json_value_album = json_obj["album"]; + QJsonValue json_duration = json_obj["duration"]; + QJsonArray json_artists = json_obj["artists"].toArray(); + + int song_id = json_obj["id"].toInt(); + + QString title = json_obj["title"].toString(); + QString urlstr = json_obj["url"].toString(); + int track = json_obj["trackNumber"].toInt(); + int disc = json_obj["volumeNumber"].toInt(); + bool allow_streaming = json_obj["allowStreaming"].toBool(); + bool stream_ready = json_obj["streamReady"].toBool(); + QString copyright = json_obj["copyright"].toString(); + + if (!json_value_artist.isObject()) { + qLog(Error) << "Tidal: Invalid Json reply, track artist is not a object."; + qLog(Debug) << json_value_artist; + return -1; + } + QJsonObject json_artist = json_value_artist.toObject(); + if (!json_artist.contains("name")) { + qLog(Error) << "Tidal: Invalid Json reply, track artist is missing name."; + qLog(Debug) << json_artist; + return -1; + } + QString artist = json_artist["name"].toString(); + + if (!json_value_album.isObject()) { + qLog(Error) << "Tidal: Invalid Json reply, track album is not a object."; + qLog(Debug) << json_value_album; + return -1; + } + QJsonObject json_album = json_value_album.toObject(); + if (!json_album.contains("id") || !json_album.contains("title") || !json_album.contains("cover")) { + qLog(Error) << "Tidal: Invalid Json reply, track album is missing id, title or cover."; + qLog(Debug) << json_album; + return -1; + } + int album_id = json_album["id"].toInt(); + if (album_id_requested != 0 && album_id_requested != album_id) { + qLog(Error) << "Tidal: Invalid Json reply, track album id is wrong."; + qLog(Debug) << json_album; + return -1; + } + QString album = json_album["title"].toString(); + QString cover = json_album["cover"].toString(); + + if (!allow_streaming) { + qLog(Error) << "Tidal: Song" << artist << album << title << "is not allowStreaming"; + } + + if (!stream_ready) { + qLog(Error) << "Tidal: Song" << artist << album << title << "is not streamReady."; + } + + QUrl url; + url.setScheme(url_handler_->scheme()); + url.setPath(QString::number(song_id)); + + QVariant q_duration = json_duration.toVariant(); + quint64 duration = 0; + if (q_duration.isValid() && (q_duration.type() == QVariant::Int || q_duration.type() == QVariant::Double)) { + duration = q_duration.toInt() * kNsecPerSec; + } + else { + qLog(Error) << "Tidal: Invalid duration for song."; + qLog(Debug) << json_duration; + return -1; + } + + cover = cover.replace("-", "/"); + QUrl cover_url (QString("%1/images/%2/%3.jpg").arg(kResourcesUrl).arg(cover).arg(service_->coversize())); + + title.remove(Song::kTitleRemoveMisc); + + //qLog(Debug) << "id" << song_id << "track" << track << "disc" << disc << "title" << title << "album" << album << "album artist" << album_artist << "artist" << artist << cover << allow_streaming << url; + + song.set_source(Song::Source_Tidal); + song.set_album_id(album_id); + if (album_artist != artist) song.set_albumartist(album_artist); + song.set_album(album); + song.set_artist(artist); + song.set_title(title); + song.set_track(track); + song.set_disc(disc); + song.set_url(url); + song.set_length_nanosec(duration); + song.set_art_automatic(cover_url.toEncoded()); + song.set_comment(copyright); + song.set_directory_id(0); + song.set_filetype(Song::FileType_Stream); + song.set_filesize(0); + song.set_mtime(0); + song.set_ctime(0); + song.set_valid(true); + + return song_id; + +} + +void TidalRequest::GetAlbumCovers() { + + for (Song &song : songs_) { + GetAlbumCover(song); + } + + if (album_covers_requested_ == 1) emit UpdateStatus(tr("Retrieving album cover for %1 album...").arg(album_covers_requested_)); + else emit UpdateStatus(tr("Retrieving album covers for %1 albums...").arg(album_covers_requested_)); + emit ProgressSetMaximum(album_covers_requested_); + emit UpdateProgress(0); + +} + +void TidalRequest::GetAlbumCover(Song &song) { + + if (requests_album_covers_.contains(song.album_id())) { + requests_album_covers_.insertMulti(song.album_id(), &song); + return; + } + + album_covers_requested_++; + requests_album_covers_.insertMulti(song.album_id(), &song); + + QUrl url(song.art_automatic()); + QNetworkRequest req(url); + QNetworkReply *reply = network_->get(req); + NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumCoverReceived(QNetworkReply*, int, QUrl)), reply, song.album_id(), url); + replies_ << reply; + +} + +void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, int album_id, QUrl url) { + + if (replies_.contains(reply)) { + replies_.removeAll(reply); + reply->deleteLater(); + } + else { + CheckFinish(); + return; + } + + if (!requests_album_covers_.contains(album_id)) { + CheckFinish(); + return; + } + + album_covers_received_++; + emit UpdateProgress(album_covers_received_); + + QString error; + if (reply->error() != QNetworkReply::NoError) { + error = Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + requests_album_covers_.remove(album_id); + return; + } + + QByteArray data = reply->readAll(); + if (data.isEmpty()) { + error = Error(QString("Received empty image data for %1").arg(url.toString())); + requests_album_covers_.remove(album_id); + return; + } + + QImage image; + if (image.loadFromData(data)) { + + QDir dir; + if (dir.mkpath(service_->CoverCacheDir())) { + QString filename(service_->CoverCacheDir() + "/" + QString::number(album_id) + "-" + url.fileName()); + if (image.save(filename, "JPG")) { + while (requests_album_covers_.contains(album_id)) { + Song *song = requests_album_covers_.take(album_id); + song->set_art_automatic(filename); + } + } + } + + } + else { + error = Error(QString("Error decoding image data from %1").arg(url.toString())); + } + + CheckFinish(); + +} + +void TidalRequest::CheckFinish() { + + if (!need_login_ && + !artist_query_ && + artist_albums_requested_ <= artist_albums_received_ && + album_songs_requested_ <= album_songs_received_ && + album_covers_requested_ <= album_covers_received_ + ) { + if (songs_.isEmpty()) { + if (IsSearch()) { + if (no_match_) emit ErrorSignal(search_id_, tr("No match")); + else if (errors_.isEmpty()) emit ErrorSignal(search_id_, tr("Unknown error")); + else emit ErrorSignal(search_id_, errors_); + } + else { + if (no_match_) emit Results(songs_); + else if (errors_.isEmpty()) emit ErrorSignal(tr("Unknown error")); + else emit ErrorSignal(errors_); + } + } + else { + if (IsSearch()) { + emit SearchResults(search_id_, songs_); + } + else { + emit Results(songs_); + } + } + + } + +} + +QString TidalRequest::Error(QString error, QVariant debug) { + + qLog(Error) << "Tidal:" << error; + if (debug.isValid()) qLog(Debug) << debug; + + if (!error.isEmpty()) { + errors_ += error; + errors_ += "
"; + } + CheckFinish(); + + return error; + +} diff --git a/src/tidal/tidalrequest.h b/src/tidal/tidalrequest.h new file mode 100644 index 000000000..df188f844 --- /dev/null +++ b/src/tidal/tidalrequest.h @@ -0,0 +1,134 @@ +/* + * Strawberry Music Player + * Copyright 2018, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef TIDALREQUEST_H +#define TIDALREQUEST_H + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/song.h" +#include "tidalbaserequest.h" + +class NetworkAccessManager; +class TidalService; +class TidalUrlHandler; + +class TidalRequest : public TidalBaseRequest { + Q_OBJECT + + public: + + TidalRequest(TidalService *service, TidalUrlHandler *url_handler, 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); + void SendArtistsSearch(); + void SendAlbumsSearch(); + void SendSongsSearch(); + + signals: + void Login(); + void Login(const QString &username, const QString &password, const QString &token); + void LoginSuccess(); + void LoginFailure(QString failure_reason); + void Results(SongList songs); + void SearchResults(int id, SongList songs); + void ErrorSignal(QString message); + void ErrorSignal(int id, QString message); + void UpdateStatus(QString text); + void ProgressSetMaximum(int max); + void UpdateProgress(int max); + void StreamURLFinished(const QUrl original_url, const QUrl url, const Song::FileType, QString error = QString()); + + public slots: + void GetArtists(); + void GetAlbums(); + void GetSongs(); + + private slots: + void LoginComplete(bool success, QString error = QString()); + void ArtistsReceived(QNetworkReply *reply); + void AlbumsReceived(QNetworkReply *reply, const int artist_id, const int offset_requested = 0); + 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 album_id); + void AlbumCoverReceived(QNetworkReply *reply, int album_id, QUrl url); + + private: + typedef QPair Param; + typedef QList ParamList; + + const bool IsSearch() { return (type_ == QueryType_SearchArtists || type_ == QueryType_SearchAlbums || type_ == QueryType_SearchSongs); } + void SendSearch(); + void GetArtistAlbums(const int artist_id, const int offset = 0); + void GetAlbumSongs(const int album_id); + void GetSongs(const int album_id); + int ParseSong(Song &song, const int album_id_requested, const QJsonValue &value, QString album_artist = QString()); + void GetAlbumCovers(); + void GetAlbumCover(Song &song); + void CheckFinish(); + QString LoginError(QString error, QVariant debug = QVariant()); + QString Error(QString error, QVariant debug = QVariant()); + + static const char *kResourcesUrl; + + TidalService *service_; + TidalUrlHandler *url_handler_; + NetworkAccessManager *network_; + + QueryType type_; + bool artist_query_; + + int search_id_; + QString search_text_; + QList requests_artist_albums_; + QHash requests_album_songs_; + QMultiMap requests_album_covers_; + int artist_albums_requested_; + int artist_albums_received_; + int album_songs_requested_; + int album_songs_received_; + int album_covers_requested_; + int album_covers_received_; + SongList songs_; + QString errors_; + bool need_login_; + bool no_match_; + QList replies_; + +}; + +#endif // TIDALREQUEST_H diff --git a/src/tidal/tidalservice.cpp b/src/tidal/tidalservice.cpp index 2454fa196..25ffb85c4 100644 --- a/src/tidal/tidalservice.cpp +++ b/src/tidal/tidalservice.cpp @@ -19,11 +19,14 @@ #include "config.h" +#include +#include + #include +#include #include -#include -#include #include +#include #include #include #include @@ -33,61 +36,104 @@ #include #include #include -#include -#include -#include #include +#include #include "core/application.h" #include "core/player.h" #include "core/closure.h" #include "core/logging.h" -#include "core/mergedproxymodel.h" #include "core/network.h" +#include "core/database.h" #include "core/song.h" -#include "core/iconloader.h" -#include "core/taskmanager.h" -#include "core/timeconstants.h" -#include "core/utilities.h" -#include "internet/internetservices.h" #include "internet/internetsearch.h" +#include "collection/collectionbackend.h" +#include "collection/collectionmodel.h" #include "tidalservice.h" #include "tidalurlhandler.h" +#include "tidalrequest.h" +#include "tidalstreamurlrequest.h" #include "settings/tidalsettingspage.h" +using std::shared_ptr; + const Song::Source TidalService::kSource = Song::Source_Tidal; -const char *TidalService::kApiUrl = "https://api.tidalhifi.com/v1"; const char *TidalService::kAuthUrl = "https://api.tidalhifi.com/v1/login/username"; -const char *TidalService::kResourcesUrl = "http://resources.tidal.com"; const char *TidalService::kApiTokenB64 = "UDVYYmVvNUxGdkVTZUR5Ng=="; -const int TidalService::kLoginAttempts = 1; +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", app, parent), app_(app), network_(new NetworkAccessManager(this)), url_handler_(new TidalUrlHandler(app, this)), + artists_collection_backend_(new CollectionBackend()), + albums_collection_backend_(new CollectionBackend()), + songs_collection_backend_(new CollectionBackend()), + 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_(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)), search_delay_(1500), artistssearchlimit_(1), albumssearchlimit_(1), songssearchlimit_(1), - fetchalbums_(false), + fetchalbums_(true), + cache_album_covers_(true), user_id_(0), pending_search_id_(0), next_pending_search_id_(1), search_id_(0), - artist_search_(false), - artist_albums_requested_(0), - artist_albums_received_(0), - album_songs_requested_(0), - album_songs_received_(0), login_sent_(false), login_attempts_(0) { + app->player()->RegisterUrlHandler(url_handler_); + + // Backends + + artists_collection_backend_->moveToThread(app_->database()->thread()); + artists_collection_backend_->Init(app_->database(), kArtistsSongsTable, QString(), QString(), kArtistsSongsFtsTable); + + albums_collection_backend_->moveToThread(app_->database()->thread()); + albums_collection_backend_->Init(app_->database(), kAlbumsSongsTable, QString(), QString(), kAlbumsSongsFtsTable); + + songs_collection_backend_->moveToThread(app_->database()->thread()); + songs_collection_backend_->Init(app_->database(), kSongsTable, QString(), QString(), kSongsFtsTable); + + 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())); @@ -97,8 +143,6 @@ TidalService::TidalService(Application *app, QObject *parent) connect(this, SIGNAL(Login()), SLOT(SendLogin())); connect(this, SIGNAL(Login(QString, QString, QString)), SLOT(SendLogin(QString, QString, QString))); - app->player()->RegisterUrlHandler(url_handler_); - ReloadSettings(); LoadSessionID(); @@ -127,10 +171,15 @@ void TidalService::ReloadSettings() { songssearchlimit_ = s.value("songssearchlimit", 100).toInt(); fetchalbums_ = s.value("fetchalbums", false).toBool(); coversize_ = s.value("coversize", "320x320").toString(); + cache_album_covers_ = s.value("cachealbumcovers", true).toBool(); s.endGroup(); } +QString TidalService::CoverCacheDir() { + return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/tidalalbumcovers"; +} + void TidalService::LoadSessionID() { QSettings s; @@ -149,31 +198,28 @@ void TidalService::SendLogin() { void TidalService::SendLogin(const QString &username, const QString &password, const QString &token) { - if (search_id_ != 0) emit UpdateStatus(tr("Authenticating...")); + emit UpdateStatus(tr("Authenticating...")); login_sent_ = true; - login_attempts_++; + ++login_attempts_; if (timer_login_attempt_->isActive()) timer_login_attempt_->stop(); timer_login_attempt_->setInterval(kTimeResetLoginAttempts); timer_login_attempt_->start(); - typedef QPair Arg; - typedef QList ArgList; + typedef QPair EncodedParam; + typedef QList EncodedParamList; - typedef QPair EncodedArg; - typedef QList EncodedArgList; - - ArgList args = ArgList() << Arg("token", token_) - << Arg("username", username) - << Arg("password", password) - << Arg("clientVersion", "2.2.1--7"); + ParamList params = ParamList() << Param("token", token_) + << Param("username", username) + << Param("password", password) + << Param("clientVersion", "2.2.1--7"); QStringList query_items; QUrlQuery url_query; - for (const Arg &arg : args) { - EncodedArg encoded_arg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second)); - query_items << QString(encoded_arg.first + "=" + encoded_arg.second); - url_query.addQueryItem(encoded_arg.first, encoded_arg.second); + for (const Param ¶m : params) { + 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(kAuthUrl); @@ -184,6 +230,8 @@ void TidalService::SendLogin(const QString &username, const QString &password, c QNetworkReply *reply = network_->post(req, url_query.toString(QUrl::FullyEncoded).toUtf8()); NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleAuthReply(QNetworkReply*)), reply); + + //qLog(Debug) << "Tidal: Sending request" << url; } @@ -265,16 +313,10 @@ void TidalService::HandleAuthReply(QNetworkReply *reply) { qLog(Debug) << "Tidal: Login successful" << "user id" << user_id_ << "session id" << session_id_ << "country code" << country_code_; - if (search_id_ != 0) { - qLog(Debug) << "Tidal: Resuming search" << search_id_; - SendSearch(); - } - for (QUrl url : queue_stream_url_) { - qLog(Debug) << "Tidal: Resuming get stream url" << url; - GetStreamURL(url); - } - queue_stream_url_.clear(); + login_attempts_ = 0; + if (timer_login_attempt_->isActive()) timer_login_attempt_->stop(); + emit LoginComplete(true); emit LoginSuccess(); } @@ -298,139 +340,87 @@ void TidalService::ResetLoginAttempts() { login_attempts_ = 0; } -QNetworkReply *TidalService::CreateRequest(const QString &ressource_name, const QList ¶ms) { +void TidalService::TryLogin() { - typedef QPair Arg; - typedef QList ArgList; + if (authenticated() || login_sent_) return; - typedef QPair EncodedArg; - typedef QList EncodedArgList; - - ArgList args = ArgList() << params - << Arg("sessionId", session_id_) - << Arg("countryCode", country_code_); - - QStringList query_items; - QUrlQuery url_query; - for (const Arg& arg : args) { - EncodedArg encoded_arg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second)); - query_items << QString(encoded_arg.first + "=" + encoded_arg.second); - url_query.addQueryItem(encoded_arg.first, encoded_arg.second); + if (login_attempts_ >= kLoginAttempts) { + emit LoginComplete(false, "Maximum number of login attempts reached."); + return; + } + if (token_.isEmpty()) { + emit LoginComplete(false, "Missing Tidal API token."); + return; + } + if (username_.isEmpty()) { + emit LoginComplete(false, "Missing Tidal username."); + return; + } + if (password_.isEmpty()) { + emit LoginComplete(false, "Missing Tidal password."); + return; } - QUrl url(kApiUrl + QString("/") + ressource_name); - url.setQuery(url_query); - QNetworkRequest req(url); - req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - req.setRawHeader("X-Tidal-SessionId", session_id_.toUtf8()); - QNetworkReply *reply = network_->get(req); - - //qLog(Debug) << "Tidal: Sending request" << url; - - return reply; + emit Login(); } -QByteArray TidalService::GetReplyData(QNetworkReply *reply, QString &error, const bool sendlogin) { +void TidalService::GetArtists() { - QByteArray data; + artists_request_.reset(new TidalRequest(this, url_handler_, network_, TidalBaseRequest::QueryType_Artists, this)); - 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 - Logout(); - if (sendlogin && login_attempts_ < kLoginAttempts && !username_.isEmpty() && !password_.isEmpty()) { - qLog(Error) << "Tidal:" << failure_reason; - qLog(Info) << "Tidal:" << "Attempting to login."; - emit Login(); - } - else { - error = Error(failure_reason); - } - } - else { // Fail - error = Error(failure_reason); - } - } - return QByteArray(); - } + connect(artists_request_.get(), SIGNAL(ErrorSignal(QString)), SIGNAL(ArtistsError(QString))); + connect(artists_request_.get(), SIGNAL(UpdateStatus(QString)), SIGNAL(ArtistsUpdateStatus(QString))); + connect(artists_request_.get(), SIGNAL(ProgressSetMaximum(int)), SIGNAL(ArtistsProgressSetMaximum(int))); + connect(artists_request_.get(), SIGNAL(UpdateProgress(int)), SIGNAL(ArtistsUpdateProgress(int))); + connect(artists_request_.get(), SIGNAL(Results(SongList)), SIGNAL(ArtistsResults(SongList))); + connect(this, SIGNAL(LoginComplete(bool, QString)), artists_request_.get(), SLOT(LoginComplete(bool, QString))); - return data; + artists_request_->Process(); } -QJsonObject TidalService::ExtractJsonObj(QByteArray &data, QString &error) { +void TidalService::UpdateArtists(SongList songs) { - 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; + artists_collection_backend_->DeleteAll(); + artists_collection_backend_->AddOrUpdateSongs(songs); + artists_collection_model_->Reset(); } -QJsonValue TidalService::ExtractItems(QByteArray &data, QString &error) { +void TidalService::GetAlbums() { - QJsonObject json_obj = ExtractJsonObj(data, error); - if (json_obj.isEmpty()) return QJsonValue(); - return ExtractItems(json_obj, error); + albums_request_.reset(new TidalRequest(this, url_handler_, network_, TidalBaseRequest::QueryType_Albums, this)); + connect(albums_request_.get(), SIGNAL(ErrorSignal(QString)), SIGNAL(AlbumsError(QString))); + connect(albums_request_.get(), SIGNAL(UpdateStatus(QString)), SIGNAL(AlbumsUpdateStatus(QString))); + connect(albums_request_.get(), SIGNAL(ProgressSetMaximum(int)), SIGNAL(AlbumsProgressSetMaximum(int))); + connect(albums_request_.get(), SIGNAL(UpdateProgress(int)), SIGNAL(AlbumsUpdateProgress(int))); + connect(albums_request_.get(), SIGNAL(Results(SongList)), SIGNAL(AlbumsResults(SongList))); + connect(this, SIGNAL(LoginComplete(bool, QString)), albums_request_.get(), SLOT(LoginComplete(bool, QString))); + + albums_request_->Process(); } -QJsonValue TidalService::ExtractItems(QJsonObject &json_obj, QString &error) { +void TidalService::UpdateAlbums(SongList songs) { - 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; + albums_collection_backend_->DeleteAll(); + albums_collection_backend_->AddOrUpdateSongs(songs); + albums_collection_model_->Reset(); + +} + +void TidalService::GetSongs() { + + songs_request_.reset(new TidalRequest(this, url_handler_, network_, TidalBaseRequest::QueryType_Songs, this)); + connect(songs_request_.get(), SIGNAL(ErrorSignal(QString)), SIGNAL(SongsError(QString))); + connect(songs_request_.get(), SIGNAL(UpdateStatus(QString)), SIGNAL(SongsUpdateStatus(QString))); + connect(songs_request_.get(), SIGNAL(ProgressSetMaximum(int)), SIGNAL(SongsProgressSetMaximum(int))); + connect(songs_request_.get(), SIGNAL(UpdateProgress(int)), SIGNAL(SongsUpdateProgress(int))); + connect(songs_request_.get(), SIGNAL(Results(SongList)), SIGNAL(SongsResults(SongList))); + connect(this, SIGNAL(LoginComplete(bool, QString)), songs_request_.get(), SLOT(LoginComplete(bool, QString))); + + songs_request_->Process(); } @@ -455,638 +445,75 @@ int TidalService::Search(const QString &text, InternetSearch::SearchType type) { void TidalService::StartSearch() { - if (username_.isEmpty() || password_.isEmpty()) { - emit SearchError(pending_search_id_, tr("Missing username and/or password.")); + if (token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) { + emit SearchError(pending_search_id_, tr("Missing token, username and/or password.")); next_pending_search_id_ = 1; ShowConfig(); return; } - ClearSearch(); + search_id_ = pending_search_id_; search_text_ = pending_search_text_; - if (authenticated()) SendSearch(); - else emit Login(username_, password_, token_); + SendSearch(); } void TidalService::CancelSearch() { - ClearSearch(); -} - -void TidalService::ClearSearch() { - - search_id_ = 0; - search_text_.clear(); - search_error_.clear(); - artist_search_ = false; - artist_albums_requested_ = 0; - artist_albums_received_ = 0; - album_songs_requested_ = 0; - album_songs_received_ = 0; - requests_artist_albums_.clear(); - requests_album_songs_.clear(); - songs_.clear(); - } void TidalService::SendSearch() { - emit UpdateStatus(tr("Searching...")); + TidalBaseRequest::QueryType type; switch (pending_search_type_) { case InternetSearch::SearchType_Artists: - SendArtistsSearch(); + type = TidalBaseRequest::QueryType_SearchArtists; break; case InternetSearch::SearchType_Albums: - SendAlbumsSearch(); + type = TidalBaseRequest::QueryType_SearchAlbums; break; case InternetSearch::SearchType_Songs: - SendSongsSearch(); + type = TidalBaseRequest::QueryType_SearchSongs; break; default: - Error("Invalid search type."); - break; - } - -} - -void TidalService::SendArtistsSearch() { - - artist_search_ = true; - - QList 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 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 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(); - - if (search_id != search_id_) return; - - QString error; - - QByteArray data = GetReplyData(reply, error, true); - if (data.isEmpty()) { - artist_search_ = false; - CheckFinish(); - return; - } - - QJsonValue json_value = ExtractItems(data, error); - if (!json_value.isArray()) { - artist_search_ = false; - CheckFinish(); - return; - } - QJsonArray json_items = json_value.toArray(); - if (json_items.isEmpty()) { - artist_search_ = false; - Error(tr("No match.")); - return; - } - - for (const QJsonValue &value : json_items) { - if (!value.isObject()) { - qLog(Error) << "Tidal: Invalid Json reply, item not a object."; - qLog(Debug) << value; - continue; - } - QJsonObject json_obj = value.toObject(); - - if (!json_obj.contains("id") || !json_obj.contains("name")) { - qLog(Error) << "Tidal: Invalid Json reply, item missing type or album."; - qLog(Debug) << json_obj; - 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) { - if (artist_albums_requested_ == 1) emit UpdateStatus(tr("Retrieving albums for %1 artist...").arg(artist_albums_requested_)); - else emit UpdateStatus(tr("Retrieving albums for %1 artists...").arg(artist_albums_requested_)); - emit ProgressSetMaximum(artist_albums_requested_); - emit UpdateProgress(0); - } - - CheckFinish(); - -} - -void TidalService::GetAlbums(const int artist_id, const int offset) { - - QList 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_); - } - - QString error; - - QByteArray data = GetReplyData(reply, error, (artist_id == 0)); - if (data.isEmpty()) { - AlbumsFinished(artist_id, offset_requested); - return; - } - - QJsonObject json_obj = ExtractJsonObj(data, error); - if (json_obj.isEmpty()) { - AlbumsFinished(artist_id, offset_requested); - return; - } - - int limit = 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); + //Error("Invalid search type."); return; - } - limit = json_obj["limit"].toInt(); - int 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, error); - 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; - } + search_request_.reset(new TidalRequest(this, url_handler_, network_, type, this)); - 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(); + connect(search_request_.get(), SIGNAL(SearchResults(int, SongList)), SIGNAL(SearchResults(int, SongList))); + connect(search_request_.get(), SIGNAL(ErrorSignal(int, QString)), SIGNAL(SearchError(int, QString))); + connect(search_request_.get(), SIGNAL(UpdateStatus(QString)), SIGNAL(SearchUpdateStatus(QString))); + connect(search_request_.get(), SIGNAL(ProgressSetMaximum(int)), SIGNAL(SearchProgressSetMaximum(int))); + connect(search_request_.get(), SIGNAL(UpdateProgress(int)), SIGNAL(SearchUpdateProgress(int))); + connect(this, SIGNAL(LoginComplete(bool, QString)), search_request_.get(), SLOT(LoginComplete(bool, QString))); - 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")) { - qLog(Error) << "Tidal: Invalid Json reply, item is missing ID or title."; - qLog(Debug) << json_obj; - continue; - } - album_id = json_obj["id"].toInt(); - album = json_obj["title"].toString(); - } - else if (json_obj.contains("album")) { // This was a tracks search - if (!fetchalbums_) { - Song song = ParseSong(0, value); - songs_ << song; - continue; - } - QJsonValue json_value_album = json_obj["album"]; - if (!json_value_album.isObject()) { - qLog(Error) << "Tidal: Invalid Json reply, item album is not a object."; - qLog(Debug) << json_value_album; - continue; - } - QJsonObject json_album = json_value_album.toObject(); - if (!json_album.contains("id") || !json_album.contains("title")) { - qLog(Error) << "Tidal: Invalid Json reply, item album is missing ID or title."; - qLog(Debug) << json_album; - continue; - } - album_id = json_album["id"].toInt(); - album = json_album["title"].toString(); - - } - else { - qLog(Error) << "Tidal: Invalid Json reply, item missing type or album."; - qLog(Debug) << json_obj; - continue; - } - - if (requests_album_songs_.contains(album_id)) continue; - - 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(Debug) << json_obj; - continue; - } - QJsonValue json_value_artist = json_obj["artist"]; - if (!json_value_artist.isObject()) { - qLog(Error) << "Tidal: Invalid Json reply, item artist is not a object."; - qLog(Debug) << json_value_artist; - continue; - } - QJsonObject json_artist = json_value_artist.toObject(); - if (!json_artist.contains("name")) { - qLog(Error) << "Tidal: Invalid Json reply, item artist missing name."; - qLog(Debug) << json_artist; - continue; - } - QString artist = json_artist["name"].toString(); - - QString quality = json_obj["audioQuality"].toString(); - QString copyright = json_obj["copyright"].toString(); - - //qLog(Debug) << "Tidal:" << artist << album << quality << copyright; - - requests_album_songs_.insert(album_id, artist); - album_songs_requested_++; - if (album_songs_requested_ >= albumssearchlimit_) break; - } - - AlbumsFinished(artist_id, offset_requested, total_albums, limit, albums); - -} - -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 i(requests_album_songs_); - while (i.hasNext()) { - i.next(); - GetSongs(i.key()); - } - - if (album_songs_requested_ > 0) { - if (album_songs_requested_ == 1) emit UpdateStatus(tr("Retrieving songs for %1 album...").arg(album_songs_requested_)); - else emit UpdateStatus(tr("Retrieving songs for %1 albums...").arg(album_songs_requested_)); - emit ProgressSetMaximum(album_songs_requested_); - emit UpdateProgress(0); - } - } - - CheckFinish(); - -} - -void TidalService::GetSongs(const int album_id) { - - QList parameters; - QNetworkReply *reply = CreateRequest(QString("albums/%1/tracks").arg(album_id), parameters); - NewClosure(reply, SIGNAL(finished()), this, SLOT(SongsReceived(QNetworkReply*, int, int)), reply, search_id_, album_id); - -} - -void TidalService::SongsReceived(QNetworkReply *reply, int search_id, int album_id) { - - reply->deleteLater(); - - if (search_id != search_id_) return; - if (!requests_album_songs_.contains(album_id)) return; - QString album_artist = requests_album_songs_[album_id]; - - album_songs_received_++; - if (!artist_search_) { - emit UpdateProgress(album_songs_received_); - } - - QString error; - - QByteArray data = GetReplyData(reply, error, false); - if (data.isEmpty()) { - CheckFinish(); - return; - } - - QJsonValue json_value = ExtractItems(data, error); - if (!json_value.isArray()) { - CheckFinish(); - return; - } - - QJsonArray json_items = json_value.toArray(); - if (json_items.isEmpty()) { - CheckFinish(); - return; - } - - bool compilation = false; - bool multidisc = false; - SongList songs; - for (const QJsonValue &value : json_items) { - Song song = ParseSong(album_id, value, album_artist); - if (!song.is_valid()) continue; - if (song.disc() >= 2) multidisc = true; - if (song.is_compilation()) compilation = true; - songs << song; - } - for (Song &song : songs) { - if (compilation) song.set_compilation_detected(true); - if (multidisc) { - QString album_full(QString("%1 - (Disc %2)").arg(song.album()).arg(song.disc())); - song.set_album(album_full); - } - songs_ << song; - } - - CheckFinish(); - -} - -Song TidalService::ParseSong(const int album_id_requested, const QJsonValue &value, QString album_artist) { - - if (!value.isObject()) { - qLog(Error) << "Tidal: Invalid Json reply, track is not a object."; - qLog(Debug) << value; - return Song(); - } - QJsonObject json_obj = value.toObject(); - - if ( - !json_obj.contains("album") || - !json_obj.contains("allowStreaming") || - !json_obj.contains("artist") || - !json_obj.contains("artists") || - !json_obj.contains("audioQuality") || - !json_obj.contains("duration") || - !json_obj.contains("id") || - !json_obj.contains("streamReady") || - !json_obj.contains("title") || - !json_obj.contains("trackNumber") || - !json_obj.contains("url") || - !json_obj.contains("volumeNumber") || - !json_obj.contains("copyright") - ) { - qLog(Error) << "Tidal: Invalid Json reply, track is missing one or more values."; - qLog(Debug) << json_obj; - return Song(); - } - - QJsonValue json_value_artist = json_obj["artist"]; - QJsonValue json_value_album = json_obj["album"]; - QJsonValue json_duration = json_obj["duration"]; - QJsonArray json_artists = json_obj["artists"].toArray(); - - int song_id = json_obj["id"].toInt(); - - QString title = json_obj["title"].toString(); - QString urlstr = json_obj["url"].toString(); - int track = json_obj["trackNumber"].toInt(); - int disc = json_obj["volumeNumber"].toInt(); - bool allow_streaming = json_obj["allowStreaming"].toBool(); - bool stream_ready = json_obj["streamReady"].toBool(); - QString copyright = json_obj["copyright"].toString(); - - if (!json_value_artist.isObject()) { - qLog(Error) << "Tidal: Invalid Json reply, track artist is not a object."; - qLog(Debug) << json_value_artist; - return Song(); - } - QJsonObject json_artist = json_value_artist.toObject(); - if (!json_artist.contains("name")) { - qLog(Error) << "Tidal: Invalid Json reply, track artist is missing name."; - qLog(Debug) << json_artist; - return Song(); - } - QString artist = json_artist["name"].toString(); - - if (!json_value_album.isObject()) { - qLog(Error) << "Tidal: Invalid Json reply, track album is not a object."; - qLog(Debug) << json_value_album; - return Song(); - } - QJsonObject json_album = json_value_album.toObject(); - if (!json_album.contains("id") || !json_album.contains("title") || !json_album.contains("cover")) { - qLog(Error) << "Tidal: Invalid Json reply, track album is missing id, title or cover."; - qLog(Debug) << json_album; - return Song(); - } - int album_id = json_album["id"].toInt(); - if (album_id_requested != 0 && album_id_requested != album_id) { - qLog(Error) << "Tidal: Invalid Json reply, track album id is wrong."; - qLog(Debug) << json_album; - return Song(); - } - QString album = json_album["title"].toString(); - QString cover = json_album["cover"].toString(); - - if (!allow_streaming) { - qLog(Error) << "Tidal: Song" << artist << album << title << "is not allowStreaming"; - } - - if (!stream_ready) { - qLog(Error) << "Tidal: Song" << artist << album << title << "is not streamReady."; - } - - QUrl url; - url.setScheme(url_handler_->scheme()); - url.setPath(QString::number(song_id)); - - QVariant q_duration = json_duration.toVariant(); - quint64 duration = 0; - if (q_duration.isValid() && (q_duration.type() == QVariant::Int || q_duration.type() == QVariant::Double)) { - duration = q_duration.toInt() * kNsecPerSec; - } - else { - qLog(Error) << "Tidal: Invalid duration for song."; - qLog(Debug) << json_duration; - return Song(); - } - - cover = cover.replace("-", "/"); - QUrl cover_url (QString("%1/images/%2/%3.jpg").arg(kResourcesUrl).arg(cover).arg(coversize_)); - - title.remove(Song::kTitleRemoveMisc); - - //qLog(Debug) << "id" << id << "track" << track << "disc" << disc << "title" << title << "album" << album << "artist" << artist << cover << allow_streaming << url; - - Song song; - song.set_source(Song::Source_Tidal); - song.set_id(song_id); - song.set_album_id(album_id); - if (album_artist != artist) song.set_albumartist(album_artist); - song.set_album(album); - song.set_artist(artist); - song.set_title(title); - song.set_track(track); - song.set_disc(disc); - song.set_url(url); - song.set_length_nanosec(duration); - song.set_art_automatic(cover_url.toEncoded()); - song.set_comment(copyright); - song.set_valid(true); - - return song; + search_request_->Search(search_id_, search_text_); + search_request_->Process(); } void TidalService::GetStreamURL(const QUrl &url) { - if (login_sent_) { - queue_stream_url_ << url; - return; - } + TidalStreamURLRequest *stream_url_req = new TidalStreamURLRequest(this, network_, url, this); - int song_id = url.path().toInt(); - requests_stream_url_.insert(song_id, url); + connect(stream_url_req, SIGNAL(TryLogin()), this, SLOT(TryLogin())); + connect(stream_url_req, SIGNAL(StreamURLFinished(QUrl, QUrl, Song::FileType, QString)), this, SIGNAL(StreamURLFinished(QUrl, QUrl, Song::FileType, QString))); + connect(this, SIGNAL(LoginComplete(bool, QString)), stream_url_req, SLOT(LoginComplete(bool, QString))); - QList parameters; - parameters << Param("soundQuality", quality_); - - QNetworkReply *reply = CreateRequest(QString("tracks/%1/streamUrl").arg(song_id), parameters); - NewClosure(reply, SIGNAL(finished()), this, SLOT(StreamURLReceived(QNetworkReply*, int, QUrl)), reply, song_id, url); - -} - -void TidalService::StreamURLReceived(QNetworkReply *reply, const int song_id, const QUrl original_url) { - - reply->deleteLater(); - - if (!requests_stream_url_.contains(song_id)) return; - requests_stream_url_.remove(song_id); - - QString error; - - QByteArray data = GetReplyData(reply, error, true); - if (data.isEmpty()) { - if (login_sent_) { - queue_stream_url_ << original_url; - return; - } - emit StreamURLFinished(original_url, original_url, Song::FileType_Stream, error); - return; - } - - QJsonObject json_obj = ExtractJsonObj(data, error); - if (json_obj.isEmpty()) { - emit StreamURLFinished(original_url, original_url, Song::FileType_Stream, error); - return; - } - - if (!json_obj.contains("url") || !json_obj.contains("codec")) { - error = Error("Invalid Json reply, stream missing url or codec.", json_obj); - emit StreamURLFinished(original_url, original_url, Song::FileType_Stream, error); - return; - } - - QUrl new_url(json_obj["url"].toString()); - QString codec(json_obj["codec"].toString().toLower()); - Song::FileType filetype(Song::FiletypeByExtension(codec)); - if (filetype == Song::FileType_Unknown) { - qLog(Debug) << "Tidal: Unknown codec" << codec; - filetype = Song::FileType_Stream; - } - - emit StreamURLFinished(original_url, new_url, filetype); - -} - -void TidalService::CheckFinish() { - - if (search_id_ == 0) return; - - if (!login_sent_ && !artist_search_ && artist_albums_requested_ <= artist_albums_received_ && album_songs_requested_ <= album_songs_received_) { - if (songs_.isEmpty()) { - if (search_error_.isEmpty()) emit SearchError(search_id_, "Unknown error"); - else emit SearchError(search_id_, search_error_); - } - else emit SearchResults(search_id_, songs_); - ClearSearch(); - } + stream_url_req->Process(); } QString TidalService::LoginError(QString error, QVariant debug) { - emit LoginFailure(error); - - for (QUrl url : queue_stream_url_) { - emit StreamURLFinished(url, url, Song::FileType_Stream, error); - } - queue_stream_url_.clear(); - - Error(error); - - return error; - -} - -QString TidalService::Error(QString error, QVariant debug) { - qLog(Error) << "Tidal:" << error; if (debug.isValid()) qLog(Debug) << debug; - if (search_id_ != 0) { - if (!error.isEmpty()) { - search_error_ += error; - search_error_ += "
"; - } - CheckFinish(); - } + emit LoginFailure(error); + emit LoginComplete(false, error); return error; diff --git a/src/tidal/tidalservice.h b/src/tidal/tidalservice.h index 1b578583c..516f30e5c 100644 --- a/src/tidal/tidalservice.h +++ b/src/tidal/tidalservice.h @@ -22,27 +22,31 @@ #include "config.h" +#include +#include + #include #include +#include #include -#include #include #include #include #include -#include -#include -#include -#include #include "core/song.h" -#include "internet/internetservices.h" #include "internet/internetservice.h" #include "internet/internetsearch.h" +class QSortFilterProxyModel; class Application; class NetworkAccessManager; class TidalUrlHandler; +class TidalRequest; +class CollectionBackend; +class CollectionModel; + +using std::shared_ptr; class TidalService : public InternetService { Q_OBJECT @@ -54,81 +58,120 @@ class TidalService : public InternetService { static const Song::Source kSource; void ReloadSettings(); + QString CoverCacheDir(); void Logout(); int Search(const QString &query, InternetSearch::SearchType type); void CancelSearch(); - const bool login_sent() { return login_sent_; } - const bool authenticated() { return (!session_id_.isEmpty() && !country_code_.isEmpty()); } + const int max_login_attempts() { return kLoginAttempts; } QString token() { return token_; } + 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 cache_album_covers() { return cache_album_covers_; } + QString session_id() { return session_id_; } + quint64 user_id() { return user_id_; } QString country_code() { return country_code_; } + const bool authenticated() { return (!session_id_.isEmpty() && !country_code_.isEmpty()); } + const bool login_sent() { return login_sent_; } + const bool login_attempts() { return login_attempts_; } + void GetStreamURL(const QUrl &url); + CollectionBackend *artists_collection_backend() { return artists_collection_backend_; } + CollectionBackend *albums_collection_backend() { return albums_collection_backend_; } + CollectionBackend *songs_collection_backend() { return songs_collection_backend_; } + + CollectionModel *artists_collection_model() { return artists_collection_model_; } + CollectionModel *albums_collection_model() { return albums_collection_model_; } + CollectionModel *songs_collection_model() { return songs_collection_model_; } + + QSortFilterProxyModel *artists_collection_sort_model() { return artists_collection_sort_model_; } + QSortFilterProxyModel *albums_collection_sort_model() { return albums_collection_sort_model_; } + QSortFilterProxyModel *songs_collection_sort_model() { return songs_collection_sort_model_; } + + enum QueryType { + QueryType_Artists, + QueryType_Albums, + QueryType_Songs, + QueryType_SearchArtists, + QueryType_SearchAlbums, + QueryType_SearchSongs, + }; + signals: - void Login(); - void Login(const QString &username, const QString &password, const QString &token); - void LoginSuccess(); - void LoginFailure(QString failure_reason); - void SearchResults(int id, SongList songs); - void SearchError(int id, QString message); - void UpdateStatus(QString text); - void ProgressSetMaximum(int max); - void UpdateProgress(int max); - void StreamURLFinished(const QUrl original_url, const QUrl url, const Song::FileType, QString error = QString()); public slots: void ShowConfig(); + void TryLogin(); void SendLogin(const QString &username, const QString &password, const QString &token); + void GetArtists(); + void GetAlbums(); + void GetSongs(); private slots: void SendLogin(); void HandleAuthReply(QNetworkReply *reply); void ResetLoginAttempts(); void StartSearch(); - void ArtistsReceived(QNetworkReply *reply, int search_id); - void AlbumsReceived(QNetworkReply *reply, int search_id, int artist_id, int offset_requested = 0); - 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); + void UpdateArtists(SongList songs); + void UpdateAlbums(SongList songs); private: typedef QPair Param; + typedef QList ParamList; - void ClearSearch(); void LoadSessionID(); - QNetworkReply *CreateRequest(const QString &ressource_name, const QList> ¶ms); - QByteArray GetReplyData(QNetworkReply *reply, QString &error, const bool sendlogin = false); - QJsonObject ExtractJsonObj(QByteArray &data, QString &error); - QJsonValue ExtractItems(QByteArray &data, QString &error); - QJsonValue ExtractItems(QJsonObject &json_obj, QString &error); void SendSearch(); - void SendArtistsSearch(); - 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(); QString LoginError(QString error, QVariant debug = QVariant()); - QString Error(QString error, QVariant debug = QVariant()); - static const char *kApiUrl; static const char *kAuthUrl; - static const char *kResourcesUrl; static const char *kApiTokenB64; static const int kLoginAttempts; static const int kTimeResetLoginAttempts; + static const char *kArtistsSongsTable; + static const char *kAlbumsSongsTable; + static const char *kSongsTable; + + static const char *kArtistsSongsFtsTable; + static const char *kAlbumsSongsFtsTable; + static const char *kSongsFtsTable; + Application *app_; NetworkAccessManager *network_; TidalUrlHandler *url_handler_; + + CollectionBackend *artists_collection_backend_; + CollectionBackend *albums_collection_backend_; + CollectionBackend *songs_collection_backend_; + + CollectionModel *artists_collection_model_; + CollectionModel *albums_collection_model_; + CollectionModel *songs_collection_model_; + + QSortFilterProxyModel *artists_collection_sort_model_; + QSortFilterProxyModel *albums_collection_sort_model_; + QSortFilterProxyModel *songs_collection_sort_model_; + QTimer *timer_search_delay_; QTimer *timer_login_attempt_; + std::shared_ptr artists_request_; + std::shared_ptr albums_request_; + std::shared_ptr songs_request_; + std::shared_ptr search_request_; + QString token_; QString username_; QString password_; @@ -139,6 +182,8 @@ class TidalService : public InternetService { int songssearchlimit_; bool fetchalbums_; QString coversize_; + bool cache_album_covers_; + QString session_id_; quint64 user_id_; QString country_code_; @@ -150,20 +195,8 @@ class TidalService : public InternetService { int search_id_; QString search_text_; - bool artist_search_; - QList requests_artist_albums_; - QHash requests_album_songs_; - QHash requests_stream_url_; - QList queue_stream_url_; - int artist_albums_requested_; - int artist_albums_received_; - int album_songs_requested_; - int album_songs_received_; - SongList songs_; - QString search_error_; bool login_sent_; int login_attempts_; - QUrl stream_request_url_; }; diff --git a/src/tidal/tidalstreamurlrequest.cpp b/src/tidal/tidalstreamurlrequest.cpp new file mode 100644 index 000000000..afe996bf5 --- /dev/null +++ b/src/tidal/tidalstreamurlrequest.cpp @@ -0,0 +1,145 @@ +/* + * 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 "core/logging.h" +#include "core/network.h" +#include "core/song.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), + reply_(nullptr), + original_url_(original_url), + song_id_(original_url.path().toInt()), + tries_(0), + need_login_(false) {} + +TidalStreamURLRequest::~TidalStreamURLRequest() { + Cancel(); +} + +void TidalStreamURLRequest::LoginComplete(bool success, QString error) { + + if (!need_login_) return; + need_login_ = false; + + if (!success) { + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error); + return; + } + + Process(); + +} + +void TidalStreamURLRequest::Process() { + + if (!authenticated()) { + need_login_ = true; + emit TryLogin(); + return; + } + GetStreamURL(); + +} + +void TidalStreamURLRequest::Cancel() { + + if (reply_) { + if (reply_->isRunning()) { + reply_->abort(); + } + reply_->deleteLater(); + reply_ = nullptr; + } + +} + +void TidalStreamURLRequest::GetStreamURL() { + + ++tries_; + + ParamList parameters; + parameters << Param("soundQuality", quality()); + + if (reply_) { + Cancel(); + } + reply_ = CreateRequest(QString("tracks/%1/streamUrl").arg(song_id_), parameters); + connect(reply_, SIGNAL(finished()), this, SLOT(StreamURLReceived())); + +} + +void TidalStreamURLRequest::StreamURLReceived() { + + if (!reply_) return; + disconnect(reply_, 0, nullptr, 0); + reply_->deleteLater(); + + QString error; + + QByteArray data = GetReplyData(reply_, error, 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, error); + return; + } + + QJsonObject json_obj = ExtractJsonObj(data, error); + if (json_obj.isEmpty()) { + reply_ = nullptr; + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error); + return; + } + + if (!json_obj.contains("url") || !json_obj.contains("codec")) { + reply_ = nullptr; + error = Error("Invalid Json reply, stream missing url or codec.", json_obj); + emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, error); + return; + } + + QUrl new_url(json_obj["url"].toString()); + QString codec(json_obj["codec"].toString().toLower()); + Song::FileType filetype(Song::FiletypeByExtension(codec)); + if (filetype == Song::FileType_Unknown) { + qLog(Debug) << "Tidal: Unknown codec" << codec; + filetype = Song::FileType_Stream; + } + + emit StreamURLFinished(original_url_, new_url, filetype, QString()); + + reply_ = nullptr; + deleteLater(); + +} diff --git a/src/tidal/tidalstreamurlrequest.h b/src/tidal/tidalstreamurlrequest.h new file mode 100644 index 000000000..feaf53101 --- /dev/null +++ b/src/tidal/tidalstreamurlrequest.h @@ -0,0 +1,68 @@ +/* + * Strawberry Music Player + * Copyright 2018, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef TIDALSTREAMURLREQUEST_H +#define TIDALSTREAMURLREQUEST_H + +#include "config.h" + +#include +#include + +#include "core/song.h" +#include "tidalbaserequest.h" + +class QNetworkReply; +class NetworkAccessManager; +class TidalService; + +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(); + + 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, QString error = QString()); + + private slots: + void LoginComplete(bool success, QString error = QString()); + void StreamURLReceived(); + + private: + QNetworkReply *reply_; + QUrl original_url_; + int song_id_; + int tries_; + bool need_login_; + +}; + +#endif // TIDALSTREAMURLREQUEST_H