{"id":15017289,"url":"https://github.com/patarapolw/ankisync2","last_synced_at":"2025-05-07T02:00:22.178Z","repository":{"id":46626694,"uuid":"199587077","full_name":"patarapolw/ankisync2","owner":"patarapolw","description":"Creating and editing *.apkg and *.anki2 safely","archived":false,"fork":false,"pushed_at":"2023-05-25T08:16:58.000Z","size":622,"stargazers_count":83,"open_issues_count":7,"forks_count":14,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-05-02T17:05:47.422Z","etag":null,"topics":["anki","anki-flashcards","hacktoberfest","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/patarapolw.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-07-30T06:15:07.000Z","updated_at":"2025-04-15T11:19:38.000Z","dependencies_parsed_at":"2024-09-20T14:41:09.698Z","dependency_job_id":null,"html_url":"https://github.com/patarapolw/ankisync2","commit_stats":{"total_commits":46,"total_committers":3,"mean_commits":"15.333333333333334","dds":0.4782608695652174,"last_synced_commit":"8cee906bba08115ec02a6a5c124f3dd029e25a9c"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patarapolw%2Fankisync2","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patarapolw%2Fankisync2/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patarapolw%2Fankisync2/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patarapolw%2Fankisync2/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/patarapolw","download_url":"https://codeload.github.com/patarapolw/ankisync2/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252798832,"owners_count":21805882,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["anki","anki-flashcards","hacktoberfest","python"],"created_at":"2024-09-24T19:50:15.967Z","updated_at":"2025-05-07T02:00:21.542Z","avatar_url":"https://github.com/patarapolw.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# AnkiSync 2\n\n[![PyPI version shields.io](https://img.shields.io/pypi/v/ankisync2.svg)](https://pypi.python.org/pypi/ankisync2/)\n[![PyPI license](https://img.shields.io/pypi/l/ankisync2.svg)](https://pypi.python.org/pypi/ankisync2/)\n\n\\*.apkg and \\*.anki2 file structure is very simple, but with some quirks of incompleteness.\n\n[\\*.apkg file structure](https://github.com/ankidroid/Anki-Android/wiki/Database-Structure) is a zip of at least two files.\n\n```\n.\n├── example\n│   ├── collection.anki2\n│   ├── collection.anki21 # newer Anki Desktop creates and uses this file instead, while retaining the old one as stub.\n│   ├── media # JSON of dict[int, str]\n│   ├── 1  # Media files with the names masked as integers\n│   ├── 2\n│   ├── 3\n|   └── ...\n└── example.apkg\n```\n\n\\*.anki2 is a SQLite file with foreign key disabled, and the usage of [some JSON schemas](/ankisync2/anki20/builder.py) instead of [some tables](/ankisync2/anki20/db.py#L51)\n\nAlso, \\*.anki2 is used internally at [`os.path.join(appdirs.user_data_dir('Anki2'), 'User 1', 'collection.anki2')`](/ankisync2/dir.py#L75), so editing the SQLite there will also edit the database.\n\nHowever, [internal \\*.anki2 has recently changed](https://github.com/patarapolw/ankisync2/issues/3). If you need to edit internally, if maybe safer to do in Anki\u003c=2.1.26. If you have trouble running two Anki versions (latest and 2.1.26), see [`/__utils__/anki2.1.26`](https://github.com/patarapolw/ankisync/tree/master/__utils__/anki2.1.26).\n\nThe `media` file is a text file of at least a string of `{}`, which is actually a dictionary of keys -- stringified int; and values -- filenames.\n\n## Usage\n\nSome [extra tables](/ankisync2/anki20/db.py#L51) are created if not exists.\n\n```python\nfrom ankisync2 import Apkg\n\nwith Apkg(\"example.apkg\") as apkg:\n    # Or Apkg(\"example/\") also works - the folder named 'example' will be created.\n    apkg.db.database.execute_sql(SQL, PARAMS)\n    apkg.zip(output=\"example1.apkg\")\n```\n\nI also support adding media.\n\n```python\napkg.add_media(\"path/to/media.jpg\")\n```\n\nTo find the wanted cards and media, iterate though the `Apkg` and `Apkg.iter_media` object.\n\n```python\nfor card in apkg:\n    print(card)\n```\n\n## Creating a new \\*.apkg\n\nYou 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.\n\n```python\napkg = Apkg(\"example\")  # Create example folder\n```\n\nAfter 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.\n\n1. Model, Deck\n2. Template, Note\n3. Card\n\n```python\nwith Apkg(\"example.apkg\") as apkg:\n    m = apkg.db.Models.create(name=\"foo\", flds=[\"field1\", \"field2\"])\n    d = apkg.db.Decks.create(name=\"bar::baz\")\n    t = [\n        apkg.db.Templates.create(name=\"fwd\", mid=m.id, qfmt=\"{{field1}}\", afmt=\"{{field2}}\"),\n        apkg.db.Templates.create(name=\"bwd\", mid=m.id, qfmt=\"{{field2}}\", afmt=\"{{field1}}\")\n    ]\n    n = apkg.db.Notes.create(mid=m.id, flds=[\"data1\", \"\u003cimg src='media.jpg'\u003e\"], tags=[\"tag1\", \"tag2\"])\n    c = [\n        apkg.db.Cards.create(nid=n.id, did=d.id, ord=i)\n        for i, _ in enumerate(t)\n    ]\n```\n\nYou can also add media, which is not related to the SQLite database.\n\n```python\napkg.add_media(\"path/to/media.jpg\")\n```\n\nFinally, finalize with\n\n```python\napkg.export(\"example1.apkg\")\n```\n\n## Updating an \\*.apkg\n\nThis is also possible, by modifying `db.Notes.data` as `sqlite_ext.JSONField`, with `peewee.signals`.\n\nIt is now as simple as,\n\n```python\nwith Apkg(\"example1.apkg\") as apkg:\n    for n in apkg.db.Notes.filter(db.Notes.data[\"field1\"] == \"data1\"):\n        n.data[\"field3\"] = \"data2\"\n        n.save()\n\n    apkg.close()\n```\n\n## JSON schema of `Col.models`, `Col.decks`, `Col.conf` and `Col.dconf`\n\nI have created `dataclasses` for this at [/ankisync2/builder.py](/ankisync2/builder.py). To serialize it, use `dataclasses.asdict` or\n\n```python\nfrom ankisync2 import DataclassJSONEncoder\nimport json\n\njson.dumps(dataclassObject, cls=DataclassJSONEncoder)\n```\n\n## Editing user's `collection.anki2`\n\nThis can be found at `${ankiPath}/${user}/collection.anki2`. Of course, do this at your own risk. Always backup first.\n\n```python\nfrom ankisync2 import AnkiDesktop\n\nAnkiDesktop.backup(\"/path/to/anki-desktop.db\")\nanki = AnkiDesktop(filename=\"/path/to/anki-desktop.db\")\n... # Edit as you please\nAnkiDesktop.restore(\"/path/to/anki-desktop.db\")\n```\n\n## Using `peewee` framework\n\nThis is based on `peewee` ORM framework. You can use Dataclasses and Lists directly, without converting them to string first.\n\n## Examples\n\nPlease see [`/__examples__`](/__examples__), and [/tests](/tests).\n\n## Installation\n\n```bash\npip install ankisync2\n```\n\n# Related projects\n\n- \u003chttps://github.com/patarapolw/ankisync\u003e\n- \u003chttps://github.com/patarapolw/AnkiTools\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatarapolw%2Fankisync2","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpatarapolw%2Fankisync2","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatarapolw%2Fankisync2/lists"}