mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
calibre now depends on dnspython for email delivery. Also remove python 2.5 builds as it is no longer officially supported, though calibre will probably continue to run on it for a little while longer.
This commit is contained in:
parent
52d968838c
commit
9ca023e9a7
@ -9,7 +9,7 @@ Create linux binary.
|
||||
'''
|
||||
|
||||
def freeze():
|
||||
import glob, sys, subprocess, tarfile, os, re, textwrap, shutil, cStringIO, bz2, codecs
|
||||
import glob, sys, tarfile, os, textwrap, shutil
|
||||
from contextlib import closing
|
||||
from cx_Freeze import Executable, setup
|
||||
from calibre.constants import __version__, __appname__
|
||||
@ -18,13 +18,13 @@ def freeze():
|
||||
from calibre.web.feeds.recipes import recipe_modules
|
||||
from calibre.ebooks.lrf.fonts import FONT_MAP
|
||||
import calibre
|
||||
|
||||
|
||||
|
||||
QTDIR = '/usr/lib/qt4'
|
||||
QTDLLS = ('QtCore', 'QtGui', 'QtNetwork', 'QtSvg', 'QtXml', 'QtWebKit')
|
||||
|
||||
|
||||
binary_excludes = ['libGLcore*', 'libGL*', 'libnvidia*']
|
||||
|
||||
|
||||
binary_includes = [
|
||||
'/usr/bin/pdftohtml',
|
||||
'/usr/lib/libunrar.so',
|
||||
@ -48,58 +48,60 @@ def freeze():
|
||||
'/usr/lib/libMagickWand.so',
|
||||
'/usr/lib/libMagickCore.so',
|
||||
]
|
||||
|
||||
|
||||
binary_includes += [os.path.join(QTDIR, 'lib%s.so.4'%x) for x in QTDLLS]
|
||||
|
||||
|
||||
|
||||
|
||||
d = os.path.dirname
|
||||
CALIBRESRC = d(d(d(os.path.abspath(calibre.__file__))))
|
||||
CALIBREPLUGINS = os.path.join(CALIBRESRC, 'src', 'calibre', 'plugins')
|
||||
FREEZE_DIR = os.path.join(CALIBRESRC, 'build', 'cx_freeze')
|
||||
DIST_DIR = os.path.join(CALIBRESRC, 'dist')
|
||||
|
||||
|
||||
os.chdir(CALIBRESRC)
|
||||
|
||||
|
||||
print 'Freezing calibre located at', CALIBRESRC
|
||||
|
||||
|
||||
sys.path.insert(0, os.path.join(CALIBRESRC, 'src'))
|
||||
|
||||
|
||||
entry_points = entry_points['console_scripts'] + entry_points['gui_scripts']
|
||||
entry_points = ['calibre_postinstall=calibre.linux:binary_install',
|
||||
entry_points = ['calibre_postinstall=calibre.linux:binary_install',
|
||||
'calibre-parallel=calibre.parallel:main'] + entry_points
|
||||
executables = {}
|
||||
for ep in entry_points:
|
||||
executables[ep.split('=')[0].strip()] = (ep.split('=')[1].split(':')[0].strip(),
|
||||
ep.split(':')[-1].strip())
|
||||
|
||||
|
||||
if os.path.exists(FREEZE_DIR):
|
||||
shutil.rmtree(FREEZE_DIR)
|
||||
os.makedirs(FREEZE_DIR)
|
||||
|
||||
|
||||
if not os.path.exists(DIST_DIR):
|
||||
os.makedirs(DIST_DIR)
|
||||
|
||||
|
||||
includes = [x[0] for x in executables.values()]
|
||||
includes += ['calibre.ebooks.lrf.fonts.prs500.'+x for x in FONT_MAP.values()]
|
||||
|
||||
excludes = ['matplotlib', "Tkconstants", "Tkinter", "tcl", "_imagingtk",
|
||||
"ImageTk", "FixTk", 'wx', 'PyQt4.QtAssistant', 'PyQt4.QtOpenGL.so',
|
||||
includes += ['email.iterators', 'email.generator']
|
||||
|
||||
|
||||
excludes = ['matplotlib', "Tkconstants", "Tkinter", "tcl", "_imagingtk",
|
||||
"ImageTk", "FixTk", 'wx', 'PyQt4.QtAssistant', 'PyQt4.QtOpenGL.so',
|
||||
'PyQt4.QtScript.so', 'PyQt4.QtSql.so', 'PyQt4.QtTest.so', 'qt',
|
||||
'glib', 'gobject']
|
||||
|
||||
packages = ['calibre', 'encodings', 'cherrypy', 'cssutils', 'xdg',
|
||||
'dateutil']
|
||||
|
||||
|
||||
packages = ['calibre', 'encodings', 'cherrypy', 'cssutils', 'xdg',
|
||||
'dateutil', 'dns', 'email']
|
||||
|
||||
includes += ['calibre.web.feeds.recipes.'+r for r in recipe_modules]
|
||||
|
||||
|
||||
LOADER = '/tmp/loader.py'
|
||||
open(LOADER, 'wb').write('# This script is never actually used.\nimport sys')
|
||||
|
||||
|
||||
INIT_SCRIPT = '/tmp/init.py'
|
||||
open(INIT_SCRIPT, 'wb').write(textwrap.dedent('''
|
||||
## Load calibre module specified in the environment variable CALIBRE_CX_EXE
|
||||
## Also restrict sys.path to the executables' directory and add the
|
||||
## executables directory to LD_LIBRARY_PATH
|
||||
## executables directory to LD_LIBRARY_PATH
|
||||
import encodings
|
||||
import os
|
||||
import sys
|
||||
@ -107,26 +109,26 @@ def freeze():
|
||||
import zipimport
|
||||
import locale
|
||||
import codecs
|
||||
|
||||
|
||||
enc = locale.getdefaultlocale()[1]
|
||||
if not enc:
|
||||
enc = locale.nl_langinfo(locale.CODESET)
|
||||
enc = codecs.lookup(enc if enc else 'UTF-8').name
|
||||
sys.setdefaultencoding(enc)
|
||||
|
||||
|
||||
paths = os.environ.get('LD_LIBRARY_PATH', '').split(os.pathsep)
|
||||
if DIR_NAME not in paths or not sys.getfilesystemencoding():
|
||||
paths.insert(0, DIR_NAME)
|
||||
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths)
|
||||
os.environ['PYTHONIOENCODING'] = enc
|
||||
os.execv(sys.executable, sys.argv)
|
||||
|
||||
|
||||
sys.path = sys.path[:3]
|
||||
sys.frozen = True
|
||||
sys.frozen_path = DIR_NAME
|
||||
|
||||
|
||||
executables = %(executables)s
|
||||
|
||||
|
||||
exe = os.environ.get('CALIBRE_CX_EXE', False)
|
||||
ret = 1
|
||||
if not exe:
|
||||
@ -141,7 +143,7 @@ def freeze():
|
||||
module = __import__(module, fromlist=[1])
|
||||
func = getattr(module, func)
|
||||
ret = func()
|
||||
|
||||
|
||||
module = sys.modules.get("threading")
|
||||
if module is not None:
|
||||
module._shutdown()
|
||||
@ -162,35 +164,35 @@ def freeze():
|
||||
'init_script' : INIT_SCRIPT,
|
||||
'copy_dependent_files' : True,
|
||||
'create_shared_zip' : False,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def copy_binary(src, dest_dir):
|
||||
dest = os.path.join(dest_dir, os.path.basename(src))
|
||||
if not os.path.exists(dest_dir):
|
||||
os.makedirs(dest_dir)
|
||||
shutil.copyfile(os.path.realpath(src), dest)
|
||||
shutil.copymode(os.path.realpath(src), dest)
|
||||
|
||||
|
||||
for f in binary_includes:
|
||||
copy_binary(f, FREEZE_DIR)
|
||||
|
||||
|
||||
for pat in binary_excludes:
|
||||
matches = glob.glob(os.path.join(FREEZE_DIR, pat))
|
||||
for f in matches:
|
||||
os.remove(f)
|
||||
|
||||
|
||||
print 'Adding calibre plugins...'
|
||||
os.makedirs(os.path.join(FREEZE_DIR, 'plugins'))
|
||||
os.makedirs(os.path.join(FREEZE_DIR, 'plugins'))
|
||||
for f in glob.glob(os.path.join(CALIBREPLUGINS, '*.so')):
|
||||
copy_binary(f, os.path.join(FREEZE_DIR, 'plugins'))
|
||||
|
||||
|
||||
print 'Adding Qt plugins...'
|
||||
plugdir = os.path.join(QTDIR, 'plugins')
|
||||
for dirpath, dirnames, filenames in os.walk(plugdir):
|
||||
for f in filenames:
|
||||
if not f.endswith('.so') or 'designer' in dirpath or 'codecs' in dirpath or 'sqldrivers' in dirpath:
|
||||
if not f.endswith('.so') or 'designer' in dirpath or 'codecs' in dirpath or 'sqldrivers' in dirpath:
|
||||
continue
|
||||
f = os.path.join(dirpath, f)
|
||||
dest_dir = dirpath.replace(plugdir, os.path.join(FREEZE_DIR, 'qtplugins'))
|
||||
@ -198,7 +200,7 @@ def freeze():
|
||||
|
||||
print 'Creating launchers'
|
||||
for exe in executables:
|
||||
path = os.path.join(FREEZE_DIR, exe)
|
||||
path = os.path.join(FREEZE_DIR, exe)
|
||||
open(path, 'wb').write(textwrap.dedent('''\
|
||||
#!/bin/sh
|
||||
export CALIBRE_CX_EXE=%s
|
||||
@ -209,15 +211,15 @@ def freeze():
|
||||
$loader "$@"
|
||||
''')%exe)
|
||||
os.chmod(path, 0755)
|
||||
|
||||
|
||||
exes = list(executables.keys())
|
||||
exes.remove('calibre_postinstall')
|
||||
exes.remove('calibre-parallel')
|
||||
open(os.path.join(FREEZE_DIR, 'manifest'), 'wb').write('\n'.join(exes))
|
||||
|
||||
|
||||
print 'Creating archive...'
|
||||
dist = open(os.path.join(DIST_DIR, 'calibre-%s-i686.tar.bz2'%__version__), 'wb')
|
||||
with closing(tarfile.open(fileobj=dist, mode='w:bz2',
|
||||
with closing(tarfile.open(fileobj=dist, mode='w:bz2',
|
||||
format=tarfile.PAX_FORMAT)) as tf:
|
||||
for f in walk(FREEZE_DIR):
|
||||
name = f.replace(FREEZE_DIR, '')[1:]
|
||||
|
@ -14,12 +14,11 @@ IMAGEMAGICK_DIR = 'C:\\ImageMagick'
|
||||
FONTCONFIG_DIR = 'C:\\fontconfig'
|
||||
VC90 = r'C:\VC90.CRT'
|
||||
|
||||
import sys, os, py2exe, shutil, zipfile, glob, subprocess, re
|
||||
import sys, os, py2exe, shutil, zipfile, glob, re
|
||||
from distutils.core import setup
|
||||
from distutils.filelist import FileList
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
sys.path.insert(0, BASE_DIR)
|
||||
from setup import VERSION, APPNAME, entry_points, scripts, basenames
|
||||
from setup import VERSION, APPNAME, scripts, basenames
|
||||
sys.path.remove(BASE_DIR)
|
||||
|
||||
ICONS = [os.path.abspath(os.path.join(BASE_DIR, 'icons', i)) for i in ('library.ico', 'viewer.ico')]
|
||||
@ -33,7 +32,7 @@ WINVER = VERSION+'.0'
|
||||
PY2EXE_DIR = os.path.join(BASE_DIR, 'build','py2exe')
|
||||
|
||||
class BuildEXE(py2exe.build_exe.py2exe):
|
||||
|
||||
|
||||
def run(self):
|
||||
py2exe.build_exe.py2exe.run(self)
|
||||
print 'Adding plugins...'
|
||||
@ -61,19 +60,19 @@ class BuildEXE(py2exe.build_exe.py2exe):
|
||||
if os.path.exists(tg):
|
||||
shutil.rmtree(tg)
|
||||
shutil.copytree(imfd, tg)
|
||||
|
||||
print
|
||||
|
||||
print
|
||||
print 'Adding main scripts'
|
||||
f = zipfile.ZipFile(os.path.join(PY2EXE_DIR, 'library.zip'), 'a', zipfile.ZIP_DEFLATED)
|
||||
for i in scripts['console'] + scripts['gui']:
|
||||
f.write(i, i.partition('\\')[-1])
|
||||
f.close()
|
||||
|
||||
|
||||
print
|
||||
print 'Copying icons'
|
||||
for icon in ICONS:
|
||||
shutil.copyfile(icon, os.path.join(PY2EXE_DIR, os.path.basename(icon)))
|
||||
|
||||
|
||||
print
|
||||
print 'Adding third party dependencies'
|
||||
print '\tAdding devcon'
|
||||
@ -96,18 +95,18 @@ class BuildEXE(py2exe.build_exe.py2exe):
|
||||
shutil.copytree(f, tgt)
|
||||
else:
|
||||
shutil.copyfile(f, tgt)
|
||||
|
||||
print
|
||||
|
||||
print
|
||||
print 'Doing DLL redirection' # See http://msdn.microsoft.com/en-us/library/ms682600(VS.85).aspx
|
||||
for f in glob.glob(os.path.join(PY2EXE_DIR, '*.exe')):
|
||||
open(f + '.local', 'w').write('\n')
|
||||
|
||||
|
||||
print
|
||||
print 'Adding Windows runtime dependencies...'
|
||||
for f in glob.glob(os.path.join(VC90, '*')):
|
||||
shutil.copyfile(f, os.path.join(PY2EXE_DIR, os.path.basename(f)))
|
||||
|
||||
|
||||
|
||||
|
||||
def exe_factory(dest_base, script, icon_resources=None):
|
||||
exe = {
|
||||
'dest_base' : dest_base,
|
||||
@ -144,7 +143,9 @@ def main(args=sys.argv):
|
||||
'includes' : [
|
||||
'sip', 'pkg_resources', 'PyQt4.QtSvg',
|
||||
'mechanize', 'ClientForm', 'wmi',
|
||||
'win32file', 'pythoncom',
|
||||
'win32file', 'pythoncom',
|
||||
'email.iterators',
|
||||
'email.generator',
|
||||
'win32process', 'win32api', 'msvcrt',
|
||||
'win32event', 'calibre.ebooks.lrf.any.*',
|
||||
'calibre.ebooks.lrf.feeds.*',
|
||||
@ -155,14 +156,14 @@ def main(args=sys.argv):
|
||||
'PyQt4.QtWebKit', 'PyQt4.QtNetwork',
|
||||
],
|
||||
'packages' : ['PIL', 'lxml', 'cherrypy',
|
||||
'dateutil'],
|
||||
'dateutil', 'dns'],
|
||||
'excludes' : ["Tkconstants", "Tkinter", "tcl",
|
||||
"_imagingtk", "ImageTk", "FixTk"
|
||||
],
|
||||
'dll_excludes' : ['mswsock.dll'],
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
)
|
||||
return 0
|
||||
|
||||
|
@ -59,8 +59,8 @@ class FetchISBNDB(Thread):
|
||||
args.extend(['--author', self.author])
|
||||
if self.publisher:
|
||||
args.extend(['--publisher', self.publisher])
|
||||
if self.verbose:
|
||||
args.extend(['--verbose'])
|
||||
if self.verbose:
|
||||
args.extend(['--verbose'])
|
||||
args.append(self.key)
|
||||
try:
|
||||
opts, args = option_parser().parse_args(args)
|
||||
|
@ -28,7 +28,8 @@ def fetch_metadata(url, max=100, timeout=5.):
|
||||
raw = urlopen(url).read()
|
||||
except Exception, err:
|
||||
raise ISBNDBError('Could not fetch ISBNDB metadata. Error: '+str(err))
|
||||
soup = BeautifulStoneSoup(raw)
|
||||
soup = BeautifulStoneSoup(raw,
|
||||
convertEntities=BeautifulStoneSoup.XML_ENTITIES)
|
||||
book_list = soup.find('booklist')
|
||||
if book_list is None:
|
||||
errmsg = soup.find('errormessage').string
|
||||
@ -41,13 +42,13 @@ def fetch_metadata(url, max=100, timeout=5.):
|
||||
return books
|
||||
finally:
|
||||
socket.setdefaulttimeout(timeout)
|
||||
|
||||
|
||||
|
||||
class ISBNDBMetadata(MetaInformation):
|
||||
|
||||
|
||||
def __init__(self, book):
|
||||
MetaInformation.__init__(self, None, [])
|
||||
|
||||
|
||||
self.isbn = book['isbn']
|
||||
self.title = book.find('titlelong').string
|
||||
if not self.title:
|
||||
@ -59,7 +60,7 @@ class ISBNDBMetadata(MetaInformation):
|
||||
for au in temp:
|
||||
if not au: continue
|
||||
self.authors.extend([a.strip() for a in au.split('&')])
|
||||
|
||||
|
||||
try:
|
||||
self.author_sort = book.find('authors').find('person').string
|
||||
if self.authors and self.author_sort == self.authors[0]:
|
||||
@ -67,12 +68,12 @@ class ISBNDBMetadata(MetaInformation):
|
||||
except:
|
||||
pass
|
||||
self.publisher = book.find('publishertext').string
|
||||
|
||||
|
||||
summ = book.find('summary')
|
||||
if summ and hasattr(summ, 'string') and summ.string:
|
||||
self.comments = 'SUMMARY:\n'+summ.string
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def build_isbn(base_url, opts):
|
||||
return base_url + 'index1=isbn&value1='+opts.isbn
|
||||
@ -85,11 +86,11 @@ def build_combined(base_url, opts):
|
||||
query = query.strip()
|
||||
if len(query) == 0:
|
||||
raise ISBNDBError('You must specify at least one of --author, --title or --publisher')
|
||||
|
||||
|
||||
query = re.sub(r'\s+', '+', query)
|
||||
if isinstance(query, unicode):
|
||||
query = query.encode('utf-8')
|
||||
return base_url+'index1=combined&value1='+quote(query, '+')
|
||||
return base_url+'index1=combined&value1='+quote(query, '+')
|
||||
|
||||
|
||||
def option_parser():
|
||||
@ -97,7 +98,7 @@ def option_parser():
|
||||
_('''
|
||||
%prog [options] key
|
||||
|
||||
Fetch metadata for books from isndb.com. You can specify either the
|
||||
Fetch metadata for books from isndb.com. You can specify either the
|
||||
books ISBN ID or its title and author. If you specify the title and author,
|
||||
then more than one book may be returned.
|
||||
|
||||
@ -112,11 +113,11 @@ key is the account key you generate after signing up for a free account from isb
|
||||
default=None, help=_('The title of the book to search for.'))
|
||||
parser.add_option('-p', '--publisher', default=None, dest='publisher',
|
||||
help=_('The publisher of the book to search for.'))
|
||||
parser.add_option('-v', '--verbose', default=False,
|
||||
parser.add_option('-v', '--verbose', default=False,
|
||||
action='store_true', help=_('Verbose processing'))
|
||||
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
|
||||
def create_books(opts, args, timeout=5.):
|
||||
base_url = BASE_URL%dict(key=args[1])
|
||||
@ -124,10 +125,10 @@ def create_books(opts, args, timeout=5.):
|
||||
url = build_isbn(base_url, opts)
|
||||
else:
|
||||
url = build_combined(base_url, opts)
|
||||
|
||||
|
||||
if opts.verbose:
|
||||
print ('ISBNDB query: '+url)
|
||||
|
||||
|
||||
return [ISBNDBMetadata(book) for book in fetch_metadata(url, timeout=timeout)]
|
||||
|
||||
def main(args=sys.argv):
|
||||
@ -137,10 +138,10 @@ def main(args=sys.argv):
|
||||
parser.print_help()
|
||||
print ('You must supply the isbndb.com key')
|
||||
return 1
|
||||
|
||||
|
||||
for book in create_books(opts, args):
|
||||
print unicode(book).encode('utf-8')
|
||||
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -337,7 +337,7 @@ class DeviceMenu(QMenu):
|
||||
|
||||
class Emailer(Thread):
|
||||
|
||||
def __init__(self, timeout=10):
|
||||
def __init__(self, timeout=60):
|
||||
Thread.__init__(self)
|
||||
self.setDaemon(True)
|
||||
self.job_lock = RLock()
|
||||
@ -369,6 +369,7 @@ class Emailer(Thread):
|
||||
def _send_mails(self, jobnames, callback, attachments,
|
||||
to_s, subjects, texts, attachment_names):
|
||||
opts = email_config().parse()
|
||||
opts.verbose = 3 if os.environ.get('CALIBRE_DEBUG_EMAIL', False) else 0
|
||||
from_ = opts.from_
|
||||
if not from_:
|
||||
from_ = 'calibre <calibre@'+socket.getfqdn()+'>'
|
||||
@ -380,7 +381,8 @@ class Emailer(Thread):
|
||||
attachment_name = attachment_names[i])
|
||||
efrom, eto = map(extract_email_address, (from_, to_s[i]))
|
||||
eto = [eto]
|
||||
sendmail(msg, efrom, eto, localhost=None, verbose=0,
|
||||
sendmail(msg, efrom, eto, localhost=None,
|
||||
verbose=opts.verbose,
|
||||
timeout=self.timeout, relay=opts.relay_host,
|
||||
username=opts.relay_username,
|
||||
password=opts.relay_password, port=opts.relay_port,
|
||||
|
@ -11,12 +11,12 @@ from PyQt4.QtCore import Qt, QObject, SIGNAL, QVariant, QThread, \
|
||||
from PyQt4.QtGui import QDialog, QItemSelectionModel
|
||||
|
||||
from calibre.gui2.dialogs.fetch_metadata_ui import Ui_FetchMetadata
|
||||
from calibre.gui2 import error_dialog, NONE, info_dialog, warning_dialog
|
||||
from calibre.gui2 import error_dialog, NONE, info_dialog
|
||||
from calibre.gui2.widgets import ProgressIndicator
|
||||
from calibre.utils.config import prefs
|
||||
|
||||
class Fetcher(QThread):
|
||||
|
||||
|
||||
def __init__(self, title, author, publisher, isbn, key):
|
||||
QThread.__init__(self)
|
||||
self.title = title
|
||||
@ -24,47 +24,47 @@ class Fetcher(QThread):
|
||||
self.publisher = publisher
|
||||
self.isbn = isbn
|
||||
self.key = key
|
||||
|
||||
|
||||
def run(self):
|
||||
from calibre.ebooks.metadata.fetch import search
|
||||
self.results, self.exceptions = search(self.title, self.author,
|
||||
self.publisher, self.isbn,
|
||||
self.publisher, self.isbn,
|
||||
self.key if self.key else None)
|
||||
|
||||
|
||||
|
||||
class Matches(QAbstractTableModel):
|
||||
|
||||
|
||||
def __init__(self, matches):
|
||||
self.matches = matches
|
||||
self.matches.sort(cmp=lambda b, a: \
|
||||
cmp(len(a.comments if a.comments else ''),
|
||||
cmp(len(a.comments if a.comments else ''),
|
||||
len(b.comments if b.comments else '')))
|
||||
QAbstractTableModel.__init__(self)
|
||||
|
||||
|
||||
def rowCount(self, *args):
|
||||
return len(self.matches)
|
||||
|
||||
|
||||
def columnCount(self, *args):
|
||||
return 5
|
||||
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if role != Qt.DisplayRole:
|
||||
return NONE
|
||||
text = ""
|
||||
if orientation == Qt.Horizontal:
|
||||
if orientation == Qt.Horizontal:
|
||||
if section == 0: text = _("Title")
|
||||
elif section == 1: text = _("Author(s)")
|
||||
elif section == 2: text = _("Author Sort")
|
||||
elif section == 3: text = _("Publisher")
|
||||
elif section == 4: text = _("ISBN")
|
||||
|
||||
|
||||
return QVariant(text)
|
||||
else:
|
||||
else:
|
||||
return QVariant(section+1)
|
||||
|
||||
|
||||
def summary(self, row):
|
||||
return self.matches[row].comments
|
||||
|
||||
|
||||
def data(self, index, role):
|
||||
row, col = index.row(), index.column()
|
||||
if role == Qt.DisplayRole:
|
||||
@ -86,18 +86,18 @@ class Matches(QAbstractTableModel):
|
||||
return NONE
|
||||
|
||||
class FetchMetadata(QDialog, Ui_FetchMetadata):
|
||||
|
||||
|
||||
def __init__(self, parent, isbn, title, author, publisher, timeout):
|
||||
QDialog.__init__(self, parent)
|
||||
Ui_FetchMetadata.__init__(self)
|
||||
self.setupUi(self)
|
||||
|
||||
|
||||
self.pi = ProgressIndicator(self)
|
||||
self.timeout = timeout
|
||||
QObject.connect(self.fetch, SIGNAL('clicked()'), self.fetch_metadata)
|
||||
|
||||
|
||||
self.key.setText(prefs['isbndb_com_key'])
|
||||
|
||||
|
||||
self.setWindowTitle(title if title else _('Unknown'))
|
||||
self.isbn = isbn
|
||||
self.title = title
|
||||
@ -106,19 +106,19 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
|
||||
self.previous_row = None
|
||||
self.warning.setVisible(False)
|
||||
self.connect(self.matches, SIGNAL('activated(QModelIndex)'), self.chosen)
|
||||
self.connect(self.matches, SIGNAL('entered(QModelIndex)'),
|
||||
self.connect(self.matches, SIGNAL('entered(QModelIndex)'),
|
||||
self.show_summary)
|
||||
self.matches.setMouseTracking(True)
|
||||
self.fetch_metadata()
|
||||
|
||||
|
||||
|
||||
|
||||
def show_summary(self, current, *args):
|
||||
row = current.row()
|
||||
if row != self.previous_row:
|
||||
summ = self.model.summary(row)
|
||||
self.summary.setText(summ if summ else '')
|
||||
self.previous_row = row
|
||||
|
||||
|
||||
def fetch_metadata(self):
|
||||
self.warning.setVisible(False)
|
||||
key = str(self.key.text())
|
||||
@ -143,7 +143,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
|
||||
self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck)
|
||||
self.start_time = time.time()
|
||||
self._hangcheck.start(100)
|
||||
|
||||
|
||||
def hangcheck(self):
|
||||
if not (self.fetcher.isFinished() or time.time() - self.start_time > 75):
|
||||
return
|
||||
@ -169,13 +169,13 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
|
||||
_('No metadata found, try adjusting the title and author '
|
||||
'or the ISBN key.')).exec_()
|
||||
return
|
||||
|
||||
|
||||
self.matches.setModel(self.model)
|
||||
QObject.connect(self.matches.selectionModel(),
|
||||
QObject.connect(self.matches.selectionModel(),
|
||||
SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
|
||||
self.show_summary)
|
||||
self.model.reset()
|
||||
self.matches.selectionModel().select(self.model.index(0, 0),
|
||||
self.matches.selectionModel().select(self.model.index(0, 0),
|
||||
QItemSelectionModel.Select | QItemSelectionModel.Rows)
|
||||
self.matches.setCurrentIndex(self.model.index(0, 0))
|
||||
finally:
|
||||
@ -183,14 +183,24 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
|
||||
self.unsetCursor()
|
||||
self.matches.resizeColumnsToContents()
|
||||
self.pi.stop()
|
||||
|
||||
|
||||
|
||||
def terminate(self):
|
||||
if hasattr(self, 'fetcher') and self.fetcher.isRunning():
|
||||
self.fetcher.terminate()
|
||||
|
||||
|
||||
def __enter__(self, *args):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.terminate()
|
||||
|
||||
def selected_book(self):
|
||||
try:
|
||||
return self.matches.model().matches[self.matches.currentIndex().row()]
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def chosen(self, index):
|
||||
self.matches.setCurrentIndex(index)
|
||||
self.accept()
|
||||
|
@ -1,7 +1,8 @@
|
||||
from __future__ import with_statement
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
'''
|
||||
The dialog used to edit meta information for a book as well as
|
||||
'''
|
||||
The dialog used to edit meta information for a book as well as
|
||||
add/remove formats
|
||||
'''
|
||||
import os, time, traceback
|
||||
@ -26,7 +27,7 @@ from calibre.utils.config import prefs
|
||||
from calibre.customize.ui import run_plugins_on_import
|
||||
|
||||
class CoverFetcher(QThread):
|
||||
|
||||
|
||||
def __init__(self, username, password, isbn, timeout):
|
||||
self.username = username
|
||||
self.password = password
|
||||
@ -34,7 +35,7 @@ class CoverFetcher(QThread):
|
||||
self.isbn = isbn
|
||||
QThread.__init__(self)
|
||||
self.exception = self.traceback = self.cover_data = None
|
||||
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
login(self.username, self.password, force=False)
|
||||
@ -42,8 +43,8 @@ class CoverFetcher(QThread):
|
||||
except Exception, e:
|
||||
self.exception = e
|
||||
self.traceback = traceback.format_exc()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class Format(QListWidgetItem):
|
||||
def __init__(self, parent, ext, size, path=None):
|
||||
@ -51,25 +52,25 @@ class Format(QListWidgetItem):
|
||||
self.ext = ext
|
||||
self.size = float(size)/(1024*1024)
|
||||
text = '%s (%.2f MB)'%(self.ext.upper(), self.size)
|
||||
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
|
||||
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
|
||||
text, parent, QListWidgetItem.UserType)
|
||||
|
||||
class AuthorCompleter(QCompleter):
|
||||
|
||||
|
||||
def __init__(self, db):
|
||||
all_authors = db.all_authors()
|
||||
all_authors.sort(cmp=lambda x, y : cmp(x[1], y[1]))
|
||||
QCompleter.__init__(self, [x[1] for x in all_authors])
|
||||
|
||||
class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
|
||||
|
||||
def do_reset_cover(self, *args):
|
||||
pix = QPixmap(':/images/book.svg')
|
||||
self.cover.setPixmap(pix)
|
||||
self.cover_changed = True
|
||||
|
||||
|
||||
def select_cover(self, checked):
|
||||
files = choose_images(self, 'change cover dialog',
|
||||
files = choose_images(self, 'change cover dialog',
|
||||
u'Choose cover for ' + qstring_to_unicode(self.title.text()))
|
||||
if not files:
|
||||
return
|
||||
@ -77,7 +78,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
if _file:
|
||||
_file = os.path.abspath(_file)
|
||||
if not os.access(_file, os.R_OK):
|
||||
d = error_dialog(self.window, _('Cannot read'),
|
||||
d = error_dialog(self.window, _('Cannot read'),
|
||||
_('You do not have permission to read the file: ') + _file)
|
||||
d.exec_()
|
||||
return
|
||||
@ -85,7 +86,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
try:
|
||||
cf = open(_file, "rb")
|
||||
cover = cf.read()
|
||||
except IOError, e:
|
||||
except IOError, e:
|
||||
d = error_dialog(self.window, _('Error reading file'),
|
||||
_("<p>There was an error reading from file: <br /><b>") + _file + "</b></p><br />"+str(e))
|
||||
d.exec_()
|
||||
@ -99,15 +100,15 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.cover_path.setText(_file)
|
||||
self.cover.setPixmap(pix)
|
||||
self.cover_changed = True
|
||||
self.cpixmap = pix
|
||||
|
||||
|
||||
self.cpixmap = pix
|
||||
|
||||
|
||||
def add_format(self, x):
|
||||
files = choose_files(self, 'add formats dialog',
|
||||
files = choose_files(self, 'add formats dialog',
|
||||
"Choose formats for " + qstring_to_unicode((self.title.text())),
|
||||
[('Books', BOOK_EXTENSIONS)])
|
||||
if not files:
|
||||
return
|
||||
if not files:
|
||||
return
|
||||
for _file in files:
|
||||
_file = os.path.abspath(_file)
|
||||
if not os.access(_file, os.R_OK):
|
||||
@ -124,13 +125,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
break
|
||||
Format(self.formats, ext, size, path=_file)
|
||||
self.formats_changed = True
|
||||
|
||||
|
||||
def remove_format(self, x):
|
||||
rows = self.formats.selectionModel().selectedRows(0)
|
||||
for row in rows:
|
||||
self.formats.takeItem(row.row())
|
||||
self.formats_changed = True
|
||||
|
||||
|
||||
def set_cover(self):
|
||||
row = self.formats.currentRow()
|
||||
fmt = self.formats.item(row)
|
||||
@ -155,19 +156,19 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
elif mi.cover_data[1] is not None:
|
||||
cdata = mi.cover_data[1]
|
||||
if cdata is None:
|
||||
error_dialog(self, _('Could not read cover'),
|
||||
error_dialog(self, _('Could not read cover'),
|
||||
_('Could not read cover from %s format')%ext).exec_()
|
||||
return
|
||||
pix = QPixmap()
|
||||
pix.loadFromData(cdata)
|
||||
if pix.isNull():
|
||||
error_dialog(self, _('Could not read cover'),
|
||||
error_dialog(self, _('Could not read cover'),
|
||||
_('The cover in the %s format is invalid')%ext).exec_()
|
||||
return
|
||||
self.cover.setPixmap(pix)
|
||||
self.cover_changed = True
|
||||
self.cpixmap = pix
|
||||
|
||||
|
||||
def sync_formats(self):
|
||||
old_extensions, new_extensions, paths = set(), set(), {}
|
||||
for row in range(self.formats.count()):
|
||||
@ -187,7 +188,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
for ext in db_extensions:
|
||||
if ext not in extensions:
|
||||
self.db.remove_format(self.row, ext, notify=False)
|
||||
|
||||
|
||||
def __init__(self, window, row, db, accepted_callback=None):
|
||||
ResizableDialog.__init__(self, window)
|
||||
self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter)
|
||||
@ -211,12 +212,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.add_format)
|
||||
QObject.connect(self.remove_format_button, SIGNAL("clicked(bool)"), \
|
||||
self.remove_format)
|
||||
QObject.connect(self.fetch_metadata_button, SIGNAL('clicked()'),
|
||||
QObject.connect(self.fetch_metadata_button, SIGNAL('clicked()'),
|
||||
self.fetch_metadata)
|
||||
|
||||
QObject.connect(self.fetch_cover_button, SIGNAL('clicked()'),
|
||||
|
||||
QObject.connect(self.fetch_cover_button, SIGNAL('clicked()'),
|
||||
self.fetch_cover)
|
||||
QObject.connect(self.tag_editor_button, SIGNAL('clicked()'),
|
||||
QObject.connect(self.tag_editor_button, SIGNAL('clicked()'),
|
||||
self.edit_tags)
|
||||
QObject.connect(self.remove_series_button, SIGNAL('clicked()'),
|
||||
self.remove_unused_series)
|
||||
@ -242,28 +243,28 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
tags = self.db.tags(row)
|
||||
self.tags.setText(tags if tags else '')
|
||||
rating = self.db.rating(row)
|
||||
if rating > 0:
|
||||
if rating > 0:
|
||||
self.rating.setValue(int(rating/2.))
|
||||
comments = self.db.comments(row)
|
||||
self.comments.setPlainText(comments if comments else '')
|
||||
cover = self.db.cover(row)
|
||||
|
||||
|
||||
exts = self.db.formats(row)
|
||||
if exts:
|
||||
exts = exts.split(',')
|
||||
exts = exts.split(',')
|
||||
for ext in exts:
|
||||
if not ext:
|
||||
ext = ''
|
||||
size = self.db.sizeof_format(row, ext)
|
||||
Format(self.formats, ext, size)
|
||||
|
||||
|
||||
|
||||
|
||||
self.initialize_series_and_publisher()
|
||||
|
||||
|
||||
self.series_index.setValue(self.db.series_index(row))
|
||||
QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.enable_series_index)
|
||||
QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.enable_series_index)
|
||||
QObject.connect(self.password_button, SIGNAL('clicked()'), self.change_password)
|
||||
QObject.connect(self.password_button, SIGNAL('clicked()'), self.change_password)
|
||||
|
||||
self.show()
|
||||
height_of_rest = self.frameGeometry().height() - self.cover.height()
|
||||
@ -274,14 +275,14 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
if cover:
|
||||
pm = QPixmap()
|
||||
pm.loadFromData(cover)
|
||||
if not pm.isNull():
|
||||
if not pm.isNull():
|
||||
self.cover.setPixmap(pm)
|
||||
|
||||
|
||||
def deduce_author_sort(self):
|
||||
au = unicode(self.authors.text())
|
||||
authors = string_to_authors(au)
|
||||
self.author_sort.setText(authors_to_sort_string(authors))
|
||||
|
||||
|
||||
def swap_title_author(self):
|
||||
title = self.title.text()
|
||||
self.title.setText(self.authors.text())
|
||||
@ -290,7 +291,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
|
||||
def cover_dropped(self):
|
||||
self.cover_changed = True
|
||||
|
||||
|
||||
def initialize_series(self):
|
||||
all_series = self.db.all_series()
|
||||
all_series.sort(cmp=lambda x, y : cmp(x[1], y[1]))
|
||||
@ -302,19 +303,19 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
idx = c
|
||||
self.series.addItem(name)
|
||||
c += 1
|
||||
|
||||
|
||||
self.series.lineEdit().setText('')
|
||||
if idx is not None:
|
||||
self.series.setCurrentIndex(idx)
|
||||
self.enable_series_index()
|
||||
|
||||
|
||||
pl = self.series.parentWidget().layout()
|
||||
for i in range(pl.count()):
|
||||
l = pl.itemAt(i).layout()
|
||||
if l:
|
||||
l.invalidate()
|
||||
l.activate()
|
||||
|
||||
|
||||
def initialize_series_and_publisher(self):
|
||||
self.initialize_series()
|
||||
all_publishers = self.db.all_publishers()
|
||||
@ -327,33 +328,33 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
idx = c
|
||||
self.publisher.addItem(name)
|
||||
c += 1
|
||||
|
||||
|
||||
self.publisher.setEditText('')
|
||||
if idx is not None:
|
||||
self.publisher.setCurrentIndex(idx)
|
||||
|
||||
|
||||
|
||||
|
||||
self.layout().activate()
|
||||
|
||||
|
||||
def edit_tags(self):
|
||||
d = TagEditor(self, self.db, self.row)
|
||||
d.exec_()
|
||||
if d.result() == QDialog.Accepted:
|
||||
tag_string = ', '.join(d.tags)
|
||||
self.tags.setText(tag_string)
|
||||
|
||||
|
||||
def lt_password_dialog(self):
|
||||
return PasswordDialog(self, 'LibraryThing account',
|
||||
return PasswordDialog(self, 'LibraryThing account',
|
||||
_('<p>Enter your username and password for <b>LibraryThing.com</b>. <br/>If you do not have one, you can <a href=\'http://www.librarything.com\'>register</a> for free!.</p>'))
|
||||
|
||||
|
||||
def change_password(self):
|
||||
d = self.lt_password_dialog()
|
||||
d = self.lt_password_dialog()
|
||||
d.exec_()
|
||||
|
||||
|
||||
def fetch_cover(self):
|
||||
isbn = qstring_to_unicode(self.isbn.text())
|
||||
if isbn:
|
||||
d = self.lt_password_dialog()
|
||||
d = self.lt_password_dialog()
|
||||
if not d.username() or not d.password():
|
||||
d.exec_()
|
||||
if d.result() != PasswordDialog.Accepted:
|
||||
@ -369,31 +370,31 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.pi.start(_('Downloading cover...'))
|
||||
self._hangcheck.start(100)
|
||||
else:
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
_('You must specify the ISBN identifier for this book.')).exec_()
|
||||
|
||||
|
||||
def hangcheck(self):
|
||||
if not (self.cover_fetcher.isFinished() or time.time()-self.cf_start_time > 150):
|
||||
return
|
||||
|
||||
|
||||
self._hangcheck.stop()
|
||||
try:
|
||||
if self.cover_fetcher.isRunning():
|
||||
self.cover_fetcher.terminate()
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
_('<b>Could not fetch cover.</b><br/>')+
|
||||
_('The download timed out.')).exec_()
|
||||
return
|
||||
if self.cover_fetcher.exception is not None:
|
||||
err = self.cover_fetcher.exception
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
_('<b>Could not fetch cover.</b><br/>')+repr(err)).exec_()
|
||||
return
|
||||
|
||||
|
||||
pix = QPixmap()
|
||||
pix.loadFromData(self.cover_fetcher.cover_data)
|
||||
if pix.isNull():
|
||||
error_dialog(self.window, _('Bad cover'),
|
||||
error_dialog(self.window, _('Bad cover'),
|
||||
_('The cover is not a valid picture')).exec_()
|
||||
else:
|
||||
self.cover.setPixmap(pix)
|
||||
@ -403,16 +404,17 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.fetch_cover_button.setEnabled(True)
|
||||
self.unsetCursor()
|
||||
self.pi.stop()
|
||||
|
||||
|
||||
|
||||
|
||||
def fetch_metadata(self):
|
||||
isbn = qstring_to_unicode(self.isbn.text())
|
||||
title = qstring_to_unicode(self.title.text())
|
||||
author = string_to_authors(unicode(self.authors.text()))[0]
|
||||
publisher = qstring_to_unicode(self.publisher.currentText())
|
||||
publisher = qstring_to_unicode(self.publisher.currentText())
|
||||
if isbn or title or author or publisher:
|
||||
d = FetchMetadata(self, isbn, title, author, publisher, self.timeout)
|
||||
d.exec_()
|
||||
with d:
|
||||
d.exec_()
|
||||
if d.result() == QDialog.Accepted:
|
||||
book = d.selected_book()
|
||||
if book:
|
||||
@ -428,13 +430,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
prefix += '\n'
|
||||
self.comments.setText(prefix + summ)
|
||||
else:
|
||||
error_dialog(self, _('Cannot fetch metadata'),
|
||||
error_dialog(self, _('Cannot fetch metadata'),
|
||||
_('You must specify at least one of ISBN, Title, '
|
||||
'Authors or Publisher'))
|
||||
|
||||
|
||||
def enable_series_index(self, *args):
|
||||
self.series_index.setEnabled(True)
|
||||
|
||||
|
||||
def remove_unused_series(self):
|
||||
self.db.remove_unused_series()
|
||||
idx = qstring_to_unicode(self.series.currentText())
|
||||
@ -445,15 +447,15 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
if qstring_to_unicode(self.series.itemText(i)) == idx:
|
||||
self.series.setCurrentIndex(i)
|
||||
break
|
||||
|
||||
|
||||
|
||||
|
||||
def accept(self):
|
||||
if self.formats_changed:
|
||||
self.sync_formats()
|
||||
title = qstring_to_unicode(self.title.text())
|
||||
self.db.set_title(self.id, title, notify=False)
|
||||
au = unicode(self.authors.text())
|
||||
if au:
|
||||
if au:
|
||||
self.db.set_authors(self.id, string_to_authors(au), notify=False)
|
||||
aus = qstring_to_unicode(self.author_sort.text())
|
||||
if aus:
|
||||
|
@ -5,7 +5,7 @@ import re, textwrap
|
||||
|
||||
DEPENDENCIES = [
|
||||
#(Generic, version, gentoo, ubuntu, fedora)
|
||||
('python', '2.5', None, None, None),
|
||||
('python', '2.6', None, None, None),
|
||||
('setuptools', '0.6c5', 'setuptools', 'python-setuptools', 'python-setuptools-devel'),
|
||||
('Python Imaging Library', '1.1.6', 'imaging', 'python-imaging', 'python-imaging'),
|
||||
('libusb', '0.1.12', None, None, None),
|
||||
@ -20,10 +20,10 @@ DEPENDENCIES = [
|
||||
('BeautifulSoup', '3.0.5', 'beautifulsoup', 'python-beautifulsoup', 'python-BeautifulSoup'),
|
||||
('dnspython', '1.6.0', 'dnspython', 'dnspython', 'dnspython', 'dnspython'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
class CoolDistro:
|
||||
|
||||
|
||||
def __init__(self, name, title, prefix=''):
|
||||
self.title = title
|
||||
url = prefix + '/chrome/dl/images/%s_logo.png'
|
||||
@ -39,7 +39,7 @@ def get_linux_data(version='1.0.0'):
|
||||
('foresight', 'Foresight 2.1'),
|
||||
('ubuntu', 'Ubuntu Jaunty Jackalope'),
|
||||
]:
|
||||
data['supported'].append(CoolDistro(name, title,
|
||||
data['supported'].append(CoolDistro(name, title,
|
||||
prefix='http://calibre.kovidgoyal.net'))
|
||||
data['dependencies'] = DEPENDENCIES
|
||||
return data
|
||||
@ -54,51 +54,51 @@ if __name__ == '__main__':
|
||||
return MarkupTemplate(raw).generate(**get_linux_data()).render('xhtml')
|
||||
index.exposed = True
|
||||
t = Test()
|
||||
t.index()
|
||||
t.index()
|
||||
cherrypy.quickstart(t)
|
||||
else:
|
||||
from pkg_resources import resource_filename
|
||||
|
||||
|
||||
from trac.core import Component, implements
|
||||
from trac.web.chrome import INavigationContributor, ITemplateProvider, add_stylesheet
|
||||
from trac.web.main import IRequestHandler
|
||||
from trac.util import Markup
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
DOWNLOAD_DIR = '/var/www/calibre.kovidgoyal.net/htdocs/downloads'
|
||||
MOBILEREAD = 'https://dev.mobileread.com/dist/kovid/calibre/'
|
||||
|
||||
|
||||
class OS(dict):
|
||||
"""Dictionary with a default value for unknown keys."""
|
||||
def __init__(self, dict):
|
||||
self.update(dict)
|
||||
if not dict.has_key('img'):
|
||||
self['img'] = self['name']
|
||||
|
||||
|
||||
|
||||
|
||||
class Download(Component):
|
||||
implements(INavigationContributor, IRequestHandler, ITemplateProvider)
|
||||
|
||||
|
||||
request_pat = re.compile(r'\/download$|\/download_\S+')
|
||||
|
||||
|
||||
# INavigationContributor methods
|
||||
def get_active_navigation_item(self, req):
|
||||
return 'download'
|
||||
|
||||
|
||||
def get_navigation_items(self, req):
|
||||
yield 'mainnav', 'download', Markup('<a href="/download">Get %s</a>'%(__appname__,))
|
||||
|
||||
|
||||
def get_templates_dirs(self):
|
||||
return [resource_filename(__name__, 'templates')]
|
||||
|
||||
|
||||
def get_htdocs_dirs(self):
|
||||
return [('dl', resource_filename(__name__, 'htdocs'))]
|
||||
|
||||
|
||||
# IRequestHandler methods
|
||||
def match_request(self, req):
|
||||
return self.__class__.request_pat.match(req.path_info)
|
||||
|
||||
|
||||
def process_request(self, req):
|
||||
add_stylesheet(req, 'dl/css/download.css')
|
||||
if req.path_info == '/download':
|
||||
@ -115,29 +115,29 @@ else:
|
||||
return self.osx(req)
|
||||
elif os == 'linux':
|
||||
return self.linux(req)
|
||||
|
||||
|
||||
def top_level(self, req):
|
||||
operating_systems = [
|
||||
OS({'name' : 'windows', 'title' : 'Windows'}),
|
||||
OS({'name' : 'osx', 'title' : 'OS X'}),
|
||||
OS({'name' : 'linux', 'title' : 'Linux'}),
|
||||
]
|
||||
data = dict(title='Get ' + __appname__,
|
||||
data = dict(title='Get ' + __appname__,
|
||||
operating_systems=operating_systems, width=200,
|
||||
font_size='xx-large', top_level=True)
|
||||
return 'download.html', data, None
|
||||
|
||||
|
||||
def version_from_filename(self):
|
||||
try:
|
||||
return open(DOWNLOAD_DIR+'/latest_version', 'rb').read().strip()
|
||||
except:
|
||||
return '0.0.0'
|
||||
|
||||
|
||||
def windows(self, req):
|
||||
version = self.version_from_filename()
|
||||
file = '%s-%s.exe'%(__appname__, version,)
|
||||
file = '%s-%s.exe'%(__appname__, version,)
|
||||
data = dict(version = version, name='windows',
|
||||
installer_name='Windows installer',
|
||||
installer_name='Windows installer',
|
||||
title='Download %s for windows'%(__appname__),
|
||||
compatibility='%s works on Windows XP and Windows Vista.'%(__appname__,),
|
||||
path=MOBILEREAD+file, app=__appname__,
|
||||
@ -146,7 +146,7 @@ else:
|
||||
<p>If you are using the <b>SONY PRS-500</b> and %(appname)s does not detect your reader, read on:</p>
|
||||
<blockquote>
|
||||
<p>
|
||||
If you are using 64-bit windows, you're out of luck.
|
||||
If you are using 64-bit windows, you're out of luck.
|
||||
</p>
|
||||
<p>
|
||||
There may be a conflict with the USB driver from SONY. In windows, you cannot install two drivers
|
||||
@ -160,7 +160,7 @@ else:
|
||||
</ul>
|
||||
You can uninstall a driver by right clicking on it and selecting uninstall.
|
||||
</li>
|
||||
<li>Once the drivers have been uninstalled, find the file prs500.inf (it will be in the
|
||||
<li>Once the drivers have been uninstalled, find the file prs500.inf (it will be in the
|
||||
driver folder in the folder in which you installed %(appname)s. Right click on it and
|
||||
select Install.</li>
|
||||
</ol>
|
||||
@ -168,12 +168,12 @@ else:
|
||||
</blockquote>
|
||||
'''%dict(appname=__appname__)))
|
||||
return 'binary.html', data, None
|
||||
|
||||
|
||||
def osx(self, req):
|
||||
version = self.version_from_filename()
|
||||
file = 'calibre-%s.dmg'%(version,)
|
||||
file = 'calibre-%s.dmg'%(version,)
|
||||
data = dict(version = version, name='osx',
|
||||
installer_name='OS X universal dmg',
|
||||
installer_name='OS X universal dmg',
|
||||
title='Download %s for OS X'%(__appname__),
|
||||
compatibility='%s works on OS X Tiger and above.'%(__appname__,),
|
||||
path=MOBILEREAD+file, app=__appname__,
|
||||
@ -181,57 +181,57 @@ else:
|
||||
u'''
|
||||
<ol>
|
||||
<li>Before trying to use the command line tools, you must run the app at least once. This will ask you for you password and then setup the symbolic links for the command line tools.</li>
|
||||
<li>The app cannot be run from within the dmg. You must drag it to a folder on your filesystem (The Desktop, Applications, wherever).</li>
|
||||
<li>The app cannot be run from within the dmg. You must drag it to a folder on your filesystem (The Desktop, Applications, wherever).</li>
|
||||
<li>In order for localization of the user interface in your language, select your language in the preferences (by pressing u\2318+P) and select your language.</li>
|
||||
</ol>
|
||||
'''))
|
||||
return 'binary.html', data, None
|
||||
|
||||
|
||||
def linux(self, req):
|
||||
data = get_linux_data(version=self.version_from_filename())
|
||||
return 'linux.html', data, None
|
||||
|
||||
|
||||
|
||||
|
||||
LINUX_INSTALLER = textwrap.dedent(r'''
|
||||
import sys, os, shutil, tarfile, subprocess, tempfile, urllib2, re, stat
|
||||
|
||||
|
||||
MOBILEREAD='https://dev.mobileread.com/dist/kovid/calibre/'
|
||||
|
||||
|
||||
class TerminalController:
|
||||
BOL = '' #: Move the cursor to the beginning of the line
|
||||
UP = '' #: Move the cursor up one line
|
||||
DOWN = '' #: Move the cursor down one line
|
||||
LEFT = '' #: Move the cursor left one char
|
||||
RIGHT = '' #: Move the cursor right one char
|
||||
|
||||
|
||||
# Deletion:
|
||||
CLEAR_SCREEN = '' #: Clear the screen and move to home position
|
||||
CLEAR_EOL = '' #: Clear to the end of the line.
|
||||
CLEAR_BOL = '' #: Clear to the beginning of the line.
|
||||
CLEAR_EOS = '' #: Clear to the end of the screen
|
||||
|
||||
|
||||
# Output modes:
|
||||
BOLD = '' #: Turn on bold mode
|
||||
BLINK = '' #: Turn on blink mode
|
||||
DIM = '' #: Turn on half-bright mode
|
||||
REVERSE = '' #: Turn on reverse-video mode
|
||||
NORMAL = '' #: Turn off all modes
|
||||
|
||||
|
||||
# Cursor display:
|
||||
HIDE_CURSOR = '' #: Make the cursor invisible
|
||||
SHOW_CURSOR = '' #: Make the cursor visible
|
||||
|
||||
|
||||
# Terminal size:
|
||||
COLS = None #: Width of the terminal (None for unknown)
|
||||
LINES = None #: Height of the terminal (None for unknown)
|
||||
|
||||
|
||||
# Foreground colors:
|
||||
BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
|
||||
|
||||
|
||||
# Background colors:
|
||||
BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
|
||||
BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
|
||||
|
||||
|
||||
_STRING_CAPABILITIES = """
|
||||
BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1
|
||||
CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold
|
||||
@ -239,29 +239,29 @@ else:
|
||||
HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split()
|
||||
_COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
|
||||
_ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
|
||||
|
||||
|
||||
def __init__(self, term_stream=sys.stdout):
|
||||
# Curses isn't available on all platforms
|
||||
try: import curses
|
||||
except: return
|
||||
|
||||
|
||||
# If the stream isn't a tty, then assume it has no capabilities.
|
||||
if not hasattr(term_stream, 'isatty') or not term_stream.isatty(): return
|
||||
|
||||
|
||||
# Check the terminal type. If we fail, then assume that the
|
||||
# terminal has no capabilities.
|
||||
try: curses.setupterm()
|
||||
except: return
|
||||
|
||||
|
||||
# Look up numeric capabilities.
|
||||
self.COLS = curses.tigetnum('cols')
|
||||
self.LINES = curses.tigetnum('lines')
|
||||
|
||||
|
||||
# Look up string capabilities.
|
||||
for capability in self._STRING_CAPABILITIES:
|
||||
(attrib, cap_name) = capability.split('=')
|
||||
setattr(self, attrib, self._tigetstr(cap_name) or '')
|
||||
|
||||
|
||||
# Colors
|
||||
set_fg = self._tigetstr('setf')
|
||||
if set_fg:
|
||||
@ -279,7 +279,7 @@ else:
|
||||
if set_bg_ansi:
|
||||
for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
|
||||
setattr(self, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '')
|
||||
|
||||
|
||||
def _tigetstr(self, cap_name):
|
||||
# String capabilities can include "delays" of the form "$<2>".
|
||||
# For any modern terminal, we should be able to just ignore
|
||||
@ -287,19 +287,19 @@ else:
|
||||
import curses
|
||||
cap = curses.tigetstr(cap_name) or ''
|
||||
return re.sub(r'\$<\d+>[/*]?', '', cap)
|
||||
|
||||
|
||||
def render(self, template):
|
||||
return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
|
||||
|
||||
|
||||
def _render_sub(self, match):
|
||||
s = match.group()
|
||||
if s == '$$': return s
|
||||
else: return getattr(self, s[2:-1])
|
||||
|
||||
|
||||
class ProgressBar:
|
||||
BAR = '%3d%% ${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}\n'
|
||||
HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n'
|
||||
|
||||
|
||||
def __init__(self, term, header):
|
||||
self.term = term
|
||||
if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL):
|
||||
@ -309,7 +309,7 @@ else:
|
||||
self.bar = term.render(self.BAR)
|
||||
self.header = self.term.render(self.HEADER % header.center(self.width))
|
||||
self.cleared = 1 #: true if we haven't drawn the bar yet.
|
||||
|
||||
|
||||
def update(self, percent, message=''):
|
||||
if isinstance(message, unicode):
|
||||
message = message.encode('utf-8', 'ignore')
|
||||
@ -323,14 +323,14 @@ else:
|
||||
(self.bar % (100*percent, '='*n, '-'*(self.width-10-n))) +
|
||||
self.term.CLEAR_EOL + msg)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def clear(self):
|
||||
if not self.cleared:
|
||||
sys.stdout.write(self.term.BOL + self.term.CLEAR_EOL +
|
||||
self.term.UP + self.term.CLEAR_EOL +
|
||||
self.term.UP + self.term.CLEAR_EOL)
|
||||
self.cleared = 1
|
||||
|
||||
|
||||
def download_tarball():
|
||||
try:
|
||||
pb = ProgressBar(TerminalController(sys.stdout), 'Downloading calibre...')
|
||||
@ -355,14 +355,14 @@ else:
|
||||
print '%d%%, '%int(percent*100),
|
||||
f.seek(0)
|
||||
return f
|
||||
|
||||
|
||||
def extract_tarball(tar, destdir):
|
||||
print 'Extracting application files...'
|
||||
if hasattr(tar, 'read'):
|
||||
subprocess.check_call(['tar', 'xjf', tar.name, '-C', destdir])
|
||||
else:
|
||||
subprocess.check_call(['tar', 'xjf', tar, '-C', destdir])
|
||||
|
||||
|
||||
def main():
|
||||
defdir = '/opt/calibre'
|
||||
destdir = raw_input('Enter the installation directory for calibre (Its contents will be deleted!)[%s]: '%defdir).strip()
|
||||
@ -372,12 +372,12 @@ else:
|
||||
if os.path.exists(destdir):
|
||||
shutil.rmtree(destdir)
|
||||
os.makedirs(destdir)
|
||||
|
||||
|
||||
f = download_tarball()
|
||||
|
||||
|
||||
print 'Extracting files to %s ...'%destdir
|
||||
extract_tarball(f, destdir)
|
||||
pi = os.path.join(destdir, 'calibre_postinstall')
|
||||
subprocess.call(pi, shell=True)
|
||||
return 0
|
||||
''')
|
||||
''')
|
||||
|
@ -32,7 +32,10 @@ def create_mail(from_, to, subject, text=None, attachment_data=None,
|
||||
if attachment_data is not None:
|
||||
from email.mime.base import MIMEBase
|
||||
assert attachment_data and attachment_name
|
||||
maintype, subtype = attachment_type.split('/', 1)
|
||||
try:
|
||||
maintype, subtype = attachment_type.split('/', 1)
|
||||
except AttributeError:
|
||||
maintype, subtype = 'application', 'octet-stream'
|
||||
msg = MIMEBase(maintype, subtype)
|
||||
msg.set_payload(attachment_data)
|
||||
encoders.encode_base64(msg)
|
||||
@ -50,9 +53,11 @@ def get_mx(host):
|
||||
|
||||
def sendmail_direct(from_, to, msg, timeout, localhost, verbose):
|
||||
import smtplib
|
||||
hosts = get_mx(to.split('@')[-1].strip())
|
||||
if 'darwin' in sys.platform:
|
||||
timeout=None # Non blocking sockets dont work on OS X
|
||||
s = smtplib.SMTP(timeout=timeout, local_hostname=localhost)
|
||||
s.set_debuglevel(verbose)
|
||||
hosts = get_mx(to.split('@')[-1].strip())
|
||||
if not hosts:
|
||||
raise ValueError('No mail server found for address: %s'%to)
|
||||
last_error = last_traceback = None
|
||||
@ -76,6 +81,8 @@ def sendmail(msg, from_, to, localhost=None, verbose=0, timeout=30,
|
||||
return sendmail_direct(from_, x, msg, timeout, localhost, verbose)
|
||||
import smtplib
|
||||
cls = smtplib.SMTP if encryption == 'TLS' else smtplib.SMTP_SSL
|
||||
if 'darwin' in sys.platform:
|
||||
timeout = None # Non-blocking sockets in OS X don't work
|
||||
s = cls(timeout=timeout, local_hostname=localhost)
|
||||
s.set_debuglevel(verbose)
|
||||
if port < 0:
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -651,11 +651,6 @@ class upload_to_pypi(OptionlessCommand):
|
||||
def run(self):
|
||||
check_call('python setup.py register'.split())
|
||||
check_call('rm -f dist/*', shell=True)
|
||||
check_call('sudo rm -rf build src/calibre/plugins/*', shell=True)
|
||||
os.mkdir('build')
|
||||
check_call('python2.5 setup.py build_ext bdist_egg --exclude-source-files upload'.split())
|
||||
check_call('sudo rm -rf build src/calibre/plugins/*', shell=True)
|
||||
os.mkdir('build')
|
||||
check_call('python setup.py build_ext bdist_egg --exclude-source-files upload'.split())
|
||||
check_call('python setup.py sdist upload'.split())
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user