diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c95f811..5e723195 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/data/data.qrc b/data/data.qrc index 3c6eb01f..828cb76b 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -13,8 +13,10 @@ schema/schema-10.sql schema/schema-11.sql schema/schema-12.sql + schema/schema-13.sql schema/device-schema.sql style/strawberry.css + style/smartplaylistsearchterm.css html/playing-tooltip.html html/oauthsuccess.html pictures/strawberry.png @@ -40,6 +42,8 @@ pictures/osd_shadow_edge.png pictures/nyancat.png pictures/rainbowdash.png + pictures/star-on.png + pictures/star-off.png fonts/HumongousofEternitySt.ttf mood/sample.mood text/ghosts.txt diff --git a/data/icons.qrc b/data/icons.qrc index c7212d4c..21307b33 100644 --- a/data/icons.qrc +++ b/data/icons.qrc @@ -89,6 +89,7 @@ icons/128x128/love.png icons/128x128/subsonic.png icons/128x128/tidal.png + icons/128x128/qobuz.png icons/128x128/multimedia-player-ipod-standard-black.png icons/64x64/albums.png icons/64x64/alsa.png @@ -180,6 +181,7 @@ icons/64x64/love.png icons/64x64/subsonic.png icons/64x64/tidal.png + icons/64x64/qobuz.png icons/64x64/multimedia-player-ipod-standard-black.png icons/48x48/albums.png icons/48x48/alsa.png @@ -275,6 +277,7 @@ icons/48x48/love.png icons/48x48/subsonic.png icons/48x48/tidal.png + icons/48x48/qobuz.png icons/48x48/multimedia-player-ipod-standard-black.png icons/32x32/albums.png icons/32x32/alsa.png @@ -370,6 +373,7 @@ icons/32x32/love.png icons/32x32/subsonic.png icons/32x32/tidal.png + icons/32x32/qobuz.png icons/32x32/multimedia-player-ipod-standard-black.png icons/22x22/albums.png icons/22x22/alsa.png @@ -465,6 +469,7 @@ icons/22x22/love.png icons/22x22/subsonic.png icons/22x22/tidal.png + icons/22x22/qobuz.png icons/22x22/multimedia-player-ipod-standard-black.png diff --git a/data/icons/128x128/qobuz.png b/data/icons/128x128/qobuz.png new file mode 100644 index 00000000..4b6b1775 Binary files /dev/null and b/data/icons/128x128/qobuz.png differ diff --git a/data/icons/22x22/qobuz.png b/data/icons/22x22/qobuz.png new file mode 100644 index 00000000..09834a5f Binary files /dev/null and b/data/icons/22x22/qobuz.png differ diff --git a/data/icons/32x32/qobuz.png b/data/icons/32x32/qobuz.png new file mode 100644 index 00000000..c9c6b8a3 Binary files /dev/null and b/data/icons/32x32/qobuz.png differ diff --git a/data/icons/48x48/qobuz.png b/data/icons/48x48/qobuz.png new file mode 100644 index 00000000..08af18ce Binary files /dev/null and b/data/icons/48x48/qobuz.png differ diff --git a/data/icons/64x64/qobuz.png b/data/icons/64x64/qobuz.png new file mode 100644 index 00000000..4c7f31b4 Binary files /dev/null and b/data/icons/64x64/qobuz.png differ diff --git a/data/icons/full/qobuz.png b/data/icons/full/qobuz.png new file mode 100644 index 00000000..c7ad97fd Binary files /dev/null and b/data/icons/full/qobuz.png differ diff --git a/data/pictures/star-off.png b/data/pictures/star-off.png new file mode 100644 index 00000000..e6246317 Binary files /dev/null and b/data/pictures/star-off.png differ diff --git a/data/pictures/star-on.png b/data/pictures/star-on.png new file mode 100644 index 00000000..5d586b3c Binary files /dev/null and b/data/pictures/star-on.png differ diff --git a/data/schema/device-schema.sql b/data/schema/device-schema.sql index 94853d0a..fbcd438a 100644 --- a/data/schema/device-schema.sql +++ b/data/schema/device-schema.sql @@ -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; diff --git a/data/schema/schema-13.sql b/data/schema/schema-13.sql new file mode 100644 index 00000000..7720c0b3 --- /dev/null +++ b/data/schema/schema-13.sql @@ -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; diff --git a/data/schema/schema.sql b/data/schema/schema.sql index ff485510..92dc3a4a 100644 --- a/data/schema/schema.sql +++ b/data/schema/schema.sql @@ -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, diff --git a/data/style/smartplaylistsearchterm.css b/data/style/smartplaylistsearchterm.css new file mode 100644 index 00000000..39964869 --- /dev/null +++ b/data/style/smartplaylistsearchterm.css @@ -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; +} + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a89f8e11..887d2782 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/collection/collectionbackend.cpp b/src/collection/collectionbackend.cpp index df9e0b3f..ee23c1af 100644 --- a/src/collection/collectionbackend.cpp +++ b/src/collection/collectionbackend.cpp @@ -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 id_list; + id_list << id; + UpdateSongsRating(id_list, rating); + +} + +void CollectionBackend::UpdateSongsRating(const QList &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& ids, const float rating) { + metaObject()->invokeMethod(this, "UpdateSongsRating", Qt::QueuedConnection, Q_ARG(QList, ids), Q_ARG(float, rating)); +} + diff --git a/src/collection/collectionbackend.h b/src/collection/collectionbackend.h index c96e4cfd..6ba823da 100644 --- a/src/collection/collectionbackend.h +++ b/src/collection/collectionbackend.h @@ -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 &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 &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(); diff --git a/src/collection/collectionmodel.cpp b/src/collection/collectionmodel.cpp index 23b6b470..792e76f2 100644 --- a/src/collection/collectionmodel.cpp +++ b/src/collection/collectionmodel.cpp @@ -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(); diff --git a/src/config.h.in b/src/config.h.in index 90c2d21d..86cdafe7 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -46,6 +46,7 @@ #cmakedefine HAVE_SUBSONIC #cmakedefine HAVE_TIDAL +#cmakedefine HAVE_QOBUZ #cmakedefine HAVE_MOODBAR diff --git a/src/core/application.cpp b/src/core/application.cpp index 70110b4e..22de1f03 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -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; }), diff --git a/src/core/database.cpp b/src/core/database.cpp index 5c31213e..6399bcd1 100644 --- a/src/core/database.cpp +++ b/src/core/database.cpp @@ -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; diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 99cec680..ecce3480 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -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() << 250 << width() - 250); + if (!settings_.contains("splitter_state") || !ui_->splitter->restoreState(settings_.value("splitter_state").toByteArray())) { + ui_->splitter->setSizes(QList() << 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 } diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 770b6d30..7398a412 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -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 track_selection_dialog_; PlaylistItemList autocomplete_tag_items_; + SmartPlaylistsViewContainer *smartplaylists_view_; + InternetSongsView *subsonic_view_; InternetTabsView *tidal_view_; + InternetTabsView *qobuz_view_; LastFMImportDialog *lastfm_import_dialog_; diff --git a/src/core/metatypes.cpp b/src/core/metatypes.cpp index aaa54986..46a00ff2 100644 --- a/src/core/metatypes.cpp +++ b/src/core/metatypes.cpp @@ -69,6 +69,8 @@ #include "internet/internetsearchview.h" +#include "smartplaylists/playlistgenerator_fwd.h" + void RegisterMetaTypes() { qRegisterMetaType("const char*"); @@ -136,4 +138,6 @@ void RegisterMetaTypes() { qRegisterMetaType("InternetSearchView::ResultList"); qRegisterMetaType("InternetSearchView::Result"); + qRegisterMetaType("PlaylistGeneratorPtr"); + } diff --git a/src/core/song.cpp b/src/core/song.cpp index 9b8c9a65..130c75d9 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -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(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()); } diff --git a/src/core/song.h b/src/core/song.h index 9ee81917..a34adbf7 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -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); diff --git a/src/covermanager/qobuzcoverprovider.cpp b/src/covermanager/qobuzcoverprovider.cpp index e9f7b65d..dc15d4a7 100644 --- a/src/covermanager/qobuzcoverprovider.cpp +++ b/src/covermanager/qobuzcoverprovider.cpp @@ -37,40 +37,24 @@ #include #include #include -#include #include #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()), + 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)), this, SLOT(HandleLoginSSLErrors(QList))); - 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 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; diff --git a/src/covermanager/qobuzcoverprovider.h b/src/covermanager/qobuzcoverprovider.h index a2e92e22..8b161539 100644 --- a/src/covermanager/qobuzcoverprovider.h +++ b/src/covermanager/qobuzcoverprovider.h @@ -31,10 +31,12 @@ #include #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 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 Param; typedef QList 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 replies_; diff --git a/src/device/devicedatabasebackend.cpp b/src/device/devicedatabasebackend.cpp index abcb5663..0c689e43 100644 --- a/src/device/devicedatabasebackend.cpp +++ b/src/device/devicedatabasebackend.cpp @@ -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), diff --git a/src/playlist/dynamicplaylistcontrols.cpp b/src/playlist/dynamicplaylistcontrols.cpp new file mode 100644 index 00000000..ef0b9de6 --- /dev/null +++ b/src/playlist/dynamicplaylistcontrols.cpp @@ -0,0 +1,31 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + + 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 . +*/ + +#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_; } diff --git a/src/playlist/dynamicplaylistcontrols.h b/src/playlist/dynamicplaylistcontrols.h new file mode 100644 index 00000000..45fa1d8e --- /dev/null +++ b/src/playlist/dynamicplaylistcontrols.h @@ -0,0 +1,41 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + + 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 . +*/ + +#ifndef DYNAMICPLAYLISTCONTROLS_H +#define DYNAMICPLAYLISTCONTROLS_H + +#include + +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 diff --git a/src/playlist/dynamicplaylistcontrols.ui b/src/playlist/dynamicplaylistcontrols.ui new file mode 100644 index 00000000..3ad3e8cb --- /dev/null +++ b/src/playlist/dynamicplaylistcontrols.ui @@ -0,0 +1,99 @@ + + + DynamicPlaylistControls + + + + 0 + 0 + 483 + 54 + + + + #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; +} + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + 0 + + + + + Dynamic mode is on + + + + + + + New tracks will be added automatically. + + + + + + + + + Expand + + + + + + + Repopulate + + + + + + + Turn off + + + + + + + + + + + diff --git a/src/playlist/playlist.cpp b/src/playlist/playlist.cpp index cd4afed9..140728c8 100644 --- a/src/playlist/playlist.cpp +++ b/src/playlist/playlist.cpp @@ -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(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(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 &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() << 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 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 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); + +} diff --git a/src/playlist/playlist.h b/src/playlist/playlist.h index 4c5286ef..6037b913 100644 --- a/src/playlist/playlist.h +++ b/src/playlist/playlist.h @@ -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(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 &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 &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 diff --git a/src/playlist/playlistbackend.cpp b/src/playlist/playlistbackend.cpp index bec60579..563d7810 100644 --- a/src/playlist/playlistbackend.cpp +++ b/src/playlist/playlistbackend.cpp @@ -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; diff --git a/src/playlist/playlistbackend.h b/src/playlist/playlistbackend.h index 4959b996..e1ef4d10 100644 --- a/src/playlist/playlistbackend.h +++ b/src/playlist/playlistbackend.h @@ -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 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(); diff --git a/src/playlist/playlistdelegates.cpp b/src/playlist/playlistdelegates.cpp index c25dbc8d..0c980209 100644 --- a/src/playlist/playlistdelegates.cpp +++ b/src/playlist/playlistdelegates.cpp @@ -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); + +} diff --git a/src/playlist/playlistdelegates.h b/src/playlist/playlistdelegates.h index 123ca3a0..1d29ec83 100644 --- a/src/playlist/playlistdelegates.h +++ b/src/playlist/playlistdelegates.h @@ -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 diff --git a/src/playlist/playlistheader.cpp b/src/playlist/playlistheader.cpp index 4c4cdaef..afe70332 100644 --- a/src/playlist/playlistheader.cpp +++ b/src/playlist/playlistheader.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -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()); +} + diff --git a/src/playlist/playlistheader.h b/src/playlist/playlistheader.h index e30bc6b6..6ba8fb37 100644 --- a/src/playlist/playlistheader.h +++ b/src/playlist/playlistheader.h @@ -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 show_actions_; }; diff --git a/src/playlist/playlistmanager.cpp b/src/playlist/playlistmanager.cpp index b4aa2147..c67f1ece 100644 --- a/src/playlist/playlistmanager.cpp +++ b/src/playlist/playlistmanager.cpp @@ -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); +} + diff --git a/src/playlist/playlistmanager.h b/src/playlist/playlistmanager.h index e8a862e2..ad47ec51 100644 --- a/src/playlist/playlistmanager.h +++ b/src/playlist/playlistmanager.h @@ -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; diff --git a/src/playlist/playlistsequence.cpp b/src/playlist/playlistsequence.cpp index 01e099bb..3f86e8c3 100644 --- a/src/playlist/playlistsequence.cpp +++ b/src/playlist/playlistsequence.cpp @@ -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 diff --git a/src/playlist/playlistsequence.h b/src/playlist/playlistsequence.h index e0e355a9..3e701c7e 100644 --- a/src/playlist/playlistsequence.h +++ b/src/playlist/playlistsequence.h @@ -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 diff --git a/src/playlist/playlistview.cpp b/src/playlist/playlistview.cpp index 9766a7d9..750224b4 100644 --- a/src/playlist/playlistview.cpp +++ b/src/playlist/playlistview.cpp @@ -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(); 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(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(); + } + +} diff --git a/src/playlist/playlistview.h b/src/playlist/playlistview.h index c42a6ba4..737b166a 100644 --- a/src/playlist/playlistview.h +++ b/src/playlist/playlistview.h @@ -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 diff --git a/src/qobuz/qobuzbaserequest.cpp b/src/qobuz/qobuzbaserequest.cpp new file mode 100644 index 00000000..727484b7 --- /dev/null +++ b/src/qobuz/qobuzbaserequest.cpp @@ -0,0 +1,196 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/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 ¶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)), this, SLOT(HandleSSLErrors(QList))); + + qLog(Debug) << "Qobuz: Sending request" << url; + + return reply; + +} + +void QobuzBaseRequest::HandleSSLErrors(QList 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 + "
"; + } + return error_html; + +} diff --git a/src/qobuz/qobuzbaserequest.h b/src/qobuz/qobuzbaserequest.h new file mode 100644 index 00000000..c35cbff4 --- /dev/null +++ b/src/qobuz/qobuzbaserequest.h @@ -0,0 +1,105 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef QOBUZBASEREQUEST_H +#define QOBUZBASEREQUEST_H + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 Param; + typedef QList ParamList; + + static const char *kApiUrl; + + QNetworkReply *CreateRequest(const QString &ressource_name, const QList ¶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 ssl_errors); + + private: + QobuzService *service_; + NetworkAccessManager *network_; + +}; + +#endif // QOBUZBASEREQUEST_H diff --git a/src/qobuz/qobuzfavoriterequest.cpp b/src/qobuz/qobuzfavoriterequest.cpp new file mode 100644 index 00000000..78f7aa0c --- /dev/null +++ b/src/qobuz/qobuzfavoriterequest.cpp @@ -0,0 +1,277 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "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; + +} diff --git a/src/qobuz/qobuzfavoriterequest.h b/src/qobuz/qobuzfavoriterequest.h new file mode 100644 index 00000000..00306796 --- /dev/null +++ b/src/qobuz/qobuzfavoriterequest.h @@ -0,0 +1,82 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef QOBUZFAVORITEREQUEST_H +#define QOBUZFAVORITEREQUEST_H + +#include "config.h" + +#include +#include +#include +#include + +#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 replies_; + +}; + +#endif // QOBUZFAVORITEREQUEST_H diff --git a/src/qobuz/qobuzrequest.cpp b/src/qobuz/qobuzrequest.cpp new file mode 100644 index 00000000..76edc08b --- /dev/null +++ b/src/qobuz/qobuzrequest.cpp @@ -0,0 +1,1343 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/logging.h" +#include "core/network.h" +#include "core/song.h" +#include "core/timeconstants.h" +#include "core/application.h" +#include "covermanager/albumcoverloader.h" +#include "qobuzservice.h" +#include "qobuzurlhandler.h" +#include "qobuzbaserequest.h" +#include "qobuzrequest.h" + +const int QobuzRequest::kMaxConcurrentArtistsRequests = 3; +const int QobuzRequest::kMaxConcurrentAlbumsRequests = 3; +const int QobuzRequest::kMaxConcurrentSongsRequests = 3; +const int QobuzRequest::kMaxConcurrentArtistAlbumsRequests = 3; +const int QobuzRequest::kMaxConcurrentAlbumSongsRequests = 3; +const int QobuzRequest::kMaxConcurrentAlbumCoverRequests = 1; + +QobuzRequest::QobuzRequest(QobuzService *service, QobuzUrlHandler *url_handler, Application *app, NetworkAccessManager *network, QueryType type, QObject *parent) + : QobuzBaseRequest(service, network, parent), + service_(service), + url_handler_(url_handler), + app_(app), + network_(network), + type_(type), + query_id_(-1), + finished_(false), + artists_requests_active_(0), + artists_total_(0), + artists_received_(0), + albums_requests_active_(0), + songs_requests_active_(0), + artist_albums_requests_active_(0), + artist_albums_requested_(0), + artist_albums_received_(0), + album_songs_requests_active_(0), + album_songs_requested_(0), + album_songs_received_(0), + album_covers_requests_active_(), + album_covers_requested_(0), + album_covers_received_(0), + no_results_(false) {} + +QobuzRequest::~QobuzRequest() { + + while (!replies_.isEmpty()) { + QNetworkReply *reply = replies_.takeFirst(); + disconnect(reply, nullptr, this, nullptr); + if (reply->isRunning()) reply->abort(); + reply->deleteLater(); + } + + while (!album_cover_replies_.isEmpty()) { + QNetworkReply *reply = album_cover_replies_.takeFirst(); + disconnect(reply, nullptr, this, nullptr); + if (reply->isRunning()) reply->abort(); + reply->deleteLater(); + } + +} + +void QobuzRequest::Process() { + + switch (type_) { + case QueryType::QueryType_Artists: + GetArtists(); + break; + case QueryType::QueryType_Albums: + GetAlbums(); + break; + case QueryType::QueryType_Songs: + GetSongs(); + break; + case QueryType::QueryType_SearchArtists: + ArtistsSearch(); + break; + case QueryType::QueryType_SearchAlbums: + AlbumsSearch(); + break; + case QueryType::QueryType_SearchSongs: + SongsSearch(); + break; + default: + Error("Invalid query type."); + break; + } + +} + +void QobuzRequest::Search(const int query_id, const QString &search_text) { + query_id_ = query_id; + search_text_ = search_text; +} + +void QobuzRequest::GetArtists() { + + emit UpdateStatus(query_id_, tr("Retrieving artists...")); + emit UpdateProgress(query_id_, 0); + AddArtistsRequest(); + +} + +void QobuzRequest::AddArtistsRequest(const int offset, const int limit) { + + Request request; + request.limit = limit; + request.offset = offset; + artists_requests_queue_.enqueue(request); + if (artists_requests_active_ < kMaxConcurrentArtistsRequests) FlushArtistsRequests(); + +} + +void QobuzRequest::FlushArtistsRequests() { + + while (!artists_requests_queue_.isEmpty() && artists_requests_active_ < kMaxConcurrentArtistsRequests) { + + Request request = artists_requests_queue_.dequeue(); + ++artists_requests_active_; + + ParamList params; + if (type_ == QueryType_Artists) { + params << Param("type", "artists"); + params << Param("user_auth_token", user_auth_token()); + } + else if (type_ == QueryType_SearchArtists) params << Param("query", search_text_); + if (request.limit > 0) params << Param("limit", QString::number(request.limit)); + if (request.offset > 0) params << Param("offset", QString::number(request.offset)); + QNetworkReply *reply = nullptr; + if (type_ == QueryType_Artists) { + reply = CreateRequest(QString("favorite/getUserFavorites"), params); + } + else if (type_ == QueryType_SearchArtists) { + reply = CreateRequest("artist/search", params); + } + if (!reply) continue; + replies_ << reply; + connect(reply, &QNetworkReply::finished, [=] { ArtistsReplyReceived(reply, request.limit, request.offset); }); + + } + +} + +void QobuzRequest::GetAlbums() { + + emit UpdateStatus(query_id_, tr("Retrieving albums...")); + emit UpdateProgress(query_id_, 0); + AddAlbumsRequest(); + +} + +void QobuzRequest::AddAlbumsRequest(const int offset, const int limit) { + + Request request; + request.limit = limit; + request.offset = offset; + albums_requests_queue_.enqueue(request); + if (albums_requests_active_ < kMaxConcurrentAlbumsRequests) FlushAlbumsRequests(); + +} + +void QobuzRequest::FlushAlbumsRequests() { + + while (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) { + + Request request = albums_requests_queue_.dequeue(); + ++albums_requests_active_; + + ParamList params; + if (type_ == QueryType_Albums) { + params << Param("type", "albums"); + params << Param("user_auth_token", user_auth_token()); + } + else if (type_ == QueryType_SearchAlbums) params << Param("query", search_text_); + if (request.limit > 0) params << Param("limit", QString::number(request.limit)); + if (request.offset > 0) params << Param("offset", QString::number(request.offset)); + QNetworkReply *reply = nullptr; + if (type_ == QueryType_Albums) { + reply = CreateRequest(QString("favorite/getUserFavorites"), params); + } + else if (type_ == QueryType_SearchAlbums) { + reply = CreateRequest("album/search", params); + } + if (!reply) continue; + replies_ << reply; + connect(reply, &QNetworkReply::finished, [=] { AlbumsReplyReceived(reply, request.limit, request.offset); }); + + } + +} + +void QobuzRequest::GetSongs() { + + emit UpdateStatus(query_id_, tr("Retrieving songs...")); + emit UpdateProgress(query_id_, 0); + AddSongsRequest(); + +} + +void QobuzRequest::AddSongsRequest(const int offset, const int limit) { + + Request request; + request.limit = limit; + request.offset = offset; + songs_requests_queue_.enqueue(request); + if (songs_requests_active_ < kMaxConcurrentSongsRequests) FlushSongsRequests(); + +} + +void QobuzRequest::FlushSongsRequests() { + + while (!songs_requests_queue_.isEmpty() && songs_requests_active_ < kMaxConcurrentSongsRequests) { + + Request request = songs_requests_queue_.dequeue(); + ++songs_requests_active_; + + ParamList params; + if (type_ == QueryType_Songs) { + params << Param("type", "tracks"); + params << Param("user_auth_token", user_auth_token()); + } + else if (type_ == QueryType_SearchSongs) params << Param("query", search_text_); + if (request.limit > 0) params << Param("limit", QString::number(request.limit)); + if (request.offset > 0) params << Param("offset", QString::number(request.offset)); + QNetworkReply *reply = nullptr; + if (type_ == QueryType_Songs) { + reply = CreateRequest(QString("favorite/getUserFavorites"), params); + } + else if (type_ == QueryType_SearchSongs) { + reply = CreateRequest("track/search", params); + } + if (!reply) continue; + replies_ << reply; + connect(reply, &QNetworkReply::finished, [=] { SongsReplyReceived(reply, request.limit, request.offset); }); + + } + +} + +void QobuzRequest::ArtistsSearch() { + + emit UpdateStatus(query_id_, tr("Searching...")); + emit UpdateProgress(query_id_, 0); + AddArtistsSearchRequest(); + +} + +void QobuzRequest::AddArtistsSearchRequest(const int offset) { + + AddArtistsRequest(offset, service_->artistssearchlimit()); + +} + +void QobuzRequest::AlbumsSearch() { + + emit UpdateStatus(query_id_, tr("Searching...")); + emit UpdateProgress(query_id_, 0); + AddAlbumsSearchRequest(); + +} + +void QobuzRequest::AddAlbumsSearchRequest(const int offset) { + + AddAlbumsRequest(offset, service_->albumssearchlimit()); + +} + +void QobuzRequest::SongsSearch() { + + emit UpdateStatus(query_id_, tr("Searching...")); + emit UpdateProgress(query_id_, 0); + AddSongsSearchRequest(); + +} + +void QobuzRequest::AddSongsSearchRequest(const int offset) { + + AddSongsRequest(offset, service_->songssearchlimit()); + +} + +void QobuzRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) { + + if (!replies_.contains(reply)) return; + replies_.removeAll(reply); + disconnect(reply, nullptr, this, nullptr); + reply->deleteLater(); + + QByteArray data = GetReplyData(reply); + + --artists_requests_active_; + + if (finished_) return; + + if (data.isEmpty()) { + ArtistsFinishCheck(); + return; + } + + QJsonObject json_obj = ExtractJsonObj(data); + if (json_obj.isEmpty()) { + ArtistsFinishCheck(); + return; + } + + if (!json_obj.contains("artists")) { + ArtistsFinishCheck(); + Error("Json object is missing artists.", json_obj); + return; + } + QJsonValue value_artists = json_obj["artists"]; + if (!value_artists.isObject()) { + Error("Json artists is not an object.", json_obj); + ArtistsFinishCheck(); + return; + } + QJsonObject obj_artists = value_artists.toObject(); + + if (!obj_artists.contains("limit") || + !obj_artists.contains("offset") || + !obj_artists.contains("total") || + !obj_artists.contains("items")) { + ArtistsFinishCheck(); + Error("Json artists object is missing values.", json_obj); + return; + } + //int limit = obj_artists["limit"].toInt(); + int offset = obj_artists["offset"].toInt(); + int artists_total = obj_artists["total"].toInt(); + + if (offset_requested == 0) { + artists_total_ = artists_total; + } + else if (artists_total != artists_total_) { + Error(QString("total returned does not match previous total! %1 != %2").arg(artists_total).arg(artists_total_)); + ArtistsFinishCheck(); + return; + } + + if (offset != offset_requested) { + Error(QString("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); + ArtistsFinishCheck(); + return; + } + + if (offset_requested == 0) { + emit ProgressSetMaximum(query_id_, artists_total_); + emit UpdateProgress(query_id_, artists_received_); + } + + QJsonValue value_items = ExtractItems(obj_artists); + if (!value_items.isArray()) { + ArtistsFinishCheck(); + return; + } + + QJsonArray array_items = value_items.toArray(); + if (array_items.isEmpty()) { // Empty array means no results + if (offset_requested == 0) no_results_ = true; + ArtistsFinishCheck(); + return; + } + + int artists_received = 0; + for (const QJsonValue &value_item : array_items) { + + ++artists_received; + + if (!value_item.isObject()) { + Error("Invalid Json reply, item not a object.", value_item); + continue; + } + QJsonObject obj_item = value_item.toObject(); + + if (obj_item.contains("item")) { + QJsonValue json_item = obj_item["item"]; + if (!json_item.isObject()) { + Error("Invalid Json reply, item not a object.", json_item); + continue; + } + obj_item = json_item.toObject(); + } + + if (!obj_item.contains("id") || !obj_item.contains("name")) { + Error("Invalid Json reply, item missing id or album.", obj_item); + continue; + } + + QString artist_id; + if (obj_item["id"].isString()) { + artist_id = obj_item["id"].toString(); + } + else { + artist_id = QString::number(obj_item["id"].toInt()); + } + if (artist_albums_requests_pending_.contains(artist_id)) continue; + artist_albums_requests_pending_.append(artist_id); + + } + artists_received_ += artists_received; + + if (offset_requested != 0) emit UpdateProgress(query_id_, artists_received_); + + ArtistsFinishCheck(limit_requested, offset, artists_received); + +} + +void QobuzRequest::ArtistsFinishCheck(const int limit, const int offset, const int artists_received) { + + if (finished_) return; + + if ((limit == 0 || limit > artists_received) && artists_received_ < artists_total_) { + int offset_next = offset + artists_received; + if (offset_next > 0 && offset_next < artists_total_) { + if (type_ == QueryType_Artists) AddArtistsRequest(offset_next); + else if (type_ == QueryType_SearchArtists) AddArtistsSearchRequest(offset_next); + } + } + + if (!artists_requests_queue_.isEmpty() && artists_requests_active_ < kMaxConcurrentArtistsRequests) FlushArtistsRequests(); + + if (artists_requests_queue_.isEmpty() && artists_requests_active_ <= 0) { // Artist query is finished, get all albums for all artists. + + // Get artist albums + for (const QString &artist_id : artist_albums_requests_pending_) { + AddArtistAlbumsRequest(artist_id); + ++artist_albums_requested_; + } + artist_albums_requests_pending_.clear(); + + if (artist_albums_requested_ > 0) { + if (artist_albums_requested_ == 1) emit UpdateStatus(query_id_, tr("Retrieving albums for %1 artist...").arg(artist_albums_requested_)); + else emit UpdateStatus(query_id_, tr("Retrieving albums for %1 artists...").arg(artist_albums_requested_)); + emit ProgressSetMaximum(query_id_, artist_albums_requested_); + emit UpdateProgress(query_id_, 0); + } + + } + + FinishCheck(); + +} + +void QobuzRequest::AlbumsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) { + --albums_requests_active_; + AlbumsReceived(reply, QString(), limit_requested, offset_requested); + if (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) FlushAlbumsRequests(); +} + +void QobuzRequest::AddArtistAlbumsRequest(const QString &artist_id, const int offset) { + + Request request; + request.artist_id = artist_id; + request.offset = offset; + artist_albums_requests_queue_.enqueue(request); + if (artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) FlushArtistAlbumsRequests(); + +} + +void QobuzRequest::FlushArtistAlbumsRequests() { + + while (!artist_albums_requests_queue_.isEmpty() && artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) { + + Request request = artist_albums_requests_queue_.dequeue(); + ++artist_albums_requests_active_; + + ParamList params = ParamList() << Param("artist_id", request.artist_id) + << Param("extra", "albums"); + + if (request.offset > 0) params << Param("offset", QString::number(request.offset)); + QNetworkReply *reply = CreateRequest(QString("artist/get"), params); + connect(reply, &QNetworkReply::finished, [=] { ArtistAlbumsReplyReceived(reply, request.artist_id, request.offset); }); + replies_ << reply; + + } + +} + +void QobuzRequest::ArtistAlbumsReplyReceived(QNetworkReply *reply, const QString artist_id, const int offset_requested) { + + --artist_albums_requests_active_; + ++artist_albums_received_; + emit UpdateProgress(query_id_, artist_albums_received_); + AlbumsReceived(reply, artist_id, 0, offset_requested); + if (!artist_albums_requests_queue_.isEmpty() && artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) FlushArtistAlbumsRequests(); + +} + +void QobuzRequest::AlbumsReceived(QNetworkReply *reply, const QString &artist_id_requested, const int limit_requested, const int offset_requested) { + + if (!replies_.contains(reply)) return; + replies_.removeAll(reply); + disconnect(reply, nullptr, this, nullptr); + reply->deleteLater(); + + QByteArray data = GetReplyData(reply); + + if (finished_) return; + + if (data.isEmpty()) { + AlbumsFinishCheck(artist_id_requested); + return; + } + + QJsonObject json_obj = ExtractJsonObj(data); + if (json_obj.isEmpty()) { + AlbumsFinishCheck(artist_id_requested); + return; + } + + QString album_artist_id = artist_id_requested; + if (json_obj.contains("id")) { + if (json_obj["id"].isString()) { + album_artist_id = json_obj["id"].toString(); + } + else { + album_artist_id = QString::number(json_obj["id"].toInt()); + } + } + QString album_artist; + if (json_obj.contains("name")) { + album_artist = json_obj["name"].toString(); + } + + if (album_artist_id != artist_id_requested) { + AlbumsFinishCheck(artist_id_requested); + Error("Artist id returned does not match artist id requested.", json_obj); + return; + } + + if (!json_obj.contains("albums")) { + AlbumsFinishCheck(artist_id_requested); + Error("Json object is missing albums.", json_obj); + return; + } + QJsonValue value_albums = json_obj["albums"]; + if (!value_albums.isObject()) { + Error("Json albums is not an object.", json_obj); + AlbumsFinishCheck(artist_id_requested); + return; + } + QJsonObject obj_albums = value_albums.toObject(); + + if (!obj_albums.contains("limit") || + !obj_albums.contains("offset") || + !obj_albums.contains("total") || + !obj_albums.contains("items")) { + AlbumsFinishCheck(artist_id_requested); + Error("Json albums object is missing values.", json_obj); + return; + } + + //int limit = json_obj["limit"].toInt(); + int offset = json_obj["offset"].toInt(); + int albums_total = json_obj["total"].toInt(); + + if (offset != offset_requested) { + Error(QString("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); + AlbumsFinishCheck(artist_id_requested); + return; + } + + QJsonValue value_items = ExtractItems(obj_albums); + if (!value_items.isArray()) { + AlbumsFinishCheck(artist_id_requested); + return; + } + QJsonArray array_items = value_items.toArray(); + if (array_items.isEmpty()) { + if ((type_ == QueryType_Albums || type_ == QueryType_SearchAlbums) && offset_requested == 0) { + no_results_ = true; + } + AlbumsFinishCheck(artist_id_requested); + return; + } + + int albums_received = 0; + for (const QJsonValue &value : array_items) { + + ++albums_received; + + if (!value.isObject()) { + Error("Invalid Json reply, item not a object.", value); + continue; + } + QJsonObject obj_item = value.toObject(); + + if (!obj_item.contains("artist") || !obj_item.contains("title") || !obj_item.contains("id")) { + Error("Invalid Json reply, item missing artist, title or id.", obj_item); + continue; + } + + QString album_id; + if (obj_item["id"].isString()) { + album_id = obj_item["id"].toString(); + } + else { + album_id = QString::number(obj_item["id"].toInt()); + } + + if (album_songs_requests_pending_.contains(album_id)) continue; + + QString album = obj_item["title"].toString(); + + QJsonValue value_artist = obj_item["artist"]; + if (!value_artist.isObject()) { + Error("Invalid Json reply, item artist is not a object.", value_artist); + continue; + } + QJsonObject obj_artist = value_artist.toObject(); + if (!obj_artist.contains("id") || !obj_artist.contains("name")) { + Error("Invalid Json reply, item artist missing id or name.", obj_artist); + continue; + } + + QString artist_id; + if (obj_artist["id"].isString()) { + artist_id = obj_artist["id"].toString(); + } + else { + artist_id = QString::number(obj_artist["id"].toInt()); + } + + QString artist = obj_artist["name"].toString(); + if (artist_id_requested != 0 && artist_id != artist_id_requested) { + qLog(Debug) << "Skipping artist" << "artist" << artist << artist_id << "does not match album artist" << album_artist_id << album_artist; + continue; + } + + Request request; + request.artist_id = artist_id; + request.album_id = album_id; + request.album_artist = artist; + request.album = album; + album_songs_requests_pending_.insert(album_id, request); + + } + + AlbumsFinishCheck(artist_id_requested, limit_requested, offset, albums_total, albums_received); + +} + +void QobuzRequest::AlbumsFinishCheck(const QString &artist_id, const int limit, const int offset, const int albums_total, const int albums_received) { + + if (finished_) return; + + if (limit == 0 || limit > albums_received) { + int offset_next = offset + albums_received; + if (offset_next > 0 && offset_next < albums_total) { + switch (type_) { + case QueryType_Albums: + AddAlbumsRequest(offset_next); + break; + case QueryType_SearchAlbums: + AddAlbumsSearchRequest(offset_next); + break; + case QueryType_Artists: + case QueryType_SearchArtists: + AddArtistAlbumsRequest(artist_id, offset_next); + break; + default: + break; + } + } + } + + if ( + albums_requests_queue_.isEmpty() && + albums_requests_active_ <= 0 && + artist_albums_requests_queue_.isEmpty() && + artist_albums_requests_active_ <= 0 + ) { // Artist albums query is finished, get all songs for all albums. + + // Get songs for all the albums. + + QHash ::iterator it; + for (it = album_songs_requests_pending_.begin() ; it != album_songs_requests_pending_.end() ; ++it) { + Request request = it.value(); + AddAlbumSongsRequest(request.artist_id, request.album_id, request.album_artist, request.album); + } + album_songs_requests_pending_.clear(); + + if (album_songs_requested_ > 0) { + if (album_songs_requested_ == 1) emit UpdateStatus(query_id_, tr("Retrieving songs for %1 album...").arg(album_songs_requested_)); + else emit UpdateStatus(query_id_, tr("Retrieving songs for %1 albums...").arg(album_songs_requested_)); + emit ProgressSetMaximum(query_id_, album_songs_requested_); + emit UpdateProgress(query_id_, 0); + } + } + + FinishCheck(); + +} + +void QobuzRequest::SongsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) { + + --songs_requests_active_; + SongsReceived(reply, QString(), QString(), limit_requested, offset_requested); + +} + +void QobuzRequest::AddAlbumSongsRequest(const QString &artist_id, const QString &album_id, const QString &album_artist, const QString &album, const int offset) { + + Request request; + request.artist_id = artist_id; + request.album_id = album_id; + request.album_artist = album_artist; + request.album = album; + request.offset = offset; + album_songs_requests_queue_.enqueue(request); + ++album_songs_requested_; + if (album_songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) FlushAlbumSongsRequests(); + +} + +void QobuzRequest::FlushAlbumSongsRequests() { + + while (!album_songs_requests_queue_.isEmpty() && album_songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) { + + Request request = album_songs_requests_queue_.dequeue(); + ++album_songs_requests_active_; + ParamList params = ParamList() << Param("album_id", request.album_id); + if (request.offset > 0) params << Param("offset", QString::number(request.offset)); + QNetworkReply *reply = CreateRequest(QString("album/get"), params); + replies_ << reply; + connect(reply, &QNetworkReply::finished, [=] { AlbumSongsReplyReceived(reply, request.artist_id, request.album_id, request.offset, request.album_artist, request.album); }); + + } + +} + +void QobuzRequest::AlbumSongsReplyReceived(QNetworkReply *reply, const QString &artist_id, const QString &album_id, const int offset_requested, const QString &album_artist, const QString &album) { + + --album_songs_requests_active_; + ++album_songs_received_; + if (offset_requested == 0) { + emit UpdateProgress(query_id_, album_songs_received_); + } + SongsReceived(reply, artist_id, album_id, 0, offset_requested, album_artist, album); + +} + +void QobuzRequest::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, const QString &album_requested) { + + if (!replies_.contains(reply)) return; + replies_.removeAll(reply); + reply->deleteLater(); + + QByteArray data = GetReplyData(reply); + + if (finished_) return; + + if (data.isEmpty()) { + SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested, album_requested); + return; + } + + QJsonObject json_obj = ExtractJsonObj(data); + if (json_obj.isEmpty()) { + SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested, album_requested); + return; + } + + if (!json_obj.contains("tracks")) { + Error("Json object is missing tracks.", json_obj); + SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested, album_requested); + return; + } + + QString artist_id = artist_id_requested; + QString album_artist = album_artist_requested; + QString album_id = album_id_requested; + QString album = album_requested; + QUrl cover_url; + + if (json_obj.contains("id")) { + if (json_obj["id"].isString()) { + album_id = json_obj["id"].toString(); + } + else { + album_id = QString::number(json_obj["id"].toInt()); + } + } + + if (json_obj.contains("title")) { + album = json_obj["title"].toString(); + } + + if (json_obj.contains("artist")) { + QJsonValue value_artist = json_obj["artist"]; + if (!value_artist.isObject()) { + Error("Invalid Json reply, album artist is not a object.", value_artist); + SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album); + return; + } + QJsonObject obj_artist = value_artist.toObject(); + if (!obj_artist.contains("id") || !obj_artist.contains("name")) { + Error("Invalid Json reply, album artist is missing id or name.", obj_artist); + SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album); + return; + } + if (obj_artist["id"].isString()) { + artist_id = obj_artist["id"].toString(); + } + else { + artist_id = QString::number(obj_artist["id"].toInt()); + } + album_artist = obj_artist["name"].toString(); + } + + if (json_obj.contains("image")) { + QJsonValue value_image = json_obj["image"]; + if (!value_image.isObject()) { + Error("Invalid Json reply, album image is not a object.", value_image); + SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album); + return; + } + QJsonObject obj_image = value_image.toObject(); + if (!obj_image.contains("large")) { + Error("Invalid Json reply, album image is missing large.", obj_image); + SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album); + return; + } + QString album_image = obj_image["large"].toString(); + if (!album_image.isEmpty()) { + cover_url = QUrl(album_image); + } + } + + QJsonValue value_tracks = json_obj["tracks"]; + if (!value_tracks.isObject()) { + Error("Json tracks is not an object.", json_obj); + SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album); + return; + } + QJsonObject obj_tracks = value_tracks.toObject(); + + if (!obj_tracks.contains("limit") || + !obj_tracks.contains("offset") || + !obj_tracks.contains("total") || + !obj_tracks.contains("items")) { + SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album); + Error("Json songs object is missing values.", json_obj); + return; + } + + //int limit = obj_tracks["limit"].toInt(); + int offset = obj_tracks["offset"].toInt(); + int songs_total = obj_tracks["total"].toInt(); + + if (offset != offset_requested) { + Error(QString("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); + SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist, album); + return; + } + + QJsonValue value_items = ExtractItems(obj_tracks); + if (!value_items.isArray()) { + SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist, album); + return; + } + + QJsonArray array_items = value_items.toArray(); + if (array_items.isEmpty()) { + if ((type_ == QueryType_Songs || type_ == QueryType_SearchSongs) && offset_requested == 0) { + no_results_ = true; + } + SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist, album); + return; + } + + bool compilation = false; + //bool multidisc = false; + SongList songs; + int songs_received = 0; + for (const QJsonValue &value_item : array_items) { + + if (!value_item.isObject()) { + Error("Invalid Json reply, track is not a object.", value_item); + continue; + } + QJsonObject obj_item = value_item.toObject(); + + ++songs_received; + Song song(Song::Source_Qobuz); + ParseSong(song, obj_item, artist_id, album_id, album_artist, album, cover_url); + if (!song.is_valid()) continue; + //if (song.disc() >= 2) multidisc = true; + if (song.is_compilation()) compilation = true; + songs << song; + } + + for (Song &song : songs) { + if (compilation) song.set_compilation_detected(true); + //if (multidisc) { + //QString album_full(QString("%1 - (Disc %2)").arg(song.album()).arg(song.disc())); + //song.set_album(album_full); + //} + songs_ << song; + } + + SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, songs_received, album_artist, album); + +} + +void QobuzRequest::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) { + + if (finished_) return; + + if (limit == 0 || limit > songs_received) { + int offset_next = offset + songs_received; + if (offset_next > 0 && offset_next < songs_total) { + switch (type_) { + case QueryType_Songs: + AddSongsRequest(offset_next); + break; + case QueryType_SearchSongs: + AddSongsSearchRequest(offset_next); + break; + case QueryType_Artists: + case QueryType_SearchArtists: + case QueryType_Albums: + case QueryType_SearchAlbums: + AddAlbumSongsRequest(artist_id, album_id, album_artist, album, offset_next); + break; + default: + break; + } + } + } + + if (!songs_requests_queue_.isEmpty() && songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) FlushAlbumSongsRequests(); + if (!album_songs_requests_queue_.isEmpty() && album_songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) FlushAlbumSongsRequests(); + + if ( + service_->download_album_covers() && + IsQuery() && + songs_requests_queue_.isEmpty() && + songs_requests_active_ <= 0 && + album_songs_requests_queue_.isEmpty() && + album_songs_requests_active_ <= 0 && + album_cover_requests_queue_.isEmpty() && + album_covers_received_ <= 0 && + album_covers_requests_sent_.isEmpty() && + album_songs_received_ >= album_songs_requested_ + ) { + GetAlbumCovers(); + } + + FinishCheck(); + +} + +QString QobuzRequest::ParseSong(Song &song, const QJsonObject &json_obj, QString artist_id, QString album_id, QString album_artist, QString album, QUrl cover_url) { + + if ( + !json_obj.contains("id") || + !json_obj.contains("title") || + !json_obj.contains("track_number") || + !json_obj.contains("duration") || + !json_obj.contains("copyright") || + !json_obj.contains("streamable") + ) { + Error("Invalid Json reply, track is missing one or more values.", json_obj); + return QString(); + } + + QString song_id; + if (json_obj["id"].isString()) { + song_id = json_obj["id"].toString(); + } + else { + song_id = QString::number(json_obj["id"].toInt()); + } + + QString title = json_obj["title"].toString(); + int track = json_obj["track_number"].toInt(); + QString copyright = json_obj["copyright"].toString(); + quint64 duration = json_obj["duration"].toInt() * kNsecPerSec; + //bool streamable = json_obj["streamable"].toBool(); + QString composer; + QString performer; + + if (json_obj.contains("album")) { + + QJsonValue value_album = json_obj["album"]; + if (!value_album.isObject()) { + Error("Invalid Json reply, album is not an object.", value_album); + return QString(); + } + QJsonObject obj_album = value_album.toObject(); + + if (obj_album.contains("id")) { + if (obj_album["id"].isString()) { + album_id = obj_album["id"].toString(); + } + else { + album_id = QString::number(obj_album["id"].toInt()); + } + } + + if (obj_album.contains("title")) { + album = obj_album["title"].toString(); + } + + if (obj_album.contains("artist")) { + QJsonValue value_artist = obj_album["artist"]; + if (!value_artist.isObject()) { + Error("Invalid Json reply, album artist is not a object.", value_artist); + return QString(); + } + QJsonObject obj_artist = value_artist.toObject(); + if (!obj_artist.contains("id") || !obj_artist.contains("name")) { + Error("Invalid Json reply, album artist is missing id or name.", obj_artist); + return QString(); + } + if (obj_artist["id"].isString()) { + artist_id = obj_artist["id"].toString(); + } + else { + artist_id = QString::number(obj_artist["id"].toInt()); + } + album_artist = obj_artist["name"].toString(); + } + + if (obj_album.contains("image")) { + QJsonValue value_image = obj_album["image"]; + if (!value_image.isObject()) { + Error("Invalid Json reply, album image is not a object.", value_image); + return QString(); + } + QJsonObject obj_image = value_image.toObject(); + if (!obj_image.contains("large")) { + Error("Invalid Json reply, album image is missing large.", obj_image); + return QString(); + } + QString album_image = obj_image["large"].toString(); + if (!album_image.isEmpty()) { + cover_url = QUrl(album_image); + } + } + } + + if (json_obj.contains("composer")) { + QJsonValue value_composer = json_obj["composer"]; + if (!value_composer.isObject()) { + Error("Invalid Json reply, track composer is not a object.", value_composer); + return QString(); + } + QJsonObject obj_composer = value_composer.toObject(); + if (!obj_composer.contains("id") || !obj_composer.contains("name")) { + Error("Invalid Json reply, track composer is missing id or name.", obj_composer); + return QString(); + } + composer = obj_composer["name"].toString(); + } + + if (json_obj.contains("performer")) { + QJsonValue value_performer = json_obj["performer"]; + if (!value_performer.isObject()) { + Error("Invalid Json reply, track performer is not a object.", value_performer); + return QString(); + } + QJsonObject obj_performer = value_performer.toObject(); + if (!obj_performer.contains("id") || !obj_performer.contains("name")) { + Error("Invalid Json reply, track performer is missing id or name.", obj_performer); + return QString(); + } + performer = obj_performer["name"].toString(); + } + + //if (!streamable) { + //Warn(QString("Song %1 %2 %3 is not streamable").arg(album_artist).arg(album).arg(title)); + //} + + QUrl url; + url.setScheme(url_handler_->scheme()); + url.setPath(song_id); + + title.remove(Song::kTitleRemoveMisc); + + //qLog(Debug) << "id" << song_id << "track" << track << "title" << title << "album" << album << "album artist" << album_artist << cover_url << streamable << url; + + song.set_source(Song::Source_Qobuz); + song.set_song_id(song_id); + song.set_album_id(album_id); + song.set_artist_id(artist_id); + song.set_album(album); + song.set_artist(album_artist); + song.set_title(title); + song.set_track(track); + song.set_url(url); + song.set_length_nanosec(duration); + song.set_art_automatic(cover_url); + song.set_comment(copyright); + song.set_directory_id(0); + song.set_filetype(Song::FileType_Stream); + song.set_filesize(0); + song.set_mtime(0); + song.set_ctime(0); + song.set_valid(true); + + return song_id; + +} + +void QobuzRequest::GetAlbumCovers() { + + for (Song &song : songs_) { + AddAlbumCoverRequest(song); + } + FlushAlbumCoverRequests(); + + if (album_covers_requested_ == 1) emit UpdateStatus(query_id_, tr("Retrieving album cover for %1 album...").arg(album_covers_requested_)); + else emit UpdateStatus(query_id_, tr("Retrieving album covers for %1 albums...").arg(album_covers_requested_)); + emit ProgressSetMaximum(query_id_, album_covers_requested_); + emit UpdateProgress(query_id_, 0); + +} + +void QobuzRequest::AddAlbumCoverRequest(Song &song) { + + QUrl cover_url(song.art_automatic()); + if (!cover_url.isValid()) return; + + if (album_covers_requests_sent_.contains(cover_url)) { + album_covers_requests_sent_.insert(cover_url, &song); + return; + } + + AlbumCoverRequest request; + request.url = cover_url; + request.filename = app_->album_cover_loader()->CoverFilePath(song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), cover_url); + if (request.filename.isEmpty()) return; + + album_covers_requests_sent_.insert(cover_url, &song); + ++album_covers_requested_; + + album_cover_requests_queue_.enqueue(request); + +} + +void QobuzRequest::FlushAlbumCoverRequests() { + + while (!album_cover_requests_queue_.isEmpty() && album_covers_requests_active_ < kMaxConcurrentAlbumCoverRequests) { + + AlbumCoverRequest request = album_cover_requests_queue_.dequeue(); + ++album_covers_requests_active_; + + QNetworkRequest req(request.url); +#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) + req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); +#else + req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); +#endif + QNetworkReply *reply = network_->get(req); + album_cover_replies_ << reply; + connect(reply, &QNetworkReply::finished, [=] { AlbumCoverReceived(reply, request.url, request.filename); }); + + } + +} + +void QobuzRequest::AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_url, const QString &filename) { + + if (album_cover_replies_.contains(reply)) { + album_cover_replies_.removeAll(reply); + reply->deleteLater(); + } + else { + AlbumCoverFinishCheck(); + return; + } + + --album_covers_requests_active_; + ++album_covers_received_; + + if (finished_) return; + + emit UpdateProgress(query_id_, album_covers_received_); + + if (!album_covers_requests_sent_.contains(cover_url)) { + AlbumCoverFinishCheck(); + return; + } + + if (reply->error() != QNetworkReply::NoError) { + Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + album_covers_requests_sent_.remove(cover_url); + AlbumCoverFinishCheck(); + return; + } + + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { + Error(QString("Received HTTP code %1 for %2.").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()).arg(cover_url.toString())); + if (album_covers_requests_sent_.contains(cover_url)) album_covers_requests_sent_.remove(cover_url); + AlbumCoverFinishCheck(); + return; + } + + QString mimetype = reply->header(QNetworkRequest::ContentTypeHeader).toString(); + if (!QImageReader::supportedMimeTypes().contains(mimetype.toUtf8())) { + Error(QString("Unsupported mimetype for image reader %1 for %2").arg(mimetype).arg(cover_url.toString())); + if (album_covers_requests_sent_.contains(cover_url)) album_covers_requests_sent_.remove(cover_url); + AlbumCoverFinishCheck(); + return; + } + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) + QList format_list = QImageReader::imageFormatsForMimeType(mimetype.toUtf8()); +#else + QList format_list = Utilities::ImageFormatsForMimeType(mimetype.toUtf8()); +#endif + + QByteArray data = reply->readAll(); + if (format_list.isEmpty() || data.isEmpty()) { + Error(QString("Received empty image data for %1").arg(cover_url.toString())); + album_covers_requests_sent_.remove(cover_url); + AlbumCoverFinishCheck(); + return; + } + QByteArray format = format_list.first(); + + QImage image; + if (image.loadFromData(data, format)) { + if (image.save(filename, format)) { + while (album_covers_requests_sent_.contains(cover_url)) { + Song *song = album_covers_requests_sent_.take(cover_url); + song->set_art_automatic(QUrl::fromLocalFile(filename)); + } + } + + } + else { + album_covers_requests_sent_.remove(cover_url); + Error(QString("Error decoding image data from %1").arg(cover_url.toString())); + } + + AlbumCoverFinishCheck(); + +} + +void QobuzRequest::AlbumCoverFinishCheck() { + + if (!album_cover_requests_queue_.isEmpty() && album_covers_requests_active_ < kMaxConcurrentAlbumCoverRequests) + FlushAlbumCoverRequests(); + + FinishCheck(); + +} + +void QobuzRequest::FinishCheck() { + + if ( + !finished_ && + albums_requests_queue_.isEmpty() && + artists_requests_queue_.isEmpty() && + songs_requests_queue_.isEmpty() && + artist_albums_requests_queue_.isEmpty() && + album_songs_requests_queue_.isEmpty() && + album_cover_requests_queue_.isEmpty() && + artist_albums_requests_pending_.isEmpty() && + album_songs_requests_pending_.isEmpty() && + album_covers_requests_sent_.isEmpty() && + artists_requests_active_ <= 0 && + albums_requests_active_ <= 0 && + songs_requests_active_ <= 0 && + artist_albums_requests_active_ <= 0 && + artist_albums_received_ >= artist_albums_requested_ && + album_songs_requests_active_ <= 0 && + album_songs_received_ >= album_songs_requested_ && + album_covers_requested_ <= album_covers_received_ && + album_covers_requests_active_ <= 0 && + album_covers_received_ >= album_covers_requested_ + ) { + finished_ = true; + if (no_results_ && songs_.isEmpty()) { + if (IsSearch()) + emit Results(query_id_, SongList(), tr("No match.")); + else + emit Results(query_id_, SongList(), QString()); + } + else { + if (songs_.isEmpty() && errors_.isEmpty()) + emit Results(query_id_, songs_, tr("Unknown error")); + else + emit Results(query_id_, songs_, ErrorsToHTML(errors_)); + } + } + +} + +void QobuzRequest::Error(const QString &error, const QVariant &debug) { + + if (!error.isEmpty()) { + errors_ << error; + qLog(Error) << "Qobuz:" << error; + } + if (debug.isValid()) qLog(Debug) << debug; + FinishCheck(); + +} + +void QobuzRequest::Warn(const QString &error, const QVariant &debug) { + + qLog(Error) << "Qobuz:" << error; + if (debug.isValid()) qLog(Debug) << debug; + +} + diff --git a/src/qobuz/qobuzrequest.h b/src/qobuz/qobuzrequest.h new file mode 100644 index 00000000..1aeff0b1 --- /dev/null +++ b/src/qobuz/qobuzrequest.h @@ -0,0 +1,205 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef QOBUZREQUEST_H +#define QOBUZREQUEST_H + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 artists_requests_queue_; + QQueue albums_requests_queue_; + QQueue songs_requests_queue_; + + QQueue artist_albums_requests_queue_; + QQueue album_songs_requests_queue_; + QQueue album_cover_requests_queue_; + + QList artist_albums_requests_pending_; + QHash album_songs_requests_pending_; + QMultiMap 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 replies_; + QList album_cover_replies_; + +}; + +#endif // QOBUZREQUEST_H diff --git a/src/qobuz/qobuzservice.cpp b/src/qobuz/qobuzservice.cpp new file mode 100644 index 00000000..81af2b98 --- /dev/null +++ b/src/qobuz/qobuzservice.cpp @@ -0,0 +1,761 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/application.h" +#include "core/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(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)), this, SLOT(HandleLoginSSLErrors(QList))); + connect(reply, &QNetworkReply::finished, [=] { HandleAuthReply(reply); }); + + qLog(Debug) << "Qobuz: Sending request" << url << query; + +} + +void QobuzService::HandleLoginSSLErrors(QList 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(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 + "
"; + } + if (debug.isValid()) qLog(Debug) << debug; + + emit LoginFailure(error_html); + emit LoginComplete(false, error_html); + + login_errors_.clear(); + +} diff --git a/src/qobuz/qobuzservice.h b/src/qobuz/qobuzservice.h new file mode 100644 index 00000000..b2ac1b86 --- /dev/null +++ b/src/qobuz/qobuzservice.h @@ -0,0 +1,231 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef QOBUZSERVICE_H +#define QOBUZSERVICE_H + +#include "config.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 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 Param; + typedef QList 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 artists_request_; + std::shared_ptr albums_request_; + std::shared_ptr songs_request_; + std::shared_ptr 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 stream_url_requests_; + + QStringList login_errors_; + + QList wait_for_exit_; + QList replies_; + +}; + +#endif // QOBUZSERVICE_H diff --git a/src/qobuz/qobuzstreamurlrequest.cpp b/src/qobuz/qobuzstreamurlrequest.cpp new file mode 100644 index 00000000..931070b5 --- /dev/null +++ b/src/qobuz/qobuzstreamurlrequest.cpp @@ -0,0 +1,249 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "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; + +} + diff --git a/src/qobuz/qobuzstreamurlrequest.h b/src/qobuz/qobuzstreamurlrequest.h new file mode 100644 index 00000000..14166cb2 --- /dev/null +++ b/src/qobuz/qobuzstreamurlrequest.h @@ -0,0 +1,76 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef QOBUZSTREAMURLREQUEST_H +#define QOBUZSTREAMURLREQUEST_H + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#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 diff --git a/src/qobuz/qobuzurlhandler.cpp b/src/qobuz/qobuzurlhandler.cpp new file mode 100644 index 00000000..c9086aea --- /dev/null +++ b/src/qobuz/qobuzurlhandler.cpp @@ -0,0 +1,68 @@ +/* + * Strawberry Music Player + * Copyright 2018, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include +#include + +#include "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; +} diff --git a/src/qobuz/qobuzurlhandler.h b/src/qobuz/qobuzurlhandler.h new file mode 100644 index 00000000..1e0b34ff --- /dev/null +++ b/src/qobuz/qobuzurlhandler.h @@ -0,0 +1,55 @@ +/* + * Strawberry Music Player + * Copyright 2018, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef QOBUZURLHANDLER_H +#define QOBUZURLHANDLER_H + +#include +#include +#include +#include + +#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 diff --git a/src/settings/coverssettingspage.cpp b/src/settings/coverssettingspage.cpp index 0cb5f08f..1f712463 100644 --- a/src/settings/coverssettingspage.cpp +++ b/src/settings/coverssettingspage.cpp @@ -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); diff --git a/src/settings/qobuzsettingspage.cpp b/src/settings/qobuzsettingspage.cpp new file mode 100644 index 00000000..db2e422c --- /dev/null +++ b/src/settings/qobuzsettingspage.cpp @@ -0,0 +1,171 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "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()) { + + 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); +} diff --git a/src/settings/qobuzsettingspage.h b/src/settings/qobuzsettingspage.h new file mode 100644 index 00000000..21db0a4f --- /dev/null +++ b/src/settings/qobuzsettingspage.h @@ -0,0 +1,62 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef QOBUZSETTINGSPAGE_H +#define QOBUZSETTINGSPAGE_H + +#include +#include + +#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 diff --git a/src/settings/qobuzsettingspage.ui b/src/settings/qobuzsettingspage.ui new file mode 100644 index 00000000..7c7b5571 --- /dev/null +++ b/src/settings/qobuzsettingspage.ui @@ -0,0 +1,306 @@ + + + QobuzSettingsPage + + + + 0 + 0 + 472 + 697 + + + + Qobuz + + + + + + Enable + + + + + + + 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. + + + true + + + 10 + + + + + + + + 0 + 0 + + + + Authentication + + + + + + + 150 + 0 + + + + App ID + + + + + + + + + + Username + + + + + + + + + + + + + + Password + + + + + + + QLineEdit::Password + + + + + + + App Secret + + + + + + + + + + + + + Login + + + + + + + + + + Preferences + + + + + + Audio format + + + + + + + + + + Search delay + + + + + + + ms + + + 0 + + + 10000 + + + 50 + + + 1500 + + + + + + + Artists search limit + + + + + + + 1 + + + 100 + + + 50 + + + + + + + Albums search limit + + + + + + + 1 + + + 1000 + + + 50 + + + + + + + Songs search limit + + + + + + + 1 + + + 1000 + + + 50 + + + + + + + Download album covers + + + + + + + + + + Qt::Vertical + + + + 20 + 30 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 64 + 64 + + + + + 64 + 64 + + + + :/icons/64x64/qobuz.png + + + + + + + + + + LoginStateWidget + QWidget +
widgets/loginstatewidget.h
+ 1 +
+
+ + enable + app_id + app_secret + username + password + button_login + format + searchdelay + artistssearchlimit + albumssearchlimit + songssearchlimit + checkbox_download_album_covers + + + + + + +
diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp index 935c051e..3deb582d 100644 --- a/src/settings/settingsdialog.cpp +++ b/src/settings/settingsdialog.cpp @@ -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*))); diff --git a/src/settings/settingsdialog.h b/src/settings/settingsdialog.h index 8b937d02..4892c6f7 100644 --- a/src/settings/settingsdialog.h +++ b/src/settings/settingsdialog.h @@ -90,6 +90,7 @@ class SettingsDialog : public QDialog { Page_Moodbar, Page_Subsonic, Page_Tidal, + Page_Qobuz, }; enum Role { diff --git a/src/smartplaylists/playlistgenerator.cpp b/src/smartplaylists/playlistgenerator.cpp new file mode 100644 index 00000000..e3e9f38a --- /dev/null +++ b/src/smartplaylists/playlistgenerator.cpp @@ -0,0 +1,43 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include + +#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); + +} diff --git a/src/smartplaylists/playlistgenerator.h b/src/smartplaylists/playlistgenerator.h new file mode 100644 index 00000000..a637a6bc --- /dev/null +++ b/src/smartplaylists/playlistgenerator.h @@ -0,0 +1,100 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef PLAYLISTGENERATOR_H +#define PLAYLISTGENERATOR_H + +#include "config.h" + +#include + +#include +#include +#include + +#include "playlist/playlistitem.h" + +class CollectionBackend; + +class PlaylistGenerator : public QObject, public std::enable_shared_from_this { + 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 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 diff --git a/src/smartplaylists/playlistgenerator_fwd.h b/src/smartplaylists/playlistgenerator_fwd.h new file mode 100644 index 00000000..bb3d58b1 --- /dev/null +++ b/src/smartplaylists/playlistgenerator_fwd.h @@ -0,0 +1,32 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef PLAYLISTGENERATOR_FWD_H +#define PLAYLISTGENERATOR_FWD_H + +#include "config.h" + +#include + +class PlaylistGenerator; + +typedef std::shared_ptr PlaylistGeneratorPtr; + +#endif // PLAYLISTGENERATOR_FWD_H diff --git a/src/smartplaylists/playlistgeneratorinserter.cpp b/src/smartplaylists/playlistgeneratorinserter.cpp new file mode 100644 index 00000000..12edac2a --- /dev/null +++ b/src/smartplaylists/playlistgeneratorinserter.cpp @@ -0,0 +1,90 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include + +#include "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 future = QtConcurrent::run(PlaylistGeneratorInserter::Generate, generator, dynamic_count); + NewClosure(future, this, SLOT(Finished(QFuture)), future); + +} + +void PlaylistGeneratorInserter::Finished(QFuture 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(); + +} diff --git a/src/smartplaylists/playlistgeneratorinserter.h b/src/smartplaylists/playlistgeneratorinserter.h new file mode 100644 index 00000000..fdcc402b --- /dev/null +++ b/src/smartplaylists/playlistgeneratorinserter.h @@ -0,0 +1,71 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef PLAYLISTGENERATORINSERTER_H +#define PLAYLISTGENERATORINSERTER_H + +#include "config.h" + +#include +#include +#include + +#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 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 diff --git a/src/smartplaylists/playlistgeneratormimedata.h b/src/smartplaylists/playlistgeneratormimedata.h new file mode 100644 index 00000000..5139610d --- /dev/null +++ b/src/smartplaylists/playlistgeneratormimedata.h @@ -0,0 +1,41 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef PLAYLISTGENERATORMIMEDATA_H +#define PLAYLISTGENERATORMIMEDATA_H + +#include "config.h" + +#include +#include + +#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 diff --git a/src/smartplaylists/playlistquerygenerator.cpp b/src/smartplaylists/playlistquerygenerator.cpp new file mode 100644 index 00000000..93a8b25c --- /dev/null +++ b/src/smartplaylists/playlistquerygenerator.cpp @@ -0,0 +1,101 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include + +#include "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; + +} diff --git a/src/smartplaylists/playlistquerygenerator.h b/src/smartplaylists/playlistquerygenerator.h new file mode 100644 index 00000000..6709d431 --- /dev/null +++ b/src/smartplaylists/playlistquerygenerator.h @@ -0,0 +1,61 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef PLAYLISTQUERYGENERATOR_H +#define PLAYLISTQUERYGENERATOR_H + +#include "config.h" + +#include +#include +#include + +#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 previous_ids_; + int current_pos_; + +}; + +#endif // PLAYLISTQUERYGENERATOR_H diff --git a/src/smartplaylists/smartplaylistquerysearchpage.ui b/src/smartplaylists/smartplaylistquerysearchpage.ui new file mode 100644 index 00000000..2fb9b0a8 --- /dev/null +++ b/src/smartplaylists/smartplaylistquerysearchpage.ui @@ -0,0 +1,134 @@ + + + SmartPlaylistQuerySearchPage + + + + 0 + 0 + 448 + 450 + + + + + 0 + 0 + + + + Form + + + #terms_scroll_area, #terms_scroll_area_content { + background: transparent; +} + + + + 0 + + + + + Search mode + + + + + + + Match every search term (AND) + + + + + Match one or more search terms (OR) + + + + + Include all songs + + + + + + + + + + + + 0 + 0 + + + + + 0 + 300 + + + + Search terms + + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + true + + + + + 0 + 0 + 418 + 251 + + + + + 0 + 0 + + + + + 0 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + diff --git a/src/smartplaylists/smartplaylistquerysortpage.ui b/src/smartplaylists/smartplaylistquerysortpage.ui new file mode 100644 index 00000000..974da600 --- /dev/null +++ b/src/smartplaylists/smartplaylistquerysortpage.ui @@ -0,0 +1,130 @@ + + + SmartPlaylistQuerySortPage + + + + 0 + 0 + 723 + 335 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Sorting + + + + + + Put songs in a random order + + + true + + + + + + + Sort songs by + + + + + + + + + + + + QComboBox::AdjustToContents + + + + + + + + + + + + Limits + + + + + + Show all the songs + + + true + + + + + + + Only show the first + + + + + + + songs + + + 1000 + + + 15 + + + + + + + + + + Qt::Horizontal + + + + + + + + + + + SmartPlaylistSearchPreview + QWidget +
smartplaylists/smartplaylistsearchpreview.h
+ 1 +
+
+ + +
diff --git a/src/smartplaylists/smartplaylistquerywizardplugin.cpp b/src/smartplaylists/smartplaylistquerywizardplugin.cpp new file mode 100644 index 00000000..d8f4fe95 --- /dev/null +++ b/src/smartplaylists/smartplaylistquerywizardplugin.cpp @@ -0,0 +1,327 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "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 terms_; + SmartPlaylistSearchTermWidget *new_term_; + + SmartPlaylistSearchPreview *preview_; + + std::unique_ptr 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(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(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 gen = std::dynamic_pointer_cast(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 gen(new PlaylistQueryGenerator); + gen->Load(MakeSearch()); + + return std::static_pointer_cast(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(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; + +} diff --git a/src/smartplaylists/smartplaylistquerywizardplugin.h b/src/smartplaylists/smartplaylistquerywizardplugin.h new file mode 100644 index 00000000..fb676003 --- /dev/null +++ b/src/smartplaylists/smartplaylistquerywizardplugin.h @@ -0,0 +1,80 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef SMARTPLAYLISTQUERYWIZARDPLUGIN_H +#define SMARTPLAYLISTQUERYWIZARDPLUGIN_H + +#include "config.h" + +#include + +#include +#include + +#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 sort_ui_; + + int previous_scrollarea_max_; +}; + +#endif // SMARTPLAYLISTQUERYWIZARDPLUGIN_H diff --git a/src/smartplaylists/smartplaylistsearch.cpp b/src/smartplaylists/smartplaylistsearch.cpp new file mode 100644 index 00000000..f686c98e --- /dev/null +++ b/src/smartplaylists/smartplaylistsearch.cpp @@ -0,0 +1,148 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include + +#include "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; + +} diff --git a/src/smartplaylists/smartplaylistsearch.h b/src/smartplaylists/smartplaylistsearch.h new file mode 100644 index 00000000..cec1fbe5 --- /dev/null +++ b/src/smartplaylists/smartplaylistsearch.h @@ -0,0 +1,69 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef SMARTPLAYLISTSEARCH_H +#define SMARTPLAYLISTSEARCH_H + +#include "config.h" + +#include +#include +#include + +#include "playlistgenerator.h" +#include "smartplaylistsearchterm.h" + +class SmartPlaylistSearch { + + public: + typedef QList 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 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 diff --git a/src/smartplaylists/smartplaylistsearchpreview.cpp b/src/smartplaylists/smartplaylistsearchpreview.cpp new file mode 100644 index 00000000..35b7247e --- /dev/null +++ b/src/smartplaylists/smartplaylistsearchpreview.cpp @@ -0,0 +1,151 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include + +#include +#include +#include +#include + +#include "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(generator_)->Load(search); + + ui_->busy_container->show(); + ui_->count_label->hide(); + QFuture future = QtConcurrent::run(DoRunSearch, generator_); + NewClosure(future, this, SLOT(SearchFinished(QFuture)), future); + +} + +void SmartPlaylistSearchPreview::SearchFinished(QFuture future) { + + last_search_ = std::dynamic_pointer_cast(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(); + +} diff --git a/src/smartplaylists/smartplaylistsearchpreview.h b/src/smartplaylists/smartplaylistsearchpreview.h new file mode 100644 index 00000000..1f7af709 --- /dev/null +++ b/src/smartplaylists/smartplaylistsearchpreview.h @@ -0,0 +1,74 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef SMARTPLAYLISTSEARCHPREVIEW_H +#define SMARTPLAYLISTSEARCHPREVIEW_H + +#include "config.h" + +#include +#include +#include + +#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 future); + + private: + Ui_SmartPlaylistSearchPreview *ui_; + QList fields_; + + CollectionBackend *backend_; + Playlist *model_; + + SmartPlaylistSearch pending_search_; + SmartPlaylistSearch last_search_; + PlaylistGeneratorPtr generator_; + +}; + +#endif // SMARTPLAYLISTSEARCHPREVIEW_H diff --git a/src/smartplaylists/smartplaylistsearchpreview.ui b/src/smartplaylists/smartplaylistsearchpreview.ui new file mode 100644 index 00000000..4f8147c6 --- /dev/null +++ b/src/smartplaylists/smartplaylistsearchpreview.ui @@ -0,0 +1,99 @@ + + + SmartPlaylistSearchPreview + + + + 0 + 0 + 651 + 377 + + + + Form + + + + 0 + + + + + + + Preview + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 0 + + + + + + + + Loading... + + + + + + + + + + + + Qt::ScrollBarAlwaysOn + + + false + + + false + + + true + + + + + + + + BusyIndicator + QWidget +
widgets/busyindicator.h
+ 1 +
+ + PlaylistView + QTreeView +
playlist/playlistview.h
+
+
+ + +
diff --git a/src/smartplaylists/smartplaylistsearchterm.cpp b/src/smartplaylists/smartplaylistsearchterm.cpp new file mode 100644 index 00000000..cc0f0589 --- /dev/null +++ b/src/smartplaylists/smartplaylistsearchterm.cpp @@ -0,0 +1,466 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "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; + +} diff --git a/src/smartplaylists/smartplaylistsearchterm.h b/src/smartplaylists/smartplaylistsearchterm.h new file mode 100644 index 00000000..4b65355c --- /dev/null +++ b/src/smartplaylists/smartplaylistsearchterm.h @@ -0,0 +1,141 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef SMARTPLAYLISTSEARCHTERM_H +#define SMARTPLAYLISTSEARCHTERM_H + +#include "config.h" + +#include +#include +#include +#include + +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 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 OperatorList; + +QDataStream &operator<<(QDataStream &s, const SmartPlaylistSearchTerm &term); +QDataStream &operator>>(QDataStream &s, SmartPlaylistSearchTerm &term); + +#endif // SMARTPLAYLISTSEARCHTERM_H diff --git a/src/smartplaylists/smartplaylistsearchtermwidget.cpp b/src/smartplaylists/smartplaylistsearchtermwidget.cpp new file mode 100644 index 00000000..27d106fa --- /dev/null +++ b/src/smartplaylists/smartplaylistsearchtermwidget.cpp @@ -0,0 +1,511 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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( + 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( + // 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(); +} diff --git a/src/smartplaylists/smartplaylistsearchtermwidget.h b/src/smartplaylists/smartplaylistsearchtermwidget.h new file mode 100644 index 00000000..3bd192fb --- /dev/null +++ b/src/smartplaylists/smartplaylistsearchtermwidget.h @@ -0,0 +1,94 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef SMARTPLAYLISTSEARCHTERMWIDGET_H +#define SMARTPLAYLISTSEARCHTERMWIDGET_H + +#include "config.h" + +#include +#include + +#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 diff --git a/src/smartplaylists/smartplaylistsearchtermwidget.ui b/src/smartplaylists/smartplaylistsearchtermwidget.ui new file mode 100644 index 00000000..6c6f2422 --- /dev/null +++ b/src/smartplaylists/smartplaylistsearchtermwidget.ui @@ -0,0 +1,350 @@ + + + SmartPlaylistSearchTermWidget + + + + 0 + 0 + 640 + 38 + + + + + 0 + 0 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + mm:ss + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 1000000 + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + + + + + + 0 + + + 0 + + + + + 1 + + + 999 + + + + + + + + + + + + 0 + + + 0 + + + + + 0 + + + 999 + + + 1 + + + + + + + + 0 + 0 + + + + and + + + Qt::AlignCenter + + + + + + + 1 + + + 999 + + + 2 + + + + + + + + + + + 0 + 0 + + + + ago + + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + RatingWidget + QWidget +
widgets/ratingwidget.h
+ 1 +
+
+ + +
diff --git a/src/smartplaylists/smartplaylistsitem.h b/src/smartplaylists/smartplaylistsitem.h new file mode 100644 index 00000000..b20daee0 --- /dev/null +++ b/src/smartplaylists/smartplaylistsitem.h @@ -0,0 +1,45 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef SMARTPLAYLISTSITEM_H +#define SMARTPLAYLISTSITEM_H + +#include "config.h" + +#include + +#include "core/simpletreeitem.h" +#include "playlistgenerator.h" + +class SmartPlaylistsItem : public SimpleTreeItem { + public: + enum Type { + Type_Root, + Type_SmartPlaylist, + }; + + SmartPlaylistsItem(SimpleTreeModel *_model) : SimpleTreeItem(Type_Root, _model) {} + SmartPlaylistsItem(const Type _type, SmartPlaylistsItem *_parent = nullptr) : SimpleTreeItem(_type, _parent) {} + + PlaylistGenerator::Type smart_playlist_type; + QByteArray smart_playlist_data; +}; + +#endif // SMARTPLAYLISTSITEM_H diff --git a/src/smartplaylists/smartplaylistsmodel.cpp b/src/smartplaylists/smartplaylistsmodel.cpp new file mode 100644 index 00000000..9ed1a367 --- /dev/null +++ b/src/smartplaylists/smartplaylistsmodel.cpp @@ -0,0 +1,312 @@ +/* + * Strawberry Music Player + * This code was part of Clementine. + * Copyright 2010, David Sansome + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include "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(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; + +} diff --git a/src/smartplaylists/smartplaylistsmodel.h b/src/smartplaylists/smartplaylistsmodel.h new file mode 100644 index 00000000..118fb2ed --- /dev/null +++ b/src/smartplaylists/smartplaylistsmodel.h @@ -0,0 +1,94 @@ +/* + * Strawberry Music Player + * This code was part of Clementine. + * Copyright 2010, David Sansome + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef SMARTPLAYLISTSMODEL_H +#define SMARTPLAYLISTSMODEL_H + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include "core/simpletreemodel.h" +#include "smartplaylistsitem.h" +#include "playlistgenerator_fwd.h" + +class Application; +class CollectionBackend; + +class QModelIndex; +class QMimeData; + +class SmartPlaylistsModel : public SimpleTreeModel { + 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 GeneratorList; + typedef QList 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 items_; + +}; + +#endif // SMARTPLAYLISTSMODEL_H diff --git a/src/smartplaylists/smartplaylistsview.cpp b/src/smartplaylists/smartplaylistsview.cpp new file mode 100644 index 00000000..4e40f46c --- /dev/null +++ b/src/smartplaylists/smartplaylistsview.cpp @@ -0,0 +1,53 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include + +#include "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(); + +} diff --git a/src/smartplaylists/smartplaylistsview.h b/src/smartplaylists/smartplaylistsview.h new file mode 100644 index 00000000..a8a1efec --- /dev/null +++ b/src/smartplaylists/smartplaylistsview.h @@ -0,0 +1,47 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef SMARTPLAYLISTSVIEW_H +#define SMARTPLAYLISTSVIEW_H + +#include "config.h" + +#include +#include +#include +#include + +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 diff --git a/src/smartplaylists/smartplaylistsviewcontainer.cpp b/src/smartplaylists/smartplaylistsviewcontainer.cpp new file mode 100644 index 00000000..47db7708 --- /dev/null +++ b/src/smartplaylists/smartplaylistsviewcontainer.cpp @@ -0,0 +1,283 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "core/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() << 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(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(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(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(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(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(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(q_mimedata)) { + mimedata->from_doubleclick_ = true; + } + emit AddToPlaylist(q_mimedata); + +} diff --git a/src/smartplaylists/smartplaylistsviewcontainer.h b/src/smartplaylists/smartplaylistsviewcontainer.h new file mode 100644 index 00000000..03e47576 --- /dev/null +++ b/src/smartplaylists/smartplaylistsviewcontainer.h @@ -0,0 +1,101 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef SMARTPLAYLISTSVIEWCONTAINER_H +#define SMARTPLAYLISTSVIEWCONTAINER_H + +#include "config.h" + +#include +#include + +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 diff --git a/src/smartplaylists/smartplaylistsviewcontainer.ui b/src/smartplaylists/smartplaylistsviewcontainer.ui new file mode 100644 index 00000000..93b0d7a1 --- /dev/null +++ b/src/smartplaylists/smartplaylistsviewcontainer.ui @@ -0,0 +1,136 @@ + + + SmartPlaylistsViewContainer + + + + 0 + 0 + 415 + 495 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + New smart playlist + + + + 22 + 22 + + + + + + + + Edit smart playlist + + + + 22 + 22 + + + + + + + + Delete smart playlist + + + + 22 + 22 + + + + + + + + Qt::Horizontal + + + + 70 + 20 + + + + + + + + + + + + 0 + 0 + + + + + + + + + SmartPlaylistsView + QWidget +
smartplaylists/smartplaylistsview.h
+ 1 +
+
+ + +
diff --git a/src/smartplaylists/smartplaylistwizard.cpp b/src/smartplaylists/smartplaylistwizard.cpp new file mode 100644 index 00000000..a373c9f0 --- /dev/null +++ b/src/smartplaylists/smartplaylistwizard.cpp @@ -0,0 +1,179 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "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); + +} diff --git a/src/smartplaylists/smartplaylistwizard.h b/src/smartplaylists/smartplaylistwizard.h new file mode 100644 index 00000000..99bc6b60 --- /dev/null +++ b/src/smartplaylists/smartplaylistwizard.h @@ -0,0 +1,68 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef SMARTPLAYLISTWIZARD_H +#define SMARTPLAYLISTWIZARD_H + +#include "config.h" + +#include + +#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 plugins_; + +}; + +#endif // SMARTPLAYLISTWIZARD_H diff --git a/src/smartplaylists/smartplaylistwizardfinishpage.ui b/src/smartplaylists/smartplaylistwizardfinishpage.ui new file mode 100644 index 00000000..cafdaba3 --- /dev/null +++ b/src/smartplaylists/smartplaylistwizardfinishpage.ui @@ -0,0 +1,69 @@ + + + SmartPlaylistWizardFinishPage + + + + 0 + 0 + 583 + 370 + + + + Form + + + + + + Name + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Use dynamic mode + + + + + + + In dynamic mode new tracks will be chosen and added to the playlist every time a song finishes. + + + true + + + 24 + + + + + + + + + + + diff --git a/src/smartplaylists/smartplaylistwizardplugin.cpp b/src/smartplaylists/smartplaylistwizardplugin.cpp new file mode 100644 index 00000000..f2c95cac --- /dev/null +++ b/src/smartplaylists/smartplaylistwizardplugin.cpp @@ -0,0 +1,32 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include + +#include "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); +} diff --git a/src/smartplaylists/smartplaylistwizardplugin.h b/src/smartplaylists/smartplaylistwizardplugin.h new file mode 100644 index 00000000..e5b233c2 --- /dev/null +++ b/src/smartplaylists/smartplaylistwizardplugin.h @@ -0,0 +1,60 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef SMARTPLAYLISTWIZARDPLUGIN_H +#define SMARTPLAYLISTWIZARDPLUGIN_H + +#include + +#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 diff --git a/src/widgets/ratingwidget.cpp b/src/widgets/ratingwidget.cpp new file mode 100644 index 00000000..8d8816b9 --- /dev/null +++ b/src/widgets/ratingwidget.cpp @@ -0,0 +1,164 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "ratingwidget.h" + +#include +#include +#include +#include +#include +#include + +const int RatingPainter::kStarCount; +const int RatingPainter::kStarSize; + +RatingPainter::RatingPainter() { + + // Load the base pixmaps + QIcon star_on(":/pictures/star-on.png"); + QPixmap on(star_on.pixmap(star_on.availableSizes().last())); + QIcon star_off(":/pictures/star-off.png"); + QPixmap off(star_off.pixmap(star_off.availableSizes().last())); + + // Generate the 10 states, better to do it now than on the fly + for (int i = 0 ; i < kStarCount * 2 + 1 ; ++i) { + const float rating = float(i) / 2.0; + + // Clear the pixmap + stars_[i] = QPixmap(kStarSize * kStarCount, kStarSize); + stars_[i].fill(Qt::transparent); + QPainter p(&stars_[i]); + + // Draw the stars + int x = 0; + for (int y = 0 ; y < kStarCount ; ++y, x += kStarSize) { + const QRect rect(x, 0, kStarSize, kStarSize); + + if (rating - 0.25 <= y) { // Totally empty + p.drawPixmap(rect, off); + } + else if (rating - 0.75 <= y) { // Half full + const QRect target_left(rect.x(), rect.y(), kStarSize / 2, kStarSize); + const QRect target_right(rect.x() + kStarSize / 2, rect.y(), kStarSize / 2, kStarSize); + const QRect source_left(0, 0, kStarSize / 2, kStarSize); + const QRect source_right(kStarSize / 2, 0, kStarSize / 2, kStarSize); + p.drawPixmap(target_left, on, source_left); + p.drawPixmap(target_right, off, source_right); + } + else { // Totally full + p.drawPixmap(rect, on); + } + } + } +} + +QRect RatingPainter::Contents(const QRect &rect) { + + const int width = kStarSize * kStarCount; + const int x = rect.x() + (rect.width() - width) / 2; + + return QRect(x, rect.y(), width, rect.height()); + +} + +double RatingPainter::RatingForPos(const QPoint &pos, const QRect &rect) { + + const QRect contents = Contents(rect); + const double raw = double(pos.x() - contents.left()) / contents.width(); + + // Round to the nearest 0.1 + return double(int(raw * kStarCount * 2 + 0.5)) / (kStarCount * 2); + +} + +void RatingPainter::Paint(QPainter* painter, const QRect &rect, float rating) const { + + QSize size(qMin(kStarSize * kStarCount, rect.width()), qMin(kStarSize, rect.height())); + QPoint pos(rect.center() - QPoint(size.width() / 2, size.height() / 2)); + + rating *= kStarCount; + + // Draw the stars + const int star = qBound(0, int(rating * 2.0 + 0.5), kStarCount * 2); + painter->drawPixmap(QRect(pos, size), stars_[star], QRect(QPoint(0, 0), size)); + +} + +RatingWidget::RatingWidget(QWidget *parent) : QWidget(parent), rating_(0.0), hover_rating_(-1.0) { + + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setMouseTracking(true); + +} + +QSize RatingWidget::sizeHint() const { + + const int frame_width = 1 + style()->pixelMetric(QStyle::PM_DefaultFrameWidth); + return QSize(RatingPainter::kStarSize * (RatingPainter::kStarCount + 2) + frame_width * 2, RatingPainter::kStarSize + frame_width * 2); + +} + +void RatingWidget::set_rating(const float rating) { + + rating_ = rating; + update(); + +} + +void RatingWidget::paintEvent(QPaintEvent*) { + + QStylePainter p(this); + + // Draw the background + QStyleOptionFrame opt; + opt.initFrom(this); + opt.state |= QStyle::State_Sunken; + opt.frameShape = QFrame::StyledPanel; + opt.lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, &opt, this); + opt.midLineWidth = 0; + + p.drawPrimitive(QStyle::PE_PanelLineEdit, opt); + + // Draw the stars + painter_.Paint(&p, rect(), hover_rating_ == -1.0 ? rating_ : hover_rating_); + +} + +void RatingWidget::mousePressEvent(QMouseEvent *e) { + + rating_ = RatingPainter::RatingForPos(e->pos(), rect()); + emit RatingChanged(rating_); + +} + +void RatingWidget::mouseMoveEvent(QMouseEvent *e) { + + hover_rating_ = RatingPainter::RatingForPos(e->pos(), rect()); + update(); + +} + +void RatingWidget::leaveEvent(QEvent*) { + + hover_rating_ = -1.0; + update(); + +} diff --git a/src/widgets/ratingwidget.h b/src/widgets/ratingwidget.h new file mode 100644 index 00000000..66870638 --- /dev/null +++ b/src/widgets/ratingwidget.h @@ -0,0 +1,71 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef RATINGWIDGET_H +#define RATINGWIDGET_H + +#include +#include +#include +#include + +class RatingPainter { + public: + RatingPainter(); + + static const int kStarCount = 5; + static const int kStarSize = 16; + static QRect Contents(const QRect &rect); + static double RatingForPos(const QPoint &pos, const QRect &rect); + + void Paint(QPainter *painter, const QRect &rect, float rating) const; + + private: + QPixmap stars_[kStarCount * 2 + 1]; +}; + +class RatingWidget : public QWidget { + Q_OBJECT + Q_PROPERTY(float rating READ rating WRITE set_rating) + + public: + RatingWidget(QWidget *parent = nullptr); + + QSize sizeHint() const override; + + float rating() const { return rating_; } + void set_rating(const float rating); + + signals: + void RatingChanged(float); + + protected: + void paintEvent(QPaintEvent*) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void leaveEvent(QEvent*) override; + + private: + RatingPainter painter_; + float rating_; + float hover_rating_; +}; + +#endif // RATINGWIDGET_H