Add tidal support
This commit is contained in:
parent
26062bd07b
commit
820124f9e1
@ -1,6 +1,7 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>schema/schema.sql</file>
|
||||
<file>schema/schema-1.sql</file>
|
||||
<file>schema/device-schema.sql</file>
|
||||
<file>style/mainwindow.css</file>
|
||||
<file>style/statusview.css</file>
|
||||
@ -113,6 +114,7 @@
|
||||
<file>icons/128x128/xine.png</file>
|
||||
<file>icons/128x128/zoom-in.png</file>
|
||||
<file>icons/128x128/zoom-out.png</file>
|
||||
<file>icons/128x128/tidal.png</file>
|
||||
<file>icons/64x64/albums.png</file>
|
||||
<file>icons/64x64/alsa.png</file>
|
||||
<file>icons/64x64/application-exit.png</file>
|
||||
@ -201,6 +203,7 @@
|
||||
<file>icons/64x64/xine.png</file>
|
||||
<file>icons/64x64/zoom-in.png</file>
|
||||
<file>icons/64x64/zoom-out.png</file>
|
||||
<file>icons/64x64/tidal.png</file>
|
||||
<file>icons/48x48/albums.png</file>
|
||||
<file>icons/48x48/alsa.png</file>
|
||||
<file>icons/48x48/application-exit.png</file>
|
||||
@ -292,6 +295,7 @@
|
||||
<file>icons/48x48/xine.png</file>
|
||||
<file>icons/48x48/zoom-in.png</file>
|
||||
<file>icons/48x48/zoom-out.png</file>
|
||||
<file>icons/48x48/tidal.png</file>
|
||||
<file>icons/32x32/albums.png</file>
|
||||
<file>icons/32x32/alsa.png</file>
|
||||
<file>icons/32x32/application-exit.png</file>
|
||||
@ -384,6 +388,7 @@
|
||||
<file>icons/32x32/xine.png</file>
|
||||
<file>icons/32x32/zoom-in.png</file>
|
||||
<file>icons/32x32/zoom-out.png</file>
|
||||
<file>icons/32x32/tidal.png</file>
|
||||
<file>icons/22x22/albums.png</file>
|
||||
<file>icons/22x22/alsa.png</file>
|
||||
<file>icons/22x22/application-exit.png</file>
|
||||
@ -476,5 +481,6 @@
|
||||
<file>icons/22x22/xine.png</file>
|
||||
<file>icons/22x22/zoom-in.png</file>
|
||||
<file>icons/22x22/zoom-out.png</file>
|
||||
<file>icons/22x22/tidal.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
BIN
data/icons/128x128/tidal.png
Normal file
BIN
data/icons/128x128/tidal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
BIN
data/icons/22x22/tidal.png
Normal file
BIN
data/icons/22x22/tidal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 933 B |
BIN
data/icons/32x32/tidal.png
Normal file
BIN
data/icons/32x32/tidal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
data/icons/48x48/tidal.png
Normal file
BIN
data/icons/48x48/tidal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
data/icons/64x64/tidal.png
Normal file
BIN
data/icons/64x64/tidal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
data/icons/full/tidal.png
Normal file
BIN
data/icons/full/tidal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
3
data/schema/schema-1.sql
Normal file
3
data/schema/schema-1.sql
Normal file
@ -0,0 +1,3 @@
|
||||
ALTER TABLE playlist_items ADD COLUMN internet_service TEXT;
|
||||
|
||||
UPDATE schema_version SET version=1;
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@ -52,6 +52,9 @@
|
||||
#include "covermanager/discogscoverprovider.h"
|
||||
#include "covermanager/musicbrainzcoverprovider.h"
|
||||
|
||||
#include "internet/internetmodel.h"
|
||||
#include "tidal/tidalsearch.h"
|
||||
|
||||
bool Application::kIsPortable = false;
|
||||
|
||||
class ApplicationImpl {
|
||||
@ -97,7 +100,9 @@ class ApplicationImpl {
|
||||
app->MoveToNewThread(loader);
|
||||
return loader;
|
||||
}),
|
||||
current_art_loader_([=]() { return new CurrentArtLoader(app, app); })
|
||||
current_art_loader_([=]() { return new CurrentArtLoader(app, app); }),
|
||||
internet_model_([=]() { return new InternetModel(app, app); }),
|
||||
tidal_search_([=]() { return new TidalSearch(app, app); })
|
||||
{ }
|
||||
|
||||
Lazy<TagReaderClient> tag_reader_client_;
|
||||
@ -113,6 +118,8 @@ class ApplicationImpl {
|
||||
Lazy<CoverProviders> cover_providers_;
|
||||
Lazy<AlbumCoverLoader> album_cover_loader_;
|
||||
Lazy<CurrentArtLoader> current_art_loader_;
|
||||
Lazy<InternetModel> internet_model_;
|
||||
Lazy<TidalSearch> tidal_search_;
|
||||
|
||||
};
|
||||
|
||||
@ -210,6 +217,13 @@ TaskManager *Application::task_manager() const {
|
||||
}
|
||||
|
||||
EngineDevice *Application::enginedevice() const {
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
return p_->enginedevice_.get();
|
||||
}
|
||||
|
||||
InternetModel* Application::internet_model() const {
|
||||
return p_->internet_model_.get();
|
||||
}
|
||||
|
||||
TidalSearch* Application::tidal_search() const {
|
||||
return p_->tidal_search_.get();
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef APPLICATION_H_
|
||||
@ -49,6 +49,8 @@ class DeviceManager;
|
||||
class CoverProviders;
|
||||
class AlbumCoverLoader;
|
||||
class CurrentArtLoader;
|
||||
class InternetModel;
|
||||
class TidalSearch;
|
||||
|
||||
class Application : public QObject {
|
||||
Q_OBJECT
|
||||
@ -79,6 +81,9 @@ class Application : public QObject {
|
||||
CollectionBackend *collection_backend() const;
|
||||
CollectionModel *collection_model() const;
|
||||
|
||||
InternetModel *internet_model() const;
|
||||
TidalSearch *tidal_search() const;
|
||||
|
||||
void MoveToNewThread(QObject *object);
|
||||
void MoveToThread(QObject *object, QThread *thread);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -126,6 +126,8 @@
|
||||
#include "settings/playlistsettingspage.h"
|
||||
#include "settings/settingsdialog.h"
|
||||
|
||||
#include "tidal/tidalsearchview.h"
|
||||
|
||||
#if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT)
|
||||
# include "musicbrainz/tagfetcher.h"
|
||||
#endif
|
||||
@ -186,6 +188,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
||||
manager->SetPlaylistManager(app->playlist_manager());
|
||||
return manager;
|
||||
}),
|
||||
tidal_search_view_(new TidalSearchView(app_, this)),
|
||||
playlist_menu_(new QMenu(this)),
|
||||
playlist_add_to_another_(nullptr),
|
||||
playlistitem_actions_separator_(nullptr),
|
||||
@ -218,7 +221,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
||||
ui_->volume->setValue(volume);
|
||||
VolumeChanged(volume);
|
||||
|
||||
// Initialise the global search widget
|
||||
// Initialise the tidal search widget
|
||||
StyleHelper::setBaseColor(palette().color(QPalette::Highlight).darker());
|
||||
|
||||
// Add tabs to the fancy tab widget
|
||||
@ -227,6 +230,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
||||
ui_->tabs->addTab(file_view_, IconLoader::Load("document-open"), tr("Files"));
|
||||
ui_->tabs->addTab(playlist_list_, IconLoader::Load("view-media-playlist"), tr("Playlists"));
|
||||
ui_->tabs->addTab(device_view_, IconLoader::Load("device"), tr("Devices"));
|
||||
ui_->tabs->addTab(tidal_search_view_, IconLoader::Load("tidal"), tr("Tidal", "Tidal"));
|
||||
//ui_->tabs->AddSpacer();
|
||||
|
||||
// Add the now playing widget to the fancy tab widget
|
||||
@ -475,6 +479,9 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
||||
collection_view_->filter()->AddMenuAction(separator);
|
||||
collection_view_->filter()->AddMenuAction(collection_config_action);
|
||||
|
||||
// Tidal
|
||||
connect(tidal_search_view_, SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
|
||||
|
||||
// Playlist menu
|
||||
playlist_play_pause_ = playlist_menu_->addAction(tr("Play"), this, SLOT(PlaylistPlay()));
|
||||
playlist_menu_->addAction(ui_->action_stop);
|
||||
@ -657,6 +664,12 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
// Tidal search shortcut
|
||||
QAction *tidal_search_action = new QAction(this);
|
||||
tidal_search_action->setShortcuts(QList<QKeySequence>() << QKeySequence("Ctrl+F") << QKeySequence("Ctrl+L"));
|
||||
addAction(tidal_search_action);
|
||||
connect(tidal_search_action, SIGNAL(triggered()), SLOT(FocusTidalSearchField()));
|
||||
|
||||
// Reload pretty OSD to avoid issues with fonts
|
||||
osd_->ReloadPrettyOSDSettings();
|
||||
|
||||
@ -745,6 +758,7 @@ void MainWindow::ReloadAllSettings() {
|
||||
osd_->ReloadSettings();
|
||||
collection_view_->ReloadSettings();
|
||||
ui_->playlist->view()->ReloadSettings();
|
||||
tidal_search_view_->ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
@ -787,7 +801,7 @@ void MainWindow::MediaPaused() {
|
||||
}
|
||||
|
||||
void MainWindow::MediaPlaying() {
|
||||
|
||||
|
||||
ui_->action_stop->setEnabled(true);
|
||||
ui_->action_stop_after_this_track->setEnabled(true);
|
||||
ui_->action_play_pause->setIcon(IconLoader::Load("media-pause"));
|
||||
@ -1789,7 +1803,7 @@ void MainWindow::EditFileTags(const QList<QUrl> &urls) {
|
||||
Song song;
|
||||
song.set_url(url);
|
||||
song.set_valid(true);
|
||||
song.set_filetype(Song::Type_Mpeg);
|
||||
song.set_filetype(Song::Type_MPEG);
|
||||
songs << song;
|
||||
}
|
||||
|
||||
@ -2261,3 +2275,37 @@ void MainWindow::keyPressEvent(QKeyEvent *event) {
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::FocusTidalSearchField() {
|
||||
ui_->tabs->setCurrentWidget(tidal_search_view_);
|
||||
tidal_search_view_->FocusSearchField();
|
||||
}
|
||||
|
||||
void MainWindow::DoTidalSearch(const QString& query) {
|
||||
FocusTidalSearchField();
|
||||
tidal_search_view_->StartSearch(query);
|
||||
}
|
||||
|
||||
void MainWindow::SearchForArtist() {
|
||||
|
||||
PlaylistItemPtr item(app_->playlist_manager()->current()->item_at(playlist_menu_index_.row()));
|
||||
Song song = item->Metadata();
|
||||
if (!song.albumartist().isEmpty()) {
|
||||
DoTidalSearch(song.albumartist().simplified());
|
||||
}
|
||||
else if (!song.artist().isEmpty()) {
|
||||
DoTidalSearch(song.artist().simplified());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::SearchForAlbum() {
|
||||
|
||||
PlaylistItemPtr item(app_->playlist_manager()->current()->item_at(playlist_menu_index_.row()));
|
||||
Song song = item->Metadata();
|
||||
if (!song.album().isEmpty()) {
|
||||
DoTidalSearch(song.album().simplified());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -84,6 +84,7 @@ class TranscodeDialog;
|
||||
#endif
|
||||
class Ui_MainWindow;
|
||||
class Windows7ThumbBar;
|
||||
class TidalSearchView;
|
||||
|
||||
class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
Q_OBJECT
|
||||
@ -263,6 +264,11 @@ signals:
|
||||
|
||||
void ShowConsole();
|
||||
|
||||
void FocusTidalSearchField();
|
||||
void DoTidalSearch(const QString& query);
|
||||
void SearchForArtist();
|
||||
void SearchForAlbum();
|
||||
|
||||
private:
|
||||
void ConnectStatusView(StatusView *statusview);
|
||||
|
||||
@ -313,6 +319,8 @@ signals:
|
||||
PlaylistItemList autocomplete_tag_items_;
|
||||
#endif
|
||||
|
||||
TidalSearchView *tidal_search_view_;
|
||||
|
||||
QAction *collection_show_all_;
|
||||
QAction *collection_show_duplicates_;
|
||||
QAction *collection_show_untagged_;
|
||||
@ -335,6 +343,9 @@ signals:
|
||||
QAction *playlist_add_to_another_;
|
||||
QList<QAction*> playlistitem_actions_;
|
||||
QAction *playlistitem_actions_separator_;
|
||||
QAction *search_for_artist_;
|
||||
QAction *search_for_album_;
|
||||
|
||||
QModelIndex playlist_menu_index_;
|
||||
|
||||
QSortFilterProxyModel *collection_sort_model_;
|
||||
|
@ -60,6 +60,8 @@
|
||||
# include "dbus/metatypes.h"
|
||||
#endif
|
||||
|
||||
#include "tidal/tidalsearch.h"
|
||||
|
||||
void RegisterMetaTypes() {
|
||||
|
||||
qRegisterMetaType<const char*>("const char*");
|
||||
@ -113,4 +115,7 @@ void RegisterMetaTypes() {
|
||||
#endif
|
||||
#endif
|
||||
|
||||
qRegisterMetaType<TidalSearch::ResultList>("TidalSearch::ResultList");
|
||||
qRegisterMetaType<TidalSearch::Result>("TidalSearch::Result");
|
||||
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef MIMEDATA_H
|
||||
@ -52,6 +52,9 @@ class MimeData : public QMimeData {
|
||||
|
||||
// If this is set then the items are added to the queue after being inserted.
|
||||
bool enqueue_now_;
|
||||
|
||||
// If this is set then the items are added to the beginning of the queue after being inserted.
|
||||
bool enqueue_next_now_;
|
||||
|
||||
// If this is set then the items are inserted into a newly created playlist.
|
||||
bool open_in_new_playlist_;
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@ -393,7 +393,7 @@ bool Mpris2::CanPause() const {
|
||||
bool Mpris2::CanSeek() const { return CanSeek(app_->player()->GetState()); }
|
||||
|
||||
bool Mpris2::CanSeek(Engine::State state) const {
|
||||
return app_->player()->GetCurrentItem() && state != Engine::Empty;
|
||||
return app_->player()->GetCurrentItem() && state != Engine::Empty && !app_->player()->GetCurrentItem()->Metadata().is_stream();
|
||||
}
|
||||
|
||||
bool Mpris2::CanControl() const { return true; }
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
@ -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 {
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef SONG_H
|
||||
@ -58,12 +58,6 @@ struct _Itdb_Track;
|
||||
struct LIBMTP_track_struct;
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_LIBLASTFM
|
||||
namespace lastfm {
|
||||
class Track;
|
||||
}
|
||||
#endif
|
||||
|
||||
class SqlRow;
|
||||
|
||||
class Song {
|
||||
@ -95,20 +89,20 @@ class Song {
|
||||
// If a new lossless file is added, also add it to IsFileLossless().
|
||||
enum FileType {
|
||||
Type_Unknown = 0,
|
||||
Type_Wav = 1,
|
||||
Type_Flac = 2,
|
||||
Type_WAV = 1,
|
||||
Type_FLAC = 2,
|
||||
Type_WavPack = 3,
|
||||
Type_OggFlac = 4,
|
||||
Type_OggVorbis = 5,
|
||||
Type_OggOpus = 6,
|
||||
Type_OggSpeex = 7,
|
||||
Type_Mpeg = 8,
|
||||
Type_Mp4 = 9,
|
||||
Type_Asf = 10,
|
||||
Type_Aiff = 11,
|
||||
Type_Mpc = 12,
|
||||
Type_MPEG = 8,
|
||||
Type_MP4 = 9,
|
||||
Type_ASF = 10,
|
||||
Type_AIFF = 11,
|
||||
Type_MPC = 12,
|
||||
Type_TrueAudio = 13,
|
||||
Type_Cdda = 90,
|
||||
Type_CDDA = 90,
|
||||
Type_Stream = 91,
|
||||
};
|
||||
|
||||
@ -127,9 +121,6 @@ class Song {
|
||||
void InitFromQuery(const SqlRow &query, bool reliable_metadata, int col = 0);
|
||||
void InitFromFilePartial(const QString &filename); // Just store the filename: incomplete but fast
|
||||
void InitArtManual(); // Check if there is already a art in the cache and store the filename in art_manual
|
||||
#ifdef HAVE_LIBLASTFM
|
||||
void InitFromLastFM(const lastfm::Track &track);
|
||||
#endif
|
||||
|
||||
void MergeFromSimpleMetaBundle(const Engine::SimpleMetaBundle &bundle);
|
||||
|
||||
@ -152,9 +143,6 @@ class Song {
|
||||
// Save
|
||||
void BindToQuery(QSqlQuery *query) const;
|
||||
void BindToFtsQuery(QSqlQuery *query) const;
|
||||
#ifdef HAVE_LIBLASTFM
|
||||
void ToLastFM(lastfm::Track *track, bool prefer_album_artist) const;
|
||||
#endif
|
||||
void ToXesam(QVariantMap *map) const;
|
||||
void ToProtobuf(pb::tagreader::SongMetadata *pb) const;
|
||||
|
||||
@ -210,6 +198,7 @@ class Song {
|
||||
const QString &effective_albumartist() const;
|
||||
|
||||
bool is_collection_song() const;
|
||||
bool is_stream() const;
|
||||
bool is_cdda() const;
|
||||
|
||||
// Playlist views are special because you don't want to fill in album artists automatically for compilations, but you do for normal albums:
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@ -51,6 +51,8 @@
|
||||
#include "song.h"
|
||||
#include "songloader.h"
|
||||
#include "tagreaderclient.h"
|
||||
#include "engine/enginetype.h"
|
||||
#include "engine/enginebase.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collectionquery.h"
|
||||
#include "collection/sqlrow.h"
|
||||
@ -78,6 +80,7 @@ SongLoader::SongLoader(CollectionBackendInterface *collection, const Player *pla
|
||||
parser_(nullptr),
|
||||
collection_(collection),
|
||||
player_(player) {
|
||||
|
||||
if (sRawUriSchemes.isEmpty()) {
|
||||
sRawUriSchemes << "udp"
|
||||
<< "mms"
|
||||
@ -97,7 +100,7 @@ SongLoader::SongLoader(CollectionBackendInterface *collection, const Player *pla
|
||||
}
|
||||
|
||||
SongLoader::~SongLoader() {
|
||||
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
if (pipeline_) {
|
||||
state_ = Finished;
|
||||
@ -121,24 +124,29 @@ SongLoader::Result SongLoader::Load(const QUrl &url) {
|
||||
return Success;
|
||||
}
|
||||
|
||||
if (player_->engine()->type() == Engine::GStreamer) {
|
||||
#ifdef HAVE_GSTREAMER
|
||||
preload_func_ = std::bind(&SongLoader::LoadRemote, this);
|
||||
preload_func_ = std::bind(&SongLoader::LoadRemote, this);
|
||||
return BlockingLoadRequired;
|
||||
#else
|
||||
return Error;
|
||||
#endif
|
||||
}
|
||||
|
||||
return BlockingLoadRequired;
|
||||
return Success;
|
||||
|
||||
}
|
||||
|
||||
void SongLoader::LoadFilenamesBlocking() {
|
||||
|
||||
|
||||
if (preload_func_) {
|
||||
preload_func_();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
SongLoader::Result SongLoader::LoadLocalPartial(const QString &filename) {
|
||||
|
||||
|
||||
qLog(Debug) << "Fast Loading local file" << filename;
|
||||
// First check to see if it's a directory - if so we can load all the songs inside right away.
|
||||
if (QFileInfo(filename).isDir()) {
|
||||
@ -149,7 +157,7 @@ SongLoader::Result SongLoader::LoadLocalPartial(const QString &filename) {
|
||||
song.InitFromFilePartial(filename);
|
||||
if (song.is_valid()) songs_ << song;
|
||||
return Success;
|
||||
|
||||
|
||||
}
|
||||
|
||||
SongLoader::Result SongLoader::LoadAudioCD() {
|
||||
@ -208,6 +216,7 @@ SongLoader::Result SongLoader::LoadLocal(const QString &filename) {
|
||||
// It's not in the database, load it asynchronously.
|
||||
preload_func_ = std::bind(&SongLoader::LoadLocalAsync, this, filename);
|
||||
return BlockingLoadRequired;
|
||||
|
||||
}
|
||||
|
||||
void SongLoader::LoadLocalAsync(const QString &filename) {
|
||||
@ -253,6 +262,7 @@ void SongLoader::LoadLocalAsync(const QString &filename) {
|
||||
Song song;
|
||||
song.InitFromFilePartial(filename);
|
||||
if (song.is_valid()) songs_ << song;
|
||||
|
||||
}
|
||||
|
||||
void SongLoader::LoadMetadataBlocking() {
|
||||
@ -274,7 +284,8 @@ void SongLoader::EffectiveSongLoad(Song *song) {
|
||||
Song collection_song = collection_->GetSongByUrl(song->url());
|
||||
if (collection_song.is_valid()) {
|
||||
*song = collection_song;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
// it's a normal media file
|
||||
QString filename = song->url().toLocalFile();
|
||||
TagReaderClient::Instance()->ReadFileBlocking(filename, song);
|
||||
@ -318,7 +329,15 @@ void SongLoader::LoadLocalDirectory(const QString &filename) {
|
||||
// so if the user has the "Start playing when adding to playlist" preference behaviour set,
|
||||
// it can enjoy the first song being played (seek it, have moodbar, etc.)
|
||||
if (!songs_.isEmpty()) EffectiveSongLoad(&(*songs_.begin()));
|
||||
}
|
||||
|
||||
void SongLoader::AddAsRawStream() {
|
||||
Song song;
|
||||
song.set_valid(true);
|
||||
song.set_filetype(Song::Type_Stream);
|
||||
song.set_url(url_);
|
||||
song.set_title(url_.toString());
|
||||
songs_ << song;
|
||||
}
|
||||
|
||||
void SongLoader::Timeout() {
|
||||
@ -348,10 +367,10 @@ void SongLoader::StopTypefind() {
|
||||
|
||||
}
|
||||
else if (success_) {
|
||||
//qLog(Debug) << "Loading" << url_ << "as raw stream";
|
||||
qLog(Debug) << "Loading" << url_ << "as raw stream";
|
||||
|
||||
// It wasn't a playlist - just put the URL in as a stream
|
||||
//AddAsRawStream();
|
||||
AddAsRawStream();
|
||||
}
|
||||
|
||||
emit LoadRemoteFinished();
|
||||
@ -413,7 +432,7 @@ void SongLoader::LoadRemote() {
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void SongLoader::TypeFound(GstElement *, uint, GstCaps *caps, void *self) {
|
||||
|
||||
|
||||
SongLoader *instance = static_cast<SongLoader*>(self);
|
||||
|
||||
if (instance->state_ != WaitingForType) return;
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef SONGLOADER_H
|
||||
@ -106,6 +106,8 @@ signals:
|
||||
void LoadLocalDirectory(const QString &filename);
|
||||
void LoadPlaylist(ParserBase *parser, const QString &filename);
|
||||
|
||||
void AddAsRawStream();
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void LoadRemote();
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@ -110,7 +110,7 @@ void CddaSongLoader::LoadSongs() {
|
||||
Song song;
|
||||
song.set_id(track_number);
|
||||
song.set_valid(true);
|
||||
song.set_filetype(Song::Type_Cdda);
|
||||
song.set_filetype(Song::Type_CDDA);
|
||||
song.set_url(GetUrlFromTrack(track_number));
|
||||
song.set_title(QString("Track %1").arg(track_number));
|
||||
song.set_track(track_number);
|
||||
@ -207,7 +207,7 @@ void CddaSongLoader::AudioCDTagsLoaded(const QString &artist, const QString &alb
|
||||
song.set_track(track_number);
|
||||
song.set_year(ret.year_);
|
||||
song.set_id(track_number);
|
||||
song.set_filetype(Song::Type_Cdda);
|
||||
song.set_filetype(Song::Type_CDDA);
|
||||
song.set_valid(true);
|
||||
// We need to set url: that's how playlist will find the correct item to update
|
||||
song.set_url(GetUrlFromTrack(track_number++));
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@ -248,8 +248,8 @@ void GPodDevice::FinishDelete(bool success) {
|
||||
}
|
||||
|
||||
bool GPodDevice::GetSupportedFiletypes(QList<Song::FileType> *ret) {
|
||||
*ret << Song::Type_Mp4;
|
||||
*ret << Song::Type_Mpeg;
|
||||
*ret << Song::Type_MP4;
|
||||
*ret << Song::Type_MPEG;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@ -209,15 +209,15 @@ bool MtpDevice::GetSupportedFiletypes(QList<Song::FileType> *ret, LIBMTP_mtpdevi
|
||||
|
||||
for (int i = 0; i < length; ++i) {
|
||||
switch (LIBMTP_filetype_t(list[i])) {
|
||||
case LIBMTP_FILETYPE_WAV: *ret << Song::Type_Wav; break;
|
||||
case LIBMTP_FILETYPE_WAV: *ret << Song::Type_WAV; break;
|
||||
case LIBMTP_FILETYPE_MP2:
|
||||
case LIBMTP_FILETYPE_MP3: *ret << Song::Type_Mpeg; break;
|
||||
case LIBMTP_FILETYPE_WMA: *ret << Song::Type_Asf; break;
|
||||
case LIBMTP_FILETYPE_MP3: *ret << Song::Type_MPEG; break;
|
||||
case LIBMTP_FILETYPE_WMA: *ret << Song::Type_ASF; break;
|
||||
case LIBMTP_FILETYPE_MP4:
|
||||
case LIBMTP_FILETYPE_M4A:
|
||||
case LIBMTP_FILETYPE_AAC: *ret << Song::Type_Mp4; break;
|
||||
case LIBMTP_FILETYPE_AAC: *ret << Song::Type_MP4; break;
|
||||
case LIBMTP_FILETYPE_FLAC:
|
||||
*ret << Song::Type_Flac;
|
||||
*ret << Song::Type_FLAC;
|
||||
*ret << Song::Type_OggFlac;
|
||||
break;
|
||||
case LIBMTP_FILETYPE_OGG:
|
||||
|
@ -786,7 +786,6 @@ void GstEngine::StartFadeoutPause() {
|
||||
|
||||
void GstEngine::StartTimers() {
|
||||
StopTimers();
|
||||
|
||||
timer_id_ = startTimer(kTimerIntervalNanosec / kNsecPerMsec);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
43
src/internet/internetmimedata.h
Normal file
43
src/internet/internetmimedata.h
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef INTERNETMIMEDATA_H
|
||||
#define INTERNETMIMEDATA_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QModelIndexList>
|
||||
|
||||
#include "core/mimedata.h"
|
||||
|
||||
class InternetModel;
|
||||
|
||||
class InternetMimeData : public MimeData {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit InternetMimeData(const InternetModel *_model) : model(_model) {}
|
||||
|
||||
const InternetModel *model;
|
||||
QModelIndexList indexes;
|
||||
};
|
||||
|
||||
#endif
|
83
src/internet/internetmodel.cpp
Normal file
83
src/internet/internetmodel.cpp
Normal file
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QStandardItemModel>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "internetmodel.h"
|
||||
#include "internetservice.h"
|
||||
#include "tidal/tidalservice.h"
|
||||
|
||||
QMap<QString, InternetService*>* InternetModel::sServices = nullptr;
|
||||
|
||||
InternetModel::InternetModel(Application *app, QObject *parent)
|
||||
: QStandardItemModel(parent),
|
||||
app_(app) {
|
||||
|
||||
if (!sServices) sServices = new QMap<QString, InternetService*>;
|
||||
Q_ASSERT(sServices->isEmpty());
|
||||
AddService(new TidalService(app, this));
|
||||
|
||||
}
|
||||
|
||||
void InternetModel::AddService(InternetService *service) {
|
||||
|
||||
qLog(Debug) << "Adding internet service:" << service->name();
|
||||
sServices->insert(service->name(), service);
|
||||
connect(service, SIGNAL(destroyed()), SLOT(ServiceDeleted()));
|
||||
if (service->has_initial_load_settings()) service->InitialLoadSettings();
|
||||
else service->ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
void InternetModel::RemoveService(InternetService *service) {
|
||||
|
||||
if (!sServices->contains(service->name())) return;
|
||||
sServices->remove(service->name());
|
||||
disconnect(service, 0, this, 0);
|
||||
|
||||
}
|
||||
|
||||
void InternetModel::ServiceDeleted() {
|
||||
|
||||
InternetService *service = qobject_cast<InternetService*>(sender());
|
||||
if (service) RemoveService(service);
|
||||
|
||||
}
|
||||
|
||||
InternetService *InternetModel::ServiceByName(const QString &name) {
|
||||
|
||||
if (sServices->contains(name)) return sServices->value(name);
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
|
||||
void InternetModel::ReloadSettings() {
|
||||
for (InternetService *service : sServices->values()) {
|
||||
service->ReloadSettings();
|
||||
}
|
||||
}
|
132
src/internet/internetmodel.h
Normal file
132
src/internet/internetmodel.h
Normal file
@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef INTERNETMODEL_H
|
||||
#define INTERNETMODEL_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QStandardItemModel>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "playlist/playlistitem.h"
|
||||
#include "settings/settingsdialog.h"
|
||||
#include "widgets/multiloadingindicator.h"
|
||||
|
||||
class Application;
|
||||
class InternetService;
|
||||
|
||||
class InternetModel : public QStandardItemModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit InternetModel(Application* app, QObject *parent = nullptr);
|
||||
|
||||
enum Role {
|
||||
// Services can use this role to distinguish between different types of items that they add.
|
||||
// The root item's type is automatically set to Type_Service,
|
||||
// but apart from that Services are free to define their own values for this field (starting from TypeCount).
|
||||
Role_Type = Qt::UserRole + 1000,
|
||||
|
||||
// If this is not set the item is not playable (ie. it can't be dragged to the playlist).
|
||||
// Otherwise it describes how this item is converted to playlist items.
|
||||
// See the PlayBehaviour enum for more details.
|
||||
Role_PlayBehaviour,
|
||||
|
||||
// The URL of the media for this item. This is required if the PlayBehaviour is set to PlayBehaviour_UseSongLoader.
|
||||
Role_Url,
|
||||
|
||||
// The metadata used in the item that is added to the playlist if the PlayBehaviour is set to PlayBehaviour_SingleItem. Ignored otherwise.
|
||||
Role_SongMetadata,
|
||||
|
||||
// If this is set to true then the model will call the service's LazyPopulate method when this item is expanded.
|
||||
// Use this if your item's children have to be downloaded or fetched remotely.
|
||||
Role_CanLazyLoad,
|
||||
|
||||
// This is automatically set on the root item for a service. It contains a pointer to an InternetService.
|
||||
// Services should not set this field themselves.
|
||||
Role_Service,
|
||||
|
||||
// Setting this to true means that the item can be changed by user action (e.g. changing remote playlists)
|
||||
Role_CanBeModified,
|
||||
RoleCount,
|
||||
Role_IsDivider = CollectionModel::Role_IsDivider,
|
||||
};
|
||||
|
||||
enum Type {
|
||||
Type_Service = 1,
|
||||
Type_Track,
|
||||
Type_UserPlaylist,
|
||||
TypeCount
|
||||
};
|
||||
|
||||
enum PlayBehaviour {
|
||||
// The item can't be played. This is the default.
|
||||
PlayBehaviour_None = 0,
|
||||
|
||||
// This item's URL is passed through the normal song loader.
|
||||
// This supports loading remote playlists, remote files and local files.
|
||||
// This is probably the most sensible behaviour to use if you're just returning normal radio stations.
|
||||
PlayBehaviour_UseSongLoader,
|
||||
|
||||
// This item's URL, Title and Artist are used in the playlist. No special behaviour occurs
|
||||
// The URL is just passed straight to gstreamer when the user starts playing.
|
||||
PlayBehaviour_SingleItem,
|
||||
|
||||
// This item's children have PlayBehaviour_SingleItem set.
|
||||
// This is used when dragging a playlist item for instance, to have all the playlit's items info loaded in the mime data.
|
||||
PlayBehaviour_MultipleItems,
|
||||
|
||||
// This item might not represent a song - the service's ItemDoubleClicked() slot will get called instead to do some custom action.
|
||||
PlayBehaviour_DoubleClickAction,
|
||||
};
|
||||
|
||||
// Needs to be static for InternetPlaylistItem::restore
|
||||
static InternetService *ServiceByName(const QString &name);
|
||||
|
||||
template <typename T>
|
||||
static T *Service() {
|
||||
return static_cast<T*>(ServiceByName(T::kServiceName));
|
||||
}
|
||||
|
||||
// Add and remove services. Ownership is not transferred and the service is not reparented.
|
||||
// If the service is deleted it will be automatically removed from the model.
|
||||
void AddService(InternetService *service);
|
||||
void RemoveService(InternetService *service);
|
||||
void ReloadSettings();
|
||||
|
||||
Application *app() const { return app_; }
|
||||
|
||||
private slots:
|
||||
void ServiceDeleted();
|
||||
|
||||
private:
|
||||
static QMap<QString, InternetService*> *sServices;
|
||||
Application *app_;
|
||||
|
||||
};
|
||||
|
||||
#endif
|
106
src/internet/internetplaylistitem.cpp
Normal file
106
src/internet/internetplaylistitem.cpp
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QSettings>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "internetplaylistitem.h"
|
||||
#include "internetservice.h"
|
||||
#include "internetmodel.h"
|
||||
#include "core/settingsprovider.h"
|
||||
#include "collection/sqlrow.h"
|
||||
#include "playlist/playlistbackend.h"
|
||||
|
||||
InternetPlaylistItem::InternetPlaylistItem(const QString &type)
|
||||
: PlaylistItem(type), set_service_icon_(false) {}
|
||||
|
||||
InternetPlaylistItem::InternetPlaylistItem(InternetService *service, const Song &metadata)
|
||||
: PlaylistItem("Internet"),
|
||||
service_name_(service->name()),
|
||||
set_service_icon_(false),
|
||||
metadata_(metadata) {
|
||||
InitMetadata();
|
||||
}
|
||||
|
||||
bool InternetPlaylistItem::InitFromQuery(const SqlRow &query) {
|
||||
|
||||
// The song tables gets joined first, plus one each for the song ROWIDs
|
||||
const int row = (Song::kColumns.count() + 1) * PlaylistBackend::kSongTableJoins;
|
||||
|
||||
service_name_ = query.value(row + 1).toString();
|
||||
|
||||
metadata_.InitFromQuery(query, false, (Song::kColumns.count() + 1) * 1);
|
||||
InitMetadata();
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
InternetService *InternetPlaylistItem::service() const {
|
||||
|
||||
InternetService *ret = InternetModel::ServiceByName(service_name_);
|
||||
|
||||
if (ret && !set_service_icon_) {
|
||||
const_cast<InternetPlaylistItem*>(this)->set_service_icon_ = true;
|
||||
|
||||
QString icon = ret->Icon();
|
||||
if (!icon.isEmpty()) {
|
||||
const_cast<InternetPlaylistItem*>(this)->metadata_.set_art_manual(icon);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QVariant InternetPlaylistItem::DatabaseValue(DatabaseColumn column) const {
|
||||
switch (column) {
|
||||
case Column_InternetService:
|
||||
return service_name_;
|
||||
default:
|
||||
return PlaylistItem::DatabaseValue(column);
|
||||
}
|
||||
}
|
||||
|
||||
void InternetPlaylistItem::InitMetadata() {
|
||||
|
||||
if (metadata_.title().isEmpty())
|
||||
metadata_.set_title(metadata_.url().toString());
|
||||
metadata_.set_filetype(Song::Type_Stream);
|
||||
metadata_.set_valid(true);
|
||||
|
||||
}
|
||||
|
||||
Song InternetPlaylistItem::Metadata() const {
|
||||
if (!set_service_icon_) {
|
||||
// Get the icon if we don't have it already
|
||||
service();
|
||||
}
|
||||
|
||||
if (HasTemporaryMetadata()) return temp_metadata_;
|
||||
return metadata_;
|
||||
}
|
||||
|
||||
QUrl InternetPlaylistItem::Url() const { return metadata_.url(); }
|
58
src/internet/internetplaylistitem.h
Normal file
58
src/internet/internetplaylistitem.h
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef INTERNETPLAYLISTITEM_H
|
||||
#define INTERNETPLAYLISTITEM_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "playlist/playlistitem.h"
|
||||
|
||||
class InternetService;
|
||||
|
||||
class InternetPlaylistItem : public PlaylistItem {
|
||||
|
||||
public:
|
||||
explicit InternetPlaylistItem(const QString &type);
|
||||
InternetPlaylistItem(InternetService *service, const Song &metadata);
|
||||
bool InitFromQuery(const SqlRow &query);
|
||||
Song Metadata() const;
|
||||
QUrl Url() const;
|
||||
|
||||
protected:
|
||||
QVariant DatabaseValue(DatabaseColumn) const;
|
||||
Song DatabaseSongMetadata() const { return metadata_; }
|
||||
|
||||
private:
|
||||
void InitMetadata();
|
||||
InternetService *service() const;
|
||||
|
||||
private:
|
||||
QString service_name_;
|
||||
bool set_service_icon_;
|
||||
Song metadata_;
|
||||
};
|
||||
|
||||
#endif
|
32
src/internet/internetservice.cpp
Normal file
32
src/internet/internetservice.cpp
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <QObject>
|
||||
#include <QStandardItem>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/mimedata.h"
|
||||
#include "internetmodel.h"
|
||||
#include "internetservice.h"
|
||||
|
||||
InternetService::InternetService(const QString &name, Application *app, InternetModel *model, QObject *parent)
|
||||
: QObject(parent), app_(app), model_(model), name_(name) {
|
||||
}
|
64
src/internet/internetservice.h
Normal file
64
src/internet/internetservice.h
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef INTERNETSERVICE_H
|
||||
#define INTERNETSERVICE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QStandardItem>
|
||||
#include <QAction>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "playlist/playlistitem.h"
|
||||
#include "settings/settingsdialog.h"
|
||||
|
||||
class Application;
|
||||
class InternetModel;
|
||||
class CollectionFilterWidget;
|
||||
|
||||
class InternetService : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
InternetService(const QString &name, Application *app, InternetModel *model, QObject *parent = nullptr);
|
||||
virtual ~InternetService() {}
|
||||
QString name() const { return name_; }
|
||||
InternetModel *model() const { return model_; }
|
||||
virtual bool has_initial_load_settings() const { return false; }
|
||||
virtual void InitialLoadSettings() {}
|
||||
virtual void ReloadSettings() {}
|
||||
virtual QString Icon() { return QString(); }
|
||||
|
||||
public slots:
|
||||
virtual void ShowConfig() {}
|
||||
|
||||
protected:
|
||||
Application *app_;
|
||||
private:
|
||||
InternetModel *model_;
|
||||
QString name_;
|
||||
|
||||
};
|
||||
Q_DECLARE_METATYPE(InternetService*);
|
||||
|
||||
#endif
|
39
src/internet/internetsongmimedata.h
Normal file
39
src/internet/internetsongmimedata.h
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef INTERNETSONGMIMEDATA_H
|
||||
#define INTERNETSONGMIMEDATA_H
|
||||
|
||||
#include "core/mimedata.h"
|
||||
#include "core/song.h"
|
||||
|
||||
class InternetService;
|
||||
|
||||
class InternetSongMimeData : public MimeData {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit InternetSongMimeData(InternetService *_service) : service(_service) {}
|
||||
|
||||
InternetService *service;
|
||||
SongList songs;
|
||||
};
|
||||
|
||||
#endif
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@ -84,6 +84,11 @@
|
||||
#include "songplaylistitem.h"
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
#include "internet/internetmodel.h"
|
||||
#include "internet/internetplaylistitem.h"
|
||||
#include "internet/internetmimedata.h"
|
||||
#include "internet/internetsongmimedata.h"
|
||||
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::shared_ptr;
|
||||
@ -153,7 +158,7 @@ Playlist::~Playlist() {
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void Playlist::InsertSongItems(const SongList &songs, int pos, bool play_now, bool enqueue) {
|
||||
void Playlist::InsertSongItems(const SongList &songs, int pos, bool play_now, bool enqueue, bool enqueue_next) {
|
||||
|
||||
PlaylistItemList items;
|
||||
|
||||
@ -161,7 +166,7 @@ void Playlist::InsertSongItems(const SongList &songs, int pos, bool play_now, bo
|
||||
items << PlaylistItemPtr(new T(song));
|
||||
}
|
||||
|
||||
InsertItems(items, pos, play_now, enqueue);
|
||||
InsertItems(items, pos, play_now, enqueue, enqueue_next);
|
||||
|
||||
}
|
||||
|
||||
@ -282,16 +287,15 @@ QVariant Playlist::data(const QModelIndex &index, int role) const {
|
||||
case Column_AlbumArtist: return song.playlist_albumartist();
|
||||
case Column_Composer: return song.composer();
|
||||
case Column_Performer: return song.performer();
|
||||
case Column_Grouping: return song.grouping();
|
||||
case Column_Grouping: return song.grouping();
|
||||
|
||||
case Column_PlayCount: return song.playcount();
|
||||
case Column_SkipCount: return song.skipcount();
|
||||
case Column_LastPlayed: return song.lastplayed();
|
||||
|
||||
case Column_Samplerate: return song.samplerate();
|
||||
case Column_Bitdepth: return song.bitdepth();
|
||||
case Column_Bitrate: return song.bitrate();
|
||||
case Column_SamplerateBitdepth: return song.SampleRateBitDepthToText();
|
||||
case Column_Bitdepth: return song.bitdepth();
|
||||
case Column_Bitrate: return song.bitrate();
|
||||
|
||||
case Column_Filename: return song.url();
|
||||
case Column_BaseFilename: return song.basefilename();
|
||||
@ -304,7 +308,7 @@ QVariant Playlist::data(const QModelIndex &index, int role) const {
|
||||
if (role == Qt::DisplayRole) return song.comment().simplified();
|
||||
return song.comment();
|
||||
|
||||
//case Column_Source: return item->Url();
|
||||
case Column_Source: return item->Url();
|
||||
|
||||
}
|
||||
|
||||
@ -323,9 +327,7 @@ QVariant Playlist::data(const QModelIndex &index, int role) const {
|
||||
if (items_[index.row()]->HasCurrentForegroundColor()) {
|
||||
return QBrush(items_[index.row()]->GetCurrentForegroundColor());
|
||||
}
|
||||
//if (index.row() < dynamic_history_length()) {
|
||||
//return QBrush(kDynamicHistoryColor);
|
||||
//}
|
||||
|
||||
return QVariant();
|
||||
|
||||
case Qt::BackgroundRole:
|
||||
@ -562,7 +564,7 @@ int Playlist::previous_row(bool ignore_repeat_track) const {
|
||||
void Playlist::set_current_row(int i, bool is_stopping) {
|
||||
|
||||
QModelIndex old_current_item_index = current_item_index_;
|
||||
//ClearStreamMetadata();
|
||||
ClearStreamMetadata();
|
||||
|
||||
current_item_index_ = QPersistentModelIndex(index(i, 0, QModelIndex()));
|
||||
|
||||
@ -636,6 +638,7 @@ bool Playlist::dropMimeData(const QMimeData *data, Qt::DropAction action, int ro
|
||||
|
||||
bool play_now = false;
|
||||
bool enqueue_now = false;
|
||||
bool enqueue_next_now = false;
|
||||
|
||||
if (const MimeData *mime_data = qobject_cast<const MimeData*>(data)) {
|
||||
if (mime_data->clear_first_) {
|
||||
@ -643,6 +646,7 @@ bool Playlist::dropMimeData(const QMimeData *data, Qt::DropAction action, int ro
|
||||
}
|
||||
play_now = mime_data->play_now_;
|
||||
enqueue_now = mime_data->enqueue_now_;
|
||||
enqueue_next_now = mime_data->enqueue_next_now_;
|
||||
}
|
||||
|
||||
if (const SongMimeData *song_data = qobject_cast<const SongMimeData*>(data)) {
|
||||
@ -651,11 +655,13 @@ bool Playlist::dropMimeData(const QMimeData *data, Qt::DropAction action, int ro
|
||||
if (song_data->backend && song_data->backend->songs_table() == SCollection::kSongsTable)
|
||||
InsertSongItems<CollectionPlaylistItem>(song_data->songs, row, play_now, enqueue_now);
|
||||
else
|
||||
InsertSongItems<SongPlaylistItem>(song_data->songs, row, play_now, enqueue_now);
|
||||
|
||||
InsertSongItems<SongPlaylistItem>(song_data->songs, row, play_now, enqueue_now, enqueue_next_now);
|
||||
}
|
||||
else if (const PlaylistItemMimeData *item_data = qobject_cast<const PlaylistItemMimeData*>(data)) {
|
||||
InsertItems(item_data->items_, row, play_now, enqueue_now);
|
||||
InsertItems(item_data->items_, row, play_now, enqueue_now, enqueue_next_now);
|
||||
}
|
||||
else if (const InternetSongMimeData* internet_song_data = qobject_cast<const InternetSongMimeData*>(data)) {
|
||||
InsertInternetItems(internet_song_data->service, internet_song_data->songs, row, play_now, enqueue_now, enqueue_next_now);
|
||||
}
|
||||
else if (data->hasFormat(kRowsMimetype)) {
|
||||
// Dragged from the playlist
|
||||
@ -719,7 +725,7 @@ bool Playlist::dropMimeData(const QMimeData *data, Qt::DropAction action, int ro
|
||||
|
||||
}
|
||||
|
||||
void Playlist::InsertUrls(const QList<QUrl> &urls, int pos, bool play_now, bool enqueue) {
|
||||
void Playlist::InsertUrls(const QList<QUrl> &urls, int pos, bool play_now, bool enqueue, bool enqueue_next) {
|
||||
|
||||
SongLoaderInserter *inserter = new SongLoaderInserter(task_manager_, collection_, backend_->app()->player());
|
||||
connect(inserter, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
|
||||
@ -832,7 +838,7 @@ void Playlist::MoveItemsWithoutUndo(int start, const QList<int> &dest_rows) {
|
||||
|
||||
}
|
||||
|
||||
void Playlist::InsertItems(const PlaylistItemList &itemsIn, int pos, bool play_now, bool enqueue) {
|
||||
void Playlist::InsertItems(const PlaylistItemList &itemsIn, int pos, bool play_now, bool enqueue, bool enqueue_next) {
|
||||
|
||||
if (itemsIn.isEmpty())
|
||||
return;
|
||||
@ -932,25 +938,37 @@ void Playlist::InsertItemsWithoutUndo(const PlaylistItemList &items, int pos, bo
|
||||
|
||||
}
|
||||
|
||||
void Playlist::InsertCollectionItems(const SongList &songs, int pos, bool play_now, bool enqueue) {
|
||||
InsertSongItems<CollectionPlaylistItem>(songs, pos, play_now, enqueue);
|
||||
void Playlist::InsertCollectionItems(const SongList &songs, int pos, bool play_now, bool enqueue, bool enqueue_next) {
|
||||
InsertSongItems<CollectionPlaylistItem>(songs, pos, play_now, enqueue, enqueue_next);
|
||||
}
|
||||
|
||||
void Playlist::InsertSongs(const SongList &songs, int pos, bool play_now, bool enqueue) {
|
||||
InsertSongItems<SongPlaylistItem>(songs, pos, play_now, enqueue);
|
||||
void Playlist::InsertSongs(const SongList &songs, int pos, bool play_now, bool enqueue, bool enqueue_next) {
|
||||
InsertSongItems<SongPlaylistItem>(songs, pos, play_now, enqueue, enqueue_next);
|
||||
}
|
||||
|
||||
void Playlist::InsertSongsOrCollectionItems(const SongList &songs, int pos, bool play_now, bool enqueue) {
|
||||
void Playlist::InsertSongsOrCollectionItems(const SongList &songs, int pos, bool play_now, bool enqueue, bool enqueue_next) {
|
||||
|
||||
PlaylistItemList items;
|
||||
for (const Song &song : songs) {
|
||||
if (song.is_collection_song()) {
|
||||
items << PlaylistItemPtr(new CollectionPlaylistItem(song));
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
items << PlaylistItemPtr(new SongPlaylistItem(song));
|
||||
}
|
||||
}
|
||||
InsertItems(items, pos, play_now, enqueue);
|
||||
InsertItems(items, pos, play_now, enqueue, enqueue_next);
|
||||
|
||||
}
|
||||
|
||||
void Playlist::InsertInternetItems(InternetService *service, const SongList &songs, int pos, bool play_now, bool enqueue, bool enqueue_next) {
|
||||
|
||||
PlaylistItemList playlist_items;
|
||||
for (const Song &song : songs) {
|
||||
playlist_items << shared_ptr<PlaylistItem>(new InternetPlaylistItem(service, song));
|
||||
}
|
||||
|
||||
InsertItems(playlist_items, pos, play_now, enqueue, enqueue_next);
|
||||
|
||||
}
|
||||
|
||||
@ -973,8 +991,10 @@ void Playlist::UpdateItems(const SongList &songs) {
|
||||
PlaylistItemPtr &item = items_[i];
|
||||
if (item->Metadata().url() == song.url() &&
|
||||
(item->Metadata().filetype() == Song::Type_Unknown ||
|
||||
// Stream may change and may need to be updated too
|
||||
item->Metadata().filetype() == Song::Type_Stream ||
|
||||
// And CD tracks as well (tags are loaded in a second step)
|
||||
item->Metadata().filetype() == Song::Type_Cdda)) {
|
||||
item->Metadata().filetype() == Song::Type_CDDA)) {
|
||||
PlaylistItemPtr new_item;
|
||||
if (song.is_collection_song()) {
|
||||
new_item = PlaylistItemPtr(new CollectionPlaylistItem(song));
|
||||
@ -1069,9 +1089,7 @@ bool Playlist::CompareItems(int column, Qt::SortOrder order, shared_ptr<Playlist
|
||||
|
||||
case Column_Bitrate: cmp(bitrate);
|
||||
case Column_Samplerate: cmp(samplerate);
|
||||
case Column_Bitdepth: cmp(bitdepth);
|
||||
case Column_SamplerateBitdepth:
|
||||
return QString::localeAwareCompare(a->Metadata().SampleRateBitDepthToText().toLower(), b->Metadata().SampleRateBitDepthToText().toLower()) < 0;
|
||||
case Column_Bitdepth: cmp(bitdepth);
|
||||
case Column_Filename:
|
||||
return (QString::localeAwareCompare(a->Url().path().toLower(), b->Url().path().toLower()) < 0);
|
||||
case Column_BaseFilename: cmp(basefilename);
|
||||
@ -1081,7 +1099,7 @@ bool Playlist::CompareItems(int column, Qt::SortOrder order, shared_ptr<Playlist
|
||||
case Column_DateCreated: cmp(ctime);
|
||||
|
||||
case Column_Comment: strcmp(comment);
|
||||
//case Column_Source: cmp(url);
|
||||
case Column_Source: cmp(url);
|
||||
}
|
||||
|
||||
#undef cmp
|
||||
@ -1126,7 +1144,6 @@ QString Playlist::column_name(Column column) {
|
||||
|
||||
case Column_Samplerate: return tr("Sample rate");
|
||||
case Column_Bitdepth: return tr("Bit depth");
|
||||
case Column_SamplerateBitdepth: return tr("Sample rate B");
|
||||
case Column_Bitrate: return tr("Bitrate");
|
||||
|
||||
case Column_Filename: return tr("File name");
|
||||
@ -1137,7 +1154,7 @@ QString Playlist::column_name(Column column) {
|
||||
case Column_DateCreated: return tr("Date created");
|
||||
|
||||
case Column_Comment: return tr("Comment");
|
||||
//case Column_Source: return tr("Source");
|
||||
case Column_Source: return tr("Source");
|
||||
default: return QString();
|
||||
}
|
||||
return "";
|
||||
@ -1757,6 +1774,7 @@ void Playlist::InvalidateDeletedSongs() {
|
||||
PlaylistItemPtr item = items_[row];
|
||||
Song song = item->Metadata();
|
||||
|
||||
if (!song.is_stream()) {
|
||||
bool exists = QFile::exists(song.url().toLocalFile());
|
||||
|
||||
if (!exists && !item->HasForegroundColor(kInvalidSongPriority)) {
|
||||
@ -1768,6 +1786,7 @@ void Playlist::InvalidateDeletedSongs() {
|
||||
item->RemoveForegroundColor(kInvalidSongPriority);
|
||||
invalidated_rows.append(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReloadItems(invalidated_rows);
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef PLAYLIST_H
|
||||
@ -54,6 +54,8 @@ class PlaylistBackend;
|
||||
class PlaylistFilter;
|
||||
class Queue;
|
||||
class TaskManager;
|
||||
class InternetModel;
|
||||
class InternetService;
|
||||
|
||||
namespace PlaylistUndoCommands {
|
||||
class InsertItems;
|
||||
@ -110,7 +112,6 @@ class Playlist : public QAbstractListModel {
|
||||
Column_Genre,
|
||||
Column_Samplerate,
|
||||
Column_Bitdepth,
|
||||
Column_SamplerateBitdepth,
|
||||
Column_Bitrate,
|
||||
Column_Filename,
|
||||
Column_BaseFilename,
|
||||
@ -123,6 +124,7 @@ class Playlist : public QAbstractListModel {
|
||||
Column_LastPlayed,
|
||||
Column_Comment,
|
||||
Column_Grouping,
|
||||
Column_Source,
|
||||
ColumnCount
|
||||
};
|
||||
|
||||
@ -212,10 +214,11 @@ class Playlist : public QAbstractListModel {
|
||||
QUndoStack *undo_stack() const { return undo_stack_; }
|
||||
|
||||
// Changing the playlist
|
||||
void InsertItems (const PlaylistItemList &items, int pos = -1, bool play_now = false, bool enqueue = false);
|
||||
void InsertCollectionItems (const SongList &items, int pos = -1, bool play_now = false, bool enqueue = false);
|
||||
void InsertSongs (const SongList &items, int pos = -1, bool play_now = false, bool enqueue = false);
|
||||
void InsertSongsOrCollectionItems (const SongList &items, int pos = -1, bool play_now = false, bool enqueue = false);
|
||||
void InsertItems (const PlaylistItemList &items, int pos = -1, bool play_now = false, bool enqueue = false, bool enqueue_next = false);
|
||||
void InsertCollectionItems (const SongList &items, int pos = -1, bool play_now = false, bool enqueue = false, bool enqueue_next = false);
|
||||
void InsertSongs (const SongList &items, int pos = -1, bool play_now = false, bool enqueue = false, bool enqueue_next = false);
|
||||
void InsertSongsOrCollectionItems (const SongList &items, int pos = -1, bool play_now = false, bool enqueue = false, bool enqueue_next = false);
|
||||
void InsertInternetItems(InternetService* service, const SongList& songs, int pos = -1, bool play_now = false, bool enqueue = false, bool enqueue_next = false);
|
||||
|
||||
void ReshuffleIndices();
|
||||
|
||||
@ -276,7 +279,7 @@ class Playlist : public QAbstractListModel {
|
||||
|
||||
void SetColumnAlignment(const ColumnAlignmentMap &alignment);
|
||||
|
||||
void InsertUrls(const QList<QUrl> &urls, int pos = -1, bool play_now = false, bool enqueue = false);
|
||||
void InsertUrls(const QList<QUrl> &urls, int pos = -1, bool play_now = false, bool enqueue = false, bool enqueue_next = false);
|
||||
// Removes items with given indices from the playlist. This operation is not undoable.
|
||||
void RemoveItemsWithoutUndo(const QList<int> &indices);
|
||||
|
||||
@ -302,7 +305,7 @@ private:
|
||||
bool FilterContainsVirtualIndex(int i) const;
|
||||
|
||||
template <typename T>
|
||||
void InsertSongItems(const SongList &songs, int pos, bool play_now, bool enqueue);
|
||||
void InsertSongItems(const SongList &songs, int pos, bool play_now, bool enqueue, bool enqueue_next = false);
|
||||
|
||||
// Modify the playlist without changing the undo stack. These are used by our friends in PlaylistUndoCommands
|
||||
void InsertItemsWithoutUndo(const PlaylistItemList &items, int pos, bool enqueue = false);
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include <memory>
|
||||
@ -145,7 +145,7 @@ QSqlQuery PlaylistBackend::GetPlaylistRows(int playlist) {
|
||||
" p.ROWID, " +
|
||||
Song::JoinSpec("p") +
|
||||
","
|
||||
" p.type"
|
||||
" p.type, p.internet_service"
|
||||
" FROM playlist_items AS p"
|
||||
" LEFT JOIN songs"
|
||||
" ON p.collection_id = songs.ROWID"
|
||||
@ -279,7 +279,7 @@ void PlaylistBackend::SavePlaylist(int playlist, const PlaylistItemList &items,
|
||||
QSqlQuery clear(db);
|
||||
clear.prepare("DELETE FROM playlist_items WHERE playlist = :playlist");
|
||||
QSqlQuery insert(db);
|
||||
insert.prepare("INSERT INTO playlist_items (playlist, type, collection_id, " + Song::kColumnSpec + ") VALUES (:playlist, :type, :collection_id, " + Song::kBindSpec + ")");
|
||||
insert.prepare("INSERT INTO playlist_items (playlist, type, collection_id, internet_service, " + Song::kColumnSpec + ") VALUES (:playlist, :type, :collection_id, :internet_service, " + Song::kBindSpec + ")");
|
||||
QSqlQuery update(db);
|
||||
update.prepare("UPDATE playlists SET last_played=:last_played WHERE ROWID=:playlist");
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@ -39,6 +39,7 @@
|
||||
#include <QString>
|
||||
#include <QStringBuilder>
|
||||
#include <QUrl>
|
||||
#include <QRegExp>
|
||||
#include <QIcon>
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
@ -460,6 +461,12 @@ QPixmap SongSourceDelegate::LookupPixmap(const QUrl &url, const QSize &size) con
|
||||
else if (url.scheme() == "cdda") {
|
||||
icon = IconLoader::Load("cd");
|
||||
}
|
||||
else if (url.scheme() == "http" || url.scheme() == "https") {
|
||||
if (url.host().contains(QRegExp(".*.tidal.com")))
|
||||
icon = IconLoader::Load("tidal");
|
||||
else
|
||||
icon = IconLoader::Load("download");
|
||||
}
|
||||
else {
|
||||
icon = IconLoader::Load("folder-sound");
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@ -445,9 +445,6 @@ FilterTree *FilterParser::createSearchTermTreeNode(
|
||||
if (columns_[col] == Playlist::Column_Length) {
|
||||
search_value = parseTime(search);
|
||||
}
|
||||
//else if (columns_[col] == Playlist::Column_Rating) {
|
||||
//search_value = static_cast<int>(search.toDouble() * 2.0 + 0.5);
|
||||
//}
|
||||
else {
|
||||
search_value = search.toInt();
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@ -37,6 +37,8 @@
|
||||
#include "playlistitem.h"
|
||||
#include "songplaylistitem.h"
|
||||
|
||||
#include "internet/internetplaylistitem.h"
|
||||
|
||||
PlaylistItem::~PlaylistItem() {
|
||||
}
|
||||
|
||||
@ -44,11 +46,13 @@ PlaylistItem* PlaylistItem::NewFromType(const QString &type) {
|
||||
|
||||
if (type == "Collection") return new CollectionPlaylistItem(type);
|
||||
else if (type == "File") return new SongPlaylistItem(type);
|
||||
|
||||
else if (type == "Internet") return new InternetPlaylistItem("Internet");
|
||||
else if (type == "Tidal") return new InternetPlaylistItem("Tidal");
|
||||
|
||||
qLog(Warning) << "Invalid PlaylistItem type:" << type;
|
||||
|
||||
return nullptr;
|
||||
|
||||
|
||||
}
|
||||
|
||||
PlaylistItem* PlaylistItem::NewFromSongsTable(const QString &table, const Song &song) {
|
||||
@ -65,6 +69,7 @@ void PlaylistItem::BindToQuery(QSqlQuery *query) const {
|
||||
|
||||
query->bindValue(":type", type());
|
||||
query->bindValue(":collection_id", DatabaseValue(Column_CollectionId));
|
||||
query->bindValue(":internet_service", DatabaseValue(Column_InternetService));
|
||||
|
||||
DatabaseSongMetadata().BindToQuery(query);
|
||||
|
||||
@ -119,3 +124,4 @@ bool PlaylistItem::HasCurrentForegroundColor() const {
|
||||
}
|
||||
void PlaylistItem::SetShouldSkip(bool val) { should_skip_ = val; }
|
||||
bool PlaylistItem::GetShouldSkip() const { return should_skip_; }
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef PLAYLISTITEM_H
|
||||
@ -104,7 +104,7 @@ class PlaylistItem : public std::enable_shared_from_this<PlaylistItem> {
|
||||
protected:
|
||||
bool should_skip_;
|
||||
|
||||
enum DatabaseColumn { Column_CollectionId, Column_InternetService, };
|
||||
enum DatabaseColumn { Column_CollectionId, Column_InternetService };
|
||||
|
||||
virtual QVariant DatabaseValue(DatabaseColumn) const {
|
||||
return QVariant(QVariant::String);
|
||||
@ -126,3 +126,4 @@ Q_DECLARE_METATYPE(QList<PlaylistItemPtr>)
|
||||
Q_DECLARE_OPERATORS_FOR_FLAGS(PlaylistItem::Options)
|
||||
|
||||
#endif // PLAYLISTITEM_H
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@ -228,20 +228,16 @@ void PlaylistView::SetItemDelegates(CollectionBackend *backend) {
|
||||
setItemDelegateForColumn(Playlist::Column_Samplerate, new PlaylistDelegateBase(this, ("Hz")));
|
||||
setItemDelegateForColumn(Playlist::Column_Bitdepth, new PlaylistDelegateBase(this, ("Bit")));
|
||||
setItemDelegateForColumn(Playlist::Column_Bitrate, new PlaylistDelegateBase(this, tr("kbps")));
|
||||
|
||||
setItemDelegateForColumn(Playlist::Column_SamplerateBitdepth, new SamplerateBitdepthItemDelegate(this));
|
||||
|
||||
setItemDelegateForColumn(Playlist::Column_Filename, new NativeSeparatorsDelegate(this));
|
||||
setItemDelegateForColumn(Playlist::Column_LastPlayed, new LastPlayedItemDelegate(this));
|
||||
|
||||
#if 0
|
||||
if (app_ && app_->player()) {
|
||||
setItemDelegateForColumn(Playlist::Column_Source, new SongSourceDelegate(this, app_->player()));
|
||||
}
|
||||
else {
|
||||
header_->HideSection(Playlist::Column_Source);
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
@ -946,7 +942,8 @@ void PlaylistView::ReloadSettings() {
|
||||
header_->SetColumnWidth(Playlist::Column_Album, 0.10);
|
||||
header_->SetColumnWidth(Playlist::Column_Length, 0.03);
|
||||
header_->SetColumnWidth(Playlist::Column_Bitrate, 0.07);
|
||||
header_->SetColumnWidth(Playlist::Column_SamplerateBitdepth, 0.07);
|
||||
header_->SetColumnWidth(Playlist::Column_Samplerate, 0.07);
|
||||
header_->SetColumnWidth(Playlist::Column_Bitdepth, 0.07);
|
||||
header_->SetColumnWidth(Playlist::Column_Filetype, 0.06);
|
||||
|
||||
setting_initial_header_layout_ = false;
|
||||
@ -1089,7 +1086,6 @@ ColumnAlignmentMap PlaylistView::DefaultColumnAlignment() {
|
||||
ret[Playlist::Column_Bitrate] =
|
||||
ret[Playlist::Column_Samplerate] =
|
||||
ret[Playlist::Column_Bitdepth] =
|
||||
ret[Playlist::Column_SamplerateBitdepth] =
|
||||
ret[Playlist::Column_Filesize] =
|
||||
ret[Playlist::Column_PlayCount] =
|
||||
ret[Playlist::Column_SkipCount] =
|
||||
@ -1216,8 +1212,7 @@ void PlaylistView::focusInEvent(QFocusEvent *event) {
|
||||
|
||||
QTreeView::focusInEvent(event);
|
||||
|
||||
if (event->reason() == Qt::TabFocusReason ||
|
||||
event->reason() == Qt::BacktabFocusReason) {
|
||||
if (event->reason() == Qt::TabFocusReason || event->reason() == Qt::BacktabFocusReason) {
|
||||
// If there's a current item but no selection it probably means the list was filtered, and the selected item does not match the filter.
|
||||
// If there's only 1 item in the view it is now impossible to select that item without using the mouse.
|
||||
const QModelIndex ¤t = selectionModel()->currentIndex();
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef PLAYLISTVIEW_H
|
||||
@ -75,7 +75,7 @@ class PlaylistHeader;
|
||||
// that uses Gtk to paint row backgrounds, ignoring any custom brush or palette the caller set in the QStyleOption.
|
||||
// That breaks our currently playing track animation, which relies on the background painted by Qt to be transparent.
|
||||
// This proxy style uses QCommonStyle to paint the affected elements.
|
||||
// This class is used by the global search view as well.
|
||||
// This class is used by tidal search view as well.
|
||||
class PlaylistProxyStyle : public QProxyStyle {
|
||||
public:
|
||||
PlaylistProxyStyle(QStyle *base);
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@ -62,6 +62,8 @@
|
||||
#include "playlistsettingspage.h"
|
||||
#include "shortcutssettingspage.h"
|
||||
#include "transcodersettingspage.h"
|
||||
#include "tidalsettingspage.h"
|
||||
|
||||
#include "ui_settingsdialog.h"
|
||||
|
||||
class QShowEvent;
|
||||
@ -122,6 +124,7 @@ SettingsDialog::SettingsDialog(Application *app, QWidget *parent)
|
||||
#ifdef HAVE_GSTREAMER
|
||||
AddPage(Page_Transcoding, new TranscoderSettingsPage(this), general);
|
||||
#endif
|
||||
AddPage(Page_Tidal, new TidalSettingsPage(this), general);
|
||||
|
||||
// User interface
|
||||
QTreeWidgetItem *iface = AddCategory(tr("User interface"));
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef SETTINGSDIALOG_H
|
||||
@ -79,6 +79,7 @@ public:
|
||||
Page_Notifications,
|
||||
Page_Proxy,
|
||||
Page_Transcoding,
|
||||
Page_Tidal,
|
||||
};
|
||||
|
||||
enum Role {
|
||||
|
118
src/settings/tidalsettingspage.cpp
Normal file
118
src/settings/tidalsettingspage.cpp
Normal file
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QSettings>
|
||||
#include <QMessageBox>
|
||||
#include <QEvent>
|
||||
|
||||
#include "tidalsettingspage.h"
|
||||
#include "ui_tidalsettingspage.h"
|
||||
#include "core/application.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "internet/internetmodel.h"
|
||||
#include "tidal/tidalservice.h"
|
||||
|
||||
const char *TidalSettingsPage::kSettingsGroup = "Tidal";
|
||||
|
||||
TidalSettingsPage::TidalSettingsPage(SettingsDialog *parent)
|
||||
: SettingsPage(parent),
|
||||
ui_(new Ui::TidalSettingsPage),
|
||||
service_(dialog()->app()->internet_model()->Service<TidalService>()) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
setWindowIcon(IconLoader::Load("tidal"));
|
||||
|
||||
connect(ui_->button_login, SIGNAL(clicked()), SLOT(LoginClicked()));
|
||||
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked()));
|
||||
|
||||
connect(service_, SIGNAL(LoginFailure(QString)), SLOT(LoginFailure(QString)));
|
||||
connect(service_, SIGNAL(LoginSuccess()), SLOT(LoginSuccess()));
|
||||
|
||||
dialog()->installEventFilter(this);
|
||||
|
||||
ui_->combobox_quality->addItem("Low", "LOW");
|
||||
ui_->combobox_quality->addItem("High", "HIGH");
|
||||
ui_->combobox_quality->addItem("Lossless", "LOSSLESS");
|
||||
|
||||
}
|
||||
|
||||
TidalSettingsPage::~TidalSettingsPage() { delete ui_; }
|
||||
|
||||
void TidalSettingsPage::Load() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
ui_->username->setText(s.value("username").toString());
|
||||
ui_->password->setText(s.value("password").toString());
|
||||
QString quality = s.value("quality", "HIGH").toString();
|
||||
ui_->combobox_quality->setCurrentIndex(ui_->combobox_quality->findData(quality));
|
||||
s.endGroup();
|
||||
|
||||
if (service_->authenticated()) ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn);
|
||||
|
||||
}
|
||||
|
||||
void TidalSettingsPage::Save() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("username", ui_->username->text());
|
||||
s.setValue("password", ui_->password->text());
|
||||
s.setValue("quality", ui_->combobox_quality->itemData(ui_->combobox_quality->currentIndex()));
|
||||
s.endGroup();
|
||||
|
||||
service_->ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
void TidalSettingsPage::LoginClicked() {
|
||||
service_->Login(ui_->username->text(), ui_->password->text());
|
||||
ui_->button_login->setEnabled(false);
|
||||
}
|
||||
|
||||
bool TidalSettingsPage::eventFilter(QObject *object, QEvent *event) {
|
||||
|
||||
if (object == dialog() && event->type() == QEvent::Enter) {
|
||||
ui_->button_login->setEnabled(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
return SettingsPage::eventFilter(object, event);
|
||||
}
|
||||
|
||||
void TidalSettingsPage::LogoutClicked() {
|
||||
service_->Logout();
|
||||
ui_->button_login->setEnabled(true);
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
|
||||
}
|
||||
|
||||
void TidalSettingsPage::LoginSuccess() {
|
||||
if (!this->isVisible()) return;
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn);
|
||||
ui_->button_login->setEnabled(false);
|
||||
}
|
||||
|
||||
void TidalSettingsPage::LoginFailure(QString failure_reason) {
|
||||
if (!this->isVisible()) return;
|
||||
QMessageBox::warning(this, tr("Authentication failed"), failure_reason);
|
||||
}
|
62
src/settings/tidalsettingspage.h
Normal file
62
src/settings/tidalsettingspage.h
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALSETTINGSPAGE_H
|
||||
#define TIDALSETTINGSPAGE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QEvent>
|
||||
|
||||
#include "settings/settingspage.h"
|
||||
|
||||
class TidalService;
|
||||
class Ui_TidalSettingsPage;
|
||||
|
||||
class TidalSettingsPage : public SettingsPage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TidalSettingsPage(SettingsDialog* parent = nullptr);
|
||||
~TidalSettingsPage();
|
||||
|
||||
enum SearchBy {
|
||||
SearchBy_Songs = 1,
|
||||
SearchBy_Albums = 2,
|
||||
};
|
||||
|
||||
static const char *kSettingsGroup;
|
||||
|
||||
void Load();
|
||||
void Save();
|
||||
|
||||
bool eventFilter(QObject *object, QEvent *event);
|
||||
|
||||
private slots:
|
||||
void LoginClicked();
|
||||
void LogoutClicked();
|
||||
void LoginSuccess();
|
||||
void LoginFailure(QString failure_reason);
|
||||
|
||||
private:
|
||||
Ui_TidalSettingsPage* ui_;
|
||||
TidalService *service_;
|
||||
};
|
||||
|
||||
#endif
|
157
src/settings/tidalsettingspage.ui
Normal file
157
src/settings/tidalsettingspage.ui
Normal file
@ -0,0 +1,157 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TidalSettingsPage</class>
|
||||
<widget class="QWidget" name="TidalSettingsPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>715</width>
|
||||
<height>425</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Tidal</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="LoginStateWidget" name="login_state" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="credential_group">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Account details</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_username">
|
||||
<property name="text">
|
||||
<string>Tidal username</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="username"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_login">
|
||||
<property name="text">
|
||||
<string>Login</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_password">
|
||||
<property name="text">
|
||||
<string>Tidal password</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="password">
|
||||
<property name="echoMode">
|
||||
<enum>QLineEdit::Password</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupbox_pref">
|
||||
<property name="title">
|
||||
<string>Preferences</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout_2">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_quality">
|
||||
<property name="text">
|
||||
<string>Audio quality</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="combobox_quality"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="spacer_middle">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<spacer name="spacer_bottom">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_tidal">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap resource="../../data/data.qrc">:/icons/64x64/tidal.png</pixmap>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>LoginStateWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/loginstatewidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>username</tabstop>
|
||||
<tabstop>password</tabstop>
|
||||
<tabstop>button_login</tabstop>
|
||||
</tabstops>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
316
src/tidal/tidalsearch.cpp
Normal file
316
src/tidal/tidalsearch.cpp
Normal file
@ -0,0 +1,316 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This code was part of Clementine (GlobalSearch)
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QStringBuilder>
|
||||
#include <QUrl>
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
#include <QIcon>
|
||||
#include <QPainter>
|
||||
#include <QTimerEvent>
|
||||
#include <QSettings>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/closure.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "covermanager/albumcoverloader.h"
|
||||
#include "internet/internetsongmimedata.h"
|
||||
#include "playlist/songmimedata.h"
|
||||
#include "tidalsearch.h"
|
||||
#include "tidalservice.h"
|
||||
#include "settings/tidalsettingspage.h"
|
||||
|
||||
const int TidalSearch::kDelayedSearchTimeoutMs = 200;
|
||||
const int TidalSearch::kMaxResultsPerEmission = 1000;
|
||||
const int TidalSearch::kArtHeight = 32;
|
||||
|
||||
TidalSearch::TidalSearch(Application *app, QObject *parent)
|
||||
: QObject(parent),
|
||||
app_(app),
|
||||
service_(app->internet_model()->Service<TidalService>()),
|
||||
name_("Tidal"),
|
||||
id_("tidal"),
|
||||
icon_(IconLoader::Load("tidal")),
|
||||
searches_next_id_(1),
|
||||
art_searches_next_id_(1) {
|
||||
|
||||
cover_loader_options_.desired_height_ = kArtHeight;
|
||||
cover_loader_options_.pad_output_image_ = true;
|
||||
cover_loader_options_.scale_output_image_ = true;
|
||||
|
||||
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(AlbumArtLoaded(quint64, QImage)));
|
||||
connect(this, SIGNAL(SearchAsyncSig(int, QString, TidalSettingsPage::SearchBy)), this, SLOT(DoSearchAsync(int, QString, TidalSettingsPage::SearchBy)));
|
||||
connect(this, SIGNAL(ResultsAvailable(int, TidalSearch::ResultList)), SLOT(ResultsAvailableSlot(int, TidalSearch::ResultList)));
|
||||
connect(this, SIGNAL(ArtLoaded(int, QImage)), SLOT(ArtLoadedSlot(int, QImage)));
|
||||
connect(service_, SIGNAL(SearchResults(int, SongList)), SLOT(SearchDone(int, SongList)));
|
||||
connect(service_, SIGNAL(SearchError(int, QString)), SLOT(HandleError(int, QString)));
|
||||
|
||||
icon_as_image_ = QImage(icon_.pixmap(48, 48).toImage());
|
||||
|
||||
}
|
||||
|
||||
TidalSearch::~TidalSearch() {}
|
||||
|
||||
QStringList TidalSearch::TokenizeQuery(const QString &query) {
|
||||
|
||||
QStringList tokens(query.split(QRegExp("\\s+")));
|
||||
|
||||
for (QStringList::iterator it = tokens.begin(); it != tokens.end(); ++it) {
|
||||
(*it).remove('(');
|
||||
(*it).remove(')');
|
||||
(*it).remove('"');
|
||||
|
||||
const int colon = (*it).indexOf(":");
|
||||
if (colon != -1) {
|
||||
(*it).remove(0, colon + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
|
||||
}
|
||||
|
||||
bool TidalSearch::Matches(const QStringList &tokens, const QString &string) {
|
||||
|
||||
for (const QString &token : tokens) {
|
||||
if (!string.contains(token, Qt::CaseInsensitive)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
int TidalSearch::SearchAsync(const QString &query, TidalSettingsPage::SearchBy searchby) {
|
||||
|
||||
const int id = searches_next_id_++;
|
||||
|
||||
emit SearchAsyncSig(id, query, searchby);
|
||||
|
||||
return id;
|
||||
|
||||
}
|
||||
|
||||
void TidalSearch::SearchAsync(int id, const QString &query, TidalSettingsPage::SearchBy searchby) {
|
||||
|
||||
const int service_id = service_->Search(query, searchby);
|
||||
pending_searches_[service_id] = PendingState(id, TokenizeQuery(query));
|
||||
|
||||
}
|
||||
|
||||
void TidalSearch::DoSearchAsync(int id, const QString &query, TidalSettingsPage::SearchBy searchby) {
|
||||
|
||||
int timer_id = startTimer(kDelayedSearchTimeoutMs);
|
||||
delayed_searches_[timer_id].id_ = id;
|
||||
delayed_searches_[timer_id].query_ = query;
|
||||
delayed_searches_[timer_id].searchby_ = searchby;
|
||||
|
||||
}
|
||||
|
||||
void TidalSearch::SearchDone(int service_id, const SongList &songs) {
|
||||
|
||||
// Map back to the original id.
|
||||
const PendingState state = pending_searches_.take(service_id);
|
||||
const int search_id = state.orig_id_;
|
||||
|
||||
ResultList ret;
|
||||
for (const Song &song : songs) {
|
||||
Result result;
|
||||
result.metadata_ = song;
|
||||
ret << result;
|
||||
}
|
||||
|
||||
emit ResultsAvailable(search_id, ret);
|
||||
MaybeSearchFinished(search_id);
|
||||
|
||||
}
|
||||
|
||||
void TidalSearch::HandleError(const int id, const QString error) {
|
||||
|
||||
emit SearchError(id, error);
|
||||
|
||||
}
|
||||
|
||||
void TidalSearch::MaybeSearchFinished(int id) {
|
||||
|
||||
if (pending_searches_.keys(PendingState(id, QStringList())).isEmpty()) {
|
||||
emit SearchFinished(id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void TidalSearch::CancelSearch(int id) {
|
||||
QMap<int, DelayedSearch>::iterator it;
|
||||
for (it = delayed_searches_.begin(); it != delayed_searches_.end(); ++it) {
|
||||
if (it.value().id_ == id) {
|
||||
killTimer(it.key());
|
||||
delayed_searches_.erase(it);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TidalSearch::timerEvent(QTimerEvent *e) {
|
||||
QMap<int, DelayedSearch>::iterator it = delayed_searches_.find(e->timerId());
|
||||
if (it != delayed_searches_.end()) {
|
||||
SearchAsync(it.value().id_, it.value().query_, it.value().searchby_);
|
||||
delayed_searches_.erase(it);
|
||||
return;
|
||||
}
|
||||
|
||||
QObject::timerEvent(e);
|
||||
}
|
||||
|
||||
void TidalSearch::ResultsAvailableSlot(int id, TidalSearch::ResultList results) {
|
||||
|
||||
if (results.isEmpty()) return;
|
||||
|
||||
// Limit the number of results that are used from each emission.
|
||||
if (results.count() > kMaxResultsPerEmission) {
|
||||
TidalSearch::ResultList::iterator begin = results.begin();
|
||||
std::advance(begin, kMaxResultsPerEmission);
|
||||
results.erase(begin, results.end());
|
||||
}
|
||||
|
||||
// Load cached pixmaps into the results
|
||||
for (TidalSearch::ResultList::iterator it = results.begin(); it != results.end(); ++it) {
|
||||
it->pixmap_cache_key_ = PixmapCacheKey(*it);
|
||||
}
|
||||
|
||||
emit AddResults(id, results);
|
||||
|
||||
}
|
||||
|
||||
QString TidalSearch::PixmapCacheKey(const TidalSearch::Result &result) const {
|
||||
return "tidal:" % result.metadata_.url().toString();
|
||||
}
|
||||
|
||||
bool TidalSearch::FindCachedPixmap(const TidalSearch::Result &result, QPixmap *pixmap) const {
|
||||
return pixmap_cache_.find(result.pixmap_cache_key_, pixmap);
|
||||
}
|
||||
|
||||
void TidalSearch::LoadArtAsync(int id, const Result &result) {
|
||||
emit ArtLoaded(id, QImage());
|
||||
}
|
||||
|
||||
int TidalSearch::LoadArtAsync(const TidalSearch::Result &result) {
|
||||
|
||||
const int id = art_searches_next_id_++;
|
||||
|
||||
pending_art_searches_[id] = result.pixmap_cache_key_;
|
||||
|
||||
quint64 loader_id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, result.metadata_);
|
||||
cover_loader_tasks_[loader_id] = id;
|
||||
|
||||
return id;
|
||||
|
||||
}
|
||||
|
||||
void TidalSearch::ArtLoadedSlot(int id, const QImage &image) {
|
||||
HandleLoadedArt(id, image);
|
||||
}
|
||||
|
||||
void TidalSearch::AlbumArtLoaded(quint64 id, const QImage &image) {
|
||||
|
||||
if (!cover_loader_tasks_.contains(id)) return;
|
||||
int orig_id = cover_loader_tasks_.take(id);
|
||||
|
||||
HandleLoadedArt(orig_id, image);
|
||||
}
|
||||
|
||||
void TidalSearch::HandleLoadedArt(int id, const QImage &image) {
|
||||
|
||||
const QString key = pending_art_searches_.take(id);
|
||||
|
||||
QPixmap pixmap = QPixmap::fromImage(image);
|
||||
pixmap_cache_.insert(key, pixmap);
|
||||
|
||||
emit ArtLoaded(id, pixmap);
|
||||
|
||||
}
|
||||
|
||||
QImage TidalSearch::ScaleAndPad(const QImage &image) {
|
||||
|
||||
if (image.isNull()) return QImage();
|
||||
|
||||
const QSize target_size = QSize(kArtHeight, kArtHeight);
|
||||
|
||||
if (image.size() == target_size) return image;
|
||||
|
||||
// Scale the image down
|
||||
QImage copy;
|
||||
copy = image.scaled(target_size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
|
||||
// Pad the image to kHeight x kHeight
|
||||
if (copy.size() == target_size) return copy;
|
||||
|
||||
QImage padded_image(kArtHeight, kArtHeight, QImage::Format_ARGB32);
|
||||
padded_image.fill(0);
|
||||
|
||||
QPainter p(&padded_image);
|
||||
p.drawImage((kArtHeight - copy.width()) / 2, (kArtHeight - copy.height()) / 2, copy);
|
||||
p.end();
|
||||
|
||||
return padded_image;
|
||||
|
||||
}
|
||||
|
||||
MimeData *TidalSearch::LoadTracks(const ResultList &results) {
|
||||
|
||||
if (results.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ResultList results_copy;
|
||||
for (const Result &result : results) {
|
||||
results_copy << result;
|
||||
}
|
||||
|
||||
SongList songs;
|
||||
for (const Result &result : results) {
|
||||
songs << result.metadata_;
|
||||
}
|
||||
|
||||
InternetSongMimeData *internet_song_mime_data = new InternetSongMimeData(service_);
|
||||
internet_song_mime_data->songs = songs;
|
||||
MimeData *mime_data = internet_song_mime_data;
|
||||
|
||||
QList<QUrl> urls;
|
||||
for (const Result &result : results) {
|
||||
urls << result.metadata_.url();
|
||||
}
|
||||
mime_data->setUrls(urls);
|
||||
|
||||
return mime_data;
|
||||
|
||||
}
|
157
src/tidal/tidalsearch.h
Normal file
157
src/tidal/tidalsearch.h
Normal file
@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This code was part of Clementine (GlobalSearch)
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALSEARCH_H
|
||||
#define TIDALSEARCH_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QFuture>
|
||||
#include <QIcon>
|
||||
#include <QMetaType>
|
||||
#include <QPixmapCache>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "covermanager/albumcoverloaderoptions.h"
|
||||
#include "settings/tidalsettingspage.h"
|
||||
|
||||
class Application;
|
||||
class MimeData;
|
||||
class AlbumCoverLoader;
|
||||
class InternetService;
|
||||
class TidalService;
|
||||
|
||||
class TidalSearch : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TidalSearch(Application *app, QObject *parent = nullptr);
|
||||
~TidalSearch();
|
||||
|
||||
struct Result {
|
||||
Song metadata_;
|
||||
QString pixmap_cache_key_;
|
||||
};
|
||||
typedef QList<Result> ResultList;
|
||||
|
||||
static const int kDelayedSearchTimeoutMs;
|
||||
static const int kMaxResultsPerEmission;
|
||||
|
||||
Application *application() const { return app_; }
|
||||
TidalService *service() const { return service_; }
|
||||
|
||||
int SearchAsync(const QString &query, TidalSettingsPage::SearchBy searchby);
|
||||
int LoadArtAsync(const TidalSearch::Result &result);
|
||||
|
||||
void CancelSearch(int id);
|
||||
void CancelArt(int id);
|
||||
|
||||
// Loads tracks for results that were previously emitted by ResultsAvailable.
|
||||
// The implementation creates a SongMimeData with one Song for each Result.
|
||||
MimeData *LoadTracks(const ResultList &results);
|
||||
|
||||
signals:
|
||||
void SearchAsyncSig(int id, const QString &query, TidalSettingsPage::SearchBy searchby);
|
||||
void ResultsAvailable(int id, const TidalSearch::ResultList &results);
|
||||
void AddResults(int id, const TidalSearch::ResultList &results);
|
||||
void SearchError(const int id, const QString error);
|
||||
void SearchFinished(int id);
|
||||
|
||||
void ArtLoaded(int id, const QPixmap &pixmap);
|
||||
void ArtLoaded(int id, const QImage &image);
|
||||
|
||||
protected:
|
||||
|
||||
struct PendingState {
|
||||
PendingState() : orig_id_(-1) {}
|
||||
PendingState(int orig_id, QStringList tokens)
|
||||
: orig_id_(orig_id), tokens_(tokens) {}
|
||||
int orig_id_;
|
||||
QStringList tokens_;
|
||||
|
||||
bool operator<(const PendingState &b) const {
|
||||
return orig_id_ < b.orig_id_;
|
||||
}
|
||||
|
||||
bool operator==(const PendingState &b) const {
|
||||
return orig_id_ == b.orig_id_;
|
||||
}
|
||||
};
|
||||
|
||||
void timerEvent(QTimerEvent *e);
|
||||
|
||||
// These functions treat queries in the same way as LibraryQuery.
|
||||
// They're useful for figuring out whether you got a result because it matched in the song title or the artist/album name.
|
||||
static QStringList TokenizeQuery(const QString &query);
|
||||
static bool Matches(const QStringList &tokens, const QString &string);
|
||||
|
||||
private slots:
|
||||
void DoSearchAsync(int id, const QString &query, TidalSettingsPage::SearchBy searchby);
|
||||
void SearchDone(int id, const SongList &songs);
|
||||
void HandleError(const int id, const QString error);
|
||||
void ResultsAvailableSlot(int id, TidalSearch::ResultList results);
|
||||
|
||||
void ArtLoadedSlot(int id, const QImage &image);
|
||||
void AlbumArtLoaded(quint64 id, const QImage &image);
|
||||
|
||||
private:
|
||||
void SearchAsync(int id, const QString &query, TidalSettingsPage::SearchBy searchby);
|
||||
void HandleLoadedArt(int id, const QImage &image);
|
||||
bool FindCachedPixmap(const TidalSearch::Result &result, QPixmap *pixmap) const;
|
||||
QString PixmapCacheKey(const TidalSearch::Result &result) const;
|
||||
void LoadArtAsync(int id, const Result &result);
|
||||
void MaybeSearchFinished(int id);
|
||||
void ShowConfig() {}
|
||||
static QImage ScaleAndPad(const QImage &image);
|
||||
|
||||
private:
|
||||
struct DelayedSearch {
|
||||
int id_;
|
||||
QString query_;
|
||||
TidalSettingsPage::SearchBy searchby_;
|
||||
};
|
||||
|
||||
static const int kArtHeight;
|
||||
|
||||
Application *app_;
|
||||
TidalService *service_;
|
||||
QString name_;
|
||||
QString id_;
|
||||
QIcon icon_;
|
||||
QImage icon_as_image_;
|
||||
int searches_next_id_;
|
||||
int art_searches_next_id_;
|
||||
|
||||
QMap<int, DelayedSearch> delayed_searches_;
|
||||
QMap<int, QString> pending_art_searches_;
|
||||
QPixmapCache pixmap_cache_;
|
||||
AlbumCoverLoaderOptions cover_loader_options_;
|
||||
QMap<quint64, int> cover_loader_tasks_;
|
||||
|
||||
QMap<int, PendingState> pending_searches_;
|
||||
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(TidalSearch::Result)
|
||||
Q_DECLARE_METATYPE(TidalSearch::ResultList)
|
||||
|
||||
#endif // TIDALSEARCH_H
|
35
src/tidal/tidalsearchitemdelegate.cpp
Normal file
35
src/tidal/tidalsearchitemdelegate.cpp
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This code was part of Clementine (GlobalSearch)
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <QPainter>
|
||||
#include <QStyleOptionViewItem>
|
||||
|
||||
#include "tidalsearchitemdelegate.h"
|
||||
#include "tidalsearchview.h"
|
||||
|
||||
TidalSearchItemDelegate::TidalSearchItemDelegate(TidalSearchView* view)
|
||||
: CollectionItemDelegate(view), view_(view) {}
|
||||
|
||||
void TidalSearchItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
|
||||
// Tell the view we painted this item so it can lazy load some art.
|
||||
const_cast<TidalSearchView*>(view_)->LazyLoadArt(index);
|
||||
|
||||
CollectionItemDelegate::paint(painter, option, index);
|
||||
}
|
41
src/tidal/tidalsearchitemdelegate.h
Normal file
41
src/tidal/tidalsearchitemdelegate.h
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This code was part of Clementine (GlobalSearch)
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALSEARCHITEMDELEGATE_H
|
||||
#define TIDALSEARCHITEMDELEGATE_H
|
||||
|
||||
#include <QPainter>
|
||||
#include <QStyleOptionViewItem>
|
||||
|
||||
#include "collection/collectionview.h"
|
||||
|
||||
class TidalSearchView;
|
||||
|
||||
class TidalSearchItemDelegate : public CollectionItemDelegate {
|
||||
public:
|
||||
TidalSearchItemDelegate(TidalSearchView *view);
|
||||
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
|
||||
|
||||
private:
|
||||
TidalSearchView* view_;
|
||||
};
|
||||
|
||||
#endif // TIDALSEARCHITEMDELEGATE_H
|
314
src/tidal/tidalsearchmodel.cpp
Normal file
314
src/tidal/tidalsearchmodel.cpp
Normal file
@ -0,0 +1,314 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This code was part of Clementine (GlobalSearch)
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QStandardItem>
|
||||
#include <QStandardItemModel>
|
||||
#include <QList>
|
||||
#include <QSet>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QPixmap>
|
||||
#include <QMimeData>
|
||||
|
||||
#include "core/mimedata.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "tidalsearch.h"
|
||||
#include "tidalsearchmodel.h"
|
||||
|
||||
TidalSearchModel::TidalSearchModel(TidalSearch *engine, QObject *parent)
|
||||
: QStandardItemModel(parent),
|
||||
engine_(engine),
|
||||
proxy_(nullptr),
|
||||
use_pretty_covers_(true),
|
||||
artist_icon_(IconLoader::Load("guitar")) {
|
||||
|
||||
group_by_[0] = CollectionModel::GroupBy_Artist;
|
||||
group_by_[1] = CollectionModel::GroupBy_Album;
|
||||
group_by_[2] = CollectionModel::GroupBy_None;
|
||||
|
||||
no_cover_icon_ = QPixmap(":/pictures/noalbumart.png").scaled(CollectionModel::kPrettyCoverSize, CollectionModel::kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
album_icon_ = no_cover_icon_;
|
||||
|
||||
}
|
||||
|
||||
void TidalSearchModel::AddResults(const TidalSearch::ResultList &results) {
|
||||
|
||||
int sort_index = 0;
|
||||
|
||||
for (const TidalSearch::Result &result : results) {
|
||||
QStandardItem *parent = invisibleRootItem();
|
||||
|
||||
// Find (or create) the container nodes for this result if we can.
|
||||
ContainerKey key;
|
||||
key.provider_index_ = sort_index;
|
||||
parent = BuildContainers(result.metadata_, parent, &key);
|
||||
|
||||
// Create the item
|
||||
QStandardItem *item = new QStandardItem;
|
||||
item->setText(result.metadata_.TitleWithCompilationArtist());
|
||||
item->setData(QVariant::fromValue(result), Role_Result);
|
||||
item->setData(sort_index, Role_ProviderIndex);
|
||||
|
||||
parent->appendRow(item);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QStandardItem *TidalSearchModel::BuildContainers(const Song &s, QStandardItem *parent, ContainerKey *key, int level) {
|
||||
|
||||
if (level >= 3) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
bool has_artist_icon = false;
|
||||
bool has_album_icon = false;
|
||||
QString display_text;
|
||||
QString sort_text;
|
||||
int unique_tag = -1;
|
||||
int year = 0;
|
||||
|
||||
switch (group_by_[level]) {
|
||||
case CollectionModel::GroupBy_Artist:
|
||||
if (s.is_compilation()) {
|
||||
display_text = tr("Various artists");
|
||||
sort_text = "aaaaaa";
|
||||
}
|
||||
else {
|
||||
display_text = CollectionModel::TextOrUnknown(s.artist());
|
||||
sort_text = CollectionModel::SortTextForArtist(s.artist());
|
||||
}
|
||||
has_artist_icon = true;
|
||||
break;
|
||||
|
||||
case CollectionModel::GroupBy_YearAlbum:
|
||||
year = qMax(0, s.year());
|
||||
display_text = CollectionModel::PrettyYearAlbum(year, s.album());
|
||||
sort_text = CollectionModel::SortTextForNumber(year) + s.album();
|
||||
unique_tag = s.album_id();
|
||||
has_album_icon = true;
|
||||
break;
|
||||
|
||||
case CollectionModel::GroupBy_OriginalYearAlbum:
|
||||
year = qMax(0, s.effective_originalyear());
|
||||
display_text = CollectionModel::PrettyYearAlbum(year, s.album());
|
||||
sort_text = CollectionModel::SortTextForNumber(year) + s.album();
|
||||
unique_tag = s.album_id();
|
||||
has_album_icon = true;
|
||||
break;
|
||||
|
||||
case CollectionModel::GroupBy_Year:
|
||||
year = qMax(0, s.year());
|
||||
display_text = QString::number(year);
|
||||
sort_text = CollectionModel::SortTextForNumber(year) + " ";
|
||||
break;
|
||||
|
||||
case CollectionModel::GroupBy_OriginalYear:
|
||||
year = qMax(0, s.effective_originalyear());
|
||||
display_text = QString::number(year);
|
||||
sort_text = CollectionModel::SortTextForNumber(year) + " ";
|
||||
break;
|
||||
|
||||
case CollectionModel::GroupBy_Composer:
|
||||
display_text = s.composer();
|
||||
case CollectionModel::GroupBy_Performer:
|
||||
display_text = s.performer();
|
||||
case CollectionModel::GroupBy_Disc:
|
||||
display_text = s.disc();
|
||||
case CollectionModel::GroupBy_Grouping:
|
||||
display_text = s.grouping();
|
||||
case CollectionModel::GroupBy_Genre:
|
||||
if (display_text.isNull()) display_text = s.genre();
|
||||
case CollectionModel::GroupBy_Album:
|
||||
unique_tag = s.album_id();
|
||||
if (display_text.isNull()) {
|
||||
display_text = s.album();
|
||||
}
|
||||
// fallthrough
|
||||
case CollectionModel::GroupBy_AlbumArtist:
|
||||
if (display_text.isNull()) display_text = s.effective_albumartist();
|
||||
display_text = CollectionModel::TextOrUnknown(display_text);
|
||||
sort_text = CollectionModel::SortTextForArtist(display_text);
|
||||
has_album_icon = true;
|
||||
break;
|
||||
|
||||
case CollectionModel::GroupBy_FileType:
|
||||
display_text = s.TextForFiletype();
|
||||
sort_text = display_text;
|
||||
break;
|
||||
|
||||
case CollectionModel::GroupBy_Bitrate:
|
||||
display_text = QString(s.bitrate(), 1);
|
||||
sort_text = display_text;
|
||||
break;
|
||||
|
||||
case CollectionModel::GroupBy_Samplerate:
|
||||
display_text = QString(s.samplerate(), 1);
|
||||
sort_text = display_text;
|
||||
break;
|
||||
|
||||
case CollectionModel::GroupBy_Bitdepth:
|
||||
display_text = QString(s.bitdepth(), 1);
|
||||
sort_text = display_text;
|
||||
break;
|
||||
|
||||
case CollectionModel::GroupBy_None:
|
||||
return parent;
|
||||
}
|
||||
|
||||
// Find a container for this level
|
||||
key->group_[level] = display_text + QString::number(unique_tag);
|
||||
QStandardItem *container = containers_[*key];
|
||||
if (!container) {
|
||||
container = new QStandardItem(display_text);
|
||||
container->setData(key->provider_index_, Role_ProviderIndex);
|
||||
container->setData(sort_text, CollectionModel::Role_SortText);
|
||||
container->setData(group_by_[level], CollectionModel::Role_ContainerType);
|
||||
|
||||
if (has_artist_icon) {
|
||||
container->setIcon(artist_icon_);
|
||||
}
|
||||
else if (has_album_icon) {
|
||||
if (use_pretty_covers_) {
|
||||
container->setData(no_cover_icon_, Qt::DecorationRole);
|
||||
}
|
||||
else {
|
||||
container->setIcon(album_icon_);
|
||||
}
|
||||
}
|
||||
|
||||
parent->appendRow(container);
|
||||
containers_[*key] = container;
|
||||
}
|
||||
|
||||
// Create the container for the next level.
|
||||
return BuildContainers(s, container, key, level + 1);
|
||||
|
||||
}
|
||||
|
||||
void TidalSearchModel::Clear() {
|
||||
containers_.clear();
|
||||
clear();
|
||||
}
|
||||
|
||||
TidalSearch::ResultList TidalSearchModel::GetChildResults(const QModelIndexList &indexes) const {
|
||||
|
||||
QList<QStandardItem*> items;
|
||||
for (const QModelIndex &index : indexes) {
|
||||
items << itemFromIndex(index);
|
||||
}
|
||||
return GetChildResults(items);
|
||||
|
||||
}
|
||||
|
||||
TidalSearch::ResultList TidalSearchModel::GetChildResults(const QList<QStandardItem*> &items) const {
|
||||
|
||||
TidalSearch::ResultList results;
|
||||
QSet<const QStandardItem*> visited;
|
||||
|
||||
for (QStandardItem *item : items) {
|
||||
GetChildResults(item, &results, &visited);
|
||||
}
|
||||
|
||||
return results;
|
||||
|
||||
}
|
||||
|
||||
void TidalSearchModel::GetChildResults(const QStandardItem *item, TidalSearch::ResultList *results, QSet<const QStandardItem*> *visited) const {
|
||||
|
||||
if (visited->contains(item)) {
|
||||
return;
|
||||
}
|
||||
visited->insert(item);
|
||||
|
||||
// Does this item have children?
|
||||
if (item->rowCount()) {
|
||||
const QModelIndex parent_proxy_index = proxy_->mapFromSource(item->index());
|
||||
|
||||
// Yes - visit all the children, but do so through the proxy so we get them
|
||||
// in the right order.
|
||||
for (int i = 0; i < item->rowCount(); ++i) {
|
||||
const QModelIndex proxy_index = parent_proxy_index.child(i, 0);
|
||||
const QModelIndex index = proxy_->mapToSource(proxy_index);
|
||||
GetChildResults(itemFromIndex(index), results, visited);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// No - maybe it's a song, add its result if valid
|
||||
QVariant result = item->data(Role_Result);
|
||||
if (result.isValid()) {
|
||||
results->append(result.value<TidalSearch::Result>());
|
||||
}
|
||||
else {
|
||||
// Maybe it's a provider then?
|
||||
bool is_provider;
|
||||
const int sort_index = item->data(Role_ProviderIndex).toInt(&is_provider);
|
||||
if (is_provider) {
|
||||
// Go through all the items (through the proxy to keep them ordered) and add the ones belonging to this provider to our list
|
||||
for (int i = 0; i < proxy_->rowCount(invisibleRootItem()->index()); ++i) {
|
||||
QModelIndex child_index = proxy_->index(i, 0, invisibleRootItem()->index());
|
||||
const QStandardItem *child_item = itemFromIndex(proxy_->mapToSource(child_index));
|
||||
if (child_item->data(Role_ProviderIndex).toInt() == sort_index) {
|
||||
GetChildResults(child_item, results, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QMimeData *TidalSearchModel::mimeData(const QModelIndexList &indexes) const {
|
||||
return engine_->LoadTracks(GetChildResults(indexes));
|
||||
}
|
||||
|
||||
namespace {
|
||||
void GatherResults(const QStandardItem *parent, TidalSearch::ResultList *results) {
|
||||
|
||||
QVariant result_variant = parent->data(TidalSearchModel::Role_Result);
|
||||
if (result_variant.isValid()) {
|
||||
TidalSearch::Result result = result_variant.value<TidalSearch::Result>();
|
||||
(*results).append(result);
|
||||
}
|
||||
|
||||
for (int i = 0; i < parent->rowCount(); ++i) {
|
||||
GatherResults(parent->child(i), results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TidalSearchModel::SetGroupBy(const CollectionModel::Grouping &grouping, bool regroup_now) {
|
||||
|
||||
const CollectionModel::Grouping old_group_by = group_by_;
|
||||
group_by_ = grouping;
|
||||
|
||||
if (regroup_now && group_by_ != old_group_by) {
|
||||
// Walk the tree gathering the results we have already
|
||||
TidalSearch::ResultList results;
|
||||
GatherResults(invisibleRootItem(), &results);
|
||||
|
||||
// Reset the model and re-add all the results using the new grouping.
|
||||
Clear();
|
||||
AddResults(results);
|
||||
}
|
||||
|
||||
}
|
109
src/tidal/tidalsearchmodel.h
Normal file
109
src/tidal/tidalsearchmodel.h
Normal file
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This code was part of Clementine (GlobalSearch)
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALSEARCHMODEL_H
|
||||
#define TIDALSEARCHMODEL_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QMimeData>
|
||||
#include <QStandardItemModel>
|
||||
#include <QStandardItem>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QMap>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QIcon>
|
||||
#include <QPixmap>
|
||||
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "tidalsearch.h"
|
||||
|
||||
class TidalSearchModel : public QStandardItemModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TidalSearchModel(TidalSearch *engine, QObject *parent = nullptr);
|
||||
|
||||
enum Role {
|
||||
Role_Result = CollectionModel::LastRole,
|
||||
Role_LazyLoadingArt,
|
||||
Role_ProviderIndex,
|
||||
LastRole
|
||||
};
|
||||
|
||||
struct ContainerKey {
|
||||
int provider_index_;
|
||||
QString group_[3];
|
||||
};
|
||||
|
||||
void set_proxy(QSortFilterProxyModel *proxy) { proxy_ = proxy; }
|
||||
void set_use_pretty_covers(bool pretty) { use_pretty_covers_ = pretty; }
|
||||
void SetGroupBy(const CollectionModel::Grouping &grouping, bool regroup_now);
|
||||
|
||||
void Clear();
|
||||
|
||||
TidalSearch::ResultList GetChildResults(const QModelIndexList &indexes) const;
|
||||
TidalSearch::ResultList GetChildResults(const QList<QStandardItem*> &items) const;
|
||||
|
||||
QMimeData *mimeData(const QModelIndexList &indexes) const;
|
||||
|
||||
public slots:
|
||||
void AddResults(const TidalSearch::ResultList &results);
|
||||
|
||||
private:
|
||||
QStandardItem *BuildContainers(const Song &metadata, QStandardItem *parent, ContainerKey *key, int level = 0);
|
||||
void GetChildResults(const QStandardItem *item, TidalSearch::ResultList *results, QSet<const QStandardItem*> *visited) const;
|
||||
|
||||
private:
|
||||
TidalSearch *engine_;
|
||||
QSortFilterProxyModel *proxy_;
|
||||
bool use_pretty_covers_;
|
||||
QIcon artist_icon_;
|
||||
QPixmap no_cover_icon_;
|
||||
QIcon album_icon_;
|
||||
CollectionModel::Grouping group_by_;
|
||||
QMap<ContainerKey, QStandardItem*> containers_;
|
||||
|
||||
};
|
||||
|
||||
inline uint qHash(const TidalSearchModel::ContainerKey &key) {
|
||||
return qHash(key.provider_index_) ^ qHash(key.group_[0]) ^ qHash(key.group_[1]) ^ qHash(key.group_[2]);
|
||||
}
|
||||
|
||||
inline bool operator<(const TidalSearchModel::ContainerKey &left, const TidalSearchModel::ContainerKey &right) {
|
||||
#define CMP(field) \
|
||||
if (left.field < right.field) return true; \
|
||||
if (left.field > right.field) return false
|
||||
|
||||
CMP(provider_index_);
|
||||
CMP(group_[0]);
|
||||
CMP(group_[1]);
|
||||
CMP(group_[2]);
|
||||
return false;
|
||||
|
||||
#undef CMP
|
||||
}
|
||||
|
||||
#endif // TIDALSEARCHMODEL_H
|
79
src/tidal/tidalsearchsortmodel.cpp
Normal file
79
src/tidal/tidalsearchsortmodel.cpp
Normal file
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This code was part of Clementine (GlobalSearch)
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QString>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "tidalsearchmodel.h"
|
||||
#include "tidalsearchsortmodel.h"
|
||||
|
||||
TidalSearchSortModel::TidalSearchSortModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent) {}
|
||||
|
||||
bool TidalSearchSortModel::lessThan(const QModelIndex &left, const QModelIndex &right) const {
|
||||
// Compare the provider sort index first.
|
||||
const int index_left = left.data(TidalSearchModel::Role_ProviderIndex).toInt();
|
||||
const int index_right = right.data(TidalSearchModel::Role_ProviderIndex).toInt();
|
||||
if (index_left < index_right) return true;
|
||||
if (index_left > index_right) return false;
|
||||
|
||||
// Dividers always go first
|
||||
if (left.data(CollectionModel::Role_IsDivider).toBool()) return true;
|
||||
if (right.data(CollectionModel::Role_IsDivider).toBool()) return false;
|
||||
|
||||
// Containers go before songs if they're at the same level
|
||||
const bool left_is_container = left.data(CollectionModel::Role_ContainerType).isValid();
|
||||
const bool right_is_container = right.data(CollectionModel::Role_ContainerType).isValid();
|
||||
if (left_is_container && !right_is_container) return true;
|
||||
if (right_is_container && !left_is_container) return false;
|
||||
|
||||
// Containers get sorted on their sort text.
|
||||
if (left_is_container) {
|
||||
return QString::localeAwareCompare(left.data(CollectionModel::Role_SortText).toString(), right.data(CollectionModel::Role_SortText).toString()) < 0;
|
||||
}
|
||||
|
||||
// Otherwise we're comparing songs. Sort by disc, track, then title.
|
||||
const TidalSearch::Result r1 = left.data(TidalSearchModel::Role_Result).value<TidalSearch::Result>();
|
||||
const TidalSearch::Result r2 = right.data(TidalSearchModel::Role_Result).value<TidalSearch::Result>();
|
||||
|
||||
#define CompareInt(field) \
|
||||
if (r1.metadata_.field() < r2.metadata_.field()) return true; \
|
||||
if (r1.metadata_.field() > r2.metadata_.field()) return false
|
||||
|
||||
int ret = 0;
|
||||
|
||||
#define CompareString(field) \
|
||||
ret = QString::localeAwareCompare(r1.metadata_.field(), r2.metadata_.field()); \
|
||||
if (ret < 0) return true; \
|
||||
if (ret > 0) return false
|
||||
|
||||
CompareInt(disc);
|
||||
CompareInt(track);
|
||||
CompareString(title);
|
||||
|
||||
return false;
|
||||
|
||||
#undef CompareInt
|
||||
#undef CompareString
|
||||
}
|
35
src/tidal/tidalsearchsortmodel.h
Normal file
35
src/tidal/tidalsearchsortmodel.h
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This code was part of Clementine (GlobalSearch)
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALSEARCHSORTMODEL_H
|
||||
#define TIDALSEARCHSORTMODEL_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
class TidalSearchSortModel : public QSortFilterProxyModel {
|
||||
public:
|
||||
TidalSearchSortModel(QObject *parent = nullptr);
|
||||
|
||||
protected:
|
||||
bool lessThan(const QModelIndex &left, const QModelIndex &right) const;
|
||||
};
|
||||
|
||||
#endif // TIDALSEARCHSORTMODEL_H
|
544
src/tidal/tidalsearchview.cpp
Normal file
544
src/tidal/tidalsearchview.cpp
Normal file
@ -0,0 +1,544 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This code was part of Clementine (GlobalSearch)
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QWidget>
|
||||
#include <QTimer>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QPixmap>
|
||||
#include <QPalette>
|
||||
#include <QColor>
|
||||
#include <QFont>
|
||||
#include <QMenu>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QStandardItem>
|
||||
#include <QSettings>
|
||||
#include <QAction>
|
||||
#include <QtEvents>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/mimedata.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "internet/internetsongmimedata.h"
|
||||
#include "collection/collectionfilterwidget.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "collection/groupbydialog.h"
|
||||
#include "playlist/songmimedata.h"
|
||||
#include "tidalsearch.h"
|
||||
#include "tidalsearchitemdelegate.h"
|
||||
#include "tidalsearchmodel.h"
|
||||
#include "tidalsearchsortmodel.h"
|
||||
#include "tidalsearchview.h"
|
||||
#include "ui_tidalsearchview.h"
|
||||
#include "settings/tidalsettingspage.h"
|
||||
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::swap;
|
||||
|
||||
const int TidalSearchView::kSwapModelsTimeoutMsec = 250;
|
||||
|
||||
TidalSearchView::TidalSearchView(Application *app, QWidget *parent)
|
||||
: QWidget(parent),
|
||||
app_(app),
|
||||
engine_(app_->tidal_search()),
|
||||
ui_(new Ui_TidalSearchView),
|
||||
context_menu_(nullptr),
|
||||
last_search_id_(0),
|
||||
front_model_(new TidalSearchModel(engine_, this)),
|
||||
back_model_(new TidalSearchModel(engine_, this)),
|
||||
current_model_(front_model_),
|
||||
front_proxy_(new TidalSearchSortModel(this)),
|
||||
back_proxy_(new TidalSearchSortModel(this)),
|
||||
current_proxy_(front_proxy_),
|
||||
swap_models_timer_(new QTimer(this)),
|
||||
search_icon_(IconLoader::Load("search")),
|
||||
warning_icon_(IconLoader::Load("dialog-warning")),
|
||||
error_(false) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
|
||||
front_model_->set_proxy(front_proxy_);
|
||||
back_model_->set_proxy(back_proxy_);
|
||||
|
||||
ui_->search->installEventFilter(this);
|
||||
ui_->results_stack->installEventFilter(this);
|
||||
|
||||
ui_->settings->setIcon(IconLoader::Load("configure"));
|
||||
|
||||
// Must be a queued connection to ensure the TidalSearch handles it first.
|
||||
connect(app_, SIGNAL(SettingsChanged()), SLOT(ReloadSettings()), Qt::QueuedConnection);
|
||||
|
||||
connect(ui_->search, SIGNAL(textChanged(QString)), SLOT(TextEdited(QString)));
|
||||
connect(ui_->results, SIGNAL(AddToPlaylistSignal(QMimeData*)), SIGNAL(AddToPlaylist(QMimeData*)));
|
||||
connect(ui_->results, SIGNAL(FocusOnFilterSignal(QKeyEvent*)), SLOT(FocusOnFilter(QKeyEvent*)));
|
||||
|
||||
// Set the appearance of the results list
|
||||
ui_->results->setItemDelegate(new TidalSearchItemDelegate(this));
|
||||
ui_->results->setAttribute(Qt::WA_MacShowFocusRect, false);
|
||||
ui_->results->setStyleSheet("QTreeView::item{padding-top:1px;}");
|
||||
|
||||
// Show the help page initially
|
||||
ui_->results_stack->setCurrentWidget(ui_->help_page);
|
||||
ui_->help_frame->setBackgroundRole(QPalette::Base);
|
||||
|
||||
// Set the colour of the help text to the disabled window text colour
|
||||
QPalette help_palette = ui_->label_helptext->palette();
|
||||
const QColor help_color = help_palette.color(QPalette::Disabled, QPalette::WindowText);
|
||||
help_palette.setColor(QPalette::Normal, QPalette::WindowText, help_color);
|
||||
help_palette.setColor(QPalette::Inactive, QPalette::WindowText, help_color);
|
||||
ui_->label_helptext->setPalette(help_palette);
|
||||
|
||||
// Make it bold
|
||||
QFont help_font = ui_->label_helptext->font();
|
||||
help_font.setBold(true);
|
||||
ui_->label_helptext->setFont(help_font);
|
||||
|
||||
// Set up the sorting proxy model
|
||||
front_proxy_->setSourceModel(front_model_);
|
||||
front_proxy_->setDynamicSortFilter(true);
|
||||
front_proxy_->sort(0);
|
||||
|
||||
back_proxy_->setSourceModel(back_model_);
|
||||
back_proxy_->setDynamicSortFilter(true);
|
||||
back_proxy_->sort(0);
|
||||
|
||||
swap_models_timer_->setSingleShot(true);
|
||||
swap_models_timer_->setInterval(kSwapModelsTimeoutMsec);
|
||||
connect(swap_models_timer_, SIGNAL(timeout()), SLOT(SwapModels()));
|
||||
|
||||
// Add actions to the settings menu
|
||||
group_by_actions_ = CollectionFilterWidget::CreateGroupByActions(this);
|
||||
QMenu *settings_menu = new QMenu(this);
|
||||
settings_menu->addActions(group_by_actions_->actions());
|
||||
settings_menu->addSeparator();
|
||||
settings_menu->addAction(IconLoader::Load("configure"), tr("Configure Tidal..."), this, SLOT(OpenSettingsDialog()));
|
||||
ui_->settings->setMenu(settings_menu);
|
||||
|
||||
connect(ui_->radiobutton_searchbyalbums, SIGNAL(clicked(bool)), SLOT(SearchByAlbumsClicked(bool)));
|
||||
connect(ui_->radiobutton_searchbysongs, SIGNAL(clicked(bool)), SLOT(SearchBySongsClicked(bool)));
|
||||
|
||||
connect(group_by_actions_, SIGNAL(triggered(QAction*)), SLOT(GroupByClicked(QAction*)));
|
||||
|
||||
// These have to be queued connections because they may get emitted before our call to Search() (or whatever) returns and we add the ID to the map.
|
||||
connect(engine_, SIGNAL(AddResults(int, TidalSearch::ResultList)), SLOT(AddResults(int, TidalSearch::ResultList)), Qt::QueuedConnection);
|
||||
connect(engine_, SIGNAL(SearchError(int, QString)), SLOT(SearchError(int, QString)), Qt::QueuedConnection);
|
||||
connect(engine_, SIGNAL(ArtLoaded(int, QPixmap)), SLOT(ArtLoaded(int, QPixmap)), Qt::QueuedConnection);
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
TidalSearchView::~TidalSearchView() { delete ui_; }
|
||||
|
||||
void TidalSearchView::ReloadSettings() {
|
||||
|
||||
QSettings s;
|
||||
|
||||
// Collection settings
|
||||
|
||||
s.beginGroup(TidalSettingsPage::kSettingsGroup);
|
||||
const bool pretty = s.value("pretty_covers", true).toBool();
|
||||
front_model_->set_use_pretty_covers(pretty);
|
||||
back_model_->set_use_pretty_covers(pretty);
|
||||
s.endGroup();
|
||||
|
||||
// Tidal search settings
|
||||
|
||||
s.beginGroup(TidalSettingsPage::kSettingsGroup);
|
||||
searchby_ = TidalSettingsPage::SearchBy(s.value("searchby", int(TidalSettingsPage::SearchBy_Songs)).toInt());
|
||||
switch (searchby_) {
|
||||
case TidalSettingsPage::SearchBy_Songs:
|
||||
ui_->radiobutton_searchbysongs->setChecked(true);
|
||||
break;
|
||||
case TidalSettingsPage::SearchBy_Albums:
|
||||
ui_->radiobutton_searchbyalbums->setChecked(true);
|
||||
break;
|
||||
}
|
||||
|
||||
SetGroupBy(CollectionModel::Grouping(
|
||||
CollectionModel::GroupBy(s.value("group_by1", int(CollectionModel::GroupBy_Artist)).toInt()),
|
||||
CollectionModel::GroupBy(s.value("group_by2", int(CollectionModel::GroupBy_Album)).toInt()),
|
||||
CollectionModel::GroupBy(s.value("group_by3", int(CollectionModel::GroupBy_None)).toInt())));
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void TidalSearchView::StartSearch(const QString &query) {
|
||||
|
||||
ui_->search->setText(query);
|
||||
TextEdited(query);
|
||||
|
||||
// Swap models immediately
|
||||
swap_models_timer_->stop();
|
||||
SwapModels();
|
||||
|
||||
}
|
||||
|
||||
void TidalSearchView::TextEdited(const QString &text) {
|
||||
|
||||
const QString trimmed(text.trimmed());
|
||||
|
||||
error_ = false;
|
||||
|
||||
// Add results to the back model, switch models after some delay.
|
||||
back_model_->Clear();
|
||||
current_model_ = back_model_;
|
||||
current_proxy_ = back_proxy_;
|
||||
swap_models_timer_->start();
|
||||
|
||||
// Cancel the last search (if any) and start the new one.
|
||||
engine_->CancelSearch(last_search_id_);
|
||||
// If text query is empty, don't start a new search
|
||||
if (trimmed.isEmpty()) {
|
||||
last_search_id_ = -1;
|
||||
ui_->label_helptext->setText("Enter search terms above to find music");
|
||||
}
|
||||
else {
|
||||
last_search_id_ = engine_->SearchAsync(trimmed, searchby_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void TidalSearchView::AddResults(int id, const TidalSearch::ResultList &results) {
|
||||
if (id != last_search_id_) return;
|
||||
if (results.isEmpty()) return;
|
||||
current_model_->AddResults(results);
|
||||
}
|
||||
|
||||
void TidalSearchView::SearchError(const int id, const QString error) {
|
||||
error_ = true;
|
||||
ui_->label_helptext->setText(error);
|
||||
ui_->results_stack->setCurrentWidget(ui_->help_page);
|
||||
}
|
||||
|
||||
void TidalSearchView::SwapModels() {
|
||||
|
||||
art_requests_.clear();
|
||||
|
||||
std::swap(front_model_, back_model_);
|
||||
std::swap(front_proxy_, back_proxy_);
|
||||
|
||||
ui_->results->setModel(front_proxy_);
|
||||
|
||||
if (ui_->search->text().trimmed().isEmpty() || error_) {
|
||||
ui_->results_stack->setCurrentWidget(ui_->help_page);
|
||||
}
|
||||
else {
|
||||
ui_->results_stack->setCurrentWidget(ui_->results_page);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void TidalSearchView::LazyLoadArt(const QModelIndex &proxy_index) {
|
||||
|
||||
if (!proxy_index.isValid() || proxy_index.model() != front_proxy_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Already loading art for this item?
|
||||
if (proxy_index.data(TidalSearchModel::Role_LazyLoadingArt).isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Should we even load art at all?
|
||||
if (!app_->collection_model()->use_pretty_covers()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Is this an album?
|
||||
const CollectionModel::GroupBy container_type = CollectionModel::GroupBy(proxy_index.data(CollectionModel::Role_ContainerType).toInt());
|
||||
if (container_type != CollectionModel::GroupBy_Album &&
|
||||
container_type != CollectionModel::GroupBy_AlbumArtist &&
|
||||
container_type != CollectionModel::GroupBy_YearAlbum &&
|
||||
container_type != CollectionModel::GroupBy_OriginalYearAlbum) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark the item as loading art
|
||||
const QModelIndex source_index = front_proxy_->mapToSource(proxy_index);
|
||||
QStandardItem *item = front_model_->itemFromIndex(source_index);
|
||||
item->setData(true, TidalSearchModel::Role_LazyLoadingArt);
|
||||
|
||||
// Walk down the item's children until we find a track
|
||||
while (item->rowCount()) {
|
||||
item = item->child(0);
|
||||
}
|
||||
|
||||
// Get the track's Result
|
||||
const TidalSearch::Result result = item->data(TidalSearchModel::Role_Result).value<TidalSearch::Result>();
|
||||
|
||||
// Load the art.
|
||||
int id = engine_->LoadArtAsync(result);
|
||||
art_requests_[id] = source_index;
|
||||
|
||||
}
|
||||
|
||||
void TidalSearchView::ArtLoaded(int id, const QPixmap &pixmap) {
|
||||
|
||||
if (!art_requests_.contains(id)) return;
|
||||
QModelIndex index = art_requests_.take(id);
|
||||
|
||||
if (!pixmap.isNull()) {
|
||||
front_model_->itemFromIndex(index)->setData(pixmap, Qt::DecorationRole);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MimeData *TidalSearchView::SelectedMimeData() {
|
||||
|
||||
if (!ui_->results->selectionModel()) return nullptr;
|
||||
|
||||
// Get all selected model indexes
|
||||
QModelIndexList indexes = ui_->results->selectionModel()->selectedRows();
|
||||
if (indexes.isEmpty()) {
|
||||
// There's nothing selected - take the first thing in the model that isn't a divider.
|
||||
for (int i = 0; i < front_proxy_->rowCount(); ++i) {
|
||||
QModelIndex index = front_proxy_->index(i, 0);
|
||||
if (!index.data(CollectionModel::Role_IsDivider).toBool()) {
|
||||
indexes << index;
|
||||
ui_->results->setCurrentIndex(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Still got nothing? Give up.
|
||||
if (indexes.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Get items for these indexes
|
||||
QList<QStandardItem*> items;
|
||||
for (const QModelIndex &index : indexes) {
|
||||
items << (front_model_->itemFromIndex(front_proxy_->mapToSource(index)));
|
||||
}
|
||||
|
||||
// Get a MimeData for these items
|
||||
return engine_->LoadTracks(front_model_->GetChildResults(items));
|
||||
|
||||
}
|
||||
|
||||
bool TidalSearchView::eventFilter(QObject *object, QEvent *event) {
|
||||
|
||||
if (object == ui_->search && event->type() == QEvent::KeyRelease) {
|
||||
if (SearchKeyEvent(static_cast<QKeyEvent*>(event))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (object == ui_->results_stack && event->type() == QEvent::ContextMenu) {
|
||||
if (ResultsContextMenuEvent(static_cast<QContextMenuEvent*>(event))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return QWidget::eventFilter(object, event);
|
||||
|
||||
}
|
||||
|
||||
bool TidalSearchView::SearchKeyEvent(QKeyEvent *event) {
|
||||
|
||||
switch (event->key()) {
|
||||
case Qt::Key_Up:
|
||||
ui_->results->UpAndFocus();
|
||||
break;
|
||||
|
||||
case Qt::Key_Down:
|
||||
ui_->results->DownAndFocus();
|
||||
break;
|
||||
|
||||
case Qt::Key_Escape:
|
||||
ui_->search->clear();
|
||||
break;
|
||||
|
||||
case Qt::Key_Return:
|
||||
AddSelectedToPlaylist();
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
event->accept();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool TidalSearchView::ResultsContextMenuEvent(QContextMenuEvent *event) {
|
||||
|
||||
context_menu_ = new QMenu(this);
|
||||
context_actions_ << context_menu_->addAction( IconLoader::Load("media-playback-start"), tr("Append to current playlist"), this, SLOT(AddSelectedToPlaylist()));
|
||||
context_actions_ << context_menu_->addAction( IconLoader::Load("media-playback-start"), tr("Replace current playlist"), this, SLOT(LoadSelected()));
|
||||
context_actions_ << context_menu_->addAction( IconLoader::Load("document-new"), tr("Open in new playlist"), this, SLOT(OpenSelectedInNewPlaylist()));
|
||||
|
||||
context_menu_->addSeparator();
|
||||
context_actions_ << context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue track"), this, SLOT(AddSelectedToPlaylistEnqueue()));
|
||||
|
||||
context_menu_->addSeparator();
|
||||
|
||||
if (ui_->results->selectionModel() && ui_->results->selectionModel()->selectedRows().length() == 1) {
|
||||
context_actions_ << context_menu_->addAction(IconLoader::Load("system-search"), tr("Search for this"), this, SLOT(SearchForThis()));
|
||||
}
|
||||
|
||||
context_menu_->addSeparator();
|
||||
context_menu_->addMenu(tr("Group by"))->addActions(group_by_actions_->actions());
|
||||
context_menu_->addAction(IconLoader::Load("configure"), tr("Configure Tidal..."), this, SLOT(OpenSettingsDialog()));
|
||||
|
||||
const bool enable_context_actions = ui_->results->selectionModel() && ui_->results->selectionModel()->hasSelection();
|
||||
|
||||
for (QAction *action : context_actions_) {
|
||||
action->setEnabled(enable_context_actions);
|
||||
}
|
||||
|
||||
context_menu_->popup(event->globalPos());
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void TidalSearchView::AddSelectedToPlaylist() {
|
||||
emit AddToPlaylist(SelectedMimeData());
|
||||
}
|
||||
|
||||
void TidalSearchView::LoadSelected() {
|
||||
MimeData *data = SelectedMimeData();
|
||||
if (!data) return;
|
||||
|
||||
data->clear_first_ = true;
|
||||
emit AddToPlaylist(data);
|
||||
}
|
||||
|
||||
void TidalSearchView::AddSelectedToPlaylistEnqueue() {
|
||||
MimeData *data = SelectedMimeData();
|
||||
if (!data) return;
|
||||
|
||||
data->enqueue_now_ = true;
|
||||
emit AddToPlaylist(data);
|
||||
}
|
||||
|
||||
void TidalSearchView::OpenSelectedInNewPlaylist() {
|
||||
MimeData *data = SelectedMimeData();
|
||||
if (!data) return;
|
||||
|
||||
data->open_in_new_playlist_ = true;
|
||||
emit AddToPlaylist(data);
|
||||
}
|
||||
|
||||
void TidalSearchView::SearchForThis() {
|
||||
StartSearch(ui_->results->selectionModel()->selectedRows().first().data().toString());
|
||||
}
|
||||
|
||||
void TidalSearchView::showEvent(QShowEvent *e) {
|
||||
QWidget::showEvent(e);
|
||||
FocusSearchField();
|
||||
}
|
||||
|
||||
void TidalSearchView::FocusSearchField() {
|
||||
ui_->search->setFocus();
|
||||
ui_->search->selectAll();
|
||||
}
|
||||
|
||||
void TidalSearchView::hideEvent(QHideEvent *e) {
|
||||
QWidget::hideEvent(e);
|
||||
}
|
||||
|
||||
void TidalSearchView::FocusOnFilter(QKeyEvent *event) {
|
||||
ui_->search->setFocus();
|
||||
QApplication::sendEvent(ui_->search, event);
|
||||
}
|
||||
|
||||
void TidalSearchView::OpenSettingsDialog() {
|
||||
app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Tidal);
|
||||
}
|
||||
|
||||
void TidalSearchView::GroupByClicked(QAction *action) {
|
||||
|
||||
if (action->property("group_by").isNull()) {
|
||||
if (!group_by_dialog_) {
|
||||
group_by_dialog_.reset(new GroupByDialog);
|
||||
connect(group_by_dialog_.data(), SIGNAL(Accepted(CollectionModel::Grouping)), SLOT(SetGroupBy(CollectionModel::Grouping)));
|
||||
}
|
||||
|
||||
group_by_dialog_->show();
|
||||
return;
|
||||
}
|
||||
|
||||
SetGroupBy(action->property("group_by").value<CollectionModel::Grouping>());
|
||||
|
||||
}
|
||||
|
||||
void TidalSearchView::SetGroupBy(const CollectionModel::Grouping &g) {
|
||||
|
||||
// Clear requests: changing "group by" on the models will cause all the items to be removed/added again,
|
||||
// so all the QModelIndex here will become invalid. New requests will be created for those
|
||||
// songs when they will be displayed again anyway (when TidalSearchItemDelegate::paint will call LazyLoadArt)
|
||||
art_requests_.clear();
|
||||
// Update the models
|
||||
front_model_->SetGroupBy(g, true);
|
||||
back_model_->SetGroupBy(g, false);
|
||||
|
||||
// Save the setting
|
||||
QSettings s;
|
||||
s.beginGroup(TidalSettingsPage::kSettingsGroup);
|
||||
s.setValue("group_by1", int(g.first));
|
||||
s.setValue("group_by2", int(g.second));
|
||||
s.setValue("group_by3", int(g.third));
|
||||
s.endGroup();
|
||||
|
||||
// Make sure the correct action is checked.
|
||||
for (QAction *action : group_by_actions_->actions()) {
|
||||
if (action->property("group_by").isNull()) continue;
|
||||
|
||||
if (g == action->property("group_by").value<CollectionModel::Grouping>()) {
|
||||
action->setChecked(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check the advanced action
|
||||
group_by_actions_->actions().last()->setChecked(true);
|
||||
|
||||
}
|
||||
|
||||
void TidalSearchView::SearchBySongsClicked(bool checked) {
|
||||
SetSearchBy(TidalSettingsPage::SearchBy_Songs);
|
||||
}
|
||||
|
||||
void TidalSearchView::SearchByAlbumsClicked(bool checked) {
|
||||
SetSearchBy(TidalSettingsPage::SearchBy_Albums);
|
||||
}
|
||||
|
||||
void TidalSearchView::SetSearchBy(TidalSettingsPage::SearchBy searchby) {
|
||||
searchby_ = searchby;
|
||||
QSettings s;
|
||||
s.beginGroup(TidalSettingsPage::kSettingsGroup);
|
||||
s.setValue("searchby", int(searchby));
|
||||
s.endGroup();
|
||||
TextEdited(ui_->search->text());
|
||||
}
|
139
src/tidal/tidalsearchview.h
Normal file
139
src/tidal/tidalsearchview.h
Normal file
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This code was part of Clementine (GlobalSearch)
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALSEARCHVIEW_H
|
||||
#define TIDALSEARCHVIEW_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
#include <QMap>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QIcon>
|
||||
#include <QPixmap>
|
||||
#include <QMimeData>
|
||||
#include <QMenu>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QAction>
|
||||
#include <QActionGroup>
|
||||
#include <QtEvents>
|
||||
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "settings/settingsdialog.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
#include "tidalsearch.h"
|
||||
#include "settings/tidalsettingspage.h"
|
||||
|
||||
class Application;
|
||||
class GroupByDialog;
|
||||
class TidalSearchModel;
|
||||
class Ui_TidalSearchView;
|
||||
|
||||
class TidalSearchView : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TidalSearchView(Application *app, QWidget *parent = nullptr);
|
||||
~TidalSearchView();
|
||||
|
||||
static const int kSwapModelsTimeoutMsec;
|
||||
|
||||
void LazyLoadArt(const QModelIndex &index);
|
||||
|
||||
void showEvent(QShowEvent *e);
|
||||
void hideEvent(QHideEvent *e);
|
||||
bool eventFilter(QObject *object, QEvent *event);
|
||||
|
||||
public slots:
|
||||
void ReloadSettings();
|
||||
void StartSearch(const QString &query);
|
||||
void FocusSearchField();
|
||||
void OpenSettingsDialog();
|
||||
|
||||
signals:
|
||||
void AddToPlaylist(QMimeData *data);
|
||||
|
||||
private slots:
|
||||
void SwapModels();
|
||||
void TextEdited(const QString &text);
|
||||
void AddResults(int id, const TidalSearch::ResultList &results);
|
||||
void SearchError(const int id, const QString error);
|
||||
void ArtLoaded(int id, const QPixmap &pixmap);
|
||||
|
||||
void FocusOnFilter(QKeyEvent *event);
|
||||
|
||||
void AddSelectedToPlaylist();
|
||||
void LoadSelected();
|
||||
void OpenSelectedInNewPlaylist();
|
||||
void AddSelectedToPlaylistEnqueue();
|
||||
|
||||
void SearchForThis();
|
||||
|
||||
void SearchBySongsClicked(bool);
|
||||
void SearchByAlbumsClicked(bool);
|
||||
void GroupByClicked(QAction *action);
|
||||
void SetSearchBy(TidalSettingsPage::SearchBy searchby);
|
||||
void SetGroupBy(const CollectionModel::Grouping &g);
|
||||
|
||||
private:
|
||||
MimeData *SelectedMimeData();
|
||||
|
||||
bool SearchKeyEvent(QKeyEvent *event);
|
||||
bool ResultsContextMenuEvent(QContextMenuEvent *event);
|
||||
|
||||
Application *app_;
|
||||
TidalSearch *engine_;
|
||||
Ui_TidalSearchView *ui_;
|
||||
QScopedPointer<GroupByDialog> group_by_dialog_;
|
||||
|
||||
QMenu *context_menu_;
|
||||
QList<QAction*> context_actions_;
|
||||
QActionGroup *group_by_actions_;
|
||||
|
||||
int last_search_id_;
|
||||
|
||||
// Like graphics APIs have a front buffer and a back buffer, there's a front model and a back model
|
||||
// The front model is the one that's shown in the UI and the back model is the one that lies in wait.
|
||||
// current_model_ will point to either the front or the back model.
|
||||
TidalSearchModel *front_model_;
|
||||
TidalSearchModel *back_model_;
|
||||
TidalSearchModel *current_model_;
|
||||
|
||||
QSortFilterProxyModel *front_proxy_;
|
||||
QSortFilterProxyModel *back_proxy_;
|
||||
QSortFilterProxyModel *current_proxy_;
|
||||
|
||||
QMap<int, QModelIndex> art_requests_;
|
||||
|
||||
QTimer *swap_models_timer_;
|
||||
|
||||
QIcon search_icon_;
|
||||
QIcon warning_icon_;
|
||||
|
||||
TidalSettingsPage::SearchBy searchby_;
|
||||
bool error_;
|
||||
|
||||
};
|
||||
|
||||
#endif // TIDALSEARCHVIEW_H
|
259
src/tidal/tidalsearchview.ui
Normal file
259
src/tidal/tidalsearchview.ui
Normal file
@ -0,0 +1,259 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TidalSearchView</class>
|
||||
<widget class="QWidget" name="TidalSearchView">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>437</width>
|
||||
<height>633</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_search" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="layout_top" stretch="0,0">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="layout_search">
|
||||
<item>
|
||||
<widget class="QSearchField" name="search" native="true">
|
||||
<property name="placeholderText" stdset="0">
|
||||
<string>Search for anything</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="settings">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="popupMode">
|
||||
<enum>QToolButton::InstantPopup</enum>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="layout_searchby">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetFixedSize</enum>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_searchby">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Search by</string>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="radiobutton_searchbyalbums">
|
||||
<property name="text">
|
||||
<string>albu&ms</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="radiobutton_searchbysongs">
|
||||
<property name="text">
|
||||
<string>songs</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="results_stack">
|
||||
<property name="currentIndex">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="results_page">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="AutoExpandingTreeView" name="results">
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::DragOnly</enum>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
<property name="allColumnsShowFocus">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="help_page">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QScrollArea" name="help_frame">
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="help_frame_contents">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>435</width>
|
||||
<height>533</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QWidget" name="widget" native="true">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>9</x>
|
||||
<y>109</y>
|
||||
<width>420</width>
|
||||
<height>100</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<property name="leftMargin">
|
||||
<number>32</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>16</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>32</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>64</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_helptext">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>80</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Enter search terms above to find music</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>QSearchField</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>3rdparty/qocoa/qsearchfield.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>AutoExpandingTreeView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>widgets/autoexpandingtreeview.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
832
src/tidal/tidalservice.cpp
Normal file
832
src/tidal/tidalservice.cpp
Normal file
@ -0,0 +1,832 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <QVector>
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QTimer>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
#include <QMenu>
|
||||
#include <QSettings>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/mergedproxymodel.h"
|
||||
#include "core/network.h"
|
||||
#include "core/song.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "core/utilities.h"
|
||||
#include "internet/internetmodel.h"
|
||||
#include "tidalservice.h"
|
||||
#include "tidalsearch.h"
|
||||
#include "settings/tidalsettingspage.h"
|
||||
|
||||
const char *TidalService::kServiceName = "Tidal";
|
||||
const char *TidalService::kApiUrl = "https://listen.tidal.com/v1";
|
||||
const char *TidalService::kAuthUrl = "https://listen.tidal.com/v1/login/username";
|
||||
const char *TidalService::kResourcesUrl = "http://resources.tidal.com";
|
||||
const char *TidalService::kApiToken = "P5Xbeo5LFvESeDy6";
|
||||
|
||||
const int TidalService::kSearchDelayMsec = 1500;
|
||||
const int TidalService::kSearchAlbumsLimit = 40;
|
||||
const int TidalService::kSearchTracksLimit = 10;
|
||||
|
||||
typedef QPair<QString, QString> Param;
|
||||
|
||||
TidalService::TidalService(Application *app, InternetModel *parent)
|
||||
: InternetService(kServiceName, app, parent, parent),
|
||||
network_(new NetworkAccessManager(this)),
|
||||
search_delay_(new QTimer(this)),
|
||||
pending_search_id_(0),
|
||||
next_pending_search_id_(1),
|
||||
search_requests_(0),
|
||||
login_sent_(false) {
|
||||
|
||||
search_delay_->setInterval(kSearchDelayMsec);
|
||||
search_delay_->setSingleShot(true);
|
||||
connect(search_delay_, SIGNAL(timeout()), SLOT(StartSearch()));
|
||||
|
||||
ReloadSettings();
|
||||
LoadSessionID();
|
||||
|
||||
}
|
||||
|
||||
TidalService::~TidalService() {}
|
||||
|
||||
void TidalService::ShowConfig() {
|
||||
app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Tidal);
|
||||
}
|
||||
|
||||
void TidalService::ReloadSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(TidalSettingsPage::kSettingsGroup);
|
||||
username_ = s.value("username").toString();
|
||||
password_ = s.value("password").toString();
|
||||
quality_ = s.value("quality").toString();
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void TidalService::LoadSessionID() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(TidalSettingsPage::kSettingsGroup);
|
||||
if (!s.contains("user_id") ||!s.contains("session_id") || !s.contains("country_code")) return;
|
||||
session_id_ = s.value("session_id").toString();
|
||||
user_id_ = s.value("user_id").toInt();
|
||||
country_code_ = s.value("country_code").toString();
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void TidalService::Login(const QString &username, const QString &password) {
|
||||
Login(nullptr, username, password);
|
||||
}
|
||||
|
||||
void TidalService::Login(TidalSearchContext *search_ctx, const QString &username, const QString &password) {
|
||||
|
||||
login_sent_ = true;
|
||||
|
||||
int id = 0;
|
||||
if (search_ctx) {
|
||||
search_ctx->login_sent = true;
|
||||
search_ctx->login_attempts++;
|
||||
id = search_ctx->id;
|
||||
}
|
||||
|
||||
typedef QPair<QString, QString> Arg;
|
||||
typedef QList<Arg> ArgList;
|
||||
|
||||
typedef QPair<QByteArray, QByteArray> EncodedArg;
|
||||
typedef QList<EncodedArg> EncodedArgList;
|
||||
|
||||
ArgList args = ArgList() <<Arg("token", kApiToken) << Arg("username", username) << Arg("password", password) << Arg("clientVersion", "2.2.1--7");
|
||||
|
||||
QStringList query_items;
|
||||
QUrlQuery url_query;
|
||||
for (const Arg &arg : args) {
|
||||
EncodedArg encoded_arg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second));
|
||||
query_items << QString(encoded_arg.first + "=" + encoded_arg.second);
|
||||
url_query.addQueryItem(encoded_arg.first, encoded_arg.second);
|
||||
}
|
||||
|
||||
QUrl url(kAuthUrl);
|
||||
QNetworkRequest req(url);
|
||||
|
||||
req.setRawHeader("Origin", "http://listen.tidal.com");
|
||||
QNetworkReply *reply = network_->post(req, url_query.toString(QUrl::FullyEncoded).toUtf8());
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleAuthReply(QNetworkReply*, int)), reply, id);
|
||||
|
||||
}
|
||||
|
||||
void TidalService::HandleAuthReply(QNetworkReply *reply, int id) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
login_sent_ = false;
|
||||
|
||||
TidalSearchContext *search_ctx(nullptr);
|
||||
if (id != 0 && requests_search_.contains(id)) {
|
||||
search_ctx = requests_search_.value(id);
|
||||
search_ctx->login_sent = false;
|
||||
}
|
||||
|
||||
//int http_status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
if (reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
if (search_ctx) Error(search_ctx, failure_reason);
|
||||
emit LoginFailure(failure_reason);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
// See if there is Json data containing "userMessage" - then use that instead.
|
||||
QByteArray data(reply->readAll());
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
|
||||
QString failure_reason;
|
||||
if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (!json_obj.isEmpty() && json_obj.contains("userMessage")) {
|
||||
failure_reason = QString("Authentication failure: %1").arg(json_obj["userMessage"].toString());
|
||||
}
|
||||
else {
|
||||
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
}
|
||||
else {
|
||||
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
if (search_ctx) Error(search_ctx, failure_reason);
|
||||
emit LoginFailure(failure_reason);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray data(reply->readAll());
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
QString failure_reason("Authentication reply from server missing Json data.");
|
||||
if (search_ctx) Error(search_ctx, failure_reason);
|
||||
emit LoginFailure(failure_reason);
|
||||
return;
|
||||
}
|
||||
|
||||
if (json_doc.isNull() || json_doc.isEmpty()) {
|
||||
QString failure_reason("Authentication reply from server has empty Json document.");
|
||||
if (search_ctx) Error(search_ctx, failure_reason);
|
||||
emit LoginFailure(failure_reason);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
QString failure_reason("Authentication reply from server has Json document that is not an object.");
|
||||
if (search_ctx) Error(search_ctx, failure_reason);
|
||||
emit LoginFailure(failure_reason);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
QString failure_reason("Authentication reply from server has empty Json object.");
|
||||
if (search_ctx) Error(search_ctx, failure_reason);
|
||||
emit LoginFailure(failure_reason);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !json_obj.contains("userId") || !json_obj.contains("sessionId") || !json_obj.contains("countryCode") ) {
|
||||
QString failure_reason = tr("Authentication reply from server is missing userId, sessionId or countryCode");
|
||||
if (search_ctx) Error(search_ctx, failure_reason);
|
||||
emit LoginFailure(failure_reason);
|
||||
return;
|
||||
}
|
||||
|
||||
country_code_ = json_obj["countryCode"].toString();
|
||||
session_id_ = json_obj["sessionId"].toString();
|
||||
user_id_ = json_obj["userId"].toInt();
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(TidalSettingsPage::kSettingsGroup);
|
||||
s.setValue("user_id", user_id_);
|
||||
s.setValue("session_id", session_id_);
|
||||
s.setValue("country_code", country_code_);
|
||||
s.endGroup();
|
||||
|
||||
qLog(Debug) << "Tidal: Login successful" << "user id" << user_id_ << "session id" << session_id_ << "country code" << country_code_;
|
||||
|
||||
if (search_ctx) {
|
||||
qLog(Debug) << "Tidal: Resuming search";
|
||||
SendSearch(search_ctx);
|
||||
}
|
||||
|
||||
emit LoginSuccess();
|
||||
|
||||
}
|
||||
|
||||
void TidalService::Logout() {
|
||||
|
||||
user_id_ = 0;
|
||||
session_id_.clear();
|
||||
country_code_.clear();
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(TidalSettingsPage::kSettingsGroup);
|
||||
s.remove("user_id");
|
||||
s.remove("session_id");
|
||||
s.remove("country_code");
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
QNetworkReply *TidalService::CreateRequest(const QString &ressource_name, const QList<Param> ¶ms) {
|
||||
|
||||
typedef QPair<QString, QString> Arg;
|
||||
typedef QList<Arg> ArgList;
|
||||
|
||||
typedef QPair<QByteArray, QByteArray> EncodedArg;
|
||||
typedef QList<EncodedArg> EncodedArgList;
|
||||
|
||||
ArgList args = ArgList() << params
|
||||
<< Arg("sessionId", session_id_)
|
||||
<< Arg("countryCode", country_code_);
|
||||
|
||||
QStringList query_items;
|
||||
QUrlQuery url_query;
|
||||
for (const Arg& arg : args) {
|
||||
EncodedArg encoded_arg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second));
|
||||
query_items << QString(encoded_arg.first + "=" + encoded_arg.second);
|
||||
url_query.addQueryItem(encoded_arg.first, encoded_arg.second);
|
||||
}
|
||||
|
||||
QUrl url(kApiUrl + QString("/") + ressource_name);
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest req(url);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
|
||||
//qLog(Debug) << "Tidal: Sending request" << url;
|
||||
|
||||
return reply;
|
||||
|
||||
}
|
||||
|
||||
QJsonObject TidalService::ExtractJsonObj(TidalSearchContext *search_ctx, QNetworkReply *reply) {
|
||||
|
||||
QByteArray data;
|
||||
|
||||
//int http_status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
data = reply->readAll();
|
||||
}
|
||||
else {
|
||||
if (reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
Error(search_ctx, failure_reason);
|
||||
}
|
||||
else {
|
||||
// See if there is Json data containing "userMessage" - then use that instead.
|
||||
data = reply->readAll();
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
|
||||
QString failure_reason;
|
||||
if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (!json_obj.isEmpty() && json_obj.contains("userMessage")) {
|
||||
failure_reason = json_obj["userMessage"].toString();
|
||||
}
|
||||
else {
|
||||
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
}
|
||||
else {
|
||||
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
if (reply->error() == QNetworkReply::ContentAccessDenied || reply->error() == QNetworkReply::ContentOperationNotPermittedError || reply->error() == QNetworkReply::AuthenticationRequiredError) {
|
||||
// Session is probably expired, attempt to login once
|
||||
Logout();
|
||||
if (search_ctx->login_attempts < 1 && !username_.isEmpty() && !password_.isEmpty()) {
|
||||
qLog(Error) << "Tidal:" << failure_reason;
|
||||
qLog(Error) << "Tidal:" << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
qLog(Error) << "Tidal:" << "Attempting to login.";
|
||||
Login(search_ctx, username_, password_);
|
||||
}
|
||||
else {
|
||||
Error(search_ctx, failure_reason);
|
||||
}
|
||||
}
|
||||
else if (reply->error() == QNetworkReply::ContentNotFoundError) { // Ignore this error
|
||||
qLog(Error) << "Tidal:" << failure_reason;
|
||||
}
|
||||
else { // Fail
|
||||
Error(search_ctx, failure_reason);
|
||||
}
|
||||
}
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
Error(search_ctx, "Reply from server missing Json data.");
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (json_doc.isNull() || json_doc.isEmpty()) {
|
||||
Error(search_ctx, "Received empty Json document.");
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
Error(search_ctx, "Json document is not an object.");
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
Error(search_ctx, "Received empty Json object.");
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
//qLog(Debug) << json_obj;
|
||||
|
||||
return json_obj;
|
||||
|
||||
}
|
||||
|
||||
QJsonArray TidalService::ExtractItems(TidalSearchContext *search_ctx, QNetworkReply *reply) {
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(search_ctx, reply);
|
||||
if (json_obj.isEmpty()) return QJsonArray();
|
||||
|
||||
if (!json_obj.contains("items")) {
|
||||
Error(search_ctx, "Json reply is missing items.");
|
||||
return QJsonArray();
|
||||
}
|
||||
|
||||
QJsonArray json_items = json_obj["items"].toArray();
|
||||
if (json_items.isEmpty()) {
|
||||
Error(search_ctx, "No match.");
|
||||
return QJsonArray();
|
||||
}
|
||||
|
||||
return json_items;
|
||||
|
||||
}
|
||||
|
||||
int TidalService::Search(const QString &text, TidalSettingsPage::SearchBy searchby) {
|
||||
|
||||
pending_search_id_ = next_pending_search_id_;
|
||||
pending_search_ = text;
|
||||
pending_searchby_ = searchby;
|
||||
|
||||
next_pending_search_id_++;
|
||||
|
||||
if (text.isEmpty()) {
|
||||
search_delay_->stop();
|
||||
return pending_search_id_;
|
||||
}
|
||||
search_delay_->start();
|
||||
|
||||
return pending_search_id_;
|
||||
|
||||
}
|
||||
|
||||
void TidalService::StartSearch() {
|
||||
|
||||
if (username_.isEmpty() || password_.isEmpty()) {
|
||||
emit SearchError(pending_search_id_, "Missing username and/or password.");
|
||||
next_pending_search_id_ = 1;
|
||||
ShowConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
TidalSearchContext *search_ctx = CreateSearch(pending_search_id_, pending_search_);
|
||||
if (authenticated()) SendSearch(search_ctx);
|
||||
else Login(search_ctx, username_, password_);
|
||||
|
||||
}
|
||||
|
||||
TidalSearchContext *TidalService::CreateSearch(const int search_id, const QString text) {
|
||||
|
||||
TidalSearchContext *search_ctx = new TidalSearchContext;
|
||||
search_ctx->id = search_id;
|
||||
search_ctx->text = text;
|
||||
search_ctx->album_requests = 0;
|
||||
search_ctx->song_requests = 0;
|
||||
search_ctx->requests_album_.clear();
|
||||
search_ctx->requests_song_.clear();
|
||||
search_ctx->login_attempts = 0;
|
||||
requests_search_.insert(search_id, search_ctx);
|
||||
return search_ctx;
|
||||
|
||||
}
|
||||
|
||||
void TidalService::SendSearch(TidalSearchContext *search_ctx) {
|
||||
|
||||
QList<Param> parameters;
|
||||
parameters << Param("query", search_ctx->text);
|
||||
|
||||
QString searchparam;
|
||||
switch (pending_searchby_) {
|
||||
case TidalSettingsPage::SearchBy_Songs:
|
||||
searchparam = "search/tracks";
|
||||
parameters << Param("limit", QString::number(kSearchTracksLimit));
|
||||
break;
|
||||
case TidalSettingsPage::SearchBy_Albums:
|
||||
default:
|
||||
searchparam = "search/albums";
|
||||
parameters << Param("limit", QString::number(kSearchAlbumsLimit));
|
||||
break;
|
||||
}
|
||||
|
||||
QNetworkReply *reply = CreateRequest(searchparam, parameters);
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(SearchFinished(QNetworkReply*, int)), reply, search_ctx->id);
|
||||
|
||||
}
|
||||
|
||||
void TidalService::SearchFinished(QNetworkReply *reply, int id) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (!requests_search_.contains(id)) return;
|
||||
TidalSearchContext *search_ctx = requests_search_.value(id);
|
||||
|
||||
QJsonArray json_items = ExtractItems(search_ctx, reply);
|
||||
if (json_items.isEmpty()) {
|
||||
CheckFinish(search_ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
//qLog(Debug) << json_items;
|
||||
|
||||
QVector<QString> albums;
|
||||
for (const QJsonValue &value : json_items) {
|
||||
if (!value.isObject()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item not a object.";
|
||||
qLog(Debug) << value;
|
||||
continue;
|
||||
}
|
||||
QJsonObject json_obj = value.toObject();
|
||||
//qLog(Debug) << json_obj;
|
||||
int album_id(0);
|
||||
QString album("");
|
||||
if (json_obj.contains("type")) {
|
||||
// This was a albums search
|
||||
if (!json_obj.contains("id") || !json_obj.contains("title")) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item is missing ID or title.";
|
||||
qLog(Debug) << json_obj;
|
||||
continue;
|
||||
}
|
||||
album_id = json_obj["id"].toInt();
|
||||
album = json_obj["title"].toString();
|
||||
}
|
||||
else if (json_obj.contains("album")) {
|
||||
// This was a tracks search
|
||||
QJsonValue json_value_album = json_obj["album"];
|
||||
if (!json_value_album.isObject()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item album is not a object.";
|
||||
qLog(Debug) << json_value_album;
|
||||
continue;
|
||||
}
|
||||
QJsonObject json_album = json_value_album.toObject();
|
||||
if (!json_album.contains("id") || !json_album.contains("title")) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item album is missing ID or title.";
|
||||
qLog(Debug) << json_album;
|
||||
continue;
|
||||
}
|
||||
album_id = json_album["id"].toInt();
|
||||
album = json_album["title"].toString();
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item missing type or album.";
|
||||
qLog(Debug) << json_obj;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (search_ctx->requests_album_.contains(album_id)) continue;
|
||||
|
||||
if (!json_obj.contains("artist") || !json_obj.contains("title") || !json_obj.contains("audioQuality")) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item missing artist, title or audioQuality.";
|
||||
qLog(Debug) << json_obj;
|
||||
continue;
|
||||
}
|
||||
QJsonValue json_value_artist = json_obj["artist"];
|
||||
if (!json_value_artist.isObject()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item artist is not a object.";
|
||||
qLog(Debug) << json_value_artist;
|
||||
continue;
|
||||
}
|
||||
QJsonObject json_artist = json_value_artist.toObject();
|
||||
if (!json_artist.contains("name")) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, item artist missing name.";
|
||||
qLog(Debug) << json_artist;
|
||||
continue;
|
||||
}
|
||||
QString artist = json_artist["name"].toString();
|
||||
|
||||
QString quality = json_obj["audioQuality"].toString();
|
||||
|
||||
//qLog(Debug) << "Tidal:" << artist << album << quality;
|
||||
|
||||
QString artist_album(QString("%1-%2").arg(artist).arg(album));
|
||||
if (albums.contains(artist_album)) {
|
||||
qLog(Debug) << "Tidal: Skipping duplicate album" << artist << album << quality;
|
||||
continue;
|
||||
}
|
||||
albums.insert(0, artist_album);
|
||||
|
||||
search_ctx->requests_album_.insert(album_id, album_id);
|
||||
GetAlbum(search_ctx, album_id);
|
||||
search_ctx->album_requests++;
|
||||
if (search_ctx->album_requests >= kSearchAlbumsLimit) break;
|
||||
}
|
||||
|
||||
CheckFinish(search_ctx);
|
||||
|
||||
}
|
||||
|
||||
void TidalService::GetAlbum(TidalSearchContext *search_ctx, const int album_id) {
|
||||
|
||||
QList<Param> parameters;
|
||||
parameters << Param("token", session_id_)
|
||||
<< Param("soundQuality", quality_);
|
||||
|
||||
QNetworkReply *reply = CreateRequest(QString("albums/%1/tracks").arg(album_id), parameters);
|
||||
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(GetAlbumFinished(QNetworkReply*, int, int)), reply, search_ctx->id, album_id);
|
||||
|
||||
}
|
||||
|
||||
void TidalService::GetAlbumFinished(QNetworkReply *reply, int search_id, int album_id) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (!requests_search_.contains(search_id)) return;
|
||||
TidalSearchContext *search_ctx = requests_search_.value(search_id);
|
||||
|
||||
if (!search_ctx->requests_album_.contains(album_id)) return;
|
||||
search_ctx->album_requests--;
|
||||
|
||||
QJsonArray json_items = ExtractItems(search_ctx, reply);
|
||||
if (json_items.isEmpty()) {
|
||||
CheckFinish(search_ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
bool compilation = false;
|
||||
bool multidisc = false;
|
||||
Song *first_song(nullptr);
|
||||
QList<Song *> songs;
|
||||
for (const QJsonValue &value : json_items) {
|
||||
Song *song = ParseSong(search_ctx, album_id, value);
|
||||
if (!song) continue;
|
||||
songs << song;
|
||||
if (song->disc() >= 2) multidisc = true;
|
||||
if (song->is_compilation() || (first_song && song->artist() != first_song->artist())) compilation = true;
|
||||
if (!first_song) first_song = song;
|
||||
}
|
||||
if (compilation || multidisc) {
|
||||
for (Song *song : songs) {
|
||||
if (compilation) song->set_compilation_detected(true);
|
||||
if (multidisc) {
|
||||
QString album_full(QString("%1 - (Disc %2)").arg(song->album()).arg(song->disc()));
|
||||
song->set_album(album_full);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckFinish(search_ctx);
|
||||
|
||||
}
|
||||
|
||||
Song *TidalService::ParseSong(TidalSearchContext *search_ctx, const int album_id, const QJsonValue &value) {
|
||||
|
||||
Song song;
|
||||
|
||||
if (!value.isObject()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, track is not a object.";
|
||||
qLog(Debug) << value;
|
||||
return nullptr;
|
||||
}
|
||||
QJsonObject json_obj = value.toObject();
|
||||
|
||||
//qLog(Debug) << json_obj;
|
||||
|
||||
if (
|
||||
!json_obj.contains("album") ||
|
||||
!json_obj.contains("allowStreaming") ||
|
||||
!json_obj.contains("artist") ||
|
||||
!json_obj.contains("artists") ||
|
||||
!json_obj.contains("audioQuality") ||
|
||||
!json_obj.contains("duration") ||
|
||||
!json_obj.contains("id") ||
|
||||
!json_obj.contains("streamReady") ||
|
||||
!json_obj.contains("title") ||
|
||||
!json_obj.contains("trackNumber") ||
|
||||
!json_obj.contains("url") ||
|
||||
!json_obj.contains("volumeNumber")
|
||||
) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, track is missing one or more values.";
|
||||
qLog(Debug) << json_obj;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QJsonValue json_value_artist = json_obj["artist"];
|
||||
QJsonValue json_value_album = json_obj["album"];
|
||||
QJsonValue json_duration = json_obj["duration"];
|
||||
QJsonArray json_artists = json_obj["artists"].toArray();
|
||||
|
||||
int id = json_obj["id"].toInt();
|
||||
QString title = json_obj["title"].toString();
|
||||
QString url = json_obj["url"].toString();
|
||||
int track = json_obj["trackNumber"].toInt();
|
||||
int disc = json_obj["volumeNumber"].toInt();
|
||||
bool allow_streaming = json_obj["allowStreaming"].toBool();
|
||||
bool stream_ready = json_obj["streamReady"].toBool();
|
||||
|
||||
if (!json_value_artist.isObject()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, track artist is not a object.";
|
||||
qLog(Debug) << json_value_artist;
|
||||
return nullptr;
|
||||
}
|
||||
QJsonObject json_artist = json_value_artist.toObject();
|
||||
if (!json_artist.contains("name")) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, track artist is missing name.";
|
||||
qLog(Debug) << json_artist;
|
||||
return nullptr;
|
||||
}
|
||||
QString artist = json_artist["name"].toString();
|
||||
|
||||
if (!json_value_album.isObject()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, track album is not a object.";
|
||||
qLog(Debug) << json_value_album;
|
||||
return nullptr;
|
||||
}
|
||||
QJsonObject json_album = json_value_album.toObject();
|
||||
if (!json_album.contains("title") || !json_album.contains("cover")) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, track album is missing title or cover.";
|
||||
qLog(Debug) << json_album;
|
||||
return nullptr;
|
||||
}
|
||||
QString album = json_album["title"].toString();
|
||||
QString cover = json_album["cover"].toString();
|
||||
|
||||
if (!allow_streaming || !stream_ready) {
|
||||
qLog(Error) << "Tidal: Skipping song" << artist << album << title << "because allowStreaming is false OR streamReady is false.";
|
||||
qLog(Debug) << json_obj;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//qLog(Debug) << "id" << id << "track" << track << "disc" << disc << "title" << title << "album" << album << "artist" << artist << cover << allow_streaming << url;
|
||||
|
||||
song.set_album_id(album_id);
|
||||
song.set_artist(artist);
|
||||
song.set_album(album);
|
||||
song.set_title(title);
|
||||
song.set_track(track);
|
||||
song.set_disc(disc);
|
||||
song.set_bitrate(0);
|
||||
song.set_samplerate(0);
|
||||
song.set_bitdepth(0);
|
||||
|
||||
QVariant q_duration = json_duration.toVariant();
|
||||
if (q_duration.isValid()) {
|
||||
quint64 duration = q_duration.toULongLong() * kNsecPerSec;
|
||||
song.set_length_nanosec(duration);
|
||||
}
|
||||
|
||||
// Check and see if there is more than 1 artist on the song.
|
||||
//int i = 0;
|
||||
//for (const QJsonValue &a : json_artists) {
|
||||
//i++;
|
||||
//qLog(Debug) << a << i;
|
||||
//}
|
||||
//if (i > 1) song.set_compilation_detected(true);
|
||||
|
||||
cover = cover.replace("-", "/");
|
||||
QUrl cover_url (QString("%1/images/%2/750x750.jpg").arg(kResourcesUrl).arg(cover));
|
||||
song.set_art_automatic(cover_url.toEncoded());
|
||||
|
||||
if (search_ctx->requests_song_.contains(id)) return search_ctx->requests_song_.value(id);
|
||||
Song *song_new = new Song(song);
|
||||
search_ctx->requests_song_.insert(id, song_new);
|
||||
search_ctx->song_requests++;
|
||||
GetStreamURL(search_ctx, album_id, id);
|
||||
|
||||
return song_new;
|
||||
|
||||
}
|
||||
|
||||
void TidalService::GetStreamURL(TidalSearchContext *search_ctx, const int album_id, const int song_id) {
|
||||
|
||||
QList<Param> parameters;
|
||||
parameters << Param("token", session_id_)
|
||||
<< Param("soundQuality", quality_);
|
||||
|
||||
QNetworkReply *reply = CreateRequest(QString("tracks/%1/streamUrl").arg(song_id), parameters);
|
||||
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(GetStreamURLFinished(QNetworkReply*, int, int)), reply, search_ctx->id, song_id);
|
||||
|
||||
}
|
||||
|
||||
void TidalService::GetStreamURLFinished(QNetworkReply *reply, const int search_id, const int song_id) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (!requests_search_.contains(search_id)) return;
|
||||
TidalSearchContext *search_ctx = requests_search_.value(search_id);
|
||||
|
||||
if (!search_ctx->requests_song_.contains(song_id)) {
|
||||
CheckFinish(search_ctx);
|
||||
return;
|
||||
}
|
||||
Song *song = search_ctx->requests_song_.value(song_id);
|
||||
|
||||
search_ctx->song_requests--;
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(search_ctx, reply);
|
||||
if (json_obj.isEmpty()) {
|
||||
delete search_ctx->requests_song_.take(song_id);
|
||||
CheckFinish(search_ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("url") || !json_obj.contains("codec")) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, stream missing url or codec.";
|
||||
qLog(Debug) << json_obj;
|
||||
delete search_ctx->requests_song_.take(song_id);
|
||||
CheckFinish(search_ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
song->set_url(QUrl(json_obj["url"].toString()));
|
||||
song->set_valid(true);
|
||||
QString codec = json_obj["codec"].toString();
|
||||
if (codec == "AAC") song->set_filetype(Song::Type_MP4);
|
||||
else qLog(Debug) << "Tidal codec" << codec;
|
||||
|
||||
//qLog(Debug) << song->artist() << song->album() << song->title() << song->url() << song->filetype();
|
||||
|
||||
search_ctx->songs << *song;
|
||||
|
||||
delete search_ctx->requests_song_.take(song_id);
|
||||
|
||||
CheckFinish(search_ctx);
|
||||
|
||||
}
|
||||
|
||||
void TidalService::CheckFinish(TidalSearchContext *search_ctx) {
|
||||
|
||||
if (!search_ctx->login_sent && search_ctx->album_requests <= 0 && search_ctx->song_requests <= 0) {
|
||||
if (search_ctx->songs.isEmpty()) emit SearchError(search_ctx->id, search_ctx->error);
|
||||
else emit SearchResults(search_ctx->id, search_ctx->songs);
|
||||
delete requests_search_.take(search_ctx->id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void TidalService::Error(TidalSearchContext *search_ctx, QString error, QString debug) {
|
||||
qLog(Error) << "Tidal:" << error;
|
||||
if (!debug.isEmpty()) qLog(Debug) << debug;
|
||||
if (search_ctx) {
|
||||
search_ctx->error = error;
|
||||
CheckFinish(search_ctx);
|
||||
}
|
||||
}
|
745
src/tidal/tidalservice.cpp.bak
Normal file
745
src/tidal/tidalservice.cpp.bak
Normal file
@ -0,0 +1,745 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <QVector>
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QTimer>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
#include <QMenu>
|
||||
#include <QSettings>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/mergedproxymodel.h"
|
||||
#include "core/network.h"
|
||||
#include "core/song.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "core/utilities.h"
|
||||
#include "internet/internetmodel.h"
|
||||
#include "tidalservice.h"
|
||||
#include "tidalsearch.h"
|
||||
#include "settings/tidalsettingspage.h"
|
||||
|
||||
const char *TidalService::kServiceName = "Tidal";
|
||||
const char *TidalService::kApiUrl = "https://listen.tidal.com/v1";
|
||||
const char *TidalService::kAuthUrl = "https://listen.tidal.com/v1/login/username";
|
||||
const char *TidalService::kResourcesUrl = "http://resources.tidal.com";
|
||||
const char *TidalService::kApiToken = "P5Xbeo5LFvESeDy6";
|
||||
|
||||
const int TidalService::kSearchDelayMsec = 1000;
|
||||
const int TidalService::kSearchAlbumsLimit = 1;
|
||||
const int TidalService::kSearchTracksLimit = 1;
|
||||
|
||||
typedef QPair<QString, QString> Param;
|
||||
|
||||
TidalService::TidalService(Application *app, InternetModel *parent)
|
||||
: InternetService(kServiceName, app, parent, parent),
|
||||
network_(new NetworkAccessManager(this)),
|
||||
search_delay_(new QTimer(this)),
|
||||
pending_search_id_(0),
|
||||
next_pending_search_id_(1),
|
||||
search_requests_(0),
|
||||
login_sent_(false) {
|
||||
|
||||
search_delay_->setInterval(kSearchDelayMsec);
|
||||
search_delay_->setSingleShot(true);
|
||||
connect(search_delay_, SIGNAL(timeout()), SLOT(StartSearch()));
|
||||
|
||||
ReloadSettings();
|
||||
LoadSessionID();
|
||||
|
||||
}
|
||||
|
||||
TidalService::~TidalService() {}
|
||||
|
||||
void TidalService::ShowConfig() {
|
||||
app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Tidal);
|
||||
}
|
||||
|
||||
void TidalService::ReloadSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(TidalSettingsPage::kSettingsGroup);
|
||||
username_ = s.value("username").toString();
|
||||
password_ = s.value("password").toString();
|
||||
quality_ = s.value("quality").toString();
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void TidalService::LoadSessionID() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(TidalSettingsPage::kSettingsGroup);
|
||||
if (!s.contains("user_id") ||!s.contains("session_id") || !s.contains("country_code")) return;
|
||||
session_id_ = s.value("session_id").toString();
|
||||
user_id_ = s.value("user_id").toInt();
|
||||
country_code_ = s.value("country_code").toString();
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void TidalService::Login(const QString &username, const QString &password) {
|
||||
Login(nullptr, username, password);
|
||||
}
|
||||
|
||||
void TidalService::Login(TidalSearchContext *search_ctx, const QString &username, const QString &password) {
|
||||
|
||||
login_sent_ = true;
|
||||
|
||||
int id = 0;
|
||||
if (search_ctx) {
|
||||
search_ctx->login_sent = true;
|
||||
search_ctx->login_attempts++;
|
||||
id = search_ctx->id;
|
||||
}
|
||||
|
||||
typedef QPair<QString, QString> Arg;
|
||||
typedef QList<Arg> ArgList;
|
||||
|
||||
typedef QPair<QByteArray, QByteArray> EncodedArg;
|
||||
typedef QList<EncodedArg> EncodedArgList;
|
||||
|
||||
ArgList args = ArgList() <<Arg("token", kApiToken) << Arg("username", username) << Arg("password", password) << Arg("clientVersion", "2.2.1--7");
|
||||
|
||||
QStringList query_items;
|
||||
QUrlQuery url_query;
|
||||
for (const Arg &arg : args) {
|
||||
EncodedArg encoded_arg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second));
|
||||
query_items << QString(encoded_arg.first + "=" + encoded_arg.second);
|
||||
url_query.addQueryItem(encoded_arg.first, encoded_arg.second);
|
||||
}
|
||||
|
||||
QUrl url(kAuthUrl);
|
||||
QNetworkRequest req(url);
|
||||
|
||||
req.setRawHeader("Origin", "http://listen.tidal.com");
|
||||
QNetworkReply *reply = network_->post(req, url_query.toString(QUrl::FullyEncoded).toUtf8());
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleAuthReply(QNetworkReply*, int)), reply, id);
|
||||
|
||||
}
|
||||
|
||||
void TidalService::HandleAuthReply(QNetworkReply *reply, int id) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
login_sent_ = false;
|
||||
|
||||
TidalSearchContext *search_ctx(nullptr);
|
||||
if (id != 0 && requests_search_.contains(id)) {
|
||||
search_ctx = requests_search_.value(id);
|
||||
search_ctx->login_sent = false;
|
||||
}
|
||||
|
||||
//int http_status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
if (reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
if (search_ctx) Error(search_ctx, failure_reason);
|
||||
emit LoginFailure(failure_reason);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
// See if there is Json data containing "userMessage" - then use that instead.
|
||||
QByteArray data(reply->readAll());
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
|
||||
QString failure_reason;
|
||||
if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (!json_obj.isEmpty() && json_obj.contains("userMessage")) {
|
||||
failure_reason = QString("Authentication failure: %1").arg(json_obj["userMessage"].toString());
|
||||
}
|
||||
else {
|
||||
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
}
|
||||
else {
|
||||
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
if (search_ctx) Error(search_ctx, failure_reason);
|
||||
emit LoginFailure(failure_reason);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray data(reply->readAll());
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
QString failure_reason("Authentication reply from server missing Json data.");
|
||||
if (search_ctx) Error(search_ctx, failure_reason);
|
||||
emit LoginFailure(failure_reason);
|
||||
return;
|
||||
}
|
||||
|
||||
if (json_doc.isNull() || json_doc.isEmpty()) {
|
||||
QString failure_reason("Authentication reply from server has empty Json document.");
|
||||
if (search_ctx) Error(search_ctx, failure_reason);
|
||||
emit LoginFailure(failure_reason);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
QString failure_reason("Authentication reply from server has Json document that is not an object.");
|
||||
if (search_ctx) Error(search_ctx, failure_reason);
|
||||
emit LoginFailure(failure_reason);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
QString failure_reason("Authentication reply from server has empty Json object.");
|
||||
if (search_ctx) Error(search_ctx, failure_reason);
|
||||
emit LoginFailure(failure_reason);
|
||||
return;
|
||||
}
|
||||
|
||||
if (json_obj["userId"].isUndefined() || json_obj["sessionId"].isUndefined() || json_obj["countryCode"].isUndefined()) {
|
||||
QString failure_reason = tr("Authentication reply from server missing userId, sessionId or countryCode");
|
||||
if (search_ctx) Error(search_ctx, failure_reason);
|
||||
emit LoginFailure(failure_reason);
|
||||
return;
|
||||
}
|
||||
|
||||
country_code_ = json_obj["countryCode"].toString();
|
||||
session_id_ = json_obj["sessionId"].toString();
|
||||
user_id_ = json_obj["userId"].toInt();
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(TidalSettingsPage::kSettingsGroup);
|
||||
s.setValue("user_id", user_id_);
|
||||
s.setValue("session_id", session_id_);
|
||||
s.setValue("country_code", country_code_);
|
||||
s.endGroup();
|
||||
|
||||
qLog(Debug) << "Tidal: Login successful" << "user id" << user_id_ << "session id" << session_id_ << "country code" << country_code_;
|
||||
|
||||
if (search_ctx) {
|
||||
qLog(Debug) << "Tidal: Resuming search";
|
||||
SendSearch(search_ctx);
|
||||
}
|
||||
|
||||
emit LoginSuccess();
|
||||
|
||||
}
|
||||
|
||||
void TidalService::Logout() {
|
||||
|
||||
user_id_ = 0;
|
||||
session_id_.clear();
|
||||
country_code_.clear();
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(TidalSettingsPage::kSettingsGroup);
|
||||
s.remove("user_id");
|
||||
s.remove("session_id");
|
||||
s.remove("country_code");
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
QNetworkReply *TidalService::CreateRequest(const QString &ressource_name, const QList<Param> ¶ms) {
|
||||
|
||||
typedef QPair<QString, QString> Arg;
|
||||
typedef QList<Arg> ArgList;
|
||||
|
||||
typedef QPair<QByteArray, QByteArray> EncodedArg;
|
||||
typedef QList<EncodedArg> EncodedArgList;
|
||||
|
||||
ArgList args = ArgList() << params
|
||||
<< Arg("sessionId", session_id_)
|
||||
<< Arg("countryCode", country_code_);
|
||||
|
||||
QStringList query_items;
|
||||
QUrlQuery url_query;
|
||||
for (const Arg& arg : args) {
|
||||
EncodedArg encoded_arg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second));
|
||||
query_items << QString(encoded_arg.first + "=" + encoded_arg.second);
|
||||
url_query.addQueryItem(encoded_arg.first, encoded_arg.second);
|
||||
}
|
||||
|
||||
QUrl url(kApiUrl + QString("/") + ressource_name);
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest req(url);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
|
||||
//qLog(Debug) << "Tidal: Sending request" << url;
|
||||
|
||||
return reply;
|
||||
|
||||
}
|
||||
|
||||
QJsonObject TidalService::ExtractJsonObj(TidalSearchContext *search_ctx, QNetworkReply *reply) {
|
||||
|
||||
//int http_status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
if (reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
Error(search_ctx, failure_reason);
|
||||
}
|
||||
else {
|
||||
// See if there is Json data containing "userMessage" - then use that instead.
|
||||
QByteArray data(reply->readAll());
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
|
||||
QString failure_reason;
|
||||
if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (!json_obj.isEmpty() && json_obj.contains("userMessage")) {
|
||||
failure_reason = json_obj["userMessage"].toString();
|
||||
}
|
||||
else {
|
||||
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
}
|
||||
else {
|
||||
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
if (reply->error() == QNetworkReply::ContentAccessDenied ||
|
||||
reply->error() == QNetworkReply::ContentOperationNotPermittedError ||
|
||||
reply->error() == QNetworkReply::ContentNotFoundError ||
|
||||
reply->error() == QNetworkReply::AuthenticationRequiredError) {
|
||||
Logout();
|
||||
if (search_ctx->login_attempts < 1 && !username_.isEmpty() && !password_.isEmpty()) {
|
||||
qLog(Error) << "Tidal:" << failure_reason;
|
||||
qLog(Error) << "Tidal:" << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
qLog(Error) << "Tidal:" << "Attempting to login.";
|
||||
Login(search_ctx, username_, password_);
|
||||
}
|
||||
else {
|
||||
Error(search_ctx, failure_reason);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Error(search_ctx, failure_reason);
|
||||
}
|
||||
}
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
QByteArray data(reply->readAll());
|
||||
|
||||
qLog(Debug) << data;
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
Error(search_ctx, "Error while extracting Json document from results.");
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
Error(search_ctx, "Json document is not an object.");
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (json_doc.isNull() || json_doc.isEmpty()) {
|
||||
Error(search_ctx, "Received empty Json document.");
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
Error(search_ctx, "Received empty Json object.");
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
//qLog(Debug) << json_obj;
|
||||
|
||||
return json_obj;
|
||||
|
||||
}
|
||||
|
||||
QJsonArray TidalService::ExtractItems(TidalSearchContext *search_ctx, QNetworkReply *reply) {
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(search_ctx, reply);
|
||||
if (json_obj.isEmpty()) return QJsonArray();
|
||||
|
||||
if (!json_obj.contains("items")) {
|
||||
Error(search_ctx, "Json reply is missing items.");
|
||||
return QJsonArray();
|
||||
}
|
||||
|
||||
QJsonArray json_items = json_obj["items"].toArray();
|
||||
if (json_items.isEmpty()) {
|
||||
Error(search_ctx, "No match.");
|
||||
return QJsonArray();
|
||||
}
|
||||
|
||||
return json_items;
|
||||
|
||||
}
|
||||
|
||||
int TidalService::Search(const QString &text, TidalSettingsPage::SearchBy searchby) {
|
||||
|
||||
pending_search_id_ = next_pending_search_id_;
|
||||
pending_search_ = text;
|
||||
pending_searchby_ = searchby;
|
||||
|
||||
next_pending_search_id_++;
|
||||
|
||||
if (text.isEmpty()) {
|
||||
search_delay_->stop();
|
||||
return pending_search_id_;
|
||||
}
|
||||
search_delay_->start();
|
||||
|
||||
return pending_search_id_;
|
||||
|
||||
}
|
||||
|
||||
void TidalService::StartSearch() {
|
||||
|
||||
if (username_.isEmpty() || password_.isEmpty()) {
|
||||
emit SearchError(pending_search_id_, "Missing username and/or password.");
|
||||
next_pending_search_id_ = 1;
|
||||
ShowConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
TidalSearchContext *search_ctx = CreateSearch(pending_search_id_, pending_search_);
|
||||
if (authenticated()) SendSearch(search_ctx);
|
||||
else Login(search_ctx, username_, password_);
|
||||
|
||||
}
|
||||
|
||||
TidalSearchContext *TidalService::CreateSearch(const int search_id, const QString text) {
|
||||
|
||||
TidalSearchContext *search_ctx = new TidalSearchContext;
|
||||
search_ctx->id = search_id;
|
||||
search_ctx->text = text;
|
||||
search_ctx->album_requests = 0;
|
||||
search_ctx->song_requests = 0;
|
||||
search_ctx->requests_album_.clear();
|
||||
search_ctx->requests_song_.clear();
|
||||
search_ctx->login_attempts = 0;
|
||||
requests_search_.insert(search_id, search_ctx);
|
||||
return search_ctx;
|
||||
|
||||
}
|
||||
|
||||
void TidalService::SendSearch(TidalSearchContext *search_ctx) {
|
||||
|
||||
QList<Param> parameters;
|
||||
parameters << Param("query", search_ctx->text);
|
||||
|
||||
QString searchparam;
|
||||
switch (pending_searchby_) {
|
||||
case TidalSettingsPage::SearchBy_Songs:
|
||||
searchparam = "search/tracks";
|
||||
parameters << Param("limit", QString::number(kSearchTracksLimit));
|
||||
break;
|
||||
case TidalSettingsPage::SearchBy_Albums:
|
||||
default:
|
||||
searchparam = "search/albums";
|
||||
parameters << Param("limit", QString::number(kSearchAlbumsLimit));
|
||||
break;
|
||||
}
|
||||
|
||||
QNetworkReply *reply = CreateRequest(searchparam, parameters);
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(SearchFinished(QNetworkReply*, int)), reply, search_ctx->id);
|
||||
|
||||
}
|
||||
|
||||
void TidalService::SearchFinished(QNetworkReply *reply, int id) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (!requests_search_.contains(id)) return;
|
||||
TidalSearchContext *search_ctx = requests_search_.value(id);
|
||||
|
||||
QJsonArray json_items = ExtractItems(search_ctx, reply);
|
||||
if (json_items.isEmpty()) {
|
||||
CheckFinish(search_ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
//qLog(Debug) << json_items;
|
||||
|
||||
QVector<QString> albums;
|
||||
for (const QJsonValue &value : json_items) {
|
||||
int album_id(0);
|
||||
QString album("");
|
||||
if (!value["type"].isUndefined()) {
|
||||
if (value["id"].isUndefined() || value["title"].isUndefined()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, missing ID or title.";
|
||||
qLog(Debug) << value;
|
||||
continue;
|
||||
}
|
||||
album_id = value["id"].toInt();
|
||||
album = value["title"].toString();
|
||||
}
|
||||
else if (!value["album"].isUndefined()) {
|
||||
QJsonValue json_album = value["album"];
|
||||
if (json_album["id"].isUndefined() || json_album["title"].isUndefined()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, missing ID or title.";
|
||||
qLog(Debug) << value;
|
||||
continue;
|
||||
}
|
||||
album_id = json_album["id"].toInt();
|
||||
album = json_album["title"].toString();
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, missing type or album.";
|
||||
qLog(Debug) << value;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (search_ctx->requests_album_.contains(album_id)) continue;
|
||||
|
||||
if (value["artist"].isUndefined() || value["title"].isUndefined()) {
|
||||
qLog(Error) << "Tidal: Invalid Json reply, missing artist or title.";
|
||||
qLog(Debug) << value;
|
||||
continue;
|
||||
}
|
||||
QJsonValue json_artist = value["artist"];
|
||||
QString artist(json_artist["name"].toString());
|
||||
QString quality(value["audioQuality"].toString());
|
||||
|
||||
//qLog(Debug) << "Tidal:" << artist << album << quality;
|
||||
|
||||
QString artist_album(QString("%1-%2").arg(artist).arg(album));
|
||||
if (albums.contains(artist_album)) {
|
||||
qLog(Debug) << "Tidal: Skipping duplicate album" << artist << album << quality;
|
||||
continue;
|
||||
}
|
||||
albums.insert(0, artist_album);
|
||||
|
||||
search_ctx->requests_album_.insert(album_id, album_id);
|
||||
GetAlbum(search_ctx, album_id);
|
||||
search_ctx->album_requests++;
|
||||
if (search_ctx->album_requests >= kSearchAlbumsLimit) break;
|
||||
}
|
||||
|
||||
CheckFinish(search_ctx);
|
||||
|
||||
}
|
||||
|
||||
void TidalService::GetAlbum(TidalSearchContext *search_ctx, const int album_id) {
|
||||
|
||||
QList<Param> parameters;
|
||||
parameters << Param("token", session_id_)
|
||||
<< Param("soundQuality", quality_);
|
||||
|
||||
QNetworkReply *reply = CreateRequest(QString("albums/%1/tracks").arg(album_id), parameters);
|
||||
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(GetAlbumFinished(QNetworkReply*, int, int)), reply, search_ctx->id, album_id);
|
||||
|
||||
}
|
||||
|
||||
void TidalService::GetAlbumFinished(QNetworkReply *reply, int search_id, int album_id) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (!requests_search_.contains(search_id)) return;
|
||||
TidalSearchContext *search_ctx = requests_search_.value(search_id);
|
||||
|
||||
if (!search_ctx->requests_album_.contains(album_id)) return;
|
||||
search_ctx->album_requests--;
|
||||
|
||||
QJsonArray json_items = ExtractItems(search_ctx, reply);
|
||||
if (json_items.isEmpty()) {
|
||||
CheckFinish(search_ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
bool compilation = false;
|
||||
bool multidisc = false;
|
||||
Song *first_song(nullptr);
|
||||
QList<Song *> songs;
|
||||
for (const QJsonValue &value : json_items) {
|
||||
Song *song = ParseSong(search_ctx, album_id, value);
|
||||
if (!song) continue;
|
||||
songs << song;
|
||||
if (song->disc() >= 2) multidisc = true;
|
||||
if (song->is_compilation() || (first_song && song->artist() != first_song->artist())) compilation = true;
|
||||
if (!first_song) first_song = song;
|
||||
}
|
||||
if (compilation || multidisc) {
|
||||
for (Song *song : songs) {
|
||||
if (compilation) song->set_compilation_detected(true);
|
||||
if (multidisc) {
|
||||
QString album_full(QString("%1 - (Disc %2)").arg(song->album()).arg(song->disc()));
|
||||
song->set_album(album_full);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckFinish(search_ctx);
|
||||
|
||||
}
|
||||
|
||||
Song *TidalService::ParseSong(TidalSearchContext *search_ctx, const int album_id, const QJsonValue &value) {
|
||||
|
||||
Song song;
|
||||
|
||||
bool allow_streaming = value["allowStreaming"].toBool();
|
||||
bool stream_ready = value["streamReady"].toBool();
|
||||
if (!allow_streaming || !stream_ready) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int id = value["id"].toInt();
|
||||
QJsonValue json_artist = value["artist"];
|
||||
QJsonArray json_artists = value["artists"].toArray();
|
||||
QJsonValue json_album = value["album"];
|
||||
QString title = value["title"].toString();
|
||||
QString artist = json_artist["name"].toString();
|
||||
QString album = json_album["title"].toString();
|
||||
QString cover = json_album["cover"].toString();
|
||||
QString url = value["url"].toString();
|
||||
int track = value["trackNumber"].toInt();
|
||||
int disc = value["volumeNumber"].toInt();
|
||||
|
||||
//qLog(Debug) << "id" << id << "track" << track << "disc" << disc << "title" << title << "album" << album << "artist" << artist << cover << allow_streaming << url;
|
||||
|
||||
song.set_album_id(album_id);
|
||||
song.set_artist(artist);
|
||||
song.set_album(album);
|
||||
song.set_title(title);
|
||||
song.set_track(track);
|
||||
song.set_disc(disc);
|
||||
song.set_bitrate(0);
|
||||
song.set_samplerate(0);
|
||||
song.set_bitdepth(0);
|
||||
|
||||
QVariant q_duration = value["duration"];
|
||||
if (q_duration.isValid()) {
|
||||
quint64 duration = q_duration.toULongLong() * kNsecPerSec;
|
||||
song.set_length_nanosec(duration);
|
||||
}
|
||||
|
||||
// Check and see if there is more than 1 artist on the song.
|
||||
//int i = 0;
|
||||
//for (const QJsonValue &artist : json_artists) {
|
||||
//i++;
|
||||
//qLog(Debug) << artist << i;
|
||||
//}
|
||||
//if (i > 1) song.set_compilation_detected(true);
|
||||
|
||||
cover = cover.replace("-", "/");
|
||||
QUrl cover_url (QString("%1/images/%2/750x750.jpg").arg(kResourcesUrl).arg(cover));
|
||||
song.set_art_automatic(cover_url.toEncoded());
|
||||
|
||||
if (search_ctx->requests_song_.contains(id)) return search_ctx->requests_song_.value(id);
|
||||
Song *song_new = new Song(song);
|
||||
search_ctx->requests_song_.insert(id, song_new);
|
||||
search_ctx->song_requests++;
|
||||
GetStreamURL(search_ctx, album_id, id);
|
||||
|
||||
return song_new;
|
||||
|
||||
}
|
||||
|
||||
void TidalService::GetStreamURL(TidalSearchContext *search_ctx, const int album_id, const int song_id) {
|
||||
|
||||
QList<Param> parameters;
|
||||
parameters << Param("token", session_id_)
|
||||
<< Param("soundQuality", quality_);
|
||||
|
||||
QNetworkReply *reply = CreateRequest(QString("tracks/%1/streamUrl").arg(song_id), parameters);
|
||||
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(GetStreamURLFinished(QNetworkReply*, int, int)), reply, search_ctx->id, song_id);
|
||||
|
||||
}
|
||||
|
||||
void TidalService::GetStreamURLFinished(QNetworkReply *reply, const int search_id, const int song_id) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (!requests_search_.contains(search_id)) return;
|
||||
TidalSearchContext *search_ctx = requests_search_.value(search_id);
|
||||
|
||||
if (!search_ctx->requests_song_.contains(song_id)) {
|
||||
CheckFinish(search_ctx);
|
||||
return;
|
||||
}
|
||||
Song *song = search_ctx->requests_song_.value(song_id);
|
||||
|
||||
search_ctx->song_requests--;
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(search_ctx, reply);
|
||||
if (json_obj.isEmpty()) {
|
||||
delete search_ctx->requests_song_.take(song_id);
|
||||
CheckFinish(search_ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
if (json_obj["url"].isUndefined() || json_obj["codec"].isUndefined()) {
|
||||
delete search_ctx->requests_song_.take(song_id);
|
||||
CheckFinish(search_ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
song->set_url(QUrl(json_obj["url"].toString()));
|
||||
song->set_valid(true);
|
||||
QString codec = json_obj["codec"].toString();
|
||||
if (codec == "AAC") song->set_filetype(Song::Type_MP4);
|
||||
else qLog(Debug) << "Tidal codec" << codec;
|
||||
|
||||
//qLog(Debug) << song->title() << song->artist() << song->album() << song->url() << song->filetype();
|
||||
|
||||
search_ctx->songs << *song;
|
||||
|
||||
delete search_ctx->requests_song_.take(song_id);
|
||||
|
||||
CheckFinish(search_ctx);
|
||||
|
||||
}
|
||||
|
||||
void TidalService::CheckFinish(TidalSearchContext *search_ctx) {
|
||||
|
||||
if (!search_ctx->login_sent && search_ctx->album_requests <= 0 && search_ctx->song_requests <= 0) {
|
||||
if (search_ctx->songs.isEmpty()) emit SearchError(search_ctx->id, search_ctx->error);
|
||||
else emit SearchResults(search_ctx->id, search_ctx->songs);
|
||||
delete requests_search_.take(search_ctx->id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void TidalService::Error(TidalSearchContext *search_ctx, QString error, QString debug) {
|
||||
qLog(Error) << "Tidal:" << error;
|
||||
if (!debug.isEmpty()) qLog(Debug) << debug;
|
||||
if (search_ctx) {
|
||||
search_ctx->error = error;
|
||||
CheckFinish(search_ctx);
|
||||
}
|
||||
}
|
134
src/tidal/tidalservice.h
Normal file
134
src/tidal/tidalservice.h
Normal file
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TIDALSERVICE_H
|
||||
#define TIDALSERVICE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
#include <QNetworkReply>
|
||||
#include <QTimer>
|
||||
#include <QDateTime>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "internet/internetmodel.h"
|
||||
#include "internet/internetservice.h"
|
||||
#include "settings/tidalsettingspage.h"
|
||||
|
||||
class NetworkAccessManager;
|
||||
|
||||
struct TidalSearchContext {
|
||||
int id;
|
||||
QString text;
|
||||
QHash<int, int> requests_album_;
|
||||
QHash<int, Song *> requests_song_;
|
||||
int album_requests;
|
||||
int song_requests;
|
||||
SongList songs;
|
||||
QString error;
|
||||
bool login_sent;
|
||||
int login_attempts;
|
||||
};
|
||||
Q_DECLARE_METATYPE(TidalSearchContext);
|
||||
|
||||
class TidalService : public InternetService {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TidalService(Application *app, InternetModel *parent);
|
||||
~TidalService();
|
||||
|
||||
static const char *kServiceName;
|
||||
|
||||
void ReloadSettings();
|
||||
|
||||
void Login(const QString &username, const QString &password);
|
||||
void Logout();
|
||||
int Search(const QString &query, TidalSettingsPage::SearchBy searchby);
|
||||
|
||||
const bool login_sent() { return login_sent_; }
|
||||
const bool authenticated() { return (!session_id_.isEmpty() && !country_code_.isEmpty()); }
|
||||
|
||||
signals:
|
||||
void LoginSuccess();
|
||||
void LoginFailure(QString failure_reason);
|
||||
void SearchResults(int id, SongList songs);
|
||||
void SearchError(int id, QString message);
|
||||
|
||||
public slots:
|
||||
void ShowConfig();
|
||||
|
||||
private slots:
|
||||
void HandleAuthReply(QNetworkReply *reply, int id);
|
||||
void StartSearch();
|
||||
void SearchFinished(QNetworkReply *reply, int id);
|
||||
void GetAlbumFinished(QNetworkReply *reply, int search_id, int album_id);
|
||||
void GetStreamURLFinished(QNetworkReply *reply, const int search_id, const int song_id);
|
||||
|
||||
private:
|
||||
void Login(TidalSearchContext *search_ctx, const QString &username, const QString &password);
|
||||
void LoadSessionID();
|
||||
QNetworkReply *CreateRequest(const QString &ressource_name, const QList<QPair<QString, QString>> ¶ms);
|
||||
QJsonObject ExtractJsonObj(TidalSearchContext *search_ctx, QNetworkReply *reply);
|
||||
QJsonArray ExtractItems(TidalSearchContext *search_ctx, QNetworkReply *reply);
|
||||
TidalSearchContext *CreateSearch(const int search_id, const QString text);
|
||||
void SendSearch(TidalSearchContext *search_ctx);
|
||||
void GetAlbum(TidalSearchContext *search_ctx, const int album_id);
|
||||
Song *ParseSong(TidalSearchContext *search_ctx, const int album_id, const QJsonValue &value);
|
||||
Song ExtractSong(TidalSearchContext *search_ctx, const QJsonValue &value);
|
||||
void GetStreamURL(TidalSearchContext *search_ctx, const int album_id, const int song_id);
|
||||
void CheckFinish(TidalSearchContext *search_ctx);
|
||||
void Error(TidalSearchContext *search_ctx, QString error, QString debug = "");
|
||||
|
||||
static const char *kApiUrl;
|
||||
static const char *kAuthUrl;
|
||||
static const char *kResourcesUrl;
|
||||
static const char *kApiToken;
|
||||
|
||||
NetworkAccessManager *network_;
|
||||
QTimer *search_delay_;
|
||||
int pending_search_id_;
|
||||
int next_pending_search_id_;
|
||||
int search_requests_;
|
||||
bool login_sent_;
|
||||
static const int kSearchAlbumsLimit;
|
||||
static const int kSearchTracksLimit;
|
||||
static const int kSearchDelayMsec;
|
||||
|
||||
QString username_;
|
||||
QString password_;
|
||||
QString quality_;
|
||||
QString session_id_;
|
||||
quint64 user_id_;
|
||||
QString country_code_;
|
||||
|
||||
QString pending_search_;
|
||||
TidalSettingsPage::SearchBy pending_searchby_;
|
||||
QHash<int, TidalSearchContext*> requests_search_;
|
||||
|
||||
};
|
||||
|
||||
#endif // TIDALSERVICE_H
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@ -222,14 +222,14 @@ Transcoder::Transcoder(QObject *parent, const QString &settings_postfix)
|
||||
QList<TranscoderPreset> Transcoder::GetAllPresets() {
|
||||
|
||||
QList<TranscoderPreset> ret;
|
||||
ret << PresetForFileType(Song::Type_Flac);
|
||||
ret << PresetForFileType(Song::Type_Mp4);
|
||||
ret << PresetForFileType(Song::Type_Mpeg);
|
||||
ret << PresetForFileType(Song::Type_FLAC);
|
||||
ret << PresetForFileType(Song::Type_MP4);
|
||||
ret << PresetForFileType(Song::Type_MPEG);
|
||||
ret << PresetForFileType(Song::Type_OggVorbis);
|
||||
ret << PresetForFileType(Song::Type_OggFlac);
|
||||
ret << PresetForFileType(Song::Type_OggSpeex);
|
||||
ret << PresetForFileType(Song::Type_Asf);
|
||||
ret << PresetForFileType(Song::Type_Wav);
|
||||
ret << PresetForFileType(Song::Type_ASF);
|
||||
ret << PresetForFileType(Song::Type_WAV);
|
||||
ret << PresetForFileType(Song::Type_OggOpus);
|
||||
return ret;
|
||||
|
||||
@ -238,11 +238,11 @@ QList<TranscoderPreset> Transcoder::GetAllPresets() {
|
||||
TranscoderPreset Transcoder::PresetForFileType(Song::FileType type) {
|
||||
|
||||
switch (type) {
|
||||
case Song::Type_Flac:
|
||||
case Song::Type_FLAC:
|
||||
return TranscoderPreset(type, tr("FLAC"), "flac", "audio/x-flac");
|
||||
case Song::Type_Mp4:
|
||||
case Song::Type_MP4:
|
||||
return TranscoderPreset(type, tr("M4A AAC"), "mp4", "audio/mpeg, mpegversion=(int)4", "audio/mp4");
|
||||
case Song::Type_Mpeg:
|
||||
case Song::Type_MPEG:
|
||||
return TranscoderPreset(type, tr("MP3"), "mp3", "audio/mpeg, mpegversion=(int)1, layer=(int)3");
|
||||
case Song::Type_OggVorbis:
|
||||
return TranscoderPreset(type, tr("Ogg Vorbis"), "ogg", "audio/x-vorbis", "application/ogg");
|
||||
@ -252,9 +252,9 @@ TranscoderPreset Transcoder::PresetForFileType(Song::FileType type) {
|
||||
return TranscoderPreset(type, tr("Ogg Speex"), "spx", "audio/x-speex", "application/ogg");
|
||||
case Song::Type_OggOpus:
|
||||
return TranscoderPreset(type, tr("Ogg Opus"), "opus", "audio/x-opus", "application/ogg");
|
||||
case Song::Type_Asf:
|
||||
case Song::Type_ASF:
|
||||
return TranscoderPreset(type, tr("Windows Media audio"), "wma", "audio/x-wma", "video/x-ms-asf");
|
||||
case Song::Type_Wav:
|
||||
case Song::Type_WAV:
|
||||
return TranscoderPreset(type, tr("Wav"), "wav", QString(), "audio/x-wav");
|
||||
default:
|
||||
qLog(Warning) << "Unsupported format in PresetForFileType:" << type;
|
||||
@ -268,9 +268,9 @@ Song::FileType Transcoder::PickBestFormat(QList<Song::FileType> supported) {
|
||||
if (supported.isEmpty()) return Song::Type_Unknown;
|
||||
|
||||
QList<Song::FileType> best_formats;
|
||||
best_formats << Song::Type_Mpeg;
|
||||
best_formats << Song::Type_MPEG;
|
||||
best_formats << Song::Type_OggVorbis;
|
||||
best_formats << Song::Type_Asf;
|
||||
best_formats << Song::Type_ASF;
|
||||
|
||||
for (Song::FileType type : best_formats) {
|
||||
if (supported.isEmpty() || supported.contains(type)) return type;
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@ -44,14 +44,14 @@ TranscoderOptionsDialog::TranscoderOptionsDialog(Song::FileType type, QWidget *p
|
||||
ui_->setupUi(this);
|
||||
|
||||
switch (type) {
|
||||
case Song::Type_Flac:
|
||||
case Song::Type_FLAC:
|
||||
case Song::Type_OggFlac: options_ = new TranscoderOptionsFlac(this); break;
|
||||
case Song::Type_Mp4: options_ = new TranscoderOptionsAAC(this); break;
|
||||
case Song::Type_Mpeg: options_ = new TranscoderOptionsMP3(this); break;
|
||||
case Song::Type_MP4: options_ = new TranscoderOptionsAAC(this); break;
|
||||
case Song::Type_MPEG: options_ = new TranscoderOptionsMP3(this); break;
|
||||
case Song::Type_OggVorbis: options_ = new TranscoderOptionsVorbis(this); break;
|
||||
case Song::Type_OggOpus: options_ = new TranscoderOptionsOpus(this); break;
|
||||
case Song::Type_OggSpeex: options_ = new TranscoderOptionsSpeex(this); break;
|
||||
case Song::Type_Asf: options_ = new TranscoderOptionsWma(this); break;
|
||||
case Song::Type_ASF: options_ = new TranscoderOptionsWma(this); break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@ -59,7 +59,6 @@ FileViewList::FileViewList(QWidget *parent)
|
||||
|
||||
void FileViewList::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
menu_selection_ = selectionModel()->selection();
|
||||
|
||||
menu_->popup(e->globalPos());
|
||||
@ -69,7 +68,6 @@ void FileViewList::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
QList<QUrl> FileViewList::UrlListFromSelection() const {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
QList<QUrl> urls;
|
||||
for (const QModelIndex& index : menu_selection_.indexes()) {
|
||||
if (index.column() == 0)
|
||||
@ -81,7 +79,6 @@ QList<QUrl> FileViewList::UrlListFromSelection() const {
|
||||
|
||||
MimeData *FileViewList::MimeDataFromSelection() const {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
MimeData *data = new MimeData;
|
||||
data->setUrls(UrlListFromSelection());
|
||||
|
||||
@ -101,7 +98,6 @@ MimeData *FileViewList::MimeDataFromSelection() const {
|
||||
|
||||
QStringList FileViewList::FilenamesFromSelection() const {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
QStringList filenames;
|
||||
for (const QModelIndex& index : menu_selection_.indexes()) {
|
||||
if (index.column() == 0)
|
||||
@ -112,14 +108,12 @@ QStringList FileViewList::FilenamesFromSelection() const {
|
||||
}
|
||||
|
||||
void FileViewList::LoadSlot() {
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
MimeData *data = MimeDataFromSelection();
|
||||
data->clear_first_ = true;
|
||||
emit AddToPlaylist(data);
|
||||
}
|
||||
|
||||
void FileViewList::AddToPlaylistSlot() {
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
emit AddToPlaylist(MimeDataFromSelection());
|
||||
}
|
||||
|
||||
@ -143,18 +137,15 @@ void FileViewList::CopyToDeviceSlot() {
|
||||
}
|
||||
|
||||
void FileViewList::DeleteSlot() {
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
emit Delete(FilenamesFromSelection());
|
||||
}
|
||||
|
||||
void FileViewList::EditTagsSlot() {
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
emit EditTags(UrlListFromSelection());
|
||||
}
|
||||
|
||||
void FileViewList::mousePressEvent(QMouseEvent *e) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
switch (e->button()) {
|
||||
case Qt::XButton1:
|
||||
emit Back();
|
||||
@ -183,6 +174,5 @@ void FileViewList::mousePressEvent(QMouseEvent *e) {
|
||||
}
|
||||
|
||||
void FileViewList::ShowInBrowser() {
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
Utilities::OpenInFileBrowser(UrlListFromSelection());
|
||||
}
|
||||
|
148
src/widgets/loginstatewidget.cpp
Normal file
148
src/widgets/loginstatewidget.cpp
Normal file
@ -0,0 +1,148 @@
|
||||
/*
|
||||
This file was part of Clementine.
|
||||
Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
#include "loginstatewidget.h"
|
||||
#include "ui_loginstatewidget.h"
|
||||
#include "core/iconloader.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QTimer>
|
||||
#include <QDate>
|
||||
#include <QString>
|
||||
#include <QLineEdit>
|
||||
#include <QtEvents>
|
||||
|
||||
LoginStateWidget::LoginStateWidget(QWidget *parent)
|
||||
: QWidget(parent), ui_(new Ui_LoginStateWidget), state_(LoggedOut) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
ui_->signed_in->hide();
|
||||
ui_->expires->hide();
|
||||
ui_->account_type->hide();
|
||||
ui_->busy->hide();
|
||||
|
||||
ui_->sign_out->setIcon(IconLoader::Load("list-remove"));
|
||||
ui_->signed_in_icon_label->setPixmap(IconLoader::Load("dialog-ok-apply").pixmap(22));
|
||||
ui_->expires_icon_label->setPixmap(IconLoader::Load("user-away").pixmap(22));
|
||||
ui_->account_type_icon_label->setPixmap(IconLoader::Load("dialog-warning").pixmap(22));
|
||||
|
||||
QFont bold_font(font());
|
||||
bold_font.setBold(true);
|
||||
ui_->signed_out_label->setFont(bold_font);
|
||||
|
||||
connect(ui_->sign_out, SIGNAL(clicked()), SLOT(Logout()));
|
||||
|
||||
}
|
||||
|
||||
LoginStateWidget::~LoginStateWidget() { delete ui_; }
|
||||
|
||||
void LoginStateWidget::Logout() {
|
||||
SetLoggedIn(LoggedOut);
|
||||
emit LogoutClicked();
|
||||
}
|
||||
|
||||
void LoginStateWidget::SetAccountTypeText(const QString &text) {
|
||||
ui_->account_type_label->setText(text);
|
||||
}
|
||||
|
||||
void LoginStateWidget::SetAccountTypeVisible(bool visible) {
|
||||
ui_->account_type->setVisible(visible);
|
||||
}
|
||||
|
||||
void LoginStateWidget::SetLoggedIn(State state, const QString &account_name) {
|
||||
|
||||
State last_state = state_;
|
||||
state_ = state;
|
||||
|
||||
ui_->signed_in->setVisible(state == LoggedIn);
|
||||
ui_->signed_out->setVisible(state != LoggedIn);
|
||||
ui_->busy->setVisible(state == LoginInProgress);
|
||||
|
||||
if (account_name.isEmpty()) ui_->signed_in_label->setText("<b>" + tr("You are signed in.") + "</b>");
|
||||
else ui_->signed_in_label->setText(tr("You are signed in as %1.").arg("<b>" + account_name + "</b>"));
|
||||
|
||||
for (QWidget *widget : credential_groups_) {
|
||||
widget->setVisible(state != LoggedIn);
|
||||
widget->setEnabled(state != LoginInProgress);
|
||||
}
|
||||
|
||||
if (state == LoggedOut && last_state == LoginInProgress) {
|
||||
// A login just failed - give focus back to the last crediental field (usually password).
|
||||
// We have to do this after control gets back to the
|
||||
// event loop because the user might have just closed a dialog and our widget might not be active yet.
|
||||
QTimer::singleShot(0, this, SLOT(FocusLastCredentialField()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void LoginStateWidget::FocusLastCredentialField() {
|
||||
|
||||
if (!credential_fields_.isEmpty()) {
|
||||
QObject *object = credential_fields_.last();
|
||||
QWidget *widget = qobject_cast<QWidget*>(object);
|
||||
QLineEdit *line_edit = qobject_cast<QLineEdit*>(object);
|
||||
|
||||
if (widget) {
|
||||
widget->setFocus();
|
||||
}
|
||||
|
||||
if (line_edit) {
|
||||
line_edit->selectAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LoginStateWidget::HideLoggedInState() {
|
||||
ui_->signed_in->hide();
|
||||
ui_->signed_out->hide();
|
||||
}
|
||||
|
||||
void LoginStateWidget::AddCredentialField(QWidget *widget) {
|
||||
widget->installEventFilter(this);
|
||||
credential_fields_ << widget;
|
||||
}
|
||||
|
||||
void LoginStateWidget::AddCredentialGroup(QWidget *widget) {
|
||||
credential_groups_ << widget;
|
||||
}
|
||||
|
||||
bool LoginStateWidget::eventFilter(QObject *object, QEvent *event) {
|
||||
if (!credential_fields_.contains(object))
|
||||
return QWidget::eventFilter(object, event);
|
||||
|
||||
if (event->type() == QEvent::KeyPress) {
|
||||
QKeyEvent *key_event = static_cast<QKeyEvent*>(event);
|
||||
if (key_event->key() == Qt::Key_Enter || key_event->key() == Qt::Key_Return) {
|
||||
emit LoginClicked();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return QWidget::eventFilter(object, event);
|
||||
}
|
||||
|
||||
void LoginStateWidget::SetExpires(const QDate &expires) {
|
||||
|
||||
ui_->expires->setVisible(expires.isValid());
|
||||
|
||||
if (expires.isValid()) {
|
||||
const QString expires_text = expires.toString(Qt::SystemLocaleLongDate);
|
||||
ui_->expires_label->setText(tr("Expires on %1").arg("<b>" + expires_text + "</b>"));
|
||||
}
|
||||
}
|
80
src/widgets/loginstatewidget.h
Normal file
80
src/widgets/loginstatewidget.h
Normal file
@ -0,0 +1,80 @@
|
||||
/*
|
||||
This file was part of Clementine.
|
||||
Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
#ifndef LOGINSTATEWIDGET_H
|
||||
#define LOGINSTATEWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QDate>
|
||||
#include <QtEvents>
|
||||
|
||||
class Ui_LoginStateWidget;
|
||||
|
||||
class LoginStateWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
LoginStateWidget(QWidget *parent = nullptr);
|
||||
~LoginStateWidget();
|
||||
|
||||
enum State { LoggedIn, LoginInProgress, LoggedOut };
|
||||
|
||||
// Installs an event handler on the field so that pressing enter will emit
|
||||
// LoginClicked() instead of doing the default action (closing the dialog).
|
||||
void AddCredentialField(QWidget *widget);
|
||||
|
||||
// This widget (usually a QGroupBox) will be hidden when SetLoggedIn(true) is called.
|
||||
void AddCredentialGroup(QWidget *widget);
|
||||
|
||||
// QObject
|
||||
bool eventFilter(QObject *object, QEvent *event);
|
||||
|
||||
public slots:
|
||||
// Changes the "You are logged in/out" label, shows/hides any QGroupBoxes added with AddCredentialGroup.
|
||||
void SetLoggedIn(State state, const QString &account_name = QString::null);
|
||||
|
||||
// Hides the "You are logged in/out" label completely.
|
||||
void HideLoggedInState();
|
||||
|
||||
void SetAccountTypeText(const QString &text);
|
||||
void SetAccountTypeVisible(bool visible);
|
||||
|
||||
void SetExpires(const QDate &expires);
|
||||
|
||||
signals:
|
||||
void LogoutClicked();
|
||||
void LoginClicked();
|
||||
|
||||
private slots:
|
||||
void Logout();
|
||||
void FocusLastCredentialField();
|
||||
|
||||
private:
|
||||
Ui_LoginStateWidget *ui_;
|
||||
|
||||
State state_;
|
||||
|
||||
QList<QObject*> credential_fields_;
|
||||
QList<QWidget*> credential_groups_;
|
||||
};
|
||||
|
||||
#endif // LOGINSTATEWIDGET_H
|
182
src/widgets/loginstatewidget.ui
Normal file
182
src/widgets/loginstatewidget.ui
Normal file
@ -0,0 +1,182 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>LoginStateWidget</class>
|
||||
<widget class="QWidget" name="LoginStateWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>526</width>
|
||||
<height>187</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWidget" name="signed_out" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>22</width>
|
||||
<height>22</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="signed_out_label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>You are not signed in.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="signed_in" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="signed_in_icon_label">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>22</width>
|
||||
<height>22</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="signed_in_label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::RichText</enum>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="sign_out">
|
||||
<property name="text">
|
||||
<string>Sign out</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="expires" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="expires_icon_label">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>22</width>
|
||||
<height>22</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="expires_label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="account_type" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="account_type_icon_label">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>22</width>
|
||||
<height>22</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="account_type_label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BusyIndicator" name="busy" native="true">
|
||||
<property name="text" stdset="0">
|
||||
<string>Signing in...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>BusyIndicator</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/busyindicator.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<connections/>
|
||||
</ui>
|
@ -14,7 +14,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
@ -84,7 +84,6 @@ StatusView::StatusView(CollectionViewContainer *collectionviewcontainer, QWidget
|
||||
label_playing_text_(nullptr),
|
||||
|
||||
album_cover_choice_controller_(new AlbumCoverChoiceController(this)),
|
||||
show_hide_animation_(new QTimeLine(500, this)),
|
||||
fade_animation_(new QTimeLine(1000, this)),
|
||||
image_blank_(""),
|
||||
image_nosong_(":/pictures/strawberry.png"),
|
||||
@ -92,8 +91,6 @@ StatusView::StatusView(CollectionViewContainer *collectionviewcontainer, QWidget
|
||||
menu_(new QMenu(this))
|
||||
{
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
collectionview_ = collectionviewcontainer->view();
|
||||
connect(collectionview_, SIGNAL(TotalSongCountUpdated_()), this, SLOT(UpdateNoSong()));
|
||||
connect(collectionview_, SIGNAL(TotalArtistCountUpdated_()), this, SLOT(UpdateNoSong()));
|
||||
@ -125,8 +122,6 @@ StatusView::~StatusView() {
|
||||
|
||||
void StatusView::AddActions() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
QList<QAction*> actions = album_cover_choice_controller_->GetAllActions();
|
||||
|
||||
// Here we add the search automatically action, too!
|
||||
@ -147,8 +142,6 @@ void StatusView::AddActions() {
|
||||
|
||||
void StatusView::CreateWidget() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
setLayout(layout_);
|
||||
|
||||
layout_->setSizeConstraint(QLayout::SetMinAndMaxSize);
|
||||
@ -174,8 +167,6 @@ void StatusView::CreateWidget() {
|
||||
|
||||
void StatusView::SetApplication(Application *app) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
app_ = app;
|
||||
|
||||
album_cover_choice_controller_->SetApplication(app_);
|
||||
@ -185,8 +176,6 @@ void StatusView::SetApplication(Application *app) {
|
||||
|
||||
void StatusView::NoSongWidget() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
if (widgetstate_ == Playing) {
|
||||
container_layout_->removeWidget(widget_playing_);
|
||||
widget_playing_->setVisible(false);
|
||||
@ -221,8 +210,6 @@ void StatusView::NoSongWidget() {
|
||||
|
||||
void StatusView::SongWidget() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
if (widgetstate_ == Stopped) {
|
||||
container_layout_->removeWidget(widget_stopped_);
|
||||
widget_stopped_->setVisible(false);
|
||||
@ -275,8 +262,6 @@ void StatusView::SongWidget() {
|
||||
|
||||
void StatusView::SwitchWidgets(WidgetState state) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
if (widgetstate_ == None) NoSongWidget();
|
||||
|
||||
if ((state == Stopped) && (widgetstate_ != Stopped)) {
|
||||
@ -291,8 +276,6 @@ void StatusView::SwitchWidgets(WidgetState state) {
|
||||
|
||||
void StatusView::UpdateSong() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
SwitchWidgets(Playing);
|
||||
|
||||
const QueryOptions opt;
|
||||
@ -342,8 +325,6 @@ void StatusView::UpdateSong() {
|
||||
|
||||
void StatusView::NoSong() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
QString html;
|
||||
QImage image_logo(":/pictures/strawberry.png");
|
||||
QImage image_logo_scaled = image_logo.scaled(300, 300, Qt::KeepAspectRatio);
|
||||
@ -377,8 +358,6 @@ void StatusView::NoSong() {
|
||||
|
||||
void StatusView::SongChanged(const Song &song) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
stopped_ = false;
|
||||
metadata_ = song;
|
||||
|
||||
@ -390,8 +369,6 @@ void StatusView::SongChanged(const Song &song) {
|
||||
|
||||
void StatusView::SongFinished() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
stopped_ = true;
|
||||
SetImage(image_blank_);
|
||||
|
||||
@ -399,8 +376,6 @@ void StatusView::SongFinished() {
|
||||
|
||||
bool StatusView::eventFilter(QObject *object, QEvent *event) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
switch(event->type()) {
|
||||
case QEvent::Paint:{
|
||||
handlePaintEvent(object, event);
|
||||
@ -416,8 +391,6 @@ bool StatusView::eventFilter(QObject *object, QEvent *event) {
|
||||
|
||||
void StatusView::handlePaintEvent(QObject *object, QEvent *event) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__ << object->objectName();
|
||||
|
||||
if (object == label_playing_album_) {
|
||||
paintEvent_album(event);
|
||||
}
|
||||
@ -428,8 +401,6 @@ void StatusView::handlePaintEvent(QObject *object, QEvent *event) {
|
||||
|
||||
void StatusView::paintEvent_album(QEvent *event) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
QPainter p(label_playing_album_);
|
||||
|
||||
DrawImage(&p);
|
||||
@ -443,8 +414,6 @@ void StatusView::paintEvent_album(QEvent *event) {
|
||||
|
||||
void StatusView::DrawImage(QPainter *p) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
p->drawPixmap(0, 0, 300, 300, pixmap_current_);
|
||||
if ((downloading_covers_) && (spinner_animation_ != nullptr)) {
|
||||
p->drawPixmap(50, 50, 16, 16, spinner_animation_->currentPixmap());
|
||||
@ -454,8 +423,6 @@ void StatusView::DrawImage(QPainter *p) {
|
||||
|
||||
void StatusView::FadePreviousTrack(qreal value) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
pixmap_previous_opacity_ = value;
|
||||
if (qFuzzyCompare(pixmap_previous_opacity_, qreal(0.0))) {
|
||||
pixmap_previous_ = QPixmap();
|
||||
@ -477,31 +444,22 @@ void StatusView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
}
|
||||
|
||||
void StatusView::mouseReleaseEvent(QMouseEvent *) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
}
|
||||
|
||||
void StatusView::dragEnterEvent(QDragEnterEvent *e) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
QWidget::dragEnterEvent(e);
|
||||
|
||||
}
|
||||
|
||||
void StatusView::dropEvent(QDropEvent *e) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
QWidget::dropEvent(e);
|
||||
|
||||
}
|
||||
|
||||
void StatusView::ScaleCover() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
pixmap_current_ = QPixmap::fromImage(AlbumCoverLoader::ScaleAndPad(cover_loader_options_, original_));
|
||||
update();
|
||||
|
||||
@ -509,8 +467,6 @@ void StatusView::ScaleCover() {
|
||||
|
||||
void StatusView::AlbumArtLoaded(const Song &metadata, const QString&, const QImage &image) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
SwitchWidgets(Playing);
|
||||
|
||||
label_playing_album_->clear();
|
||||
@ -527,8 +483,6 @@ void StatusView::AlbumArtLoaded(const Song &metadata, const QString&, const QIma
|
||||
|
||||
void StatusView::SetImage(const QImage &image) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
// Cache the current pixmap so we can fade between them
|
||||
pixmap_previous_ = QPixmap(size());
|
||||
pixmap_previous_.fill(palette().background().color());
|
||||
@ -543,7 +497,7 @@ void StatusView::SetImage(const QImage &image) {
|
||||
ScaleCover();
|
||||
|
||||
// Were we waiting for this cover to load before we started fading?
|
||||
if (!pixmap_previous_.isNull() && fade_animation_ != nullptr) {
|
||||
if (!pixmap_previous_.isNull() && fade_animation_) {
|
||||
fade_animation_->start();
|
||||
}
|
||||
|
||||
@ -551,8 +505,6 @@ void StatusView::SetImage(const QImage &image) {
|
||||
|
||||
bool StatusView::GetCoverAutomatically() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
SwitchWidgets(Playing);
|
||||
|
||||
// Search for cover automatically?
|
||||
@ -581,8 +533,6 @@ bool StatusView::GetCoverAutomatically() {
|
||||
|
||||
void StatusView::AutomaticCoverSearchDone() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
downloading_covers_ = false;
|
||||
spinner_animation_.reset();
|
||||
update();
|
||||
@ -591,8 +541,6 @@ void StatusView::AutomaticCoverSearchDone() {
|
||||
|
||||
void StatusView::UpdateNoSong() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
if (widgetstate_ == Playing) return;
|
||||
|
||||
NoSong();
|
||||
|
@ -14,7 +14,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef STATUSVIEW_H
|
||||
@ -131,7 +131,6 @@ private:
|
||||
int small_ideal_height_;
|
||||
int total_height_;
|
||||
bool fit_width_;
|
||||
QTimeLine *show_hide_animation_;
|
||||
QTimeLine *fade_animation_;
|
||||
QImage image_blank_;
|
||||
QImage image_nosong_;
|
||||
@ -175,3 +174,4 @@ protected:
|
||||
};
|
||||
|
||||
#endif // STATUSVIEW_H
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user