db: Give System Playlists special property handling

I need to do something slightly different for each system Playlist:

* Collection: I disallow setting loop to "None".
* Favorite Tracks: I set the user-tracks property to "True"
* Most Played Tracks: I add playcount as the first sort field.
* Previous Tracks: I disallow changing loop, shuffle, and sort-order.
* Queued Tracks: I set the user-tracks and tracks-movable properties to "True"

User created playlists also set the user-tracks and tracks-movable
properties to "True".  I also disable autodelete on the Table so
playlists aren't deleted unexpectedly.

New Tracks and Unplayed Tracks have no special properties set.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2022-10-27 10:03:50 -04:00
parent 85c42216ab
commit 99496ca8bf
3 changed files with 208 additions and 8 deletions

View File

@ -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;
/*************************
* *

View File

@ -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:

View File

@ -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)