diff --git a/data/data.qrc b/data/data.qrc
index 386868118..e5780faa4 100644
--- a/data/data.qrc
+++ b/data/data.qrc
@@ -1,6 +1,7 @@
schema/schema.sql
+ schema/schema-1.sql
schema/device-schema.sql
style/mainwindow.css
style/statusview.css
@@ -113,6 +114,7 @@
icons/128x128/xine.png
icons/128x128/zoom-in.png
icons/128x128/zoom-out.png
+ icons/128x128/tidal.png
icons/64x64/albums.png
icons/64x64/alsa.png
icons/64x64/application-exit.png
@@ -201,6 +203,7 @@
icons/64x64/xine.png
icons/64x64/zoom-in.png
icons/64x64/zoom-out.png
+ icons/64x64/tidal.png
icons/48x48/albums.png
icons/48x48/alsa.png
icons/48x48/application-exit.png
@@ -292,6 +295,7 @@
icons/48x48/xine.png
icons/48x48/zoom-in.png
icons/48x48/zoom-out.png
+ icons/48x48/tidal.png
icons/32x32/albums.png
icons/32x32/alsa.png
icons/32x32/application-exit.png
@@ -384,6 +388,7 @@
icons/32x32/xine.png
icons/32x32/zoom-in.png
icons/32x32/zoom-out.png
+ icons/32x32/tidal.png
icons/22x22/albums.png
icons/22x22/alsa.png
icons/22x22/application-exit.png
@@ -476,5 +481,6 @@
icons/22x22/xine.png
icons/22x22/zoom-in.png
icons/22x22/zoom-out.png
+ icons/22x22/tidal.png
diff --git a/data/icons/128x128/tidal.png b/data/icons/128x128/tidal.png
new file mode 100644
index 000000000..5b7e2f11e
Binary files /dev/null and b/data/icons/128x128/tidal.png differ
diff --git a/data/icons/22x22/tidal.png b/data/icons/22x22/tidal.png
new file mode 100644
index 000000000..496105541
Binary files /dev/null and b/data/icons/22x22/tidal.png differ
diff --git a/data/icons/32x32/tidal.png b/data/icons/32x32/tidal.png
new file mode 100644
index 000000000..c206ac0a3
Binary files /dev/null and b/data/icons/32x32/tidal.png differ
diff --git a/data/icons/48x48/tidal.png b/data/icons/48x48/tidal.png
new file mode 100644
index 000000000..fb0143432
Binary files /dev/null and b/data/icons/48x48/tidal.png differ
diff --git a/data/icons/64x64/tidal.png b/data/icons/64x64/tidal.png
new file mode 100644
index 000000000..cbd33703d
Binary files /dev/null and b/data/icons/64x64/tidal.png differ
diff --git a/data/icons/full/tidal.png b/data/icons/full/tidal.png
new file mode 100644
index 000000000..316831c69
Binary files /dev/null and b/data/icons/full/tidal.png differ
diff --git a/data/schema/schema-1.sql b/data/schema/schema-1.sql
new file mode 100644
index 000000000..1873b21ac
--- /dev/null
+++ b/data/schema/schema-1.sql
@@ -0,0 +1,3 @@
+ALTER TABLE playlist_items ADD COLUMN internet_service TEXT;
+
+UPDATE schema_version SET version=1;
diff --git a/data/schema/schema.sql b/data/schema/schema.sql
index fd588c91f..5e13c17b4 100644
--- a/data/schema/schema.sql
+++ b/data/schema/schema.sql
@@ -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)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index c1f01a79a..e5efec282 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -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)
diff --git a/src/collection/collectionbackend.cpp b/src/collection/collectionbackend.cpp
index fffa49f0a..f22afd3d8 100644
--- a/src/collection/collectionbackend.cpp
+++ b/src/collection/collectionbackend.cpp
@@ -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
diff --git a/src/core/application.cpp b/src/core/application.cpp
index 529283a6c..801bf8774 100644
--- a/src/core/application.cpp
+++ b/src/core/application.cpp
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#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 tag_reader_client_;
@@ -113,6 +118,8 @@ class ApplicationImpl {
Lazy cover_providers_;
Lazy album_cover_loader_;
Lazy current_art_loader_;
+ Lazy internet_model_;
+ Lazy 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();
+}
diff --git a/src/core/application.h b/src/core/application.h
index 322b8dac2..7ad8cc684 100644
--- a/src/core/application.h
+++ b/src/core/application.h
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#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);
diff --git a/src/core/database.cpp b/src/core/database.cpp
index d50ba1921..ae7c1c640 100644
--- a/src/core/database.cpp
+++ b/src/core/database.cpp
@@ -52,7 +52,7 @@
#include "scopedtransaction.h"
const char *Database::kDatabaseFilename = "strawberry.db";
-const int Database::kSchemaVersion = 0;
+const int Database::kSchemaVersion = 1;
const char *Database::kMagicAllSongsTables = "%allsongstables";
int Database::sNextConnectionId = 1;
diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp
index 22a345d4f..c43e69e6b 100644
--- a/src/core/mainwindow.cpp
+++ b/src/core/mainwindow.cpp
@@ -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("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 &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());
+ }
+
+}
+
+
diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h
index 2c6841129..6516a70a4 100644
--- a/src/core/mainwindow.h
+++ b/src/core/mainwindow.h
@@ -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 playlistitem_actions_;
QAction *playlistitem_actions_separator_;
+ QAction *search_for_artist_;
+ QAction *search_for_album_;
+
QModelIndex playlist_menu_index_;
QSortFilterProxyModel *collection_sort_model_;
diff --git a/src/core/metatypes.cpp b/src/core/metatypes.cpp
index 204864755..079a7aa99 100644
--- a/src/core/metatypes.cpp
+++ b/src/core/metatypes.cpp
@@ -60,6 +60,8 @@
# include "dbus/metatypes.h"
#endif
+#include "tidal/tidalsearch.h"
+
void RegisterMetaTypes() {
qRegisterMetaType("const char*");
@@ -113,4 +115,7 @@ void RegisterMetaTypes() {
#endif
#endif
+ qRegisterMetaType("TidalSearch::ResultList");
+ qRegisterMetaType("TidalSearch::Result");
+
}
diff --git a/src/core/mimedata.h b/src/core/mimedata.h
index 0d22d77c5..bf1b75aa3 100644
--- a/src/core/mimedata.h
+++ b/src/core/mimedata.h
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#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_;
diff --git a/src/core/mpris2.cpp b/src/core/mpris2.cpp
index f26ef69b0..700f2b9ab 100644
--- a/src/core/mpris2.cpp
+++ b/src/core/mpris2.cpp
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#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; }
diff --git a/src/core/network.cpp b/src/core/network.cpp
index 369bbbd87..650359a62 100644
--- a/src/core/network.cpp
+++ b/src/core/network.cpp
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#include "config.h"
diff --git a/src/core/song.cpp b/src/core/song.cpp
index 2924e8705..3f6a89966 100644
--- a/src/core/song.cpp
+++ b/src/core/song.cpp
@@ -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 {
diff --git a/src/core/song.h b/src/core/song.h
index 74f836d39..6062aed80 100644
--- a/src/core/song.h
+++ b/src/core/song.h
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#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:
diff --git a/src/core/songloader.cpp b/src/core/songloader.cpp
index e4e4d7e49..ad4b03efb 100644
--- a/src/core/songloader.cpp
+++ b/src/core/songloader.cpp
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#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(self);
if (instance->state_ != WaitingForType) return;
diff --git a/src/core/songloader.h b/src/core/songloader.h
index f7d900ce7..b099c8acc 100644
--- a/src/core/songloader.h
+++ b/src/core/songloader.h
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#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();
diff --git a/src/device/cddasongloader.cpp b/src/device/cddasongloader.cpp
index 9f3a581b5..8bcbf3fab 100644
--- a/src/device/cddasongloader.cpp
+++ b/src/device/cddasongloader.cpp
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#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++));
diff --git a/src/device/gpoddevice.cpp b/src/device/gpoddevice.cpp
index 00707661b..efb6bd039 100644
--- a/src/device/gpoddevice.cpp
+++ b/src/device/gpoddevice.cpp
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#include "config.h"
@@ -248,8 +248,8 @@ void GPodDevice::FinishDelete(bool success) {
}
bool GPodDevice::GetSupportedFiletypes(QList *ret) {
- *ret << Song::Type_Mp4;
- *ret << Song::Type_Mpeg;
+ *ret << Song::Type_MP4;
+ *ret << Song::Type_MPEG;
return true;
}
diff --git a/src/device/mtpdevice.cpp b/src/device/mtpdevice.cpp
index cb7c2af6f..74db3d8bf 100644
--- a/src/device/mtpdevice.cpp
+++ b/src/device/mtpdevice.cpp
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#include "config.h"
@@ -209,15 +209,15 @@ bool MtpDevice::GetSupportedFiletypes(QList *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:
diff --git a/src/engine/gstengine.cpp b/src/engine/gstengine.cpp
index b3909b4f5..27e053b79 100644
--- a/src/engine/gstengine.cpp
+++ b/src/engine/gstengine.cpp
@@ -786,7 +786,6 @@ void GstEngine::StartFadeoutPause() {
void GstEngine::StartTimers() {
StopTimers();
-
timer_id_ = startTimer(kTimerIntervalNanosec / kNsecPerMsec);
}
diff --git a/src/engine/vlcengine.cpp b/src/engine/vlcengine.cpp
index 8c2f2963a..3a32f216c 100644
--- a/src/engine/vlcengine.cpp
+++ b/src/engine/vlcengine.cpp
@@ -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;
diff --git a/src/internet/internetmimedata.h b/src/internet/internetmimedata.h
new file mode 100644
index 000000000..eaa532ff0
--- /dev/null
+++ b/src/internet/internetmimedata.h
@@ -0,0 +1,43 @@
+/*
+ * Strawberry Music Player
+ * This file was part of Clementine.
+ * Copyright 2010, David Sansome
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef INTERNETMIMEDATA_H
+#define INTERNETMIMEDATA_H
+
+#include "config.h"
+
+#include
+#include
+
+#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
diff --git a/src/internet/internetmodel.cpp b/src/internet/internetmodel.cpp
new file mode 100644
index 000000000..81d8433a0
--- /dev/null
+++ b/src/internet/internetmodel.cpp
@@ -0,0 +1,83 @@
+/*
+ * Strawberry Music Player
+ * This file was part of Clementine.
+ * Copyright 2010, David Sansome
+ * Copyright 2018, Jonas Kvinge
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "core/logging.h"
+#include "internetmodel.h"
+#include "internetservice.h"
+#include "tidal/tidalservice.h"
+
+QMap* InternetModel::sServices = nullptr;
+
+InternetModel::InternetModel(Application *app, QObject *parent)
+ : QStandardItemModel(parent),
+ app_(app) {
+
+ if (!sServices) sServices = new QMap;
+ 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(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();
+ }
+}
diff --git a/src/internet/internetmodel.h b/src/internet/internetmodel.h
new file mode 100644
index 000000000..af3cf83da
--- /dev/null
+++ b/src/internet/internetmodel.h
@@ -0,0 +1,132 @@
+/*
+ * Strawberry Music Player
+ * This file was part of Clementine.
+ * Copyright 2010, David Sansome
+ * Copyright 2018, Jonas Kvinge
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef INTERNETMODEL_H
+#define INTERNETMODEL_H
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "core/song.h"
+#include "collection/collectionmodel.h"
+#include "playlist/playlistitem.h"
+#include "settings/settingsdialog.h"
+#include "widgets/multiloadingindicator.h"
+
+class Application;
+class InternetService;
+
+class 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
+ static T *Service() {
+ return static_cast(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 *sServices;
+ Application *app_;
+
+};
+
+#endif
diff --git a/src/internet/internetplaylistitem.cpp b/src/internet/internetplaylistitem.cpp
new file mode 100644
index 000000000..6ce03e3ce
--- /dev/null
+++ b/src/internet/internetplaylistitem.cpp
@@ -0,0 +1,106 @@
+/*
+ * Strawberry Music Player
+ * This file was part of Clementine.
+ * Copyright 2010, David Sansome
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "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(this)->set_service_icon_ = true;
+
+ QString icon = ret->Icon();
+ if (!icon.isEmpty()) {
+ const_cast(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(); }
diff --git a/src/internet/internetplaylistitem.h b/src/internet/internetplaylistitem.h
new file mode 100644
index 000000000..2a6d8f98f
--- /dev/null
+++ b/src/internet/internetplaylistitem.h
@@ -0,0 +1,58 @@
+/*
+ * Strawberry Music Player
+ * This file was part of Clementine.
+ * Copyright 2010, David Sansome
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef INTERNETPLAYLISTITEM_H
+#define INTERNETPLAYLISTITEM_H
+
+#include "config.h"
+
+#include
+#include
+#include
+
+#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
diff --git a/src/internet/internetservice.cpp b/src/internet/internetservice.cpp
new file mode 100644
index 000000000..25139ba6f
--- /dev/null
+++ b/src/internet/internetservice.cpp
@@ -0,0 +1,32 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2018, Jonas Kvinge
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#include
+#include
+#include
+#include
+
+#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) {
+}
diff --git a/src/internet/internetservice.h b/src/internet/internetservice.h
new file mode 100644
index 000000000..808648f49
--- /dev/null
+++ b/src/internet/internetservice.h
@@ -0,0 +1,64 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2018, Jonas Kvinge
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef INTERNETSERVICE_H
+#define INTERNETSERVICE_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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
diff --git a/src/internet/internetsongmimedata.h b/src/internet/internetsongmimedata.h
new file mode 100644
index 000000000..68541f49f
--- /dev/null
+++ b/src/internet/internetsongmimedata.h
@@ -0,0 +1,39 @@
+/*
+ * Strawberry Music Player
+ * This file was part of Clementine.
+ * Copyright 2011, David Sansome
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef 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
diff --git a/src/playlist/playlist.cpp b/src/playlist/playlist.cpp
index 084919cec..35489c136 100644
--- a/src/playlist/playlist.cpp
+++ b/src/playlist/playlist.cpp
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#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
-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(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(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(song_data->songs, row, play_now, enqueue_now);
else
- InsertSongItems(song_data->songs, row, play_now, enqueue_now);
-
+ InsertSongItems(song_data->songs, row, play_now, enqueue_now, enqueue_next_now);
}
else if (const PlaylistItemMimeData *item_data = qobject_cast(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(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 &urls, int pos, bool play_now, bool enqueue) {
+void Playlist::InsertUrls(const QList &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 &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(songs, pos, play_now, enqueue);
+void Playlist::InsertCollectionItems(const SongList &songs, int pos, bool play_now, bool enqueue, bool enqueue_next) {
+ InsertSongItems(songs, pos, play_now, enqueue, enqueue_next);
}
-void Playlist::InsertSongs(const SongList &songs, int pos, bool play_now, bool enqueue) {
- InsertSongItems(songs, pos, play_now, enqueue);
+void Playlist::InsertSongs(const SongList &songs, int pos, bool play_now, bool enqueue, bool enqueue_next) {
+ InsertSongItems(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(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_ptrMetadata().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_ptrMetadata();
+ 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);
diff --git a/src/playlist/playlist.h b/src/playlist/playlist.h
index deb7e0a02..4099fa1d6 100644
--- a/src/playlist/playlist.h
+++ b/src/playlist/playlist.h
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#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 &urls, int pos = -1, bool play_now = false, bool enqueue = false);
+ void InsertUrls(const QList &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 &indices);
@@ -302,7 +305,7 @@ private:
bool FilterContainsVirtualIndex(int i) const;
template
- 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);
diff --git a/src/playlist/playlistbackend.cpp b/src/playlist/playlistbackend.cpp
index 20870c40e..e38e41ac7 100644
--- a/src/playlist/playlistbackend.cpp
+++ b/src/playlist/playlistbackend.cpp
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#include
@@ -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");
diff --git a/src/playlist/playlistdelegates.cpp b/src/playlist/playlistdelegates.cpp
index ada0a2b51..6ae34349e 100644
--- a/src/playlist/playlistdelegates.cpp
+++ b/src/playlist/playlistdelegates.cpp
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#include "config.h"
@@ -39,6 +39,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -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");
}
diff --git a/src/playlist/playlistfilterparser.cpp b/src/playlist/playlistfilterparser.cpp
index 11f4a00c4..01aa0806a 100644
--- a/src/playlist/playlistfilterparser.cpp
+++ b/src/playlist/playlistfilterparser.cpp
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#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(search.toDouble() * 2.0 + 0.5);
- //}
else {
search_value = search.toInt();
}
diff --git a/src/playlist/playlistitem.cpp b/src/playlist/playlistitem.cpp
index 26bcb0aa6..13eb7a1b6 100644
--- a/src/playlist/playlistitem.cpp
+++ b/src/playlist/playlistitem.cpp
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#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_; }
+
diff --git a/src/playlist/playlistitem.h b/src/playlist/playlistitem.h
index f71d73d04..a7358bb1c 100644
--- a/src/playlist/playlistitem.h
+++ b/src/playlist/playlistitem.h
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#ifndef PLAYLISTITEM_H
@@ -104,7 +104,7 @@ class PlaylistItem : public std::enable_shared_from_this {
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)
Q_DECLARE_OPERATORS_FOR_FLAGS(PlaylistItem::Options)
#endif // PLAYLISTITEM_H
+
diff --git a/src/playlist/playlistview.cpp b/src/playlist/playlistview.cpp
index dd0b17183..5624a2900 100644
--- a/src/playlist/playlistview.cpp
+++ b/src/playlist/playlistview.cpp
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#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 ¤t = selectionModel()->currentIndex();
diff --git a/src/playlist/playlistview.h b/src/playlist/playlistview.h
index 14916dcfc..59ddd977d 100644
--- a/src/playlist/playlistview.h
+++ b/src/playlist/playlistview.h
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#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);
diff --git a/src/settings/playlistsettingspage.cpp b/src/settings/playlistsettingspage.cpp
index c32f6f6be..a76b03259 100644
--- a/src/settings/playlistsettingspage.cpp
+++ b/src/settings/playlistsettingspage.cpp
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#include "config.h"
diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp
index d562fff62..e1864fc9a 100644
--- a/src/settings/settingsdialog.cpp
+++ b/src/settings/settingsdialog.cpp
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#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"));
diff --git a/src/settings/settingsdialog.h b/src/settings/settingsdialog.h
index 4577b81e4..0f7e51980 100644
--- a/src/settings/settingsdialog.h
+++ b/src/settings/settingsdialog.h
@@ -15,7 +15,7 @@
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see .
- *
+ *
*/
#ifndef SETTINGSDIALOG_H
@@ -79,6 +79,7 @@ public:
Page_Notifications,
Page_Proxy,
Page_Transcoding,
+ Page_Tidal,
};
enum Role {
diff --git a/src/settings/tidalsettingspage.cpp b/src/settings/tidalsettingspage.cpp
new file mode 100644
index 000000000..73399bd2e
--- /dev/null
+++ b/src/settings/tidalsettingspage.cpp
@@ -0,0 +1,118 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2018, Jonas Kvinge
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "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()) {
+
+ 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);
+}
diff --git a/src/settings/tidalsettingspage.h b/src/settings/tidalsettingspage.h
new file mode 100644
index 000000000..567f10ca5
--- /dev/null
+++ b/src/settings/tidalsettingspage.h
@@ -0,0 +1,62 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2018, Jonas Kvinge
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef TIDALSETTINGSPAGE_H
+#define TIDALSETTINGSPAGE_H
+
+#include
+#include
+#include
+
+#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
diff --git a/src/settings/tidalsettingspage.ui b/src/settings/tidalsettingspage.ui
new file mode 100644
index 000000000..dc2389f5c
--- /dev/null
+++ b/src/settings/tidalsettingspage.ui
@@ -0,0 +1,157 @@
+
+
+ TidalSettingsPage
+
+
+
+ 0
+ 0
+ 715
+ 425
+
+
+
+ Tidal
+
+
+ -
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Account details
+
+
+
-
+
+
+ Tidal username
+
+
+
+ -
+
+
-
+
+
+ -
+
+
+ Login
+
+
+
+
+
+ -
+
+
+ Tidal password
+
+
+
+ -
+
+
+ QLineEdit::Password
+
+
+
+
+
+
+ -
+
+
+ Preferences
+
+
+
-
+
+
+ Audio quality
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 30
+
+
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 64
+ 64
+
+
+
+
+ 64
+ 64
+
+
+
+ :/icons/64x64/tidal.png
+
+
+
+
+
+
+
+
+
+ LoginStateWidget
+ QWidget
+ widgets/loginstatewidget.h
+ 1
+
+
+
+ username
+ password
+ button_login
+
+
+
+
+
+
diff --git a/src/tidal/tidalsearch.cpp b/src/tidal/tidalsearch.cpp
new file mode 100644
index 000000000..c8326f4e3
--- /dev/null
+++ b/src/tidal/tidalsearch.cpp
@@ -0,0 +1,316 @@
+/*
+ * Strawberry Music Player
+ * This code was part of Clementine (GlobalSearch)
+ * Copyright 2010, David Sansome
+ * Copyright 2018, Jonas Kvinge
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#include "config.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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()),
+ 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::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::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 urls;
+ for (const Result &result : results) {
+ urls << result.metadata_.url();
+ }
+ mime_data->setUrls(urls);
+
+ return mime_data;
+
+}
diff --git a/src/tidal/tidalsearch.h b/src/tidal/tidalsearch.h
new file mode 100644
index 000000000..29cacfd41
--- /dev/null
+++ b/src/tidal/tidalsearch.h
@@ -0,0 +1,157 @@
+/*
+ * Strawberry Music Player
+ * This code was part of Clementine (GlobalSearch)
+ * Copyright 2010, David Sansome
+ * Copyright 2018, Jonas Kvinge
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef TIDALSEARCH_H
+#define TIDALSEARCH_H
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#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 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 delayed_searches_;
+ QMap pending_art_searches_;
+ QPixmapCache pixmap_cache_;
+ AlbumCoverLoaderOptions cover_loader_options_;
+ QMap cover_loader_tasks_;
+
+ QMap pending_searches_;
+
+};
+
+Q_DECLARE_METATYPE(TidalSearch::Result)
+Q_DECLARE_METATYPE(TidalSearch::ResultList)
+
+#endif // TIDALSEARCH_H
diff --git a/src/tidal/tidalsearchitemdelegate.cpp b/src/tidal/tidalsearchitemdelegate.cpp
new file mode 100644
index 000000000..e1039f788
--- /dev/null
+++ b/src/tidal/tidalsearchitemdelegate.cpp
@@ -0,0 +1,35 @@
+/*
+ * Strawberry Music Player
+ * This code was part of Clementine (GlobalSearch)
+ * Copyright 2012, David Sansome
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#include
+#include
+
+#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(view_)->LazyLoadArt(index);
+
+ CollectionItemDelegate::paint(painter, option, index);
+}
diff --git a/src/tidal/tidalsearchitemdelegate.h b/src/tidal/tidalsearchitemdelegate.h
new file mode 100644
index 000000000..26d4705e7
--- /dev/null
+++ b/src/tidal/tidalsearchitemdelegate.h
@@ -0,0 +1,41 @@
+/*
+ * Strawberry Music Player
+ * This code was part of Clementine (GlobalSearch)
+ * Copyright 2012, David Sansome
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef TIDALSEARCHITEMDELEGATE_H
+#define TIDALSEARCHITEMDELEGATE_H
+
+#include
+#include
+
+#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
diff --git a/src/tidal/tidalsearchmodel.cpp b/src/tidal/tidalsearchmodel.cpp
new file mode 100644
index 000000000..6e9f87278
--- /dev/null
+++ b/src/tidal/tidalsearchmodel.cpp
@@ -0,0 +1,314 @@
+/*
+ * Strawberry Music Player
+ * This code was part of Clementine (GlobalSearch)
+ * Copyright 2012, David Sansome
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "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 items;
+ for (const QModelIndex &index : indexes) {
+ items << itemFromIndex(index);
+ }
+ return GetChildResults(items);
+
+}
+
+TidalSearch::ResultList TidalSearchModel::GetChildResults(const QList &items) const {
+
+ TidalSearch::ResultList results;
+ QSet visited;
+
+ for (QStandardItem *item : items) {
+ GetChildResults(item, &results, &visited);
+ }
+
+ return results;
+
+}
+
+void TidalSearchModel::GetChildResults(const QStandardItem *item, TidalSearch::ResultList *results, QSet *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());
+ }
+ 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();
+ (*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);
+ }
+
+}
diff --git a/src/tidal/tidalsearchmodel.h b/src/tidal/tidalsearchmodel.h
new file mode 100644
index 000000000..d142516b1
--- /dev/null
+++ b/src/tidal/tidalsearchmodel.h
@@ -0,0 +1,109 @@
+/*
+ * Strawberry Music Player
+ * This code was part of Clementine (GlobalSearch)
+ * Copyright 2012, David Sansome
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef TIDALSEARCHMODEL_H
+#define TIDALSEARCHMODEL_H
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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 &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 *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 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
diff --git a/src/tidal/tidalsearchsortmodel.cpp b/src/tidal/tidalsearchsortmodel.cpp
new file mode 100644
index 000000000..f516998d0
--- /dev/null
+++ b/src/tidal/tidalsearchsortmodel.cpp
@@ -0,0 +1,79 @@
+/*
+ * Strawberry Music Player
+ * This code was part of Clementine (GlobalSearch)
+ * Copyright 2010, David Sansome
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+
+#include "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();
+ const TidalSearch::Result r2 = right.data(TidalSearchModel::Role_Result).value();
+
+#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
+}
diff --git a/src/tidal/tidalsearchsortmodel.h b/src/tidal/tidalsearchsortmodel.h
new file mode 100644
index 000000000..66a6ca9b3
--- /dev/null
+++ b/src/tidal/tidalsearchsortmodel.h
@@ -0,0 +1,35 @@
+/*
+ * Strawberry Music Player
+ * This code was part of Clementine (GlobalSearch)
+ * Copyright 2010, David Sansome
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef TIDALSEARCHSORTMODEL_H
+#define TIDALSEARCHSORTMODEL_H
+
+#include
+#include
+
+class TidalSearchSortModel : public QSortFilterProxyModel {
+ public:
+ TidalSearchSortModel(QObject *parent = nullptr);
+
+ protected:
+ bool lessThan(const QModelIndex &left, const QModelIndex &right) const;
+};
+
+#endif // TIDALSEARCHSORTMODEL_H
diff --git a/src/tidal/tidalsearchview.cpp b/src/tidal/tidalsearchview.cpp
new file mode 100644
index 000000000..380895c9c
--- /dev/null
+++ b/src/tidal/tidalsearchview.cpp
@@ -0,0 +1,544 @@
+/*
+ * Strawberry Music Player
+ * This code was part of Clementine (GlobalSearch)
+ * Copyright 2012, David Sansome
+ * Copyright 2018, Jonas Kvinge
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#include "config.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/application.h"
+#include "core/logging.h"
+#include "core/mimedata.h"
+#include "core/timeconstants.h"
+#include "core/iconloader.h"
+#include "internet/internetsongmimedata.h"
+#include "collection/collectionfilterwidget.h"
+#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();
+
+ // 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 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(event))) {
+ return true;
+ }
+ }
+ else if (object == ui_->results_stack && event->type() == QEvent::ContextMenu) {
+ if (ResultsContextMenuEvent(static_cast(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());
+
+}
+
+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()) {
+ 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());
+}
diff --git a/src/tidal/tidalsearchview.h b/src/tidal/tidalsearchview.h
new file mode 100644
index 000000000..d4dfe7a8d
--- /dev/null
+++ b/src/tidal/tidalsearchview.h
@@ -0,0 +1,139 @@
+/*
+ * Strawberry Music Player
+ * This code was part of Clementine (GlobalSearch)
+ * Copyright 2012, David Sansome
+ * Copyright 2018, Jonas Kvinge
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#ifndef TIDALSEARCHVIEW_H
+#define TIDALSEARCHVIEW_H
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "collection/collectionmodel.h"
+#include "settings/settingsdialog.h"
+#include "playlist/playlistmanager.h"
+#include "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 group_by_dialog_;
+
+ QMenu *context_menu_;
+ QList 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 art_requests_;
+
+ QTimer *swap_models_timer_;
+
+ QIcon search_icon_;
+ QIcon warning_icon_;
+
+ TidalSettingsPage::SearchBy searchby_;
+ bool error_;
+
+};
+
+#endif // TIDALSEARCHVIEW_H
diff --git a/src/tidal/tidalsearchview.ui b/src/tidal/tidalsearchview.ui
new file mode 100644
index 000000000..a1cab7181
--- /dev/null
+++ b/src/tidal/tidalsearchview.ui
@@ -0,0 +1,259 @@
+
+
+ TidalSearchView
+
+
+
+ 0
+ 0
+ 437
+ 633
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+ 0
+
+
-
+
+
-
+
+
-
+
+
+ Search for anything
+
+
+
+ -
+
+
+
+ 20
+ 0
+
+
+
+ QToolButton::InstantPopup
+
+
+ true
+
+
+
+
+
+ -
+
+
+ QLayout::SetFixedSize
+
+
-
+
+
+ true
+
+
+ Search by
+
+
+ 10
+
+
+
+ -
+
+
+ albu&ms
+
+
+
+ -
+
+
+ songs
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ 1
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ true
+
+
+ QAbstractItemView::DragOnly
+
+
+ QAbstractItemView::ExtendedSelection
+
+
+ true
+
+
+ false
+
+
+
+
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ Qt::ScrollBarAlwaysOff
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 435
+ 533
+
+
+
+
+
+ 9
+ 109
+ 420
+ 100
+
+
+
+
+ 32
+
+
+ 16
+
+
+ 32
+
+
+ 64
+
+
-
+
+
+
+ 0
+ 80
+
+
+
+ Enter search terms above to find music
+
+
+ Qt::AlignCenter
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ QSearchField
+ QWidget
+ 3rdparty/qocoa/qsearchfield.h
+
+
+ AutoExpandingTreeView
+ QTreeView
+ widgets/autoexpandingtreeview.h
+
+
+
+
+
diff --git a/src/tidal/tidalservice.cpp b/src/tidal/tidalservice.cpp
new file mode 100644
index 000000000..b93f262ec
--- /dev/null
+++ b/src/tidal/tidalservice.cpp
@@ -0,0 +1,832 @@
+/*
+ * Strawberry Music Player
+ * Copyright 2018, Jonas Kvinge
+ *
+ * Strawberry is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Strawberry is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Strawberry. If not, see .
+ *
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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 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