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:
parent
6eec4dbfc3
commit
85c42216ab
|
@ -25,7 +25,14 @@ CREATE TABLE settings (
|
||||||
|
|
||||||
CREATE TABLE playlist_properties (
|
CREATE TABLE playlist_properties (
|
||||||
propertyid INTEGER PRIMARY KEY,
|
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
|
CREATE TRIGGER playlists_active_trigger
|
||||||
|
|
|
@ -17,17 +17,25 @@ class Playlist(table.Row):
|
||||||
name = GObject.Property(type=str)
|
name = GObject.Property(type=str)
|
||||||
active = GObject.Property(type=bool, default=False)
|
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)
|
tracks = GObject.Property(type=TrackidSet)
|
||||||
n_tracks = GObject.Property(type=int)
|
n_tracks = GObject.Property(type=int)
|
||||||
|
user_tracks = GObject.Property(type=bool, default=False)
|
||||||
tracks_loaded = GObject.Property(type=bool, default=False)
|
tracks_loaded = GObject.Property(type=bool, default=False)
|
||||||
tracks_movable = 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)
|
children = GObject.Property(type=Gtk.FilterListModel)
|
||||||
|
|
||||||
def __init__(self, table: Gio.ListModel, propertyid: int,
|
def __init__(self, table: Gio.ListModel, propertyid: int,
|
||||||
name: str, **kwargs):
|
name: str, current_trackid: int | None = 0, **kwargs):
|
||||||
"""Initialize a Playlist object."""
|
"""Initialize a Playlist object."""
|
||||||
|
current_trackid = 0 if current_trackid is None else current_trackid
|
||||||
super().__init__(table=table, propertyid=propertyid, name=name,
|
super().__init__(table=table, propertyid=propertyid, name=name,
|
||||||
|
current_trackid=current_trackid,
|
||||||
tracks=TrackidSet(), **kwargs)
|
tracks=TrackidSet(), **kwargs)
|
||||||
self.tracks.bind_property("n-trackids", self, "n-tracks")
|
self.tracks.bind_property("n-trackids", self, "n-tracks")
|
||||||
|
|
||||||
|
@ -50,7 +58,7 @@ class Playlist(table.Row):
|
||||||
"""Update a Playlist object."""
|
"""Update a Playlist object."""
|
||||||
match column:
|
match column:
|
||||||
case "propertyid" | "name" | "n-tracks" | "children" | \
|
case "propertyid" | "name" | "n-tracks" | "children" | \
|
||||||
"tracks-loaded" | "tracks-movable": pass
|
"user-tracks" | "tracks-loaded" | "tracks-movable": pass
|
||||||
case _: return super().do_update(column)
|
case _: return super().do_update(column)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -59,6 +67,10 @@ class Playlist(table.Row):
|
||||||
if self.table.add_track(self, track):
|
if self.table.add_track(self, track):
|
||||||
self.table.queue.push(self.__add_track, track, now=not idle)
|
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:
|
def has_track(self, track: Track) -> bool:
|
||||||
"""Check if a Track is on this Playlist."""
|
"""Check if a Track is on this Playlist."""
|
||||||
return track in self.tracks
|
return track in self.tracks
|
||||||
|
@ -134,6 +146,10 @@ class Table(table.Table):
|
||||||
"""Get a sort key for the requested Playlist."""
|
"""Get a sort key for the requested Playlist."""
|
||||||
return format.sort_key(playlist.name)
|
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:
|
def do_move_track_down(self, playlist: Playlist, track: Track) -> bool:
|
||||||
"""Move a track down in the sort order."""
|
"""Move a track down in the sort order."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
@ -199,17 +215,29 @@ class Table(table.Table):
|
||||||
self.__autodelete(playlist)
|
self.__autodelete(playlist)
|
||||||
return res
|
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:
|
def move_track_down(self, playlist: Playlist, track: Track) -> bool:
|
||||||
"""Move a track down in the playlist."""
|
"""Move a track down in the playlist."""
|
||||||
if not playlist.tracks_movable:
|
if not playlist.tracks_movable:
|
||||||
return False
|
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:
|
def move_track_up(self, playlist: Playlist, track: Track) -> bool:
|
||||||
"""Move a track up in the playlist."""
|
"""Move a track up in the playlist."""
|
||||||
if not playlist.tracks_movable:
|
if not playlist.tracks_movable:
|
||||||
return False
|
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:
|
def remove_system_track(self, playlist: Playlist, track: Track) -> bool:
|
||||||
"""Remove a Track from a system Playlist."""
|
"""Remove a Track from a system Playlist."""
|
||||||
|
@ -230,7 +258,8 @@ class Table(table.Table):
|
||||||
def update(self, playlist: Playlist, column: str, newval) -> bool:
|
def update(self, playlist: Playlist, column: str, newval) -> bool:
|
||||||
"""Update a Playlist in the Database."""
|
"""Update a Playlist in the Database."""
|
||||||
match column:
|
match column:
|
||||||
case "active":
|
case "active" | "loop" | "shuffle" | \
|
||||||
|
"sort-order" | "current-trackid":
|
||||||
return self.update_playlist_property(playlist, column, newval)
|
return self.update_playlist_property(playlist, column, newval)
|
||||||
case _:
|
case _:
|
||||||
return super().update(playlist, column, newval)
|
return super().update(playlist, column, newval)
|
||||||
|
@ -241,6 +270,11 @@ class Table(table.Table):
|
||||||
match column:
|
match column:
|
||||||
case "active":
|
case "active":
|
||||||
self.active_playlist = playlist if playlist.active else None
|
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
|
return self.sql(f"""UPDATE playlist_properties
|
||||||
SET {column}=? WHERE propertyid=?""",
|
SET {column}=? WHERE propertyid=?""",
|
||||||
|
|
|
@ -20,6 +20,7 @@ class TestPlaylistRow(unittest.TestCase):
|
||||||
self.table.move_track_down = unittest.mock.Mock(return_value=True)
|
self.table.move_track_down = unittest.mock.Mock(return_value=True)
|
||||||
self.table.move_track_up = 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_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.queue = emmental.db.idle.Queue()
|
||||||
self.table.update = unittest.mock.Mock(return_value=True)
|
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.assertEqual(self.playlist.name, "Test Playlist")
|
||||||
self.assertFalse(self.playlist.active)
|
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.assertEqual(self.playlist.n_tracks, 0)
|
||||||
|
self.assertFalse(self.playlist.user_tracks)
|
||||||
self.assertFalse(self.playlist.tracks_loaded)
|
self.assertFalse(self.playlist.tracks_loaded)
|
||||||
|
|
||||||
playlist2 = emmental.db.playlist.Playlist(table=self.table,
|
playlist2 = emmental.db.playlist.Playlist(table=self.table,
|
||||||
propertyid=1,
|
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.propertyid, 1)
|
||||||
self.assertEqual(playlist2.name, "Test Name")
|
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)
|
self.assertTrue(playlist2.active)
|
||||||
|
|
||||||
def test_children(self):
|
def test_children(self):
|
||||||
|
@ -68,14 +82,20 @@ class TestPlaylistRow(unittest.TestCase):
|
||||||
"""Test the do_update() function."""
|
"""Test the do_update() function."""
|
||||||
for (prop, value) in [("name", "New Name"), ("propertyid", 12345),
|
for (prop, value) in [("name", "New Name"), ("propertyid", 12345),
|
||||||
("children", Gtk.FilterListModel()),
|
("children", Gtk.FilterListModel()),
|
||||||
("n-tracks", 42), ("tracks-loaded", True),
|
("n-tracks", 42), ("user-tracks", True),
|
||||||
|
("tracks-loaded", True),
|
||||||
("tracks-movable", True)]:
|
("tracks-movable", True)]:
|
||||||
with self.subTest(property=prop):
|
with self.subTest(property=prop):
|
||||||
self.playlist.set_property(prop, value)
|
self.playlist.set_property(prop, value)
|
||||||
self.table.update.assert_not_called()
|
self.table.update.assert_not_called()
|
||||||
|
|
||||||
self.playlist.active = True
|
for (prop, value) in [("active", True), ("loop", "Track"),
|
||||||
self.table.update.assert_called_with(self.playlist, "active", True)
|
("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):
|
def test_add_track(self):
|
||||||
"""Test adding a track to the playlist."""
|
"""Test adding a track to the playlist."""
|
||||||
|
@ -95,6 +115,13 @@ class TestPlaylistRow(unittest.TestCase):
|
||||||
self.playlist.add_track(self.track)
|
self.playlist.add_track(self.track)
|
||||||
self.assertNotIn(self.track, self.playlist.tracks)
|
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):
|
def test_has_track(self):
|
||||||
"""Test the playlist has_track() function."""
|
"""Test the playlist has_track() function."""
|
||||||
self.assertFalse(self.playlist.has_track(self.track))
|
self.assertFalse(self.playlist.has_track(self.track))
|
||||||
|
@ -277,6 +304,26 @@ class TestPlaylistTable(tests.util.TestCase):
|
||||||
with self.assertRaises(NotImplementedError):
|
with self.assertRaises(NotImplementedError):
|
||||||
self.table.get_trackids(plist)
|
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):
|
def test_move_track_down(self):
|
||||||
"""Test moving tracks down in the sort order."""
|
"""Test moving tracks down in the sort order."""
|
||||||
plist = self.table.create("Test Playlist")
|
plist = self.table.create("Test Playlist")
|
||||||
|
@ -286,6 +333,14 @@ class TestPlaylistTable(tests.util.TestCase):
|
||||||
with self.assertRaises(NotImplementedError):
|
with self.assertRaises(NotImplementedError):
|
||||||
self.table.move_track_down(plist, self.track)
|
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):
|
def test_move_track_up(self):
|
||||||
"""Test moving tracks up in the sort order."""
|
"""Test moving tracks up in the sort order."""
|
||||||
plist = self.table.create("Test Playlist")
|
plist = self.table.create("Test Playlist")
|
||||||
|
@ -295,6 +350,14 @@ class TestPlaylistTable(tests.util.TestCase):
|
||||||
with self.assertRaises(NotImplementedError):
|
with self.assertRaises(NotImplementedError):
|
||||||
self.table.move_track_up(plist, self.track)
|
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):
|
def test_remove_track(self):
|
||||||
"""Test adding a track to a playlist."""
|
"""Test adding a track to a playlist."""
|
||||||
self.assertTrue(self.table.system_tracks)
|
self.assertTrue(self.table.system_tracks)
|
||||||
|
@ -317,17 +380,30 @@ class TestPlaylistTable(tests.util.TestCase):
|
||||||
plist1 = self.table.create("Test Playlist 1")
|
plist1 = self.table.create("Test Playlist 1")
|
||||||
plist2 = self.table.create("Test Playlist 2")
|
plist2 = self.table.create("Test Playlist 2")
|
||||||
plist1.active = True
|
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)
|
self.assertEqual(self.table.active_playlist, plist1)
|
||||||
row = self.sql("""SELECT active FROM playlist_properties
|
row = self.sql("""SELECT active, loop, shuffle,
|
||||||
WHERE propertyid=?""", plist1.propertyid).fetchone()
|
current_trackid, sort_order
|
||||||
|
FROM playlist_properties
|
||||||
|
WHERE propertyid=?""", plist1.propertyid).fetchone()
|
||||||
self.assertEqual(row["active"], True)
|
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
|
plist2.active = True
|
||||||
self.assertEqual(self.table.active_playlist, plist2)
|
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()
|
plist1.propertyid).fetchone()
|
||||||
self.assertEqual(row["active"], False)
|
self.assertEqual(row["active"], False)
|
||||||
|
self.assertEqual(row["current_trackid"], None)
|
||||||
|
|
||||||
def test_autodelete(self):
|
def test_autodelete(self):
|
||||||
"""Test automatically deleting playlists."""
|
"""Test automatically deleting playlists."""
|
||||||
|
|
Loading…
Reference in New Issue