move more logic into mediasync.py, handle auth errors
This commit is contained in:
parent
347ac80086
commit
93c768cab9
@ -84,7 +84,7 @@ class AnkiQt(QMainWindow):
|
|||||||
self.opts = opts
|
self.opts = opts
|
||||||
self.col: Optional[_Collection] = None
|
self.col: Optional[_Collection] = None
|
||||||
self.taskman = TaskManager()
|
self.taskman = TaskManager()
|
||||||
self.media_syncer = MediaSyncer(self.taskman, self._on_media_sync_start_stop)
|
self.media_syncer = MediaSyncer(self)
|
||||||
aqt.mw = self
|
aqt.mw = self
|
||||||
self.app = app
|
self.app = app
|
||||||
self.pm = profileManager
|
self.pm = profileManager
|
||||||
@ -833,7 +833,7 @@ title="%s" %s>%s</button>""" % (
|
|||||||
# collection after sync completes
|
# collection after sync completes
|
||||||
def onSync(self):
|
def onSync(self):
|
||||||
if self.media_syncer.is_syncing():
|
if self.media_syncer.is_syncing():
|
||||||
self._show_sync_log()
|
self.media_syncer.show_sync_log()
|
||||||
else:
|
else:
|
||||||
self.unloadCollection(self._onSync)
|
self.unloadCollection(self._onSync)
|
||||||
|
|
||||||
@ -841,7 +841,7 @@ title="%s" %s>%s</button>""" % (
|
|||||||
self._sync()
|
self._sync()
|
||||||
if not self.loadCollection():
|
if not self.loadCollection():
|
||||||
return
|
return
|
||||||
self._sync_media()
|
self.media_syncer.start()
|
||||||
|
|
||||||
# expects a current profile, but no collection loaded
|
# expects a current profile, but no collection loaded
|
||||||
def maybeAutoSync(self) -> None:
|
def maybeAutoSync(self) -> None:
|
||||||
@ -863,31 +863,6 @@ title="%s" %s>%s</button>""" % (
|
|||||||
self.syncer = SyncManager(self, self.pm)
|
self.syncer = SyncManager(self, self.pm)
|
||||||
self.syncer.sync()
|
self.syncer.sync()
|
||||||
|
|
||||||
# fixme: self.pm.profile["syncMedia"]
|
|
||||||
# fixme: mediaSanity
|
|
||||||
# fixme: corruptMediaDB
|
|
||||||
# fixme: hkey
|
|
||||||
# fixme: shard
|
|
||||||
# fixme: dialog
|
|
||||||
# fixme: autosync
|
|
||||||
# elif evt == "mediaSanity":
|
|
||||||
# showWarning(
|
|
||||||
# _(
|
|
||||||
# """\
|
|
||||||
# A problem occurred while syncing media. Please use Tools>Check Media, then \
|
|
||||||
# sync again to correct the issue."""
|
|
||||||
# )
|
|
||||||
# )
|
|
||||||
|
|
||||||
def _sync_media(self):
|
|
||||||
self.media_syncer.start(self.col, self.pm.sync_key(), None)
|
|
||||||
|
|
||||||
def _on_media_sync_start_stop(self):
|
|
||||||
self.toolbar.set_sync_active(self.media_syncer.is_syncing())
|
|
||||||
|
|
||||||
def _show_sync_log(self):
|
|
||||||
aqt.dialogs.open("sync_log", self, self.media_syncer)
|
|
||||||
|
|
||||||
# Tools
|
# Tools
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
@ -7,14 +7,14 @@ import time
|
|||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Callable, List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
import anki
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
from anki.lang import _
|
from anki.lang import _
|
||||||
from anki.media import media_paths_from_col_path
|
from anki.media import media_paths_from_col_path
|
||||||
from anki.rsbackend import (
|
from anki.rsbackend import (
|
||||||
|
AnkiWebAuthFailed,
|
||||||
Interrupted,
|
Interrupted,
|
||||||
MediaSyncDownloadedChanges,
|
MediaSyncDownloadedChanges,
|
||||||
MediaSyncDownloadedFiles,
|
MediaSyncDownloadedFiles,
|
||||||
@ -28,7 +28,7 @@ from anki.types import assert_impossible
|
|||||||
from anki.utils import intTime
|
from anki.utils import intTime
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
from aqt.qt import QDialog, QDialogButtonBox, QPushButton
|
from aqt.qt import QDialog, QDialogButtonBox, QPushButton
|
||||||
from aqt.taskman import TaskManager
|
from aqt.utils import showInfo
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -40,30 +40,24 @@ class MediaSyncState:
|
|||||||
removed_files: int = 0
|
removed_files: int = 0
|
||||||
|
|
||||||
|
|
||||||
# fixme: make sure we don't run twice
|
|
||||||
# fixme: handle auth errors
|
|
||||||
# fixme: handle network errors
|
# fixme: handle network errors
|
||||||
# fixme: show progress in UI
|
|
||||||
# fixme: abort when closing collection/app
|
# fixme: abort when closing collection/app
|
||||||
# fixme: handle no hkey
|
|
||||||
# fixme: shards
|
# fixme: shards
|
||||||
# fixme: dialog should be a singleton
|
# fixme: concurrent modifications during upload step
|
||||||
# fixme: abort button should not be default
|
# fixme: mediaSanity
|
||||||
|
# fixme: corruptMediaDB
|
||||||
|
# fixme: autosync
|
||||||
|
# elif evt == "mediaSanity":
|
||||||
|
# showWarning(
|
||||||
|
# _(
|
||||||
|
# """\
|
||||||
|
# A problem occurred while syncing media. Please use Tools>Check Media, then \
|
||||||
|
# sync again to correct the issue."""
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
class SyncBegun:
|
LogEntry = Union[MediaSyncState, str]
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SyncEnded:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SyncAborted:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
LogEntry = Union[MediaSyncState, SyncBegun, SyncEnded, SyncAborted]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -73,12 +67,11 @@ class LogEntryWithTime:
|
|||||||
|
|
||||||
|
|
||||||
class MediaSyncer:
|
class MediaSyncer:
|
||||||
def __init__(self, taskman: TaskManager, on_start_stop: Callable[[], None]):
|
def __init__(self, mw: aqt.main.AnkiQt):
|
||||||
self._taskman = taskman
|
self.mw = mw
|
||||||
self._sync_state: Optional[MediaSyncState] = None
|
self._sync_state: Optional[MediaSyncState] = None
|
||||||
self._log: List[LogEntryWithTime] = []
|
self._log: List[LogEntryWithTime] = []
|
||||||
self._want_stop = False
|
self._want_stop = False
|
||||||
self._on_start_stop = on_start_stop
|
|
||||||
hooks.rust_progress_callback.append(self._on_rust_progress)
|
hooks.rust_progress_callback.append(self._on_rust_progress)
|
||||||
|
|
||||||
def _on_rust_progress(self, proceed: bool, progress: Progress) -> bool:
|
def _on_rust_progress(self, proceed: bool, progress: Progress) -> bool:
|
||||||
@ -104,14 +97,22 @@ class MediaSyncer:
|
|||||||
elif isinstance(progress, MediaSyncRemovedFiles):
|
elif isinstance(progress, MediaSyncRemovedFiles):
|
||||||
self._sync_state.removed_files += progress.files
|
self._sync_state.removed_files += progress.files
|
||||||
|
|
||||||
def start(
|
def start(self) -> None:
|
||||||
self, col: anki.storage._Collection, hkey: str, shard: Optional[int]
|
|
||||||
) -> None:
|
|
||||||
"Start media syncing in the background, if it's not already running."
|
"Start media syncing in the background, if it's not already running."
|
||||||
if self._sync_state is not None:
|
if self._sync_state is not None:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._log_and_notify(SyncBegun())
|
hkey = self.mw.pm.sync_key()
|
||||||
|
if hkey is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.mw.pm.media_syncing_enabled():
|
||||||
|
self._log_and_notify(_("Media syncing disabled."))
|
||||||
|
return
|
||||||
|
|
||||||
|
shard = None
|
||||||
|
|
||||||
|
self._log_and_notify(_("Media sync starting..."))
|
||||||
self._sync_state = MediaSyncState()
|
self._sync_state = MediaSyncState()
|
||||||
self._want_stop = False
|
self._want_stop = False
|
||||||
self._on_start_stop()
|
self._on_start_stop()
|
||||||
@ -122,17 +123,17 @@ class MediaSyncer:
|
|||||||
shard_str = ""
|
shard_str = ""
|
||||||
endpoint = f"https://sync{shard_str}ankiweb.net"
|
endpoint = f"https://sync{shard_str}ankiweb.net"
|
||||||
|
|
||||||
(media_folder, media_db) = media_paths_from_col_path(col.path)
|
(media_folder, media_db) = media_paths_from_col_path(self.mw.col.path)
|
||||||
|
|
||||||
def run() -> None:
|
def run() -> None:
|
||||||
col.backend.sync_media(hkey, media_folder, media_db, endpoint)
|
self.mw.col.backend.sync_media(hkey, media_folder, media_db, endpoint)
|
||||||
|
|
||||||
self._taskman.run_in_background(run, self._on_finished)
|
self.mw.taskman.run_in_background(run, self._on_finished)
|
||||||
|
|
||||||
def _log_and_notify(self, entry: LogEntry) -> None:
|
def _log_and_notify(self, entry: LogEntry) -> None:
|
||||||
entry_with_time = LogEntryWithTime(time=intTime(), entry=entry)
|
entry_with_time = LogEntryWithTime(time=intTime(), entry=entry)
|
||||||
self._log.append(entry_with_time)
|
self._log.append(entry_with_time)
|
||||||
self._taskman.run_on_main(
|
self.mw.taskman.run_on_main(
|
||||||
lambda: gui_hooks.media_sync_did_progress(entry_with_time)
|
lambda: gui_hooks.media_sync_did_progress(entry_with_time)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -142,22 +143,38 @@ class MediaSyncer:
|
|||||||
|
|
||||||
exc = future.exception()
|
exc = future.exception()
|
||||||
if exc is not None:
|
if exc is not None:
|
||||||
if isinstance(exc, Interrupted):
|
self._handle_sync_error(exc)
|
||||||
self._log_and_notify(SyncAborted())
|
|
||||||
else:
|
|
||||||
raise exc
|
|
||||||
else:
|
else:
|
||||||
self._log_and_notify(SyncEnded())
|
self._log_and_notify(_("Media sync complete."))
|
||||||
|
|
||||||
|
def _handle_sync_error(self, exc: BaseException):
|
||||||
|
if isinstance(exc, AnkiWebAuthFailed):
|
||||||
|
self.mw.pm.set_sync_key(None)
|
||||||
|
self._log_and_notify(_("Authentication failed."))
|
||||||
|
showInfo(_("AnkiWeb ID or password was incorrect; please try again."))
|
||||||
|
elif isinstance(exc, Interrupted):
|
||||||
|
self._log_and_notify(_("Media sync aborted."))
|
||||||
|
else:
|
||||||
|
raise exc
|
||||||
|
|
||||||
def entries(self) -> List[LogEntryWithTime]:
|
def entries(self) -> List[LogEntryWithTime]:
|
||||||
return self._log
|
return self._log
|
||||||
|
|
||||||
def abort(self) -> None:
|
def abort(self) -> None:
|
||||||
|
if not self.is_syncing():
|
||||||
|
return
|
||||||
|
self._log_and_notify(_("Media sync aborting..."))
|
||||||
self._want_stop = True
|
self._want_stop = True
|
||||||
|
|
||||||
def is_syncing(self) -> bool:
|
def is_syncing(self) -> bool:
|
||||||
return self._sync_state is not None
|
return self._sync_state is not None
|
||||||
|
|
||||||
|
def _on_start_stop(self):
|
||||||
|
self.mw.toolbar.set_sync_active(self.is_syncing())
|
||||||
|
|
||||||
|
def show_sync_log(self):
|
||||||
|
aqt.dialogs.open("sync_log", self.mw, self)
|
||||||
|
|
||||||
|
|
||||||
class MediaSyncDialog(QDialog):
|
class MediaSyncDialog(QDialog):
|
||||||
silentlyClose = True
|
silentlyClose = True
|
||||||
@ -170,6 +187,7 @@ class MediaSyncDialog(QDialog):
|
|||||||
self.form.setupUi(self)
|
self.form.setupUi(self)
|
||||||
self.abort_button = QPushButton(_("Abort"))
|
self.abort_button = QPushButton(_("Abort"))
|
||||||
self.abort_button.clicked.connect(self._on_abort) # type: ignore
|
self.abort_button.clicked.connect(self._on_abort) # type: ignore
|
||||||
|
self.abort_button.setAutoDefault(False)
|
||||||
self.form.buttonBox.addButton(self.abort_button, QDialogButtonBox.ActionRole)
|
self.form.buttonBox.addButton(self.abort_button, QDialogButtonBox.ActionRole)
|
||||||
|
|
||||||
gui_hooks.media_sync_did_progress.append(self._on_log_entry)
|
gui_hooks.media_sync_did_progress.append(self._on_log_entry)
|
||||||
@ -191,9 +209,6 @@ class MediaSyncDialog(QDialog):
|
|||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def _on_abort(self, *args) -> None:
|
def _on_abort(self, *args) -> None:
|
||||||
self.form.plainTextEdit.appendPlainText(
|
|
||||||
self._time_and_text(intTime(), _("Aborting..."))
|
|
||||||
)
|
|
||||||
self._syncer.abort()
|
self._syncer.abort()
|
||||||
self.abort_button.setHidden(True)
|
self.abort_button.setHidden(True)
|
||||||
|
|
||||||
@ -202,12 +217,8 @@ class MediaSyncDialog(QDialog):
|
|||||||
return f"{asctime}: {text}"
|
return f"{asctime}: {text}"
|
||||||
|
|
||||||
def _entry_to_text(self, entry: LogEntryWithTime):
|
def _entry_to_text(self, entry: LogEntryWithTime):
|
||||||
if isinstance(entry.entry, SyncBegun):
|
if isinstance(entry.entry, str):
|
||||||
txt = _("Media sync starting...")
|
txt = entry.entry
|
||||||
elif isinstance(entry.entry, SyncEnded):
|
|
||||||
txt = _("Media sync complete.")
|
|
||||||
elif isinstance(entry.entry, SyncAborted):
|
|
||||||
txt = _("Aborted.")
|
|
||||||
elif isinstance(entry.entry, MediaSyncState):
|
elif isinstance(entry.entry, MediaSyncState):
|
||||||
txt = self._logentry_to_text(entry.entry)
|
txt = self._logentry_to_text(entry.entry)
|
||||||
else:
|
else:
|
||||||
@ -227,3 +238,5 @@ class MediaSyncDialog(QDialog):
|
|||||||
|
|
||||||
def _on_log_entry(self, entry: LogEntryWithTime):
|
def _on_log_entry(self, entry: LogEntryWithTime):
|
||||||
self.form.plainTextEdit.appendPlainText(self._entry_to_text(entry))
|
self.form.plainTextEdit.appendPlainText(self._entry_to_text(entry))
|
||||||
|
if not self._syncer.is_syncing():
|
||||||
|
self.abort_button.setHidden(True)
|
||||||
|
@ -515,6 +515,12 @@ please see:
|
|||||||
def sync_key(self) -> Optional[str]:
|
def sync_key(self) -> Optional[str]:
|
||||||
return self.profile.get("syncKey")
|
return self.profile.get("syncKey")
|
||||||
|
|
||||||
|
def set_sync_key(self, val: Optional[str]) -> None:
|
||||||
|
self.profile["syncKey"] = val
|
||||||
|
|
||||||
|
def media_syncing_enabled(self) -> bool:
|
||||||
|
return self.profile["syncMedia"]
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def apply_profile_options(self) -> None:
|
def apply_profile_options(self) -> None:
|
||||||
|
Loading…
Reference in New Issue
Block a user