deck and packaged deck export

This commit is contained in:
Damien Elmes 2012-02-26 01:44:10 +09:00
parent 8539c081b3
commit cd5dfa2116
2 changed files with 171 additions and 120 deletions

View file

@ -2,13 +2,12 @@
# Copyright: Damien Elmes <anki@ichi2.net> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # 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 operator import itemgetter
from anki.cards import Card from anki.cards import Card
from anki.lang import _ from anki.lang import _
from anki.utils import stripHTML, ids2str, splitFields from anki.utils import stripHTML, ids2str, splitFields
from anki import Collection
# remove beautifulsoup dependency
class Exporter(object): class Exporter(object):
def __init__(self, col, did=None): def __init__(self, col, did=None):
@ -39,46 +38,44 @@ class Exporter(object):
###################################################################### ######################################################################
class TextCardExporter(Exporter): class TextCardExporter(Exporter):
pass
key = _("Text files (*.txt)") # key = _("Text files (*.txt)")
ext = ".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): # def tags(self, id):
Exporter.__init__(self, col) # return "\t" + ", ".join(parseTags(self.cardTags[id]))
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]))
# Notes as TSV # Notes as TSV
###################################################################### ######################################################################
class TextNoteExporter(Exporter): class TextNoteExporter(Exporter):
key = _("Text files (*.txt)") key = _("Notes in Plain Text")
ext = ".txt" ext = ".txt"
def __init__(self, col): def __init__(self, col):
@ -108,94 +105,133 @@ where cards.id in %s)""" % ids2str(cardIds)):
out = "\n".join(data) out = "\n".join(data)
file.write(out.encode("utf-8")) file.write(out.encode("utf-8"))
def tags(self, id): # Anki decks
if self.includeTags:
return "\t" + self.noteTags[id]
return ""
# Anki collection exporter
###################################################################### ######################################################################
# media files are stored in self.mediaFiles, but not exported.
class AnkiExporter(Exporter): class AnkiExporter(Exporter):
key = _("Anki Collection (*.anki)") key = _("Anki 2.0 Deck")
ext = ".anki" ext = ".anki2"
def __init__(self, col): def __init__(self, col):
Exporter.__init__(self, col) Exporter.__init__(self, col)
self.includeSchedulingInfo = False self.includeSched = False
self.includeMedia = True self.includeMedia = True
def exportInto(self, path): def exportInto(self, path):
n = 3 # create a new collection at the target
if not self.includeSchedulingInfo:
n += 1
try: try:
os.unlink(path) os.unlink(path)
except (IOError, OSError): except (IOError, OSError):
pass pass
self.newCol = DeckStorage.Deck(path) self.dst = Collection(path)
client = SyncClient(self.col) self.src = self.col
server = SyncServer(self.newDeck) # find cards
client.setServer(server) if not self.did:
client.localTime = self.col.modified cids = self.src.db.list("select id from cards")
client.remoteTime = 0 else:
self.col.db.flush() cids = self.src.decks.cids(self.did, children=True)
# set up a custom change list and sync # copy cards, noting used nids
lsum = self.localSummary() nids = {}
rsum = server.summary(0) data = []
payload = client.genPayload((lsum, rsum)) for row in self.src.db.execute(
res = server.applyPayload(payload) "select * from cards where id in "+ids2str(cids)):
if not self.includeSchedulingInfo: nids[row[1]] = True
self.newCol.resetCards() data.append(row)
# media 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: if self.includeMedia:
server.col.mediaPrefix = "" for row in notedata:
copyLocalMedia(client.col, server.col) flds = row[7]
# need to save manually mid = row[2]
self.newCol.rebuildCounts() for file in self.src.media.filesInStr(mid, flds):
# FIXME media[file] = True
#self.exportedCards = self.newCol.cardCount self.mediaFiles = media.keys()
self.newCol.crt = 0 # todo: tags?
self.newCol.db.commit() self.dst.setMod()
self.newCol.close() self.dst.close()
def localSummary(self): # Packaged Anki decks
cardIds = self.cardIds() ######################################################################
cStrIds = ids2str(cardIds)
cards = self.col.db.all(""" class AnkiPackageExporter(AnkiExporter):
select id, modified from cards
where id in %s""" % cStrIds) key = _("Packaged Anki Deck")
notes = self.col.db.all(""" ext = ".apkg"
select notes.id, notes.modified from cards, notes where
notes.id = cards.noteId and def __init__(self, col):
cards.id in %s""" % cStrIds) AnkiExporter.__init__(self, col)
models = self.col.db.all("""
select models.id, models.modified from models, notes where def exportInto(self, path):
notes.modelId = models.id and # export into the anki2 file
notes.id in %s""" % ids2str([f[0] for f in notes])) colfile = path.replace(".apkg", ".anki2")
media = self.col.db.all(""" AnkiExporter.exportInto(self, colfile)
select id, modified from media""") # zip the deck up
return { z = zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED)
# cards z.write(colfile, "collection.anki2")
"cards": cards, # and media
"delcards": [], media = {}
# notes for c, file in enumerate(self.mediaFiles):
"notes": notes, c = str(c)
"delnotes": [], z.write(file, c)
# models media[c] = file
"models": models, # media map
"delmodels": [], z.writestr("media", simplejson.dumps(media))
# media z.close()
"media": media,
"delmedia": [],
}
# Export modules # Export modules
########################################################################## ##########################################################################
def exporters(): def exporters():
def id(obj):
return "%s (*%s)" % (obj.key, obj.ext)
return ( return (
(_("Anki Deck (*.anki)"), AnkiExporter), id(TextNoteExporter),
(_("Cards in tab-separated text file (*.txt)"), TextCardExporter), id(AnkiPackageExporter),
(_("Notes in tab-separated text file (*.txt)"), TextNoteExporter)) )

View file

@ -17,39 +17,51 @@ def setup1():
f = deck.newNote() f = deck.newNote()
f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = ["tag", "tag2"] f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = ["tag", "tag2"]
deck.addNote(f) deck.addNote(f)
# with a different deck
f = deck.newNote() f = deck.newNote()
f['Front'] = u"baz"; f['Back'] = u"qux" f['Front'] = u"baz"; f['Back'] = u"qux"
f.did = deck.decks.id("new deck")
deck.addNote(f) deck.addNote(f)
########################################################################## ##########################################################################
@nose.with_setup(setup1) @nose.with_setup(setup1)
def test_export_anki(): def test_export_anki():
oldTime = deck.modified
e = AnkiExporter(deck) e = AnkiExporter(deck)
newname = unicode(tempfile.mkstemp(prefix="ankitest")[1]) newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".anki2")[1])
os.unlink(newname) os.unlink(newname)
e.exportInto(newname) e.exportInto(newname)
assert deck.modified == oldTime
# connect to new deck # connect to new deck
d2 = aopen(newname, backup=False) d2 = aopen(newname)
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)
assert d2.cardCount() == 2 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) @nose.with_setup(setup1)
def test_export_textcard(): def test_export_ankipkg():
e = TextCardExporter(deck) # add a test file to the media folder
f = unicode(tempfile.mkstemp(prefix="ankitest")[1]) open(os.path.join(deck.media.dir(), u"今日.mp3"), "w").write("test")
os.unlink(f) n = deck.newNote()
e.exportInto(f) n['Front'] = u'[sound:今日.mp3]'
e.includeTags = True deck.addNote(n)
e.exportInto(f) 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) @nose.with_setup(setup1)
def test_export_textnote(): def test_export_textnote():
@ -59,3 +71,6 @@ def test_export_textnote():
e.exportInto(f) e.exportInto(f)
e.includeTags = True e.includeTags = True
e.exportInto(f) e.exportInto(f)
def test_exporters():
assert "*.apkg" in str(exporters())