# Copyright 2022 (c) Anna Schumaker """Tests our playlist Gio.ListModel.""" import datetime import pathlib import unittest.mock import emmental.db import tests.util class TestPlaylistObject(tests.util.TestCase): """Tests our playlist object.""" def setUp(self): """Set up common variables.""" super().setUp() self.table = self.sql.playlists self.playlist = emmental.db.playlists.Playlist(table=self.table, playlistid=12345, propertyid=67890, name="Test Playlist") def test_init(self): """Test that the Playlist is set up properly.""" self.assertIsInstance(self.playlist, emmental.db.playlist.Playlist) self.assertEqual(self.playlist.table, self.table) self.assertEqual(self.playlist.propertyid, 67890) self.assertEqual(self.playlist.playlistid, 12345) self.assertEqual(self.playlist.primary_key, 12345) self.assertEqual(self.playlist.name, "Test Playlist") self.assertIsNone(self.playlist.image) self.assertIsNone(self.playlist.parent) def test_image_path(self): """Test the image-path property.""" path = pathlib.Path("/a/b/c.jpg") playlist = emmental.db.playlists.Playlist(table=self.table, playlistid=1, propertyid=1, image=path, name="Test") self.assertEqual(playlist.image, path) def test_rename(self): """Test the rename() function.""" with unittest.mock.patch.object(self.table, "rename", return_value=True) as mock_rename: self.assertTrue(self.playlist.rename("New Name")) mock_rename.assert_called_with(self.playlist, "New Name") class TestPlaylistTable(tests.util.TestCase): """Tests our playlist table.""" def setUp(self): """Set up common variables.""" super().setUp() 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, number=1) 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.assertFalse(self.table.system_tracks) self.assertFalse(self.table.have_collection_tracks) self.assertEqual(len(self.table), 0) self.assertIsNone(self.table.collection) self.assertIsNone(self.table.favorites) self.assertIsNone(self.table.most_played) self.assertIsNone(self.table.new_tracks) self.assertIsNone(self.table.previous) self.assertIsNone(self.table.queued) self.assertIsNone(self.table.unplayed) def test_add_track(self): """Test adding tracks to user Playlists.""" playlist = self.table.create("Test Playlist") playlist.add_track(self.track) self.assertTrue(playlist.has_track(self.track)) rows = self.sql("SELECT trackid FROM user_tracks WHERE propertyid=?", playlist.propertyid).fetchall() self.assertEqual(len(rows), 1) self.assertEqual(rows[0]["trackid"], self.track.trackid) self.assertFalse(playlist.add_track(self.track)) def test_construct(self): """Test constructing a playlist.""" playlist = self.table.construct(playlistid=1, propertyid=1, name="Test Playlist") self.assertIsInstance(playlist, emmental.db.playlists.Playlist) self.assertEqual(playlist.table, self.table) self.assertEqual(playlist.propertyid, 1) 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) self.assertTrue(playlist.tracks_movable) cur = self.sql("SELECT COUNT(name) FROM playlists") self.assertEqual(cur.fetchone()["COUNT(name)"], 1) self.assertEqual(len(self.table), 1) self.assertEqual(self.table.get_item(0), playlist) cur = self.sql("SELECT COUNT(*) FROM playlist_properties") self.assertEqual(cur.fetchone()["COUNT(*)"], playlist.propertyid) for name in ["", " ", "Test Playlist", "test playlist"]: self.assertIsNone(self.table.create(name)) self.assertEqual(len(self.table), 1) cur = self.sql("SELECT COUNT(rowid) FROM playlists") self.assertEqual(cur.fetchone()["COUNT(rowid)"], 1) def test_delete(self): """Test deleting a playlist.""" playlist = self.table.create("Test Playlist") self.assertTrue(playlist.delete()) self.assertIsNone(self.table.index(playlist)) cur = self.sql("SELECT COUNT(name) FROM playlists") self.assertEqual(cur.fetchone()["COUNT(name)"], 0) self.assertEqual(len(self.table), 0) self.assertIsNone(self.table.get_item(0)) cur = self.sql("SELECT COUNT(*) FROM playlist_properties") self.assertEqual(cur.fetchone()["COUNT(*)"], playlist.propertyid - 1) self.assertFalse(playlist.delete()) def test_filter(self): """Test filtering the playlist model.""" self.table.create("Playlist 1") self.table.create("Playlist 2") self.table.filter("*1", now=True) self.assertSetEqual(self.table.get_filter().keys, {1}) self.table.filter("playlist*", now=True) self.assertSetEqual(self.table.get_filter().keys, {1, 2}) def test_get_trackids(self): """Test loading playlist tracks from the database.""" playlist = self.table.create("Test Playlist") self.assertSetEqual(self.table.get_trackids(playlist), set()) playlist.add_track(self.track) self.assertSetEqual(self.table.get_trackids(playlist), {self.track.trackid}) def test_get_track_order(self): """Test getting the user track order for a playlist.""" track2 = self.sql.tracks.create(self.library, pathlib.Path("/a/b/d.ogg"), self.medium, self.year) playlist = self.table.create("Test Playlist") playlist.add_track(self.track) playlist.add_track(track2) playlist.sort_order = "user" self.assertDictEqual(self.table.get_track_order(playlist), {self.track.trackid: 0, track2.trackid: 1}) self.sql("UPDATE user_tracks SET position=? WHERE trackid=?", 3, track2.trackid) self.assertDictEqual(self.table.get_track_order(playlist), {self.track.trackid: 1, track2.trackid: 0}) def test_load(self): """Test loading playlists from the database.""" self.table.create("Playlist 1").image = tests.util.COVER_JPG self.table.create("Playlist 2") playlists2 = emmental.db.playlists.Table(self.sql) playlists2.load(now=True) self.assertEqual(len(playlists2), 2) self.assertEqual(playlists2[0].name, "Playlist 1") self.assertEqual(playlists2[0].image, tests.util.COVER_JPG) self.assertEqual(playlists2[1].name, "Playlist 2") self.assertIsNone(playlists2[1].image) def test_lookup(self): """Test looking up a playlist.""" playlist = self.table.create("Test Playlist") self.assertEqual(self.table.lookup("Test Playlist"), playlist) self.assertEqual(self.table.lookup("test playlist"), playlist) self.assertIsNone(self.table.lookup("No Playlist")) def test_move_track_down(self): """Test moving a track down in the sort order.""" track2 = self.sql.tracks.create(self.library, pathlib.Path("/a/b/d.ogg"), self.medium, self.year, number=2) track3 = self.sql.tracks.create(self.library, pathlib.Path("/a/b/e.ogg"), self.medium, self.year, number=3) playlist = self.table.create("Test Playlist") for track in [self.track, track2, track3]: playlist.add_track(track) playlist.sort_order = "number" self.assertFalse(self.table.move_track_down(playlist, track3)) self.assertEqual(playlist.sort_order, "number") self.assertTrue(self.table.move_track_down(playlist, self.track)) self.assertEqual(playlist.sort_order, "user") self.assertDictEqual(self.table.get_track_order(playlist), {track2.trackid: 0, self.track.trackid: 1, track3.trackid: 2}) def test_move_track_up(self): """Test moving a track up in the sort order.""" track2 = self.sql.tracks.create(self.library, pathlib.Path("/a/b/d.ogg"), self.medium, self.year, number=2) track3 = self.sql.tracks.create(self.library, pathlib.Path("/a/b/e.ogg"), self.medium, self.year, number=3) playlist = self.table.create("Test Playlist") for track in [self.track, track2, track3]: playlist.add_track(track) playlist.sort_order = "number" self.assertFalse(self.table.move_track_up(playlist, self.track)) self.assertEqual(playlist.sort_order, "number") self.assertTrue(self.table.move_track_up(playlist, track3)) self.assertEqual(playlist.sort_order, "user") self.assertDictEqual(self.table.get_track_order(playlist), {self.track.trackid: 0, track3.trackid: 1, track2.trackid: 2}) def test_remove_track(self): """Test removing tracks from user Playlists.""" playlist = self.table.create("Test Playlist") playlist.add_track(self.track) playlist.remove_track(self.track) self.assertFalse(playlist.has_track(self.track)) rows = self.sql("SELECT trackid FROM user_tracks WHERE propertyid=?", playlist.propertyid).fetchall() self.assertEqual(len(rows), 0) def test_rename(self): """Test renaming a playlist.""" playlist = self.table.create("Test Playlist") self.table.store.append = unittest.mock.Mock() self.table.store.remove = unittest.mock.Mock() self.assertTrue(playlist.rename(" New Name ")) self.assertEqual(playlist.name, "New Name") self.table.store.remove.assert_called_with(playlist) self.table.store.append.assert_called_with(playlist) rows = self.sql("SELECT name FROM playlists").fetchall() self.assertEqual(len(rows), 1) self.assertEqual(rows[0]["name"], "New Name") self.table.create("Other Name") self.assertFalse(playlist.rename("New Name")) self.assertFalse(playlist.rename("Other Name")) def test_update(self): """Test updating playlist properties.""" 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, 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"]) class TestSystemPlaylists(tests.util.TestCase): """Tests our system playlists.""" def setUp(self): """Set up common variables.""" super().setUp() self.table = self.sql.playlists self.table.load(now=True) 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/1.ogg"), self.medium, self.year) def test_midnight_alarm(self): """Test playlist maintenance run every night at midnight.""" with unittest.mock.patch.object(self.table.new_tracks, "reload_tracks") as mock_reload: self.table._Table__at_midnight() mock_reload.assert_called() with unittest.mock.patch("emmental.alarm.set_alarm") as mock_set: table2 = emmental.db.playlists.Table(self.sql) mock_set.assert_called_with(datetime.time(second=5), table2._Table__at_midnight) def test_collection(self): """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_collection_tracks(self): """Test the Collection playlist track functions.""" for (enabled, deleting) in [(False, False), (False, True), (True, True), (True, False)]: with self.subTest(enabled=enabled, deleting=deleting): self.library.enabled = enabled self.library.deleting = deleting self.table.collection.add_track(self.track) self.assertEqual(self.table.collection.has_track(self.track), enabled and not deleting) self.assertEqual(self.table.have_collection_tracks, enabled and not deleting) self.table.collection.remove_track(self.track) self.assertFalse(self.table.collection.has_track(self.track)) self.table.collection.reload_tracks() self.assertTrue(self.table.collection.has_track(self.track)) self.library.enabled = False self.table.queue.complete() self.assertFalse(self.table.collection.has_track(self.track)) self.library.enabled = True self.table.queue.complete() self.assertTrue(self.table.collection.has_track(self.track)) self.library.deleting = True self.table.collection.reload_tracks() self.assertFalse(self.table.collection.has_track(self.track)) 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) def test_favorite_tracks(self): """Test the Favorite Tracks track functions.""" self.table.favorites.add_track(self.track) self.assertTrue(self.table.favorites.has_track(self.track)) self.assertTrue(self.track.favorite) self.library.deleting = True self.table.favorites.reload_tracks() self.assertFalse(self.table.favorites.has_track(self.track)) self.library.deleting = False self.table.favorites.reload_tracks() self.assertTrue(self.table.favorites.has_track(self.track)) self.table.favorites.remove_track(self.track) self.assertFalse(self.table.favorites.has_track(self.track)) self.assertFalse(self.track.favorite) def test_most_played(self): """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) def test_most_played_tracks(self): """Test the Most Played Tracks track functions.""" track2 = self.sql.tracks.create(self.library, pathlib.Path("/a/b/2.ogg"), self.medium, self.year) self.sql("UPDATE tracks SET playcount=2 WHERE trackid=?", self.track.trackid) self.sql("UPDATE tracks SET playcount=1 WHERE trackid=?", track2.trackid) self.table.most_played.add_track(self.track) self.assertFalse(self.table.most_played.has_track(self.track)) self.table.most_played.tracks.add_track(self.track) self.table.most_played.remove_track(self.track) self.assertFalse(self.table.most_played.has_track(self.track)) self.table.most_played.reload_tracks() self.assertFalse(self.table.most_played.has_track(self.track)) self.sql("UPDATE tracks SET playcount=5 WHERE trackid=?", self.track.trackid) self.table.most_played.reload_tracks() self.assertTrue(self.table.most_played.has_track(self.track)) self.library.deleting = True self.table.most_played.reload_tracks() self.assertFalse(self.table.most_played.has_track(self.track)) def test_new_tracks(self): """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) def test_new_tracks_tracks(self): """Test the New Tracks track functions.""" self.table.new_tracks.add_track(self.track) self.assertTrue(self.table.new_tracks.has_track(self.track)) self.library.deleting = True self.table.new_tracks.reload_tracks() self.assertFalse(self.table.new_tracks.has_track(self.track)) self.library.deleting = False self.table.new_tracks.reload_tracks() self.assertTrue(self.table.new_tracks.has_track(self.track)) self.table.new_tracks.remove_track(self.track) self.assertFalse(self.table.new_tracks.has_track(self.track)) def test_previous(self): """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_previous_tracks(self): """Test the Previous Tracks track functions.""" self.table.previous.add_track(self.track) self.assertTrue(self.table.previous.has_track(self.track)) rows = self.sql("SELECT trackid FROM system_tracks WHERE propertyid=?", self.table.previous.propertyid).fetchall() self.assertEqual(len(rows), 1) self.assertEqual(rows[0]["trackid"], self.track.trackid) self.library.deleting = True self.table.previous.reload_tracks() self.assertFalse(self.table.previous.has_track(self.track)) self.library.deleting = False self.table.previous.reload_tracks() self.assertTrue(self.table.previous.has_track(self.track)) self.table.previous.remove_track(self.track) self.assertFalse(self.table.previous.has_track(self.track)) rows = self.sql("SELECT trackid FROM system_tracks WHERE propertyid=?", self.table.previous.propertyid).fetchall() self.assertEqual(len(rows), 0) def test_previous_tracks_reset(self): """Test that the Previous Tracks are reset during startup.""" self.table.previous.add_track(self.track) self.table.load(now=True) rows = self.sql("SELECT trackid FROM system_tracks WHERE propertyid=?", self.table.previous.propertyid).fetchall() self.assertEqual(len(rows), 0) 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) def test_queued_tracks(self): """Test the Queued Tracks track functions.""" self.table.queued.add_track(self.track) self.assertTrue(self.table.queued.has_track(self.track)) rows = self.sql("SELECT trackid FROM user_tracks WHERE propertyid=?", self.table.queued.propertyid).fetchall() self.assertEqual(len(rows), 1) self.assertEqual(rows[0]["trackid"], self.track.trackid) self.assertEqual(self.sql.active_playlist, self.table.queued) self.sql.set_active_playlist(self.table.collection) self.table.queued.add_track(self.track) self.assertEqual(self.sql.active_playlist, self.table.queued) self.library.deleting = True self.table.queued.reload_tracks() self.assertFalse(self.table.queued.has_track(self.track)) self.library.deleting = False self.table.queued.reload_tracks() self.assertTrue(self.table.queued.has_track(self.track)) self.table.queued.remove_track(self.track) self.assertFalse(self.table.queued.has_track(self.track)) rows = self.sql("SELECT trackid FROM user_tracks WHERE propertyid=?", self.table.queued.propertyid).fetchall() self.assertEqual(len(rows), 0) def test_unplayed(self): """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) def test_unplayed_tracks(self): """Test the Unplayed Tracks track functions.""" self.table.unplayed.add_track(self.track) self.assertTrue(self.table.unplayed.has_track(self.track)) self.library.deleting = True self.table.unplayed.reload_tracks() self.assertFalse(self.table.unplayed.has_track(self.track)) self.library.deleting = False self.table.unplayed.reload_tracks() self.assertTrue(self.table.unplayed.has_track(self.track)) self.table.unplayed.remove_track(self.track) self.assertFalse(self.table.unplayed.has_track(self.track)) self.sql("UPDATE tracks SET playcount=1 WHERE trackid=?", self.track.trackid) self.table.unplayed.reload_tracks() self.assertFalse(self.table.unplayed.has_track(self.track))