From 1b9458c27812cf43300cff9f0f2a6b8b7deb13ad Mon Sep 17 00:00:00 2001 From: Anna Schumaker Date: Mon, 3 Oct 2022 14:40:32 -0400 Subject: [PATCH] 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 --- emmental/db/playlist.py | 58 ++++++++++++++++++++-- tests/db/test_playlist.py | 101 +++++++++++++++++++++++++++++++++++++- 2 files changed, 154 insertions(+), 5 deletions(-) diff --git a/emmental/db/playlist.py b/emmental/db/playlist.py index edf74ec..bc60842 100644 --- a/emmental/db/playlist.py +++ b/emmental/db/playlist.py @@ -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.""" diff --git a/tests/db/test_playlist.py b/tests/db/test_playlist.py index 9c220fc..c279381 100644 --- a/tests/db/test_playlist.py +++ b/tests/db/test_playlist.py @@ -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."""