Add internet tabs view and tidal favorites (#167)

This commit is contained in:
Jonas Kvinge 2019-05-27 21:10:37 +02:00 committed by GitHub
parent c4b732ff93
commit 890fba0f61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 3844 additions and 1081 deletions

View File

@ -4,6 +4,7 @@
<file>schema/schema-1.sql</file>
<file>schema/schema-2.sql</file>
<file>schema/schema-3.sql</file>
<file>schema/schema-4.sql</file>
<file>schema/device-schema.sql</file>
<file>style/strawberry.css</file>
<file>html/playing-tooltip-plain.html</file>

205
data/schema/schema-4.sql Normal file
View File

@ -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;

View File

@ -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,

View File

@ -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

View File

@ -0,0 +1,165 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QtGlobal>
#include <QAbstractItemView>
#include <QStyleOptionViewItem>
#include <QVariant>
#include <QString>
#include <QLocale>
#include <QPainter>
#include <QPalette>
#include <QPen>
#include <QRect>
#include <QSize>
#include <QBrush>
#include <QColor>
#include <QFont>
#include <QPixmap>
#include <QIcon>
#include <QLinearGradient>
#include <QToolTip>
#include <QWhatsThis>
#include <QtEvents>
#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<QPixmap>()) {
pixmap = decoration.value<QPixmap>();
}
else if (decoration.canConvert<QIcon>()) {
pixmap = decoration.value<QIcon>().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<QHelpEvent*>(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;
}

View File

@ -0,0 +1,46 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef COLLECTIONITEMDELEGATE_H
#define COLLECTIONITEMDELEGATE_H
#include "config.h"
#include <stdbool.h>
#include <QStyledItemDelegate>
#include <QAbstractItemView>
#include <QPainter>
#include <QStyleOptionViewItem>
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

View File

@ -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;

View File

@ -20,8 +20,6 @@
#include "config.h"
#include <qcoreevent.h>
#include <QtGlobal>
#include <QWidget>
#include <QItemSelectionModel>
@ -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<QPixmap>()) {
pixmap = decoration.value<QPixmap>();
}
else if (decoration.canConvert<QIcon>()) {
pixmap = decoration.value<QIcon>().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<QHelpEvent*>(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),

View File

@ -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

View File

@ -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;

View File

@ -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
}

View File

@ -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_;

View File

@ -19,8 +19,7 @@
#include "config.h"
#include <algorithm>
#include <functional>
#include <stdbool.h>
#include <QObject>
#include <QList>
@ -29,19 +28,18 @@
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonValue>
#include <QJsonObject>
#include <QJsonArray>
#include <QSettings>
#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"

View File

@ -32,7 +32,6 @@
#include <QNetworkReply>
#include <QJsonValue>
#include <QJsonObject>
#include <QJsonArray>
#include "coverprovider.h"

View File

@ -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"

View File

@ -41,7 +41,7 @@
#include <QContextMenuEvent>
#include "core/song.h"
#include "collection/collectionview.h"
#include "collection/collectionitemdelegate.h"
#include "widgets/autoexpandingtreeview.h"
class Application;

View File

@ -0,0 +1,441 @@
/*
* Strawberry Music Player
* This code was part of Clementine
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QtGlobal>
#include <QTreeView>
#include <QSortFilterProxyModel>
#include <QAbstractItemView>
#include <QVariant>
#include <QString>
#include <QPainter>
#include <QRect>
#include <QFont>
#include <QFontMetrics>
#include <QMimeData>
#include <QMenu>
#include <QtEvents>
#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<QSortFilterProxyModel*>(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<QSortFilterProxyModel*>(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<QSortFilterProxyModel*>(model())->mapToSource(context_menu_index_);
QModelIndexList selected_indexes = qobject_cast<QSortFilterProxyModel*>(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<MimeData*>(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<MimeData*>(data)) {
mime_data->enqueue_now_ = true;
}
emit AddToPlaylistSignal(data);
}
void InternetCollectionView::AddToPlaylistEnqueueNext() {
QMimeData *data = model()->mimeData(selectedIndexes());
if (MimeData *mime_data = qobject_cast<MimeData*>(data)) {
mime_data->enqueue_next_now_ = true;
}
emit AddToPlaylistSignal(data);
}
void InternetCollectionView::OpenInNewPlaylist() {
QMimeData *data = model()->mimeData(selectedIndexes());
if (MimeData* mime_data = qobject_cast<MimeData*>(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<QSortFilterProxyModel*>(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_;
}

View File

@ -0,0 +1,146 @@
/*
* Strawberry Music Player
* This code was part of Clementine
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef INTERNETCOLLECTIONVIEW_H
#define INTERNETCOLLECTIONVIEW_H
#include "config.h"
#include <stdbool.h>
#include <QObject>
#include <QWidget>
#include <QSet>
#include <QString>
#include <QPixmap>
#include <QAction>
#include <QMenu>
#include <QtEvents>
#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<QString> last_selected_path_;
};
#endif // INTERNETCOLLECTIONVIEW_H

View File

@ -0,0 +1,58 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QWidget>
#include <QPushButton>
#include <QLabel>
#include <QProgressBar>
#include <QKeyEvent>
#include <QContextMenuEvent>
#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) {
}

View File

@ -0,0 +1,66 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef INTERNETCOLLECTIONVIEWCONTAINER_H
#define INTERNETCOLLECTIONVIEWCONTAINER_H
#include "config.h"
#include <QWidget>
#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

View File

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>InternetCollectionViewContainer</class>
<widget class="QWidget" name="InternetCollectionViewContainer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="refresh">
<property name="text">
<string>Refresh catalogue</string>
</property>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="stacked">
<widget class="QWidget" name="help_page">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="status">
<property name="enabled">
<bool>true</bool>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>40</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressbar">
<property name="enabled">
<bool>true</bool>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="internetcollection_page">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="CollectionFilterWidget" name="filter" native="true"/>
</item>
<item>
<widget class="InternetCollectionView" name="view"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>CollectionFilterWidget</class>
<extends>QWidget</extends>
<header>collection/collectionfilterwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>InternetCollectionView</class>
<extends>QTreeView</extends>
<header location="global">internet/internetcollectionview.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -21,8 +21,6 @@
#include "config.h"
#include <algorithm>
#include <QtGlobal>
#include <QObject>
#include <QList>
@ -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<int, DelayedSearch>::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<int, DelayedSearch>::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);
}

View File

@ -24,10 +24,13 @@
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QFuture>
#include <QIcon>
#include <QMetaType>
#include <QMap>
#include <QString>
#include <QStringList>
#include <QImage>
#include <QPixmap>
#include <QPixmapCache>
#include "core/song.h"
@ -58,7 +61,6 @@ class InternetSearch : public QObject {
typedef QList<Result> 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);

View File

@ -18,18 +18,20 @@
*
*/
#include <QPainter>
#include <QStyleOptionViewItem>
#include <QPainter>
#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<InternetSearchView*>(view_)->LazyLoadArt(index);
CollectionItemDelegate::paint(painter, option, index);
}

View File

@ -21,11 +21,11 @@
#ifndef INTERNETSEARCHITEMDELEGATE_H
#define INTERNETSEARCHITEMDELEGATE_H
#include <QPainter>
#include <QStyleOptionViewItem>
#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

View File

@ -20,15 +20,13 @@
#include "config.h"
#include <QObject>
#include <QStandardItem>
#include <QStandardItemModel>
#include <QMimeData>
#include <QList>
#include <QSet>
#include <QVariant>
#include <QString>
#include <QPixmap>
#include <QMimeData>
#include "core/mimedata.h"
#include "core/iconloader.h"

View File

@ -21,30 +21,24 @@
#include "config.h"
#include <functional>
#include <algorithm>
#include <QtGlobal>
#include <QWidget>
#include <QTimer>
#include <QList>
#include <QString>
#include <QStringList>
#include <QPixmap>
#include <QPalette>
#include <QColor>
#include <QFont>
#include <QMenu>
#include <QSortFilterProxyModel>
#include <QStandardItem>
#include <QSettings>
#include <QMenu>
#include <QAction>
#include <QSettings>
#include <QtEvents>
#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() {

View File

@ -25,25 +25,28 @@
#include "config.h"
#include <QWidget>
#include <QObject>
#include <QTimer>
#include <QMap>
#include <QList>
#include <QString>
#include <QIcon>
#include <QPixmap>
#include <QMimeData>
#include <QMenu>
#include <QSortFilterProxyModel>
#include <QAction>
#include <QActionGroup>
#include <QtEvents>
#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);

View File

@ -72,7 +72,7 @@
<bool>true</bool>
</property>
<property name="text">
<string>Search for</string>
<string>Search type</string>
</property>
<property name="margin">
<number>10</number>
@ -215,7 +215,7 @@
<x>0</x>
<y>0</y>
<width>398</width>
<height>521</height>
<height>511</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">

View File

@ -18,14 +18,12 @@
*/
#include <QObject>
#include <QStandardItem>
#include <QVariant>
#include <QString>
#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) {

View File

@ -21,23 +21,17 @@
#define INTERNETSERVICE_H
#include <QObject>
#include <QStandardItem>
#include <QAction>
#include <QObject>
#include <QList>
#include <QString>
#include <QUrl>
#include <QIcon>
#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_;

View File

@ -24,19 +24,11 @@
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QStandardItemModel>
#include <QMap>
#include <QString>
#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 {

View File

@ -0,0 +1,213 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QtGlobal>
#include <QWidget>
#include <QString>
#include <QStackedWidget>
#include <QContextMenuEvent>
#include <QSettings>
#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);
}

View File

@ -0,0 +1,76 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef INTERNETTABSVIEW_H
#define INTERNETTABSVIEW_H
#include "config.h"
#include <QWidget>
#include <QString>
#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

View File

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>InternetTabsView</class>
<widget class="QWidget" name="InternetTabsView">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>660</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTabWidget" name="tabs">
<property name="currentIndex">
<number>3</number>
</property>
<widget class="QWidget" name="artists">
<attribute name="title">
<string>Artists</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_7">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="InternetCollectionViewContainer" name="artists_collection" native="true"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="albums">
<attribute name="title">
<string>Albums</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_8">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="InternetCollectionViewContainer" name="albums_collection" native="true"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="songs">
<attribute name="title">
<string>Songs</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_9">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="InternetCollectionViewContainer" name="songs_collection" native="true"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="search">
<attribute name="title">
<string>Search</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="InternetSearchView" name="search_view" native="true"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>InternetSearchView</class>
<extends>QWidget</extends>
<header>internet/internetsearchview.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>InternetCollectionViewContainer</class>
<extends>QWidget</extends>
<header>internet/internetcollectionviewcontainer.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -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();

View File

@ -352,6 +352,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkbox_cache_album_covers">
<property name="text">
<string>Cache album covers</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="layout_coversize">
<item>

View File

@ -0,0 +1,212 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QObject>
#include <QByteArray>
#include <QPair>
#include <QList>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonParseError>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#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<Param> &params_provided) {
typedef QPair<QByteArray, QByteArray> EncodedParam;
typedef QList<EncodedParam> 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;
}

View File

@ -0,0 +1,111 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef TIDALBASEREQUEST_H
#define TIDALBASEREQUEST_H
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QList>
#include <QPair>
#include <QString>
#include <QUrl>
#include <QNetworkReply>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#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<QString, QString> Param;
typedef QList<Param> ParamList;
QNetworkReply *CreateRequest(const QString &ressource_name, const QList<Param> &params_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<QNetworkReply*> replies_;
};
#endif // TIDALBASEREQUEST_H

821
src/tidal/tidalrequest.cpp Normal file
View File

@ -0,0 +1,821 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QObject>
#include <QByteArray>
#include <QDir>
#include <QString>
#include <QUrl>
#include <QImage>
#include <QNetworkReply>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#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<int, QString> 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_ += "<br />";
}
CheckFinish();
return error;
}

134
src/tidal/tidalrequest.h Normal file
View File

@ -0,0 +1,134 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef TIDALREQUEST_H
#define TIDALREQUEST_H
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QPair>
#include <QList>
#include <QHash>
#include <QMap>
#include <QMultiMap>
#include <QString>
#include <QUrl>
#include <QNetworkReply>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#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<QString, QString> Param;
typedef QList<Param> 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<int> requests_artist_albums_;
QHash<int, QString> requests_album_songs_;
QMultiMap<int, Song*> 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<QNetworkReply*> replies_;
};
#endif // TIDALREQUEST_H

File diff suppressed because it is too large Load Diff

View File

@ -22,27 +22,31 @@
#include "config.h"
#include <memory>
#include <stdbool.h>
#include <QtGlobal>
#include <QObject>
#include <QPair>
#include <QList>
#include <QHash>
#include <QString>
#include <QUrl>
#include <QNetworkReply>
#include <QTimer>
#include <QDateTime>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#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<QString, QString> Param;
typedef QList<Param> ParamList;
void ClearSearch();
void LoadSessionID();
QNetworkReply *CreateRequest(const QString &ressource_name, const QList<QPair<QString, QString>> &params);
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<TidalRequest> artists_request_;
std::shared_ptr<TidalRequest> albums_request_;
std::shared_ptr<TidalRequest> songs_request_;
std::shared_ptr<TidalRequest> 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<int> requests_artist_albums_;
QHash<int, QString> requests_album_songs_;
QHash<int, QUrl> requests_stream_url_;
QList<QUrl> 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_;
};

View File

@ -0,0 +1,145 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QObject>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QJsonObject>
#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();
}

View File

@ -0,0 +1,68 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef TIDALSTREAMURLREQUEST_H
#define TIDALSTREAMURLREQUEST_H
#include "config.h"
#include <QString>
#include <QUrl>
#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