db: Give Playlists extra properties

* loop is based on the mpris loop property, and can be set to "None",
      "Track", or "Playlist".
* shuffle is a boolean True / False value.
* sort-order saves a user-configured sort order for each playlist.
* current-trackid is an integer referring to the trackid of the currently
      playing track.
* user-tracks: is a boolean representing if the user should be allowed
      to manually add and remove tracks.

I also use the sort-order property to implement a get_track_order()
function to get the sort keys for tracks in a Playlist.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2022-10-03 14:40:32 -04:00
parent 6eec4dbfc3
commit 85c42216ab
3 changed files with 130 additions and 13 deletions

View File

@ -25,7 +25,14 @@ CREATE TABLE settings (
CREATE TABLE playlist_properties (
propertyid INTEGER PRIMARY KEY,
active BOOLEAN NOT NULL DEFAULT FALSE
active BOOLEAN NOT NULL DEFAULT FALSE,
loop STRING NOT NULL DEFAULT "None",
shuffle BOOLEAN NOT NULL DEFAULT FALSE,
sort_order STRING NOT NULL DEFAULT "",
current_trackid INTEGER DEFAULT NULL REFERENCES tracks (trackid)
ON DELETE SET NULL
ON UPDATE CASCADE,
CHECK (loop IN ("None", "Track", "Playlist"))
);
CREATE TRIGGER playlists_active_trigger

View File

@ -17,17 +17,25 @@ class Playlist(table.Row):
name = GObject.Property(type=str)
active = GObject.Property(type=bool, default=False)
loop = GObject.Property(type=str, default="None")
shuffle = GObject.Property(type=bool, default=False)
sort_order = GObject.Property(type=str)
tracks = GObject.Property(type=TrackidSet)
n_tracks = GObject.Property(type=int)
user_tracks = GObject.Property(type=bool, default=False)
tracks_loaded = GObject.Property(type=bool, default=False)
tracks_movable = GObject.Property(type=bool, default=False)
current_trackid = GObject.Property(type=int)
children = GObject.Property(type=Gtk.FilterListModel)
def __init__(self, table: Gio.ListModel, propertyid: int,
name: str, **kwargs):
name: str, current_trackid: int | None = 0, **kwargs):
"""Initialize a Playlist object."""
current_trackid = 0 if current_trackid is None else current_trackid
super().__init__(table=table, propertyid=propertyid, name=name,
current_trackid=current_trackid,
tracks=TrackidSet(), **kwargs)
self.tracks.bind_property("n-trackids", self, "n-tracks")
@ -50,7 +58,7 @@ class Playlist(table.Row):
"""Update a Playlist object."""
match column:
case "propertyid" | "name" | "n-tracks" | "children" | \
"tracks-loaded" | "tracks-movable": pass
"user-tracks" | "tracks-loaded" | "tracks-movable": pass
case _: return super().do_update(column)
return True
@ -59,6 +67,10 @@ class Playlist(table.Row):
if self.table.add_track(self, track):
self.table.queue.push(self.__add_track, track, now=not idle)
def get_track_order(self) -> dict[int, int]:
"""Get a dictionary mapping for trackid -> sorted position."""
return self.table.get_track_order(self)
def has_track(self, track: Track) -> bool:
"""Check if a Track is on this Playlist."""
return track in self.tracks
@ -134,6 +146,10 @@ class Table(table.Table):
"""Get a sort key for the requested Playlist."""
return format.sort_key(playlist.name)
def do_get_user_track_order(self, playlist: Playlist) -> dict[int, int]:
"""Get a mapping of sort keys for the tracks in this Playlist."""
raise NotImplementedError
def do_move_track_down(self, playlist: Playlist, track: Track) -> bool:
"""Move a track down in the sort order."""
raise NotImplementedError
@ -199,17 +215,29 @@ class Table(table.Table):
self.__autodelete(playlist)
return res
def get_track_order(self, playlist: Playlist) -> dict[int, int]:
"""Get the track sort order for a playlist."""
if playlist.tracks_movable and playlist.sort_order == "user":
return self.do_get_user_track_order(playlist)
return self.sql.tracks.map_sort_order(playlist.sort_order)
def move_track_down(self, playlist: Playlist, track: Track) -> bool:
"""Move a track down in the playlist."""
if not playlist.tracks_movable:
return False
return self.do_move_track_down(playlist, track)
if res := self.do_move_track_down(playlist, track):
if playlist.sort_order != "user":
playlist.sort_order = "user"
return res
def move_track_up(self, playlist: Playlist, track: Track) -> bool:
"""Move a track up in the playlist."""
if not playlist.tracks_movable:
return False
return self.do_move_track_up(playlist, track)
if res := self.do_move_track_up(playlist, track):
if playlist.sort_order != "user":
playlist.sort_order = "user"
return res
def remove_system_track(self, playlist: Playlist, track: Track) -> bool:
"""Remove a Track from a system Playlist."""
@ -230,7 +258,8 @@ class Table(table.Table):
def update(self, playlist: Playlist, column: str, newval) -> bool:
"""Update a Playlist in the Database."""
match column:
case "active":
case "active" | "loop" | "shuffle" | \
"sort-order" | "current-trackid":
return self.update_playlist_property(playlist, column, newval)
case _:
return super().update(playlist, column, newval)
@ -241,6 +270,11 @@ class Table(table.Table):
match column:
case "active":
self.active_playlist = playlist if playlist.active else None
case "current-trackid":
column = "current_trackid"
newval = None if newval == 0 else newval
case "sort-order":
column = "sort_order"
return self.sql(f"""UPDATE playlist_properties
SET {column}=? WHERE propertyid=?""",

View File

@ -20,6 +20,7 @@ class TestPlaylistRow(unittest.TestCase):
self.table.move_track_down = unittest.mock.Mock(return_value=True)
self.table.move_track_up = unittest.mock.Mock(return_value=True)
self.table.get_trackids = unittest.mock.Mock(return_value={1, 2, 3})
self.table.get_track_order = unittest.mock.Mock()
self.table.queue = emmental.db.idle.Queue()
self.table.update = unittest.mock.Mock(return_value=True)
@ -39,14 +40,27 @@ class TestPlaylistRow(unittest.TestCase):
self.assertEqual(self.playlist.name, "Test Playlist")
self.assertFalse(self.playlist.active)
self.assertEqual(self.playlist.loop, "None")
self.assertFalse(self.playlist.shuffle)
self.assertEqual(self.playlist.current_trackid, 0)
self.assertEqual(self.playlist.sort_order, "")
self.assertEqual(self.playlist.n_tracks, 0)
self.assertFalse(self.playlist.user_tracks)
self.assertFalse(self.playlist.tracks_loaded)
playlist2 = emmental.db.playlist.Playlist(table=self.table,
propertyid=1,
name="Test Name", active=1)
name="Test Name", active=1,
shuffle=True, loop="Track",
current_trackid=42,
sort_order="Track Number")
self.assertEqual(playlist2.propertyid, 1)
self.assertEqual(playlist2.name, "Test Name")
self.assertEqual(playlist2.current_trackid, 42)
self.assertEqual(playlist2.sort_order, "Track Number")
self.assertEqual(playlist2.loop, "Track")
self.assertTrue(playlist2.shuffle)
self.assertTrue(playlist2.active)
def test_children(self):
@ -68,14 +82,20 @@ class TestPlaylistRow(unittest.TestCase):
"""Test the do_update() function."""
for (prop, value) in [("name", "New Name"), ("propertyid", 12345),
("children", Gtk.FilterListModel()),
("n-tracks", 42), ("tracks-loaded", True),
("n-tracks", 42), ("user-tracks", True),
("tracks-loaded", True),
("tracks-movable", True)]:
with self.subTest(property=prop):
self.playlist.set_property(prop, value)
self.table.update.assert_not_called()
self.playlist.active = True
self.table.update.assert_called_with(self.playlist, "active", True)
for (prop, value) in [("active", True), ("loop", "Track"),
("shuffle", True), ("sort-order", "my order"),
("current-trackid", 42)]:
with self.subTest(property=prop):
self.playlist.set_property(prop, value)
self.table.update.assert_called_with(self.playlist,
prop, value)
def test_add_track(self):
"""Test adding a track to the playlist."""
@ -95,6 +115,13 @@ class TestPlaylistRow(unittest.TestCase):
self.playlist.add_track(self.track)
self.assertNotIn(self.track, self.playlist.tracks)
def test_get_track_order(self):
"""Test the get_track_order() function."""
self.table.get_track_order.return_value = {1: 3, 2: 2, 3: 1}
self.assertDictEqual(self.playlist.get_track_order(),
{1: 3, 2: 2, 3: 1})
self.table.get_track_order.assert_called_with(self.playlist)
def test_has_track(self):
"""Test the playlist has_track() function."""
self.assertFalse(self.playlist.has_track(self.track))
@ -277,6 +304,26 @@ class TestPlaylistTable(tests.util.TestCase):
with self.assertRaises(NotImplementedError):
self.table.get_trackids(plist)
def test_get_track_order(self):
"""Test getting track sort keys for a playlist."""
plist = self.table.create("Test Playlist")
plist.sort_order = "my order"
self.sql.tracks.map_sort_order = unittest.mock.Mock()
self.sql.tracks.map_sort_order.return_value = {1: 3, 2: 2, 3: 1}
self.assertDictEqual(self.table.get_track_order(plist),
{1: 3, 2: 2, 3: 1})
self.sql.tracks.map_sort_order.assert_called_with("my order")
self.sql.tracks.map_sort_order.reset_mock()
plist.tracks_movable = True
self.assertDictEqual(self.table.get_track_order(plist),
{1: 3, 2: 2, 3: 1})
self.sql.tracks.map_sort_order.assert_called_with("my order")
plist.sort_order = "user"
with self.assertRaises(NotImplementedError):
self.table.get_track_order(plist)
def test_move_track_down(self):
"""Test moving tracks down in the sort order."""
plist = self.table.create("Test Playlist")
@ -286,6 +333,14 @@ class TestPlaylistTable(tests.util.TestCase):
with self.assertRaises(NotImplementedError):
self.table.move_track_down(plist, self.track)
self.table.do_move_track_down = unittest.mock.Mock(return_value=False)
self.table.move_track_down(plist, self.track)
self.assertEqual(plist.sort_order, "")
self.table.do_move_track_down.return_value = True
self.table.move_track_down(plist, self.track)
self.assertEqual(plist.sort_order, "user")
def test_move_track_up(self):
"""Test moving tracks up in the sort order."""
plist = self.table.create("Test Playlist")
@ -295,6 +350,14 @@ class TestPlaylistTable(tests.util.TestCase):
with self.assertRaises(NotImplementedError):
self.table.move_track_up(plist, self.track)
self.table.do_move_track_up = unittest.mock.Mock(return_value=False)
self.table.move_track_up(plist, self.track)
self.assertEqual(plist.sort_order, "")
self.table.do_move_track_up.return_value = True
self.table.move_track_up(plist, self.track)
self.assertEqual(plist.sort_order, "user")
def test_remove_track(self):
"""Test adding a track to a playlist."""
self.assertTrue(self.table.system_tracks)
@ -317,17 +380,30 @@ class TestPlaylistTable(tests.util.TestCase):
plist1 = self.table.create("Test Playlist 1")
plist2 = self.table.create("Test Playlist 2")
plist1.active = True
plist1.loop = "Track"
plist1.shuffle = True
plist1.sort_order = "Track Number"
plist1.current_trackid = self.track.trackid
self.assertEqual(self.table.active_playlist, plist1)
row = self.sql("""SELECT active FROM playlist_properties
WHERE propertyid=?""", plist1.propertyid).fetchone()
row = self.sql("""SELECT active, loop, shuffle,
current_trackid, sort_order
FROM playlist_properties
WHERE propertyid=?""", plist1.propertyid).fetchone()
self.assertEqual(row["active"], True)
self.assertEqual(row["current_trackid"], self.track.trackid)
self.assertEqual(row["sort_order"], "Track Number")
self.assertEqual(row["loop"], "Track")
self.assertTrue(row["shuffle"])
plist1.current_trackid = 0
plist2.active = True
self.assertEqual(self.table.active_playlist, plist2)
row = self.sql("SELECT active FROM playlist_properties WHERE rowid=?",
row = self.sql("""SELECT active, current_trackid
FROM playlist_properties WHERE propertyid=?""",
plist1.propertyid).fetchone()
self.assertEqual(row["active"], False)
self.assertEqual(row["current_trackid"], None)
def test_autodelete(self):
"""Test automatically deleting playlists."""