mirror of
https://github.com/ankitects/anki.git
synced 2025-09-20 23:12:21 -04:00
connect media sync progress to Python, add a progress dialog
This commit is contained in:
parent
c82cff3836
commit
ea4de9a6de
16 changed files with 672 additions and 32 deletions
|
@ -26,6 +26,7 @@ message BackendInput {
|
||||||
ExtractAVTagsIn extract_av_tags = 24;
|
ExtractAVTagsIn extract_av_tags = 24;
|
||||||
string expand_clozes_to_reveal_latex = 25;
|
string expand_clozes_to_reveal_latex = 25;
|
||||||
AddFileToMediaFolderIn add_file_to_media_folder = 26;
|
AddFileToMediaFolderIn add_file_to_media_folder = 26;
|
||||||
|
SyncMediaIn sync_media = 27;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@ message BackendOutput {
|
||||||
ExtractAVTagsOut extract_av_tags = 24;
|
ExtractAVTagsOut extract_av_tags = 24;
|
||||||
string expand_clozes_to_reveal_latex = 25;
|
string expand_clozes_to_reveal_latex = 25;
|
||||||
string add_file_to_media_folder = 26;
|
string add_file_to_media_folder = 26;
|
||||||
|
Empty sync_media = 27;
|
||||||
|
|
||||||
BackendError error = 2047;
|
BackendError error = 2047;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +52,7 @@ message BackendOutput {
|
||||||
message BackendError {
|
message BackendError {
|
||||||
oneof value {
|
oneof value {
|
||||||
StringError invalid_input = 1;
|
StringError invalid_input = 1;
|
||||||
StringError template_parse = 2;
|
TemplateParseError template_parse = 2;
|
||||||
StringError io_error = 3;
|
StringError io_error = 3;
|
||||||
StringError db_error = 4;
|
StringError db_error = 4;
|
||||||
StringError network_error = 5;
|
StringError network_error = 5;
|
||||||
|
@ -61,11 +63,35 @@ message BackendError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Progress {
|
||||||
|
oneof value {
|
||||||
|
MediaSyncProgress media_sync = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
message StringError {
|
message StringError {
|
||||||
string info = 1;
|
string info = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TemplateParseError {
|
||||||
|
string info = 1;
|
||||||
bool q_side = 2;
|
bool q_side = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message MediaSyncProgress {
|
||||||
|
oneof value {
|
||||||
|
uint32 downloaded_changes = 1;
|
||||||
|
uint32 downloaded_files = 2;
|
||||||
|
MediaSyncUploadProgress uploaded = 3;
|
||||||
|
uint32 removed_files = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message MediaSyncUploadProgress {
|
||||||
|
uint32 files = 1;
|
||||||
|
uint32 deletions = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message TemplateRequirementsIn {
|
message TemplateRequirementsIn {
|
||||||
repeated string template_front = 1;
|
repeated string template_front = 1;
|
||||||
map<string, uint32> field_names_to_ordinals = 2;
|
map<string, uint32> field_names_to_ordinals = 2;
|
||||||
|
@ -190,3 +216,10 @@ message AddFileToMediaFolderIn {
|
||||||
string desired_name = 1;
|
string desired_name = 1;
|
||||||
bytes data = 2;
|
bytes data = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message SyncMediaIn {
|
||||||
|
string hkey = 1;
|
||||||
|
string media_folder = 2;
|
||||||
|
string media_db = 3;
|
||||||
|
string endpoint = 4;
|
||||||
|
}
|
|
@ -360,6 +360,33 @@ class _NotesWillBeDeletedHook:
|
||||||
notes_will_be_deleted = _NotesWillBeDeletedHook()
|
notes_will_be_deleted = _NotesWillBeDeletedHook()
|
||||||
|
|
||||||
|
|
||||||
|
class _RustProgressCallbackFilter:
|
||||||
|
"""Warning: this is called on a background thread."""
|
||||||
|
|
||||||
|
_hooks: List[Callable[[bool, "anki.rsbackend.Progress"], bool]] = []
|
||||||
|
|
||||||
|
def append(self, cb: Callable[[bool, "anki.rsbackend.Progress"], bool]) -> None:
|
||||||
|
"""(proceed: bool, progress: anki.rsbackend.Progress)"""
|
||||||
|
self._hooks.append(cb)
|
||||||
|
|
||||||
|
def remove(self, cb: Callable[[bool, "anki.rsbackend.Progress"], bool]) -> None:
|
||||||
|
if cb in self._hooks:
|
||||||
|
self._hooks.remove(cb)
|
||||||
|
|
||||||
|
def __call__(self, proceed: bool, progress: anki.rsbackend.Progress) -> bool:
|
||||||
|
for filter in self._hooks:
|
||||||
|
try:
|
||||||
|
proceed = filter(proceed, progress)
|
||||||
|
except:
|
||||||
|
# if the hook fails, remove it
|
||||||
|
self._hooks.remove(filter)
|
||||||
|
raise
|
||||||
|
return proceed
|
||||||
|
|
||||||
|
|
||||||
|
rust_progress_callback = _RustProgressCallbackFilter()
|
||||||
|
|
||||||
|
|
||||||
class _Schedv2DidAnswerReviewCardHook:
|
class _Schedv2DidAnswerReviewCardHook:
|
||||||
_hooks: List[Callable[["anki.cards.Card", int, bool], None]] = []
|
_hooks: List[Callable[["anki.cards.Card", int, bool], None]] = []
|
||||||
|
|
||||||
|
|
|
@ -1,32 +1,78 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
# pylint: skip-file
|
# pylint: skip-file
|
||||||
|
import enum
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, List, Tuple, Union
|
from typing import Callable, Dict, List, NewType, NoReturn, Optional, Tuple, Union
|
||||||
|
|
||||||
import ankirspy # pytype: disable=import-error
|
import ankirspy # pytype: disable=import-error
|
||||||
|
|
||||||
import anki.backend_pb2 as pb
|
import anki.backend_pb2 as pb
|
||||||
import anki.buildinfo
|
import anki.buildinfo
|
||||||
|
from anki import hooks
|
||||||
from anki.models import AllTemplateReqs
|
from anki.models import AllTemplateReqs
|
||||||
from anki.sound import AVTag, SoundOrVideoTag, TTSTag
|
from anki.sound import AVTag, SoundOrVideoTag, TTSTag
|
||||||
|
from anki.types import assert_impossible_literal
|
||||||
|
|
||||||
assert ankirspy.buildhash() == anki.buildinfo.buildhash
|
assert ankirspy.buildhash() == anki.buildinfo.buildhash
|
||||||
|
|
||||||
SchedTimingToday = pb.SchedTimingTodayOut
|
SchedTimingToday = pb.SchedTimingTodayOut
|
||||||
|
|
||||||
|
|
||||||
class BackendException(Exception):
|
class Interrupted(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class StringError(Exception):
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
err: pb.BackendError = self.args[0] # pylint: disable=unsubscriptable-object
|
return self.args[0] # pylint: disable=unsubscriptable-object
|
||||||
kind = err.WhichOneof("value")
|
|
||||||
if kind == "invalid_input":
|
|
||||||
return f"invalid input: {err.invalid_input.info}"
|
class NetworkError(StringError):
|
||||||
elif kind == "template_parse":
|
pass
|
||||||
return err.template_parse.info
|
|
||||||
|
|
||||||
|
class IOError(StringError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DBError(StringError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateError(StringError):
|
||||||
|
def q_side(self) -> bool:
|
||||||
|
return self.args[1]
|
||||||
|
|
||||||
|
|
||||||
|
class AnkiWebError(StringError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AnkiWebAuthFailed(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def proto_exception_to_native(err: pb.BackendError) -> Exception:
|
||||||
|
val = err.WhichOneof("value")
|
||||||
|
if val == "interrupted":
|
||||||
|
return Interrupted()
|
||||||
|
elif val == "network_error":
|
||||||
|
return NetworkError(err.network_error.info)
|
||||||
|
elif val == "io_error":
|
||||||
|
return IOError(err.io_error.info)
|
||||||
|
elif val == "db_error":
|
||||||
|
return DBError(err.db_error.info)
|
||||||
|
elif val == "template_parse":
|
||||||
|
return TemplateError(err.template_parse.info, err.template_parse.q_side)
|
||||||
|
elif val == "invalid_input":
|
||||||
|
return StringError(err.invalid_input.info)
|
||||||
|
elif val == "ankiweb_auth_failed":
|
||||||
|
return AnkiWebAuthFailed()
|
||||||
|
elif val == "ankiweb_misc_error":
|
||||||
|
return AnkiWebError(err.ankiweb_misc_error.info)
|
||||||
else:
|
else:
|
||||||
return f"unhandled error: {err}"
|
assert_impossible_literal(val)
|
||||||
|
|
||||||
|
|
||||||
def proto_template_reqs_to_legacy(
|
def proto_template_reqs_to_legacy(
|
||||||
|
@ -71,6 +117,45 @@ class TemplateReplacement:
|
||||||
TemplateReplacementList = List[Union[str, TemplateReplacement]]
|
TemplateReplacementList = List[Union[str, TemplateReplacement]]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MediaSyncDownloadedChanges:
|
||||||
|
changes: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MediaSyncDownloadedFiles:
|
||||||
|
files: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MediaSyncUploaded:
|
||||||
|
files: int
|
||||||
|
deletions: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MediaSyncRemovedFiles:
|
||||||
|
files: int
|
||||||
|
|
||||||
|
|
||||||
|
MediaSyncProgress = Union[
|
||||||
|
MediaSyncDownloadedChanges,
|
||||||
|
MediaSyncDownloadedFiles,
|
||||||
|
MediaSyncUploaded,
|
||||||
|
MediaSyncRemovedFiles,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressKind(enum.Enum):
|
||||||
|
MediaSyncProgress = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Progress:
|
||||||
|
kind: ProgressKind
|
||||||
|
val: Union[MediaSyncProgress]
|
||||||
|
|
||||||
|
|
||||||
def proto_replacement_list_to_native(
|
def proto_replacement_list_to_native(
|
||||||
nodes: List[pb.RenderedTemplateNode],
|
nodes: List[pb.RenderedTemplateNode],
|
||||||
) -> TemplateReplacementList:
|
) -> TemplateReplacementList:
|
||||||
|
@ -89,6 +174,36 @@ def proto_replacement_list_to_native(
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def proto_progress_to_native(progress: pb.Progress) -> Progress:
|
||||||
|
kind = progress.WhichOneof("value")
|
||||||
|
if kind == "media_sync":
|
||||||
|
ikind = progress.media_sync.WhichOneof("value")
|
||||||
|
pkind = ProgressKind.MediaSyncProgress
|
||||||
|
if ikind == "downloaded_changes":
|
||||||
|
return Progress(
|
||||||
|
kind=pkind,
|
||||||
|
val=MediaSyncDownloadedChanges(progress.media_sync.downloaded_changes),
|
||||||
|
)
|
||||||
|
elif ikind == "downloaded_files":
|
||||||
|
return Progress(
|
||||||
|
kind=pkind,
|
||||||
|
val=MediaSyncDownloadedFiles(progress.media_sync.downloaded_files),
|
||||||
|
)
|
||||||
|
elif ikind == "uploaded":
|
||||||
|
up = progress.media_sync.uploaded
|
||||||
|
return Progress(
|
||||||
|
kind=pkind,
|
||||||
|
val=MediaSyncUploaded(files=up.files, deletions=up.deletions),
|
||||||
|
)
|
||||||
|
elif ikind == "removed_files":
|
||||||
|
return Progress(
|
||||||
|
kind=pkind, val=MediaSyncRemovedFiles(progress.media_sync.removed_files)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert_impossible_literal(ikind)
|
||||||
|
assert_impossible_literal(kind)
|
||||||
|
|
||||||
|
|
||||||
class RustBackend:
|
class RustBackend:
|
||||||
def __init__(self, col_path: str, media_folder_path: str, media_db_path: str):
|
def __init__(self, col_path: str, media_folder_path: str, media_db_path: str):
|
||||||
init_msg = pb.BackendInit(
|
init_msg = pb.BackendInit(
|
||||||
|
@ -97,15 +212,24 @@ class RustBackend:
|
||||||
media_db_path=media_db_path,
|
media_db_path=media_db_path,
|
||||||
)
|
)
|
||||||
self._backend = ankirspy.open_backend(init_msg.SerializeToString())
|
self._backend = ankirspy.open_backend(init_msg.SerializeToString())
|
||||||
|
self._backend.set_progress_callback(self._on_progress)
|
||||||
|
|
||||||
def _run_command(self, input: pb.BackendInput) -> pb.BackendOutput:
|
def _on_progress(self, progress_bytes: bytes) -> bool:
|
||||||
|
progress = pb.Progress()
|
||||||
|
progress.ParseFromString(progress_bytes)
|
||||||
|
native_progress = proto_progress_to_native(progress)
|
||||||
|
return hooks.rust_progress_callback(True, native_progress)
|
||||||
|
|
||||||
|
def _run_command(
|
||||||
|
self, input: pb.BackendInput, release_gil: bool = False
|
||||||
|
) -> pb.BackendOutput:
|
||||||
input_bytes = input.SerializeToString()
|
input_bytes = input.SerializeToString()
|
||||||
output_bytes = self._backend.command(input_bytes)
|
output_bytes = self._backend.command(input_bytes, release_gil)
|
||||||
output = pb.BackendOutput()
|
output = pb.BackendOutput()
|
||||||
output.ParseFromString(output_bytes)
|
output.ParseFromString(output_bytes)
|
||||||
kind = output.WhichOneof("value")
|
kind = output.WhichOneof("value")
|
||||||
if kind == "error":
|
if kind == "error":
|
||||||
raise BackendException(output.error)
|
raise proto_exception_to_native(output.error)
|
||||||
else:
|
else:
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
@ -195,3 +319,18 @@ class RustBackend:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).add_file_to_media_folder
|
).add_file_to_media_folder
|
||||||
|
|
||||||
|
def sync_media(
|
||||||
|
self, hkey: str, media_folder: str, media_db: str, endpoint: str
|
||||||
|
) -> None:
|
||||||
|
self._run_command(
|
||||||
|
pb.BackendInput(
|
||||||
|
sync_media=pb.SyncMediaIn(
|
||||||
|
hkey=hkey,
|
||||||
|
media_folder=media_folder,
|
||||||
|
media_db=media_db,
|
||||||
|
endpoint=endpoint,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
release_gil=True,
|
||||||
|
)
|
||||||
|
|
|
@ -120,10 +120,8 @@ def render_card(
|
||||||
# render
|
# render
|
||||||
try:
|
try:
|
||||||
output = render_card_from_context(ctx)
|
output = render_card_from_context(ctx)
|
||||||
except anki.rsbackend.BackendException as e:
|
except anki.rsbackend.TemplateError as e:
|
||||||
# fixme: specific exception in 2.1.21
|
if e.q_side():
|
||||||
err = e.args[0].template_parse # pylint: disable=no-member
|
|
||||||
if err.q_side:
|
|
||||||
side = _("Front")
|
side = _("Front")
|
||||||
else:
|
else:
|
||||||
side = _("Back")
|
side = _("Back")
|
||||||
|
|
16
pylib/anki/types.py
Normal file
16
pylib/anki/types.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import enum
|
||||||
|
from typing import Any, NoReturn
|
||||||
|
|
||||||
|
|
||||||
|
class _Impossible(enum.Enum):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def assert_impossible(arg: NoReturn) -> NoReturn:
|
||||||
|
raise Exception(f"unexpected arg received: {type(arg)} {arg}")
|
||||||
|
|
||||||
|
|
||||||
|
# mypy is not yet smart enough to do exhaustiveness checking on literal types,
|
||||||
|
# so this will fail at runtime instead of typecheck time :-(
|
||||||
|
def assert_impossible_literal(arg: Any) -> NoReturn:
|
||||||
|
raise Exception(f"unexpected arg received: {type(arg)} {arg}")
|
|
@ -50,6 +50,12 @@ hooks = [
|
||||||
),
|
),
|
||||||
Hook(name="sync_stage_did_change", args=["stage: str"], legacy_hook="sync"),
|
Hook(name="sync_stage_did_change", args=["stage: str"], legacy_hook="sync"),
|
||||||
Hook(name="sync_progress_did_change", args=["msg: str"], legacy_hook="syncMsg"),
|
Hook(name="sync_progress_did_change", args=["msg: str"], legacy_hook="syncMsg"),
|
||||||
|
Hook(
|
||||||
|
name="rust_progress_callback",
|
||||||
|
args=["proceed: bool", "progress: anki.rsbackend.Progress"],
|
||||||
|
return_type="bool",
|
||||||
|
doc="Warning: this is called on a background thread.",
|
||||||
|
),
|
||||||
Hook(
|
Hook(
|
||||||
name="tag_added", args=["tag: str"], legacy_hook="newTag", legacy_no_args=True,
|
name="tag_added", args=["tag: str"], legacy_hook="newTag", legacy_no_args=True,
|
||||||
),
|
),
|
||||||
|
|
|
@ -697,6 +697,30 @@ class _EditorWillUseFontForFieldFilter:
|
||||||
editor_will_use_font_for_field = _EditorWillUseFontForFieldFilter()
|
editor_will_use_font_for_field = _EditorWillUseFontForFieldFilter()
|
||||||
|
|
||||||
|
|
||||||
|
class _MediaSyncDidProgressHook:
|
||||||
|
_hooks: List[Callable[["aqt.mediasync.LogEntryWithTime"], None]] = []
|
||||||
|
|
||||||
|
def append(self, cb: Callable[["aqt.mediasync.LogEntryWithTime"], None]) -> None:
|
||||||
|
"""(entry: aqt.mediasync.LogEntryWithTime)"""
|
||||||
|
self._hooks.append(cb)
|
||||||
|
|
||||||
|
def remove(self, cb: Callable[["aqt.mediasync.LogEntryWithTime"], None]) -> None:
|
||||||
|
if cb in self._hooks:
|
||||||
|
self._hooks.remove(cb)
|
||||||
|
|
||||||
|
def __call__(self, entry: aqt.mediasync.LogEntryWithTime) -> None:
|
||||||
|
for hook in self._hooks:
|
||||||
|
try:
|
||||||
|
hook(entry)
|
||||||
|
except:
|
||||||
|
# if the hook fails, remove it
|
||||||
|
self._hooks.remove(hook)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
media_sync_did_progress = _MediaSyncDidProgressHook()
|
||||||
|
|
||||||
|
|
||||||
class _OverviewDidRefreshHook:
|
class _OverviewDidRefreshHook:
|
||||||
"""Allow to update the overview window. E.g. add the deck name in the
|
"""Allow to update the overview window. E.g. add the deck name in the
|
||||||
title."""
|
title."""
|
||||||
|
|
206
qt/aqt/mediasync.py
Normal file
206
qt/aqt/mediasync.py
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
import time
|
||||||
|
from concurrent.futures import Future
|
||||||
|
from copy import copy
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
import anki
|
||||||
|
import aqt
|
||||||
|
from anki import hooks
|
||||||
|
from anki.lang import _
|
||||||
|
from anki.media import media_paths_from_col_path
|
||||||
|
from anki.rsbackend import (
|
||||||
|
Interrupted,
|
||||||
|
MediaSyncDownloadedChanges,
|
||||||
|
MediaSyncDownloadedFiles,
|
||||||
|
MediaSyncProgress,
|
||||||
|
MediaSyncRemovedFiles,
|
||||||
|
MediaSyncUploaded,
|
||||||
|
Progress,
|
||||||
|
ProgressKind,
|
||||||
|
)
|
||||||
|
from anki.types import assert_impossible
|
||||||
|
from anki.utils import intTime
|
||||||
|
from aqt import gui_hooks
|
||||||
|
from aqt.qt import QDialog, QDialogButtonBox, QPushButton, QWidget
|
||||||
|
from aqt.taskman import TaskManager
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MediaSyncState:
|
||||||
|
downloaded_changes: int = 0
|
||||||
|
downloaded_files: int = 0
|
||||||
|
uploaded_files: int = 0
|
||||||
|
uploaded_removals: int = 0
|
||||||
|
removed_files: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
# fixme: make sure we don't run twice
|
||||||
|
# fixme: handle auth errors
|
||||||
|
# fixme: handle network errors
|
||||||
|
# fixme: show progress in UI
|
||||||
|
# fixme: abort when closing collection/app
|
||||||
|
# fixme: handle no hkey
|
||||||
|
# fixme: shards
|
||||||
|
# fixme: dialog should be a singleton
|
||||||
|
# fixme: abort button should not be default
|
||||||
|
|
||||||
|
|
||||||
|
class SyncBegun:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SyncEnded:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SyncAborted:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
LogEntry = Union[MediaSyncState, SyncBegun, SyncEnded, SyncAborted]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LogEntryWithTime:
|
||||||
|
time: int
|
||||||
|
entry: LogEntry
|
||||||
|
|
||||||
|
|
||||||
|
class MediaSyncer:
|
||||||
|
def __init__(self, taskman: TaskManager):
|
||||||
|
self._taskman = taskman
|
||||||
|
self._sync_state: Optional[MediaSyncState] = None
|
||||||
|
self._log: List[LogEntryWithTime] = []
|
||||||
|
self._want_stop = False
|
||||||
|
hooks.rust_progress_callback.append(self._on_rust_progress)
|
||||||
|
|
||||||
|
def _on_rust_progress(self, proceed: bool, progress: Progress) -> bool:
|
||||||
|
if progress.kind != ProgressKind.MediaSyncProgress:
|
||||||
|
return proceed
|
||||||
|
|
||||||
|
self._update_state(progress.val)
|
||||||
|
self._log_and_notify(copy(self._sync_state))
|
||||||
|
|
||||||
|
if self._want_stop:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return proceed
|
||||||
|
|
||||||
|
def _update_state(self, progress: MediaSyncProgress) -> None:
|
||||||
|
if isinstance(progress, MediaSyncDownloadedChanges):
|
||||||
|
self._sync_state.downloaded_changes += progress.changes
|
||||||
|
elif isinstance(progress, MediaSyncDownloadedFiles):
|
||||||
|
self._sync_state.downloaded_files += progress.files
|
||||||
|
elif isinstance(progress, MediaSyncUploaded):
|
||||||
|
self._sync_state.uploaded_files += progress.files
|
||||||
|
self._sync_state.uploaded_removals += progress.deletions
|
||||||
|
elif isinstance(progress, MediaSyncRemovedFiles):
|
||||||
|
self._sync_state.removed_files += progress.files
|
||||||
|
|
||||||
|
def start(
|
||||||
|
self, col: anki.storage._Collection, hkey: str, shard: Optional[int]
|
||||||
|
) -> None:
|
||||||
|
"Start media syncing in the background, if it's not already running."
|
||||||
|
if self._sync_state is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._log_and_notify(SyncBegun())
|
||||||
|
self._sync_state = MediaSyncState()
|
||||||
|
self._want_stop = False
|
||||||
|
|
||||||
|
if shard is not None:
|
||||||
|
shard_str = str(shard)
|
||||||
|
else:
|
||||||
|
shard_str = ""
|
||||||
|
endpoint = f"https://sync{shard_str}ankiweb.net"
|
||||||
|
|
||||||
|
(media_folder, media_db) = media_paths_from_col_path(col.path)
|
||||||
|
|
||||||
|
def run() -> None:
|
||||||
|
col.backend.sync_media(hkey, media_folder, media_db, endpoint)
|
||||||
|
|
||||||
|
self._taskman.run_in_background(run, self._on_finished)
|
||||||
|
|
||||||
|
def _log_and_notify(self, entry: LogEntry) -> None:
|
||||||
|
entry_with_time = LogEntryWithTime(time=intTime(), entry=entry)
|
||||||
|
self._log.append(entry_with_time)
|
||||||
|
self._taskman.run_on_main(
|
||||||
|
lambda: gui_hooks.media_sync_did_progress(entry_with_time)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_finished(self, future: Future) -> None:
|
||||||
|
self._sync_state = None
|
||||||
|
|
||||||
|
exc = future.exception()
|
||||||
|
if exc is not None:
|
||||||
|
if isinstance(exc, Interrupted):
|
||||||
|
self._log_and_notify(SyncAborted())
|
||||||
|
else:
|
||||||
|
raise exc
|
||||||
|
else:
|
||||||
|
self._log_and_notify(SyncEnded())
|
||||||
|
|
||||||
|
def entries(self) -> List[LogEntryWithTime]:
|
||||||
|
return self._log
|
||||||
|
|
||||||
|
def abort(self) -> None:
|
||||||
|
self._want_stop = True
|
||||||
|
|
||||||
|
|
||||||
|
class MediaSyncDialog(QDialog):
|
||||||
|
def __init__(self, parent: QWidget, syncer: MediaSyncer) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self._syncer = syncer
|
||||||
|
self.form = aqt.forms.synclog.Ui_Dialog()
|
||||||
|
self.form.setupUi(self)
|
||||||
|
self.abort_button = QPushButton(_("Abort"))
|
||||||
|
self.abort_button.clicked.connect(self._on_abort) # type: ignore
|
||||||
|
self.form.buttonBox.addButton(self.abort_button, QDialogButtonBox.ActionRole)
|
||||||
|
|
||||||
|
gui_hooks.media_sync_did_progress.append(self._on_log_entry)
|
||||||
|
|
||||||
|
self.form.plainTextEdit.setPlainText(
|
||||||
|
"\n".join(self._entry_to_text(x) for x in syncer.entries())
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_abort(self, *args) -> None:
|
||||||
|
self.form.plainTextEdit.appendPlainText(
|
||||||
|
self._time_and_text(intTime(), _("Aborting..."))
|
||||||
|
)
|
||||||
|
self._syncer.abort()
|
||||||
|
self.abort_button.setHidden(True)
|
||||||
|
|
||||||
|
def _time_and_text(self, stamp: int, text: str) -> str:
|
||||||
|
asctime = time.asctime(time.localtime(stamp))
|
||||||
|
return f"{asctime}: {text}"
|
||||||
|
|
||||||
|
def _entry_to_text(self, entry: LogEntryWithTime):
|
||||||
|
if isinstance(entry.entry, SyncBegun):
|
||||||
|
txt = _("Sync starting...")
|
||||||
|
elif isinstance(entry.entry, SyncEnded):
|
||||||
|
txt = _("Sync complete.")
|
||||||
|
elif isinstance(entry.entry, SyncAborted):
|
||||||
|
txt = _("Aborted.")
|
||||||
|
elif isinstance(entry.entry, MediaSyncState):
|
||||||
|
txt = self._logentry_to_text(entry.entry)
|
||||||
|
else:
|
||||||
|
assert_impossible(entry.entry)
|
||||||
|
return self._time_and_text(entry.time, txt)
|
||||||
|
|
||||||
|
def _logentry_to_text(self, e: MediaSyncState) -> str:
|
||||||
|
return _(
|
||||||
|
"Added: %(a_up)s ↑, %(a_dwn)s ↓, Removed: %(r_up)s ↑, %(r_dwn)s ↓, Checked: %(chk)s"
|
||||||
|
) % dict(
|
||||||
|
a_up=e.uploaded_files,
|
||||||
|
a_dwn=e.downloaded_files,
|
||||||
|
r_up=e.uploaded_removals,
|
||||||
|
r_dwn=e.removed_files,
|
||||||
|
chk=e.downloaded_changes,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_log_entry(self, entry: LogEntryWithTime):
|
||||||
|
self.form.plainTextEdit.appendPlainText(self._entry_to_text(entry))
|
|
@ -11,7 +11,7 @@ import locale
|
||||||
import pickle
|
import pickle
|
||||||
import random
|
import random
|
||||||
import shutil
|
import shutil
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from send2trash import send2trash
|
from send2trash import send2trash
|
||||||
|
|
||||||
|
@ -502,7 +502,7 @@ please see:
|
||||||
def set_night_mode(self, on: bool) -> None:
|
def set_night_mode(self, on: bool) -> None:
|
||||||
self.meta["night_mode"] = on
|
self.meta["night_mode"] = on
|
||||||
|
|
||||||
# Profile-specific options
|
# Profile-specific
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def interrupt_audio(self) -> bool:
|
def interrupt_audio(self) -> bool:
|
||||||
|
@ -512,6 +512,9 @@ please see:
|
||||||
self.profile["interrupt_audio"] = val
|
self.profile["interrupt_audio"] = val
|
||||||
aqt.sound.av_player.interrupt_current_audio = val
|
aqt.sound.av_player.interrupt_current_audio = val
|
||||||
|
|
||||||
|
def sync_key(self) -> Optional[str]:
|
||||||
|
return self.profile.get("syncKey")
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def apply_profile_options(self) -> None:
|
def apply_profile_options(self) -> None:
|
||||||
|
|
74
qt/designer/synclog.ui
Normal file
74
qt/designer/synclog.ui
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Dialog</class>
|
||||||
|
<widget class="QDialog" name="Dialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>557</width>
|
||||||
|
<height>295</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Sync</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QPlainTextEdit" name="plainTextEdit">
|
||||||
|
<property name="readOnly">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="plainText">
|
||||||
|
<string notr="true"/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Close</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>Dialog</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>248</x>
|
||||||
|
<y>254</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>157</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>Dialog</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>316</x>
|
||||||
|
<y>260</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
|
@ -266,6 +266,9 @@ hooks = [
|
||||||
return_type="str",
|
return_type="str",
|
||||||
legacy_hook="setupStyle",
|
legacy_hook="setupStyle",
|
||||||
),
|
),
|
||||||
|
Hook(
|
||||||
|
name="media_sync_did_progress", args=["entry: aqt.mediasync.LogEntryWithTime"],
|
||||||
|
),
|
||||||
# Adding cards
|
# Adding cards
|
||||||
###################
|
###################
|
||||||
Hook(
|
Hook(
|
||||||
|
|
|
@ -3,9 +3,10 @@
|
||||||
|
|
||||||
use crate::backend_proto as pt;
|
use crate::backend_proto as pt;
|
||||||
use crate::backend_proto::backend_input::Value;
|
use crate::backend_proto::backend_input::Value;
|
||||||
use crate::backend_proto::{Empty, RenderedTemplateReplacement};
|
use crate::backend_proto::{Empty, RenderedTemplateReplacement, SyncMediaIn};
|
||||||
use crate::cloze::expand_clozes_to_reveal_latex;
|
use crate::cloze::expand_clozes_to_reveal_latex;
|
||||||
use crate::err::{AnkiError, Result};
|
use crate::err::{AnkiError, Result};
|
||||||
|
use crate::media::sync::{sync_media, Progress as MediaSyncProgress};
|
||||||
use crate::media::MediaManager;
|
use crate::media::MediaManager;
|
||||||
use crate::sched::{local_minutes_west_for_stamp, sched_timing_today};
|
use crate::sched::{local_minutes_west_for_stamp, sched_timing_today};
|
||||||
use crate::template::{
|
use crate::template::{
|
||||||
|
@ -16,11 +17,19 @@ use crate::text::{extract_av_tags, strip_av_tags, AVTag};
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
|
pub type ProtoProgressCallback = Box<dyn Fn(Vec<u8>) -> bool + Send>;
|
||||||
|
|
||||||
pub struct Backend {
|
pub struct Backend {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
col_path: PathBuf,
|
col_path: PathBuf,
|
||||||
media_manager: Option<MediaManager>,
|
media_manager: Option<MediaManager>,
|
||||||
|
progress_callback: Option<ProtoProgressCallback>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Progress {
|
||||||
|
MediaSync(MediaSyncProgress),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert an Anki error to a protobuf error.
|
/// Convert an Anki error to a protobuf error.
|
||||||
|
@ -77,6 +86,7 @@ impl Backend {
|
||||||
Ok(Backend {
|
Ok(Backend {
|
||||||
col_path: col_path.into(),
|
col_path: col_path.into(),
|
||||||
media_manager,
|
media_manager,
|
||||||
|
progress_callback: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,9 +152,26 @@ impl Backend {
|
||||||
Value::AddFileToMediaFolder(input) => {
|
Value::AddFileToMediaFolder(input) => {
|
||||||
OValue::AddFileToMediaFolder(self.add_file_to_media_folder(input)?)
|
OValue::AddFileToMediaFolder(self.add_file_to_media_folder(input)?)
|
||||||
}
|
}
|
||||||
|
Value::SyncMedia(input) => {
|
||||||
|
self.sync_media(input)?;
|
||||||
|
OValue::SyncMedia(Empty {})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fire_progress_callback(&self, progress: Progress) -> bool {
|
||||||
|
if let Some(cb) = &self.progress_callback {
|
||||||
|
let bytes = progress_to_proto_bytes(progress);
|
||||||
|
cb(bytes)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_progress_callback(&mut self, progress_cb: Option<ProtoProgressCallback>) {
|
||||||
|
self.progress_callback = progress_cb;
|
||||||
|
}
|
||||||
|
|
||||||
fn template_requirements(
|
fn template_requirements(
|
||||||
&self,
|
&self,
|
||||||
input: pt::TemplateRequirementsIn,
|
input: pt::TemplateRequirementsIn,
|
||||||
|
@ -263,6 +290,17 @@ impl Backend {
|
||||||
.add_file(&input.desired_name, &input.data)?
|
.add_file(&input.desired_name, &input.data)?
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sync_media(&self, input: SyncMediaIn) -> Result<()> {
|
||||||
|
let mut mgr = MediaManager::new(&input.media_folder, &input.media_db)?;
|
||||||
|
|
||||||
|
let callback = |progress: MediaSyncProgress| {
|
||||||
|
self.fire_progress_callback(Progress::MediaSync(progress))
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut rt = Runtime::new().unwrap();
|
||||||
|
rt.block_on(sync_media(&mut mgr, &input.hkey, callback))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ords_hash_to_set(ords: HashSet<u16>) -> Vec<u32> {
|
fn ords_hash_to_set(ords: HashSet<u16>) -> Vec<u32> {
|
||||||
|
@ -292,3 +330,28 @@ fn rendered_node_to_proto(node: RenderedNode) -> pt::rendered_template_node::Val
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn progress_to_proto_bytes(progress: Progress) -> Vec<u8> {
|
||||||
|
let proto = pt::Progress {
|
||||||
|
value: Some(match progress {
|
||||||
|
Progress::MediaSync(progress) => {
|
||||||
|
use pt::media_sync_progress::Value as V;
|
||||||
|
use MediaSyncProgress as P;
|
||||||
|
let val = match progress {
|
||||||
|
P::DownloadedChanges(n) => V::DownloadedChanges(n as u32),
|
||||||
|
P::DownloadedFiles(n) => V::DownloadedFiles(n as u32),
|
||||||
|
P::Uploaded { files, deletions } => V::Uploaded(pt::MediaSyncUploadProgress {
|
||||||
|
files: files as u32,
|
||||||
|
deletions: deletions as u32,
|
||||||
|
}),
|
||||||
|
P::RemovedFiles(n) => V::RemovedFiles(n as u32),
|
||||||
|
};
|
||||||
|
pt::progress::Value::MediaSync(pt::MediaSyncProgress { value: Some(val) })
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buf = vec![];
|
||||||
|
proto.encode(&mut buf).expect("encode failed");
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
|
@ -80,9 +80,11 @@ impl From<rusqlite::types::FromSqlError> for AnkiError {
|
||||||
|
|
||||||
impl From<reqwest::Error> for AnkiError {
|
impl From<reqwest::Error> for AnkiError {
|
||||||
fn from(err: reqwest::Error) -> Self {
|
fn from(err: reqwest::Error) -> Self {
|
||||||
AnkiError::NetworkError {
|
let url = err.url().map(|url| url.as_str()).unwrap_or("");
|
||||||
info: format!("{:?}", err),
|
let str_err = format!("{}", err);
|
||||||
}
|
// strip url from error to avoid exposing keys
|
||||||
|
let str_err = str_err.replace(url, "");
|
||||||
|
AnkiError::NetworkError { info: str_err }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
use crate::err::{AnkiError, Result};
|
use crate::err::{AnkiError, Result};
|
||||||
use crate::media::database::{MediaDatabaseContext, MediaEntry};
|
use crate::media::database::{MediaDatabaseContext, MediaDatabaseMetadata, MediaEntry};
|
||||||
use crate::media::files::{
|
use crate::media::files::{
|
||||||
add_file_from_ankiweb, data_for_file, normalize_filename, remove_files, AddedFile,
|
add_file_from_ankiweb, data_for_file, normalize_filename, remove_files, AddedFile,
|
||||||
};
|
};
|
||||||
|
@ -95,8 +95,8 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_changes(&mut self, client_usn: i32) -> Result<()> {
|
async fn fetch_changes(&mut self, mut meta: MediaDatabaseMetadata) -> Result<()> {
|
||||||
let mut last_usn = client_usn;
|
let mut last_usn = meta.last_sync_usn;
|
||||||
loop {
|
loop {
|
||||||
debug!("fetching record batch starting from usn {}", last_usn);
|
debug!("fetching record batch starting from usn {}", last_usn);
|
||||||
|
|
||||||
|
@ -140,6 +140,11 @@ where
|
||||||
record_removals(ctx, &to_delete)?;
|
record_removals(ctx, &to_delete)?;
|
||||||
record_additions(ctx, downloaded)?;
|
record_additions(ctx, downloaded)?;
|
||||||
record_clean(ctx, &to_remove_pending)?;
|
record_clean(ctx, &to_remove_pending)?;
|
||||||
|
|
||||||
|
// update usn
|
||||||
|
meta.last_sync_usn = last_usn;
|
||||||
|
ctx.set_meta(&meta)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
@ -214,7 +219,8 @@ where
|
||||||
// make sure media DB is up to date
|
// make sure media DB is up to date
|
||||||
register_changes(&mut sctx.ctx, mgr.media_folder.as_path())?;
|
register_changes(&mut sctx.ctx, mgr.media_folder.as_path())?;
|
||||||
|
|
||||||
let client_usn = sctx.ctx.get_meta()?.last_sync_usn;
|
let meta = sctx.ctx.get_meta()?;
|
||||||
|
let client_usn = meta.last_sync_usn;
|
||||||
|
|
||||||
debug!("beginning media sync");
|
debug!("beginning media sync");
|
||||||
let (sync_key, server_usn) = sctx.sync_begin(hkey).await?;
|
let (sync_key, server_usn) = sctx.sync_begin(hkey).await?;
|
||||||
|
@ -226,7 +232,7 @@ where
|
||||||
// need to fetch changes from server?
|
// need to fetch changes from server?
|
||||||
if client_usn != server_usn {
|
if client_usn != server_usn {
|
||||||
debug!("differs from local usn {}, fetching changes", client_usn);
|
debug!("differs from local usn {}, fetching changes", client_usn);
|
||||||
sctx.fetch_changes(client_usn).await?;
|
sctx.fetch_changes(meta).await?;
|
||||||
actions_performed = true;
|
actions_performed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,9 @@ authors = ["Ankitects Pty Ltd and contributors"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anki = { path = "../rslib" }
|
anki = { path = "../rslib" }
|
||||||
|
log = "0.4.8"
|
||||||
|
env_logger = "0.7.1"
|
||||||
|
tokio = "0.2.11"
|
||||||
|
|
||||||
[dependencies.pyo3]
|
[dependencies.pyo3]
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use anki::backend::{init_backend, Backend as RustBackend};
|
use anki::backend::{init_backend, Backend as RustBackend};
|
||||||
|
use log::error;
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
use pyo3::types::PyBytes;
|
use pyo3::types::PyBytes;
|
||||||
use pyo3::{exceptions, wrap_pyfunction};
|
use pyo3::{exceptions, wrap_pyfunction};
|
||||||
|
@ -23,11 +24,45 @@ fn open_backend(init_msg: &PyBytes) -> PyResult<Backend> {
|
||||||
|
|
||||||
#[pymethods]
|
#[pymethods]
|
||||||
impl Backend {
|
impl Backend {
|
||||||
fn command(&mut self, py: Python, input: &PyBytes) -> PyObject {
|
fn command(&mut self, py: Python, input: &PyBytes, release_gil: bool) -> PyObject {
|
||||||
let out_bytes = self.backend.run_command_bytes(input.as_bytes());
|
let in_bytes = input.as_bytes();
|
||||||
|
let out_bytes = if release_gil {
|
||||||
|
py.allow_threads(move || self.backend.run_command_bytes(in_bytes))
|
||||||
|
} else {
|
||||||
|
self.backend.run_command_bytes(in_bytes)
|
||||||
|
};
|
||||||
let out_obj = PyBytes::new(py, &out_bytes);
|
let out_obj = PyBytes::new(py, &out_bytes);
|
||||||
out_obj.into()
|
out_obj.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_progress_callback(&mut self, callback: PyObject) {
|
||||||
|
if callback.is_none() {
|
||||||
|
self.backend.set_progress_callback(None);
|
||||||
|
} else {
|
||||||
|
let func = move |bytes: Vec<u8>| {
|
||||||
|
let gil = Python::acquire_gil();
|
||||||
|
let py = gil.python();
|
||||||
|
let out_bytes = PyBytes::new(py, &bytes);
|
||||||
|
let out_obj: PyObject = out_bytes.into();
|
||||||
|
let res: PyObject = match callback.call1(py, (out_obj,)) {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(e) => {
|
||||||
|
error!("error calling callback:");
|
||||||
|
e.print(py);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match res.extract(py) {
|
||||||
|
Ok(cont) => cont,
|
||||||
|
Err(e) => {
|
||||||
|
error!("callback did not return bool: {:?}", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.backend.set_progress_callback(Some(Box::new(func)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pymodule]
|
#[pymodule]
|
||||||
|
@ -36,5 +71,7 @@ fn ankirspy(_py: Python, m: &PyModule) -> PyResult<()> {
|
||||||
m.add_wrapped(wrap_pyfunction!(buildhash)).unwrap();
|
m.add_wrapped(wrap_pyfunction!(buildhash)).unwrap();
|
||||||
m.add_wrapped(wrap_pyfunction!(open_backend)).unwrap();
|
m.add_wrapped(wrap_pyfunction!(open_backend)).unwrap();
|
||||||
|
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue