diff --git a/emmental/db/emmental.sql b/emmental/db/emmental.sql index 11e6684..cda7f01 100644 --- a/emmental/db/emmental.sql +++ b/emmental/db/emmental.sql @@ -60,14 +60,23 @@ CREATE TABLE playlists ( ); CREATE VIEW playlists_view AS - SELECT playlistid, propertyid, name, image, active + SELECT playlistid, propertyid, name, image, + active, loop, shuffle, sort_order, current_trackid FROM playlists JOIN playlist_properties USING (propertyid); CREATE TRIGGER playlists_insert_trigger AFTER INSERT ON playlists BEGIN - INSERT INTO playlist_properties (active) - VALUES (NEW.name == "Collection"); + INSERT INTO playlist_properties (active, loop, sort_order) + VALUES (NEW.name == "Collection", + IIF(NEW.name == "Collection", "Playlist", "None"), + CASE + WHEN NEW.name == "Most Played Tracks" + THEN "playcount DESC, albumartist, album, mediumno, number" + WHEN NEW.name == "Previous Tracks" + THEN "laststarted DESC" + ELSE "albumartist, album, mediumno, number" + END); UPDATE playlists SET propertyid = last_insert_rowid() WHERE playlistid = NEW.playlistid; END; @@ -77,6 +86,42 @@ CREATE TRIGGER playlists_delete_trigger AFTER DELETE ON playlists DELETE FROM playlist_properties WHERE propertyid = OLD.propertyid; END; +CREATE TRIGGER collection_loop_trigger + BEFORE UPDATE OF loop ON playlist_properties + WHEN NEW.loop == "None" AND NEW.propertyid == (SELECT propertyid + FROM playlists + WHERE name='Collection') + BEGIN + SELECT RAISE(ABORT, "Collection playlist cannot disable loop"); + END; + +CREATE TRIGGER previous_loop_trigger + BEFORE UPDATE OF loop ON playlist_properties + WHEN NEW.loop != "None" AND NEW.propertyid == (SELECT propertyid + FROM playlists + WHERE name='Previous Tracks') + BEGIN + SELECT RAISE(ABORT, "Previous Tracks cannot be looped"); + END; + +CREATE TRIGGER previous_shuffle_trigger + BEFORE UPDATE OF shuffle ON playlist_properties + WHEN NEW.shuffle = TRUE AND NEW.propertyid == (SELECT propertyid + FROM playlists + WHERE name='Previous Tracks') + BEGIN + SELECT RAISE(ABORT, "Previous Tracks cannot be shuffled"); + END; + +CREATE TRIGGER previous_sort_order_trigger + BEFORE UPDATE OF sort_order ON playlist_properties + WHEN NEW.sort_order != "laststarted DESC" AND NEW.propertyid == (SELECT propertyid + FROM playlists + WHERE name='Previous Tracks') + BEGIN + SELECT RAISE(ABORT, "Previous Tracks cannot be sorted"); + END; + /************************* * * diff --git a/emmental/db/playlists.py b/emmental/db/playlists.py index 166ef72..e84a5a3 100644 --- a/emmental/db/playlists.py +++ b/emmental/db/playlists.py @@ -11,6 +11,21 @@ class Playlist(playlist.Playlist): playlistid = GObject.Property(type=int) image = GObject.Property(type=GObject.TYPE_PYOBJECT) + def do_update(self, column: str) -> None: + """Update a playlist object.""" + match (self.name, column, self.get_property(column)): + case ("Collection", "loop", "None"): + self.loop = "Playlist" + case ("Previous Tracks", "loop", "Playlist") | \ + ("Previous Tracks", "loop", "Track"): + self.loop = "None" + case ("Previous Tracks", "shuffle", True): + self.shuffle = False + case ("Previous Tracks", "sort-order", _): + if self.sort_order != "laststarted DESC": + self.sort_order = "laststarted DESC" + case (_, _, _): super().do_update(column) + def rename(self, new_name: str) -> bool: """Rename this playlist.""" return self.table.rename(self, new_name) @@ -32,16 +47,28 @@ class Table(playlist.Table): queued = GObject.Property(type=Playlist) unplayed = GObject.Property(type=Playlist) + def __init__(self, sql: GObject.TYPE_PYOBJECT, **kwargs): + """Initialize the Playlists Table.""" + super().__init__(sql=sql, **kwargs) + def do_construct(self, **kwargs) -> Playlist: """Construct a new playlist.""" match (plist := Playlist(**kwargs)).name: case "Collection": self.collection = plist - case "Favorite Tracks": self.favorites = plist + case "Favorite Tracks": + self.favorites = plist + self.favorites.user_tracks = True case "Most Played Tracks": self.most_played = plist case "New Tracks": self.new_tracks = plist case "Previous Tracks": self.previous = plist - case "Queued Tracks": self.queued = plist + case "Queued Tracks": + self.queued = plist + self.queued.user_tracks = True + self.queued.tracks_movable = True case "Unplayed Tracks": self.unplayed = plist + case _: + plist.user_tracks = True + plist.tracks_movable = True return plist def do_sql_delete(self, playlist: Playlist) -> sqlite3.Cursor: diff --git a/tests/db/test_playlists.py b/tests/db/test_playlists.py index 4917207..08bf7ee 100644 --- a/tests/db/test_playlists.py +++ b/tests/db/test_playlists.py @@ -54,9 +54,19 @@ class TestPlaylistTable(tests.util.TestCase): self.sql("DELETE FROM playlists") self.table = self.sql.playlists + self.library = self.sql.libraries.create(pathlib.Path("/a/b")) + self.album = self.sql.albums.create("Test Album", "Artist", "2023-04") + self.medium = self.sql.media.create(self.album, "", number=1) + self.year = self.sql.years.create(2023) + + self.track = self.sql.tracks.create(self.library, + pathlib.Path("/a/b/c.ogg"), + self.medium, self.year) + def test_init(self): """Test that the playlist model is configured correctly.""" self.assertIsInstance(self.table, emmental.db.playlist.Table) + self.assertFalse(self.table.autodelete) self.assertEqual(len(self.table), 0) self.assertIsNone(self.table.collection) @@ -77,12 +87,17 @@ class TestPlaylistTable(tests.util.TestCase): self.assertEqual(playlist.playlistid, 1) self.assertEqual(playlist.name, "Test Playlist") self.assertIsNone(playlist.image) + self.assertTrue(playlist.user_tracks) + self.assertTrue(playlist.tracks_movable) def test_create(self): """Test creating a playlist.""" playlist = self.table.create(" Test Playlist ") self.assertIsInstance(playlist, emmental.db.playlists.Playlist) self.assertEqual(playlist.name, "Test Playlist") + self.assertEqual(playlist.loop, "None") + self.assertEqual(playlist.sort_order, + "albumartist, album, mediumno, number") self.assertIsNone(playlist.image) cur = self.sql("SELECT COUNT(name) FROM playlists") @@ -91,7 +106,7 @@ class TestPlaylistTable(tests.util.TestCase): self.assertEqual(self.table.get_item(0), playlist) cur = self.sql("SELECT COUNT(*) FROM playlist_properties") - self.assertEqual(cur.fetchone()["COUNT(*)"], 1) + self.assertEqual(cur.fetchone()["COUNT(*)"], playlist.propertyid) for name in ["", " ", "Test Playlist", "test playlist"]: self.assertIsNone(self.table.create(name)) @@ -111,7 +126,7 @@ class TestPlaylistTable(tests.util.TestCase): self.assertIsNone(self.table.get_item(0)) cur = self.sql("SELECT COUNT(*) FROM playlist_properties") - self.assertEqual(cur.fetchone()["COUNT(*)"], 0) + self.assertEqual(cur.fetchone()["COUNT(*)"], playlist.propertyid - 1) self.assertFalse(playlist.delete()) @@ -171,11 +186,21 @@ class TestPlaylistTable(tests.util.TestCase): playlist = self.table.create("Test Playlist") playlist.image = tests.util.COVER_JPG playlist.active = True + playlist.loop = "Track" + playlist.sort_order = "trackid" + playlist.shuffle = True + playlist.current_trackid = self.track.trackid - cur = self.sql("""SELECT image, active FROM playlists_view + cur = self.sql("""SELECT image, active, loop, shuffle, + sort_order, current_trackid + FROM playlists_view WHERE playlistid=?""", playlist.playlistid) row = cur.fetchone() self.assertEqual(row["image"], tests.util.COVER_JPG) + self.assertEqual(row["loop"], "Track") + self.assertEqual(row["shuffle"], True) + self.assertEqual(row["sort_order"], "trackid") + self.assertEqual(row["current_trackid"], self.track.trackid) self.assertTrue(row["active"]) @@ -192,18 +217,48 @@ class TestSystemPlaylists(tests.util.TestCase): """Test the Collection playlist.""" self.assertIsInstance(self.table.collection, emmental.db.playlists.Playlist) + self.assertEqual(self.table.collection.name, "Collection") + self.assertEqual(self.table.collection.loop, "Playlist") + self.assertEqual(self.table.collection.sort_order, + "albumartist, album, mediumno, number") + self.assertTrue(self.table.collection.active) + self.assertFalse(self.table.collection.shuffle) + self.assertFalse(self.table.collection.user_tracks) + self.assertFalse(self.table.collection.tracks_movable) + self.assertEqual(self.table.lookup("Collection"), self.table.collection) + def test_collection_loop(self): + """Test that the Collection::loop property cannot be disabled.""" + self.assertIsNone(self.sql("""UPDATE playlist_properties + SET loop='None' WHERE propertyid=?""", + self.table.collection.propertyid)) + + self.assertEqual(self.table.collection.loop, "Playlist") + self.table.collection.loop = "Track" + self.assertEqual(self.table.collection.loop, "Track") + self.table.collection.loop = "None" + self.assertEqual(self.table.collection.loop, "Playlist") + def test_favorites(self): """Test the favorite tracks playlist.""" self.assertIsInstance(self.table.favorites, emmental.db.playlists.Playlist) + self.assertEqual(self.table.favorites.name, "Favorite Tracks") + self.assertEqual(self.table.favorites.loop, "None") + self.assertEqual(self.table.favorites.sort_order, + "albumartist, album, mediumno, number") + + self.assertTrue(self.table.favorites.user_tracks) + + self.assertFalse(self.table.favorites.shuffle) self.assertFalse(self.table.favorites.active) + self.assertFalse(self.table.favorites.tracks_movable) self.assertEqual(self.table.lookup("Favorite Tracks"), self.table.favorites) @@ -212,8 +267,16 @@ class TestSystemPlaylists(tests.util.TestCase): """Test the most-played tracks playlist.""" self.assertIsInstance(self.table.most_played, emmental.db.playlists.Playlist) + + sort_order = "playcount DESC, albumartist, album, mediumno, number" self.assertEqual(self.table.most_played.name, "Most Played Tracks") + self.assertEqual(self.table.most_played.loop, "None") + self.assertEqual(self.table.most_played.sort_order, sort_order) + self.assertFalse(self.table.most_played.active) + self.assertFalse(self.table.most_played.shuffle) + self.assertFalse(self.table.most_played.user_tracks) + self.assertFalse(self.table.most_played.tracks_movable) self.assertEqual(self.table.lookup("Most Played Tracks"), self.table.most_played) @@ -222,8 +285,16 @@ class TestSystemPlaylists(tests.util.TestCase): """Test the new tracks playlist.""" self.assertIsInstance(self.table.new_tracks, emmental.db.playlists.Playlist) + self.assertEqual(self.table.new_tracks.name, "New Tracks") + self.assertEqual(self.table.new_tracks.loop, "None") + self.assertEqual(self.table.new_tracks.sort_order, + "albumartist, album, mediumno, number") + self.assertFalse(self.table.new_tracks.active) + self.assertFalse(self.table.new_tracks.shuffle) + self.assertFalse(self.table.new_tracks.user_tracks) + self.assertFalse(self.table.new_tracks.tracks_movable) self.assertEqual(self.table.lookup("New Tracks"), self.table.new_tracks) @@ -232,18 +303,67 @@ class TestSystemPlaylists(tests.util.TestCase): """Test the previous tracks playlist.""" self.assertIsInstance(self.table.previous, emmental.db.playlists.Playlist) + self.assertEqual(self.table.previous.name, "Previous Tracks") + self.assertEqual(self.table.previous.loop, "None") + self.assertEqual(self.table.previous.sort_order, "laststarted DESC") + self.assertFalse(self.table.previous.active) + self.assertFalse(self.table.previous.shuffle) + self.assertFalse(self.table.previous.user_tracks) + self.assertFalse(self.table.previous.tracks_movable) self.assertEqual(self.table.lookup("Previous Tracks"), self.table.previous) + def test_previous_loop(self): + """Test that the Previous::loop property cannot be disabled.""" + for loop in ["Track", "Playlist"]: + with self.subTest(loop=loop): + cur = self.sql("""UPDATE playlist_properties + SET loop=? WHERE propertyid=?""", + loop, self.table.previous.propertyid) + self.assertIsNone(cur) + + self.table.previous.loop = loop + self.assertEqual(self.table.previous.loop, "None") + + def test_previous_shuffle(self): + """Test that the Previous::shuffle property cannot be enabled.""" + self.assertIsNone(self.sql("""UPDATE playlist_properties + SET shuffle=TRUE WHERE propertyid=?""", + self.table.previous.propertyid)) + + self.assertFalse(self.table.previous.shuffle) + self.table.previous.shuffle = True + self.assertFalse(self.table.previous.shuffle) + + def test_previous_sort_order(self): + """Test that the Previous::sort-order property cannot be changed.""" + self.assertIsNone(self.sql("""UPDATE playlist_properties + SET sort_order='trackid' + WHERE propertyid=?""", + self.table.previous.propertyid)) + + self.assertEqual(self.table.previous.sort_order, "laststarted DESC") + self.table.previous.sort_order = "trackid" + self.assertEqual(self.table.previous.sort_order, "laststarted DESC") + def test_queued(self): """Test the queued tracks playlist.""" self.assertIsInstance(self.table.queued, emmental.db.playlists.Playlist) + self.assertEqual(self.table.queued.name, "Queued Tracks") + self.assertEqual(self.table.queued.loop, "None") + self.assertEqual(self.table.queued.sort_order, + "albumartist, album, mediumno, number") + + self.assertTrue(self.table.queued.user_tracks) + self.assertTrue(self.table.queued.tracks_movable) + self.assertFalse(self.table.queued.active) + self.assertFalse(self.table.queued.shuffle) self.assertEqual(self.table.lookup("Queued Tracks"), self.table.queued) @@ -252,8 +372,16 @@ class TestSystemPlaylists(tests.util.TestCase): """Test the unplayed tracks playlist.""" self.assertIsInstance(self.table.unplayed, emmental.db.playlists.Playlist) + self.assertEqual(self.table.unplayed.name, "Unplayed Tracks") + self.assertEqual(self.table.unplayed.loop, "None") + self.assertEqual(self.table.unplayed.sort_order, + "albumartist, album, mediumno, number") + self.assertFalse(self.table.unplayed.active) + self.assertFalse(self.table.unplayed.shuffle) + self.assertFalse(self.table.unplayed.user_tracks) + self.assertFalse(self.table.unplayed.tracks_movable) self.assertEqual(self.table.lookup("Unplayed Tracks"), self.table.unplayed)