191 lines
6.1 KiB
Python
191 lines
6.1 KiB
Python
# Copyright: Ankitects Pty Ltd and contributors
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from concurrent.futures import Future
|
|
from datetime import datetime
|
|
from typing import Any, Callable
|
|
|
|
import aqt
|
|
import aqt.forms
|
|
import aqt.main
|
|
from anki.collection import Collection
|
|
from anki.errors import Interrupted
|
|
from anki.utils import int_time
|
|
from aqt import gui_hooks
|
|
from aqt.operations import QueryOp
|
|
from aqt.qt import QDialog, QDialogButtonBox, QPushButton, Qt, QTimer, qconnect
|
|
from aqt.utils import disable_help_button, show_info, tr
|
|
|
|
|
|
class MediaSyncer:
|
|
def __init__(self, mw: aqt.main.AnkiQt) -> None:
|
|
self.mw = mw
|
|
self._syncing: bool = False
|
|
self.last_progress = ""
|
|
self._last_progress_at = 0
|
|
gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop)
|
|
|
|
def start(self) -> None:
|
|
"Start media syncing in the background, if it's not already running."
|
|
if not self.mw.pm.media_syncing_enabled() or not (
|
|
auth := self.mw.pm.sync_auth()
|
|
):
|
|
return
|
|
|
|
def run(col: Collection) -> None:
|
|
col.sync_media(auth)
|
|
|
|
# this will exit after the thread is spawned, but may block if there's an existing
|
|
# backend lock
|
|
QueryOp(parent=aqt.mw, op=run, success=lambda _: 1).run_in_background()
|
|
|
|
self.start_monitoring()
|
|
|
|
def start_monitoring(self) -> None:
|
|
if self._syncing:
|
|
return
|
|
self._syncing = True
|
|
gui_hooks.media_sync_did_start_or_stop(True)
|
|
self._update_progress(tr.sync_media_starting())
|
|
|
|
def monitor() -> None:
|
|
while True:
|
|
resp = self.mw.col.media_sync_status()
|
|
if not resp.active:
|
|
return
|
|
if p := resp.progress:
|
|
self._update_progress(f"{p.added}, {p.removed}, {p.checked}")
|
|
|
|
time.sleep(0.25)
|
|
|
|
self.mw.taskman.run_in_background(
|
|
monitor, self._on_finished, uses_collection=False
|
|
)
|
|
|
|
def _update_progress(self, progress: str) -> None:
|
|
self.last_progress = progress
|
|
self.mw.taskman.run_on_main(lambda: gui_hooks.media_sync_did_progress(progress))
|
|
|
|
def _on_finished(self, future: Future) -> None:
|
|
self._syncing = False
|
|
self._last_progress_at = int_time()
|
|
gui_hooks.media_sync_did_start_or_stop(False)
|
|
|
|
exc = future.exception()
|
|
if exc is not None:
|
|
self._handle_sync_error(exc)
|
|
else:
|
|
self._update_progress(tr.sync_media_complete())
|
|
|
|
def _handle_sync_error(self, exc: BaseException) -> None:
|
|
if isinstance(exc, Interrupted):
|
|
self._update_progress(tr.sync_media_aborted())
|
|
return
|
|
else:
|
|
show_info(str(exc), modality=Qt.WindowModality.NonModal)
|
|
return
|
|
|
|
def abort(self) -> None:
|
|
if not self.is_syncing():
|
|
return
|
|
self.mw.col.set_wants_abort()
|
|
self.mw.col.abort_media_sync()
|
|
self._update_progress(tr.sync_media_aborting())
|
|
|
|
def is_syncing(self) -> bool:
|
|
return self._syncing
|
|
|
|
def _on_start_stop(self, running: bool) -> None:
|
|
self.mw.toolbar.set_sync_active(running)
|
|
|
|
def show_sync_log(self) -> None:
|
|
aqt.dialogs.open("sync_log", self.mw, self)
|
|
|
|
def show_diag_until_finished(self, on_finished: Callable[[], None]) -> None:
|
|
# nothing to do if not syncing
|
|
if not self.is_syncing():
|
|
return on_finished()
|
|
|
|
diag: MediaSyncDialog = aqt.dialogs.open("sync_log", self.mw, self, True)
|
|
diag.show()
|
|
|
|
timer: QTimer | None = None
|
|
|
|
def check_finished() -> None:
|
|
if not self.is_syncing():
|
|
timer.deleteLater()
|
|
on_finished()
|
|
|
|
timer = self.mw.progress.timer(150, check_finished, True, False, parent=self.mw)
|
|
|
|
def seconds_since_last_sync(self) -> int:
|
|
if self.is_syncing():
|
|
return 0
|
|
|
|
return int_time() - self._last_progress_at
|
|
|
|
|
|
class MediaSyncDialog(QDialog):
|
|
silentlyClose = True
|
|
|
|
def __init__(
|
|
self, mw: aqt.main.AnkiQt, syncer: MediaSyncer, close_when_done: bool = False
|
|
) -> None:
|
|
super().__init__(mw)
|
|
self.mw = mw
|
|
self._syncer = syncer
|
|
self._close_when_done = close_when_done
|
|
self.form = aqt.forms.synclog.Ui_Dialog()
|
|
self.form.setupUi(self)
|
|
self.setWindowTitle(tr.sync_media_log_title())
|
|
disable_help_button(self)
|
|
self.abort_button = QPushButton(tr.sync_abort_button())
|
|
qconnect(self.abort_button.clicked, self._on_abort)
|
|
self.abort_button.setAutoDefault(False)
|
|
self.form.buttonBox.addButton(
|
|
self.abort_button, QDialogButtonBox.ButtonRole.ActionRole
|
|
)
|
|
self.abort_button.setHidden(not self._syncer.is_syncing())
|
|
|
|
gui_hooks.media_sync_did_progress.append(self._on_log_entry)
|
|
gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop)
|
|
|
|
self._on_log_entry(syncer.last_progress)
|
|
self.show()
|
|
|
|
def reject(self) -> None:
|
|
if self._close_when_done and self._syncer.is_syncing():
|
|
# closing while syncing on close starts an abort
|
|
self._on_abort()
|
|
return
|
|
|
|
aqt.dialogs.markClosed("sync_log")
|
|
QDialog.reject(self)
|
|
|
|
def reopen(
|
|
self, mw: aqt.AnkiQt, syncer: Any, close_when_done: bool = False
|
|
) -> None:
|
|
self._close_when_done = close_when_done
|
|
self.show()
|
|
|
|
def _on_abort(self, *_args: Any) -> None:
|
|
self._syncer.abort()
|
|
self.abort_button.setHidden(True)
|
|
|
|
def _on_log_entry(self, entry: str) -> None:
|
|
dt = datetime.fromtimestamp(int_time())
|
|
time = dt.strftime("%H:%M:%S")
|
|
text = f"{time}: {entry}"
|
|
self.form.log_label.setText(text)
|
|
if not self._syncer.is_syncing():
|
|
self.abort_button.setHidden(True)
|
|
|
|
def _on_start_stop(self, running: bool) -> None:
|
|
if not running and self._close_when_done:
|
|
aqt.dialogs.markClosed("sync_log")
|
|
self._close_when_done = False
|
|
self.close()
|