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:
Kovid Goyal 2009-03-25 18:47:23 -07:00
parent 52d968838c
commit 9ca023e9a7
11 changed files with 480 additions and 460 deletions

View File

@ -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:]

View File

@ -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

View File

@ -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)

View File

@ -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__':

View File

@ -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,

View File

@ -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()

View File

@ -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:

View File

@ -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
''')
''')

View File

@ -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

View File

@ -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())