diff --git a/proto/backend.proto b/proto/backend.proto index e1a124bd0..4251d5ff3 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -29,6 +29,10 @@ message String { string val = 1; } +message Bytes { + bytes val = 1; +} + // New style RPC definitions /////////////////////////////////////////////////////////// @@ -41,9 +45,16 @@ service BackendService { rpc SearchNotes (SearchNotesIn) returns (SearchNotesOut); rpc CheckMedia (Empty) returns (CheckMediaOut); rpc LocalMinutesWest (Int64) returns (Int32); - rpc StripAvTags (String) returns (String); + rpc StripAVTags (String) returns (String); rpc ExtractAVTags (ExtractAVTagsIn) returns (ExtractAVTagsOut); rpc ExtractLatex (ExtractLatexIn) returns (ExtractLatexOut); + rpc DeckTreeLegacy (Empty) returns (Bytes); + rpc CheckDatabase (Empty) returns (CheckDatabaseOut); + rpc GetEmptyCards (Empty) returns (EmptyCardsReport); + rpc SyncMedia (SyncMediaIn) returns (Empty); + rpc TrashMediaFiles (TrashMediaFilesIn) returns (Empty); + rpc GetDeckLegacy (Int64) returns (Bytes); + rpc GetDeckIDByName (String) returns (Int64); } // Protobuf stored in .anki2 files @@ -290,8 +301,6 @@ message I18nBackendInit { message BackendInput { oneof value { AddMediaFileIn add_media_file = 26; - SyncMediaIn sync_media = 27; - TrashMediaFilesIn trash_media_files = 29; TranslateStringIn translate_string = 30; FormatTimeSpanIn format_time_span = 31; StudiedTodayIn studied_today = 32; @@ -320,7 +329,6 @@ message BackendInput { int32 get_changed_notetypes = 56; AddOrUpdateNotetypeIn add_or_update_notetype = 57; Empty get_all_decks = 58; - Empty check_database = 59; StockNoteType get_stock_notetype_legacy = 60; int64 get_notetype_legacy = 61; Empty get_notetype_names = 62; @@ -331,14 +339,10 @@ message BackendInput { AddNoteIn add_note = 67; Note update_note = 68; int64 get_note = 69; - Empty get_empty_cards = 70; - int64 get_deck_legacy = 71; - string get_deck_id_by_name = 72; GetDeckNamesIn get_deck_names = 73; AddOrUpdateDeckLegacyIn add_or_update_deck_legacy = 74; bool new_deck_legacy = 75; int64 remove_deck = 76; - Empty deck_tree_legacy = 77; FieldNamesForNotesIn field_names_for_notes = 78; FindAndReplaceIn find_and_replace = 79; AfterNoteUpdatesIn after_note_updates = 80; @@ -363,8 +367,6 @@ message BackendOutput { // fallible commands string add_media_file = 26; - Empty sync_media = 27; - Empty trash_media_files = 29; Empty empty_trash = 34; Empty restore_trash = 35; Empty open_collection = 36; @@ -388,7 +390,6 @@ message BackendOutput { bytes get_changed_notetypes = 56; int64 add_or_update_notetype = 57; bytes get_all_decks = 58; - CheckDatabaseOut check_database = 59; bytes get_notetype_legacy = 61; NoteTypeNames get_notetype_names = 62; NoteTypeUseCounts get_notetype_names_and_counts = 63; @@ -398,14 +399,10 @@ message BackendOutput { int64 add_note = 67; Empty update_note = 68; Note get_note = 69; - EmptyCardsReport get_empty_cards = 70; - bytes get_deck_legacy = 71; - int64 get_deck_id_by_name = 72; DeckNames get_deck_names = 73; int64 add_or_update_deck_legacy = 74; bytes new_deck_legacy = 75; Empty remove_deck = 76; - bytes deck_tree_legacy = 77; FieldNamesForNotesOut field_names_for_notes = 78; uint32 find_and_replace = 79; Empty after_note_updates = 80; diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index ea54a6091..c01579742 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -593,7 +593,7 @@ select id from notes where mid = ?) limit 1""" """ self.save(trx=False) try: - problems = self.backend.check_database() + problems = list(self.backend.check_database()) ok = not problems problems.append(self.tr(TR.DATABASE_CHECK_REBUILT)) except DBError as e: diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 0441f4483..99ca29787 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -11,7 +11,7 @@ import anki.backend_pb2 as pb from anki.consts import * from anki.errors import DeckRenameError from anki.lang import _ -from anki.rsbackend import DeckTreeNode +from anki.rsbackend import DeckTreeNode, NotFoundError, from_json_bytes from anki.utils import ids2str, intTime # legacy code may pass this in as the type argument to .id() @@ -113,10 +113,13 @@ class DeckManager: ) def id_for_name(self, name: str) -> Optional[int]: - return self.col.backend.get_deck_id_by_name(name) + return self.col.backend.get_deck_id_by_name(name) or None def get_legacy(self, did: int) -> Optional[Dict]: - return self.col.backend.get_deck_legacy(did) + try: + return from_json_bytes(self.col.backend.get_deck_legacy(did)) + except NotFoundError: + return None def have(self, id: int) -> bool: return not self.get_legacy(int(id)) diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index cf3c28b1c..b0b78ea0c 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -246,17 +246,6 @@ class RustBackend: ) ).add_media_file - def sync_media(self, hkey: str, endpoint: str) -> None: - self._run_command( - pb.BackendInput(sync_media=pb.SyncMediaIn(hkey=hkey, endpoint=endpoint,)), - release_gil=True, - ) - - def trash_media_files(self, fnames: List[str]) -> None: - self._run_command( - pb.BackendInput(trash_media_files=pb.TrashMediaFilesIn(fnames=fnames)) - ) - def translate(self, key: TR, **kwargs: Union[str, int, float]) -> str: return self._run_command( pb.BackendInput(translate_string=translate_string_in(key, **kwargs)) @@ -506,20 +495,6 @@ class RustBackend: except NotFoundError: return None - def empty_cards_report(self) -> pb.EmptyCardsReport: - return self._run_command( - pb.BackendInput(get_empty_cards=pb.Empty()), release_gil=True - ).get_empty_cards - - def get_deck_legacy(self, did: int) -> Optional[Dict]: - try: - bytes = self._run_command( - pb.BackendInput(get_deck_legacy=did) - ).get_deck_legacy - return orjson.loads(bytes) - except NotFoundError: - return None - def get_deck_names_and_ids( self, skip_empty_default: bool, include_filtered: bool = True ) -> Sequence[pb.DeckNameID]: @@ -551,30 +526,9 @@ class RustBackend: ).new_deck_legacy return orjson.loads(jstr) - def get_deck_id_by_name(self, name: str) -> Optional[int]: - return ( - self._run_command( - pb.BackendInput(get_deck_id_by_name=name) - ).get_deck_id_by_name - or None - ) - def remove_deck(self, did: int) -> None: self._run_command(pb.BackendInput(remove_deck=did)) - def check_database(self) -> List[str]: - return list( - self._run_command( - pb.BackendInput(check_database=pb.Empty()), release_gil=True - ).check_database.problems - ) - - def legacy_deck_tree(self) -> Sequence: - bytes = self._run_command( - pb.BackendInput(deck_tree_legacy=pb.Empty()) - ).deck_tree_legacy - return orjson.loads(bytes)[5] - def field_names_for_note_ids(self, nids: List[int]) -> Sequence[str]: return self._run_command( pb.BackendInput(field_names_for_notes=pb.FieldNamesForNotesIn(nids=nids)), @@ -725,7 +679,7 @@ class RustBackend: output.ParseFromString(self._run_command2(9, input)) return output.val - def extract_a_v_tags(self, text: str, question_side: bool) -> pb.ExtractAVTagsOut: + def extract_av_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)) @@ -739,6 +693,48 @@ class RustBackend: output.ParseFromString(self._run_command2(11, input)) return output + def deck_tree_legacy(self) -> bytes: + input = pb.Empty() + output = pb.Bytes() + output.ParseFromString(self._run_command2(12, input)) + return output.val + + def check_database(self) -> Sequence[str]: + input = pb.Empty() + output = pb.CheckDatabaseOut() + output.ParseFromString(self._run_command2(13, input)) + return output.problems + + def get_empty_cards(self) -> pb.EmptyCardsReport: + input = pb.Empty() + output = pb.EmptyCardsReport() + output.ParseFromString(self._run_command2(14, input)) + return output + + def sync_media(self, hkey: str, endpoint: str) -> pb.Empty: + input = pb.SyncMediaIn(hkey=hkey, endpoint=endpoint) + output = pb.Empty() + output.ParseFromString(self._run_command2(15, input)) + return output + + def trash_media_files(self, fnames: Sequence[str]) -> pb.Empty: + input = pb.TrashMediaFilesIn(fnames=fnames) + output = pb.Empty() + output.ParseFromString(self._run_command2(16, input)) + return output + + def get_deck_legacy(self, val: int) -> bytes: + input = pb.Int64(val=val) + output = pb.Bytes() + output.ParseFromString(self._run_command2(17, input)) + return output.val + + def get_deck_id_by_name(self, val: str) -> int: + input = pb.String(val=val) + output = pb.Int64() + output.ParseFromString(self._run_command2(18, input)) + return output.val + # @@AUTOGEN@@ diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index 28eef6a77..21446671e 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -213,7 +213,7 @@ order by due""" print( "deckDueTree() is deprecated; use decks.deck_tree() for a tree without counts, or sched.deck_due_tree()" ) - return self.col.backend.legacy_deck_tree() + return self.col.backend.deck_tree_legacy() def deck_due_tree(self, top_deck_id: int = 0) -> DeckTreeNode: """Returns a tree of decks with counts. diff --git a/pylib/anki/template.py b/pylib/anki/template.py index 6c4aafbf2..4d54208ed 100644 --- a/pylib/anki/template.py +++ b/pylib/anki/template.py @@ -215,10 +215,10 @@ class TemplateRenderContext: ) qtext = apply_custom_filters(partial.qnodes, self, front_side=None) - qout = self.col().backend.extract_a_v_tags(qtext, True) + qout = self.col().backend.extract_av_tags(qtext, True) atext = apply_custom_filters(partial.anodes, self, front_side=qtext) - aout = self.col().backend.extract_a_v_tags(atext, False) + aout = self.col().backend.extract_av_tags(atext, False) output = TemplateRenderOutput( question_text=qout.text, diff --git a/pylib/tests/test_cards.py b/pylib/tests/test_cards.py index cd3f61bc9..28d7c8a51 100644 --- a/pylib/tests/test_cards.py +++ b/pylib/tests/test_cards.py @@ -51,7 +51,7 @@ def test_genrem(): t = m["tmpls"][1] t["qfmt"] = "{{Back}}" mm.save(m, templates=True) - rep = d.backend.empty_cards_report() + rep = d.backend.get_empty_cards() for note in rep.notes: d.remCards(note.card_ids) assert len(f.cards()) == 1 diff --git a/pylib/tools/genbackend.py b/pylib/tools/genbackend.py index 2224dc388..47c4c1428 100755 --- a/pylib/tools/genbackend.py +++ b/pylib/tools/genbackend.py @@ -55,6 +55,17 @@ def python_type_inner(field): raise Exception(f"unknown type: {type}") +# get_deck_i_d -> get_deck_id etc +def fix_snakecase(name): + for fix in "a_v", "i_d": + name = re.sub( + f"(\w)({fix})(\w)", + lambda m: m.group(1) + m.group(2).replace("_", "") + m.group(3), + name, + ) + return name + + def get_input_args(msg): fields = sorted(msg.fields, key=lambda x: x.number) return ", ".join(["self"] + [f"{f.name}: {python_type(f)}" for f in fields]) @@ -68,7 +79,7 @@ def get_input_assign(msg): def render_method(method, idx): input_args = get_input_args(method.input_type) input_assign = get_input_assign(method.input_type) - name = stringcase.snakecase(method.name) + name = fix_snakecase(stringcase.snakecase(method.name)) if len(method.output_type.fields) == 1: # unwrap single return arg f = method.output_type.fields[0] diff --git a/qt/aqt/emptycards.py b/qt/aqt/emptycards.py index 909090411..ce5c597b1 100644 --- a/qt/aqt/emptycards.py +++ b/qt/aqt/emptycards.py @@ -24,7 +24,7 @@ def show_empty_cards(mw: aqt.main.AnkiQt) -> None: diag = EmptyCardsDialog(mw, report) diag.show() - mw.taskman.run_in_background(mw.col.backend.empty_cards_report, on_done) + mw.taskman.run_in_background(mw.col.backend.get_empty_cards, on_done) class EmptyCardsDialog(QDialog): diff --git a/qt/aqt/legacy.py b/qt/aqt/legacy.py index d13e3a8aa..828025a58 100644 --- a/qt/aqt/legacy.py +++ b/qt/aqt/legacy.py @@ -11,6 +11,7 @@ from typing import List import anki import aqt from anki.sound import SoundOrVideoTag +from anki.template import av_tags_to_native from aqt.theme import theme_manager # Routines removed from pylib/ @@ -24,8 +25,12 @@ def bodyClass(col, card) -> str: def allSounds(text) -> List: print("allSounds() deprecated") - text, tags = aqt.mw.col.backend.extract_av_tags(text, True) - return [x.filename for x in tags if isinstance(x, SoundOrVideoTag)] + out = aqt.mw.col.backend.extract_av_tags(text, True) + return [ + x.filename + for x in av_tags_to_native(out.av_tags) + if isinstance(x, SoundOrVideoTag) + ] def stripSounds(text) -> str: diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 9273d1320..0d667847c 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -147,6 +147,30 @@ pub fn init_backend(init_msg: &[u8]) -> std::result::Result { Ok(Backend::new(i18n, input.server)) } +impl From> for pb::Bytes { + fn from(val: Vec) -> Self { + pb::Bytes { val } + } +} + +impl From for pb::String { + fn from(val: String) -> Self { + pb::String { val } + } +} + +impl From for pb::Int64 { + fn from(val: i64) -> Self { + pb::Int64 { val } + } +} + +impl From<()> for pb::Empty { + fn from(_val: ()) -> Self { + pb::Empty {} + } +} + impl BackendService for Backend { fn render_existing_card( &mut self, @@ -310,6 +334,92 @@ impl BackendService for Backend { .collect(), }) } + + fn check_database(&mut self, _input: pb::Empty) -> BackendResult { + self.with_col(|col| { + col.check_database().map(|problems| pb::CheckDatabaseOut { + problems: problems.to_i18n_strings(&col.i18n), + }) + }) + } + + fn deck_tree_legacy(&mut self, _input: pb::Empty) -> BackendResult { + self.with_col(|col| { + let tree = col.legacy_deck_tree()?; + serde_json::to_vec(&tree) + .map_err(Into::into) + .map(Into::into) + }) + } + + fn get_empty_cards(&mut self, _input: pb::Empty) -> Result { + self.with_col(|col| { + let mut empty = col.empty_cards()?; + let report = col.empty_cards_report(&mut empty)?; + + let mut outnotes = vec![]; + for (_ntid, notes) in empty { + outnotes.extend(notes.into_iter().map(|e| pb::NoteWithEmptyCards { + note_id: e.nid.0, + will_delete_note: e.empty.len() == e.current_count, + card_ids: e.empty.into_iter().map(|(_ord, id)| id.0).collect(), + })) + } + Ok(pb::EmptyCardsReport { + report, + notes: outnotes, + }) + }) + } + + fn get_deck_legacy(&mut self, input: pb::Int64) -> Result { + self.with_col(|col| { + let deck: DeckSchema11 = col + .storage + .get_deck(DeckID(input.val))? + .ok_or(AnkiError::NotFound)? + .into(); + serde_json::to_vec(&deck) + .map_err(Into::into) + .map(Into::into) + }) + } + + fn get_deck_id_by_name(&mut self, input: pb::String) -> Result { + self.with_col(|col| { + col.get_deck_id(&input.val) + .map(|d| d.map(|d| d.0).unwrap_or_default()) + .map(Into::into) + }) + } + + fn sync_media(&mut self, input: SyncMediaIn) -> BackendResult { + let mut guard = self.col.lock().unwrap(); + + let col = guard.as_mut().unwrap(); + col.set_media_sync_running()?; + + let folder = col.media_folder.clone(); + let db = col.media_db.clone(); + let log = col.log.clone(); + + drop(guard); + + let res = self.sync_media_inner(input, folder, db, log); + + self.with_col(|col| col.set_media_sync_finished())?; + + res.map(Into::into) + } + + fn trash_media_files(&mut self, input: pb::TrashMediaFilesIn) -> BackendResult { + self.with_col(|col| { + let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; + let mut ctx = mgr.dbctx(); + mgr.remove_files(&mut ctx, &input.fnames) + }) + .map(Into::into) + } } impl Backend { @@ -403,14 +513,6 @@ impl Backend { use pb::backend_output::Value as OValue; Ok(match ival { Value::AddMediaFile(input) => OValue::AddMediaFile(self.add_media_file(input)?), - Value::SyncMedia(input) => { - self.sync_media(input)?; - OValue::SyncMedia(Empty {}) - } - Value::TrashMediaFiles(input) => { - self.remove_media_files(&input.fnames)?; - OValue::TrashMediaFiles(Empty {}) - } Value::TranslateString(input) => OValue::TranslateString(self.translate_string(input)), Value::FormatTimeSpan(input) => OValue::FormatTimeSpan(self.format_time_span(input)), Value::StudiedToday(input) => OValue::StudiedToday(studied_today( @@ -509,11 +611,6 @@ impl Backend { OValue::UpdateNote(pb::Empty {}) } Value::GetNote(nid) => OValue::GetNote(self.get_note(nid)?), - Value::GetEmptyCards(_) => OValue::GetEmptyCards(self.get_empty_cards()?), - Value::GetDeckLegacy(did) => OValue::GetDeckLegacy(self.get_deck_legacy(did)?), - Value::GetDeckIdByName(name) => { - OValue::GetDeckIdByName(self.get_deck_id_by_name(&name)?) - } Value::GetDeckNames(input) => OValue::GetDeckNames(self.get_deck_names(input)?), Value::AddOrUpdateDeckLegacy(input) => { OValue::AddOrUpdateDeckLegacy(self.add_or_update_deck_legacy(input)?) @@ -526,8 +623,6 @@ impl Backend { self.remove_deck(did)?; pb::Empty {} }), - Value::CheckDatabase(_) => OValue::CheckDatabase(self.check_database()?), - Value::DeckTreeLegacy(_) => OValue::DeckTreeLegacy(self.deck_tree_legacy()?), Value::FieldNamesForNotes(input) => { OValue::FieldNamesForNotes(self.field_names_for_notes(input)?) } @@ -625,25 +720,6 @@ impl Backend { }) } - fn sync_media(&mut self, input: SyncMediaIn) -> Result<()> { - let mut guard = self.col.lock().unwrap(); - - let col = guard.as_mut().unwrap(); - col.set_media_sync_running()?; - - let folder = col.media_folder.clone(); - let db = col.media_db.clone(); - let log = col.log.clone(); - - drop(guard); - - let res = self.sync_media_inner(input, folder, db, log); - - self.with_col(|col| col.set_media_sync_finished())?; - - res - } - fn sync_media_inner( &mut self, input: pb::SyncMediaIn, @@ -679,14 +755,6 @@ impl Backend { } } - fn remove_media_files(&self, fnames: &[String]) -> Result<()> { - self.with_col(|col| { - let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; - let mut ctx = mgr.dbctx(); - mgr.remove_files(&mut ctx, fnames) - }) - } - fn translate_string(&self, input: pb::TranslateStringIn) -> String { let key = match pb::FluentString::from_i32(input.key) { Some(key) => key, @@ -1010,44 +1078,6 @@ impl Backend { }) } - fn get_empty_cards(&self) -> Result { - self.with_col(|col| { - let mut empty = col.empty_cards()?; - let report = col.empty_cards_report(&mut empty)?; - - let mut outnotes = vec![]; - for (_ntid, notes) in empty { - outnotes.extend(notes.into_iter().map(|e| pb::NoteWithEmptyCards { - note_id: e.nid.0, - will_delete_note: e.empty.len() == e.current_count, - card_ids: e.empty.into_iter().map(|(_ord, id)| id.0).collect(), - })) - } - Ok(pb::EmptyCardsReport { - report, - notes: outnotes, - }) - }) - } - - fn get_deck_legacy(&self, did: i64) -> Result> { - self.with_col(|col| { - let deck: DeckSchema11 = col - .storage - .get_deck(DeckID(did))? - .ok_or(AnkiError::NotFound)? - .into(); - serde_json::to_vec(&deck).map_err(Into::into) - }) - } - - fn get_deck_id_by_name(&self, human_name: &str) -> Result { - self.with_col(|col| { - col.get_deck_id(human_name) - .map(|d| d.map(|d| d.0).unwrap_or_default()) - }) - } - fn get_deck_names(&self, input: pb::GetDeckNamesIn) -> Result { self.with_col(|col| { let names = if input.include_filtered { @@ -1094,21 +1124,6 @@ impl Backend { self.with_col(|col| col.remove_deck_and_child_decks(DeckID(did))) } - fn check_database(&self) -> Result { - self.with_col(|col| { - col.check_database().map(|problems| pb::CheckDatabaseOut { - problems: problems.to_i18n_strings(&col.i18n), - }) - }) - } - - fn deck_tree_legacy(&self) -> Result> { - self.with_col(|col| { - let tree = col.legacy_deck_tree()?; - serde_json::to_vec(&tree).map_err(Into::into) - }) - } - fn field_names_for_notes( &self, input: pb::FieldNamesForNotesIn,