Skip to content

Commit

Permalink
updates
Browse files Browse the repository at this point in the history
  • Loading branch information
74Genesis committed Dec 5, 2023
1 parent d9a5bdc commit 9842369
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 29 deletions.
95 changes: 92 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,94 @@
# anki-apkg-parser
## anki-apkg-parser

Node.js library to parse anki decks (.apkg files). Supports parsing notes, cards, media files, custom db queries
> Node.js library to parse anki decks (.apkg files). Supports parsing notes, cards, media files, custom db queries
# documentation coming soon...
### Installation `anki-apkg-parser`

Installation guide.

`coming soon...`
`$ npm i`

### Usage

The library provides two classes `Unpack` and `Deck`

Anki decks are compressed archives, so we should unpack it first using `Unpack` class:

```typescript
import { Unpack, Deck } from 'anki-apkg-parser';

const deckPath = './favorite-cards.apkg';
const outPath = './deck-folder';

try {
const unpack = new Unpack();
// pass the deck path and the output path for unpacking the deck
await unpack.unpack(deckPath, outPath);
}
...
```

Some files you can see after unpacking the deck:

`0`, `1`, `2` - files with number names are media files (images, audio, video)
`collection.anki2` - old version of anki database
`collection.anki21` - more modern version of database
`collection.anki21b` - the latest databse version
`media` - Associative list of media files. Their numbers and real names. (Protocol Buffer or json file)
`meta` - Meta data (Protocol Buffer or json file)

Quick usage of `Deck` class:

```typescript
import { Unpack, Deck } from 'anki-apkg-parser';

// create deck instance using path to unpacked deck
const outPath = './deck-folder';
const deck = new Deck(outPath);


try {
// open the database
const db = await deck.dbOpen();

// fetch list of all notes
await db.getNotes();

// fetch colleciton raw
await db.getCollection();


/**
* Anki deck contains sqlite databases
* so when you call dbOpen(), you will recieve an instance of sqlite library
* You are free to use any 'sqlite' api
*/

// make an SQL request by native sqlite api
await db.get('SELECT * FROM col');
}
...
```

### Advanced usage

TODO: descripbe this part

```typescript
import { Unpack, Deck } from 'anki-apkg-parser';

const outPath = './deck-folder';

try {
const outPath = './deck-folder';
const deck = new Deck(outPath);

let db;

// get cards from anki2 db file
if (deck.anki2) db = await deck.anki2.open()
db.get('SELECT * FROM cards')
}
...
```
28 changes: 21 additions & 7 deletions src/core/Deck.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { Database } from 'sqlite';
import protobuf from 'protobufjs';
import { DB_FILES } from '../constants.js';
import * as fs from 'fs';
import * as path from 'path';
import sqlite3 from 'sqlite3';
import { fileURLToPath } from 'url';
import DbNotFoundError from './errors/DbNotFoundError.js';
import Anki21bDb from './db/Anki21bDb.js';
import Anki21Db from './db/Anki21Db.js';
import Anki2Db from './db/Anki2Db.js';
import Db from './db/Db.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default class Deck {
public anki2: Database | null = null; // oldest db version
public anki21: Database | null = null; // old db version
public anki21b: Database | null = null; // latest db version
public anki2: Db | null = null; // oldest db version
public anki21: Db | null = null; // old db version
public anki21b: Db | null = null; // latest db version

folder: string = '';

Expand All @@ -30,7 +29,10 @@ export default class Deck {
*/
private setDatabases() {
let file = path.join(this.folder, DB_FILES.anki21b);
if (fs.existsSync(file)) this.anki21b = new Anki21bDb(file);
if (fs.existsSync(file)) {
this.anki21b = new Anki21bDb(file);
this.anki21b.open();
}

file = path.join(this.folder, DB_FILES.anki21);
if (fs.existsSync(file)) this.anki21b = new Anki21Db(file);
Expand All @@ -42,12 +44,24 @@ export default class Deck {
/**
* Detect and return current db version used by anki
*/
public get db(): Database | null {
private get db(): Db | null {
if (this.anki21b) return this.anki21b;
if (this.anki21) return this.anki21;
return this.anki2;
}

/**
* Open and return main database
* @returns current database
*/
public async dbOpen(): Promise<Db> {
if (!this.db) throw new DbNotFoundError('Database not found');

await this.db.open();

return this.db;
}

// TODO: read meta file first, to understand version of the deck and how to read media file.
/**
* Returns json with media files list
Expand Down
11 changes: 6 additions & 5 deletions src/core/Unpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as child_process from 'child_process';
import unzipit from 'unzipit';
// import sqlite3 from 'sqlite3';
//@ts-ignore
import { fileURLToPath } from 'url';
// import { DB_FILES } from '../constants.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Expand Down Expand Up @@ -37,7 +34,7 @@ export default class Unpack {
async unpack(p: string, o: string): Promise<void> {
if (!fs.existsSync(p)) throw new Error('Deck file not found in: ' + path);

this.createTempDir(o);
this.createDir(o);

const buf = fs.readFileSync(p);
const { entries } = await unzipit.unzip(new Uint8Array(buf));
Expand Down Expand Up @@ -71,7 +68,11 @@ export default class Unpack {
}
}

private createTempDir(path: string) {
/**
* Creates new dir if it doesn't exists
* @param path folder path
*/
private createDir(path: string) {
try {
if (!fs.existsSync(path)) {
fs.mkdirSync(path, { recursive: true });
Expand Down
32 changes: 18 additions & 14 deletions test/parse.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import test from 'ava';
import { Unpack, AnkiDb } from 'anki-apkg-parser';
import { Unpack, Deck } from 'anki-apkg-parser';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { new_deck, legacy_deck } from './mocks/decks.js';
Expand All @@ -20,20 +20,24 @@ test('Invalid file', async (t) => {
});

test('Get Notes from new deck', async (t) => {
const deck = __dirname + '/decks/new_deck.apkg';
const deckpath = __dirname + '/decks/new_deck.apkg';
const temp = __dirname + '/temp/';

if (fs.existsSync(temp)) fs.rmSync(temp, { recursive: true });

const p = new Unpack();
await p.unpack(deck, temp);

const db = new AnkiDb(temp);
await db.open();

const res = await db.getNotes();
console.log(res);
t.truthy(p);
await p.unpack(deckpath, temp);

const deck = new Deck(temp);
try {
const db = await deck.dbOpen();
const res = await db.getNotes();

console.log(res);
t.truthy(p);
} catch (e) {
t.fail();
}
});

test('Get Notes from old deck', async (t) => {
Expand All @@ -45,7 +49,7 @@ test('Get Notes from old deck', async (t) => {
const p = new Unpack();
await p.unpack(deck, temp);

const db = new AnkiDb(temp);
const db = new Deck(temp);
await db.open();

const res = await db.getNotes();
Expand All @@ -62,7 +66,7 @@ test('Get Media legacy', async (t) => {
const p = new Unpack();
await p.unpack(deck, temp);

const db = new AnkiDb(temp);
const db = new Deck(temp);
await db.open();

const res = await db.getMedia();
Expand All @@ -78,7 +82,7 @@ test('Get Media new deck', async (t) => {
const p = new Unpack();
await p.unpack(deck, temp);

const db = new AnkiDb(temp);
const db = new Deck(temp);
await db.open();

const res = await db.getMedia();
Expand All @@ -94,7 +98,7 @@ test.only('Get Templates', async (t) => {
const p = new Unpack();
await p.unpack(deck, temp);

const db = new AnkiDb(temp);
const db = new Deck(temp);
await db.open();

const res = await db.getMedia();
Expand Down

0 comments on commit 9842369

Please sign in to comment.