Skip to content

Commit 478b9bd

Browse files
authored
perf: Speed up media_names_for_ankihub_deck (#1380)
* Use .iterator() in media_names_for_ankihub_deck() to avoid holding rows in memory * Extract media names from AnkiHub DB instead of collection * Fix tests * Avoid scanning the notes table twice and use .tuples() * Revert "Extract media names from AnkiHub DB instead of collection" This reverts commit bf3e891. * Clarify why a collection scan is needed.
1 parent 8a14bfb commit 478b9bd

3 files changed

Lines changed: 44 additions & 25 deletions

File tree

ankihub/db/db.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -448,28 +448,28 @@ def downloadable_media_for_ankihub_deck(self, ah_did: uuid.UUID) -> List[DeckMed
448448

449449
def media_names_for_ankihub_deck(self, ah_did: uuid.UUID) -> Set[str]:
450450
"""Returns the names of all media files which are referenced on notes in the given deck."""
451-
notes = AnkiHubNote.select(AnkiHubNote.anki_note_type_id, AnkiHubNote.fields).filter(
452-
NOTE_NOT_DELETED_CONDITION,
453-
ankihub_deck_id=ah_did,
451+
result: Set[str] = set()
452+
note_types: Dict[int, Optional[NotetypeDict]] = {}
453+
rows: Iterable[Tuple[int, Optional[Dict[str, str]]]] = (
454+
AnkiHubNote.select(AnkiHubNote.anki_note_type_id, AnkiHubNote.fields)
455+
.filter(NOTE_NOT_DELETED_CONDITION, ankihub_deck_id=ah_did)
456+
.tuples()
457+
.iterator()
454458
)
455-
note_type_ids = set(note.anki_note_type_id for note in notes)
456-
note_types: Dict[int, NotetypeDict] = {
457-
nt.anki_note_type_id: nt.note_type_dict
458-
for nt in AnkiHubNoteType.select(AnkiHubNoteType.anki_note_type_id, AnkiHubNoteType.note_type_dict).filter(
459-
anki_note_type_id__in=note_type_ids
460-
)
461-
}
462-
note_type_refs = {
463-
name for note_type in note_types.values() for name in get_media_names_from_note_type(note_type)
464-
}
465-
note_refs = {
466-
media_name
467-
for note in notes
468-
for field_value in (note.fields.values() if note.fields else [])
469-
for media_name in get_media_names_from_note_field(field_value, note_types[note.anki_note_type_id])
470-
}
459+
for mid, fields in rows:
460+
if mid not in note_types:
461+
note_type = note_types[mid] = self.note_type_dict(NotetypeId(mid))
462+
if note_type is not None:
463+
result.update(get_media_names_from_note_type(note_type))
464+
465+
note_type = note_types[mid]
466+
if note_type is None or not fields:
467+
continue
471468

472-
return {*note_type_refs, *note_refs}
469+
for field_value in fields.values():
470+
result.update(get_media_names_from_note_field(field_value, note_type))
471+
472+
return result
473473

474474
def media_names_exist_for_ankihub_deck(self, ah_did: uuid.UUID, media_names: Set[str]) -> Dict[str, bool]:
475475
"""Returns a dictionary where each key is a media name and the corresponding value is a boolean

ankihub/gui/media_sync.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,11 @@ def _update_deck_media(self, ankihub_did: uuid.UUID) -> None:
336336
LOGGER.info("No new media updates for deck.", ah_did=ankihub_did)
337337

338338
def _media_referenced_by_notes(self, ah_did: uuid.UUID) -> Set[str]:
339-
"""Scan all notes in the AnkiHub deck and return the set of referenced media filenames."""
339+
"""
340+
Scan all notes in the AnkiHub deck and return the set of referenced media filenames.
341+
This requires scanning the Anki collection rather than the AnkiHub DB
342+
to filter out media from emptied protected fields (INTP-338)
343+
"""
340344
anki_nids: List[NoteId] = ankihub_db.anki_nids_for_ankihub_deck(ah_did)
341345

342346
media_names: Set[str] = set()

tests/addon/test_integration.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8479,6 +8479,7 @@ def test_download_media(
84798479
self,
84808480
anki_session_with_addon_data: AnkiSession,
84818481
install_sample_ah_deck: InstallSampleAHDeck,
8482+
import_ah_note: ImportAHNote,
84828483
mocker: MockerFixture,
84838484
qtbot: QtBot,
84848485
):
@@ -8504,8 +8505,13 @@ def test_download_media(
85048505
],
85058506
)
85068507

8507-
# Mock the _media_referenced_by_notes method to include the test media
8508-
mocker.patch.object(media_sync, "_media_referenced_by_notes", return_value={"image.png"})
8508+
# Add a note to the deck which references the test media
8509+
import_ah_note(
8510+
ah_did=ah_did,
8511+
note_data=NoteInfoFactory.create(
8512+
fields=[Field(name="Front", value='<img src="image.png">'), Field(name="Back", value="")],
8513+
),
8514+
)
85098515

85108516
# Mock the client method for downloading media
85118517
download_media_mock = mocker.patch.object(AnkiHubClient, "download_media")
@@ -8567,6 +8573,7 @@ def test_download_media_with_filtering_by_referenced_media(
85678573
self,
85688574
anki_session_with_addon_data: AnkiSession,
85698575
install_sample_ah_deck: InstallSampleAHDeck,
8576+
import_ah_note: ImportAHNote,
85708577
mocker: MockerFixture,
85718578
qtbot: QtBot,
85728579
):
@@ -8603,8 +8610,16 @@ def test_download_media_with_filtering_by_referenced_media(
86038610
],
86048611
)
86058612

8606-
# Mock the _media_referenced_by_notes method to return only one media file
8607-
mocker.patch.object(media_sync, "_media_referenced_by_notes", return_value={"referenced_image.png"})
8613+
# Add a note to the deck which references only one of the media files
8614+
import_ah_note(
8615+
ah_did=ah_did,
8616+
note_data=NoteInfoFactory.create(
8617+
fields=[
8618+
Field(name="Front", value='<img src="referenced_image.png">'),
8619+
Field(name="Back", value=""),
8620+
],
8621+
),
8622+
)
86088623

86098624
download_media_mock = mocker.patch.object(AnkiHubClient, "download_media")
86108625

0 commit comments

Comments
 (0)