react-native-anki-reader
v0.1.2
Published
Read Anki .apkg / .anki2 decks in React Native. Unzips the package, opens the embedded SQLite collection, and returns parsed decks, notes, and cards.
Maintainers
Readme
react-native-anki-reader
Read Anki .apkg / .anki2 decks in React Native (and Node). The library
unzips the package, opens the embedded SQLite collection, and returns parsed
decks, notes, and cards — with note fields already split and mapped to
their field names.
It ships no native dependencies. Instead you plug in small adapters that wrap the unzip / SQLite / filesystem libraries you already use. This keeps the package lightweight, testable, and resilient to breaking changes in those native modules.
Install
npm install react-native-anki-readerThen install whatever native modules you'll wrap in the adapters. A common, well-supported combination:
npm install @op-engineering/op-sqlite react-native-zip-archive react-native-fs
cd ios && pod installSetup: write your adapters once
// anki-adapters.ts
import { open } from '@op-engineering/op-sqlite';
import { unzip } from 'react-native-zip-archive';
import RNFS from 'react-native-fs';
import type { Adapters } from 'react-native-anki-reader';
export const ankiAdapters: Adapters = {
zip: {
unzip: (source, target) => unzip(source, target),
},
sqlite: {
open: async (dbPath) => {
const db = open({ name: dbPath });
return {
query: async (sql, params = []) => {
const res = await db.executeAsync(sql, params);
return res.rows?._array ?? [];
},
close: async () => db.close(),
};
},
},
fs: {
exists: (p) => RNFS.exists(p),
readTextFile: (p) => RNFS.readFile(p, 'utf8'),
remove: async (p) => {
if (await RNFS.exists(p)) await RNFS.unlink(p);
},
join: (...segments) => segments.join('/'),
cacheDir: RNFS.CachesDirectoryPath,
// Optional — only needed if you want clearCache() to remove stragglers.
listDir: async (p) => (await RNFS.readDir(p)).map((e) => e.name),
},
};Usage
import { openApkg } from 'react-native-anki-reader';
import { ankiAdapters } from './anki-adapters';
const pkg = await openApkg(ankiAdapters, '/path/to/deck.apkg');
try {
const decks = await pkg.getDecks();
const models = await pkg.getModels();
// Cards joined with their note, model, and template — ready to render.
const cards = await pkg.getCardsWithNotes();
for (const card of cards) {
console.log(card.deckName, card.note.fields.Front, '→', card.note.fields.Back);
}
} finally {
await pkg.close(); // releases the DB handle and deletes extracted files
}Keeping media around for rendering
By default close() deletes the extracted directory. If you need the media
files (images/audio) while rendering, open with cleanupOnClose: false and
clean up yourself later.
const pkg = await openApkg(ankiAdapters, path, { cleanupOnClose: false });
const audioPath = await pkg.resolveMediaPath('hello.mp3'); // absolute path on diskTemporary files & cleanup
Reading a deck requires unzipping it to disk first. This library treats that extraction as temporary:
It extracts into your fs adapter's
cacheDir(on iOS/Android the OS automatically reclaims this directory under storage pressure).close()deletes the extraction (unless you passedcleanupOnClose: false).Always call
close()in afinallyblock so cleanup runs even if reading throws:const pkg = await openApkg(ankiAdapters, path); try { /* read */ } finally { await pkg.close(); }Self-healing:
openApkgremoves any leftover extraction for the same deck before extracting fresh, and if opening fails partway it cleans up after itself. So a previous crash won't leave a growing pile of files.clearCache(adapters)removes any straggler extractions this library created. Safe to call at app startup as belt-and-suspenders housekeeping (requires the optionallistDirfs method):import { clearCache } from 'react-native-anki-reader'; await clearCache(ankiAdapters); // e.g. on app launch
API
openApkg(adapters, apkgPath, options?) → Promise<AnkiPackage>
clearCache(adapters) → Promise<number> — remove straggler extractions; returns how many were removed.
The AnkiPackage instance exposes:
getDecks()→AnkiDeck[]getModels()→AnkiModel[](note types, with fields + templates)getNotes()→AnkiNote[](fields split and mapped by name)getCards()→AnkiCard[](raw scheduling data)getCardsWithNotes()→ResolvedCard[](cards + note + model + template + deck name)getMedia()→MediaMap({ "0": "hello.mp3", ... })resolveMediaPath(originalName)→string | nullrawQuery(sql, params?)→ run your own read query against the collectionclose()→ release resources
All types are exported and fully documented.
Notes & limitations
.apkgis just a ZIP. Inside is a SQLite database namedcollection.anki2(legacy) orcollection.anki21(Anki 2.1+), plus numbered media files and amediaJSON mapping.collection.anki21bis not supported. Very recent Anki versions can export a zstd-compressed database. If a package contains only that file, the reader throwsUnsupportedCollectionError. Re-export the deck with "Support older Anki versions" enabled to produce a readable.anki21.- Field separator. Anki packs a note's fields into one string separated by
the ASCII Unit Separator (
0x1f); this library splits and names them for you.
Node / testing
The same adapters work on Node. The repository's test/nodeAdapters.ts uses
node:sqlite and fflate and doubles as a reference implementation.
License
MIT
