diff --git a/anki/importing/__init__.py b/anki/importing/__init__.py index d6dd7ed13..997694cf6 100644 --- a/anki/importing/__init__.py +++ b/anki/importing/__init__.py @@ -7,6 +7,7 @@ from anki.importing.apkg import AnkiPackageImporter from anki.importing.anki2 import Anki2Importer from anki.importing.anki1 import Anki1Importer from anki.importing.supermemo_xml import SupermemoXmlImporter +from anki.importing.mnemo import MnemosyneImporter from anki.lang import _ Importers = ( diff --git a/anki/importing/mnemo.py b/anki/importing/mnemo.py new file mode 100644 index 000000000..8b8b79103 --- /dev/null +++ b/anki/importing/mnemo.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import time, re +from anki.db import DB +from anki.importing.base import Importer +from anki.importing.noteimp import NoteImporter, ForeignNote, ForeignCard +from anki.utils import checksum, base91 +from anki.stdmodels import addBasicModel + +class MnemosyneImporter(NoteImporter): + + def run(self): + db = DB(self.file) + ver = db.scalar( + "select value from global_variables where key='version'") + assert ver.startswith('Mnemosyne SQL 1') + # gather facts into temp objects + curid = None + notes = {} + note = None + for _id, id, k, v in db.execute(""" +select _id, id, key, value from facts f, data_for_fact d where +f._id=d._fact_id"""): + if id != curid: + if note: + notes[note['_id']] = note + note = {'_id': _id} + curid = id + note[k] = v + if note: + notes[note['_id']] = note + # gather cards + front = [] + frontback = [] + vocabulary = [] + for row in db.execute(""" +select _fact_id, fact_view_id, tags, next_rep, last_rep, easiness, +acq_reps+ret_reps, lapses from cards"""): + # categorize note + note = notes[row[0]] + if row[1] == "1.1": + front.append(note) + elif row[1] == "2.1": + frontback.append(note) + elif row[1] == "3.1": + vocabulary.append(note) + # merge tags into note + tags = row[2].replace(", ", "\x1f").replace(" ", "_") + tags = tags.replace("\x1f", " ") + if "tags" not in note: + note['tags'] = [] + note['tags'] += self.col.tags.split(tags) + note['tags'] = self.col.tags.canonify(note['tags']) + # if it's a new card we can go with the defaults + if row[3] == -1: + continue + # add the card + c = ForeignCard() + c.factor = row[5] + c.reps = row[6] + c.lapses = row[7] + # ivl is inferred in mnemosyne + next, prev = row[3:5] + c.ivl = max(1, (next - prev)/86400) + # work out how long we've got left + rem = int((next - time.time())/86400) + c.due = max(0, self.col.sched.today+rem) + # get ord + m = re.match("\d+\.(\d+)", row[1]) + ord = int(m.group(1))-1 + if 'cards' not in note: + note['cards'] = {} + note['cards'][ord] = c + self._addFronts(front) + self._addFrontBacks(frontback) + self._addVocabulary(vocabulary) + + def fields(self): + return self._fields + + def _addFronts(self, notes, model=None, fields=("f", "b")): + data = [] + for orig in notes: + # create a foreign note object + n = ForeignNote() + n.fields = [] + for f in fields: + n.fields.append(orig.get(f, '')) + n.tags = orig['tags'] + n.cards = orig.get('cards', {}) + data.append(n) + # add a basic model + if not model: + model = addBasicModel(self.col) + model['name'] = "Mnemosyne-FrontOnly" + mm = self.col.models + mm.save(model) + mm.setCurrent(model) + self.model = model + self._fields = len(model['flds']) + self.initMapping() + # import + self.importNotes(data) + + def _addFrontBacks(self, notes): + m = addBasicModel(self.col) + m['name'] = "Mnemosyne-FrontBack" + mm = self.col.models + t = mm.newTemplate("Back") + t['qfmt'] = "{{Back}}" + t['afmt'] = t['qfmt'] + "\n\n
\n\n{{Front}}" + mm.addTemplate(m, t) + self._addFronts(notes, m) + + def _addVocabulary(self, notes): + mm = self.col.models + m = mm.new("Mnemosyne-Vocabulary") + for f in "Expression", "Pronunciation", "Meaning", "Notes": + fm = mm.newField(f) + mm.addField(m, fm) + t = mm.newTemplate("Recognition") + t['qfmt'] = "{{Expression}}" + t['afmt'] = t['qfmt'] + """\n\n
\n\n\ +{{Pronunciation}}
\n{{Meaning}}
\n{{Notes}}""" + mm.addTemplate(m, t) + t = mm.newTemplate("Production") + t['qfmt'] = "{{Meaning}}" + t['afmt'] = t['qfmt'] + """\n\n
\n\n\ +{{Expression}}
\n{{Pronunciation}}
\n{{Notes}}""" + mm.addTemplate(m, t) + mm.add(m) + self._addFronts(notes, m, fields=("f", "p_1", "m_1", "n")) diff --git a/anki/importing/noteimp.py b/anki/importing/noteimp.py index 603af1373..7bdef12ff 100644 --- a/anki/importing/noteimp.py +++ b/anki/importing/noteimp.py @@ -18,6 +18,15 @@ class ForeignNote(object): self.fields = [] self.tags = [] self.deck = None + self.cards = {} # map of ord -> card + +class ForeignCard(object): + def __init__(self): + self.due = 0 + self.ivl = 1 + self.factor = 2.5 + self.reps = 0 + self.lapses = 0 # Base class for CSV and similar text-based imports ###################################################################### @@ -84,6 +93,7 @@ class NoteImporter(Importer): updates = [] new = [] self._ids = [] + self._cards = [] for n in notes: fld0 = n.fields[fld0idx] csum = fieldChecksum(fld0) @@ -122,7 +132,10 @@ class NoteImporter(Importer): self.addNew(new) self.addUpdates(updates) self.col.updateFieldCache(self._ids) + # generate cards assert not self.col.genCards(self._ids) + # apply scheduling updates + self.updateCards() # make sure to update sflds, etc self.total = len(self._ids) @@ -133,6 +146,10 @@ class NoteImporter(Importer): if not self.processFields(n): print "no cards generated" return + # note id for card updates later + for ord, c in n.cards.items(): + self._cards.append((id, ord, c)) + self.col.tags.register(n.tags) return [id, guid64(), self.model['id'], self.didForNote(n), intTime(), self.col.usn(), self.col.tags.join(n.tags), n.fieldsStr, "", "", 0, ""] @@ -148,6 +165,7 @@ class NoteImporter(Importer): if not self.processFields(n): print "no cards generated" return + self.col.tags.register(n.tags) tags = self.col.tags.join(n.tags) return [intTime(), self.col.usn(), n.fieldsStr, tags, id, n.fieldsStr, tags] @@ -178,3 +196,12 @@ where id = ? and (flds != ? or tags != ?)""", rows) fields[sidx] = note.fields[c] note.fieldsStr = joinFields(fields) return self.col.models.availOrds(self.model, note.fieldsStr) + + def updateCards(self): + data = [] + for nid, ord, c in self._cards: + data.append((c.ivl, c.due, c.factor, c.reps, c.lapses, nid, ord)) + # we assume any updated cards are reviews + self.col.db.executemany(""" +update cards set type = 2, queue = 2, ivl = ?, due = ?, +factor = ?, reps = ?, lapses = ? where nid = ? and ord = ?""", data) diff --git a/tests/support/mnemo.db b/tests/support/mnemo.db new file mode 100644 index 000000000..677ed73e5 Binary files /dev/null and b/tests/support/mnemo.db differ diff --git a/tests/test_importing.py b/tests/test_importing.py index d4834fb31..a665acfaa 100644 --- a/tests/test_importing.py +++ b/tests/test_importing.py @@ -6,7 +6,7 @@ from anki.upgrade import Upgrader from anki.utils import ids2str from anki.errors import * from anki.importing import Anki1Importer, Anki2Importer, TextImporter, \ - SupermemoXmlImporter + SupermemoXmlImporter, MnemosyneImporter from anki.notes import Note from anki.db import * @@ -142,3 +142,12 @@ def test_updating(): i.mapping[1] = 0 i.run() deck.close() + +def test_mnemo(): + deck = getEmptyDeck() + file = unicode(os.path.join(testDir, "support/mnemo.db")) + i = MnemosyneImporter(deck, file) + i.run() + assert deck.cardCount() == 7 + assert "a_longer_tag" in deck.tags.all() + assert deck.db.scalar("select count() from cards where type = 0") == 1