Compare commits

...

4 Commits

Author SHA1 Message Date
Anna Schumaker c5f9608c49 tracklist: Commit the database after adding, moving, or removing tracks
Otherwise we could lose the changes if the app crashes.

Fixes: #63 ("The database isn't being committed enough")
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-07 14:58:19 -04:00
Anna Schumaker cae93cae11 sidebar: Commit the database after creating a new playlist
Otherwise we could have data loss if the application crashes.

Fixes: #63 ("The database isn't being committed enough")
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-07 14:58:19 -04:00
Anna Schumaker 01a37dbbc1 db: Commit the database after deleting a Table Row
Fixes: #63 ("The database isn't being committed enough")
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-07 14:54:32 -04:00
Anna Schumaker 14c487c295 db: Commit the database when a Track has been started or stopped
Leaving the database in a dirty state could cause unintentional data
loss if the app crashes.

Fixes: #63 ("The database isn't being committed enough")
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-07 13:26:55 -04:00
9 changed files with 115 additions and 72 deletions

View File

@ -75,6 +75,10 @@ class Connection(GObject.GObject):
self._sql.close()
self.connected = False
def commit(self) -> None:
"""Commit pending changes."""
self._sql.commit()
def executemany(self, statement: str, *args) -> sqlite3.Cursor | None:
"""Execute several similar SQL statements at once."""
try:

View File

@ -188,6 +188,7 @@ class Table(Gtk.FilterListModel):
def delete(self, row: Row) -> bool:
"""Delete a Row from the Table."""
if row in self and self.do_sql_delete(row).rowcount == 1:
self.sql.commit()
self.store.remove(row)
del self.rows[row.primary_key]
return True

View File

@ -241,6 +241,7 @@ class Table(table.Table):
track.active = True
track.laststarted = cur.fetchone()["laststarted"]
self.current_track = track
self.sql.commit()
def stop_track(self, track: Track, played: bool) -> None:
"""Mark that a Track has been stopped."""
@ -270,6 +271,8 @@ class Table(table.Table):
self.sql.playlists.queued.remove_track(track)
self.sql.playlists.unplayed.remove_track(track)
self.sql.commit()
class TrackidSet(GObject.GObject):
"""Manage a set of Track IDs."""

View File

@ -59,6 +59,7 @@ class Section(section.Section):
def __add_new_playlist(self, entry: Gtk.Entry) -> None:
if self.table.create(entry.get_text()) is not None:
self.table.sql.commit()
self.extra_widget.popdown()
def __entry_changed(self, entry: Gtk.Entry) -> None:

View File

@ -152,6 +152,7 @@ class MoveButtons(Gtk.Box):
class OSD(Gtk.Overlay):
"""An Overlay with extra controls for the Tracklist."""
sql = GObject.Property(type=db.Connection)
playlist = GObject.Property(type=playlist.playlist.Playlist)
selection = GObject.Property(type=Gtk.SelectionModel)
@ -161,7 +162,7 @@ class OSD(Gtk.Overlay):
def __init__(self, sql: db.Connection,
selection: Gtk.SelectionModel, **kwargs):
"""Initialize an OSD."""
super().__init__(selection=selection, **kwargs)
super().__init__(sql=sql, selection=selection, **kwargs)
self._add = PopoverButton(child=Adw.ButtonContent(label="Add",
icon_name="list-add-symbolic"),
halign=Gtk.Align.START, valign=Gtk.Align.END,
@ -206,12 +207,14 @@ class OSD(Gtk.Overlay):
playlist: db.playlists.Playlist) -> None:
for track in self.__get_selected_tracks():
playlist.add_track(track)
self.sql.commit()
self.clear_selection()
def __remove_clicked(self, button: Gtk.Button) -> None:
if self.playlist is not None:
for track in self.__get_selected_tracks():
self.playlist.remove_track(track)
self.sql.commit()
self.clear_selection()
def __move_track_down(self, move: MoveButtons) -> None:
@ -219,6 +222,7 @@ class OSD(Gtk.Overlay):
index = self.selection.get_selection().get_nth(0)
self.selection.get_model().set_incremental(False)
self.playlist.move_track_down(self.selection[index])
self.sql.commit()
self.selection.get_model().set_incremental(True)
self.__update_visibility()
@ -227,6 +231,7 @@ class OSD(Gtk.Overlay):
index = self.selection.get_selection().get_nth(0)
self.selection.get_model().set_incremental(False)
self.playlist.move_track_up(self.selection[index])
self.sql.commit()
self.selection.get_model().set_incremental(True)
self.__update_visibility()

View File

@ -308,9 +308,12 @@ class TestTableFunctions(tests.util.TestCase):
def test_delete(self):
"""Test deleting rows."""
row = self.table.create(number=1)
self.assertTrue(row.delete())
self.assertEqual(len(self.table), 0)
self.assertDictEqual(self.table.rows, dict())
with unittest.mock.patch.object(self.sql, "commit") as mock_commit:
self.assertTrue(row.delete())
self.assertEqual(len(self.table), 0)
self.assertDictEqual(self.table.rows, dict())
mock_commit.assert_called()
self.assertFalse(row.delete())

View File

@ -508,16 +508,20 @@ class TestTrackTable(tests.util.TestCase):
track = self.tracks.create(self.library, pathlib.Path("/a/b/1.ogg"),
self.medium, self.year)
track.start()
row = self.sql("SELECT laststarted FROM tracks WHERE trackid=?",
track.trackid).fetchone()
self.assertTrue(track.active)
self.assertIsNotNone(track.laststarted)
self.assertEqual(track.laststarted, row["laststarted"])
self.assertEqual(self.tracks.current_track, track)
with unittest.mock.patch.object(self.sql, "commit",
wraps=self.sql.commit) as mock_commit:
track.start()
mock_commit.assert_called()
self.playlists.previous.remove_track.assert_called_with(track)
self.playlists.previous.add_track.assert_called_with(track)
row = self.sql("SELECT laststarted FROM tracks WHERE trackid=?",
track.trackid).fetchone()
self.assertTrue(track.active)
self.assertIsNotNone(track.laststarted)
self.assertEqual(track.laststarted, row["laststarted"])
self.assertEqual(self.tracks.current_track, track)
self.playlists.previous.remove_track.assert_called_with(track)
self.playlists.previous.add_track.assert_called_with(track)
def test_stop_started_track(self):
"""Test marking that a Track has stopped playback."""
@ -525,31 +529,39 @@ class TestTrackTable(tests.util.TestCase):
self.medium, self.year, length=10)
track.start()
track.stop(3)
row = self.sql("SELECT lastplayed FROM tracks WHERE trackid=?",
track.trackid).fetchone()
self.assertFalse(track.active)
self.assertEqual(track.playcount, 0)
self.assertIsNone(row["lastplayed"])
self.assertIsNone(track.lastplayed)
self.assertIsNone(self.tracks.current_track)
with unittest.mock.patch.object(self.sql, "commit",
wraps=self.sql.commit) as mock_commit:
track.stop(3)
mock_commit.assert_called()
self.playlists.most_played.reload_tracks.assert_not_called()
self.playlists.queued.remove_track.assert_not_called()
self.playlists.unplayed.remove_track.assert_not_called()
row = self.sql("SELECT lastplayed FROM tracks WHERE trackid=?",
track.trackid).fetchone()
self.assertFalse(track.active)
self.assertEqual(track.playcount, 0)
self.assertIsNone(row["lastplayed"])
self.assertIsNone(track.lastplayed)
self.assertIsNone(self.tracks.current_track)
self.playlists.most_played.reload_tracks.assert_not_called()
self.playlists.queued.remove_track.assert_not_called()
self.playlists.unplayed.remove_track.assert_not_called()
track.start()
track.stop(8)
row = self.sql("""SELECT lastplayed, playcount FROM tracks
WHERE trackid=?""", track.trackid).fetchone()
self.assertEqual(row["playcount"], 1)
self.assertEqual(track.playcount, 1)
self.assertEqual(row["lastplayed"], track.laststarted)
self.assertEqual(track.lastplayed, track.laststarted)
with unittest.mock.patch.object(self.sql, "commit",
wraps=self.sql.commit) as mock_commit:
track.stop(8)
mock_commit.assert_called()
self.playlists.most_played.reload_tracks.assert_called()
self.playlists.queued.remove_track.assert_called_with(track)
self.playlists.unplayed.remove_track.assert_called_with(track)
row = self.sql("""SELECT lastplayed, playcount FROM tracks
WHERE trackid=?""", track.trackid).fetchone()
self.assertEqual(row["playcount"], 1)
self.assertEqual(track.playcount, 1)
self.assertEqual(row["lastplayed"], track.laststarted)
self.assertEqual(track.lastplayed, track.laststarted)
self.playlists.most_played.reload_tracks.assert_called()
self.playlists.queued.remove_track.assert_called_with(track)
self.playlists.unplayed.remove_track.assert_called_with(track)
def test_stop_restarted_track(self):
"""Test marking that a restarted Track has stopped playback."""

View File

@ -68,23 +68,28 @@ class TestPlaylists(tests.util.TestCase):
with unittest.mock.patch.object(self.playlists.extra_widget,
"popdown") as mock_popdown:
self.playlists._entry.emit("activate")
self.assertEqual(len(self.sql.playlists), 0)
mock_popdown.assert_not_called()
with unittest.mock.patch.object(self.sql, "commit") as mock_commit:
self.playlists._entry.emit("activate")
self.assertEqual(len(self.sql.playlists), 0)
mock_popdown.assert_not_called()
mock_commit.assert_not_called()
self.playlists._entry.set_text("Test 1")
self.playlists._entry.emit("activate")
self.assertEqual(len(self.sql.playlists), 1)
self.assertEqual(self.sql.playlists.get_item(0).name, "Test 1")
mock_popdown.assert_called()
self.playlists._entry.set_text("Test 1")
self.playlists._entry.emit("activate")
self.assertEqual(len(self.sql.playlists), 1)
self.assertEqual(self.sql.playlists.get_item(0).name, "Test 1")
mock_popdown.assert_called()
mock_commit.assert_called()
mock_popdown.reset_mock()
self.playlists._entry.set_text("Test 2")
self.playlists._entry.emit("icon-release",
Gtk.EntryIconPosition.PRIMARY)
self.assertEqual(len(self.sql.playlists), 2)
self.assertEqual(self.sql.playlists.get_item(1).name, "Test 2")
mock_popdown.assert_called()
mock_popdown.reset_mock()
mock_commit.reset_mock()
self.playlists._entry.set_text("Test 2")
self.playlists._entry.emit("icon-release",
Gtk.EntryIconPosition.PRIMARY)
self.assertEqual(len(self.sql.playlists), 2)
self.assertEqual(self.sql.playlists.get_item(1).name, "Test 2")
mock_popdown.assert_called()
mock_commit.assert_called()
self.playlists._entry.set_text("Test 3")
self.assertEqual(self.playlists._entry.get_icon_name(

View File

@ -249,6 +249,7 @@ class TestOsd(tests.util.TestCase):
self.assertIsInstance(self.osd, Gtk.Overlay)
self.assertIsInstance(self.osd._sizegroup, Gtk.SizeGroup)
self.assertEqual(self.osd.selection, self.selection)
self.assertEqual(self.osd.sql, self.sql)
self.assertIsNone(self.osd.playlist)
self.assertEqual(self.osd._sizegroup.get_mode(),
@ -296,12 +297,14 @@ class TestOsd(tests.util.TestCase):
with unittest.mock.patch.object(self.db_plist,
"add_track") as mock_add:
self.selection.select_all()
self.osd._add.popover_child.emit("playlist-selected",
self.db_plist)
mock_add.assert_has_calls([unittest.mock.call(self.model[0]),
unittest.mock.call(self.model[1]),
unittest.mock.call(self.model[2])])
self.assertEqual(self.osd.n_selected, 0)
with unittest.mock.patch.object(self.sql, "commit") as mock_commit:
self.osd._add.popover_child.emit("playlist-selected",
self.db_plist)
mock_add.assert_has_calls([unittest.mock.call(self.model[0]),
unittest.mock.call(self.model[1]),
unittest.mock.call(self.model[2])])
mock_commit.assert_called_once()
self.assertEqual(self.osd.n_selected, 0)
def test_remove_button(self):
"""Test the remove tracks button."""
@ -341,16 +344,18 @@ class TestOsd(tests.util.TestCase):
"""Test clicking the remove button."""
with unittest.mock.patch.object(self.db_plist,
"remove_track") as mock_remove:
self.selection.select_all()
self.osd._remove.emit("clicked")
mock_remove.assert_not_called()
with unittest.mock.patch.object(self.sql, "commit") as mock_commit:
self.selection.select_all()
self.osd._remove.emit("clicked")
mock_remove.assert_not_called()
mock_commit.assert_not_called()
self.osd.playlist = self.playlist
self.selection.select_all()
self.osd._remove.emit("clicked")
mock_remove.assert_has_calls([unittest.mock.call(self.model[0]),
unittest.mock.call(self.model[1]),
unittest.mock.call(self.model[2])])
self.osd.playlist = self.playlist
self.selection.select_all()
self.osd._remove.emit("clicked")
mock_remove.assert_has_calls([unittest.mock.call(self.model[i])
for i in range(3)])
mock_commit.assert_called_once()
def test_move_buttons(self):
"""Test the move buttons."""
@ -372,18 +377,22 @@ class TestOsd(tests.util.TestCase):
with unittest.mock.patch.object(self.playlist,
"move_track_down") as mock_move_down:
self.osd._move.emit("move-down")
mock_move_down.assert_called_with(self.model[1])
set_incremental.assert_has_calls([unittest.mock.call(False),
unittest.mock.call(True)])
with unittest.mock.patch.object(self.sql, "commit") as mock_commit:
self.osd._move.emit("move-down")
mock_move_down.assert_called_with(self.model[1])
set_incremental.assert_has_calls([unittest.mock.call(False),
unittest.mock.call(True)])
mock_commit.assert_called_once()
set_incremental.reset_mock()
with unittest.mock.patch.object(self.playlist,
"move_track_up") as mock_move_up:
self.osd._move.emit("move-up")
mock_move_up.assert_called_with(self.model[1])
set_incremental.assert_has_calls([unittest.mock.call(False),
unittest.mock.call(True)])
with unittest.mock.patch.object(self.sql, "commit") as mock_commit:
self.osd._move.emit("move-up")
mock_move_up.assert_called_with(self.model[1])
set_incremental.assert_has_calls([unittest.mock.call(False),
unittest.mock.call(True)])
mock_commit.assert_called_once()
def test_move_buttons_sensitive(self):
"""Test the move button sensitivity."""