migrate more methods to service

This commit is contained in:
Damien Elmes 2020-05-22 21:25:25 +10:00
parent 9c20d9a02b
commit 175afa9fee
10 changed files with 296 additions and 292 deletions

View file

@ -4,6 +4,9 @@ import "fluent.proto";
package backend_proto;
// Generic containers
///////////////////////////////////////////////////////////
message Empty {}
message OptionalInt32 {
@ -14,9 +17,33 @@ message OptionalUInt32 {
uint32 val = 1;
}
message Int32 {
sint32 val = 1;
}
message Int64 {
int64 val = 1;
}
message String {
string val = 1;
}
// New style RPC definitions
///////////////////////////////////////////////////////////
service BackendService {
rpc RenderExistingCard (RenderExistingCardIn) returns (RenderCardOut);
rpc RenderUncommittedCard (RenderUncommittedCardIn) returns (RenderCardOut);
rpc SchedTimingToday (Empty) returns (SchedTimingTodayOut);
rpc DeckTree (DeckTreeIn) returns (DeckTreeNode);
rpc SearchCards (SearchCardsIn) returns (SearchCardsOut);
rpc SearchNotes (SearchNotesIn) returns (SearchNotesOut);
rpc CheckMedia (Empty) returns (CheckMediaOut);
rpc LocalMinutesWest (Int64) returns (Int32);
rpc StripAvTags (String) returns (String);
rpc ExtractAVTags (ExtractAVTagsIn) returns (ExtractAVTagsOut);
rpc ExtractLatex (ExtractLatexIn) returns (ExtractLatexOut);
}
// Protobuf stored in .anki2 files
@ -257,20 +284,13 @@ message I18nBackendInit {
string locale_folder_path = 5;
}
// Legacy enum - needs migrating to RPC above
///////////////////////////////////////////////////////////
message BackendInput {
oneof value {
Empty sched_timing_today = 17;
DeckTreeIn deck_tree = 18;
SearchCardsIn search_cards = 19;
SearchNotesIn search_notes = 20;
RenderUncommittedCardIn render_uncommitted_card = 21;
int64 local_minutes_west = 22;
string strip_av_tags = 23;
ExtractAVTagsIn extract_av_tags = 24;
ExtractLatexIn extract_latex = 25;
AddMediaFileIn add_media_file = 26;
SyncMediaIn sync_media = 27;
Empty check_media = 28;
TrashMediaFilesIn trash_media_files = 29;
TranslateStringIn translate_string = 30;
FormatTimeSpanIn format_time_span = 31;
@ -327,7 +347,6 @@ message BackendInput {
int32 set_local_minutes_west = 83;
Empty get_preferences = 84;
Preferences set_preferences = 85;
RenderExistingCardIn render_existing_card = 86;
Note cloze_numbers_in_note = 87;
}
}
@ -335,10 +354,6 @@ message BackendInput {
message BackendOutput {
oneof value {
// infallible commands
sint32 local_minutes_west = 22;
string strip_av_tags = 23;
ExtractAVTagsOut extract_av_tags = 24;
ExtractLatexOut extract_latex = 25;
string translate_string = 30;
string format_time_span = 31;
string studied_today = 32;
@ -347,14 +362,8 @@ message BackendOutput {
ClozeNumbersInNoteOut cloze_numbers_in_note = 87;
// fallible commands
SchedTimingTodayOut sched_timing_today = 17;
DeckTreeNode deck_tree = 18;
SearchCardsOut search_cards = 19;
SearchNotesOut search_notes = 20;
RenderCardOut render_uncommitted_card = 21;
string add_media_file = 26;
Empty sync_media = 27;
MediaCheckOut check_media = 28;
Empty trash_media_files = 29;
Empty empty_trash = 34;
Empty restore_trash = 35;
@ -405,7 +414,6 @@ message BackendOutput {
Empty set_local_minutes_west = 83;
Preferences get_preferences = 84;
Empty set_preferences = 85;
RenderCardOut render_existing_card = 86;
bytes get_stock_notetype_legacy = 60;
BackendError error = 2047;
@ -580,7 +588,7 @@ message SyncMediaIn {
string endpoint = 2;
}
message MediaCheckOut {
message CheckMediaOut {
repeated string unused = 1;
repeated string missing = 2;
string report = 3;

View file

@ -26,7 +26,7 @@ from anki.lang import _
from anki.media import MediaManager, media_paths_from_col_path
from anki.models import ModelManager
from anki.notes import Note
from anki.rsbackend import TR, DBError, RustBackend
from anki.rsbackend import TR, DBError, RustBackend, pb
from anki.sched import Scheduler as V1Scheduler
from anki.schedv2 import Scheduler as V2Scheduler
from anki.tags import TagManager
@ -393,7 +393,21 @@ select id from notes where id in %s and id not in (select nid from cards)"""
def find_cards(
self, query: str, order: Union[bool, str, int] = False, reverse: bool = False,
) -> Sequence[int]:
return self.backend.search_cards(query, order, reverse)
if isinstance(order, str):
mode = pb.SortOrder(custom=order)
elif order is True:
mode = pb.SortOrder(from_config=pb.Empty())
elif order is False:
mode = pb.SortOrder(none=pb.Empty())
else:
# sadly we can't use the protobuf type in a Union, so we
# have to accept an int and convert it
BKind = pb.BuiltinSearchOrder.BuiltinSortKind # pylint: disable=no-member
kind = BKind.Value(BKind.Name(order))
mode = pb.SortOrder(
builtin=pb.BuiltinSearchOrder(kind=kind, reverse=reverse)
)
return self.backend.search_cards(query, mode)
def find_notes(self, query: str) -> Sequence[int]:
return self.backend.search_notes(query)

View file

@ -128,7 +128,7 @@ class DeckManager:
return self.col.backend.new_deck_legacy(filtered)
def deck_tree(self) -> pb.DeckTreeNode:
return self.col.backend.deck_tree(include_counts=False)
return self.col.backend.deck_tree(include_counts=False, top_deck_id=0)
@classmethod
def find_deck_in_tree(

View file

@ -6,13 +6,14 @@ from __future__ import annotations
import html
import os
import re
from dataclasses import dataclass
from typing import Any, List, Optional, Tuple
import anki
from anki import hooks
from anki.lang import _
from anki.models import NoteType
from anki.rsbackend import ExtractedLatex
from anki.rsbackend import pb
from anki.template import TemplateRenderContext, TemplateRenderOutput
from anki.utils import call, isMac, namedtmp, tmpdir
@ -33,6 +34,28 @@ if isMac:
os.environ["PATH"] += ":/usr/texbin:/Library/TeX/texbin"
@dataclass
class ExtractedLatex:
filename: str
latex_body: str
@dataclass
class ExtractedLatexOutput:
html: str
latex: List[ExtractedLatex]
@staticmethod
def from_proto(proto: pb.ExtractLatexOut) -> ExtractedLatexOutput:
return ExtractedLatexOutput(
html=proto.text,
latex=[
ExtractedLatex(filename=l.filename, latex_body=l.latex_body)
for l in proto.latex
],
)
def on_card_did_render(
output: TemplateRenderOutput, ctx: TemplateRenderContext
) -> None:
@ -63,7 +86,8 @@ def render_latex_returning_errors(
header = model["latexPre"]
footer = model["latexPost"]
out = col.backend.extract_latex(html, svg, expand_clozes)
proto = col.backend.extract_latex(html, svg, expand_clozes)
out = ExtractedLatexOutput.from_proto(proto)
errors = []
html = out.html

View file

@ -15,7 +15,7 @@ from typing import Any, Callable, List, Optional, Tuple
import anki
from anki.consts import *
from anki.latex import render_latex, render_latex_returning_errors
from anki.rsbackend import MediaCheckOutput
from anki.rsbackend import pb
from anki.utils import intTime
@ -170,7 +170,7 @@ class MediaManager:
# Checking media
##########################################################################
def check(self) -> MediaCheckOutput:
def check(self) -> pb.CheckMediaOut:
output = self.col.backend.check_media()
# files may have been renamed on disk, so an undo at this point could
# break file references

View file

@ -151,39 +151,11 @@ def proto_exception_to_native(err: pb.BackendError) -> Exception:
return StringError(err.localized)
def av_tag_to_native(tag: pb.AVTag) -> AVTag:
val = tag.WhichOneof("value")
if val == "sound_or_video":
return SoundOrVideoTag(filename=tag.sound_or_video)
else:
return TTSTag(
field_text=tag.tts.field_text,
lang=tag.tts.lang,
voices=list(tag.tts.voices),
other_args=list(tag.tts.other_args),
speed=tag.tts.speed,
)
MediaSyncProgress = pb.MediaSyncProgress
MediaCheckOutput = pb.MediaCheckOut
FormatTimeSpanContext = pb.FormatTimeSpanIn.Context
@dataclass
class ExtractedLatex:
filename: str
latex_body: str
@dataclass
class ExtractedLatexOutput:
html: str
latex: List[ExtractedLatex]
class ProgressKind(enum.Enum):
MediaSync = 0
MediaCheck = 1
@ -267,52 +239,6 @@ class RustBackend:
release_gil=True,
)
def sched_timing_today(self,) -> SchedTimingToday:
return self._run_command(
pb.BackendInput(sched_timing_today=pb.Empty())
).sched_timing_today
def local_minutes_west(self, stamp: int) -> int:
return self._run_command(
pb.BackendInput(local_minutes_west=stamp)
).local_minutes_west
def strip_av_tags(self, text: str) -> str:
return self._run_command(pb.BackendInput(strip_av_tags=text)).strip_av_tags
def extract_av_tags(
self, text: str, question_side: bool
) -> Tuple[str, List[AVTag]]:
out = self._run_command(
pb.BackendInput(
extract_av_tags=pb.ExtractAVTagsIn(
text=text, question_side=question_side
)
)
).extract_av_tags
native_tags = list(map(av_tag_to_native, out.av_tags))
return out.text, native_tags
def extract_latex(
self, text: str, svg: bool, expand_clozes: bool
) -> ExtractedLatexOutput:
out = self._run_command(
pb.BackendInput(
extract_latex=pb.ExtractLatexIn(
text=text, svg=svg, expand_clozes=expand_clozes
)
)
).extract_latex
return ExtractedLatexOutput(
html=out.text,
latex=[
ExtractedLatex(filename=l.filename, latex_body=l.latex_body)
for l in out.latex
],
)
def add_file_to_media_folder(self, desired_name: str, data: bytes) -> str:
return self._run_command(
pb.BackendInput(
@ -326,11 +252,6 @@ class RustBackend:
release_gil=True,
)
def check_media(self) -> MediaCheckOutput:
return self._run_command(
pb.BackendInput(check_media=pb.Empty()), release_gil=True,
).check_media
def trash_media_files(self, fnames: List[str]) -> None:
self._run_command(
pb.BackendInput(trash_media_files=pb.TrashMediaFilesIn(fnames=fnames))
@ -396,32 +317,6 @@ class RustBackend:
def _db_command(self, input: Dict[str, Any]) -> Any:
return orjson.loads(self._backend.db_command(orjson.dumps(input)))
def search_cards(
self, search: str, order: Union[bool, str, int], reverse: bool = False
) -> Sequence[int]:
if isinstance(order, str):
mode = pb.SortOrder(custom=order)
elif order is True:
mode = pb.SortOrder(from_config=pb.Empty())
elif order is False:
mode = pb.SortOrder(none=pb.Empty())
else:
# sadly we can't use the protobuf type in a Union, so we
# have to accept an int and convert it
kind = BuiltinSortKind.Value(BuiltinSortKind.Name(order))
mode = pb.SortOrder(
builtin=pb.BuiltinSearchOrder(kind=kind, reverse=reverse)
)
return self._run_command(
pb.BackendInput(search_cards=pb.SearchCardsIn(search=search, order=mode))
).search_cards.card_ids
def search_notes(self, search: str) -> Sequence[int]:
return self._run_command(
pb.BackendInput(search_notes=pb.SearchNotesIn(search=search))
).search_notes.note_ids
def get_card(self, cid: int) -> Optional[pb.Card]:
output = self._run_command(pb.BackendInput(get_card=cid)).get_card
if output.HasField("card"):
@ -667,15 +562,6 @@ class RustBackend:
def remove_deck(self, did: int) -> None:
self._run_command(pb.BackendInput(remove_deck=did))
def deck_tree(self, include_counts: bool, top_deck_id: int = 0) -> DeckTreeNode:
return self._run_command(
pb.BackendInput(
deck_tree=pb.DeckTreeIn(
include_counts=include_counts, top_deck_id=top_deck_id
)
)
).deck_tree
def check_database(self) -> List[str]:
return list(
self._run_command(
@ -797,6 +683,62 @@ class RustBackend:
output.ParseFromString(self._run_command2(2, input))
return output
def sched_timing_today(self) -> pb.SchedTimingTodayOut:
input = pb.Empty()
output = pb.SchedTimingTodayOut()
output.ParseFromString(self._run_command2(3, input))
return output
def deck_tree(self, include_counts: bool, top_deck_id: int) -> pb.DeckTreeNode:
input = pb.DeckTreeIn(include_counts=include_counts, top_deck_id=top_deck_id)
output = pb.DeckTreeNode()
output.ParseFromString(self._run_command2(4, input))
return output
def search_cards(self, search: str, order: pb.SortOrder) -> Sequence[int]:
input = pb.SearchCardsIn(search=search, order=order)
output = pb.SearchCardsOut()
output.ParseFromString(self._run_command2(5, input))
return output.card_ids
def search_notes(self, search: str) -> Sequence[int]:
input = pb.SearchNotesIn(search=search)
output = pb.SearchNotesOut()
output.ParseFromString(self._run_command2(6, input))
return output.note_ids
def check_media(self) -> pb.CheckMediaOut:
input = pb.Empty()
output = pb.CheckMediaOut()
output.ParseFromString(self._run_command2(7, input))
return output
def local_minutes_west(self, val: int) -> int:
input = pb.Int64(val=val)
output = pb.Int32()
output.ParseFromString(self._run_command2(8, input))
return output.val
def strip_av_tags(self, val: str) -> str:
input = pb.String(val=val)
output = pb.String()
output.ParseFromString(self._run_command2(9, input))
return output.val
def extract_a_v_tags(self, text: str, question_side: bool) -> pb.ExtractAVTagsOut:
input = pb.ExtractAVTagsIn(text=text, question_side=question_side)
output = pb.ExtractAVTagsOut()
output.ParseFromString(self._run_command2(10, input))
return output
def extract_latex(
self, text: str, svg: bool, expand_clozes: bool
) -> pb.ExtractLatexOut:
input = pb.ExtractLatexIn(text=text, svg=svg, expand_clozes=expand_clozes)
output = pb.ExtractLatexOut()
output.ParseFromString(self._run_command2(11, input))
return output
# @@AUTOGEN@@

View file

@ -38,7 +38,7 @@ from anki.decks import DeckManager
from anki.models import NoteType
from anki.notes import Note
from anki.rsbackend import pb, to_json_bytes
from anki.sound import AVTag
from anki.sound import AVTag, SoundOrVideoTag, TTSTag
CARD_BLANK_HELP = (
"https://anki.tenderapp.com/kb/card-appearance/the-front-of-this-card-is-blank"
@ -86,6 +86,24 @@ class PartiallyRenderedCard:
return results
def av_tag_to_native(tag: pb.AVTag) -> AVTag:
val = tag.WhichOneof("value")
if val == "sound_or_video":
return SoundOrVideoTag(filename=tag.sound_or_video)
else:
return TTSTag(
field_text=tag.tts.field_text,
lang=tag.tts.lang,
voices=list(tag.tts.voices),
other_args=list(tag.tts.other_args),
speed=tag.tts.speed,
)
def av_tags_to_native(tags: Sequence[pb.AVTag]) -> List[AVTag]:
return list(map(av_tag_to_native, tags))
class TemplateRenderContext:
"""Holds information for the duration of one card render.
@ -197,16 +215,16 @@ class TemplateRenderContext:
)
qtext = apply_custom_filters(partial.qnodes, self, front_side=None)
qtext, q_avtags = self.col().backend.extract_av_tags(qtext, True)
qout = self.col().backend.extract_a_v_tags(qtext, True)
atext = apply_custom_filters(partial.anodes, self, front_side=qtext)
atext, a_avtags = self.col().backend.extract_av_tags(atext, False)
aout = self.col().backend.extract_a_v_tags(atext, False)
output = TemplateRenderOutput(
question_text=qtext,
answer_text=atext,
question_av_tags=q_avtags,
answer_av_tags=a_avtags,
question_text=qout.text,
answer_text=aout.text,
question_av_tags=av_tags_to_native(qout.av_tags),
answer_av_tags=av_tags_to_native(aout.av_tags),
css=self.note_type()["css"],
)

View file

@ -33,7 +33,7 @@ LABEL_REPEATED = 3
def python_type(field):
type = python_type_inner(field)
if field.label == LABEL_REPEATED:
type = f"List[{type}]"
type = f"Sequence[{type}]"
return type

View file

@ -10,7 +10,7 @@ from typing import Iterable, List, Optional, Sequence, TypeVar
import aqt
from anki import hooks
from anki.rsbackend import TR, Interrupted, MediaCheckOutput, Progress, ProgressKind
from anki.rsbackend import TR, Interrupted, Progress, ProgressKind, pb
from aqt.qt import *
from aqt.utils import askUser, restoreGeom, saveGeom, showText, tooltip, tr
@ -52,7 +52,7 @@ class MediaChecker:
self.mw.taskman.run_on_main(lambda: self.mw.progress.update(progress.val))
return True
def _check(self) -> MediaCheckOutput:
def _check(self) -> pb.CheckMediaOut:
"Run the check on a background thread."
return self.mw.col.media.check()
@ -65,7 +65,7 @@ class MediaChecker:
if isinstance(exc, Interrupted):
return
output: MediaCheckOutput = future.result()
output: pb.CheckMediaOut = future.result()
report = output.report
# show report and offer to delete

View file

@ -175,6 +175,141 @@ impl BackendService for Backend {
.map(Into::into)
})
}
fn sched_timing_today(&mut self, _input: pb::Empty) -> Result<pb::SchedTimingTodayOut> {
self.with_col(|col| col.timing_today().map(Into::into))
}
fn deck_tree(&mut self, input: pb::DeckTreeIn) -> Result<pb::DeckTreeNode> {
let lim = if input.top_deck_id > 0 {
Some(DeckID(input.top_deck_id))
} else {
None
};
self.with_col(|col| col.deck_tree(input.include_counts, lim))
}
fn check_media(&mut self, _input: pb::Empty) -> Result<pb::CheckMediaOut> {
let callback =
|progress: usize| self.fire_progress_callback(Progress::MediaCheck(progress as u32));
self.with_col(|col| {
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
col.transact(None, |ctx| {
let mut checker = MediaChecker::new(ctx, &mgr, callback);
let mut output = checker.check()?;
let report = checker.summarize_output(&mut output);
Ok(pb::CheckMediaOut {
unused: output.unused,
missing: output.missing,
report,
have_trash: output.trash_count > 0,
})
})
})
}
fn search_cards(&mut self, input: pb::SearchCardsIn) -> Result<pb::SearchCardsOut> {
self.with_col(|col| {
let order = if let Some(order) = input.order {
use pb::sort_order::Value as V;
match order.value {
Some(V::None(_)) => SortMode::NoOrder,
Some(V::Custom(s)) => SortMode::Custom(s),
Some(V::FromConfig(_)) => SortMode::FromConfig,
Some(V::Builtin(b)) => SortMode::Builtin {
kind: sort_kind_from_pb(b.kind),
reverse: b.reverse,
},
None => SortMode::FromConfig,
}
} else {
SortMode::FromConfig
};
let cids = col.search_cards(&input.search, order)?;
Ok(pb::SearchCardsOut {
card_ids: cids.into_iter().map(|v| v.0).collect(),
})
})
}
fn search_notes(&mut self, input: pb::SearchNotesIn) -> Result<pb::SearchNotesOut> {
self.with_col(|col| {
let nids = col.search_notes(&input.search)?;
Ok(pb::SearchNotesOut {
note_ids: nids.into_iter().map(|v| v.0).collect(),
})
})
}
fn local_minutes_west(&mut self, input: pb::Int64) -> BackendResult<pb::Int32> {
Ok(pb::Int32 {
val: local_minutes_west_for_stamp(input.val),
})
}
fn strip_av_tags(&mut self, input: pb::String) -> BackendResult<pb::String> {
Ok(pb::String {
val: strip_av_tags(&input.val).into(),
})
}
fn extract_av_tags(
&mut self,
input: pb::ExtractAvTagsIn,
) -> BackendResult<pb::ExtractAvTagsOut> {
let (text, tags) = extract_av_tags(&input.text, input.question_side);
let pt_tags = tags
.into_iter()
.map(|avtag| match avtag {
AVTag::SoundOrVideo(file) => pb::AvTag {
value: Some(pb::av_tag::Value::SoundOrVideo(file)),
},
AVTag::TextToSpeech {
field_text,
lang,
voices,
other_args,
speed,
} => pb::AvTag {
value: Some(pb::av_tag::Value::Tts(pb::TtsTag {
field_text,
lang,
voices,
other_args,
speed,
})),
},
})
.collect();
Ok(pb::ExtractAvTagsOut {
text: text.into(),
av_tags: pt_tags,
})
}
fn extract_latex(&mut self, input: pb::ExtractLatexIn) -> BackendResult<pb::ExtractLatexOut> {
let func = if input.expand_clozes {
extract_latex_expanding_clozes
} else {
extract_latex
};
let (text, extracted) = func(&input.text, input.svg);
Ok(pb::ExtractLatexOut {
text,
latex: extracted
.into_iter()
.map(|e: ExtractedLatex| pb::ExtractedLatex {
filename: e.fname,
latex_body: e.latex,
})
.collect(),
})
}
}
impl Backend {
@ -267,20 +402,11 @@ impl Backend {
) -> Result<pb::backend_output::Value> {
use pb::backend_output::Value as OValue;
Ok(match ival {
Value::SchedTimingToday(_) => OValue::SchedTimingToday(self.sched_timing_today()?),
Value::DeckTree(input) => OValue::DeckTree(self.deck_tree(input)?),
Value::LocalMinutesWest(stamp) => {
OValue::LocalMinutesWest(local_minutes_west_for_stamp(stamp))
}
Value::StripAvTags(text) => OValue::StripAvTags(strip_av_tags(&text).into()),
Value::ExtractAvTags(input) => OValue::ExtractAvTags(self.extract_av_tags(input)),
Value::ExtractLatex(input) => OValue::ExtractLatex(self.extract_latex(input)),
Value::AddMediaFile(input) => OValue::AddMediaFile(self.add_media_file(input)?),
Value::SyncMedia(input) => {
self.sync_media(input)?;
OValue::SyncMedia(Empty {})
}
Value::CheckMedia(_) => OValue::CheckMedia(self.check_media()?),
Value::TrashMediaFiles(input) => {
self.remove_media_files(&input.fnames)?;
OValue::TrashMediaFiles(Empty {})
@ -313,8 +439,6 @@ impl Backend {
self.close_collection(input.downgrade_to_schema11)?;
OValue::CloseCollection(Empty {})
}
Value::SearchCards(input) => OValue::SearchCards(self.search_cards(input)?),
Value::SearchNotes(input) => OValue::SearchNotes(self.search_notes(input)?),
Value::GetCard(cid) => OValue::GetCard(self.get_card(cid)?),
Value::UpdateCard(card) => {
self.update_card(card)?;
@ -422,12 +546,6 @@ impl Backend {
self.set_preferences(prefs)?;
pb::Empty {}
}),
Value::RenderExistingCard(input) => {
OValue::RenderExistingCard(self.render_existing_card(input)?)
}
Value::RenderUncommittedCard(input) => {
OValue::RenderUncommittedCard(self.render_uncommitted_card(input)?)
}
Value::ClozeNumbersInNote(note) => {
OValue::ClozeNumbersInNote(self.cloze_numbers_in_note(note))
}
@ -497,71 +615,6 @@ impl Backend {
self.progress_callback = progress_cb;
}
fn sched_timing_today(&self) -> Result<pb::SchedTimingTodayOut> {
self.with_col(|col| col.timing_today().map(Into::into))
}
fn deck_tree(&self, input: pb::DeckTreeIn) -> Result<pb::DeckTreeNode> {
let lim = if input.top_deck_id > 0 {
Some(DeckID(input.top_deck_id))
} else {
None
};
self.with_col(|col| col.deck_tree(input.include_counts, lim))
}
fn extract_av_tags(&self, input: pb::ExtractAvTagsIn) -> pb::ExtractAvTagsOut {
let (text, tags) = extract_av_tags(&input.text, input.question_side);
let pt_tags = tags
.into_iter()
.map(|avtag| match avtag {
AVTag::SoundOrVideo(file) => pb::AvTag {
value: Some(pb::av_tag::Value::SoundOrVideo(file)),
},
AVTag::TextToSpeech {
field_text,
lang,
voices,
other_args,
speed,
} => pb::AvTag {
value: Some(pb::av_tag::Value::Tts(pb::TtsTag {
field_text,
lang,
voices,
other_args,
speed,
})),
},
})
.collect();
pb::ExtractAvTagsOut {
text: text.into(),
av_tags: pt_tags,
}
}
fn extract_latex(&self, input: pb::ExtractLatexIn) -> pb::ExtractLatexOut {
let func = if input.expand_clozes {
extract_latex_expanding_clozes
} else {
extract_latex
};
let (text, extracted) = func(&input.text, input.svg);
pb::ExtractLatexOut {
text,
latex: extracted
.into_iter()
.map(|e: ExtractedLatex| pb::ExtractedLatex {
filename: e.fname,
latex_body: e.latex,
})
.collect(),
}
}
fn add_media_file(&mut self, input: pb::AddMediaFileIn) -> Result<String> {
self.with_col(|col| {
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
@ -626,28 +679,6 @@ impl Backend {
}
}
fn check_media(&self) -> Result<pb::MediaCheckOut> {
let callback =
|progress: usize| self.fire_progress_callback(Progress::MediaCheck(progress as u32));
self.with_col(|col| {
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
col.transact(None, |ctx| {
let mut checker = MediaChecker::new(ctx, &mgr, callback);
let mut output = checker.check()?;
let report = checker.summarize_output(&mut output);
Ok(pb::MediaCheckOut {
unused: output.unused,
missing: output.missing,
report,
have_trash: output.trash_count > 0,
})
})
})
}
fn remove_media_files(&self, fnames: &[String]) -> Result<()> {
self.with_col(|col| {
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
@ -720,39 +751,6 @@ impl Backend {
self.with_col(|col| db_command_bytes(&col.storage, input))
}
fn search_cards(&self, input: pb::SearchCardsIn) -> Result<pb::SearchCardsOut> {
self.with_col(|col| {
let order = if let Some(order) = input.order {
use pb::sort_order::Value as V;
match order.value {
Some(V::None(_)) => SortMode::NoOrder,
Some(V::Custom(s)) => SortMode::Custom(s),
Some(V::FromConfig(_)) => SortMode::FromConfig,
Some(V::Builtin(b)) => SortMode::Builtin {
kind: sort_kind_from_pb(b.kind),
reverse: b.reverse,
},
None => SortMode::FromConfig,
}
} else {
SortMode::FromConfig
};
let cids = col.search_cards(&input.search, order)?;
Ok(pb::SearchCardsOut {
card_ids: cids.into_iter().map(|v| v.0).collect(),
})
})
}
fn search_notes(&self, input: pb::SearchNotesIn) -> Result<pb::SearchNotesOut> {
self.with_col(|col| {
let nids = col.search_notes(&input.search)?;
Ok(pb::SearchNotesOut {
note_ids: nids.into_iter().map(|v| v.0).collect(),
})
})
}
fn get_card(&self, cid: i64) -> Result<pb::GetCardOut> {
let card = self.with_col(|col| col.storage.get_card(CardID(cid)))?;
Ok(pb::GetCardOut {