From 6f0bf58d4977b8b801de3fe287bacd25af37cb2d Mon Sep 17 00:00:00 2001 From: Abdo Date: Sat, 16 Sep 2023 06:51:32 +0300 Subject: [PATCH] Add a backend method to add notes in bulk (#2659) * Add a backend method to add notes in bulk * i -> idx * Remove duplicate assignment * Allow add_notes to work with multiple deck IDs * Rename note_deck_id to requests --- proto/anki/notes.proto | 10 +++++ pylib/anki/collection.py | 25 ++++++++++++- rslib/src/image_occlusion/imagedata.rs | 7 +--- rslib/src/notes/mod.rs | 52 ++++++++++++++++++-------- rslib/src/notes/service.rs | 18 +++++++++ 5 files changed, 90 insertions(+), 22 deletions(-) diff --git a/proto/anki/notes.proto b/proto/anki/notes.proto index 667cb815f..39bbcf1e2 100644 --- a/proto/anki/notes.proto +++ b/proto/anki/notes.proto @@ -15,6 +15,7 @@ import "anki/cards.proto"; service NotesService { rpc NewNote(notetypes.NotetypeId) returns (Note); rpc AddNote(AddNoteRequest) returns (AddNoteResponse); + rpc AddNotes(AddNotesRequest) returns (AddNotesResponse); rpc DefaultsForAdding(DefaultsForAddingRequest) returns (DeckAndNotetype); rpc DefaultDeckForNotetype(notetypes.NotetypeId) returns (decks.DeckId); rpc UpdateNotes(UpdateNotesRequest) returns (collection.OpChanges); @@ -62,6 +63,15 @@ message AddNoteResponse { int64 note_id = 2; } +message AddNotesRequest { + repeated AddNoteRequest requests = 1; +} + +message AddNotesResponse { + collection.OpChanges changes = 1; + repeated int64 nids = 2; +} + message UpdateNotesRequest { repeated Note notes = 1; bool skip_undo_entry = 2; diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index a17664e6a..4cd355fdd 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Any, Generator, Literal, Sequence, Union, cast +from typing import Any, Generator, Iterable, Literal, Sequence, Union, cast from anki import ( ankiweb_pb2, @@ -14,6 +14,7 @@ from anki import ( image_occlusion_pb2, import_export_pb2, links_pb2, + notes_pb2, search_pb2, stats_pb2, sync_pb2, @@ -127,6 +128,12 @@ class CardIdsLimit: ExportLimit = Union[DeckIdLimit, NoteIdsLimit, CardIdsLimit, None] +@dataclass +class AddNoteRequest: + note: Note + deck_id: DeckId + + class Collection(DeprecatedNamesMixin): sched: V1Scheduler | V2Scheduler | V3Scheduler @@ -580,6 +587,22 @@ class Collection(DeprecatedNamesMixin): note.id = NoteId(out.note_id) return out.changes + def add_notes(self, requests: Iterable[AddNoteRequest]) -> OpChanges: + for request in requests: + hooks.note_will_be_added(self, request.note, request.deck_id) + out = self._backend.add_notes( + requests=[ + notes_pb2.AddNoteRequest( + note=request.note._to_backend_note(), deck_id=request.deck_id + ) + for request in requests + ] + ) + for idx, request in enumerate(requests): + request.note.id = NoteId(out.nids[idx]) + + return out.changes + def remove_notes(self, note_ids: Sequence[NoteId]) -> OpChangesWithCount: hooks.notes_will_be_deleted(self, note_ids) return self._backend.remove_notes(note_ids=note_ids, card_ids=[]) diff --git a/rslib/src/image_occlusion/imagedata.rs b/rslib/src/image_occlusion/imagedata.rs index 7c47a6e75..3035d3b52 100644 --- a/rslib/src/image_occlusion/imagedata.rs +++ b/rslib/src/image_occlusion/imagedata.rs @@ -13,7 +13,6 @@ use anki_proto::image_occlusion::GetImageOcclusionNoteResponse; use regex::Regex; use crate::media::MediaManager; -use crate::notetype::CardGenContext; use crate::prelude::*; impl Collection { @@ -66,11 +65,7 @@ impl Collection { note.set_field(2, header)?; note.set_field(3, back_extra)?; note.tags = tags; - - let last_deck = col.get_last_deck_added_to_for_notetype(note.notetype_id); - let ctx = CardGenContext::new(nt.as_ref(), last_deck, col.usn()?); - let norm = col.get_config_bool(BoolKey::NormalizeNoteText); - col.add_note_inner(&ctx, &mut note, current_deck.id, norm)?; + col.add_note_inner(&mut note, current_deck.id)?; Ok(()) }) diff --git a/rslib/src/notes/mod.rs b/rslib/src/notes/mod.rs index c0f28265a..1efd1c2ba 100644 --- a/rslib/src/notes/mod.rs +++ b/rslib/src/notes/mod.rs @@ -15,6 +15,9 @@ use sha1::Sha1; use crate::cloze::contains_cloze; use crate::define_newtype; +use crate::error; +use crate::error::AnkiError; +use crate::error::OrInvalid; use crate::notetype::CardGenContext; use crate::notetype::NoteField; use crate::ops::StateChanges; @@ -66,16 +69,35 @@ impl Note { } } +#[derive(Debug, Clone)] +pub struct AddNoteRequest { + pub note: Note, + pub deck_id: DeckId, +} + +impl TryFrom for AddNoteRequest { + type Error = AnkiError; + + fn try_from(request: anki_proto::notes::AddNoteRequest) -> error::Result { + Ok(Self { + note: request.note.or_invalid("no note provided")?.into(), + deck_id: DeckId(request.deck_id), + }) + } +} + impl Collection { pub fn add_note(&mut self, note: &mut Note, did: DeckId) -> Result> { + self.transact(Op::AddNote, |col| col.add_note_inner(note, did)) + } + + pub fn add_notes(&mut self, requests: &mut [AddNoteRequest]) -> Result> { self.transact(Op::AddNote, |col| { - let nt = col - .get_notetype(note.notetype_id)? - .or_invalid("missing note type")?; - let last_deck = col.get_last_deck_added_to_for_notetype(note.notetype_id); - let ctx = CardGenContext::new(nt.as_ref(), last_deck, col.usn()?); - let norm = col.get_config_bool(BoolKey::NormalizeNoteText); - col.add_note_inner(&ctx, note, did, norm) + for request in requests { + col.add_note_inner(&mut request.note, request.deck_id)?; + } + + Ok(()) }) } @@ -333,18 +355,18 @@ impl Collection { Ok(()) } - pub(crate) fn add_note_inner( - &mut self, - ctx: &CardGenContext<&Notetype>, - note: &mut Note, - did: DeckId, - normalize_text: bool, - ) -> Result<()> { + pub(crate) fn add_note_inner(&mut self, note: &mut Note, did: DeckId) -> Result<()> { + let nt = self + .get_notetype(note.notetype_id)? + .or_invalid("missing note type")?; + let last_deck = self.get_last_deck_added_to_for_notetype(note.notetype_id); + let ctx = CardGenContext::new(nt.as_ref(), last_deck, self.usn()?); + let normalize_text = self.get_config_bool(BoolKey::NormalizeNoteText); self.canonify_note_tags(note, ctx.usn)?; note.prepare_for_update(ctx.notetype, normalize_text)?; note.set_modified(ctx.usn); self.add_note_only_undoable(note)?; - self.generate_cards_for_new_note(ctx, note, did)?; + self.generate_cards_for_new_note(&ctx, note, did)?; self.set_last_deck_for_notetype(note.notetype_id, did)?; self.set_last_notetype_for_deck(did, note.notetype_id)?; self.set_current_notetype_id(note.notetype_id) diff --git a/rslib/src/notes/service.rs b/rslib/src/notes/service.rs index 47ecb9711..e5388da3a 100644 --- a/rslib/src/notes/service.rs +++ b/rslib/src/notes/service.rs @@ -6,8 +6,10 @@ use crate::cloze::add_cloze_numbers_in_string; use crate::collection::Collection; use crate::decks::DeckId; use crate::error; +use crate::error::AnkiError; use crate::error::OrInvalid; use crate::error::OrNotFound; +use crate::notes::AddNoteRequest; use crate::notes::Note; use crate::notes::NoteId; use crate::prelude::IntoNewtypeVec; @@ -39,6 +41,22 @@ impl crate::services::NotesService for Collection { }) } + fn add_notes( + &mut self, + input: anki_proto::notes::AddNotesRequest, + ) -> error::Result { + let mut requests = input + .requests + .into_iter() + .map(TryInto::try_into) + .collect::, AnkiError>>()?; + let changes = self.add_notes(&mut requests)?; + Ok(anki_proto::notes::AddNotesResponse { + nids: requests.iter().map(|r| r.note.id.0).collect(), + changes: Some(changes.into()), + }) + } + fn defaults_for_adding( &mut self, input: anki_proto::notes::DefaultsForAddingRequest,