Add tidal support
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="/">
|
<qresource prefix="/">
|
||||||
<file>schema/schema.sql</file>
|
<file>schema/schema.sql</file>
|
||||||
|
<file>schema/schema-1.sql</file>
|
||||||
<file>schema/device-schema.sql</file>
|
<file>schema/device-schema.sql</file>
|
||||||
<file>style/mainwindow.css</file>
|
<file>style/mainwindow.css</file>
|
||||||
<file>style/statusview.css</file>
|
<file>style/statusview.css</file>
|
||||||
@@ -113,6 +114,7 @@
|
|||||||
<file>icons/128x128/xine.png</file>
|
<file>icons/128x128/xine.png</file>
|
||||||
<file>icons/128x128/zoom-in.png</file>
|
<file>icons/128x128/zoom-in.png</file>
|
||||||
<file>icons/128x128/zoom-out.png</file>
|
<file>icons/128x128/zoom-out.png</file>
|
||||||
|
<file>icons/128x128/tidal.png</file>
|
||||||
<file>icons/64x64/albums.png</file>
|
<file>icons/64x64/albums.png</file>
|
||||||
<file>icons/64x64/alsa.png</file>
|
<file>icons/64x64/alsa.png</file>
|
||||||
<file>icons/64x64/application-exit.png</file>
|
<file>icons/64x64/application-exit.png</file>
|
||||||
@@ -201,6 +203,7 @@
|
|||||||
<file>icons/64x64/xine.png</file>
|
<file>icons/64x64/xine.png</file>
|
||||||
<file>icons/64x64/zoom-in.png</file>
|
<file>icons/64x64/zoom-in.png</file>
|
||||||
<file>icons/64x64/zoom-out.png</file>
|
<file>icons/64x64/zoom-out.png</file>
|
||||||
|
<file>icons/64x64/tidal.png</file>
|
||||||
<file>icons/48x48/albums.png</file>
|
<file>icons/48x48/albums.png</file>
|
||||||
<file>icons/48x48/alsa.png</file>
|
<file>icons/48x48/alsa.png</file>
|
||||||
<file>icons/48x48/application-exit.png</file>
|
<file>icons/48x48/application-exit.png</file>
|
||||||
@@ -292,6 +295,7 @@
|
|||||||
<file>icons/48x48/xine.png</file>
|
<file>icons/48x48/xine.png</file>
|
||||||
<file>icons/48x48/zoom-in.png</file>
|
<file>icons/48x48/zoom-in.png</file>
|
||||||
<file>icons/48x48/zoom-out.png</file>
|
<file>icons/48x48/zoom-out.png</file>
|
||||||
|
<file>icons/48x48/tidal.png</file>
|
||||||
<file>icons/32x32/albums.png</file>
|
<file>icons/32x32/albums.png</file>
|
||||||
<file>icons/32x32/alsa.png</file>
|
<file>icons/32x32/alsa.png</file>
|
||||||
<file>icons/32x32/application-exit.png</file>
|
<file>icons/32x32/application-exit.png</file>
|
||||||
@@ -384,6 +388,7 @@
|
|||||||
<file>icons/32x32/xine.png</file>
|
<file>icons/32x32/xine.png</file>
|
||||||
<file>icons/32x32/zoom-in.png</file>
|
<file>icons/32x32/zoom-in.png</file>
|
||||||
<file>icons/32x32/zoom-out.png</file>
|
<file>icons/32x32/zoom-out.png</file>
|
||||||
|
<file>icons/32x32/tidal.png</file>
|
||||||
<file>icons/22x22/albums.png</file>
|
<file>icons/22x22/albums.png</file>
|
||||||
<file>icons/22x22/alsa.png</file>
|
<file>icons/22x22/alsa.png</file>
|
||||||
<file>icons/22x22/application-exit.png</file>
|
<file>icons/22x22/application-exit.png</file>
|
||||||
@@ -476,5 +481,6 @@
|
|||||||
<file>icons/22x22/xine.png</file>
|
<file>icons/22x22/xine.png</file>
|
||||||
<file>icons/22x22/zoom-in.png</file>
|
<file>icons/22x22/zoom-in.png</file>
|
||||||
<file>icons/22x22/zoom-out.png</file>
|
<file>icons/22x22/zoom-out.png</file>
|
||||||
|
<file>icons/22x22/tidal.png</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</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
|
version INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
DELETE FROM schema_version;
|
||||||
|
REPLACE INTO schema_version (version) VALUES (1);
|
||||||
|
|
||||||
INSERT INTO schema_version (version) VALUES (0);
|
CREATE TABLE IF NOT EXISTS directories (
|
||||||
|
|
||||||
CREATE TABLE directories (
|
|
||||||
path TEXT NOT NULL,
|
path TEXT NOT NULL,
|
||||||
subdirs INTEGER NOT NULL
|
subdirs INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE subdirectories (
|
CREATE TABLE IF NOT EXISTS subdirectories (
|
||||||
directory_id INTEGER NOT NULL,
|
directory_id INTEGER NOT NULL,
|
||||||
path TEXT NOT NULL,
|
path TEXT NOT NULL,
|
||||||
mtime INTEGER NOT NULL
|
mtime INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE songs (
|
CREATE TABLE IF NOT EXISTS songs (
|
||||||
|
|
||||||
/* Metadata from taglib */
|
/* Metadata from taglib */
|
||||||
|
|
||||||
@@ -67,12 +67,12 @@ CREATE TABLE songs (
|
|||||||
|
|
||||||
effective_albumartist TEXT,
|
effective_albumartist TEXT,
|
||||||
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
cue_path TEXT
|
cue_path TEXT
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE playlists (
|
CREATE TABLE IF NOT EXISTS playlists (
|
||||||
|
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
last_played INTEGER NOT NULL DEFAULT -1,
|
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,
|
playlist INTEGER NOT NULL,
|
||||||
type TEXT NOT NULL,
|
type TEXT NOT NULL,
|
||||||
collection_id INTEGER,
|
collection_id INTEGER,
|
||||||
|
internet_service TEXT,
|
||||||
url TEXT,
|
url TEXT,
|
||||||
|
|
||||||
/* Metadata from taglib */
|
/* 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,
|
unique_id TEXT NOT NULL,
|
||||||
friendly_name TEXT,
|
friendly_name TEXT,
|
||||||
size INTEGER,
|
size INTEGER,
|
||||||
@@ -155,17 +156,17 @@ CREATE TABLE devices (
|
|||||||
transcode_format NOT NULL DEFAULT 5
|
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,
|
ftstitle,
|
||||||
ftsalbum,
|
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,
|
ftstitle,
|
||||||
ftsalbum,
|
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,
|
ftstitle,
|
||||||
ftsalbum,
|
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;
|
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)
|
INSERT INTO %allsongstables_fts (ROWID, ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment)
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ set(SOURCES
|
|||||||
settings/shortcutssettingspage.cpp
|
settings/shortcutssettingspage.cpp
|
||||||
settings/appearancesettingspage.cpp
|
settings/appearancesettingspage.cpp
|
||||||
settings/notificationssettingspage.cpp
|
settings/notificationssettingspage.cpp
|
||||||
|
settings/tidalsettingspage.cpp
|
||||||
|
|
||||||
dialogs/about.cpp
|
dialogs/about.cpp
|
||||||
dialogs/console.cpp
|
dialogs/console.cpp
|
||||||
@@ -246,6 +247,7 @@ set(SOURCES
|
|||||||
widgets/tracksliderpopup.cpp
|
widgets/tracksliderpopup.cpp
|
||||||
widgets/tracksliderslider.cpp
|
widgets/tracksliderslider.cpp
|
||||||
widgets/widgetfadehelper.cpp
|
widgets/widgetfadehelper.cpp
|
||||||
|
widgets/loginstatewidget.cpp
|
||||||
|
|
||||||
musicbrainz/acoustidclient.cpp
|
musicbrainz/acoustidclient.cpp
|
||||||
musicbrainz/musicbrainzclient.cpp
|
musicbrainz/musicbrainzclient.cpp
|
||||||
@@ -266,6 +268,16 @@ set(SOURCES
|
|||||||
device/deviceviewcontainer.cpp
|
device/deviceviewcontainer.cpp
|
||||||
device/filesystemdevice.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
|
set(HEADERS
|
||||||
@@ -356,7 +368,7 @@ set(HEADERS
|
|||||||
covermanager/amazoncoverprovider.h
|
covermanager/amazoncoverprovider.h
|
||||||
covermanager/musicbrainzcoverprovider.h
|
covermanager/musicbrainzcoverprovider.h
|
||||||
covermanager/discogscoverprovider.h
|
covermanager/discogscoverprovider.h
|
||||||
|
|
||||||
settings/settingsdialog.h
|
settings/settingsdialog.h
|
||||||
settings/settingspage.h
|
settings/settingspage.h
|
||||||
settings/behavioursettingspage.h
|
settings/behavioursettingspage.h
|
||||||
@@ -368,7 +380,8 @@ set(HEADERS
|
|||||||
settings/shortcutssettingspage.h
|
settings/shortcutssettingspage.h
|
||||||
settings/appearancesettingspage.h
|
settings/appearancesettingspage.h
|
||||||
settings/notificationssettingspage.h
|
settings/notificationssettingspage.h
|
||||||
|
settings/tidalsettingspage.h
|
||||||
|
|
||||||
dialogs/about.h
|
dialogs/about.h
|
||||||
dialogs/errordialog.h
|
dialogs/errordialog.h
|
||||||
dialogs/console.h
|
dialogs/console.h
|
||||||
@@ -405,6 +418,7 @@ set(HEADERS
|
|||||||
widgets/tracksliderpopup.h
|
widgets/tracksliderpopup.h
|
||||||
widgets/tracksliderslider.h
|
widgets/tracksliderslider.h
|
||||||
widgets/widgetfadehelper.h
|
widgets/widgetfadehelper.h
|
||||||
|
widgets/loginstatewidget.h
|
||||||
|
|
||||||
musicbrainz/acoustidclient.h
|
musicbrainz/acoustidclient.h
|
||||||
musicbrainz/musicbrainzclient.h
|
musicbrainz/musicbrainzclient.h
|
||||||
@@ -424,6 +438,16 @@ set(HEADERS
|
|||||||
device/deviceview.h
|
device/deviceview.h
|
||||||
device/filesystemdevice.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
|
set(UI
|
||||||
@@ -457,6 +481,7 @@ set(UI
|
|||||||
settings/shortcutssettingspage.ui
|
settings/shortcutssettingspage.ui
|
||||||
settings/appearancesettingspage.ui
|
settings/appearancesettingspage.ui
|
||||||
settings/notificationssettingspage.ui
|
settings/notificationssettingspage.ui
|
||||||
|
settings/tidalsettingspage.ui
|
||||||
|
|
||||||
equalizer/equalizer.ui
|
equalizer/equalizer.ui
|
||||||
equalizer/equalizerslider.ui
|
equalizer/equalizerslider.ui
|
||||||
@@ -470,12 +495,15 @@ set(UI
|
|||||||
widgets/trackslider.ui
|
widgets/trackslider.ui
|
||||||
widgets/osdpretty.ui
|
widgets/osdpretty.ui
|
||||||
widgets/fileview.ui
|
widgets/fileview.ui
|
||||||
|
widgets/loginstatewidget.ui
|
||||||
|
|
||||||
device/deviceproperties.ui
|
device/deviceproperties.ui
|
||||||
device/deviceviewcontainer.ui
|
device/deviceviewcontainer.ui
|
||||||
|
|
||||||
globalshortcuts/globalshortcutgrabber.ui
|
globalshortcuts/globalshortcutgrabber.ui
|
||||||
|
|
||||||
|
tidal/tidalsearchview.ui
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
set(RESOURCES ../data/data.qrc)
|
set(RESOURCES ../data/data.qrc)
|
||||||
|
|||||||
@@ -789,7 +789,7 @@ void CollectionBackend::UpdateCompilations() {
|
|||||||
info.artists.insert(artist);
|
info.artists.insert(artist);
|
||||||
info.directories.insert(filename.left(last_separator));
|
info.directories.insert(filename.left(last_separator));
|
||||||
if (compilation_detected) info.has_compilation_detected = true;
|
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
|
// 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -52,6 +52,9 @@
|
|||||||
#include "covermanager/discogscoverprovider.h"
|
#include "covermanager/discogscoverprovider.h"
|
||||||
#include "covermanager/musicbrainzcoverprovider.h"
|
#include "covermanager/musicbrainzcoverprovider.h"
|
||||||
|
|
||||||
|
#include "internet/internetmodel.h"
|
||||||
|
#include "tidal/tidalsearch.h"
|
||||||
|
|
||||||
bool Application::kIsPortable = false;
|
bool Application::kIsPortable = false;
|
||||||
|
|
||||||
class ApplicationImpl {
|
class ApplicationImpl {
|
||||||
@@ -97,7 +100,9 @@ class ApplicationImpl {
|
|||||||
app->MoveToNewThread(loader);
|
app->MoveToNewThread(loader);
|
||||||
return 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_;
|
Lazy<TagReaderClient> tag_reader_client_;
|
||||||
@@ -113,6 +118,8 @@ class ApplicationImpl {
|
|||||||
Lazy<CoverProviders> cover_providers_;
|
Lazy<CoverProviders> cover_providers_;
|
||||||
Lazy<AlbumCoverLoader> album_cover_loader_;
|
Lazy<AlbumCoverLoader> album_cover_loader_;
|
||||||
Lazy<CurrentArtLoader> current_art_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 {
|
EngineDevice *Application::enginedevice() const {
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
return p_->enginedevice_.get();
|
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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef APPLICATION_H_
|
#ifndef APPLICATION_H_
|
||||||
@@ -49,6 +49,8 @@ class DeviceManager;
|
|||||||
class CoverProviders;
|
class CoverProviders;
|
||||||
class AlbumCoverLoader;
|
class AlbumCoverLoader;
|
||||||
class CurrentArtLoader;
|
class CurrentArtLoader;
|
||||||
|
class InternetModel;
|
||||||
|
class TidalSearch;
|
||||||
|
|
||||||
class Application : public QObject {
|
class Application : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -79,6 +81,9 @@ class Application : public QObject {
|
|||||||
CollectionBackend *collection_backend() const;
|
CollectionBackend *collection_backend() const;
|
||||||
CollectionModel *collection_model() const;
|
CollectionModel *collection_model() const;
|
||||||
|
|
||||||
|
InternetModel *internet_model() const;
|
||||||
|
TidalSearch *tidal_search() const;
|
||||||
|
|
||||||
void MoveToNewThread(QObject *object);
|
void MoveToNewThread(QObject *object);
|
||||||
void MoveToThread(QObject *object, QThread *thread);
|
void MoveToThread(QObject *object, QThread *thread);
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
#include "scopedtransaction.h"
|
#include "scopedtransaction.h"
|
||||||
|
|
||||||
const char *Database::kDatabaseFilename = "strawberry.db";
|
const char *Database::kDatabaseFilename = "strawberry.db";
|
||||||
const int Database::kSchemaVersion = 0;
|
const int Database::kSchemaVersion = 1;
|
||||||
const char *Database::kMagicAllSongsTables = "%allsongstables";
|
const char *Database::kMagicAllSongsTables = "%allsongstables";
|
||||||
|
|
||||||
int Database::sNextConnectionId = 1;
|
int Database::sNextConnectionId = 1;
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
#include "settings/playlistsettingspage.h"
|
#include "settings/playlistsettingspage.h"
|
||||||
#include "settings/settingsdialog.h"
|
#include "settings/settingsdialog.h"
|
||||||
|
|
||||||
|
#include "tidal/tidalsearchview.h"
|
||||||
|
|
||||||
#if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT)
|
#if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT)
|
||||||
# include "musicbrainz/tagfetcher.h"
|
# include "musicbrainz/tagfetcher.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -186,6 +188,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
|||||||
manager->SetPlaylistManager(app->playlist_manager());
|
manager->SetPlaylistManager(app->playlist_manager());
|
||||||
return manager;
|
return manager;
|
||||||
}),
|
}),
|
||||||
|
tidal_search_view_(new TidalSearchView(app_, this)),
|
||||||
playlist_menu_(new QMenu(this)),
|
playlist_menu_(new QMenu(this)),
|
||||||
playlist_add_to_another_(nullptr),
|
playlist_add_to_another_(nullptr),
|
||||||
playlistitem_actions_separator_(nullptr),
|
playlistitem_actions_separator_(nullptr),
|
||||||
@@ -218,7 +221,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
|||||||
ui_->volume->setValue(volume);
|
ui_->volume->setValue(volume);
|
||||||
VolumeChanged(volume);
|
VolumeChanged(volume);
|
||||||
|
|
||||||
// Initialise the global search widget
|
// Initialise the tidal search widget
|
||||||
StyleHelper::setBaseColor(palette().color(QPalette::Highlight).darker());
|
StyleHelper::setBaseColor(palette().color(QPalette::Highlight).darker());
|
||||||
|
|
||||||
// Add tabs to the fancy tab widget
|
// 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(file_view_, IconLoader::Load("document-open"), tr("Files"));
|
||||||
ui_->tabs->addTab(playlist_list_, IconLoader::Load("view-media-playlist"), tr("Playlists"));
|
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(device_view_, IconLoader::Load("device"), tr("Devices"));
|
||||||
|
ui_->tabs->addTab(tidal_search_view_, IconLoader::Load("tidal"), tr("Tidal", "Tidal"));
|
||||||
//ui_->tabs->AddSpacer();
|
//ui_->tabs->AddSpacer();
|
||||||
|
|
||||||
// Add the now playing widget to the fancy tab widget
|
// 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(separator);
|
||||||
collection_view_->filter()->AddMenuAction(collection_config_action);
|
collection_view_->filter()->AddMenuAction(collection_config_action);
|
||||||
|
|
||||||
|
// Tidal
|
||||||
|
connect(tidal_search_view_, SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
|
||||||
|
|
||||||
// Playlist menu
|
// Playlist menu
|
||||||
playlist_play_pause_ = playlist_menu_->addAction(tr("Play"), this, SLOT(PlaylistPlay()));
|
playlist_play_pause_ = playlist_menu_->addAction(tr("Play"), this, SLOT(PlaylistPlay()));
|
||||||
playlist_menu_->addAction(ui_->action_stop);
|
playlist_menu_->addAction(ui_->action_stop);
|
||||||
@@ -657,6 +664,12 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co
|
|||||||
|
|
||||||
ReloadSettings();
|
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
|
// Reload pretty OSD to avoid issues with fonts
|
||||||
osd_->ReloadPrettyOSDSettings();
|
osd_->ReloadPrettyOSDSettings();
|
||||||
|
|
||||||
@@ -745,6 +758,7 @@ void MainWindow::ReloadAllSettings() {
|
|||||||
osd_->ReloadSettings();
|
osd_->ReloadSettings();
|
||||||
collection_view_->ReloadSettings();
|
collection_view_->ReloadSettings();
|
||||||
ui_->playlist->view()->ReloadSettings();
|
ui_->playlist->view()->ReloadSettings();
|
||||||
|
tidal_search_view_->ReloadSettings();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,7 +801,7 @@ void MainWindow::MediaPaused() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::MediaPlaying() {
|
void MainWindow::MediaPlaying() {
|
||||||
|
|
||||||
ui_->action_stop->setEnabled(true);
|
ui_->action_stop->setEnabled(true);
|
||||||
ui_->action_stop_after_this_track->setEnabled(true);
|
ui_->action_stop_after_this_track->setEnabled(true);
|
||||||
ui_->action_play_pause->setIcon(IconLoader::Load("media-pause"));
|
ui_->action_play_pause->setIcon(IconLoader::Load("media-pause"));
|
||||||
@@ -1789,7 +1803,7 @@ void MainWindow::EditFileTags(const QList<QUrl> &urls) {
|
|||||||
Song song;
|
Song song;
|
||||||
song.set_url(url);
|
song.set_url(url);
|
||||||
song.set_valid(true);
|
song.set_valid(true);
|
||||||
song.set_filetype(Song::Type_Mpeg);
|
song.set_filetype(Song::Type_MPEG);
|
||||||
songs << song;
|
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
|
#endif
|
||||||
class Ui_MainWindow;
|
class Ui_MainWindow;
|
||||||
class Windows7ThumbBar;
|
class Windows7ThumbBar;
|
||||||
|
class TidalSearchView;
|
||||||
|
|
||||||
class MainWindow : public QMainWindow, public PlatformInterface {
|
class MainWindow : public QMainWindow, public PlatformInterface {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -263,6 +264,11 @@ signals:
|
|||||||
|
|
||||||
void ShowConsole();
|
void ShowConsole();
|
||||||
|
|
||||||
|
void FocusTidalSearchField();
|
||||||
|
void DoTidalSearch(const QString& query);
|
||||||
|
void SearchForArtist();
|
||||||
|
void SearchForAlbum();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void ConnectStatusView(StatusView *statusview);
|
void ConnectStatusView(StatusView *statusview);
|
||||||
|
|
||||||
@@ -313,6 +319,8 @@ signals:
|
|||||||
PlaylistItemList autocomplete_tag_items_;
|
PlaylistItemList autocomplete_tag_items_;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
TidalSearchView *tidal_search_view_;
|
||||||
|
|
||||||
QAction *collection_show_all_;
|
QAction *collection_show_all_;
|
||||||
QAction *collection_show_duplicates_;
|
QAction *collection_show_duplicates_;
|
||||||
QAction *collection_show_untagged_;
|
QAction *collection_show_untagged_;
|
||||||
@@ -335,6 +343,9 @@ signals:
|
|||||||
QAction *playlist_add_to_another_;
|
QAction *playlist_add_to_another_;
|
||||||
QList<QAction*> playlistitem_actions_;
|
QList<QAction*> playlistitem_actions_;
|
||||||
QAction *playlistitem_actions_separator_;
|
QAction *playlistitem_actions_separator_;
|
||||||
|
QAction *search_for_artist_;
|
||||||
|
QAction *search_for_album_;
|
||||||
|
|
||||||
QModelIndex playlist_menu_index_;
|
QModelIndex playlist_menu_index_;
|
||||||
|
|
||||||
QSortFilterProxyModel *collection_sort_model_;
|
QSortFilterProxyModel *collection_sort_model_;
|
||||||
|
|||||||
@@ -60,6 +60,8 @@
|
|||||||
# include "dbus/metatypes.h"
|
# include "dbus/metatypes.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include "tidal/tidalsearch.h"
|
||||||
|
|
||||||
void RegisterMetaTypes() {
|
void RegisterMetaTypes() {
|
||||||
|
|
||||||
qRegisterMetaType<const char*>("const char*");
|
qRegisterMetaType<const char*>("const char*");
|
||||||
@@ -113,4 +115,7 @@ void RegisterMetaTypes() {
|
|||||||
#endif
|
#endif
|
||||||
#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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef MIMEDATA_H
|
#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.
|
// If this is set then the items are added to the queue after being inserted.
|
||||||
bool enqueue_now_;
|
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.
|
// If this is set then the items are inserted into a newly created playlist.
|
||||||
bool open_in_new_playlist_;
|
bool open_in_new_playlist_;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -393,7 +393,7 @@ bool Mpris2::CanPause() const {
|
|||||||
bool Mpris2::CanSeek() const { return CanSeek(app_->player()->GetState()); }
|
bool Mpris2::CanSeek() const { return CanSeek(app_->player()->GetState()); }
|
||||||
|
|
||||||
bool Mpris2::CanSeek(Engine::State state) const {
|
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; }
|
bool Mpris2::CanControl() const { return true; }
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|||||||
@@ -287,9 +287,10 @@ uint Song::mtime() const { return d->mtime_; }
|
|||||||
uint Song::ctime() const { return d->ctime_; }
|
uint Song::ctime() const { return d->ctime_; }
|
||||||
int Song::filesize() const { return d->filesize_; }
|
int Song::filesize() const { return d->filesize_; }
|
||||||
Song::FileType Song::filetype() const { return d->filetype_; }
|
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 {
|
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_automatic() const { return d->art_automatic_; }
|
||||||
const QString &Song::art_manual() const { return d->art_manual_; }
|
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_directory_id(int v) { d->directory_id_ = v; }
|
||||||
void Song::set_url(const QUrl &v) {
|
void Song::set_url(const QUrl &v) {
|
||||||
if (Application::kIsPortable) {
|
if (Application::kIsPortable) {
|
||||||
QUrl base =
|
QUrl base = QUrl::fromLocalFile(QCoreApplication::applicationDirPath() + "/");
|
||||||
QUrl::fromLocalFile(QCoreApplication::applicationDirPath() + "/");
|
|
||||||
d->url_ = base.resolved(v);
|
d->url_ = base.resolved(v);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
d->url_ = v;
|
d->url_ = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,36 +365,35 @@ QString Song::JoinSpec(const QString &table) {
|
|||||||
QString Song::TextForFiletype(FileType type) {
|
QString Song::TextForFiletype(FileType type) {
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case Song::Type_Wav: return QObject::tr("Wav");
|
case Song::Type_WAV: return QObject::tr("Wav");
|
||||||
case Song::Type_Flac: return QObject::tr("FLAC");
|
case Song::Type_FLAC: return QObject::tr("FLAC");
|
||||||
case Song::Type_WavPack: return QObject::tr("WavPack");
|
case Song::Type_WavPack: return QObject::tr("WavPack");
|
||||||
case Song::Type_OggFlac: return QObject::tr("Ogg FLAC");
|
case Song::Type_OggFlac: return QObject::tr("Ogg FLAC");
|
||||||
case Song::Type_OggVorbis: return QObject::tr("Ogg Vorbis");
|
case Song::Type_OggVorbis: return QObject::tr("Ogg Vorbis");
|
||||||
case Song::Type_OggOpus: return QObject::tr("Ogg Opus");
|
case Song::Type_OggOpus: return QObject::tr("Ogg Opus");
|
||||||
case Song::Type_OggSpeex: return QObject::tr("Ogg Speex");
|
case Song::Type_OggSpeex: return QObject::tr("Ogg Speex");
|
||||||
case Song::Type_Mpeg: return QObject::tr("MP3");
|
case Song::Type_MPEG: return QObject::tr("MP3");
|
||||||
case Song::Type_Mp4: return QObject::tr("MP4 AAC");
|
case Song::Type_MP4: return QObject::tr("MP4 AAC");
|
||||||
case Song::Type_Asf: return QObject::tr("Windows Media audio");
|
case Song::Type_ASF: return QObject::tr("Windows Media audio");
|
||||||
case Song::Type_Aiff: return QObject::tr("AIFF");
|
case Song::Type_AIFF: return QObject::tr("AIFF");
|
||||||
case Song::Type_Mpc: return QObject::tr("MPC");
|
case Song::Type_MPC: return QObject::tr("MPC");
|
||||||
case Song::Type_TrueAudio: return QObject::tr("TrueAudio");
|
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:
|
case Song::Type_Unknown:
|
||||||
default:
|
default:
|
||||||
return QObject::tr("Unknown");
|
return QObject::tr("Unknown");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Song::IsFileLossless() const {
|
bool Song::IsFileLossless() const {
|
||||||
switch (filetype()) {
|
switch (filetype()) {
|
||||||
case Song::Type_Wav:
|
case Song::Type_WAV:
|
||||||
case Song::Type_Flac:
|
case Song::Type_FLAC:
|
||||||
case Song::Type_OggFlac:
|
case Song::Type_OggFlac:
|
||||||
case Song::Type_WavPack:
|
case Song::Type_WavPack:
|
||||||
case Song::Type_Aiff:
|
case Song::Type_AIFF:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@@ -628,7 +628,7 @@ void Song::InitFromQuery(const SqlRow &q, bool reliable_metadata, int col) {
|
|||||||
else if (Song::kColumns.value(i) == "unavailable") {
|
else if (Song::kColumns.value(i) == "unavailable") {
|
||||||
d->unavailable_ = q.value(x).toBool();
|
d->unavailable_ = q.value(x).toBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (Song::kColumns.value(i) == "playcount") {
|
else if (Song::kColumns.value(i) == "playcount") {
|
||||||
d->playcount_ = q.value(x).isNull() ? 0 : q.value(x).toInt();
|
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) == "compilation_effective") {
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (Song::kColumns.value(i) == "art_automatic") {
|
else if (Song::kColumns.value(i) == "art_automatic") {
|
||||||
d->art_automatic_ = q.value(x).toString();
|
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) == "effective_originalyear") {
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (Song::kColumns.value(i) == "cue_path") {
|
else if (Song::kColumns.value(i) == "cue_path") {
|
||||||
d->cue_path_ = tostr(x);
|
d->cue_path_ = tostr(x);
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
qLog(Error) << "Forgot to handle" << Song::kColumns.value(i);
|
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->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->filesize_ = track->size;
|
||||||
d->mtime_ = track->time_modified;
|
d->mtime_ = track->time_modified;
|
||||||
d->ctime_ = track->time_added;
|
d->ctime_ = track->time_added;
|
||||||
@@ -785,7 +785,7 @@ void Song::ToItdb(Itdb_Track *track) const {
|
|||||||
//track->bithdepth = d->bithdepth_;
|
//track->bithdepth = d->bithdepth_;
|
||||||
|
|
||||||
track->type1 = 0;
|
track->type1 = 0;
|
||||||
track->type2 = d->filetype_ == Type_Mp4 ? 0 : 1;
|
track->type2 = d->filetype_ == Type_MP4 ? 0 : 1;
|
||||||
track->mediatype = 1; // Audio
|
track->mediatype = 1; // Audio
|
||||||
track->size = d->filesize_;
|
track->size = d->filesize_;
|
||||||
track->time_modified = d->mtime_;
|
track->time_modified = d->mtime_;
|
||||||
@@ -825,15 +825,15 @@ void Song::InitFromMTP(const LIBMTP_track_t *track, const QString &host) {
|
|||||||
d->playcount_ = track->usecount;
|
d->playcount_ = track->usecount;
|
||||||
|
|
||||||
switch (track->filetype) {
|
switch (track->filetype) {
|
||||||
case LIBMTP_FILETYPE_WAV: d->filetype_ = Type_Wav; break;
|
case LIBMTP_FILETYPE_WAV: d->filetype_ = Type_WAV; break;
|
||||||
case LIBMTP_FILETYPE_MP3: d->filetype_ = Type_Mpeg; break;
|
case LIBMTP_FILETYPE_MP3: d->filetype_ = Type_MPEG; break;
|
||||||
case LIBMTP_FILETYPE_WMA: d->filetype_ = Type_Asf; break;
|
case LIBMTP_FILETYPE_WMA: d->filetype_ = Type_ASF; break;
|
||||||
case LIBMTP_FILETYPE_OGG: d->filetype_ = Type_OggVorbis; break;
|
case LIBMTP_FILETYPE_OGG: d->filetype_ = Type_OggVorbis; break;
|
||||||
case LIBMTP_FILETYPE_MP4: 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_AAC: d->filetype_ = Type_MP4; break;
|
||||||
case LIBMTP_FILETYPE_FLAC: d->filetype_ = Type_OggFlac; break;
|
case LIBMTP_FILETYPE_FLAC: d->filetype_ = Type_OggFlac; break;
|
||||||
case LIBMTP_FILETYPE_MP2: d->filetype_ = Type_Mpeg; break;
|
case LIBMTP_FILETYPE_MP2: d->filetype_ = Type_MPEG; break;
|
||||||
case LIBMTP_FILETYPE_M4A: d->filetype_ = Type_Mp4; break;
|
case LIBMTP_FILETYPE_M4A: d->filetype_ = Type_MP4; break;
|
||||||
default: d->filetype_ = Type_Unknown; break;
|
default: d->filetype_ = Type_Unknown; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -868,14 +868,14 @@ void Song::ToMTP(LIBMTP_track_t *track) const {
|
|||||||
track->usecount = d->playcount_;
|
track->usecount = d->playcount_;
|
||||||
|
|
||||||
switch (d->filetype_) {
|
switch (d->filetype_) {
|
||||||
case Type_Asf: track->filetype = LIBMTP_FILETYPE_ASF; break;
|
case Type_ASF: track->filetype = LIBMTP_FILETYPE_ASF; break;
|
||||||
case Type_Mp4: track->filetype = LIBMTP_FILETYPE_MP4; break;
|
case Type_MP4: track->filetype = LIBMTP_FILETYPE_MP4; break;
|
||||||
case Type_Mpeg: track->filetype = LIBMTP_FILETYPE_MP3; break;
|
case Type_MPEG: track->filetype = LIBMTP_FILETYPE_MP3; break;
|
||||||
case Type_Flac:
|
case Type_FLAC:
|
||||||
case Type_OggFlac: track->filetype = LIBMTP_FILETYPE_FLAC; break;
|
case Type_OggFlac: track->filetype = LIBMTP_FILETYPE_FLAC; break;
|
||||||
case Type_OggSpeex:
|
case Type_OggSpeex:
|
||||||
case Type_OggVorbis: track->filetype = LIBMTP_FILETYPE_OGG; break;
|
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;
|
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(":performer", strval(d->performer_));
|
||||||
query->bindValue(":grouping", strval(d->grouping_));
|
query->bindValue(":grouping", strval(d->grouping_));
|
||||||
query->bindValue(":comment", strval(d->comment_));
|
query->bindValue(":comment", strval(d->comment_));
|
||||||
|
|
||||||
query->bindValue(":beginning", d->beginning_);
|
query->bindValue(":beginning", d->beginning_);
|
||||||
query->bindValue(":length", intval(length_nanosec()));
|
query->bindValue(":length", intval(length_nanosec()));
|
||||||
|
|
||||||
@@ -1037,7 +1037,8 @@ QString Song::TitleWithCompilationArtist() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString Song::SampleRateBitDepthToText() const {
|
QString Song::SampleRateBitDepthToText() const {
|
||||||
|
|
||||||
|
if (d->samplerate_ == -1) return QString("");
|
||||||
if (d->bitdepth_ == -1) return QString("%1 hz").arg(d->samplerate_);
|
if (d->bitdepth_ == -1) return QString("%1 hz").arg(d->samplerate_);
|
||||||
|
|
||||||
return QString("%1 hz / %2 bit").arg(d->samplerate_).arg(d->bitdepth_);
|
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 {
|
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 {
|
bool Song::operator==(const Song &other) const {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef SONG_H
|
#ifndef SONG_H
|
||||||
@@ -58,12 +58,6 @@ struct _Itdb_Track;
|
|||||||
struct LIBMTP_track_struct;
|
struct LIBMTP_track_struct;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAVE_LIBLASTFM
|
|
||||||
namespace lastfm {
|
|
||||||
class Track;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
class SqlRow;
|
class SqlRow;
|
||||||
|
|
||||||
class Song {
|
class Song {
|
||||||
@@ -95,20 +89,20 @@ class Song {
|
|||||||
// If a new lossless file is added, also add it to IsFileLossless().
|
// If a new lossless file is added, also add it to IsFileLossless().
|
||||||
enum FileType {
|
enum FileType {
|
||||||
Type_Unknown = 0,
|
Type_Unknown = 0,
|
||||||
Type_Wav = 1,
|
Type_WAV = 1,
|
||||||
Type_Flac = 2,
|
Type_FLAC = 2,
|
||||||
Type_WavPack = 3,
|
Type_WavPack = 3,
|
||||||
Type_OggFlac = 4,
|
Type_OggFlac = 4,
|
||||||
Type_OggVorbis = 5,
|
Type_OggVorbis = 5,
|
||||||
Type_OggOpus = 6,
|
Type_OggOpus = 6,
|
||||||
Type_OggSpeex = 7,
|
Type_OggSpeex = 7,
|
||||||
Type_Mpeg = 8,
|
Type_MPEG = 8,
|
||||||
Type_Mp4 = 9,
|
Type_MP4 = 9,
|
||||||
Type_Asf = 10,
|
Type_ASF = 10,
|
||||||
Type_Aiff = 11,
|
Type_AIFF = 11,
|
||||||
Type_Mpc = 12,
|
Type_MPC = 12,
|
||||||
Type_TrueAudio = 13,
|
Type_TrueAudio = 13,
|
||||||
Type_Cdda = 90,
|
Type_CDDA = 90,
|
||||||
Type_Stream = 91,
|
Type_Stream = 91,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -127,9 +121,6 @@ class Song {
|
|||||||
void InitFromQuery(const SqlRow &query, bool reliable_metadata, int col = 0);
|
void InitFromQuery(const SqlRow &query, bool reliable_metadata, int col = 0);
|
||||||
void InitFromFilePartial(const QString &filename); // Just store the filename: incomplete but fast
|
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
|
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);
|
void MergeFromSimpleMetaBundle(const Engine::SimpleMetaBundle &bundle);
|
||||||
|
|
||||||
@@ -152,9 +143,6 @@ class Song {
|
|||||||
// Save
|
// Save
|
||||||
void BindToQuery(QSqlQuery *query) const;
|
void BindToQuery(QSqlQuery *query) const;
|
||||||
void BindToFtsQuery(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 ToXesam(QVariantMap *map) const;
|
||||||
void ToProtobuf(pb::tagreader::SongMetadata *pb) const;
|
void ToProtobuf(pb::tagreader::SongMetadata *pb) const;
|
||||||
|
|
||||||
@@ -210,6 +198,7 @@ class Song {
|
|||||||
const QString &effective_albumartist() const;
|
const QString &effective_albumartist() const;
|
||||||
|
|
||||||
bool is_collection_song() const;
|
bool is_collection_song() const;
|
||||||
|
bool is_stream() const;
|
||||||
bool is_cdda() 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:
|
// 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -51,6 +51,8 @@
|
|||||||
#include "song.h"
|
#include "song.h"
|
||||||
#include "songloader.h"
|
#include "songloader.h"
|
||||||
#include "tagreaderclient.h"
|
#include "tagreaderclient.h"
|
||||||
|
#include "engine/enginetype.h"
|
||||||
|
#include "engine/enginebase.h"
|
||||||
#include "collection/collectionbackend.h"
|
#include "collection/collectionbackend.h"
|
||||||
#include "collection/collectionquery.h"
|
#include "collection/collectionquery.h"
|
||||||
#include "collection/sqlrow.h"
|
#include "collection/sqlrow.h"
|
||||||
@@ -78,6 +80,7 @@ SongLoader::SongLoader(CollectionBackendInterface *collection, const Player *pla
|
|||||||
parser_(nullptr),
|
parser_(nullptr),
|
||||||
collection_(collection),
|
collection_(collection),
|
||||||
player_(player) {
|
player_(player) {
|
||||||
|
|
||||||
if (sRawUriSchemes.isEmpty()) {
|
if (sRawUriSchemes.isEmpty()) {
|
||||||
sRawUriSchemes << "udp"
|
sRawUriSchemes << "udp"
|
||||||
<< "mms"
|
<< "mms"
|
||||||
@@ -97,7 +100,7 @@ SongLoader::SongLoader(CollectionBackendInterface *collection, const Player *pla
|
|||||||
}
|
}
|
||||||
|
|
||||||
SongLoader::~SongLoader() {
|
SongLoader::~SongLoader() {
|
||||||
|
|
||||||
#ifdef HAVE_GSTREAMER
|
#ifdef HAVE_GSTREAMER
|
||||||
if (pipeline_) {
|
if (pipeline_) {
|
||||||
state_ = Finished;
|
state_ = Finished;
|
||||||
@@ -121,24 +124,29 @@ SongLoader::Result SongLoader::Load(const QUrl &url) {
|
|||||||
return Success;
|
return Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (player_->engine()->type() == Engine::GStreamer) {
|
||||||
#ifdef HAVE_GSTREAMER
|
#ifdef HAVE_GSTREAMER
|
||||||
preload_func_ = std::bind(&SongLoader::LoadRemote, this);
|
preload_func_ = std::bind(&SongLoader::LoadRemote, this);
|
||||||
|
return BlockingLoadRequired;
|
||||||
|
#else
|
||||||
|
return Error;
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
return BlockingLoadRequired;
|
return Success;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SongLoader::LoadFilenamesBlocking() {
|
void SongLoader::LoadFilenamesBlocking() {
|
||||||
|
|
||||||
if (preload_func_) {
|
if (preload_func_) {
|
||||||
preload_func_();
|
preload_func_();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SongLoader::Result SongLoader::LoadLocalPartial(const QString &filename) {
|
SongLoader::Result SongLoader::LoadLocalPartial(const QString &filename) {
|
||||||
|
|
||||||
qLog(Debug) << "Fast Loading local file" << 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.
|
// First check to see if it's a directory - if so we can load all the songs inside right away.
|
||||||
if (QFileInfo(filename).isDir()) {
|
if (QFileInfo(filename).isDir()) {
|
||||||
@@ -149,7 +157,7 @@ SongLoader::Result SongLoader::LoadLocalPartial(const QString &filename) {
|
|||||||
song.InitFromFilePartial(filename);
|
song.InitFromFilePartial(filename);
|
||||||
if (song.is_valid()) songs_ << song;
|
if (song.is_valid()) songs_ << song;
|
||||||
return Success;
|
return Success;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SongLoader::Result SongLoader::LoadAudioCD() {
|
SongLoader::Result SongLoader::LoadAudioCD() {
|
||||||
@@ -208,6 +216,7 @@ SongLoader::Result SongLoader::LoadLocal(const QString &filename) {
|
|||||||
// It's not in the database, load it asynchronously.
|
// It's not in the database, load it asynchronously.
|
||||||
preload_func_ = std::bind(&SongLoader::LoadLocalAsync, this, filename);
|
preload_func_ = std::bind(&SongLoader::LoadLocalAsync, this, filename);
|
||||||
return BlockingLoadRequired;
|
return BlockingLoadRequired;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SongLoader::LoadLocalAsync(const QString &filename) {
|
void SongLoader::LoadLocalAsync(const QString &filename) {
|
||||||
@@ -253,6 +262,7 @@ void SongLoader::LoadLocalAsync(const QString &filename) {
|
|||||||
Song song;
|
Song song;
|
||||||
song.InitFromFilePartial(filename);
|
song.InitFromFilePartial(filename);
|
||||||
if (song.is_valid()) songs_ << song;
|
if (song.is_valid()) songs_ << song;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SongLoader::LoadMetadataBlocking() {
|
void SongLoader::LoadMetadataBlocking() {
|
||||||
@@ -274,7 +284,8 @@ void SongLoader::EffectiveSongLoad(Song *song) {
|
|||||||
Song collection_song = collection_->GetSongByUrl(song->url());
|
Song collection_song = collection_->GetSongByUrl(song->url());
|
||||||
if (collection_song.is_valid()) {
|
if (collection_song.is_valid()) {
|
||||||
*song = collection_song;
|
*song = collection_song;
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
// it's a normal media file
|
// it's a normal media file
|
||||||
QString filename = song->url().toLocalFile();
|
QString filename = song->url().toLocalFile();
|
||||||
TagReaderClient::Instance()->ReadFileBlocking(filename, song);
|
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,
|
// 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.)
|
// it can enjoy the first song being played (seek it, have moodbar, etc.)
|
||||||
if (!songs_.isEmpty()) EffectiveSongLoad(&(*songs_.begin()));
|
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() {
|
void SongLoader::Timeout() {
|
||||||
@@ -348,10 +367,10 @@ void SongLoader::StopTypefind() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
else if (success_) {
|
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
|
// It wasn't a playlist - just put the URL in as a stream
|
||||||
//AddAsRawStream();
|
AddAsRawStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
emit LoadRemoteFinished();
|
emit LoadRemoteFinished();
|
||||||
@@ -413,7 +432,7 @@ void SongLoader::LoadRemote() {
|
|||||||
|
|
||||||
#ifdef HAVE_GSTREAMER
|
#ifdef HAVE_GSTREAMER
|
||||||
void SongLoader::TypeFound(GstElement *, uint, GstCaps *caps, void *self) {
|
void SongLoader::TypeFound(GstElement *, uint, GstCaps *caps, void *self) {
|
||||||
|
|
||||||
SongLoader *instance = static_cast<SongLoader*>(self);
|
SongLoader *instance = static_cast<SongLoader*>(self);
|
||||||
|
|
||||||
if (instance->state_ != WaitingForType) return;
|
if (instance->state_ != WaitingForType) return;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef SONGLOADER_H
|
#ifndef SONGLOADER_H
|
||||||
@@ -106,6 +106,8 @@ signals:
|
|||||||
void LoadLocalDirectory(const QString &filename);
|
void LoadLocalDirectory(const QString &filename);
|
||||||
void LoadPlaylist(ParserBase *parser, const QString &filename);
|
void LoadPlaylist(ParserBase *parser, const QString &filename);
|
||||||
|
|
||||||
|
void AddAsRawStream();
|
||||||
|
|
||||||
#ifdef HAVE_GSTREAMER
|
#ifdef HAVE_GSTREAMER
|
||||||
void LoadRemote();
|
void LoadRemote();
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -110,7 +110,7 @@ void CddaSongLoader::LoadSongs() {
|
|||||||
Song song;
|
Song song;
|
||||||
song.set_id(track_number);
|
song.set_id(track_number);
|
||||||
song.set_valid(true);
|
song.set_valid(true);
|
||||||
song.set_filetype(Song::Type_Cdda);
|
song.set_filetype(Song::Type_CDDA);
|
||||||
song.set_url(GetUrlFromTrack(track_number));
|
song.set_url(GetUrlFromTrack(track_number));
|
||||||
song.set_title(QString("Track %1").arg(track_number));
|
song.set_title(QString("Track %1").arg(track_number));
|
||||||
song.set_track(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_track(track_number);
|
||||||
song.set_year(ret.year_);
|
song.set_year(ret.year_);
|
||||||
song.set_id(track_number);
|
song.set_id(track_number);
|
||||||
song.set_filetype(Song::Type_Cdda);
|
song.set_filetype(Song::Type_CDDA);
|
||||||
song.set_valid(true);
|
song.set_valid(true);
|
||||||
// We need to set url: that's how playlist will find the correct item to update
|
// We need to set url: that's how playlist will find the correct item to update
|
||||||
song.set_url(GetUrlFromTrack(track_number++));
|
song.set_url(GetUrlFromTrack(track_number++));
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -248,8 +248,8 @@ void GPodDevice::FinishDelete(bool success) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool GPodDevice::GetSupportedFiletypes(QList<Song::FileType> *ret) {
|
bool GPodDevice::GetSupportedFiletypes(QList<Song::FileType> *ret) {
|
||||||
*ret << Song::Type_Mp4;
|
*ret << Song::Type_MP4;
|
||||||
*ret << Song::Type_Mpeg;
|
*ret << Song::Type_MPEG;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -209,15 +209,15 @@ bool MtpDevice::GetSupportedFiletypes(QList<Song::FileType> *ret, LIBMTP_mtpdevi
|
|||||||
|
|
||||||
for (int i = 0; i < length; ++i) {
|
for (int i = 0; i < length; ++i) {
|
||||||
switch (LIBMTP_filetype_t(list[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_MP2:
|
||||||
case LIBMTP_FILETYPE_MP3: *ret << Song::Type_Mpeg; break;
|
case LIBMTP_FILETYPE_MP3: *ret << Song::Type_MPEG; break;
|
||||||
case LIBMTP_FILETYPE_WMA: *ret << Song::Type_Asf; break;
|
case LIBMTP_FILETYPE_WMA: *ret << Song::Type_ASF; break;
|
||||||
case LIBMTP_FILETYPE_MP4:
|
case LIBMTP_FILETYPE_MP4:
|
||||||
case LIBMTP_FILETYPE_M4A:
|
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:
|
case LIBMTP_FILETYPE_FLAC:
|
||||||
*ret << Song::Type_Flac;
|
*ret << Song::Type_FLAC;
|
||||||
*ret << Song::Type_OggFlac;
|
*ret << Song::Type_OggFlac;
|
||||||
break;
|
break;
|
||||||
case LIBMTP_FILETYPE_OGG:
|
case LIBMTP_FILETYPE_OGG:
|
||||||
|
|||||||
@@ -786,7 +786,6 @@ void GstEngine::StartFadeoutPause() {
|
|||||||
|
|
||||||
void GstEngine::StartTimers() {
|
void GstEngine::StartTimers() {
|
||||||
StopTimers();
|
StopTimers();
|
||||||
|
|
||||||
timer_id_ = startTimer(kTimerIntervalNanosec / kNsecPerMsec);
|
timer_id_ = startTimer(kTimerIntervalNanosec / kNsecPerMsec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ EngineBase::PluginDetailsList VLCEngine::GetPluginList() const {
|
|||||||
ret << details;
|
ret << details;
|
||||||
//GetDevicesList(audio_output->psz_name);
|
//GetDevicesList(audio_output->psz_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
libvlc_audio_output_list_release(audio_output_list);
|
libvlc_audio_output_list_release(audio_output_list);
|
||||||
|
|
||||||
return ret;
|
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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -84,6 +84,11 @@
|
|||||||
#include "songplaylistitem.h"
|
#include "songplaylistitem.h"
|
||||||
#include "tagreadermessages.pb.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::_1;
|
||||||
using std::placeholders::_2;
|
using std::placeholders::_2;
|
||||||
using std::shared_ptr;
|
using std::shared_ptr;
|
||||||
@@ -153,7 +158,7 @@ Playlist::~Playlist() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <typename T>
|
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;
|
PlaylistItemList items;
|
||||||
|
|
||||||
@@ -161,7 +166,7 @@ void Playlist::InsertSongItems(const SongList &songs, int pos, bool play_now, bo
|
|||||||
items << PlaylistItemPtr(new T(song));
|
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_AlbumArtist: return song.playlist_albumartist();
|
||||||
case Column_Composer: return song.composer();
|
case Column_Composer: return song.composer();
|
||||||
case Column_Performer: return song.performer();
|
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_PlayCount: return song.playcount();
|
||||||
case Column_SkipCount: return song.skipcount();
|
case Column_SkipCount: return song.skipcount();
|
||||||
case Column_LastPlayed: return song.lastplayed();
|
case Column_LastPlayed: return song.lastplayed();
|
||||||
|
|
||||||
case Column_Samplerate: return song.samplerate();
|
case Column_Samplerate: return song.samplerate();
|
||||||
case Column_Bitdepth: return song.bitdepth();
|
case Column_Bitdepth: return song.bitdepth();
|
||||||
case Column_Bitrate: return song.bitrate();
|
case Column_Bitrate: return song.bitrate();
|
||||||
case Column_SamplerateBitdepth: return song.SampleRateBitDepthToText();
|
|
||||||
|
|
||||||
case Column_Filename: return song.url();
|
case Column_Filename: return song.url();
|
||||||
case Column_BaseFilename: return song.basefilename();
|
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();
|
if (role == Qt::DisplayRole) return song.comment().simplified();
|
||||||
return song.comment();
|
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()) {
|
if (items_[index.row()]->HasCurrentForegroundColor()) {
|
||||||
return QBrush(items_[index.row()]->GetCurrentForegroundColor());
|
return QBrush(items_[index.row()]->GetCurrentForegroundColor());
|
||||||
}
|
}
|
||||||
//if (index.row() < dynamic_history_length()) {
|
|
||||||
//return QBrush(kDynamicHistoryColor);
|
|
||||||
//}
|
|
||||||
return QVariant();
|
return QVariant();
|
||||||
|
|
||||||
case Qt::BackgroundRole:
|
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) {
|
void Playlist::set_current_row(int i, bool is_stopping) {
|
||||||
|
|
||||||
QModelIndex old_current_item_index = current_item_index_;
|
QModelIndex old_current_item_index = current_item_index_;
|
||||||
//ClearStreamMetadata();
|
ClearStreamMetadata();
|
||||||
|
|
||||||
current_item_index_ = QPersistentModelIndex(index(i, 0, QModelIndex()));
|
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 play_now = false;
|
||||||
bool enqueue_now = false;
|
bool enqueue_now = false;
|
||||||
|
bool enqueue_next_now = false;
|
||||||
|
|
||||||
if (const MimeData *mime_data = qobject_cast<const MimeData*>(data)) {
|
if (const MimeData *mime_data = qobject_cast<const MimeData*>(data)) {
|
||||||
if (mime_data->clear_first_) {
|
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_;
|
play_now = mime_data->play_now_;
|
||||||
enqueue_now = mime_data->enqueue_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)) {
|
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)
|
if (song_data->backend && song_data->backend->songs_table() == SCollection::kSongsTable)
|
||||||
InsertSongItems<CollectionPlaylistItem>(song_data->songs, row, play_now, enqueue_now);
|
InsertSongItems<CollectionPlaylistItem>(song_data->songs, row, play_now, enqueue_now);
|
||||||
else
|
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)) {
|
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)) {
|
else if (data->hasFormat(kRowsMimetype)) {
|
||||||
// Dragged from the playlist
|
// 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());
|
SongLoaderInserter *inserter = new SongLoaderInserter(task_manager_, collection_, backend_->app()->player());
|
||||||
connect(inserter, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
|
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())
|
if (itemsIn.isEmpty())
|
||||||
return;
|
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) {
|
void Playlist::InsertCollectionItems(const SongList &songs, int pos, bool play_now, bool enqueue, bool enqueue_next) {
|
||||||
InsertSongItems<CollectionPlaylistItem>(songs, pos, play_now, enqueue);
|
InsertSongItems<CollectionPlaylistItem>(songs, pos, play_now, enqueue, enqueue_next);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Playlist::InsertSongs(const SongList &songs, int pos, bool play_now, bool enqueue) {
|
void Playlist::InsertSongs(const SongList &songs, int pos, bool play_now, bool enqueue, bool enqueue_next) {
|
||||||
InsertSongItems<SongPlaylistItem>(songs, pos, play_now, enqueue);
|
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;
|
PlaylistItemList items;
|
||||||
for (const Song &song : songs) {
|
for (const Song &song : songs) {
|
||||||
if (song.is_collection_song()) {
|
if (song.is_collection_song()) {
|
||||||
items << PlaylistItemPtr(new CollectionPlaylistItem(song));
|
items << PlaylistItemPtr(new CollectionPlaylistItem(song));
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
items << PlaylistItemPtr(new SongPlaylistItem(song));
|
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];
|
PlaylistItemPtr &item = items_[i];
|
||||||
if (item->Metadata().url() == song.url() &&
|
if (item->Metadata().url() == song.url() &&
|
||||||
(item->Metadata().filetype() == Song::Type_Unknown ||
|
(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)
|
// 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;
|
PlaylistItemPtr new_item;
|
||||||
if (song.is_collection_song()) {
|
if (song.is_collection_song()) {
|
||||||
new_item = PlaylistItemPtr(new CollectionPlaylistItem(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_Bitrate: cmp(bitrate);
|
||||||
case Column_Samplerate: cmp(samplerate);
|
case Column_Samplerate: cmp(samplerate);
|
||||||
case Column_Bitdepth: cmp(bitdepth);
|
case Column_Bitdepth: cmp(bitdepth);
|
||||||
case Column_SamplerateBitdepth:
|
|
||||||
return QString::localeAwareCompare(a->Metadata().SampleRateBitDepthToText().toLower(), b->Metadata().SampleRateBitDepthToText().toLower()) < 0;
|
|
||||||
case Column_Filename:
|
case Column_Filename:
|
||||||
return (QString::localeAwareCompare(a->Url().path().toLower(), b->Url().path().toLower()) < 0);
|
return (QString::localeAwareCompare(a->Url().path().toLower(), b->Url().path().toLower()) < 0);
|
||||||
case Column_BaseFilename: cmp(basefilename);
|
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_DateCreated: cmp(ctime);
|
||||||
|
|
||||||
case Column_Comment: strcmp(comment);
|
case Column_Comment: strcmp(comment);
|
||||||
//case Column_Source: cmp(url);
|
case Column_Source: cmp(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
#undef cmp
|
#undef cmp
|
||||||
@@ -1126,7 +1144,6 @@ QString Playlist::column_name(Column column) {
|
|||||||
|
|
||||||
case Column_Samplerate: return tr("Sample rate");
|
case Column_Samplerate: return tr("Sample rate");
|
||||||
case Column_Bitdepth: return tr("Bit depth");
|
case Column_Bitdepth: return tr("Bit depth");
|
||||||
case Column_SamplerateBitdepth: return tr("Sample rate B");
|
|
||||||
case Column_Bitrate: return tr("Bitrate");
|
case Column_Bitrate: return tr("Bitrate");
|
||||||
|
|
||||||
case Column_Filename: return tr("File name");
|
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_DateCreated: return tr("Date created");
|
||||||
|
|
||||||
case Column_Comment: return tr("Comment");
|
case Column_Comment: return tr("Comment");
|
||||||
//case Column_Source: return tr("Source");
|
case Column_Source: return tr("Source");
|
||||||
default: return QString();
|
default: return QString();
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
@@ -1757,6 +1774,7 @@ void Playlist::InvalidateDeletedSongs() {
|
|||||||
PlaylistItemPtr item = items_[row];
|
PlaylistItemPtr item = items_[row];
|
||||||
Song song = item->Metadata();
|
Song song = item->Metadata();
|
||||||
|
|
||||||
|
if (!song.is_stream()) {
|
||||||
bool exists = QFile::exists(song.url().toLocalFile());
|
bool exists = QFile::exists(song.url().toLocalFile());
|
||||||
|
|
||||||
if (!exists && !item->HasForegroundColor(kInvalidSongPriority)) {
|
if (!exists && !item->HasForegroundColor(kInvalidSongPriority)) {
|
||||||
@@ -1768,6 +1786,7 @@ void Playlist::InvalidateDeletedSongs() {
|
|||||||
item->RemoveForegroundColor(kInvalidSongPriority);
|
item->RemoveForegroundColor(kInvalidSongPriority);
|
||||||
invalidated_rows.append(row);
|
invalidated_rows.append(row);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ReloadItems(invalidated_rows);
|
ReloadItems(invalidated_rows);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef PLAYLIST_H
|
#ifndef PLAYLIST_H
|
||||||
@@ -54,6 +54,8 @@ class PlaylistBackend;
|
|||||||
class PlaylistFilter;
|
class PlaylistFilter;
|
||||||
class Queue;
|
class Queue;
|
||||||
class TaskManager;
|
class TaskManager;
|
||||||
|
class InternetModel;
|
||||||
|
class InternetService;
|
||||||
|
|
||||||
namespace PlaylistUndoCommands {
|
namespace PlaylistUndoCommands {
|
||||||
class InsertItems;
|
class InsertItems;
|
||||||
@@ -110,7 +112,6 @@ class Playlist : public QAbstractListModel {
|
|||||||
Column_Genre,
|
Column_Genre,
|
||||||
Column_Samplerate,
|
Column_Samplerate,
|
||||||
Column_Bitdepth,
|
Column_Bitdepth,
|
||||||
Column_SamplerateBitdepth,
|
|
||||||
Column_Bitrate,
|
Column_Bitrate,
|
||||||
Column_Filename,
|
Column_Filename,
|
||||||
Column_BaseFilename,
|
Column_BaseFilename,
|
||||||
@@ -123,6 +124,7 @@ class Playlist : public QAbstractListModel {
|
|||||||
Column_LastPlayed,
|
Column_LastPlayed,
|
||||||
Column_Comment,
|
Column_Comment,
|
||||||
Column_Grouping,
|
Column_Grouping,
|
||||||
|
Column_Source,
|
||||||
ColumnCount
|
ColumnCount
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -212,10 +214,11 @@ class Playlist : public QAbstractListModel {
|
|||||||
QUndoStack *undo_stack() const { return undo_stack_; }
|
QUndoStack *undo_stack() const { return undo_stack_; }
|
||||||
|
|
||||||
// Changing the playlist
|
// Changing the playlist
|
||||||
void InsertItems (const PlaylistItemList &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);
|
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);
|
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);
|
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();
|
void ReshuffleIndices();
|
||||||
|
|
||||||
@@ -276,7 +279,7 @@ class Playlist : public QAbstractListModel {
|
|||||||
|
|
||||||
void SetColumnAlignment(const ColumnAlignmentMap &alignment);
|
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.
|
// Removes items with given indices from the playlist. This operation is not undoable.
|
||||||
void RemoveItemsWithoutUndo(const QList<int> &indices);
|
void RemoveItemsWithoutUndo(const QList<int> &indices);
|
||||||
|
|
||||||
@@ -302,7 +305,7 @@ private:
|
|||||||
bool FilterContainsVirtualIndex(int i) const;
|
bool FilterContainsVirtualIndex(int i) const;
|
||||||
|
|
||||||
template <typename T>
|
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
|
// 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);
|
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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -145,7 +145,7 @@ QSqlQuery PlaylistBackend::GetPlaylistRows(int playlist) {
|
|||||||
" p.ROWID, " +
|
" p.ROWID, " +
|
||||||
Song::JoinSpec("p") +
|
Song::JoinSpec("p") +
|
||||||
","
|
","
|
||||||
" p.type"
|
" p.type, p.internet_service"
|
||||||
" FROM playlist_items AS p"
|
" FROM playlist_items AS p"
|
||||||
" LEFT JOIN songs"
|
" LEFT JOIN songs"
|
||||||
" ON p.collection_id = songs.ROWID"
|
" ON p.collection_id = songs.ROWID"
|
||||||
@@ -279,7 +279,7 @@ void PlaylistBackend::SavePlaylist(int playlist, const PlaylistItemList &items,
|
|||||||
QSqlQuery clear(db);
|
QSqlQuery clear(db);
|
||||||
clear.prepare("DELETE FROM playlist_items WHERE playlist = :playlist");
|
clear.prepare("DELETE FROM playlist_items WHERE playlist = :playlist");
|
||||||
QSqlQuery insert(db);
|
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);
|
QSqlQuery update(db);
|
||||||
update.prepare("UPDATE playlists SET last_played=:last_played WHERE ROWID=:playlist");
|
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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringBuilder>
|
#include <QStringBuilder>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
#include <QRegExp>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
@@ -460,6 +461,12 @@ QPixmap SongSourceDelegate::LookupPixmap(const QUrl &url, const QSize &size) con
|
|||||||
else if (url.scheme() == "cdda") {
|
else if (url.scheme() == "cdda") {
|
||||||
icon = IconLoader::Load("cd");
|
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 {
|
else {
|
||||||
icon = IconLoader::Load("folder-sound");
|
icon = IconLoader::Load("folder-sound");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -445,9 +445,6 @@ FilterTree *FilterParser::createSearchTermTreeNode(
|
|||||||
if (columns_[col] == Playlist::Column_Length) {
|
if (columns_[col] == Playlist::Column_Length) {
|
||||||
search_value = parseTime(search);
|
search_value = parseTime(search);
|
||||||
}
|
}
|
||||||
//else if (columns_[col] == Playlist::Column_Rating) {
|
|
||||||
//search_value = static_cast<int>(search.toDouble() * 2.0 + 0.5);
|
|
||||||
//}
|
|
||||||
else {
|
else {
|
||||||
search_value = search.toInt();
|
search_value = search.toInt();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -37,6 +37,8 @@
|
|||||||
#include "playlistitem.h"
|
#include "playlistitem.h"
|
||||||
#include "songplaylistitem.h"
|
#include "songplaylistitem.h"
|
||||||
|
|
||||||
|
#include "internet/internetplaylistitem.h"
|
||||||
|
|
||||||
PlaylistItem::~PlaylistItem() {
|
PlaylistItem::~PlaylistItem() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,11 +46,13 @@ PlaylistItem* PlaylistItem::NewFromType(const QString &type) {
|
|||||||
|
|
||||||
if (type == "Collection") return new CollectionPlaylistItem(type);
|
if (type == "Collection") return new CollectionPlaylistItem(type);
|
||||||
else if (type == "File") return new SongPlaylistItem(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;
|
qLog(Warning) << "Invalid PlaylistItem type:" << type;
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaylistItem* PlaylistItem::NewFromSongsTable(const QString &table, const Song &song) {
|
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(":type", type());
|
||||||
query->bindValue(":collection_id", DatabaseValue(Column_CollectionId));
|
query->bindValue(":collection_id", DatabaseValue(Column_CollectionId));
|
||||||
|
query->bindValue(":internet_service", DatabaseValue(Column_InternetService));
|
||||||
|
|
||||||
DatabaseSongMetadata().BindToQuery(query);
|
DatabaseSongMetadata().BindToQuery(query);
|
||||||
|
|
||||||
@@ -119,3 +124,4 @@ bool PlaylistItem::HasCurrentForegroundColor() const {
|
|||||||
}
|
}
|
||||||
void PlaylistItem::SetShouldSkip(bool val) { should_skip_ = val; }
|
void PlaylistItem::SetShouldSkip(bool val) { should_skip_ = val; }
|
||||||
bool PlaylistItem::GetShouldSkip() const { return should_skip_; }
|
bool PlaylistItem::GetShouldSkip() const { return should_skip_; }
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef PLAYLISTITEM_H
|
#ifndef PLAYLISTITEM_H
|
||||||
@@ -104,7 +104,7 @@ class PlaylistItem : public std::enable_shared_from_this<PlaylistItem> {
|
|||||||
protected:
|
protected:
|
||||||
bool should_skip_;
|
bool should_skip_;
|
||||||
|
|
||||||
enum DatabaseColumn { Column_CollectionId, Column_InternetService, };
|
enum DatabaseColumn { Column_CollectionId, Column_InternetService };
|
||||||
|
|
||||||
virtual QVariant DatabaseValue(DatabaseColumn) const {
|
virtual QVariant DatabaseValue(DatabaseColumn) const {
|
||||||
return QVariant(QVariant::String);
|
return QVariant(QVariant::String);
|
||||||
@@ -126,3 +126,4 @@ Q_DECLARE_METATYPE(QList<PlaylistItemPtr>)
|
|||||||
Q_DECLARE_OPERATORS_FOR_FLAGS(PlaylistItem::Options)
|
Q_DECLARE_OPERATORS_FOR_FLAGS(PlaylistItem::Options)
|
||||||
|
|
||||||
#endif // PLAYLISTITEM_H
|
#endif // PLAYLISTITEM_H
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -228,20 +228,16 @@ void PlaylistView::SetItemDelegates(CollectionBackend *backend) {
|
|||||||
setItemDelegateForColumn(Playlist::Column_Samplerate, new PlaylistDelegateBase(this, ("Hz")));
|
setItemDelegateForColumn(Playlist::Column_Samplerate, new PlaylistDelegateBase(this, ("Hz")));
|
||||||
setItemDelegateForColumn(Playlist::Column_Bitdepth, new PlaylistDelegateBase(this, ("Bit")));
|
setItemDelegateForColumn(Playlist::Column_Bitdepth, new PlaylistDelegateBase(this, ("Bit")));
|
||||||
setItemDelegateForColumn(Playlist::Column_Bitrate, new PlaylistDelegateBase(this, tr("kbps")));
|
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_Filename, new NativeSeparatorsDelegate(this));
|
||||||
setItemDelegateForColumn(Playlist::Column_LastPlayed, new LastPlayedItemDelegate(this));
|
setItemDelegateForColumn(Playlist::Column_LastPlayed, new LastPlayedItemDelegate(this));
|
||||||
|
|
||||||
#if 0
|
|
||||||
if (app_ && app_->player()) {
|
if (app_ && app_->player()) {
|
||||||
setItemDelegateForColumn(Playlist::Column_Source, new SongSourceDelegate(this, app_->player()));
|
setItemDelegateForColumn(Playlist::Column_Source, new SongSourceDelegate(this, app_->player()));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
header_->HideSection(Playlist::Column_Source);
|
header_->HideSection(Playlist::Column_Source);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -946,7 +942,8 @@ void PlaylistView::ReloadSettings() {
|
|||||||
header_->SetColumnWidth(Playlist::Column_Album, 0.10);
|
header_->SetColumnWidth(Playlist::Column_Album, 0.10);
|
||||||
header_->SetColumnWidth(Playlist::Column_Length, 0.03);
|
header_->SetColumnWidth(Playlist::Column_Length, 0.03);
|
||||||
header_->SetColumnWidth(Playlist::Column_Bitrate, 0.07);
|
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);
|
header_->SetColumnWidth(Playlist::Column_Filetype, 0.06);
|
||||||
|
|
||||||
setting_initial_header_layout_ = false;
|
setting_initial_header_layout_ = false;
|
||||||
@@ -1089,7 +1086,6 @@ ColumnAlignmentMap PlaylistView::DefaultColumnAlignment() {
|
|||||||
ret[Playlist::Column_Bitrate] =
|
ret[Playlist::Column_Bitrate] =
|
||||||
ret[Playlist::Column_Samplerate] =
|
ret[Playlist::Column_Samplerate] =
|
||||||
ret[Playlist::Column_Bitdepth] =
|
ret[Playlist::Column_Bitdepth] =
|
||||||
ret[Playlist::Column_SamplerateBitdepth] =
|
|
||||||
ret[Playlist::Column_Filesize] =
|
ret[Playlist::Column_Filesize] =
|
||||||
ret[Playlist::Column_PlayCount] =
|
ret[Playlist::Column_PlayCount] =
|
||||||
ret[Playlist::Column_SkipCount] =
|
ret[Playlist::Column_SkipCount] =
|
||||||
@@ -1216,8 +1212,7 @@ void PlaylistView::focusInEvent(QFocusEvent *event) {
|
|||||||
|
|
||||||
QTreeView::focusInEvent(event);
|
QTreeView::focusInEvent(event);
|
||||||
|
|
||||||
if (event->reason() == Qt::TabFocusReason ||
|
if (event->reason() == Qt::TabFocusReason || event->reason() == Qt::BacktabFocusReason) {
|
||||||
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 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.
|
// 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();
|
const QModelIndex ¤t = selectionModel()->currentIndex();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef PLAYLISTVIEW_H
|
#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 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.
|
// 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 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 {
|
class PlaylistProxyStyle : public QProxyStyle {
|
||||||
public:
|
public:
|
||||||
PlaylistProxyStyle(QStyle *base);
|
PlaylistProxyStyle(QStyle *base);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -62,6 +62,8 @@
|
|||||||
#include "playlistsettingspage.h"
|
#include "playlistsettingspage.h"
|
||||||
#include "shortcutssettingspage.h"
|
#include "shortcutssettingspage.h"
|
||||||
#include "transcodersettingspage.h"
|
#include "transcodersettingspage.h"
|
||||||
|
#include "tidalsettingspage.h"
|
||||||
|
|
||||||
#include "ui_settingsdialog.h"
|
#include "ui_settingsdialog.h"
|
||||||
|
|
||||||
class QShowEvent;
|
class QShowEvent;
|
||||||
@@ -122,6 +124,7 @@ SettingsDialog::SettingsDialog(Application *app, QWidget *parent)
|
|||||||
#ifdef HAVE_GSTREAMER
|
#ifdef HAVE_GSTREAMER
|
||||||
AddPage(Page_Transcoding, new TranscoderSettingsPage(this), general);
|
AddPage(Page_Transcoding, new TranscoderSettingsPage(this), general);
|
||||||
#endif
|
#endif
|
||||||
|
AddPage(Page_Tidal, new TidalSettingsPage(this), general);
|
||||||
|
|
||||||
// User interface
|
// User interface
|
||||||
QTreeWidgetItem *iface = AddCategory(tr("User interface"));
|
QTreeWidgetItem *iface = AddCategory(tr("User interface"));
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef SETTINGSDIALOG_H
|
#ifndef SETTINGSDIALOG_H
|
||||||
@@ -79,6 +79,7 @@ public:
|
|||||||
Page_Notifications,
|
Page_Notifications,
|
||||||
Page_Proxy,
|
Page_Proxy,
|
||||||
Page_Transcoding,
|
Page_Transcoding,
|
||||||
|
Page_Tidal,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum Role {
|
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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -222,14 +222,14 @@ Transcoder::Transcoder(QObject *parent, const QString &settings_postfix)
|
|||||||
QList<TranscoderPreset> Transcoder::GetAllPresets() {
|
QList<TranscoderPreset> Transcoder::GetAllPresets() {
|
||||||
|
|
||||||
QList<TranscoderPreset> ret;
|
QList<TranscoderPreset> ret;
|
||||||
ret << PresetForFileType(Song::Type_Flac);
|
ret << PresetForFileType(Song::Type_FLAC);
|
||||||
ret << PresetForFileType(Song::Type_Mp4);
|
ret << PresetForFileType(Song::Type_MP4);
|
||||||
ret << PresetForFileType(Song::Type_Mpeg);
|
ret << PresetForFileType(Song::Type_MPEG);
|
||||||
ret << PresetForFileType(Song::Type_OggVorbis);
|
ret << PresetForFileType(Song::Type_OggVorbis);
|
||||||
ret << PresetForFileType(Song::Type_OggFlac);
|
ret << PresetForFileType(Song::Type_OggFlac);
|
||||||
ret << PresetForFileType(Song::Type_OggSpeex);
|
ret << PresetForFileType(Song::Type_OggSpeex);
|
||||||
ret << PresetForFileType(Song::Type_Asf);
|
ret << PresetForFileType(Song::Type_ASF);
|
||||||
ret << PresetForFileType(Song::Type_Wav);
|
ret << PresetForFileType(Song::Type_WAV);
|
||||||
ret << PresetForFileType(Song::Type_OggOpus);
|
ret << PresetForFileType(Song::Type_OggOpus);
|
||||||
return ret;
|
return ret;
|
||||||
|
|
||||||
@@ -238,11 +238,11 @@ QList<TranscoderPreset> Transcoder::GetAllPresets() {
|
|||||||
TranscoderPreset Transcoder::PresetForFileType(Song::FileType type) {
|
TranscoderPreset Transcoder::PresetForFileType(Song::FileType type) {
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case Song::Type_Flac:
|
case Song::Type_FLAC:
|
||||||
return TranscoderPreset(type, tr("FLAC"), "flac", "audio/x-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");
|
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");
|
return TranscoderPreset(type, tr("MP3"), "mp3", "audio/mpeg, mpegversion=(int)1, layer=(int)3");
|
||||||
case Song::Type_OggVorbis:
|
case Song::Type_OggVorbis:
|
||||||
return TranscoderPreset(type, tr("Ogg Vorbis"), "ogg", "audio/x-vorbis", "application/ogg");
|
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");
|
return TranscoderPreset(type, tr("Ogg Speex"), "spx", "audio/x-speex", "application/ogg");
|
||||||
case Song::Type_OggOpus:
|
case Song::Type_OggOpus:
|
||||||
return TranscoderPreset(type, tr("Ogg Opus"), "opus", "audio/x-opus", "application/ogg");
|
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");
|
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");
|
return TranscoderPreset(type, tr("Wav"), "wav", QString(), "audio/x-wav");
|
||||||
default:
|
default:
|
||||||
qLog(Warning) << "Unsupported format in PresetForFileType:" << type;
|
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;
|
if (supported.isEmpty()) return Song::Type_Unknown;
|
||||||
|
|
||||||
QList<Song::FileType> best_formats;
|
QList<Song::FileType> best_formats;
|
||||||
best_formats << Song::Type_Mpeg;
|
best_formats << Song::Type_MPEG;
|
||||||
best_formats << Song::Type_OggVorbis;
|
best_formats << Song::Type_OggVorbis;
|
||||||
best_formats << Song::Type_Asf;
|
best_formats << Song::Type_ASF;
|
||||||
|
|
||||||
for (Song::FileType type : best_formats) {
|
for (Song::FileType type : best_formats) {
|
||||||
if (supported.isEmpty() || supported.contains(type)) return type;
|
if (supported.isEmpty() || supported.contains(type)) return type;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -44,14 +44,14 @@ TranscoderOptionsDialog::TranscoderOptionsDialog(Song::FileType type, QWidget *p
|
|||||||
ui_->setupUi(this);
|
ui_->setupUi(this);
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case Song::Type_Flac:
|
case Song::Type_FLAC:
|
||||||
case Song::Type_OggFlac: options_ = new TranscoderOptionsFlac(this); break;
|
case Song::Type_OggFlac: options_ = new TranscoderOptionsFlac(this); break;
|
||||||
case Song::Type_Mp4: options_ = new TranscoderOptionsAAC(this); break;
|
case Song::Type_MP4: options_ = new TranscoderOptionsAAC(this); break;
|
||||||
case Song::Type_Mpeg: options_ = new TranscoderOptionsMP3(this); break;
|
case Song::Type_MPEG: options_ = new TranscoderOptionsMP3(this); break;
|
||||||
case Song::Type_OggVorbis: options_ = new TranscoderOptionsVorbis(this); break;
|
case Song::Type_OggVorbis: options_ = new TranscoderOptionsVorbis(this); break;
|
||||||
case Song::Type_OggOpus: options_ = new TranscoderOptionsOpus(this); break;
|
case Song::Type_OggOpus: options_ = new TranscoderOptionsOpus(this); break;
|
||||||
case Song::Type_OggSpeex: options_ = new TranscoderOptionsSpeex(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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -59,7 +59,6 @@ FileViewList::FileViewList(QWidget *parent)
|
|||||||
|
|
||||||
void FileViewList::contextMenuEvent(QContextMenuEvent *e) {
|
void FileViewList::contextMenuEvent(QContextMenuEvent *e) {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
menu_selection_ = selectionModel()->selection();
|
menu_selection_ = selectionModel()->selection();
|
||||||
|
|
||||||
menu_->popup(e->globalPos());
|
menu_->popup(e->globalPos());
|
||||||
@@ -69,7 +68,6 @@ void FileViewList::contextMenuEvent(QContextMenuEvent *e) {
|
|||||||
|
|
||||||
QList<QUrl> FileViewList::UrlListFromSelection() const {
|
QList<QUrl> FileViewList::UrlListFromSelection() const {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
QList<QUrl> urls;
|
QList<QUrl> urls;
|
||||||
for (const QModelIndex& index : menu_selection_.indexes()) {
|
for (const QModelIndex& index : menu_selection_.indexes()) {
|
||||||
if (index.column() == 0)
|
if (index.column() == 0)
|
||||||
@@ -81,7 +79,6 @@ QList<QUrl> FileViewList::UrlListFromSelection() const {
|
|||||||
|
|
||||||
MimeData *FileViewList::MimeDataFromSelection() const {
|
MimeData *FileViewList::MimeDataFromSelection() const {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
MimeData *data = new MimeData;
|
MimeData *data = new MimeData;
|
||||||
data->setUrls(UrlListFromSelection());
|
data->setUrls(UrlListFromSelection());
|
||||||
|
|
||||||
@@ -101,7 +98,6 @@ MimeData *FileViewList::MimeDataFromSelection() const {
|
|||||||
|
|
||||||
QStringList FileViewList::FilenamesFromSelection() const {
|
QStringList FileViewList::FilenamesFromSelection() const {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
QStringList filenames;
|
QStringList filenames;
|
||||||
for (const QModelIndex& index : menu_selection_.indexes()) {
|
for (const QModelIndex& index : menu_selection_.indexes()) {
|
||||||
if (index.column() == 0)
|
if (index.column() == 0)
|
||||||
@@ -112,14 +108,12 @@ QStringList FileViewList::FilenamesFromSelection() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void FileViewList::LoadSlot() {
|
void FileViewList::LoadSlot() {
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
MimeData *data = MimeDataFromSelection();
|
MimeData *data = MimeDataFromSelection();
|
||||||
data->clear_first_ = true;
|
data->clear_first_ = true;
|
||||||
emit AddToPlaylist(data);
|
emit AddToPlaylist(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileViewList::AddToPlaylistSlot() {
|
void FileViewList::AddToPlaylistSlot() {
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
emit AddToPlaylist(MimeDataFromSelection());
|
emit AddToPlaylist(MimeDataFromSelection());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,18 +137,15 @@ void FileViewList::CopyToDeviceSlot() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void FileViewList::DeleteSlot() {
|
void FileViewList::DeleteSlot() {
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
emit Delete(FilenamesFromSelection());
|
emit Delete(FilenamesFromSelection());
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileViewList::EditTagsSlot() {
|
void FileViewList::EditTagsSlot() {
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
emit EditTags(UrlListFromSelection());
|
emit EditTags(UrlListFromSelection());
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileViewList::mousePressEvent(QMouseEvent *e) {
|
void FileViewList::mousePressEvent(QMouseEvent *e) {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
switch (e->button()) {
|
switch (e->button()) {
|
||||||
case Qt::XButton1:
|
case Qt::XButton1:
|
||||||
emit Back();
|
emit Back();
|
||||||
@@ -183,6 +174,5 @@ void FileViewList::mousePressEvent(QMouseEvent *e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void FileViewList::ShowInBrowser() {
|
void FileViewList::ShowInBrowser() {
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
Utilities::OpenInFileBrowser(UrlListFromSelection());
|
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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -84,7 +84,6 @@ StatusView::StatusView(CollectionViewContainer *collectionviewcontainer, QWidget
|
|||||||
label_playing_text_(nullptr),
|
label_playing_text_(nullptr),
|
||||||
|
|
||||||
album_cover_choice_controller_(new AlbumCoverChoiceController(this)),
|
album_cover_choice_controller_(new AlbumCoverChoiceController(this)),
|
||||||
show_hide_animation_(new QTimeLine(500, this)),
|
|
||||||
fade_animation_(new QTimeLine(1000, this)),
|
fade_animation_(new QTimeLine(1000, this)),
|
||||||
image_blank_(""),
|
image_blank_(""),
|
||||||
image_nosong_(":/pictures/strawberry.png"),
|
image_nosong_(":/pictures/strawberry.png"),
|
||||||
@@ -92,8 +91,6 @@ StatusView::StatusView(CollectionViewContainer *collectionviewcontainer, QWidget
|
|||||||
menu_(new QMenu(this))
|
menu_(new QMenu(this))
|
||||||
{
|
{
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
collectionview_ = collectionviewcontainer->view();
|
collectionview_ = collectionviewcontainer->view();
|
||||||
connect(collectionview_, SIGNAL(TotalSongCountUpdated_()), this, SLOT(UpdateNoSong()));
|
connect(collectionview_, SIGNAL(TotalSongCountUpdated_()), this, SLOT(UpdateNoSong()));
|
||||||
connect(collectionview_, SIGNAL(TotalArtistCountUpdated_()), this, SLOT(UpdateNoSong()));
|
connect(collectionview_, SIGNAL(TotalArtistCountUpdated_()), this, SLOT(UpdateNoSong()));
|
||||||
@@ -125,8 +122,6 @@ StatusView::~StatusView() {
|
|||||||
|
|
||||||
void StatusView::AddActions() {
|
void StatusView::AddActions() {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
QList<QAction*> actions = album_cover_choice_controller_->GetAllActions();
|
QList<QAction*> actions = album_cover_choice_controller_->GetAllActions();
|
||||||
|
|
||||||
// Here we add the search automatically action, too!
|
// Here we add the search automatically action, too!
|
||||||
@@ -147,8 +142,6 @@ void StatusView::AddActions() {
|
|||||||
|
|
||||||
void StatusView::CreateWidget() {
|
void StatusView::CreateWidget() {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
setLayout(layout_);
|
setLayout(layout_);
|
||||||
|
|
||||||
layout_->setSizeConstraint(QLayout::SetMinAndMaxSize);
|
layout_->setSizeConstraint(QLayout::SetMinAndMaxSize);
|
||||||
@@ -174,8 +167,6 @@ void StatusView::CreateWidget() {
|
|||||||
|
|
||||||
void StatusView::SetApplication(Application *app) {
|
void StatusView::SetApplication(Application *app) {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
app_ = app;
|
app_ = app;
|
||||||
|
|
||||||
album_cover_choice_controller_->SetApplication(app_);
|
album_cover_choice_controller_->SetApplication(app_);
|
||||||
@@ -185,8 +176,6 @@ void StatusView::SetApplication(Application *app) {
|
|||||||
|
|
||||||
void StatusView::NoSongWidget() {
|
void StatusView::NoSongWidget() {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
if (widgetstate_ == Playing) {
|
if (widgetstate_ == Playing) {
|
||||||
container_layout_->removeWidget(widget_playing_);
|
container_layout_->removeWidget(widget_playing_);
|
||||||
widget_playing_->setVisible(false);
|
widget_playing_->setVisible(false);
|
||||||
@@ -221,8 +210,6 @@ void StatusView::NoSongWidget() {
|
|||||||
|
|
||||||
void StatusView::SongWidget() {
|
void StatusView::SongWidget() {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
if (widgetstate_ == Stopped) {
|
if (widgetstate_ == Stopped) {
|
||||||
container_layout_->removeWidget(widget_stopped_);
|
container_layout_->removeWidget(widget_stopped_);
|
||||||
widget_stopped_->setVisible(false);
|
widget_stopped_->setVisible(false);
|
||||||
@@ -275,8 +262,6 @@ void StatusView::SongWidget() {
|
|||||||
|
|
||||||
void StatusView::SwitchWidgets(WidgetState state) {
|
void StatusView::SwitchWidgets(WidgetState state) {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
if (widgetstate_ == None) NoSongWidget();
|
if (widgetstate_ == None) NoSongWidget();
|
||||||
|
|
||||||
if ((state == Stopped) && (widgetstate_ != Stopped)) {
|
if ((state == Stopped) && (widgetstate_ != Stopped)) {
|
||||||
@@ -291,8 +276,6 @@ void StatusView::SwitchWidgets(WidgetState state) {
|
|||||||
|
|
||||||
void StatusView::UpdateSong() {
|
void StatusView::UpdateSong() {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
SwitchWidgets(Playing);
|
SwitchWidgets(Playing);
|
||||||
|
|
||||||
const QueryOptions opt;
|
const QueryOptions opt;
|
||||||
@@ -342,8 +325,6 @@ void StatusView::UpdateSong() {
|
|||||||
|
|
||||||
void StatusView::NoSong() {
|
void StatusView::NoSong() {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
QString html;
|
QString html;
|
||||||
QImage image_logo(":/pictures/strawberry.png");
|
QImage image_logo(":/pictures/strawberry.png");
|
||||||
QImage image_logo_scaled = image_logo.scaled(300, 300, Qt::KeepAspectRatio);
|
QImage image_logo_scaled = image_logo.scaled(300, 300, Qt::KeepAspectRatio);
|
||||||
@@ -377,8 +358,6 @@ void StatusView::NoSong() {
|
|||||||
|
|
||||||
void StatusView::SongChanged(const Song &song) {
|
void StatusView::SongChanged(const Song &song) {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
stopped_ = false;
|
stopped_ = false;
|
||||||
metadata_ = song;
|
metadata_ = song;
|
||||||
|
|
||||||
@@ -390,8 +369,6 @@ void StatusView::SongChanged(const Song &song) {
|
|||||||
|
|
||||||
void StatusView::SongFinished() {
|
void StatusView::SongFinished() {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
stopped_ = true;
|
stopped_ = true;
|
||||||
SetImage(image_blank_);
|
SetImage(image_blank_);
|
||||||
|
|
||||||
@@ -399,8 +376,6 @@ void StatusView::SongFinished() {
|
|||||||
|
|
||||||
bool StatusView::eventFilter(QObject *object, QEvent *event) {
|
bool StatusView::eventFilter(QObject *object, QEvent *event) {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
switch(event->type()) {
|
switch(event->type()) {
|
||||||
case QEvent::Paint:{
|
case QEvent::Paint:{
|
||||||
handlePaintEvent(object, event);
|
handlePaintEvent(object, event);
|
||||||
@@ -416,8 +391,6 @@ bool StatusView::eventFilter(QObject *object, QEvent *event) {
|
|||||||
|
|
||||||
void StatusView::handlePaintEvent(QObject *object, QEvent *event) {
|
void StatusView::handlePaintEvent(QObject *object, QEvent *event) {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__ << object->objectName();
|
|
||||||
|
|
||||||
if (object == label_playing_album_) {
|
if (object == label_playing_album_) {
|
||||||
paintEvent_album(event);
|
paintEvent_album(event);
|
||||||
}
|
}
|
||||||
@@ -428,8 +401,6 @@ void StatusView::handlePaintEvent(QObject *object, QEvent *event) {
|
|||||||
|
|
||||||
void StatusView::paintEvent_album(QEvent *event) {
|
void StatusView::paintEvent_album(QEvent *event) {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
QPainter p(label_playing_album_);
|
QPainter p(label_playing_album_);
|
||||||
|
|
||||||
DrawImage(&p);
|
DrawImage(&p);
|
||||||
@@ -443,8 +414,6 @@ void StatusView::paintEvent_album(QEvent *event) {
|
|||||||
|
|
||||||
void StatusView::DrawImage(QPainter *p) {
|
void StatusView::DrawImage(QPainter *p) {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
p->drawPixmap(0, 0, 300, 300, pixmap_current_);
|
p->drawPixmap(0, 0, 300, 300, pixmap_current_);
|
||||||
if ((downloading_covers_) && (spinner_animation_ != nullptr)) {
|
if ((downloading_covers_) && (spinner_animation_ != nullptr)) {
|
||||||
p->drawPixmap(50, 50, 16, 16, spinner_animation_->currentPixmap());
|
p->drawPixmap(50, 50, 16, 16, spinner_animation_->currentPixmap());
|
||||||
@@ -454,8 +423,6 @@ void StatusView::DrawImage(QPainter *p) {
|
|||||||
|
|
||||||
void StatusView::FadePreviousTrack(qreal value) {
|
void StatusView::FadePreviousTrack(qreal value) {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
pixmap_previous_opacity_ = value;
|
pixmap_previous_opacity_ = value;
|
||||||
if (qFuzzyCompare(pixmap_previous_opacity_, qreal(0.0))) {
|
if (qFuzzyCompare(pixmap_previous_opacity_, qreal(0.0))) {
|
||||||
pixmap_previous_ = QPixmap();
|
pixmap_previous_ = QPixmap();
|
||||||
@@ -477,31 +444,22 @@ void StatusView::contextMenuEvent(QContextMenuEvent *e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void StatusView::mouseReleaseEvent(QMouseEvent *) {
|
void StatusView::mouseReleaseEvent(QMouseEvent *) {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void StatusView::dragEnterEvent(QDragEnterEvent *e) {
|
void StatusView::dragEnterEvent(QDragEnterEvent *e) {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
QWidget::dragEnterEvent(e);
|
QWidget::dragEnterEvent(e);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void StatusView::dropEvent(QDropEvent *e) {
|
void StatusView::dropEvent(QDropEvent *e) {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
QWidget::dropEvent(e);
|
QWidget::dropEvent(e);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void StatusView::ScaleCover() {
|
void StatusView::ScaleCover() {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
pixmap_current_ = QPixmap::fromImage(AlbumCoverLoader::ScaleAndPad(cover_loader_options_, original_));
|
pixmap_current_ = QPixmap::fromImage(AlbumCoverLoader::ScaleAndPad(cover_loader_options_, original_));
|
||||||
update();
|
update();
|
||||||
|
|
||||||
@@ -509,8 +467,6 @@ void StatusView::ScaleCover() {
|
|||||||
|
|
||||||
void StatusView::AlbumArtLoaded(const Song &metadata, const QString&, const QImage &image) {
|
void StatusView::AlbumArtLoaded(const Song &metadata, const QString&, const QImage &image) {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
SwitchWidgets(Playing);
|
SwitchWidgets(Playing);
|
||||||
|
|
||||||
label_playing_album_->clear();
|
label_playing_album_->clear();
|
||||||
@@ -527,8 +483,6 @@ void StatusView::AlbumArtLoaded(const Song &metadata, const QString&, const QIma
|
|||||||
|
|
||||||
void StatusView::SetImage(const QImage &image) {
|
void StatusView::SetImage(const QImage &image) {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
// Cache the current pixmap so we can fade between them
|
// Cache the current pixmap so we can fade between them
|
||||||
pixmap_previous_ = QPixmap(size());
|
pixmap_previous_ = QPixmap(size());
|
||||||
pixmap_previous_.fill(palette().background().color());
|
pixmap_previous_.fill(palette().background().color());
|
||||||
@@ -543,7 +497,7 @@ void StatusView::SetImage(const QImage &image) {
|
|||||||
ScaleCover();
|
ScaleCover();
|
||||||
|
|
||||||
// Were we waiting for this cover to load before we started fading?
|
// 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();
|
fade_animation_->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,8 +505,6 @@ void StatusView::SetImage(const QImage &image) {
|
|||||||
|
|
||||||
bool StatusView::GetCoverAutomatically() {
|
bool StatusView::GetCoverAutomatically() {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
SwitchWidgets(Playing);
|
SwitchWidgets(Playing);
|
||||||
|
|
||||||
// Search for cover automatically?
|
// Search for cover automatically?
|
||||||
@@ -581,8 +533,6 @@ bool StatusView::GetCoverAutomatically() {
|
|||||||
|
|
||||||
void StatusView::AutomaticCoverSearchDone() {
|
void StatusView::AutomaticCoverSearchDone() {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
downloading_covers_ = false;
|
downloading_covers_ = false;
|
||||||
spinner_animation_.reset();
|
spinner_animation_.reset();
|
||||||
update();
|
update();
|
||||||
@@ -591,8 +541,6 @@ void StatusView::AutomaticCoverSearchDone() {
|
|||||||
|
|
||||||
void StatusView::UpdateNoSong() {
|
void StatusView::UpdateNoSong() {
|
||||||
|
|
||||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
|
||||||
|
|
||||||
if (widgetstate_ == Playing) return;
|
if (widgetstate_ == Playing) return;
|
||||||
|
|
||||||
NoSong();
|
NoSong();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef STATUSVIEW_H
|
#ifndef STATUSVIEW_H
|
||||||
@@ -131,7 +131,6 @@ private:
|
|||||||
int small_ideal_height_;
|
int small_ideal_height_;
|
||||||
int total_height_;
|
int total_height_;
|
||||||
bool fit_width_;
|
bool fit_width_;
|
||||||
QTimeLine *show_hide_animation_;
|
|
||||||
QTimeLine *fade_animation_;
|
QTimeLine *fade_animation_;
|
||||||
QImage image_blank_;
|
QImage image_blank_;
|
||||||
QImage image_nosong_;
|
QImage image_nosong_;
|
||||||
@@ -175,3 +174,4 @@ protected:
|
|||||||
};
|
};
|
||||||
|
|
||||||
#endif // STATUSVIEW_H
|
#endif // STATUSVIEW_H
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user