/ankisync2

Creating and editing *.apkg and *.anki2 safely

Primary LanguagePythonMIT LicenseMIT

AnkiSync 2

PyPI version shields.io PyPI license

*.apkg and *.anki2 file structure is very simple, but with some quirks of incompleteness.

*.apkg file structure is a zip of at least two files.

.
├── example
│   ├── collection.anki2
│   ├── collection.anki21 # newer Anki Desktop creates and uses this file instead, while retaining the old one as stub.
│   ├── media # JSON of dict[int, str]
│   ├── 1  # Media files with the names masked as integers
│   ├── 2
│   ├── 3
|   └── ...
└── example.apkg

*.anki2 is a SQLite file with foreign key disabled, and the usage of some JSON schemas instead of some tables

Also, *.anki2 is used internally at os.path.join(appdirs.user_data_dir('Anki2'), 'User 1', 'collection.anki2'), so editing the SQLite there will also edit the database.

However, internal *.anki2 has recently changed. If you need to edit internally, if maybe safer to do in Anki<=2.1.26. If you have trouble running two Anki versions (latest and 2.1.26), see /__utils__/anki2.1.26.

The media file is a text file of at least a string of {}, which is actually a dictionary of keys -- stringified int; and values -- filenames.

Usage

Some extra tables are created if not exists.

from ankisync2 import Apkg

with Apkg("example.apkg") as apkg:
    # Or Apkg("example/") also works - the folder named 'example' will be created.
    apkg.db.database.execute_sql(SQL, PARAMS)
    apkg.zip(output="example1.apkg")

I also support adding media.

apkg.add_media("path/to/media.jpg")

To find the wanted cards and media, iterate though the Apkg and Apkg.iter_media object.

for card in apkg:
    print(card)

Creating a new *.apkg

You can create a new *.apkg via Apkg with any custom filename (and *.anki2 via Anki2()). A folder required to create *.apkg needs to be created first.

apkg = Apkg("example")  # Create example folder

After that, the Apkg will require at least 1 card, which is connected to at least 1 note, 1 model, 1 template, and 1 deck; which should be created in this order.

  1. Model, Deck
  2. Template, Note
  3. Card
with Apkg("example.apkg") as apkg:
    m = apkg.db.Models.create(name="foo", flds=["field1", "field2"])
    d = apkg.db.Decks.create(name="bar::baz")
    t = [
        apkg.db.Templates.create(name="fwd", mid=m.id, qfmt="{{field1}}", afmt="{{field2}}"),
        apkg.db.Templates.create(name="bwd", mid=m.id, qfmt="{{field2}}", afmt="{{field1}}")
    ]
    n = apkg.db.Notes.create(mid=m.id, flds=["data1", "<img src='media.jpg'>"], tags=["tag1", "tag2"])
    c = [
        apkg.db.Cards.create(nid=n.id, did=d.id, ord=i)
        for i, _ in enumerate(t)
    ]

You can also add media, which is not related to the SQLite database.

apkg.add_media("path/to/media.jpg")

Finally, finalize with

apkg.export("example1.apkg")

Updating an *.apkg

This is also possible, by modifying db.Notes.data as sqlite_ext.JSONField, with peewee.signals.

It is now as simple as,

with Apkg("example1.apkg") as apkg:
    for n in apkg.db.Notes.filter(db.Notes.data["field1"] == "data1"):
        n.data["field3"] = "data2"
        n.save()

    apkg.close()

JSON schema of Col.models, Col.decks, Col.conf and Col.dconf

I have created dataclasses for this at /ankisync2/builder.py. To serialize it, use dataclasses.asdict or

from ankisync2 import DataclassJSONEncoder
import json

json.dumps(dataclassObject, cls=DataclassJSONEncoder)

Editing user's collection.anki2

This can be found at ${ankiPath}/${user}/collection.anki2. Of course, do this at your own risk. Always backup first.

from ankisync2 import AnkiDesktop

AnkiDesktop.backup("/path/to/anki-desktop.db")
anki = AnkiDesktop(filename="/path/to/anki-desktop.db")
... # Edit as you please
AnkiDesktop.restore("/path/to/anki-desktop.db")

Using peewee framework

This is based on peewee ORM framework. You can use Dataclasses and Lists directly, without converting them to string first.

Examples

Please see /__examples__, and /tests.

Installation

pip install ankisync2

Related projects