anki-apkg-export
Version:
Generate decks for Anki (spaced repetition software)
178 lines (148 loc) • 5.58 kB
JavaScript
import sha1 from 'sha1';
import Zip from 'jszip';
export default class {
constructor(deckName, { template, sql }) {
this.db = new sql.Database();
this.db.run(template);
const now = Date.now();
const topDeckId = this._getId('cards', 'did', now);
const topModelId = this._getId('notes', 'mid', now);
this.deckName = deckName;
this.zip = new Zip();
this.media = [];
this.topDeckId = topDeckId;
this.topModelId = topModelId;
this.separator = '\u001F';
const decks = this._getInitialRowValue('col', 'decks');
const deck = getLastItem(decks);
deck.name = this.deckName;
deck.id = topDeckId;
decks[topDeckId + ''] = deck;
this._update('update col set decks=:decks where id=1', { ':decks': JSON.stringify(decks) });
const models = this._getInitialRowValue('col', 'models');
const model = getLastItem(models);
model.name = this.deckName;
model.did = this.topDeckId;
model.id = topModelId;
models[`${topModelId}`] = model;
this._update('update col set models=:models where id=1', { ':models': JSON.stringify(models) });
}
save(options) {
const { zip, db, media } = this;
const binaryArray = db.export();
const mediaObj = media.reduce((prev, curr, idx) => {
prev[idx] = curr.filename;
return prev;
}, {});
zip.file('collection.anki2', new Buffer(binaryArray));
zip.file('media', JSON.stringify(mediaObj));
media.forEach((item, i) => zip.file(i, item.data));
if (process.env.APP_ENV === 'browser' || typeof window !== 'undefined') {
return zip.generateAsync(Object.assign({}, { type: 'blob' }, options));
} else {
return zip.generateAsync(
Object.assign(
{},
{
type: 'nodebuffer',
base64: false,
compression: 'DEFLATE'
},
options
)
);
}
}
addMedia(filename, data) {
this.media.push({ filename, data });
}
addCard(front, back, { tags } = {}) {
const { topDeckId, topModelId, separator } = this;
const now = Date.now();
const note_guid = this._getNoteGuid(topDeckId, front, back);
const note_id = this._getNoteId(note_guid, now);
let strTags = '';
if (typeof tags === 'string') {
strTags = tags;
} else if (Array.isArray(tags)) {
strTags = this._tagsToStr(tags);
}
this._update('insert or replace into notes values(:id,:guid,:mid,:mod,:usn,:tags,:flds,:sfld,:csum,:flags,:data)', {
':id': note_id, // integer primary key,
':guid': note_guid, // text not null,
':mid': topModelId, // integer not null,
':mod': this._getId('notes', 'mod', now), // integer not null,
':usn': -1, // integer not null,
':tags': strTags, // text not null,
':flds': front + separator + back, // text not null,
':sfld': front, // integer not null,
':csum': this._checksum(front + separator + back), //integer not null,
':flags': 0, // integer not null,
':data': '' // text not null,
});
return this._update(
'insert or replace into cards values(:id,:nid,:did,:ord,:mod,:usn,:type,:queue,:due,:ivl,:factor,:reps,:lapses,:left,:odue,:odid,:flags,:data)',
{
':id': this._getCardId(note_id, now), // integer primary key,
':nid': note_id, // integer not null,
':did': topDeckId, // integer not null,
':ord': 0, // integer not null,
':mod': this._getId('cards', 'mod', now), // integer not null,
':usn': -1, // integer not null,
':type': 0, // integer not null,
':queue': 0, // integer not null,
':due': 179, // integer not null,
':ivl': 0, // integer not null,
':factor': 0, // integer not null,
':reps': 0, // integer not null,
':lapses': 0, // integer not null,
':left': 0, // integer not null,
':odue': 0, // integer not null,
':odid': 0, // integer not null,
':flags': 0, // integer not null,
':data': '' // text not null
}
);
}
_update(query, obj) {
this.db.prepare(query).getAsObject(obj);
}
_getInitialRowValue(table, column = 'id') {
const query = `select ${column} from ${table}`;
return this._getFirstVal(query);
}
_checksum(str) {
return parseInt(sha1(str).substr(0, 8), 16);
}
_getFirstVal(query) {
return JSON.parse(this.db.exec(query)[0].values[0]);
}
_tagsToStr(tags = []) {
return ' ' + tags.map(tag => tag.replace(/ /g, '_')).join(' ') + ' ';
}
_getId(table, col, ts) {
const query = `SELECT ${col} from ${table} WHERE ${col} >= :ts ORDER BY ${col} DESC LIMIT 1`;
const rowObj = this.db.prepare(query).getAsObject({ ':ts': ts });
return rowObj[col] ? +rowObj[col] + 1 : ts;
}
_getNoteId(guid, ts) {
const query = `SELECT id from notes WHERE guid = :guid ORDER BY id DESC LIMIT 1`;
const rowObj = this.db.prepare(query).getAsObject({ ':guid': guid });
return rowObj.id || this._getId('notes', 'id', ts);
}
_getNoteGuid(topDeckId, front, back) {
return sha1(`${topDeckId}${front}${back}`);
}
_getCardId(note_id, ts) {
const query = `SELECT id from cards WHERE nid = :note_id ORDER BY id DESC LIMIT 1`;
const rowObj = this.db.prepare(query).getAsObject({ ':note_id': note_id });
return rowObj.id || this._getId('cards', 'id', ts);
}
}
export const getLastItem = obj => {
const keys = Object.keys(obj);
const lastKey = keys[keys.length - 1];
const item = obj[lastKey];
delete obj[lastKey];
return item;
};