Skip to content

Commit

Permalink
Allow disambiguating duplicated note model UUIDs in collection
Browse files Browse the repository at this point in the history
The issue of note model UUIDs being cloned along with the note models
themselves has been fixed (Stvad#136).  However, many users' collections
will already contain such duplicated note model UUIDs.  Also, if a
user clones a note type on another platform, then the UUID will still
be duplicated.

The disambiguation is run before export and snapshot, since that's
when it's most needed to avoid broken `deck.json`s.

In the long, run we could perhaps switch to running the code only
after syncing (since our "attack surface" would be cloning of note
models on other platforms).

Running `disambiguate_note_model_uuids` takes 10 ms on a collection
with 20 note types (without duplicates), which is IMO an acceptable
overhead.

The file `disambiguate_uuids.py` could also contain the (far lower
priority) disambiguation of deck config UUIDs (see Stvad#135).
  • Loading branch information
aplaice committed Dec 5, 2021
1 parent 029140a commit 0c85a5d
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 0 deletions.
5 changes: 5 additions & 0 deletions crowd_anki/export/anki_exporter_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from ..config.config_settings import ConfigSettings
from ..utils import constants
from ..utils.notifier import AnkiModalNotifier, Notifier
from ..utils.disambiguate_uuids import disambiguate_note_model_uuids

EXPORT_FAILED_TITLE = "Export failed"

Expand Down Expand Up @@ -45,6 +46,10 @@ def exportInto(self, directory_path):
self.notifier.warning(EXPORT_FAILED_TITLE, "CrowdAnki does not support export for dynamic decks.")
return

# Clean up duplicate note models. See
# https://github.com/Stvad/CrowdAnki/wiki/Workarounds-%E2%80%94-Duplicate-note-model-uuids.
disambiguate_note_model_uuids(self.collection)

# .parent because we receive name with random numbers at the end (hacking around internals of Anki) :(
export_path = Path(directory_path).parent
self.anki_json_exporter.export_to_directory(deck, export_path, self.includeMedia,
Expand Down
5 changes: 5 additions & 0 deletions crowd_anki/history/archiver_vendor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from ..config.config_settings import ConfigSettings
from ..export.anki_exporter import AnkiJsonExporter
from ..utils.notifier import Notifier, AnkiTooltipNotifier
from ..utils.disambiguate_uuids import disambiguate_note_model_uuids


@dataclass
Expand Down Expand Up @@ -41,6 +42,10 @@ def snapshot_on_sync(self):
self.do_snapshot('CrowdAnki: Snapshot on sync')

def do_snapshot(self, reason):
# Clean up duplicate note models. See
# https://github.com/Stvad/CrowdAnki/wiki/Workarounds-%E2%80%94-Duplicate-note-model-uuids.
disambiguate_note_model_uuids(self.window.col)

with progress_indicator(self.window, 'Taking CrowdAnki snapshot of all decks'):
self.all_deck_archiver().archive(overrides=self.overrides(),
reason=reason)
Expand Down
54 changes: 54 additions & 0 deletions crowd_anki/utils/disambiguate_uuids.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from uuid import uuid1

from .notifier import AnkiModalNotifier

def disambiguate_note_model_uuids(collection):
"""Disambiguate duplicate note model UUIDs.
In CrowdAnki ≤ 0.9, cloning a note model with an already assigned
crowdanki_uuid resulted in the clone inheriting the "original's"
crowdanki_uuid.
This has been fixed (#136), but:
1. Users will still have duplicate UUIDs from before the upgrade.
2. Users who create note models on platforms other than Anki with
CrowdAnki installed — for instance on mobile — will still have the
old issue, since cloning note models on those platforms will again
clone UUIDs.
"""
notifier = AnkiModalNotifier()
uuids = []
full_message = ""
for model in filter(lambda model: "crowdanki_uuid" in model,
sorted(collection.models.all(), key=lambda m: m["id"])):
# We're sorting by note model id, because it almost always
# (see the discussion in the PR for this addition) corresponds
# to the time of creation of the note model, in milliseconds
# since the epoch, so the copies will almost always have
# larger ids than the originals, so we'll hopefully be
# changing the UUIDs of the copies, not the originals.
crowdanki_uuid = model["crowdanki_uuid"]
if crowdanki_uuid in uuids:
new_crowdanki_uuid = str(uuid1())
model["crowdanki_uuid"] = new_crowdanki_uuid
collection.models.save(model)
message = (f"Replacing duplicate UUID ({crowdanki_uuid}) for note model "
f"“{model['name']}” with new UUID ({new_crowdanki_uuid})!\n")
# Printing in the unlikely case there's a crash later in
# the loop.
print(message)
full_message += message
else:
uuids.append(crowdanki_uuid)

if full_message:
full_message += (
"\nFor details, please see "
"https://github.com/Stvad/CrowdAnki/wiki/Workarounds-—-Duplicate-note-model-uuids .\n\n"
"The replacement should be a one-off occurrence. "
"If this message appears frequently, please open an issue!"
)
notifier.info("UUIDs disambiguated", full_message)

0 comments on commit 0c85a5d

Please sign in to comment.