From 4d428f9fa85c1a8f358330c37f9f5829c4302269 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 26 Mar 2020 09:09:11 +1000 Subject: [PATCH 01/15] high due numbers shouldn't force a full sync --- pylib/anki/collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index fedd21f76..026ccb886 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -956,7 +956,7 @@ and type=0""", ) rowcount = self.db.scalar("select changes()") if rowcount: - problems.append( + syncable_problems.append( "Found %d new cards with a due number >= 1,000,000 - consider repositioning them in the Browse screen." % rowcount ) From 3ca9f6173391bda959693267dc2c21c1ddcc4df2 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 26 Mar 2020 09:53:15 +1000 Subject: [PATCH 02/15] reduce the chances of add-ons preventing collections from loading https://anki.tenderapp.com/discussions/ankidesktop/39953-sync-fail-with-images#comment_48182205 --- qt/aqt/main.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 7e84f59aa..03d89c6f8 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -442,7 +442,7 @@ close the profile or restart Anki.""" def loadCollection(self) -> bool: try: - return self._loadCollection() + self._loadCollection() except Exception as e: showWarning( tr(TR.ERRORS_UNABLE_OPEN_COLLECTION) + "\n" + traceback.format_exc() @@ -460,15 +460,22 @@ close the profile or restart Anki.""" self.showProfileManager() return False - def _loadCollection(self) -> bool: + # make sure we don't get into an inconsistent state if an add-on + # has broken the deck browser or the did_load hook + try: + self.maybeEnableUndo() + gui_hooks.collection_did_load(self.col) + self.moveToState("deckBrowser") + except Exception as e: + # dump error to stderr so it gets picked up by errors.py + traceback.print_exc() + + return True + + def _loadCollection(self): cpath = self.pm.collectionPath() self.col = Collection(cpath, backend=self.backend) - self.setEnabled(True) - self.maybeEnableUndo() - gui_hooks.collection_did_load(self.col) - self.moveToState("deckBrowser") - return True def reopen(self): cpath = self.pm.collectionPath() From bfc0287e51ca2e53dd75d0874d6035d4bd66faca Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 26 Mar 2020 12:59:51 +1000 Subject: [PATCH 03/15] use newtypes for distinguishing between second and millisecond stamps --- rslib/src/lib.rs | 2 +- rslib/src/notes.rs | 12 ++++----- rslib/src/storage/mod.rs | 1 + rslib/src/storage/sqlite.rs | 11 +++++--- rslib/src/storage/timestamp.rs | 40 +++++++++++++++++++++++++++++ rslib/src/{time.rs => timestamp.rs} | 20 ++++++++++++--- rslib/src/types.rs | 1 - 7 files changed, 71 insertions(+), 16 deletions(-) create mode 100644 rslib/src/storage/timestamp.rs rename rslib/src/{time.rs => timestamp.rs} (68%) diff --git a/rslib/src/lib.rs b/rslib/src/lib.rs index da4d91107..87253f998 100644 --- a/rslib/src/lib.rs +++ b/rslib/src/lib.rs @@ -28,5 +28,5 @@ pub mod storage; pub mod template; pub mod template_filters; pub mod text; -pub mod time; +pub mod timestamp; pub mod types; diff --git a/rslib/src/notes.rs b/rslib/src/notes.rs index cad1f614c..7b70659bc 100644 --- a/rslib/src/notes.rs +++ b/rslib/src/notes.rs @@ -5,10 +5,10 @@ /// the media DB check. use crate::err::{AnkiError, DBErrorKind, Result}; use crate::text::strip_html_preserving_image_filenames; -use crate::time::i64_unix_secs; +use crate::timestamp::TimestampSecs; use crate::{ notetypes::NoteType, - types::{ObjID, Timestamp, Usn}, + types::{ObjID, Usn}, }; use rusqlite::{params, Connection, Row, NO_PARAMS}; use std::convert::TryInto; @@ -17,7 +17,7 @@ use std::convert::TryInto; pub(super) struct Note { pub id: ObjID, pub mid: ObjID, - pub mtime_secs: Timestamp, + pub mtime: TimestampSecs, pub usn: Usn, fields: Vec, } @@ -73,7 +73,7 @@ fn row_to_note(row: &Row) -> Result { Ok(Note { id: row.get(0)?, mid: row.get(1)?, - mtime_secs: row.get(2)?, + mtime: row.get(2)?, usn: row.get(3)?, fields: row .get_raw(4) @@ -85,7 +85,7 @@ fn row_to_note(row: &Row) -> Result { } pub(super) fn set_note(db: &Connection, note: &mut Note, note_type: &NoteType) -> Result<()> { - note.mtime_secs = i64_unix_secs(); + note.mtime = TimestampSecs::now(); // hard-coded for now note.usn = -1; let field1_nohtml = strip_html_preserving_image_filenames(¬e.fields()[0]); @@ -106,7 +106,7 @@ pub(super) fn set_note(db: &Connection, note: &mut Note, note_type: &NoteType) - let mut stmt = db.prepare_cached("update notes set mod=?,usn=?,flds=?,sfld=?,csum=? where id=?")?; stmt.execute(params![ - note.mtime_secs, + note.mtime, note.usn, note.fields().join("\x1f"), sort_field, diff --git a/rslib/src/storage/mod.rs b/rslib/src/storage/mod.rs index 2ed04892c..a84013282 100644 --- a/rslib/src/storage/mod.rs +++ b/rslib/src/storage/mod.rs @@ -1,3 +1,4 @@ mod sqlite; +mod timestamp; pub(crate) use sqlite::{SqliteStorage, StorageContext}; diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index b56510717..083b55e00 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -5,7 +5,7 @@ use crate::collection::CollectionOp; use crate::config::Config; use crate::err::Result; use crate::err::{AnkiError, DBErrorKind}; -use crate::time::{i64_unix_millis, i64_unix_secs}; +use crate::timestamp::{TimestampMillis, TimestampSecs}; use crate::{ decks::Deck, notetypes::NoteType, @@ -168,7 +168,10 @@ impl SqliteStorage { if create { db.prepare_cached("begin exclusive")?.execute(NO_PARAMS)?; db.execute_batch(include_str!("schema11.sql"))?; - db.execute("update col set crt=?, ver=?", params![i64_unix_secs(), ver])?; + db.execute( + "update col set crt=?, ver=?", + params![TimestampSecs::now(), ver], + )?; db.prepare_cached("commit")?.execute(NO_PARAMS)?; } else { if ver > SCHEMA_MAX_VERSION { @@ -278,7 +281,7 @@ impl StorageContext<'_> { pub(crate) fn mark_modified(&self) -> Result<()> { self.db .prepare_cached("update col set mod=?")? - .execute(params![i64_unix_millis()])?; + .execute(params![TimestampMillis::now()])?; Ok(()) } @@ -339,7 +342,7 @@ impl StorageContext<'_> { self.timing_today = Some(sched_timing_today( crt, - i64_unix_secs(), + TimestampSecs::now().0, conf.creation_offset, now_offset, conf.rollover, diff --git a/rslib/src/storage/timestamp.rs b/rslib/src/storage/timestamp.rs new file mode 100644 index 000000000..74fc40965 --- /dev/null +++ b/rslib/src/storage/timestamp.rs @@ -0,0 +1,40 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::timestamp::{TimestampMillis, TimestampSecs}; +use rusqlite::{ + types::{FromSql, FromSqlError, ToSqlOutput, Value, ValueRef}, + ToSql, +}; + +impl FromSql for TimestampSecs { + fn column_result(value: ValueRef<'_>) -> std::result::Result { + if let ValueRef::Integer(i) = value { + Ok(TimestampSecs(i)) + } else { + Err(FromSqlError::InvalidType) + } + } +} + +impl ToSql for TimestampSecs { + fn to_sql(&self) -> rusqlite::Result> { + Ok(ToSqlOutput::Owned(Value::Integer(self.0))) + } +} + +impl FromSql for TimestampMillis { + fn column_result(value: ValueRef<'_>) -> std::result::Result { + if let ValueRef::Integer(i) = value { + Ok(TimestampMillis(i)) + } else { + Err(FromSqlError::InvalidType) + } + } +} + +impl ToSql for TimestampMillis { + fn to_sql(&self) -> rusqlite::Result> { + Ok(ToSqlOutput::Owned(Value::Integer(self.0))) + } +} diff --git a/rslib/src/time.rs b/rslib/src/timestamp.rs similarity index 68% rename from rslib/src/time.rs rename to rslib/src/timestamp.rs index e392cbb6e..35089b36f 100644 --- a/rslib/src/time.rs +++ b/rslib/src/timestamp.rs @@ -3,12 +3,24 @@ use std::time; -pub(crate) fn i64_unix_secs() -> i64 { - elapsed().as_secs() as i64 +#[repr(transparent)] +#[derive(Debug, Clone)] +pub struct TimestampSecs(pub i64); + +impl TimestampSecs { + pub fn now() -> Self { + Self(elapsed().as_secs() as i64) + } } -pub(crate) fn i64_unix_millis() -> i64 { - elapsed().as_millis() as i64 +#[repr(transparent)] +#[derive(Debug, Clone)] +pub struct TimestampMillis(pub i64); + +impl TimestampMillis { + pub fn now() -> Self { + Self(elapsed().as_millis() as i64) + } } #[cfg(not(test))] diff --git a/rslib/src/types.rs b/rslib/src/types.rs index 0ae6e2a83..51d66858c 100644 --- a/rslib/src/types.rs +++ b/rslib/src/types.rs @@ -6,4 +6,3 @@ pub type ObjID = i64; pub type Usn = i32; -pub type Timestamp = i64; From d95cb93d7af23d034958a788147a03aad1e53829 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 26 Mar 2020 13:06:02 +1000 Subject: [PATCH 04/15] usn newtype --- rslib/src/notes.rs | 2 +- rslib/src/storage/mod.rs | 1 + rslib/src/storage/sqlite.rs | 2 +- rslib/src/storage/usn.rs | 24 ++++++++++++++++++++++++ rslib/src/timestamp.rs | 4 ++-- rslib/src/types.rs | 5 ++++- 6 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 rslib/src/storage/usn.rs diff --git a/rslib/src/notes.rs b/rslib/src/notes.rs index 7b70659bc..ffe047f4e 100644 --- a/rslib/src/notes.rs +++ b/rslib/src/notes.rs @@ -87,7 +87,7 @@ fn row_to_note(row: &Row) -> Result { pub(super) fn set_note(db: &Connection, note: &mut Note, note_type: &NoteType) -> Result<()> { note.mtime = TimestampSecs::now(); // hard-coded for now - note.usn = -1; + note.usn = Usn(-1); let field1_nohtml = strip_html_preserving_image_filenames(¬e.fields()[0]); let csum = field_checksum(field1_nohtml.as_ref()); let sort_field = if note_type.sort_field_idx == 0 { diff --git a/rslib/src/storage/mod.rs b/rslib/src/storage/mod.rs index a84013282..1fe26fcb7 100644 --- a/rslib/src/storage/mod.rs +++ b/rslib/src/storage/mod.rs @@ -1,4 +1,5 @@ mod sqlite; mod timestamp; +mod usn; pub(crate) use sqlite::{SqliteStorage, StorageContext}; diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index 083b55e00..6b1a47057 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -297,7 +297,7 @@ impl StorageContext<'_> { } Ok(*self.usn.as_ref().unwrap()) } else { - Ok(-1) + Ok(Usn(-1)) } } diff --git a/rslib/src/storage/usn.rs b/rslib/src/storage/usn.rs new file mode 100644 index 000000000..d911604af --- /dev/null +++ b/rslib/src/storage/usn.rs @@ -0,0 +1,24 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::types::Usn; +use rusqlite::{ + types::{FromSql, FromSqlError, ToSqlOutput, Value, ValueRef}, + ToSql, +}; + +impl FromSql for Usn { + fn column_result(value: ValueRef<'_>) -> std::result::Result { + if let ValueRef::Integer(i) = value { + Ok(Self(i as i32)) + } else { + Err(FromSqlError::InvalidType) + } + } +} + +impl ToSql for Usn { + fn to_sql(&self) -> rusqlite::Result> { + Ok(ToSqlOutput::Owned(Value::Integer(self.0 as i64))) + } +} diff --git a/rslib/src/timestamp.rs b/rslib/src/timestamp.rs index 35089b36f..79d86685a 100644 --- a/rslib/src/timestamp.rs +++ b/rslib/src/timestamp.rs @@ -4,7 +4,7 @@ use std::time; #[repr(transparent)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct TimestampSecs(pub i64); impl TimestampSecs { @@ -14,7 +14,7 @@ impl TimestampSecs { } #[repr(transparent)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct TimestampMillis(pub i64); impl TimestampMillis { diff --git a/rslib/src/types.rs b/rslib/src/types.rs index 51d66858c..8912fd3cf 100644 --- a/rslib/src/types.rs +++ b/rslib/src/types.rs @@ -5,4 +5,7 @@ // signed integers, so these numbers are signed as well. pub type ObjID = i64; -pub type Usn = i32; + +#[repr(transparent)] +#[derive(Debug, Clone, Copy)] +pub struct Usn(pub i32); From bb0b817d9c63a6017ec20c3110b8ab454b29328e Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 26 Mar 2020 13:50:20 +1000 Subject: [PATCH 05/15] use a macro for newtype defs --- rslib/src/storage/mod.rs | 2 -- rslib/src/storage/timestamp.rs | 40 ---------------------------------- rslib/src/storage/usn.rs | 24 -------------------- rslib/src/timestamp.rs | 10 +++------ rslib/src/types.rs | 36 +++++++++++++++++++++++++----- 5 files changed, 33 insertions(+), 79 deletions(-) delete mode 100644 rslib/src/storage/timestamp.rs delete mode 100644 rslib/src/storage/usn.rs diff --git a/rslib/src/storage/mod.rs b/rslib/src/storage/mod.rs index 1fe26fcb7..2ed04892c 100644 --- a/rslib/src/storage/mod.rs +++ b/rslib/src/storage/mod.rs @@ -1,5 +1,3 @@ mod sqlite; -mod timestamp; -mod usn; pub(crate) use sqlite::{SqliteStorage, StorageContext}; diff --git a/rslib/src/storage/timestamp.rs b/rslib/src/storage/timestamp.rs deleted file mode 100644 index 74fc40965..000000000 --- a/rslib/src/storage/timestamp.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright: Ankitects Pty Ltd and contributors -// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -use crate::timestamp::{TimestampMillis, TimestampSecs}; -use rusqlite::{ - types::{FromSql, FromSqlError, ToSqlOutput, Value, ValueRef}, - ToSql, -}; - -impl FromSql for TimestampSecs { - fn column_result(value: ValueRef<'_>) -> std::result::Result { - if let ValueRef::Integer(i) = value { - Ok(TimestampSecs(i)) - } else { - Err(FromSqlError::InvalidType) - } - } -} - -impl ToSql for TimestampSecs { - fn to_sql(&self) -> rusqlite::Result> { - Ok(ToSqlOutput::Owned(Value::Integer(self.0))) - } -} - -impl FromSql for TimestampMillis { - fn column_result(value: ValueRef<'_>) -> std::result::Result { - if let ValueRef::Integer(i) = value { - Ok(TimestampMillis(i)) - } else { - Err(FromSqlError::InvalidType) - } - } -} - -impl ToSql for TimestampMillis { - fn to_sql(&self) -> rusqlite::Result> { - Ok(ToSqlOutput::Owned(Value::Integer(self.0))) - } -} diff --git a/rslib/src/storage/usn.rs b/rslib/src/storage/usn.rs deleted file mode 100644 index d911604af..000000000 --- a/rslib/src/storage/usn.rs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright: Ankitects Pty Ltd and contributors -// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -use crate::types::Usn; -use rusqlite::{ - types::{FromSql, FromSqlError, ToSqlOutput, Value, ValueRef}, - ToSql, -}; - -impl FromSql for Usn { - fn column_result(value: ValueRef<'_>) -> std::result::Result { - if let ValueRef::Integer(i) = value { - Ok(Self(i as i32)) - } else { - Err(FromSqlError::InvalidType) - } - } -} - -impl ToSql for Usn { - fn to_sql(&self) -> rusqlite::Result> { - Ok(ToSqlOutput::Owned(Value::Integer(self.0 as i64))) - } -} diff --git a/rslib/src/timestamp.rs b/rslib/src/timestamp.rs index 79d86685a..b1a419579 100644 --- a/rslib/src/timestamp.rs +++ b/rslib/src/timestamp.rs @@ -1,11 +1,11 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use crate::define_newtype; use std::time; -#[repr(transparent)] -#[derive(Debug, Clone, Copy)] -pub struct TimestampSecs(pub i64); +define_newtype!(TimestampSecs, i64); +define_newtype!(TimestampMillis, i64); impl TimestampSecs { pub fn now() -> Self { @@ -13,10 +13,6 @@ impl TimestampSecs { } } -#[repr(transparent)] -#[derive(Debug, Clone, Copy)] -pub struct TimestampMillis(pub i64); - impl TimestampMillis { pub fn now() -> Self { Self(elapsed().as_millis() as i64) diff --git a/rslib/src/types.rs b/rslib/src/types.rs index 8912fd3cf..1d3fef25a 100644 --- a/rslib/src/types.rs +++ b/rslib/src/types.rs @@ -1,11 +1,35 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -// while Anki tends to only use positive numbers, sqlite only supports -// signed integers, so these numbers are signed as well. - pub type ObjID = i64; -#[repr(transparent)] -#[derive(Debug, Clone, Copy)] -pub struct Usn(pub i32); +#[macro_export] +macro_rules! define_newtype { + ( $name:ident, $type:ident ) => { + #[repr(transparent)] + #[derive(Debug, Clone, Copy)] + pub struct $name(pub $type); + + impl rusqlite::types::FromSql for $name { + fn column_result( + value: rusqlite::types::ValueRef<'_>, + ) -> std::result::Result { + if let rusqlite::types::ValueRef::Integer(i) = value { + Ok(Self(i as $type)) + } else { + Err(rusqlite::types::FromSqlError::InvalidType) + } + } + } + + impl rusqlite::ToSql for $name { + fn to_sql(&self) -> ::rusqlite::Result> { + Ok(rusqlite::types::ToSqlOutput::Owned( + rusqlite::types::Value::Integer(self.0 as i64), + )) + } + } + }; +} + +define_newtype!(Usn, i32); From 3395a565a6d8aba3a4f877b4de6eb590f2f92b31 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 26 Mar 2020 14:42:43 +1000 Subject: [PATCH 06/15] newtype NoteID --- rslib/src/notes.rs | 5 ++++- rslib/src/notetypes.rs | 6 ++++-- rslib/src/types.rs | 16 +++++++++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/rslib/src/notes.rs b/rslib/src/notes.rs index ffe047f4e..68279b3a1 100644 --- a/rslib/src/notes.rs +++ b/rslib/src/notes.rs @@ -7,15 +7,18 @@ use crate::err::{AnkiError, DBErrorKind, Result}; use crate::text::strip_html_preserving_image_filenames; use crate::timestamp::TimestampSecs; use crate::{ + define_newtype, notetypes::NoteType, types::{ObjID, Usn}, }; use rusqlite::{params, Connection, Row, NO_PARAMS}; use std::convert::TryInto; +define_newtype!(NoteID, i64); + #[derive(Debug)] pub(super) struct Note { - pub id: ObjID, + pub id: NoteID, pub mid: ObjID, pub mtime: TimestampSecs, pub usn: Usn, diff --git a/rslib/src/notetypes.rs b/rslib/src/notetypes.rs index 4b9295939..9324c8be3 100644 --- a/rslib/src/notetypes.rs +++ b/rslib/src/notetypes.rs @@ -1,14 +1,16 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use crate::types::ObjID; +use crate::define_newtype; use serde_aux::field_attributes::deserialize_number_from_string; use serde_derive::Deserialize; +define_newtype!(NoteTypeID, i64); + #[derive(Deserialize, Debug)] pub(crate) struct NoteType { #[serde(deserialize_with = "deserialize_number_from_string")] - pub id: ObjID, + pub id: NoteTypeID, pub name: String, #[serde(rename = "sortf")] pub sort_field_idx: u16, diff --git a/rslib/src/types.rs b/rslib/src/types.rs index 1d3fef25a..760d5a3e8 100644 --- a/rslib/src/types.rs +++ b/rslib/src/types.rs @@ -7,9 +7,23 @@ pub type ObjID = i64; macro_rules! define_newtype { ( $name:ident, $type:ident ) => { #[repr(transparent)] - #[derive(Debug, Clone, Copy)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, + serde::Serialize, serde::Deserialize)] pub struct $name(pub $type); + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + + impl std::str::FromStr for $name { + type Err = std::num::ParseIntError; + fn from_str(s: &std::primitive::str) -> std::result::Result { + $type::from_str(s).map(|n| $name(n)) + } + } + impl rusqlite::types::FromSql for $name { fn column_result( value: rusqlite::types::ValueRef<'_>, From 41504807098b3ee85a680dba266713523d7fa6c4 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 26 Mar 2020 15:00:24 +1000 Subject: [PATCH 07/15] NoteTypeID --- rslib/src/backend/mod.rs | 4 +++- rslib/src/media/check.rs | 2 +- rslib/src/notes.rs | 13 +++++-------- rslib/src/search/notes.rs | 6 +++--- rslib/src/search/parser.rs | 8 ++++---- rslib/src/search/sqlwriter.rs | 7 +++---- rslib/src/storage/sqlite.rs | 8 +++++--- rslib/src/types.rs | 16 +++++++++++++--- 8 files changed, 37 insertions(+), 27 deletions(-) diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 0240d8e9d..314b84383 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -608,7 +608,9 @@ impl Backend { self.with_col(|col| { col.with_ctx(|ctx| { let nids = search_notes(ctx, &input.search)?; - Ok(pb::SearchNotesOut { note_ids: nids }) + Ok(pb::SearchNotesOut { + note_ids: nids.into_iter().map(|v| v.0).collect(), + }) }) }) } diff --git a/rslib/src/media/check.rs b/rslib/src/media/check.rs index 971747b50..dd1c4bde3 100644 --- a/rslib/src/media/check.rs +++ b/rslib/src/media/check.rs @@ -390,7 +390,7 @@ where self.maybe_fire_progress_cb()?; } let nt = note_types - .get(¬e.mid) + .get(¬e.ntid) .ok_or_else(|| AnkiError::DBError { info: "missing note type".to_string(), kind: DBErrorKind::MissingEntity, diff --git a/rslib/src/notes.rs b/rslib/src/notes.rs index 68279b3a1..74f69200f 100644 --- a/rslib/src/notes.rs +++ b/rslib/src/notes.rs @@ -4,13 +4,10 @@ /// At the moment, this is just basic note reading/updating functionality for /// the media DB check. use crate::err::{AnkiError, DBErrorKind, Result}; +use crate::notetypes::NoteTypeID; use crate::text::strip_html_preserving_image_filenames; use crate::timestamp::TimestampSecs; -use crate::{ - define_newtype, - notetypes::NoteType, - types::{ObjID, Usn}, -}; +use crate::{define_newtype, notetypes::NoteType, types::Usn}; use rusqlite::{params, Connection, Row, NO_PARAMS}; use std::convert::TryInto; @@ -19,7 +16,7 @@ define_newtype!(NoteID, i64); #[derive(Debug)] pub(super) struct Note { pub id: NoteID, - pub mid: ObjID, + pub ntid: NoteTypeID, pub mtime: TimestampSecs, pub usn: Usn, fields: Vec, @@ -51,7 +48,7 @@ pub(crate) fn field_checksum(text: &str) -> u32 { } #[allow(dead_code)] -fn get_note(db: &Connection, nid: ObjID) -> Result> { +fn get_note(db: &Connection, nid: NoteID) -> Result> { let mut stmt = db.prepare_cached("select id, mid, mod, usn, flds from notes where id=?")?; let note = stmt.query_and_then(params![nid], row_to_note)?.next(); @@ -75,7 +72,7 @@ pub(super) fn for_every_note Result<()>>( fn row_to_note(row: &Row) -> Result { Ok(Note { id: row.get(0)?, - mid: row.get(1)?, + ntid: row.get(1)?, mtime: row.get(2)?, usn: row.get(3)?, fields: row diff --git a/rslib/src/search/notes.rs b/rslib/src/search/notes.rs index 50021a92e..5af735809 100644 --- a/rslib/src/search/notes.rs +++ b/rslib/src/search/notes.rs @@ -4,13 +4,13 @@ use super::{parser::Node, sqlwriter::node_to_sql}; use crate::collection::RequestContext; use crate::err::Result; +use crate::notes::NoteID; use crate::search::parser::parse; -use crate::types::ObjID; pub(crate) fn search_notes<'a, 'b>( req: &'a mut RequestContext<'b>, search: &'a str, -) -> Result> { +) -> Result> { let top_node = Node::Group(parse(search)?); let (sql, args) = node_to_sql(req, &top_node)?; @@ -20,7 +20,7 @@ pub(crate) fn search_notes<'a, 'b>( ); let mut stmt = req.storage.db.prepare(&sql)?; - let ids: Vec = stmt + let ids: Vec<_> = stmt .query_map(&args, |row| row.get(0))? .collect::>()?; diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index 4018cc56c..a42370152 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -2,7 +2,7 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use crate::err::{AnkiError, Result}; -use crate::types::ObjID; +use crate::notetypes::NoteTypeID; use nom::branch::alt; use nom::bytes::complete::{escaped, is_not, tag, take_while1}; use nom::character::complete::{anychar, char, one_of}; @@ -58,7 +58,7 @@ pub(super) enum SearchNode<'a> { AddedInDays(u32), CardTemplate(TemplateKind), Deck(Cow<'a, str>), - NoteTypeID(ObjID), + NoteTypeID(NoteTypeID), NoteType(Cow<'a, str>), Rated { days: u32, @@ -66,7 +66,7 @@ pub(super) enum SearchNode<'a> { }, Tag(Cow<'a, str>), Duplicates { - note_type_id: ObjID, + note_type_id: NoteTypeID, text: String, }, State(StateKind), @@ -339,7 +339,7 @@ fn parse_rated(val: &str) -> ParseResult> { /// eg dupes:1231,hello fn parse_dupes(val: &str) -> ParseResult> { let mut it = val.splitn(2, ','); - let mid: ObjID = it.next().unwrap().parse()?; + let mid: NoteTypeID = it.next().unwrap().parse()?; let text = it.next().ok_or(ParseError {})?; Ok(SearchNode::Duplicates { note_type_id: mid, diff --git a/rslib/src/search/sqlwriter.rs b/rslib/src/search/sqlwriter.rs index 1f5e7ef0c..f1348ba58 100644 --- a/rslib/src/search/sqlwriter.rs +++ b/rslib/src/search/sqlwriter.rs @@ -7,11 +7,10 @@ use crate::decks::child_ids; use crate::decks::get_deck; use crate::err::{AnkiError, Result}; use crate::notes::field_checksum; +use crate::notetypes::NoteTypeID; use crate::text::matches_wildcard; use crate::text::without_combining; -use crate::{ - collection::RequestContext, text::strip_html_preserving_image_filenames, types::ObjID, -}; +use crate::{collection::RequestContext, text::strip_html_preserving_image_filenames}; use std::fmt::Write; struct SqlWriter<'a, 'b> { @@ -342,7 +341,7 @@ impl SqlWriter<'_, '_> { Ok(()) } - fn write_dupes(&mut self, ntid: ObjID, text: &str) { + fn write_dupes(&mut self, ntid: NoteTypeID, text: &str) { let text_nohtml = strip_html_preserving_image_filenames(text); let csum = field_checksum(text_nohtml.as_ref()); write!( diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index 6b1a47057..036e42b5d 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -5,6 +5,7 @@ use crate::collection::CollectionOp; use crate::config::Config; use crate::err::Result; use crate::err::{AnkiError, DBErrorKind}; +use crate::notetypes::NoteTypeID; use crate::timestamp::{TimestampMillis, TimestampSecs}; use crate::{ decks::Deck, @@ -315,11 +316,12 @@ impl StorageContext<'_> { }) } - pub(crate) fn all_note_types(&self) -> Result> { + pub(crate) fn all_note_types(&self) -> Result> { let mut stmt = self.db.prepare("select models from col")?; let note_types = stmt - .query_and_then(NO_PARAMS, |row| -> Result> { - let v: HashMap = serde_json::from_str(row.get_raw(0).as_str()?)?; + .query_and_then(NO_PARAMS, |row| -> Result> { + let v: HashMap = + serde_json::from_str(row.get_raw(0).as_str()?)?; Ok(v) })? .next() diff --git a/rslib/src/types.rs b/rslib/src/types.rs index 760d5a3e8..de4425e35 100644 --- a/rslib/src/types.rs +++ b/rslib/src/types.rs @@ -7,8 +7,18 @@ pub type ObjID = i64; macro_rules! define_newtype { ( $name:ident, $type:ident ) => { #[repr(transparent)] - #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, - serde::Serialize, serde::Deserialize)] + #[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + serde::Serialize, + serde::Deserialize, + )] pub struct $name(pub $type); impl std::fmt::Display for $name { @@ -20,7 +30,7 @@ macro_rules! define_newtype { impl std::str::FromStr for $name { type Err = std::num::ParseIntError; fn from_str(s: &std::primitive::str) -> std::result::Result { - $type::from_str(s).map(|n| $name(n)) + $type::from_str(s).map($name) } } From 1af3de6f70596d3fc55799691b6ac60b2763404c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 26 Mar 2020 15:10:40 +1000 Subject: [PATCH 08/15] DeckID, CardID --- rslib/src/backend/mod.rs | 4 +++- rslib/src/card.rs | 2 ++ rslib/src/config.rs | 4 ++-- rslib/src/decks.rs | 11 +++++++---- rslib/src/search/cards.rs | 6 +++--- rslib/src/storage/sqlite.rs | 5 +++-- rslib/src/types.rs | 2 -- 7 files changed, 20 insertions(+), 14 deletions(-) diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 314b84383..470fc579d 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -599,7 +599,9 @@ impl Backend { SortMode::FromConfig }; let cids = search_cards(ctx, &input.search, order)?; - Ok(pb::SearchCardsOut { card_ids: cids }) + Ok(pb::SearchCardsOut { + card_ids: cids.into_iter().map(|v| v.0).collect(), + }) }) }) } diff --git a/rslib/src/card.rs b/rslib/src/card.rs index 1ee1386b0..c79d0958c 100644 --- a/rslib/src/card.rs +++ b/rslib/src/card.rs @@ -1,9 +1,11 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use crate::define_newtype; use num_enum::TryFromPrimitive; use serde_repr::{Deserialize_repr, Serialize_repr}; +define_newtype!(CardID, i64); #[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, TryFromPrimitive, Clone, Copy)] #[repr(u8)] pub enum CardType { diff --git a/rslib/src/config.rs b/rslib/src/config.rs index c768b1122..46c952828 100644 --- a/rslib/src/config.rs +++ b/rslib/src/config.rs @@ -1,7 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use crate::types::ObjID; +use crate::decks::DeckID; use serde::Deserialize as DeTrait; use serde_aux::field_attributes::deserialize_number_from_string; use serde_derive::Deserialize; @@ -22,7 +22,7 @@ pub struct Config { rename = "curDeck", deserialize_with = "deserialize_number_from_string" )] - pub(crate) current_deck_id: ObjID, + pub(crate) current_deck_id: DeckID, pub(crate) rollover: Option, pub(crate) creation_offset: Option, pub(crate) local_offset: Option, diff --git a/rslib/src/decks.rs b/rslib/src/decks.rs index 16b80b261..2511d5af1 100644 --- a/rslib/src/decks.rs +++ b/rslib/src/decks.rs @@ -1,18 +1,21 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use crate::types::ObjID; +use crate::define_newtype; use serde_aux::field_attributes::deserialize_number_from_string; use serde_derive::Deserialize; +define_newtype!(DeckID, i64); +define_newtype!(DeckConfID, i64); + #[derive(Deserialize)] pub struct Deck { #[serde(deserialize_with = "deserialize_number_from_string")] - pub(crate) id: ObjID, + pub(crate) id: DeckID, pub(crate) name: String, } -pub(crate) fn child_ids<'a>(decks: &'a [Deck], name: &str) -> impl Iterator + 'a { +pub(crate) fn child_ids<'a>(decks: &'a [Deck], name: &str) -> impl Iterator + 'a { let prefix = format!("{}::", name.to_ascii_lowercase()); decks .iter() @@ -20,7 +23,7 @@ pub(crate) fn child_ids<'a>(decks: &'a [Deck], name: &str) -> impl Iterator Option<&Deck> { +pub(crate) fn get_deck(decks: &[Deck], id: DeckID) -> Option<&Deck> { for d in decks { if d.id == id { return Some(d); diff --git a/rslib/src/search/cards.rs b/rslib/src/search/cards.rs index 8ab045b7b..db9c8effa 100644 --- a/rslib/src/search/cards.rs +++ b/rslib/src/search/cards.rs @@ -2,12 +2,12 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use super::{parser::Node, sqlwriter::node_to_sql}; +use crate::card::CardID; use crate::card::CardType; use crate::collection::RequestContext; use crate::config::SortKind; use crate::err::Result; use crate::search::parser::parse; -use crate::types::ObjID; use rusqlite::params; pub(crate) enum SortMode { @@ -21,7 +21,7 @@ pub(crate) fn search_cards<'a, 'b>( req: &'a mut RequestContext<'b>, search: &'a str, order: SortMode, -) -> Result> { +) -> Result> { let top_node = Node::Group(parse(search)?); let (sql, args) = node_to_sql(req, &top_node)?; @@ -50,7 +50,7 @@ pub(crate) fn search_cards<'a, 'b>( } let mut stmt = req.storage.db.prepare(&sql)?; - let ids: Vec = stmt + let ids: Vec<_> = stmt .query_map(&args, |row| row.get(0))? .collect::>()?; diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index 036e42b5d..32a9d0524 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -3,6 +3,7 @@ use crate::collection::CollectionOp; use crate::config::Config; +use crate::decks::DeckID; use crate::err::Result; use crate::err::{AnkiError, DBErrorKind}; use crate::notetypes::NoteTypeID; @@ -12,7 +13,7 @@ use crate::{ notetypes::NoteType, sched::cutoff::{sched_timing_today, SchedTimingToday}, text::without_combining, - types::{ObjID, Usn}, + types::Usn, }; use regex::Regex; use rusqlite::{functions::FunctionFlags, params, Connection, NO_PARAMS}; @@ -302,7 +303,7 @@ impl StorageContext<'_> { } } - pub(crate) fn all_decks(&self) -> Result> { + pub(crate) fn all_decks(&self) -> Result> { self.db .query_row_and_then("select decks from col", NO_PARAMS, |row| -> Result<_> { Ok(serde_json::from_str(row.get_raw(0).as_str()?)?) diff --git a/rslib/src/types.rs b/rslib/src/types.rs index de4425e35..6d672d61e 100644 --- a/rslib/src/types.rs +++ b/rslib/src/types.rs @@ -1,8 +1,6 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -pub type ObjID = i64; - #[macro_export] macro_rules! define_newtype { ( $name:ident, $type:ident ) => { From b2fba4e1ffe82ff9fadd061ca80bf17f45287c64 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 26 Mar 2020 17:47:43 +1000 Subject: [PATCH 09/15] use backend to get card --- proto/backend.proto | 27 ++++++++++++++ pylib/anki/cards.py | 41 +++++++++++---------- pylib/anki/rsbackend.py | 4 +++ pylib/tests/test_models.py | 2 +- rslib/src/backend/mod.rs | 32 +++++++++++++++++ rslib/src/card.rs | 51 ++++++++++++++++++++++++++ rslib/src/storage/card.rs | 72 +++++++++++++++++++++++++++++++++++++ rslib/src/storage/mod.rs | 1 + rslib/src/storage/sqlite.rs | 14 ++++++++ 9 files changed, 222 insertions(+), 22 deletions(-) create mode 100644 rslib/src/storage/card.rs diff --git a/proto/backend.proto b/proto/backend.proto index 66bcebf25..f285feb83 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -47,6 +47,7 @@ message BackendInput { Empty restore_trash = 35; OpenCollectionIn open_collection = 36; Empty close_collection = 37; + int64 get_card = 38; } } @@ -77,6 +78,7 @@ message BackendOutput { Empty restore_trash = 35; Empty open_collection = 36; Empty close_collection = 37; + GetCardOut get_card = 38; BackendError error = 2047; } @@ -367,3 +369,28 @@ enum BuiltinSortKind { CARD_DECK = 11; CARD_TEMPLATE = 12; } + +message GetCardOut { + Card card = 1; +} + +message Card { + int64 id = 1; + int64 nid = 2; + int64 did = 3; + uint32 ord = 4; + int64 mtime = 5; + sint32 usn = 6; + uint32 ctype = 7; + sint32 queue = 8; + int64 due = 9; + int64 ivl = 10; + uint32 factor = 11; + int64 reps = 12; + int64 lapses = 13; + int64 left = 14; + int64 odue = 15; + int64 odid = 16; + int64 flags = 17; + string data = 18; +} diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index f8ac57314..e856fc0ba 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -34,7 +34,7 @@ class Card: ord: int def __init__( - self, col: anki.collection._Collection, id: Optional[int] = None + self, col: anki.storage._Collection, id: Optional[int] = None ) -> None: self.col = col.weakref() self.timerStarted = None @@ -61,28 +61,27 @@ class Card: self.data = "" def load(self) -> None: - ( - self.id, - self.nid, - self.did, - self.ord, - self.mod, - self.usn, - self.type, - self.queue, - self.due, - self.ivl, - self.factor, - self.reps, - self.lapses, - self.left, - self.odue, - self.odid, - self.flags, - self.data, - ) = self.col.db.first("select * from cards where id = ?", self.id) self._render_output = None self._note = None + c = self.col.backend.get_card(self.id) + assert c + self.nid = c.nid + self.did = c.did + self.ord = c.ord + self.mod = c.mtime + self.usn = c.usn + self.type = c.ctype + self.queue = c.queue + self.due = c.due + self.ivl = c.ivl + self.factor = c.factor + self.reps = c.reps + self.lapses = c.lapses + self.left = c.left + self.odue = c.odue + self.odid = c.odid + self.flags = c.flags + self.data = c.data def _preFlush(self) -> None: hooks.card_will_flush(self) diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 630d74368..d6bf3fab0 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -480,6 +480,10 @@ class RustBackend: pb.BackendInput(search_notes=pb.SearchNotesIn(search=search)) ).search_notes.note_ids + def get_card(self, cid: int) -> Optional[pb.Card]: + return self._run_command( + pb.BackendInput(get_card=cid) + ).get_card.card def translate_string_in( key: TR, **kwargs: Union[str, int, float] diff --git a/pylib/tests/test_models.py b/pylib/tests/test_models.py index 8f6ceecee..ebcc3d0fa 100644 --- a/pylib/tests/test_models.py +++ b/pylib/tests/test_models.py @@ -318,7 +318,7 @@ def test_modelChange(): try: c1.load() assert 0 - except TypeError: + except AssertionError: pass # but we have two cards, as a new one was generated assert len(f.cards()) == 2 diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 470fc579d..f3c862248 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -4,6 +4,7 @@ use crate::backend::dbproxy::db_command_bytes; use crate::backend_proto::backend_input::Value; use crate::backend_proto::{BuiltinSortKind, Empty, RenderedTemplateReplacement, SyncMediaIn}; +use crate::card::{Card, CardID}; use crate::collection::{open_collection, Collection}; use crate::config::SortKind; use crate::err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind}; @@ -248,6 +249,7 @@ impl Backend { } 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)?), }) } @@ -616,6 +618,13 @@ impl Backend { }) }) } + + fn get_card(&self, cid: i64) -> Result { + let card = self.with_col(|col| col.with_ctx(|ctx| ctx.storage.get_card(CardID(cid))))?; + Ok(pb::GetCardOut { + card: card.map(card_to_pb), + }) + } } fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue { @@ -708,3 +717,26 @@ fn sort_kind_from_pb(kind: i32) -> SortKind { _ => SortKind::NoteCreation, } } + +fn card_to_pb(c: Card) -> pb::Card { + pb::Card { + id: c.id.0, + nid: c.nid.0, + did: c.did.0, + ord: c.ord as u32, + mtime: c.mtime.0, + usn: c.usn.0, + ctype: c.ctype as u32, + queue: c.queue as i32, + due: c.due, + ivl: c.ivl, + factor: c.factor as u32, + reps: c.reps, + lapses: c.lapses, + left: c.left, + odue: c.odue, + odid: c.odid.0, + flags: c.flags, + data: c.data, + } +} diff --git a/rslib/src/card.rs b/rslib/src/card.rs index c79d0958c..d1267fc87 100644 --- a/rslib/src/card.rs +++ b/rslib/src/card.rs @@ -1,11 +1,15 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use crate::decks::DeckID; use crate::define_newtype; +use crate::notes::NoteID; +use crate::{timestamp::TimestampSecs, types::Usn}; use num_enum::TryFromPrimitive; use serde_repr::{Deserialize_repr, Serialize_repr}; define_newtype!(CardID, i64); + #[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, TryFromPrimitive, Clone, Copy)] #[repr(u8)] pub enum CardType { @@ -33,3 +37,50 @@ pub enum CardQueue { UserBuried = -2, SchedBuried = -3, } + +#[derive(Debug, Clone)] +pub struct Card { + pub(crate) id: CardID, + pub(crate) nid: NoteID, + pub(crate) did: DeckID, + pub(crate) ord: u16, + pub(crate) mtime: TimestampSecs, + pub(crate) usn: Usn, + pub(crate) ctype: CardType, + pub(crate) queue: CardQueue, + pub(crate) due: i64, + pub(crate) ivl: i64, + pub(crate) factor: u16, + pub(crate) reps: i64, + pub(crate) lapses: i64, + pub(crate) left: i64, + pub(crate) odue: i64, + pub(crate) odid: DeckID, + pub(crate) flags: i64, + pub(crate) data: String, +} + +impl Default for Card { + fn default() -> Self { + Self { + id: CardID(0), + nid: NoteID(0), + did: DeckID(0), + ord: 0, + mtime: TimestampSecs(0), + usn: Usn(0), + ctype: CardType::New, + queue: CardQueue::New, + due: 0, + ivl: 0, + factor: 0, + reps: 0, + lapses: 0, + left: 0, + odue: 0, + odid: DeckID(0), + flags: 0, + data: "".to_string(), + } + } +} diff --git a/rslib/src/storage/card.rs b/rslib/src/storage/card.rs new file mode 100644 index 000000000..28232b75e --- /dev/null +++ b/rslib/src/storage/card.rs @@ -0,0 +1,72 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::cached_sql; +use crate::card::{Card, CardID, CardQueue, CardType}; +use crate::err::Result; +use rusqlite::params; +use rusqlite::{ + types::{FromSql, FromSqlError, ValueRef}, + OptionalExtension, +}; +use std::convert::TryFrom; + +impl FromSql for CardType { + fn column_result(value: ValueRef<'_>) -> std::result::Result { + if let ValueRef::Integer(i) = value { + Ok(Self::try_from(i as u8).map_err(|_| FromSqlError::InvalidType)?) + } else { + Err(FromSqlError::InvalidType) + } + } +} + +impl FromSql for CardQueue { + fn column_result(value: ValueRef<'_>) -> std::result::Result { + if let ValueRef::Integer(i) = value { + Ok(Self::try_from(i as i8).map_err(|_| FromSqlError::InvalidType)?) + } else { + Err(FromSqlError::InvalidType) + } + } +} + +impl super::StorageContext<'_> { + pub fn get_card(&mut self, cid: CardID) -> Result> { + // the casts are required as Anki didn't prevent add-ons from + // storing strings or floats in columns before + let stmt = cached_sql!( + self.get_card_stmt, + self.db, + " +select nid, did, ord, cast(mod as integer), usn, type, queue, due, +cast(ivl as integer), factor, reps, lapses, left, odue, odid, +flags, data from cards where id=?" + ); + + stmt.query_row(params![cid], |row| { + Ok(Card { + id: cid, + nid: row.get(0)?, + did: row.get(1)?, + ord: row.get(2)?, + mtime: row.get(3)?, + usn: row.get(4)?, + ctype: row.get(5)?, + queue: row.get(6)?, + due: row.get(7)?, + ivl: row.get(8)?, + factor: row.get(9)?, + reps: row.get(10)?, + lapses: row.get(11)?, + left: row.get(12)?, + odue: row.get(13)?, + odid: row.get(14)?, + flags: row.get(15)?, + data: row.get(16)?, + }) + }) + .optional() + .map_err(Into::into) + } +} diff --git a/rslib/src/storage/mod.rs b/rslib/src/storage/mod.rs index 2ed04892c..7a21f0336 100644 --- a/rslib/src/storage/mod.rs +++ b/rslib/src/storage/mod.rs @@ -1,3 +1,4 @@ +mod card; mod sqlite; pub(crate) use sqlite::{SqliteStorage, StorageContext}; diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index 32a9d0524..28d3de18b 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -42,6 +42,16 @@ pub struct SqliteStorage { path: PathBuf, } +#[macro_export] +macro_rules! cached_sql { + ( $label:expr, $db:expr, $sql:expr ) => {{ + if $label.is_none() { + $label = Some($db.prepare_cached($sql)?); + } + $label.as_mut().unwrap() + }}; +} + fn open_or_create_collection_db(path: &Path) -> Result { let mut db = Connection::open(path)?; @@ -211,6 +221,9 @@ pub(crate) struct StorageContext<'a> { usn: Option, timing_today: Option, + + // cards + pub(super) get_card_stmt: Option>, } impl StorageContext<'_> { @@ -220,6 +233,7 @@ impl StorageContext<'_> { server, usn: None, timing_today: None, + get_card_stmt: None, } } From 245a31a432853de5c81375531edec174c2fda2f4 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 26 Mar 2020 17:47:53 +1000 Subject: [PATCH 10/15] tweak code for mypy --- pylib/anki/stats.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py index 69ab0c415..59ed68ea8 100644 --- a/pylib/anki/stats.py +++ b/pylib/anki/stats.py @@ -40,14 +40,15 @@ class CardStats: self.addLine(_("First Review"), self.date(first / 1000)) self.addLine(_("Latest Review"), self.date(last / 1000)) if c.type in (CARD_TYPE_LRN, CARD_TYPE_REV): + next: Optional[str] = None if c.odid or c.queue < QUEUE_TYPE_NEW: - next = None + pass else: if c.queue in (QUEUE_TYPE_REV, QUEUE_TYPE_DAY_LEARN_RELEARN): - next = time.time() + ((c.due - self.col.sched.today) * 86400) + n = time.time() + ((c.due - self.col.sched.today) * 86400) else: - next = c.due - next = self.date(next) + n = c.due + next = self.date(n) if next: self.addLine(self.col.tr(TR.STATISTICS_DUE_DATE), next) if c.queue == QUEUE_TYPE_REV: From 0510ab7c9c5f9134d936ca2fe68939cdff99dd2e Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 26 Mar 2020 18:54:20 +1000 Subject: [PATCH 11/15] update_card() --- proto/backend.proto | 2 ++ pylib/anki/cards.py | 48 +++++++++++++++++-------------------- pylib/anki/rsbackend.py | 9 ++++--- rslib/src/backend/mod.rs | 38 +++++++++++++++++++++++++++++ rslib/src/storage/card.rs | 46 ++++++++++++++++++++++++++++++++++- rslib/src/storage/sqlite.rs | 2 ++ 6 files changed, 115 insertions(+), 30 deletions(-) diff --git a/proto/backend.proto b/proto/backend.proto index f285feb83..6a4fb91dc 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -48,6 +48,7 @@ message BackendInput { OpenCollectionIn open_collection = 36; Empty close_collection = 37; int64 get_card = 38; + Card update_card = 39; } } @@ -79,6 +80,7 @@ message BackendOutput { Empty open_collection = 36; Empty close_collection = 37; GetCardOut get_card = 38; + Empty update_card = 39; BackendError error = 2047; } diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index e856fc0ba..823d8f1de 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -12,6 +12,7 @@ from anki import hooks from anki.consts import * from anki.models import NoteType, Template from anki.notes import Note +from anki.rsbackend import BackendCard from anki.sound import AVTag from anki.utils import intTime, joinFields, timestampID @@ -33,9 +34,7 @@ class Card: lastIvl: int ord: int - def __init__( - self, col: anki.storage._Collection, id: Optional[int] = None - ) -> None: + def __init__(self, col: anki.storage._Collection, id: Optional[int] = None) -> None: self.col = col.weakref() self.timerStarted = None self._render_output: Optional[anki.template.TemplateRenderOutput] = None @@ -98,30 +97,27 @@ class Card: def flush(self) -> None: self._preFlush() - self.col.db.execute( - """ -insert or replace into cards values -(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", - self.id, - self.nid, - self.did, - self.ord, - self.mod, - self.usn, - self.type, - self.queue, - self.due, - self.ivl, - self.factor, - self.reps, - self.lapses, - self.left, - self.odue, - self.odid, - self.flags, - self.data, + card = BackendCard( + id=self.id, + nid=self.nid, + did=self.did, + ord=self.ord, + mtime=self.mod, + usn=self.usn, + ctype=self.type, + queue=self.queue, + due=self.due, + ivl=self.ivl, + factor=self.factor, + reps=self.reps, + lapses=self.lapses, + left=self.left, + odue=self.odue, + odid=self.odid, + flags=self.flags, + data=self.data, ) - self.col.log(self) + self.col.backend.update_card(card) def question(self, reload: bool = False, browser: bool = False) -> str: return self.css() + self.render_output(reload, browser).question_text diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index d6bf3fab0..8a98b9be4 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -36,6 +36,7 @@ assert ankirspy.buildhash() == anki.buildinfo.buildhash SchedTimingToday = pb.SchedTimingTodayOut BuiltinSortKind = pb.BuiltinSortKind +BackendCard = pb.Card try: import orjson @@ -481,9 +482,11 @@ class RustBackend: ).search_notes.note_ids def get_card(self, cid: int) -> Optional[pb.Card]: - return self._run_command( - pb.BackendInput(get_card=cid) - ).get_card.card + return self._run_command(pb.BackendInput(get_card=cid)).get_card.card + + def update_card(self, card: BackendCard) -> None: + self._run_command(pb.BackendInput(update_card=card)) + def translate_string_in( key: TR, **kwargs: Union[str, int, float] diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index f3c862248..55c2065e6 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -5,8 +5,10 @@ use crate::backend::dbproxy::db_command_bytes; use crate::backend_proto::backend_input::Value; use crate::backend_proto::{BuiltinSortKind, Empty, RenderedTemplateReplacement, SyncMediaIn}; use crate::card::{Card, CardID}; +use crate::card::{CardQueue, CardType}; use crate::collection::{open_collection, Collection}; use crate::config::SortKind; +use crate::decks::DeckID; use crate::err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind}; use crate::i18n::{tr_args, FString, I18n}; use crate::latex::{extract_latex, extract_latex_expanding_clozes, ExtractedLatex}; @@ -14,6 +16,7 @@ use crate::log::{default_logger, Logger}; use crate::media::check::MediaChecker; use crate::media::sync::MediaSyncProgress; use crate::media::MediaManager; +use crate::notes::NoteID; use crate::sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today}; use crate::sched::timespan::{answer_button_time, learning_congrats, studied_today, time_span}; use crate::search::{search_cards, search_notes, SortMode}; @@ -22,10 +25,13 @@ use crate::template::{ RenderedNode, }; use crate::text::{extract_av_tags, strip_av_tags, AVTag}; +use crate::timestamp::TimestampSecs; +use crate::types::Usn; use crate::{backend_proto as pb, log}; use fluent::FluentValue; use prost::Message; use std::collections::{HashMap, HashSet}; +use std::convert::TryFrom; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use tokio::runtime::Runtime; @@ -250,6 +256,10 @@ impl Backend { 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)?; + OValue::UpdateCard(pb::Empty {}) + } }) } @@ -625,6 +635,11 @@ impl Backend { card: card.map(card_to_pb), }) } + + fn update_card(&self, pbcard: pb::Card) -> Result<()> { + let card = pbcard_to_native(pbcard); + self.with_col(|col| col.with_ctx(|ctx| ctx.storage.update_card(&card))) + } } fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue { @@ -740,3 +755,26 @@ fn card_to_pb(c: Card) -> pb::Card { data: c.data, } } + +fn pbcard_to_native(c: pb::Card) -> Card { + Card { + id: CardID(c.id), + nid: NoteID(c.nid), + did: DeckID(c.did), + ord: c.ord as u16, + mtime: TimestampSecs(c.mtime), + usn: Usn(c.usn), + ctype: CardType::try_from(c.ctype as u8).unwrap_or(CardType::New), + queue: CardQueue::try_from(c.queue as i8).unwrap_or(CardQueue::New), + due: c.due, + ivl: c.ivl, + factor: c.factor as u16, + reps: c.reps, + lapses: c.lapses, + left: c.left, + odue: c.odue, + odid: DeckID(c.odid), + flags: c.flags, + data: c.data, + } +} diff --git a/rslib/src/storage/card.rs b/rslib/src/storage/card.rs index 28232b75e..e7a998121 100644 --- a/rslib/src/storage/card.rs +++ b/rslib/src/storage/card.rs @@ -3,7 +3,7 @@ use crate::cached_sql; use crate::card::{Card, CardID, CardQueue, CardType}; -use crate::err::Result; +use crate::err::{AnkiError, Result}; use rusqlite::params; use rusqlite::{ types::{FromSql, FromSqlError, ValueRef}, @@ -69,4 +69,48 @@ flags, data from cards where id=?" .optional() .map_err(Into::into) } + + pub(crate) fn update_card(&mut self, card: &Card) -> Result<()> { + if card.id.0 == 0 { + return Err(AnkiError::invalid_input("card id not set")); + } + self.flush_card(card) + } + + fn flush_card(&mut self, card: &Card) -> Result<()> { + let stmt = cached_sql!( + self.update_card_stmt, + self.db, + " +insert or replace into cards +(id, nid, did, ord, mod, usn, type, queue, due, ivl, factor, +reps, lapses, left, odue, odid, flags, data) +values +(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +" + ); + + stmt.execute(params![ + card.id, + card.nid, + card.did, + card.ord, + card.mtime, + card.usn, + card.ctype as u8, + card.queue as i8, + card.due, + card.ivl, + card.factor, + card.reps, + card.lapses, + card.left, + card.odue, + card.odid, + card.flags, + card.data, + ])?; + + Ok(()) + } } diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index 28d3de18b..a3bb4caf0 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -224,6 +224,7 @@ pub(crate) struct StorageContext<'a> { // cards pub(super) get_card_stmt: Option>, + pub(super) update_card_stmt: Option>, } impl StorageContext<'_> { @@ -234,6 +235,7 @@ impl StorageContext<'_> { usn: None, timing_today: None, get_card_stmt: None, + update_card_stmt: None, } } From 369457a1c7b7d3e7d3304298d7141deb30e3e691 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 26 Mar 2020 19:05:18 +1000 Subject: [PATCH 12/15] fix two cases where a float was being written into due --- pylib/anki/sched.py | 2 +- pylib/tests/test_schedv2.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pylib/anki/sched.py b/pylib/anki/sched.py index 0c59773bc..0e9c07967 100644 --- a/pylib/anki/sched.py +++ b/pylib/anki/sched.py @@ -341,7 +341,7 @@ limit %d""" resched = self._resched(card) if "mult" in conf and resched: # review that's lapsed - card.ivl = max(1, conf["minInt"], card.ivl * conf["mult"]) + card.ivl = max(1, conf["minInt"], int(card.ivl * conf["mult"])) else: # new card; no ivl adjustment pass diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv2.py index 1feb94f4c..cd98b1968 100644 --- a/pylib/tests/test_schedv2.py +++ b/pylib/tests/test_schedv2.py @@ -968,7 +968,7 @@ def test_timing(): c2 = d.sched.getCard() assert c2.queue == QUEUE_TYPE_REV # if the failed card becomes due, it should show first - c.due = time.time() - 1 + c.due = intTime() - 1 c.flush() d.reset() c = d.sched.getCard() From aa33bf85ef01706a4b0ba329bd212c7d2e1a6abd Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 26 Mar 2020 19:32:35 +1000 Subject: [PATCH 13/15] report invalid type/queue --- rslib/src/backend/mod.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 55c2065e6..66282c5fc 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -637,7 +637,7 @@ impl Backend { } fn update_card(&self, pbcard: pb::Card) -> Result<()> { - let card = pbcard_to_native(pbcard); + let card = pbcard_to_native(pbcard)?; self.with_col(|col| col.with_ctx(|ctx| ctx.storage.update_card(&card))) } } @@ -756,16 +756,20 @@ fn card_to_pb(c: Card) -> pb::Card { } } -fn pbcard_to_native(c: pb::Card) -> Card { - Card { +fn pbcard_to_native(c: pb::Card) -> Result { + let ctype = CardType::try_from(c.ctype as u8) + .map_err(|_| AnkiError::invalid_input("invalid card type"))?; + let queue = CardQueue::try_from(c.queue as i8) + .map_err(|_| AnkiError::invalid_input("invalid card queue"))?; + Ok(Card { id: CardID(c.id), nid: NoteID(c.nid), did: DeckID(c.did), ord: c.ord as u16, mtime: TimestampSecs(c.mtime), usn: Usn(c.usn), - ctype: CardType::try_from(c.ctype as u8).unwrap_or(CardType::New), - queue: CardQueue::try_from(c.queue as i8).unwrap_or(CardQueue::New), + ctype, + queue, due: c.due, ivl: c.ivl, factor: c.factor as u16, @@ -776,5 +780,5 @@ fn pbcard_to_native(c: pb::Card) -> Card { odid: DeckID(c.odid), flags: c.flags, data: c.data, - } + }) } From e28fdde98423b83b5f3ba2303b716d5e07bef444 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 26 Mar 2020 20:55:04 +1000 Subject: [PATCH 14/15] handle mtime/usn bump in backend, and tweak integer sizes --- proto/backend.proto | 14 +++++++------- pylib/anki/cards.py | 13 ++++--------- rslib/src/backend/mod.rs | 8 ++++---- rslib/src/card.rs | 25 +++++++++++++++++-------- rslib/src/storage/sqlite.rs | 5 +---- 5 files changed, 33 insertions(+), 32 deletions(-) diff --git a/proto/backend.proto b/proto/backend.proto index 6a4fb91dc..ad16f6bed 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -385,14 +385,14 @@ message Card { sint32 usn = 6; uint32 ctype = 7; sint32 queue = 8; - int64 due = 9; - int64 ivl = 10; + sint32 due = 9; + uint32 ivl = 10; uint32 factor = 11; - int64 reps = 12; - int64 lapses = 13; - int64 left = 14; - int64 odue = 15; + uint32 reps = 12; + uint32 lapses = 13; + uint32 left = 14; + sint32 odue = 15; int64 odid = 16; - int64 flags = 17; + uint32 flags = 17; string data = 18; } diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index 823d8f1de..d3887410c 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -82,28 +82,23 @@ class Card: self.flags = c.flags self.data = c.data - def _preFlush(self) -> None: - hooks.card_will_flush(self) - self.mod = intTime() - self.usn = self.col.usn() - # bug check + def _bugcheck(self) -> None: if ( self.queue == QUEUE_TYPE_REV and self.odue and not self.col.decks.isDyn(self.did) ): hooks.card_odue_was_invalid() - assert self.due < 4294967296 def flush(self) -> None: - self._preFlush() + self._bugcheck() + hooks.card_will_flush(self) + # mtime & usn are set by backend card = BackendCard( id=self.id, nid=self.nid, did=self.did, ord=self.ord, - mtime=self.mod, - usn=self.usn, ctype=self.type, queue=self.queue, due=self.due, diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 66282c5fc..81a71c266 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -637,8 +637,8 @@ impl Backend { } fn update_card(&self, pbcard: pb::Card) -> Result<()> { - let card = pbcard_to_native(pbcard)?; - self.with_col(|col| col.with_ctx(|ctx| ctx.storage.update_card(&card))) + let mut card = pbcard_to_native(pbcard)?; + self.with_col(|col| col.with_ctx(|ctx| ctx.update_card(&mut card))) } } @@ -751,7 +751,7 @@ fn card_to_pb(c: Card) -> pb::Card { left: c.left, odue: c.odue, odid: c.odid.0, - flags: c.flags, + flags: c.flags as u32, data: c.data, } } @@ -778,7 +778,7 @@ fn pbcard_to_native(c: pb::Card) -> Result { left: c.left, odue: c.odue, odid: DeckID(c.odid), - flags: c.flags, + flags: c.flags as u8, data: c.data, }) } diff --git a/rslib/src/card.rs b/rslib/src/card.rs index d1267fc87..05bd3d857 100644 --- a/rslib/src/card.rs +++ b/rslib/src/card.rs @@ -3,8 +3,9 @@ use crate::decks::DeckID; use crate::define_newtype; +use crate::err::Result; use crate::notes::NoteID; -use crate::{timestamp::TimestampSecs, types::Usn}; +use crate::{collection::RequestContext, timestamp::TimestampSecs, types::Usn}; use num_enum::TryFromPrimitive; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -48,15 +49,15 @@ pub struct Card { pub(crate) usn: Usn, pub(crate) ctype: CardType, pub(crate) queue: CardQueue, - pub(crate) due: i64, - pub(crate) ivl: i64, + pub(crate) due: i32, + pub(crate) ivl: u32, pub(crate) factor: u16, - pub(crate) reps: i64, - pub(crate) lapses: i64, - pub(crate) left: i64, - pub(crate) odue: i64, + pub(crate) reps: u32, + pub(crate) lapses: u32, + pub(crate) left: u32, + pub(crate) odue: i32, pub(crate) odid: DeckID, - pub(crate) flags: i64, + pub(crate) flags: u8, pub(crate) data: String, } @@ -84,3 +85,11 @@ impl Default for Card { } } } + +impl RequestContext<'_> { + pub fn update_card(&mut self, card: &mut Card) -> Result<()> { + card.mtime = TimestampSecs::now(); + card.usn = self.storage.usn()?; + self.storage.update_card(card) + } +} diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index a3bb4caf0..b57a3fd4f 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -215,9 +215,7 @@ impl SqliteStorage { pub(crate) struct StorageContext<'a> { pub(crate) db: &'a Connection, - #[allow(dead_code)] server: bool, - #[allow(dead_code)] usn: Option, timing_today: Option, @@ -303,7 +301,6 @@ impl StorageContext<'_> { Ok(()) } - #[allow(dead_code)] pub(crate) fn usn(&mut self) -> Result { if self.server { if self.usn.is_none() { @@ -313,7 +310,7 @@ impl StorageContext<'_> { .query_row(NO_PARAMS, |row| row.get(0))?, ); } - Ok(*self.usn.as_ref().unwrap()) + Ok(self.usn.clone().unwrap()) } else { Ok(Usn(-1)) } From 0f0b90d427254db46d6b05353ed61b5b494f519e Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 26 Mar 2020 21:05:02 +1000 Subject: [PATCH 15/15] fix progress dialog preventing schema mod https://anki.tenderapp.com/discussions/beta-testing/1868-anki-2124-beta#comment_48183062 This is an ugly hack - in the future the calling code should be updated to perform the check prior to starting the operation instead. --- qt/aqt/main.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 03d89c6f8..3f605db6f 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1248,7 +1248,10 @@ and if the problem comes up again, please ask on the support site.""" ########################################################################## def onSchemaMod(self, arg): - return askUser( + progress_shown = self.progress.busy() + if progress_shown: + self.progress.finish() + ret = askUser( _( """\ The requested change will require a full upload of the database when \ @@ -1257,6 +1260,9 @@ waiting on another device that haven't been synchronized here yet, they \ will be lost. Continue?""" ) ) + if progress_shown: + self.progress.start() + return ret # Advanced features ##########################################################################