db: Add Track support to Playlist Objects

Playlists use a tracks.TrackidSet to manage a set of trackids
representing the Tracks in this Playlist.

I have two functions for loading tracks: load_tracks() and
reload_tracks(). Calling load_tracks() checks if the tracks have been
loaded first before doing any work, but calling reload_tracks() will
force the Playlist to go to the database to load the latest tracks.

Finally, I add a have-next-track property to the main database
connection. This is set to True whenever the active playlist has one or
more tracks.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2022-10-03 14:40:32 -04:00
parent 11560d781e
commit 1b9458c278
2 changed files with 154 additions and 5 deletions

View File

@ -3,6 +3,7 @@
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
from .tracks import Track, TrackidSet
from .. import format
from . import table
@ -15,15 +16,28 @@ class Playlist(table.Row):
name = GObject.Property(type=str)
active = GObject.Property(type=bool, default=False)
tracks = GObject.Property(type=TrackidSet)
n_tracks = GObject.Property(type=int)
tracks_loaded = GObject.Property(type=bool, default=False)
tracks_movable = GObject.Property(type=bool, default=False)
children = GObject.Property(type=Gtk.FilterListModel)
def __init__(self, table: Gio.ListModel, propertyid: int,
name: str, **kwargs):
"""Initialize a Playlist object."""
super().__init__(table=table, propertyid=propertyid,
name=name, **kwargs)
super().__init__(table=table, propertyid=propertyid, name=name,
tracks=TrackidSet(), **kwargs)
self.tracks.bind_property("n-trackids", self, "n-tracks")
def __add_track(self, track: Track) -> bool:
self.tracks.add_track(track)
return True
def __remove_track(self, track: Track) -> bool:
self.tracks.remove_track(track)
self.table.remove_track(self, track)
return True
def add_children(self, child_table: table.Table,
child_filter: Gtk.Filter) -> None:
@ -34,10 +48,48 @@ class Playlist(table.Row):
def do_update(self, column: str) -> bool:
"""Update a Playlist object."""
match column:
case "propertyid" | "name" | "n-tracks" | "children": pass
case "propertyid" | "name" | "n-tracks" | "children" | \
"tracks-loaded" | "tracks-movable": pass
case _: return super().do_update(column)
return True
def add_track(self, track: Track, *, idle: bool = False) -> None:
"""Add a Track to this Playlist."""
if self.table.add_track(self, track):
self.table.queue.push(self.__add_track, track, now=not idle)
def has_track(self, track: Track) -> bool:
"""Check if a Track is on this Playlist."""
return track in self.tracks
def load_tracks(self) -> bool:
"""Load this Playlist's Tracks (if they haven't been loaded yet)."""
if not self.tracks_loaded:
self.tracks.trackids = self.table.get_trackids(self)
self.tracks_loaded = True
return True
def move_track_down(self, track: Track) -> bool:
"""Move a track down in the sort order."""
return self.table.move_track_down(self, track)
def move_track_up(self, track: Track) -> bool:
"""Move a track up in the sort order."""
return self.table.move_track_up(self, track)
def reload_tracks(self, *, idle: bool = False) -> None:
"""Load this Playlist's Tracks."""
self.tracks_loaded = False
self.table.queue.push(self.load_tracks, now=not idle)
def remove_track(self, track: table.Row, *, idle: bool = False) -> None:
"""Remove a Track from this Playlist."""
self.table.queue.push(self.__remove_track, track, now=not idle)
def rename(self, new_name: str) -> bool:
"""Rename this playlist."""
return self.table.rename(self, new_name)
@GObject.Property(type=table.Row)
def parent(self) -> table.Row | None:
"""Get this playlist's parent playlist."""

View File

@ -14,20 +14,33 @@ class TestPlaylistRow(unittest.TestCase):
def setUp(self):
"""Set up common variables."""
self.table = Gio.ListStore()
self.table.add_track = unittest.mock.Mock(return_value=True)
self.table.remove_track = 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.get_trackids = unittest.mock.Mock(return_value={1, 2, 3})
self.table.queue = emmental.db.idle.Queue()
self.table.update = unittest.mock.Mock(return_value=True)
self.playlist = emmental.db.playlist.Playlist(table=self.table,
propertyid=0,
name="Test Playlist")
self.track = emmental.db.tracks.Track(table=None, trackid=12345)
def test_init(self):
"""Test that the Playlist object is configured correctly."""
self.assertIsInstance(self.playlist, emmental.db.table.Row)
self.assertIsInstance(self.playlist.tracks,
emmental.db.tracks.TrackidSet)
self.assertEqual(self.playlist.table, self.table)
self.assertEqual(self.playlist.propertyid, 0)
self.assertEqual(self.playlist.name, "Test Playlist")
self.assertEqual(self.playlist.n_tracks, 0)
self.assertFalse(self.playlist.active)
self.assertEqual(self.playlist.n_tracks, 0)
self.assertFalse(self.playlist.tracks_loaded)
playlist2 = emmental.db.playlist.Playlist(table=self.table,
propertyid=1,
name="Test Name", active=1)
@ -54,7 +67,8 @@ class TestPlaylistRow(unittest.TestCase):
"""Test the do_update() function."""
for (prop, value) in [("name", "New Name"), ("propertyid", 12345),
("children", Gtk.FilterListModel()),
("n-tracks", 42)]:
("n-tracks", 42), ("tracks-loaded", True),
("tracks-movable", True)]:
with self.subTest(property=prop):
self.playlist.set_property(prop, value)
self.table.update.assert_not_called()
@ -62,6 +76,89 @@ class TestPlaylistRow(unittest.TestCase):
self.playlist.active = True
self.table.update.assert_called_with(self.playlist, "active", True)
def test_add_track(self):
"""Test adding a track to the playlist."""
self.playlist.add_track(self.track, idle=True)
self.table.add_track.assert_called_with(self.playlist, self.track)
self.assertNotIn(self.track, self.playlist.tracks)
self.assertEqual(self.table.queue[0],
(self.playlist._Playlist__add_track, self.track))
self.playlist.add_track(self.track)
self.table.add_track.assert_called_with(self.playlist, self.track)
self.assertIn(self.track, self.playlist.tracks)
self.assertEqual(self.playlist.n_tracks, 1)
self.playlist.tracks.trackids.clear()
self.table.add_track.return_value = False
self.playlist.add_track(self.track)
self.assertNotIn(self.track, self.playlist.tracks)
def test_has_track(self):
"""Test the playlist has_track() function."""
self.assertFalse(self.playlist.has_track(self.track))
self.playlist.add_track(self.track)
self.assertTrue(self.playlist.has_track(self.track))
self.playlist.remove_track(self.track)
self.assertFalse(self.playlist.has_track(self.track))
def test_move_tracks(self):
"""Test the move_track_up() and move_track_down() functions."""
self.assertFalse(self.playlist.tracks_movable)
self.assertTrue(self.playlist.move_track_down(self.track))
self.table.move_track_down.assert_called_with(self.playlist,
self.track)
self.assertTrue(self.playlist.move_track_up(self.track))
self.table.move_track_up.assert_called_with(self.playlist, self.track)
def test_remove_track(self):
"""Test removing a track from the playlist."""
self.playlist.tracks.trackids.add(self.track.trackid)
self.playlist.remove_track(self.track, idle=True)
self.table.remove_track.assert_not_called()
self.assertIn(self.track, self.playlist.tracks)
self.assertEqual(self.table.queue[0],
(self.playlist._Playlist__remove_track, self.track))
self.playlist.remove_track(self.track)
self.table.remove_track.assert_called_with(self.playlist, self.track)
self.assertNotIn(self.track, self.playlist.tracks)
self.assertEqual(self.playlist.n_tracks, 0)
self.playlist.tracks.trackids.add(self.track.trackid)
self.table.remove_track.return_value = False
self.playlist.remove_track(self.track)
self.table.remove_track.assert_called_with(self.playlist, self.track)
self.assertNotIn(self.track, self.playlist.tracks)
def test_load_tracks(self):
"""Test loading a Playlist's Tracks."""
self.assertEqual(self.playlist.n_tracks, 0)
self.assertFalse(self.playlist.tracks_loaded)
self.assertTrue(self.playlist.load_tracks())
self.table.get_trackids.assert_called_with(self.playlist)
self.assertSetEqual(self.playlist.tracks.trackids, {1, 2, 3})
self.assertTrue(self.playlist.tracks_loaded)
self.table.get_trackids.reset_mock()
self.assertTrue(self.playlist.load_tracks())
self.table.get_trackids.assert_not_called()
def test_reload_tracks(self):
"""Test reloading a Playlist's Tracks."""
self.playlist.tracks_loaded = True
self.playlist.reload_tracks(idle=True)
self.table.get_trackids.assert_not_called()
self.assertEqual(self.table.queue[0], (self.playlist.load_tracks,))
self.playlist.tracks_loaded = True
self.playlist.reload_tracks()
self.table.get_trackids.assert_called_with(self.playlist)
self.assertSetEqual(self.playlist.tracks.trackids, {1, 2, 3})
class TestPlaylistTable(tests.util.TestCase):
"""Tests our Playlist Table."""