mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00

- ankiqt->aqt - move all the files in ui into the top level - move icons_rc.py into aqt/forms, and the qrc into designer/ - remove setuptools
322 lines
12 KiB
Python
Executable file
322 lines
12 KiB
Python
Executable file
# Copyright: Damien Elmes <anki@ichi2.net>
|
|
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
|
|
|
|
from PyQt4.QtGui import *
|
|
from PyQt4.QtCore import *
|
|
import os, types, socket, time, traceback
|
|
import aqt
|
|
import anki
|
|
from anki.sync import SyncClient, HttpSyncServerProxy, copyLocalMedia
|
|
from anki.sync import SYNC_HOST, SYNC_PORT
|
|
from anki.errors import *
|
|
from anki import Deck
|
|
from anki.db import sqlite
|
|
import aqt.forms
|
|
from anki.hooks import addHook, removeHook
|
|
|
|
# Synchronising a deck with a public server
|
|
##########################################################################
|
|
|
|
class Sync(QThread):
|
|
|
|
def __init__(self, parent, user, pwd, interactive, onlyMerge):
|
|
QThread.__init__(self)
|
|
self.parent = parent
|
|
self.interactive = interactive
|
|
self.user = user
|
|
self.pwd = pwd
|
|
self.ok = True
|
|
self.onlyMerge = onlyMerge
|
|
self.proxy = None
|
|
addHook('fullSyncStarted', self.fullSyncStarted)
|
|
addHook('fullSyncFinished', self.fullSyncFinished)
|
|
addHook('fullSyncProgress', self.fullSyncProgress)
|
|
|
|
def setStatus(self, msg, timeout=5000):
|
|
self.emit(SIGNAL("setStatus"), msg, timeout)
|
|
|
|
def run(self):
|
|
if self.parent.syncName:
|
|
self.syncDeck()
|
|
else:
|
|
self.syncAllDecks()
|
|
removeHook('fullSyncStarted', self.fullSyncStarted)
|
|
removeHook('fullSyncFinished', self.fullSyncFinished)
|
|
removeHook('fullSyncProgress', self.fullSyncProgress)
|
|
|
|
def fullSyncStarted(self, max):
|
|
self.emit(SIGNAL("fullSyncStarted"), max)
|
|
|
|
def fullSyncFinished(self):
|
|
self.emit(SIGNAL("fullSyncFinished"))
|
|
|
|
def fullSyncProgress(self, type, val):
|
|
self.emit(SIGNAL("fullSyncProgress"), type, val)
|
|
|
|
def error(self, error):
|
|
if getattr(error, 'data', None) is None:
|
|
error.data = {}
|
|
if error.data.get('type') == 'clockOff':
|
|
pass
|
|
else:
|
|
error = self.getErrorMessage(error)
|
|
self.emit(SIGNAL("showWarning"), error)
|
|
if self.onlyMerge:
|
|
# new file needs cleaning up
|
|
self.emit(SIGNAL("cleanNewDeck"))
|
|
else:
|
|
self.emit(SIGNAL("syncFinished"))
|
|
|
|
def getErrorMessage(self, error):
|
|
if error.data.get('status') == "invalidUserPass":
|
|
msg=_("Please double-check your username/password.")
|
|
self.emit(SIGNAL("badUserPass"))
|
|
elif error.data.get('status') == "oldVersion":
|
|
msg=_("The sync protocol has changed. Please upgrade.")
|
|
elif "busy" in error.data.get('status', ''):
|
|
msg=_("""\
|
|
AnkiWeb is under heavy load at the moment. Please try again in a little while.""")
|
|
elif error.data.get('type') == 'noResponse':
|
|
msg=_("""\
|
|
The server didn't reply. Please try again shortly, and if the problem \
|
|
persists, please report it on the forums.""")
|
|
elif error.data.get('type') == 'connectionError':
|
|
msg=_("""\
|
|
There was a connection error. If it persists, please try disabling your
|
|
firewall software temporarily, or try again from a different network.
|
|
|
|
Debugging info: %s""") % error.data.get("exc", "<none>")
|
|
else:
|
|
tb = traceback.format_exc()
|
|
if "missingFacts" in tb:
|
|
msg=_("""Facts were missing after sync, so the \
|
|
sync was aborted. Please report this error.""")
|
|
else:
|
|
msg=_("Unknown error: %s") % tb
|
|
return msg
|
|
|
|
def connect(self, *args):
|
|
# connect, check auth
|
|
if not self.proxy:
|
|
self.setStatus(_("Connecting..."), 0)
|
|
proxy = HttpSyncServerProxy(self.user, self.pwd)
|
|
proxy.connect("aqt-" + ankiqt.appVersion)
|
|
self.proxy = proxy
|
|
# check clock
|
|
if proxy.timediff > 300:
|
|
self.emit(SIGNAL("syncClockOff"), proxy.timediff)
|
|
raise SyncError(type="clockOff")
|
|
return self.proxy
|
|
|
|
def syncAllDecks(self):
|
|
decks = self.parent.syncDecks
|
|
for d in decks:
|
|
ret = self.syncDeck(deck=d)
|
|
if not ret:
|
|
# failed but not cleaned up
|
|
break
|
|
elif ret == -1:
|
|
# failed and already cleaned up
|
|
return
|
|
elif ret == -2:
|
|
# current deck set not to sync
|
|
continue
|
|
self.setStatus(_("Sync Finished."), 0)
|
|
time.sleep(1)
|
|
self.emit(SIGNAL("syncFinished"))
|
|
|
|
def syncDeck(self, deck=None):
|
|
try:
|
|
if deck:
|
|
# multi-mode setup
|
|
sqlpath = deck.encode("utf-8")
|
|
c = sqlite.connect(sqlpath)
|
|
(syncName, localMod, localSync) = c.execute(
|
|
"select syncName, modified, lastSync from decks").fetchone()
|
|
c.close()
|
|
if not syncName:
|
|
return -2
|
|
syncName = os.path.splitext(os.path.basename(deck))[0]
|
|
path = deck
|
|
else:
|
|
syncName = self.parent.syncName
|
|
path = self.parent.deckPath
|
|
sqlpath = path.encode("utf-8")
|
|
c = sqlite.connect(sqlpath)
|
|
(localMod, localSync) = c.execute(
|
|
"select modified, lastSync from decks").fetchone()
|
|
c.close()
|
|
except Exception, e:
|
|
# we don't know which db library we're using, so do string match
|
|
if "locked" in unicode(e):
|
|
return
|
|
# unknown error
|
|
self.error(e)
|
|
return -1
|
|
# ensure deck mods cached
|
|
try:
|
|
proxy = self.connect()
|
|
except SyncError, e:
|
|
self.error(e)
|
|
return -1
|
|
# exists on server?
|
|
deckCreated = False
|
|
if not proxy.hasDeck(syncName):
|
|
if self.onlyMerge:
|
|
keys = [k for (k,v) in proxy.decks.items() if v[1] != -1]
|
|
self.emit(SIGNAL("noMatchingDeck"), keys)
|
|
self.setStatus("")
|
|
return
|
|
try:
|
|
proxy.createDeck(syncName)
|
|
deckCreated = True
|
|
except SyncError, e:
|
|
self.error(e)
|
|
return -1
|
|
# check conflicts
|
|
proxy.deckName = syncName
|
|
remoteMod = proxy.modified()
|
|
remoteSync = proxy._lastSync()
|
|
minSync = min(localSync, remoteSync)
|
|
self.conflictResolution = None
|
|
if (localMod != remoteMod and minSync > 0 and
|
|
localMod > minSync and remoteMod > minSync):
|
|
self.emit(SIGNAL("syncConflicts"), syncName)
|
|
while not self.conflictResolution:
|
|
time.sleep(0.2)
|
|
if self.conflictResolution == "cancel":
|
|
# alert we're finished early
|
|
self.emit(SIGNAL("syncFinished"))
|
|
return -1
|
|
# reopen
|
|
self.setStatus(_("Syncing <b>%s</b>...") % syncName, 0)
|
|
self.deck = None
|
|
try:
|
|
self.deck = DeckStorage.Deck(path)
|
|
disable = False
|
|
if deck and not self.deck.syncName:
|
|
# multi-mode sync and syncing has been disabled by upgrade
|
|
disable = True
|
|
client = SyncClient(self.deck)
|
|
client.setServer(proxy)
|
|
# need to do anything?
|
|
start = time.time()
|
|
if client.prepareSync(proxy.timediff) and not disable:
|
|
if self.deck.lastSync <= 0:
|
|
if client.remoteTime > client.localTime:
|
|
self.conflictResolution = "keepRemote"
|
|
else:
|
|
self.conflictResolution = "keepLocal"
|
|
changes = True
|
|
# summary
|
|
if not self.conflictResolution and not self.onlyMerge:
|
|
self.setStatus(_("Fetching summary from server..."), 0)
|
|
sums = client.summaries()
|
|
if (self.conflictResolution or
|
|
self.onlyMerge or client.needFullSync(sums)):
|
|
self.setStatus(_("Preparing full sync..."), 0)
|
|
if self.conflictResolution == "keepLocal":
|
|
client.remoteTime = 0
|
|
elif self.conflictResolution == "keepRemote" or self.onlyMerge:
|
|
client.localTime = 0
|
|
lastSync = self.deck.lastSync
|
|
ret = client.prepareFullSync()
|
|
if ret[0] == "fromLocal":
|
|
if not self.conflictResolution:
|
|
if lastSync <= 0 and not deckCreated:
|
|
self.clobberChoice = None
|
|
self.emit(SIGNAL("syncClobber"), syncName)
|
|
while not self.clobberChoice:
|
|
time.sleep(0.2)
|
|
if self.clobberChoice == "cancel":
|
|
# disable syncing on this deck
|
|
c = sqlite.connect(sqlpath)
|
|
c.execute(
|
|
"update decks set syncName = null, "
|
|
"lastSync = 0")
|
|
c.commit()
|
|
c.close()
|
|
if not deck:
|
|
# alert we're finished early
|
|
self.emit(SIGNAL("syncFinished"))
|
|
return True
|
|
self.setStatus(_("Uploading..."), 0)
|
|
client.fullSyncFromLocal(ret[1], ret[2])
|
|
else:
|
|
self.setStatus(_("Downloading..."), 0)
|
|
client.fullSyncFromServer(ret[1], ret[2])
|
|
self.setStatus(_("Sync complete."), 0)
|
|
else:
|
|
# diff
|
|
self.setStatus(_("Determining differences..."), 0)
|
|
payload = client.genPayload(sums)
|
|
# send payload
|
|
if not deck:
|
|
pr = client.payloadChangeReport(payload)
|
|
self.setStatus("<br>" + pr + "<br>", 0)
|
|
self.setStatus(_("Transferring payload..."), 0)
|
|
res = client.server.applyPayload(payload)
|
|
# apply reply
|
|
self.setStatus(_("Applying reply..."), 0)
|
|
client.applyPayloadReply(res)
|
|
# now that both sides have successfully applied, tell
|
|
# server to save, then save local
|
|
client.server.finish()
|
|
self.deck.lastLoaded = self.deck.modified
|
|
self.deck.db.commit()
|
|
self.setStatus(_("Sync complete."))
|
|
else:
|
|
changes = False
|
|
if disable:
|
|
self.setStatus(_("Disabled by upgrade."))
|
|
elif not deck:
|
|
self.setStatus(_("No changes found."))
|
|
# close and send signal to main thread
|
|
self.deck.close()
|
|
if not deck:
|
|
taken = time.time() - start
|
|
if changes and taken < 2.5:
|
|
time.sleep(2.5 - taken)
|
|
else:
|
|
time.sleep(0.25)
|
|
self.emit(SIGNAL("syncFinished"))
|
|
return True
|
|
except Exception, e:
|
|
self.ok = False
|
|
if self.deck:
|
|
self.deck.close()
|
|
self.error(e)
|
|
return -1
|
|
|
|
# Downloading personal decks
|
|
##########################################################################
|
|
|
|
class DeckChooser(QDialog):
|
|
|
|
def __init__(self, parent, decks):
|
|
QDialog.__init__(self, parent, Qt.Window)
|
|
self.parent = parent
|
|
self.decks = decks
|
|
self.dialog = aqt.forms.syncdeck.Ui_DeckChooser()
|
|
self.dialog.setupUi(self)
|
|
self.dialog.topLabel.setText(_("<h1>Download Personal Deck</h1>"))
|
|
self.decks.sort()
|
|
for name in decks:
|
|
name = os.path.splitext(name)[0]
|
|
msg = name
|
|
item = QListWidgetItem(msg)
|
|
self.dialog.decks.addItem(item)
|
|
self.dialog.decks.setCurrentRow(0)
|
|
# the list widget will swallow the enter key
|
|
s = QShortcut(QKeySequence("Return"), self)
|
|
self.connect(s, SIGNAL("activated()"), self.accept)
|
|
self.name = None
|
|
|
|
def getName(self):
|
|
self.exec_()
|
|
return self.name
|
|
|
|
def accept(self):
|
|
idx = self.dialog.decks.currentRow()
|
|
self.name = self.decks[self.dialog.decks.currentRow()]
|
|
self.close()
|