revamped addon downloading

This commit is contained in:
Damien Elmes 2012-04-10 16:17:51 +09:00
parent c8ef15c3d0
commit 79e3984a7a
4 changed files with 216 additions and 263 deletions

View file

@ -2,10 +2,17 @@
# -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import sys, os, re, traceback
import sys, os, re, traceback, time
from cStringIO import StringIO
from aqt.qt import *
from aqt.utils import showInfo, showWarning, openFolder, isWin
from anki.hooks import runHook
from aqt.utils import showInfo, showWarning, openFolder, isWin, openLink
from anki.hooks import runHook, addHook, remHook
from aqt.webview import AnkiWebView
from zipfile import ZipFile
import aqt.forms
import aqt
from anki.sync import httpCon
import aqt.sync # monkey-patches httplib2
class AddonManager(object):
@ -15,6 +22,7 @@ class AddonManager(object):
self.mw.connect(f.actionOpenPluginFolder, s, self.onOpenAddonFolder)
self.mw.connect(f.actionEnableAllPlugins, s, self.onEnableAllAddons)
self.mw.connect(f.actionDisableAllPlugins, s, self.onDisableAllAddons)
self.mw.connect(f.actionDownloadSharedPlugin, s, self.onGetAddons)
if isWin:
self.clearAddonCache()
sys.path.insert(0, self.addonsFolder())
@ -112,3 +120,106 @@ class AddonManager(object):
def registerAddon(self, name, updateId):
# not currently used
return
# Installing add-ons
######################################################################
def onGetAddons(self):
GetAddons(self.mw)
def install(self, data, fname):
if fname.endswith(".py"):
# .py files go directly into the addon folder
path = os.path.join(self.addonsFolder(), fname)
open(path, "w").write(data)
return
# .zip file
z = ZipFile(StringIO(data))
base = self.addonsFolder()
for n in z.namelist():
if n.endswith("/"):
# folder; ignore
continue
# write
z.extract(n, base)
class GetAddons(QDialog):
def __init__(self, mw):
QDialog.__init__(self, mw)
self.mw = mw
self.form = aqt.forms.getaddons.Ui_Dialog()
self.form.setupUi(self)
b = self.form.buttonBox.addButton(
_("Browse"), QDialogButtonBox.ActionRole)
self.connect(b, SIGNAL("clicked()"), self.onBrowse)
self.exec_()
def onBrowse(self):
openLink(aqt.appShared + "addons/")
def accept(self):
try:
code = int(self.form.code.text())
except ValueError:
showWarning(_("Invalid code."))
return
QDialog.accept(self)
# create downloader thread
self.thread = AddonDownloader(code)
self.connect(self.thread, SIGNAL("recv"), self.onRecv)
self.recvBytes = 0
self.thread.start()
self.mw.progress.start(immediate=True)
while not self.thread.isFinished():
self.mw.app.processEvents()
self.thread.wait(100)
if not self.thread.error:
# success
self.mw.addonManager.install(self.thread.data, self.thread.fname)
self.mw.progress.finish()
showInfo(_("Download successful. Please restart Anki."))
else:
self.mw.progress.finish()
showWarning(_("Download failed: %s") % self.thread.error)
def onRecv(self, total):
self.mw.progress.update(label="%dKB downloaded" % (total/1024))
class AddonDownloader(QThread):
def __init__(self, code):
QThread.__init__(self)
self.code = code
def run(self):
# setup progress handler
self.byteUpdate = time.time()
self.recvTotal = 0
def canPost():
if (time.time() - self.byteUpdate) > 0.1:
self.byteUpdate = time.time()
return True
def recvEvent(bytes):
self.recvTotal += bytes
if canPost():
self.emit(SIGNAL("recv"), self.recvTotal)
addHook("httpRecv", recvEvent)
con = httpCon()
try:
resp, cont = con.request(
aqt.appShared + "download/%d" % self.code)
except Exception, e:
self.error = unicode(e)
return
finally:
remHook("httpRecv", recvEvent)
if resp['status'] == '200':
self.error = None
self.fname = re.match("attachment; filename=(.+)",
resp['content-disposition']).group(1)
self.data = cont
elif resp['status'] == '403':
self.error = _("Invalid code.")
else:
self.error = _("Error downloading: %s") % resp['status']

View file

@ -1,259 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from aqt.qt import *
import aqt, simplejson, time, cStringIO, zipfile, os, re, gzip
import traceback, urllib2, socket, cgi
from aqt.ui.utils import saveGeom, restoreGeom, showInfo
from anki.utils import fmtTimeSpan
URL = "http://ankiweb.net/file/"
#URL = "http://localhost:8001/file/"
R_ID = 0
R_USERNAME = 1
R_TITLE = 2
R_DESCRIPTION = 3
R_TAGS = 4
R_VERSION = 5
R_FACTS = 6
R_SIZE = 7
R_COUNT = 8
R_MODIFIED = 9
R_FNAME = 10
class GetShared(QDialog):
def __init__(self, parent, type):
QDialog.__init__(self, parent, Qt.Window)
self.parent = parent
aqt.form = ankiqt.forms.getshared.Ui_Dialog()
self.form.setupUi(self)
self.ok = True
self.conErrMsg = _("""\
<b>Unable to connect to the server.<br><br>
Please check your network connection or try again in a few minutes.</b><br>
<br>
Error was:<pre>%s</pre>""")
restoreGeom(self, "getshared")
self.setupTable()
self.onChangeType(type)
if type == 0:
self.setWindowTitle(_("Download Shared Col"))
else:
self.setWindowTitle(_("Download Shared Plugin"))
if self.ok:
self.exec_()
def setupTable(self):
self.connect(
self.form.table, SIGNAL("currentCellChanged(int,int,int,int)"),
self.onCellChanged)
self.form.table.verticalHeader().setDefaultSectionSize(
self.parent.pm.profile['editLineSize'])
self.connect(self.form.search, SIGNAL("textChanged(QString)"),
self.limit)
def fetchData(self):
self.parent.setProgressParent(None)
self.parent.startProgress()
self.parent.updateProgress()
try:
socket.setdefaulttimeout(30)
try:
sock = urllib2.urlopen(
URL + "search?t=%d&c=1" % self.type)
data = sock.read()
try:
data = gzip.GzipFile(fileobj=cStringIO.StringIO(data)).read()
except:
# the server is sending gzipped data, but a transparent
# proxy or antivirus software may be decompressing it
# before we get it
pass
self.allList = simplejson.loads(unicode(data))
except:
showInfo(self.conErrMsg % cgi.escape(unicode(
traceback.format_exc(), "utf-8", "replace")))
self.close()
self.ok = False
return
finally:
self.parent.finishProgress()
socket.setdefaulttimeout(None)
self.form.search.setFocus()
self.typeChanged()
self.limit()
def limit(self, txt=""):
if not txt:
self.curList = self.allList
else:
txt = unicode(txt).lower()
self.curList = [
l for l in self.allList
if (txt in l[R_TITLE].lower() or
txt in l[R_DESCRIPTION].lower() or
txt in l[R_TAGS].lower())]
self.redraw()
def redraw(self):
self.form.table.setSortingEnabled(False)
self.form.table.setRowCount(len(self.curList))
self.items = {}
if self.type == 0:
cols = (R_TITLE, R_FACTS, R_COUNT, R_MODIFIED)
else:
cols = (R_TITLE, R_COUNT, R_MODIFIED)
for rc, r in enumerate(self.curList):
for cc, c in enumerate(cols):
if c == R_FACTS or c == R_COUNT:
txt = unicode("%15d" % r[c])
elif c == R_MODIFIED:
days = int(((time.time() - r[c])/(24*60*60)))
txt = ngettext("%6d day ago", "%6d days ago", days) % days
else:
txt = unicode(r[c])
item = QTableWidgetItem(txt)
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
self.items[item] = r
self.form.table.setItem(rc, cc, item)
self.form.table.setSortingEnabled(True)
if self.type == 0:
self.form.table.sortItems(2, Qt.DescendingOrder)
else:
self.form.table.sortItems(1, Qt.DescendingOrder)
self.form.table.selectRow(0)
self.onCellChanged(None, None, None, None)
def onCellChanged(self, row, col, x, y):
ci = self.form.table.currentItem()
if not ci:
self.form.bottomLabel.setText(_("Nothing selected."))
return
r = self.items[ci]
self.curRow = r
self.form.bottomLabel.setText(_("""\
<b>Title</b>: %(title)s<br>
<b>Tags</b>: %(tags)s<br>
<b>Size</b>: %(size)0.2fKB<br>
<b>Uploader</b>: %(author)s<br>
<b>Downloads</b>: %(count)s<br>
<b>Modified</b>: %(mod)s ago<br>
<br>%(description)s""") % {
'title': r[R_TITLE],
'tags': r[R_TAGS],
'size': r[R_SIZE] / 1024.0,
'author': r[R_USERNAME],
'count': r[R_COUNT],
'mod': fmtTimeSpan(time.time() - r[R_MODIFIED]),
'description': r[R_DESCRIPTION].replace("\n", "<br>"),
})
self.form.scrollAreaWidgetContents.adjustSize()
self.form.scrollArea.setWidget(self.form.scrollAreaWidgetContents)
def onChangeType(self, type):
self.type = type
self.fetchData()
def typeChanged(self):
self.form.table.clear()
if self.type == 0:
self.form.table.setColumnCount(4)
self.form.table.setHorizontalHeaderLabels([
_("Title"), _("Facts"), _("Downloads"), _("Modified")])
else:
self.form.table.setColumnCount(3)
self.form.table.setHorizontalHeaderLabels([
_("Title"), _("Downloads"), _("Modified")])
self.form.table.horizontalHeader().setResizeMode(
0, QHeaderView.Stretch)
self.form.table.verticalHeader().hide()
def accept(self):
if self.type == 0:
if not self.parent.saveAndClose(hideWelcome=True, parent=self):
return QDialog.accept(self)
# fixme: use namedtmp
(fd, tmpname) = tempfile.mkstemp(prefix="anki")
tmpfile = os.fdopen(fd, "w+b")
cnt = 0
try:
socket.setdefaulttimeout(30)
self.parent.setProgressParent(self)
self.parent.startProgress()
self.parent.updateProgress()
try:
sock = urllib2.urlopen(
URL + "get?id=%d" %
self.curRow[R_ID])
while 1:
data = sock.read(32768)
if not data:
break
cnt += len(data)
tmpfile.write(data)
self.parent.updateProgress(
label=_("Downloaded %dKB") % (cnt/1024.0))
except:
showInfo(self.conErrMsg % cgi.escape(unicode(
traceback.format_exc(), "utf-8", "replace")))
self.close()
return
finally:
socket.setdefaulttimeout(None)
self.parent.setProgressParent(None)
self.parent.finishProgress()
QDialog.accept(self)
# file is fetched
tmpfile.seek(0)
self.handleFile(tmpfile)
QDialog.accept(self)
def handleFile(self, file):
ext = os.path.splitext(self.curRow[R_FNAME])[1]
if ext == ".zip":
z = zipfile.ZipFile(file)
else:
z = None
tit = self.curRow[R_TITLE]
tit = re.sub("[^][A-Za-z0-9 ()\-]", "", tit)
tit = tit[0:40]
if self.type == 0:
# col
dd = self.parent.pm.profile['documentDir']
p = os.path.join(dd, tit + ".anki")
if os.path.exists(p):
tit += "%d" % time.time()
for l in z.namelist():
if l == "shared.anki":
dpath = os.path.join(dd, tit + ".anki")
open(dpath, "wb").write(z.read(l))
elif l.startswith("shared.media/"):
try:
os.mkdir(os.path.join(dd, tit + ".media"))
except OSError:
pass
open(os.path.join(dd, tit + ".media",
os.path.basename(l)),"wb").write(z.read(l))
self.parent.loadCol(dpath)
else:
pd = self.parent.pluginsFolder()
if z:
for l in z.infolist():
try:
os.makedirs(os.path.join(
pd, os.path.dirname(l.filename)))
except OSError:
pass
if l.filename.endswith("/"):
# directory
continue
path = os.path.join(pd, l.filename)
open(path, "wb").write(z.read(l.filename))
else:
open(os.path.join(pd, tit + ext), "wb").write(file.read())
showInfo(_("Plugin downloaded. Please restart Anki."),
parent=self)

101
designer/getaddons.ui Normal file
View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>367</width>
<height>204</height>
</rect>
</property>
<property name="windowTitle">
<string>Install Add-on</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>To browse add-ons, please click the browse button below.&lt;br&gt;&lt;br&gt;When you've found an add-on you like, please paste its code below.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Code:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="code"/>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -200,7 +200,7 @@
</action>
<action name="actionDownloadSharedPlugin">
<property name="text">
<string>Get Add-ons...</string>
<string>Browse &amp;&amp; Install...</string>
</property>
<property name="statusTip">
<string>Download a plugin to add new features or change Anki's behaviour</string>