Repairs
genanki2 changes automatically some of the values it is given or it reads. Most of these changes would happen anyway: Anki silently updates the value on an import, on a save or when "Check Database" is run. Any genanki2 repair that mirrors what "Check Database" does emits a warning.
Other repairs available in genanki2 are opt in. Many warnings and errors name the call, or the edit, that makes them go away.
This page lists the automatic and opt in repairs and how to solve errors and warnings. The reasons behind each automatic repair, and where Anki makes the same one, are in Repairs that alter what they read.
Automatic repairs
Normalized values on save
These run whenever a value is set, whether by the caller or by a read, and they never warn. Anki would make each change itself, so the package imports the same either way.
- Normalizing names and text: deck names, tags, notetype names, field names and template names;
notes.sfldandcsum: a note's sort field and checksum are recomputed from its fields on every write;- Out-of-range preset values: 18 preset scalars go back to their default when set outside the range that Anki accepts. The learning and relearning steps are clamped to the nearest bound.
- Unreadable dues and durations:
Card.dueandCard.original_dueoutside the 32-bit signed range, andReview.time_msoutside the 32-bit unsigned range, become0, which is what Anki reads in their place. - A normal deck's preset id: a deck's
Deck.config.id(if set) has priority overDeck.kind.config_id, which gets overwritten; - Normalizing media file names: extraction and
keep_source_media=Truestore each file under the normalized name (usingnormalize_media_name()), with a suffix when two names repair to the same one. The references in notes, CSS and templates are not updated and a warning lists the renames to apply (see Media references below).
A validator runs when a field is assigned, but there are some edge cases. One such case is mutable containers which don't trigger them, so a list changed in place is not repaired. Examples:
DeckConfig.config.learn_steps.append(...)skips the clamp and the write refuses the steps with a message asking to assign the list back.- Changing
Deck.kind.config_iddirectly skips the preset id copy and the write refuses the deck untilDeck.sync_normal_kind_config_id()runs.
Anki's "Check Database"
Anki stores these values until the user runs Tools > Check Database. genanki2 repairs them when set or read and warns each time about it.
| Value | Repair | Method that makes the same repair |
|---|---|---|
Card.due >= 2 000 000 on a new card |
wrapped into [1 000 000, 1 999 999] |
Card.repair_due_above_ceiling() |
Card.original_due on a card with no original deck |
cleared | Card.repair_unexplained_original_due() |
Card.original_deck and original_due on a card in a normal deck |
both cleared, again at write time | Card.repair_unexplained_original_data() |
Card.template_ord > 30 000 |
clamped to 30 000 |
Card.repair_template_ord_above_ceiling() |
Card.interval_days > 2 147 483 647 |
clamped to 2 147 483 647 |
Card.repair_interval_days_above_ceiling() |
Note.modification_time_s in the future |
set to now | Note.repair_modification_time() |
Review.time_ms > 2 147 483 647 |
clamped to 2 147 483 647 |
Review.repair_time_above_ceiling() |
dbcheck_repairs_disabled() keeps the values unchanged. The block has to cover the read, the update and the write because a validator repairs a value on any assignment to its model and the original deck is cleared again at write time:
from genanki2 import Package, dbcheck_repairs_disabled
with dbcheck_repairs_disabled():
pkg = Package.read_apkg("italian.apkg", keep_source_media=True)
... # the update
pkg.write_apkg("italian-v2.apkg")
A kept value warns at write time and names the method in the table. A repair called inside the block works, so specific fixes can be applied. dbcheck_repairs_enabled() turns the repairs back on inside a disabled block.
Repairs at write time
Writing a package or a collection first collects every object referenced by another object: notes, decks, notetypes, etc. When a deck references preset 1 and the collection does not hold it, the builtin DEFAULT_DECK_CONFIG is added. A card's original deck and due are checked again once the decks are final, as in the table above.
Opt in repairs
Nothing in this section is called inside genanki2.
Media references
Reading a package can rename media files (see above), and a write refuses a name Anki would not store, listing the name each file should have. In both cases the references to the old names need the same rename.
| Call | What it changes |
|---|---|
Package.replace_media_references(renames) |
every reference to a renamed file: note fields, and names beginning with _ in notetype CSS and templates |
Package.read_apkg(..., repair_media_references=True) |
applies the renames the read made before returning, instead of warning |
Note.replace_media_references(renames), Notetype.replace_underscored_css_imports(renames), Template.replace_underscored_references(renames) |
the same rename for one note, stylesheet or template |
Package.trim_unused_media() |
drops every file nothing references, like Anki's Tools > Check Media |
A read that stores a file under a new name prints the renames as a map that replace_media_references() can take:
pkg = Package.read_apkg("shared.apkg", keep_source_media=True)
# Genanki2Warning: MediaEntries: media map contains filenames that Anki would not
# store. [...] Pass this mapping to `genanki2.Package.replace_media_references()`:
# {
# 'casa?.mp3': 'casa.mp3',
# }
pkg.replace_media_references({"casa?.mp3": "casa.mp3"})
repair_media_references=True makes the same call during the read, without the warning:
pkg = Package.read_apkg(
"shared.apkg", keep_source_media=True, repair_media_references=True
)
Cards and notes
| Call | What it changes | Anki equivalent |
|---|---|---|
Collection.generate_missing_cards(deck=None) |
adds the cards that a note renders and does not have yet, for example after filling a field that a template's front needs | the cards Anki adds when a note is edited |
Collection.remove_empty_cards(keep_notes=False) |
removes the cards that a note no longer renders, with their reviews, and a note whose cards are all empty unless keep_notes=True |
Tools > Empty Cards |
Collection.repair_next_new_card_position() |
moves the new-card position counter past the highest position a new card holds, so added notes are studied after the existing ones | Check Database |
Card.repair_due_above_ceiling(), Card.repair_unexplained_original_due(), Card.repair_unexplained_original_data(), Card.repair_template_ord_above_ceiling(), Card.repair_interval_days_above_ceiling(), Note.repair_modification_time(), Review.repair_time_above_ceiling() |
the Check Database repairs above, for one card, note or review | Check Database |
Decks and presets
| Call | What it changes | Anki equivalent |
|---|---|---|
Collection.collect_default_deck() |
adds the builtin DEFAULT_DECK when no deck has id 1 |
none: Anki cannot study a collection without it |
Collection.repair_missing_deck_configs() |
points every normal deck whose preset the collection does not hold at preset 1 | the fallback Anki makes each time it reads such a deck, without saving it |
DeckConfig.repair_unsearchable_name() |
drops the double quotes and trailing backslashes that keep the FSRS optimizer from finding the preset's cards, and warns on an empty name | none |
Notetypes and templates
| Call | What it changes | Anki equivalent |
|---|---|---|
Notetype.fill_missing_ids() |
gives every field and template without an id a random one, so a later rename is matched by id | the id Anki gives a field or template when it creates one |
Notetype.repair_sort_field_idx() |
clamps a sort field index past the last field to the last field | the same clamp on save |
Template.repair_target_deck_id() |
clears a deck override, so the template's cards go to the deck of their siblings; an override to deck 1 is kept | none |
fill_missing_ids() generates new ids on every run. Call it only in the first build and read the package from then on: a script that rebuilds an already imported deck and calls it each time duplicates the notetype on the next import.
Solving warnings and errors
Each row indicates the class that raises the message, whether it is a warning or an error and how the message starts. The messages are raised during writing, if not mentioned otherwise. N stands for a number and X for a name or a list.
Media
| Class | Type | Message | What to do |
|---|---|---|---|
MediaEntries |
warning, on a read | media map contains filenames that Anki would not store |
pass the printed mapping to Package.replace_media_references(), or read with repair_media_references=True |
MediaEntries |
warning, on a read | media map contains duplicate filenames |
rename the files, then rename their references with Package.replace_media_references() |
Package |
warning, on a read | the package carries N media files |
read with extract_dir or keep_source_media=True |
Package |
error | not all media file names are normalized |
rename each file on disk to the name the listing gives, then rename the references with Package.replace_media_references() |
Package |
error | some notes, notetypes or templates reference media files the package does not carry |
add each file to media, or correct the reference; when the source package stores the file under a repaired name, the message prints the mapping for Package.replace_media_references() or repair_media_references=True |
CollectionPackage |
warning | some notes, notetypes or templates reference media files the package does not carry |
add each file to media, or correct the reference; when the source package stores the file under a repaired name, the message prints the mapping for Package.replace_media_references() or repair_media_references=True |
Package |
warning | no note references media files X |
normalize the references with normalize_media_name() to see which files are unused, then Package.trim_unused_media() |
Cards, notes and reviews
| Class | Type | Message | What to do |
|---|---|---|---|
Card |
warning | new card position N is at or above 2,000,000 and was not repaired |
Card.repair_due_above_ceiling() |
Card |
warning | original due N should be cleared |
Card.repair_unexplained_original_due() |
Card |
warning | original deck N and original due N should be cleared |
Card.repair_unexplained_original_data() |
Card |
warning | template ordinal N is above 30,000 and was not repaired |
Card.repair_template_ord_above_ceiling() |
Card |
warning | interval of N days is above 2**31 - 1 and was not repaired |
Card.repair_interval_days_above_ceiling() |
Card |
warning | card N is typed as review or relearning but carries no ease factor |
set the factor to the one the card would have graduated with, 2500 by default |
Card |
error | card N counts due in days since the collection was created, but N holds what looks like a timestamp |
compute the day count with Collection.days_elapsed_since_creation |
Note |
warning | modification time is in the future (N), now is N, and was not repaired |
Note.repair_modification_time() |
Note |
warning | note id=N has cloze markup in X, but note type "X" is not a cloze note type |
use a cloze notetype, or remove the markup |
Review |
warning | entry N is typed as a learning review but took no time and pressed no button |
set ReviewType.RESCHEDULED, or add the time and the button |
Review |
warning | entry N took N ms, which is above 2**31 - 1, and was not repaired |
Review.repair_time_above_ceiling() |
Collection |
warning, from add_notes() and generate_missing_cards() |
the new-card position counter (N) is at or below the highest position an existing new card holds (N) |
call Collection.repair_next_new_card_position() before adding the notes |
Decks, presets and notetypes
| Class | Type | Message | What to do |
|---|---|---|---|
Collection |
warning, not for an .apkg |
no deck has id 1 |
Collection.collect_default_deck() |
Collection |
error, for a .colpkg |
the collection has no deck with id 1 |
Collection.collect_default_deck() |
Collection |
warning | a normal deck names a preset the collection does not hold |
Collection.repair_missing_deck_configs(), or add the missing preset |
Deck |
error | deck "X" carries preset N ("X"), but its kind blob points at preset N |
Deck.sync_normal_kind_config_id() |
DeckConfig |
error | learn_steps X holds a value outside [0, N] minutes (or relearn_steps) |
assign the list back to the preset's learn_steps or relearn_steps, which clamps it |
DeckConfig |
warning | "X" (id N) changed since it was read |
give the preset a new id with monotonic_id() and move the decks to it, since an import keeps the preset a user already has under the old id |
Note, Notetype |
warning | changed since its last stamp, but its modification time (N) is not later than that stamp (N) |
set modification_time_s to a later time |
DeckConfig |
warning | "X" has a name Anki cannot search for |
DeckConfig.repair_unsearchable_name() |
Notetype |
warning | "X" has a sort field index (N) that exceeds the number of fields (N) |
Notetype.repair_sort_field_idx() |
Template |
warning | "X" sets a deck override (target_deck_id=N) |
point the override at a normal deck in the collection, or Template.repair_target_deck_id() |
Collection |
warning, from add_notes() and generate_missing_cards() |
Template "X" overrides the deck with id N, which is not in the collection or is a filtered deck |
point the override at a normal deck in the collection, or Template.repair_target_deck_id() |
Collection settings and file names
| Class | Type | Message | What to do |
|---|---|---|---|
Collection |
warning, not for an .apkg |
the `schedVer` config entry is N |
ConfigEntry(key="schedVer", value=b"2") |
Collection |
error, for a .colpkg |
the collection's `schedVer` config entry is N |
ConfigEntry(key="schedVer", value=b"2") |
Collection |
warning, not for an .apkg |
the `sched2021` config entry is missing or does not hold `true` |
ConfigEntry(key="sched2021", value=b"true") |
Package |
error, from write_apkg() |
Invalid file name: Anki reads "X" as a whole collection |
choose another name, or write a CollectionPackage |
CollectionPackage |
error, from write_colpkg() |
Invalid file name: "X" does not end in ".colpkg" |
add the extension, or use Package.write_apkg() for a deck to share |