mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
move dupe check to backend
This commit is contained in:
parent
d2dd619f18
commit
c8d13209cd
7 changed files with 95 additions and 41 deletions
|
@ -136,6 +136,7 @@ service BackendService {
|
||||||
rpc ClozeNumbersInNote (Note) returns (ClozeNumbersInNoteOut);
|
rpc ClozeNumbersInNote (Note) returns (ClozeNumbersInNoteOut);
|
||||||
rpc AfterNoteUpdates (AfterNoteUpdatesIn) returns (Empty);
|
rpc AfterNoteUpdates (AfterNoteUpdatesIn) returns (Empty);
|
||||||
rpc FieldNamesForNotes (FieldNamesForNotesIn) returns (FieldNamesForNotesOut);
|
rpc FieldNamesForNotes (FieldNamesForNotesIn) returns (FieldNamesForNotesOut);
|
||||||
|
rpc NoteIsDuplicateOrEmpty (Note) returns (NoteIsDuplicateOrEmptyOut);
|
||||||
|
|
||||||
// note types
|
// note types
|
||||||
|
|
||||||
|
@ -865,3 +866,12 @@ message GetDeckNamesIn {
|
||||||
// if unset, implies skip_empty_default
|
// if unset, implies skip_empty_default
|
||||||
bool include_filtered = 2;
|
bool include_filtered = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message NoteIsDuplicateOrEmptyOut {
|
||||||
|
enum State {
|
||||||
|
NORMAL = 0;
|
||||||
|
EMPTY = 1;
|
||||||
|
DUPLICATE = 2;
|
||||||
|
}
|
||||||
|
State state = 1;
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import anki # pylint: disable=unused-import
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
from anki.models import NoteType
|
from anki.models import NoteType
|
||||||
from anki.rsbackend import BackendNote
|
from anki.rsbackend import BackendNote
|
||||||
from anki.utils import fieldChecksum, joinFields, splitFields, stripHTMLMedia
|
from anki.utils import joinFields
|
||||||
|
|
||||||
|
|
||||||
class Note:
|
class Note:
|
||||||
|
@ -140,18 +140,5 @@ class Note:
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
def dupeOrEmpty(self) -> int:
|
def dupeOrEmpty(self) -> int:
|
||||||
"1 if first is empty; 2 if first is a duplicate, False otherwise."
|
"1 if first is empty; 2 if first is a duplicate, 0 otherwise."
|
||||||
val = self.fields[0]
|
return self.col.backend.note_is_duplicate_or_empty(self.to_backend_note())
|
||||||
if not val.strip():
|
|
||||||
return 1
|
|
||||||
csum = fieldChecksum(val)
|
|
||||||
# find any matching csums and compare
|
|
||||||
for flds in self.col.db.list(
|
|
||||||
"select flds from notes where csum = ? and id != ? and mid = ?",
|
|
||||||
csum,
|
|
||||||
self.id or 0,
|
|
||||||
self.mid,
|
|
||||||
):
|
|
||||||
if stripHTMLMedia(splitFields(flds)[0]) == stripHTMLMedia(self.fields[0]):
|
|
||||||
return 2
|
|
||||||
return False
|
|
||||||
|
|
|
@ -543,6 +543,13 @@ class RustBackend:
|
||||||
output.ParseFromString(self._run_command(45, input))
|
output.ParseFromString(self._run_command(45, input))
|
||||||
return output.fields
|
return output.fields
|
||||||
|
|
||||||
|
def note_is_duplicate_or_empty(
|
||||||
|
self, input: pb.Note
|
||||||
|
) -> pb.NoteIsDuplicateOrEmptyOut.State:
|
||||||
|
output = pb.NoteIsDuplicateOrEmptyOut()
|
||||||
|
output.ParseFromString(self._run_command(46, input))
|
||||||
|
return output.state
|
||||||
|
|
||||||
def add_or_update_notetype(
|
def add_or_update_notetype(
|
||||||
self, *, json: bytes, preserve_usn_and_mtime: bool
|
self, *, json: bytes, preserve_usn_and_mtime: bool
|
||||||
) -> int:
|
) -> int:
|
||||||
|
@ -550,43 +557,43 @@ class RustBackend:
|
||||||
json=json, preserve_usn_and_mtime=preserve_usn_and_mtime
|
json=json, preserve_usn_and_mtime=preserve_usn_and_mtime
|
||||||
)
|
)
|
||||||
output = pb.NoteTypeID()
|
output = pb.NoteTypeID()
|
||||||
output.ParseFromString(self._run_command(46, input))
|
output.ParseFromString(self._run_command(47, input))
|
||||||
return output.ntid
|
return output.ntid
|
||||||
|
|
||||||
def get_stock_notetype_legacy(self, kind: pb.StockNoteType) -> bytes:
|
def get_stock_notetype_legacy(self, kind: pb.StockNoteType) -> bytes:
|
||||||
input = pb.GetStockNotetypeIn(kind=kind)
|
input = pb.GetStockNotetypeIn(kind=kind)
|
||||||
output = pb.Json()
|
output = pb.Json()
|
||||||
output.ParseFromString(self._run_command(47, input))
|
output.ParseFromString(self._run_command(48, input))
|
||||||
return output.json
|
return output.json
|
||||||
|
|
||||||
def get_notetype_legacy(self, ntid: int) -> bytes:
|
def get_notetype_legacy(self, ntid: int) -> bytes:
|
||||||
input = pb.NoteTypeID(ntid=ntid)
|
input = pb.NoteTypeID(ntid=ntid)
|
||||||
output = pb.Json()
|
output = pb.Json()
|
||||||
output.ParseFromString(self._run_command(48, input))
|
output.ParseFromString(self._run_command(49, input))
|
||||||
return output.json
|
return output.json
|
||||||
|
|
||||||
def get_notetype_names(self) -> Sequence[pb.NoteTypeNameID]:
|
def get_notetype_names(self) -> Sequence[pb.NoteTypeNameID]:
|
||||||
input = pb.Empty()
|
input = pb.Empty()
|
||||||
output = pb.NoteTypeNames()
|
output = pb.NoteTypeNames()
|
||||||
output.ParseFromString(self._run_command(49, input))
|
output.ParseFromString(self._run_command(50, input))
|
||||||
return output.entries
|
return output.entries
|
||||||
|
|
||||||
def get_notetype_names_and_counts(self) -> Sequence[pb.NoteTypeNameIDUseCount]:
|
def get_notetype_names_and_counts(self) -> Sequence[pb.NoteTypeNameIDUseCount]:
|
||||||
input = pb.Empty()
|
input = pb.Empty()
|
||||||
output = pb.NoteTypeUseCounts()
|
output = pb.NoteTypeUseCounts()
|
||||||
output.ParseFromString(self._run_command(50, input))
|
output.ParseFromString(self._run_command(51, input))
|
||||||
return output.entries
|
return output.entries
|
||||||
|
|
||||||
def get_notetype_id_by_name(self, val: str) -> int:
|
def get_notetype_id_by_name(self, val: str) -> int:
|
||||||
input = pb.String(val=val)
|
input = pb.String(val=val)
|
||||||
output = pb.NoteTypeID()
|
output = pb.NoteTypeID()
|
||||||
output.ParseFromString(self._run_command(51, input))
|
output.ParseFromString(self._run_command(52, input))
|
||||||
return output.ntid
|
return output.ntid
|
||||||
|
|
||||||
def remove_notetype(self, ntid: int) -> pb.Empty:
|
def remove_notetype(self, ntid: int) -> pb.Empty:
|
||||||
input = pb.NoteTypeID(ntid=ntid)
|
input = pb.NoteTypeID(ntid=ntid)
|
||||||
output = pb.Empty()
|
output = pb.Empty()
|
||||||
output.ParseFromString(self._run_command(52, input))
|
output.ParseFromString(self._run_command(53, input))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def open_collection(
|
def open_collection(
|
||||||
|
@ -604,42 +611,42 @@ class RustBackend:
|
||||||
log_path=log_path,
|
log_path=log_path,
|
||||||
)
|
)
|
||||||
output = pb.Empty()
|
output = pb.Empty()
|
||||||
output.ParseFromString(self._run_command(53, input))
|
output.ParseFromString(self._run_command(54, input))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def close_collection(self, downgrade_to_schema11: bool) -> pb.Empty:
|
def close_collection(self, downgrade_to_schema11: bool) -> pb.Empty:
|
||||||
input = pb.CloseCollectionIn(downgrade_to_schema11=downgrade_to_schema11)
|
input = pb.CloseCollectionIn(downgrade_to_schema11=downgrade_to_schema11)
|
||||||
output = pb.Empty()
|
output = pb.Empty()
|
||||||
output.ParseFromString(self._run_command(54, input))
|
output.ParseFromString(self._run_command(55, input))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def check_database(self) -> Sequence[str]:
|
def check_database(self) -> Sequence[str]:
|
||||||
input = pb.Empty()
|
input = pb.Empty()
|
||||||
output = pb.CheckDatabaseOut()
|
output = pb.CheckDatabaseOut()
|
||||||
output.ParseFromString(self._run_command(55, input))
|
output.ParseFromString(self._run_command(56, input))
|
||||||
return output.problems
|
return output.problems
|
||||||
|
|
||||||
def sync_media(self, *, hkey: str, endpoint: str) -> pb.Empty:
|
def sync_media(self, *, hkey: str, endpoint: str) -> pb.Empty:
|
||||||
input = pb.SyncMediaIn(hkey=hkey, endpoint=endpoint)
|
input = pb.SyncMediaIn(hkey=hkey, endpoint=endpoint)
|
||||||
output = pb.Empty()
|
output = pb.Empty()
|
||||||
output.ParseFromString(self._run_command(56, input))
|
output.ParseFromString(self._run_command(57, input))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def abort_media_sync(self) -> pb.Empty:
|
def abort_media_sync(self) -> pb.Empty:
|
||||||
input = pb.Empty()
|
input = pb.Empty()
|
||||||
output = pb.Empty()
|
output = pb.Empty()
|
||||||
output.ParseFromString(self._run_command(57, input))
|
output.ParseFromString(self._run_command(58, input))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def before_upload(self) -> pb.Empty:
|
def before_upload(self) -> pb.Empty:
|
||||||
input = pb.Empty()
|
input = pb.Empty()
|
||||||
output = pb.Empty()
|
output = pb.Empty()
|
||||||
output.ParseFromString(self._run_command(58, input))
|
output.ParseFromString(self._run_command(59, input))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def translate_string(self, input: pb.TranslateStringIn) -> str:
|
def translate_string(self, input: pb.TranslateStringIn) -> str:
|
||||||
output = pb.String()
|
output = pb.String()
|
||||||
output.ParseFromString(self._run_command(59, input))
|
output.ParseFromString(self._run_command(60, input))
|
||||||
return output.val
|
return output.val
|
||||||
|
|
||||||
def format_timespan(
|
def format_timespan(
|
||||||
|
@ -647,7 +654,7 @@ class RustBackend:
|
||||||
) -> str:
|
) -> str:
|
||||||
input = pb.FormatTimespanIn(seconds=seconds, context=context)
|
input = pb.FormatTimespanIn(seconds=seconds, context=context)
|
||||||
output = pb.String()
|
output = pb.String()
|
||||||
output.ParseFromString(self._run_command(60, input))
|
output.ParseFromString(self._run_command(61, input))
|
||||||
return output.val
|
return output.val
|
||||||
|
|
||||||
def register_tags(
|
def register_tags(
|
||||||
|
@ -657,61 +664,61 @@ class RustBackend:
|
||||||
tags=tags, preserve_usn=preserve_usn, usn=usn, clear_first=clear_first
|
tags=tags, preserve_usn=preserve_usn, usn=usn, clear_first=clear_first
|
||||||
)
|
)
|
||||||
output = pb.Bool()
|
output = pb.Bool()
|
||||||
output.ParseFromString(self._run_command(61, input))
|
output.ParseFromString(self._run_command(62, input))
|
||||||
return output.val
|
return output.val
|
||||||
|
|
||||||
def all_tags(self) -> Sequence[pb.TagUsnTuple]:
|
def all_tags(self) -> Sequence[pb.TagUsnTuple]:
|
||||||
input = pb.Empty()
|
input = pb.Empty()
|
||||||
output = pb.AllTagsOut()
|
output = pb.AllTagsOut()
|
||||||
output.ParseFromString(self._run_command(62, input))
|
output.ParseFromString(self._run_command(63, input))
|
||||||
return output.tags
|
return output.tags
|
||||||
|
|
||||||
def get_changed_tags(self, val: int) -> Sequence[str]:
|
def get_changed_tags(self, val: int) -> Sequence[str]:
|
||||||
input = pb.Int32(val=val)
|
input = pb.Int32(val=val)
|
||||||
output = pb.GetChangedTagsOut()
|
output = pb.GetChangedTagsOut()
|
||||||
output.ParseFromString(self._run_command(63, input))
|
output.ParseFromString(self._run_command(64, input))
|
||||||
return output.tags
|
return output.tags
|
||||||
|
|
||||||
def get_config_json(self, val: str) -> bytes:
|
def get_config_json(self, val: str) -> bytes:
|
||||||
input = pb.String(val=val)
|
input = pb.String(val=val)
|
||||||
output = pb.Json()
|
output = pb.Json()
|
||||||
output.ParseFromString(self._run_command(64, input))
|
output.ParseFromString(self._run_command(65, input))
|
||||||
return output.json
|
return output.json
|
||||||
|
|
||||||
def set_config_json(self, *, key: str, value_json: bytes) -> pb.Empty:
|
def set_config_json(self, *, key: str, value_json: bytes) -> pb.Empty:
|
||||||
input = pb.SetConfigJsonIn(key=key, value_json=value_json)
|
input = pb.SetConfigJsonIn(key=key, value_json=value_json)
|
||||||
output = pb.Empty()
|
output = pb.Empty()
|
||||||
output.ParseFromString(self._run_command(65, input))
|
output.ParseFromString(self._run_command(66, input))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def remove_config(self, val: str) -> pb.Empty:
|
def remove_config(self, val: str) -> pb.Empty:
|
||||||
input = pb.String(val=val)
|
input = pb.String(val=val)
|
||||||
output = pb.Empty()
|
output = pb.Empty()
|
||||||
output.ParseFromString(self._run_command(66, input))
|
output.ParseFromString(self._run_command(67, input))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def set_all_config(self, json: bytes) -> pb.Empty:
|
def set_all_config(self, json: bytes) -> pb.Empty:
|
||||||
input = pb.Json(json=json)
|
input = pb.Json(json=json)
|
||||||
output = pb.Empty()
|
output = pb.Empty()
|
||||||
output.ParseFromString(self._run_command(67, input))
|
output.ParseFromString(self._run_command(68, input))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def get_all_config(self) -> bytes:
|
def get_all_config(self) -> bytes:
|
||||||
input = pb.Empty()
|
input = pb.Empty()
|
||||||
output = pb.Json()
|
output = pb.Json()
|
||||||
output.ParseFromString(self._run_command(68, input))
|
output.ParseFromString(self._run_command(69, input))
|
||||||
return output.json
|
return output.json
|
||||||
|
|
||||||
def get_preferences(self) -> pb.CollectionSchedulingSettings:
|
def get_preferences(self) -> pb.CollectionSchedulingSettings:
|
||||||
input = pb.Empty()
|
input = pb.Empty()
|
||||||
output = pb.Preferences()
|
output = pb.Preferences()
|
||||||
output.ParseFromString(self._run_command(69, input))
|
output.ParseFromString(self._run_command(70, input))
|
||||||
return output.sched
|
return output.sched
|
||||||
|
|
||||||
def set_preferences(self, sched: pb.CollectionSchedulingSettings) -> pb.Empty:
|
def set_preferences(self, sched: pb.CollectionSchedulingSettings) -> pb.Empty:
|
||||||
input = pb.Preferences(sched=sched)
|
input = pb.Preferences(sched=sched)
|
||||||
output = pb.Empty()
|
output = pb.Empty()
|
||||||
output.ParseFromString(self._run_command(70, input))
|
output.ParseFromString(self._run_command(71, input))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
# @@AUTOGEN@@
|
# @@AUTOGEN@@
|
||||||
|
|
|
@ -682,6 +682,17 @@ impl BackendService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn note_is_duplicate_or_empty(
|
||||||
|
&mut self,
|
||||||
|
input: pb::Note,
|
||||||
|
) -> BackendResult<pb::NoteIsDuplicateOrEmptyOut> {
|
||||||
|
let note: Note = input.into();
|
||||||
|
self.with_col(|col| {
|
||||||
|
col.note_is_duplicate_or_empty(¬e)
|
||||||
|
.map(|r| pb::NoteIsDuplicateOrEmptyOut { state: r as i32 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// notetypes
|
// notetypes
|
||||||
//-------------------------------------------------------------------
|
//-------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// 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
|
||||||
|
|
||||||
|
use crate::backend_proto::note_is_duplicate_or_empty_out::State as DuplicateState;
|
||||||
use crate::{
|
use crate::{
|
||||||
backend_proto as pb,
|
backend_proto as pb,
|
||||||
collection::Collection,
|
collection::Collection,
|
||||||
|
@ -382,6 +383,28 @@ impl Collection {
|
||||||
|
|
||||||
Ok(changed_notes)
|
Ok(changed_notes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn note_is_duplicate_or_empty(&self, note: &Note) -> Result<DuplicateState> {
|
||||||
|
if let Some(field1) = note.fields.get(0) {
|
||||||
|
let stripped = strip_html_preserving_image_filenames(field1);
|
||||||
|
if stripped.trim().is_empty() {
|
||||||
|
Ok(DuplicateState::Empty)
|
||||||
|
} else {
|
||||||
|
let csum = field_checksum(&stripped);
|
||||||
|
for field in self
|
||||||
|
.storage
|
||||||
|
.note_fields_by_checksum(note.id, note.ntid, csum)?
|
||||||
|
{
|
||||||
|
if strip_html_preserving_image_filenames(&field) == stripped {
|
||||||
|
return Ok(DuplicateState::Duplicate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(DuplicateState::Normal)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(DuplicateState::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
err::Result,
|
err::Result,
|
||||||
notes::{Note, NoteID},
|
notes::{Note, NoteID},
|
||||||
|
notetype::NoteTypeID,
|
||||||
tags::{join_tags, split_tags},
|
tags::{join_tags, split_tags},
|
||||||
timestamp::TimestampMillis,
|
timestamp::TimestampMillis,
|
||||||
};
|
};
|
||||||
|
@ -95,4 +96,18 @@ impl super::SqliteStorage {
|
||||||
.execute(NO_PARAMS)?;
|
.execute(NO_PARAMS)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the first field of other notes with the same checksum.
|
||||||
|
/// The field of the provided note ID is not returned.
|
||||||
|
pub(crate) fn note_fields_by_checksum(
|
||||||
|
&self,
|
||||||
|
nid: NoteID,
|
||||||
|
ntid: NoteTypeID,
|
||||||
|
csum: u32,
|
||||||
|
) -> Result<Vec<String>> {
|
||||||
|
self.db
|
||||||
|
.prepare("select field_at_index(flds, 0) from notes where csum=? and mid=? and id !=?")?
|
||||||
|
.query_and_then(params![csum, ntid, nid], |r| r.get(0).map_err(Into::into))?
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,6 +105,7 @@ fn want_release_gil(method: u32) -> bool {
|
||||||
BackendMethod::GetAllConfig => true,
|
BackendMethod::GetAllConfig => true,
|
||||||
BackendMethod::GetPreferences => true,
|
BackendMethod::GetPreferences => true,
|
||||||
BackendMethod::SetPreferences => true,
|
BackendMethod::SetPreferences => true,
|
||||||
|
BackendMethod::NoteIsDuplicateOrEmpty => true,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
|
Loading…
Reference in a new issue