Add tidal support

This commit is contained in:
Jonas Kvinge 2018-08-09 18:10:03 +02:00
parent 26062bd07b
commit 820124f9e1
74 changed files with 5420 additions and 273 deletions

View File

@ -1,6 +1,7 @@
<RCC>
<qresource prefix="/">
<file>schema/schema.sql</file>
<file>schema/schema-1.sql</file>
<file>schema/device-schema.sql</file>
<file>style/mainwindow.css</file>
<file>style/statusview.css</file>
@ -113,6 +114,7 @@
<file>icons/128x128/xine.png</file>
<file>icons/128x128/zoom-in.png</file>
<file>icons/128x128/zoom-out.png</file>
<file>icons/128x128/tidal.png</file>
<file>icons/64x64/albums.png</file>
<file>icons/64x64/alsa.png</file>
<file>icons/64x64/application-exit.png</file>
@ -201,6 +203,7 @@
<file>icons/64x64/xine.png</file>
<file>icons/64x64/zoom-in.png</file>
<file>icons/64x64/zoom-out.png</file>
<file>icons/64x64/tidal.png</file>
<file>icons/48x48/albums.png</file>
<file>icons/48x48/alsa.png</file>
<file>icons/48x48/application-exit.png</file>
@ -292,6 +295,7 @@
<file>icons/48x48/xine.png</file>
<file>icons/48x48/zoom-in.png</file>
<file>icons/48x48/zoom-out.png</file>
<file>icons/48x48/tidal.png</file>
<file>icons/32x32/albums.png</file>
<file>icons/32x32/alsa.png</file>
<file>icons/32x32/application-exit.png</file>
@ -384,6 +388,7 @@
<file>icons/32x32/xine.png</file>
<file>icons/32x32/zoom-in.png</file>
<file>icons/32x32/zoom-out.png</file>
<file>icons/32x32/tidal.png</file>
<file>icons/22x22/albums.png</file>
<file>icons/22x22/alsa.png</file>
<file>icons/22x22/application-exit.png</file>
@ -476,5 +481,6 @@
<file>icons/22x22/xine.png</file>
<file>icons/22x22/zoom-in.png</file>
<file>icons/22x22/zoom-out.png</file>
<file>icons/22x22/tidal.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
data/icons/22x22/tidal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

BIN
data/icons/32x32/tidal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
data/icons/48x48/tidal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
data/icons/64x64/tidal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
data/icons/full/tidal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

3
data/schema/schema-1.sql Normal file
View File

@ -0,0 +1,3 @@
ALTER TABLE playlist_items ADD COLUMN internet_service TEXT;
UPDATE schema_version SET version=1;

View File

@ -1,21 +1,21 @@
CREATE TABLE schema_version (
CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER NOT NULL
);
DELETE FROM schema_version;
REPLACE INTO schema_version (version) VALUES (1);
INSERT INTO schema_version (version) VALUES (0);
CREATE TABLE directories (
CREATE TABLE IF NOT EXISTS directories (
path TEXT NOT NULL,
subdirs INTEGER NOT NULL
);
CREATE TABLE subdirectories (
CREATE TABLE IF NOT EXISTS subdirectories (
directory_id INTEGER NOT NULL,
path TEXT NOT NULL,
mtime INTEGER NOT NULL
);
CREATE TABLE songs (
CREATE TABLE IF NOT EXISTS songs (
/* Metadata from taglib */
@ -67,12 +67,12 @@ CREATE TABLE songs (
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT
);
CREATE TABLE playlists (
CREATE TABLE IF NOT EXISTS playlists (
name TEXT NOT NULL,
last_played INTEGER NOT NULL DEFAULT -1,
@ -83,11 +83,12 @@ CREATE TABLE playlists (
);
CREATE TABLE playlist_items (
CREATE TABLE IF NOT EXISTS playlist_items (
playlist INTEGER NOT NULL,
type TEXT NOT NULL,
collection_id INTEGER,
internet_service TEXT,
url TEXT,
/* Metadata from taglib */
@ -145,7 +146,7 @@ CREATE TABLE playlist_items (
);
CREATE TABLE devices (
CREATE TABLE IF NOT EXISTS devices (
unique_id TEXT NOT NULL,
friendly_name TEXT,
size INTEGER,
@ -155,17 +156,17 @@ CREATE TABLE devices (
transcode_format NOT NULL DEFAULT 5
);
CREATE INDEX idx_filename ON songs (filename);
CREATE INDEX IF NOT EXISTS idx_filename ON songs (filename);
CREATE INDEX idx_comp_artist ON songs (compilation_effective, artist);
CREATE INDEX IF NOT EXISTS idx_comp_artist ON songs (compilation_effective, artist);
CREATE INDEX idx_album ON songs (album);
CREATE INDEX IF NOT EXISTS idx_album ON songs (album);
CREATE INDEX idx_title ON songs (title);
CREATE INDEX IF NOT EXISTS idx_title ON songs (title);
CREATE VIEW duplicated_songs as select artist dup_artist, album dup_album, title dup_title from songs as inner_songs where artist != '' and album != '' and title != '' and unavailable = 0 group by artist, album , title having count(*) > 1;
CREATE VIEW IF NOT EXISTS duplicated_songs as select artist dup_artist, album dup_album, title dup_title from songs as inner_songs where artist != '' and album != '' and title != '' and unavailable = 0 group by artist, album , title having count(*) > 1;
CREATE VIRTUAL TABLE songs_fts USING fts3(
CREATE VIRTUAL TABLE IF NOT EXISTS songs_fts USING fts3(
ftstitle,
ftsalbum,
@ -180,7 +181,7 @@ CREATE VIRTUAL TABLE songs_fts USING fts3(
);
CREATE VIRTUAL TABLE playlist_items_fts_ USING fts3(
CREATE VIRTUAL TABLE IF NOT EXISTS playlist_items_fts_ USING fts3(
ftstitle,
ftsalbum,
@ -195,7 +196,7 @@ CREATE VIRTUAL TABLE playlist_items_fts_ USING fts3(
);
CREATE VIRTUAL TABLE %allsongstables_fts USING fts3(
CREATE VIRTUAL TABLE IF NOT EXISTS %allsongstables_fts USING fts3(
ftstitle,
ftsalbum,
@ -211,7 +212,7 @@ CREATE VIRTUAL TABLE %allsongstables_fts USING fts3(
);
INSERT INTO songs_fts (ROWID, ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment)
INSERT INTO songs_fts (ROWID, ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment)
SELECT ROWID, title, album, artist, albumartist, composer, performer, grouping, genre, comment FROM songs;
INSERT INTO %allsongstables_fts (ROWID, ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment)

View File

@ -207,6 +207,7 @@ set(SOURCES
settings/shortcutssettingspage.cpp
settings/appearancesettingspage.cpp
settings/notificationssettingspage.cpp
settings/tidalsettingspage.cpp
dialogs/about.cpp
dialogs/console.cpp
@ -246,6 +247,7 @@ set(SOURCES
widgets/tracksliderpopup.cpp
widgets/tracksliderslider.cpp
widgets/widgetfadehelper.cpp
widgets/loginstatewidget.cpp
musicbrainz/acoustidclient.cpp
musicbrainz/musicbrainzclient.cpp
@ -266,6 +268,16 @@ set(SOURCES
device/deviceviewcontainer.cpp
device/filesystemdevice.cpp
internet/internetmodel.cpp
internet/internetservice.cpp
internet/internetplaylistitem.cpp
tidal/tidalservice.cpp
tidal/tidalsearch.cpp
tidal/tidalsearchview.cpp
tidal/tidalsearchmodel.cpp
tidal/tidalsearchsortmodel.cpp
tidal/tidalsearchitemdelegate.cpp
)
set(HEADERS
@ -356,7 +368,7 @@ set(HEADERS
covermanager/amazoncoverprovider.h
covermanager/musicbrainzcoverprovider.h
covermanager/discogscoverprovider.h
settings/settingsdialog.h
settings/settingspage.h
settings/behavioursettingspage.h
@ -368,7 +380,8 @@ set(HEADERS
settings/shortcutssettingspage.h
settings/appearancesettingspage.h
settings/notificationssettingspage.h
settings/tidalsettingspage.h
dialogs/about.h
dialogs/errordialog.h
dialogs/console.h
@ -405,6 +418,7 @@ set(HEADERS
widgets/tracksliderpopup.h
widgets/tracksliderslider.h
widgets/widgetfadehelper.h
widgets/loginstatewidget.h
musicbrainz/acoustidclient.h
musicbrainz/musicbrainzclient.h
@ -424,6 +438,16 @@ set(HEADERS
device/deviceview.h
device/filesystemdevice.h
internet/internetmodel.h
internet/internetservice.h
internet/internetmimedata.h
internet/internetsongmimedata.h
tidal/tidalservice.h
tidal/tidalsearch.h
tidal/tidalsearchview.h
tidal/tidalsearchmodel.h
)
set(UI
@ -457,6 +481,7 @@ set(UI
settings/shortcutssettingspage.ui
settings/appearancesettingspage.ui
settings/notificationssettingspage.ui
settings/tidalsettingspage.ui
equalizer/equalizer.ui
equalizer/equalizerslider.ui
@ -470,12 +495,15 @@ set(UI
widgets/trackslider.ui
widgets/osdpretty.ui
widgets/fileview.ui
widgets/loginstatewidget.ui
device/deviceproperties.ui
device/deviceviewcontainer.ui
globalshortcuts/globalshortcutgrabber.ui
tidal/tidalsearchview.ui
)
set(RESOURCES ../data/data.qrc)

View File

@ -789,7 +789,7 @@ void CollectionBackend::UpdateCompilations() {
info.artists.insert(artist);
info.directories.insert(filename.left(last_separator));
if (compilation_detected) info.has_compilation_detected = true;
else info.has_not_compilation_detected = true;
else info.has_not_compilation_detected = true;
}
// Now mark the songs that we think are in compilations

View File

@ -15,7 +15,7 @@
*
* 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"
@ -52,6 +52,9 @@
#include "covermanager/discogscoverprovider.h"
#include "covermanager/musicbrainzcoverprovider.h"
#include "internet/internetmodel.h"
#include "tidal/tidalsearch.h"
bool Application::kIsPortable = false;
class ApplicationImpl {
@ -97,7 +100,9 @@ class ApplicationImpl {
app->MoveToNewThread(loader);
return loader;
}),
current_art_loader_([=]() { return new CurrentArtLoader(app, app); })
current_art_loader_([=]() { return new CurrentArtLoader(app, app); }),
internet_model_([=]() { return new InternetModel(app, app); }),
tidal_search_([=]() { return new TidalSearch(app, app); })
{ }
Lazy<TagReaderClient> tag_reader_client_;
@ -113,6 +118,8 @@ class ApplicationImpl {
Lazy<CoverProviders> cover_providers_;
Lazy<AlbumCoverLoader> album_cover_loader_;
Lazy<CurrentArtLoader> current_art_loader_;
Lazy<InternetModel> internet_model_;
Lazy<TidalSearch> tidal_search_;
};
@ -210,6 +217,13 @@ TaskManager *Application::task_manager() const {
}
EngineDevice *Application::enginedevice() const {
//qLog(Debug) << __PRETTY_FUNCTION__;
return p_->enginedevice_.get();
}
InternetModel* Application::internet_model() const {
return p_->internet_model_.get();
}
TidalSearch* Application::tidal_search() const {
return p_->tidal_search_.get();
}

View File

@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*
*/
#ifndef APPLICATION_H_
@ -49,6 +49,8 @@ class DeviceManager;
class CoverProviders;
class AlbumCoverLoader;
class CurrentArtLoader;
class InternetModel;
class TidalSearch;
class Application : public QObject {
Q_OBJECT
@ -79,6 +81,9 @@ class Application : public QObject {
CollectionBackend *collection_backend() const;
CollectionModel *collection_model() const;
InternetModel *internet_model() const;
TidalSearch *tidal_search() const;
void MoveToNewThread(QObject *object);
void MoveToThread(QObject *object, QThread *thread);

View File

@ -52,7 +52,7 @@
#include "scopedtransaction.h"
const char *Database::kDatabaseFilename = "strawberry.db";
const int Database::kSchemaVersion = 0;
const int Database::kSchemaVersion = 1;
const char *Database::kMagicAllSongsTables = "%allsongstables";
int Database::sNextConnectionId = 1;

View File

@ -126,6 +126,8 @@
#include "settings/playlistsettingspage.h"
#include "settings/settingsdialog.h"
#include "tidal/tidalsearchview.h"
#if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT)
# include "musicbrainz/tagfetcher.h"
#endif
@ -186,6 +188,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
manager->SetPlaylistManager(app->playlist_manager());
return manager;
}),
tidal_search_view_(new TidalSearchView(app_, this)),
playlist_menu_(new QMenu(this)),
playlist_add_to_another_(nullptr),
playlistitem_actions_separator_(nullptr),
@ -218,7 +221,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
ui_->volume->setValue(volume);
VolumeChanged(volume);
// Initialise the global search widget
// Initialise the tidal search widget
StyleHelper::setBaseColor(palette().color(QPalette::Highlight).darker());
// Add tabs to the fancy tab widget
@ -227,6 +230,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
ui_->tabs->addTab(file_view_, IconLoader::Load("document-open"), tr("Files"));
ui_->tabs->addTab(playlist_list_, IconLoader::Load("view-media-playlist"), tr("Playlists"));
ui_->tabs->addTab(device_view_, IconLoader::Load("device"), tr("Devices"));
ui_->tabs->addTab(tidal_search_view_, IconLoader::Load("tidal"), tr("Tidal", "Tidal"));
//ui_->tabs->AddSpacer();
// Add the now playing widget to the fancy tab widget
@ -475,6 +479,9 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
collection_view_->filter()->AddMenuAction(separator);
collection_view_->filter()->AddMenuAction(collection_config_action);
// Tidal
connect(tidal_search_view_, SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
// Playlist menu
playlist_play_pause_ = playlist_menu_->addAction(tr("Play"), this, SLOT(PlaylistPlay()));
playlist_menu_->addAction(ui_->action_stop);
@ -657,6 +664,12 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
ReloadSettings();
// Tidal search shortcut
QAction *tidal_search_action = new QAction(this);
tidal_search_action->setShortcuts(QList<QKeySequence>() << QKeySequence("Ctrl+F") << QKeySequence("Ctrl+L"));
addAction(tidal_search_action);
connect(tidal_search_action, SIGNAL(triggered()), SLOT(FocusTidalSearchField()));
// Reload pretty OSD to avoid issues with fonts
osd_->ReloadPrettyOSDSettings();
@ -745,6 +758,7 @@ void MainWindow::ReloadAllSettings() {
osd_->ReloadSettings();
collection_view_->ReloadSettings();
ui_->playlist->view()->ReloadSettings();
tidal_search_view_->ReloadSettings();
}
@ -787,7 +801,7 @@ void MainWindow::MediaPaused() {
}
void MainWindow::MediaPlaying() {
ui_->action_stop->setEnabled(true);
ui_->action_stop_after_this_track->setEnabled(true);
ui_->action_play_pause->setIcon(IconLoader::Load("media-pause"));
@ -1789,7 +1803,7 @@ void MainWindow::EditFileTags(const QList<QUrl> &urls) {
Song song;
song.set_url(url);
song.set_valid(true);
song.set_filetype(Song::Type_Mpeg);
song.set_filetype(Song::Type_MPEG);
songs << song;
}
@ -2261,3 +2275,37 @@ void MainWindow::keyPressEvent(QKeyEvent *event) {
}
}
void MainWindow::FocusTidalSearchField() {
ui_->tabs->setCurrentWidget(tidal_search_view_);
tidal_search_view_->FocusSearchField();
}
void MainWindow::DoTidalSearch(const QString& query) {
FocusTidalSearchField();
tidal_search_view_->StartSearch(query);
}
void MainWindow::SearchForArtist() {
PlaylistItemPtr item(app_->playlist_manager()->current()->item_at(playlist_menu_index_.row()));
Song song = item->Metadata();
if (!song.albumartist().isEmpty()) {
DoTidalSearch(song.albumartist().simplified());
}
else if (!song.artist().isEmpty()) {
DoTidalSearch(song.artist().simplified());
}
}
void MainWindow::SearchForAlbum() {
PlaylistItemPtr item(app_->playlist_manager()->current()->item_at(playlist_menu_index_.row()));
Song song = item->Metadata();
if (!song.album().isEmpty()) {
DoTidalSearch(song.album().simplified());
}
}

View File

@ -84,6 +84,7 @@ class TranscodeDialog;
#endif
class Ui_MainWindow;
class Windows7ThumbBar;
class TidalSearchView;
class MainWindow : public QMainWindow, public PlatformInterface {
Q_OBJECT
@ -263,6 +264,11 @@ signals:
void ShowConsole();
void FocusTidalSearchField();
void DoTidalSearch(const QString& query);
void SearchForArtist();
void SearchForAlbum();
private:
void ConnectStatusView(StatusView *statusview);
@ -313,6 +319,8 @@ signals:
PlaylistItemList autocomplete_tag_items_;
#endif
TidalSearchView *tidal_search_view_;
QAction *collection_show_all_;
QAction *collection_show_duplicates_;
QAction *collection_show_untagged_;
@ -335,6 +343,9 @@ signals:
QAction *playlist_add_to_another_;
QList<QAction*> playlistitem_actions_;
QAction *playlistitem_actions_separator_;
QAction *search_for_artist_;
QAction *search_for_album_;
QModelIndex playlist_menu_index_;
QSortFilterProxyModel *collection_sort_model_;

View File

@ -60,6 +60,8 @@
# include "dbus/metatypes.h"
#endif
#include "tidal/tidalsearch.h"
void RegisterMetaTypes() {
qRegisterMetaType<const char*>("const char*");
@ -113,4 +115,7 @@ void RegisterMetaTypes() {
#endif
#endif
qRegisterMetaType<TidalSearch::ResultList>("TidalSearch::ResultList");
qRegisterMetaType<TidalSearch::Result>("TidalSearch::Result");
}

View File

@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*
*/
#ifndef MIMEDATA_H
@ -52,6 +52,9 @@ class MimeData : public QMimeData {
// If this is set then the items are added to the queue after being inserted.
bool enqueue_now_;
// If this is set then the items are added to the beginning of the queue after being inserted.
bool enqueue_next_now_;
// If this is set then the items are inserted into a newly created playlist.
bool open_in_new_playlist_;

View File

@ -15,7 +15,7 @@
*
* 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"
@ -393,7 +393,7 @@ bool Mpris2::CanPause() const {
bool Mpris2::CanSeek() const { return CanSeek(app_->player()->GetState()); }
bool Mpris2::CanSeek(Engine::State state) const {
return app_->player()->GetCurrentItem() && state != Engine::Empty;
return app_->player()->GetCurrentItem() && state != Engine::Empty && !app_->player()->GetCurrentItem()->Metadata().is_stream();
}
bool Mpris2::CanControl() const { return true; }

View File

@ -15,7 +15,7 @@
*
* 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"

View File

@ -287,9 +287,10 @@ uint Song::mtime() const { return d->mtime_; }
uint Song::ctime() const { return d->ctime_; }
int Song::filesize() const { return d->filesize_; }
Song::FileType Song::filetype() const { return d->filetype_; }
bool Song::is_cdda() const { return d->filetype_ == Type_Cdda; }
bool Song::is_stream() const { return d->filetype_ == Type_Stream; }
bool Song::is_cdda() const { return d->filetype_ == Type_CDDA; }
bool Song::is_collection_song() const {
return !is_cdda() && id() != -1;
return !is_cdda() && !is_stream() && id() != -1;
}
const QString &Song::art_automatic() const { return d->art_automatic_; }
const QString &Song::art_manual() const { return d->art_manual_; }
@ -329,10 +330,10 @@ void Song::set_bitdepth(int v) { d->bitdepth_ = v; }
void Song::set_directory_id(int v) { d->directory_id_ = v; }
void Song::set_url(const QUrl &v) {
if (Application::kIsPortable) {
QUrl base =
QUrl::fromLocalFile(QCoreApplication::applicationDirPath() + "/");
QUrl base = QUrl::fromLocalFile(QCoreApplication::applicationDirPath() + "/");
d->url_ = base.resolved(v);
} else {
}
else {
d->url_ = v;
}
}
@ -364,36 +365,35 @@ QString Song::JoinSpec(const QString &table) {
QString Song::TextForFiletype(FileType type) {
switch (type) {
case Song::Type_Wav: return QObject::tr("Wav");
case Song::Type_Flac: return QObject::tr("FLAC");
case Song::Type_WAV: return QObject::tr("Wav");
case Song::Type_FLAC: return QObject::tr("FLAC");
case Song::Type_WavPack: return QObject::tr("WavPack");
case Song::Type_OggFlac: return QObject::tr("Ogg FLAC");
case Song::Type_OggVorbis: return QObject::tr("Ogg Vorbis");
case Song::Type_OggOpus: return QObject::tr("Ogg Opus");
case Song::Type_OggSpeex: return QObject::tr("Ogg Speex");
case Song::Type_Mpeg: return QObject::tr("MP3");
case Song::Type_Mp4: return QObject::tr("MP4 AAC");
case Song::Type_Asf: return QObject::tr("Windows Media audio");
case Song::Type_Aiff: return QObject::tr("AIFF");
case Song::Type_Mpc: return QObject::tr("MPC");
case Song::Type_MPEG: return QObject::tr("MP3");
case Song::Type_MP4: return QObject::tr("MP4 AAC");
case Song::Type_ASF: return QObject::tr("Windows Media audio");
case Song::Type_AIFF: return QObject::tr("AIFF");
case Song::Type_MPC: return QObject::tr("MPC");
case Song::Type_TrueAudio: return QObject::tr("TrueAudio");
case Song::Type_Cdda: return QObject::tr("CDDA");
case Song::Type_CDDA: return QObject::tr("CDDA");
case Song::Type_Unknown:
default:
return QObject::tr("Unknown");
}
}
bool Song::IsFileLossless() const {
switch (filetype()) {
case Song::Type_Wav:
case Song::Type_Flac:
case Song::Type_WAV:
case Song::Type_FLAC:
case Song::Type_OggFlac:
case Song::Type_WavPack:
case Song::Type_Aiff:
case Song::Type_AIFF:
return true;
default:
return false;
@ -628,7 +628,7 @@ void Song::InitFromQuery(const SqlRow &q, bool reliable_metadata, int col) {
else if (Song::kColumns.value(i) == "unavailable") {
d->unavailable_ = q.value(x).toBool();
}
else if (Song::kColumns.value(i) == "playcount") {
d->playcount_ = q.value(x).isNull() ? 0 : q.value(x).toInt();
}
@ -650,7 +650,7 @@ void Song::InitFromQuery(const SqlRow &q, bool reliable_metadata, int col) {
}
else if (Song::kColumns.value(i) == "compilation_effective") {
}
else if (Song::kColumns.value(i) == "art_automatic") {
d->art_automatic_ = q.value(x).toString();
}
@ -662,11 +662,11 @@ void Song::InitFromQuery(const SqlRow &q, bool reliable_metadata, int col) {
}
else if (Song::kColumns.value(i) == "effective_originalyear") {
}
else if (Song::kColumns.value(i) == "cue_path") {
d->cue_path_ = tostr(x);
}
else {
qLog(Error) << "Forgot to handle" << Song::kColumns.value(i);
}
@ -752,7 +752,7 @@ void Song::InitFromItdb(const Itdb_Track *track, const QString &prefix) {
}
d->basefilename_ = QFileInfo(filename).fileName();
d->filetype_ = track->type2 ? Type_Mpeg : Type_Mp4;
d->filetype_ = track->type2 ? Type_MPEG : Type_MP4;
d->filesize_ = track->size;
d->mtime_ = track->time_modified;
d->ctime_ = track->time_added;
@ -785,7 +785,7 @@ void Song::ToItdb(Itdb_Track *track) const {
//track->bithdepth = d->bithdepth_;
track->type1 = 0;
track->type2 = d->filetype_ == Type_Mp4 ? 0 : 1;
track->type2 = d->filetype_ == Type_MP4 ? 0 : 1;
track->mediatype = 1; // Audio
track->size = d->filesize_;
track->time_modified = d->mtime_;
@ -825,15 +825,15 @@ void Song::InitFromMTP(const LIBMTP_track_t *track, const QString &host) {
d->playcount_ = track->usecount;
switch (track->filetype) {
case LIBMTP_FILETYPE_WAV: d->filetype_ = Type_Wav; break;
case LIBMTP_FILETYPE_MP3: d->filetype_ = Type_Mpeg; break;
case LIBMTP_FILETYPE_WMA: d->filetype_ = Type_Asf; break;
case LIBMTP_FILETYPE_WAV: d->filetype_ = Type_WAV; break;
case LIBMTP_FILETYPE_MP3: d->filetype_ = Type_MPEG; break;
case LIBMTP_FILETYPE_WMA: d->filetype_ = Type_ASF; break;
case LIBMTP_FILETYPE_OGG: d->filetype_ = Type_OggVorbis; break;
case LIBMTP_FILETYPE_MP4: d->filetype_ = Type_Mp4; break;
case LIBMTP_FILETYPE_AAC: d->filetype_ = Type_Mp4; break;
case LIBMTP_FILETYPE_MP4: d->filetype_ = Type_MP4; break;
case LIBMTP_FILETYPE_AAC: d->filetype_ = Type_MP4; break;
case LIBMTP_FILETYPE_FLAC: d->filetype_ = Type_OggFlac; break;
case LIBMTP_FILETYPE_MP2: d->filetype_ = Type_Mpeg; break;
case LIBMTP_FILETYPE_M4A: d->filetype_ = Type_Mp4; break;
case LIBMTP_FILETYPE_MP2: d->filetype_ = Type_MPEG; break;
case LIBMTP_FILETYPE_M4A: d->filetype_ = Type_MP4; break;
default: d->filetype_ = Type_Unknown; break;
}
@ -868,14 +868,14 @@ void Song::ToMTP(LIBMTP_track_t *track) const {
track->usecount = d->playcount_;
switch (d->filetype_) {
case Type_Asf: track->filetype = LIBMTP_FILETYPE_ASF; break;
case Type_Mp4: track->filetype = LIBMTP_FILETYPE_MP4; break;
case Type_Mpeg: track->filetype = LIBMTP_FILETYPE_MP3; break;
case Type_Flac:
case Type_ASF: track->filetype = LIBMTP_FILETYPE_ASF; break;
case Type_MP4: track->filetype = LIBMTP_FILETYPE_MP4; break;
case Type_MPEG: track->filetype = LIBMTP_FILETYPE_MP3; break;
case Type_FLAC:
case Type_OggFlac: track->filetype = LIBMTP_FILETYPE_FLAC; break;
case Type_OggSpeex:
case Type_OggVorbis: track->filetype = LIBMTP_FILETYPE_OGG; break;
case Type_Wav: track->filetype = LIBMTP_FILETYPE_WAV; break;
case Type_WAV: track->filetype = LIBMTP_FILETYPE_WAV; break;
default: track->filetype = LIBMTP_FILETYPE_UNDEF_AUDIO; break;
}
@ -927,7 +927,7 @@ void Song::BindToQuery(QSqlQuery *query) const {
query->bindValue(":performer", strval(d->performer_));
query->bindValue(":grouping", strval(d->grouping_));
query->bindValue(":comment", strval(d->comment_));
query->bindValue(":beginning", d->beginning_);
query->bindValue(":length", intval(length_nanosec()));
@ -1037,7 +1037,8 @@ QString Song::TitleWithCompilationArtist() const {
}
QString Song::SampleRateBitDepthToText() const {
if (d->samplerate_ == -1) return QString("");
if (d->bitdepth_ == -1) return QString("%1 hz").arg(d->samplerate_);
return QString("%1 hz / %2 bit").arg(d->samplerate_).arg(d->bitdepth_);
@ -1071,7 +1072,7 @@ bool Song::IsMetadataEqual(const Song &other) const {
}
bool Song::IsEditable() const {
return d->valid_ && !d->url_.isEmpty() && d->filetype_ != Type_Unknown && !has_cue();
return d->valid_ && !d->url_.isEmpty() && !is_stream() && d->filetype_ != Type_Unknown && !has_cue();
}
bool Song::operator==(const Song &other) const {

View File

@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*
*/
#ifndef SONG_H
@ -58,12 +58,6 @@ struct _Itdb_Track;
struct LIBMTP_track_struct;
#endif
#ifdef HAVE_LIBLASTFM
namespace lastfm {
class Track;
}
#endif
class SqlRow;
class Song {
@ -95,20 +89,20 @@ class Song {
// If a new lossless file is added, also add it to IsFileLossless().
enum FileType {
Type_Unknown = 0,
Type_Wav = 1,
Type_Flac = 2,
Type_WAV = 1,
Type_FLAC = 2,
Type_WavPack = 3,
Type_OggFlac = 4,
Type_OggVorbis = 5,
Type_OggOpus = 6,
Type_OggSpeex = 7,
Type_Mpeg = 8,
Type_Mp4 = 9,
Type_Asf = 10,
Type_Aiff = 11,
Type_Mpc = 12,
Type_MPEG = 8,
Type_MP4 = 9,
Type_ASF = 10,
Type_AIFF = 11,
Type_MPC = 12,
Type_TrueAudio = 13,
Type_Cdda = 90,
Type_CDDA = 90,
Type_Stream = 91,
};
@ -127,9 +121,6 @@ class Song {
void InitFromQuery(const SqlRow &query, bool reliable_metadata, int col = 0);
void InitFromFilePartial(const QString &filename); // Just store the filename: incomplete but fast
void InitArtManual(); // Check if there is already a art in the cache and store the filename in art_manual
#ifdef HAVE_LIBLASTFM
void InitFromLastFM(const lastfm::Track &track);
#endif
void MergeFromSimpleMetaBundle(const Engine::SimpleMetaBundle &bundle);
@ -152,9 +143,6 @@ class Song {
// Save
void BindToQuery(QSqlQuery *query) const;
void BindToFtsQuery(QSqlQuery *query) const;
#ifdef HAVE_LIBLASTFM
void ToLastFM(lastfm::Track *track, bool prefer_album_artist) const;
#endif
void ToXesam(QVariantMap *map) const;
void ToProtobuf(pb::tagreader::SongMetadata *pb) const;
@ -210,6 +198,7 @@ class Song {
const QString &effective_albumartist() const;
bool is_collection_song() const;
bool is_stream() const;
bool is_cdda() const;
// Playlist views are special because you don't want to fill in album artists automatically for compilations, but you do for normal albums:

View File

@ -15,7 +15,7 @@
*
* 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"
@ -51,6 +51,8 @@
#include "song.h"
#include "songloader.h"
#include "tagreaderclient.h"
#include "engine/enginetype.h"
#include "engine/enginebase.h"
#include "collection/collectionbackend.h"
#include "collection/collectionquery.h"
#include "collection/sqlrow.h"
@ -78,6 +80,7 @@ SongLoader::SongLoader(CollectionBackendInterface *collection, const Player *pla
parser_(nullptr),
collection_(collection),
player_(player) {
if (sRawUriSchemes.isEmpty()) {
sRawUriSchemes << "udp"
<< "mms"
@ -97,7 +100,7 @@ SongLoader::SongLoader(CollectionBackendInterface *collection, const Player *pla
}
SongLoader::~SongLoader() {
#ifdef HAVE_GSTREAMER
if (pipeline_) {
state_ = Finished;
@ -121,24 +124,29 @@ SongLoader::Result SongLoader::Load(const QUrl &url) {
return Success;
}
if (player_->engine()->type() == Engine::GStreamer) {
#ifdef HAVE_GSTREAMER
preload_func_ = std::bind(&SongLoader::LoadRemote, this);
preload_func_ = std::bind(&SongLoader::LoadRemote, this);
return BlockingLoadRequired;
#else
return Error;
#endif
}
return BlockingLoadRequired;
return Success;
}
void SongLoader::LoadFilenamesBlocking() {
if (preload_func_) {
preload_func_();
}
}
SongLoader::Result SongLoader::LoadLocalPartial(const QString &filename) {
qLog(Debug) << "Fast Loading local file" << filename;
// First check to see if it's a directory - if so we can load all the songs inside right away.
if (QFileInfo(filename).isDir()) {
@ -149,7 +157,7 @@ SongLoader::Result SongLoader::LoadLocalPartial(const QString &filename) {
song.InitFromFilePartial(filename);
if (song.is_valid()) songs_ << song;
return Success;
}
SongLoader::Result SongLoader::LoadAudioCD() {
@ -208,6 +216,7 @@ SongLoader::Result SongLoader::LoadLocal(const QString &filename) {
// It's not in the database, load it asynchronously.
preload_func_ = std::bind(&SongLoader::LoadLocalAsync, this, filename);
return BlockingLoadRequired;
}
void SongLoader::LoadLocalAsync(const QString &filename) {
@ -253,6 +262,7 @@ void SongLoader::LoadLocalAsync(const QString &filename) {
Song song;
song.InitFromFilePartial(filename);
if (song.is_valid()) songs_ << song;
}
void SongLoader::LoadMetadataBlocking() {
@ -274,7 +284,8 @@ void SongLoader::EffectiveSongLoad(Song *song) {
Song collection_song = collection_->GetSongByUrl(song->url());
if (collection_song.is_valid()) {
*song = collection_song;
} else {
}
else {
// it's a normal media file
QString filename = song->url().toLocalFile();
TagReaderClient::Instance()->ReadFileBlocking(filename, song);
@ -318,7 +329,15 @@ void SongLoader::LoadLocalDirectory(const QString &filename) {
// so if the user has the "Start playing when adding to playlist" preference behaviour set,
// it can enjoy the first song being played (seek it, have moodbar, etc.)
if (!songs_.isEmpty()) EffectiveSongLoad(&(*songs_.begin()));
}
void SongLoader::AddAsRawStream() {
Song song;
song.set_valid(true);
song.set_filetype(Song::Type_Stream);
song.set_url(url_);
song.set_title(url_.toString());
songs_ << song;
}
void SongLoader::Timeout() {
@ -348,10 +367,10 @@ void SongLoader::StopTypefind() {
}
else if (success_) {
//qLog(Debug) << "Loading" << url_ << "as raw stream";
qLog(Debug) << "Loading" << url_ << "as raw stream";
// It wasn't a playlist - just put the URL in as a stream
//AddAsRawStream();
AddAsRawStream();
}
emit LoadRemoteFinished();
@ -413,7 +432,7 @@ void SongLoader::LoadRemote() {
#ifdef HAVE_GSTREAMER
void SongLoader::TypeFound(GstElement *, uint, GstCaps *caps, void *self) {
SongLoader *instance = static_cast<SongLoader*>(self);
if (instance->state_ != WaitingForType) return;

View File

@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*
*/
#ifndef SONGLOADER_H
@ -106,6 +106,8 @@ signals:
void LoadLocalDirectory(const QString &filename);
void LoadPlaylist(ParserBase *parser, const QString &filename);
void AddAsRawStream();
#ifdef HAVE_GSTREAMER
void LoadRemote();

View File

@ -15,7 +15,7 @@
*
* 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"
@ -110,7 +110,7 @@ void CddaSongLoader::LoadSongs() {
Song song;
song.set_id(track_number);
song.set_valid(true);
song.set_filetype(Song::Type_Cdda);
song.set_filetype(Song::Type_CDDA);
song.set_url(GetUrlFromTrack(track_number));
song.set_title(QString("Track %1").arg(track_number));
song.set_track(track_number);
@ -207,7 +207,7 @@ void CddaSongLoader::AudioCDTagsLoaded(const QString &artist, const QString &alb
song.set_track(track_number);
song.set_year(ret.year_);
song.set_id(track_number);
song.set_filetype(Song::Type_Cdda);
song.set_filetype(Song::Type_CDDA);
song.set_valid(true);
// We need to set url: that's how playlist will find the correct item to update
song.set_url(GetUrlFromTrack(track_number++));

View File

@ -15,7 +15,7 @@
*
* 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"
@ -248,8 +248,8 @@ void GPodDevice::FinishDelete(bool success) {
}
bool GPodDevice::GetSupportedFiletypes(QList<Song::FileType> *ret) {
*ret << Song::Type_Mp4;
*ret << Song::Type_Mpeg;
*ret << Song::Type_MP4;
*ret << Song::Type_MPEG;
return true;
}

View File

@ -15,7 +15,7 @@
*
* 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"
@ -209,15 +209,15 @@ bool MtpDevice::GetSupportedFiletypes(QList<Song::FileType> *ret, LIBMTP_mtpdevi
for (int i = 0; i < length; ++i) {
switch (LIBMTP_filetype_t(list[i])) {
case LIBMTP_FILETYPE_WAV: *ret << Song::Type_Wav; break;
case LIBMTP_FILETYPE_WAV: *ret << Song::Type_WAV; break;
case LIBMTP_FILETYPE_MP2:
case LIBMTP_FILETYPE_MP3: *ret << Song::Type_Mpeg; break;
case LIBMTP_FILETYPE_WMA: *ret << Song::Type_Asf; break;
case LIBMTP_FILETYPE_MP3: *ret << Song::Type_MPEG; break;
case LIBMTP_FILETYPE_WMA: *ret << Song::Type_ASF; break;
case LIBMTP_FILETYPE_MP4:
case LIBMTP_FILETYPE_M4A:
case LIBMTP_FILETYPE_AAC: *ret << Song::Type_Mp4; break;
case LIBMTP_FILETYPE_AAC: *ret << Song::Type_MP4; break;
case LIBMTP_FILETYPE_FLAC:
*ret << Song::Type_Flac;
*ret << Song::Type_FLAC;
*ret << Song::Type_OggFlac;
break;
case LIBMTP_FILETYPE_OGG:

View File

@ -786,7 +786,6 @@ void GstEngine::StartFadeoutPause() {
void GstEngine::StartTimers() {
StopTimers();
timer_id_ = startTimer(kTimerIntervalNanosec / kNsecPerMsec);
}

View File

@ -336,7 +336,7 @@ EngineBase::PluginDetailsList VLCEngine::GetPluginList() const {
ret << details;
//GetDevicesList(audio_output->psz_name);
}
libvlc_audio_output_list_release(audio_output_list);
return ret;

View File

@ -0,0 +1,43 @@
/*
* 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 INTERNETMIMEDATA_H
#define INTERNETMIMEDATA_H
#include "config.h"
#include <QObject>
#include <QModelIndexList>
#include "core/mimedata.h"
class InternetModel;
class InternetMimeData : public MimeData {
Q_OBJECT
public:
explicit InternetMimeData(const InternetModel *_model) : model(_model) {}
const InternetModel *model;
QModelIndexList indexes;
};
#endif

View File

@ -0,0 +1,83 @@
/*
* Strawberry Music Player
* This file 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 <QObject>
#include <QMap>
#include <QString>
#include <QStandardItemModel>
#include <QtDebug>
#include "core/logging.h"
#include "internetmodel.h"
#include "internetservice.h"
#include "tidal/tidalservice.h"
QMap<QString, InternetService*>* InternetModel::sServices = nullptr;
InternetModel::InternetModel(Application *app, QObject *parent)
: QStandardItemModel(parent),
app_(app) {
if (!sServices) sServices = new QMap<QString, InternetService*>;
Q_ASSERT(sServices->isEmpty());
AddService(new TidalService(app, this));
}
void InternetModel::AddService(InternetService *service) {
qLog(Debug) << "Adding internet service:" << service->name();
sServices->insert(service->name(), service);
connect(service, SIGNAL(destroyed()), SLOT(ServiceDeleted()));
if (service->has_initial_load_settings()) service->InitialLoadSettings();
else service->ReloadSettings();
}
void InternetModel::RemoveService(InternetService *service) {
if (!sServices->contains(service->name())) return;
sServices->remove(service->name());
disconnect(service, 0, this, 0);
}
void InternetModel::ServiceDeleted() {
InternetService *service = qobject_cast<InternetService*>(sender());
if (service) RemoveService(service);
}
InternetService *InternetModel::ServiceByName(const QString &name) {
if (sServices->contains(name)) return sServices->value(name);
return nullptr;
}
void InternetModel::ReloadSettings() {
for (InternetService *service : sServices->values()) {
service->ReloadSettings();
}
}

View File

@ -0,0 +1,132 @@
/*
* Strawberry Music Player
* This file 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 INTERNETMODEL_H
#define INTERNETMODEL_H
#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 InternetModel : public QStandardItemModel {
Q_OBJECT
public:
explicit InternetModel(Application* app, QObject *parent = nullptr);
enum Role {
// Services can use this role to distinguish between different types of items that they add.
// The root item's type is automatically set to Type_Service,
// but apart from that Services are free to define their own values for this field (starting from TypeCount).
Role_Type = Qt::UserRole + 1000,
// If this is not set the item is not playable (ie. it can't be dragged to the playlist).
// Otherwise it describes how this item is converted to playlist items.
// See the PlayBehaviour enum for more details.
Role_PlayBehaviour,
// The URL of the media for this item. This is required if the PlayBehaviour is set to PlayBehaviour_UseSongLoader.
Role_Url,
// The metadata used in the item that is added to the playlist if the PlayBehaviour is set to PlayBehaviour_SingleItem. Ignored otherwise.
Role_SongMetadata,
// If this is set to true then the model will call the service's LazyPopulate method when this item is expanded.
// Use this if your item's children have to be downloaded or fetched remotely.
Role_CanLazyLoad,
// This is automatically set on the root item for a service. It contains a pointer to an InternetService.
// Services should not set this field themselves.
Role_Service,
// Setting this to true means that the item can be changed by user action (e.g. changing remote playlists)
Role_CanBeModified,
RoleCount,
Role_IsDivider = CollectionModel::Role_IsDivider,
};
enum Type {
Type_Service = 1,
Type_Track,
Type_UserPlaylist,
TypeCount
};
enum PlayBehaviour {
// The item can't be played. This is the default.
PlayBehaviour_None = 0,
// This item's URL is passed through the normal song loader.
// This supports loading remote playlists, remote files and local files.
// This is probably the most sensible behaviour to use if you're just returning normal radio stations.
PlayBehaviour_UseSongLoader,
// This item's URL, Title and Artist are used in the playlist. No special behaviour occurs
// The URL is just passed straight to gstreamer when the user starts playing.
PlayBehaviour_SingleItem,
// This item's children have PlayBehaviour_SingleItem set.
// This is used when dragging a playlist item for instance, to have all the playlit's items info loaded in the mime data.
PlayBehaviour_MultipleItems,
// This item might not represent a song - the service's ItemDoubleClicked() slot will get called instead to do some custom action.
PlayBehaviour_DoubleClickAction,
};
// Needs to be static for InternetPlaylistItem::restore
static InternetService *ServiceByName(const QString &name);
template <typename T>
static T *Service() {
return static_cast<T*>(ServiceByName(T::kServiceName));
}
// Add and remove services. Ownership is not transferred and the service is not reparented.
// If the service is deleted it will be automatically removed from the model.
void AddService(InternetService *service);
void RemoveService(InternetService *service);
void ReloadSettings();
Application *app() const { return app_; }
private slots:
void ServiceDeleted();
private:
static QMap<QString, InternetService*> *sServices;
Application *app_;
};
#endif

View File

@ -0,0 +1,106 @@
/*
* 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 <QApplication>
#include <QSettings>
#include <QVariant>
#include <QString>
#include <QtDebug>
#include "internetplaylistitem.h"
#include "internetservice.h"
#include "internetmodel.h"
#include "core/settingsprovider.h"
#include "collection/sqlrow.h"
#include "playlist/playlistbackend.h"
InternetPlaylistItem::InternetPlaylistItem(const QString &type)
: PlaylistItem(type), set_service_icon_(false) {}
InternetPlaylistItem::InternetPlaylistItem(InternetService *service, const Song &metadata)
: PlaylistItem("Internet"),
service_name_(service->name()),
set_service_icon_(false),
metadata_(metadata) {
InitMetadata();
}
bool InternetPlaylistItem::InitFromQuery(const SqlRow &query) {
// The song tables gets joined first, plus one each for the song ROWIDs
const int row = (Song::kColumns.count() + 1) * PlaylistBackend::kSongTableJoins;
service_name_ = query.value(row + 1).toString();
metadata_.InitFromQuery(query, false, (Song::kColumns.count() + 1) * 1);
InitMetadata();
return true;
}
InternetService *InternetPlaylistItem::service() const {
InternetService *ret = InternetModel::ServiceByName(service_name_);
if (ret && !set_service_icon_) {
const_cast<InternetPlaylistItem*>(this)->set_service_icon_ = true;
QString icon = ret->Icon();
if (!icon.isEmpty()) {
const_cast<InternetPlaylistItem*>(this)->metadata_.set_art_manual(icon);
}
}
return ret;
}
QVariant InternetPlaylistItem::DatabaseValue(DatabaseColumn column) const {
switch (column) {
case Column_InternetService:
return service_name_;
default:
return PlaylistItem::DatabaseValue(column);
}
}
void InternetPlaylistItem::InitMetadata() {
if (metadata_.title().isEmpty())
metadata_.set_title(metadata_.url().toString());
metadata_.set_filetype(Song::Type_Stream);
metadata_.set_valid(true);
}
Song InternetPlaylistItem::Metadata() const {
if (!set_service_icon_) {
// Get the icon if we don't have it already
service();
}
if (HasTemporaryMetadata()) return temp_metadata_;
return metadata_;
}
QUrl InternetPlaylistItem::Url() const { return metadata_.url(); }

View File

@ -0,0 +1,58 @@
/*
* 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 INTERNETPLAYLISTITEM_H
#define INTERNETPLAYLISTITEM_H
#include "config.h"
#include <QString>
#include <QVariant>
#include <QUrl>
#include "core/song.h"
#include "playlist/playlistitem.h"
class InternetService;
class InternetPlaylistItem : public PlaylistItem {
public:
explicit InternetPlaylistItem(const QString &type);
InternetPlaylistItem(InternetService *service, const Song &metadata);
bool InitFromQuery(const SqlRow &query);
Song Metadata() const;
QUrl Url() const;
protected:
QVariant DatabaseValue(DatabaseColumn) const;
Song DatabaseSongMetadata() const { return metadata_; }
private:
void InitMetadata();
InternetService *service() const;
private:
QString service_name_;
bool set_service_icon_;
Song metadata_;
};
#endif

View File

@ -0,0 +1,32 @@
/*
* 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 <QObject>
#include <QStandardItem>
#include <QVariant>
#include <QString>
#include "core/logging.h"
#include "core/mimedata.h"
#include "internetmodel.h"
#include "internetservice.h"
InternetService::InternetService(const QString &name, Application *app, InternetModel *model, QObject *parent)
: QObject(parent), app_(app), model_(model), name_(name) {
}

View File

@ -0,0 +1,64 @@
/*
* 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 INTERNETSERVICE_H
#define INTERNETSERVICE_H
#include <QObject>
#include <QStandardItem>
#include <QAction>
#include <QObject>
#include <QList>
#include <QString>
#include <QUrl>
#include "core/song.h"
#include "playlist/playlistitem.h"
#include "settings/settingsdialog.h"
class Application;
class InternetModel;
class CollectionFilterWidget;
class InternetService : public QObject {
Q_OBJECT
public:
InternetService(const QString &name, Application *app, InternetModel *model, QObject *parent = nullptr);
virtual ~InternetService() {}
QString name() const { return name_; }
InternetModel *model() const { return model_; }
virtual bool has_initial_load_settings() const { return false; }
virtual void InitialLoadSettings() {}
virtual void ReloadSettings() {}
virtual QString Icon() { return QString(); }
public slots:
virtual void ShowConfig() {}
protected:
Application *app_;
private:
InternetModel *model_;
QString name_;
};
Q_DECLARE_METATYPE(InternetService*);
#endif

View File

@ -0,0 +1,39 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2011, 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 INTERNETSONGMIMEDATA_H
#define INTERNETSONGMIMEDATA_H
#include "core/mimedata.h"
#include "core/song.h"
class InternetService;
class InternetSongMimeData : public MimeData {
Q_OBJECT
public:
explicit InternetSongMimeData(InternetService *_service) : service(_service) {}
InternetService *service;
SongList songs;
};
#endif

View File

@ -15,7 +15,7 @@
*
* 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"
@ -84,6 +84,11 @@
#include "songplaylistitem.h"
#include "tagreadermessages.pb.h"
#include "internet/internetmodel.h"
#include "internet/internetplaylistitem.h"
#include "internet/internetmimedata.h"
#include "internet/internetsongmimedata.h"
using std::placeholders::_1;
using std::placeholders::_2;
using std::shared_ptr;
@ -153,7 +158,7 @@ Playlist::~Playlist() {
}
template <typename T>
void Playlist::InsertSongItems(const SongList &songs, int pos, bool play_now, bool enqueue) {
void Playlist::InsertSongItems(const SongList &songs, int pos, bool play_now, bool enqueue, bool enqueue_next) {
PlaylistItemList items;
@ -161,7 +166,7 @@ void Playlist::InsertSongItems(const SongList &songs, int pos, bool play_now, bo
items << PlaylistItemPtr(new T(song));
}
InsertItems(items, pos, play_now, enqueue);
InsertItems(items, pos, play_now, enqueue, enqueue_next);
}
@ -282,16 +287,15 @@ QVariant Playlist::data(const QModelIndex &index, int role) const {
case Column_AlbumArtist: return song.playlist_albumartist();
case Column_Composer: return song.composer();
case Column_Performer: return song.performer();
case Column_Grouping: return song.grouping();
case Column_Grouping: return song.grouping();
case Column_PlayCount: return song.playcount();
case Column_SkipCount: return song.skipcount();
case Column_LastPlayed: return song.lastplayed();
case Column_Samplerate: return song.samplerate();
case Column_Bitdepth: return song.bitdepth();
case Column_Bitrate: return song.bitrate();
case Column_SamplerateBitdepth: return song.SampleRateBitDepthToText();
case Column_Bitdepth: return song.bitdepth();
case Column_Bitrate: return song.bitrate();
case Column_Filename: return song.url();
case Column_BaseFilename: return song.basefilename();
@ -304,7 +308,7 @@ QVariant Playlist::data(const QModelIndex &index, int role) const {
if (role == Qt::DisplayRole) return song.comment().simplified();
return song.comment();
//case Column_Source: return item->Url();
case Column_Source: return item->Url();
}
@ -323,9 +327,7 @@ QVariant Playlist::data(const QModelIndex &index, int role) const {
if (items_[index.row()]->HasCurrentForegroundColor()) {
return QBrush(items_[index.row()]->GetCurrentForegroundColor());
}
//if (index.row() < dynamic_history_length()) {
//return QBrush(kDynamicHistoryColor);
//}
return QVariant();
case Qt::BackgroundRole:
@ -562,7 +564,7 @@ int Playlist::previous_row(bool ignore_repeat_track) const {
void Playlist::set_current_row(int i, bool is_stopping) {
QModelIndex old_current_item_index = current_item_index_;
//ClearStreamMetadata();
ClearStreamMetadata();
current_item_index_ = QPersistentModelIndex(index(i, 0, QModelIndex()));
@ -636,6 +638,7 @@ bool Playlist::dropMimeData(const QMimeData *data, Qt::DropAction action, int ro
bool play_now = false;
bool enqueue_now = false;
bool enqueue_next_now = false;
if (const MimeData *mime_data = qobject_cast<const MimeData*>(data)) {
if (mime_data->clear_first_) {
@ -643,6 +646,7 @@ bool Playlist::dropMimeData(const QMimeData *data, Qt::DropAction action, int ro
}
play_now = mime_data->play_now_;
enqueue_now = mime_data->enqueue_now_;
enqueue_next_now = mime_data->enqueue_next_now_;
}
if (const SongMimeData *song_data = qobject_cast<const SongMimeData*>(data)) {
@ -651,11 +655,13 @@ bool Playlist::dropMimeData(const QMimeData *data, Qt::DropAction action, int ro
if (song_data->backend && song_data->backend->songs_table() == SCollection::kSongsTable)
InsertSongItems<CollectionPlaylistItem>(song_data->songs, row, play_now, enqueue_now);
else
InsertSongItems<SongPlaylistItem>(song_data->songs, row, play_now, enqueue_now);
InsertSongItems<SongPlaylistItem>(song_data->songs, row, play_now, enqueue_now, enqueue_next_now);
}
else if (const PlaylistItemMimeData *item_data = qobject_cast<const PlaylistItemMimeData*>(data)) {
InsertItems(item_data->items_, row, play_now, enqueue_now);
InsertItems(item_data->items_, row, play_now, enqueue_now, enqueue_next_now);
}
else if (const InternetSongMimeData* internet_song_data = qobject_cast<const InternetSongMimeData*>(data)) {
InsertInternetItems(internet_song_data->service, internet_song_data->songs, row, play_now, enqueue_now, enqueue_next_now);
}
else if (data->hasFormat(kRowsMimetype)) {
// Dragged from the playlist
@ -719,7 +725,7 @@ bool Playlist::dropMimeData(const QMimeData *data, Qt::DropAction action, int ro
}
void Playlist::InsertUrls(const QList<QUrl> &urls, int pos, bool play_now, bool enqueue) {
void Playlist::InsertUrls(const QList<QUrl> &urls, int pos, bool play_now, bool enqueue, bool enqueue_next) {
SongLoaderInserter *inserter = new SongLoaderInserter(task_manager_, collection_, backend_->app()->player());
connect(inserter, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
@ -832,7 +838,7 @@ void Playlist::MoveItemsWithoutUndo(int start, const QList<int> &dest_rows) {
}
void Playlist::InsertItems(const PlaylistItemList &itemsIn, int pos, bool play_now, bool enqueue) {
void Playlist::InsertItems(const PlaylistItemList &itemsIn, int pos, bool play_now, bool enqueue, bool enqueue_next) {
if (itemsIn.isEmpty())
return;
@ -932,25 +938,37 @@ void Playlist::InsertItemsWithoutUndo(const PlaylistItemList &items, int pos, bo
}
void Playlist::InsertCollectionItems(const SongList &songs, int pos, bool play_now, bool enqueue) {
InsertSongItems<CollectionPlaylistItem>(songs, pos, play_now, enqueue);
void Playlist::InsertCollectionItems(const SongList &songs, int pos, bool play_now, bool enqueue, bool enqueue_next) {
InsertSongItems<CollectionPlaylistItem>(songs, pos, play_now, enqueue, enqueue_next);
}
void Playlist::InsertSongs(const SongList &songs, int pos, bool play_now, bool enqueue) {
InsertSongItems<SongPlaylistItem>(songs, pos, play_now, enqueue);
void Playlist::InsertSongs(const SongList &songs, int pos, bool play_now, bool enqueue, bool enqueue_next) {
InsertSongItems<SongPlaylistItem>(songs, pos, play_now, enqueue, enqueue_next);
}
void Playlist::InsertSongsOrCollectionItems(const SongList &songs, int pos, bool play_now, bool enqueue) {
void Playlist::InsertSongsOrCollectionItems(const SongList &songs, int pos, bool play_now, bool enqueue, bool enqueue_next) {
PlaylistItemList items;
for (const Song &song : songs) {
if (song.is_collection_song()) {
items << PlaylistItemPtr(new CollectionPlaylistItem(song));
} else {
}
else {
items << PlaylistItemPtr(new SongPlaylistItem(song));
}
}
InsertItems(items, pos, play_now, enqueue);
InsertItems(items, pos, play_now, enqueue, enqueue_next);
}
void Playlist::InsertInternetItems(InternetService *service, const SongList &songs, int pos, bool play_now, bool enqueue, bool enqueue_next) {
PlaylistItemList playlist_items;
for (const Song &song : songs) {
playlist_items << shared_ptr<PlaylistItem>(new InternetPlaylistItem(service, song));
}
InsertItems(playlist_items, pos, play_now, enqueue, enqueue_next);
}
@ -973,8 +991,10 @@ void Playlist::UpdateItems(const SongList &songs) {
PlaylistItemPtr &item = items_[i];
if (item->Metadata().url() == song.url() &&
(item->Metadata().filetype() == Song::Type_Unknown ||
// Stream may change and may need to be updated too
item->Metadata().filetype() == Song::Type_Stream ||
// And CD tracks as well (tags are loaded in a second step)
item->Metadata().filetype() == Song::Type_Cdda)) {
item->Metadata().filetype() == Song::Type_CDDA)) {
PlaylistItemPtr new_item;
if (song.is_collection_song()) {
new_item = PlaylistItemPtr(new CollectionPlaylistItem(song));
@ -1069,9 +1089,7 @@ bool Playlist::CompareItems(int column, Qt::SortOrder order, shared_ptr<Playlist
case Column_Bitrate: cmp(bitrate);
case Column_Samplerate: cmp(samplerate);
case Column_Bitdepth: cmp(bitdepth);
case Column_SamplerateBitdepth:
return QString::localeAwareCompare(a->Metadata().SampleRateBitDepthToText().toLower(), b->Metadata().SampleRateBitDepthToText().toLower()) < 0;
case Column_Bitdepth: cmp(bitdepth);
case Column_Filename:
return (QString::localeAwareCompare(a->Url().path().toLower(), b->Url().path().toLower()) < 0);
case Column_BaseFilename: cmp(basefilename);
@ -1081,7 +1099,7 @@ bool Playlist::CompareItems(int column, Qt::SortOrder order, shared_ptr<Playlist
case Column_DateCreated: cmp(ctime);
case Column_Comment: strcmp(comment);
//case Column_Source: cmp(url);
case Column_Source: cmp(url);
}
#undef cmp
@ -1126,7 +1144,6 @@ QString Playlist::column_name(Column column) {
case Column_Samplerate: return tr("Sample rate");
case Column_Bitdepth: return tr("Bit depth");
case Column_SamplerateBitdepth: return tr("Sample rate B");
case Column_Bitrate: return tr("Bitrate");
case Column_Filename: return tr("File name");
@ -1137,7 +1154,7 @@ QString Playlist::column_name(Column column) {
case Column_DateCreated: return tr("Date created");
case Column_Comment: return tr("Comment");
//case Column_Source: return tr("Source");
case Column_Source: return tr("Source");
default: return QString();
}
return "";
@ -1757,6 +1774,7 @@ void Playlist::InvalidateDeletedSongs() {
PlaylistItemPtr item = items_[row];
Song song = item->Metadata();
if (!song.is_stream()) {
bool exists = QFile::exists(song.url().toLocalFile());
if (!exists && !item->HasForegroundColor(kInvalidSongPriority)) {
@ -1768,6 +1786,7 @@ void Playlist::InvalidateDeletedSongs() {
item->RemoveForegroundColor(kInvalidSongPriority);
invalidated_rows.append(row);
}
}
}
ReloadItems(invalidated_rows);

View File

@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*
*/
#ifndef PLAYLIST_H
@ -54,6 +54,8 @@ class PlaylistBackend;
class PlaylistFilter;
class Queue;
class TaskManager;
class InternetModel;
class InternetService;
namespace PlaylistUndoCommands {
class InsertItems;
@ -110,7 +112,6 @@ class Playlist : public QAbstractListModel {
Column_Genre,
Column_Samplerate,
Column_Bitdepth,
Column_SamplerateBitdepth,
Column_Bitrate,
Column_Filename,
Column_BaseFilename,
@ -123,6 +124,7 @@ class Playlist : public QAbstractListModel {
Column_LastPlayed,
Column_Comment,
Column_Grouping,
Column_Source,
ColumnCount
};
@ -212,10 +214,11 @@ class Playlist : public QAbstractListModel {
QUndoStack *undo_stack() const { return undo_stack_; }
// Changing the playlist
void InsertItems (const PlaylistItemList &items, int pos = -1, bool play_now = false, bool enqueue = false);
void InsertCollectionItems (const SongList &items, int pos = -1, bool play_now = false, bool enqueue = false);
void InsertSongs (const SongList &items, int pos = -1, bool play_now = false, bool enqueue = false);
void InsertSongsOrCollectionItems (const SongList &items, int pos = -1, bool play_now = false, bool enqueue = false);
void InsertItems (const PlaylistItemList &items, int pos = -1, bool play_now = false, bool enqueue = false, bool enqueue_next = false);
void InsertCollectionItems (const SongList &items, int pos = -1, bool play_now = false, bool enqueue = false, bool enqueue_next = false);
void InsertSongs (const SongList &items, int pos = -1, bool play_now = false, bool enqueue = false, bool enqueue_next = false);
void InsertSongsOrCollectionItems (const SongList &items, int pos = -1, bool play_now = false, bool enqueue = false, bool enqueue_next = false);
void InsertInternetItems(InternetService* service, const SongList& songs, int pos = -1, bool play_now = false, bool enqueue = false, bool enqueue_next = false);
void ReshuffleIndices();
@ -276,7 +279,7 @@ class Playlist : public QAbstractListModel {
void SetColumnAlignment(const ColumnAlignmentMap &alignment);
void InsertUrls(const QList<QUrl> &urls, int pos = -1, bool play_now = false, bool enqueue = false);
void InsertUrls(const QList<QUrl> &urls, int pos = -1, bool play_now = false, bool enqueue = false, bool enqueue_next = false);
// Removes items with given indices from the playlist. This operation is not undoable.
void RemoveItemsWithoutUndo(const QList<int> &indices);
@ -302,7 +305,7 @@ private:
bool FilterContainsVirtualIndex(int i) const;
template <typename T>
void InsertSongItems(const SongList &songs, int pos, bool play_now, bool enqueue);
void InsertSongItems(const SongList &songs, int pos, bool play_now, bool enqueue, bool enqueue_next = false);
// Modify the playlist without changing the undo stack. These are used by our friends in PlaylistUndoCommands
void InsertItemsWithoutUndo(const PlaylistItemList &items, int pos, bool enqueue = false);

View File

@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*
*/
#include <memory>
@ -145,7 +145,7 @@ QSqlQuery PlaylistBackend::GetPlaylistRows(int playlist) {
" p.ROWID, " +
Song::JoinSpec("p") +
","
" p.type"
" p.type, p.internet_service"
" FROM playlist_items AS p"
" LEFT JOIN songs"
" ON p.collection_id = songs.ROWID"
@ -279,7 +279,7 @@ void PlaylistBackend::SavePlaylist(int playlist, const PlaylistItemList &items,
QSqlQuery clear(db);
clear.prepare("DELETE FROM playlist_items WHERE playlist = :playlist");
QSqlQuery insert(db);
insert.prepare("INSERT INTO playlist_items (playlist, type, collection_id, " + Song::kColumnSpec + ") VALUES (:playlist, :type, :collection_id, " + Song::kBindSpec + ")");
insert.prepare("INSERT INTO playlist_items (playlist, type, collection_id, internet_service, " + Song::kColumnSpec + ") VALUES (:playlist, :type, :collection_id, :internet_service, " + Song::kBindSpec + ")");
QSqlQuery update(db);
update.prepare("UPDATE playlists SET last_played=:last_played WHERE ROWID=:playlist");

View File

@ -15,7 +15,7 @@
*
* 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"
@ -39,6 +39,7 @@
#include <QString>
#include <QStringBuilder>
#include <QUrl>
#include <QRegExp>
#include <QIcon>
#include <QPixmap>
#include <QPainter>
@ -460,6 +461,12 @@ QPixmap SongSourceDelegate::LookupPixmap(const QUrl &url, const QSize &size) con
else if (url.scheme() == "cdda") {
icon = IconLoader::Load("cd");
}
else if (url.scheme() == "http" || url.scheme() == "https") {
if (url.host().contains(QRegExp(".*.tidal.com")))
icon = IconLoader::Load("tidal");
else
icon = IconLoader::Load("download");
}
else {
icon = IconLoader::Load("folder-sound");
}

View File

@ -15,7 +15,7 @@
*
* 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"
@ -445,9 +445,6 @@ FilterTree *FilterParser::createSearchTermTreeNode(
if (columns_[col] == Playlist::Column_Length) {
search_value = parseTime(search);
}
//else if (columns_[col] == Playlist::Column_Rating) {
//search_value = static_cast<int>(search.toDouble() * 2.0 + 0.5);
//}
else {
search_value = search.toInt();
}

View File

@ -15,7 +15,7 @@
*
* 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"
@ -37,6 +37,8 @@
#include "playlistitem.h"
#include "songplaylistitem.h"
#include "internet/internetplaylistitem.h"
PlaylistItem::~PlaylistItem() {
}
@ -44,11 +46,13 @@ PlaylistItem* PlaylistItem::NewFromType(const QString &type) {
if (type == "Collection") return new CollectionPlaylistItem(type);
else if (type == "File") return new SongPlaylistItem(type);
else if (type == "Internet") return new InternetPlaylistItem("Internet");
else if (type == "Tidal") return new InternetPlaylistItem("Tidal");
qLog(Warning) << "Invalid PlaylistItem type:" << type;
return nullptr;
}
PlaylistItem* PlaylistItem::NewFromSongsTable(const QString &table, const Song &song) {
@ -65,6 +69,7 @@ void PlaylistItem::BindToQuery(QSqlQuery *query) const {
query->bindValue(":type", type());
query->bindValue(":collection_id", DatabaseValue(Column_CollectionId));
query->bindValue(":internet_service", DatabaseValue(Column_InternetService));
DatabaseSongMetadata().BindToQuery(query);
@ -119,3 +124,4 @@ bool PlaylistItem::HasCurrentForegroundColor() const {
}
void PlaylistItem::SetShouldSkip(bool val) { should_skip_ = val; }
bool PlaylistItem::GetShouldSkip() const { return should_skip_; }

View File

@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*
*/
#ifndef PLAYLISTITEM_H
@ -104,7 +104,7 @@ class PlaylistItem : public std::enable_shared_from_this<PlaylistItem> {
protected:
bool should_skip_;
enum DatabaseColumn { Column_CollectionId, Column_InternetService, };
enum DatabaseColumn { Column_CollectionId, Column_InternetService };
virtual QVariant DatabaseValue(DatabaseColumn) const {
return QVariant(QVariant::String);
@ -126,3 +126,4 @@ Q_DECLARE_METATYPE(QList<PlaylistItemPtr>)
Q_DECLARE_OPERATORS_FOR_FLAGS(PlaylistItem::Options)
#endif // PLAYLISTITEM_H

View File

@ -15,7 +15,7 @@
*
* 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"
@ -228,20 +228,16 @@ void PlaylistView::SetItemDelegates(CollectionBackend *backend) {
setItemDelegateForColumn(Playlist::Column_Samplerate, new PlaylistDelegateBase(this, ("Hz")));
setItemDelegateForColumn(Playlist::Column_Bitdepth, new PlaylistDelegateBase(this, ("Bit")));
setItemDelegateForColumn(Playlist::Column_Bitrate, new PlaylistDelegateBase(this, tr("kbps")));
setItemDelegateForColumn(Playlist::Column_SamplerateBitdepth, new SamplerateBitdepthItemDelegate(this));
setItemDelegateForColumn(Playlist::Column_Filename, new NativeSeparatorsDelegate(this));
setItemDelegateForColumn(Playlist::Column_LastPlayed, new LastPlayedItemDelegate(this));
#if 0
if (app_ && app_->player()) {
setItemDelegateForColumn(Playlist::Column_Source, new SongSourceDelegate(this, app_->player()));
}
else {
header_->HideSection(Playlist::Column_Source);
}
#endif
}
@ -946,7 +942,8 @@ void PlaylistView::ReloadSettings() {
header_->SetColumnWidth(Playlist::Column_Album, 0.10);
header_->SetColumnWidth(Playlist::Column_Length, 0.03);
header_->SetColumnWidth(Playlist::Column_Bitrate, 0.07);
header_->SetColumnWidth(Playlist::Column_SamplerateBitdepth, 0.07);
header_->SetColumnWidth(Playlist::Column_Samplerate, 0.07);
header_->SetColumnWidth(Playlist::Column_Bitdepth, 0.07);
header_->SetColumnWidth(Playlist::Column_Filetype, 0.06);
setting_initial_header_layout_ = false;
@ -1089,7 +1086,6 @@ ColumnAlignmentMap PlaylistView::DefaultColumnAlignment() {
ret[Playlist::Column_Bitrate] =
ret[Playlist::Column_Samplerate] =
ret[Playlist::Column_Bitdepth] =
ret[Playlist::Column_SamplerateBitdepth] =
ret[Playlist::Column_Filesize] =
ret[Playlist::Column_PlayCount] =
ret[Playlist::Column_SkipCount] =
@ -1216,8 +1212,7 @@ void PlaylistView::focusInEvent(QFocusEvent *event) {
QTreeView::focusInEvent(event);
if (event->reason() == Qt::TabFocusReason ||
event->reason() == Qt::BacktabFocusReason) {
if (event->reason() == Qt::TabFocusReason || event->reason() == Qt::BacktabFocusReason) {
// If there's a current item but no selection it probably means the list was filtered, and the selected item does not match the filter.
// If there's only 1 item in the view it is now impossible to select that item without using the mouse.
const QModelIndex &current = selectionModel()->currentIndex();

View File

@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*
*/
#ifndef PLAYLISTVIEW_H
@ -75,7 +75,7 @@ class PlaylistHeader;
// that uses Gtk to paint row backgrounds, ignoring any custom brush or palette the caller set in the QStyleOption.
// That breaks our currently playing track animation, which relies on the background painted by Qt to be transparent.
// This proxy style uses QCommonStyle to paint the affected elements.
// This class is used by the global search view as well.
// This class is used by tidal search view as well.
class PlaylistProxyStyle : public QProxyStyle {
public:
PlaylistProxyStyle(QStyle *base);

View File

@ -15,7 +15,7 @@
*
* 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"

View File

@ -15,7 +15,7 @@
*
* 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"
@ -62,6 +62,8 @@
#include "playlistsettingspage.h"
#include "shortcutssettingspage.h"
#include "transcodersettingspage.h"
#include "tidalsettingspage.h"
#include "ui_settingsdialog.h"
class QShowEvent;
@ -122,6 +124,7 @@ SettingsDialog::SettingsDialog(Application *app, QWidget *parent)
#ifdef HAVE_GSTREAMER
AddPage(Page_Transcoding, new TranscoderSettingsPage(this), general);
#endif
AddPage(Page_Tidal, new TidalSettingsPage(this), general);
// User interface
QTreeWidgetItem *iface = AddCategory(tr("User interface"));

View File

@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*
*/
#ifndef SETTINGSDIALOG_H
@ -79,6 +79,7 @@ public:
Page_Notifications,
Page_Proxy,
Page_Transcoding,
Page_Tidal,
};
enum Role {

View File

@ -0,0 +1,118 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QObject>
#include <QString>
#include <QSettings>
#include <QMessageBox>
#include <QEvent>
#include "tidalsettingspage.h"
#include "ui_tidalsettingspage.h"
#include "core/application.h"
#include "core/iconloader.h"
#include "internet/internetmodel.h"
#include "tidal/tidalservice.h"
const char *TidalSettingsPage::kSettingsGroup = "Tidal";
TidalSettingsPage::TidalSettingsPage(SettingsDialog *parent)
: SettingsPage(parent),
ui_(new Ui::TidalSettingsPage),
service_(dialog()->app()->internet_model()->Service<TidalService>()) {
ui_->setupUi(this);
setWindowIcon(IconLoader::Load("tidal"));
connect(ui_->button_login, SIGNAL(clicked()), SLOT(LoginClicked()));
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked()));
connect(service_, SIGNAL(LoginFailure(QString)), SLOT(LoginFailure(QString)));
connect(service_, SIGNAL(LoginSuccess()), SLOT(LoginSuccess()));
dialog()->installEventFilter(this);
ui_->combobox_quality->addItem("Low", "LOW");
ui_->combobox_quality->addItem("High", "HIGH");
ui_->combobox_quality->addItem("Lossless", "LOSSLESS");
}
TidalSettingsPage::~TidalSettingsPage() { delete ui_; }
void TidalSettingsPage::Load() {
QSettings s;
s.beginGroup(kSettingsGroup);
ui_->username->setText(s.value("username").toString());
ui_->password->setText(s.value("password").toString());
QString quality = s.value("quality", "HIGH").toString();
ui_->combobox_quality->setCurrentIndex(ui_->combobox_quality->findData(quality));
s.endGroup();
if (service_->authenticated()) ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn);
}
void TidalSettingsPage::Save() {
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("username", ui_->username->text());
s.setValue("password", ui_->password->text());
s.setValue("quality", ui_->combobox_quality->itemData(ui_->combobox_quality->currentIndex()));
s.endGroup();
service_->ReloadSettings();
}
void TidalSettingsPage::LoginClicked() {
service_->Login(ui_->username->text(), ui_->password->text());
ui_->button_login->setEnabled(false);
}
bool TidalSettingsPage::eventFilter(QObject *object, QEvent *event) {
if (object == dialog() && event->type() == QEvent::Enter) {
ui_->button_login->setEnabled(true);
return false;
}
return SettingsPage::eventFilter(object, event);
}
void TidalSettingsPage::LogoutClicked() {
service_->Logout();
ui_->button_login->setEnabled(true);
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
}
void TidalSettingsPage::LoginSuccess() {
if (!this->isVisible()) return;
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn);
ui_->button_login->setEnabled(false);
}
void TidalSettingsPage::LoginFailure(QString failure_reason) {
if (!this->isVisible()) return;
QMessageBox::warning(this, tr("Authentication failed"), failure_reason);
}

View File

@ -0,0 +1,62 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef TIDALSETTINGSPAGE_H
#define TIDALSETTINGSPAGE_H
#include <QObject>
#include <QString>
#include <QEvent>
#include "settings/settingspage.h"
class TidalService;
class Ui_TidalSettingsPage;
class TidalSettingsPage : public SettingsPage {
Q_OBJECT
public:
explicit TidalSettingsPage(SettingsDialog* parent = nullptr);
~TidalSettingsPage();
enum SearchBy {
SearchBy_Songs = 1,
SearchBy_Albums = 2,
};
static const char *kSettingsGroup;
void Load();
void Save();
bool eventFilter(QObject *object, QEvent *event);
private slots:
void LoginClicked();
void LogoutClicked();
void LoginSuccess();
void LoginFailure(QString failure_reason);
private:
Ui_TidalSettingsPage* ui_;
TidalService *service_;
};
#endif

View File

@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TidalSettingsPage</class>
<widget class="QWidget" name="TidalSettingsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>715</width>
<height>425</height>
</rect>
</property>
<property name="windowTitle">
<string>Tidal</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="LoginStateWidget" name="login_state" native="true"/>
</item>
<item>
<widget class="QGroupBox" name="credential_group">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Account details</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_username">
<property name="text">
<string>Tidal username</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="username"/>
</item>
<item>
<widget class="QPushButton" name="button_login">
<property name="text">
<string>Login</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_password">
<property name="text">
<string>Tidal password</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupbox_pref">
<property name="title">
<string>Preferences</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_quality">
<property name="text">
<string>Audio quality</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="combobox_quality"/>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="spacer_middle">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>30</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<spacer name="spacer_bottom">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_tidal">
<property name="minimumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="pixmap">
<pixmap resource="../../data/data.qrc">:/icons/64x64/tidal.png</pixmap>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LoginStateWidget</class>
<extends>QWidget</extends>
<header>widgets/loginstatewidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>username</tabstop>
<tabstop>password</tabstop>
<tabstop>button_login</tabstop>
</tabstops>
<resources>
<include location="../../data/data.qrc"/>
</resources>
<connections/>
</ui>

316
src/tidal/tidalsearch.cpp Normal file
View File

@ -0,0 +1,316 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* 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 <algorithm>
#include <QtGlobal>
#include <QObject>
#include <QList>
#include <QMap>
#include <QString>
#include <QStringList>
#include <QStringBuilder>
#include <QUrl>
#include <QImage>
#include <QPixmap>
#include <QIcon>
#include <QPainter>
#include <QTimerEvent>
#include <QSettings>
#include "core/application.h"
#include "core/logging.h"
#include "core/closure.h"
#include "core/iconloader.h"
#include "covermanager/albumcoverloader.h"
#include "internet/internetsongmimedata.h"
#include "playlist/songmimedata.h"
#include "tidalsearch.h"
#include "tidalservice.h"
#include "settings/tidalsettingspage.h"
const int TidalSearch::kDelayedSearchTimeoutMs = 200;
const int TidalSearch::kMaxResultsPerEmission = 1000;
const int TidalSearch::kArtHeight = 32;
TidalSearch::TidalSearch(Application *app, QObject *parent)
: QObject(parent),
app_(app),
service_(app->internet_model()->Service<TidalService>()),
name_("Tidal"),
id_("tidal"),
icon_(IconLoader::Load("tidal")),
searches_next_id_(1),
art_searches_next_id_(1) {
cover_loader_options_.desired_height_ = kArtHeight;
cover_loader_options_.pad_output_image_ = true;
cover_loader_options_.scale_output_image_ = true;
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(AlbumArtLoaded(quint64, QImage)));
connect(this, SIGNAL(SearchAsyncSig(int, QString, TidalSettingsPage::SearchBy)), this, SLOT(DoSearchAsync(int, QString, TidalSettingsPage::SearchBy)));
connect(this, SIGNAL(ResultsAvailable(int, TidalSearch::ResultList)), SLOT(ResultsAvailableSlot(int, TidalSearch::ResultList)));
connect(this, SIGNAL(ArtLoaded(int, QImage)), SLOT(ArtLoadedSlot(int, QImage)));
connect(service_, SIGNAL(SearchResults(int, SongList)), SLOT(SearchDone(int, SongList)));
connect(service_, SIGNAL(SearchError(int, QString)), SLOT(HandleError(int, QString)));
icon_as_image_ = QImage(icon_.pixmap(48, 48).toImage());
}
TidalSearch::~TidalSearch() {}
QStringList TidalSearch::TokenizeQuery(const QString &query) {
QStringList tokens(query.split(QRegExp("\\s+")));
for (QStringList::iterator it = tokens.begin(); it != tokens.end(); ++it) {
(*it).remove('(');
(*it).remove(')');
(*it).remove('"');
const int colon = (*it).indexOf(":");
if (colon != -1) {
(*it).remove(0, colon + 1);
}
}
return tokens;
}
bool TidalSearch::Matches(const QStringList &tokens, const QString &string) {
for (const QString &token : tokens) {
if (!string.contains(token, Qt::CaseInsensitive)) {
return false;
}
}
return true;
}
int TidalSearch::SearchAsync(const QString &query, TidalSettingsPage::SearchBy searchby) {
const int id = searches_next_id_++;
emit SearchAsyncSig(id, query, searchby);
return id;
}
void TidalSearch::SearchAsync(int id, const QString &query, TidalSettingsPage::SearchBy searchby) {
const int service_id = service_->Search(query, searchby);
pending_searches_[service_id] = PendingState(id, TokenizeQuery(query));
}
void TidalSearch::DoSearchAsync(int id, const QString &query, TidalSettingsPage::SearchBy searchby) {
int timer_id = startTimer(kDelayedSearchTimeoutMs);
delayed_searches_[timer_id].id_ = id;
delayed_searches_[timer_id].query_ = query;
delayed_searches_[timer_id].searchby_ = searchby;
}
void TidalSearch::SearchDone(int service_id, const SongList &songs) {
// Map back to the original id.
const PendingState state = pending_searches_.take(service_id);
const int search_id = state.orig_id_;
ResultList ret;
for (const Song &song : songs) {
Result result;
result.metadata_ = song;
ret << result;
}
emit ResultsAvailable(search_id, ret);
MaybeSearchFinished(search_id);
}
void TidalSearch::HandleError(const int id, const QString error) {
emit SearchError(id, error);
}
void TidalSearch::MaybeSearchFinished(int id) {
if (pending_searches_.keys(PendingState(id, QStringList())).isEmpty()) {
emit SearchFinished(id);
}
}
void TidalSearch::CancelSearch(int id) {
QMap<int, DelayedSearch>::iterator it;
for (it = delayed_searches_.begin(); it != delayed_searches_.end(); ++it) {
if (it.value().id_ == id) {
killTimer(it.key());
delayed_searches_.erase(it);
return;
}
}
}
void TidalSearch::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().searchby_);
delayed_searches_.erase(it);
return;
}
QObject::timerEvent(e);
}
void TidalSearch::ResultsAvailableSlot(int id, TidalSearch::ResultList results) {
if (results.isEmpty()) return;
// Limit the number of results that are used from each emission.
if (results.count() > kMaxResultsPerEmission) {
TidalSearch::ResultList::iterator begin = results.begin();
std::advance(begin, kMaxResultsPerEmission);
results.erase(begin, results.end());
}
// Load cached pixmaps into the results
for (TidalSearch::ResultList::iterator it = results.begin(); it != results.end(); ++it) {
it->pixmap_cache_key_ = PixmapCacheKey(*it);
}
emit AddResults(id, results);
}
QString TidalSearch::PixmapCacheKey(const TidalSearch::Result &result) const {
return "tidal:" % result.metadata_.url().toString();
}
bool TidalSearch::FindCachedPixmap(const TidalSearch::Result &result, QPixmap *pixmap) const {
return pixmap_cache_.find(result.pixmap_cache_key_, pixmap);
}
void TidalSearch::LoadArtAsync(int id, const Result &result) {
emit ArtLoaded(id, QImage());
}
int TidalSearch::LoadArtAsync(const TidalSearch::Result &result) {
const int id = art_searches_next_id_++;
pending_art_searches_[id] = result.pixmap_cache_key_;
quint64 loader_id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, result.metadata_);
cover_loader_tasks_[loader_id] = id;
return id;
}
void TidalSearch::ArtLoadedSlot(int id, const QImage &image) {
HandleLoadedArt(id, image);
}
void TidalSearch::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 TidalSearch::HandleLoadedArt(int id, const QImage &image) {
const QString key = pending_art_searches_.take(id);
QPixmap pixmap = QPixmap::fromImage(image);
pixmap_cache_.insert(key, pixmap);
emit ArtLoaded(id, pixmap);
}
QImage TidalSearch::ScaleAndPad(const QImage &image) {
if (image.isNull()) return QImage();
const QSize target_size = QSize(kArtHeight, kArtHeight);
if (image.size() == target_size) return image;
// Scale the image down
QImage copy;
copy = image.scaled(target_size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
// Pad the image to kHeight x kHeight
if (copy.size() == target_size) return copy;
QImage padded_image(kArtHeight, kArtHeight, QImage::Format_ARGB32);
padded_image.fill(0);
QPainter p(&padded_image);
p.drawImage((kArtHeight - copy.width()) / 2, (kArtHeight - copy.height()) / 2, copy);
p.end();
return padded_image;
}
MimeData *TidalSearch::LoadTracks(const ResultList &results) {
if (results.isEmpty()) {
return nullptr;
}
ResultList results_copy;
for (const Result &result : results) {
results_copy << result;
}
SongList songs;
for (const Result &result : results) {
songs << result.metadata_;
}
InternetSongMimeData *internet_song_mime_data = new InternetSongMimeData(service_);
internet_song_mime_data->songs = songs;
MimeData *mime_data = internet_song_mime_data;
QList<QUrl> urls;
for (const Result &result : results) {
urls << result.metadata_.url();
}
mime_data->setUrls(urls);
return mime_data;
}

157
src/tidal/tidalsearch.h Normal file
View File

@ -0,0 +1,157 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* 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 TIDALSEARCH_H
#define TIDALSEARCH_H
#include "config.h"
#include <QObject>
#include <QFuture>
#include <QIcon>
#include <QMetaType>
#include <QPixmapCache>
#include "core/song.h"
#include "covermanager/albumcoverloaderoptions.h"
#include "settings/tidalsettingspage.h"
class Application;
class MimeData;
class AlbumCoverLoader;
class InternetService;
class TidalService;
class TidalSearch : public QObject {
Q_OBJECT
public:
TidalSearch(Application *app, QObject *parent = nullptr);
~TidalSearch();
struct Result {
Song metadata_;
QString pixmap_cache_key_;
};
typedef QList<Result> ResultList;
static const int kDelayedSearchTimeoutMs;
static const int kMaxResultsPerEmission;
Application *application() const { return app_; }
TidalService *service() const { return service_; }
int SearchAsync(const QString &query, TidalSettingsPage::SearchBy searchby);
int LoadArtAsync(const TidalSearch::Result &result);
void CancelSearch(int id);
void CancelArt(int id);
// Loads tracks for results that were previously emitted by ResultsAvailable.
// The implementation creates a SongMimeData with one Song for each Result.
MimeData *LoadTracks(const ResultList &results);
signals:
void SearchAsyncSig(int id, const QString &query, TidalSettingsPage::SearchBy searchby);
void ResultsAvailable(int id, const TidalSearch::ResultList &results);
void AddResults(int id, const TidalSearch::ResultList &results);
void SearchError(const int id, const QString error);
void SearchFinished(int id);
void ArtLoaded(int id, const QPixmap &pixmap);
void ArtLoaded(int id, const QImage &image);
protected:
struct PendingState {
PendingState() : orig_id_(-1) {}
PendingState(int orig_id, QStringList tokens)
: orig_id_(orig_id), tokens_(tokens) {}
int orig_id_;
QStringList tokens_;
bool operator<(const PendingState &b) const {
return orig_id_ < b.orig_id_;
}
bool operator==(const PendingState &b) const {
return orig_id_ == b.orig_id_;
}
};
void timerEvent(QTimerEvent *e);
// These functions treat queries in the same way as LibraryQuery.
// 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);
private slots:
void DoSearchAsync(int id, const QString &query, TidalSettingsPage::SearchBy searchby);
void SearchDone(int id, const SongList &songs);
void HandleError(const int id, const QString error);
void ResultsAvailableSlot(int id, TidalSearch::ResultList results);
void ArtLoadedSlot(int id, const QImage &image);
void AlbumArtLoaded(quint64 id, const QImage &image);
private:
void SearchAsync(int id, const QString &query, TidalSettingsPage::SearchBy searchby);
void HandleLoadedArt(int id, const QImage &image);
bool FindCachedPixmap(const TidalSearch::Result &result, QPixmap *pixmap) const;
QString PixmapCacheKey(const TidalSearch::Result &result) const;
void LoadArtAsync(int id, const Result &result);
void MaybeSearchFinished(int id);
void ShowConfig() {}
static QImage ScaleAndPad(const QImage &image);
private:
struct DelayedSearch {
int id_;
QString query_;
TidalSettingsPage::SearchBy searchby_;
};
static const int kArtHeight;
Application *app_;
TidalService *service_;
QString name_;
QString id_;
QIcon icon_;
QImage icon_as_image_;
int searches_next_id_;
int art_searches_next_id_;
QMap<int, DelayedSearch> delayed_searches_;
QMap<int, QString> pending_art_searches_;
QPixmapCache pixmap_cache_;
AlbumCoverLoaderOptions cover_loader_options_;
QMap<quint64, int> cover_loader_tasks_;
QMap<int, PendingState> pending_searches_;
};
Q_DECLARE_METATYPE(TidalSearch::Result)
Q_DECLARE_METATYPE(TidalSearch::ResultList)
#endif // TIDALSEARCH_H

View File

@ -0,0 +1,35 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* Copyright 2012, 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 <QPainter>
#include <QStyleOptionViewItem>
#include "tidalsearchitemdelegate.h"
#include "tidalsearchview.h"
TidalSearchItemDelegate::TidalSearchItemDelegate(TidalSearchView* view)
: CollectionItemDelegate(view), view_(view) {}
void TidalSearchItemDelegate::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<TidalSearchView*>(view_)->LazyLoadArt(index);
CollectionItemDelegate::paint(painter, option, index);
}

View File

@ -0,0 +1,41 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* Copyright 2012, 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 TIDALSEARCHITEMDELEGATE_H
#define TIDALSEARCHITEMDELEGATE_H
#include <QPainter>
#include <QStyleOptionViewItem>
#include "collection/collectionview.h"
class TidalSearchView;
class TidalSearchItemDelegate : public CollectionItemDelegate {
public:
TidalSearchItemDelegate(TidalSearchView *view);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
private:
TidalSearchView* view_;
};
#endif // TIDALSEARCHITEMDELEGATE_H

View File

@ -0,0 +1,314 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* Copyright 2012, 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 <QObject>
#include <QStandardItem>
#include <QStandardItemModel>
#include <QList>
#include <QSet>
#include <QVariant>
#include <QString>
#include <QPixmap>
#include <QMimeData>
#include "core/mimedata.h"
#include "core/iconloader.h"
#include "tidalsearch.h"
#include "tidalsearchmodel.h"
TidalSearchModel::TidalSearchModel(TidalSearch *engine, QObject *parent)
: QStandardItemModel(parent),
engine_(engine),
proxy_(nullptr),
use_pretty_covers_(true),
artist_icon_(IconLoader::Load("guitar")) {
group_by_[0] = CollectionModel::GroupBy_Artist;
group_by_[1] = CollectionModel::GroupBy_Album;
group_by_[2] = CollectionModel::GroupBy_None;
no_cover_icon_ = QPixmap(":/pictures/noalbumart.png").scaled(CollectionModel::kPrettyCoverSize, CollectionModel::kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
album_icon_ = no_cover_icon_;
}
void TidalSearchModel::AddResults(const TidalSearch::ResultList &results) {
int sort_index = 0;
for (const TidalSearch::Result &result : results) {
QStandardItem *parent = invisibleRootItem();
// Find (or create) the container nodes for this result if we can.
ContainerKey key;
key.provider_index_ = sort_index;
parent = BuildContainers(result.metadata_, parent, &key);
// Create the item
QStandardItem *item = new QStandardItem;
item->setText(result.metadata_.TitleWithCompilationArtist());
item->setData(QVariant::fromValue(result), Role_Result);
item->setData(sort_index, Role_ProviderIndex);
parent->appendRow(item);
}
}
QStandardItem *TidalSearchModel::BuildContainers(const Song &s, QStandardItem *parent, ContainerKey *key, int level) {
if (level >= 3) {
return parent;
}
bool has_artist_icon = false;
bool has_album_icon = false;
QString display_text;
QString sort_text;
int unique_tag = -1;
int year = 0;
switch (group_by_[level]) {
case CollectionModel::GroupBy_Artist:
if (s.is_compilation()) {
display_text = tr("Various artists");
sort_text = "aaaaaa";
}
else {
display_text = CollectionModel::TextOrUnknown(s.artist());
sort_text = CollectionModel::SortTextForArtist(s.artist());
}
has_artist_icon = true;
break;
case CollectionModel::GroupBy_YearAlbum:
year = qMax(0, s.year());
display_text = CollectionModel::PrettyYearAlbum(year, s.album());
sort_text = CollectionModel::SortTextForNumber(year) + s.album();
unique_tag = s.album_id();
has_album_icon = true;
break;
case CollectionModel::GroupBy_OriginalYearAlbum:
year = qMax(0, s.effective_originalyear());
display_text = CollectionModel::PrettyYearAlbum(year, s.album());
sort_text = CollectionModel::SortTextForNumber(year) + s.album();
unique_tag = s.album_id();
has_album_icon = true;
break;
case CollectionModel::GroupBy_Year:
year = qMax(0, s.year());
display_text = QString::number(year);
sort_text = CollectionModel::SortTextForNumber(year) + " ";
break;
case CollectionModel::GroupBy_OriginalYear:
year = qMax(0, s.effective_originalyear());
display_text = QString::number(year);
sort_text = CollectionModel::SortTextForNumber(year) + " ";
break;
case CollectionModel::GroupBy_Composer:
display_text = s.composer();
case CollectionModel::GroupBy_Performer:
display_text = s.performer();
case CollectionModel::GroupBy_Disc:
display_text = s.disc();
case CollectionModel::GroupBy_Grouping:
display_text = s.grouping();
case CollectionModel::GroupBy_Genre:
if (display_text.isNull()) display_text = s.genre();
case CollectionModel::GroupBy_Album:
unique_tag = s.album_id();
if (display_text.isNull()) {
display_text = s.album();
}
// fallthrough
case CollectionModel::GroupBy_AlbumArtist:
if (display_text.isNull()) display_text = s.effective_albumartist();
display_text = CollectionModel::TextOrUnknown(display_text);
sort_text = CollectionModel::SortTextForArtist(display_text);
has_album_icon = true;
break;
case CollectionModel::GroupBy_FileType:
display_text = s.TextForFiletype();
sort_text = display_text;
break;
case CollectionModel::GroupBy_Bitrate:
display_text = QString(s.bitrate(), 1);
sort_text = display_text;
break;
case CollectionModel::GroupBy_Samplerate:
display_text = QString(s.samplerate(), 1);
sort_text = display_text;
break;
case CollectionModel::GroupBy_Bitdepth:
display_text = QString(s.bitdepth(), 1);
sort_text = display_text;
break;
case CollectionModel::GroupBy_None:
return parent;
}
// Find a container for this level
key->group_[level] = display_text + QString::number(unique_tag);
QStandardItem *container = containers_[*key];
if (!container) {
container = new QStandardItem(display_text);
container->setData(key->provider_index_, Role_ProviderIndex);
container->setData(sort_text, CollectionModel::Role_SortText);
container->setData(group_by_[level], CollectionModel::Role_ContainerType);
if (has_artist_icon) {
container->setIcon(artist_icon_);
}
else if (has_album_icon) {
if (use_pretty_covers_) {
container->setData(no_cover_icon_, Qt::DecorationRole);
}
else {
container->setIcon(album_icon_);
}
}
parent->appendRow(container);
containers_[*key] = container;
}
// Create the container for the next level.
return BuildContainers(s, container, key, level + 1);
}
void TidalSearchModel::Clear() {
containers_.clear();
clear();
}
TidalSearch::ResultList TidalSearchModel::GetChildResults(const QModelIndexList &indexes) const {
QList<QStandardItem*> items;
for (const QModelIndex &index : indexes) {
items << itemFromIndex(index);
}
return GetChildResults(items);
}
TidalSearch::ResultList TidalSearchModel::GetChildResults(const QList<QStandardItem*> &items) const {
TidalSearch::ResultList results;
QSet<const QStandardItem*> visited;
for (QStandardItem *item : items) {
GetChildResults(item, &results, &visited);
}
return results;
}
void TidalSearchModel::GetChildResults(const QStandardItem *item, TidalSearch::ResultList *results, QSet<const QStandardItem*> *visited) const {
if (visited->contains(item)) {
return;
}
visited->insert(item);
// Does this item have children?
if (item->rowCount()) {
const QModelIndex parent_proxy_index = proxy_->mapFromSource(item->index());
// Yes - visit all the children, but do so through the proxy so we get them
// in the right order.
for (int i = 0; i < item->rowCount(); ++i) {
const QModelIndex proxy_index = parent_proxy_index.child(i, 0);
const QModelIndex index = proxy_->mapToSource(proxy_index);
GetChildResults(itemFromIndex(index), results, visited);
}
}
else {
// No - maybe it's a song, add its result if valid
QVariant result = item->data(Role_Result);
if (result.isValid()) {
results->append(result.value<TidalSearch::Result>());
}
else {
// Maybe it's a provider then?
bool is_provider;
const int sort_index = item->data(Role_ProviderIndex).toInt(&is_provider);
if (is_provider) {
// Go through all the items (through the proxy to keep them ordered) and add the ones belonging to this provider to our list
for (int i = 0; i < proxy_->rowCount(invisibleRootItem()->index()); ++i) {
QModelIndex child_index = proxy_->index(i, 0, invisibleRootItem()->index());
const QStandardItem *child_item = itemFromIndex(proxy_->mapToSource(child_index));
if (child_item->data(Role_ProviderIndex).toInt() == sort_index) {
GetChildResults(child_item, results, visited);
}
}
}
}
}
}
QMimeData *TidalSearchModel::mimeData(const QModelIndexList &indexes) const {
return engine_->LoadTracks(GetChildResults(indexes));
}
namespace {
void GatherResults(const QStandardItem *parent, TidalSearch::ResultList *results) {
QVariant result_variant = parent->data(TidalSearchModel::Role_Result);
if (result_variant.isValid()) {
TidalSearch::Result result = result_variant.value<TidalSearch::Result>();
(*results).append(result);
}
for (int i = 0; i < parent->rowCount(); ++i) {
GatherResults(parent->child(i), results);
}
}
}
void TidalSearchModel::SetGroupBy(const CollectionModel::Grouping &grouping, bool regroup_now) {
const CollectionModel::Grouping old_group_by = group_by_;
group_by_ = grouping;
if (regroup_now && group_by_ != old_group_by) {
// Walk the tree gathering the results we have already
TidalSearch::ResultList results;
GatherResults(invisibleRootItem(), &results);
// Reset the model and re-add all the results using the new grouping.
Clear();
AddResults(results);
}
}

View File

@ -0,0 +1,109 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* Copyright 2012, 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 TIDALSEARCHMODEL_H
#define TIDALSEARCHMODEL_H
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QMimeData>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QSortFilterProxyModel>
#include <QMap>
#include <QSet>
#include <QList>
#include <QString>
#include <QStringList>
#include <QIcon>
#include <QPixmap>
#include "collection/collectionmodel.h"
#include "tidalsearch.h"
class TidalSearchModel : public QStandardItemModel {
Q_OBJECT
public:
TidalSearchModel(TidalSearch *engine, QObject *parent = nullptr);
enum Role {
Role_Result = CollectionModel::LastRole,
Role_LazyLoadingArt,
Role_ProviderIndex,
LastRole
};
struct ContainerKey {
int provider_index_;
QString group_[3];
};
void set_proxy(QSortFilterProxyModel *proxy) { proxy_ = proxy; }
void set_use_pretty_covers(bool pretty) { use_pretty_covers_ = pretty; }
void SetGroupBy(const CollectionModel::Grouping &grouping, bool regroup_now);
void Clear();
TidalSearch::ResultList GetChildResults(const QModelIndexList &indexes) const;
TidalSearch::ResultList GetChildResults(const QList<QStandardItem*> &items) const;
QMimeData *mimeData(const QModelIndexList &indexes) const;
public slots:
void AddResults(const TidalSearch::ResultList &results);
private:
QStandardItem *BuildContainers(const Song &metadata, QStandardItem *parent, ContainerKey *key, int level = 0);
void GetChildResults(const QStandardItem *item, TidalSearch::ResultList *results, QSet<const QStandardItem*> *visited) const;
private:
TidalSearch *engine_;
QSortFilterProxyModel *proxy_;
bool use_pretty_covers_;
QIcon artist_icon_;
QPixmap no_cover_icon_;
QIcon album_icon_;
CollectionModel::Grouping group_by_;
QMap<ContainerKey, QStandardItem*> containers_;
};
inline uint qHash(const TidalSearchModel::ContainerKey &key) {
return qHash(key.provider_index_) ^ qHash(key.group_[0]) ^ qHash(key.group_[1]) ^ qHash(key.group_[2]);
}
inline bool operator<(const TidalSearchModel::ContainerKey &left, const TidalSearchModel::ContainerKey &right) {
#define CMP(field) \
if (left.field < right.field) return true; \
if (left.field > right.field) return false
CMP(provider_index_);
CMP(group_[0]);
CMP(group_[1]);
CMP(group_[2]);
return false;
#undef CMP
}
#endif // TIDALSEARCHMODEL_H

View File

@ -0,0 +1,79 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* 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 <QObject>
#include <QSortFilterProxyModel>
#include <QString>
#include "core/logging.h"
#include "tidalsearchmodel.h"
#include "tidalsearchsortmodel.h"
TidalSearchSortModel::TidalSearchSortModel(QObject *parent)
: QSortFilterProxyModel(parent) {}
bool TidalSearchSortModel::lessThan(const QModelIndex &left, const QModelIndex &right) const {
// Compare the provider sort index first.
const int index_left = left.data(TidalSearchModel::Role_ProviderIndex).toInt();
const int index_right = right.data(TidalSearchModel::Role_ProviderIndex).toInt();
if (index_left < index_right) return true;
if (index_left > index_right) return false;
// Dividers always go first
if (left.data(CollectionModel::Role_IsDivider).toBool()) return true;
if (right.data(CollectionModel::Role_IsDivider).toBool()) return false;
// Containers go before songs if they're at the same level
const bool left_is_container = left.data(CollectionModel::Role_ContainerType).isValid();
const bool right_is_container = right.data(CollectionModel::Role_ContainerType).isValid();
if (left_is_container && !right_is_container) return true;
if (right_is_container && !left_is_container) return false;
// Containers get sorted on their sort text.
if (left_is_container) {
return QString::localeAwareCompare(left.data(CollectionModel::Role_SortText).toString(), right.data(CollectionModel::Role_SortText).toString()) < 0;
}
// Otherwise we're comparing songs. Sort by disc, track, then title.
const TidalSearch::Result r1 = left.data(TidalSearchModel::Role_Result).value<TidalSearch::Result>();
const TidalSearch::Result r2 = right.data(TidalSearchModel::Role_Result).value<TidalSearch::Result>();
#define CompareInt(field) \
if (r1.metadata_.field() < r2.metadata_.field()) return true; \
if (r1.metadata_.field() > r2.metadata_.field()) return false
int ret = 0;
#define CompareString(field) \
ret = QString::localeAwareCompare(r1.metadata_.field(), r2.metadata_.field()); \
if (ret < 0) return true; \
if (ret > 0) return false
CompareInt(disc);
CompareInt(track);
CompareString(title);
return false;
#undef CompareInt
#undef CompareString
}

View File

@ -0,0 +1,35 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* 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 TIDALSEARCHSORTMODEL_H
#define TIDALSEARCHSORTMODEL_H
#include <QObject>
#include <QSortFilterProxyModel>
class TidalSearchSortModel : public QSortFilterProxyModel {
public:
TidalSearchSortModel(QObject *parent = nullptr);
protected:
bool lessThan(const QModelIndex &left, const QModelIndex &right) const;
};
#endif // TIDALSEARCHSORTMODEL_H

View File

@ -0,0 +1,544 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* Copyright 2012, 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 <functional>
#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 <QAction>
#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"
#include "collection/collectionmodel.h"
#include "collection/groupbydialog.h"
#include "playlist/songmimedata.h"
#include "tidalsearch.h"
#include "tidalsearchitemdelegate.h"
#include "tidalsearchmodel.h"
#include "tidalsearchsortmodel.h"
#include "tidalsearchview.h"
#include "ui_tidalsearchview.h"
#include "settings/tidalsettingspage.h"
using std::placeholders::_1;
using std::placeholders::_2;
using std::swap;
const int TidalSearchView::kSwapModelsTimeoutMsec = 250;
TidalSearchView::TidalSearchView(Application *app, QWidget *parent)
: QWidget(parent),
app_(app),
engine_(app_->tidal_search()),
ui_(new Ui_TidalSearchView),
context_menu_(nullptr),
last_search_id_(0),
front_model_(new TidalSearchModel(engine_, this)),
back_model_(new TidalSearchModel(engine_, this)),
current_model_(front_model_),
front_proxy_(new TidalSearchSortModel(this)),
back_proxy_(new TidalSearchSortModel(this)),
current_proxy_(front_proxy_),
swap_models_timer_(new QTimer(this)),
search_icon_(IconLoader::Load("search")),
warning_icon_(IconLoader::Load("dialog-warning")),
error_(false) {
ui_->setupUi(this);
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 TidalSearch 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 TidalSearchItemDelegate(this));
ui_->results->setAttribute(Qt::WA_MacShowFocusRect, false);
ui_->results->setStyleSheet("QTreeView::item{padding-top:1px;}");
// Show the help page initially
ui_->results_stack->setCurrentWidget(ui_->help_page);
ui_->help_frame->setBackgroundRole(QPalette::Base);
// Set the colour of the help text to the disabled window text colour
QPalette help_palette = ui_->label_helptext->palette();
const QColor help_color = help_palette.color(QPalette::Disabled, QPalette::WindowText);
help_palette.setColor(QPalette::Normal, QPalette::WindowText, help_color);
help_palette.setColor(QPalette::Inactive, QPalette::WindowText, help_color);
ui_->label_helptext->setPalette(help_palette);
// Make it bold
QFont help_font = ui_->label_helptext->font();
help_font.setBold(true);
ui_->label_helptext->setFont(help_font);
// Set up the sorting proxy model
front_proxy_->setSourceModel(front_model_);
front_proxy_->setDynamicSortFilter(true);
front_proxy_->sort(0);
back_proxy_->setSourceModel(back_model_);
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);
settings_menu->addActions(group_by_actions_->actions());
settings_menu->addSeparator();
settings_menu->addAction(IconLoader::Load("configure"), tr("Configure Tidal..."), this, SLOT(OpenSettingsDialog()));
ui_->settings->setMenu(settings_menu);
connect(ui_->radiobutton_searchbyalbums, SIGNAL(clicked(bool)), SLOT(SearchByAlbumsClicked(bool)));
connect(ui_->radiobutton_searchbysongs, SIGNAL(clicked(bool)), SLOT(SearchBySongsClicked(bool)));
connect(group_by_actions_, SIGNAL(triggered(QAction*)), SLOT(GroupByClicked(QAction*)));
// 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(AddResults(int, TidalSearch::ResultList)), SLOT(AddResults(int, TidalSearch::ResultList)), Qt::QueuedConnection);
connect(engine_, SIGNAL(SearchError(int, QString)), SLOT(SearchError(int, QString)), Qt::QueuedConnection);
connect(engine_, SIGNAL(ArtLoaded(int, QPixmap)), SLOT(ArtLoaded(int, QPixmap)), Qt::QueuedConnection);
ReloadSettings();
}
TidalSearchView::~TidalSearchView() { delete ui_; }
void TidalSearchView::ReloadSettings() {
QSettings s;
// Collection settings
s.beginGroup(TidalSettingsPage::kSettingsGroup);
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();
// Tidal search settings
s.beginGroup(TidalSettingsPage::kSettingsGroup);
searchby_ = TidalSettingsPage::SearchBy(s.value("searchby", int(TidalSettingsPage::SearchBy_Songs)).toInt());
switch (searchby_) {
case TidalSettingsPage::SearchBy_Songs:
ui_->radiobutton_searchbysongs->setChecked(true);
break;
case TidalSettingsPage::SearchBy_Albums:
ui_->radiobutton_searchbyalbums->setChecked(true);
break;
}
SetGroupBy(CollectionModel::Grouping(
CollectionModel::GroupBy(s.value("group_by1", int(CollectionModel::GroupBy_Artist)).toInt()),
CollectionModel::GroupBy(s.value("group_by2", int(CollectionModel::GroupBy_Album)).toInt()),
CollectionModel::GroupBy(s.value("group_by3", int(CollectionModel::GroupBy_None)).toInt())));
s.endGroup();
}
void TidalSearchView::StartSearch(const QString &query) {
ui_->search->setText(query);
TextEdited(query);
// Swap models immediately
swap_models_timer_->stop();
SwapModels();
}
void TidalSearchView::TextEdited(const QString &text) {
const QString trimmed(text.trimmed());
error_ = false;
// Add results to the back model, switch models after some delay.
back_model_->Clear();
current_model_ = back_model_;
current_proxy_ = back_proxy_;
swap_models_timer_->start();
// Cancel the last search (if any) and start the new one.
engine_->CancelSearch(last_search_id_);
// If text query is empty, don't start a new search
if (trimmed.isEmpty()) {
last_search_id_ = -1;
ui_->label_helptext->setText("Enter search terms above to find music");
}
else {
last_search_id_ = engine_->SearchAsync(trimmed, searchby_);
}
}
void TidalSearchView::AddResults(int id, const TidalSearch::ResultList &results) {
if (id != last_search_id_) return;
if (results.isEmpty()) return;
current_model_->AddResults(results);
}
void TidalSearchView::SearchError(const int id, const QString error) {
error_ = true;
ui_->label_helptext->setText(error);
ui_->results_stack->setCurrentWidget(ui_->help_page);
}
void TidalSearchView::SwapModels() {
art_requests_.clear();
std::swap(front_model_, back_model_);
std::swap(front_proxy_, back_proxy_);
ui_->results->setModel(front_proxy_);
if (ui_->search->text().trimmed().isEmpty() || error_) {
ui_->results_stack->setCurrentWidget(ui_->help_page);
}
else {
ui_->results_stack->setCurrentWidget(ui_->results_page);
}
}
void TidalSearchView::LazyLoadArt(const QModelIndex &proxy_index) {
if (!proxy_index.isValid() || proxy_index.model() != front_proxy_) {
return;
}
// Already loading art for this item?
if (proxy_index.data(TidalSearchModel::Role_LazyLoadingArt).isValid()) {
return;
}
// Should we even load art at all?
if (!app_->collection_model()->use_pretty_covers()) {
return;
}
// Is this an album?
const CollectionModel::GroupBy container_type = CollectionModel::GroupBy(proxy_index.data(CollectionModel::Role_ContainerType).toInt());
if (container_type != CollectionModel::GroupBy_Album &&
container_type != CollectionModel::GroupBy_AlbumArtist &&
container_type != CollectionModel::GroupBy_YearAlbum &&
container_type != CollectionModel::GroupBy_OriginalYearAlbum) {
return;
}
// Mark the item as loading art
const QModelIndex source_index = front_proxy_->mapToSource(proxy_index);
QStandardItem *item = front_model_->itemFromIndex(source_index);
item->setData(true, TidalSearchModel::Role_LazyLoadingArt);
// Walk down the item's children until we find a track
while (item->rowCount()) {
item = item->child(0);
}
// Get the track's Result
const TidalSearch::Result result = item->data(TidalSearchModel::Role_Result).value<TidalSearch::Result>();
// Load the art.
int id = engine_->LoadArtAsync(result);
art_requests_[id] = source_index;
}
void TidalSearchView::ArtLoaded(int id, const QPixmap &pixmap) {
if (!art_requests_.contains(id)) return;
QModelIndex index = art_requests_.take(id);
if (!pixmap.isNull()) {
front_model_->itemFromIndex(index)->setData(pixmap, Qt::DecorationRole);
}
}
MimeData *TidalSearchView::SelectedMimeData() {
if (!ui_->results->selectionModel()) return nullptr;
// Get all selected model indexes
QModelIndexList indexes = ui_->results->selectionModel()->selectedRows();
if (indexes.isEmpty()) {
// There's nothing selected - take the first thing in the model that isn't a divider.
for (int i = 0; i < front_proxy_->rowCount(); ++i) {
QModelIndex index = front_proxy_->index(i, 0);
if (!index.data(CollectionModel::Role_IsDivider).toBool()) {
indexes << index;
ui_->results->setCurrentIndex(index);
break;
}
}
}
// Still got nothing? Give up.
if (indexes.isEmpty()) {
return nullptr;
}
// Get items for these indexes
QList<QStandardItem*> items;
for (const QModelIndex &index : indexes) {
items << (front_model_->itemFromIndex(front_proxy_->mapToSource(index)));
}
// Get a MimeData for these items
return engine_->LoadTracks(front_model_->GetChildResults(items));
}
bool TidalSearchView::eventFilter(QObject *object, QEvent *event) {
if (object == ui_->search && event->type() == QEvent::KeyRelease) {
if (SearchKeyEvent(static_cast<QKeyEvent*>(event))) {
return true;
}
}
else if (object == ui_->results_stack && event->type() == QEvent::ContextMenu) {
if (ResultsContextMenuEvent(static_cast<QContextMenuEvent*>(event))) {
return true;
}
}
return QWidget::eventFilter(object, event);
}
bool TidalSearchView::SearchKeyEvent(QKeyEvent *event) {
switch (event->key()) {
case Qt::Key_Up:
ui_->results->UpAndFocus();
break;
case Qt::Key_Down:
ui_->results->DownAndFocus();
break;
case Qt::Key_Escape:
ui_->search->clear();
break;
case Qt::Key_Return:
AddSelectedToPlaylist();
break;
default:
return false;
}
event->accept();
return true;
}
bool TidalSearchView::ResultsContextMenuEvent(QContextMenuEvent *event) {
context_menu_ = new QMenu(this);
context_actions_ << context_menu_->addAction( IconLoader::Load("media-playback-start"), tr("Append to current playlist"), this, SLOT(AddSelectedToPlaylist()));
context_actions_ << context_menu_->addAction( IconLoader::Load("media-playback-start"), tr("Replace current playlist"), this, SLOT(LoadSelected()));
context_actions_ << context_menu_->addAction( IconLoader::Load("document-new"), tr("Open in new playlist"), this, SLOT(OpenSelectedInNewPlaylist()));
context_menu_->addSeparator();
context_actions_ << context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue track"), this, SLOT(AddSelectedToPlaylistEnqueue()));
context_menu_->addSeparator();
if (ui_->results->selectionModel() && ui_->results->selectionModel()->selectedRows().length() == 1) {
context_actions_ << context_menu_->addAction(IconLoader::Load("system-search"), tr("Search for this"), this, SLOT(SearchForThis()));
}
context_menu_->addSeparator();
context_menu_->addMenu(tr("Group by"))->addActions(group_by_actions_->actions());
context_menu_->addAction(IconLoader::Load("configure"), tr("Configure Tidal..."), this, SLOT(OpenSettingsDialog()));
const bool enable_context_actions = ui_->results->selectionModel() && ui_->results->selectionModel()->hasSelection();
for (QAction *action : context_actions_) {
action->setEnabled(enable_context_actions);
}
context_menu_->popup(event->globalPos());
return true;
}
void TidalSearchView::AddSelectedToPlaylist() {
emit AddToPlaylist(SelectedMimeData());
}
void TidalSearchView::LoadSelected() {
MimeData *data = SelectedMimeData();
if (!data) return;
data->clear_first_ = true;
emit AddToPlaylist(data);
}
void TidalSearchView::AddSelectedToPlaylistEnqueue() {
MimeData *data = SelectedMimeData();
if (!data) return;
data->enqueue_now_ = true;
emit AddToPlaylist(data);
}
void TidalSearchView::OpenSelectedInNewPlaylist() {
MimeData *data = SelectedMimeData();
if (!data) return;
data->open_in_new_playlist_ = true;
emit AddToPlaylist(data);
}
void TidalSearchView::SearchForThis() {
StartSearch(ui_->results->selectionModel()->selectedRows().first().data().toString());
}
void TidalSearchView::showEvent(QShowEvent *e) {
QWidget::showEvent(e);
FocusSearchField();
}
void TidalSearchView::FocusSearchField() {
ui_->search->setFocus();
ui_->search->selectAll();
}
void TidalSearchView::hideEvent(QHideEvent *e) {
QWidget::hideEvent(e);
}
void TidalSearchView::FocusOnFilter(QKeyEvent *event) {
ui_->search->setFocus();
QApplication::sendEvent(ui_->search, event);
}
void TidalSearchView::OpenSettingsDialog() {
app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Tidal);
}
void TidalSearchView::GroupByClicked(QAction *action) {
if (action->property("group_by").isNull()) {
if (!group_by_dialog_) {
group_by_dialog_.reset(new GroupByDialog);
connect(group_by_dialog_.data(), SIGNAL(Accepted(CollectionModel::Grouping)), SLOT(SetGroupBy(CollectionModel::Grouping)));
}
group_by_dialog_->show();
return;
}
SetGroupBy(action->property("group_by").value<CollectionModel::Grouping>());
}
void TidalSearchView::SetGroupBy(const CollectionModel::Grouping &g) {
// Clear requests: changing "group by" on the models will cause all the items to be removed/added again,
// so all the QModelIndex here will become invalid. New requests will be created for those
// songs when they will be displayed again anyway (when TidalSearchItemDelegate::paint will call LazyLoadArt)
art_requests_.clear();
// Update the models
front_model_->SetGroupBy(g, true);
back_model_->SetGroupBy(g, false);
// Save the setting
QSettings s;
s.beginGroup(TidalSettingsPage::kSettingsGroup);
s.setValue("group_by1", int(g.first));
s.setValue("group_by2", int(g.second));
s.setValue("group_by3", int(g.third));
s.endGroup();
// Make sure the correct action is checked.
for (QAction *action : group_by_actions_->actions()) {
if (action->property("group_by").isNull()) continue;
if (g == action->property("group_by").value<CollectionModel::Grouping>()) {
action->setChecked(true);
return;
}
}
// Check the advanced action
group_by_actions_->actions().last()->setChecked(true);
}
void TidalSearchView::SearchBySongsClicked(bool checked) {
SetSearchBy(TidalSettingsPage::SearchBy_Songs);
}
void TidalSearchView::SearchByAlbumsClicked(bool checked) {
SetSearchBy(TidalSettingsPage::SearchBy_Albums);
}
void TidalSearchView::SetSearchBy(TidalSettingsPage::SearchBy searchby) {
searchby_ = searchby;
QSettings s;
s.beginGroup(TidalSettingsPage::kSettingsGroup);
s.setValue("searchby", int(searchby));
s.endGroup();
TextEdited(ui_->search->text());
}

139
src/tidal/tidalsearchview.h Normal file
View File

@ -0,0 +1,139 @@
/*
* Strawberry Music Player
* This code was part of Clementine (GlobalSearch)
* Copyright 2012, 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 TIDALSEARCHVIEW_H
#define TIDALSEARCHVIEW_H
#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 "tidalsearch.h"
#include "settings/tidalsettingspage.h"
class Application;
class GroupByDialog;
class TidalSearchModel;
class Ui_TidalSearchView;
class TidalSearchView : public QWidget {
Q_OBJECT
public:
TidalSearchView(Application *app, QWidget *parent = nullptr);
~TidalSearchView();
static const int kSwapModelsTimeoutMsec;
void LazyLoadArt(const QModelIndex &index);
void showEvent(QShowEvent *e);
void hideEvent(QHideEvent *e);
bool eventFilter(QObject *object, QEvent *event);
public slots:
void ReloadSettings();
void StartSearch(const QString &query);
void FocusSearchField();
void OpenSettingsDialog();
signals:
void AddToPlaylist(QMimeData *data);
private slots:
void SwapModels();
void TextEdited(const QString &text);
void AddResults(int id, const TidalSearch::ResultList &results);
void SearchError(const int id, const QString error);
void ArtLoaded(int id, const QPixmap &pixmap);
void FocusOnFilter(QKeyEvent *event);
void AddSelectedToPlaylist();
void LoadSelected();
void OpenSelectedInNewPlaylist();
void AddSelectedToPlaylistEnqueue();
void SearchForThis();
void SearchBySongsClicked(bool);
void SearchByAlbumsClicked(bool);
void GroupByClicked(QAction *action);
void SetSearchBy(TidalSettingsPage::SearchBy searchby);
void SetGroupBy(const CollectionModel::Grouping &g);
private:
MimeData *SelectedMimeData();
bool SearchKeyEvent(QKeyEvent *event);
bool ResultsContextMenuEvent(QContextMenuEvent *event);
Application *app_;
TidalSearch *engine_;
Ui_TidalSearchView *ui_;
QScopedPointer<GroupByDialog> group_by_dialog_;
QMenu *context_menu_;
QList<QAction*> context_actions_;
QActionGroup *group_by_actions_;
int last_search_id_;
// Like graphics APIs have a front buffer and a back buffer, there's a front model and a back model
// The front model is the one that's shown in the UI and the back model is the one that lies in wait.
// current_model_ will point to either the front or the back model.
TidalSearchModel *front_model_;
TidalSearchModel *back_model_;
TidalSearchModel *current_model_;
QSortFilterProxyModel *front_proxy_;
QSortFilterProxyModel *back_proxy_;
QSortFilterProxyModel *current_proxy_;
QMap<int, QModelIndex> art_requests_;
QTimer *swap_models_timer_;
QIcon search_icon_;
QIcon warning_icon_;
TidalSettingsPage::SearchBy searchby_;
bool error_;
};
#endif // TIDALSEARCHVIEW_H

View File

@ -0,0 +1,259 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TidalSearchView</class>
<widget class="QWidget" name="TidalSearchView">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>437</width>
<height>633</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="QWidget" name="widget_search" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<layout class="QVBoxLayout" name="layout_top" stretch="0,0">
<item>
<layout class="QHBoxLayout" name="layout_search">
<item>
<widget class="QSearchField" name="search" native="true">
<property name="placeholderText" stdset="0">
<string>Search for anything</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="settings">
<property name="minimumSize">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="layout_searchby">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<item>
<widget class="QLabel" name="label_searchby">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Search by</string>
</property>
<property name="margin">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radiobutton_searchbyalbums">
<property name="text">
<string>albu&amp;ms</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radiobutton_searchbysongs">
<property name="text">
<string>songs</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="results_stack">
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="results_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="AutoExpandingTreeView" name="results">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragOnly</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="allColumnsShowFocus">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="help_page">
<layout class="QVBoxLayout" name="verticalLayout_6">
<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="QScrollArea" name="help_frame">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="help_frame_contents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>435</width>
<height>533</height>
</rect>
</property>
<widget class="QWidget" name="widget" native="true">
<property name="geometry">
<rect>
<x>9</x>
<y>109</y>
<width>420</width>
<height>100</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="leftMargin">
<number>32</number>
</property>
<property name="topMargin">
<number>16</number>
</property>
<property name="rightMargin">
<number>32</number>
</property>
<property name="bottomMargin">
<number>64</number>
</property>
<item>
<widget class="QLabel" name="label_helptext">
<property name="minimumSize">
<size>
<width>0</width>
<height>80</height>
</size>
</property>
<property name="text">
<string>Enter search terms above to find music</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>QSearchField</class>
<extends>QWidget</extends>
<header>3rdparty/qocoa/qsearchfield.h</header>
</customwidget>
<customwidget>
<class>AutoExpandingTreeView</class>
<extends>QTreeView</extends>
<header>widgets/autoexpandingtreeview.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

832
src/tidal/tidalservice.cpp Normal file
View File

@ -0,0 +1,832 @@
/*
* 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 <QList>
#include <QVector>
#include <QPair>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QTimer>
#include <QJsonParseError>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include <QMenu>
#include <QSettings>
#include "core/application.h"
#include "core/closure.h"
#include "core/logging.h"
#include "core/mergedproxymodel.h"
#include "core/network.h"
#include "core/song.h"
#include "core/iconloader.h"
#include "core/taskmanager.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "internet/internetmodel.h"
#include "tidalservice.h"
#include "tidalsearch.h"
#include "settings/tidalsettingspage.h"
const char *TidalService::kServiceName = "Tidal";
const char *TidalService::kApiUrl = "https://listen.tidal.com/v1";
const char *TidalService::kAuthUrl = "https://listen.tidal.com/v1/login/username";
const char *TidalService::kResourcesUrl = "http://resources.tidal.com";
const char *TidalService::kApiToken = "P5Xbeo5LFvESeDy6";
const int TidalService::kSearchDelayMsec = 1500;
const int TidalService::kSearchAlbumsLimit = 40;
const int TidalService::kSearchTracksLimit = 10;
typedef QPair<QString, QString> Param;
TidalService::TidalService(Application *app, InternetModel *parent)
: InternetService(kServiceName, app, parent, parent),
network_(new NetworkAccessManager(this)),
search_delay_(new QTimer(this)),
pending_search_id_(0),
next_pending_search_id_(1),
search_requests_(0),
login_sent_(false) {
search_delay_->setInterval(kSearchDelayMsec);
search_delay_->setSingleShot(true);
connect(search_delay_, SIGNAL(timeout()), SLOT(StartSearch()));
ReloadSettings();
LoadSessionID();
}
TidalService::~TidalService() {}
void TidalService::ShowConfig() {
app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Tidal);
}
void TidalService::ReloadSettings() {
QSettings s;
s.beginGroup(TidalSettingsPage::kSettingsGroup);
username_ = s.value("username").toString();
password_ = s.value("password").toString();
quality_ = s.value("quality").toString();
s.endGroup();
}
void TidalService::LoadSessionID() {
QSettings s;
s.beginGroup(TidalSettingsPage::kSettingsGroup);
if (!s.contains("user_id") ||!s.contains("session_id") || !s.contains("country_code")) return;
session_id_ = s.value("session_id").toString();
user_id_ = s.value("user_id").toInt();
country_code_ = s.value("country_code").toString();
s.endGroup();
}
void TidalService::Login(const QString &username, const QString &password) {
Login(nullptr, username, password);
}
void TidalService::Login(TidalSearchContext *search_ctx, const QString &username, const QString &password) {
login_sent_ = true;
int id = 0;
if (search_ctx) {
search_ctx->login_sent = true;
search_ctx->login_attempts++;
id = search_ctx->id;
}
typedef QPair<QString, QString> Arg;
typedef QList<Arg> ArgList;
typedef QPair<QByteArray, QByteArray> EncodedArg;
typedef QList<EncodedArg> EncodedArgList;
ArgList args = ArgList() <<Arg("token", kApiToken) << Arg("username", username) << Arg("password", password) << Arg("clientVersion", "2.2.1--7");
QStringList query_items;
QUrlQuery url_query;
for (const Arg &arg : args) {
EncodedArg encoded_arg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second));
query_items << QString(encoded_arg.first + "=" + encoded_arg.second);
url_query.addQueryItem(encoded_arg.first, encoded_arg.second);
}
QUrl url(kAuthUrl);
QNetworkRequest req(url);
req.setRawHeader("Origin", "http://listen.tidal.com");
QNetworkReply *reply = network_->post(req, url_query.toString(QUrl::FullyEncoded).toUtf8());
NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleAuthReply(QNetworkReply*, int)), reply, id);
}
void TidalService::HandleAuthReply(QNetworkReply *reply, int id) {
reply->deleteLater();
login_sent_ = false;
TidalSearchContext *search_ctx(nullptr);
if (id != 0 && requests_search_.contains(id)) {
search_ctx = requests_search_.value(id);
search_ctx->login_sent = false;
}
//int http_status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (reply->error() != QNetworkReply::NoError) {
if (reply->error() < 200) {
// This is a network error, there is nothing more to do.
QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
if (search_ctx) Error(search_ctx, failure_reason);
emit LoginFailure(failure_reason);
return;
}
else {
// See if there is Json data containing "userMessage" - then use that instead.
QByteArray data(reply->readAll());
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
QString failure_reason;
if (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("userMessage")) {
failure_reason = QString("Authentication failure: %1").arg(json_obj["userMessage"].toString());
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
if (search_ctx) Error(search_ctx, failure_reason);
emit LoginFailure(failure_reason);
return;
}
}
QByteArray data(reply->readAll());
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
QString failure_reason("Authentication reply from server missing Json data.");
if (search_ctx) Error(search_ctx, failure_reason);
emit LoginFailure(failure_reason);
return;
}
if (json_doc.isNull() || json_doc.isEmpty()) {
QString failure_reason("Authentication reply from server has empty Json document.");
if (search_ctx) Error(search_ctx, failure_reason);
emit LoginFailure(failure_reason);
return;
}
if (!json_doc.isObject()) {
QString failure_reason("Authentication reply from server has Json document that is not an object.");
if (search_ctx) Error(search_ctx, failure_reason);
emit LoginFailure(failure_reason);
return;
}
QJsonObject json_obj = json_doc.object();
if (json_obj.isEmpty()) {
QString failure_reason("Authentication reply from server has empty Json object.");
if (search_ctx) Error(search_ctx, failure_reason);
emit LoginFailure(failure_reason);
return;
}
if ( !json_obj.contains("userId") || !json_obj.contains("sessionId") || !json_obj.contains("countryCode") ) {
QString failure_reason = tr("Authentication reply from server is missing userId, sessionId or countryCode");
if (search_ctx) Error(search_ctx, failure_reason);
emit LoginFailure(failure_reason);
return;
}
country_code_ = json_obj["countryCode"].toString();
session_id_ = json_obj["sessionId"].toString();
user_id_ = json_obj["userId"].toInt();
QSettings s;
s.beginGroup(TidalSettingsPage::kSettingsGroup);
s.setValue("user_id", user_id_);
s.setValue("session_id", session_id_);
s.setValue("country_code", country_code_);
s.endGroup();
qLog(Debug) << "Tidal: Login successful" << "user id" << user_id_ << "session id" << session_id_ << "country code" << country_code_;
if (search_ctx) {
qLog(Debug) << "Tidal: Resuming search";
SendSearch(search_ctx);
}
emit LoginSuccess();
}
void TidalService::Logout() {
user_id_ = 0;
session_id_.clear();
country_code_.clear();
QSettings s;
s.beginGroup(TidalSettingsPage::kSettingsGroup);
s.remove("user_id");
s.remove("session_id");
s.remove("country_code");
s.endGroup();
}
QNetworkReply *TidalService::CreateRequest(const QString &ressource_name, const QList<Param> &params) {
typedef QPair<QString, QString> Arg;
typedef QList<Arg> ArgList;
typedef QPair<QByteArray, QByteArray> EncodedArg;
typedef QList<EncodedArg> EncodedArgList;
ArgList args = ArgList() << params
<< Arg("sessionId", session_id_)
<< Arg("countryCode", country_code_);
QStringList query_items;
QUrlQuery url_query;
for (const Arg& arg : args) {
EncodedArg encoded_arg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second));
query_items << QString(encoded_arg.first + "=" + encoded_arg.second);
url_query.addQueryItem(encoded_arg.first, encoded_arg.second);
}
QUrl url(kApiUrl + QString("/") + ressource_name);
url.setQuery(url_query);
QNetworkRequest req(url);
QNetworkReply *reply = network_->get(req);
//qLog(Debug) << "Tidal: Sending request" << url;
return reply;
}
QJsonObject TidalService::ExtractJsonObj(TidalSearchContext *search_ctx, QNetworkReply *reply) {
QByteArray data;
//int http_status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (reply->error() == QNetworkReply::NoError) {
data = reply->readAll();
}
else {
if (reply->error() < 200) {
// This is a network error, there is nothing more to do.
QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
Error(search_ctx, failure_reason);
}
else {
// See if there is Json data containing "userMessage" - then use that instead.
data = reply->readAll();
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
QString failure_reason;
if (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("userMessage")) {
failure_reason = json_obj["userMessage"].toString();
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
if (reply->error() == QNetworkReply::ContentAccessDenied || reply->error() == QNetworkReply::ContentOperationNotPermittedError || reply->error() == QNetworkReply::AuthenticationRequiredError) {
// Session is probably expired, attempt to login once
Logout();
if (search_ctx->login_attempts < 1 && !username_.isEmpty() && !password_.isEmpty()) {
qLog(Error) << "Tidal:" << failure_reason;
qLog(Error) << "Tidal:" << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
qLog(Error) << "Tidal:" << "Attempting to login.";
Login(search_ctx, username_, password_);
}
else {
Error(search_ctx, failure_reason);
}
}
else if (reply->error() == QNetworkReply::ContentNotFoundError) { // Ignore this error
qLog(Error) << "Tidal:" << failure_reason;
}
else { // Fail
Error(search_ctx, failure_reason);
}
}
return QJsonObject();
}
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
Error(search_ctx, "Reply from server missing Json data.");
return QJsonObject();
}
if (json_doc.isNull() || json_doc.isEmpty()) {
Error(search_ctx, "Received empty Json document.");
return QJsonObject();
}
if (!json_doc.isObject()) {
Error(search_ctx, "Json document is not an object.");
return QJsonObject();
}
QJsonObject json_obj = json_doc.object();
if (json_obj.isEmpty()) {
Error(search_ctx, "Received empty Json object.");
return QJsonObject();
}
//qLog(Debug) << json_obj;
return json_obj;
}
QJsonArray TidalService::ExtractItems(TidalSearchContext *search_ctx, QNetworkReply *reply) {
QJsonObject json_obj = ExtractJsonObj(search_ctx, reply);
if (json_obj.isEmpty()) return QJsonArray();
if (!json_obj.contains("items")) {
Error(search_ctx, "Json reply is missing items.");
return QJsonArray();
}
QJsonArray json_items = json_obj["items"].toArray();
if (json_items.isEmpty()) {
Error(search_ctx, "No match.");
return QJsonArray();
}
return json_items;
}
int TidalService::Search(const QString &text, TidalSettingsPage::SearchBy searchby) {
pending_search_id_ = next_pending_search_id_;
pending_search_ = text;
pending_searchby_ = searchby;
next_pending_search_id_++;
if (text.isEmpty()) {
search_delay_->stop();
return pending_search_id_;
}
search_delay_->start();
return pending_search_id_;
}
void TidalService::StartSearch() {
if (username_.isEmpty() || password_.isEmpty()) {
emit SearchError(pending_search_id_, "Missing username and/or password.");
next_pending_search_id_ = 1;
ShowConfig();
return;
}
TidalSearchContext *search_ctx = CreateSearch(pending_search_id_, pending_search_);
if (authenticated()) SendSearch(search_ctx);
else Login(search_ctx, username_, password_);
}
TidalSearchContext *TidalService::CreateSearch(const int search_id, const QString text) {
TidalSearchContext *search_ctx = new TidalSearchContext;
search_ctx->id = search_id;
search_ctx->text = text;
search_ctx->album_requests = 0;
search_ctx->song_requests = 0;
search_ctx->requests_album_.clear();
search_ctx->requests_song_.clear();
search_ctx->login_attempts = 0;
requests_search_.insert(search_id, search_ctx);
return search_ctx;
}
void TidalService::SendSearch(TidalSearchContext *search_ctx) {
QList<Param> parameters;
parameters << Param("query", search_ctx->text);
QString searchparam;
switch (pending_searchby_) {
case TidalSettingsPage::SearchBy_Songs:
searchparam = "search/tracks";
parameters << Param("limit", QString::number(kSearchTracksLimit));
break;
case TidalSettingsPage::SearchBy_Albums:
default:
searchparam = "search/albums";
parameters << Param("limit", QString::number(kSearchAlbumsLimit));
break;
}
QNetworkReply *reply = CreateRequest(searchparam, parameters);
NewClosure(reply, SIGNAL(finished()), this, SLOT(SearchFinished(QNetworkReply*, int)), reply, search_ctx->id);
}
void TidalService::SearchFinished(QNetworkReply *reply, int id) {
reply->deleteLater();
if (!requests_search_.contains(id)) return;
TidalSearchContext *search_ctx = requests_search_.value(id);
QJsonArray json_items = ExtractItems(search_ctx, reply);
if (json_items.isEmpty()) {
CheckFinish(search_ctx);
return;
}
//qLog(Debug) << json_items;
QVector<QString> albums;
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();
//qLog(Debug) << json_obj;
int album_id(0);
QString album("");
if (json_obj.contains("type")) {
// This was a albums search
if (!json_obj.contains("id") || !json_obj.contains("title")) {
qLog(Error) << "Tidal: Invalid Json reply, item is missing ID or title.";
qLog(Debug) << json_obj;
continue;
}
album_id = json_obj["id"].toInt();
album = json_obj["title"].toString();
}
else if (json_obj.contains("album")) {
// This was a tracks search
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 (search_ctx->requests_album_.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();
//qLog(Debug) << "Tidal:" << artist << album << quality;
QString artist_album(QString("%1-%2").arg(artist).arg(album));
if (albums.contains(artist_album)) {
qLog(Debug) << "Tidal: Skipping duplicate album" << artist << album << quality;
continue;
}
albums.insert(0, artist_album);
search_ctx->requests_album_.insert(album_id, album_id);
GetAlbum(search_ctx, album_id);
search_ctx->album_requests++;
if (search_ctx->album_requests >= kSearchAlbumsLimit) break;
}
CheckFinish(search_ctx);
}
void TidalService::GetAlbum(TidalSearchContext *search_ctx, const int album_id) {
QList<Param> parameters;
parameters << Param("token", session_id_)
<< Param("soundQuality", quality_);
QNetworkReply *reply = CreateRequest(QString("albums/%1/tracks").arg(album_id), parameters);
NewClosure(reply, SIGNAL(finished()), this, SLOT(GetAlbumFinished(QNetworkReply*, int, int)), reply, search_ctx->id, album_id);
}
void TidalService::GetAlbumFinished(QNetworkReply *reply, int search_id, int album_id) {
reply->deleteLater();
if (!requests_search_.contains(search_id)) return;
TidalSearchContext *search_ctx = requests_search_.value(search_id);
if (!search_ctx->requests_album_.contains(album_id)) return;
search_ctx->album_requests--;
QJsonArray json_items = ExtractItems(search_ctx, reply);
if (json_items.isEmpty()) {
CheckFinish(search_ctx);
return;
}
bool compilation = false;
bool multidisc = false;
Song *first_song(nullptr);
QList<Song *> songs;
for (const QJsonValue &value : json_items) {
Song *song = ParseSong(search_ctx, album_id, value);
if (!song) continue;
songs << song;
if (song->disc() >= 2) multidisc = true;
if (song->is_compilation() || (first_song && song->artist() != first_song->artist())) compilation = true;
if (!first_song) first_song = song;
}
if (compilation || multidisc) {
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);
}
}
}
CheckFinish(search_ctx);
}
Song *TidalService::ParseSong(TidalSearchContext *search_ctx, const int album_id, const QJsonValue &value) {
Song song;
if (!value.isObject()) {
qLog(Error) << "Tidal: Invalid Json reply, track is not a object.";
qLog(Debug) << value;
return nullptr;
}
QJsonObject json_obj = value.toObject();
//qLog(Debug) << json_obj;
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")
) {
qLog(Error) << "Tidal: Invalid Json reply, track is missing one or more values.";
qLog(Debug) << json_obj;
return nullptr;
}
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 id = json_obj["id"].toInt();
QString title = json_obj["title"].toString();
QString url = 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();
if (!json_value_artist.isObject()) {
qLog(Error) << "Tidal: Invalid Json reply, track artist is not a object.";
qLog(Debug) << json_value_artist;
return nullptr;
}
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 nullptr;
}
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 nullptr;
}
QJsonObject json_album = json_value_album.toObject();
if (!json_album.contains("title") || !json_album.contains("cover")) {
qLog(Error) << "Tidal: Invalid Json reply, track album is missing title or cover.";
qLog(Debug) << json_album;
return nullptr;
}
QString album = json_album["title"].toString();
QString cover = json_album["cover"].toString();
if (!allow_streaming || !stream_ready) {
qLog(Error) << "Tidal: Skipping song" << artist << album << title << "because allowStreaming is false OR streamReady is false.";
qLog(Debug) << json_obj;
return nullptr;
}
//qLog(Debug) << "id" << id << "track" << track << "disc" << disc << "title" << title << "album" << album << "artist" << artist << cover << allow_streaming << url;
song.set_album_id(album_id);
song.set_artist(artist);
song.set_album(album);
song.set_title(title);
song.set_track(track);
song.set_disc(disc);
song.set_bitrate(0);
song.set_samplerate(0);
song.set_bitdepth(0);
QVariant q_duration = json_duration.toVariant();
if (q_duration.isValid()) {
quint64 duration = q_duration.toULongLong() * kNsecPerSec;
song.set_length_nanosec(duration);
}
// Check and see if there is more than 1 artist on the song.
//int i = 0;
//for (const QJsonValue &a : json_artists) {
//i++;
//qLog(Debug) << a << i;
//}
//if (i > 1) song.set_compilation_detected(true);
cover = cover.replace("-", "/");
QUrl cover_url (QString("%1/images/%2/750x750.jpg").arg(kResourcesUrl).arg(cover));
song.set_art_automatic(cover_url.toEncoded());
if (search_ctx->requests_song_.contains(id)) return search_ctx->requests_song_.value(id);
Song *song_new = new Song(song);
search_ctx->requests_song_.insert(id, song_new);
search_ctx->song_requests++;
GetStreamURL(search_ctx, album_id, id);
return song_new;
}
void TidalService::GetStreamURL(TidalSearchContext *search_ctx, const int album_id, const int song_id) {
QList<Param> parameters;
parameters << Param("token", session_id_)
<< Param("soundQuality", quality_);
QNetworkReply *reply = CreateRequest(QString("tracks/%1/streamUrl").arg(song_id), parameters);
NewClosure(reply, SIGNAL(finished()), this, SLOT(GetStreamURLFinished(QNetworkReply*, int, int)), reply, search_ctx->id, song_id);
}
void TidalService::GetStreamURLFinished(QNetworkReply *reply, const int search_id, const int song_id) {
reply->deleteLater();
if (!requests_search_.contains(search_id)) return;
TidalSearchContext *search_ctx = requests_search_.value(search_id);
if (!search_ctx->requests_song_.contains(song_id)) {
CheckFinish(search_ctx);
return;
}
Song *song = search_ctx->requests_song_.value(song_id);
search_ctx->song_requests--;
QJsonObject json_obj = ExtractJsonObj(search_ctx, reply);
if (json_obj.isEmpty()) {
delete search_ctx->requests_song_.take(song_id);
CheckFinish(search_ctx);
return;
}
if (!json_obj.contains("url") || !json_obj.contains("codec")) {
qLog(Error) << "Tidal: Invalid Json reply, stream missing url or codec.";
qLog(Debug) << json_obj;
delete search_ctx->requests_song_.take(song_id);
CheckFinish(search_ctx);
return;
}
song->set_url(QUrl(json_obj["url"].toString()));
song->set_valid(true);
QString codec = json_obj["codec"].toString();
if (codec == "AAC") song->set_filetype(Song::Type_MP4);
else qLog(Debug) << "Tidal codec" << codec;
//qLog(Debug) << song->artist() << song->album() << song->title() << song->url() << song->filetype();
search_ctx->songs << *song;
delete search_ctx->requests_song_.take(song_id);
CheckFinish(search_ctx);
}
void TidalService::CheckFinish(TidalSearchContext *search_ctx) {
if (!search_ctx->login_sent && search_ctx->album_requests <= 0 && search_ctx->song_requests <= 0) {
if (search_ctx->songs.isEmpty()) emit SearchError(search_ctx->id, search_ctx->error);
else emit SearchResults(search_ctx->id, search_ctx->songs);
delete requests_search_.take(search_ctx->id);
}
}
void TidalService::Error(TidalSearchContext *search_ctx, QString error, QString debug) {
qLog(Error) << "Tidal:" << error;
if (!debug.isEmpty()) qLog(Debug) << debug;
if (search_ctx) {
search_ctx->error = error;
CheckFinish(search_ctx);
}
}

View File

@ -0,0 +1,745 @@
/*
* 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 <QList>
#include <QVector>
#include <QPair>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QTimer>
#include <QJsonParseError>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include <QMenu>
#include <QSettings>
#include "core/application.h"
#include "core/closure.h"
#include "core/logging.h"
#include "core/mergedproxymodel.h"
#include "core/network.h"
#include "core/song.h"
#include "core/iconloader.h"
#include "core/taskmanager.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "internet/internetmodel.h"
#include "tidalservice.h"
#include "tidalsearch.h"
#include "settings/tidalsettingspage.h"
const char *TidalService::kServiceName = "Tidal";
const char *TidalService::kApiUrl = "https://listen.tidal.com/v1";
const char *TidalService::kAuthUrl = "https://listen.tidal.com/v1/login/username";
const char *TidalService::kResourcesUrl = "http://resources.tidal.com";
const char *TidalService::kApiToken = "P5Xbeo5LFvESeDy6";
const int TidalService::kSearchDelayMsec = 1000;
const int TidalService::kSearchAlbumsLimit = 1;
const int TidalService::kSearchTracksLimit = 1;
typedef QPair<QString, QString> Param;
TidalService::TidalService(Application *app, InternetModel *parent)
: InternetService(kServiceName, app, parent, parent),
network_(new NetworkAccessManager(this)),
search_delay_(new QTimer(this)),
pending_search_id_(0),
next_pending_search_id_(1),
search_requests_(0),
login_sent_(false) {
search_delay_->setInterval(kSearchDelayMsec);
search_delay_->setSingleShot(true);
connect(search_delay_, SIGNAL(timeout()), SLOT(StartSearch()));
ReloadSettings();
LoadSessionID();
}
TidalService::~TidalService() {}
void TidalService::ShowConfig() {
app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Tidal);
}
void TidalService::ReloadSettings() {
QSettings s;
s.beginGroup(TidalSettingsPage::kSettingsGroup);
username_ = s.value("username").toString();
password_ = s.value("password").toString();
quality_ = s.value("quality").toString();
s.endGroup();
}
void TidalService::LoadSessionID() {
QSettings s;
s.beginGroup(TidalSettingsPage::kSettingsGroup);
if (!s.contains("user_id") ||!s.contains("session_id") || !s.contains("country_code")) return;
session_id_ = s.value("session_id").toString();
user_id_ = s.value("user_id").toInt();
country_code_ = s.value("country_code").toString();
s.endGroup();
}
void TidalService::Login(const QString &username, const QString &password) {
Login(nullptr, username, password);
}
void TidalService::Login(TidalSearchContext *search_ctx, const QString &username, const QString &password) {
login_sent_ = true;
int id = 0;
if (search_ctx) {
search_ctx->login_sent = true;
search_ctx->login_attempts++;
id = search_ctx->id;
}
typedef QPair<QString, QString> Arg;
typedef QList<Arg> ArgList;
typedef QPair<QByteArray, QByteArray> EncodedArg;
typedef QList<EncodedArg> EncodedArgList;
ArgList args = ArgList() <<Arg("token", kApiToken) << Arg("username", username) << Arg("password", password) << Arg("clientVersion", "2.2.1--7");
QStringList query_items;
QUrlQuery url_query;
for (const Arg &arg : args) {
EncodedArg encoded_arg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second));
query_items << QString(encoded_arg.first + "=" + encoded_arg.second);
url_query.addQueryItem(encoded_arg.first, encoded_arg.second);
}
QUrl url(kAuthUrl);
QNetworkRequest req(url);
req.setRawHeader("Origin", "http://listen.tidal.com");
QNetworkReply *reply = network_->post(req, url_query.toString(QUrl::FullyEncoded).toUtf8());
NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleAuthReply(QNetworkReply*, int)), reply, id);
}
void TidalService::HandleAuthReply(QNetworkReply *reply, int id) {
reply->deleteLater();
login_sent_ = false;
TidalSearchContext *search_ctx(nullptr);
if (id != 0 && requests_search_.contains(id)) {
search_ctx = requests_search_.value(id);
search_ctx->login_sent = false;
}
//int http_status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (reply->error() != QNetworkReply::NoError) {
if (reply->error() < 200) {
// This is a network error, there is nothing more to do.
QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
if (search_ctx) Error(search_ctx, failure_reason);
emit LoginFailure(failure_reason);
return;
}
else {
// See if there is Json data containing "userMessage" - then use that instead.
QByteArray data(reply->readAll());
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
QString failure_reason;
if (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("userMessage")) {
failure_reason = QString("Authentication failure: %1").arg(json_obj["userMessage"].toString());
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
if (search_ctx) Error(search_ctx, failure_reason);
emit LoginFailure(failure_reason);
return;
}
}
QByteArray data(reply->readAll());
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
QString failure_reason("Authentication reply from server missing Json data.");
if (search_ctx) Error(search_ctx, failure_reason);
emit LoginFailure(failure_reason);
return;
}
if (json_doc.isNull() || json_doc.isEmpty()) {
QString failure_reason("Authentication reply from server has empty Json document.");
if (search_ctx) Error(search_ctx, failure_reason);
emit LoginFailure(failure_reason);
return;
}
if (!json_doc.isObject()) {
QString failure_reason("Authentication reply from server has Json document that is not an object.");
if (search_ctx) Error(search_ctx, failure_reason);
emit LoginFailure(failure_reason);
return;
}
QJsonObject json_obj = json_doc.object();
if (json_obj.isEmpty()) {
QString failure_reason("Authentication reply from server has empty Json object.");
if (search_ctx) Error(search_ctx, failure_reason);
emit LoginFailure(failure_reason);
return;
}
if (json_obj["userId"].isUndefined() || json_obj["sessionId"].isUndefined() || json_obj["countryCode"].isUndefined()) {
QString failure_reason = tr("Authentication reply from server missing userId, sessionId or countryCode");
if (search_ctx) Error(search_ctx, failure_reason);
emit LoginFailure(failure_reason);
return;
}
country_code_ = json_obj["countryCode"].toString();
session_id_ = json_obj["sessionId"].toString();
user_id_ = json_obj["userId"].toInt();
QSettings s;
s.beginGroup(TidalSettingsPage::kSettingsGroup);
s.setValue("user_id", user_id_);
s.setValue("session_id", session_id_);
s.setValue("country_code", country_code_);
s.endGroup();
qLog(Debug) << "Tidal: Login successful" << "user id" << user_id_ << "session id" << session_id_ << "country code" << country_code_;
if (search_ctx) {
qLog(Debug) << "Tidal: Resuming search";
SendSearch(search_ctx);
}
emit LoginSuccess();
}
void TidalService::Logout() {
user_id_ = 0;
session_id_.clear();
country_code_.clear();
QSettings s;
s.beginGroup(TidalSettingsPage::kSettingsGroup);
s.remove("user_id");
s.remove("session_id");
s.remove("country_code");
s.endGroup();
}
QNetworkReply *TidalService::CreateRequest(const QString &ressource_name, const QList<Param> &params) {
typedef QPair<QString, QString> Arg;
typedef QList<Arg> ArgList;
typedef QPair<QByteArray, QByteArray> EncodedArg;
typedef QList<EncodedArg> EncodedArgList;
ArgList args = ArgList() << params
<< Arg("sessionId", session_id_)
<< Arg("countryCode", country_code_);
QStringList query_items;
QUrlQuery url_query;
for (const Arg& arg : args) {
EncodedArg encoded_arg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second));
query_items << QString(encoded_arg.first + "=" + encoded_arg.second);
url_query.addQueryItem(encoded_arg.first, encoded_arg.second);
}
QUrl url(kApiUrl + QString("/") + ressource_name);
url.setQuery(url_query);
QNetworkRequest req(url);
QNetworkReply *reply = network_->get(req);
//qLog(Debug) << "Tidal: Sending request" << url;
return reply;
}
QJsonObject TidalService::ExtractJsonObj(TidalSearchContext *search_ctx, QNetworkReply *reply) {
//int http_status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (reply->error() != QNetworkReply::NoError) {
if (reply->error() < 200) {
// This is a network error, there is nothing more to do.
QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
Error(search_ctx, failure_reason);
}
else {
// See if there is Json data containing "userMessage" - then use that instead.
QByteArray data(reply->readAll());
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
QString failure_reason;
if (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("userMessage")) {
failure_reason = json_obj["userMessage"].toString();
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
if (reply->error() == QNetworkReply::ContentAccessDenied ||
reply->error() == QNetworkReply::ContentOperationNotPermittedError ||
reply->error() == QNetworkReply::ContentNotFoundError ||
reply->error() == QNetworkReply::AuthenticationRequiredError) {
Logout();
if (search_ctx->login_attempts < 1 && !username_.isEmpty() && !password_.isEmpty()) {
qLog(Error) << "Tidal:" << failure_reason;
qLog(Error) << "Tidal:" << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
qLog(Error) << "Tidal:" << "Attempting to login.";
Login(search_ctx, username_, password_);
}
else {
Error(search_ctx, failure_reason);
}
}
else {
Error(search_ctx, failure_reason);
}
}
return QJsonObject();
}
QByteArray data(reply->readAll());
qLog(Debug) << data;
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
Error(search_ctx, "Error while extracting Json document from results.");
return QJsonObject();
}
if (!json_doc.isObject()) {
Error(search_ctx, "Json document is not an object.");
return QJsonObject();
}
if (json_doc.isNull() || json_doc.isEmpty()) {
Error(search_ctx, "Received empty Json document.");
return QJsonObject();
}
QJsonObject json_obj = json_doc.object();
if (json_obj.isEmpty()) {
Error(search_ctx, "Received empty Json object.");
return QJsonObject();
}
//qLog(Debug) << json_obj;
return json_obj;
}
QJsonArray TidalService::ExtractItems(TidalSearchContext *search_ctx, QNetworkReply *reply) {
QJsonObject json_obj = ExtractJsonObj(search_ctx, reply);
if (json_obj.isEmpty()) return QJsonArray();
if (!json_obj.contains("items")) {
Error(search_ctx, "Json reply is missing items.");
return QJsonArray();
}
QJsonArray json_items = json_obj["items"].toArray();
if (json_items.isEmpty()) {
Error(search_ctx, "No match.");
return QJsonArray();
}
return json_items;
}
int TidalService::Search(const QString &text, TidalSettingsPage::SearchBy searchby) {
pending_search_id_ = next_pending_search_id_;
pending_search_ = text;
pending_searchby_ = searchby;
next_pending_search_id_++;
if (text.isEmpty()) {
search_delay_->stop();
return pending_search_id_;
}
search_delay_->start();
return pending_search_id_;
}
void TidalService::StartSearch() {
if (username_.isEmpty() || password_.isEmpty()) {
emit SearchError(pending_search_id_, "Missing username and/or password.");
next_pending_search_id_ = 1;
ShowConfig();
return;
}
TidalSearchContext *search_ctx = CreateSearch(pending_search_id_, pending_search_);
if (authenticated()) SendSearch(search_ctx);
else Login(search_ctx, username_, password_);
}
TidalSearchContext *TidalService::CreateSearch(const int search_id, const QString text) {
TidalSearchContext *search_ctx = new TidalSearchContext;
search_ctx->id = search_id;
search_ctx->text = text;
search_ctx->album_requests = 0;
search_ctx->song_requests = 0;
search_ctx->requests_album_.clear();
search_ctx->requests_song_.clear();
search_ctx->login_attempts = 0;
requests_search_.insert(search_id, search_ctx);
return search_ctx;
}
void TidalService::SendSearch(TidalSearchContext *search_ctx) {
QList<Param> parameters;
parameters << Param("query", search_ctx->text);
QString searchparam;
switch (pending_searchby_) {
case TidalSettingsPage::SearchBy_Songs:
searchparam = "search/tracks";
parameters << Param("limit", QString::number(kSearchTracksLimit));
break;
case TidalSettingsPage::SearchBy_Albums:
default:
searchparam = "search/albums";
parameters << Param("limit", QString::number(kSearchAlbumsLimit));
break;
}
QNetworkReply *reply = CreateRequest(searchparam, parameters);
NewClosure(reply, SIGNAL(finished()), this, SLOT(SearchFinished(QNetworkReply*, int)), reply, search_ctx->id);
}
void TidalService::SearchFinished(QNetworkReply *reply, int id) {
reply->deleteLater();
if (!requests_search_.contains(id)) return;
TidalSearchContext *search_ctx = requests_search_.value(id);
QJsonArray json_items = ExtractItems(search_ctx, reply);
if (json_items.isEmpty()) {
CheckFinish(search_ctx);
return;
}
//qLog(Debug) << json_items;
QVector<QString> albums;
for (const QJsonValue &value : json_items) {
int album_id(0);
QString album("");
if (!value["type"].isUndefined()) {
if (value["id"].isUndefined() || value["title"].isUndefined()) {
qLog(Error) << "Tidal: Invalid Json reply, missing ID or title.";
qLog(Debug) << value;
continue;
}
album_id = value["id"].toInt();
album = value["title"].toString();
}
else if (!value["album"].isUndefined()) {
QJsonValue json_album = value["album"];
if (json_album["id"].isUndefined() || json_album["title"].isUndefined()) {
qLog(Error) << "Tidal: Invalid Json reply, missing ID or title.";
qLog(Debug) << value;
continue;
}
album_id = json_album["id"].toInt();
album = json_album["title"].toString();
}
else {
qLog(Error) << "Tidal: Invalid Json reply, missing type or album.";
qLog(Debug) << value;
continue;
}
if (search_ctx->requests_album_.contains(album_id)) continue;
if (value["artist"].isUndefined() || value["title"].isUndefined()) {
qLog(Error) << "Tidal: Invalid Json reply, missing artist or title.";
qLog(Debug) << value;
continue;
}
QJsonValue json_artist = value["artist"];
QString artist(json_artist["name"].toString());
QString quality(value["audioQuality"].toString());
//qLog(Debug) << "Tidal:" << artist << album << quality;
QString artist_album(QString("%1-%2").arg(artist).arg(album));
if (albums.contains(artist_album)) {
qLog(Debug) << "Tidal: Skipping duplicate album" << artist << album << quality;
continue;
}
albums.insert(0, artist_album);
search_ctx->requests_album_.insert(album_id, album_id);
GetAlbum(search_ctx, album_id);
search_ctx->album_requests++;
if (search_ctx->album_requests >= kSearchAlbumsLimit) break;
}
CheckFinish(search_ctx);
}
void TidalService::GetAlbum(TidalSearchContext *search_ctx, const int album_id) {
QList<Param> parameters;
parameters << Param("token", session_id_)
<< Param("soundQuality", quality_);
QNetworkReply *reply = CreateRequest(QString("albums/%1/tracks").arg(album_id), parameters);
NewClosure(reply, SIGNAL(finished()), this, SLOT(GetAlbumFinished(QNetworkReply*, int, int)), reply, search_ctx->id, album_id);
}
void TidalService::GetAlbumFinished(QNetworkReply *reply, int search_id, int album_id) {
reply->deleteLater();
if (!requests_search_.contains(search_id)) return;
TidalSearchContext *search_ctx = requests_search_.value(search_id);
if (!search_ctx->requests_album_.contains(album_id)) return;
search_ctx->album_requests--;
QJsonArray json_items = ExtractItems(search_ctx, reply);
if (json_items.isEmpty()) {
CheckFinish(search_ctx);
return;
}
bool compilation = false;
bool multidisc = false;
Song *first_song(nullptr);
QList<Song *> songs;
for (const QJsonValue &value : json_items) {
Song *song = ParseSong(search_ctx, album_id, value);
if (!song) continue;
songs << song;
if (song->disc() >= 2) multidisc = true;
if (song->is_compilation() || (first_song && song->artist() != first_song->artist())) compilation = true;
if (!first_song) first_song = song;
}
if (compilation || multidisc) {
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);
}
}
}
CheckFinish(search_ctx);
}
Song *TidalService::ParseSong(TidalSearchContext *search_ctx, const int album_id, const QJsonValue &value) {
Song song;
bool allow_streaming = value["allowStreaming"].toBool();
bool stream_ready = value["streamReady"].toBool();
if (!allow_streaming || !stream_ready) {
return nullptr;
}
int id = value["id"].toInt();
QJsonValue json_artist = value["artist"];
QJsonArray json_artists = value["artists"].toArray();
QJsonValue json_album = value["album"];
QString title = value["title"].toString();
QString artist = json_artist["name"].toString();
QString album = json_album["title"].toString();
QString cover = json_album["cover"].toString();
QString url = value["url"].toString();
int track = value["trackNumber"].toInt();
int disc = value["volumeNumber"].toInt();
//qLog(Debug) << "id" << id << "track" << track << "disc" << disc << "title" << title << "album" << album << "artist" << artist << cover << allow_streaming << url;
song.set_album_id(album_id);
song.set_artist(artist);
song.set_album(album);
song.set_title(title);
song.set_track(track);
song.set_disc(disc);
song.set_bitrate(0);
song.set_samplerate(0);
song.set_bitdepth(0);
QVariant q_duration = value["duration"];
if (q_duration.isValid()) {
quint64 duration = q_duration.toULongLong() * kNsecPerSec;
song.set_length_nanosec(duration);
}
// Check and see if there is more than 1 artist on the song.
//int i = 0;
//for (const QJsonValue &artist : json_artists) {
//i++;
//qLog(Debug) << artist << i;
//}
//if (i > 1) song.set_compilation_detected(true);
cover = cover.replace("-", "/");
QUrl cover_url (QString("%1/images/%2/750x750.jpg").arg(kResourcesUrl).arg(cover));
song.set_art_automatic(cover_url.toEncoded());
if (search_ctx->requests_song_.contains(id)) return search_ctx->requests_song_.value(id);
Song *song_new = new Song(song);
search_ctx->requests_song_.insert(id, song_new);
search_ctx->song_requests++;
GetStreamURL(search_ctx, album_id, id);
return song_new;
}
void TidalService::GetStreamURL(TidalSearchContext *search_ctx, const int album_id, const int song_id) {
QList<Param> parameters;
parameters << Param("token", session_id_)
<< Param("soundQuality", quality_);
QNetworkReply *reply = CreateRequest(QString("tracks/%1/streamUrl").arg(song_id), parameters);
NewClosure(reply, SIGNAL(finished()), this, SLOT(GetStreamURLFinished(QNetworkReply*, int, int)), reply, search_ctx->id, song_id);
}
void TidalService::GetStreamURLFinished(QNetworkReply *reply, const int search_id, const int song_id) {
reply->deleteLater();
if (!requests_search_.contains(search_id)) return;
TidalSearchContext *search_ctx = requests_search_.value(search_id);
if (!search_ctx->requests_song_.contains(song_id)) {
CheckFinish(search_ctx);
return;
}
Song *song = search_ctx->requests_song_.value(song_id);
search_ctx->song_requests--;
QJsonObject json_obj = ExtractJsonObj(search_ctx, reply);
if (json_obj.isEmpty()) {
delete search_ctx->requests_song_.take(song_id);
CheckFinish(search_ctx);
return;
}
if (json_obj["url"].isUndefined() || json_obj["codec"].isUndefined()) {
delete search_ctx->requests_song_.take(song_id);
CheckFinish(search_ctx);
return;
}
song->set_url(QUrl(json_obj["url"].toString()));
song->set_valid(true);
QString codec = json_obj["codec"].toString();
if (codec == "AAC") song->set_filetype(Song::Type_MP4);
else qLog(Debug) << "Tidal codec" << codec;
//qLog(Debug) << song->title() << song->artist() << song->album() << song->url() << song->filetype();
search_ctx->songs << *song;
delete search_ctx->requests_song_.take(song_id);
CheckFinish(search_ctx);
}
void TidalService::CheckFinish(TidalSearchContext *search_ctx) {
if (!search_ctx->login_sent && search_ctx->album_requests <= 0 && search_ctx->song_requests <= 0) {
if (search_ctx->songs.isEmpty()) emit SearchError(search_ctx->id, search_ctx->error);
else emit SearchResults(search_ctx->id, search_ctx->songs);
delete requests_search_.take(search_ctx->id);
}
}
void TidalService::Error(TidalSearchContext *search_ctx, QString error, QString debug) {
qLog(Error) << "Tidal:" << error;
if (!debug.isEmpty()) qLog(Debug) << debug;
if (search_ctx) {
search_ctx->error = error;
CheckFinish(search_ctx);
}
}

134
src/tidal/tidalservice.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 TIDALSERVICE_H
#define TIDALSERVICE_H
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QHash>
#include <QString>
#include <QNetworkReply>
#include <QTimer>
#include <QDateTime>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include "core/song.h"
#include "internet/internetmodel.h"
#include "internet/internetservice.h"
#include "settings/tidalsettingspage.h"
class NetworkAccessManager;
struct TidalSearchContext {
int id;
QString text;
QHash<int, int> requests_album_;
QHash<int, Song *> requests_song_;
int album_requests;
int song_requests;
SongList songs;
QString error;
bool login_sent;
int login_attempts;
};
Q_DECLARE_METATYPE(TidalSearchContext);
class TidalService : public InternetService {
Q_OBJECT
public:
TidalService(Application *app, InternetModel *parent);
~TidalService();
static const char *kServiceName;
void ReloadSettings();
void Login(const QString &username, const QString &password);
void Logout();
int Search(const QString &query, TidalSettingsPage::SearchBy searchby);
const bool login_sent() { return login_sent_; }
const bool authenticated() { return (!session_id_.isEmpty() && !country_code_.isEmpty()); }
signals:
void LoginSuccess();
void LoginFailure(QString failure_reason);
void SearchResults(int id, SongList songs);
void SearchError(int id, QString message);
public slots:
void ShowConfig();
private slots:
void HandleAuthReply(QNetworkReply *reply, int id);
void StartSearch();
void SearchFinished(QNetworkReply *reply, int id);
void GetAlbumFinished(QNetworkReply *reply, int search_id, int album_id);
void GetStreamURLFinished(QNetworkReply *reply, const int search_id, const int song_id);
private:
void Login(TidalSearchContext *search_ctx, const QString &username, const QString &password);
void LoadSessionID();
QNetworkReply *CreateRequest(const QString &ressource_name, const QList<QPair<QString, QString>> &params);
QJsonObject ExtractJsonObj(TidalSearchContext *search_ctx, QNetworkReply *reply);
QJsonArray ExtractItems(TidalSearchContext *search_ctx, QNetworkReply *reply);
TidalSearchContext *CreateSearch(const int search_id, const QString text);
void SendSearch(TidalSearchContext *search_ctx);
void GetAlbum(TidalSearchContext *search_ctx, const int album_id);
Song *ParseSong(TidalSearchContext *search_ctx, const int album_id, const QJsonValue &value);
Song ExtractSong(TidalSearchContext *search_ctx, const QJsonValue &value);
void GetStreamURL(TidalSearchContext *search_ctx, const int album_id, const int song_id);
void CheckFinish(TidalSearchContext *search_ctx);
void Error(TidalSearchContext *search_ctx, QString error, QString debug = "");
static const char *kApiUrl;
static const char *kAuthUrl;
static const char *kResourcesUrl;
static const char *kApiToken;
NetworkAccessManager *network_;
QTimer *search_delay_;
int pending_search_id_;
int next_pending_search_id_;
int search_requests_;
bool login_sent_;
static const int kSearchAlbumsLimit;
static const int kSearchTracksLimit;
static const int kSearchDelayMsec;
QString username_;
QString password_;
QString quality_;
QString session_id_;
quint64 user_id_;
QString country_code_;
QString pending_search_;
TidalSettingsPage::SearchBy pending_searchby_;
QHash<int, TidalSearchContext*> requests_search_;
};
#endif // TIDALSERVICE_H

View File

@ -15,7 +15,7 @@
*
* 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"
@ -222,14 +222,14 @@ Transcoder::Transcoder(QObject *parent, const QString &settings_postfix)
QList<TranscoderPreset> Transcoder::GetAllPresets() {
QList<TranscoderPreset> ret;
ret << PresetForFileType(Song::Type_Flac);
ret << PresetForFileType(Song::Type_Mp4);
ret << PresetForFileType(Song::Type_Mpeg);
ret << PresetForFileType(Song::Type_FLAC);
ret << PresetForFileType(Song::Type_MP4);
ret << PresetForFileType(Song::Type_MPEG);
ret << PresetForFileType(Song::Type_OggVorbis);
ret << PresetForFileType(Song::Type_OggFlac);
ret << PresetForFileType(Song::Type_OggSpeex);
ret << PresetForFileType(Song::Type_Asf);
ret << PresetForFileType(Song::Type_Wav);
ret << PresetForFileType(Song::Type_ASF);
ret << PresetForFileType(Song::Type_WAV);
ret << PresetForFileType(Song::Type_OggOpus);
return ret;
@ -238,11 +238,11 @@ QList<TranscoderPreset> Transcoder::GetAllPresets() {
TranscoderPreset Transcoder::PresetForFileType(Song::FileType type) {
switch (type) {
case Song::Type_Flac:
case Song::Type_FLAC:
return TranscoderPreset(type, tr("FLAC"), "flac", "audio/x-flac");
case Song::Type_Mp4:
case Song::Type_MP4:
return TranscoderPreset(type, tr("M4A AAC"), "mp4", "audio/mpeg, mpegversion=(int)4", "audio/mp4");
case Song::Type_Mpeg:
case Song::Type_MPEG:
return TranscoderPreset(type, tr("MP3"), "mp3", "audio/mpeg, mpegversion=(int)1, layer=(int)3");
case Song::Type_OggVorbis:
return TranscoderPreset(type, tr("Ogg Vorbis"), "ogg", "audio/x-vorbis", "application/ogg");
@ -252,9 +252,9 @@ TranscoderPreset Transcoder::PresetForFileType(Song::FileType type) {
return TranscoderPreset(type, tr("Ogg Speex"), "spx", "audio/x-speex", "application/ogg");
case Song::Type_OggOpus:
return TranscoderPreset(type, tr("Ogg Opus"), "opus", "audio/x-opus", "application/ogg");
case Song::Type_Asf:
case Song::Type_ASF:
return TranscoderPreset(type, tr("Windows Media audio"), "wma", "audio/x-wma", "video/x-ms-asf");
case Song::Type_Wav:
case Song::Type_WAV:
return TranscoderPreset(type, tr("Wav"), "wav", QString(), "audio/x-wav");
default:
qLog(Warning) << "Unsupported format in PresetForFileType:" << type;
@ -268,9 +268,9 @@ Song::FileType Transcoder::PickBestFormat(QList<Song::FileType> supported) {
if (supported.isEmpty()) return Song::Type_Unknown;
QList<Song::FileType> best_formats;
best_formats << Song::Type_Mpeg;
best_formats << Song::Type_MPEG;
best_formats << Song::Type_OggVorbis;
best_formats << Song::Type_Asf;
best_formats << Song::Type_ASF;
for (Song::FileType type : best_formats) {
if (supported.isEmpty() || supported.contains(type)) return type;

View File

@ -15,7 +15,7 @@
*
* 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"
@ -44,14 +44,14 @@ TranscoderOptionsDialog::TranscoderOptionsDialog(Song::FileType type, QWidget *p
ui_->setupUi(this);
switch (type) {
case Song::Type_Flac:
case Song::Type_FLAC:
case Song::Type_OggFlac: options_ = new TranscoderOptionsFlac(this); break;
case Song::Type_Mp4: options_ = new TranscoderOptionsAAC(this); break;
case Song::Type_Mpeg: options_ = new TranscoderOptionsMP3(this); break;
case Song::Type_MP4: options_ = new TranscoderOptionsAAC(this); break;
case Song::Type_MPEG: options_ = new TranscoderOptionsMP3(this); break;
case Song::Type_OggVorbis: options_ = new TranscoderOptionsVorbis(this); break;
case Song::Type_OggOpus: options_ = new TranscoderOptionsOpus(this); break;
case Song::Type_OggSpeex: options_ = new TranscoderOptionsSpeex(this); break;
case Song::Type_Asf: options_ = new TranscoderOptionsWma(this); break;
case Song::Type_ASF: options_ = new TranscoderOptionsWma(this); break;
default:
break;
}

View File

@ -15,7 +15,7 @@
*
* 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"
@ -59,7 +59,6 @@ FileViewList::FileViewList(QWidget *parent)
void FileViewList::contextMenuEvent(QContextMenuEvent *e) {
//qLog(Debug) << __PRETTY_FUNCTION__;
menu_selection_ = selectionModel()->selection();
menu_->popup(e->globalPos());
@ -69,7 +68,6 @@ void FileViewList::contextMenuEvent(QContextMenuEvent *e) {
QList<QUrl> FileViewList::UrlListFromSelection() const {
//qLog(Debug) << __PRETTY_FUNCTION__;
QList<QUrl> urls;
for (const QModelIndex& index : menu_selection_.indexes()) {
if (index.column() == 0)
@ -81,7 +79,6 @@ QList<QUrl> FileViewList::UrlListFromSelection() const {
MimeData *FileViewList::MimeDataFromSelection() const {
//qLog(Debug) << __PRETTY_FUNCTION__;
MimeData *data = new MimeData;
data->setUrls(UrlListFromSelection());
@ -101,7 +98,6 @@ MimeData *FileViewList::MimeDataFromSelection() const {
QStringList FileViewList::FilenamesFromSelection() const {
//qLog(Debug) << __PRETTY_FUNCTION__;
QStringList filenames;
for (const QModelIndex& index : menu_selection_.indexes()) {
if (index.column() == 0)
@ -112,14 +108,12 @@ QStringList FileViewList::FilenamesFromSelection() const {
}
void FileViewList::LoadSlot() {
//qLog(Debug) << __PRETTY_FUNCTION__;
MimeData *data = MimeDataFromSelection();
data->clear_first_ = true;
emit AddToPlaylist(data);
}
void FileViewList::AddToPlaylistSlot() {
//qLog(Debug) << __PRETTY_FUNCTION__;
emit AddToPlaylist(MimeDataFromSelection());
}
@ -143,18 +137,15 @@ void FileViewList::CopyToDeviceSlot() {
}
void FileViewList::DeleteSlot() {
//qLog(Debug) << __PRETTY_FUNCTION__;
emit Delete(FilenamesFromSelection());
}
void FileViewList::EditTagsSlot() {
//qLog(Debug) << __PRETTY_FUNCTION__;
emit EditTags(UrlListFromSelection());
}
void FileViewList::mousePressEvent(QMouseEvent *e) {
//qLog(Debug) << __PRETTY_FUNCTION__;
switch (e->button()) {
case Qt::XButton1:
emit Back();
@ -183,6 +174,5 @@ void FileViewList::mousePressEvent(QMouseEvent *e) {
}
void FileViewList::ShowInBrowser() {
//qLog(Debug) << __PRETTY_FUNCTION__;
Utilities::OpenInFileBrowser(UrlListFromSelection());
}

View File

@ -0,0 +1,148 @@
/*
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 "loginstatewidget.h"
#include "ui_loginstatewidget.h"
#include "core/iconloader.h"
#include <QWidget>
#include <QTimer>
#include <QDate>
#include <QString>
#include <QLineEdit>
#include <QtEvents>
LoginStateWidget::LoginStateWidget(QWidget *parent)
: QWidget(parent), ui_(new Ui_LoginStateWidget), state_(LoggedOut) {
ui_->setupUi(this);
ui_->signed_in->hide();
ui_->expires->hide();
ui_->account_type->hide();
ui_->busy->hide();
ui_->sign_out->setIcon(IconLoader::Load("list-remove"));
ui_->signed_in_icon_label->setPixmap(IconLoader::Load("dialog-ok-apply").pixmap(22));
ui_->expires_icon_label->setPixmap(IconLoader::Load("user-away").pixmap(22));
ui_->account_type_icon_label->setPixmap(IconLoader::Load("dialog-warning").pixmap(22));
QFont bold_font(font());
bold_font.setBold(true);
ui_->signed_out_label->setFont(bold_font);
connect(ui_->sign_out, SIGNAL(clicked()), SLOT(Logout()));
}
LoginStateWidget::~LoginStateWidget() { delete ui_; }
void LoginStateWidget::Logout() {
SetLoggedIn(LoggedOut);
emit LogoutClicked();
}
void LoginStateWidget::SetAccountTypeText(const QString &text) {
ui_->account_type_label->setText(text);
}
void LoginStateWidget::SetAccountTypeVisible(bool visible) {
ui_->account_type->setVisible(visible);
}
void LoginStateWidget::SetLoggedIn(State state, const QString &account_name) {
State last_state = state_;
state_ = state;
ui_->signed_in->setVisible(state == LoggedIn);
ui_->signed_out->setVisible(state != LoggedIn);
ui_->busy->setVisible(state == LoginInProgress);
if (account_name.isEmpty()) ui_->signed_in_label->setText("<b>" + tr("You are signed in.") + "</b>");
else ui_->signed_in_label->setText(tr("You are signed in as %1.").arg("<b>" + account_name + "</b>"));
for (QWidget *widget : credential_groups_) {
widget->setVisible(state != LoggedIn);
widget->setEnabled(state != LoginInProgress);
}
if (state == LoggedOut && last_state == LoginInProgress) {
// A login just failed - give focus back to the last crediental field (usually password).
// We have to do this after control gets back to the
// event loop because the user might have just closed a dialog and our widget might not be active yet.
QTimer::singleShot(0, this, SLOT(FocusLastCredentialField()));
}
}
void LoginStateWidget::FocusLastCredentialField() {
if (!credential_fields_.isEmpty()) {
QObject *object = credential_fields_.last();
QWidget *widget = qobject_cast<QWidget*>(object);
QLineEdit *line_edit = qobject_cast<QLineEdit*>(object);
if (widget) {
widget->setFocus();
}
if (line_edit) {
line_edit->selectAll();
}
}
}
void LoginStateWidget::HideLoggedInState() {
ui_->signed_in->hide();
ui_->signed_out->hide();
}
void LoginStateWidget::AddCredentialField(QWidget *widget) {
widget->installEventFilter(this);
credential_fields_ << widget;
}
void LoginStateWidget::AddCredentialGroup(QWidget *widget) {
credential_groups_ << widget;
}
bool LoginStateWidget::eventFilter(QObject *object, QEvent *event) {
if (!credential_fields_.contains(object))
return QWidget::eventFilter(object, event);
if (event->type() == QEvent::KeyPress) {
QKeyEvent *key_event = static_cast<QKeyEvent*>(event);
if (key_event->key() == Qt::Key_Enter || key_event->key() == Qt::Key_Return) {
emit LoginClicked();
return true;
}
}
return QWidget::eventFilter(object, event);
}
void LoginStateWidget::SetExpires(const QDate &expires) {
ui_->expires->setVisible(expires.isValid());
if (expires.isValid()) {
const QString expires_text = expires.toString(Qt::SystemLocaleLongDate);
ui_->expires_label->setText(tr("Expires on %1").arg("<b>" + expires_text + "</b>"));
}
}

View File

@ -0,0 +1,80 @@
/*
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 LOGINSTATEWIDGET_H
#define LOGINSTATEWIDGET_H
#include <QWidget>
#include <QObject>
#include <QList>
#include <QString>
#include <QDate>
#include <QtEvents>
class Ui_LoginStateWidget;
class LoginStateWidget : public QWidget {
Q_OBJECT
public:
LoginStateWidget(QWidget *parent = nullptr);
~LoginStateWidget();
enum State { LoggedIn, LoginInProgress, LoggedOut };
// Installs an event handler on the field so that pressing enter will emit
// LoginClicked() instead of doing the default action (closing the dialog).
void AddCredentialField(QWidget *widget);
// This widget (usually a QGroupBox) will be hidden when SetLoggedIn(true) is called.
void AddCredentialGroup(QWidget *widget);
// QObject
bool eventFilter(QObject *object, QEvent *event);
public slots:
// Changes the "You are logged in/out" label, shows/hides any QGroupBoxes added with AddCredentialGroup.
void SetLoggedIn(State state, const QString &account_name = QString::null);
// Hides the "You are logged in/out" label completely.
void HideLoggedInState();
void SetAccountTypeText(const QString &text);
void SetAccountTypeVisible(bool visible);
void SetExpires(const QDate &expires);
signals:
void LogoutClicked();
void LoginClicked();
private slots:
void Logout();
void FocusLastCredentialField();
private:
Ui_LoginStateWidget *ui_;
State state_;
QList<QObject*> credential_fields_;
QList<QWidget*> credential_groups_;
};
#endif // LOGINSTATEWIDGET_H

View File

@ -0,0 +1,182 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LoginStateWidget</class>
<widget class="QWidget" name="LoginStateWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>526</width>
<height>187</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="signed_out" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_5">
<property name="minimumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="signed_out_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>You are not signed in.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="signed_in" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="signed_in_icon_label">
<property name="minimumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="signed_in_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="sign_out">
<property name="text">
<string>Sign out</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="expires" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="expires_icon_label">
<property name="minimumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="expires_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="account_type" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="account_type_icon_label">
<property name="minimumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="account_type_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="BusyIndicator" name="busy" native="true">
<property name="text" stdset="0">
<string>Signing in...</string>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>BusyIndicator</class>
<extends>QWidget</extends>
<header>widgets/busyindicator.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<connections/>
</ui>

View File

@ -14,7 +14,7 @@
*
* 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"
@ -84,7 +84,6 @@ StatusView::StatusView(CollectionViewContainer *collectionviewcontainer, QWidget
label_playing_text_(nullptr),
album_cover_choice_controller_(new AlbumCoverChoiceController(this)),
show_hide_animation_(new QTimeLine(500, this)),
fade_animation_(new QTimeLine(1000, this)),
image_blank_(""),
image_nosong_(":/pictures/strawberry.png"),
@ -92,8 +91,6 @@ StatusView::StatusView(CollectionViewContainer *collectionviewcontainer, QWidget
menu_(new QMenu(this))
{
//qLog(Debug) << __PRETTY_FUNCTION__;
collectionview_ = collectionviewcontainer->view();
connect(collectionview_, SIGNAL(TotalSongCountUpdated_()), this, SLOT(UpdateNoSong()));
connect(collectionview_, SIGNAL(TotalArtistCountUpdated_()), this, SLOT(UpdateNoSong()));
@ -125,8 +122,6 @@ StatusView::~StatusView() {
void StatusView::AddActions() {
//qLog(Debug) << __PRETTY_FUNCTION__;
QList<QAction*> actions = album_cover_choice_controller_->GetAllActions();
// Here we add the search automatically action, too!
@ -147,8 +142,6 @@ void StatusView::AddActions() {
void StatusView::CreateWidget() {
//qLog(Debug) << __PRETTY_FUNCTION__;
setLayout(layout_);
layout_->setSizeConstraint(QLayout::SetMinAndMaxSize);
@ -174,8 +167,6 @@ void StatusView::CreateWidget() {
void StatusView::SetApplication(Application *app) {
//qLog(Debug) << __PRETTY_FUNCTION__;
app_ = app;
album_cover_choice_controller_->SetApplication(app_);
@ -185,8 +176,6 @@ void StatusView::SetApplication(Application *app) {
void StatusView::NoSongWidget() {
//qLog(Debug) << __PRETTY_FUNCTION__;
if (widgetstate_ == Playing) {
container_layout_->removeWidget(widget_playing_);
widget_playing_->setVisible(false);
@ -221,8 +210,6 @@ void StatusView::NoSongWidget() {
void StatusView::SongWidget() {
//qLog(Debug) << __PRETTY_FUNCTION__;
if (widgetstate_ == Stopped) {
container_layout_->removeWidget(widget_stopped_);
widget_stopped_->setVisible(false);
@ -275,8 +262,6 @@ void StatusView::SongWidget() {
void StatusView::SwitchWidgets(WidgetState state) {
//qLog(Debug) << __PRETTY_FUNCTION__;
if (widgetstate_ == None) NoSongWidget();
if ((state == Stopped) && (widgetstate_ != Stopped)) {
@ -291,8 +276,6 @@ void StatusView::SwitchWidgets(WidgetState state) {
void StatusView::UpdateSong() {
//qLog(Debug) << __PRETTY_FUNCTION__;
SwitchWidgets(Playing);
const QueryOptions opt;
@ -342,8 +325,6 @@ void StatusView::UpdateSong() {
void StatusView::NoSong() {
//qLog(Debug) << __PRETTY_FUNCTION__;
QString html;
QImage image_logo(":/pictures/strawberry.png");
QImage image_logo_scaled = image_logo.scaled(300, 300, Qt::KeepAspectRatio);
@ -377,8 +358,6 @@ void StatusView::NoSong() {
void StatusView::SongChanged(const Song &song) {
//qLog(Debug) << __PRETTY_FUNCTION__;
stopped_ = false;
metadata_ = song;
@ -390,8 +369,6 @@ void StatusView::SongChanged(const Song &song) {
void StatusView::SongFinished() {
//qLog(Debug) << __PRETTY_FUNCTION__;
stopped_ = true;
SetImage(image_blank_);
@ -399,8 +376,6 @@ void StatusView::SongFinished() {
bool StatusView::eventFilter(QObject *object, QEvent *event) {
//qLog(Debug) << __PRETTY_FUNCTION__;
switch(event->type()) {
case QEvent::Paint:{
handlePaintEvent(object, event);
@ -416,8 +391,6 @@ bool StatusView::eventFilter(QObject *object, QEvent *event) {
void StatusView::handlePaintEvent(QObject *object, QEvent *event) {
//qLog(Debug) << __PRETTY_FUNCTION__ << object->objectName();
if (object == label_playing_album_) {
paintEvent_album(event);
}
@ -428,8 +401,6 @@ void StatusView::handlePaintEvent(QObject *object, QEvent *event) {
void StatusView::paintEvent_album(QEvent *event) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QPainter p(label_playing_album_);
DrawImage(&p);
@ -443,8 +414,6 @@ void StatusView::paintEvent_album(QEvent *event) {
void StatusView::DrawImage(QPainter *p) {
//qLog(Debug) << __PRETTY_FUNCTION__;
p->drawPixmap(0, 0, 300, 300, pixmap_current_);
if ((downloading_covers_) && (spinner_animation_ != nullptr)) {
p->drawPixmap(50, 50, 16, 16, spinner_animation_->currentPixmap());
@ -454,8 +423,6 @@ void StatusView::DrawImage(QPainter *p) {
void StatusView::FadePreviousTrack(qreal value) {
//qLog(Debug) << __PRETTY_FUNCTION__;
pixmap_previous_opacity_ = value;
if (qFuzzyCompare(pixmap_previous_opacity_, qreal(0.0))) {
pixmap_previous_ = QPixmap();
@ -477,31 +444,22 @@ void StatusView::contextMenuEvent(QContextMenuEvent *e) {
}
void StatusView::mouseReleaseEvent(QMouseEvent *) {
//qLog(Debug) << __PRETTY_FUNCTION__;
}
void StatusView::dragEnterEvent(QDragEnterEvent *e) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QWidget::dragEnterEvent(e);
}
void StatusView::dropEvent(QDropEvent *e) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QWidget::dropEvent(e);
}
void StatusView::ScaleCover() {
//qLog(Debug) << __PRETTY_FUNCTION__;
pixmap_current_ = QPixmap::fromImage(AlbumCoverLoader::ScaleAndPad(cover_loader_options_, original_));
update();
@ -509,8 +467,6 @@ void StatusView::ScaleCover() {
void StatusView::AlbumArtLoaded(const Song &metadata, const QString&, const QImage &image) {
//qLog(Debug) << __PRETTY_FUNCTION__;
SwitchWidgets(Playing);
label_playing_album_->clear();
@ -527,8 +483,6 @@ void StatusView::AlbumArtLoaded(const Song &metadata, const QString&, const QIma
void StatusView::SetImage(const QImage &image) {
//qLog(Debug) << __PRETTY_FUNCTION__;
// Cache the current pixmap so we can fade between them
pixmap_previous_ = QPixmap(size());
pixmap_previous_.fill(palette().background().color());
@ -543,7 +497,7 @@ void StatusView::SetImage(const QImage &image) {
ScaleCover();
// Were we waiting for this cover to load before we started fading?
if (!pixmap_previous_.isNull() && fade_animation_ != nullptr) {
if (!pixmap_previous_.isNull() && fade_animation_) {
fade_animation_->start();
}
@ -551,8 +505,6 @@ void StatusView::SetImage(const QImage &image) {
bool StatusView::GetCoverAutomatically() {
//qLog(Debug) << __PRETTY_FUNCTION__;
SwitchWidgets(Playing);
// Search for cover automatically?
@ -581,8 +533,6 @@ bool StatusView::GetCoverAutomatically() {
void StatusView::AutomaticCoverSearchDone() {
//qLog(Debug) << __PRETTY_FUNCTION__;
downloading_covers_ = false;
spinner_animation_.reset();
update();
@ -591,8 +541,6 @@ void StatusView::AutomaticCoverSearchDone() {
void StatusView::UpdateNoSong() {
//qLog(Debug) << __PRETTY_FUNCTION__;
if (widgetstate_ == Playing) return;
NoSong();

View File

@ -14,7 +14,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*
*/
#ifndef STATUSVIEW_H
@ -131,7 +131,6 @@ private:
int small_ideal_height_;
int total_height_;
bool fit_width_;
QTimeLine *show_hide_animation_;
QTimeLine *fade_animation_;
QImage image_blank_;
QImage image_nosong_;
@ -175,3 +174,4 @@ protected:
};
#endif // STATUSVIEW_H