v0.2.2: device-sync guards — quit warning, sleep/shutdown inhibition
Quitting mid-transfer now asks (Keep Syncing / Quit Anyway) from closeEvent, which every quit path hits — including the self-update restart, which previously bypassed closeEvent via a bare quit() and now routes through close(). A running sync holds its own GNOME inhibitor (suspend + logout, "Syncing a playlist to a device") so the machine won't sleep or shutdown/restart under a copy. Along the way: the Inhibit D-Bus call marshaled Python ints as signed against GNOME's (susu) signature, so every call was rejected and the playback sleep inhibitor had silently never worked — _uint fixes both holders (verified live: cookies taken, IsInhibited flips, releases clean). Also investigated trav's mid-copy Syncthing question: the transfer works from a click-time snapshot and never reads live library state, so remote changes can't corrupt it — no state freeze needed. Hardened the one real gap: a source file relocated under the queue (remote metadata edit moving files) is now skipped, dropped from the m3u, and reported, instead of aborting the whole sync.
This commit is contained in:
@@ -12,7 +12,7 @@ from PyQt6.QtGui import QAction, QKeySequence
|
||||
from lintunes import device_sync, mpris, tagging, theme
|
||||
from lintunes.art_search import AlbumArtFetcher
|
||||
from lintunes.eventlog import log_control
|
||||
from lintunes.inhibit import SleepInhibitor
|
||||
from lintunes.inhibit import INHIBIT_LOGOUT, INHIBIT_SUSPEND, SleepInhibitor
|
||||
from lintunes.player import Player
|
||||
from lintunes.importers import file_importer
|
||||
from lintunes.gui.album_art_dialog import AlbumArtDialog
|
||||
@@ -116,6 +116,12 @@ class MainWindow(QMainWindow):
|
||||
self.statusBar().addPermanentWidget(self._sync_progress)
|
||||
self._sync_worker = None
|
||||
self._sync_device_name = ""
|
||||
# Separate instance from the playback inhibitor: pausing music
|
||||
# mid-sync must not drop the sync's hold. Logout flag included so
|
||||
# GNOME's shutdown/restart dialog names the transfer as the blocker.
|
||||
self._sync_inhibitor = SleepInhibitor(
|
||||
reason="Syncing a playlist to a device",
|
||||
flags=INHIBIT_LOGOUT | INHIBIT_SUSPEND)
|
||||
|
||||
# Wiring
|
||||
self._transport.play_clicked.connect(self.play_pause)
|
||||
@@ -158,10 +164,14 @@ class MainWindow(QMainWindow):
|
||||
self._update_totals()
|
||||
|
||||
def _restart_for_update(self):
|
||||
# The pull already succeeded; quit through the normal path (flushes
|
||||
# the library, tears down the player) and let main() re-exec us.
|
||||
# The pull already succeeded. Route through close() — not a bare
|
||||
# quit() — so closeEvent runs: it flushes the library, tears down the
|
||||
# player, and can veto the restart while a device sync is copying.
|
||||
self.restart_requested = True
|
||||
QApplication.instance().quit()
|
||||
if self.close():
|
||||
QApplication.instance().quit()
|
||||
else:
|
||||
self.restart_requested = False
|
||||
|
||||
# ---- status bar totals ----
|
||||
|
||||
@@ -322,6 +332,7 @@ class MainWindow(QMainWindow):
|
||||
self._sync_progress.setValue(0)
|
||||
self._sync_label.show()
|
||||
self._sync_progress.show()
|
||||
self._sync_inhibitor.inhibit()
|
||||
self._sync_worker.start()
|
||||
|
||||
def _on_sync_progress(self, done_kib, total_kib, label):
|
||||
@@ -333,6 +344,7 @@ class MainWindow(QMainWindow):
|
||||
f"{device_sync.format_bytes(total_kib * 1024)}")
|
||||
|
||||
def _on_sync_finished(self, summary):
|
||||
self._sync_inhibitor.release()
|
||||
self._sync_label.hide()
|
||||
self._sync_progress.hide()
|
||||
msg = (f"Synced “{summary['playlist']}” to the {summary['device']}: "
|
||||
@@ -340,9 +352,13 @@ class MainWindow(QMainWindow):
|
||||
f"{summary['removed']} removed")
|
||||
if summary["skipped"]:
|
||||
msg += f", {summary['skipped']} skipped (no local file)"
|
||||
if summary.get("vanished"):
|
||||
msg += (f", {summary['vanished']} changed under us "
|
||||
"(re-sync to pick them up)")
|
||||
self.statusBar().showMessage(msg, 8000)
|
||||
|
||||
def _on_sync_failed(self, message):
|
||||
self._sync_inhibitor.release()
|
||||
self._sync_label.hide()
|
||||
self._sync_progress.hide()
|
||||
QMessageBox.warning(self, "Sync failed", message)
|
||||
@@ -682,7 +698,26 @@ class MainWindow(QMainWindow):
|
||||
self._inhibitor.release()
|
||||
|
||||
def closeEvent(self, event):
|
||||
# Quitting mid-transfer abandons a partial file on the device (it
|
||||
# self-heals next sync, but silently) — make it a deliberate choice.
|
||||
if self._sync_worker is not None and self._sync_worker.busy():
|
||||
box = QMessageBox(self)
|
||||
box.setWindowTitle("Sync in progress")
|
||||
box.setText(
|
||||
f"Still copying to the {self._sync_device_name}.\n"
|
||||
"If you quit now, the unfinished track will be re-copied "
|
||||
"on the next sync.")
|
||||
quit_button = box.addButton(
|
||||
"Quit Anyway", QMessageBox.ButtonRole.DestructiveRole)
|
||||
keep = box.addButton(
|
||||
"Keep Syncing", QMessageBox.ButtonRole.RejectRole)
|
||||
box.setDefaultButton(keep)
|
||||
box.exec()
|
||||
if box.clickedButton() is not quit_button:
|
||||
event.ignore()
|
||||
return
|
||||
self._inhibitor.release()
|
||||
self._sync_inhibitor.release()
|
||||
self.player.shutdown()
|
||||
self._manager.flush()
|
||||
super().closeEvent(event)
|
||||
|
||||
Reference in New Issue
Block a user