# Copyright: Damien Elmes # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from aqt.qt import * import os, types, socket, time, traceback, gc import aqt from anki import Collection from anki.sync import Syncer, RemoteServer, FullSyncer, MediaSyncer, \ RemoteMediaServer from anki.hooks import addHook, removeHook from aqt.utils import tooltip, askUserDialog # Sync manager ###################################################################### # are we doing this in main? # self.closeAllDeckWindows() class SyncManager(QObject): def __init__(self, mw, pm): QObject.__init__(self, mw) self.mw = mw self.pm = pm def sync(self, auto=False): if not self.pm.profile['syncKey']: if auto: return auth = self._getUserPass() if not auth: return self._sync(auth) else: self._sync() def _sync(self, auth=None): # to avoid gui widgets being garbage collected in the worker thread, # run gc in advance gc.collect() # create the thread, setup signals and start running t = self.thread = SyncThread( self.pm.collectionPath(), self.pm.profile['syncKey'], auth) print "created thread" self.connect(t, SIGNAL("event"), self.onEvent) self.mw.progress.start(immediate=True, label=_("Connecting...")) print "starting thread" self.thread.start() while not self.thread.isFinished(): self.mw.app.processEvents() self.thread.wait(100) print "finished" self.mw.progress.finish() def onEvent(self, evt, *args): if evt == "badAuth": return tooltip( _("AnkiWeb ID or password was incorrect; please try again."), parent=self.mw) elif evt == "newKey": self.pm.profile['syncKey'] = args[0] self.pm.save() print "saved hkey" elif evt == "sync": self.mw.progress.update(label="sync: "+args[0]) elif evt == "mediaSync": self.mw.progress.update(label="media: "+args[0]) elif evt == "error": print "error occurred", args[0] elif evt == "clockOff": print "clock is wrong" elif evt == "noChanges": print "no changes found" elif evt == "fullSync": self._confirmFullSync() elif evt == "success": print "sync successful" elif evt == "upload": print "upload successful" elif evt == "download": print "download successful" elif evt == "noMediaChanges": print "no media changes" elif evt == "mediaSuccess": print "media sync successful" else: print "unknown evt", evt def _getUserPass(self): d = QDialog(self.mw) d.setWindowTitle("Anki") vbox = QVBoxLayout() l = QLabel(_("""\

Account Required

A free account is required to keep your collection synchronized. Please \ sign up for an account, then \ enter your details below.""")) l.setOpenExternalLinks(True) l.setWordWrap(True) vbox.addWidget(l) vbox.addSpacing(20) g = QGridLayout() l1 = QLabel(_("AnkiWeb ID:")) g.addWidget(l1, 0, 0) user = QLineEdit() g.addWidget(user, 0, 1) l2 = QLabel(_("Password:")) g.addWidget(l2, 1, 0) passwd = QLineEdit() passwd.setEchoMode(QLineEdit.Password) g.addWidget(passwd, 1, 1) vbox.addLayout(g) bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) bb.button(QDialogButtonBox.Ok).setAutoDefault(True) self.connect(bb, SIGNAL("accepted()"), d.accept) self.connect(bb, SIGNAL("rejected()"), d.reject) vbox.addWidget(bb) d.setLayout(vbox) d.show() d.exec_() u = user.text() p = passwd.text() if not u or not p: return return (u, p) def _confirmFullSync(self): diag = askUserDialog(_("""\ Because this is your first time synchronizing, or because unmergable \ changes have been made, your collection needs to be either uploaded or \ downloaded in full. Do you want to keep the local version, overwriting the AnkiWeb version? Or \ do you want to keep the AnkiWeb version, overwriting the version here?"""), [_("Keep Local"), _("Keep AnkiWeb"), _("Cancel")]) diag.setDefault(2) ret = diag.run() if ret == _("Keep Local"): self.thread.fullSyncChoice = "upload" elif ret == _("Keep AnkiWeb"): self.thread.fullSyncChoice = "download" else: self.thread.fullSyncChoice = "cancel" def syncClockOff(self, diff): showWarning( _("The time or date on your computer is not correct.\n") + ngettext("It is off by %d second.\n\n", "It is off by %d seconds.\n\n", diff) % diff + _("Since this can cause many problems with syncing,\n" "syncing is disabled until you fix the problem.") ) self.onSyncFinished() def badUserPass(self): aqt.preferences.Preferences(self, self.pm.profile).dialog.tabWidget.\ setCurrentIndex(1) # Sync thread ###################################################################### class SyncThread(QThread): def __init__(self, path, hkey, auth=None): QThread.__init__(self) self.path = path self.hkey = hkey self.auth = auth def run(self): self.col = Collection(self.path) self.server = RemoteServer(self.hkey) self.client = Syncer(self.col, self.server) def syncEvent(type): self.fireEvent("sync", type) def mediaSync(type): self.fireEvent("mediaSync", type) addHook("sync", syncEvent) addHook("mediaSync", mediaSync) # run sync and catch any errors try: self._sync() except Exception, e: print e self.fireEvent("error", unicode(e)) finally: # don't bump mod time unless we explicitly save self.col.close(save=False) def _sync(self): if self.auth: # need to authenticate and obtain host key hkey = self.server.hostKey(*self.auth) print "hkey was", hkey if not hkey: # provided details were invalid return self.fireEvent("badAuth") else: # write new details and tell calling thread to save self.fireEvent("newKey", hkey) # run sync and check state ret = self.client.sync() if ret == "badAuth": return self.fireEvent("badAuth") elif ret == "clockOff": return self.fireEvent("clockOff") # note mediaUSN for later self.mediaUsn = self.client.mediaUsn # full sync? if ret == "fullSync": return self._fullSync() # save and note success state self.col.save() if ret == "noChanges": self.fireEvent("noChanges") else: self.fireEvent("success") # then move on to media sync self._syncMedia() def _fullSync(self): # tell the calling thread we need a decision on sync direction, and # wait for a reply self.fullSyncChoice = False self.fireEvent("fullSync") while not self.fullSyncChoice: time.sleep(0.1) f = self.fullSyncChoice if f == "cancel": return self.client = FullSyncer(self.col, self.hkey, self.server.con) if f == "upload": self.client.upload() self.fireEvent("upload") else: self.client.download() self.fireEvent("download") # move on to media sync self._syncMedia() def _syncMedia(self): self.server = RemoteMediaServer(self.hkey, self.server.con) self.client = MediaSyncer(self.col, self.server) ret = self.client.sync(self.mediaUsn) if ret == "noChanges": self.fireEvent("noMediaChanges") else: self.fireEvent("mediaSuccess") def fireEvent(self, *args): self.emit(SIGNAL("event"), *args)