@ -367,6 +367,7 @@ option(INSTALL_TRANSLATIONS "Install translations" OFF)
|
||||
|
||||
optional_component(SUBSONIC ON "Subsonic support")
|
||||
optional_component(TIDAL ON "Tidal support")
|
||||
optional_component(QOBUZ ON "Qobuz support")
|
||||
|
||||
optional_component(MOODBAR ON "Moodbar"
|
||||
DEPENDS "fftw3" FFTW3_FOUND
|
||||
|
@ -13,8 +13,10 @@
|
||||
<file>schema/schema-10.sql</file>
|
||||
<file>schema/schema-11.sql</file>
|
||||
<file>schema/schema-12.sql</file>
|
||||
<file>schema/schema-13.sql</file>
|
||||
<file>schema/device-schema.sql</file>
|
||||
<file>style/strawberry.css</file>
|
||||
<file>style/smartplaylistsearchterm.css</file>
|
||||
<file>html/playing-tooltip.html</file>
|
||||
<file>html/oauthsuccess.html</file>
|
||||
<file>pictures/strawberry.png</file>
|
||||
@ -40,6 +42,8 @@
|
||||
<file>pictures/osd_shadow_edge.png</file>
|
||||
<file>pictures/nyancat.png</file>
|
||||
<file>pictures/rainbowdash.png</file>
|
||||
<file>pictures/star-on.png</file>
|
||||
<file>pictures/star-off.png</file>
|
||||
<file>fonts/HumongousofEternitySt.ttf</file>
|
||||
<file>mood/sample.mood</file>
|
||||
<file>text/ghosts.txt</file>
|
||||
|
@ -89,6 +89,7 @@
|
||||
<file>icons/128x128/love.png</file>
|
||||
<file>icons/128x128/subsonic.png</file>
|
||||
<file>icons/128x128/tidal.png</file>
|
||||
<file>icons/128x128/qobuz.png</file>
|
||||
<file>icons/128x128/multimedia-player-ipod-standard-black.png</file>
|
||||
<file>icons/64x64/albums.png</file>
|
||||
<file>icons/64x64/alsa.png</file>
|
||||
@ -180,6 +181,7 @@
|
||||
<file>icons/64x64/love.png</file>
|
||||
<file>icons/64x64/subsonic.png</file>
|
||||
<file>icons/64x64/tidal.png</file>
|
||||
<file>icons/64x64/qobuz.png</file>
|
||||
<file>icons/64x64/multimedia-player-ipod-standard-black.png</file>
|
||||
<file>icons/48x48/albums.png</file>
|
||||
<file>icons/48x48/alsa.png</file>
|
||||
@ -275,6 +277,7 @@
|
||||
<file>icons/48x48/love.png</file>
|
||||
<file>icons/48x48/subsonic.png</file>
|
||||
<file>icons/48x48/tidal.png</file>
|
||||
<file>icons/48x48/qobuz.png</file>
|
||||
<file>icons/48x48/multimedia-player-ipod-standard-black.png</file>
|
||||
<file>icons/32x32/albums.png</file>
|
||||
<file>icons/32x32/alsa.png</file>
|
||||
@ -370,6 +373,7 @@
|
||||
<file>icons/32x32/love.png</file>
|
||||
<file>icons/32x32/subsonic.png</file>
|
||||
<file>icons/32x32/tidal.png</file>
|
||||
<file>icons/32x32/qobuz.png</file>
|
||||
<file>icons/32x32/multimedia-player-ipod-standard-black.png</file>
|
||||
<file>icons/22x22/albums.png</file>
|
||||
<file>icons/22x22/alsa.png</file>
|
||||
@ -465,6 +469,7 @@
|
||||
<file>icons/22x22/love.png</file>
|
||||
<file>icons/22x22/subsonic.png</file>
|
||||
<file>icons/22x22/tidal.png</file>
|
||||
<file>icons/22x22/qobuz.png</file>
|
||||
<file>icons/22x22/multimedia-player-ipod-standard-black.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
BIN
data/icons/128x128/qobuz.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
data/icons/22x22/qobuz.png
Normal file
After Width: | Height: | Size: 964 B |
BIN
data/icons/32x32/qobuz.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
data/icons/48x48/qobuz.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
data/icons/64x64/qobuz.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
data/icons/full/qobuz.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
data/pictures/star-off.png
Normal file
After Width: | Height: | Size: 351 B |
BIN
data/pictures/star-on.png
Normal file
After Width: | Height: | Size: 284 B |
@ -62,7 +62,9 @@ CREATE TABLE device_%deviceid_songs (
|
||||
effective_albumartist TEXT,
|
||||
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
cue_path TEXT
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
|
||||
);
|
||||
|
||||
@ -75,4 +77,4 @@ CREATE VIRTUAL TABLE device_%deviceid_fts USING fts5(
|
||||
tokenize = "unicode61 remove_diacritics 1"
|
||||
);
|
||||
|
||||
UPDATE devices SET schema_version=1 WHERE ROWID=%deviceid;
|
||||
UPDATE devices SET schema_version=2 WHERE ROWID=%deviceid;
|
||||
|
231
data/schema/schema-13.sql
Normal file
@ -0,0 +1,231 @@
|
||||
ALTER TABLE %allsongstables ADD COLUMN rating INTEGER DEFAULT -1;
|
||||
|
||||
ALTER TABLE playlists ADD COLUMN dynamic_playlist_type INTEGER;
|
||||
|
||||
ALTER TABLE playlists ADD COLUMN dynamic_playlist_backend TEXT;
|
||||
|
||||
ALTER TABLE playlists ADD COLUMN dynamic_playlist_data BLOB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
||||
|
||||
title TEXT,
|
||||
album TEXT,
|
||||
artist TEXT,
|
||||
albumartist TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
originalyear INTEGER NOT NULL DEFAULT -1,
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
performer TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
|
||||
artist_id TEXT,
|
||||
album_id TEXT,
|
||||
song_id TEXT,
|
||||
|
||||
beginning INTEGER NOT NULL DEFAULT 0,
|
||||
length INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
bitrate INTEGER NOT NULL DEFAULT -1,
|
||||
samplerate INTEGER NOT NULL DEFAULT -1,
|
||||
bitdepth INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
source INTEGER NOT NULL DEFAULT 0,
|
||||
directory_id INTEGER NOT NULL DEFAULT -1,
|
||||
url TEXT NOT NULL,
|
||||
filetype INTEGER NOT NULL DEFAULT 0,
|
||||
filesize INTEGER NOT NULL DEFAULT -1,
|
||||
mtime INTEGER NOT NULL DEFAULT -1,
|
||||
ctime INTEGER NOT NULL DEFAULT -1,
|
||||
unavailable INTEGER DEFAULT 0,
|
||||
|
||||
playcount INTEGER NOT NULL DEFAULT 0,
|
||||
skipcount INTEGER NOT NULL DEFAULT 0,
|
||||
lastplayed INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
compilation_detected INTEGER DEFAULT 0,
|
||||
compilation_on INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_off INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_effective INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
art_automatic TEXT,
|
||||
art_manual TEXT,
|
||||
|
||||
effective_albumartist TEXT,
|
||||
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
||||
|
||||
title TEXT,
|
||||
album TEXT,
|
||||
artist TEXT,
|
||||
albumartist TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
originalyear INTEGER NOT NULL DEFAULT -1,
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
performer TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
|
||||
artist_id TEXT,
|
||||
album_id TEXT,
|
||||
song_id TEXT,
|
||||
|
||||
beginning INTEGER NOT NULL DEFAULT 0,
|
||||
length INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
bitrate INTEGER NOT NULL DEFAULT -1,
|
||||
samplerate INTEGER NOT NULL DEFAULT -1,
|
||||
bitdepth INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
source INTEGER NOT NULL DEFAULT 0,
|
||||
directory_id INTEGER NOT NULL DEFAULT -1,
|
||||
url TEXT NOT NULL,
|
||||
filetype INTEGER NOT NULL DEFAULT 0,
|
||||
filesize INTEGER NOT NULL DEFAULT -1,
|
||||
mtime INTEGER NOT NULL DEFAULT -1,
|
||||
ctime INTEGER NOT NULL DEFAULT -1,
|
||||
unavailable INTEGER DEFAULT 0,
|
||||
|
||||
playcount INTEGER NOT NULL DEFAULT 0,
|
||||
skipcount INTEGER NOT NULL DEFAULT 0,
|
||||
lastplayed INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
compilation_detected INTEGER DEFAULT 0,
|
||||
compilation_on INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_off INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_effective INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
art_automatic TEXT,
|
||||
art_manual TEXT,
|
||||
|
||||
effective_albumartist TEXT,
|
||||
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qobuz_songs (
|
||||
|
||||
title TEXT,
|
||||
album TEXT,
|
||||
artist TEXT,
|
||||
albumartist TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
originalyear INTEGER NOT NULL DEFAULT -1,
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
performer TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
|
||||
artist_id TEXT,
|
||||
album_id TEXT,
|
||||
song_id TEXT,
|
||||
|
||||
beginning INTEGER NOT NULL DEFAULT 0,
|
||||
length INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
bitrate INTEGER NOT NULL DEFAULT -1,
|
||||
samplerate INTEGER NOT NULL DEFAULT -1,
|
||||
bitdepth INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
source INTEGER NOT NULL DEFAULT 0,
|
||||
directory_id INTEGER NOT NULL DEFAULT -1,
|
||||
url TEXT NOT NULL,
|
||||
filetype INTEGER NOT NULL DEFAULT 0,
|
||||
filesize INTEGER NOT NULL DEFAULT -1,
|
||||
mtime INTEGER NOT NULL DEFAULT -1,
|
||||
ctime INTEGER NOT NULL DEFAULT -1,
|
||||
unavailable INTEGER DEFAULT 0,
|
||||
|
||||
playcount INTEGER NOT NULL DEFAULT 0,
|
||||
skipcount INTEGER NOT NULL DEFAULT 0,
|
||||
lastplayed INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
compilation_detected INTEGER DEFAULT 0,
|
||||
compilation_on INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_off INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_effective INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
art_automatic TEXT,
|
||||
art_manual TEXT,
|
||||
|
||||
effective_albumartist TEXT,
|
||||
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_artists_songs_fts USING fts5(
|
||||
|
||||
ftstitle,
|
||||
ftsalbum,
|
||||
ftsartist,
|
||||
ftsalbumartist,
|
||||
ftscomposer,
|
||||
ftsperformer,
|
||||
ftsgrouping,
|
||||
ftsgenre,
|
||||
ftscomment,
|
||||
tokenize = "unicode61 remove_diacritics 1"
|
||||
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_albums_songs_fts USING fts5(
|
||||
|
||||
ftstitle,
|
||||
ftsalbum,
|
||||
ftsartist,
|
||||
ftsalbumartist,
|
||||
ftscomposer,
|
||||
ftsperformer,
|
||||
ftsgrouping,
|
||||
ftsgenre,
|
||||
ftscomment,
|
||||
tokenize = "unicode61 remove_diacritics 1"
|
||||
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_songs_fts USING fts5(
|
||||
|
||||
ftstitle,
|
||||
ftsalbum,
|
||||
ftsartist,
|
||||
ftsalbumartist,
|
||||
ftscomposer,
|
||||
ftsperformer,
|
||||
ftsgrouping,
|
||||
ftsgenre,
|
||||
ftscomment,
|
||||
tokenize = "unicode61 remove_diacritics 1"
|
||||
|
||||
);
|
||||
|
||||
UPDATE schema_version SET version=13;
|
@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
|
||||
|
||||
DELETE FROM schema_version;
|
||||
|
||||
INSERT INTO schema_version (version) VALUES (12);
|
||||
INSERT INTO schema_version (version) VALUES (13);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS directories (
|
||||
path TEXT NOT NULL,
|
||||
@ -70,178 +70,9 @@ CREATE TABLE IF NOT EXISTS songs (
|
||||
effective_albumartist TEXT,
|
||||
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
cue_path TEXT
|
||||
cue_path TEXT,
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
||||
|
||||
title TEXT,
|
||||
album TEXT,
|
||||
artist TEXT,
|
||||
albumartist TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
originalyear INTEGER NOT NULL DEFAULT -1,
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
performer TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
|
||||
artist_id TEXT,
|
||||
album_id TEXT,
|
||||
song_id TEXT,
|
||||
|
||||
beginning INTEGER NOT NULL DEFAULT 0,
|
||||
length INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
bitrate INTEGER NOT NULL DEFAULT -1,
|
||||
samplerate INTEGER NOT NULL DEFAULT -1,
|
||||
bitdepth INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
source INTEGER NOT NULL DEFAULT 0,
|
||||
directory_id INTEGER NOT NULL DEFAULT -1,
|
||||
url TEXT NOT NULL,
|
||||
filetype INTEGER NOT NULL DEFAULT 0,
|
||||
filesize INTEGER NOT NULL DEFAULT -1,
|
||||
mtime INTEGER NOT NULL DEFAULT -1,
|
||||
ctime INTEGER NOT NULL DEFAULT -1,
|
||||
unavailable INTEGER DEFAULT 0,
|
||||
|
||||
playcount INTEGER NOT NULL DEFAULT 0,
|
||||
skipcount INTEGER NOT NULL DEFAULT 0,
|
||||
lastplayed INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
compilation_detected INTEGER DEFAULT 0,
|
||||
compilation_on INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_off INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_effective INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
art_automatic TEXT,
|
||||
art_manual TEXT,
|
||||
|
||||
effective_albumartist TEXT,
|
||||
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
cue_path TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
||||
|
||||
title TEXT,
|
||||
album TEXT,
|
||||
artist TEXT,
|
||||
albumartist TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
originalyear INTEGER NOT NULL DEFAULT -1,
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
performer TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
|
||||
artist_id TEXT,
|
||||
album_id TEXT,
|
||||
song_id TEXT,
|
||||
|
||||
beginning INTEGER NOT NULL DEFAULT 0,
|
||||
length INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
bitrate INTEGER NOT NULL DEFAULT -1,
|
||||
samplerate INTEGER NOT NULL DEFAULT -1,
|
||||
bitdepth INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
source INTEGER NOT NULL DEFAULT 0,
|
||||
directory_id INTEGER NOT NULL DEFAULT -1,
|
||||
url TEXT NOT NULL,
|
||||
filetype INTEGER NOT NULL DEFAULT 0,
|
||||
filesize INTEGER NOT NULL DEFAULT -1,
|
||||
mtime INTEGER NOT NULL DEFAULT -1,
|
||||
ctime INTEGER NOT NULL DEFAULT -1,
|
||||
unavailable INTEGER DEFAULT 0,
|
||||
|
||||
playcount INTEGER NOT NULL DEFAULT 0,
|
||||
skipcount INTEGER NOT NULL DEFAULT 0,
|
||||
lastplayed INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
compilation_detected INTEGER DEFAULT 0,
|
||||
compilation_on INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_off INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_effective INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
art_automatic TEXT,
|
||||
art_manual TEXT,
|
||||
|
||||
effective_albumartist TEXT,
|
||||
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
cue_path TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tidal_songs (
|
||||
|
||||
title TEXT,
|
||||
album TEXT,
|
||||
artist TEXT,
|
||||
albumartist TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
originalyear INTEGER NOT NULL DEFAULT -1,
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
performer TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
|
||||
artist_id TEXT,
|
||||
album_id TEXT,
|
||||
song_id TEXT,
|
||||
|
||||
beginning INTEGER NOT NULL DEFAULT 0,
|
||||
length INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
bitrate INTEGER NOT NULL DEFAULT -1,
|
||||
samplerate INTEGER NOT NULL DEFAULT -1,
|
||||
bitdepth INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
source INTEGER NOT NULL DEFAULT 0,
|
||||
directory_id INTEGER NOT NULL DEFAULT -1,
|
||||
url TEXT NOT NULL,
|
||||
filetype INTEGER NOT NULL DEFAULT 0,
|
||||
filesize INTEGER NOT NULL DEFAULT -1,
|
||||
mtime INTEGER NOT NULL DEFAULT -1,
|
||||
ctime INTEGER NOT NULL DEFAULT -1,
|
||||
unavailable INTEGER DEFAULT 0,
|
||||
|
||||
playcount INTEGER NOT NULL DEFAULT 0,
|
||||
skipcount INTEGER NOT NULL DEFAULT 0,
|
||||
lastplayed INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
compilation_detected INTEGER DEFAULT 0,
|
||||
compilation_on INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_off INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_effective INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
art_automatic TEXT,
|
||||
art_manual TEXT,
|
||||
|
||||
effective_albumartist TEXT,
|
||||
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
cue_path TEXT
|
||||
rating INTEGER DEFAULT -1
|
||||
|
||||
);
|
||||
|
||||
@ -298,7 +129,363 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
|
||||
effective_albumartist TEXT,
|
||||
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
cue_path TEXT
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
||||
|
||||
title TEXT,
|
||||
album TEXT,
|
||||
artist TEXT,
|
||||
albumartist TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
originalyear INTEGER NOT NULL DEFAULT -1,
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
performer TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
|
||||
artist_id TEXT,
|
||||
album_id TEXT,
|
||||
song_id TEXT,
|
||||
|
||||
beginning INTEGER NOT NULL DEFAULT 0,
|
||||
length INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
bitrate INTEGER NOT NULL DEFAULT -1,
|
||||
samplerate INTEGER NOT NULL DEFAULT -1,
|
||||
bitdepth INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
source INTEGER NOT NULL DEFAULT 0,
|
||||
directory_id INTEGER NOT NULL DEFAULT -1,
|
||||
url TEXT NOT NULL,
|
||||
filetype INTEGER NOT NULL DEFAULT 0,
|
||||
filesize INTEGER NOT NULL DEFAULT -1,
|
||||
mtime INTEGER NOT NULL DEFAULT -1,
|
||||
ctime INTEGER NOT NULL DEFAULT -1,
|
||||
unavailable INTEGER DEFAULT 0,
|
||||
|
||||
playcount INTEGER NOT NULL DEFAULT 0,
|
||||
skipcount INTEGER NOT NULL DEFAULT 0,
|
||||
lastplayed INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
compilation_detected INTEGER DEFAULT 0,
|
||||
compilation_on INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_off INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_effective INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
art_automatic TEXT,
|
||||
art_manual TEXT,
|
||||
|
||||
effective_albumartist TEXT,
|
||||
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
||||
|
||||
title TEXT,
|
||||
album TEXT,
|
||||
artist TEXT,
|
||||
albumartist TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
originalyear INTEGER NOT NULL DEFAULT -1,
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
performer TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
|
||||
artist_id TEXT,
|
||||
album_id TEXT,
|
||||
song_id TEXT,
|
||||
|
||||
beginning INTEGER NOT NULL DEFAULT 0,
|
||||
length INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
bitrate INTEGER NOT NULL DEFAULT -1,
|
||||
samplerate INTEGER NOT NULL DEFAULT -1,
|
||||
bitdepth INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
source INTEGER NOT NULL DEFAULT 0,
|
||||
directory_id INTEGER NOT NULL DEFAULT -1,
|
||||
url TEXT NOT NULL,
|
||||
filetype INTEGER NOT NULL DEFAULT 0,
|
||||
filesize INTEGER NOT NULL DEFAULT -1,
|
||||
mtime INTEGER NOT NULL DEFAULT -1,
|
||||
ctime INTEGER NOT NULL DEFAULT -1,
|
||||
unavailable INTEGER DEFAULT 0,
|
||||
|
||||
playcount INTEGER NOT NULL DEFAULT 0,
|
||||
skipcount INTEGER NOT NULL DEFAULT 0,
|
||||
lastplayed INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
compilation_detected INTEGER DEFAULT 0,
|
||||
compilation_on INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_off INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_effective INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
art_automatic TEXT,
|
||||
art_manual TEXT,
|
||||
|
||||
effective_albumartist TEXT,
|
||||
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tidal_songs (
|
||||
|
||||
title TEXT,
|
||||
album TEXT,
|
||||
artist TEXT,
|
||||
albumartist TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
originalyear INTEGER NOT NULL DEFAULT -1,
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
performer TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
|
||||
artist_id TEXT,
|
||||
album_id TEXT,
|
||||
song_id TEXT,
|
||||
|
||||
beginning INTEGER NOT NULL DEFAULT 0,
|
||||
length INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
bitrate INTEGER NOT NULL DEFAULT -1,
|
||||
samplerate INTEGER NOT NULL DEFAULT -1,
|
||||
bitdepth INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
source INTEGER NOT NULL DEFAULT 0,
|
||||
directory_id INTEGER NOT NULL DEFAULT -1,
|
||||
url TEXT NOT NULL,
|
||||
filetype INTEGER NOT NULL DEFAULT 0,
|
||||
filesize INTEGER NOT NULL DEFAULT -1,
|
||||
mtime INTEGER NOT NULL DEFAULT -1,
|
||||
ctime INTEGER NOT NULL DEFAULT -1,
|
||||
unavailable INTEGER DEFAULT 0,
|
||||
|
||||
playcount INTEGER NOT NULL DEFAULT 0,
|
||||
skipcount INTEGER NOT NULL DEFAULT 0,
|
||||
lastplayed INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
compilation_detected INTEGER DEFAULT 0,
|
||||
compilation_on INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_off INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_effective INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
art_automatic TEXT,
|
||||
art_manual TEXT,
|
||||
|
||||
effective_albumartist TEXT,
|
||||
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
||||
|
||||
title TEXT,
|
||||
album TEXT,
|
||||
artist TEXT,
|
||||
albumartist TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
originalyear INTEGER NOT NULL DEFAULT -1,
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
performer TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
|
||||
artist_id TEXT,
|
||||
album_id TEXT,
|
||||
song_id TEXT,
|
||||
|
||||
beginning INTEGER NOT NULL DEFAULT 0,
|
||||
length INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
bitrate INTEGER NOT NULL DEFAULT -1,
|
||||
samplerate INTEGER NOT NULL DEFAULT -1,
|
||||
bitdepth INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
source INTEGER NOT NULL DEFAULT 0,
|
||||
directory_id INTEGER NOT NULL DEFAULT -1,
|
||||
url TEXT NOT NULL,
|
||||
filetype INTEGER NOT NULL DEFAULT 0,
|
||||
filesize INTEGER NOT NULL DEFAULT -1,
|
||||
mtime INTEGER NOT NULL DEFAULT -1,
|
||||
ctime INTEGER NOT NULL DEFAULT -1,
|
||||
unavailable INTEGER DEFAULT 0,
|
||||
|
||||
playcount INTEGER NOT NULL DEFAULT 0,
|
||||
skipcount INTEGER NOT NULL DEFAULT 0,
|
||||
lastplayed INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
compilation_detected INTEGER DEFAULT 0,
|
||||
compilation_on INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_off INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_effective INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
art_automatic TEXT,
|
||||
art_manual TEXT,
|
||||
|
||||
effective_albumartist TEXT,
|
||||
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
||||
|
||||
title TEXT,
|
||||
album TEXT,
|
||||
artist TEXT,
|
||||
albumartist TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
originalyear INTEGER NOT NULL DEFAULT -1,
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
performer TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
|
||||
artist_id TEXT,
|
||||
album_id TEXT,
|
||||
song_id TEXT,
|
||||
|
||||
beginning INTEGER NOT NULL DEFAULT 0,
|
||||
length INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
bitrate INTEGER NOT NULL DEFAULT -1,
|
||||
samplerate INTEGER NOT NULL DEFAULT -1,
|
||||
bitdepth INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
source INTEGER NOT NULL DEFAULT 0,
|
||||
directory_id INTEGER NOT NULL DEFAULT -1,
|
||||
url TEXT NOT NULL,
|
||||
filetype INTEGER NOT NULL DEFAULT 0,
|
||||
filesize INTEGER NOT NULL DEFAULT -1,
|
||||
mtime INTEGER NOT NULL DEFAULT -1,
|
||||
ctime INTEGER NOT NULL DEFAULT -1,
|
||||
unavailable INTEGER DEFAULT 0,
|
||||
|
||||
playcount INTEGER NOT NULL DEFAULT 0,
|
||||
skipcount INTEGER NOT NULL DEFAULT 0,
|
||||
lastplayed INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
compilation_detected INTEGER DEFAULT 0,
|
||||
compilation_on INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_off INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_effective INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
art_automatic TEXT,
|
||||
art_manual TEXT,
|
||||
|
||||
effective_albumartist TEXT,
|
||||
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qobuz_songs (
|
||||
|
||||
title TEXT,
|
||||
album TEXT,
|
||||
artist TEXT,
|
||||
albumartist TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
originalyear INTEGER NOT NULL DEFAULT -1,
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
performer TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
|
||||
artist_id TEXT,
|
||||
album_id TEXT,
|
||||
song_id TEXT,
|
||||
|
||||
beginning INTEGER NOT NULL DEFAULT 0,
|
||||
length INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
bitrate INTEGER NOT NULL DEFAULT -1,
|
||||
samplerate INTEGER NOT NULL DEFAULT -1,
|
||||
bitdepth INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
source INTEGER NOT NULL DEFAULT 0,
|
||||
directory_id INTEGER NOT NULL DEFAULT -1,
|
||||
url TEXT NOT NULL,
|
||||
filetype INTEGER NOT NULL DEFAULT 0,
|
||||
filesize INTEGER NOT NULL DEFAULT -1,
|
||||
mtime INTEGER NOT NULL DEFAULT -1,
|
||||
ctime INTEGER NOT NULL DEFAULT -1,
|
||||
unavailable INTEGER DEFAULT 0,
|
||||
|
||||
playcount INTEGER NOT NULL DEFAULT 0,
|
||||
skipcount INTEGER NOT NULL DEFAULT 0,
|
||||
lastplayed INTEGER NOT NULL DEFAULT -1,
|
||||
|
||||
compilation_detected INTEGER DEFAULT 0,
|
||||
compilation_on INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_off INTEGER NOT NULL DEFAULT 0,
|
||||
compilation_effective INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
art_automatic TEXT,
|
||||
art_manual TEXT,
|
||||
|
||||
effective_albumartist TEXT,
|
||||
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
|
||||
);
|
||||
|
||||
@ -309,7 +496,11 @@ CREATE TABLE IF NOT EXISTS playlists (
|
||||
ui_order INTEGER NOT NULL DEFAULT 0,
|
||||
special_type TEXT,
|
||||
ui_path TEXT,
|
||||
is_favorite INTEGER NOT NULL DEFAULT 0
|
||||
is_favorite INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
dynamic_playlist_type INTEGER,
|
||||
dynamic_playlist_backend TEXT,
|
||||
dynamic_playlist_data BLOB
|
||||
|
||||
);
|
||||
|
||||
@ -371,7 +562,9 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
||||
effective_albumartist TEXT,
|
||||
effective_originalyear INTEGER,
|
||||
|
||||
cue_path TEXT
|
||||
cue_path TEXT,
|
||||
|
||||
rating INTEGER DEFAULT -1
|
||||
|
||||
);
|
||||
|
||||
@ -414,6 +607,21 @@ CREATE VIRTUAL TABLE IF NOT EXISTS songs_fts USING fts5(
|
||||
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS subsonic_songs_fts USING fts5(
|
||||
|
||||
ftstitle,
|
||||
ftsalbum,
|
||||
ftsartist,
|
||||
ftsalbumartist,
|
||||
ftscomposer,
|
||||
ftsperformer,
|
||||
ftsgrouping,
|
||||
ftsgenre,
|
||||
ftscomment,
|
||||
tokenize = "unicode61 remove_diacritics 1"
|
||||
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS tidal_artists_songs_fts USING fts5(
|
||||
|
||||
ftstitle,
|
||||
@ -459,7 +667,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS tidal_songs_fts USING fts5(
|
||||
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS subsonic_songs_fts USING fts5(
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_artists_songs_fts USING fts5(
|
||||
|
||||
ftstitle,
|
||||
ftsalbum,
|
||||
@ -474,7 +682,22 @@ CREATE VIRTUAL TABLE IF NOT EXISTS subsonic_songs_fts USING fts5(
|
||||
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS playlist_items_fts_ USING fts5(
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_albums_songs_fts USING fts5(
|
||||
|
||||
ftstitle,
|
||||
ftsalbum,
|
||||
ftsartist,
|
||||
ftsalbumartist,
|
||||
ftscomposer,
|
||||
ftsperformer,
|
||||
ftsgrouping,
|
||||
ftsgenre,
|
||||
ftscomment,
|
||||
tokenize = "unicode61 remove_diacritics 1"
|
||||
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_songs_fts USING fts5(
|
||||
|
||||
ftstitle,
|
||||
ftsalbum,
|
||||
|
43
data/style/smartplaylistsearchterm.css
Normal file
@ -0,0 +1,43 @@
|
||||
#frame {
|
||||
border: 1px solid palette(mid);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#container {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
#remove {
|
||||
border-top-left-radius: 0px;
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 5px;
|
||||
border: 0px solid transparent;
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 %light,
|
||||
stop:0.4 %light,
|
||||
stop:0.6 %dark,
|
||||
stop:1 %dark);
|
||||
|
||||
margin-left: 5px;
|
||||
padding: 0px 5px;
|
||||
}
|
||||
|
||||
#remove:hover {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 %light2,
|
||||
stop:0.4 %light2,
|
||||
stop:0.6 %base,
|
||||
stop:1 %base);
|
||||
border: 0px solid transparent;
|
||||
}
|
||||
|
||||
#remove:pressed {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 %base,
|
||||
stop:0.4 %base,
|
||||
stop:0.6 %light2,
|
||||
stop:1 %light2);
|
||||
border: 0px solid transparent;
|
||||
}
|
||||
|
@ -96,6 +96,7 @@ set(SOURCES
|
||||
playlist/playlistview.cpp
|
||||
playlist/songloaderinserter.cpp
|
||||
playlist/songplaylistitem.cpp
|
||||
playlist/dynamicplaylistcontrols.cpp
|
||||
|
||||
queue/queue.cpp
|
||||
queue/queueview.cpp
|
||||
@ -111,6 +112,20 @@ set(SOURCES
|
||||
playlistparsers/xmlparser.cpp
|
||||
playlistparsers/xspfparser.cpp
|
||||
|
||||
smartplaylists/playlistgenerator.cpp
|
||||
smartplaylists/playlistgeneratorinserter.cpp
|
||||
smartplaylists/playlistquerygenerator.cpp
|
||||
smartplaylists/smartplaylistquerywizardplugin.cpp
|
||||
smartplaylists/smartplaylistsearch.cpp
|
||||
smartplaylists/smartplaylistsearchpreview.cpp
|
||||
smartplaylists/smartplaylistsearchterm.cpp
|
||||
smartplaylists/smartplaylistsearchtermwidget.cpp
|
||||
smartplaylists/smartplaylistsmodel.cpp
|
||||
smartplaylists/smartplaylistsviewcontainer.cpp
|
||||
smartplaylists/smartplaylistsview.cpp
|
||||
smartplaylists/smartplaylistwizard.cpp
|
||||
smartplaylists/smartplaylistwizardplugin.cpp
|
||||
|
||||
covermanager/albumcovermanager.cpp
|
||||
covermanager/albumcovermanagerlist.cpp
|
||||
covermanager/albumcoverloader.cpp
|
||||
@ -195,6 +210,7 @@ set(SOURCES
|
||||
widgets/tracksliderpopup.cpp
|
||||
widgets/tracksliderslider.cpp
|
||||
widgets/loginstatewidget.cpp
|
||||
widgets/ratingwidget.cpp
|
||||
|
||||
osd/osdbase.cpp
|
||||
osd/osdpretty.cpp
|
||||
@ -301,6 +317,7 @@ set(HEADERS
|
||||
playlist/playlistitemmimedata.h
|
||||
playlist/songloaderinserter.h
|
||||
playlist/songmimedata.h
|
||||
playlist/dynamicplaylistcontrols.h
|
||||
|
||||
queue/queue.h
|
||||
queue/queueview.h
|
||||
@ -314,6 +331,18 @@ set(HEADERS
|
||||
playlistparsers/plsparser.h
|
||||
playlistparsers/xspfparser.h
|
||||
|
||||
smartplaylists/playlistgenerator.h
|
||||
smartplaylists/playlistgeneratorinserter.h
|
||||
smartplaylists/playlistgeneratormimedata.h
|
||||
smartplaylists/smartplaylistquerywizardplugin.h
|
||||
smartplaylists/smartplaylistsearchpreview.h
|
||||
smartplaylists/smartplaylistsearchtermwidget.h
|
||||
smartplaylists/smartplaylistsmodel.h
|
||||
smartplaylists/smartplaylistsviewcontainer.h
|
||||
smartplaylists/smartplaylistsview.h
|
||||
smartplaylists/smartplaylistwizard.h
|
||||
smartplaylists/smartplaylistwizardplugin.h
|
||||
|
||||
covermanager/albumcovermanager.h
|
||||
covermanager/albumcovermanagerlist.h
|
||||
covermanager/albumcoverloader.h
|
||||
@ -397,6 +426,7 @@ set(HEADERS
|
||||
widgets/tracksliderslider.h
|
||||
widgets/loginstatewidget.h
|
||||
widgets/qsearchfield.h
|
||||
widgets/ratingwidget.h
|
||||
|
||||
osd/osdbase.h
|
||||
osd/osdpretty.h
|
||||
@ -446,9 +476,17 @@ set(UI
|
||||
playlist/playlistlistcontainer.ui
|
||||
playlist/playlistsaveoptionsdialog.ui
|
||||
playlist/playlistsequence.ui
|
||||
playlist/dynamicplaylistcontrols.ui
|
||||
|
||||
queue/queueview.ui
|
||||
|
||||
smartplaylists/smartplaylistquerysearchpage.ui
|
||||
smartplaylists/smartplaylistquerysortpage.ui
|
||||
smartplaylists/smartplaylistsearchpreview.ui
|
||||
smartplaylists/smartplaylistsearchtermwidget.ui
|
||||
smartplaylists/smartplaylistsviewcontainer.ui
|
||||
smartplaylists/smartplaylistwizardfinishpage.ui
|
||||
|
||||
covermanager/albumcoverexport.ui
|
||||
covermanager/albumcovermanager.ui
|
||||
covermanager/albumcoversearcher.ui
|
||||
@ -899,6 +937,27 @@ optional_source(HAVE_TIDAL
|
||||
settings/tidalsettingspage.ui
|
||||
)
|
||||
|
||||
optional_source(HAVE_QOBUZ
|
||||
SOURCES
|
||||
qobuz/qobuzservice.cpp
|
||||
qobuz/qobuzurlhandler.cpp
|
||||
qobuz/qobuzbaserequest.cpp
|
||||
qobuz/qobuzrequest.cpp
|
||||
qobuz/qobuzstreamurlrequest.cpp
|
||||
qobuz/qobuzfavoriterequest.cpp
|
||||
settings/qobuzsettingspage.cpp
|
||||
HEADERS
|
||||
qobuz/qobuzservice.h
|
||||
qobuz/qobuzurlhandler.h
|
||||
qobuz/qobuzbaserequest.h
|
||||
qobuz/qobuzrequest.h
|
||||
qobuz/qobuzstreamurlrequest.h
|
||||
qobuz/qobuzfavoriterequest.h
|
||||
settings/qobuzsettingspage.h
|
||||
UI
|
||||
settings/qobuzsettingspage.ui
|
||||
)
|
||||
|
||||
# Moodbar
|
||||
optional_source(HAVE_MOODBAR
|
||||
SOURCES
|
||||
|
@ -45,6 +45,7 @@
|
||||
#include "core/logging.h"
|
||||
#include "core/database.h"
|
||||
#include "core/scopedtransaction.h"
|
||||
#include "smartplaylists/smartplaylistsearch.h"
|
||||
|
||||
#include "directory.h"
|
||||
#include "collectionbackend.h"
|
||||
@ -1303,6 +1304,38 @@ void CollectionBackend::DeleteAll() {
|
||||
|
||||
}
|
||||
|
||||
SongList CollectionBackend::FindSongs(const SmartPlaylistSearch &search) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
// Build the query
|
||||
QString sql = search.ToSql(songs_table());
|
||||
|
||||
// Run the query
|
||||
SongList ret;
|
||||
QSqlQuery query(db);
|
||||
query.prepare(sql);
|
||||
query.exec();
|
||||
if (db_->CheckErrors(query)) return ret;
|
||||
|
||||
// Read the results
|
||||
while (query.next()) {
|
||||
Song song;
|
||||
song.InitFromQuery(query, true);
|
||||
ret << song;
|
||||
}
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
SongList CollectionBackend::GetAllSongs() {
|
||||
|
||||
// Get all the songs!
|
||||
return FindSongs(SmartPlaylistSearch(SmartPlaylistSearch::Type_All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::Sort_FieldAsc, SmartPlaylistSearchTerm::Field_Artist, -1));
|
||||
|
||||
}
|
||||
|
||||
SongList CollectionBackend::GetSongsBy(const QString &artist, const QString &album, const QString &title) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
@ -1378,3 +1411,44 @@ void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &ti
|
||||
emit SongsStatisticsChanged(SongList() << songs);
|
||||
|
||||
}
|
||||
|
||||
void CollectionBackend::UpdateSongRating(const int id, const float rating) {
|
||||
|
||||
if (id == -1) return;
|
||||
|
||||
QList<int> id_list;
|
||||
id_list << id;
|
||||
UpdateSongsRating(id_list, rating);
|
||||
|
||||
}
|
||||
|
||||
void CollectionBackend::UpdateSongsRating(const QList<int> &id_list, const float rating) {
|
||||
|
||||
if (id_list.isEmpty()) return;
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QStringList id_str_list;
|
||||
for (int i : id_list) {
|
||||
id_str_list << QString::number(i);
|
||||
}
|
||||
QString ids = id_str_list.join(",");
|
||||
QSqlQuery q(db);
|
||||
q.prepare(QString("UPDATE %1 SET rating = :rating WHERE ROWID IN (%2)").arg(songs_table_, ids));
|
||||
q.bindValue(":rating", rating);
|
||||
q.exec();
|
||||
if (db_->CheckErrors(q)) return;
|
||||
SongList new_song_list = GetSongsById(id_str_list, db);
|
||||
emit SongsRatingChanged(new_song_list);
|
||||
|
||||
}
|
||||
|
||||
void CollectionBackend::UpdateSongRatingAsync(const int id, const float rating) {
|
||||
metaObject()->invokeMethod(this, "UpdateSongRating", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(float, rating));
|
||||
}
|
||||
|
||||
void CollectionBackend::UpdateSongsRatingAsync(const QList<int>& ids, const float rating) {
|
||||
metaObject()->invokeMethod(this, "UpdateSongsRating", Qt::QueuedConnection, Q_ARG(QList<int>, ids), Q_ARG(float, rating));
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,7 @@
|
||||
|
||||
class QThread;
|
||||
class Database;
|
||||
class SmartPlaylistSearch;
|
||||
|
||||
class CollectionBackendInterface : public QObject {
|
||||
Q_OBJECT
|
||||
@ -182,10 +183,16 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
Song GetSongBySongId(const QString &song_id);
|
||||
SongList GetSongsBySongId(const QStringList &song_ids);
|
||||
|
||||
SongList GetAllSongs();
|
||||
SongList FindSongs(const SmartPlaylistSearch &search);
|
||||
|
||||
Song::Source Source() const;
|
||||
|
||||
void AddOrUpdateSongsAsync(const SongList &songs);
|
||||
|
||||
void UpdateSongRatingAsync(const int id, const float rating);
|
||||
void UpdateSongsRatingAsync(const QList<int> &ids, const float rating);
|
||||
|
||||
public slots:
|
||||
void Exit();
|
||||
void LoadDirectories();
|
||||
@ -209,19 +216,23 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
void UpdateLastPlayed(const QString &artist, const QString &album, const QString &title, const int lastplayed);
|
||||
void UpdatePlayCount(const QString &artist, const QString &title, const int playcount);
|
||||
|
||||
signals:
|
||||
void DirectoryDiscovered(const Directory &dir, const SubdirectoryList &subdirs);
|
||||
void DirectoryDeleted(const Directory &dir);
|
||||
void UpdateSongRating(const int id, const float rating);
|
||||
void UpdateSongsRating(const QList<int> &id_list, const float rating);
|
||||
|
||||
void SongsDiscovered(const SongList &songs);
|
||||
void SongsDeleted(const SongList &songs);
|
||||
void SongsStatisticsChanged(const SongList& songs);
|
||||
signals:
|
||||
void DirectoryDiscovered(Directory, SubdirectoryList);
|
||||
void DirectoryDeleted(Directory);
|
||||
|
||||
void SongsDiscovered(SongList);
|
||||
void SongsDeleted(SongList);
|
||||
void SongsStatisticsChanged(SongList);
|
||||
|
||||
void DatabaseReset();
|
||||
|
||||
void TotalSongCountUpdated(const int total);
|
||||
void TotalArtistCountUpdated(const int total);
|
||||
void TotalAlbumCountUpdated(const int total);
|
||||
void TotalSongCountUpdated(int);
|
||||
void TotalArtistCountUpdated(int);
|
||||
void TotalAlbumCountUpdated(int);
|
||||
void SongsRatingChanged(SongList);
|
||||
|
||||
void ExitFinished();
|
||||
|
||||
|
@ -127,6 +127,7 @@ CollectionModel::CollectionModel(CollectionBackend *backend, Application *app, Q
|
||||
connect(backend_, SIGNAL(TotalArtistCountUpdated(int)), SLOT(TotalArtistCountUpdatedSlot(int)));
|
||||
connect(backend_, SIGNAL(TotalAlbumCountUpdated(int)), SLOT(TotalAlbumCountUpdatedSlot(int)));
|
||||
connect(backend_, SIGNAL(SongsStatisticsChanged(SongList)), SLOT(SongsSlightlyChanged(SongList)));
|
||||
connect(backend_, SIGNAL(SongsRatingChanged(SongList)), SLOT(SongsSlightlyChanged(SongList)));
|
||||
|
||||
backend_->UpdateTotalSongCountAsync();
|
||||
backend_->UpdateTotalArtistCountAsync();
|
||||
|
@ -46,6 +46,7 @@
|
||||
|
||||
#cmakedefine HAVE_SUBSONIC
|
||||
#cmakedefine HAVE_TIDAL
|
||||
#cmakedefine HAVE_QOBUZ
|
||||
|
||||
#cmakedefine HAVE_MOODBAR
|
||||
|
||||
|
@ -56,7 +56,6 @@
|
||||
#include "covermanager/discogscoverprovider.h"
|
||||
#include "covermanager/musicbrainzcoverprovider.h"
|
||||
#include "covermanager/deezercoverprovider.h"
|
||||
#include "covermanager/qobuzcoverprovider.h"
|
||||
#include "covermanager/musixmatchcoverprovider.h"
|
||||
#include "covermanager/spotifycoverprovider.h"
|
||||
|
||||
@ -83,6 +82,11 @@
|
||||
# include "covermanager/tidalcoverprovider.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_QOBUZ
|
||||
# include "qobuz/qobuzservice.h"
|
||||
# include "covermanager/qobuzcoverprovider.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
# include "moodbar/moodbarcontroller.h"
|
||||
# include "moodbar/moodbarloader.h"
|
||||
@ -124,11 +128,13 @@ class ApplicationImpl {
|
||||
cover_providers->AddProvider(new MusicbrainzCoverProvider(app, app));
|
||||
cover_providers->AddProvider(new DiscogsCoverProvider(app, app));
|
||||
cover_providers->AddProvider(new DeezerCoverProvider(app, app));
|
||||
cover_providers->AddProvider(new QobuzCoverProvider(app, app));
|
||||
cover_providers->AddProvider(new MusixmatchCoverProvider(app, app));
|
||||
cover_providers->AddProvider(new SpotifyCoverProvider(app, app));
|
||||
#ifdef HAVE_TIDAL
|
||||
cover_providers->AddProvider(new TidalCoverProvider(app, app));
|
||||
#endif
|
||||
#ifdef HAVE_QOBUZ
|
||||
cover_providers->AddProvider(new QobuzCoverProvider(app, app));
|
||||
#endif
|
||||
cover_providers->ReloadSettings();
|
||||
return cover_providers;
|
||||
@ -159,6 +165,9 @@ class ApplicationImpl {
|
||||
#endif
|
||||
#ifdef HAVE_TIDAL
|
||||
internet_services->AddService(new TidalService(app, internet_services));
|
||||
#endif
|
||||
#ifdef HAVE_QOBUZ
|
||||
internet_services->AddService(new QobuzService(app, internet_services));
|
||||
#endif
|
||||
return internet_services;
|
||||
}),
|
||||
|
@ -54,7 +54,7 @@
|
||||
#include "scopedtransaction.h"
|
||||
|
||||
const char *Database::kDatabaseFilename = "strawberry.db";
|
||||
const int Database::kSchemaVersion = 12;
|
||||
const int Database::kSchemaVersion = 13;
|
||||
const char *Database::kMagicAllSongsTables = "%allsongstables";
|
||||
|
||||
int Database::sNextConnectionId = 1;
|
||||
|
@ -161,6 +161,9 @@
|
||||
# include "tidal/tidalservice.h"
|
||||
# include "settings/tidalsettingspage.h"
|
||||
#endif
|
||||
#ifdef HAVE_QOBUZ
|
||||
# include "settings/qobuzsettingspage.h"
|
||||
#endif
|
||||
|
||||
#include "internet/internetservices.h"
|
||||
#include "internet/internetservice.h"
|
||||
@ -185,6 +188,9 @@
|
||||
# include "moodbar/moodbarproxystyle.h"
|
||||
#endif
|
||||
|
||||
#include "smartplaylists/smartplaylistsviewcontainer.h"
|
||||
#include "smartplaylists/smartplaylistsview.h"
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
# include "windows7thumbbar.h"
|
||||
#endif
|
||||
@ -257,11 +263,15 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
|
||||
connect(add_stream_dialog, SIGNAL(accepted()), this, SLOT(AddStreamAccepted()));
|
||||
return add_stream_dialog;
|
||||
}),
|
||||
smartplaylists_view_(new SmartPlaylistsViewContainer(app, this)),
|
||||
#ifdef HAVE_SUBSONIC
|
||||
subsonic_view_(new InternetSongsView(app_, app->internet_services()->ServiceBySource(Song::Source_Subsonic), SubsonicSettingsPage::kSettingsGroup, SettingsDialog::Page_Subsonic, this)),
|
||||
#endif
|
||||
#ifdef HAVE_TIDAL
|
||||
tidal_view_(new InternetTabsView(app_, app->internet_services()->ServiceBySource(Song::Source_Tidal), TidalSettingsPage::kSettingsGroup, SettingsDialog::Page_Tidal, this)),
|
||||
#endif
|
||||
#ifdef HAVE_QOBUZ
|
||||
qobuz_view_(new InternetTabsView(app_, app->internet_services()->ServiceBySource(Song::Source_Qobuz), QobuzSettingsPage::kSettingsGroup, SettingsDialog::Page_Qobuz, this)),
|
||||
#endif
|
||||
lastfm_import_dialog_(new LastFMImportDialog(app_->lastfm_import(), this)),
|
||||
collection_show_all_(nullptr),
|
||||
@ -296,6 +306,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
|
||||
playing_widget_(true),
|
||||
doubleclick_addmode_(BehaviourSettingsPage::AddBehaviour_Append),
|
||||
doubleclick_playmode_(BehaviourSettingsPage::PlayBehaviour_Never),
|
||||
doubleclick_playlist_addmode_(BehaviourSettingsPage::PlaylistAddBehaviour_Play),
|
||||
menu_playmode_(BehaviourSettingsPage::PlayBehaviour_Never),
|
||||
exit_count_(0),
|
||||
delete_files_(false)
|
||||
@ -321,9 +332,10 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
|
||||
// Add tabs to the fancy tab widget
|
||||
ui_->tabs->AddTab(context_view_, "context", IconLoader::Load("strawberry"), tr("Context"));
|
||||
ui_->tabs->AddTab(collection_view_, "collection", IconLoader::Load("library-music"), tr("Collection"));
|
||||
ui_->tabs->AddTab(file_view_, "files", IconLoader::Load("document-open"), tr("Files"));
|
||||
ui_->tabs->AddTab(playlist_list_, "playlists", IconLoader::Load("view-media-playlist"), tr("Playlists"));
|
||||
ui_->tabs->AddTab(queue_view_, "queue", IconLoader::Load("footsteps"), tr("Queue"));
|
||||
ui_->tabs->AddTab(playlist_list_, "playlists", IconLoader::Load("view-media-playlist"), tr("Playlists"));
|
||||
ui_->tabs->AddTab(smartplaylists_view_, "smartplaylists", IconLoader::Load("view-media-playlist"), tr("Smart playlists"));
|
||||
ui_->tabs->AddTab(file_view_, "files", IconLoader::Load("document-open"), tr("Files"));
|
||||
#ifndef Q_OS_WIN
|
||||
ui_->tabs->AddTab(device_view_, "devices", IconLoader::Load("device"), tr("Devices"));
|
||||
#endif
|
||||
@ -333,6 +345,9 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
|
||||
#ifdef HAVE_TIDAL
|
||||
ui_->tabs->AddTab(tidal_view_, "tidal", IconLoader::Load("tidal"), tr("Tidal"));
|
||||
#endif
|
||||
#ifdef HAVE_QOBUZ
|
||||
ui_->tabs->AddTab(qobuz_view_, "qobuz", IconLoader::Load("qobuz"), tr("Qobuz"));
|
||||
#endif
|
||||
|
||||
// Add the playing widget to the fancy tab widget
|
||||
ui_->tabs->addBottomWidget(ui_->widget_playing);
|
||||
@ -644,6 +659,13 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
|
||||
connect(this, SIGNAL(AuthorizationUrlReceived(QUrl)), tidalservice, SLOT(AuthorizationUrlReceived(QUrl)));
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_QOBUZ
|
||||
connect(qobuz_view_->artists_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
|
||||
connect(qobuz_view_->albums_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
|
||||
connect(qobuz_view_->songs_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
|
||||
connect(qobuz_view_->search_view(), SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
|
||||
#endif
|
||||
|
||||
// Playlist menu
|
||||
connect(playlist_menu_, SIGNAL(aboutToHide()), SLOT(PlaylistMenuHidden()));
|
||||
playlist_play_pause_ = playlist_menu_->addAction(tr("Play"), this, SLOT(PlaylistPlay()));
|
||||
@ -829,6 +851,9 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
|
||||
connect(app_->playlist_manager()->sequence(), SIGNAL(RepeatModeChanged(PlaylistSequence::RepeatMode)), osd_, SLOT(RepeatModeChanged(PlaylistSequence::RepeatMode)));
|
||||
connect(app_->playlist_manager()->sequence(), SIGNAL(ShuffleModeChanged(PlaylistSequence::ShuffleMode)), osd_, SLOT(ShuffleModeChanged(PlaylistSequence::ShuffleMode)));
|
||||
|
||||
// Smart playlists
|
||||
connect(smartplaylists_view_, SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
|
||||
|
||||
ScrobbleButtonVisibilityChanged(app_->scrobbler()->ScrobbleButton());
|
||||
LoveButtonVisibilityChanged(app_->scrobbler()->LoveButton());
|
||||
ScrobblingEnabledChanged(app_->scrobbler()->IsEnabled());
|
||||
@ -849,8 +874,8 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
|
||||
restoreGeometry(settings_.value("geometry").toByteArray());
|
||||
}
|
||||
|
||||
if (!ui_->splitter->restoreState(settings_.value("splitter_state").toByteArray())) {
|
||||
ui_->splitter->setSizes(QList<int>() << 250 << width() - 250);
|
||||
if (!settings_.contains("splitter_state") || !ui_->splitter->restoreState(settings_.value("splitter_state").toByteArray())) {
|
||||
ui_->splitter->setSizes(QList<int>() << 20 << (width() - 20));
|
||||
}
|
||||
|
||||
ui_->tabs->setCurrentIndex(settings_.value("current_tab", 1).toInt());
|
||||
@ -981,9 +1006,9 @@ void MainWindow::ReloadSettings() {
|
||||
playing_widget_ = s.value("playing_widget", true).toBool();
|
||||
if (playing_widget_ != ui_->widget_playing->IsEnabled()) TabSwitched();
|
||||
doubleclick_addmode_ = BehaviourSettingsPage::AddBehaviour(s.value("doubleclick_addmode", BehaviourSettingsPage::AddBehaviour_Append).toInt());
|
||||
doubleclick_playmode_ = BehaviourSettingsPage::PlayBehaviour(s.value("doubleclick_playmode", BehaviourSettingsPage::PlayBehaviour_IfStopped).toInt());
|
||||
doubleclick_playlist_addmode_ = BehaviourSettingsPage::PlaylistAddBehaviour(s.value("doubleclick_playlist_addmode", BehaviourSettingsPage::PlaylistAddBehaviour_Play).toInt());
|
||||
menu_playmode_ = BehaviourSettingsPage::PlayBehaviour(s.value("menu_playmode", BehaviourSettingsPage::PlayBehaviour_IfStopped).toInt());
|
||||
doubleclick_playmode_ = BehaviourSettingsPage::PlayBehaviour(s.value("doubleclick_playmode", BehaviourSettingsPage::PlayBehaviour_Never).toInt());
|
||||
doubleclick_playlist_addmode_ = BehaviourSettingsPage::PlaylistAddBehaviour(s.value("doubleclick_playlist_addmode", BehaviourSettingsPage::PlayBehaviour_Never).toInt());
|
||||
menu_playmode_ = BehaviourSettingsPage::PlayBehaviour(s.value("menu_playmode", BehaviourSettingsPage::PlayBehaviour_Never).toInt());
|
||||
s.endGroup();
|
||||
|
||||
s.beginGroup(AppearanceSettingsPage::kSettingsGroup);
|
||||
@ -1039,6 +1064,16 @@ void MainWindow::ReloadSettings() {
|
||||
ui_->tabs->DisableTab(tidal_view_);
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_QOBUZ
|
||||
s.beginGroup(QobuzSettingsPage::kSettingsGroup);
|
||||
bool enable_qobuz = s.value("enabled", false).toBool();
|
||||
s.endGroup();
|
||||
if (enable_qobuz)
|
||||
ui_->tabs->EnableTab(qobuz_view_);
|
||||
else
|
||||
ui_->tabs->DisableTab(qobuz_view_);
|
||||
#endif
|
||||
|
||||
ui_->tabs->ReloadSettings();
|
||||
|
||||
}
|
||||
@ -1061,6 +1096,7 @@ void MainWindow::ReloadAllSettings() {
|
||||
file_view_->ReloadSettings();
|
||||
queue_view_->ReloadSettings();
|
||||
playlist_list_->ReloadSettings();
|
||||
smartplaylists_view_->ReloadSettings();
|
||||
app_->cover_providers()->ReloadSettings();
|
||||
app_->lyrics_providers()->ReloadSettings();
|
||||
#ifdef HAVE_SUBSONIC
|
||||
@ -1069,6 +1105,9 @@ void MainWindow::ReloadAllSettings() {
|
||||
#ifdef HAVE_TIDAL
|
||||
tidal_view_->ReloadSettings();
|
||||
#endif
|
||||
#ifdef HAVE_QOBUZ
|
||||
qobuz_view_->ReloadSettings();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
|
@ -93,6 +93,7 @@ class TranscodeDialog;
|
||||
class Ui_MainWindow;
|
||||
class InternetSongsView;
|
||||
class InternetTabsView;
|
||||
class SmartPlaylistsViewContainer;
|
||||
#ifdef Q_OS_WIN
|
||||
class Windows7ThumbBar;
|
||||
#endif
|
||||
@ -325,8 +326,11 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
std::unique_ptr<TrackSelectionDialog> track_selection_dialog_;
|
||||
PlaylistItemList autocomplete_tag_items_;
|
||||
|
||||
SmartPlaylistsViewContainer *smartplaylists_view_;
|
||||
|
||||
InternetSongsView *subsonic_view_;
|
||||
InternetTabsView *tidal_view_;
|
||||
InternetTabsView *qobuz_view_;
|
||||
|
||||
LastFMImportDialog *lastfm_import_dialog_;
|
||||
|
||||
|
@ -69,6 +69,8 @@
|
||||
|
||||
#include "internet/internetsearchview.h"
|
||||
|
||||
#include "smartplaylists/playlistgenerator_fwd.h"
|
||||
|
||||
void RegisterMetaTypes() {
|
||||
|
||||
qRegisterMetaType<const char*>("const char*");
|
||||
@ -136,4 +138,6 @@ void RegisterMetaTypes() {
|
||||
qRegisterMetaType<InternetSearchView::ResultList>("InternetSearchView::ResultList");
|
||||
qRegisterMetaType<InternetSearchView::Result>("InternetSearchView::Result");
|
||||
|
||||
qRegisterMetaType<PlaylistGeneratorPtr>("PlaylistGeneratorPtr");
|
||||
|
||||
}
|
||||
|
@ -125,6 +125,8 @@ const QStringList Song::kColumns = QStringList() << "title"
|
||||
|
||||
<< "cue_path"
|
||||
|
||||
<< "rating"
|
||||
|
||||
;
|
||||
|
||||
const QString Song::kColumnSpec = Song::kColumns.join(", ");
|
||||
@ -217,6 +219,8 @@ struct Song::Private : public QSharedData {
|
||||
|
||||
QString cue_path_; // If the song has a CUE, this contains it's path.
|
||||
|
||||
float rating_; // Database rating, not read from tags.
|
||||
|
||||
QUrl stream_url_; // Temporary stream url set by url handler.
|
||||
QImage image_; // Album Cover image set by album cover loader.
|
||||
bool init_from_file_; // Whether this song was loaded from a file using taglib.
|
||||
@ -259,6 +263,8 @@ Song::Private::Private(Song::Source source)
|
||||
compilation_on_(false),
|
||||
compilation_off_(false),
|
||||
|
||||
rating_(-1),
|
||||
|
||||
init_from_file_(false),
|
||||
suspicious_tags_(false)
|
||||
|
||||
@ -347,6 +353,8 @@ const QImage &Song::image() const { return d->image_; }
|
||||
const QString &Song::cue_path() const { return d->cue_path_; }
|
||||
bool Song::has_cue() const { return !d->cue_path_.isEmpty(); }
|
||||
|
||||
float Song::rating() const { return d->rating_; }
|
||||
|
||||
bool Song::is_collection_song() const { return d->source_ == Source_Collection; }
|
||||
bool Song::is_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); }
|
||||
bool Song::is_stream() const { return d->source_ == Source_Stream || d->source_ == Source_Tidal || d->source_ == Source_Subsonic || d->source_ == Source_Qobuz; }
|
||||
@ -444,6 +452,8 @@ void Song::set_art_automatic(const QUrl &v) { d->art_automatic_ = v; }
|
||||
void Song::set_art_manual(const QUrl &v) { d->art_manual_ = v; }
|
||||
void Song::set_cue_path(const QString &v) { d->cue_path_ = v; }
|
||||
|
||||
void Song::set_rating(float v) { d->rating_ = v; }
|
||||
|
||||
void Song::set_stream_url(const QUrl &v) { d->stream_url_ = v; }
|
||||
void Song::set_image(const QImage &i) { d->image_ = i; }
|
||||
|
||||
@ -1000,6 +1010,10 @@ void Song::InitFromQuery(const SqlRow &q, bool reliable_metadata, int col) {
|
||||
d->cue_path_ = tostr(x);
|
||||
}
|
||||
|
||||
else if (Song::kColumns.value(i) == "rating") {
|
||||
d->rating_ = tofloat(x);
|
||||
}
|
||||
|
||||
else {
|
||||
qLog(Error) << "Forgot to handle" << Song::kColumns.value(i);
|
||||
}
|
||||
@ -1363,6 +1377,8 @@ void Song::BindToQuery(QSqlQuery *query) const {
|
||||
|
||||
query->bindValue(":cue_path", d->cue_path_);
|
||||
|
||||
query->bindValue(":rating", intval(d->rating_));
|
||||
|
||||
#undef intval
|
||||
#undef notnullintval
|
||||
#undef strval
|
||||
@ -1441,6 +1457,16 @@ QString Song::SampleRateBitDepthToText() const {
|
||||
|
||||
}
|
||||
|
||||
QString Song::PrettyRating() const {
|
||||
|
||||
float rating = d->rating_;
|
||||
|
||||
if (rating == -1.0f) return "0";
|
||||
|
||||
return QString::number(static_cast<int>(rating * 100));
|
||||
|
||||
}
|
||||
|
||||
bool Song::IsMetadataEqual(const Song &other) const {
|
||||
|
||||
return d->title_ == other.d->title_ &&
|
||||
@ -1547,5 +1573,6 @@ void Song::MergeUserSetData(const Song &other) {
|
||||
set_art_manual(other.art_manual());
|
||||
set_compilation_on(other.compilation_on());
|
||||
set_compilation_off(other.compilation_off());
|
||||
set_rating(other.rating());
|
||||
|
||||
}
|
||||
|
@ -245,6 +245,8 @@ class Song {
|
||||
const QString &cue_path() const;
|
||||
bool has_cue() const;
|
||||
|
||||
float rating() const;
|
||||
|
||||
const QString &effective_album() const;
|
||||
int effective_originalyear() const;
|
||||
const QString &effective_albumartist() const;
|
||||
@ -288,6 +290,8 @@ class Song {
|
||||
|
||||
QString SampleRateBitDepthToText() const;
|
||||
|
||||
QString PrettyRating() const;
|
||||
|
||||
// Setters
|
||||
bool IsEditable() const;
|
||||
|
||||
@ -346,6 +350,8 @@ class Song {
|
||||
|
||||
void set_cue_path(const QString &v);
|
||||
|
||||
void set_rating(const float v);
|
||||
|
||||
void set_stream_url(const QUrl &v);
|
||||
void set_image(const QImage &i);
|
||||
|
||||
|
@ -37,40 +37,24 @@
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QSettings>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/network.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/song.h"
|
||||
#include "core/utilities.h"
|
||||
#include "dialogs/userpassdialog.h"
|
||||
#include "internet/internetservices.h"
|
||||
#include "qobuz/qobuzservice.h"
|
||||
#include "qobuz/qobuzbaserequest.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "qobuzcoverprovider.h"
|
||||
|
||||
const char *QobuzCoverProvider::kSettingsGroup = "Qobuz";
|
||||
const char *QobuzCoverProvider::kAuthUrl = "https://www.qobuz.com/api.json/0.2/user/login";
|
||||
const char *QobuzCoverProvider::kApiUrl = "https://www.qobuz.com/api.json/0.2";
|
||||
const char *QobuzCoverProvider::kAppID = "OTQyODUyNTY3";
|
||||
const int QobuzCoverProvider::kLimit = 10;
|
||||
|
||||
QobuzCoverProvider::QobuzCoverProvider(Application *app, QObject *parent) : JsonCoverProvider("Qobuz", true, true, 2.0, true, true, app, parent), network_(new NetworkAccessManager(this)) {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
username_ = s.value("username").toString();
|
||||
QByteArray password = s.value("password").toByteArray();
|
||||
if (password.isEmpty()) password_.clear();
|
||||
else password_ = QString::fromUtf8(QByteArray::fromBase64(password));
|
||||
user_auth_token_ = s.value("user_auth_token").toString();
|
||||
user_id_ = s.value("user_id").toLongLong();
|
||||
credential_id_ = s.value("credential_id").toLongLong();
|
||||
device_id_ = s.value("device_id").toString();
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
QobuzCoverProvider::QobuzCoverProvider(Application *app, QObject *parent) : JsonCoverProvider("Qobuz", true, true, 2.0, true, true, app, parent),
|
||||
service_(app->internet_services()->Service<QobuzService>()),
|
||||
network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
QobuzCoverProvider::~QobuzCoverProvider() {
|
||||
|
||||
@ -83,221 +67,6 @@ QobuzCoverProvider::~QobuzCoverProvider() {
|
||||
|
||||
}
|
||||
|
||||
void QobuzCoverProvider::Authenticate() {
|
||||
|
||||
login_errors_.clear();
|
||||
|
||||
if (username_.isEmpty() || password_.isEmpty()) {
|
||||
UserPassDialog dialog;
|
||||
if (dialog.exec() == QDialog::Rejected || dialog.username().isEmpty() || dialog.password().isEmpty()) {
|
||||
AuthError(tr("Missing username and password."));
|
||||
return;
|
||||
}
|
||||
username_ = dialog.username();
|
||||
password_ = dialog.password();
|
||||
}
|
||||
|
||||
const ParamList params = ParamList() << Param("app_id", QString::fromUtf8(QByteArray::fromBase64(kAppID)))
|
||||
<< Param("username", username_)
|
||||
<< Param("password", password_)
|
||||
<< Param("device_manufacturer_id", Utilities::MacAddress());
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QUrl url(kAuthUrl);
|
||||
QNetworkRequest req(url);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
#else
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
#endif
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
|
||||
QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
|
||||
QNetworkReply *reply = network_->post(req, query);
|
||||
replies_ << reply;
|
||||
connect(reply, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(HandleLoginSSLErrors(QList<QSslError>)));
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleAuthReply(reply); });
|
||||
|
||||
}
|
||||
|
||||
void QobuzCoverProvider::HandleAuthReply(QNetworkReply *reply) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
AuthError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
return;
|
||||
}
|
||||
else {
|
||||
// See if there is Json data containing "status", "code" and "message" - then use that instead.
|
||||
QByteArray data(reply->readAll());
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("code") && json_obj.contains("message")) {
|
||||
QString status = json_obj["status"].toString();
|
||||
int code = json_obj["code"].toInt();
|
||||
QString message = json_obj["message"].toString();
|
||||
login_errors_ << QString("%1 (%2)").arg(message).arg(code);
|
||||
if (code == 401) {
|
||||
username_.clear();
|
||||
password_.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (login_errors_.isEmpty()) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
if(reply->error() == QNetworkReply::AuthenticationRequiredError) {
|
||||
username_.clear();
|
||||
password_.clear();
|
||||
}
|
||||
}
|
||||
else {
|
||||
login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
|
||||
}
|
||||
}
|
||||
AuthError();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
login_errors_.clear();
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
|
||||
if (json_error.error != QJsonParseError::NoError) {
|
||||
AuthError("Authentication reply from server missing Json data.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
AuthError("Authentication reply from server has empty Json document.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
AuthError("Authentication reply from server has Json document that is not an object.", json_doc);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
AuthError("Authentication reply from server has empty Json object.", json_doc);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("user_auth_token")) {
|
||||
AuthError("Authentication reply from server is missing user_auth_token", json_obj);
|
||||
return;
|
||||
}
|
||||
user_auth_token_ = json_obj["user_auth_token"].toString();
|
||||
|
||||
if (!json_obj.contains("user")) {
|
||||
AuthError("Authentication reply from server is missing user", json_obj);
|
||||
return;
|
||||
}
|
||||
QJsonValue json_user = json_obj["user"];
|
||||
if (!json_user.isObject()) {
|
||||
AuthError("Authentication reply user is not a object", json_obj);
|
||||
return;
|
||||
}
|
||||
QJsonObject json_obj_user = json_user.toObject();
|
||||
|
||||
if (!json_obj_user.contains("id")) {
|
||||
AuthError("Authentication reply from server is missing user id", json_obj_user);
|
||||
return;
|
||||
}
|
||||
user_id_ = json_obj_user["id"].toVariant().toLongLong();
|
||||
|
||||
if (!json_obj_user.contains("device")) {
|
||||
AuthError("Authentication reply from server is missing user device", json_obj_user);
|
||||
return;
|
||||
}
|
||||
QJsonValue json_device = json_obj_user["device"];
|
||||
if (!json_device.isObject()) {
|
||||
AuthError("Authentication reply from server user device is not a object", json_device);
|
||||
return;
|
||||
}
|
||||
QJsonObject json_obj_device = json_device.toObject();
|
||||
|
||||
if (!json_obj_device.contains("device_manufacturer_id")) {
|
||||
AuthError("Authentication reply from server device is missing device_manufacturer_id", json_obj_device);
|
||||
return;
|
||||
}
|
||||
device_id_ = json_obj_device["device_manufacturer_id"].toString();
|
||||
|
||||
if (!json_obj_user.contains("credential")) {
|
||||
AuthError("Authentication reply from server is missing user credential", json_obj_user);
|
||||
return;
|
||||
}
|
||||
QJsonValue json_credential = json_obj_user["credential"];
|
||||
if (!json_credential.isObject()) {
|
||||
AuthError("Authentication reply from serve userr credential is not a object", json_device);
|
||||
return;
|
||||
}
|
||||
QJsonObject json_obj_credential = json_credential.toObject();
|
||||
|
||||
if (!json_obj_credential.contains("id")) {
|
||||
AuthError("Authentication reply user credential from server is missing user credential id", json_obj_device);
|
||||
return;
|
||||
}
|
||||
//credential_id_ = json_obj_credential["id"].toInt();
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("username", username_);
|
||||
s.setValue("password", password_.toUtf8().toBase64());
|
||||
s.setValue("user_auth_token", user_auth_token_);
|
||||
s.setValue("user_id", user_id_);
|
||||
s.setValue("credential_id", credential_id_);
|
||||
s.setValue("device_id", device_id_);
|
||||
s.endGroup();
|
||||
|
||||
qLog(Debug) << "Qobuz: Login successful" << "user id" << user_id_ << "user auth token" << user_auth_token_ << "device id" << device_id_;
|
||||
|
||||
emit AuthenticationComplete(true);
|
||||
emit AuthenticationSuccess();
|
||||
|
||||
}
|
||||
|
||||
void QobuzCoverProvider::Deauthenticate() {
|
||||
|
||||
user_auth_token_.clear();
|
||||
user_id_ = 0;
|
||||
credential_id_ = 0;
|
||||
device_id_.clear();
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.remove("user_auth_token");
|
||||
s.remove("user_id");
|
||||
s.remove("credential_id");
|
||||
s.remove("device_id");
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void QobuzCoverProvider::HandleLoginSSLErrors(QList<QSslError> ssl_errors) {
|
||||
|
||||
for (const QSslError &ssl_error : ssl_errors) {
|
||||
login_errors_ += ssl_error.errorString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
if (artist.isEmpty() && album.isEmpty() && title.isEmpty()) return false;
|
||||
@ -319,7 +88,7 @@ bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album
|
||||
|
||||
ParamList params = ParamList() << Param("query", query)
|
||||
<< Param("limit", QString::number(kLimit))
|
||||
<< Param("app_id", QString::fromUtf8(QByteArray::fromBase64(kAppID)));
|
||||
<< Param("app_id", service_->app_id().toUtf8());
|
||||
|
||||
std::sort(params.begin(), params.end());
|
||||
|
||||
@ -328,7 +97,7 @@ bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album
|
||||
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QUrl url(kApiUrl + QString("/") + resource);
|
||||
QUrl url(QobuzBaseRequest::kApiUrl + QString("/") + resource);
|
||||
url.setQuery(url_query);
|
||||
|
||||
QNetworkRequest req(url);
|
||||
@ -338,7 +107,7 @@ bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
#endif
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
req.setRawHeader("X-App-Id", kAppID);
|
||||
req.setRawHeader("X-App-Id", service_->app_id().toUtf8());
|
||||
req.setRawHeader("X-User-Auth-Token", user_auth_token_.toUtf8());
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
@ -517,20 +286,6 @@ void QobuzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
|
||||
|
||||
}
|
||||
|
||||
void QobuzCoverProvider::AuthError(const QString &error, const QVariant &debug) {
|
||||
|
||||
if (!error.isEmpty()) login_errors_ << error;
|
||||
|
||||
for (const QString &e : login_errors_) Error(e);
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
emit AuthenticationFailure(login_errors_);
|
||||
emit AuthenticationComplete(false, login_errors_);
|
||||
|
||||
login_errors_.clear();
|
||||
|
||||
}
|
||||
|
||||
void QobuzCoverProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "Qobuz:" << error;
|
||||
|
@ -31,10 +31,12 @@
|
||||
#include <QSslError>
|
||||
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "qobuz/qobuzservice.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class Application;
|
||||
class QobuzService;
|
||||
|
||||
class QobuzCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
@ -46,32 +48,23 @@ class QobuzCoverProvider : public JsonCoverProvider {
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) override;
|
||||
void CancelSearch(const int id) override;
|
||||
|
||||
void Authenticate() override;
|
||||
void Deauthenticate() override;
|
||||
bool IsAuthenticated() const override { return !user_auth_token_.isEmpty(); }
|
||||
|
||||
private slots:
|
||||
void HandleLoginSSLErrors(QList<QSslError> ssl_errors);
|
||||
void HandleAuthReply(QNetworkReply *reply);
|
||||
bool IsAuthenticated() const override { return service_ && service_->authenticated(); }
|
||||
void Deauthenticate() override { if (service_) service_->Logout(); }
|
||||
|
||||
private slots:
|
||||
void HandleSearchReply(QNetworkReply *reply, const int id);
|
||||
|
||||
private:
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
void AuthError(const QString &error = QString(), const QVariant &debug = QVariant());
|
||||
void Error(const QString &error, const QVariant &debug = QVariant()) override;
|
||||
|
||||
private:
|
||||
typedef QPair<QString, QString> Param;
|
||||
typedef QList<Param> ParamList;
|
||||
|
||||
static const char *kSettingsGroup;
|
||||
static const char *kAuthUrl;
|
||||
static const char *kApiUrl;
|
||||
static const char *kAppID;
|
||||
static const int kLimit;
|
||||
|
||||
QobuzService *service_;
|
||||
QNetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
|
@ -37,7 +37,7 @@
|
||||
#include "core/scopedtransaction.h"
|
||||
#include "devicedatabasebackend.h"
|
||||
|
||||
const int DeviceDatabaseBackend::kDeviceSchemaVersion = 1;
|
||||
const int DeviceDatabaseBackend::kDeviceSchemaVersion = 2;
|
||||
|
||||
DeviceDatabaseBackend::DeviceDatabaseBackend(QObject *parent) :
|
||||
QObject(parent),
|
||||
|
31
src/playlist/dynamicplaylistcontrols.cpp
Normal file
@ -0,0 +1,31 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
|
||||
Clementine 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.
|
||||
|
||||
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "dynamicplaylistcontrols.h"
|
||||
#include "ui_dynamicplaylistcontrols.h"
|
||||
|
||||
DynamicPlaylistControls::DynamicPlaylistControls(QWidget *parent)
|
||||
: QWidget(parent), ui_(new Ui_DynamicPlaylistControls) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
|
||||
connect(ui_->expand, SIGNAL(clicked()), SIGNAL(Expand()));
|
||||
connect(ui_->repopulate, SIGNAL(clicked()), SIGNAL(Repopulate()));
|
||||
connect(ui_->off, SIGNAL(clicked()), SIGNAL(TurnOff()));
|
||||
}
|
||||
|
||||
DynamicPlaylistControls::~DynamicPlaylistControls() { delete ui_; }
|
41
src/playlist/dynamicplaylistcontrols.h
Normal file
@ -0,0 +1,41 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
|
||||
Clementine 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.
|
||||
|
||||
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef DYNAMICPLAYLISTCONTROLS_H
|
||||
#define DYNAMICPLAYLISTCONTROLS_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class Ui_DynamicPlaylistControls;
|
||||
|
||||
class DynamicPlaylistControls : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DynamicPlaylistControls(QWidget *parent = nullptr);
|
||||
~DynamicPlaylistControls();
|
||||
|
||||
signals:
|
||||
void Expand();
|
||||
void Repopulate();
|
||||
void TurnOff();
|
||||
|
||||
private:
|
||||
Ui_DynamicPlaylistControls* ui_;
|
||||
};
|
||||
|
||||
#endif // DYNAMICPLAYLISTCONTROLS_H
|
99
src/playlist/dynamicplaylistcontrols.ui
Normal file
@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>DynamicPlaylistControls</class>
|
||||
<widget class="QWidget" name="DynamicPlaylistControls">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>483</width>
|
||||
<height>54</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#container {
|
||||
background: rgba(200, 200, 200, 50%);
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(200, 200, 200, 75%);
|
||||
}
|
||||
|
||||
#label1 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#label2 {
|
||||
font-size: 7.5pt;
|
||||
}</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="layout_dynamic_playlist_controls">
|
||||
<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="QFrame" name="container">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="layout_container">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="layout_labels">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label1">
|
||||
<property name="text">
|
||||
<string>Dynamic mode is on</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label2">
|
||||
<property name="text">
|
||||
<string>New tracks will be added automatically.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="expand">
|
||||
<property name="text">
|
||||
<string>Expand</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="repopulate">
|
||||
<property name="text">
|
||||
<string>Repopulate</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="off">
|
||||
<property name="text">
|
||||
<string>Turn off</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -84,6 +84,10 @@
|
||||
#include "songplaylistitem.h"
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
#include "smartplaylists/playlistgenerator.h"
|
||||
#include "smartplaylists/playlistgeneratorinserter.h"
|
||||
#include "smartplaylists/playlistgeneratormimedata.h"
|
||||
|
||||
#include "internet/internetplaylistitem.h"
|
||||
#include "internet/internetsongmimedata.h"
|
||||
|
||||
@ -271,6 +275,9 @@ QVariant Playlist::data(const QModelIndex &idx, int role) const {
|
||||
case Role_QueuePosition:
|
||||
return queue_->PositionOf(idx);
|
||||
|
||||
case Role_CanSetRating:
|
||||
return idx.column() == Column_Rating && items_[idx.row()]->IsLocalCollectionItem() && items_[idx.row()]->Metadata().id() != -1;
|
||||
|
||||
case Qt::EditRole:
|
||||
case Qt::ToolTipRole:
|
||||
case Qt::DisplayRole: {
|
||||
@ -314,6 +321,8 @@ QVariant Playlist::data(const QModelIndex &idx, int role) const {
|
||||
|
||||
case Column_Source: return song.source();
|
||||
|
||||
case Column_Rating: return song.rating();
|
||||
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
@ -331,6 +340,9 @@ QVariant Playlist::data(const QModelIndex &idx, int role) const {
|
||||
if (items_[idx.row()]->HasCurrentForegroundColor()) {
|
||||
return QBrush(items_[idx.row()]->GetCurrentForegroundColor());
|
||||
}
|
||||
if (idx.row() < dynamic_history_length()) {
|
||||
return QBrush(kDynamicHistoryColor);
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
|
||||
@ -634,6 +646,35 @@ void Playlist::set_current_row(const int i, const AutoScroll autoscroll, const b
|
||||
InformOfCurrentSongChange(autoscroll);
|
||||
}
|
||||
|
||||
// The structure of a dynamic playlist is as follows:
|
||||
// history - active song - future
|
||||
// We have to ensure that this invariant is maintained.
|
||||
if (dynamic_playlist_ && current_item_index_.isValid()) {
|
||||
|
||||
// When advancing to the next track
|
||||
if (i > old_current_item_index.row()) {
|
||||
// Move the new item one position ahead of the last item in the history.
|
||||
MoveItemWithoutUndo(current_item_index_.row(), dynamic_history_length());
|
||||
|
||||
// Compute the number of new items that have to be inserted. This is not
|
||||
// necessarily 1 because the user might have added or removed items
|
||||
// manually. Note that the future excludes the current item.
|
||||
const int count = dynamic_history_length() + 1 + dynamic_playlist_->GetDynamicFuture() - items_.count();
|
||||
if (count > 0) {
|
||||
InsertDynamicItems(count);
|
||||
}
|
||||
|
||||
// Shrink the history, again this is not necessarily by 1, because the
|
||||
// user might have moved items by hand.
|
||||
const int remove_count = dynamic_history_length() - dynamic_playlist_->GetDynamicHistory();
|
||||
if (0 < remove_count) RemoveItemsWithoutUndo(0, remove_count);
|
||||
}
|
||||
|
||||
// the above actions make all commands on the undo stack invalid, so we
|
||||
// better clear it.
|
||||
undo_stack_->clear();
|
||||
}
|
||||
|
||||
if (current_item_index_.isValid()) {
|
||||
last_played_item_index_ = current_item_index_;
|
||||
Save();
|
||||
@ -643,6 +684,16 @@ void Playlist::set_current_row(const int i, const AutoScroll autoscroll, const b
|
||||
|
||||
}
|
||||
|
||||
void Playlist::InsertDynamicItems(const int count) {
|
||||
|
||||
PlaylistGeneratorInserter* inserter = new PlaylistGeneratorInserter(task_manager_, collection_, this);
|
||||
connect(inserter, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
|
||||
connect(inserter, SIGNAL(PlayRequested(QModelIndex)), SIGNAL(PlayRequested(QModelIndex)));
|
||||
|
||||
inserter->Load(this, -1, false, false, false, dynamic_playlist_, count);
|
||||
|
||||
}
|
||||
|
||||
Qt::ItemFlags Playlist::flags(const QModelIndex &idx) const {
|
||||
|
||||
if (idx.isValid()) {
|
||||
@ -697,6 +748,9 @@ bool Playlist::dropMimeData(const QMimeData *data, Qt::DropAction action, int ro
|
||||
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 (const PlaylistGeneratorMimeData *generator_data = qobject_cast<const PlaylistGeneratorMimeData*>(data)) {
|
||||
InsertSmartPlaylist(generator_data->generator_, row, play_now, enqueue_now, enqueue_next_now);
|
||||
}
|
||||
else if (data->hasFormat(kRowsMimetype)) {
|
||||
// Dragged from the playlist
|
||||
// Rearranging it is tricky...
|
||||
@ -768,6 +822,34 @@ void Playlist::InsertUrls(const QList<QUrl> &urls, const int pos, const bool pla
|
||||
|
||||
}
|
||||
|
||||
void Playlist::InsertSmartPlaylist(PlaylistGeneratorPtr generator, const int pos, const bool play_now, const bool enqueue, const bool enqueue_next) {
|
||||
|
||||
// Hack: If the generator hasn't got a collection set then use the main one
|
||||
if (!generator->collection()) {
|
||||
generator->set_collection(collection_);
|
||||
}
|
||||
|
||||
PlaylistGeneratorInserter *inserter = new PlaylistGeneratorInserter(task_manager_, collection_, this);
|
||||
connect(inserter, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
|
||||
|
||||
inserter->Load(this, pos, play_now, enqueue, enqueue_next, generator);
|
||||
|
||||
if (generator->is_dynamic()) {
|
||||
TurnOnDynamicPlaylist(generator);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Playlist::TurnOnDynamicPlaylist(PlaylistGeneratorPtr gen) {
|
||||
|
||||
dynamic_playlist_ = gen;
|
||||
playlist_sequence_->SetUsingDynamicPlaylist(true);
|
||||
ShuffleModeChanged(PlaylistSequence::Shuffle_Off);
|
||||
emit DynamicModeChanged(true);
|
||||
Save();
|
||||
|
||||
}
|
||||
|
||||
void Playlist::MoveItemWithoutUndo(const int source, const int dest) {
|
||||
MoveItemsWithoutUndo(QList<int>() << source, dest);
|
||||
}
|
||||
@ -1139,6 +1221,9 @@ bool Playlist::CompareItems(const int column, const Qt::SortOrder order, std::sh
|
||||
|
||||
case Column_Comment: strcmp(comment);
|
||||
case Column_Source: cmp(source);
|
||||
|
||||
case Column_Rating: cmp(rating);
|
||||
|
||||
default: qLog(Error) << "No such column" << column;
|
||||
}
|
||||
|
||||
@ -1196,6 +1281,7 @@ QString Playlist::column_name(Column column) {
|
||||
case Column_Comment: return tr("Comment");
|
||||
case Column_Source: return tr("Source");
|
||||
case Column_Mood: return tr("Mood");
|
||||
case Column_Rating: return tr("Rating");
|
||||
default: qLog(Error) << "No such column" << column;;
|
||||
}
|
||||
return "";
|
||||
@ -1226,6 +1312,9 @@ void Playlist::sort(int column, Qt::SortOrder order) {
|
||||
PlaylistItemList new_items(items_);
|
||||
PlaylistItemList::iterator begin = new_items.begin();
|
||||
|
||||
if (dynamic_playlist_ && current_item_index_.isValid())
|
||||
begin += current_item_index_.row() + 1;
|
||||
|
||||
if (column == Column_Album) {
|
||||
// When sorting by album, also take into account discs and tracks.
|
||||
std::stable_sort(begin, new_items.end(), std::bind(&Playlist::CompareItems, Column_Track, order, _1, _2));
|
||||
@ -1291,7 +1380,7 @@ void Playlist::Save() const {
|
||||
|
||||
if (!backend_ || is_loading_) return;
|
||||
|
||||
backend_->SavePlaylistAsync(id_, items_, last_played_row());
|
||||
backend_->SavePlaylistAsync(id_, items_, last_played_row(), dynamic_playlist_);
|
||||
|
||||
}
|
||||
|
||||
@ -1334,6 +1423,22 @@ void Playlist::ItemsLoaded(QFuture<PlaylistItemList> future) {
|
||||
// The newly loaded list of items might be shorter than it was before so look out for a bad last_played index
|
||||
last_played_item_index_ = p.last_played == -1 || p.last_played >= rowCount() ? QModelIndex() : index(p.last_played);
|
||||
|
||||
if (p.dynamic_type == PlaylistGenerator::Type_Query) {
|
||||
PlaylistGeneratorPtr gen = PlaylistGenerator::Create(p.dynamic_type);
|
||||
if (gen) {
|
||||
|
||||
CollectionBackend *backend = nullptr;
|
||||
if (p.dynamic_backend == collection_->songs_table()) backend = collection_;
|
||||
|
||||
if (backend) {
|
||||
gen->set_collection(backend);
|
||||
gen->Load(p.dynamic_data);
|
||||
TurnOnDynamicPlaylist(gen);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
emit RestoreFinished();
|
||||
|
||||
QSettings s;
|
||||
@ -1572,10 +1677,29 @@ void Playlist::Clear() {
|
||||
undo_stack_->push(new PlaylistUndoCommands::RemoveItems(this, 0, count));
|
||||
}
|
||||
|
||||
TurnOffDynamicPlaylist();
|
||||
|
||||
Save();
|
||||
|
||||
}
|
||||
|
||||
void Playlist::RepopulateDynamicPlaylist() {
|
||||
|
||||
if (!dynamic_playlist_) return;
|
||||
|
||||
RemoveItemsNotInQueue();
|
||||
InsertSmartPlaylist(dynamic_playlist_);
|
||||
|
||||
}
|
||||
|
||||
void Playlist::ExpandDynamicPlaylist() {
|
||||
|
||||
if (!dynamic_playlist_) return;
|
||||
|
||||
InsertDynamicItems(5);
|
||||
|
||||
}
|
||||
|
||||
void Playlist::RemoveItemsNotInQueue() {
|
||||
|
||||
if (queue_->is_empty() && !current_item_index_.isValid()) {
|
||||
@ -1650,6 +1774,9 @@ void Playlist::Shuffle() {
|
||||
begin = 1;
|
||||
}
|
||||
|
||||
if (dynamic_playlist_ && current_item_index_.isValid())
|
||||
begin += current_item_index_.row() + 1;
|
||||
|
||||
const int count = items_.count();
|
||||
for (int i = begin; i < count; ++i) {
|
||||
int new_pos = i + (rand() % (count - i));
|
||||
@ -2036,3 +2163,48 @@ void Playlist::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int Playlist::dynamic_history_length() const {
|
||||
return dynamic_playlist_ && last_played_item_index_.isValid() ? last_played_item_index_.row() + 1 : 0;
|
||||
}
|
||||
|
||||
void Playlist::TurnOffDynamicPlaylist() {
|
||||
|
||||
dynamic_playlist_.reset();
|
||||
|
||||
if (playlist_sequence_) {
|
||||
playlist_sequence_->SetUsingDynamicPlaylist(false);
|
||||
ShuffleModeChanged(playlist_sequence_->shuffle_mode());
|
||||
}
|
||||
|
||||
emit DynamicModeChanged(false);
|
||||
Save();
|
||||
|
||||
}
|
||||
|
||||
void Playlist::RateSong(const QModelIndex &idx, const double rating) {
|
||||
|
||||
if (has_item_at(idx.row())) {
|
||||
PlaylistItemPtr item = item_at(idx.row());
|
||||
if (item && item->IsLocalCollectionItem() && item->Metadata().id() != -1) {
|
||||
collection_->UpdateSongRatingAsync(item->Metadata().id(), rating);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Playlist::RateSongs(const QModelIndexList &index_list, const double rating) {
|
||||
|
||||
QList<int> id_list;
|
||||
for (const QModelIndex &idx : index_list) {
|
||||
const int row = idx.row();
|
||||
if (has_item_at(row)) {
|
||||
PlaylistItemPtr item = item_at(row);
|
||||
if (item && item->IsLocalCollectionItem() && item->Metadata().id() != -1) {
|
||||
id_list << item->Metadata().id();
|
||||
}
|
||||
}
|
||||
}
|
||||
collection_->UpdateSongsRatingAsync(id_list, rating);
|
||||
|
||||
}
|
||||
|
@ -46,6 +46,7 @@
|
||||
#include "covermanager/albumcoverloaderresult.h"
|
||||
#include "playlistitem.h"
|
||||
#include "playlistsequence.h"
|
||||
#include "smartplaylists/playlistgenerator_fwd.h"
|
||||
|
||||
class QMimeData;
|
||||
class QSortFilterProxyModel;
|
||||
@ -128,6 +129,7 @@ class Playlist : public QAbstractListModel {
|
||||
Column_Grouping,
|
||||
Column_Source,
|
||||
Column_Mood,
|
||||
Column_Rating,
|
||||
ColumnCount
|
||||
};
|
||||
|
||||
@ -135,7 +137,8 @@ class Playlist : public QAbstractListModel {
|
||||
Role_IsCurrent = Qt::UserRole + 1,
|
||||
Role_IsPaused,
|
||||
Role_StopAfter,
|
||||
Role_QueuePosition
|
||||
Role_QueuePosition,
|
||||
Role_CanSetRating,
|
||||
};
|
||||
|
||||
enum Path {
|
||||
@ -203,6 +206,8 @@ class Playlist : public QAbstractListModel {
|
||||
const QModelIndex current_index() const;
|
||||
|
||||
bool stop_after_current() const;
|
||||
bool is_dynamic() const { return static_cast<bool>(dynamic_playlist_); }
|
||||
int dynamic_history_length() const;
|
||||
|
||||
QString special_type() const { return special_type_; }
|
||||
void set_special_type(const QString &v) { special_type_ = v; }
|
||||
@ -240,6 +245,7 @@ class Playlist : public QAbstractListModel {
|
||||
void InsertSongs(const SongList &songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
|
||||
void InsertSongsOrCollectionItems(const SongList &songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
|
||||
void InsertInternetItems(InternetService* service, const SongList& songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
|
||||
void InsertSmartPlaylist(PlaylistGeneratorPtr gen, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
|
||||
|
||||
void ReshuffleIndices();
|
||||
|
||||
@ -284,6 +290,10 @@ class Playlist : public QAbstractListModel {
|
||||
|
||||
static bool ComparePathDepths(Qt::SortOrder, PlaylistItemPtr, PlaylistItemPtr);
|
||||
|
||||
// Changes rating of a song to the given value asynchronously
|
||||
void RateSong(const QModelIndex &idx, const double rating);
|
||||
void RateSongs(const QModelIndexList &index_list, const double rating);
|
||||
|
||||
public slots:
|
||||
void set_current_row(const int i, const AutoScroll autoscroll = AutoScroll_Maybe, const bool is_stopping = false);
|
||||
void Paused();
|
||||
@ -309,6 +319,10 @@ class Playlist : public QAbstractListModel {
|
||||
// Removes items with given indices from the playlist. This operation is not undoable.
|
||||
void RemoveItemsWithoutUndo(const QList<int> &indicesIn);
|
||||
|
||||
void ExpandDynamicPlaylist();
|
||||
void RepopulateDynamicPlaylist();
|
||||
void TurnOffDynamicPlaylist();
|
||||
|
||||
signals:
|
||||
void RestoreFinished();
|
||||
void PlaylistLoaded();
|
||||
@ -349,6 +363,9 @@ class Playlist : public QAbstractListModel {
|
||||
// Removes rows with given indices from this playlist.
|
||||
bool removeRows(QList<int> &rows);
|
||||
|
||||
void TurnOnDynamicPlaylist(PlaylistGeneratorPtr gen);
|
||||
void InsertDynamicItems(const int count);
|
||||
|
||||
private slots:
|
||||
void TracksAboutToBeDequeued(const QModelIndex&, const int begin, const int end);
|
||||
void TracksDequeued();
|
||||
@ -412,6 +429,8 @@ class Playlist : public QAbstractListModel {
|
||||
|
||||
int editing_;
|
||||
|
||||
PlaylistGeneratorPtr dynamic_playlist_;
|
||||
|
||||
};
|
||||
|
||||
#endif // PLAYLIST_H
|
||||
|
@ -55,6 +55,7 @@
|
||||
#include "songplaylistitem.h"
|
||||
#include "playlistbackend.h"
|
||||
#include "playlistparsers/cueparser.h"
|
||||
#include "smartplaylists/playlistgenerator.h"
|
||||
|
||||
using std::placeholders::_1;
|
||||
|
||||
@ -121,7 +122,7 @@ PlaylistBackend::PlaylistList PlaylistBackend::GetPlaylists(GetPlaylistsFlags fl
|
||||
}
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare("SELECT ROWID, name, last_played, special_type, ui_path, is_favorite FROM playlists " + condition + " ORDER BY ui_order");
|
||||
q.prepare("SELECT ROWID, name, last_played, special_type, ui_path, is_favorite, dynamic_playlist_type, dynamic_playlist_data, dynamic_playlist_backend FROM playlists " + condition + " ORDER BY ui_order");
|
||||
q.exec();
|
||||
if (db_->CheckErrors(q)) return ret;
|
||||
|
||||
@ -133,6 +134,9 @@ PlaylistBackend::PlaylistList PlaylistBackend::GetPlaylists(GetPlaylistsFlags fl
|
||||
p.special_type = q.value(3).toString();
|
||||
p.ui_path = q.value(4).toString();
|
||||
p.favorite = q.value(5).toBool();
|
||||
p.dynamic_type = PlaylistGenerator::Type(q.value(6).toInt());
|
||||
p.dynamic_data = q.value(7).toByteArray();
|
||||
p.dynamic_backend = q.value(8).toString();
|
||||
ret << p;
|
||||
}
|
||||
|
||||
@ -146,7 +150,7 @@ PlaylistBackend::Playlist PlaylistBackend::GetPlaylist(int id) {
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare("SELECT ROWID, name, last_played, special_type, ui_path, is_favorite FROM playlists WHERE ROWID=:id");
|
||||
q.prepare("SELECT ROWID, name, last_played, special_type, ui_path, is_favorite, dynamic_playlist_type, dynamic_playlist_data, dynamic_playlist_backend FROM playlists WHERE ROWID=:id");
|
||||
|
||||
q.bindValue(":id", id);
|
||||
q.exec();
|
||||
@ -161,6 +165,9 @@ PlaylistBackend::Playlist PlaylistBackend::GetPlaylist(int id) {
|
||||
p.special_type = q.value(3).toString();
|
||||
p.ui_path = q.value(4).toString();
|
||||
p.favorite = q.value(5).toBool();
|
||||
p.dynamic_type = PlaylistGenerator::Type(q.value(6).toInt());
|
||||
p.dynamic_data = q.value(7).toByteArray();
|
||||
p.dynamic_backend = q.value(8).toString();
|
||||
|
||||
return p;
|
||||
|
||||
@ -315,13 +322,13 @@ PlaylistItemPtr PlaylistBackend::RestoreCueData(PlaylistItemPtr item, std::share
|
||||
|
||||
}
|
||||
|
||||
void PlaylistBackend::SavePlaylistAsync(int playlist, const PlaylistItemList &items, int last_played) {
|
||||
void PlaylistBackend::SavePlaylistAsync(int playlist, const PlaylistItemList &items, int last_played, PlaylistGeneratorPtr dynamic) {
|
||||
|
||||
metaObject()->invokeMethod(this, "SavePlaylist", Qt::QueuedConnection, Q_ARG(int, playlist), Q_ARG(PlaylistItemList, items), Q_ARG(int, last_played));
|
||||
metaObject()->invokeMethod(this, "SavePlaylist", Qt::QueuedConnection, Q_ARG(int, playlist), Q_ARG(PlaylistItemList, items), Q_ARG(int, last_played), Q_ARG(PlaylistGeneratorPtr, dynamic));
|
||||
|
||||
}
|
||||
|
||||
void PlaylistBackend::SavePlaylist(int playlist, const PlaylistItemList &items, int last_played) {
|
||||
void PlaylistBackend::SavePlaylist(int playlist, const PlaylistItemList &items, int last_played, PlaylistGeneratorPtr dynamic) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
@ -333,7 +340,7 @@ void PlaylistBackend::SavePlaylist(int playlist, const PlaylistItemList &items,
|
||||
QSqlQuery insert(db);
|
||||
insert.prepare("INSERT INTO playlist_items (playlist, type, collection_id, " + Song::kColumnSpec + ") VALUES (:playlist, :type, :collection_id, " + Song::kBindSpec + ")");
|
||||
QSqlQuery update(db);
|
||||
update.prepare("UPDATE playlists SET last_played=:last_played WHERE ROWID=:playlist");
|
||||
update.prepare("UPDATE playlists SET last_played=:last_played, dynamic_playlist_type=:dynamic_type, dynamic_playlist_data=:dynamic_data, dynamic_playlist_backend=:dynamic_backend WHERE ROWID=:playlist");
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
|
||||
@ -353,6 +360,16 @@ void PlaylistBackend::SavePlaylist(int playlist, const PlaylistItemList &items,
|
||||
|
||||
// Update the last played track number
|
||||
update.bindValue(":last_played", last_played);
|
||||
if (dynamic) {
|
||||
update.bindValue(":dynamic_type", dynamic->type());
|
||||
update.bindValue(":dynamic_data", dynamic->Save());
|
||||
update.bindValue(":dynamic_backend", dynamic->collection()->songs_table());
|
||||
}
|
||||
else {
|
||||
update.bindValue(":dynamic_type", 0);
|
||||
update.bindValue(":dynamic_data", QByteArray());
|
||||
update.bindValue(":dynamic_backend", QString());
|
||||
}
|
||||
update.bindValue(":playlist", playlist);
|
||||
update.exec();
|
||||
if (db_->CheckErrors(update)) return;
|
||||
|
@ -37,6 +37,7 @@
|
||||
#include "core/song.h"
|
||||
#include "collection/sqlrow.h"
|
||||
#include "playlistitem.h"
|
||||
#include "smartplaylists/playlistgenerator.h"
|
||||
|
||||
class QThread;
|
||||
class Application;
|
||||
@ -57,6 +58,9 @@ class PlaylistBackend : public QObject {
|
||||
bool favorite;
|
||||
int last_played;
|
||||
QString special_type;
|
||||
PlaylistGenerator::Type dynamic_type;
|
||||
QString dynamic_backend;
|
||||
QByteArray dynamic_data;
|
||||
};
|
||||
typedef QList<Playlist> PlaylistList;
|
||||
|
||||
@ -77,7 +81,7 @@ class PlaylistBackend : public QObject {
|
||||
void SetPlaylistUiPath(int id, const QString &path);
|
||||
|
||||
int CreatePlaylist(const QString &name, const QString &special_type);
|
||||
void SavePlaylistAsync(int playlist, const PlaylistItemList &items, int last_played);
|
||||
void SavePlaylistAsync(int playlist, const PlaylistItemList &items, int last_played, PlaylistGeneratorPtr dynamic);
|
||||
void RenamePlaylist(int id, const QString &new_name);
|
||||
void FavoritePlaylist(int id, bool is_favorite);
|
||||
void RemovePlaylist(int id);
|
||||
@ -86,7 +90,7 @@ class PlaylistBackend : public QObject {
|
||||
|
||||
public slots:
|
||||
void Exit();
|
||||
void SavePlaylist(int playlist, const PlaylistItemList &items, int last_played);
|
||||
void SavePlaylist(int playlist, const PlaylistItemList &items, int last_played, PlaylistGeneratorPtr dynamic);
|
||||
|
||||
signals:
|
||||
void ExitFinished();
|
||||
|
@ -493,3 +493,40 @@ void SongSourceDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op
|
||||
painter->drawPixmap(draw_rect, pixmap);
|
||||
|
||||
}
|
||||
|
||||
RatingItemDelegate::RatingItemDelegate(QObject *parent) : PlaylistDelegateBase(parent) {}
|
||||
|
||||
void RatingItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &idx) const {
|
||||
|
||||
// Draw the background
|
||||
option.widget->style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, option.widget);
|
||||
|
||||
// Don't draw anything else if the user can't set the rating of this item
|
||||
if (!idx.data(Playlist::Role_CanSetRating).toBool()) return;
|
||||
|
||||
const bool hover = mouse_over_index_.isValid() && (mouse_over_index_ == idx || (selected_indexes_.contains(mouse_over_index_) && selected_indexes_.contains(idx)));
|
||||
|
||||
const double rating = (hover ? RatingPainter::RatingForPos(mouse_over_pos_, option.rect) : idx.data().toDouble());
|
||||
|
||||
painter_.Paint(painter, option.rect, rating);
|
||||
|
||||
}
|
||||
|
||||
QSize RatingItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &idx) const {
|
||||
|
||||
QSize size = PlaylistDelegateBase::sizeHint(option, idx);
|
||||
size.setWidth(size.height() * RatingPainter::kStarCount);
|
||||
return size;
|
||||
|
||||
}
|
||||
|
||||
QString RatingItemDelegate::displayText(const QVariant &value, const QLocale&) const {
|
||||
|
||||
if (value.isNull() || value.toDouble() <= 0) return QString();
|
||||
|
||||
// Round to the nearest 0.5
|
||||
const double rating = float(int(value.toDouble() * RatingPainter::kStarCount * 2 + 0.5)) / 2;
|
||||
|
||||
return QString::number(rating, 'f', 1);
|
||||
|
||||
}
|
||||
|
@ -51,6 +51,7 @@
|
||||
|
||||
#include "playlist.h"
|
||||
#include "core/song.h"
|
||||
#include "widgets/ratingwidget.h"
|
||||
|
||||
class CollectionBackend;
|
||||
class Player;
|
||||
@ -185,4 +186,29 @@ class SongSourceDelegate : public PlaylistDelegateBase {
|
||||
mutable QPixmapCache pixmap_cache_;
|
||||
};
|
||||
|
||||
class RatingItemDelegate : public PlaylistDelegateBase {
|
||||
public:
|
||||
RatingItemDelegate(QObject *parent);
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &idx) const override;
|
||||
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &idx) const override;
|
||||
QString displayText(const QVariant &value, const QLocale &locale) const override;
|
||||
|
||||
void set_mouse_over(const QModelIndex &idx, const QModelIndexList &selected_indexes, const QPoint &pos) {
|
||||
mouse_over_index_ = idx;
|
||||
selected_indexes_ = selected_indexes;
|
||||
mouse_over_pos_ = pos;
|
||||
}
|
||||
|
||||
void set_mouse_out() { mouse_over_index_ = QModelIndex(); }
|
||||
bool is_mouse_over() const { return mouse_over_index_.isValid(); }
|
||||
QModelIndex mouse_over_index() const { return mouse_over_index_; }
|
||||
|
||||
private:
|
||||
RatingPainter painter_;
|
||||
|
||||
QModelIndex mouse_over_index_;
|
||||
QPoint mouse_over_pos_;
|
||||
QModelIndexList selected_indexes_;
|
||||
};
|
||||
|
||||
#endif // PLAYLISTDELEGATES_H
|
||||
|
@ -29,6 +29,7 @@
|
||||
#include <QMenu>
|
||||
#include <QAction>
|
||||
#include <QActionGroup>
|
||||
#include <QSettings>
|
||||
#include <QEvent>
|
||||
#include <QContextMenuEvent>
|
||||
#include <QEnterEvent>
|
||||
@ -36,25 +37,37 @@
|
||||
#include "playlistheader.h"
|
||||
#include "playlistview.h"
|
||||
|
||||
#include "settings/playlistsettingspage.h"
|
||||
|
||||
PlaylistHeader::PlaylistHeader(Qt::Orientation orientation, PlaylistView *view, QWidget *parent)
|
||||
: StretchHeaderView(orientation, parent),
|
||||
view_(view),
|
||||
menu_(new QMenu(this)) {
|
||||
menu_(new QMenu(this)),
|
||||
action_hide_(nullptr),
|
||||
action_reset_(nullptr),
|
||||
action_stretch_(nullptr),
|
||||
action_rating_lock_(nullptr),
|
||||
action_align_left_(nullptr),
|
||||
action_align_center_(nullptr),
|
||||
action_align_right_(nullptr)
|
||||
{
|
||||
|
||||
hide_action_ = menu_->addAction(tr("&Hide..."), this, SLOT(HideCurrent()));
|
||||
stretch_action_ = menu_->addAction(tr("&Stretch columns to fit window"), this, SLOT(ToggleStretchEnabled()));
|
||||
reset_action_ = menu_->addAction(tr("&Reset columns to default"), this, SLOT(ResetColumns()));
|
||||
action_hide_ = menu_->addAction(tr("&Hide..."), this, SLOT(HideCurrent()));
|
||||
action_stretch_ = menu_->addAction(tr("&Stretch columns to fit window"), this, SLOT(ToggleStretchEnabled()));
|
||||
action_reset_ = menu_->addAction(tr("&Reset columns to default"), this, SLOT(ResetColumns()));
|
||||
action_rating_lock_ = menu_->addAction(tr("&Lock rating"), this, SLOT(ToggleRatingEditStatus()));
|
||||
action_rating_lock_->setCheckable(true);
|
||||
menu_->addSeparator();
|
||||
|
||||
QMenu *align_menu = new QMenu(tr("&Align text"), this);
|
||||
QActionGroup *align_group = new QActionGroup(this);
|
||||
align_left_action_ = new QAction(tr("&Left"), align_group);
|
||||
align_center_action_ = new QAction(tr("&Center"), align_group);
|
||||
align_right_action_ = new QAction(tr("&Right"), align_group);
|
||||
action_align_left_ = new QAction(tr("&Left"), align_group);
|
||||
action_align_center_ = new QAction(tr("&Center"), align_group);
|
||||
action_align_right_ = new QAction(tr("&Right"), align_group);
|
||||
|
||||
align_left_action_->setCheckable(true);
|
||||
align_center_action_->setCheckable(true);
|
||||
align_right_action_->setCheckable(true);
|
||||
action_align_left_->setCheckable(true);
|
||||
action_align_center_->setCheckable(true);
|
||||
action_align_right_->setCheckable(true);
|
||||
align_menu->addActions(align_group->actions());
|
||||
|
||||
connect(align_group, SIGNAL(triggered(QAction*)), SLOT(SetColumnAlignment(QAction*)));
|
||||
@ -62,10 +75,15 @@ PlaylistHeader::PlaylistHeader(Qt::Orientation orientation, PlaylistView *view,
|
||||
menu_->addMenu(align_menu);
|
||||
menu_->addSeparator();
|
||||
|
||||
stretch_action_->setCheckable(true);
|
||||
stretch_action_->setChecked(is_stretch_enabled());
|
||||
action_stretch_->setCheckable(true);
|
||||
action_stretch_->setChecked(is_stretch_enabled());
|
||||
|
||||
connect(this, SIGNAL(StretchEnabledChanged(bool)), stretch_action_, SLOT(setChecked(bool)));
|
||||
connect(this, SIGNAL(StretchEnabledChanged(bool)), action_stretch_, SLOT(setChecked(bool)));
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(PlaylistSettingsPage::kSettingsGroup);
|
||||
action_rating_lock_->setChecked(s.value("rating_locked", false).toBool());
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
@ -74,17 +92,21 @@ void PlaylistHeader::contextMenuEvent(QContextMenuEvent *e) {
|
||||
menu_section_ = logicalIndexAt(e->pos());
|
||||
|
||||
if (menu_section_ == -1 || (menu_section_ == logicalIndex(0) && logicalIndex(1) == -1))
|
||||
hide_action_->setVisible(false);
|
||||
action_hide_->setVisible(false);
|
||||
else {
|
||||
hide_action_->setVisible(true);
|
||||
action_hide_->setVisible(true);
|
||||
|
||||
QString title(model()->headerData(menu_section_, Qt::Horizontal).toString());
|
||||
hide_action_->setText(tr("&Hide %1").arg(title));
|
||||
action_hide_->setText(tr("&Hide %1").arg(title));
|
||||
|
||||
Qt::Alignment alignment = view_->column_alignment(menu_section_);
|
||||
if (alignment & Qt::AlignLeft) align_left_action_->setChecked(true);
|
||||
else if (alignment & Qt::AlignHCenter) align_center_action_->setChecked(true);
|
||||
else if (alignment & Qt::AlignRight) align_right_action_->setChecked(true);
|
||||
if (alignment & Qt::AlignLeft) action_align_left_->setChecked(true);
|
||||
else if (alignment & Qt::AlignHCenter) action_align_center_->setChecked(true);
|
||||
else if (alignment & Qt::AlignRight) action_align_right_->setChecked(true);
|
||||
|
||||
// Show rating lock action only for ratings section
|
||||
action_rating_lock_->setVisible(menu_section_ == Playlist::Column_Rating);
|
||||
|
||||
}
|
||||
|
||||
qDeleteAll(show_actions_);
|
||||
@ -126,9 +148,9 @@ void PlaylistHeader::SetColumnAlignment(QAction *action) {
|
||||
|
||||
Qt::Alignment alignment = Qt::AlignVCenter;
|
||||
|
||||
if (action == align_left_action_) alignment |= Qt::AlignLeft;
|
||||
if (action == align_center_action_) alignment |= Qt::AlignHCenter;
|
||||
if (action == align_right_action_) alignment |= Qt::AlignRight;
|
||||
if (action == action_align_left_) alignment |= Qt::AlignLeft;
|
||||
if (action == action_align_center_) alignment |= Qt::AlignHCenter;
|
||||
if (action == action_align_right_) alignment |= Qt::AlignRight;
|
||||
|
||||
view_->SetColumnAlignment(menu_section_, alignment);
|
||||
|
||||
@ -150,3 +172,8 @@ void PlaylistHeader::enterEvent(QEvent*) {
|
||||
void PlaylistHeader::ResetColumns() {
|
||||
view_->ResetHeaderState();
|
||||
}
|
||||
|
||||
void PlaylistHeader::ToggleRatingEditStatus() {
|
||||
emit SectionRatingLockStatusChanged(action_rating_lock_->isChecked());
|
||||
}
|
||||
|
||||
|
@ -54,12 +54,14 @@ class PlaylistHeader : public StretchHeaderView {
|
||||
signals:
|
||||
void SectionVisibilityChanged(int logical, bool visible);
|
||||
void MouseEntered();
|
||||
void SectionRatingLockStatusChanged(bool);
|
||||
|
||||
private slots:
|
||||
void HideCurrent();
|
||||
void ToggleVisible(int section);
|
||||
void ResetColumns();
|
||||
void SetColumnAlignment(QAction *action);
|
||||
void ToggleRatingEditStatus();
|
||||
|
||||
private:
|
||||
void AddColumnAction(int index);
|
||||
@ -69,12 +71,13 @@ class PlaylistHeader : public StretchHeaderView {
|
||||
|
||||
int menu_section_;
|
||||
QMenu *menu_;
|
||||
QAction *hide_action_;
|
||||
QAction *stretch_action_;
|
||||
QAction *reset_action_;
|
||||
QAction *align_left_action_;
|
||||
QAction *align_center_action_;
|
||||
QAction *align_right_action_;
|
||||
QAction *action_hide_;
|
||||
QAction *action_reset_;
|
||||
QAction *action_stretch_;
|
||||
QAction *action_rating_lock_;
|
||||
QAction *action_align_left_;
|
||||
QAction *action_align_center_;
|
||||
QAction *action_align_right_;
|
||||
QList<QAction*> show_actions_;
|
||||
|
||||
};
|
||||
|
@ -96,6 +96,7 @@ void PlaylistManager::Init(CollectionBackend *collection_backend, PlaylistBacken
|
||||
|
||||
connect(collection_backend_, SIGNAL(SongsDiscovered(SongList)), SLOT(SongsDiscovered(SongList)));
|
||||
connect(collection_backend_, SIGNAL(SongsStatisticsChanged(SongList)), SLOT(SongsDiscovered(SongList)));
|
||||
connect(collection_backend_, SIGNAL(SongsRatingChanged(SongList)), SLOT(SongsDiscovered(SongList)));
|
||||
|
||||
for (const PlaylistBackend::Playlist &p : playlist_backend->GetAllOpenPlaylists()) {
|
||||
++playlists_loading_;
|
||||
@ -602,3 +603,26 @@ void PlaylistManager::SetCurrentOrOpen(const int id) {
|
||||
bool PlaylistManager::IsPlaylistOpen(const int id) {
|
||||
return playlists_.contains(id);
|
||||
}
|
||||
|
||||
void PlaylistManager::PlaySmartPlaylist(PlaylistGeneratorPtr generator, bool as_new, bool clear) {
|
||||
|
||||
if (as_new) {
|
||||
New(generator->name());
|
||||
}
|
||||
|
||||
if (clear) {
|
||||
current()->Clear();
|
||||
}
|
||||
|
||||
current()->InsertSmartPlaylist(generator);
|
||||
|
||||
}
|
||||
|
||||
void PlaylistManager::RateCurrentSong(const double rating) {
|
||||
active()->RateSong(active()->current_index(), rating);
|
||||
}
|
||||
|
||||
void PlaylistManager::RateCurrentSong(const int rating) {
|
||||
RateCurrentSong(rating / 5.0);
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,7 @@
|
||||
|
||||
#include "core/song.h"
|
||||
#include "playlist.h"
|
||||
#include "smartplaylists/playlistgenerator.h"
|
||||
|
||||
class QModelIndex;
|
||||
|
||||
@ -76,6 +77,8 @@ class PlaylistManagerInterface : public QObject {
|
||||
virtual PlaylistParser *parser() const = 0;
|
||||
virtual PlaylistContainer *playlist_container() const = 0;
|
||||
|
||||
virtual void PlaySmartPlaylist(PlaylistGeneratorPtr generator, const bool as_new, const bool clear) = 0;
|
||||
|
||||
public slots:
|
||||
virtual void New(const QString &name, const SongList& songs = SongList(), const QString &special_type = QString()) = 0;
|
||||
virtual void Load(const QString &filename) = 0;
|
||||
@ -103,6 +106,11 @@ class PlaylistManagerInterface : public QObject {
|
||||
virtual void SetActivePaused() = 0;
|
||||
virtual void SetActiveStopped() = 0;
|
||||
|
||||
// Rate current song using 0.0 - 1.0 scale.
|
||||
virtual void RateCurrentSong(const double rating) = 0;
|
||||
// Rate current song using 0 - 5 scale.
|
||||
virtual void RateCurrentSong(const int rating) = 0;
|
||||
|
||||
signals:
|
||||
void PlaylistManagerInitialized();
|
||||
void AllPlaylistsLoaded();
|
||||
@ -196,7 +204,6 @@ class PlaylistManager : public PlaylistManagerInterface {
|
||||
void ShuffleCurrent() override;
|
||||
void RemoveDuplicatesCurrent() override;
|
||||
void RemoveUnavailableCurrent() override;
|
||||
//void SetActiveStreamMetadata(const QUrl& url, const Song& song);
|
||||
|
||||
void SongChangeRequestProcessed(const QUrl& url, const bool valid) override;
|
||||
|
||||
@ -207,6 +214,13 @@ class PlaylistManager : public PlaylistManagerInterface {
|
||||
// Remove the current playing song
|
||||
void RemoveCurrentSong();
|
||||
|
||||
void PlaySmartPlaylist(PlaylistGeneratorPtr generator, const bool as_new, const bool clear) override;
|
||||
|
||||
// Rate current song using 0.0 - 1.0 scale.
|
||||
void RateCurrentSong(const double rating) override;
|
||||
// Rate current song using 0 - 5 scale.
|
||||
void RateCurrentSong(const int rating) override;
|
||||
|
||||
private slots:
|
||||
void SetActivePlaying() override;
|
||||
void SetActivePaused() override;
|
||||
|
@ -47,7 +47,8 @@ PlaylistSequence::PlaylistSequence(QWidget *parent, SettingsProvider *settings)
|
||||
shuffle_menu_(new QMenu(this)),
|
||||
loading_(false),
|
||||
repeat_mode_(Repeat_Off),
|
||||
shuffle_mode_(Shuffle_Off)
|
||||
shuffle_mode_(Shuffle_Off),
|
||||
dynamic_(false)
|
||||
{
|
||||
|
||||
ui_->setupUi(this);
|
||||
@ -161,7 +162,7 @@ void PlaylistSequence::ShuffleActionTriggered(QAction *action) {
|
||||
|
||||
}
|
||||
|
||||
void PlaylistSequence::SetRepeatMode(RepeatMode mode) {
|
||||
void PlaylistSequence::SetRepeatMode(const RepeatMode mode) {
|
||||
|
||||
ui_->repeat->setChecked(mode != Repeat_Off);
|
||||
|
||||
@ -184,7 +185,7 @@ void PlaylistSequence::SetRepeatMode(RepeatMode mode) {
|
||||
|
||||
}
|
||||
|
||||
void PlaylistSequence::SetShuffleMode(ShuffleMode mode) {
|
||||
void PlaylistSequence::SetShuffleMode(const ShuffleMode mode) {
|
||||
|
||||
ui_->shuffle->setChecked(mode != Shuffle_Off);
|
||||
|
||||
@ -204,12 +205,23 @@ void PlaylistSequence::SetShuffleMode(ShuffleMode mode) {
|
||||
|
||||
}
|
||||
|
||||
void PlaylistSequence::SetUsingDynamicPlaylist(const bool dynamic) {
|
||||
|
||||
dynamic_ = dynamic;
|
||||
const QString not_available(tr("Not available while using a dynamic playlist"));
|
||||
|
||||
setEnabled(!dynamic);
|
||||
ui_->shuffle->setToolTip(dynamic ? not_available : tr("Shuffle"));
|
||||
ui_->repeat->setToolTip(dynamic ? not_available : tr("Repeat"));
|
||||
|
||||
}
|
||||
|
||||
PlaylistSequence::ShuffleMode PlaylistSequence::shuffle_mode() const {
|
||||
return shuffle_mode_;
|
||||
return dynamic_ ? Shuffle_Off : shuffle_mode_;
|
||||
}
|
||||
|
||||
PlaylistSequence::RepeatMode PlaylistSequence::repeat_mode() const {
|
||||
return repeat_mode_;
|
||||
return dynamic_ ? Repeat_Off : repeat_mode_;
|
||||
}
|
||||
|
||||
//called from global shortcut
|
||||
|
@ -68,14 +68,15 @@ class PlaylistSequence : public QWidget {
|
||||
QMenu *shuffle_menu() const { return shuffle_menu_; }
|
||||
|
||||
public slots:
|
||||
void SetRepeatMode(PlaylistSequence::RepeatMode mode);
|
||||
void SetShuffleMode(PlaylistSequence::ShuffleMode mode);
|
||||
void SetRepeatMode(const PlaylistSequence::RepeatMode mode);
|
||||
void SetShuffleMode(const PlaylistSequence::ShuffleMode mode);
|
||||
void CycleShuffleMode();
|
||||
void CycleRepeatMode();
|
||||
void SetUsingDynamicPlaylist(const bool dynamic);
|
||||
|
||||
signals:
|
||||
void RepeatModeChanged(PlaylistSequence::RepeatMode mode);
|
||||
void ShuffleModeChanged(PlaylistSequence::ShuffleMode mode);
|
||||
void RepeatModeChanged(const PlaylistSequence::RepeatMode mode);
|
||||
void ShuffleModeChanged(const PlaylistSequence::ShuffleMode mode);
|
||||
|
||||
private slots:
|
||||
void RepeatActionTriggered(QAction *);
|
||||
@ -97,7 +98,7 @@ class PlaylistSequence : public QWidget {
|
||||
bool loading_;
|
||||
RepeatMode repeat_mode_;
|
||||
ShuffleMode shuffle_mode_;
|
||||
|
||||
bool dynamic_;
|
||||
};
|
||||
|
||||
#endif // PLAYLISTSEQUENCE_H
|
||||
|
@ -78,6 +78,7 @@
|
||||
#include "covermanager/albumcoverloaderresult.h"
|
||||
#include "settings/appearancesettingspage.h"
|
||||
#include "settings/playlistsettingspage.h"
|
||||
#include "dynamicplaylistcontrols.h"
|
||||
|
||||
#ifdef HAVE_MOODBAR
|
||||
# include "moodbar/moodbaritemdelegate.h"
|
||||
@ -168,7 +169,11 @@ PlaylistView::PlaylistView(QWidget *parent)
|
||||
cached_current_row_row_(-1),
|
||||
drop_indicator_row_(-1),
|
||||
drag_over_(false),
|
||||
column_alignment_(DefaultColumnAlignment()) {
|
||||
header_state_version_(1),
|
||||
column_alignment_(DefaultColumnAlignment()),
|
||||
rating_locked_(false),
|
||||
dynamic_controls_(new DynamicPlaylistControls(this)),
|
||||
rating_delegate_(nullptr) {
|
||||
|
||||
setHeader(header_);
|
||||
header_->setSectionsMovable(true);
|
||||
@ -195,6 +200,9 @@ PlaylistView::PlaylistView(QWidget *parent)
|
||||
connect(header_, SIGNAL(SectionVisibilityChanged(int, bool)), SLOT(InvalidateCachedCurrentPixmap()));
|
||||
connect(header_, SIGNAL(StretchEnabledChanged(bool)), SLOT(StretchChanged(bool)));
|
||||
|
||||
connect(header_, SIGNAL(SectionRatingLockStatusChanged(bool)), SLOT(SetRatingLockStatus(bool)));
|
||||
connect(header_, SIGNAL(MouseEntered()), SLOT(RatingHoverOut()));
|
||||
|
||||
inhibit_autoscroll_timer_->setInterval(kAutoscrollGraceTimeout * 1000);
|
||||
inhibit_autoscroll_timer_->setSingleShot(true);
|
||||
connect(inhibit_autoscroll_timer_, SIGNAL(timeout()), SLOT(InhibitAutoscrollTimeout()));
|
||||
@ -202,6 +210,8 @@ PlaylistView::PlaylistView(QWidget *parent)
|
||||
horizontalScrollBar()->installEventFilter(this);
|
||||
verticalScrollBar()->installEventFilter(this);
|
||||
|
||||
dynamic_controls_->hide();
|
||||
|
||||
// For fading
|
||||
connect(fade_animation_, SIGNAL(valueChanged(qreal)), SLOT(FadePreviousBackgroundImage(qreal)));
|
||||
fade_animation_->setDirection(QTimeLine::Backward); // 1.0 -> 0.0
|
||||
@ -258,12 +268,16 @@ void PlaylistView::SetItemDelegates() {
|
||||
setItemDelegateForColumn(Playlist::Column_Mood, new MoodbarItemDelegate(app_, this, this));
|
||||
#endif
|
||||
|
||||
rating_delegate_ = new RatingItemDelegate(this);
|
||||
setItemDelegateForColumn(Playlist::Column_Rating, rating_delegate_);
|
||||
|
||||
}
|
||||
|
||||
void PlaylistView::setModel(QAbstractItemModel *m) {
|
||||
|
||||
if (model()) {
|
||||
disconnect(model(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(InvalidateCachedCurrentPixmap()));
|
||||
disconnect(model(), SIGNAL(layoutAboutToBeChanged()), this, SLOT(RatingHoverOut()));
|
||||
|
||||
// When changing the model, always invalidate the current pixmap.
|
||||
// If a remote client uses "stop after", without invaliding the stop mark would not appear.
|
||||
@ -273,6 +287,7 @@ void PlaylistView::setModel(QAbstractItemModel *m) {
|
||||
QTreeView::setModel(m);
|
||||
|
||||
connect(model(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(InvalidateCachedCurrentPixmap()));
|
||||
connect(model(), SIGNAL(layoutAboutToBeChanged()), this, SLOT(RatingHoverOut()));
|
||||
|
||||
}
|
||||
|
||||
@ -282,10 +297,16 @@ void PlaylistView::SetPlaylist(Playlist *playlist) {
|
||||
disconnect(playlist_, SIGNAL(MaybeAutoscroll(Playlist::AutoScroll)), this, SLOT(MaybeAutoscroll(Playlist::AutoScroll)));
|
||||
disconnect(playlist_, SIGNAL(destroyed()), this, SLOT(PlaylistDestroyed()));
|
||||
disconnect(playlist_, SIGNAL(QueueChanged()), this, SLOT(update()));
|
||||
|
||||
disconnect(playlist_, SIGNAL(DynamicModeChanged(bool)), this, SLOT(DynamicModeChanged(bool)));
|
||||
disconnect(dynamic_controls_, SIGNAL(Expand()), playlist_, SLOT(ExpandDynamicPlaylist()));
|
||||
disconnect(dynamic_controls_, SIGNAL(Repopulate()), playlist_, SLOT(RepopulateDynamicPlaylist()));
|
||||
disconnect(dynamic_controls_, SIGNAL(TurnOff()), playlist_, SLOT(TurnOffDynamicPlaylist()));
|
||||
}
|
||||
|
||||
playlist_ = playlist;
|
||||
RestoreHeaderState();
|
||||
DynamicModeChanged(playlist->is_dynamic());
|
||||
setFocus();
|
||||
JumpToLastPlayedTrack();
|
||||
|
||||
@ -294,13 +315,21 @@ void PlaylistView::SetPlaylist(Playlist *playlist) {
|
||||
connect(playlist_, SIGNAL(destroyed()), SLOT(PlaylistDestroyed()));
|
||||
connect(playlist_, SIGNAL(QueueChanged()), SLOT(update()));
|
||||
|
||||
connect(playlist_, SIGNAL(DynamicModeChanged(bool)), SLOT(DynamicModeChanged(bool)));
|
||||
connect(dynamic_controls_, SIGNAL(Expand()), playlist_, SLOT(ExpandDynamicPlaylist()));
|
||||
connect(dynamic_controls_, SIGNAL(Repopulate()), playlist_, SLOT(RepopulateDynamicPlaylist()));
|
||||
connect(dynamic_controls_, SIGNAL(TurnOff()), playlist_, SLOT(TurnOffDynamicPlaylist()));
|
||||
|
||||
}
|
||||
|
||||
void PlaylistView::LoadHeaderState() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(Playlist::kSettingsGroup);
|
||||
if (s.contains("state")) header_state_ = s.value("state").toByteArray();
|
||||
if (s.contains("state")) {
|
||||
header_state_version_ = s.value("state_version", 0).toInt();
|
||||
header_state_ = s.value("state").toByteArray();
|
||||
}
|
||||
if (s.contains("column_alignments")) column_alignment_ = s.value("column_alignments").value<ColumnAlignmentMap>();
|
||||
s.endGroup();
|
||||
|
||||
@ -322,6 +351,7 @@ void PlaylistView::SetHeaderState() {
|
||||
void PlaylistView::ResetHeaderState() {
|
||||
|
||||
set_initial_header_layout_ = true;
|
||||
header_state_version_ = 1;
|
||||
header_state_ = header_->ResetState();
|
||||
RestoreHeaderState();
|
||||
|
||||
@ -355,6 +385,7 @@ void PlaylistView::RestoreHeaderState() {
|
||||
header_->HideSection(Playlist::Column_Comment);
|
||||
header_->HideSection(Playlist::Column_Grouping);
|
||||
header_->HideSection(Playlist::Column_Mood);
|
||||
header_->HideSection(Playlist::Column_Rating);
|
||||
|
||||
header_->moveSection(header_->visualIndex(Playlist::Column_Track), 0);
|
||||
|
||||
@ -378,6 +409,11 @@ void PlaylistView::RestoreHeaderState() {
|
||||
|
||||
}
|
||||
|
||||
if (header_state_version_ < 1) {
|
||||
header_->HideSection(Playlist::Column_Rating);
|
||||
header_state_version_ = 1;
|
||||
}
|
||||
|
||||
// Make sure at least one column is visible
|
||||
bool all_hidden = true;
|
||||
for (int i = 0; i < header_->count(); ++i) {
|
||||
@ -744,6 +780,17 @@ void PlaylistView::closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHi
|
||||
|
||||
void PlaylistView::mouseMoveEvent(QMouseEvent *event) {
|
||||
|
||||
// Check whether rating section is locked by user or not
|
||||
if (!rating_locked_) {
|
||||
QModelIndex idx = indexAt(event->pos());
|
||||
if (idx.isValid() && idx.data(Playlist::Role_CanSetRating).toBool()) {
|
||||
RatingHoverIn(idx, event->pos());
|
||||
}
|
||||
else if (rating_delegate_->is_mouse_over()) {
|
||||
RatingHoverOut();
|
||||
}
|
||||
}
|
||||
|
||||
if (!drag_over_) {
|
||||
QTreeView::mouseMoveEvent(event);
|
||||
}
|
||||
@ -752,6 +799,10 @@ void PlaylistView::mouseMoveEvent(QMouseEvent *event) {
|
||||
|
||||
void PlaylistView::leaveEvent(QEvent *e) {
|
||||
|
||||
if (rating_delegate_->is_mouse_over() && !rating_locked_) {
|
||||
RatingHoverOut();
|
||||
}
|
||||
|
||||
QTreeView::leaveEvent(e);
|
||||
|
||||
}
|
||||
@ -764,19 +815,47 @@ void PlaylistView::mousePressEvent(QMouseEvent *event) {
|
||||
}
|
||||
|
||||
QModelIndex idx = indexAt(event->pos());
|
||||
if (event->button() == Qt::XButton1 && idx.isValid()) {
|
||||
app_->player()->Previous();
|
||||
}
|
||||
else if (event->button() == Qt::XButton2 && idx.isValid()) {
|
||||
app_->player()->Next();
|
||||
}
|
||||
else {
|
||||
QTreeView::mousePressEvent(event);
|
||||
if (idx.isValid()) {
|
||||
switch (event->button()) {
|
||||
case Qt::XButton1:
|
||||
app_->player()->Previous();
|
||||
break;
|
||||
case Qt::XButton2:
|
||||
app_->player()->Next();
|
||||
break;
|
||||
case Qt::LeftButton:{
|
||||
if (idx.data(Playlist::Role_CanSetRating).toBool() && !rating_locked_) {
|
||||
// Calculate which star was clicked
|
||||
double new_rating = RatingPainter::RatingForPos(event->pos(), visualRect(idx));
|
||||
if (selectedIndexes().contains(idx)) {
|
||||
// Update all the selected item ratings
|
||||
QModelIndexList src_index_list;
|
||||
for (const QModelIndex &i : selectedIndexes()) {
|
||||
if (i.data(Playlist::Role_CanSetRating).toBool()) {
|
||||
src_index_list << playlist_->proxy()->mapToSource(i);
|
||||
}
|
||||
}
|
||||
if (!src_index_list.isEmpty()) {
|
||||
playlist_->RateSongs(src_index_list, new_rating);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Update only this item rating
|
||||
playlist_->RateSong(playlist_->proxy()->mapToSource(idx), new_rating);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
inhibit_autoscroll_ = true;
|
||||
inhibit_autoscroll_timer_->start();
|
||||
|
||||
QTreeView::mousePressEvent(event);
|
||||
|
||||
}
|
||||
|
||||
void PlaylistView::scrollContentsBy(int dx, int dy) {
|
||||
@ -1150,8 +1229,10 @@ void PlaylistView::SaveSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(Playlist::kSettingsGroup);
|
||||
s.setValue("state_version", header_state_version_);
|
||||
s.setValue("state", header_->SaveState());
|
||||
s.setValue("column_alignments", QVariant::fromValue<ColumnAlignmentMap>(column_alignment_));
|
||||
s.setValue("rating_locked", rating_locked_);
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
@ -1165,6 +1246,15 @@ void PlaylistView::StretchChanged(const bool stretch) {
|
||||
|
||||
}
|
||||
|
||||
void PlaylistView::resizeEvent(QResizeEvent *e) {
|
||||
|
||||
QTreeView::resizeEvent(e);
|
||||
if (dynamic_controls_->isVisible()) {
|
||||
RepositionDynamicControls();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool PlaylistView::eventFilter(QObject *object, QEvent *event) {
|
||||
|
||||
if (event->type() == QEvent::Enter && (object == horizontalScrollBar() || object == verticalScrollBar())) {
|
||||
@ -1357,3 +1447,75 @@ void PlaylistView::focusInEvent(QFocusEvent *event) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PlaylistView::DynamicModeChanged(bool dynamic) {
|
||||
|
||||
if (!dynamic) {
|
||||
dynamic_controls_->hide();
|
||||
}
|
||||
else {
|
||||
RepositionDynamicControls();
|
||||
dynamic_controls_->show();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PlaylistView::RepositionDynamicControls() {
|
||||
|
||||
dynamic_controls_->resize(dynamic_controls_->sizeHint());
|
||||
dynamic_controls_->move((width() - dynamic_controls_->width()) / 2, height() - dynamic_controls_->height() - 20);
|
||||
|
||||
}
|
||||
|
||||
void PlaylistView::SetRatingLockStatus(const bool state) {
|
||||
|
||||
if (!header_state_loaded_) return;
|
||||
|
||||
rating_locked_ = state;
|
||||
|
||||
}
|
||||
|
||||
void PlaylistView::RatingHoverIn(const QModelIndex &idx, const QPoint &pos) {
|
||||
|
||||
if (editTriggers() & QAbstractItemView::NoEditTriggers) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QModelIndex old_index = rating_delegate_->mouse_over_index();
|
||||
rating_delegate_->set_mouse_over(idx, selectedIndexes(), pos);
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
|
||||
update(idx);
|
||||
update(old_index);
|
||||
for (const QModelIndex &i : selectedIndexes()) {
|
||||
if (i.column() == Playlist::Column_Rating) update(i);
|
||||
}
|
||||
|
||||
if (idx.data(Playlist::Role_IsCurrent).toBool() || old_index.data(Playlist::Role_IsCurrent).toBool()) {
|
||||
InvalidateCachedCurrentPixmap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PlaylistView::RatingHoverOut() {
|
||||
|
||||
if (editTriggers() & QAbstractItemView::NoEditTriggers) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QModelIndex old_index = rating_delegate_->mouse_over_index();
|
||||
rating_delegate_->set_mouse_out();
|
||||
setCursor(QCursor());
|
||||
|
||||
update(old_index);
|
||||
for (const QModelIndex &i : selectedIndexes()) {
|
||||
if (i.column() == Playlist::Column_Rating) {
|
||||
update(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (old_index.data(Playlist::Role_IsCurrent).toBool()) {
|
||||
InvalidateCachedCurrentPixmap();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -72,6 +72,8 @@ class QTimerEvent;
|
||||
class Application;
|
||||
class CollectionBackend;
|
||||
class PlaylistHeader;
|
||||
class DynamicPlaylistControls;
|
||||
class RatingItemDelegate;
|
||||
|
||||
// This proxy style works around a bug/feature introduced in Qt 4.7's QGtkStyle
|
||||
// that uses Gtk to paint row backgrounds, ignoring any custom brush or palette the caller set in the QStyleOption.
|
||||
@ -148,6 +150,7 @@ class PlaylistView : public QTreeView {
|
||||
void dropEvent(QDropEvent *event) override;
|
||||
bool eventFilter(QObject *object, QEvent *event) override;
|
||||
void focusInEvent(QFocusEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
// QTreeView
|
||||
void drawTree(QPainter *painter, const QRegion ®ion) const;
|
||||
@ -177,6 +180,10 @@ class PlaylistView : public QTreeView {
|
||||
void Stopped();
|
||||
void SongChanged(const Song &song);
|
||||
void AlbumCoverLoaded(const Song &song, AlbumCoverLoaderResult result = AlbumCoverLoaderResult());
|
||||
void DynamicModeChanged(const bool dynamic);
|
||||
void SetRatingLockStatus(const bool state);
|
||||
void RatingHoverIn(const QModelIndex &idx, const QPoint &pos);
|
||||
void RatingHoverOut();
|
||||
|
||||
private:
|
||||
void LoadHeaderState();
|
||||
@ -206,6 +213,8 @@ class PlaylistView : public QTreeView {
|
||||
QModelIndex NextEditableIndex(const QModelIndex ¤t);
|
||||
QModelIndex PrevEditableIndex(const QModelIndex ¤t);
|
||||
|
||||
void RepositionDynamicControls();
|
||||
|
||||
Application *app_;
|
||||
PlaylistProxyStyle *style_;
|
||||
Playlist *playlist_;
|
||||
@ -275,11 +284,16 @@ class PlaylistView : public QTreeView {
|
||||
int drop_indicator_row_;
|
||||
bool drag_over_;
|
||||
|
||||
int header_state_version_;
|
||||
QByteArray header_state_;
|
||||
ColumnAlignmentMap column_alignment_;
|
||||
bool rating_locked_;
|
||||
|
||||
Song song_playing_;
|
||||
|
||||
DynamicPlaylistControls *dynamic_controls_;
|
||||
RatingItemDelegate *rating_delegate_;
|
||||
|
||||
};
|
||||
|
||||
#endif // PLAYLISTVIEW_H
|
||||
|
196
src/qobuz/qobuzbaserequest.cpp
Normal file
@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019, 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 <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QPair>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QSslError>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "qobuzservice.h"
|
||||
#include "qobuzbaserequest.h"
|
||||
|
||||
const char *QobuzBaseRequest::kApiUrl = "https://www.qobuz.com/api.json/0.2";
|
||||
|
||||
QobuzBaseRequest::QobuzBaseRequest(QobuzService *service, NetworkAccessManager *network, QObject *parent) :
|
||||
QObject(parent),
|
||||
service_(service),
|
||||
network_(network)
|
||||
{}
|
||||
|
||||
QobuzBaseRequest::~QobuzBaseRequest() {}
|
||||
|
||||
QNetworkReply *QobuzBaseRequest::CreateRequest(const QString &ressource_name, const QList<Param> ¶ms_provided) {
|
||||
|
||||
ParamList params = ParamList() << params_provided
|
||||
<< Param("app_id", app_id());
|
||||
|
||||
std::sort(params.begin(), params.end());
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QUrl url(kApiUrl + QString("/") + ressource_name);
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest req(url);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
#else
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
#endif
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
req.setRawHeader("X-App-Id", app_id().toUtf8());
|
||||
if (authenticated()) req.setRawHeader("X-User-Auth-Token", user_auth_token().toUtf8());
|
||||
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
connect(reply, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(HandleSSLErrors(QList<QSslError>)));
|
||||
|
||||
qLog(Debug) << "Qobuz: Sending request" << url;
|
||||
|
||||
return reply;
|
||||
|
||||
}
|
||||
|
||||
void QobuzBaseRequest::HandleSSLErrors(QList<QSslError> ssl_errors) {
|
||||
|
||||
for (QSslError &ssl_error : ssl_errors) {
|
||||
Error(ssl_error.errorString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QByteArray QobuzBaseRequest::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
QByteArray data;
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
|
||||
data = reply->readAll();
|
||||
}
|
||||
else {
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
}
|
||||
else {
|
||||
// See if there is Json data containing "status", "code" and "message" - then use that instead.
|
||||
data = reply->readAll();
|
||||
QString error;
|
||||
QJsonParseError parse_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error);
|
||||
if (parse_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("code") && json_obj.contains("message")) {
|
||||
QString status = json_obj["status"].toString();
|
||||
int code = json_obj["code"].toInt();
|
||||
QString message = json_obj["message"].toString();
|
||||
error = QString("%1 (%2)").arg(message).arg(code);
|
||||
}
|
||||
}
|
||||
if (error.isEmpty()) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
else {
|
||||
error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
|
||||
}
|
||||
}
|
||||
Error(error);
|
||||
}
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
QJsonObject QobuzBaseRequest::ExtractJsonObj(QByteArray &data) {
|
||||
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
|
||||
if (json_error.error != QJsonParseError::NoError) {
|
||||
Error("Reply from server missing Json data.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
Error("Received empty Json document.", data);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
Error("Json document is not an object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
Error("Received empty Json object.", json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
return json_obj;
|
||||
|
||||
}
|
||||
|
||||
QJsonValue QobuzBaseRequest::ExtractItems(QByteArray &data) {
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(data);
|
||||
if (json_obj.isEmpty()) return QJsonValue();
|
||||
return ExtractItems(json_obj);
|
||||
|
||||
}
|
||||
|
||||
QJsonValue QobuzBaseRequest::ExtractItems(QJsonObject &json_obj) {
|
||||
|
||||
if (!json_obj.contains("items")) {
|
||||
Error("Json reply is missing items.", json_obj);
|
||||
return QJsonArray();
|
||||
}
|
||||
QJsonValue json_items = json_obj["items"];
|
||||
return json_items;
|
||||
|
||||
}
|
||||
|
||||
QString QobuzBaseRequest::ErrorsToHTML(const QStringList &errors) {
|
||||
|
||||
QString error_html;
|
||||
for (const QString &error : errors) {
|
||||
error_html += error + "<br />";
|
||||
}
|
||||
return error_html;
|
||||
|
||||
}
|
105
src/qobuz/qobuzbaserequest.h
Normal file
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019, 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 QOBUZBASEREQUEST_H
|
||||
#define QOBUZBASEREQUEST_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "qobuzservice.h"
|
||||
|
||||
class QNetworkReply;
|
||||
class NetworkAccessManager;
|
||||
|
||||
class QobuzBaseRequest : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
enum QueryType {
|
||||
QueryType_None,
|
||||
QueryType_Artists,
|
||||
QueryType_Albums,
|
||||
QueryType_Songs,
|
||||
QueryType_SearchArtists,
|
||||
QueryType_SearchAlbums,
|
||||
QueryType_SearchSongs,
|
||||
QueryType_StreamURL,
|
||||
};
|
||||
|
||||
explicit QobuzBaseRequest(QobuzService *service, NetworkAccessManager *network, QObject *parent);
|
||||
~QobuzBaseRequest();
|
||||
|
||||
typedef QPair<QString, QString> Param;
|
||||
typedef QList<Param> ParamList;
|
||||
|
||||
static const char *kApiUrl;
|
||||
|
||||
QNetworkReply *CreateRequest(const QString &ressource_name, const QList<Param> ¶ms_provided);
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(QByteArray &data);
|
||||
QJsonValue ExtractItems(QByteArray &data);
|
||||
QJsonValue ExtractItems(QJsonObject &json_obj);
|
||||
|
||||
virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0;
|
||||
QString ErrorsToHTML(const QStringList &errors);
|
||||
|
||||
QString api_url() { return QString(kApiUrl); }
|
||||
QString app_id() { return service_->app_id(); }
|
||||
QString app_secret() { return service_->app_secret(); }
|
||||
QString username() { return service_->username(); }
|
||||
QString password() { return service_->password(); }
|
||||
int format() { return service_->format(); }
|
||||
int artistssearchlimit() { return service_->artistssearchlimit(); }
|
||||
int albumssearchlimit() { return service_->albumssearchlimit(); }
|
||||
int songssearchlimit() { return service_->songssearchlimit(); }
|
||||
|
||||
qint64 user_id() { return service_->user_id(); }
|
||||
QString user_auth_token() { return service_->user_auth_token(); }
|
||||
QString device_id() { return service_->device_id(); }
|
||||
qint64 credential_id() { return service_->credential_id(); }
|
||||
|
||||
bool authenticated() { return service_->authenticated(); }
|
||||
bool login_sent() { return service_->login_sent(); }
|
||||
int max_login_attempts() { return service_->max_login_attempts(); }
|
||||
int login_attempts() { return service_->login_attempts(); }
|
||||
|
||||
private slots:
|
||||
void HandleSSLErrors(QList<QSslError> ssl_errors);
|
||||
|
||||
private:
|
||||
QobuzService *service_;
|
||||
NetworkAccessManager *network_;
|
||||
|
||||
};
|
||||
|
||||
#endif // QOBUZBASEREQUEST_H
|
277
src/qobuz/qobuzfavoriterequest.cpp
Normal file
@ -0,0 +1,277 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019, 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 <QPair>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkReply>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "core/song.h"
|
||||
#include "qobuzservice.h"
|
||||
#include "qobuzbaserequest.h"
|
||||
#include "qobuzfavoriterequest.h"
|
||||
|
||||
QobuzFavoriteRequest::QobuzFavoriteRequest(QobuzService *service, NetworkAccessManager *network, QObject *parent)
|
||||
: QobuzBaseRequest(service, network, parent),
|
||||
service_(service),
|
||||
network_(network) {}
|
||||
|
||||
QobuzFavoriteRequest::~QobuzFavoriteRequest() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QString QobuzFavoriteRequest::FavoriteText(const FavoriteType type) {
|
||||
|
||||
switch (type) {
|
||||
case FavoriteType_Artists:
|
||||
return "artists";
|
||||
case FavoriteType_Albums:
|
||||
return "albums";
|
||||
case FavoriteType_Songs:
|
||||
default:
|
||||
return "tracks";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void QobuzFavoriteRequest::AddArtists(const SongList &songs) {
|
||||
AddFavorites(FavoriteType_Artists, songs);
|
||||
}
|
||||
|
||||
void QobuzFavoriteRequest::AddAlbums(const SongList &songs) {
|
||||
AddFavorites(FavoriteType_Albums, songs);
|
||||
}
|
||||
|
||||
void QobuzFavoriteRequest::AddSongs(const SongList &songs) {
|
||||
AddFavorites(FavoriteType_Songs, songs);
|
||||
}
|
||||
|
||||
void QobuzFavoriteRequest::AddFavorites(const FavoriteType type, const SongList &songs) {
|
||||
|
||||
if (songs.isEmpty()) return;
|
||||
|
||||
QString text;
|
||||
switch (type) {
|
||||
case FavoriteType_Artists:
|
||||
text = "artist_ids";
|
||||
break;
|
||||
case FavoriteType_Albums:
|
||||
text = "album_ids";
|
||||
break;
|
||||
case FavoriteType_Songs:
|
||||
text = "track_ids";
|
||||
break;
|
||||
}
|
||||
|
||||
QStringList ids_list;
|
||||
for (const Song &song : songs) {
|
||||
QString id;
|
||||
switch (type) {
|
||||
case FavoriteType_Artists:
|
||||
if (song.artist_id().isEmpty()) continue;
|
||||
id = song.artist_id();
|
||||
break;
|
||||
case FavoriteType_Albums:
|
||||
if (song.album_id().isEmpty()) continue;
|
||||
id = song.album_id();
|
||||
break;
|
||||
case FavoriteType_Songs:
|
||||
if (song.song_id().isEmpty()) continue;
|
||||
id = song.song_id();
|
||||
break;
|
||||
}
|
||||
if (id.isEmpty()) continue;
|
||||
if (!ids_list.contains(id)) {
|
||||
ids_list << id;
|
||||
}
|
||||
}
|
||||
if (ids_list.isEmpty()) return;
|
||||
|
||||
QString ids = ids_list.join(',');
|
||||
|
||||
ParamList params = ParamList() << Param("app_id", app_id())
|
||||
<< Param("user_auth_token", user_auth_token())
|
||||
<< Param(text, ids);
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QNetworkReply *reply = CreateRequest("favorite/create", params);
|
||||
connect(reply, &QNetworkReply::finished, [=] { AddFavoritesReply(reply, type, songs); });
|
||||
replies_ << reply;
|
||||
|
||||
}
|
||||
|
||||
void QobuzFavoriteRequest::AddFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs) {
|
||||
|
||||
if (replies_.contains(reply)) {
|
||||
replies_.removeAll(reply);
|
||||
reply->deleteLater();
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
return;
|
||||
}
|
||||
|
||||
qLog(Debug) << "Qobuz:" << songs.count() << "songs added to" << FavoriteText(type) << "favorites.";
|
||||
|
||||
switch (type) {
|
||||
case FavoriteType_Artists:
|
||||
emit ArtistsAdded(songs);
|
||||
break;
|
||||
case FavoriteType_Albums:
|
||||
emit AlbumsAdded(songs);
|
||||
break;
|
||||
case FavoriteType_Songs:
|
||||
emit SongsAdded(songs);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void QobuzFavoriteRequest::RemoveArtists(const SongList &songs) {
|
||||
RemoveFavorites(FavoriteType_Artists, songs);
|
||||
}
|
||||
|
||||
void QobuzFavoriteRequest::RemoveAlbums(const SongList &songs) {
|
||||
RemoveFavorites(FavoriteType_Albums, songs);
|
||||
}
|
||||
|
||||
void QobuzFavoriteRequest::RemoveSongs(const SongList &songs) {
|
||||
RemoveFavorites(FavoriteType_Songs, songs);
|
||||
}
|
||||
|
||||
void QobuzFavoriteRequest::RemoveFavorites(const FavoriteType type, const SongList &songs) {
|
||||
|
||||
if (songs.isEmpty()) return;
|
||||
|
||||
QString text;
|
||||
switch (type) {
|
||||
case FavoriteType_Artists:
|
||||
text = "artist_ids";
|
||||
break;
|
||||
case FavoriteType_Albums:
|
||||
text = "album_ids";
|
||||
break;
|
||||
case FavoriteType_Songs:
|
||||
text = "track_ids";
|
||||
break;
|
||||
}
|
||||
|
||||
QStringList ids_list;
|
||||
for (const Song &song : songs) {
|
||||
QString id;
|
||||
switch (type) {
|
||||
case FavoriteType_Artists:
|
||||
if (song.artist_id().isEmpty()) continue;
|
||||
id = song.artist_id();
|
||||
break;
|
||||
case FavoriteType_Albums:
|
||||
if (song.album_id().isEmpty()) continue;
|
||||
id = song.album_id();
|
||||
break;
|
||||
case FavoriteType_Songs:
|
||||
if (song.song_id().isEmpty()) continue;
|
||||
id = song.song_id();
|
||||
break;
|
||||
}
|
||||
if (id.isEmpty()) continue;
|
||||
if (!ids_list.contains(id)) {
|
||||
ids_list << id;
|
||||
}
|
||||
}
|
||||
if (ids_list.isEmpty()) return;
|
||||
|
||||
QString ids = ids_list.join(',');
|
||||
|
||||
ParamList params = ParamList() << Param("app_id", app_id())
|
||||
<< Param("user_auth_token", user_auth_token())
|
||||
<< Param(text, ids);
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QNetworkReply *reply = CreateRequest("favorite/delete", params);
|
||||
connect(reply, &QNetworkReply::finished, [=] { RemoveFavoritesReply(reply, type, songs); });
|
||||
replies_ << reply;
|
||||
|
||||
}
|
||||
|
||||
void QobuzFavoriteRequest::RemoveFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs) {
|
||||
|
||||
if (replies_.contains(reply)) {
|
||||
replies_.removeAll(reply);
|
||||
reply->deleteLater();
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
return;
|
||||
}
|
||||
|
||||
qLog(Debug) << "Qobuz:" << songs.count() << "songs removed from" << FavoriteText(type) << "favorites.";
|
||||
|
||||
switch (type) {
|
||||
case FavoriteType_Artists:
|
||||
emit ArtistsRemoved(songs);
|
||||
break;
|
||||
case FavoriteType_Albums:
|
||||
emit AlbumsRemoved(songs);
|
||||
break;
|
||||
case FavoriteType_Songs:
|
||||
emit SongsRemoved(songs);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void QobuzFavoriteRequest::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "Qobuz:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
}
|
82
src/qobuz/qobuzfavoriterequest.h
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019, 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 QOBUZFAVORITEREQUEST_H
|
||||
#define QOBUZFAVORITEREQUEST_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
|
||||
#include "qobuzbaserequest.h"
|
||||
#include "core/song.h"
|
||||
|
||||
class QNetworkReply;
|
||||
class QobuzService;
|
||||
class NetworkAccessManager;
|
||||
|
||||
class QobuzFavoriteRequest : public QobuzBaseRequest {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QobuzFavoriteRequest(QobuzService *service, NetworkAccessManager *network, QObject *parent);
|
||||
~QobuzFavoriteRequest();
|
||||
|
||||
enum FavoriteType {
|
||||
FavoriteType_Artists,
|
||||
FavoriteType_Albums,
|
||||
FavoriteType_Songs
|
||||
};
|
||||
|
||||
signals:
|
||||
void ArtistsAdded(SongList);
|
||||
void AlbumsAdded(SongList);
|
||||
void SongsAdded(SongList);
|
||||
void ArtistsRemoved(SongList);
|
||||
void AlbumsRemoved(SongList);
|
||||
void SongsRemoved(SongList);
|
||||
|
||||
private slots:
|
||||
void AddArtists(const SongList &songs);
|
||||
void AddAlbums(const SongList &songs);
|
||||
void AddSongs(const SongList &songs);
|
||||
|
||||
void RemoveArtists(const SongList &songs);
|
||||
void RemoveAlbums(const SongList &songs);
|
||||
void RemoveSongs(const SongList &songs);
|
||||
|
||||
void AddFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs);
|
||||
void RemoveFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs);
|
||||
|
||||
private:
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
QString FavoriteText(const FavoriteType type);
|
||||
void AddFavorites(const FavoriteType type, const SongList &songs);
|
||||
void RemoveFavorites(const FavoriteType type, const SongList &songs);
|
||||
|
||||
QobuzService *service_;
|
||||
NetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
#endif // QOBUZFAVORITEREQUEST_H
|
1343
src/qobuz/qobuzrequest.cpp
Normal file
205
src/qobuz/qobuzrequest.h
Normal file
@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019, 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 QOBUZREQUEST_H
|
||||
#define QOBUZREQUEST_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QHash>
|
||||
#include <QMap>
|
||||
#include <QMultiMap>
|
||||
#include <QQueue>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "qobuzbaserequest.h"
|
||||
|
||||
class QNetworkReply;
|
||||
class Application;
|
||||
class NetworkAccessManager;
|
||||
class QobuzService;
|
||||
class QobuzUrlHandler;
|
||||
|
||||
class QobuzRequest : public QobuzBaseRequest {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
explicit QobuzRequest(QobuzService *service, QobuzUrlHandler *url_handler, Application *app, NetworkAccessManager *network, QueryType type, QObject *parent);
|
||||
~QobuzRequest();
|
||||
|
||||
void ReloadSettings();
|
||||
|
||||
void Process();
|
||||
void Search(const int search_id, const QString &search_text);
|
||||
|
||||
signals:
|
||||
void Login();
|
||||
void Login(const QString &username, const QString &password, const QString &token);
|
||||
void LoginSuccess();
|
||||
void LoginFailure(QString failure_reason);
|
||||
void Results(const int id, const SongList &songs, const QString &error);
|
||||
void UpdateStatus(const int id, const QString &text);
|
||||
void ProgressSetMaximum(const int id, const int max);
|
||||
void UpdateProgress(const int id, const int max);
|
||||
void StreamURLFinished(const QUrl original_url, const QUrl url, const Song::FileType, QString error = QString());
|
||||
|
||||
private slots:
|
||||
|
||||
void ArtistsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested);
|
||||
|
||||
void AlbumsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested);
|
||||
void AlbumsReceived(QNetworkReply *reply, const QString &artist_id_requested, const int limit_requested, const int offset_requested);
|
||||
|
||||
void SongsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested);
|
||||
void SongsReceived(QNetworkReply *reply, const QString &artist_id_requested, const QString &album_id_requested, const int limit_requested, const int offset_requested, const QString &album_artist_requested = QString(), const QString &album_requested = QString());
|
||||
|
||||
void ArtistAlbumsReplyReceived(QNetworkReply *reply, const QString artist_id, const int offset_requested);
|
||||
void AlbumSongsReplyReceived(QNetworkReply *reply, const QString &artist_id, const QString &album_id, const int offset_requested, const QString &album_artist, const QString &album);
|
||||
void AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_url, const QString &filename);
|
||||
|
||||
private:
|
||||
|
||||
struct Request {
|
||||
Request() : offset(0), limit(0) {}
|
||||
QString artist_id;
|
||||
QString album_id;
|
||||
QString song_id;
|
||||
int offset;
|
||||
int limit;
|
||||
QString album_artist;
|
||||
QString album;
|
||||
};
|
||||
struct AlbumCoverRequest {
|
||||
QUrl url;
|
||||
QString filename;
|
||||
};
|
||||
|
||||
bool IsQuery() { return (type_ == QueryType_Artists || type_ == QueryType_Albums || type_ == QueryType_Songs); }
|
||||
bool IsSearch() { return (type_ == QueryType_SearchArtists || type_ == QueryType_SearchAlbums || type_ == QueryType_SearchSongs); }
|
||||
|
||||
void GetArtists();
|
||||
void GetAlbums();
|
||||
void GetSongs();
|
||||
|
||||
void ArtistsSearch();
|
||||
void AlbumsSearch();
|
||||
void SongsSearch();
|
||||
|
||||
void AddArtistsRequest(const int offset = 0, const int limit = 0);
|
||||
void AddArtistsSearchRequest(const int offset = 0);
|
||||
void FlushArtistsRequests();
|
||||
void AddAlbumsRequest(const int offset = 0, const int limit = 0);
|
||||
void AddAlbumsSearchRequest(const int offset = 0);
|
||||
void FlushAlbumsRequests();
|
||||
void AddSongsRequest(const int offset = 0, const int limit = 0);
|
||||
void AddSongsSearchRequest(const int offset = 0);
|
||||
void FlushSongsRequests();
|
||||
|
||||
void ArtistsFinishCheck(const int limit = 0, const int offset = 0, const int artists_received = 0);
|
||||
void AlbumsFinishCheck(const QString &artist_id, const int limit = 0, const int offset = 0, const int albums_total = 0, const int albums_received = 0);
|
||||
void SongsFinishCheck(const QString &artist_id, const QString &album_id, const int limit, const int offset, const int songs_total, const int songs_received, const QString &album_artist, const QString &album);
|
||||
|
||||
void AddArtistAlbumsRequest(const QString &artist_id, const int offset = 0);
|
||||
void FlushArtistAlbumsRequests();
|
||||
|
||||
void AddAlbumSongsRequest(const QString &artist_id, const QString &album_id, const QString &album_artist, const QString &album, const int offset = 0);
|
||||
void FlushAlbumSongsRequests();
|
||||
|
||||
QString ParseSong(Song &song, const QJsonObject &json_obj, QString artist_id, QString album_id, QString album_artist, QString album, QUrl cover_url);
|
||||
|
||||
QString AlbumCoverFileName(const Song &song);
|
||||
|
||||
void GetAlbumCovers();
|
||||
void AddAlbumCoverRequest(Song &song);
|
||||
void FlushAlbumCoverRequests();
|
||||
void AlbumCoverFinishCheck();
|
||||
|
||||
void FinishCheck();
|
||||
void Warn(const QString &error, const QVariant &debug = QVariant());
|
||||
void Error(const QString &error, const QVariant &debug = QVariant()) override;
|
||||
|
||||
static const int kMaxConcurrentArtistsRequests;
|
||||
static const int kMaxConcurrentAlbumsRequests;
|
||||
static const int kMaxConcurrentSongsRequests;
|
||||
static const int kMaxConcurrentArtistAlbumsRequests;
|
||||
static const int kMaxConcurrentAlbumSongsRequests;
|
||||
static const int kMaxConcurrentAlbumCoverRequests;
|
||||
|
||||
QobuzService *service_;
|
||||
QobuzUrlHandler *url_handler_;
|
||||
Application *app_;
|
||||
NetworkAccessManager *network_;
|
||||
|
||||
QueryType type_;
|
||||
int query_id_;
|
||||
QString search_text_;
|
||||
|
||||
bool finished_;
|
||||
|
||||
QQueue<Request> artists_requests_queue_;
|
||||
QQueue<Request> albums_requests_queue_;
|
||||
QQueue<Request> songs_requests_queue_;
|
||||
|
||||
QQueue<Request> artist_albums_requests_queue_;
|
||||
QQueue<Request> album_songs_requests_queue_;
|
||||
QQueue<AlbumCoverRequest> album_cover_requests_queue_;
|
||||
|
||||
QList<QString> artist_albums_requests_pending_;
|
||||
QHash<QString, Request> album_songs_requests_pending_;
|
||||
QMultiMap<QUrl, Song*> album_covers_requests_sent_;
|
||||
|
||||
int artists_requests_active_;
|
||||
int artists_total_;
|
||||
int artists_received_;
|
||||
|
||||
int albums_requests_active_;
|
||||
int songs_requests_active_;
|
||||
|
||||
int artist_albums_requests_active_;
|
||||
int artist_albums_requested_;
|
||||
int artist_albums_received_;
|
||||
|
||||
int album_songs_requests_active_;
|
||||
int album_songs_requested_;
|
||||
int album_songs_received_;
|
||||
|
||||
int album_covers_requests_active_;
|
||||
int album_covers_requested_;
|
||||
int album_covers_received_;
|
||||
|
||||
SongList songs_;
|
||||
QStringList errors_;
|
||||
bool no_results_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
QList<QNetworkReply*> album_cover_replies_;
|
||||
|
||||
};
|
||||
|
||||
#endif // QOBUZREQUEST_H
|
761
src/qobuz/qobuzservice.cpp
Normal file
@ -0,0 +1,761 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019, 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 <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QPair>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QTimer>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSettings>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QSslError>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/player.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "core/database.h"
|
||||
#include "core/song.h"
|
||||
#include "core/utilities.h"
|
||||
#include "internet/internetsearchview.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "qobuzservice.h"
|
||||
#include "qobuzurlhandler.h"
|
||||
#include "qobuzbaserequest.h"
|
||||
#include "qobuzrequest.h"
|
||||
#include "qobuzfavoriterequest.h"
|
||||
#include "qobuzstreamurlrequest.h"
|
||||
#include "settings/settingsdialog.h"
|
||||
#include "settings/qobuzsettingspage.h"
|
||||
|
||||
using std::shared_ptr;
|
||||
|
||||
const Song::Source QobuzService::kSource = Song::Source_Qobuz;
|
||||
const char *QobuzService::kAuthUrl = "https://www.qobuz.com/api.json/0.2/user/login";
|
||||
const int QobuzService::kLoginAttempts = 2;
|
||||
const int QobuzService::kTimeResetLoginAttempts = 60000;
|
||||
|
||||
const char *QobuzService::kArtistsSongsTable = "qobuz_artists_songs";
|
||||
const char *QobuzService::kAlbumsSongsTable = "qobuz_albums_songs";
|
||||
const char *QobuzService::kSongsTable = "qobuz_songs";
|
||||
|
||||
const char *QobuzService::kArtistsSongsFtsTable = "qobuz_artists_songs_fts";
|
||||
const char *QobuzService::kAlbumsSongsFtsTable = "qobuz_albums_songs_fts";
|
||||
const char *QobuzService::kSongsFtsTable = "qobuz_songs_fts";
|
||||
|
||||
QobuzService::QobuzService(Application *app, QObject *parent)
|
||||
: InternetService(Song::Source_Qobuz, "Qobuz", "qobuz", QobuzSettingsPage::kSettingsGroup, SettingsDialog::Page_Qobuz, app, parent),
|
||||
app_(app),
|
||||
network_(new NetworkAccessManager(this)),
|
||||
url_handler_(new QobuzUrlHandler(app, this)),
|
||||
artists_collection_backend_(nullptr),
|
||||
albums_collection_backend_(nullptr),
|
||||
songs_collection_backend_(nullptr),
|
||||
artists_collection_model_(nullptr),
|
||||
albums_collection_model_(nullptr),
|
||||
songs_collection_model_(nullptr),
|
||||
artists_collection_sort_model_(new QSortFilterProxyModel(this)),
|
||||
albums_collection_sort_model_(new QSortFilterProxyModel(this)),
|
||||
songs_collection_sort_model_(new QSortFilterProxyModel(this)),
|
||||
timer_search_delay_(new QTimer(this)),
|
||||
timer_login_attempt_(new QTimer(this)),
|
||||
favorite_request_(new QobuzFavoriteRequest(this, network_, this)),
|
||||
format_(0),
|
||||
search_delay_(1500),
|
||||
artistssearchlimit_(1),
|
||||
albumssearchlimit_(1),
|
||||
songssearchlimit_(1),
|
||||
download_album_covers_(true),
|
||||
user_id_(-1),
|
||||
credential_id_(-1),
|
||||
pending_search_id_(0),
|
||||
next_pending_search_id_(1),
|
||||
search_id_(0),
|
||||
login_sent_(false),
|
||||
login_attempts_(0)
|
||||
{
|
||||
|
||||
app->player()->RegisterUrlHandler(url_handler_);
|
||||
|
||||
// Backends
|
||||
|
||||
artists_collection_backend_ = new CollectionBackend();
|
||||
artists_collection_backend_->moveToThread(app_->database()->thread());
|
||||
artists_collection_backend_->Init(app_->database(), Song::Source_Qobuz, kArtistsSongsTable, QString(), QString(), kArtistsSongsFtsTable);
|
||||
|
||||
albums_collection_backend_ = new CollectionBackend();
|
||||
albums_collection_backend_->moveToThread(app_->database()->thread());
|
||||
albums_collection_backend_->Init(app_->database(), Song::Source_Qobuz, kAlbumsSongsTable, QString(), QString(), kAlbumsSongsFtsTable);
|
||||
|
||||
songs_collection_backend_ = new CollectionBackend();
|
||||
songs_collection_backend_->moveToThread(app_->database()->thread());
|
||||
songs_collection_backend_->Init(app_->database(), Song::Source_Qobuz, kSongsTable, QString(), QString(), kSongsFtsTable);
|
||||
|
||||
artists_collection_model_ = new CollectionModel(artists_collection_backend_, app_, this);
|
||||
albums_collection_model_ = new CollectionModel(albums_collection_backend_, app_, this);
|
||||
songs_collection_model_ = new CollectionModel(songs_collection_backend_, app_, this);
|
||||
|
||||
artists_collection_sort_model_->setSourceModel(artists_collection_model_);
|
||||
artists_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
|
||||
artists_collection_sort_model_->setDynamicSortFilter(true);
|
||||
artists_collection_sort_model_->setSortLocaleAware(true);
|
||||
artists_collection_sort_model_->sort(0);
|
||||
|
||||
albums_collection_sort_model_->setSourceModel(albums_collection_model_);
|
||||
albums_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
|
||||
albums_collection_sort_model_->setDynamicSortFilter(true);
|
||||
albums_collection_sort_model_->setSortLocaleAware(true);
|
||||
albums_collection_sort_model_->sort(0);
|
||||
|
||||
songs_collection_sort_model_->setSourceModel(songs_collection_model_);
|
||||
songs_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
|
||||
songs_collection_sort_model_->setDynamicSortFilter(true);
|
||||
songs_collection_sort_model_->setSortLocaleAware(true);
|
||||
songs_collection_sort_model_->sort(0);
|
||||
|
||||
// Search
|
||||
|
||||
timer_search_delay_->setSingleShot(true);
|
||||
connect(timer_search_delay_, SIGNAL(timeout()), SLOT(StartSearch()));
|
||||
|
||||
timer_login_attempt_->setSingleShot(true);
|
||||
connect(timer_login_attempt_, SIGNAL(timeout()), SLOT(ResetLoginAttempts()));
|
||||
|
||||
connect(this, SIGNAL(Login()), SLOT(SendLogin()));
|
||||
connect(this, SIGNAL(Login(QString, QString, QString)), SLOT(SendLogin(QString, QString, QString)));
|
||||
|
||||
connect(this, SIGNAL(AddArtists(SongList)), favorite_request_, SLOT(AddArtists(SongList)));
|
||||
connect(this, SIGNAL(AddAlbums(SongList)), favorite_request_, SLOT(AddAlbums(SongList)));
|
||||
connect(this, SIGNAL(AddSongs(SongList)), favorite_request_, SLOT(AddSongs(SongList)));
|
||||
|
||||
connect(this, SIGNAL(RemoveArtists(SongList)), favorite_request_, SLOT(RemoveArtists(SongList)));
|
||||
connect(this, SIGNAL(RemoveAlbums(SongList)), favorite_request_, SLOT(RemoveAlbums(SongList)));
|
||||
connect(this, SIGNAL(RemoveSongs(SongList)), favorite_request_, SLOT(RemoveSongs(SongList)));
|
||||
|
||||
connect(favorite_request_, SIGNAL(ArtistsAdded(SongList)), artists_collection_backend_, SLOT(AddOrUpdateSongs(SongList)));
|
||||
connect(favorite_request_, SIGNAL(AlbumsAdded(SongList)), albums_collection_backend_, SLOT(AddOrUpdateSongs(SongList)));
|
||||
connect(favorite_request_, SIGNAL(SongsAdded(SongList)), songs_collection_backend_, SLOT(AddOrUpdateSongs(SongList)));
|
||||
|
||||
connect(favorite_request_, SIGNAL(ArtistsRemoved(SongList)), artists_collection_backend_, SLOT(DeleteSongs(SongList)));
|
||||
connect(favorite_request_, SIGNAL(AlbumsRemoved(SongList)), albums_collection_backend_, SLOT(DeleteSongs(SongList)));
|
||||
connect(favorite_request_, SIGNAL(SongsRemoved(SongList)), songs_collection_backend_, SLOT(DeleteSongs(SongList)));
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
QobuzService::~QobuzService() {
|
||||
|
||||
while (!stream_url_requests_.isEmpty()) {
|
||||
QobuzStreamURLRequest *stream_url_req = stream_url_requests_.takeFirst();
|
||||
disconnect(stream_url_req, 0, this, 0);
|
||||
stream_url_req->deleteLater();
|
||||
}
|
||||
|
||||
artists_collection_backend_->deleteLater();
|
||||
albums_collection_backend_->deleteLater();
|
||||
songs_collection_backend_->deleteLater();
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::Exit() {
|
||||
|
||||
wait_for_exit_ << artists_collection_backend_ << albums_collection_backend_ << songs_collection_backend_;
|
||||
|
||||
connect(artists_collection_backend_, SIGNAL(ExitFinished()), this, SLOT(ExitReceived()));
|
||||
connect(albums_collection_backend_, SIGNAL(ExitFinished()), this, SLOT(ExitReceived()));
|
||||
connect(songs_collection_backend_, SIGNAL(ExitFinished()), this, SLOT(ExitReceived()));
|
||||
|
||||
artists_collection_backend_->ExitAsync();
|
||||
albums_collection_backend_->ExitAsync();
|
||||
songs_collection_backend_->ExitAsync();
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::ExitReceived() {
|
||||
|
||||
QObject *obj = qobject_cast<QObject*>(sender());
|
||||
disconnect(obj, nullptr, this, nullptr);
|
||||
qLog(Debug) << obj << "successfully exited.";
|
||||
wait_for_exit_.removeAll(obj);
|
||||
if (wait_for_exit_.isEmpty()) emit ExitFinished();
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::ShowConfig() {
|
||||
app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Qobuz);
|
||||
}
|
||||
|
||||
void QobuzService::ReloadSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(QobuzSettingsPage::kSettingsGroup);
|
||||
|
||||
app_id_ = s.value("app_id").toString();
|
||||
app_secret_ = s.value("app_secret").toString();
|
||||
|
||||
username_ = s.value("username").toString();
|
||||
QByteArray password = s.value("password").toByteArray();
|
||||
if (password.isEmpty()) password_.clear();
|
||||
else password_ = QString::fromUtf8(QByteArray::fromBase64(password));
|
||||
|
||||
format_ = s.value("format", 27).toInt();
|
||||
search_delay_ = s.value("searchdelay", 1500).toInt();
|
||||
artistssearchlimit_ = s.value("artistssearchlimit", 4).toInt();
|
||||
albumssearchlimit_ = s.value("albumssearchlimit", 10).toInt();
|
||||
songssearchlimit_ = s.value("songssearchlimit", 10).toInt();
|
||||
download_album_covers_ = s.value("downloadalbumcovers", true).toBool();
|
||||
|
||||
user_id_ = s.value("user_id").toInt();
|
||||
device_id_ = s.value("device_id").toString();
|
||||
user_auth_token_ = s.value("user_auth_token").toString();
|
||||
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::SendLogin() {
|
||||
SendLogin(app_id_, username_, password_);
|
||||
}
|
||||
|
||||
void QobuzService::SendLogin(const QString &app_id, const QString &username, const QString &password) {
|
||||
|
||||
emit UpdateStatus(tr("Authenticating..."));
|
||||
login_errors_.clear();
|
||||
|
||||
login_sent_ = true;
|
||||
++login_attempts_;
|
||||
if (timer_login_attempt_->isActive()) timer_login_attempt_->stop();
|
||||
timer_login_attempt_->setInterval(kTimeResetLoginAttempts);
|
||||
timer_login_attempt_->start();
|
||||
|
||||
const ParamList params = ParamList() << Param("app_id", app_id)
|
||||
<< Param("username", username)
|
||||
<< Param("password", password)
|
||||
<< Param("device_manufacturer_id", Utilities::MacAddress());
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
}
|
||||
|
||||
QUrl url(kAuthUrl);
|
||||
QNetworkRequest req(url);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
#else
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
#endif
|
||||
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
|
||||
QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
|
||||
QNetworkReply *reply = network_->post(req, query);
|
||||
replies_ << reply;
|
||||
connect(reply, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(HandleLoginSSLErrors(QList<QSslError>)));
|
||||
connect(reply, &QNetworkReply::finished, [=] { HandleAuthReply(reply); });
|
||||
|
||||
qLog(Debug) << "Qobuz: Sending request" << url << query;
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::HandleLoginSSLErrors(QList<QSslError> ssl_errors) {
|
||||
|
||||
for (QSslError &ssl_error : ssl_errors) {
|
||||
login_errors_ += ssl_error.errorString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::HandleAuthReply(QNetworkReply *reply) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
login_sent_ = false;
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
LoginError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
return;
|
||||
}
|
||||
else {
|
||||
// See if there is Json data containing "status", "code" and "message" - then use that instead.
|
||||
QByteArray data(reply->readAll());
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("code") && json_obj.contains("message")) {
|
||||
QString status = json_obj["status"].toString();
|
||||
int code = json_obj["code"].toInt();
|
||||
QString message = json_obj["message"].toString();
|
||||
login_errors_ << QString("%1 (%2)").arg(message).arg(code);
|
||||
}
|
||||
}
|
||||
if (login_errors_.isEmpty()) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
else {
|
||||
login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
|
||||
}
|
||||
}
|
||||
LoginError();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
login_errors_.clear();
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
|
||||
if (json_error.error != QJsonParseError::NoError) {
|
||||
LoginError("Authentication reply from server missing Json data.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
LoginError("Authentication reply from server has empty Json document.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
LoginError("Authentication reply from server has Json document that is not an object.", json_doc);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
LoginError("Authentication reply from server has empty Json object.", json_doc);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("user_auth_token")) {
|
||||
LoginError("Authentication reply from server is missing user_auth_token", json_obj);
|
||||
return;
|
||||
}
|
||||
user_auth_token_ = json_obj["user_auth_token"].toString();
|
||||
|
||||
if (!json_obj.contains("user")) {
|
||||
LoginError("Authentication reply from server is missing user", json_obj);
|
||||
return;
|
||||
}
|
||||
QJsonValue value_user = json_obj["user"];
|
||||
if (!value_user.isObject()) {
|
||||
LoginError("Authentication reply user is not a object", json_obj);
|
||||
return;
|
||||
}
|
||||
QJsonObject obj_user = value_user.toObject();
|
||||
|
||||
if (!obj_user.contains("id")) {
|
||||
LoginError("Authentication reply from server is missing user id", obj_user);
|
||||
return;
|
||||
}
|
||||
user_id_ = obj_user["id"].toInt();
|
||||
|
||||
if (!obj_user.contains("device")) {
|
||||
LoginError("Authentication reply from server is missing user device", obj_user);
|
||||
return;
|
||||
}
|
||||
QJsonValue value_device = obj_user["device"];
|
||||
if (!value_device.isObject()) {
|
||||
LoginError("Authentication reply from server user device is not a object", value_device);
|
||||
return;
|
||||
}
|
||||
QJsonObject obj_device = value_device.toObject();
|
||||
|
||||
if (!obj_device.contains("device_manufacturer_id")) {
|
||||
LoginError("Authentication reply from server device is missing device_manufacturer_id", obj_device);
|
||||
return;
|
||||
}
|
||||
device_id_ = obj_device["device_manufacturer_id"].toString();
|
||||
|
||||
if (!obj_user.contains("credential")) {
|
||||
LoginError("Authentication reply from server is missing user credential", obj_user);
|
||||
return;
|
||||
}
|
||||
QJsonValue value_credential = obj_user["credential"];
|
||||
if (!value_credential.isObject()) {
|
||||
LoginError("Authentication reply from serve userr credential is not a object", value_device);
|
||||
return;
|
||||
}
|
||||
QJsonObject obj_credential = value_credential.toObject();
|
||||
|
||||
if (!obj_credential.contains("id")) {
|
||||
LoginError("Authentication reply user credential from server is missing user credential id", obj_credential);
|
||||
return;
|
||||
}
|
||||
credential_id_ = obj_credential["id"].toInt();
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(QobuzSettingsPage::kSettingsGroup);
|
||||
s.setValue("user_auth_token", user_auth_token_);
|
||||
s.setValue("user_id", user_id_);
|
||||
s.setValue("credential_id", credential_id_);
|
||||
s.setValue("device_id", device_id_);
|
||||
s.endGroup();
|
||||
|
||||
qLog(Debug) << "Qobuz: Login successful" << "user id" << user_id_ << "device id" << device_id_;
|
||||
|
||||
login_attempts_ = 0;
|
||||
if (timer_login_attempt_->isActive()) timer_login_attempt_->stop();
|
||||
|
||||
emit LoginComplete(true);
|
||||
emit LoginSuccess();
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::Logout() {
|
||||
|
||||
user_auth_token_.clear();
|
||||
device_id_.clear();
|
||||
user_id_ = -1;
|
||||
credential_id_ = -1;
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(QobuzSettingsPage::kSettingsGroup);
|
||||
s.remove("user_id");
|
||||
s.remove("credential_id");
|
||||
s.remove("device_id");
|
||||
s.remove("user_auth_token");
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::ResetLoginAttempts() {
|
||||
login_attempts_ = 0;
|
||||
}
|
||||
|
||||
void QobuzService::TryLogin() {
|
||||
|
||||
if (authenticated() || login_sent_) return;
|
||||
|
||||
if (login_attempts_ >= kLoginAttempts) {
|
||||
emit LoginComplete(false, tr("Maximum number of login attempts reached."));
|
||||
return;
|
||||
}
|
||||
if (app_id_.isEmpty()) {
|
||||
emit LoginComplete(false, tr("Missing Qobuz app ID."));
|
||||
return;
|
||||
}
|
||||
if (username_.isEmpty()) {
|
||||
emit LoginComplete(false, tr("Missing Qobuz username."));
|
||||
return;
|
||||
}
|
||||
if (password_.isEmpty()) {
|
||||
emit LoginComplete(false, tr("Missing Qobuz password."));
|
||||
return;
|
||||
}
|
||||
|
||||
emit Login();
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::ResetArtistsRequest() {
|
||||
|
||||
if (artists_request_.get()) {
|
||||
disconnect(artists_request_.get(), 0, this, 0);
|
||||
disconnect(this, 0, artists_request_.get(), 0);
|
||||
artists_request_.reset();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::GetArtists() {
|
||||
|
||||
if (app_id().isEmpty()) {
|
||||
emit ArtistsResults(SongList(), tr("Missing Qobuz app ID."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authenticated()) {
|
||||
emit ArtistsResults(SongList(), tr("Not authenticated with Qobuz."));
|
||||
return;
|
||||
}
|
||||
|
||||
ResetArtistsRequest();
|
||||
|
||||
artists_request_.reset(new QobuzRequest(this, url_handler_, app_, network_, QobuzBaseRequest::QueryType_Artists, this));
|
||||
|
||||
connect(artists_request_.get(), SIGNAL(Results(int, SongList, QString)), SLOT(ArtistsResultsReceived(int, SongList, QString)));
|
||||
connect(artists_request_.get(), SIGNAL(UpdateStatus(int, QString)), SLOT(ArtistsUpdateStatusReceived(int, QString)));
|
||||
connect(artists_request_.get(), SIGNAL(ProgressSetMaximum(int, int)), SLOT(ArtistsProgressSetMaximumReceived(int, int)));
|
||||
connect(artists_request_.get(), SIGNAL(UpdateProgress(int, int)), SLOT(ArtistsUpdateProgressReceived(int, int)));
|
||||
|
||||
artists_request_->Process();
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::ArtistsResultsReceived(const int id, const SongList &songs, const QString &error) {
|
||||
Q_UNUSED(id);
|
||||
emit ArtistsResults(songs, error);
|
||||
}
|
||||
|
||||
void QobuzService::ArtistsUpdateStatusReceived(const int id, const QString &text) {
|
||||
Q_UNUSED(id);
|
||||
emit ArtistsUpdateStatus(text);
|
||||
}
|
||||
|
||||
void QobuzService::ArtistsProgressSetMaximumReceived(const int id, const int max) {
|
||||
Q_UNUSED(id);
|
||||
emit ArtistsProgressSetMaximum(max);
|
||||
}
|
||||
|
||||
void QobuzService::ArtistsUpdateProgressReceived(const int id, const int progress) {
|
||||
Q_UNUSED(id);
|
||||
emit ArtistsUpdateProgress(progress);
|
||||
}
|
||||
|
||||
void QobuzService::ResetAlbumsRequest() {
|
||||
|
||||
if (albums_request_.get()) {
|
||||
disconnect(albums_request_.get(), 0, this, 0);
|
||||
disconnect(this, 0, albums_request_.get(), 0);
|
||||
albums_request_.reset();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::GetAlbums() {
|
||||
|
||||
if (app_id().isEmpty()) {
|
||||
emit AlbumsResults(SongList(), tr("Missing Qobuz app ID."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authenticated()) {
|
||||
emit AlbumsResults(SongList(), tr("Not authenticated with Qobuz."));
|
||||
return;
|
||||
}
|
||||
|
||||
ResetAlbumsRequest();
|
||||
albums_request_.reset(new QobuzRequest(this, url_handler_, app_, network_, QobuzBaseRequest::QueryType_Albums, this));
|
||||
connect(albums_request_.get(), SIGNAL(Results(int, SongList, QString)), SLOT(AlbumsResultsReceived(int, SongList, QString)));
|
||||
connect(albums_request_.get(), SIGNAL(UpdateStatus(int, QString)), SLOT(AlbumsUpdateStatusReceived(int, QString)));
|
||||
connect(albums_request_.get(), SIGNAL(ProgressSetMaximum(int, int)), SLOT(AlbumsProgressSetMaximumReceived(int, int)));
|
||||
connect(albums_request_.get(), SIGNAL(UpdateProgress(int, int)), SLOT(AlbumsUpdateProgressReceived(int, int)));
|
||||
|
||||
albums_request_->Process();
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::AlbumsResultsReceived(const int id, const SongList &songs, const QString &error) {
|
||||
Q_UNUSED(id);
|
||||
emit AlbumsResults(songs, error);
|
||||
}
|
||||
|
||||
void QobuzService::AlbumsUpdateStatusReceived(const int id, const QString &text) {
|
||||
Q_UNUSED(id);
|
||||
emit AlbumsUpdateStatus(text);
|
||||
}
|
||||
|
||||
void QobuzService::AlbumsProgressSetMaximumReceived(const int id, const int max) {
|
||||
Q_UNUSED(id);
|
||||
emit AlbumsProgressSetMaximum(max);
|
||||
}
|
||||
|
||||
void QobuzService::AlbumsUpdateProgressReceived(const int id, const int progress) {
|
||||
Q_UNUSED(id);
|
||||
emit AlbumsUpdateProgress(progress);
|
||||
}
|
||||
|
||||
void QobuzService::ResetSongsRequest() {
|
||||
|
||||
if (songs_request_.get()) {
|
||||
disconnect(songs_request_.get(), 0, this, 0);
|
||||
disconnect(this, 0, songs_request_.get(), 0);
|
||||
songs_request_.reset();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::GetSongs() {
|
||||
|
||||
if (app_id().isEmpty()) {
|
||||
emit SongsResults(SongList(), tr("Missing Qobuz app ID."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authenticated()) {
|
||||
emit SongsResults(SongList(), tr("Not authenticated with Qobuz."));
|
||||
return;
|
||||
}
|
||||
|
||||
ResetSongsRequest();
|
||||
songs_request_.reset(new QobuzRequest(this, url_handler_, app_, network_, QobuzBaseRequest::QueryType_Songs, this));
|
||||
connect(songs_request_.get(), SIGNAL(Results(int, SongList, QString)), SLOT(SongsResultsReceived(int, SongList, QString)));
|
||||
connect(songs_request_.get(), SIGNAL(UpdateStatus(int, QString)), SLOT(SongsUpdateStatusReceived(int, QString)));
|
||||
connect(songs_request_.get(), SIGNAL(ProgressSetMaximum(int, int)), SLOT(SongsProgressSetMaximumReceived(int, int)));
|
||||
connect(songs_request_.get(), SIGNAL(UpdateProgress(int, int)), SLOT(SongsUpdateProgressReceived(int, int)));
|
||||
|
||||
songs_request_->Process();
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::SongsResultsReceived(const int id, const SongList &songs, const QString &error) {
|
||||
Q_UNUSED(id);
|
||||
emit SongsResults(songs, error);
|
||||
}
|
||||
|
||||
void QobuzService::SongsUpdateStatusReceived(const int id, const QString &text) {
|
||||
Q_UNUSED(id);
|
||||
emit SongsUpdateStatus(text);
|
||||
}
|
||||
|
||||
void QobuzService::SongsProgressSetMaximumReceived(const int id, const int max) {
|
||||
Q_UNUSED(id);
|
||||
emit SongsProgressSetMaximum(max);
|
||||
}
|
||||
|
||||
void QobuzService::SongsUpdateProgressReceived(const int id, const int progress) {
|
||||
Q_UNUSED(id);
|
||||
emit SongsUpdateProgress(progress);
|
||||
}
|
||||
|
||||
int QobuzService::Search(const QString &text, InternetSearchView::SearchType type) {
|
||||
|
||||
pending_search_id_ = next_pending_search_id_;
|
||||
pending_search_text_ = text;
|
||||
pending_search_type_ = type;
|
||||
|
||||
next_pending_search_id_++;
|
||||
|
||||
if (text.isEmpty()) {
|
||||
timer_search_delay_->stop();
|
||||
return pending_search_id_;
|
||||
}
|
||||
timer_search_delay_->setInterval(search_delay_);
|
||||
timer_search_delay_->start();
|
||||
|
||||
return pending_search_id_;
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::StartSearch() {
|
||||
|
||||
search_id_ = pending_search_id_;
|
||||
search_text_ = pending_search_text_;
|
||||
|
||||
if (app_id_.isEmpty()) { // App ID is the only thing needed to search.
|
||||
emit SearchResults(search_id_, SongList(), tr("Missing Qobuz app ID."));
|
||||
return;
|
||||
}
|
||||
|
||||
SendSearch();
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::CancelSearch() {
|
||||
}
|
||||
|
||||
void QobuzService::SendSearch() {
|
||||
|
||||
QobuzBaseRequest::QueryType type;
|
||||
|
||||
switch (pending_search_type_) {
|
||||
case InternetSearchView::SearchType_Artists:
|
||||
type = QobuzBaseRequest::QueryType_SearchArtists;
|
||||
break;
|
||||
case InternetSearchView::SearchType_Albums:
|
||||
type = QobuzBaseRequest::QueryType_SearchAlbums;
|
||||
break;
|
||||
case InternetSearchView::SearchType_Songs:
|
||||
type = QobuzBaseRequest::QueryType_SearchSongs;
|
||||
break;
|
||||
}
|
||||
|
||||
search_request_.reset(new QobuzRequest(this, url_handler_, app_, network_, type, this));
|
||||
|
||||
connect(search_request_.get(), SIGNAL(Results(int, SongList, QString)), SLOT(SearchResultsReceived(int, SongList, QString)));
|
||||
connect(search_request_.get(), SIGNAL(UpdateStatus(int, QString)), SIGNAL(SearchUpdateStatus(int, QString)));
|
||||
connect(search_request_.get(), SIGNAL(ProgressSetMaximum(int, int)), SIGNAL(SearchProgressSetMaximum(int, int)));
|
||||
connect(search_request_.get(), SIGNAL(UpdateProgress(int, int)), SIGNAL(SearchUpdateProgress(int, int)));
|
||||
|
||||
search_request_->Search(search_id_, search_text_);
|
||||
search_request_->Process();
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::SearchResultsReceived(const int id, const SongList &songs, const QString &error) {
|
||||
emit SearchResults(id, songs, error);
|
||||
}
|
||||
|
||||
void QobuzService::GetStreamURL(const QUrl &url) {
|
||||
|
||||
if (app_id().isEmpty() || app_secret().isEmpty()) { // Don't check for login here, because we allow automatic login.
|
||||
emit StreamURLFinished(url, url, Song::FileType_Stream, -1, -1, -1, tr("Missing Qobuz app ID or secret."));
|
||||
return;
|
||||
}
|
||||
|
||||
QobuzStreamURLRequest *stream_url_req = new QobuzStreamURLRequest(this, network_, url, this);
|
||||
stream_url_requests_ << stream_url_req;
|
||||
|
||||
connect(stream_url_req, SIGNAL(TryLogin()), this, SLOT(TryLogin()));
|
||||
connect(stream_url_req, SIGNAL(StreamURLFinished(QUrl, QUrl, Song::FileType, int, int, qint64, QString)), this, SLOT(HandleStreamURLFinished(QUrl, QUrl, Song::FileType, int, int, qint64, QString)));
|
||||
connect(this, SIGNAL(LoginComplete(bool, QString)), stream_url_req, SLOT(LoginComplete(bool, QString)));
|
||||
|
||||
stream_url_req->Process();
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::HandleStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error) {
|
||||
|
||||
QobuzStreamURLRequest *stream_url_req = qobject_cast<QobuzStreamURLRequest*>(sender());
|
||||
if (!stream_url_req || !stream_url_requests_.contains(stream_url_req)) return;
|
||||
stream_url_req->deleteLater();
|
||||
stream_url_requests_.removeAll(stream_url_req);
|
||||
|
||||
emit StreamURLFinished(original_url, stream_url, filetype, samplerate, bit_depth, duration, error);
|
||||
|
||||
}
|
||||
|
||||
void QobuzService::LoginError(const QString &error, const QVariant &debug) {
|
||||
|
||||
if (!error.isEmpty()) login_errors_ << error;
|
||||
|
||||
QString error_html;
|
||||
for (const QString &e : login_errors_) {
|
||||
qLog(Error) << "Qobuz:" << e;
|
||||
error_html += e + "<br />";
|
||||
}
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
emit LoginFailure(error_html);
|
||||
emit LoginComplete(false, error_html);
|
||||
|
||||
login_errors_.clear();
|
||||
|
||||
}
|
231
src/qobuz/qobuzservice.h
Normal file
@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019, 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 QOBUZSERVICE_H
|
||||
#define QOBUZSERVICE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QSslError>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "internet/internetservice.h"
|
||||
#include "internet/internetsearchview.h"
|
||||
|
||||
class QTimer;
|
||||
class QNetworkReply;
|
||||
class QSortFilterProxyModel;
|
||||
class Application;
|
||||
class NetworkAccessManager;
|
||||
class QobuzUrlHandler;
|
||||
class QobuzRequest;
|
||||
class QobuzFavoriteRequest;
|
||||
class QobuzStreamURLRequest;
|
||||
class CollectionBackend;
|
||||
class CollectionModel;
|
||||
|
||||
using std::shared_ptr;
|
||||
|
||||
class QobuzService : public InternetService {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QobuzService(Application *app, QObject *parent);
|
||||
~QobuzService();
|
||||
|
||||
static const Song::Source kSource;
|
||||
|
||||
void Exit() override;
|
||||
void ReloadSettings() override;
|
||||
|
||||
void Logout();
|
||||
int Search(const QString &text, InternetSearchView::SearchType type) override;
|
||||
void CancelSearch() override;
|
||||
|
||||
int max_login_attempts() { return kLoginAttempts; }
|
||||
|
||||
Application *app() { return app_; }
|
||||
QString app_id() { return app_id_; }
|
||||
QString app_secret() { return app_secret_; }
|
||||
QString username() { return username_; }
|
||||
QString password() { return password_; }
|
||||
int format() { return format_; }
|
||||
int search_delay() { return search_delay_; }
|
||||
int artistssearchlimit() { return artistssearchlimit_; }
|
||||
int albumssearchlimit() { return albumssearchlimit_; }
|
||||
int songssearchlimit() { return songssearchlimit_; }
|
||||
bool download_album_covers() { return download_album_covers_; }
|
||||
|
||||
QString user_auth_token() { return user_auth_token_; }
|
||||
qint64 user_id() { return user_id_; }
|
||||
QString device_id() { return device_id_; }
|
||||
qint64 credential_id() { return credential_id_; }
|
||||
|
||||
bool authenticated() override { return (!app_id_.isEmpty() && !app_secret_.isEmpty() && !user_auth_token_.isEmpty()); }
|
||||
bool login_sent() { return login_sent_; }
|
||||
bool login_attempts() { return login_attempts_; }
|
||||
|
||||
void GetStreamURL(const QUrl &url);
|
||||
|
||||
CollectionBackend *artists_collection_backend() override { return artists_collection_backend_; }
|
||||
CollectionBackend *albums_collection_backend() override { return albums_collection_backend_; }
|
||||
CollectionBackend *songs_collection_backend() override { return songs_collection_backend_; }
|
||||
|
||||
CollectionModel *artists_collection_model() override { return artists_collection_model_; }
|
||||
CollectionModel *albums_collection_model() override { return albums_collection_model_; }
|
||||
CollectionModel *songs_collection_model() override { return songs_collection_model_; }
|
||||
|
||||
QSortFilterProxyModel *artists_collection_sort_model() override { return artists_collection_sort_model_; }
|
||||
QSortFilterProxyModel *albums_collection_sort_model() override { return albums_collection_sort_model_; }
|
||||
QSortFilterProxyModel *songs_collection_sort_model() override { return songs_collection_sort_model_; }
|
||||
|
||||
enum QueryType {
|
||||
QueryType_Artists,
|
||||
QueryType_Albums,
|
||||
QueryType_Songs,
|
||||
QueryType_SearchArtists,
|
||||
QueryType_SearchAlbums,
|
||||
QueryType_SearchSongs,
|
||||
};
|
||||
|
||||
public slots:
|
||||
void ShowConfig() override;
|
||||
void TryLogin();
|
||||
void SendLogin(const QString &app_id, const QString &username, const QString &password);
|
||||
void GetArtists() override;
|
||||
void GetAlbums() override;
|
||||
void GetSongs() override;
|
||||
void ResetArtistsRequest() override;
|
||||
void ResetAlbumsRequest() override;
|
||||
void ResetSongsRequest() override;
|
||||
|
||||
private slots:
|
||||
void ExitReceived();
|
||||
void SendLogin();
|
||||
void HandleLoginSSLErrors(QList<QSslError> ssl_errors);
|
||||
void HandleAuthReply(QNetworkReply *reply);
|
||||
void ResetLoginAttempts();
|
||||
void StartSearch();
|
||||
void ArtistsResultsReceived(const int id, const SongList &songs, const QString &error);
|
||||
void AlbumsResultsReceived(const int id, const SongList &songs, const QString &error);
|
||||
void SongsResultsReceived(const int id, const SongList &songs, const QString &error);
|
||||
void SearchResultsReceived(const int id, const SongList &songs, const QString &error);
|
||||
void ArtistsUpdateStatusReceived(const int id, const QString &text);
|
||||
void AlbumsUpdateStatusReceived(const int id, const QString &text);
|
||||
void SongsUpdateStatusReceived(const int id, const QString &text);
|
||||
void ArtistsProgressSetMaximumReceived(const int id, const int max);
|
||||
void AlbumsProgressSetMaximumReceived(const int id, const int max);
|
||||
void SongsProgressSetMaximumReceived(const int id, const int max);
|
||||
void ArtistsUpdateProgressReceived(const int id, const int progress);
|
||||
void AlbumsUpdateProgressReceived(const int id, const int progress);
|
||||
void SongsUpdateProgressReceived(const int id, const int progress);
|
||||
void HandleStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error);
|
||||
|
||||
private:
|
||||
typedef QPair<QString, QString> Param;
|
||||
typedef QList<Param> ParamList;
|
||||
|
||||
void SendSearch();
|
||||
void LoginError(const QString &error = QString(), const QVariant &debug = QVariant());
|
||||
|
||||
static const char *kAuthUrl;
|
||||
static const int kLoginAttempts;
|
||||
static const int kTimeResetLoginAttempts;
|
||||
|
||||
static const char *kArtistsSongsTable;
|
||||
static const char *kAlbumsSongsTable;
|
||||
static const char *kSongsTable;
|
||||
|
||||
static const char *kArtistsSongsFtsTable;
|
||||
static const char *kAlbumsSongsFtsTable;
|
||||
static const char *kSongsFtsTable;
|
||||
|
||||
Application *app_;
|
||||
NetworkAccessManager *network_;
|
||||
QobuzUrlHandler *url_handler_;
|
||||
|
||||
CollectionBackend *artists_collection_backend_;
|
||||
CollectionBackend *albums_collection_backend_;
|
||||
CollectionBackend *songs_collection_backend_;
|
||||
|
||||
CollectionModel *artists_collection_model_;
|
||||
CollectionModel *albums_collection_model_;
|
||||
CollectionModel *songs_collection_model_;
|
||||
|
||||
QSortFilterProxyModel *artists_collection_sort_model_;
|
||||
QSortFilterProxyModel *albums_collection_sort_model_;
|
||||
QSortFilterProxyModel *songs_collection_sort_model_;
|
||||
|
||||
QTimer *timer_search_delay_;
|
||||
QTimer *timer_login_attempt_;
|
||||
|
||||
std::shared_ptr<QobuzRequest> artists_request_;
|
||||
std::shared_ptr<QobuzRequest> albums_request_;
|
||||
std::shared_ptr<QobuzRequest> songs_request_;
|
||||
std::shared_ptr<QobuzRequest> search_request_;
|
||||
QobuzFavoriteRequest *favorite_request_;
|
||||
|
||||
QString app_id_;
|
||||
QString app_secret_;
|
||||
QString username_;
|
||||
QString password_;
|
||||
int format_;
|
||||
int search_delay_;
|
||||
int artistssearchlimit_;
|
||||
int albumssearchlimit_;
|
||||
int songssearchlimit_;
|
||||
bool download_album_covers_;
|
||||
|
||||
qint64 user_id_;
|
||||
QString user_auth_token_;
|
||||
QString device_id_;
|
||||
qint64 credential_id_;
|
||||
|
||||
int pending_search_id_;
|
||||
int next_pending_search_id_;
|
||||
QString pending_search_text_;
|
||||
InternetSearchView::SearchType pending_search_type_;
|
||||
|
||||
int search_id_;
|
||||
QString search_text_;
|
||||
bool login_sent_;
|
||||
int login_attempts_;
|
||||
|
||||
QList<QobuzStreamURLRequest*> stream_url_requests_;
|
||||
|
||||
QStringList login_errors_;
|
||||
|
||||
QList<QObject*> wait_for_exit_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
#endif // QOBUZSERVICE_H
|
249
src/qobuz/qobuzstreamurlrequest.cpp
Normal file
@ -0,0 +1,249 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019, 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 <QObject>
|
||||
#include <QMimeDatabase>
|
||||
#include <QMimeType>
|
||||
#include <QPair>
|
||||
#include <QList>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QChar>
|
||||
#include <QUrl>
|
||||
#include <QDateTime>
|
||||
#include <QNetworkReply>
|
||||
#include <QCryptographicHash>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "core/song.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "qobuzservice.h"
|
||||
#include "qobuzbaserequest.h"
|
||||
#include "qobuzstreamurlrequest.h"
|
||||
|
||||
QobuzStreamURLRequest::QobuzStreamURLRequest(QobuzService *service, NetworkAccessManager *network, const QUrl &original_url, QObject *parent)
|
||||
: QobuzBaseRequest(service, network, parent),
|
||||
service_(service),
|
||||
reply_(nullptr),
|
||||
original_url_(original_url),
|
||||
song_id_(original_url.path().toInt()),
|
||||
tries_(0),
|
||||
need_login_(false) {}
|
||||
|
||||
QobuzStreamURLRequest::~QobuzStreamURLRequest() {
|
||||
|
||||
if (reply_) {
|
||||
disconnect(reply_, 0, this, 0);
|
||||
if (reply_->isRunning()) reply_->abort();
|
||||
reply_->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void QobuzStreamURLRequest::LoginComplete(const bool success, const QString &error) {
|
||||
|
||||
if (!need_login_) return;
|
||||
need_login_ = false;
|
||||
|
||||
if (!success) {
|
||||
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error);
|
||||
return;
|
||||
}
|
||||
|
||||
Process();
|
||||
|
||||
}
|
||||
|
||||
void QobuzStreamURLRequest::Process() {
|
||||
|
||||
if (app_id().isEmpty() || app_secret().isEmpty()) {
|
||||
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, tr("Missing Qobuz app ID or secret."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authenticated()) {
|
||||
need_login_ = true;
|
||||
emit TryLogin();
|
||||
return;
|
||||
}
|
||||
GetStreamURL();
|
||||
|
||||
}
|
||||
|
||||
void QobuzStreamURLRequest::Cancel() {
|
||||
|
||||
if (reply_ && reply_->isRunning()) {
|
||||
reply_->abort();
|
||||
}
|
||||
else {
|
||||
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, tr("Cancelled."));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void QobuzStreamURLRequest::GetStreamURL() {
|
||||
|
||||
++tries_;
|
||||
|
||||
if (reply_) {
|
||||
disconnect(reply_, 0, this, 0);
|
||||
if (reply_->isRunning()) reply_->abort();
|
||||
reply_->deleteLater();
|
||||
}
|
||||
|
||||
#if 0
|
||||
QByteArray appid = app_id().toUtf8();
|
||||
QByteArray secret_decoded = QByteArray::fromBase64(app_secret().toUtf8());
|
||||
QString secret;
|
||||
for (int x = 0, y = 0; x < secret_decoded.length(); ++x , ++y) {
|
||||
if (y == appid.length()) y = 0;
|
||||
secret.append(QChar(secret_decoded[x] ^ appid[y]));
|
||||
}
|
||||
#endif
|
||||
|
||||
QString secret = app_secret();
|
||||
quint64 timestamp = QDateTime::currentDateTime().toSecsSinceEpoch();
|
||||
|
||||
ParamList params_to_sign = ParamList() << Param("format_id", QString::number(format()))
|
||||
<< Param("track_id", QString::number(song_id_));
|
||||
|
||||
std::sort(params_to_sign.begin(), params_to_sign.end());
|
||||
|
||||
QString data_to_sign;
|
||||
data_to_sign += "trackgetFileUrl";
|
||||
for (const Param ¶m : params_to_sign) {
|
||||
data_to_sign += param.first + param.second;
|
||||
}
|
||||
data_to_sign += QString::number(timestamp);
|
||||
data_to_sign += secret.toUtf8();
|
||||
|
||||
QByteArray const digest = QCryptographicHash::hash(data_to_sign.toUtf8(), QCryptographicHash::Md5);
|
||||
QString signature = QString::fromLatin1(digest.toHex()).rightJustified(32, '0').toLower();
|
||||
|
||||
ParamList params = params_to_sign;
|
||||
params << Param("request_ts", QString::number(timestamp));
|
||||
params << Param("request_sig", signature);
|
||||
params << Param("user_auth_token", user_auth_token());
|
||||
|
||||
std::sort(params.begin(), params.end());
|
||||
|
||||
reply_ = CreateRequest(QString("track/getFileUrl"), params);
|
||||
connect(reply_, SIGNAL(finished()), this, SLOT(StreamURLReceived()));
|
||||
|
||||
}
|
||||
|
||||
void QobuzStreamURLRequest::StreamURLReceived() {
|
||||
|
||||
if (!reply_) return;
|
||||
|
||||
QByteArray data = GetReplyData(reply_);
|
||||
|
||||
disconnect(reply_, 0, this, 0);
|
||||
reply_->deleteLater();
|
||||
reply_ = nullptr;
|
||||
|
||||
if (data.isEmpty()) {
|
||||
if (!authenticated() && login_sent() && tries_ <= 1) {
|
||||
need_login_ = true;
|
||||
return;
|
||||
}
|
||||
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(data);
|
||||
if (json_obj.isEmpty()) {
|
||||
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("track_id")) {
|
||||
Error("Invalid Json reply, stream url is missing track_id.", json_obj);
|
||||
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
|
||||
return;
|
||||
}
|
||||
|
||||
int track_id = json_obj["track_id"].toInt();
|
||||
if (track_id != song_id_) {
|
||||
Error("Incorrect track ID returned.", json_obj);
|
||||
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("mime_type") || !json_obj.contains("url")) {
|
||||
Error("Invalid Json reply, stream url is missing url or mime_type.", json_obj);
|
||||
emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first());
|
||||
return;
|
||||
}
|
||||
|
||||
QUrl url(json_obj["url"].toString());
|
||||
QString mimetype = json_obj["mime_type"].toString();
|
||||
|
||||
Song::FileType filetype(Song::FileType_Unknown);
|
||||
QMimeDatabase mimedb;
|
||||
for (QString suffix : mimedb.mimeTypeForName(mimetype.toUtf8()).suffixes()) {
|
||||
filetype = Song::FiletypeByExtension(suffix);
|
||||
if (filetype != Song::FileType_Unknown) break;
|
||||
}
|
||||
if (filetype == Song::FileType_Unknown) {
|
||||
qLog(Debug) << "Qobuz: Unknown mimetype" << mimetype;
|
||||
filetype = Song::FileType_Stream;
|
||||
}
|
||||
|
||||
if (!url.isValid()) {
|
||||
Error("Returned stream url is invalid.", json_obj);
|
||||
emit StreamURLFinished(original_url_, original_url_, filetype, -1, -1, -1, errors_.first());
|
||||
return;
|
||||
}
|
||||
|
||||
qint64 duration = -1;
|
||||
if (json_obj.contains("duration")) {
|
||||
duration = json_obj["duration"].toDouble() * kNsecPerSec;
|
||||
}
|
||||
int samplerate = -1;
|
||||
if (json_obj.contains("sampling_rate")) {
|
||||
samplerate = json_obj["sampling_rate"].toDouble() * 1000;
|
||||
}
|
||||
int bit_depth = -1;
|
||||
if (json_obj.contains("bit_depth")) {
|
||||
bit_depth = json_obj["bit_depth"].toDouble();
|
||||
}
|
||||
|
||||
emit StreamURLFinished(original_url_, url, filetype, samplerate, bit_depth, duration);
|
||||
|
||||
}
|
||||
|
||||
void QobuzStreamURLRequest::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
if (!error.isEmpty()) {
|
||||
qLog(Error) << "Qobuz:" << error;
|
||||
errors_ << error;
|
||||
}
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
}
|
||||
|
76
src/qobuz/qobuzstreamurlrequest.h
Normal file
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019, 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 QOBUZSTREAMURLREQUEST_H
|
||||
#define QOBUZSTREAMURLREQUEST_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "qobuzbaserequest.h"
|
||||
|
||||
class QNetworkReply;
|
||||
class NetworkAccessManager;
|
||||
class QobuzService;
|
||||
|
||||
class QobuzStreamURLRequest : public QobuzBaseRequest {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QobuzStreamURLRequest(QobuzService *service, NetworkAccessManager *network, const QUrl &original_url, QObject *parent);
|
||||
~QobuzStreamURLRequest();
|
||||
|
||||
void GetStreamURL();
|
||||
void Process();
|
||||
void NeedLogin() { need_login_ = true; }
|
||||
void Cancel();
|
||||
|
||||
QUrl original_url() { return original_url_; }
|
||||
int song_id() { return song_id_; }
|
||||
bool need_login() { return need_login_; }
|
||||
|
||||
signals:
|
||||
void TryLogin();
|
||||
void StreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error = QString());
|
||||
|
||||
private slots:
|
||||
void LoginComplete(const bool success, const QString &error = QString());
|
||||
void StreamURLReceived();
|
||||
|
||||
private:
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
QobuzService *service_;
|
||||
QNetworkReply *reply_;
|
||||
QUrl original_url_;
|
||||
int song_id_;
|
||||
int tries_;
|
||||
bool need_login_;
|
||||
QStringList errors_;
|
||||
|
||||
};
|
||||
|
||||
#endif // QOBUZSTREAMURLREQUEST_H
|
68
src/qobuz/qobuzurlhandler.cpp
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "core/song.h"
|
||||
#include "qobuz/qobuzservice.h"
|
||||
#include "qobuzurlhandler.h"
|
||||
|
||||
QobuzUrlHandler::QobuzUrlHandler(Application *app, QobuzService *service) :
|
||||
UrlHandler(service),
|
||||
app_(app),
|
||||
service_(service),
|
||||
task_id_(-1)
|
||||
{
|
||||
|
||||
connect(service, SIGNAL(StreamURLFinished(QUrl, QUrl, Song::FileType, int, int, qint64, QString)), this, SLOT(GetStreamURLFinished(QUrl, QUrl, Song::FileType, int, int, qint64, QString)));
|
||||
|
||||
}
|
||||
|
||||
UrlHandler::LoadResult QobuzUrlHandler::StartLoading(const QUrl &url) {
|
||||
|
||||
LoadResult ret(url);
|
||||
if (task_id_ != -1) return ret;
|
||||
task_id_ = app_->task_manager()->StartTask(QString("Loading %1 stream...").arg(url.scheme()));
|
||||
service_->GetStreamURL(url);
|
||||
ret.type_ = LoadResult::WillLoadAsynchronously;
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void QobuzUrlHandler::GetStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error) {
|
||||
|
||||
if (task_id_ == -1) return;
|
||||
CancelTask();
|
||||
if (error.isEmpty()) {
|
||||
emit AsyncLoadComplete(LoadResult(original_url, LoadResult::TrackAvailable, stream_url, filetype, samplerate, bit_depth, duration));
|
||||
}
|
||||
else {
|
||||
emit AsyncLoadComplete(LoadResult(original_url, LoadResult::Error, error));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void QobuzUrlHandler::CancelTask() {
|
||||
app_->task_manager()->SetTaskFinished(task_id_);
|
||||
task_id_ = -1;
|
||||
}
|
55
src/qobuz/qobuzurlhandler.h
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 QOBUZURLHANDLER_H
|
||||
#define QOBUZURLHANDLER_H
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/urlhandler.h"
|
||||
#include "core/song.h"
|
||||
#include "qobuz/qobuzservice.h"
|
||||
|
||||
class Application;
|
||||
|
||||
class QobuzUrlHandler : public UrlHandler {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QobuzUrlHandler(Application *app, QobuzService *service);
|
||||
|
||||
QString scheme() const { return service_->url_scheme(); }
|
||||
LoadResult StartLoading(const QUrl &url);
|
||||
|
||||
void CancelTask();
|
||||
|
||||
private slots:
|
||||
void GetStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error = QString());
|
||||
|
||||
private:
|
||||
Application *app_;
|
||||
QobuzService *service_;
|
||||
int task_id_;
|
||||
|
||||
};
|
||||
|
||||
#endif
|
@ -121,6 +121,10 @@ void CoversSettingsPage::CurrentItemChanged(QListWidgetItem *item_current, QList
|
||||
DisableAuthentication();
|
||||
ui_->label_auth_info->setText(tr("Use Tidal settings to authenticate."));
|
||||
}
|
||||
else if (provider->name() == "Qobuz" && !provider->IsAuthenticated()) {
|
||||
DisableAuthentication();
|
||||
ui_->label_auth_info->setText(tr("Use Qobuz settings to authenticate."));
|
||||
}
|
||||
else {
|
||||
ui_->login_state->SetLoggedIn(provider->IsAuthenticated() ? LoginStateWidget::LoggedIn : LoginStateWidget::LoggedOut);
|
||||
ui_->button_authenticate->setEnabled(true);
|
||||
@ -229,6 +233,10 @@ void CoversSettingsPage::LogoutClicked() {
|
||||
DisableAuthentication();
|
||||
ui_->label_auth_info->setText(tr("Use Tidal settings to authenticate."));
|
||||
}
|
||||
else if (provider->name() == "Qobuz") {
|
||||
DisableAuthentication();
|
||||
ui_->label_auth_info->setText(tr("Use Qobuz settings to authenticate."));
|
||||
}
|
||||
else {
|
||||
ui_->button_authenticate->setEnabled(true);
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
|
||||
|
171
src/settings/qobuzsettingspage.cpp
Normal file
@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019, 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 <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QSettings>
|
||||
#include <QMessageBox>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QEvent>
|
||||
|
||||
#include "settingsdialog.h"
|
||||
#include "qobuzsettingspage.h"
|
||||
#include "ui_qobuzsettingspage.h"
|
||||
#include "core/application.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "widgets/loginstatewidget.h"
|
||||
#include "internet/internetservices.h"
|
||||
#include "qobuz/qobuzservice.h"
|
||||
|
||||
const char *QobuzSettingsPage::kSettingsGroup = "Qobuz";
|
||||
|
||||
QobuzSettingsPage::QobuzSettingsPage(SettingsDialog *parent)
|
||||
: SettingsPage(parent),
|
||||
ui_(new Ui::QobuzSettingsPage),
|
||||
service_(dialog()->app()->internet_services()->Service<QobuzService>()) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
setWindowIcon(IconLoader::Load("qobuz"));
|
||||
|
||||
connect(ui_->button_login, SIGNAL(clicked()), SLOT(LoginClicked()));
|
||||
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked()));
|
||||
|
||||
connect(this, SIGNAL(Login(QString, QString, QString)), service_, SLOT(SendLogin(QString, QString, QString)));
|
||||
|
||||
connect(service_, SIGNAL(LoginFailure(QString)), SLOT(LoginFailure(QString)));
|
||||
connect(service_, SIGNAL(LoginSuccess()), SLOT(LoginSuccess()));
|
||||
|
||||
dialog()->installEventFilter(this);
|
||||
|
||||
ui_->format->addItem("MP3 320", 5);
|
||||
ui_->format->addItem("FLAC Lossless", 6);
|
||||
ui_->format->addItem("FLAC Hi-Res <= 96kHz", 7);
|
||||
ui_->format->addItem("FLAC Hi-Res > 96kHz", 27);
|
||||
|
||||
}
|
||||
|
||||
QobuzSettingsPage::~QobuzSettingsPage() { delete ui_; }
|
||||
|
||||
void QobuzSettingsPage::Load() {
|
||||
|
||||
QSettings s;
|
||||
|
||||
s.beginGroup(kSettingsGroup);
|
||||
ui_->enable->setChecked(s.value("enabled", false).toBool());
|
||||
ui_->app_id->setText(s.value("app_id").toString());
|
||||
ui_->app_secret->setText(s.value("app_secret").toString());
|
||||
|
||||
ui_->username->setText(s.value("username").toString());
|
||||
QByteArray password = s.value("password").toByteArray();
|
||||
if (password.isEmpty()) ui_->password->clear();
|
||||
else ui_->password->setText(QString::fromUtf8(QByteArray::fromBase64(password)));
|
||||
|
||||
dialog()->ComboBoxLoadFromSettings(s, ui_->format, "format", 27);
|
||||
ui_->searchdelay->setValue(s.value("searchdelay", 1500).toInt());
|
||||
ui_->artistssearchlimit->setValue(s.value("artistssearchlimit", 4).toInt());
|
||||
ui_->albumssearchlimit->setValue(s.value("albumssearchlimit", 10).toInt());
|
||||
ui_->songssearchlimit->setValue(s.value("songssearchlimit", 10).toInt());
|
||||
ui_->checkbox_download_album_covers->setChecked(s.value("downloadalbumcovers", true).toBool());
|
||||
|
||||
s.endGroup();
|
||||
|
||||
if (service_->authenticated()) ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn);
|
||||
|
||||
Init(ui_->layout_qobuzsettingspage->parentWidget());
|
||||
|
||||
}
|
||||
|
||||
void QobuzSettingsPage::Save() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("enabled", ui_->enable->isChecked());
|
||||
s.setValue("app_id", ui_->app_id->text());
|
||||
s.setValue("app_secret", ui_->app_secret->text());
|
||||
|
||||
s.setValue("username", ui_->username->text());
|
||||
s.setValue("password", QString::fromUtf8(ui_->password->text().toUtf8().toBase64()));
|
||||
|
||||
s.setValue("format", ui_->format->itemData(ui_->format->currentIndex()));
|
||||
s.setValue("searchdelay", ui_->searchdelay->value());
|
||||
s.setValue("artistssearchlimit", ui_->artistssearchlimit->value());
|
||||
s.setValue("albumssearchlimit", ui_->albumssearchlimit->value());
|
||||
s.setValue("songssearchlimit", ui_->songssearchlimit->value());
|
||||
s.setValue("downloadalbumcovers", ui_->checkbox_download_album_covers->isChecked());
|
||||
s.endGroup();
|
||||
|
||||
service_->ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
void QobuzSettingsPage::LoginClicked() {
|
||||
|
||||
if (ui_->app_id->text().isEmpty()) {
|
||||
QMessageBox::critical(this, tr("Configuration incomplete"), tr("Missing app id."));
|
||||
return;
|
||||
}
|
||||
if (ui_->username->text().isEmpty()) {
|
||||
QMessageBox::critical(this, tr("Configuration incomplete"), tr("Missing username."));
|
||||
return;
|
||||
}
|
||||
if (ui_->password->text().isEmpty()) {
|
||||
QMessageBox::critical(this, tr("Configuration incomplete"), tr("Missing password."));
|
||||
return;
|
||||
}
|
||||
|
||||
emit Login(ui_->app_id->text(), ui_->username->text(), ui_->password->text());
|
||||
ui_->button_login->setEnabled(false);
|
||||
|
||||
}
|
||||
|
||||
bool QobuzSettingsPage::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 QobuzSettingsPage::LogoutClicked() {
|
||||
service_->Logout();
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
|
||||
ui_->button_login->setEnabled(true);
|
||||
}
|
||||
|
||||
void QobuzSettingsPage::LoginSuccess() {
|
||||
if (!this->isVisible()) return;
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn);
|
||||
ui_->button_login->setEnabled(true);
|
||||
}
|
||||
|
||||
void QobuzSettingsPage::LoginFailure(QString failure_reason) {
|
||||
if (!this->isVisible()) return;
|
||||
QMessageBox::warning(this, tr("Authentication failed"), failure_reason);
|
||||
}
|
62
src/settings/qobuzsettingspage.h
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019, 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 QOBUZSETTINGSPAGE_H
|
||||
#define QOBUZSETTINGSPAGE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "settings/settingspage.h"
|
||||
|
||||
class QEvent;
|
||||
class SettingsDialog;
|
||||
class QobuzService;
|
||||
class Ui_QobuzSettingsPage;
|
||||
|
||||
class QobuzSettingsPage : public SettingsPage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QobuzSettingsPage(SettingsDialog* parent = nullptr);
|
||||
~QobuzSettingsPage();
|
||||
|
||||
static const char *kSettingsGroup;
|
||||
|
||||
void Load();
|
||||
void Save();
|
||||
|
||||
bool eventFilter(QObject *object, QEvent *event);
|
||||
|
||||
signals:
|
||||
void Login();
|
||||
void Login(const QString &username, const QString &password, const QString &token);
|
||||
|
||||
private slots:
|
||||
void LoginClicked();
|
||||
void LogoutClicked();
|
||||
void LoginSuccess();
|
||||
void LoginFailure(QString failure_reason);
|
||||
|
||||
private:
|
||||
Ui_QobuzSettingsPage* ui_;
|
||||
QobuzService *service_;
|
||||
};
|
||||
|
||||
#endif
|
306
src/settings/qobuzsettingspage.ui
Normal file
@ -0,0 +1,306 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>QobuzSettingsPage</class>
|
||||
<widget class="QWidget" name="QobuzSettingsPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>472</width>
|
||||
<height>697</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Qobuz</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="layout_qobuzsettingspage">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="enable">
|
||||
<property name="text">
|
||||
<string>Enable</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_warning">
|
||||
<property name="text">
|
||||
<string>Qobuz support is not official and requires an API app ID and secret from a registered application to work. We can't help you getting these.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</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>Authentication</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="layout_credential_group">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_app_id">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>App ID</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="app_id"/>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_username">
|
||||
<property name="text">
|
||||
<string>Username</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="username"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_password">
|
||||
<property name="text">
|
||||
<string>Password</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLineEdit" name="password">
|
||||
<property name="echoMode">
|
||||
<enum>QLineEdit::Password</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_app_secret">
|
||||
<property name="text">
|
||||
<string>App Secret</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="app_secret"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_login">
|
||||
<property name="text">
|
||||
<string>Login</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="LoginStateWidget" name="login_state" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupbox_preferences">
|
||||
<property name="title">
|
||||
<string>Preferences</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="layout_preferences">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_format">
|
||||
<property name="text">
|
||||
<string>Audio format</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="format"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_searchdelay">
|
||||
<property name="text">
|
||||
<string>Search delay</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="searchdelay">
|
||||
<property name="suffix">
|
||||
<string>ms</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>50</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>1500</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_artistssearchlimit">
|
||||
<property name="text">
|
||||
<string>Artists search limit</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QSpinBox" name="artistssearchlimit">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>50</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_albumssearchlimit">
|
||||
<property name="text">
|
||||
<string>Albums search limit</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QSpinBox" name="albumssearchlimit">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>1000</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>50</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_songssearchlimit">
|
||||
<property name="text">
|
||||
<string>Songs search limit</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QSpinBox" name="songssearchlimit">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>1000</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>50</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QCheckBox" name="checkbox_download_album_covers">
|
||||
<property name="text">
|
||||
<string>Download album covers</string>
|
||||
</property>
|
||||
</widget>
|
||||
</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="layout_bottom">
|
||||
<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_qobuz">
|
||||
<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/icons.qrc">:/icons/64x64/qobuz.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>enable</tabstop>
|
||||
<tabstop>app_id</tabstop>
|
||||
<tabstop>app_secret</tabstop>
|
||||
<tabstop>username</tabstop>
|
||||
<tabstop>password</tabstop>
|
||||
<tabstop>button_login</tabstop>
|
||||
<tabstop>format</tabstop>
|
||||
<tabstop>searchdelay</tabstop>
|
||||
<tabstop>artistssearchlimit</tabstop>
|
||||
<tabstop>albumssearchlimit</tabstop>
|
||||
<tabstop>songssearchlimit</tabstop>
|
||||
<tabstop>checkbox_download_album_covers</tabstop>
|
||||
</tabstops>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
<include location="../../data/icons.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
@ -78,6 +78,9 @@
|
||||
#ifdef HAVE_TIDAL
|
||||
# include "tidalsettingspage.h"
|
||||
#endif
|
||||
#ifdef HAVE_QOBUZ
|
||||
# include "qobuzsettingspage.h"
|
||||
#endif
|
||||
|
||||
#include "ui_settingsdialog.h"
|
||||
|
||||
@ -153,7 +156,7 @@ SettingsDialog::SettingsDialog(Application *app, OSDBase *osd, QMainWindow *main
|
||||
AddPage(Page_Moodbar, new MoodbarSettingsPage(this), iface);
|
||||
#endif
|
||||
|
||||
#if defined(HAVE_SUBSONIC) || defined(HAVE_TIDAL)
|
||||
#if defined(HAVE_SUBSONIC) || defined(HAVE_TIDAL) || defined(HAVE_QOBUZ)
|
||||
QTreeWidgetItem *streaming = AddCategory(tr("Streaming"));
|
||||
#endif
|
||||
|
||||
@ -163,6 +166,9 @@ SettingsDialog::SettingsDialog(Application *app, OSDBase *osd, QMainWindow *main
|
||||
#ifdef HAVE_TIDAL
|
||||
AddPage(Page_Tidal, new TidalSettingsPage(this), streaming);
|
||||
#endif
|
||||
#ifdef HAVE_QOBUZ
|
||||
AddPage(Page_Qobuz, new QobuzSettingsPage(this), streaming);
|
||||
#endif
|
||||
|
||||
// List box
|
||||
connect(ui_->list, SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)), SLOT(CurrentItemChanged(QTreeWidgetItem*)));
|
||||
|
@ -90,6 +90,7 @@ class SettingsDialog : public QDialog {
|
||||
Page_Moodbar,
|
||||
Page_Subsonic,
|
||||
Page_Tidal,
|
||||
Page_Qobuz,
|
||||
};
|
||||
|
||||
enum Role {
|
||||
|
43
src/smartplaylists/playlistgenerator.cpp
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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "core/logging.h"
|
||||
|
||||
#include "playlistgenerator.h"
|
||||
#include "playlistquerygenerator.h"
|
||||
|
||||
const int PlaylistGenerator::kDefaultLimit = 20;
|
||||
const int PlaylistGenerator::kDefaultDynamicHistory = 5;
|
||||
const int PlaylistGenerator::kDefaultDynamicFuture = 15;
|
||||
|
||||
PlaylistGenerator::PlaylistGenerator() : QObject(nullptr) {}
|
||||
|
||||
PlaylistGeneratorPtr PlaylistGenerator::Create(const Type type) {
|
||||
|
||||
Q_UNUSED(type)
|
||||
|
||||
return PlaylistGeneratorPtr(new PlaylistQueryGenerator);
|
||||
|
||||
}
|
100
src/smartplaylists/playlistgenerator.h
Normal file
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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 PLAYLISTGENERATOR_H
|
||||
#define PLAYLISTGENERATOR_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include "playlist/playlistitem.h"
|
||||
|
||||
class CollectionBackend;
|
||||
|
||||
class PlaylistGenerator : public QObject, public std::enable_shared_from_this<PlaylistGenerator> {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PlaylistGenerator();
|
||||
|
||||
static const int kDefaultLimit;
|
||||
static const int kDefaultDynamicHistory;
|
||||
static const int kDefaultDynamicFuture;
|
||||
|
||||
enum Type {
|
||||
Type_None = 0,
|
||||
Type_Query = 1
|
||||
};
|
||||
|
||||
// Creates a new PlaylistGenerator of the given type
|
||||
static std::shared_ptr<PlaylistGenerator> Create(const Type type = Type_Query);
|
||||
|
||||
// Should be called before Load on a new PlaylistGenerator
|
||||
void set_collection(CollectionBackend *backend) { backend_ = backend; }
|
||||
void set_name(const QString &name) { name_ = name; }
|
||||
CollectionBackend *collection() const { return backend_; }
|
||||
QString name() const { return name_; }
|
||||
|
||||
// Name of the subclass
|
||||
virtual Type type() const = 0;
|
||||
|
||||
// Serializes the PlaylistGenerator's settings
|
||||
// Called on UI-thread.
|
||||
virtual void Load(const QByteArray &data) = 0;
|
||||
// Called on UI-thread.
|
||||
virtual QByteArray Save() const = 0;
|
||||
|
||||
// Creates and returns a playlist
|
||||
// Called from non-UI thread.
|
||||
virtual PlaylistItemList Generate() = 0;
|
||||
|
||||
// If the generator can be used as a dynamic playlist then GenerateMore should return the next tracks in the sequence.
|
||||
// The subclass should remember the last GetDynamicHistory() + GetDynamicFuture() tracks,
|
||||
// and ensure that the tracks returned from this method are not in that set.
|
||||
virtual bool is_dynamic() const { return false; }
|
||||
virtual void set_dynamic(const bool dynamic) { Q_UNUSED(dynamic); }
|
||||
// Called from non-UI thread.
|
||||
virtual PlaylistItemList GenerateMore(int count) {
|
||||
Q_UNUSED(count);
|
||||
return PlaylistItemList();
|
||||
}
|
||||
|
||||
virtual int GetDynamicHistory() { return kDefaultDynamicHistory; }
|
||||
virtual int GetDynamicFuture() { return kDefaultDynamicFuture; }
|
||||
|
||||
signals:
|
||||
void Error(const QString& message);
|
||||
|
||||
protected:
|
||||
CollectionBackend *backend_;
|
||||
|
||||
private:
|
||||
QString name_;
|
||||
|
||||
};
|
||||
|
||||
#include "playlistgenerator_fwd.h"
|
||||
|
||||
#endif // PLAYLISTGENERATOR_H
|
32
src/smartplaylists/playlistgenerator_fwd.h
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 PLAYLISTGENERATOR_FWD_H
|
||||
#define PLAYLISTGENERATOR_FWD_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
class PlaylistGenerator;
|
||||
|
||||
typedef std::shared_ptr<PlaylistGenerator> PlaylistGeneratorPtr;
|
||||
|
||||
#endif // PLAYLISTGENERATOR_FWD_H
|
90
src/smartplaylists/playlistgeneratorinserter.cpp
Normal file
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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 <QObject>
|
||||
#include <QString>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
#include "core/closure.h"
|
||||
#include "core/taskmanager.h"
|
||||
|
||||
#include "playlist/playlist.h"
|
||||
#include "playlistgenerator.h"
|
||||
#include "playlistgeneratorinserter.h"
|
||||
|
||||
class CollectionBackend;
|
||||
|
||||
PlaylistGeneratorInserter::PlaylistGeneratorInserter(TaskManager *task_manager, CollectionBackend *collection, QObject *parent)
|
||||
: QObject(parent),
|
||||
task_manager_(task_manager),
|
||||
collection_(collection),
|
||||
task_id_(-1),
|
||||
is_dynamic_(false)
|
||||
{}
|
||||
|
||||
PlaylistItemList PlaylistGeneratorInserter::Generate(PlaylistGeneratorPtr generator, int dynamic_count) {
|
||||
|
||||
if (dynamic_count) {
|
||||
return generator->GenerateMore(dynamic_count);
|
||||
}
|
||||
else {
|
||||
return generator->Generate();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PlaylistGeneratorInserter::Load(Playlist *destination, const int row, const bool play_now, const bool enqueue, const bool enqueue_next, PlaylistGeneratorPtr generator, const int dynamic_count) {
|
||||
|
||||
task_id_ = task_manager_->StartTask(tr("Loading smart playlist"));
|
||||
|
||||
destination_ = destination;
|
||||
row_ = row;
|
||||
play_now_ = play_now;
|
||||
enqueue_ = enqueue;
|
||||
enqueue_next_ = enqueue_next;
|
||||
is_dynamic_ = generator->is_dynamic();
|
||||
|
||||
connect(generator.get(), SIGNAL(Error(QString)), SIGNAL(Error(QString)));
|
||||
|
||||
QFuture<PlaylistItemList> future = QtConcurrent::run(PlaylistGeneratorInserter::Generate, generator, dynamic_count);
|
||||
NewClosure(future, this, SLOT(Finished(QFuture<PlaylistItemList>)), future);
|
||||
|
||||
}
|
||||
|
||||
void PlaylistGeneratorInserter::Finished(QFuture<PlaylistItemList> future) {
|
||||
|
||||
PlaylistItemList items = future.result();
|
||||
|
||||
if (items.isEmpty()) {
|
||||
if (is_dynamic_) {
|
||||
destination_->TurnOffDynamicPlaylist();
|
||||
}
|
||||
}
|
||||
else {
|
||||
destination_->InsertItems(items, row_, play_now_, enqueue_);
|
||||
}
|
||||
|
||||
task_manager_->SetTaskFinished(task_id_);
|
||||
|
||||
deleteLater();
|
||||
|
||||
}
|
71
src/smartplaylists/playlistgeneratorinserter.h
Normal file
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 PLAYLISTGENERATORINSERTER_H
|
||||
#define PLAYLISTGENERATORINSERTER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QFuture>
|
||||
#include <QString>
|
||||
|
||||
#include "playlist/playlist.h"
|
||||
#include "playlist/playlistitem.h"
|
||||
|
||||
#include "playlistgenerator_fwd.h"
|
||||
|
||||
class TaskManager;
|
||||
class CollectionBackend;
|
||||
class Playlist;
|
||||
|
||||
class PlaylistGeneratorInserter : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PlaylistGeneratorInserter(TaskManager *task_manager, CollectionBackend *collection, QObject *parent);
|
||||
|
||||
void Load(Playlist *destination, const int row, const bool play_now, const bool enqueue, const bool enqueue_next, PlaylistGeneratorPtr generator, const int dynamic_count = 0);
|
||||
|
||||
private:
|
||||
static PlaylistItemList Generate(PlaylistGeneratorPtr generator, const int dynamic_count);
|
||||
|
||||
signals:
|
||||
void Error(const QString &message);
|
||||
void PlayRequested(const QModelIndex &idx);
|
||||
|
||||
private slots:
|
||||
void Finished(QFuture<PlaylistItemList> future);
|
||||
|
||||
private:
|
||||
TaskManager *task_manager_;
|
||||
CollectionBackend *collection_;
|
||||
int task_id_;
|
||||
|
||||
Playlist *destination_;
|
||||
int row_;
|
||||
bool play_now_;
|
||||
bool enqueue_;
|
||||
bool enqueue_next_;
|
||||
bool is_dynamic_;
|
||||
|
||||
};
|
||||
|
||||
#endif // PLAYLISTGENERATORINSERTER_H
|
41
src/smartplaylists/playlistgeneratormimedata.h
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 PLAYLISTGENERATORMIMEDATA_H
|
||||
#define PLAYLISTGENERATORMIMEDATA_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QMimeData>
|
||||
|
||||
#include "core/mimedata.h"
|
||||
#include "playlistgenerator_fwd.h"
|
||||
|
||||
class PlaylistGeneratorMimeData : public MimeData {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PlaylistGeneratorMimeData(PlaylistGeneratorPtr generator) : generator_(generator) {}
|
||||
|
||||
PlaylistGeneratorPtr generator_;
|
||||
};
|
||||
|
||||
#endif // PLAYLISTGENERATORMIMEDATA_H
|
101
src/smartplaylists/playlistquerygenerator.cpp
Normal file
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 <QIODevice>
|
||||
#include <QDataStream>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "playlistquerygenerator.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
|
||||
PlaylistQueryGenerator::PlaylistQueryGenerator() : dynamic_(false), current_pos_(0) {}
|
||||
|
||||
PlaylistQueryGenerator::PlaylistQueryGenerator(const QString &name, const SmartPlaylistSearch &search, const bool dynamic)
|
||||
: search_(search), dynamic_(dynamic), current_pos_(0) {
|
||||
|
||||
set_name(name);
|
||||
|
||||
}
|
||||
|
||||
void PlaylistQueryGenerator::Load(const SmartPlaylistSearch &search) {
|
||||
|
||||
search_ = search;
|
||||
dynamic_ = false;
|
||||
current_pos_ = 0;
|
||||
|
||||
}
|
||||
|
||||
void PlaylistQueryGenerator::Load(const QByteArray &data) {
|
||||
|
||||
QDataStream s(data);
|
||||
s >> search_;
|
||||
s >> dynamic_;
|
||||
|
||||
}
|
||||
|
||||
QByteArray PlaylistQueryGenerator::Save() const {
|
||||
|
||||
QByteArray ret;
|
||||
QDataStream s(&ret, QIODevice::WriteOnly);
|
||||
s << search_;
|
||||
s << dynamic_;
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
PlaylistItemList PlaylistQueryGenerator::Generate() {
|
||||
|
||||
previous_ids_.clear();
|
||||
current_pos_ = 0;
|
||||
return GenerateMore(0);
|
||||
|
||||
}
|
||||
|
||||
PlaylistItemList PlaylistQueryGenerator::GenerateMore(const int count) {
|
||||
|
||||
SmartPlaylistSearch search_copy = search_;
|
||||
search_copy.id_not_in_ = previous_ids_;
|
||||
if (count) {
|
||||
search_copy.limit_ = count;
|
||||
}
|
||||
|
||||
if (search_copy.sort_type_ != SmartPlaylistSearch::Sort_Random) {
|
||||
search_copy.first_item_ = current_pos_;
|
||||
current_pos_ += search_copy.limit_;
|
||||
}
|
||||
|
||||
SongList songs = backend_->FindSongs(search_copy);
|
||||
PlaylistItemList items;
|
||||
for (const Song &song : songs) {
|
||||
items << PlaylistItemPtr(PlaylistItem::NewFromSong(song));
|
||||
previous_ids_ << song.id();
|
||||
|
||||
if (previous_ids_.count() > GetDynamicFuture() + GetDynamicHistory())
|
||||
previous_ids_.removeFirst();
|
||||
}
|
||||
|
||||
return items;
|
||||
|
||||
}
|
61
src/smartplaylists/playlistquerygenerator.h
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 PLAYLISTQUERYGENERATOR_H
|
||||
#define PLAYLISTQUERYGENERATOR_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include "playlistgenerator.h"
|
||||
#include "smartplaylistsearch.h"
|
||||
|
||||
class PlaylistQueryGenerator : public PlaylistGenerator {
|
||||
public:
|
||||
explicit PlaylistQueryGenerator();
|
||||
explicit PlaylistQueryGenerator(const QString &name, const SmartPlaylistSearch &search, const bool dynamic = false);
|
||||
|
||||
Type type() const { return Type_Query; }
|
||||
|
||||
void Load(const SmartPlaylistSearch &search);
|
||||
void Load(const QByteArray &data);
|
||||
QByteArray Save() const;
|
||||
|
||||
PlaylistItemList Generate();
|
||||
PlaylistItemList GenerateMore(const int count);
|
||||
bool is_dynamic() const { return dynamic_; }
|
||||
void set_dynamic(bool dynamic) { dynamic_ = dynamic; }
|
||||
|
||||
SmartPlaylistSearch search() const { return search_; }
|
||||
int GetDynamicFuture() { return search_.limit_; }
|
||||
|
||||
private:
|
||||
SmartPlaylistSearch search_;
|
||||
bool dynamic_;
|
||||
|
||||
QList<int> previous_ids_;
|
||||
int current_pos_;
|
||||
|
||||
};
|
||||
|
||||
#endif // PLAYLISTQUERYGENERATOR_H
|
134
src/smartplaylists/smartplaylistquerysearchpage.ui
Normal file
@ -0,0 +1,134 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SmartPlaylistQuerySearchPage</class>
|
||||
<widget class="QWidget" name="SmartPlaylistQuerySearchPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>448</width>
|
||||
<height>450</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#terms_scroll_area, #terms_scroll_area_content {
|
||||
background: transparent;
|
||||
}</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_3">
|
||||
<property name="title">
|
||||
<string>Search mode</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QComboBox" name="type">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Match every search term (AND)</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Match one or more search terms (OR)</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Include all songs</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="terms_group">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Search terms</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<widget class="QScrollArea" name="terms_scroll_area">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="terms_scroll_area_content">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>418</width>
|
||||
<height>251</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
130
src/smartplaylists/smartplaylistquerysortpage.ui
Normal file
@ -0,0 +1,130 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SmartPlaylistQuerySortPage</class>
|
||||
<widget class="QWidget" name="SmartPlaylistQuerySortPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>723</width>
|
||||
<height>335</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<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="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Sorting</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QRadioButton" name="random">
|
||||
<property name="text">
|
||||
<string>Put songs in a random order</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QRadioButton" name="field">
|
||||
<property name="text">
|
||||
<string>Sort songs by</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QComboBox" name="field_value"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="order">
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QComboBox::AdjustToContents</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Limits</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout_2">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QRadioButton" name="limit_none">
|
||||
<property name="text">
|
||||
<string>Show all the songs</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QRadioButton" name="limit_limit">
|
||||
<property name="text">
|
||||
<string>Only show the first</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="limit_value">
|
||||
<property name="suffix">
|
||||
<string> songs</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>1000</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>15</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="SmartPlaylistSearchPreview" name="preview" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>SmartPlaylistSearchPreview</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>smartplaylists/smartplaylistsearchpreview.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
327
src/smartplaylists/smartplaylistquerywizardplugin.cpp
Normal file
@ -0,0 +1,327 @@
|
||||
/*
|
||||
* 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 <QWizardPage>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QVBoxLayout>
|
||||
#include <QScrollBar>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "playlistquerygenerator.h"
|
||||
#include "smartplaylistquerywizardplugin.h"
|
||||
#include "smartplaylistsearchtermwidget.h"
|
||||
#include "ui_smartplaylistquerysearchpage.h"
|
||||
#include "ui_smartplaylistquerysortpage.h"
|
||||
|
||||
class SmartPlaylistQueryWizardPlugin::SearchPage : public QWizardPage {
|
||||
|
||||
friend class SmartPlaylistQueryWizardPlugin;
|
||||
|
||||
public:
|
||||
SearchPage(QWidget *parent = 0)
|
||||
: QWizardPage(parent), ui_(new Ui_SmartPlaylistQuerySearchPage) {
|
||||
ui_->setupUi(this);
|
||||
}
|
||||
|
||||
bool isComplete() const {
|
||||
if (ui_->type->currentIndex() == 2) // All songs
|
||||
return true;
|
||||
|
||||
for (SmartPlaylistSearchTermWidget *widget : terms_) {
|
||||
if (!widget->Term().is_valid()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QVBoxLayout *layout_;
|
||||
QList<SmartPlaylistSearchTermWidget*> terms_;
|
||||
SmartPlaylistSearchTermWidget *new_term_;
|
||||
|
||||
SmartPlaylistSearchPreview *preview_;
|
||||
|
||||
std::unique_ptr<Ui_SmartPlaylistQuerySearchPage> ui_;
|
||||
};
|
||||
|
||||
class SmartPlaylistQueryWizardPlugin::SortPage : public QWizardPage {
|
||||
public:
|
||||
SortPage(SmartPlaylistQueryWizardPlugin *plugin, QWidget *parent, int next_id)
|
||||
: QWizardPage(parent), next_id_(next_id), plugin_(plugin) {}
|
||||
|
||||
void showEvent(QShowEvent*) { plugin_->UpdateSortPreview(); }
|
||||
|
||||
int nextId() const { return next_id_; }
|
||||
int next_id_;
|
||||
|
||||
SmartPlaylistQueryWizardPlugin *plugin_;
|
||||
};
|
||||
|
||||
SmartPlaylistQueryWizardPlugin::SmartPlaylistQueryWizardPlugin(Application *app, CollectionBackend *collection, QObject *parent)
|
||||
: SmartPlaylistWizardPlugin(app, collection, parent),
|
||||
search_page_(nullptr),
|
||||
previous_scrollarea_max_(0) {}
|
||||
|
||||
SmartPlaylistQueryWizardPlugin::~SmartPlaylistQueryWizardPlugin() {}
|
||||
|
||||
QString SmartPlaylistQueryWizardPlugin::name() const { return tr("Collection search"); }
|
||||
|
||||
QString SmartPlaylistQueryWizardPlugin::description() const {
|
||||
return tr("Find songs in your collection that match the criteria you specify.");
|
||||
}
|
||||
|
||||
int SmartPlaylistQueryWizardPlugin::CreatePages(QWizard *wizard, int finish_page_id) {
|
||||
|
||||
// Create the UI
|
||||
search_page_ = new SearchPage(wizard);
|
||||
|
||||
QWizardPage *sort_page = new SortPage(this, wizard, finish_page_id);
|
||||
sort_ui_.reset(new Ui_SmartPlaylistQuerySortPage);
|
||||
sort_ui_->setupUi(sort_page);
|
||||
|
||||
sort_ui_->limit_value->setValue(PlaylistGenerator::kDefaultLimit);
|
||||
|
||||
connect(search_page_->ui_->type, SIGNAL(currentIndexChanged(int)), SLOT(SearchTypeChanged()));
|
||||
|
||||
// Create the new search term widget
|
||||
search_page_->new_term_ = new SmartPlaylistSearchTermWidget(collection_, search_page_);
|
||||
search_page_->new_term_->SetActive(false);
|
||||
connect(search_page_->new_term_, SIGNAL(Clicked()), SLOT(AddSearchTerm()));
|
||||
|
||||
// Add an empty initial term
|
||||
search_page_->layout_ = static_cast<QVBoxLayout*>(search_page_->ui_->terms_scroll_area_content->layout());
|
||||
search_page_->layout_->addWidget(search_page_->new_term_);
|
||||
AddSearchTerm();
|
||||
|
||||
// Ensure that the terms are scrolled to the bottom when a new one is added
|
||||
connect(search_page_->ui_->terms_scroll_area->verticalScrollBar(), SIGNAL(rangeChanged(int, int)), this, SLOT(MoveTermListToBottom(int, int)));
|
||||
|
||||
// Add the preview widget at the bottom of the search terms page
|
||||
QVBoxLayout *terms_page_layout = static_cast<QVBoxLayout*>(search_page_->layout());
|
||||
terms_page_layout->addStretch();
|
||||
search_page_->preview_ = new SmartPlaylistSearchPreview(search_page_);
|
||||
search_page_->preview_->set_application(app_);
|
||||
search_page_->preview_->set_collection(collection_);
|
||||
terms_page_layout->addWidget(search_page_->preview_);
|
||||
|
||||
// Add sort field texts
|
||||
for (int i = 0; i < SmartPlaylistSearchTerm::FieldCount; ++i) {
|
||||
const SmartPlaylistSearchTerm::Field field = SmartPlaylistSearchTerm::Field(i);
|
||||
const QString field_name = SmartPlaylistSearchTerm::FieldName(field);
|
||||
sort_ui_->field_value->addItem(field_name);
|
||||
}
|
||||
connect(sort_ui_->field_value, SIGNAL(currentIndexChanged(int)), SLOT(UpdateSortOrder()));
|
||||
UpdateSortOrder();
|
||||
|
||||
// Set the sort and limit radio buttons back to their defaults - they would
|
||||
// have been changed by setupUi
|
||||
sort_ui_->random->setChecked(true);
|
||||
sort_ui_->limit_none->setChecked(true);
|
||||
|
||||
// Set up the preview widget that's already at the bottom of the sort page
|
||||
sort_ui_->preview->set_application(app_);
|
||||
sort_ui_->preview->set_collection(collection_);
|
||||
connect(sort_ui_->field, SIGNAL(toggled(bool)), SLOT(UpdateSortPreview()));
|
||||
connect(sort_ui_->field_value, SIGNAL(currentIndexChanged(int)), SLOT(UpdateSortPreview()));
|
||||
connect(sort_ui_->limit_limit, SIGNAL(toggled(bool)), SLOT(UpdateSortPreview()));
|
||||
connect(sort_ui_->limit_none, SIGNAL(toggled(bool)), SLOT(UpdateSortPreview()));
|
||||
connect(sort_ui_->limit_value, SIGNAL(valueChanged(int)), SLOT(UpdateSortPreview()));
|
||||
connect(sort_ui_->order, SIGNAL(currentIndexChanged(int)), SLOT(UpdateSortPreview()));
|
||||
connect(sort_ui_->random, SIGNAL(toggled(bool)), SLOT(UpdateSortPreview()));
|
||||
|
||||
// Configure the page text
|
||||
search_page_->setTitle(tr("Search terms"));
|
||||
search_page_->setSubTitle(tr("A song will be included in the playlist if it matches these conditions."));
|
||||
sort_page->setTitle(tr("Search options"));
|
||||
sort_page->setSubTitle(tr("Choose how the playlist is sorted and how many songs it will contain."));
|
||||
|
||||
// Add the pages
|
||||
const int first_page = wizard->addPage(search_page_);
|
||||
wizard->addPage(sort_page);
|
||||
return first_page;
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistQueryWizardPlugin::SetGenerator(PlaylistGeneratorPtr g) {
|
||||
|
||||
std::shared_ptr<PlaylistQueryGenerator> gen = std::dynamic_pointer_cast<PlaylistQueryGenerator>(g);
|
||||
if (!gen) return;
|
||||
SmartPlaylistSearch search = gen->search();
|
||||
|
||||
// Search type
|
||||
search_page_->ui_->type->setCurrentIndex(search.search_type_);
|
||||
|
||||
// Search terms
|
||||
qDeleteAll(search_page_->terms_);
|
||||
search_page_->terms_.clear();
|
||||
|
||||
for (const SmartPlaylistSearchTerm& term : search.terms_) {
|
||||
AddSearchTerm();
|
||||
search_page_->terms_.last()->SetTerm(term);
|
||||
}
|
||||
|
||||
// Sort order
|
||||
if (search.sort_type_ == SmartPlaylistSearch::Sort_Random) {
|
||||
sort_ui_->random->setChecked(true);
|
||||
}
|
||||
else {
|
||||
sort_ui_->field->setChecked(true);
|
||||
sort_ui_->order->setCurrentIndex(
|
||||
search.sort_type_ == SmartPlaylistSearch::Sort_FieldAsc ? 0 : 1);
|
||||
sort_ui_->field_value->setCurrentIndex(search.sort_field_);
|
||||
}
|
||||
|
||||
// Limit
|
||||
if (search.limit_ == -1) {
|
||||
sort_ui_->limit_none->setChecked(true);
|
||||
}
|
||||
else {
|
||||
sort_ui_->limit_limit->setChecked(true);
|
||||
sort_ui_->limit_value->setValue(search.limit_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PlaylistGeneratorPtr SmartPlaylistQueryWizardPlugin::CreateGenerator() const {
|
||||
|
||||
std::shared_ptr<PlaylistQueryGenerator> gen(new PlaylistQueryGenerator);
|
||||
gen->Load(MakeSearch());
|
||||
|
||||
return std::static_pointer_cast<PlaylistGenerator>(gen);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistQueryWizardPlugin::UpdateSortOrder() {
|
||||
|
||||
const SmartPlaylistSearchTerm::Field field = SmartPlaylistSearchTerm::Field(sort_ui_->field_value->currentIndex());
|
||||
const SmartPlaylistSearchTerm::Type type = SmartPlaylistSearchTerm::TypeOf(field);
|
||||
const QString asc = SmartPlaylistSearchTerm::FieldSortOrderText(type, true);
|
||||
const QString desc = SmartPlaylistSearchTerm::FieldSortOrderText(type, false);
|
||||
|
||||
const int old_current_index = sort_ui_->order->currentIndex();
|
||||
sort_ui_->order->clear();
|
||||
sort_ui_->order->addItem(asc);
|
||||
sort_ui_->order->addItem(desc);
|
||||
sort_ui_->order->setCurrentIndex(old_current_index);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistQueryWizardPlugin::AddSearchTerm() {
|
||||
|
||||
SmartPlaylistSearchTermWidget *widget = new SmartPlaylistSearchTermWidget(collection_, search_page_);
|
||||
connect(widget, SIGNAL(RemoveClicked()), SLOT(RemoveSearchTerm()));
|
||||
connect(widget, SIGNAL(Changed()), SLOT(UpdateTermPreview()));
|
||||
|
||||
search_page_->layout_->insertWidget(search_page_->terms_.count(), widget);
|
||||
search_page_->terms_ << widget;
|
||||
|
||||
UpdateTermPreview();
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistQueryWizardPlugin::RemoveSearchTerm() {
|
||||
|
||||
SmartPlaylistSearchTermWidget *widget = qobject_cast<SmartPlaylistSearchTermWidget*>(sender());
|
||||
if (!widget) return;
|
||||
|
||||
const int index = search_page_->terms_.indexOf(widget);
|
||||
if (index == -1) return;
|
||||
|
||||
search_page_->terms_.takeAt(index)->deleteLater();
|
||||
UpdateTermPreview();
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistQueryWizardPlugin::UpdateTermPreview() {
|
||||
|
||||
SmartPlaylistSearch search = MakeSearch();
|
||||
emit search_page_->completeChanged();
|
||||
// When removing last term, update anyway the search
|
||||
if (!search.is_valid() && !search_page_->terms_.isEmpty()) return;
|
||||
|
||||
// Don't apply limits in the term page
|
||||
search.limit_ = -1;
|
||||
|
||||
search_page_->preview_->Update(search);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistQueryWizardPlugin::UpdateSortPreview() {
|
||||
|
||||
SmartPlaylistSearch search = MakeSearch();
|
||||
if (!search.is_valid()) return;
|
||||
|
||||
sort_ui_->preview->Update(search);
|
||||
|
||||
}
|
||||
|
||||
SmartPlaylistSearch SmartPlaylistQueryWizardPlugin::MakeSearch() const {
|
||||
|
||||
SmartPlaylistSearch ret;
|
||||
|
||||
// Search type
|
||||
ret.search_type_ = SmartPlaylistSearch::SearchType(search_page_->ui_->type->currentIndex());
|
||||
|
||||
// Search terms
|
||||
for (SmartPlaylistSearchTermWidget *widget : search_page_->terms_) {
|
||||
SmartPlaylistSearchTerm term = widget->Term();
|
||||
if (term.is_valid()) ret.terms_ << term;
|
||||
}
|
||||
|
||||
// Sort order
|
||||
if (sort_ui_->random->isChecked()) {
|
||||
ret.sort_type_ = SmartPlaylistSearch::Sort_Random;
|
||||
}
|
||||
else {
|
||||
const bool ascending = sort_ui_->order->currentIndex() == 0;
|
||||
ret.sort_type_ = ascending ? SmartPlaylistSearch::Sort_FieldAsc : SmartPlaylistSearch::Sort_FieldDesc;
|
||||
ret.sort_field_ = SmartPlaylistSearchTerm::Field(sort_ui_->field_value->currentIndex());
|
||||
}
|
||||
|
||||
// Limit
|
||||
if (sort_ui_->limit_none->isChecked())
|
||||
ret.limit_ = -1;
|
||||
else
|
||||
ret.limit_ = sort_ui_->limit_value->value();
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistQueryWizardPlugin::SearchTypeChanged() {
|
||||
|
||||
const bool all = search_page_->ui_->type->currentIndex() == 2;
|
||||
search_page_->ui_->terms_scroll_area_content->setEnabled(!all);
|
||||
|
||||
UpdateTermPreview();
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistQueryWizardPlugin::MoveTermListToBottom(int min, int max) {
|
||||
|
||||
Q_UNUSED(min);
|
||||
// Only scroll to the bottom if a new term is added
|
||||
if (previous_scrollarea_max_ < max)
|
||||
search_page_->ui_->terms_scroll_area->verticalScrollBar()->setValue(max);
|
||||
|
||||
previous_scrollarea_max_ = max;
|
||||
|
||||
}
|
80
src/smartplaylists/smartplaylistquerywizardplugin.h
Normal file
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 SMARTPLAYLISTQUERYWIZARDPLUGIN_H
|
||||
#define SMARTPLAYLISTQUERYWIZARDPLUGIN_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "smartplaylistwizardplugin.h"
|
||||
#include "smartplaylistsearch.h"
|
||||
|
||||
class QWizard;
|
||||
|
||||
class CollectionBackend;
|
||||
class SmartPlaylistSearch;
|
||||
class Ui_SmartPlaylistQuerySortPage;
|
||||
|
||||
class SmartPlaylistQueryWizardPlugin : public SmartPlaylistWizardPlugin {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SmartPlaylistQueryWizardPlugin(Application *app, CollectionBackend *collection, QObject *parent);
|
||||
~SmartPlaylistQueryWizardPlugin();
|
||||
|
||||
QString type() const { return "Query"; }
|
||||
QString name() const;
|
||||
QString description() const;
|
||||
bool is_dynamic() const { return true; }
|
||||
|
||||
int CreatePages(QWizard *wizard, const int finish_page_id);
|
||||
void SetGenerator(PlaylistGeneratorPtr);
|
||||
PlaylistGeneratorPtr CreateGenerator() const;
|
||||
|
||||
private slots:
|
||||
void AddSearchTerm();
|
||||
void RemoveSearchTerm();
|
||||
|
||||
void SearchTypeChanged();
|
||||
|
||||
void UpdateTermPreview();
|
||||
void UpdateSortPreview();
|
||||
void UpdateSortOrder();
|
||||
|
||||
void MoveTermListToBottom(const int min, const int max);
|
||||
|
||||
private:
|
||||
class SearchPage;
|
||||
class SortPage;
|
||||
|
||||
SmartPlaylistSearch MakeSearch() const;
|
||||
|
||||
SearchPage *search_page_;
|
||||
std::unique_ptr<Ui_SmartPlaylistQuerySortPage> sort_ui_;
|
||||
|
||||
int previous_scrollarea_max_;
|
||||
};
|
||||
|
||||
#endif // SMARTPLAYLISTQUERYWIZARDPLUGIN_H
|
148
src/smartplaylists/smartplaylistsearch.cpp
Normal file
@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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 <QString>
|
||||
#include <QStringList>
|
||||
#include <QDataStream>
|
||||
|
||||
#include "search.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/song.h"
|
||||
|
||||
#include "smartplaylistsearch.h"
|
||||
|
||||
SmartPlaylistSearch::SmartPlaylistSearch() { Reset(); }
|
||||
|
||||
SmartPlaylistSearch::SmartPlaylistSearch(const SearchType type, const TermList terms, const SortType sort_type, const SmartPlaylistSearchTerm::Field sort_field, const int limit)
|
||||
: search_type_(type),
|
||||
terms_(terms),
|
||||
sort_type_(sort_type),
|
||||
sort_field_(sort_field),
|
||||
limit_(limit),
|
||||
first_item_(0) {}
|
||||
|
||||
void SmartPlaylistSearch::Reset() {
|
||||
|
||||
search_type_ = Type_And;
|
||||
terms_.clear();
|
||||
sort_type_ = Sort_Random;
|
||||
sort_field_ = SmartPlaylistSearchTerm::Field_Title;
|
||||
limit_ = -1;
|
||||
first_item_ = 0;
|
||||
|
||||
}
|
||||
|
||||
QString SmartPlaylistSearch::ToSql(const QString &songs_table) const {
|
||||
|
||||
QString sql = "SELECT ROWID," + Song::kColumnSpec + " FROM " + songs_table;
|
||||
|
||||
// Add search terms
|
||||
QStringList where_clauses;
|
||||
QStringList term_where_clauses;
|
||||
for (const SmartPlaylistSearchTerm &term : terms_) {
|
||||
term_where_clauses << term.ToSql();
|
||||
}
|
||||
|
||||
if (!terms_.isEmpty() && search_type_ != Type_All) {
|
||||
QString boolean_op = search_type_ == Type_And ? " AND " : " OR ";
|
||||
where_clauses << "(" + term_where_clauses.join(boolean_op) + ")";
|
||||
}
|
||||
|
||||
// Restrict the IDs of songs if we're making a dynamic playlist
|
||||
if (!id_not_in_.isEmpty()) {
|
||||
QString numbers;
|
||||
for (int id : id_not_in_) {
|
||||
numbers += (numbers.isEmpty() ? "" : ",") + QString::number(id);
|
||||
}
|
||||
where_clauses << "(ROWID NOT IN (" + numbers + "))";
|
||||
}
|
||||
|
||||
// We never want to include songs that have been deleted,
|
||||
// but are still kept in the database in case the directory containing them has just been unmounted.
|
||||
where_clauses << "unavailable = 0";
|
||||
|
||||
if (!where_clauses.isEmpty()) {
|
||||
sql += " WHERE " + where_clauses.join(" AND ");
|
||||
}
|
||||
|
||||
// Add sort by
|
||||
if (sort_type_ == Sort_Random) {
|
||||
sql += " ORDER BY random()";
|
||||
}
|
||||
else {
|
||||
sql += " ORDER BY " + SmartPlaylistSearchTerm::FieldColumnName(sort_field_) + (sort_type_ == Sort_FieldAsc ? " ASC" : " DESC");
|
||||
}
|
||||
|
||||
// Add limit
|
||||
if (first_item_) {
|
||||
sql += QString(" LIMIT %1 OFFSET %2").arg(limit_).arg(first_item_);
|
||||
}
|
||||
else if (limit_ != -1) {
|
||||
sql += " LIMIT " + QString::number(limit_);
|
||||
}
|
||||
//qLog(Debug) << sql;
|
||||
|
||||
return sql;
|
||||
|
||||
}
|
||||
|
||||
bool SmartPlaylistSearch::is_valid() const {
|
||||
|
||||
if (search_type_ == Type_All) return true;
|
||||
return !terms_.isEmpty();
|
||||
|
||||
}
|
||||
|
||||
bool SmartPlaylistSearch::operator==(const SmartPlaylistSearch &other) const {
|
||||
|
||||
return search_type_ == other.search_type_ &&
|
||||
terms_ == other.terms_ &&
|
||||
sort_type_ == other.sort_type_ &&
|
||||
sort_field_ == other.sort_field_ &&
|
||||
limit_ == other.limit_;
|
||||
}
|
||||
|
||||
QDataStream &operator<<(QDataStream &s, const SmartPlaylistSearch &search) {
|
||||
|
||||
s << search.terms_;
|
||||
s << quint8(search.sort_type_);
|
||||
s << quint8(search.sort_field_);
|
||||
s << qint32(search.limit_);
|
||||
s << quint8(search.search_type_);
|
||||
return s;
|
||||
|
||||
}
|
||||
|
||||
QDataStream &operator>>(QDataStream &s, SmartPlaylistSearch &search) {
|
||||
|
||||
quint8 sort_type, sort_field, search_type;
|
||||
qint32 limit;
|
||||
|
||||
s >> search.terms_ >> sort_type >> sort_field >> limit >> search_type;
|
||||
search.sort_type_ = SmartPlaylistSearch::SortType(sort_type);
|
||||
search.sort_field_ = SmartPlaylistSearchTerm::Field(sort_field);
|
||||
search.limit_ = limit;
|
||||
search.search_type_ = SmartPlaylistSearch::SearchType(search_type);
|
||||
|
||||
return s;
|
||||
|
||||
}
|
69
src/smartplaylists/smartplaylistsearch.h
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 SMARTPLAYLISTSEARCH_H
|
||||
#define SMARTPLAYLISTSEARCH_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QDataStream>
|
||||
|
||||
#include "playlistgenerator.h"
|
||||
#include "smartplaylistsearchterm.h"
|
||||
|
||||
class SmartPlaylistSearch {
|
||||
|
||||
public:
|
||||
typedef QList<SmartPlaylistSearchTerm> TermList;
|
||||
|
||||
// These values are persisted, so add to the end of the enum only
|
||||
enum SearchType { Type_And = 0, Type_Or, Type_All, };
|
||||
|
||||
// These values are persisted, so add to the end of the enum only
|
||||
enum SortType { Sort_Random = 0, Sort_FieldAsc, Sort_FieldDesc, };
|
||||
|
||||
explicit SmartPlaylistSearch();
|
||||
explicit SmartPlaylistSearch(const SearchType type, const TermList terms, const SortType sort_type, const SmartPlaylistSearchTerm::Field sort_field, const int limit = PlaylistGenerator::kDefaultLimit);
|
||||
|
||||
bool is_valid() const;
|
||||
bool operator==(const SmartPlaylistSearch &other) const;
|
||||
bool operator!=(const SmartPlaylistSearch &other) const { return !(*this == other); }
|
||||
|
||||
SearchType search_type_;
|
||||
TermList terms_;
|
||||
SortType sort_type_;
|
||||
SmartPlaylistSearchTerm::Field sort_field_;
|
||||
int limit_;
|
||||
|
||||
// Not persisted, used to alter the behaviour of the query
|
||||
QList<int> id_not_in_;
|
||||
int first_item_;
|
||||
|
||||
void Reset();
|
||||
QString ToSql(const QString &songs_table) const;
|
||||
|
||||
};
|
||||
|
||||
QDataStream &operator<<(QDataStream &s, const SmartPlaylistSearch &search);
|
||||
QDataStream &operator>>(QDataStream &s, SmartPlaylistSearch &search);
|
||||
|
||||
#endif // SMARTPLAYLISTSEARCH_H
|
151
src/smartplaylists/smartplaylistsearchpreview.cpp
Normal file
@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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 <memory>
|
||||
|
||||
#include <QWidget>
|
||||
#include <QAbstractItemView>
|
||||
#include <QString>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
#include "smartplaylistsearchpreview.h"
|
||||
#include "ui_smartplaylistsearchpreview.h"
|
||||
|
||||
#include "core/closure.h"
|
||||
#include "playlist/playlist.h"
|
||||
#include "playlistquerygenerator.h"
|
||||
|
||||
SmartPlaylistSearchPreview::SmartPlaylistSearchPreview(QWidget* parent)
|
||||
: QWidget(parent),
|
||||
ui_(new Ui_SmartPlaylistSearchPreview),
|
||||
model_(nullptr) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
|
||||
// Prevent editing songs and saving settings (like header columns and geometry)
|
||||
ui_->tree->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
ui_->tree->SetReadOnlySettings(true);
|
||||
|
||||
QFont bold_font;
|
||||
bold_font.setBold(true);
|
||||
ui_->preview_label->setFont(bold_font);
|
||||
ui_->busy_container->hide();
|
||||
|
||||
}
|
||||
|
||||
SmartPlaylistSearchPreview::~SmartPlaylistSearchPreview() {
|
||||
delete ui_;
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchPreview::set_application(Application *app) {
|
||||
|
||||
ui_->tree->Init(app);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchPreview::set_collection(CollectionBackend *backend) {
|
||||
|
||||
backend_ = backend;
|
||||
|
||||
model_ = new Playlist(nullptr, nullptr, backend_, -1, QString(), false, this);
|
||||
ui_->tree->setModel(model_);
|
||||
ui_->tree->SetPlaylist(model_);
|
||||
ui_->tree->SetItemDelegates();
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchPreview::Update(const SmartPlaylistSearch &search) {
|
||||
|
||||
if (search == last_search_) {
|
||||
// This search was the same as the last one we did
|
||||
return;
|
||||
}
|
||||
|
||||
if (generator_ || isHidden()) {
|
||||
// It's busy generating something already, or the widget isn't visible
|
||||
pending_search_ = search;
|
||||
return;
|
||||
}
|
||||
|
||||
RunSearch(search);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchPreview::showEvent(QShowEvent *e) {
|
||||
|
||||
if (pending_search_.is_valid() && !generator_) {
|
||||
// There was a search waiting while we were hidden, so run it now
|
||||
RunSearch(pending_search_);
|
||||
pending_search_ = SmartPlaylistSearch();
|
||||
}
|
||||
|
||||
QWidget::showEvent(e);
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
PlaylistItemList DoRunSearch(PlaylistGeneratorPtr gen) { return gen->Generate(); }
|
||||
} // namespace
|
||||
|
||||
void SmartPlaylistSearchPreview::RunSearch(const SmartPlaylistSearch &search) {
|
||||
|
||||
generator_.reset(new PlaylistQueryGenerator);
|
||||
generator_->set_collection(backend_);
|
||||
std::dynamic_pointer_cast<PlaylistQueryGenerator>(generator_)->Load(search);
|
||||
|
||||
ui_->busy_container->show();
|
||||
ui_->count_label->hide();
|
||||
QFuture<PlaylistItemList> future = QtConcurrent::run(DoRunSearch, generator_);
|
||||
NewClosure(future, this, SLOT(SearchFinished(QFuture<PlaylistItemList>)), future);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchPreview::SearchFinished(QFuture<PlaylistItemList> future) {
|
||||
|
||||
last_search_ = std::dynamic_pointer_cast<PlaylistQueryGenerator>(generator_)->search();
|
||||
generator_.reset();
|
||||
|
||||
if (pending_search_.is_valid() && pending_search_ != last_search_) {
|
||||
// There was another search done while we were running
|
||||
// throw away these results and do that one now instead
|
||||
RunSearch(pending_search_);
|
||||
pending_search_ = SmartPlaylistSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
PlaylistItemList all_items = future.result();
|
||||
PlaylistItemList displayed_items = all_items.mid(0, PlaylistGenerator::kDefaultLimit);
|
||||
|
||||
model_->Clear();
|
||||
model_->InsertItems(displayed_items);
|
||||
|
||||
if (displayed_items.count() < all_items.count()) {
|
||||
ui_->count_label->setText(tr("%1 songs found (showing %2)").arg(all_items.count()).arg(displayed_items.count()));
|
||||
}
|
||||
else {
|
||||
ui_->count_label->setText(tr("%1 songs found").arg(all_items.count()));
|
||||
}
|
||||
|
||||
ui_->busy_container->hide();
|
||||
ui_->count_label->show();
|
||||
|
||||
}
|
74
src/smartplaylists/smartplaylistsearchpreview.h
Normal file
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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 SMARTPLAYLISTSEARCHPREVIEW_H
|
||||
#define SMARTPLAYLISTSEARCHPREVIEW_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QFuture>
|
||||
#include <QList>
|
||||
|
||||
#include "smartplaylistsearch.h"
|
||||
#include "playlistgenerator_fwd.h"
|
||||
|
||||
class QShowEvent;
|
||||
|
||||
class Application;
|
||||
class CollectionBackend;
|
||||
class Playlist;
|
||||
class Ui_SmartPlaylistSearchPreview;
|
||||
|
||||
class SmartPlaylistSearchPreview : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SmartPlaylistSearchPreview(QWidget *parent = nullptr);
|
||||
~SmartPlaylistSearchPreview();
|
||||
|
||||
void set_application(Application *app);
|
||||
void set_collection(CollectionBackend *backend);
|
||||
|
||||
void Update(const SmartPlaylistSearch &search);
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent*);
|
||||
|
||||
private:
|
||||
void RunSearch(const SmartPlaylistSearch &search);
|
||||
|
||||
private slots:
|
||||
void SearchFinished(QFuture<PlaylistItemList> future);
|
||||
|
||||
private:
|
||||
Ui_SmartPlaylistSearchPreview *ui_;
|
||||
QList<SmartPlaylistSearchTerm::Field> fields_;
|
||||
|
||||
CollectionBackend *backend_;
|
||||
Playlist *model_;
|
||||
|
||||
SmartPlaylistSearch pending_search_;
|
||||
SmartPlaylistSearch last_search_;
|
||||
PlaylistGeneratorPtr generator_;
|
||||
|
||||
};
|
||||
|
||||
#endif // SMARTPLAYLISTSEARCHPREVIEW_H
|
99
src/smartplaylists/smartplaylistsearchpreview.ui
Normal file
@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SmartPlaylistSearchPreview</class>
|
||||
<widget class="QWidget" name="SmartPlaylistSearchPreview">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>651</width>
|
||||
<height>377</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="preview_label">
|
||||
<property name="text">
|
||||
<string>Preview</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>
|
||||
<item>
|
||||
<widget class="QLabel" name="count_label"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="busy_container" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="BusyIndicator" name="widget" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Loading...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="PlaylistView" name="tree">
|
||||
<property name="verticalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOn</enum>
|
||||
</property>
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="itemsExpandable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="allColumnsShowFocus">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>BusyIndicator</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/busyindicator.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>PlaylistView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>playlist/playlistview.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
466
src/smartplaylists/smartplaylistsearchterm.cpp
Normal file
@ -0,0 +1,466 @@
|
||||
/*
|
||||
* 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 <QObject>
|
||||
#include <QDataStream>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include "smartplaylistsearchterm.h"
|
||||
#include "playlist/playlist.h"
|
||||
|
||||
SmartPlaylistSearchTerm::SmartPlaylistSearchTerm() : field_(Field_Title), operator_(Op_Equals) {}
|
||||
|
||||
SmartPlaylistSearchTerm::SmartPlaylistSearchTerm(Field field, Operator op, const QVariant &value)
|
||||
: field_(field), operator_(op), value_(value) {}
|
||||
|
||||
QString SmartPlaylistSearchTerm::ToSql() const {
|
||||
|
||||
QString col = FieldColumnName(field_);
|
||||
QString date = DateName(date_, true);
|
||||
QString value = value_.toString();
|
||||
value.replace('\'', "''");
|
||||
|
||||
QString second_value;
|
||||
|
||||
bool special_date_query = (operator_ == SmartPlaylistSearchTerm::Op_NumericDate ||
|
||||
operator_ == SmartPlaylistSearchTerm::Op_NumericDateNot ||
|
||||
operator_ == SmartPlaylistSearchTerm::Op_RelativeDate);
|
||||
|
||||
// Floating point problems...
|
||||
// Theoretically 0.0 == 0 stars, 0.1 == 0.5 star, 0.2 == 1 star etc.
|
||||
// but in reality we need to consider anything from [0.05, 0.15) range to be 0.5 star etc.
|
||||
// To make this simple, I transform the ranges to integeres and then operate on ints: [0.0, 0.05) -> 0, [0.05, 0.15) -> 1 etc.
|
||||
if (TypeOf(field_) == Type_Date) {
|
||||
if (!special_date_query) {
|
||||
// We have the exact date
|
||||
// The calendar widget specifies no time so ditch the possible time part
|
||||
// from integers representing the dates.
|
||||
col = "DATE(" + col + ", 'unixepoch', 'localtime')";
|
||||
value = "DATE(" + value + ", 'unixepoch', 'localtime')";
|
||||
}
|
||||
else {
|
||||
// We have a numeric date, consider also the time for more precision
|
||||
col = "DATETIME(" + col + ", 'unixepoch', 'localtime')";
|
||||
second_value = second_value_.toString();
|
||||
second_value.replace('\'', "''");
|
||||
if (date == "weeks") {
|
||||
// Sqlite doesn't know weeks, transform them to days
|
||||
date = "days";
|
||||
value = QString::number(value_.toInt() * 7);
|
||||
second_value = QString::number(second_value_.toInt() * 7);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (TypeOf(field_) == Type_Time) {
|
||||
// Convert seconds to nanoseconds
|
||||
value = "CAST (" + value + " *1000000000 AS INTEGER)";
|
||||
}
|
||||
|
||||
// File paths need some extra processing since they are stored as encoded urls in the database.
|
||||
if (field_ == Field_Filepath) {
|
||||
if (operator_ == Op_StartsWith || operator_ == Op_Equals) {
|
||||
value = QUrl::fromLocalFile(value).toEncoded();
|
||||
}
|
||||
else {
|
||||
value = QUrl(value).toEncoded();
|
||||
}
|
||||
}
|
||||
else if (TypeOf(field_) == Type_Rating) {
|
||||
col = "CAST ((" + col + " + 0.05) * 10 AS INTEGER)";
|
||||
value = "CAST ((" + value + " + 0.05) * 10 AS INTEGER)";
|
||||
}
|
||||
|
||||
switch (operator_) {
|
||||
case Op_Contains:
|
||||
return col + " LIKE '%" + value + "%'";
|
||||
case Op_NotContains:
|
||||
return col + " NOT LIKE '%" + value + "%'";
|
||||
case Op_StartsWith:
|
||||
return col + " LIKE '" + value + "%'";
|
||||
case Op_EndsWith:
|
||||
return col + " LIKE '%" + value + "'";
|
||||
case Op_Equals:
|
||||
if (TypeOf(field_) == Type_Text)
|
||||
return col + " LIKE '" + value + "'";
|
||||
else if (TypeOf(field_) == Type_Date || TypeOf(field_) == Type_Time || TypeOf(field_) == Type_Rating)
|
||||
return col + " = " + value;
|
||||
else
|
||||
return col + " = '" + value + "'";
|
||||
case Op_GreaterThan:
|
||||
if (TypeOf(field_) == Type_Date || TypeOf(field_) == Type_Time || TypeOf(field_) == Type_Rating)
|
||||
return col + " > " + value;
|
||||
else
|
||||
return col + " > '" + value + "'";
|
||||
case Op_LessThan:
|
||||
if (TypeOf(field_) == Type_Date || TypeOf(field_) == Type_Time || TypeOf(field_) == Type_Rating)
|
||||
return col + " < " + value;
|
||||
else
|
||||
return col + " < '" + value + "'";
|
||||
case Op_NumericDate:
|
||||
return col + " > " + "DATETIME('now', '-" + value + " " + date + "', 'localtime')";
|
||||
case Op_NumericDateNot:
|
||||
return col + " < " + "DATETIME('now', '-" + value + " " + date + "', 'localtime')";
|
||||
case Op_RelativeDate:
|
||||
// Consider the time range before the first date but after the second one
|
||||
return "(" + col + " < " + "DATETIME('now', '-" + value + " " + date + "', 'localtime') AND " + col + " > " + "DATETIME('now', '-" + second_value + " " + date + "', 'localtime'))";
|
||||
case Op_NotEquals:
|
||||
if (TypeOf(field_) == Type_Text) {
|
||||
return col + " <> '" + value + "'";
|
||||
}
|
||||
else {
|
||||
return col + " <> " + value;
|
||||
}
|
||||
case Op_Empty:
|
||||
return col + " = ''";
|
||||
case Op_NotEmpty:
|
||||
return col + " <> ''";
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
bool SmartPlaylistSearchTerm::is_valid() const {
|
||||
|
||||
// We can accept also a zero value in these cases
|
||||
if (operator_ == SmartPlaylistSearchTerm::Op_NumericDate) {
|
||||
return value_.toInt() >= 0;
|
||||
}
|
||||
else if (operator_ == SmartPlaylistSearchTerm::Op_RelativeDate) {
|
||||
return (value_.toInt() >= 0 && value_.toInt() < second_value_.toInt());
|
||||
}
|
||||
|
||||
switch (TypeOf(field_)) {
|
||||
case Type_Text:
|
||||
if (operator_ == SmartPlaylistSearchTerm::Op_Empty || operator_ == SmartPlaylistSearchTerm::Op_NotEmpty) {
|
||||
return true;
|
||||
}
|
||||
// Empty fields should be possible.
|
||||
// All values for Type_Text should be valid.
|
||||
return !value_.toString().isEmpty();
|
||||
case Type_Date:
|
||||
return value_.toInt() != 0;
|
||||
case Type_Number:
|
||||
return value_.toInt() >= 0;
|
||||
case Type_Time:
|
||||
return true;
|
||||
case Type_Rating:
|
||||
return value_.toFloat() >= 0.0;
|
||||
case Type_Invalid:
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool SmartPlaylistSearchTerm::operator==(const SmartPlaylistSearchTerm &other) const {
|
||||
return field_ == other.field_ && operator_ == other.operator_ &&
|
||||
value_ == other.value_ && date_ == other.date_ &&
|
||||
second_value_ == other.second_value_;
|
||||
}
|
||||
|
||||
SmartPlaylistSearchTerm::Type SmartPlaylistSearchTerm::TypeOf(const Field field) {
|
||||
|
||||
switch (field) {
|
||||
case Field_Length:
|
||||
return Type_Time;
|
||||
|
||||
case Field_Track:
|
||||
case Field_Disc:
|
||||
case Field_Year:
|
||||
case Field_OriginalYear:
|
||||
case Field_Filesize:
|
||||
case Field_PlayCount:
|
||||
case Field_SkipCount:
|
||||
case Field_Samplerate:
|
||||
case Field_Bitdepth:
|
||||
case Field_Bitrate:
|
||||
return Type_Number;
|
||||
|
||||
case Field_LastPlayed:
|
||||
case Field_DateCreated:
|
||||
case Field_DateModified:
|
||||
return Type_Date;
|
||||
|
||||
case Field_Rating:
|
||||
return Type_Rating;
|
||||
|
||||
default:
|
||||
return Type_Text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
OperatorList SmartPlaylistSearchTerm::OperatorsForType(const Type type) {
|
||||
|
||||
switch (type) {
|
||||
case Type_Text:
|
||||
return OperatorList() << Op_Contains << Op_NotContains << Op_Equals
|
||||
<< Op_NotEquals << Op_Empty << Op_NotEmpty
|
||||
<< Op_StartsWith << Op_EndsWith;
|
||||
case Type_Date:
|
||||
return OperatorList() << Op_Equals << Op_NotEquals << Op_GreaterThan
|
||||
<< Op_LessThan << Op_NumericDate
|
||||
<< Op_NumericDateNot << Op_RelativeDate;
|
||||
default:
|
||||
return OperatorList() << Op_Equals << Op_NotEquals << Op_GreaterThan
|
||||
<< Op_LessThan;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QString SmartPlaylistSearchTerm::OperatorText(const Type type, const Operator op) {
|
||||
|
||||
if (type == Type_Date) {
|
||||
switch (op) {
|
||||
case Op_GreaterThan:
|
||||
return QObject::tr("after");
|
||||
case Op_LessThan:
|
||||
return QObject::tr("before");
|
||||
case Op_Equals:
|
||||
return QObject::tr("on");
|
||||
case Op_NotEquals:
|
||||
return QObject::tr("not on");
|
||||
case Op_NumericDate:
|
||||
return QObject::tr("in the last");
|
||||
case Op_NumericDateNot:
|
||||
return QObject::tr("not in the last");
|
||||
case Op_RelativeDate:
|
||||
return QObject::tr("between");
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
switch (op) {
|
||||
case Op_Contains:
|
||||
return QObject::tr("contains");
|
||||
case Op_NotContains:
|
||||
return QObject::tr("does not contain");
|
||||
case Op_StartsWith:
|
||||
return QObject::tr("starts with");
|
||||
case Op_EndsWith:
|
||||
return QObject::tr("ends with");
|
||||
case Op_GreaterThan:
|
||||
return QObject::tr("greater than");
|
||||
case Op_LessThan:
|
||||
return QObject::tr("less than");
|
||||
case Op_Equals:
|
||||
return QObject::tr("equals");
|
||||
case Op_NotEquals:
|
||||
return QObject::tr("not equals");
|
||||
case Op_Empty:
|
||||
return QObject::tr("empty");
|
||||
case Op_NotEmpty:
|
||||
return QObject::tr("not empty");
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
|
||||
return QString();
|
||||
|
||||
}
|
||||
|
||||
QString SmartPlaylistSearchTerm::FieldColumnName(const Field field) {
|
||||
|
||||
switch (field) {
|
||||
case Field_AlbumArtist:
|
||||
return "albumartist";
|
||||
case Field_Artist:
|
||||
return "artist";
|
||||
case Field_Album:
|
||||
return "album";
|
||||
case Field_Title:
|
||||
return "title";
|
||||
case Field_Track:
|
||||
return "track";
|
||||
case Field_Disc:
|
||||
return "disc";
|
||||
case Field_Year:
|
||||
return "year";
|
||||
case Field_OriginalYear:
|
||||
return "originalyear";
|
||||
case Field_Genre:
|
||||
return "genre";
|
||||
case Field_Composer:
|
||||
return "composer";
|
||||
case Field_Performer:
|
||||
return "performer";
|
||||
case Field_Grouping:
|
||||
return "grouping";
|
||||
case Field_Comment:
|
||||
return "comment";
|
||||
case Field_Length:
|
||||
return "length";
|
||||
case Field_Filepath:
|
||||
return "filename";
|
||||
case Field_Filetype:
|
||||
return "filetype";
|
||||
case Field_Filesize:
|
||||
return "filesize";
|
||||
case Field_DateCreated:
|
||||
return "ctime";
|
||||
case Field_DateModified:
|
||||
return "mtime";
|
||||
case Field_PlayCount:
|
||||
return "playcount";
|
||||
case Field_SkipCount:
|
||||
return "skipcount";
|
||||
case Field_LastPlayed:
|
||||
return "lastplayed";
|
||||
case Field_Rating:
|
||||
return "rating";
|
||||
case Field_Samplerate:
|
||||
return "samplerate";
|
||||
case Field_Bitdepth:
|
||||
return "bitdepth";
|
||||
case Field_Bitrate:
|
||||
return "bitrate";
|
||||
case FieldCount:
|
||||
Q_ASSERT(0);
|
||||
}
|
||||
return QString();
|
||||
|
||||
}
|
||||
|
||||
QString SmartPlaylistSearchTerm::FieldName(const Field field) {
|
||||
|
||||
switch (field) {
|
||||
case Field_AlbumArtist:
|
||||
return Playlist::column_name(Playlist::Column_AlbumArtist);
|
||||
case Field_Artist:
|
||||
return Playlist::column_name(Playlist::Column_Artist);
|
||||
case Field_Album:
|
||||
return Playlist::column_name(Playlist::Column_Album);
|
||||
case Field_Title:
|
||||
return Playlist::column_name(Playlist::Column_Title);
|
||||
case Field_Track:
|
||||
return Playlist::column_name(Playlist::Column_Track);
|
||||
case Field_Disc:
|
||||
return Playlist::column_name(Playlist::Column_Disc);
|
||||
case Field_Year:
|
||||
return Playlist::column_name(Playlist::Column_Year);
|
||||
case Field_OriginalYear:
|
||||
return Playlist::column_name(Playlist::Column_OriginalYear);
|
||||
case Field_Genre:
|
||||
return Playlist::column_name(Playlist::Column_Genre);
|
||||
case Field_Composer:
|
||||
return Playlist::column_name(Playlist::Column_Composer);
|
||||
case Field_Performer:
|
||||
return Playlist::column_name(Playlist::Column_Performer);
|
||||
case Field_Grouping:
|
||||
return Playlist::column_name(Playlist::Column_Grouping);
|
||||
case Field_Comment:
|
||||
return QObject::tr("Comment");
|
||||
case Field_Length:
|
||||
return Playlist::column_name(Playlist::Column_Length);
|
||||
case Field_Filepath:
|
||||
return Playlist::column_name(Playlist::Column_Filename);
|
||||
case Field_Filetype:
|
||||
return Playlist::column_name(Playlist::Column_Filetype);
|
||||
case Field_Filesize:
|
||||
return Playlist::column_name(Playlist::Column_Filesize);
|
||||
case Field_DateCreated:
|
||||
return Playlist::column_name(Playlist::Column_DateCreated);
|
||||
case Field_DateModified:
|
||||
return Playlist::column_name(Playlist::Column_DateModified);
|
||||
case Field_PlayCount:
|
||||
return Playlist::column_name(Playlist::Column_PlayCount);
|
||||
case Field_SkipCount:
|
||||
return Playlist::column_name(Playlist::Column_SkipCount);
|
||||
case Field_LastPlayed:
|
||||
return Playlist::column_name(Playlist::Column_LastPlayed);
|
||||
case Field_Rating:
|
||||
return Playlist::column_name(Playlist::Column_Rating);
|
||||
case Field_Samplerate:
|
||||
return Playlist::column_name(Playlist::Column_Samplerate);
|
||||
case Field_Bitdepth:
|
||||
return Playlist::column_name(Playlist::Column_Bitdepth);
|
||||
case Field_Bitrate:
|
||||
return Playlist::column_name(Playlist::Column_Bitrate);
|
||||
case FieldCount:
|
||||
Q_ASSERT(0);
|
||||
}
|
||||
return QString();
|
||||
|
||||
}
|
||||
|
||||
QString SmartPlaylistSearchTerm::FieldSortOrderText(const Type type, const bool ascending) {
|
||||
|
||||
switch (type) {
|
||||
case Type_Text:
|
||||
return ascending ? QObject::tr("A-Z") : QObject::tr("Z-A");
|
||||
case Type_Date:
|
||||
return ascending ? QObject::tr("oldest first") : QObject::tr("newest first");
|
||||
case Type_Time:
|
||||
return ascending ? QObject::tr("shortest first") : QObject::tr("longest first");
|
||||
case Type_Number:
|
||||
case Type_Rating:
|
||||
return ascending ? QObject::tr("smallest first") : QObject::tr("biggest first");
|
||||
case Type_Invalid:
|
||||
return QString();
|
||||
}
|
||||
return QString();
|
||||
|
||||
}
|
||||
|
||||
QString SmartPlaylistSearchTerm::DateName(const DateType date, const bool forQuery) {
|
||||
|
||||
// If forQuery is true, untranslated keywords are returned
|
||||
switch (date) {
|
||||
case Date_Hour:
|
||||
return (forQuery ? "hours" : QObject::tr("Hours"));
|
||||
case Date_Day:
|
||||
return (forQuery ? "days" : QObject::tr("Days"));
|
||||
case Date_Week:
|
||||
return (forQuery ? "weeks" : QObject::tr("Weeks"));
|
||||
case Date_Month:
|
||||
return (forQuery ? "months" : QObject::tr("Months"));
|
||||
case Date_Year:
|
||||
return (forQuery ? "years" : QObject::tr("Years"));
|
||||
}
|
||||
return QString();
|
||||
|
||||
}
|
||||
|
||||
QDataStream &operator<<(QDataStream &s, const SmartPlaylistSearchTerm &term) {
|
||||
|
||||
s << quint8(term.field_);
|
||||
s << quint8(term.operator_);
|
||||
s << term.value_;
|
||||
s << term.second_value_;
|
||||
s << quint8(term.date_);
|
||||
return s;
|
||||
|
||||
}
|
||||
|
||||
QDataStream &operator>>(QDataStream &s, SmartPlaylistSearchTerm &term) {
|
||||
|
||||
quint8 field, op, date;
|
||||
s >> field >> op >> term.value_ >> term.second_value_ >> date;
|
||||
term.field_ = SmartPlaylistSearchTerm::Field(field);
|
||||
term.operator_ = SmartPlaylistSearchTerm::Operator(op);
|
||||
term.date_ = SmartPlaylistSearchTerm::DateType(date);
|
||||
return s;
|
||||
|
||||
}
|
141
src/smartplaylists/smartplaylistsearchterm.h
Normal file
@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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 SMARTPLAYLISTSEARCHTERM_H
|
||||
#define SMARTPLAYLISTSEARCHTERM_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QDataStream>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
|
||||
class SmartPlaylistSearchTerm {
|
||||
public:
|
||||
// These values are persisted, so add to the end of the enum only
|
||||
enum Field {
|
||||
Field_AlbumArtist = 0,
|
||||
Field_Artist,
|
||||
Field_Album,
|
||||
Field_Title,
|
||||
Field_Track,
|
||||
Field_Disc,
|
||||
Field_Year,
|
||||
Field_OriginalYear,
|
||||
Field_Genre,
|
||||
Field_Composer,
|
||||
Field_Performer,
|
||||
Field_Grouping,
|
||||
Field_Comment,
|
||||
Field_Length,
|
||||
Field_Filepath,
|
||||
Field_Filetype,
|
||||
Field_Filesize,
|
||||
Field_DateCreated,
|
||||
Field_DateModified,
|
||||
Field_PlayCount,
|
||||
Field_SkipCount,
|
||||
Field_LastPlayed,
|
||||
Field_Rating,
|
||||
Field_Samplerate,
|
||||
Field_Bitdepth,
|
||||
Field_Bitrate,
|
||||
FieldCount
|
||||
};
|
||||
|
||||
// These values are persisted, so add to the end of the enum only
|
||||
enum Operator {
|
||||
// For text
|
||||
Op_Contains = 0,
|
||||
Op_NotContains = 1,
|
||||
Op_StartsWith = 2,
|
||||
Op_EndsWith = 3,
|
||||
|
||||
// For numbers
|
||||
Op_GreaterThan = 4,
|
||||
Op_LessThan = 5,
|
||||
|
||||
// For everything
|
||||
Op_Equals = 6,
|
||||
Op_NotEquals = 9,
|
||||
|
||||
// For numeric dates (e.g. in the last X days)
|
||||
Op_NumericDate = 7,
|
||||
// For relative dates
|
||||
Op_RelativeDate = 8,
|
||||
|
||||
// For numeric dates (e.g. not in the last X days)
|
||||
Op_NumericDateNot = 10,
|
||||
|
||||
Op_Empty = 11,
|
||||
Op_NotEmpty = 12,
|
||||
|
||||
// Next value = 13
|
||||
};
|
||||
|
||||
enum Type {
|
||||
Type_Text,
|
||||
Type_Date,
|
||||
Type_Time,
|
||||
Type_Number,
|
||||
Type_Rating,
|
||||
Type_Invalid
|
||||
};
|
||||
|
||||
// These values are persisted, so add to the end of the enum only
|
||||
enum DateType {
|
||||
Date_Hour = 0,
|
||||
Date_Day,
|
||||
Date_Week,
|
||||
Date_Month, Date_Year
|
||||
};
|
||||
|
||||
explicit SmartPlaylistSearchTerm();
|
||||
explicit SmartPlaylistSearchTerm(const Field field, const Operator op, const QVariant &value);
|
||||
|
||||
Field field_;
|
||||
Operator operator_;
|
||||
QVariant value_;
|
||||
DateType date_;
|
||||
// For relative dates, we need a second parameter, might be useful somewhere else
|
||||
QVariant second_value_;
|
||||
|
||||
QString ToSql() const;
|
||||
bool is_valid() const;
|
||||
bool operator==(const SmartPlaylistSearchTerm &other) const;
|
||||
bool operator!=(const SmartPlaylistSearchTerm &other) const { return !(*this == other); }
|
||||
|
||||
static Type TypeOf(const Field field);
|
||||
static QList<Operator> OperatorsForType(const Type type);
|
||||
static QString OperatorText(const Type type, const Operator op);
|
||||
static QString FieldName(const Field field);
|
||||
static QString FieldColumnName(const Field field);
|
||||
static QString FieldSortOrderText(const Type type, const bool ascending);
|
||||
static QString DateName(const DateType date, const bool forQuery);
|
||||
|
||||
};
|
||||
|
||||
typedef QList<SmartPlaylistSearchTerm::Operator> OperatorList;
|
||||
|
||||
QDataStream &operator<<(QDataStream &s, const SmartPlaylistSearchTerm &term);
|
||||
QDataStream &operator>>(QDataStream &s, SmartPlaylistSearchTerm &term);
|
||||
|
||||
#endif // SMARTPLAYLISTSEARCHTERM_H
|
511
src/smartplaylists/smartplaylistsearchtermwidget.cpp
Normal file
@ -0,0 +1,511 @@
|
||||
/*
|
||||
* 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 <QWidget>
|
||||
#include <QTimer>
|
||||
#include <QIODevice>
|
||||
#include <QFile>
|
||||
#include <QMessageBox>
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QDateTime>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QtDebug>
|
||||
#include <QKeyEvent>
|
||||
#include <QEnterEvent>
|
||||
|
||||
#include "core/utilities.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "playlist/playlist.h"
|
||||
#include "playlist/playlistdelegates.h"
|
||||
#include "smartplaylistsearchterm.h"
|
||||
#include "smartplaylistsearchtermwidget.h"
|
||||
#include "ui_smartplaylistsearchtermwidget.h"
|
||||
|
||||
// Exported by QtGui
|
||||
void qt_blurImage(QPainter *p, QImage &blurImage, qreal radius, bool quality, bool alphaOnly, int transposed = 0);
|
||||
|
||||
class SmartPlaylistSearchTermWidget::Overlay : public QWidget {
|
||||
public:
|
||||
explicit Overlay(SmartPlaylistSearchTermWidget *parent);
|
||||
void Grab();
|
||||
void SetOpacity(const float opacity);
|
||||
float opacity() const { return opacity_; }
|
||||
|
||||
static const int kSpacing;
|
||||
static const int kIconSize;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
void mouseReleaseEvent(QMouseEvent*) override;
|
||||
void keyReleaseEvent(QKeyEvent *e) override;
|
||||
|
||||
private:
|
||||
SmartPlaylistSearchTermWidget *parent_;
|
||||
|
||||
float opacity_;
|
||||
QString text_;
|
||||
QPixmap pixmap_;
|
||||
QPixmap icon_;
|
||||
|
||||
};
|
||||
|
||||
const int SmartPlaylistSearchTermWidget::Overlay::kSpacing = 6;
|
||||
const int SmartPlaylistSearchTermWidget::Overlay::kIconSize = 22;
|
||||
|
||||
SmartPlaylistSearchTermWidget::SmartPlaylistSearchTermWidget(CollectionBackend* collection, QWidget* parent)
|
||||
: QWidget(parent),
|
||||
ui_(new Ui_SmartPlaylistSearchTermWidget),
|
||||
collection_(collection),
|
||||
overlay_(nullptr),
|
||||
animation_(new QPropertyAnimation(this, "overlay_opacity", this)),
|
||||
active_(true),
|
||||
initialized_(false),
|
||||
current_field_type_(SmartPlaylistSearchTerm::Type_Invalid) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
connect(ui_->field, SIGNAL(currentIndexChanged(int)), SLOT(FieldChanged(int)));
|
||||
connect(ui_->op, SIGNAL(currentIndexChanged(int)), SLOT(OpChanged(int)));
|
||||
connect(ui_->remove, SIGNAL(clicked()), SIGNAL(RemoveClicked()));
|
||||
|
||||
connect(ui_->value_date, SIGNAL(dateChanged(QDate)), SIGNAL(Changed()));
|
||||
connect(ui_->value_number, SIGNAL(valueChanged(int)), SIGNAL(Changed()));
|
||||
connect(ui_->value_text, SIGNAL(textChanged(QString)), SIGNAL(Changed()));
|
||||
connect(ui_->value_time, SIGNAL(timeChanged(QTime)), SIGNAL(Changed()));
|
||||
connect(ui_->value_date_numeric, SIGNAL(valueChanged(int)), SIGNAL(Changed()));
|
||||
connect(ui_->value_date_numeric1, SIGNAL(valueChanged(int)), SLOT(RelativeValueChanged()));
|
||||
connect(ui_->value_date_numeric2, SIGNAL(valueChanged(int)), SLOT(RelativeValueChanged()));
|
||||
connect(ui_->date_type, SIGNAL(currentIndexChanged(int)), SIGNAL(Changed()));
|
||||
connect(ui_->date_type_relative, SIGNAL(currentIndexChanged(int)), SIGNAL(Changed()));
|
||||
connect(ui_->value_rating, SIGNAL(RatingChanged(float)), SIGNAL(Changed()));
|
||||
|
||||
ui_->value_date->setDate(QDate::currentDate());
|
||||
|
||||
// Populate the combo boxes
|
||||
for (int i = 0; i < SmartPlaylistSearchTerm::FieldCount; ++i) {
|
||||
ui_->field->addItem(SmartPlaylistSearchTerm::FieldName(SmartPlaylistSearchTerm::Field(i)));
|
||||
ui_->field->setItemData(i, i);
|
||||
}
|
||||
ui_->field->model()->sort(0);
|
||||
|
||||
// Populate the date type combo box
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
ui_->date_type->addItem(SmartPlaylistSearchTerm::DateName(SmartPlaylistSearchTerm::DateType(i), false));
|
||||
ui_->date_type->setItemData(i, i);
|
||||
|
||||
ui_->date_type_relative->addItem(SmartPlaylistSearchTerm::DateName(SmartPlaylistSearchTerm::DateType(i), false));
|
||||
ui_->date_type_relative->setItemData(i, i);
|
||||
}
|
||||
|
||||
// Icons on the buttons
|
||||
ui_->remove->setIcon(IconLoader::Load("list-remove"));
|
||||
|
||||
// Set stylesheet
|
||||
QFile stylesheet_file(":/style/smartplaylistsearchterm.css");
|
||||
stylesheet_file.open(QIODevice::ReadOnly);
|
||||
QString stylesheet = QString::fromLatin1(stylesheet_file.readAll());
|
||||
const QColor base(222, 97, 97, 128);
|
||||
stylesheet.replace("%light2", Utilities::ColorToRgba(base.lighter(140)));
|
||||
stylesheet.replace("%light", Utilities::ColorToRgba(base.lighter(120)));
|
||||
stylesheet.replace("%dark", Utilities::ColorToRgba(base.darker(120)));
|
||||
stylesheet.replace("%base", Utilities::ColorToRgba(base));
|
||||
setStyleSheet(stylesheet);
|
||||
|
||||
}
|
||||
|
||||
SmartPlaylistSearchTermWidget::~SmartPlaylistSearchTermWidget() { delete ui_; }
|
||||
|
||||
void SmartPlaylistSearchTermWidget::FieldChanged(int index) {
|
||||
|
||||
SmartPlaylistSearchTerm::Field field = SmartPlaylistSearchTerm::Field(ui_->field->itemData(index).toInt());
|
||||
SmartPlaylistSearchTerm::Type type = SmartPlaylistSearchTerm::TypeOf(field);
|
||||
|
||||
// Populate the operator combo box
|
||||
if (type != current_field_type_) {
|
||||
ui_->op->clear();
|
||||
for (SmartPlaylistSearchTerm::Operator op : SmartPlaylistSearchTerm::OperatorsForType(type)) {
|
||||
const int i = ui_->op->count();
|
||||
ui_->op->addItem(SmartPlaylistSearchTerm::OperatorText(type, op));
|
||||
ui_->op->setItemData(i, op);
|
||||
}
|
||||
current_field_type_ = type;
|
||||
}
|
||||
|
||||
// Show the correct value editor
|
||||
QWidget* page = nullptr;
|
||||
SmartPlaylistSearchTerm::Operator op = static_cast<SmartPlaylistSearchTerm::Operator>(
|
||||
ui_->op->itemData(ui_->op->currentIndex()).toInt()
|
||||
);
|
||||
switch (type) {
|
||||
case SmartPlaylistSearchTerm::Type_Time:
|
||||
page = ui_->page_time;
|
||||
break;
|
||||
case SmartPlaylistSearchTerm::Type_Number:
|
||||
page = ui_->page_number;
|
||||
break;
|
||||
case SmartPlaylistSearchTerm::Type_Date:
|
||||
page = ui_->page_date;
|
||||
break;
|
||||
case SmartPlaylistSearchTerm::Type_Text:
|
||||
if (op == SmartPlaylistSearchTerm::Op_Empty || op == SmartPlaylistSearchTerm::Op_NotEmpty) {
|
||||
page = ui_->page_empty;
|
||||
}
|
||||
else {
|
||||
page = ui_->page_text;
|
||||
}
|
||||
break;
|
||||
case SmartPlaylistSearchTerm::Type_Rating:
|
||||
page = ui_->page_rating;
|
||||
break;
|
||||
case SmartPlaylistSearchTerm::Type_Invalid:
|
||||
page = nullptr;
|
||||
break;
|
||||
}
|
||||
ui_->value_stack->setCurrentWidget(page);
|
||||
|
||||
// Maybe set a tag completer
|
||||
switch (field) {
|
||||
case SmartPlaylistSearchTerm::Field_Artist:
|
||||
new TagCompleter(collection_, Playlist::Column_Artist, ui_->value_text);
|
||||
break;
|
||||
|
||||
case SmartPlaylistSearchTerm::Field_Album:
|
||||
new TagCompleter(collection_, Playlist::Column_Album, ui_->value_text);
|
||||
break;
|
||||
|
||||
default:
|
||||
ui_->value_text->setCompleter(nullptr);
|
||||
}
|
||||
|
||||
emit Changed();
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchTermWidget::OpChanged(int idx) {
|
||||
|
||||
Q_UNUSED(idx);
|
||||
|
||||
// Determine the currently selected operator
|
||||
SmartPlaylistSearchTerm::Operator op = static_cast<SmartPlaylistSearchTerm::Operator>(
|
||||
// This uses the operators’s index in the combobox to get its enum value
|
||||
ui_->op->itemData(ui_->op->currentIndex()).toInt()
|
||||
);
|
||||
|
||||
// We need to change the page only in the following case
|
||||
if ((ui_->value_stack->currentWidget() == ui_->page_text) || (ui_->value_stack->currentWidget() == ui_->page_empty)) {
|
||||
QWidget* page = nullptr;
|
||||
if (op == SmartPlaylistSearchTerm::Op_Empty || op == SmartPlaylistSearchTerm::Op_NotEmpty) {
|
||||
page = ui_->page_empty;
|
||||
}
|
||||
else {
|
||||
page = ui_->page_text;
|
||||
}
|
||||
ui_->value_stack->setCurrentWidget(page);
|
||||
}
|
||||
else if (
|
||||
(ui_->value_stack->currentWidget() == ui_->page_date) ||
|
||||
(ui_->value_stack->currentWidget() == ui_->page_date_numeric) ||
|
||||
(ui_->value_stack->currentWidget() == ui_->page_date_relative)
|
||||
) {
|
||||
QWidget* page = nullptr;
|
||||
if (op == SmartPlaylistSearchTerm::Op_NumericDate || op == SmartPlaylistSearchTerm::Op_NumericDateNot) {
|
||||
page = ui_->page_date_numeric;
|
||||
}
|
||||
else if (op == SmartPlaylistSearchTerm::Op_RelativeDate) {
|
||||
page = ui_->page_date_relative;
|
||||
}
|
||||
else {
|
||||
page = ui_->page_date;
|
||||
}
|
||||
ui_->value_stack->setCurrentWidget(page);
|
||||
}
|
||||
|
||||
emit Changed();
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchTermWidget::SetActive(bool active) {
|
||||
|
||||
active_ = active;
|
||||
|
||||
if (overlay_) {
|
||||
delete overlay_;
|
||||
overlay_ = nullptr;
|
||||
}
|
||||
|
||||
ui_->container->setEnabled(active);
|
||||
|
||||
if (!active) {
|
||||
overlay_ = new Overlay(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
void SmartPlaylistSearchTermWidget::enterEvent(QEnterEvent*) {
|
||||
#else
|
||||
void SmartPlaylistSearchTermWidget::enterEvent(QEvent*) {
|
||||
#endif
|
||||
|
||||
if (!overlay_ || !isEnabled()) return;
|
||||
|
||||
animation_->stop();
|
||||
animation_->setEndValue(1.0);
|
||||
animation_->setDuration(80);
|
||||
animation_->start();
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchTermWidget::leaveEvent(QEvent*) {
|
||||
|
||||
if (!overlay_) return;
|
||||
|
||||
animation_->stop();
|
||||
animation_->setEndValue(0.0);
|
||||
animation_->setDuration(160);
|
||||
animation_->start();
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchTermWidget::resizeEvent(QResizeEvent* e) {
|
||||
|
||||
QWidget::resizeEvent(e);
|
||||
if (overlay_ && overlay_->isVisible()) {
|
||||
QTimer::singleShot(0, this, SLOT(Grab()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchTermWidget::showEvent(QShowEvent* e) {
|
||||
|
||||
QWidget::showEvent(e);
|
||||
if (overlay_) {
|
||||
QTimer::singleShot(0, this, SLOT(Grab()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchTermWidget::Grab() { overlay_->Grab(); }
|
||||
|
||||
void SmartPlaylistSearchTermWidget::set_overlay_opacity(float opacity) {
|
||||
if (overlay_) overlay_->SetOpacity(opacity);
|
||||
}
|
||||
|
||||
float SmartPlaylistSearchTermWidget::overlay_opacity() const {
|
||||
return overlay_ ? overlay_->opacity() : 0.0;
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchTermWidget::SetTerm(const SmartPlaylistSearchTerm &term) {
|
||||
|
||||
ui_->field->setCurrentIndex(ui_->field->findData(term.field_));
|
||||
ui_->op->setCurrentIndex(ui_->op->findData(term.operator_));
|
||||
|
||||
// The value depends on the data type
|
||||
switch (SmartPlaylistSearchTerm::TypeOf(term.field_)) {
|
||||
case SmartPlaylistSearchTerm::Type_Text:
|
||||
if (ui_->value_stack->currentWidget() == ui_->page_empty) {
|
||||
ui_->value_text->setText("");
|
||||
} else {
|
||||
ui_->value_text->setText(term.value_.toString());
|
||||
}
|
||||
break;
|
||||
|
||||
case SmartPlaylistSearchTerm::Type_Number:
|
||||
ui_->value_number->setValue(term.value_.toInt());
|
||||
break;
|
||||
|
||||
case SmartPlaylistSearchTerm::Type_Date:
|
||||
if (ui_->value_stack->currentWidget() == ui_->page_date_numeric) {
|
||||
ui_->value_date_numeric->setValue(term.value_.toInt());
|
||||
ui_->date_type->setCurrentIndex(term.date_);
|
||||
}
|
||||
else if (ui_->value_stack->currentWidget() == ui_->page_date_relative) {
|
||||
ui_->value_date_numeric1->setValue(term.value_.toInt());
|
||||
ui_->value_date_numeric2->setValue(term.second_value_.toInt());
|
||||
ui_->date_type_relative->setCurrentIndex(term.date_);
|
||||
}
|
||||
else if (ui_->value_stack->currentWidget() == ui_->page_date) {
|
||||
ui_->value_date->setDateTime(QDateTime::fromSecsSinceEpoch(term.value_.toInt()));
|
||||
}
|
||||
break;
|
||||
|
||||
case SmartPlaylistSearchTerm::Type_Time:
|
||||
ui_->value_time->setTime(QTime(0, 0).addSecs(term.value_.toInt()));
|
||||
break;
|
||||
|
||||
case SmartPlaylistSearchTerm::Type_Rating:
|
||||
ui_->value_rating->set_rating(term.value_.toFloat());
|
||||
break;
|
||||
|
||||
case SmartPlaylistSearchTerm::Type_Invalid:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SmartPlaylistSearchTerm SmartPlaylistSearchTermWidget::Term() const {
|
||||
|
||||
const int field = ui_->field->itemData(ui_->field->currentIndex()).toInt();
|
||||
const int op = ui_->op->itemData(ui_->op->currentIndex()).toInt();
|
||||
|
||||
SmartPlaylistSearchTerm ret;
|
||||
ret.field_ = SmartPlaylistSearchTerm::Field(field);
|
||||
ret.operator_ = SmartPlaylistSearchTerm::Operator(op);
|
||||
|
||||
// The value depends on the data type
|
||||
const QWidget *value_page = ui_->value_stack->currentWidget();
|
||||
if (value_page == ui_->page_text) {
|
||||
ret.value_ = ui_->value_text->text();
|
||||
}
|
||||
else if (value_page == ui_->page_empty) {
|
||||
ret.value_ = "";
|
||||
}
|
||||
else if (value_page == ui_->page_number) {
|
||||
ret.value_ = ui_->value_number->value();
|
||||
}
|
||||
else if (value_page == ui_->page_date) {
|
||||
ret.value_ = ui_->value_date->dateTime().toSecsSinceEpoch();
|
||||
}
|
||||
else if (value_page == ui_->page_time) {
|
||||
ret.value_ = QTime(0, 0).secsTo(ui_->value_time->time());
|
||||
}
|
||||
else if (value_page == ui_->page_date_numeric) {
|
||||
ret.date_ = SmartPlaylistSearchTerm::DateType(ui_->date_type->currentIndex());
|
||||
ret.value_ = ui_->value_date_numeric->value();
|
||||
}
|
||||
else if (value_page == ui_->page_date_relative) {
|
||||
ret.date_ = SmartPlaylistSearchTerm::DateType(ui_->date_type_relative->currentIndex());
|
||||
ret.value_ = ui_->value_date_numeric1->value();
|
||||
ret.second_value_ = ui_->value_date_numeric2->value();
|
||||
}
|
||||
else if (value_page == ui_->page_rating) {
|
||||
ret.value_ = ui_->value_rating->rating();
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchTermWidget::RelativeValueChanged() {
|
||||
|
||||
// Don't check for validity when creating the widget
|
||||
if (!initialized_) {
|
||||
initialized_ = true;
|
||||
return;
|
||||
}
|
||||
// Explain the user why he can't proceed
|
||||
if (ui_->value_date_numeric1->value() >= ui_->value_date_numeric2->value()) {
|
||||
QMessageBox::warning(this, tr("Strawberry"), tr("The second value must be greater than the first one!"));
|
||||
}
|
||||
// Emit the signal in any case, so the Next button will be disabled
|
||||
emit Changed();
|
||||
|
||||
}
|
||||
|
||||
SmartPlaylistSearchTermWidget::Overlay::Overlay(SmartPlaylistSearchTermWidget *parent)
|
||||
: QWidget(parent),
|
||||
parent_(parent),
|
||||
opacity_(0.0),
|
||||
text_(tr("Add search term")),
|
||||
icon_(IconLoader::Load("list-add").pixmap(kIconSize)) {
|
||||
|
||||
raise();
|
||||
setFocusPolicy(Qt::TabFocus);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchTermWidget::Overlay::SetOpacity(const float opacity) {
|
||||
|
||||
opacity_ = opacity;
|
||||
update();
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchTermWidget::Overlay::Grab() {
|
||||
|
||||
hide();
|
||||
|
||||
// Take a "screenshot" of the window
|
||||
QPixmap pixmap = parent_->grab();
|
||||
QImage image = pixmap.toImage();
|
||||
|
||||
// Blur it
|
||||
QImage blurred(image.size(), QImage::Format_ARGB32_Premultiplied);
|
||||
blurred.fill(Qt::transparent);
|
||||
|
||||
QPainter blur_painter(&blurred);
|
||||
qt_blurImage(&blur_painter, image, 10.0, true, false);
|
||||
blur_painter.end();
|
||||
|
||||
pixmap_ = QPixmap::fromImage(blurred);
|
||||
|
||||
resize(parent_->size());
|
||||
show();
|
||||
update();
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchTermWidget::Overlay::paintEvent(QPaintEvent*) {
|
||||
|
||||
QPainter p(this);
|
||||
|
||||
// Background
|
||||
p.fillRect(rect(), palette().window());
|
||||
|
||||
// Blurred parent widget
|
||||
p.setOpacity(0.25 + opacity_ * 0.25);
|
||||
p.drawPixmap(0, 0, pixmap_);
|
||||
|
||||
// Draw a frame
|
||||
p.setOpacity(1.0);
|
||||
p.setPen(palette().color(QPalette::Mid));
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
p.drawRoundedRect(rect(), 5, 5);
|
||||
|
||||
// Geometry
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
|
||||
const QSize contents_size(kIconSize + kSpacing + fontMetrics().horizontalAdvance(text_), qMax(kIconSize, fontMetrics().height()));
|
||||
#else
|
||||
const QSize contents_size(kIconSize + kSpacing + fontMetrics().width(text_), qMax(kIconSize, fontMetrics().height()));
|
||||
#endif
|
||||
|
||||
const QRect contents(QPoint((width() - contents_size.width()) / 2, (height() - contents_size.height()) / 2), contents_size);
|
||||
const QRect icon(contents.topLeft(), QSize(kIconSize, kIconSize));
|
||||
const QRect text(icon.right() + kSpacing, icon.top(), contents.width() - kSpacing - kIconSize, contents.height());
|
||||
|
||||
// Icon and text
|
||||
p.setPen(palette().color(QPalette::Text));
|
||||
p.drawPixmap(icon, icon_);
|
||||
p.drawText(text, Qt::TextDontClip | Qt::AlignVCenter, text_);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchTermWidget::Overlay::mouseReleaseEvent(QMouseEvent*) {
|
||||
emit parent_->Clicked();
|
||||
}
|
||||
|
||||
void SmartPlaylistSearchTermWidget::Overlay::keyReleaseEvent(QKeyEvent *e) {
|
||||
if (e->key() == Qt::Key_Space) emit parent_->Clicked();
|
||||
}
|
94
src/smartplaylists/smartplaylistsearchtermwidget.h
Normal file
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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 SMARTPLAYLISTSEARCHTERMWIDGET_H
|
||||
#define SMARTPLAYLISTSEARCHTERMWIDGET_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QPushButton>
|
||||
|
||||
#include "smartplaylistsearchterm.h"
|
||||
|
||||
class QPropertyAnimation;
|
||||
class QEvent;
|
||||
class QShowEvent;
|
||||
class QEnterEvent;
|
||||
class QResizeEvent;
|
||||
|
||||
class CollectionBackend;
|
||||
class Ui_SmartPlaylistSearchTermWidget;
|
||||
|
||||
class SmartPlaylistSearchTermWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(float overlay_opacity READ overlay_opacity WRITE set_overlay_opacity)
|
||||
|
||||
public:
|
||||
explicit SmartPlaylistSearchTermWidget(CollectionBackend *collection, QWidget *parent);
|
||||
~SmartPlaylistSearchTermWidget();
|
||||
|
||||
void SetActive(const bool active);
|
||||
|
||||
float overlay_opacity() const;
|
||||
void set_overlay_opacity(const float opacity);
|
||||
|
||||
void SetTerm(const SmartPlaylistSearchTerm& term);
|
||||
SmartPlaylistSearchTerm Term() const;
|
||||
|
||||
signals:
|
||||
void Clicked();
|
||||
void RemoveClicked();
|
||||
|
||||
void Changed();
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent*) override;
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
void enterEvent(QEnterEvent*) override;
|
||||
#else
|
||||
void enterEvent(QEvent*) override;
|
||||
#endif
|
||||
void leaveEvent(QEvent*) override;
|
||||
void resizeEvent(QResizeEvent*) override;
|
||||
|
||||
private slots:
|
||||
void FieldChanged(const int index);
|
||||
void OpChanged(const int index);
|
||||
void RelativeValueChanged();
|
||||
void Grab();
|
||||
|
||||
private:
|
||||
class Overlay;
|
||||
friend class Overlay;
|
||||
|
||||
Ui_SmartPlaylistSearchTermWidget *ui_;
|
||||
CollectionBackend *collection_;
|
||||
|
||||
Overlay *overlay_;
|
||||
QPropertyAnimation *animation_;
|
||||
bool active_;
|
||||
bool initialized_;
|
||||
|
||||
SmartPlaylistSearchTerm::Type current_field_type_;
|
||||
};
|
||||
|
||||
#endif // SMARTPLAYLISTSEARCHTERMWIDGET_H
|
350
src/smartplaylists/smartplaylistsearchtermwidget.ui
Normal file
@ -0,0 +1,350 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SmartPlaylistSearchTermWidget</class>
|
||||
<widget class="QWidget" name="SmartPlaylistSearchTermWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>640</width>
|
||||
<height>38</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="layout_smartplaylistsearchtermwidget">
|
||||
<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="QFrame" name="frame">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="layout_frame">
|
||||
<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="QFrame" name="container">
|
||||
<layout class="QHBoxLayout" name="layout_container">
|
||||
<property name="spacing">
|
||||
<number>6</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="QComboBox" name="field"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="op"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="value_stack">
|
||||
<widget class="QWidget" name="page_text">
|
||||
<layout class="QVBoxLayout" name="layout_page_text">
|
||||
<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="QLineEdit" name="value_text"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_empty">
|
||||
<layout class="QVBoxLayout" name="layout_page_empty">
|
||||
<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>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_rating">
|
||||
<layout class="QVBoxLayout" name="layout_page_rating">
|
||||
<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="RatingWidget" name="value_rating" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_time">
|
||||
<layout class="QVBoxLayout" name="layout_page_time">
|
||||
<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="QTimeEdit" name="value_time">
|
||||
<property name="displayFormat">
|
||||
<string notr="true">mm:ss</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_number">
|
||||
<layout class="QVBoxLayout" name="layout_page_number">
|
||||
<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="QSpinBox" name="value_number">
|
||||
<property name="maximum">
|
||||
<number>1000000</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_date">
|
||||
<layout class="QVBoxLayout" name="layout_page_date">
|
||||
<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="QDateEdit" name="value_date">
|
||||
<property name="calendarPopup">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_date_numeric">
|
||||
<layout class="QHBoxLayout" name="layout_page_numeric">
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="value_date_numeric">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>999</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="date_type"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_date_relative">
|
||||
<layout class="QHBoxLayout" name="layout_page_date_relative">
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="value_date_numeric1">
|
||||
<property name="minimum">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>999</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>1</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_1">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>and</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="value_date_numeric2">
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>999</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="date_type_relative"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>ago</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="remove">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>RatingWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/ratingwidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
45
src/smartplaylists/smartplaylistsitem.h
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 SMARTPLAYLISTSITEM_H
|
||||
#define SMARTPLAYLISTSITEM_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QByteArray>
|
||||
|
||||
#include "core/simpletreeitem.h"
|
||||
#include "playlistgenerator.h"
|
||||
|
||||
class SmartPlaylistsItem : public SimpleTreeItem<SmartPlaylistsItem> {
|
||||
public:
|
||||
enum Type {
|
||||
Type_Root,
|
||||
Type_SmartPlaylist,
|
||||
};
|
||||
|
||||
SmartPlaylistsItem(SimpleTreeModel<SmartPlaylistsItem> *_model) : SimpleTreeItem<SmartPlaylistsItem>(Type_Root, _model) {}
|
||||
SmartPlaylistsItem(const Type _type, SmartPlaylistsItem *_parent = nullptr) : SimpleTreeItem<SmartPlaylistsItem>(_type, _parent) {}
|
||||
|
||||
PlaylistGenerator::Type smart_playlist_type;
|
||||
QByteArray smart_playlist_data;
|
||||
};
|
||||
|
||||
#endif // SMARTPLAYLISTSITEM_H
|
312
src/smartplaylists/smartplaylistsmodel.cpp
Normal file
@ -0,0 +1,312 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This code was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2019, 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 <QAbstractListModel>
|
||||
#include <QVariant>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QMimeData>
|
||||
#include <QSettings>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/database.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/simpletreemodel.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "playlist/songmimedata.h"
|
||||
|
||||
#include "smartplaylistsitem.h"
|
||||
#include "smartplaylistsmodel.h"
|
||||
#include "smartplaylistsview.h"
|
||||
#include "smartplaylistsearch.h"
|
||||
#include "playlistgenerator.h"
|
||||
#include "playlistgeneratormimedata.h"
|
||||
#include "playlistquerygenerator.h"
|
||||
|
||||
const char *SmartPlaylistsModel::kSettingsGroup = "SerializedSmartPlaylists";
|
||||
const char *SmartPlaylistsModel::kSmartPlaylistsMimeType = "application/x-strawberry-smart-playlist-generator";
|
||||
const int SmartPlaylistsModel::kSmartPlaylistsVersion = 1;
|
||||
|
||||
SmartPlaylistsModel::SmartPlaylistsModel(CollectionBackend *backend, QObject *parent)
|
||||
: SimpleTreeModel<SmartPlaylistsItem>(new SmartPlaylistsItem(this), parent),
|
||||
backend_(backend),
|
||||
icon_(IconLoader::Load("view-media-playlist")) {
|
||||
|
||||
root_->lazy_loaded = true;
|
||||
|
||||
}
|
||||
|
||||
SmartPlaylistsModel::~SmartPlaylistsModel() { delete root_; }
|
||||
|
||||
void SmartPlaylistsModel::Init() {
|
||||
|
||||
default_smart_playlists_ =
|
||||
SmartPlaylistsModel::DefaultGenerators()
|
||||
<< (SmartPlaylistsModel::GeneratorList()
|
||||
<< PlaylistGeneratorPtr(
|
||||
new PlaylistQueryGenerator(
|
||||
QT_TRANSLATE_NOOP("SmartPlaylists", "Newest tracks"),
|
||||
SmartPlaylistSearch(SmartPlaylistSearch::Type_All, SmartPlaylistSearch::TermList(),
|
||||
SmartPlaylistSearch::Sort_FieldDesc,
|
||||
SmartPlaylistSearchTerm::Field_DateCreated)
|
||||
)
|
||||
)
|
||||
<< PlaylistGeneratorPtr(new PlaylistQueryGenerator(
|
||||
QT_TRANSLATE_NOOP("SmartPlaylists", "50 random tracks"),
|
||||
SmartPlaylistSearch(SmartPlaylistSearch::Type_All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::Sort_Random, SmartPlaylistSearchTerm::Field_Title, 50)
|
||||
)
|
||||
)
|
||||
<< PlaylistGeneratorPtr(
|
||||
new PlaylistQueryGenerator(
|
||||
QT_TRANSLATE_NOOP("SmartPlaylists", "Ever played"),
|
||||
SmartPlaylistSearch(SmartPlaylistSearch::Type_And, SmartPlaylistSearch::TermList() << SmartPlaylistSearchTerm( SmartPlaylistSearchTerm::Field_PlayCount, SmartPlaylistSearchTerm::Op_GreaterThan, 0), SmartPlaylistSearch::Sort_Random, SmartPlaylistSearchTerm::Field_Title)
|
||||
)
|
||||
)
|
||||
<< PlaylistGeneratorPtr(
|
||||
new PlaylistQueryGenerator(
|
||||
QT_TRANSLATE_NOOP("SmartPlaylists", "Never played"),
|
||||
SmartPlaylistSearch(SmartPlaylistSearch::Type_And, SmartPlaylistSearch::TermList() << SmartPlaylistSearchTerm(SmartPlaylistSearchTerm::Field_PlayCount, SmartPlaylistSearchTerm::Op_Equals, 0), SmartPlaylistSearch::Sort_Random, SmartPlaylistSearchTerm::Field_Title)
|
||||
)
|
||||
)
|
||||
<< PlaylistGeneratorPtr(
|
||||
new PlaylistQueryGenerator(
|
||||
QT_TRANSLATE_NOOP("SmartPlaylists", "Last played"),
|
||||
SmartPlaylistSearch(SmartPlaylistSearch::Type_All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::Sort_FieldDesc, SmartPlaylistSearchTerm::Field_LastPlayed)
|
||||
)
|
||||
)
|
||||
<< PlaylistGeneratorPtr(
|
||||
new PlaylistQueryGenerator(
|
||||
QT_TRANSLATE_NOOP("SmartPlaylists", "Most played"),
|
||||
SmartPlaylistSearch(SmartPlaylistSearch::Type_All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::Sort_FieldDesc, SmartPlaylistSearchTerm::Field_PlayCount)
|
||||
)
|
||||
)
|
||||
<< PlaylistGeneratorPtr(
|
||||
new PlaylistQueryGenerator(
|
||||
QT_TRANSLATE_NOOP("SmartPlaylists", "Favourite tracks"),
|
||||
SmartPlaylistSearch(SmartPlaylistSearch::Type_All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::Sort_FieldDesc, SmartPlaylistSearchTerm::Field_Rating)
|
||||
)
|
||||
)
|
||||
<< PlaylistGeneratorPtr(
|
||||
new PlaylistQueryGenerator(
|
||||
QT_TRANSLATE_NOOP("Library", "Least favourite tracks"),
|
||||
SmartPlaylistSearch(SmartPlaylistSearch::Type_Or, SmartPlaylistSearch::TermList()
|
||||
<< SmartPlaylistSearchTerm(SmartPlaylistSearchTerm::Field_Rating, SmartPlaylistSearchTerm::Op_LessThan, 0.5)
|
||||
<< SmartPlaylistSearchTerm(SmartPlaylistSearchTerm::Field_SkipCount, SmartPlaylistSearchTerm::Op_GreaterThan, 4), SmartPlaylistSearch::Sort_FieldDesc, SmartPlaylistSearchTerm::Field_SkipCount)
|
||||
)
|
||||
)
|
||||
)
|
||||
<< (SmartPlaylistsModel::GeneratorList() << PlaylistGeneratorPtr(new PlaylistQueryGenerator(QT_TRANSLATE_NOOP("SmartPlaylists", "All tracks"), SmartPlaylistSearch(SmartPlaylistSearch::Type_All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::Sort_FieldAsc, SmartPlaylistSearchTerm::Field_Artist, -1))))
|
||||
<< (SmartPlaylistsModel::GeneratorList() << PlaylistGeneratorPtr(new PlaylistQueryGenerator( QT_TRANSLATE_NOOP("SmartPlaylists", "Dynamic random mix"), SmartPlaylistSearch(SmartPlaylistSearch::Type_All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::Sort_Random, SmartPlaylistSearchTerm::Field_Title), true)));
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
int version = s.value(backend_->songs_table() + "_version", 0).toInt();
|
||||
|
||||
// How many defaults do we have to write?
|
||||
int unwritten_defaults = 0;
|
||||
for (int i = version; i < default_smart_playlists_.count(); ++i) {
|
||||
unwritten_defaults += default_smart_playlists_[i].count();
|
||||
}
|
||||
|
||||
// Save the defaults if there are any unwritten ones
|
||||
if (unwritten_defaults) {
|
||||
// How many items are stored already?
|
||||
int playlist_index = s.beginReadArray(backend_->songs_table());
|
||||
s.endArray();
|
||||
|
||||
// Append the new ones
|
||||
s.beginWriteArray(backend_->songs_table(), playlist_index + unwritten_defaults);
|
||||
for (; version < default_smart_playlists_.count(); ++version) {
|
||||
for (PlaylistGeneratorPtr gen : default_smart_playlists_[version]) {
|
||||
SaveGenerator(&s, playlist_index++, gen);
|
||||
}
|
||||
}
|
||||
s.endArray();
|
||||
}
|
||||
|
||||
s.setValue(backend_->songs_table() + "_version", version);
|
||||
|
||||
const int count = s.beginReadArray(backend_->songs_table());
|
||||
for (int i = 0; i < count; ++i) {
|
||||
s.setArrayIndex(i);
|
||||
ItemFromSmartPlaylist(s, false);
|
||||
}
|
||||
s.endArray();
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsModel::ItemFromSmartPlaylist(const QSettings &s, const bool notify) {
|
||||
|
||||
SmartPlaylistsItem *item = new SmartPlaylistsItem(SmartPlaylistsItem::Type_SmartPlaylist, notify ? nullptr : root_);
|
||||
item->display_text = tr(qPrintable(s.value("name").toString()));
|
||||
item->sort_text = item->display_text;
|
||||
item->smart_playlist_type = PlaylistGenerator::Type(s.value("type").toInt());
|
||||
item->smart_playlist_data = s.value("data").toByteArray();
|
||||
item->lazy_loaded = true;
|
||||
|
||||
if (notify) item->InsertNotify(root_);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsModel::AddGenerator(PlaylistGeneratorPtr gen) {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
|
||||
// Count the existing items
|
||||
const int count = s.beginReadArray(backend_->songs_table());
|
||||
s.endArray();
|
||||
|
||||
// Add this one to the end
|
||||
s.beginWriteArray(backend_->songs_table(), count + 1);
|
||||
SaveGenerator(&s, count, gen);
|
||||
|
||||
// Add it to the model
|
||||
ItemFromSmartPlaylist(s, true);
|
||||
|
||||
s.endArray();
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsModel::UpdateGenerator(const QModelIndex &idx, PlaylistGeneratorPtr gen) {
|
||||
|
||||
if (idx.parent() != ItemToIndex(root_)) return;
|
||||
SmartPlaylistsItem *item = IndexToItem(idx);
|
||||
if (!item) return;
|
||||
|
||||
// Update the config
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
|
||||
// Count the existing items
|
||||
const int count = s.beginReadArray(backend_->songs_table());
|
||||
s.endArray();
|
||||
|
||||
s.beginWriteArray(backend_->songs_table(), count);
|
||||
SaveGenerator(&s, idx.row(), gen);
|
||||
|
||||
s.endGroup();
|
||||
|
||||
// Update the text of the item
|
||||
item->display_text = gen->name();
|
||||
item->sort_text = item->display_text;
|
||||
item->smart_playlist_type = gen->type();
|
||||
item->smart_playlist_data = gen->Save();
|
||||
item->ChangedNotify();
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsModel::DeleteGenerator(const QModelIndex &idx) {
|
||||
|
||||
if (idx.parent() != ItemToIndex(root_)) return;
|
||||
|
||||
// Remove the item from the tree
|
||||
root_->DeleteNotify(idx.row());
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
|
||||
// Rewrite all the items to the settings
|
||||
s.beginWriteArray(backend_->songs_table(), root_->children.count());
|
||||
int i = 0;
|
||||
for (SmartPlaylistsItem *item : root_->children) {
|
||||
s.setArrayIndex(i++);
|
||||
s.setValue("name", item->display_text);
|
||||
s.setValue("type", item->smart_playlist_type);
|
||||
s.setValue("data", item->smart_playlist_data);
|
||||
}
|
||||
s.endArray();
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsModel::SaveGenerator(QSettings *s, const int i, PlaylistGeneratorPtr generator) const {
|
||||
|
||||
s->setArrayIndex(i);
|
||||
s->setValue("name", generator->name());
|
||||
s->setValue("type", generator->type());
|
||||
s->setValue("data", generator->Save());
|
||||
|
||||
}
|
||||
|
||||
PlaylistGeneratorPtr SmartPlaylistsModel::CreateGenerator(const QModelIndex &idx) const {
|
||||
|
||||
PlaylistGeneratorPtr ret;
|
||||
|
||||
const SmartPlaylistsItem *item = IndexToItem(idx);
|
||||
if (!item || item->type != SmartPlaylistsItem::Type_SmartPlaylist) return ret;
|
||||
|
||||
ret = PlaylistGenerator::Create(item->smart_playlist_type);
|
||||
if (!ret) return ret;
|
||||
|
||||
ret->set_name(item->display_text);
|
||||
ret->set_collection(backend_);
|
||||
ret->Load(item->smart_playlist_data);
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QVariant SmartPlaylistsModel::data(const QModelIndex &idx, const int role) const {
|
||||
|
||||
if (!idx.isValid()) return QVariant();
|
||||
const SmartPlaylistsItem *item = IndexToItem(idx);
|
||||
if (!item) return QVariant();
|
||||
|
||||
switch (role) {
|
||||
case Qt::DecorationRole:
|
||||
return icon_;
|
||||
case Qt::DisplayRole:
|
||||
case Qt::ToolTipRole:
|
||||
return item->DisplayText();
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsModel::LazyPopulate(SmartPlaylistsItem *parent, const bool signal) {
|
||||
Q_UNUSED(parent);
|
||||
Q_UNUSED(signal);
|
||||
}
|
||||
|
||||
QStringList SmartPlaylistsModel::mimeTypes() const {
|
||||
return QStringList() << "text/uri-list";
|
||||
}
|
||||
|
||||
QMimeData *SmartPlaylistsModel::mimeData(const QModelIndexList &indexes) const {
|
||||
|
||||
if (indexes.isEmpty()) return nullptr;
|
||||
|
||||
PlaylistGeneratorPtr generator = CreateGenerator(indexes.first());
|
||||
if (!generator) return nullptr;
|
||||
|
||||
PlaylistGeneratorMimeData *data = new PlaylistGeneratorMimeData(generator);
|
||||
data->setData(kSmartPlaylistsMimeType, QByteArray());
|
||||
data->name_for_new_playlist_ = this->data(indexes.first()).toString();
|
||||
return data;
|
||||
|
||||
}
|
94
src/smartplaylists/smartplaylistsmodel.h
Normal file
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This code was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2019, 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 SMARTPLAYLISTSMODEL_H
|
||||
#define SMARTPLAYLISTSMODEL_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QStringList>
|
||||
#include <QSettings>
|
||||
#include <QIcon>
|
||||
|
||||
#include "core/simpletreemodel.h"
|
||||
#include "smartplaylistsitem.h"
|
||||
#include "playlistgenerator_fwd.h"
|
||||
|
||||
class Application;
|
||||
class CollectionBackend;
|
||||
|
||||
class QModelIndex;
|
||||
class QMimeData;
|
||||
|
||||
class SmartPlaylistsModel : public SimpleTreeModel<SmartPlaylistsItem> {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SmartPlaylistsModel(CollectionBackend *backend, QObject *parent = nullptr);
|
||||
~SmartPlaylistsModel();
|
||||
|
||||
void Init();
|
||||
|
||||
enum Role {
|
||||
Role_Type = Qt::UserRole + 1,
|
||||
Role_SortText,
|
||||
Role_DisplayText,
|
||||
Role_Smartplaylist,
|
||||
LastRole
|
||||
};
|
||||
|
||||
typedef QList<PlaylistGeneratorPtr> GeneratorList;
|
||||
typedef QList<GeneratorList> DefaultGenerators;
|
||||
|
||||
PlaylistGeneratorPtr CreateGenerator(const QModelIndex &idx) const;
|
||||
void AddGenerator(PlaylistGeneratorPtr gen);
|
||||
void UpdateGenerator(const QModelIndex &idx, PlaylistGeneratorPtr gen);
|
||||
void DeleteGenerator(const QModelIndex &idx);
|
||||
|
||||
private:
|
||||
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||
QStringList mimeTypes() const override;
|
||||
QMimeData *mimeData(const QModelIndexList &indexes) const override;
|
||||
|
||||
protected:
|
||||
void LazyPopulate(SmartPlaylistsItem *item) override { LazyPopulate(item, true); }
|
||||
void LazyPopulate(SmartPlaylistsItem *item, bool signal);
|
||||
|
||||
private:
|
||||
static const char *kSettingsGroup;
|
||||
static const char *kSmartPlaylistsMimeType;
|
||||
static const int kSmartPlaylistsVersion;
|
||||
|
||||
void SaveGenerator(QSettings *s, const int i, PlaylistGeneratorPtr generator) const;
|
||||
void ItemFromSmartPlaylist(const QSettings &s, const bool notify);
|
||||
|
||||
private:
|
||||
CollectionBackend *backend_;
|
||||
QIcon icon_;
|
||||
DefaultGenerators default_smart_playlists_;
|
||||
QList<SmartPlaylistsItem*> items_;
|
||||
|
||||
};
|
||||
|
||||
#endif // SMARTPLAYLISTSMODEL_H
|
53
src/smartplaylists/smartplaylistsview.cpp
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019, 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 <QListView>
|
||||
#include <QContextMenuEvent>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/mimedata.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "smartplaylistsmodel.h"
|
||||
#include "smartplaylistsview.h"
|
||||
#include "smartplaylistwizard.h"
|
||||
|
||||
SmartPlaylistsView::SmartPlaylistsView(QWidget *_parent) : QListView(_parent) {
|
||||
|
||||
setAttribute(Qt::WA_MacShowFocusRect, false);
|
||||
setDragEnabled(true);
|
||||
setDragDropMode(QAbstractItemView::DragOnly);
|
||||
setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
|
||||
}
|
||||
|
||||
SmartPlaylistsView::~SmartPlaylistsView() {}
|
||||
|
||||
void SmartPlaylistsView::selectionChanged(const QItemSelection&, const QItemSelection&) {
|
||||
emit ItemsSelectedChanged();
|
||||
}
|
||||
|
||||
void SmartPlaylistsView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
emit RightClicked(e->globalPos(), indexAt(e->pos()));
|
||||
e->accept();
|
||||
|
||||
}
|
47
src/smartplaylists/smartplaylistsview.h
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019, 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 SMARTPLAYLISTSVIEW_H
|
||||
#define SMARTPLAYLISTSVIEW_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QListView>
|
||||
#include <QItemSelection>
|
||||
#include <QModelIndex>
|
||||
#include <QPoint>
|
||||
|
||||
class SmartPlaylistsView : public QListView {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SmartPlaylistsView(QWidget *parent = nullptr);
|
||||
~SmartPlaylistsView();
|
||||
|
||||
protected:
|
||||
void selectionChanged(const QItemSelection&, const QItemSelection&) override;
|
||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||
|
||||
signals:
|
||||
void ItemsSelectedChanged();
|
||||
void RightClicked(QPoint global_pos, QModelIndex idx);
|
||||
|
||||
};
|
||||
|
||||
#endif // SMARTPLAYLISTSVIEW_H
|
283
src/smartplaylists/smartplaylistsviewcontainer.cpp
Normal file
@ -0,0 +1,283 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QMenu>
|
||||
#include <QSettings>
|
||||
#include <QShowEvent>
|
||||
#include <QContextMenuEvent>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/mimedata.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "settings/appearancesettingspage.h"
|
||||
|
||||
#include "smartplaylistsviewcontainer.h"
|
||||
#include "smartplaylistsmodel.h"
|
||||
#include "smartplaylistsview.h"
|
||||
#include "smartplaylistsearchterm.h"
|
||||
#include "smartplaylistwizard.h"
|
||||
#include "playlistquerygenerator.h"
|
||||
#include "playlistgenerator_fwd.h"
|
||||
|
||||
#include "ui_smartplaylistsviewcontainer.h"
|
||||
|
||||
SmartPlaylistsViewContainer::SmartPlaylistsViewContainer(Application *app, QWidget *parent)
|
||||
: QWidget(parent),
|
||||
ui_(new Ui_SmartPlaylistsViewContainer),
|
||||
app_(app),
|
||||
context_menu_(new QMenu(this)),
|
||||
context_menu_selected_(new QMenu(this)),
|
||||
action_new_smart_playlist_(nullptr),
|
||||
action_edit_smart_playlist_(nullptr),
|
||||
action_delete_smart_playlist_(nullptr),
|
||||
action_append_to_playlist_(nullptr),
|
||||
action_replace_current_playlist_(nullptr),
|
||||
action_open_in_new_playlist_(nullptr),
|
||||
action_add_to_playlist_enqueue_(nullptr),
|
||||
action_add_to_playlist_enqueue_next_(nullptr)
|
||||
{
|
||||
|
||||
ui_->setupUi(this);
|
||||
|
||||
model_ = new SmartPlaylistsModel(app_->collection_backend(), this);
|
||||
ui_->view->setModel(model_);
|
||||
|
||||
model_->Init();
|
||||
|
||||
action_new_smart_playlist_ = context_menu_->addAction(IconLoader::Load("document-new"), tr("New smart playlist..."), this, SLOT(NewSmartPlaylist()));
|
||||
|
||||
action_append_to_playlist_ = context_menu_selected_->addAction(IconLoader::Load("media-playback-start"), tr("Append to current playlist"), this, SLOT(AppendToPlaylist()));
|
||||
action_replace_current_playlist_ = context_menu_selected_->addAction(IconLoader::Load("media-playback-start"), tr("Replace current playlist"), this, SLOT(ReplaceCurrentPlaylist()));
|
||||
action_open_in_new_playlist_ = context_menu_selected_->addAction(IconLoader::Load("document-new"), tr("Open in new playlist"), this, SLOT(OpenInNewPlaylist()));
|
||||
|
||||
context_menu_selected_->addSeparator();
|
||||
action_add_to_playlist_enqueue_ = context_menu_selected_->addAction(IconLoader::Load("go-next"), tr("Queue track"), this, SLOT(AddToPlaylistEnqueue()));
|
||||
action_add_to_playlist_enqueue_next_ = context_menu_selected_->addAction(IconLoader::Load("go-next"), tr("Play next"), this, SLOT(AddToPlaylistEnqueueNext()));
|
||||
context_menu_selected_->addSeparator();
|
||||
|
||||
context_menu_selected_->addSeparator();
|
||||
context_menu_selected_->addActions(QList<QAction*>() << action_new_smart_playlist_);
|
||||
action_edit_smart_playlist_ = context_menu_selected_->addAction(IconLoader::Load("edit-rename"), tr("Edit smart playlist..."), this, SLOT(EditSmartPlaylistFromContext()));
|
||||
action_delete_smart_playlist_ = context_menu_selected_->addAction(IconLoader::Load("edit-delete"), tr("Delete smart playlist"), this, SLOT(DeleteSmartPlaylistFromContext()));
|
||||
|
||||
context_menu_selected_->addSeparator();
|
||||
|
||||
ui_->new_->setDefaultAction(action_new_smart_playlist_);
|
||||
ui_->edit_->setIcon(IconLoader::Load("edit-rename"));
|
||||
ui_->delete_->setIcon(IconLoader::Load("edit-delete"));
|
||||
connect(ui_->edit_, SIGNAL(clicked()), SLOT(EditSmartPlaylistFromButton()));
|
||||
connect(ui_->delete_, SIGNAL(clicked()), SLOT(DeleteSmartPlaylistFromButton()));
|
||||
|
||||
connect(ui_->view, SIGNAL(ItemsSelectedChanged()), SLOT(ItemsSelectedChanged()));
|
||||
connect(ui_->view, SIGNAL(doubleClicked(QModelIndex)), SLOT(ItemDoubleClicked(QModelIndex)));
|
||||
connect(ui_->view, SIGNAL(RightClicked(QPoint, QModelIndex)), SLOT(RightClicked(QPoint, QModelIndex)));
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
ItemsSelectedChanged();
|
||||
|
||||
}
|
||||
|
||||
SmartPlaylistsViewContainer::~SmartPlaylistsViewContainer() { delete ui_; }
|
||||
|
||||
SmartPlaylistsView *SmartPlaylistsViewContainer::view() const { return ui_->view; }
|
||||
|
||||
void SmartPlaylistsViewContainer::showEvent(QShowEvent *e) {
|
||||
|
||||
ItemsSelectedChanged();
|
||||
|
||||
QWidget::showEvent(e);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::ReloadSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(AppearanceSettingsPage::kSettingsGroup);
|
||||
int iconsize = s.value(AppearanceSettingsPage::kIconSizeLeftPanelButtons, 22).toInt();
|
||||
s.endGroup();
|
||||
|
||||
ui_->new_->setIconSize(QSize(iconsize, iconsize));
|
||||
ui_->delete_->setIconSize(QSize(iconsize, iconsize));
|
||||
ui_->edit_->setIconSize(QSize(iconsize, iconsize));
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::ItemsSelectedChanged() {
|
||||
|
||||
ui_->edit_->setEnabled(ui_->view->selectionModel()->selectedRows().count() > 0);
|
||||
ui_->delete_->setEnabled(ui_->view->selectionModel()->selectedRows().count() > 0);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::RightClicked(const QPoint &global_pos, const QModelIndex &idx) {
|
||||
|
||||
context_menu_index_ = idx;
|
||||
if (context_menu_index_.isValid()) {
|
||||
context_menu_selected_->popup(global_pos);
|
||||
}
|
||||
else {
|
||||
context_menu_->popup(global_pos);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::ReplaceCurrentPlaylist() {
|
||||
|
||||
QMimeData *q_mimedata = ui_->view->model()->mimeData(ui_->view->selectionModel()->selectedIndexes());
|
||||
if (MimeData *mimedata = qobject_cast<MimeData*>(q_mimedata)) {
|
||||
mimedata->clear_first_ = true;
|
||||
}
|
||||
emit AddToPlaylist(q_mimedata);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::AppendToPlaylist() {
|
||||
|
||||
emit AddToPlaylist(ui_->view->model()->mimeData(ui_->view->selectionModel()->selectedIndexes()));
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::OpenInNewPlaylist() {
|
||||
|
||||
QMimeData *q_mimedata = ui_->view->model()->mimeData(ui_->view->selectionModel()->selectedIndexes());
|
||||
if (MimeData *mimedata = qobject_cast<MimeData*>(q_mimedata)) {
|
||||
mimedata->open_in_new_playlist_ = true;
|
||||
}
|
||||
emit AddToPlaylist(q_mimedata);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::AddToPlaylistEnqueue() {
|
||||
|
||||
QMimeData *q_mimedata = ui_->view->model()->mimeData(ui_->view->selectionModel()->selectedIndexes());
|
||||
if (MimeData *mimedata = qobject_cast<MimeData*>(q_mimedata)) {
|
||||
mimedata->enqueue_now_ = true;
|
||||
}
|
||||
emit AddToPlaylist(q_mimedata);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::AddToPlaylistEnqueueNext() {
|
||||
|
||||
QMimeData *q_mimedata = ui_->view->model()->mimeData(ui_->view->selectionModel()->selectedIndexes());
|
||||
if (MimeData *mimedata = qobject_cast<MimeData*>(q_mimedata)) {
|
||||
mimedata->enqueue_next_now_ = true;
|
||||
}
|
||||
emit AddToPlaylist(q_mimedata);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::NewSmartPlaylist() {
|
||||
|
||||
SmartPlaylistWizard *wizard = new SmartPlaylistWizard(app_, app_->collection_backend(), this);
|
||||
wizard->setAttribute(Qt::WA_DeleteOnClose);
|
||||
connect(wizard, SIGNAL(accepted()), SLOT(NewSmartPlaylistFinished()));
|
||||
|
||||
wizard->show();
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::EditSmartPlaylist(const QModelIndex &idx) {
|
||||
|
||||
if (!idx.isValid()) return;
|
||||
|
||||
SmartPlaylistWizard *wizard = new SmartPlaylistWizard(app_, app_->collection_backend(), this);
|
||||
wizard->setAttribute(Qt::WA_DeleteOnClose);
|
||||
connect(wizard, SIGNAL(accepted()), SLOT(EditSmartPlaylistFinished()));
|
||||
|
||||
wizard->show();
|
||||
wizard->SetGenerator(model_->CreateGenerator(idx));
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::EditSmartPlaylistFromContext() {
|
||||
|
||||
if (!context_menu_index_.isValid()) return;
|
||||
|
||||
EditSmartPlaylist(context_menu_index_);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::EditSmartPlaylistFromButton() {
|
||||
|
||||
if (ui_->view->selectionModel()->selectedIndexes().count() == 0) return;
|
||||
|
||||
EditSmartPlaylist(ui_->view->selectionModel()->selectedIndexes().first());
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::DeleteSmartPlaylist(const QModelIndex &idx) {
|
||||
|
||||
if (!idx.isValid()) return;
|
||||
model_->DeleteGenerator(idx);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::DeleteSmartPlaylistFromContext() {
|
||||
|
||||
if (!context_menu_index_.isValid()) return;
|
||||
DeleteSmartPlaylist(context_menu_index_);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::DeleteSmartPlaylistFromButton() {
|
||||
|
||||
if (ui_->view->selectionModel()->selectedIndexes().count() == 0) return;
|
||||
|
||||
DeleteSmartPlaylist(ui_->view->selectionModel()->selectedIndexes().first());
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::NewSmartPlaylistFinished() {
|
||||
|
||||
SmartPlaylistWizard *wizard = qobject_cast<SmartPlaylistWizard*>(sender());
|
||||
if (!wizard) return;
|
||||
disconnect(wizard, SIGNAL(accepted()), this, SLOT(NewSmartPlaylistFinished()));
|
||||
model_->AddGenerator(wizard->CreateGenerator());
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::EditSmartPlaylistFinished() {
|
||||
|
||||
if (!context_menu_index_.isValid()) return;
|
||||
|
||||
const SmartPlaylistWizard *wizard = qobject_cast<SmartPlaylistWizard*>(sender());
|
||||
if (!wizard) return;
|
||||
|
||||
disconnect(wizard, SIGNAL(accepted()), this, SLOT(EditSmartPlaylistFinished()));
|
||||
|
||||
model_->UpdateGenerator(context_menu_index_, wizard->CreateGenerator());
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistsViewContainer::ItemDoubleClicked(const QModelIndex &idx) {
|
||||
|
||||
QMimeData *q_mimedata = ui_->view->model()->mimeData(QModelIndexList() << idx);
|
||||
if (MimeData *mimedata = qobject_cast<MimeData*>(q_mimedata)) {
|
||||
mimedata->from_doubleclick_ = true;
|
||||
}
|
||||
emit AddToPlaylist(q_mimedata);
|
||||
|
||||
}
|
101
src/smartplaylists/smartplaylistsviewcontainer.h
Normal file
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019, 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 SMARTPLAYLISTSVIEWCONTAINER_H
|
||||
#define SMARTPLAYLISTSVIEWCONTAINER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QModelIndex>
|
||||
|
||||
class QMimeData;
|
||||
class QMenu;
|
||||
class QAction;
|
||||
class QShowEvent;
|
||||
|
||||
class Application;
|
||||
class SmartPlaylistsModel;
|
||||
class SmartPlaylistsView;
|
||||
class Ui_SmartPlaylistsViewContainer;
|
||||
|
||||
class SmartPlaylistsViewContainer : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SmartPlaylistsViewContainer(Application *app, QWidget *parent = nullptr);
|
||||
~SmartPlaylistsViewContainer();
|
||||
|
||||
SmartPlaylistsView *view() const;
|
||||
|
||||
void ReloadSettings();
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *e) override;
|
||||
|
||||
private slots:
|
||||
void ItemsSelectedChanged();
|
||||
void ItemDoubleClicked(const QModelIndex &idx);
|
||||
|
||||
void RightClicked(const QPoint &global_pos, const QModelIndex &idx);
|
||||
|
||||
void AppendToPlaylist();
|
||||
void ReplaceCurrentPlaylist();
|
||||
void OpenInNewPlaylist();
|
||||
|
||||
void AddToPlaylistEnqueue();
|
||||
void AddToPlaylistEnqueueNext();
|
||||
|
||||
void NewSmartPlaylist();
|
||||
|
||||
void EditSmartPlaylist(const QModelIndex &idx);
|
||||
void DeleteSmartPlaylist(const QModelIndex &idx);
|
||||
|
||||
void EditSmartPlaylistFromButton();
|
||||
void DeleteSmartPlaylistFromButton();
|
||||
void EditSmartPlaylistFromContext();
|
||||
void DeleteSmartPlaylistFromContext();
|
||||
|
||||
void NewSmartPlaylistFinished();
|
||||
void EditSmartPlaylistFinished();
|
||||
|
||||
signals:
|
||||
void AddToPlaylist(QMimeData *data);
|
||||
void ItemsSelectedChanged(bool);
|
||||
|
||||
private:
|
||||
Ui_SmartPlaylistsViewContainer *ui_;
|
||||
Application *app_;
|
||||
SmartPlaylistsModel *model_;
|
||||
|
||||
QMenu *context_menu_;
|
||||
QMenu *context_menu_selected_;
|
||||
QAction *action_new_smart_playlist_;
|
||||
QAction *action_edit_smart_playlist_;
|
||||
QAction *action_delete_smart_playlist_;
|
||||
QAction *action_append_to_playlist_;
|
||||
QAction *action_replace_current_playlist_;
|
||||
QAction *action_open_in_new_playlist_;
|
||||
QAction *action_add_to_playlist_enqueue_;
|
||||
QAction *action_add_to_playlist_enqueue_next_;
|
||||
QModelIndex context_menu_index_;
|
||||
|
||||
};
|
||||
|
||||
#endif // SMARTPLAYLISTSVIEWCONTAINER_H
|
136
src/smartplaylists/smartplaylistsviewcontainer.ui
Normal file
@ -0,0 +1,136 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SmartPlaylistsViewContainer</class>
|
||||
<widget class="QWidget" name="SmartPlaylistsViewContainer">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>415</width>
|
||||
<height>495</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</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" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<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="QToolButton" name="new_">
|
||||
<property name="toolTip">
|
||||
<string>New smart playlist</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>22</width>
|
||||
<height>22</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="edit_">
|
||||
<property name="toolTip">
|
||||
<string>Edit smart playlist</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>22</width>
|
||||
<height>22</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="delete_">
|
||||
<property name="toolTip">
|
||||
<string>Delete smart playlist</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>22</width>
|
||||
<height>22</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="spacer_buttons">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>70</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="SmartPlaylistsView" name="view" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Ignored" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>SmartPlaylistsView</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>smartplaylists/smartplaylistsview.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
179
src/smartplaylists/smartplaylistwizard.cpp
Normal file
@ -0,0 +1,179 @@
|
||||
/*
|
||||
* 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 <QWizard>
|
||||
#include <QWizardPage>
|
||||
#include <QLabel>
|
||||
#include <QRadioButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/iconloader.h"
|
||||
|
||||
#include "smartplaylistquerywizardplugin.h"
|
||||
#include "smartplaylistwizard.h"
|
||||
#include "smartplaylistwizardplugin.h"
|
||||
#include "ui_smartplaylistwizardfinishpage.h"
|
||||
|
||||
class SmartPlaylistWizard::TypePage : public QWizardPage {
|
||||
public:
|
||||
TypePage(QWidget *parent) : QWizardPage(parent), next_id_(-1) {}
|
||||
|
||||
int nextId() const { return next_id_; }
|
||||
int next_id_;
|
||||
};
|
||||
|
||||
class SmartPlaylistWizard::FinishPage : public QWizardPage {
|
||||
public:
|
||||
FinishPage(QWidget *parent) : QWizardPage(parent), ui_(new Ui_SmartPlaylistWizardFinishPage) {
|
||||
ui_->setupUi(this);
|
||||
connect(ui_->name, SIGNAL(textChanged(QString)), SIGNAL(completeChanged()));
|
||||
}
|
||||
|
||||
~FinishPage() { delete ui_; }
|
||||
|
||||
int nextId() const { return -1; }
|
||||
bool isComplete() const { return !ui_->name->text().isEmpty(); }
|
||||
|
||||
Ui_SmartPlaylistWizardFinishPage *ui_;
|
||||
|
||||
};
|
||||
|
||||
SmartPlaylistWizard::SmartPlaylistWizard(Application *app, CollectionBackend *collection, QWidget *parent)
|
||||
: QWizard(parent),
|
||||
app_(app),
|
||||
collection_(collection),
|
||||
type_page_(new TypePage(this)),
|
||||
finish_page_(new FinishPage(this)),
|
||||
type_index_(-1) {
|
||||
|
||||
setWindowIcon(IconLoader::Load("strawberry"));
|
||||
setWindowTitle(tr("Smart playlist"));
|
||||
|
||||
resize(788, 628);
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
// MacStyle leaves an ugly empty space on the left side of the dialog.
|
||||
setWizardStyle(QWizard::ClassicStyle);
|
||||
#endif
|
||||
|
||||
// Type page
|
||||
type_page_->setTitle(tr("Playlist type"));
|
||||
type_page_->setSubTitle(tr("A smart playlist is a dynamic list of songs that come from your collection. There are different types of smart playlist that offer different ways of selecting songs."));
|
||||
type_page_->setStyleSheet("QRadioButton { font-weight: bold; } QLabel { margin-bottom: 1em; margin-left: 24px; }");
|
||||
addPage(type_page_);
|
||||
|
||||
// Finish page
|
||||
finish_page_->setTitle(tr("Finish"));
|
||||
finish_page_->setSubTitle(tr("Choose a name for your smart playlist"));
|
||||
finish_id_ = addPage(finish_page_);
|
||||
|
||||
new QVBoxLayout(type_page_);
|
||||
AddPlugin(new SmartPlaylistQueryWizardPlugin(app_, collection_, this));
|
||||
|
||||
// Skip the type page - remove this when we have more than one type
|
||||
setStartId(2);
|
||||
|
||||
}
|
||||
|
||||
SmartPlaylistWizard::~SmartPlaylistWizard() {
|
||||
qDeleteAll(plugins_);
|
||||
}
|
||||
|
||||
void SmartPlaylistWizard::SetGenerator(PlaylistGeneratorPtr gen) {
|
||||
|
||||
// Find the right type and jump to the start page
|
||||
for (int i = 0; i < plugins_.count(); ++i) {
|
||||
if (plugins_[i]->type() == gen->type()) {
|
||||
TypeChanged(i);
|
||||
// TODO: Put this back in when the setStartId is removed from the ctor next();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (type_index_ == -1) {
|
||||
qLog(Error) << "Plugin was not found for generator type" << gen->type();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the name
|
||||
if (!gen->name().isEmpty()) {
|
||||
setWindowTitle(windowTitle() + " - " + gen->name());
|
||||
}
|
||||
finish_page_->ui_->name->setText(gen->name());
|
||||
finish_page_->ui_->dynamic->setChecked(gen->is_dynamic());
|
||||
|
||||
// Tell the plugin to load
|
||||
plugins_[type_index_]->SetGenerator(gen);
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistWizard::AddPlugin(SmartPlaylistWizardPlugin *plugin) {
|
||||
|
||||
const int index = plugins_.count();
|
||||
plugins_ << plugin;
|
||||
plugin->Init(this, finish_id_);
|
||||
|
||||
// Create the radio button
|
||||
QRadioButton *radio_button = new QRadioButton(plugin->name(), type_page_);
|
||||
QLabel *description = new QLabel(plugin->description(), type_page_);
|
||||
type_page_->layout()->addWidget(radio_button);
|
||||
type_page_->layout()->addWidget(description);
|
||||
|
||||
connect(radio_button, &QRadioButton::clicked, [this, index]() { TypeChanged(index); } );
|
||||
|
||||
if (index == 0) {
|
||||
radio_button->setChecked(true);
|
||||
TypeChanged(0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistWizard::TypeChanged(const int index) {
|
||||
|
||||
type_index_ = index;
|
||||
type_page_->next_id_ = plugins_[type_index_]->start_page();
|
||||
|
||||
}
|
||||
|
||||
PlaylistGeneratorPtr SmartPlaylistWizard::CreateGenerator() const {
|
||||
|
||||
PlaylistGeneratorPtr ret;
|
||||
if (type_index_ == -1) return ret;
|
||||
|
||||
ret = plugins_[type_index_]->CreateGenerator();
|
||||
if (!ret) return ret;
|
||||
|
||||
ret->set_name(finish_page_->ui_->name->text());
|
||||
ret->set_dynamic(finish_page_->ui_->dynamic->isChecked());
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void SmartPlaylistWizard::initializePage(const int id) {
|
||||
|
||||
if (id == finish_id_) {
|
||||
finish_page_->ui_->dynamic_container->setEnabled(plugins_[type_index_]->is_dynamic());
|
||||
}
|
||||
QWizard::initializePage(id);
|
||||
|
||||
}
|
68
src/smartplaylists/smartplaylistwizard.h
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 SMARTPLAYLISTWIZARD_H
|
||||
#define SMARTPLAYLISTWIZARD_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QWizard>
|
||||
|
||||
#include "playlistgenerator_fwd.h"
|
||||
|
||||
class Application;
|
||||
class CollectionBackend;
|
||||
class SmartPlaylistWizardPlugin;
|
||||
|
||||
class SmartPlaylistWizard : public QWizard {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SmartPlaylistWizard(Application *app, CollectionBackend *collection, QWidget *parent);
|
||||
~SmartPlaylistWizard();
|
||||
|
||||
void SetGenerator(PlaylistGeneratorPtr gen);
|
||||
PlaylistGeneratorPtr CreateGenerator() const;
|
||||
|
||||
protected:
|
||||
void initializePage(const int id);
|
||||
|
||||
private:
|
||||
class TypePage;
|
||||
class FinishPage;
|
||||
|
||||
void AddPlugin(SmartPlaylistWizardPlugin *plugin);
|
||||
|
||||
private slots:
|
||||
void TypeChanged(const int index);
|
||||
|
||||
private:
|
||||
Application *app_;
|
||||
CollectionBackend *collection_;
|
||||
TypePage *type_page_;
|
||||
FinishPage *finish_page_;
|
||||
int finish_id_;
|
||||
|
||||
int type_index_;
|
||||
QList<SmartPlaylistWizardPlugin*> plugins_;
|
||||
|
||||
};
|
||||
|
||||
#endif // SMARTPLAYLISTWIZARD_H
|
69
src/smartplaylists/smartplaylistwizardfinishpage.ui
Normal file
@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SmartPlaylistWizardFinishPage</class>
|
||||
<widget class="QWidget" name="SmartPlaylistWizardFinishPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>583</width>
|
||||
<height>370</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="name"/>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QWidget" name="dynamic_container" native="true">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<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="QCheckBox" name="dynamic">
|
||||
<property name="text">
|
||||
<string>Use dynamic mode</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>In dynamic mode new tracks will be chosen and added to the playlist every time a song finishes.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>24</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
32
src/smartplaylists/smartplaylistwizardplugin.cpp
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 <QObject>
|
||||
#include <QWizard>
|
||||
|
||||
#include "smartplaylistwizardplugin.h"
|
||||
|
||||
SmartPlaylistWizardPlugin::SmartPlaylistWizardPlugin(Application *app, CollectionBackend *collection, QObject *parent) : QObject(parent), app_(app), collection_(collection), start_page_(-1) {}
|
||||
|
||||
void SmartPlaylistWizardPlugin::Init(QWizard *wizard, const int finish_page_id) {
|
||||
start_page_ = CreatePages(wizard, finish_page_id);
|
||||
}
|
60
src/smartplaylists/smartplaylistwizardplugin.h
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 SMARTPLAYLISTWIZARDPLUGIN_H
|
||||
#define SMARTPLAYLISTWIZARDPLUGIN_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "playlistgenerator_fwd.h"
|
||||
|
||||
class QWizard;
|
||||
|
||||
class Application;
|
||||
class CollectionBackend;
|
||||
|
||||
class SmartPlaylistWizardPlugin : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SmartPlaylistWizardPlugin(Application *app, CollectionBackend *collection, QObject *parent);
|
||||
|
||||
virtual QString type() const = 0;
|
||||
virtual QString name() const = 0;
|
||||
virtual QString description() const = 0;
|
||||
virtual bool is_dynamic() const { return false; }
|
||||
int start_page() const { return start_page_; }
|
||||
|
||||
virtual void SetGenerator(PlaylistGeneratorPtr gen) = 0;
|
||||
virtual PlaylistGeneratorPtr CreateGenerator() const = 0;
|
||||
|
||||
void Init(QWizard *wizard, const int finish_page_id);
|
||||
|
||||
protected:
|
||||
virtual int CreatePages(QWizard *wizard, const int finish_page_id) = 0;
|
||||
|
||||
Application *app_;
|
||||
CollectionBackend *collection_;
|
||||
|
||||
private:
|
||||
int start_page_;
|
||||
};
|
||||
|
||||
#endif // SMARTPLAYLISTWIZARDPLUGIN_H
|