From cd5dfa21165584dd2d02339d82feb451670f2050 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 26 Feb 2012 01:44:10 +0900 Subject: [PATCH] deck and packaged deck export --- anki/exporting.py | 240 +++++++++++++++++++++++----------------- tests/test_exporting.py | 51 ++++++--- 2 files changed, 171 insertions(+), 120 deletions(-) diff --git a/anki/exporting.py b/anki/exporting.py index 33706426f..c78c6cc04 100644 --- a/anki/exporting.py +++ b/anki/exporting.py @@ -2,13 +2,12 @@ # Copyright: Damien Elmes # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import itertools, time, re, os, HTMLParser +import itertools, time, re, os, HTMLParser, zipfile, simplejson from operator import itemgetter from anki.cards import Card from anki.lang import _ from anki.utils import stripHTML, ids2str, splitFields - -# remove beautifulsoup dependency +from anki import Collection class Exporter(object): def __init__(self, col, did=None): @@ -39,46 +38,44 @@ class Exporter(object): ###################################################################### class TextCardExporter(Exporter): + pass - key = _("Text files (*.txt)") - ext = ".txt" +# key = _("Text files (*.txt)") +# ext = ".txt" - # add option to strip html +# def __init__(self, col): +# Exporter.__init__(self, col) +# def doExport(self, file): +# ids = self.cardIds() +# strids = ids2str(ids) +# cards = self.col.db.all(""" +# select cards.question, cards.answer, cards.id from cards +# where cards.id in %s +# order by cards.created""" % strids) +# self.cardTags = dict(self.col.db.all(""" +# select cards.id, notes.tags from cards, notes +# where cards.noteId = notes.id +# and cards.id in %s +# order by cards.created""" % strids)) +# out = u"\n".join(["%s\t%s%s" % ( +# self.escapeText(c[0], removeFields=True), +# self.escapeText(c[1], removeFields=True), +# self.tags(c[2])) +# for c in cards]) +# if out: +# out += "\n" +# file.write(out.encode("utf-8")) - def __init__(self, col): - Exporter.__init__(self, col) - - def doExport(self, file): - ids = self.cardIds() - strids = ids2str(ids) - cards = self.col.db.all(""" -select cards.question, cards.answer, cards.id from cards -where cards.id in %s -order by cards.created""" % strids) - self.cardTags = dict(self.col.db.all(""" -select cards.id, notes.tags from cards, notes -where cards.noteId = notes.id -and cards.id in %s -order by cards.created""" % strids)) - out = u"\n".join(["%s\t%s%s" % ( - self.escapeText(c[0], removeFields=True), - self.escapeText(c[1], removeFields=True), - self.tags(c[2])) - for c in cards]) - if out: - out += "\n" - file.write(out.encode("utf-8")) - - def tags(self, id): - return "\t" + ", ".join(parseTags(self.cardTags[id])) +# def tags(self, id): +# return "\t" + ", ".join(parseTags(self.cardTags[id])) # Notes as TSV ###################################################################### class TextNoteExporter(Exporter): - key = _("Text files (*.txt)") + key = _("Notes in Plain Text") ext = ".txt" def __init__(self, col): @@ -108,94 +105,133 @@ where cards.id in %s)""" % ids2str(cardIds)): out = "\n".join(data) file.write(out.encode("utf-8")) - def tags(self, id): - if self.includeTags: - return "\t" + self.noteTags[id] - return "" - -# Anki collection exporter +# Anki decks ###################################################################### +# media files are stored in self.mediaFiles, but not exported. class AnkiExporter(Exporter): - key = _("Anki Collection (*.anki)") - ext = ".anki" + key = _("Anki 2.0 Deck") + ext = ".anki2" def __init__(self, col): Exporter.__init__(self, col) - self.includeSchedulingInfo = False + self.includeSched = False self.includeMedia = True def exportInto(self, path): - n = 3 - if not self.includeSchedulingInfo: - n += 1 + # create a new collection at the target try: os.unlink(path) except (IOError, OSError): pass - self.newCol = DeckStorage.Deck(path) - client = SyncClient(self.col) - server = SyncServer(self.newDeck) - client.setServer(server) - client.localTime = self.col.modified - client.remoteTime = 0 - self.col.db.flush() - # set up a custom change list and sync - lsum = self.localSummary() - rsum = server.summary(0) - payload = client.genPayload((lsum, rsum)) - res = server.applyPayload(payload) - if not self.includeSchedulingInfo: - self.newCol.resetCards() - # media + self.dst = Collection(path) + self.src = self.col + # find cards + if not self.did: + cids = self.src.db.list("select id from cards") + else: + cids = self.src.decks.cids(self.did, children=True) + # copy cards, noting used nids + nids = {} + data = [] + for row in self.src.db.execute( + "select * from cards where id in "+ids2str(cids)): + nids[row[1]] = True + data.append(row) + self.dst.db.executemany( + "insert into cards values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", + data) + # notes + strnids = ids2str(nids.keys()) + notedata = self.src.db.all("select * from notes where id in "+ + strnids) + self.dst.db.executemany( + "insert into notes values (?,?,?,?,?,?,?,?,?,?,?,?)", + notedata) + # models used by the notes + mids = self.dst.db.list("select distinct mid from notes where id in "+ + strnids) + # card history and revlog + if self.includeSched: + data = self.src.db.all( + "select * from revlog where cid in "+ids2str(cids)) + self.dst.db.executemany( + "insert into revlog values (?,?,?,?,?,?,?,?,?)", + data) + else: + # need to reset card state + self.dst.sched.forgetCards(cids) + # models + for m in self.src.models.all(): + if m['id'] in mids: + self.dst.models.update(m) + # decks + if not self.did: + dids = [] + else: + dids = [self.did] + self.src.decks.children(self.did) + dconfs = {} + for d in self.src.decks.all(): + if d['id'] == 1: + continue + if dids and d['id'] not in dids: + continue + if d['conf'] != 1: + dconfs[d['conf']] = True + self.dst.decks.update(d) + # copy used deck confs + for dc in self.src.decks.allConf(): + if dc['id'] in dconfs: + self.dst.decks.updateConf(dc) + # find used media + media = {} if self.includeMedia: - server.col.mediaPrefix = "" - copyLocalMedia(client.col, server.col) - # need to save manually - self.newCol.rebuildCounts() - # FIXME - #self.exportedCards = self.newCol.cardCount - self.newCol.crt = 0 - self.newCol.db.commit() - self.newCol.close() + for row in notedata: + flds = row[7] + mid = row[2] + for file in self.src.media.filesInStr(mid, flds): + media[file] = True + self.mediaFiles = media.keys() + # todo: tags? + self.dst.setMod() + self.dst.close() - def localSummary(self): - cardIds = self.cardIds() - cStrIds = ids2str(cardIds) - cards = self.col.db.all(""" -select id, modified from cards -where id in %s""" % cStrIds) - notes = self.col.db.all(""" -select notes.id, notes.modified from cards, notes where -notes.id = cards.noteId and -cards.id in %s""" % cStrIds) - models = self.col.db.all(""" -select models.id, models.modified from models, notes where -notes.modelId = models.id and -notes.id in %s""" % ids2str([f[0] for f in notes])) - media = self.col.db.all(""" -select id, modified from media""") - return { - # cards - "cards": cards, - "delcards": [], - # notes - "notes": notes, - "delnotes": [], - # models - "models": models, - "delmodels": [], - # media - "media": media, - "delmedia": [], - } +# Packaged Anki decks +###################################################################### + +class AnkiPackageExporter(AnkiExporter): + + key = _("Packaged Anki Deck") + ext = ".apkg" + + def __init__(self, col): + AnkiExporter.__init__(self, col) + + def exportInto(self, path): + # export into the anki2 file + colfile = path.replace(".apkg", ".anki2") + AnkiExporter.exportInto(self, colfile) + # zip the deck up + z = zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) + z.write(colfile, "collection.anki2") + # and media + media = {} + for c, file in enumerate(self.mediaFiles): + c = str(c) + z.write(file, c) + media[c] = file + # media map + z.writestr("media", simplejson.dumps(media)) + z.close() # Export modules ########################################################################## def exporters(): + def id(obj): + return "%s (*%s)" % (obj.key, obj.ext) return ( - (_("Anki Deck (*.anki)"), AnkiExporter), - (_("Cards in tab-separated text file (*.txt)"), TextCardExporter), - (_("Notes in tab-separated text file (*.txt)"), TextNoteExporter)) + id(TextNoteExporter), + id(AnkiPackageExporter), + ) diff --git a/tests/test_exporting.py b/tests/test_exporting.py index cb05fbe2c..571423cf2 100644 --- a/tests/test_exporting.py +++ b/tests/test_exporting.py @@ -17,39 +17,51 @@ def setup1(): f = deck.newNote() f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = ["tag", "tag2"] deck.addNote(f) + # with a different deck f = deck.newNote() f['Front'] = u"baz"; f['Back'] = u"qux" + f.did = deck.decks.id("new deck") deck.addNote(f) ########################################################################## @nose.with_setup(setup1) def test_export_anki(): - oldTime = deck.modified e = AnkiExporter(deck) - newname = unicode(tempfile.mkstemp(prefix="ankitest")[1]) + newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".anki2")[1]) os.unlink(newname) e.exportInto(newname) - assert deck.modified == oldTime # connect to new deck - d2 = aopen(newname, backup=False) - assert d2.cardCount() == 4 - # try again, limited to a tag - newname = unicode(tempfile.mkstemp(prefix="ankitest")[1]) - os.unlink(newname) - e.limitTags = ['tag'] - e.exportInto(newname) - d2 = aopen(newname, backup=False) + d2 = aopen(newname) assert d2.cardCount() == 2 + # try again, limited to a deck + newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".anki2")[1]) + os.unlink(newname) + e.did = 1 + e.exportInto(newname) + d2 = aopen(newname) + assert d2.cardCount() == 1 @nose.with_setup(setup1) -def test_export_textcard(): - e = TextCardExporter(deck) - f = unicode(tempfile.mkstemp(prefix="ankitest")[1]) - os.unlink(f) - e.exportInto(f) - e.includeTags = True - e.exportInto(f) +def test_export_ankipkg(): + # add a test file to the media folder + open(os.path.join(deck.media.dir(), u"今日.mp3"), "w").write("test") + n = deck.newNote() + n['Front'] = u'[sound:今日.mp3]' + deck.addNote(n) + e = AnkiPackageExporter(deck) + newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".apkg")[1]) + os.unlink(newname) + e.exportInto(newname) + +# @nose.with_setup(setup1) +# def test_export_textcard(): +# e = TextCardExporter(deck) +# f = unicode(tempfile.mkstemp(prefix="ankitest")[1]) +# os.unlink(f) +# e.exportInto(f) +# e.includeTags = True +# e.exportInto(f) @nose.with_setup(setup1) def test_export_textnote(): @@ -59,3 +71,6 @@ def test_export_textnote(): e.exportInto(f) e.includeTags = True e.exportInto(f) + +def test_exporters(): + assert "*.apkg" in str(exporters())