Update to 0.4.72 from trunk

This commit is contained in:
Kovid Goyal 2008-06-16 14:09:33 -07:00
commit cd1340871c
29 changed files with 680 additions and 530 deletions

View File

@ -1,8 +1,10 @@
PYTHON = python PYTHON = python
all : plugins pictureflow gui2 translations resources all : plugins gui2 translations resources
plugins: plugins : src/calibre/plugins pictureflow
src/calibre/plugins:
mkdir -p src/calibre/plugins mkdir -p src/calibre/plugins
clean : clean :

153
linux_installer.py Normal file
View File

@ -0,0 +1,153 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Create linux binary.
'''
import glob, sys, subprocess, tarfile, os, re
HOME = '/home/kovid'
PYINSTALLER = os.path.expanduser('~/build/pyinstaller')
CALIBREPREFIX = '___'
CLIT = '/usr/bin/clit'
PDFTOHTML = '/usr/bin/pdftohtml'
LIBUNRAR = '/usr/lib/libunrar.so'
QTDIR = '/usr/lib/qt4'
QTDLLS = ('QtCore', 'QtGui', 'QtNetwork', 'QtSvg', 'QtXml')
EXTRAS = ('/usr/lib/python2.5/site-packages/PIL', os.path.expanduser('~/ipython/IPython'))
CALIBRESRC = os.path.join(CALIBREPREFIX, 'src')
CALIBREPLUGINS = os.path.join(CALIBRESRC, 'calibre', 'plugins')
sys.path.insert(0, CALIBRESRC)
from calibre import __version__
def run_pyinstaller(args=sys.argv):
subprocess.check_call(('/usr/bin/sudo', 'chown', '-R', 'kovid:users', glob.glob('/usr/lib/python*/site-packages/')[-1]))
subprocess.check_call('rm -rf %(py)s/dist/* %(py)s/build/*'%dict(py=PYINSTALLER), shell=True)
subprocess.check_call('make plugins', shell=True)
cp = HOME+'/build/'+os.path.basename(os.getcwd())
spec = open(os.path.join(PYINSTALLER, 'calibre', 'calibre.spec'), 'wb')
raw = re.sub(r'CALIBREPREFIX\s+=\s+\'___\'', 'CALIBREPREFIX = '+repr(cp),
open(__file__).read())
spec.write(raw)
spec.close()
os.chdir(PYINSTALLER)
subprocess.check_call('python -OO Build.py calibre/calibre.spec', shell=True)
return 0
if __name__ == '__main__' and 'linux_installer.py' in __file__:
sys.exit(run_pyinstaller())
loader = os.path.join(os.path.expanduser('~/temp'), 'calibre_installer_loader.py')
if not os.path.exists(loader):
open(loader, 'wb').write('''
import sys, os
sys.frozen_path = os.getcwd()
os.chdir(os.environ.get("ORIGWD", "."))
sys.path.insert(0, os.path.join(sys.frozen_path, "library.pyz"))
sys.path.insert(0, sys.frozen_path)
from PyQt4.QtCore import QCoreApplication
QCoreApplication.setLibraryPaths([sys.frozen_path, os.path.join(sys.frozen_path, "plugins")])
''')
excludes = ['gtk._gtk', 'gtk.glade', 'qt', 'matplotlib.nxutils', 'matplotlib._cntr',
'matplotlib.ttconv', 'matplotlib._image', 'matplotlib.ft2font',
'matplotlib._transforms', 'matplotlib._agg', 'matplotlib.backends._backend_agg',
'matplotlib.axes', 'matplotlib', 'matplotlib.pyparsing',
'TKinter', 'atk', 'gobject._gobject', 'pango', 'PIL', 'Image', 'IPython']
temp = ['keyword', 'codeop']
recipes = ['calibre', 'web', 'feeds', 'recipes']
prefix = '.'.join(recipes)+'.'
for f in glob.glob(os.path.join(CALIBRESRC, *(recipes+['*.py']))):
temp.append(prefix + os.path.basename(f).partition('.')[0])
hook = os.path.expanduser('~/temp/hook-calibre.py')
f = open(hook, 'wb')
hook_script = 'hiddenimports = %s'%repr(temp)
f.write(hook_script)
sys.path.insert(0, CALIBRESRC)
from calibre.linux import entry_points
executables, scripts = ['calibre_postinstall', 'parallel'], \
[os.path.join(CALIBRESRC, 'calibre', 'linux.py'), os.path.join(CALIBRESRC, 'calibre', 'parallel.py')]
for entry in entry_points['console_scripts'] + entry_points['gui_scripts']:
fields = entry.split('=')
executables.append(fields[0].strip())
scripts.append(os.path.join(CALIBRESRC, *map(lambda x: x.strip(), fields[1].split(':')[0].split('.')))+'.py')
recipes = Analysis(glob.glob(os.path.join(CALIBRESRC, 'calibre', 'web', 'feeds', 'recipes', '*.py')),
pathex=[CALIBRESRC], hookspath=[os.path.dirname(hook)], excludes=excludes)
analyses = [Analysis([os.path.join(HOMEPATH,'support/_mountzlib.py'), os.path.join(HOMEPATH,'support/useUnicode.py'), loader, script],
pathex=[PYINSTALLER, CALIBRESRC, CALIBREPLUGINS], excludes=excludes) for script in scripts]
pyz = TOC()
binaries = TOC()
for a in analyses:
pyz = a.pure + pyz
binaries = a.binaries + binaries
pyz = PYZ(pyz + recipes.pure, name='library.pyz')
built_executables = []
for script, exe, a in zip(scripts, executables, analyses):
built_executables.append(EXE(PYZ(TOC()),
a.scripts+[('O','','OPTION'),],
exclude_binaries=1,
name=os.path.join('buildcalibre', exe),
debug=False,
strip=True,
upx=False,
excludes=excludes,
console=1))
print 'Adding plugins...'
for f in glob.glob(os.path.join(CALIBREPLUGINS, '*.so')):
binaries += [(os.path.basename(f), f, 'BINARY')]
print 'Adding external programs...'
binaries += [('clit', CLIT, 'BINARY'), ('pdftohtml', PDFTOHTML, 'BINARY'),
('libunrar.so', LIBUNRAR, 'BINARY')]
qt = []
for dll in QTDLLS:
path = os.path.join(QTDIR, 'lib'+dll+'.so.4')
qt.append((os.path.basename(path), path, 'BINARY'))
binaries += 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 'codcs' in dirpath or 'sqldrivers' in dirpath : continue
f = os.path.join(dirpath, f)
plugins.append(('plugins/'+f.replace(plugdir, ''), f, 'BINARY'))
binaries += plugins
manifest = '/tmp/manifest'
open(manifest, 'wb').write('\\n'.join(executables))
version = '/tmp/version'
open(version, 'wb').write(__version__)
coll = COLLECT(binaries, pyz,
[('manifest', manifest, 'DATA'), ('version', version, 'DATA')],
*built_executables,
**dict(strip=True,
upx=False,
excludes=excludes,
name='dist'))
os.chdir(os.path.join(HOMEPATH, 'calibre', 'dist'))
for folder in EXTRAS:
subprocess.check_call('cp -rf %s .'%folder, shell=True)
print 'Building tarball...'
tbz2 = 'calibre-%s-i686.tar.bz2'%__version__
tf = tarfile.open(os.path.join('/tmp', tbz2), 'w:bz2')
for f in os.listdir('.'):
tf.add(f)

View File

@ -99,8 +99,7 @@ _check_symlinks_prescript()
includes=list(self.includes) + main_modules['console'], includes=list(self.includes) + main_modules['console'],
packages=self.packages, packages=self.packages,
excludes=self.excludes, excludes=self.excludes,
debug=debug, debug=debug)
)
@classmethod @classmethod
def makedmg(cls, d, volname, def makedmg(cls, d, volname,
@ -249,6 +248,11 @@ _check_symlinks_prescript()
else: else:
os.link(src, os.path.join(module_dir, dest)) os.link(src, os.path.join(module_dir, dest))
print print
print 'Adding IPython'
dst = os.path.join(resource_dir, 'lib', 'python2.5', 'IPython')
if os.path.exists(dst): shutil.rmtree(dst)
shutil.copytree(os.path.expanduser('~/build/ipython/IPython'), dst)
print
print 'Installing prescipt' print 'Installing prescipt'
sf = [os.path.basename(s) for s in all_names] sf = [os.path.basename(s) for s in all_names]
cs = BuildAPP.CHECK_SYMLINKS_PRESCRIPT % dict(dest_path=repr('/usr/bin'), cs = BuildAPP.CHECK_SYMLINKS_PRESCRIPT % dict(dest_path=repr('/usr/bin'),
@ -277,13 +281,7 @@ sys.frameworks_dir = os.path.join(os.path.dirname(os.environ['RESOURCEPATH']), '
def main(): def main():
# auto = '--auto' in sys.argv sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
# if auto:
# sys.argv.remove('--auto')
# if auto and not os.path.exists('dist/auto'):
# print '%s does not exist'%os.path.abspath('dist/auto')
# return 1
#
sys.argv[1:2] = ['py2app'] sys.argv[1:2] = ['py2app']
setup( setup(
name = APPNAME, name = APPNAME,
@ -300,12 +298,12 @@ def main():
'PyQt4.QtSvg', 'PyQt4.QtSvg',
'mechanize', 'ClientForm', 'usbobserver', 'mechanize', 'ClientForm', 'usbobserver',
'genshi', 'calibre.web.feeds.recipes.*', 'genshi', 'calibre.web.feeds.recipes.*',
'IPython.Extensions.*', 'pydoc'], 'keyword', 'codeop', 'pydoc'],
'packages' : ['PIL', 'Authorization', 'rtf2xml', 'lxml'], 'packages' : ['PIL', 'Authorization', 'rtf2xml', 'lxml'],
'excludes' : [], 'excludes' : ['IPython'],
'plist' : { 'CFBundleGetInfoString' : '''calibre, an E-book management application.''' 'plist' : { 'CFBundleGetInfoString' : '''calibre, an E-book management application.'''
''' Visit http://calibre.kovidgoyal.net for details.''', ''' Visit http://calibre.kovidgoyal.net for details.''',
'CFBundleIdentifier':'net.kovidgoyal.librs500', 'CFBundleIdentifier':'net.kovidgoyal.calibre',
'CFBundleShortVersionString':VERSION, 'CFBundleShortVersionString':VERSION,
'CFBundleVersion':APPNAME + ' ' + VERSION, 'CFBundleVersion':APPNAME + ' ' + VERSION,
'LSMinimumSystemVersion':'10.4.3', 'LSMinimumSystemVersion':'10.4.3',

View File

@ -1,7 +1,7 @@
''' E-book management software''' ''' E-book management software'''
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
__version__ = '0.4.70' __version__ = '0.4.72'
__docformat__ = "epytext" __docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid at kovidgoyal.net>" __author__ = "Kovid Goyal <kovid at kovidgoyal.net>"
__appname__ = 'calibre' __appname__ = 'calibre'
@ -447,14 +447,13 @@ class Settings(QSettings):
self.setValue(str(key), QVariant(QByteArray(val))) self.setValue(str(key), QVariant(QByteArray(val)))
_settings = Settings() _settings = Settings()
if not _settings.get('rationalized'): if not _settings.get('rationalized'):
__settings = Settings(name='calibre') __settings = Settings(name='calibre')
dbpath = os.path.join(os.path.expanduser('~'), 'library1.db').decode(sys.getfilesystemencoding()) dbpath = os.path.join(os.path.expanduser('~'), 'library1.db').decode(sys.getfilesystemencoding())
dbpath = unicode(__settings.value('database path', dbpath = unicode(__settings.value('database path',
QVariant(QString.fromUtf8(dbpath.encode('utf-8')))).toString()) QVariant(QString.fromUtf8(dbpath.encode('utf-8')))).toString())
cmdline = __settings.value('LRF conversion defaults', QVariant(QByteArray(''))).toByteArray().data() cmdline = __settings.value('LRF conversion defaults', QVariant(QByteArray(''))).toByteArray().data()
_settings.set('database path', dbpath) _settings.set('database path', dbpath)
if cmdline: if cmdline:
cmdline = cPickle.loads(cmdline) cmdline = cPickle.loads(cmdline)
@ -464,6 +463,7 @@ if not _settings.get('rationalized'):
os.unlink(unicode(__settings.fileName())) os.unlink(unicode(__settings.fileName()))
except: except:
pass pass
_settings.set('database path', dbpath)
_spat = re.compile(r'^the\s+|^a\s+|^an\s+', re.IGNORECASE) _spat = re.compile(r'^the\s+|^a\s+|^an\s+', re.IGNORECASE)
def english_sort(x, y): def english_sort(x, y):

View File

@ -275,10 +275,10 @@ class PRS505(Device):
if not iswindows: if not iswindows:
if self._main_prefix is not None: if self._main_prefix is not None:
stats = os.statvfs(self._main_prefix) stats = os.statvfs(self._main_prefix)
msz = stats.f_bsize * stats.f_bavail msz = stats.f_frsize * stats.f_bavail
if self._card_prefix is not None: if self._card_prefix is not None:
stats = os.statvfs(self._card_prefix) stats = os.statvfs(self._card_prefix)
csz = stats.f_bsize * stats.f_bavail csz = stats.f_frsize * stats.f_bavail
else: else:
msz = self._windows_space(self._main_prefix)[1] msz = self._windows_space(self._main_prefix)[1]
csz = self._windows_space(self._card_prefix)[1] csz = self._windows_space(self._card_prefix)[1]

View File

@ -12,7 +12,7 @@ from PyQt4.QtGui import QDialog, QItemSelectionModel
from calibre.gui2.dialogs.fetch_metadata_ui import Ui_FetchMetadata from calibre.gui2.dialogs.fetch_metadata_ui import Ui_FetchMetadata
from calibre.gui2 import error_dialog, NONE, info_dialog from calibre.gui2 import error_dialog, NONE, info_dialog
from calibre.ebooks.metadata.isbndb import create_books, option_parser from calibre.ebooks.metadata.isbndb import create_books, option_parser, ISBNDBError
from calibre import Settings from calibre import Settings
class Matches(QAbstractTableModel): class Matches(QAbstractTableModel):
@ -88,8 +88,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
self.connect(self.matches, SIGNAL('activated(QModelIndex)'), self.chosen) self.connect(self.matches, SIGNAL('activated(QModelIndex)'), self.chosen)
key = str(self.key.text()) key = str(self.key.text())
if key: if key:
QTimer.singleShot(100, self.fetch.click) QTimer.singleShot(100, self.fetch_metadata)
def show_summary(self, current, previous): def show_summary(self, current, previous):
@ -106,7 +105,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
_('You must specify a valid access key for isbndb.com')) _('You must specify a valid access key for isbndb.com'))
return return
else: else:
Settings().set('isbndb.com key', str(self.key.text())) Settings().set('isbndb.com key', key)
args = ['isbndb'] args = ['isbndb']
if self.isbn: if self.isbn:
@ -121,36 +120,41 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
self.fetch.setEnabled(False) self.fetch.setEnabled(False)
self.setCursor(Qt.WaitCursor) self.setCursor(Qt.WaitCursor)
QCoreApplication.instance().processEvents() QCoreApplication.instance().processEvents()
try:
args.append(key) args.append(key)
parser = option_parser() parser = option_parser()
opts, args = parser.parse_args(args) opts, args = parser.parse_args(args)
self.logger = logging.getLogger('Job #'+str(id)) self.logger = logging.getLogger('Job #'+str(id))
self.logger.setLevel(logging.DEBUG) self.logger.setLevel(logging.DEBUG)
self.log_dest = cStringIO.StringIO() self.log_dest = cStringIO.StringIO()
handler = logging.StreamHandler(self.log_dest) handler = logging.StreamHandler(self.log_dest)
handler.setLevel(logging.DEBUG) handler.setLevel(logging.DEBUG)
handler.setFormatter(logging.Formatter('[%(levelname)s] %(filename)s:%(lineno)s: %(message)s')) handler.setFormatter(logging.Formatter('[%(levelname)s] %(filename)s:%(lineno)s: %(message)s'))
self.logger.addHandler(handler) self.logger.addHandler(handler)
books = create_books(opts, args, self.logger, self.timeout) try:
books = create_books(opts, args, self.logger, self.timeout)
self.model = Matches(books) except ISBNDBError, err:
if self.model.rowCount() < 1: error_dialog(self, _('Error fetching metadata'), str(err)).exec_()
info_dialog(self, _('No metadata found'), _('No metadata found, try adjusting the title and author or the ISBN key.')).exec_() return
self.reject()
self.model = Matches(books)
self.matches.setModel(self.model) if self.model.rowCount() < 1:
QObject.connect(self.matches.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'), info_dialog(self, _('No metadata found'), _('No metadata found, try adjusting the title and author or the ISBN key.')).exec_()
self.show_summary) self.reject()
self.model.reset()
self.matches.selectionModel().select(self.model.index(0, 0), self.matches.setModel(self.model)
QItemSelectionModel.Select | QItemSelectionModel.Rows) QObject.connect(self.matches.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
self.matches.setCurrentIndex(self.model.index(0, 0)) self.show_summary)
self.fetch.setEnabled(True) self.model.reset()
self.unsetCursor() self.matches.selectionModel().select(self.model.index(0, 0),
self.matches.resizeColumnsToContents() QItemSelectionModel.Select | QItemSelectionModel.Rows)
self.matches.setCurrentIndex(self.model.index(0, 0))
finally:
self.fetch.setEnabled(True)
self.unsetCursor()
self.matches.resizeColumnsToContents()

View File

@ -47,7 +47,7 @@
<item> <item>
<widget class="QLabel" name="label_2" > <widget class="QLabel" name="label_2" >
<property name="text" > <property name="text" >
<string>&amp;Access Key;</string> <string>&amp;Access Key:</string>
</property> </property>
<property name="buddy" > <property name="buddy" >
<cstring>key</cstring> <cstring>key</cstring>

View File

@ -6,7 +6,7 @@ add/remove formats
''' '''
import os import os
from PyQt4.QtCore import SIGNAL, QObject, QCoreApplication, Qt, QVariant from PyQt4.QtCore import SIGNAL, QObject, QCoreApplication, Qt
from PyQt4.QtGui import QPixmap, QListWidgetItem, QErrorMessage, QDialog from PyQt4.QtGui import QPixmap, QListWidgetItem, QErrorMessage, QDialog

View File

@ -8,8 +8,8 @@ from math import cos, sin, pi
from itertools import repeat from itertools import repeat
from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \ from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \ QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
QPen, QStyle, QPainter, QLineEdit, QApplication, \ QPen, QStyle, QPainter, QLineEdit, \
QPalette, QImage QPalette, QImage, QApplication
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \ from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
QCoreApplication, SIGNAL, QObject, QSize, QModelIndex QCoreApplication, SIGNAL, QObject, QSize, QModelIndex
@ -22,7 +22,7 @@ class LibraryDelegate(QItemDelegate):
COLOR = QColor("blue") COLOR = QColor("blue")
SIZE = 16 SIZE = 16
PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
def __init__(self, parent): def __init__(self, parent):
QItemDelegate.__init__(self, parent) QItemDelegate.__init__(self, parent)
self.star_path = QPainterPath() self.star_path = QPainterPath()
@ -41,10 +41,10 @@ class LibraryDelegate(QItemDelegate):
def sizeHint(self, option, index): def sizeHint(self, option, index):
#num = index.model().data(index, Qt.DisplayRole).toInt()[0] #num = index.model().data(index, Qt.DisplayRole).toInt()[0]
return QSize(5*(self.SIZE), self.SIZE+4) return QSize(5*(self.SIZE), self.SIZE+4)
def paint(self, painter, option, index): def paint(self, painter, option, index):
num = index.model().data(index, Qt.DisplayRole).toInt()[0] num = index.model().data(index, Qt.DisplayRole).toInt()[0]
def draw_star(): def draw_star():
painter.save() painter.save()
painter.scale(self.factor, self.factor) painter.scale(self.factor, self.factor)
painter.translate(50.0, 50.0) painter.translate(50.0, 50.0)
@ -52,16 +52,18 @@ class LibraryDelegate(QItemDelegate):
painter.translate(-50.0, -50.0) painter.translate(-50.0, -50.0)
painter.drawPath(self.star_path) painter.drawPath(self.star_path)
painter.restore() painter.restore()
painter.save() painter.save()
if hasattr(QStyle, 'CE_ItemViewItem'):
QApplication.style().drawControl(QStyle.CE_ItemViewItem, option, painter)
elif option.state & QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight())
try: try:
if option.state & QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight())
painter.setRenderHint(QPainter.Antialiasing) painter.setRenderHint(QPainter.Antialiasing)
y = option.rect.center().y()-self.SIZE/2. y = option.rect.center().y()-self.SIZE/2.
x = option.rect.right() - self.SIZE x = option.rect.right() - self.SIZE
painter.setPen(self.PEN) painter.setPen(self.PEN)
painter.setBrush(self.brush) painter.setBrush(self.brush)
painter.translate(x, y) painter.translate(x, y)
i = 0 i = 0
while i < num: while i < num:
@ -71,7 +73,7 @@ class LibraryDelegate(QItemDelegate):
except Exception, e: except Exception, e:
traceback.print_exc(e) traceback.print_exc(e)
painter.restore() painter.restore()
def createEditor(self, parent, option, index): def createEditor(self, parent, option, index):
sb = QItemDelegate.createEditor(self, parent, option, index) sb = QItemDelegate.createEditor(self, parent, option, index)
sb.setMinimum(0) sb.setMinimum(0)
@ -105,22 +107,22 @@ class BooksModel(QAbstractTableModel):
self.read_config() self.read_config()
self.buffer_size = buffer self.buffer_size = buffer
self.cover_cache = None self.cover_cache = None
def clear_caches(self): def clear_caches(self):
if self.cover_cache: if self.cover_cache:
self.cover_cache.clear_cache() self.cover_cache.clear_cache()
def read_config(self): def read_config(self):
self.use_roman_numbers = Settings().get('use roman numerals for series number', True) self.use_roman_numbers = Settings().get('use roman numerals for series number', True)
def set_database(self, db): def set_database(self, db):
if isinstance(db, (QString, basestring)): if isinstance(db, (QString, basestring)):
if isinstance(db, QString): if isinstance(db, QString):
db = qstring_to_unicode(db) db = qstring_to_unicode(db)
db = LibraryDatabase(os.path.expanduser(db)) db = LibraryDatabase(os.path.expanduser(db))
self.db = db self.db = db
def refresh_ids(self, ids, current_row=-1): def refresh_ids(self, ids, current_row=-1):
rows = self.db.refresh_ids(ids) rows = self.db.refresh_ids(ids)
for row in rows: for row in rows:
@ -132,24 +134,24 @@ class BooksModel(QAbstractTableModel):
self.get_book_display_info(row)) self.get_book_display_info(row))
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
self.index(row, 0), self.index(row, self.columnCount(None)-1)) self.index(row, 0), self.index(row, self.columnCount(None)-1))
def close(self): def close(self):
self.db.close() self.db.close()
self.db = None self.db = None
self.reset() self.reset()
def add_books(self, paths, formats, metadata, uris=[], add_duplicates=False): def add_books(self, paths, formats, metadata, uris=[], add_duplicates=False):
return self.db.add_books(paths, formats, metadata, uris, return self.db.add_books(paths, formats, metadata, uris,
add_duplicates=add_duplicates) add_duplicates=add_duplicates)
def row_indices(self, index): def row_indices(self, index):
''' Return list indices of all cells in index.row()''' ''' Return list indices of all cells in index.row()'''
return [ self.index(index.row(), c) for c in range(self.columnCount(None))] return [ self.index(index.row(), c) for c in range(self.columnCount(None))]
def save_to_disk(self, rows, path, single_dir=False): def save_to_disk(self, rows, path, single_dir=False):
rows = [row.row() for row in rows] rows = [row.row() for row in rows]
self.db.export_to_dir(path, rows, self.sorted_on[0] == 1, single_dir=single_dir) self.db.export_to_dir(path, rows, self.sorted_on[0] == 1, single_dir=single_dir)
def delete_books(self, indices): def delete_books(self, indices):
ids = [ self.id(i) for i in indices ] ids = [ self.id(i) for i in indices ]
for id in ids: for id in ids:
@ -159,10 +161,10 @@ class BooksModel(QAbstractTableModel):
self.endRemoveRows() self.endRemoveRows()
self.clear_caches() self.clear_caches()
self.reset() self.reset()
def search_tokens(self, text): def search_tokens(self, text):
return text_to_tokens(text) return text_to_tokens(text)
def search(self, text, refinement, reset=True): def search(self, text, refinement, reset=True):
tokens, OR = self.search_tokens(text) tokens, OR = self.search_tokens(text)
self.db.filter(tokens, refilter=refinement, OR=OR) self.db.filter(tokens, refilter=refinement, OR=OR)
@ -170,7 +172,7 @@ class BooksModel(QAbstractTableModel):
if reset: if reset:
self.clear_caches() self.clear_caches()
self.reset() self.reset()
def sort(self, col, order, reset=True): def sort(self, col, order, reset=True):
if not self.db: if not self.db:
return return
@ -179,30 +181,30 @@ class BooksModel(QAbstractTableModel):
self.research() self.research()
if reset: if reset:
self.clear_caches() self.clear_caches()
self.reset() self.reset()
self.sorted_on = (col, order) self.sorted_on = (col, order)
def resort(self, reset=True): def resort(self, reset=True):
self.sort(*self.sorted_on, **dict(reset=reset)) self.sort(*self.sorted_on, **dict(reset=reset))
def research(self, reset=True): def research(self, reset=True):
self.search(self.last_search, False, reset=reset) self.search(self.last_search, False, reset=reset)
def database_needs_migration(self): def database_needs_migration(self):
path = os.path.expanduser('~/library.db') path = os.path.expanduser('~/library.db')
return self.db.is_empty() and \ return self.db.is_empty() and \
os.path.exists(path) and\ os.path.exists(path) and\
LibraryDatabase.sizeof_old_database(path) > 0 LibraryDatabase.sizeof_old_database(path) > 0
def columnCount(self, parent): def columnCount(self, parent):
return len(self.cols) return len(self.cols)
def rowCount(self, parent): def rowCount(self, parent):
return self.db.rows() if self.db else 0 return self.db.rows() if self.db else 0
def count(self): def count(self):
return self.rowCount(None) return self.rowCount(None)
def get_book_display_info(self, idx): def get_book_display_info(self, idx):
data = {} data = {}
cdata = self.cover(idx) cdata = self.cover(idx)
@ -229,9 +231,9 @@ class BooksModel(QAbstractTableModel):
sidx = self.db.series_index(idx) sidx = self.db.series_index(idx)
sidx = self.__class__.roman(sidx) if self.use_roman_numbers else str(sidx) sidx = self.__class__.roman(sidx) if self.use_roman_numbers else str(sidx)
data[_('Series')] = _('Book <font face="serif">%s</font> of %s.')%(sidx, series) data[_('Series')] = _('Book <font face="serif">%s</font> of %s.')%(sidx, series)
return data return data
def set_cache(self, idx): def set_cache(self, idx):
l, r = 0, self.count()-1 l, r = 0, self.count()-1
if self.cover_cache: if self.cover_cache:
@ -244,7 +246,7 @@ class BooksModel(QAbstractTableModel):
ids = ids + [i for i in range(l, r, 1) if i not in ids] ids = ids + [i for i in range(l, r, 1) if i not in ids]
ids = [self.db.id(i) for i in ids] ids = [self.db.id(i) for i in ids]
self.cover_cache.set_cache(ids) self.cover_cache.set_cache(ids)
def current_changed(self, current, previous, emit_signal=True): def current_changed(self, current, previous, emit_signal=True):
idx = current.row() idx = current.row()
self.set_cache(idx) self.set_cache(idx)
@ -253,7 +255,7 @@ class BooksModel(QAbstractTableModel):
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data) self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
else: else:
return data return data
def get_book_info(self, index): def get_book_info(self, index):
data = self.current_changed(index, None, False) data = self.current_changed(index, None, False)
row = index.row() row = index.row()
@ -264,8 +266,8 @@ class BooksModel(QAbstractTableModel):
au = ', '.join([a.strip() for a in au.split(',')]) au = ', '.join([a.strip() for a in au.split(',')])
data[_('Author(s)')] = au data[_('Author(s)')] = au
return data return data
def get_metadata(self, rows): def get_metadata(self, rows):
metadata = [] metadata = []
for row in rows: for row in rows:
@ -296,11 +298,11 @@ class BooksModel(QAbstractTableModel):
'comments': self.db.comments(row), 'comments': self.db.comments(row),
} }
if series is not None: if series is not None:
mi['tag order'] = {series:self.db.books_in_series_of(row)} mi['tag order'] = {series:self.db.books_in_series_of(row)}
metadata.append(mi) metadata.append(mi)
return metadata return metadata
def get_preferred_formats(self, rows, formats): def get_preferred_formats(self, rows, formats):
ans = [] ans = []
for row in (row.row() for row in rows): for row in (row.row() for row in rows):
@ -313,17 +315,17 @@ class BooksModel(QAbstractTableModel):
pt = PersistentTemporaryFile(suffix='.'+format) pt = PersistentTemporaryFile(suffix='.'+format)
pt.write(self.db.format(row, format)) pt.write(self.db.format(row, format))
pt.seek(0) pt.seek(0)
ans.append(pt) ans.append(pt)
else: else:
ans.append(None) ans.append(None)
return ans return ans
def id(self, row): def id(self, row):
return self.db.id(row.row()) return self.db.id(row.row())
def title(self, row_number): def title(self, row_number):
return self.db.title(row_number) return self.db.title(row_number)
def cover(self, row_number): def cover(self, row_number):
id = self.db.id(row_number) id = self.db.id(row_number)
data = None data = None
@ -342,15 +344,15 @@ class BooksModel(QAbstractTableModel):
if img.isNull(): if img.isNull():
img = self.default_image img = self.default_image
return img return img
def data(self, index, role): def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole: if role == Qt.DisplayRole or role == Qt.EditRole:
row, col = index.row(), index.column() row, col = index.row(), index.column()
if col == 0: if col == 0:
text = self.db.title(row) text = self.db.title(row)
if text: if text:
return QVariant(text) return QVariant(text)
elif col == 1: elif col == 1:
au = self.db.authors(row) au = self.db.authors(row)
if au: if au:
au = au.split(',') au = au.split(',')
@ -364,18 +366,18 @@ class BooksModel(QAbstractTableModel):
if dt: if dt:
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight) dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
return QVariant(dt.strftime(BooksView.TIME_FMT).decode(preferred_encoding, 'replace')) return QVariant(dt.strftime(BooksView.TIME_FMT).decode(preferred_encoding, 'replace'))
elif col == 4: elif col == 4:
r = self.db.rating(row) r = self.db.rating(row)
r = r/2 if r else 0 r = r/2 if r else 0
return QVariant(r) return QVariant(r)
elif col == 5: elif col == 5:
pub = self.db.publisher(row) pub = self.db.publisher(row)
if pub: if pub:
return QVariant(pub) return QVariant(pub)
elif col == 6: elif col == 6:
tags = self.db.tags(row) tags = self.db.tags(row)
if tags: if tags:
return QVariant(', '.join(tags.split(','))) return QVariant(', '.join(tags.split(',')))
elif col == 7: elif col == 7:
series = self.db.series(row) series = self.db.series(row)
if series: if series:
@ -383,16 +385,16 @@ class BooksModel(QAbstractTableModel):
return NONE return NONE
elif role == Qt.TextAlignmentRole and index.column() in [2, 3, 4]: elif role == Qt.TextAlignmentRole and index.column() in [2, 3, 4]:
return QVariant(Qt.AlignRight | Qt.AlignVCenter) return QVariant(Qt.AlignRight | Qt.AlignVCenter)
elif role == Qt.ToolTipRole and index.isValid(): elif role == Qt.ToolTipRole and index.isValid():
if index.column() in self.editable_cols: if index.column() in self.editable_cols:
return QVariant(_("Double click to <b>edit</b> me<br><br>")) return QVariant(_("Double click to <b>edit</b> me<br><br>"))
return NONE return NONE
def headerData(self, section, orientation, role): def headerData(self, section, orientation, role):
if role != Qt.DisplayRole: if role != Qt.DisplayRole:
return NONE return NONE
text = "" text = ""
if orientation == Qt.Horizontal: if orientation == Qt.Horizontal:
if section == 0: text = _("Title") if section == 0: text = _("Title")
elif section == 1: text = _("Author(s)") elif section == 1: text = _("Author(s)")
elif section == 2: text = _("Size (MB)") elif section == 2: text = _("Size (MB)")
@ -402,16 +404,16 @@ class BooksModel(QAbstractTableModel):
elif section == 6: text = _("Tags") elif section == 6: text = _("Tags")
elif section == 7: text = _("Series") elif section == 7: text = _("Series")
return QVariant(text) return QVariant(text)
else: else:
return QVariant(section+1) return QVariant(section+1)
def flags(self, index): def flags(self, index):
flags = QAbstractTableModel.flags(self, index) flags = QAbstractTableModel.flags(self, index)
if index.isValid(): if index.isValid():
if index.column() in self.editable_cols: if index.column() in self.editable_cols:
flags |= Qt.ItemIsEditable flags |= Qt.ItemIsEditable
return flags return flags
def setData(self, index, value, role): def setData(self, index, value, role):
done = False done = False
if role == Qt.EditRole: if role == Qt.EditRole:
@ -424,7 +426,7 @@ class BooksModel(QAbstractTableModel):
val = 0 if val < 0 else 5 if val > 5 else val val = 0 if val < 0 else 5 if val > 5 else val
val *= 2 val *= 2
column = self.cols[col] column = self.cols[col]
self.db.set(row, column, val) self.db.set(row, column, val)
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \ self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
index, index) index, index)
if col == self.sorted_on[0]: if col == self.sorted_on[0]:
@ -435,17 +437,17 @@ class BooksModel(QAbstractTableModel):
class BooksView(TableView): class BooksView(TableView):
TIME_FMT = '%d %b %Y' TIME_FMT = '%d %b %Y'
wrapper = textwrap.TextWrapper(width=20) wrapper = textwrap.TextWrapper(width=20)
@classmethod @classmethod
def wrap(cls, s, width=20): def wrap(cls, s, width=20):
cls.wrapper.width = width cls.wrapper.width = width
return cls.wrapper.fill(s) return cls.wrapper.fill(s)
@classmethod @classmethod
def human_readable(cls, size, precision=1): def human_readable(cls, size, precision=1):
""" Convert a size in bytes into megabytes """ """ Convert a size in bytes into megabytes """
return ('%.'+str(precision)+'f') % ((size/(1024.*1024.)),) return ('%.'+str(precision)+'f') % ((size/(1024.*1024.)),)
def __init__(self, parent, modelcls=BooksModel): def __init__(self, parent, modelcls=BooksModel):
TableView.__init__(self, parent) TableView.__init__(self, parent)
self.display_parent = parent self.display_parent = parent
@ -454,7 +456,7 @@ class BooksView(TableView):
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSortingEnabled(True) self.setSortingEnabled(True)
if self.__class__.__name__ == 'BooksView': # Subclasses may not have rating as col 4 if self.__class__.__name__ == 'BooksView': # Subclasses may not have rating as col 4
self.setItemDelegateForColumn(4, LibraryDelegate(self)) self.setItemDelegateForColumn(4, LibraryDelegate(self))
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'), QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
self._model.current_changed) self._model.current_changed)
# Adding and removing rows should resize rows to contents # Adding and removing rows should resize rows to contents
@ -463,42 +465,42 @@ class BooksView(TableView):
# Resetting the model should resize rows (model is reset after search and sort operations) # Resetting the model should resize rows (model is reset after search and sort operations)
QObject.connect(self.model(), SIGNAL('modelReset()'), self.resizeRowsToContents) QObject.connect(self.model(), SIGNAL('modelReset()'), self.resizeRowsToContents)
self.set_visible_columns() self.set_visible_columns()
@classmethod @classmethod
def paths_from_event(cls, event): def paths_from_event(cls, event):
''' '''
Accept a drop event and return a list of paths that can be read from Accept a drop event and return a list of paths that can be read from
and represent files with extensions. and represent files with extensions.
''' '''
if event.mimeData().hasFormat('text/uri-list'): if event.mimeData().hasFormat('text/uri-list'):
urls = [qstring_to_unicode(u.toLocalFile()) for u in event.mimeData().urls()] urls = [qstring_to_unicode(u.toLocalFile()) for u in event.mimeData().urls()]
return [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)] return [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
def dragEnterEvent(self, event): def dragEnterEvent(self, event):
if int(event.possibleActions() & Qt.CopyAction) + \ if int(event.possibleActions() & Qt.CopyAction) + \
int(event.possibleActions() & Qt.MoveAction) == 0: int(event.possibleActions() & Qt.MoveAction) == 0:
return return
paths = self.paths_from_event(event) paths = self.paths_from_event(event)
if paths: if paths:
event.acceptProposedAction() event.acceptProposedAction()
def dragMoveEvent(self, event): def dragMoveEvent(self, event):
event.acceptProposedAction() event.acceptProposedAction()
def dropEvent(self, event): def dropEvent(self, event):
paths = self.paths_from_event(event) paths = self.paths_from_event(event)
event.setDropAction(Qt.CopyAction) event.setDropAction(Qt.CopyAction)
event.accept() event.accept()
self.emit(SIGNAL('files_dropped(PyQt_PyObject)'), paths, Qt.QueuedConnection) self.emit(SIGNAL('files_dropped(PyQt_PyObject)'), paths, Qt.QueuedConnection)
def set_database(self, db): def set_database(self, db):
self._model.set_database(db) self._model.set_database(db)
def close(self): def close(self):
self._model.close() self._model.close()
def migrate_database(self): def migrate_database(self):
if self.model().database_needs_migration(): if self.model().database_needs_migration():
print 'Migrating database from pre 0.4.0 version' print 'Migrating database from pre 0.4.0 version'
@ -510,39 +512,39 @@ class BooksView(TableView):
progress.setModal(True) progress.setModal(True)
progress.setValue(0) progress.setValue(0)
app = QCoreApplication.instance() app = QCoreApplication.instance()
def meter(count): def meter(count):
progress.setValue(count) progress.setValue(count)
app.processEvents() app.processEvents()
progress.setWindowTitle('Upgrading database') progress.setWindowTitle('Upgrading database')
progress.show() progress.show()
LibraryDatabase.import_old_database(path, self._model.db.conn, meter) LibraryDatabase.import_old_database(path, self._model.db.conn, meter)
def connect_to_search_box(self, sb): def connect_to_search_box(self, sb):
QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'),
self._model.search) self._model.search)
def connect_to_book_display(self, bd): def connect_to_book_display(self, bd):
QObject.connect(self._model, SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), QObject.connect(self._model, SIGNAL('new_bookdisplay_data(PyQt_PyObject)'),
bd) bd)
class DeviceBooksView(BooksView): class DeviceBooksView(BooksView):
def __init__(self, parent): def __init__(self, parent):
BooksView.__init__(self, parent, DeviceBooksModel) BooksView.__init__(self, parent, DeviceBooksModel)
self.columns_resized = False self.columns_resized = False
self.resize_on_select = False self.resize_on_select = False
def resizeColumnsToContents(self): def resizeColumnsToContents(self):
QTableView.resizeColumnsToContents(self) QTableView.resizeColumnsToContents(self)
self.columns_resized = True self.columns_resized = True
def connect_dirtied_signal(self, slot): def connect_dirtied_signal(self, slot):
QObject.connect(self._model, SIGNAL('booklist_dirtied()'), slot) QObject.connect(self._model, SIGNAL('booklist_dirtied()'), slot)
class DeviceBooksModel(BooksModel): class DeviceBooksModel(BooksModel):
def __init__(self, parent): def __init__(self, parent):
BooksModel.__init__(self, parent) BooksModel.__init__(self, parent)
self.db = [] self.db = []
@ -550,15 +552,15 @@ class DeviceBooksModel(BooksModel):
self.sorted_map = [] self.sorted_map = []
self.unknown = str(self.trUtf8('Unknown')) self.unknown = str(self.trUtf8('Unknown'))
self.marked_for_deletion = {} self.marked_for_deletion = {}
def mark_for_deletion(self, id, rows): def mark_for_deletion(self, id, rows):
self.marked_for_deletion[id] = self.indices(rows) self.marked_for_deletion[id] = self.indices(rows)
for row in rows: for row in rows:
indices = self.row_indices(row) indices = self.row_indices(row)
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1]) self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
def deletion_done(self, id, succeeded=True): def deletion_done(self, id, succeeded=True):
if not self.marked_for_deletion.has_key(id): if not self.marked_for_deletion.has_key(id):
return return
@ -566,29 +568,29 @@ class DeviceBooksModel(BooksModel):
for row in rows: for row in rows:
if not succeeded: if not succeeded:
indices = self.row_indices(self.index(row, 0)) indices = self.row_indices(self.index(row, 0))
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1]) self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
def paths_deleted(self, paths): def paths_deleted(self, paths):
self.map = list(range(0, len(self.db))) self.map = list(range(0, len(self.db)))
self.resort(False) self.resort(False)
self.research(True) self.research(True)
def indices_to_be_deleted(self): def indices_to_be_deleted(self):
ans = [] ans = []
for v in self.marked_for_deletion.values(): for v in self.marked_for_deletion.values():
ans.extend(v) ans.extend(v)
return ans return ans
def flags(self, index): def flags(self, index):
if self.map[index.row()] in self.indices_to_be_deleted(): if self.map[index.row()] in self.indices_to_be_deleted():
return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python
flags = QAbstractTableModel.flags(self, index) flags = QAbstractTableModel.flags(self, index)
if index.isValid(): if index.isValid():
if index.column() in [0, 1] or (index.column() == 4 and self.db.supports_tags()): if index.column() in [0, 1] or (index.column() == 4 and self.db.supports_tags()):
flags |= Qt.ItemIsEditable flags |= Qt.ItemIsEditable
return flags return flags
def search(self, text, refinement, reset=True): def search(self, text, refinement, reset=True):
tokens, OR = self.search_tokens(text) tokens, OR = self.search_tokens(text)
base = self.map if refinement else self.sorted_map base = self.map if refinement else self.sorted_map
@ -611,13 +613,13 @@ class DeviceBooksModel(BooksModel):
break break
if add: if add:
result.append(i) result.append(i)
self.map = result self.map = result
if reset: if reset:
self.reset() self.reset()
self.last_search = text self.last_search = text
def sort(self, col, order, reset=True): def sort(self, col, order, reset=True):
descending = order != Qt.AscendingOrder descending = order != Qt.AscendingOrder
def strcmp(attr): def strcmp(attr):
@ -632,7 +634,7 @@ class DeviceBooksModel(BooksModel):
x, y = x.strip().lower(), y.strip().lower() x, y = x.strip().lower(), y.strip().lower()
return cmp(x, y) return cmp(x, y)
return _strcmp return _strcmp
def datecmp(x, y): def datecmp(x, y):
x = self.db[x].datetime x = self.db[x].datetime
y = self.db[y].datetime y = self.db[y].datetime
return cmp(datetime(*x[0:6]), datetime(*y[0:6])) return cmp(datetime(*x[0:6]), datetime(*y[0:6]))
@ -643,7 +645,7 @@ class DeviceBooksModel(BooksModel):
x, y = ','.join(self.db[x].tags), ','.join(self.db[y].tags) x, y = ','.join(self.db[x].tags), ','.join(self.db[y].tags)
return cmp(x, y) return cmp(x, y)
fcmp = strcmp('title_sorter') if col == 0 else strcmp('authors') if col == 1 else \ fcmp = strcmp('title_sorter') if col == 0 else strcmp('authors') if col == 1 else \
sizecmp if col == 2 else datecmp if col == 3 else tagscmp sizecmp if col == 2 else datecmp if col == 3 else tagscmp
self.map.sort(cmp=fcmp, reverse=descending) self.map.sort(cmp=fcmp, reverse=descending)
if len(self.map) == len(self.db): if len(self.map) == len(self.db):
self.sorted_map = list(self.map) self.sorted_map = list(self.map)
@ -653,17 +655,17 @@ class DeviceBooksModel(BooksModel):
self.sorted_on = (col, order) self.sorted_on = (col, order)
if reset: if reset:
self.reset() self.reset()
def columnCount(self, parent): def columnCount(self, parent):
return 5 return 5
def rowCount(self, parent): def rowCount(self, parent):
return len(self.map) return len(self.map)
def set_database(self, db): def set_database(self, db):
self.db = db self.db = db
self.map = list(range(0, len(db))) self.map = list(range(0, len(db)))
def current_changed(self, current, previous): def current_changed(self, current, previous):
data = {} data = {}
item = self.db[self.map[current.row()]] item = self.db[self.map[current.row()]]
@ -686,26 +688,26 @@ class DeviceBooksModel(BooksModel):
data[_('Timestamp')] = dt.ctime() data[_('Timestamp')] = dt.ctime()
data[_('Tags')] = ', '.join(item.tags) data[_('Tags')] = ', '.join(item.tags)
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data) self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
def paths(self, rows): def paths(self, rows):
return [self.db[self.map[r.row()]].path for r in rows ] return [self.db[self.map[r.row()]].path for r in rows ]
def indices(self, rows): def indices(self, rows):
''' '''
Return indices into underlying database from rows Return indices into underlying database from rows
''' '''
return [ self.map[r.row()] for r in rows] return [ self.map[r.row()] for r in rows]
def data(self, index, role): def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole: if role == Qt.DisplayRole or role == Qt.EditRole:
row, col = index.row(), index.column() row, col = index.row(), index.column()
if col == 0: if col == 0:
text = self.db[self.map[row]].title text = self.db[self.map[row]].title
if not text: if not text:
text = self.unknown text = self.unknown
return QVariant(text) return QVariant(text)
elif col == 1: elif col == 1:
au = self.db[self.map[row]].authors au = self.db[self.map[row]].authors
if not au: if not au:
au = self.unknown au = self.unknown
@ -726,40 +728,40 @@ class DeviceBooksModel(BooksModel):
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight) dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
return QVariant(dt.strftime(BooksView.TIME_FMT)) return QVariant(dt.strftime(BooksView.TIME_FMT))
elif col == 4: elif col == 4:
tags = self.db[self.map[row]].tags tags = self.db[self.map[row]].tags
if tags: if tags:
return QVariant(', '.join(tags)) return QVariant(', '.join(tags))
elif role == Qt.TextAlignmentRole and index.column() in [2, 3]: elif role == Qt.TextAlignmentRole and index.column() in [2, 3]:
return QVariant(Qt.AlignRight | Qt.AlignVCenter) return QVariant(Qt.AlignRight | Qt.AlignVCenter)
elif role == Qt.ToolTipRole and index.isValid(): elif role == Qt.ToolTipRole and index.isValid():
if self.map[index.row()] in self.indices_to_be_deleted(): if self.map[index.row()] in self.indices_to_be_deleted():
return QVariant('Marked for deletion') return QVariant('Marked for deletion')
col = index.column() col = index.column()
if col in [0, 1] or (col == 4 and self.db.supports_tags()): if col in [0, 1] or (col == 4 and self.db.supports_tags()):
return QVariant("Double click to <b>edit</b> me<br><br>") return QVariant("Double click to <b>edit</b> me<br><br>")
return NONE return NONE
def headerData(self, section, orientation, role): def headerData(self, section, orientation, role):
if role != Qt.DisplayRole: if role != Qt.DisplayRole:
return NONE return NONE
text = "" text = ""
if orientation == Qt.Horizontal: if orientation == Qt.Horizontal:
if section == 0: text = _("Title") if section == 0: text = _("Title")
elif section == 1: text = _("Author(s)") elif section == 1: text = _("Author(s)")
elif section == 2: text = _("Size (MB)") elif section == 2: text = _("Size (MB)")
elif section == 3: text = _("Date") elif section == 3: text = _("Date")
elif section == 4: text = _("Tags") elif section == 4: text = _("Tags")
return QVariant(text) return QVariant(text)
else: else:
return QVariant(section+1) return QVariant(section+1)
def setData(self, index, value, role): def setData(self, index, value, role):
done = False done = False
if role == Qt.EditRole: if role == Qt.EditRole:
row, col = index.row(), index.column() row, col = index.row(), index.column()
if col in [2, 3]: if col in [2, 3]:
return False return False
val = qstring_to_unicode(value.toString()).strip() val = qstring_to_unicode(value.toString()).strip()
idx = self.map[row] idx = self.map[row]
if col == 0: if col == 0:
self.db[idx].title = val self.db[idx].title = val
@ -778,9 +780,9 @@ class DeviceBooksModel(BooksModel):
return done return done
class SearchBox(QLineEdit): class SearchBox(QLineEdit):
INTERVAL = 1000 #: Time to wait before emitting search signal INTERVAL = 1000 #: Time to wait before emitting search signal
def __init__(self, parent): def __init__(self, parent):
QLineEdit.__init__(self, parent) QLineEdit.__init__(self, parent)
self.help_text = _('Search (For Advanced Search click the button to the left)') self.help_text = _('Search (For Advanced Search click the button to the left)')
@ -792,40 +794,40 @@ class SearchBox(QLineEdit):
self.timer = None self.timer = None
self.clear_to_help() self.clear_to_help()
QObject.connect(self, SIGNAL('textEdited(QString)'), self.text_edited_slot) QObject.connect(self, SIGNAL('textEdited(QString)'), self.text_edited_slot)
def normalize_state(self): def normalize_state(self):
self.setText('') self.setText('')
self.setPalette(self.default_palette) self.setPalette(self.default_palette)
def clear_to_help(self): def clear_to_help(self):
self.setPalette(self.gray) self.setPalette(self.gray)
self.setText(self.help_text) self.setText(self.help_text)
self.home(False) self.home(False)
self.initial_state = True self.initial_state = True
def clear(self): def clear(self):
self.clear_to_help() self.clear_to_help()
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), '', False) self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), '', False)
def keyPressEvent(self, event): def keyPressEvent(self, event):
if self.initial_state: if self.initial_state:
self.normalize_state() self.normalize_state()
self.initial_state = False self.initial_state = False
QLineEdit.keyPressEvent(self, event) QLineEdit.keyPressEvent(self, event)
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
if self.initial_state: if self.initial_state:
self.normalize_state() self.normalize_state()
self.initial_state = False self.initial_state = False
QLineEdit.mouseReleaseEvent(self, event) QLineEdit.mouseReleaseEvent(self, event)
def text_edited_slot(self, text): def text_edited_slot(self, text):
text = qstring_to_unicode(text) if isinstance(text, QString) else unicode(text) text = qstring_to_unicode(text) if isinstance(text, QString) else unicode(text)
self.prev_text = text self.prev_text = text
self.timer = self.startTimer(self.__class__.INTERVAL) self.timer = self.startTimer(self.__class__.INTERVAL)
def timerEvent(self, event): def timerEvent(self, event):
self.killTimer(event.timerId()) self.killTimer(event.timerId())
if event.timerId() == self.timer: if event.timerId() == self.timer:
@ -833,7 +835,7 @@ class SearchBox(QLineEdit):
refinement = text.startswith(self.prev_search) and ':' not in text refinement = text.startswith(self.prev_search) and ':' not in text
self.prev_search = text self.prev_search = text
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), text, refinement) self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), text, refinement)
def set_search_string(self, txt): def set_search_string(self, txt):
self.normalize_state() self.normalize_state()
self.setText(txt) self.setText(txt)

View File

@ -4,7 +4,7 @@ import os, sys, textwrap, collections, traceback, time
from xml.parsers.expat import ExpatError from xml.parsers.expat import ExpatError
from functools import partial from functools import partial
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \ from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \
QVariant, QThread, QSize, QUrl QVariant, QThread, QUrl, QSize
from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \ from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \
QToolButton, QDialog, QDesktopServices QToolButton, QDialog, QDesktopServices
from PyQt4.QtSvg import QSvgRenderer from PyQt4.QtSvg import QSvgRenderer
@ -1210,4 +1210,14 @@ def main(args=sys.argv):
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(main()) try:
sys.exit(main())
except:
if not iswindows: raise
from PyQt4.QtGui import QErrorMessage
logfile = os.path.expanduser('~/calibre.log')
if os.path.exists(logfile):
log = open(logfile).read()
if log.strip():
d = QErrorMessage()
d.showMessage(log)

View File

@ -27,9 +27,9 @@
<property name="geometry" > <property name="geometry" >
<rect> <rect>
<x>0</x> <x>0</x>
<y>86</y> <y>79</y>
<width>865</width> <width>865</width>
<height>712</height> <height>716</height>
</rect> </rect>
</property> </property>
<layout class="QGridLayout" > <layout class="QGridLayout" >
@ -44,7 +44,7 @@
<item> <item>
<widget class="LocationView" name="location_view" > <widget class="LocationView" name="location_view" >
<property name="sizePolicy" > <property name="sizePolicy" >
<sizepolicy vsizetype="Minimum" hsizetype="Expanding" > <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
@ -52,19 +52,25 @@
<property name="maximumSize" > <property name="maximumSize" >
<size> <size>
<width>10000</width> <width>10000</width>
<height>100</height> <height>110</height>
</size> </size>
</property> </property>
<property name="verticalScrollBarPolicy" > <property name="verticalScrollBarPolicy" >
<enum>Qt::ScrollBarAlwaysOff</enum> <enum>Qt::ScrollBarAlwaysOff</enum>
</property> </property>
<property name="horizontalScrollBarPolicy" > <property name="horizontalScrollBarPolicy" >
<enum>Qt::ScrollBarAlwaysOff</enum> <enum>Qt::ScrollBarAsNeeded</enum>
</property>
<property name="tabKeyNavigation" >
<bool>true</bool>
</property>
<property name="showDropIndicator" stdset="0" >
<bool>true</bool>
</property> </property>
<property name="iconSize" > <property name="iconSize" >
<size> <size>
<width>32</width> <width>40</width>
<height>32</height> <height>40</height>
</size> </size>
</property> </property>
<property name="movement" > <property name="movement" >
@ -77,14 +83,11 @@
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="spacing" > <property name="spacing" >
<number>20</number> <number>10</number>
</property> </property>
<property name="viewMode" > <property name="viewMode" >
<enum>QListView::IconMode</enum> <enum>QListView::IconMode</enum>
</property> </property>
<property name="uniformItemSizes" >
<bool>true</bool>
</property>
</widget> </widget>
</item> </item>
<item> <item>
@ -332,8 +335,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>847</width> <width>857</width>
<height>553</height> <height>552</height>
</rect> </rect>
</property> </property>
<layout class="QGridLayout" > <layout class="QGridLayout" >
@ -380,7 +383,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>865</width> <width>865</width>
<height>86</height> <height>79</height>
</rect> </rect>
</property> </property>
<property name="minimumSize" > <property name="minimumSize" >
@ -425,9 +428,9 @@
<property name="geometry" > <property name="geometry" >
<rect> <rect>
<x>0</x> <x>0</x>
<y>798</y> <y>795</y>
<width>865</width> <width>865</width>
<height>24</height> <height>27</height>
</rect> </rect>
</property> </property>
<property name="mouseTracking" > <property name="mouseTracking" >

View File

@ -1,4 +1,4 @@
import os, sys, glob import os, sys, glob, shutil
import sipconfig import sipconfig
if os.environ.get('PYQT4PATH', None): if os.environ.get('PYQT4PATH', None):
print os.environ['PYQT4PATH'] print os.environ['PYQT4PATH']
@ -37,7 +37,7 @@ makefile = pyqtconfig.QtGuiModuleMakefile (
# ".dll" extension on Windows). # ".dll" extension on Windows).
if 'linux' in sys.platform: if 'linux' in sys.platform:
for f in glob.glob('../../.build/libpictureflow.a'): for f in glob.glob('../../.build/libpictureflow.a'):
os.link(f, './'+os.path.basename(f)) shutil.copyfile(f, os.path.basename(f))
makefile.extra_lib_dirs = ['.'] makefile.extra_lib_dirs = ['.']
else: else:
makefile.extra_lib_dirs = ['..\\..\\.build\\release', '../../.build', '.'] makefile.extra_lib_dirs = ['..\\..\\.build\\release', '../../.build', '.']

View File

@ -7,9 +7,9 @@ import re, os
from PyQt4.QtGui import QListView, QIcon, QFont, QLabel, QListWidget, \ from PyQt4.QtGui import QListView, QIcon, QFont, QLabel, QListWidget, \
QListWidgetItem, QTextCharFormat, QApplication, \ QListWidgetItem, QTextCharFormat, QApplication, \
QSyntaxHighlighter, QCursor, QColor, QWidget, \ QSyntaxHighlighter, QCursor, QColor, QWidget, \
QAbstractItemDelegate, QPixmap QAbstractItemDelegate, QPixmap, QStyle, QFontMetrics
from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, QRect, SIGNAL, \ from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, SIGNAL, \
QObject, QRegExp, QRectF QObject, QRegExp, QString
from calibre.gui2.jobs import DetailView from calibre.gui2.jobs import DetailView
from calibre.gui2 import human_readable, NONE, TableView, qstring_to_unicode, error_dialog from calibre.gui2 import human_readable, NONE, TableView, qstring_to_unicode, error_dialog
@ -123,40 +123,43 @@ class LocationDelegate(QAbstractItemDelegate):
def __init__(self): def __init__(self):
QAbstractItemDelegate.__init__(self) QAbstractItemDelegate.__init__(self)
self.icon_rect = QRect(0, 10, 150, 45) self.pixmap = QPixmap(40, 40)
self.buffer = 5 self.text = QString('Reader\n999.9 MB Available202')
def get_rects(self, index, option): def rects(self, option):
row = index.row() style = QApplication.style()
irect = QRect(self.icon_rect) font = QFont(option.font)
irect.translate(row*(irect.width()+self.buffer), 0) font.setBold(True)
trect = irect.translated(0, irect.height()) irect = style.itemPixmapRect(option.rect, Qt.AlignHCenter|Qt.AlignTop, self.pixmap)
trect.adjust(0, 7, 0, 0) trect = style.itemTextRect(QFontMetrics(font), option.rect,
return irect.adjusted(50, 0, -50, 0), trect Qt.AlignHCenter|Qt.AlignTop, True, self.text)
trect.moveTop(irect.bottom())
return irect, trect
def sizeHint(self, option, index): def sizeHint(self, option, index):
irect, trect = self.get_rects(index, option) irect, trect = self.rects(option)
return irect.united(trect).size() return irect.united(trect).size()
def paint(self, painter, option, index): def paint(self, painter, option, index):
font = QFont() style = QApplication.style()
font.setPointSize(9)
icon = QIcon(index.model().data(index, Qt.DecorationRole))
highlight = getattr(index.model(), 'highlight_row', -1) == index.row()
text = index.model().data(index, Qt.DisplayRole).toString()
painter.save() painter.save()
irect, trect = self.get_rects(index, option) if hasattr(QStyle, 'CE_ItemViewItem'):
QApplication.style().drawControl(QStyle.CE_ItemViewItem, option, painter)
mode = QIcon.Normal highlight = getattr(index.model(), 'highlight_row', -1) == index.row()
if highlight: mode = QIcon.Active if highlight else QIcon.Normal
font.setBold(True) pixmap = QIcon(index.model().data(index, Qt.DecorationRole)).pixmap(self.pixmap.size())
mode = QIcon.Active pixmap = style.generatedIconPixmap(mode, pixmap, option)
text = index.model().data(index, Qt.DisplayRole).toString()
irect, trect = self.rects(option)
style.drawItemPixmap(painter, irect, Qt.AlignHCenter|Qt.AlignTop, pixmap)
font = QFont(option.font)
font.setBold(highlight)
painter.setFont(font) painter.setFont(font)
icon.paint(painter, irect, Qt.AlignHCenter|Qt.AlignTop, mode, QIcon.On) style.drawItemText(painter, trect, Qt.AlignHCenter|Qt.AlignBottom,
painter.drawText(QRectF(trect), Qt.AlignTop|Qt.AlignHCenter, text) option.palette, True, text)
painter.restore() painter.restore()
class LocationModel(QAbstractListModel): class LocationModel(QAbstractListModel):
def __init__(self, parent): def __init__(self, parent):
QAbstractListModel.__init__(self, parent) QAbstractListModel.__init__(self, parent)

View File

@ -15,6 +15,7 @@ from calibre.gui2 import SingleApplication
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.library.database2 import LibraryDatabase2 from calibre.library.database2 import LibraryDatabase2
from calibre.library.database import text_to_tokens from calibre.library.database import text_to_tokens
from calibre.ebooks.metadata.opf import OPFCreator, OPFReader
FIELDS = set(['title', 'authors', 'publisher', 'rating', 'timestamp', 'size', 'tags', 'comments', 'series', 'series_index', 'formats']) FIELDS = set(['title', 'authors', 'publisher', 'rating', 'timestamp', 'size', 'tags', 'comments', 'series', 'series_index', 'formats'])
@ -304,9 +305,67 @@ do nothing.
do_remove_format(get_db(dbpath, opts), id, fmt) do_remove_format(get_db(dbpath, opts), id, fmt)
return 0 return 0
def do_show_metadata(db, id, as_opf):
if not db.has_id(id):
raise ValueError('Id #%d is not present in database.'%id)
mi = db.get_metadata(id, index_is_id=True)
if as_opf:
mi = OPFCreator(os.getcwd(), mi)
mi.render(sys.stdout)
else:
print mi
def command_show_metadata(args, dbpath):
parser = get_parser(_(
'''
%prog show_metadata [options] id
Show the metadata stored in the calibre database for the book identified by id.
id is an id number from the list command.
'''))
parser.add_option('--as-opf', default=False, action='store_true',
help=_('Print metadata in OPF form (XML)'))
opts, args = parser.parse_args(sys.argv[1:]+args)
if len(args) < 2:
parser.print_help()
print
print _('You must specify an id')
return 1
id = int(args[1])
do_show_metadata(get_db(dbpath, opts), id, opts.as_opf)
return 0
def do_set_metadata(db, id, stream):
mi = OPFReader(stream)
db.set_metadata(id, mi)
do_show_metadata(db, id, False)
if SingleApplication is not None:
sa = SingleApplication('calibre GUI')
sa.send_message('refreshdb:')
def command_set_metadata(args, dbpath):
parser = get_parser(_(
'''
%prog set_metadata [options] id /path/to/metadata.opf
Set the metadata stored in the calibre database for the book identified by id
from the OPF file metadata.opf. id is an id number from the list command. You
can get a quick feel for the OPF format by using the --as-opf switch to the
show_metadata command.
'''))
opts, args = parser.parse_args(sys.argv[1:]+args)
if len(args) < 3:
parser.print_help()
print
print _('You must specify an id and a metadata file')
return 1
id, opf = int(args[1]), open(args[2], 'rb')
do_set_metadata(get_db(dbpath, opts), id, opf)
return 0
def main(args=sys.argv): def main(args=sys.argv):
commands = ('list', 'add', 'remove', 'add_format', 'remove_format') commands = ('list', 'add', 'remove', 'add_format', 'remove_format',
'show_metadata', 'set_metadata')
parser = OptionParser(_( parser = OptionParser(_(
'''\ '''\
%%prog command [options] [arguments] %%prog command [options] [arguments]

View File

@ -911,13 +911,19 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
def title(self, index, index_is_id=False): def title(self, index, index_is_id=False):
if not index_is_id: if not index_is_id:
return self.data[index][1] return self.data[index][1]
return self.conn.execute('SELECT title FROM meta WHERE id=?',(index,)).fetchone()[0] try:
return self.conn.execute('SELECT title FROM meta WHERE id=?',(index,)).fetchone()[0]
except:
return _('Unknown')
def authors(self, index, index_is_id=False): def authors(self, index, index_is_id=False):
''' Authors as a comma separated list or None''' ''' Authors as a comma separated list or None'''
if not index_is_id: if not index_is_id:
return self.data[index][2] return self.data[index][2]
return self.conn.execute('SELECT authors FROM meta WHERE id=?',(index,)).fetchone()[0] try:
return self.conn.execute('SELECT authors FROM meta WHERE id=?',(index,)).fetchone()[0]
except:
pass
def isbn(self, idx, index_is_id=False): def isbn(self, idx, index_is_id=False):
id = idx if index_is_id else self.id(idx) id = idx if index_is_id else self.id(idx)
@ -929,22 +935,22 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
def publisher(self, index, index_is_id=False): def publisher(self, index, index_is_id=False):
if index_is_id: if index_is_id:
return self.conn.execute('SELECT publisher FROM meta WHERE id=?', (id,)).fetchone()[0] return self.conn.execute('SELECT publisher FROM meta WHERE id=?', (index,)).fetchone()[0]
return self.data[index][3] return self.data[index][3]
def rating(self, index, index_is_id=False): def rating(self, index, index_is_id=False):
if index_is_id: if index_is_id:
return self.conn.execute('SELECT rating FROM meta WHERE id=?', (id,)).fetchone()[0] return self.conn.execute('SELECT rating FROM meta WHERE id=?', (index,)).fetchone()[0]
return self.data[index][4] return self.data[index][4]
def timestamp(self, index, index_is_id=False): def timestamp(self, index, index_is_id=False):
if index_is_id: if index_is_id:
return self.conn.execute('SELECT timestamp FROM meta WHERE id=?', (id,)).fetchone()[0] return self.conn.execute('SELECT timestamp FROM meta WHERE id=?', (index,)).fetchone()[0]
return self.data[index][5] return self.data[index][5]
def max_size(self, index, index_is_id=False): def max_size(self, index, index_is_id=False):
if index_is_id: if index_is_id:
return self.conn.execute('SELECT size FROM meta WHERE id=?', (id,)).fetchone()[0] return self.conn.execute('SELECT size FROM meta WHERE id=?', (index,)).fetchone()[0]
return self.data[index][6] return self.data[index][6]
def cover(self, index, index_is_id=False): def cover(self, index, index_is_id=False):
@ -1251,6 +1257,8 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
''' '''
Set metadata for the book C{id} from the L{MetaInformation} object C{mi} Set metadata for the book C{id} from the L{MetaInformation} object C{mi}
''' '''
if mi.title:
self.set_title(id, mi.title)
if not mi.authors: if not mi.authors:
mi.authors = ['Unknown'] mi.authors = ['Unknown']
authors = [] authors = []
@ -1516,6 +1524,9 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
def has_book(self, mi): def has_book(self, mi):
return bool(self.conn.execute('SELECT id FROM books where title=?', (mi.title,)).fetchone()) return bool(self.conn.execute('SELECT id FROM books where title=?', (mi.title,)).fetchone())
def has_id(self, id):
return self.conn.execute('SELECT id FROM books where id=?', (id,)).fetchone() is not None
def recursive_import(self, root, single_book_per_directory=True): def recursive_import(self, root, single_book_per_directory=True):
root = os.path.abspath(root) root = os.path.abspath(root)
duplicates = [] duplicates = []

View File

@ -131,7 +131,7 @@ Why does |app| show only some of my fonts on OS X?
The graphical user interface of |app| is not starting on Windows? The graphical user interface of |app| is not starting on Windows?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you've never used the graphical user interface before, try deleting the file library1.db (it will be somewhere under :file:`C:\\Documents and Settings` on Windows XP and :file:`C:\\Users` on Windows Vista. If that doesn't fix the problem, locate the file libprs500.log (in the same places as library1.db) and post its contents in a help message on the `Forums <http://calibre.kovidgoyal.net/discussion>`_. If you've never used the graphical user interface before, try deleting the file library1.db (it will be somewhere under :file:`C:\\Documents and Settings` on Windows XP and :file:`C:\\Users` on Windows Vista. If that doesn't fix the problem, locate the file calibre.log (in the same places as library1.db) and post its contents in a help message on the `Forums <http://calibre.kovidgoyal.net/discussion>`_.
I want some feature added to |app|. What can I do? I want some feature added to |app|. What can I do?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2008-06-14 07:16+0000\n" "X-Launchpad-Export-Date: 2008-06-15 22:20+0000\n"
"X-Generator: Launchpad (build Unknown)\n" "X-Generator: Launchpad (build Unknown)\n"
"Generated-By: pygettext.py 1.5\n" "Generated-By: pygettext.py 1.5\n"

View File

@ -17,7 +17,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2008-06-14 07:16+0000\n" "X-Launchpad-Export-Date: 2008-06-15 22:20+0000\n"
"X-Generator: Launchpad (build Unknown)\n" "X-Generator: Launchpad (build Unknown)\n"
#~ msgid "" #~ msgid ""

View File

@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2008-06-14 07:16+0000\n" "X-Launchpad-Export-Date: 2008-06-15 22:20+0000\n"
"X-Generator: Launchpad (build Unknown)\n" "X-Generator: Launchpad (build Unknown)\n"
"Generated-By: pygettext.py 1.5\n" "Generated-By: pygettext.py 1.5\n"

View File

@ -11,13 +11,13 @@ msgstr ""
"Project-Id-Version: es\n" "Project-Id-Version: es\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2008-06-12 20:18+0000\n" "POT-Creation-Date: 2008-06-12 20:18+0000\n"
"PO-Revision-Date: 2008-06-12 22:40+0000\n" "PO-Revision-Date: 2008-06-15 08:31+0000\n"
"Last-Translator: S. Dorscht <Unknown>\n" "Last-Translator: S. Dorscht <Unknown>\n"
"Language-Team: Spanish\n" "Language-Team: Spanish\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2008-06-14 07:16+0000\n" "X-Launchpad-Export-Date: 2008-06-15 22:20+0000\n"
"X-Generator: Launchpad (build Unknown)\n" "X-Generator: Launchpad (build Unknown)\n"
#~ msgid "" #~ msgid ""
@ -151,7 +151,7 @@ msgstr "Creado por "
#: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:146 #: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:146
#: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:174 #: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:174
msgid "Unable to detect the %s disk drive. Try rebooting." msgid "Unable to detect the %s disk drive. Try rebooting."
msgstr "" msgstr "No se ha podido detectar la unidad de disco %s. Trate de reiniciar."
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/__init__.py:73 #: /home/kovid/work/calibre/src/calibre/ebooks/lrf/__init__.py:73
msgid "Set the title. Default: filename." msgid "Set the title. Default: filename."
@ -671,7 +671,6 @@ msgid "Creating XML..."
msgstr "Creando XML..." msgstr "Creando XML..."
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/lrfparser.py:156 #: /home/kovid/work/calibre/src/calibre/ebooks/lrf/lrfparser.py:156
#, fuzzy
msgid "LRS written to " msgid "LRS written to "
msgstr "LRS escrito en " msgstr "LRS escrito en "
@ -1130,7 +1129,6 @@ msgid "Show &text in toolbar buttons"
msgstr "Mostrar &texto en los botones de la barra de herramientas" msgstr "Mostrar &texto en los botones de la barra de herramientas"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:219 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:219
#, fuzzy
msgid "Free unused diskspace from the database" msgid "Free unused diskspace from the database"
msgstr "Espacio de disco disponible de la base de datos" msgstr "Espacio de disco disponible de la base de datos"
@ -1896,18 +1894,16 @@ msgstr ""
"actual" "actual"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:63 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:63
#, fuzzy
msgid "No recipe selected" msgid "No recipe selected"
msgstr "No hay ninguna receta seleccionada" msgstr "No hay ninguna receta seleccionada"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:69 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:69
#, fuzzy
msgid "The attached file: %s is a recipe to download %s." msgid "The attached file: %s is a recipe to download %s."
msgstr "el archivo adjunto: %s es una receta para descargar %s" msgstr "El archivo adjunto: %s es una receta para descargar %s"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:70 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:70
msgid "Recipe for " msgid "Recipe for "
msgstr "" msgstr "Receta para "
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:86 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:86
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:96 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:96
@ -1941,9 +1937,8 @@ msgid "Already exists"
msgstr "Ya existe" msgstr "Ya existe"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:121 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:121
#, fuzzy
msgid "This feed has already been added to the recipe" msgid "This feed has already been added to the recipe"
msgstr "el Feed ya se ha añadido a la receta" msgstr "Este Feed ya se ha añadido a la receta"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:162 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:162
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:171 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:171
@ -1968,9 +1963,8 @@ msgid "A custom recipe named %s already exists. Do you want to replace it?"
msgstr "una receta personalizada llamada %s ya existe. Quiere reemplazarla?" msgstr "una receta personalizada llamada %s ya existe. Quiere reemplazarla?"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:187 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:187
#, fuzzy
msgid "Choose a recipe file" msgid "Choose a recipe file"
msgstr "Seleccionarr un archivo de receta" msgstr "Seleccionar un archivo de receta"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:187 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:187
msgid "Recipes" msgid "Recipes"
@ -2651,6 +2645,9 @@ msgid ""
"href=\"http://calibre.kovidgoyal.net/wiki/Changelog\">new features</a>. " "href=\"http://calibre.kovidgoyal.net/wiki/Changelog\">new features</a>. "
"Visit the download page?" "Visit the download page?"
msgstr "" msgstr ""
"%s se ha actualizado a la versión %s. Ver las <a "
"href=\"http://calibre.kovidgoyal.net/wiki/Changelog\">nuevas "
"características</a>. Visita la página de descarga?"
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1155 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:1155
msgid "Update available" msgid "Update available"
@ -2833,6 +2830,8 @@ msgid ""
"Path to the calibre database. Default is to use the path stored in the " "Path to the calibre database. Default is to use the path stored in the "
"settings." "settings."
msgstr "" msgstr ""
"Camino a la base de datos calibre. El valor predeterminado es a usar la ruta "
"almacenada en la configuración."
#: /home/kovid/work/calibre/src/calibre/library/cli.py:80 #: /home/kovid/work/calibre/src/calibre/library/cli.py:80
msgid "" msgid ""
@ -2840,6 +2839,9 @@ msgid ""
"\n" "\n"
"List the books available in the calibre database. \n" "List the books available in the calibre database. \n"
msgstr "" msgstr ""
"%prog list [options]\n"
"\n"
"Mostrar los libros disponibles en la base de datos calibre. \n"
#: /home/kovid/work/calibre/src/calibre/library/cli.py:88 #: /home/kovid/work/calibre/src/calibre/library/cli.py:88
msgid "" msgid ""
@ -2866,6 +2868,9 @@ msgid ""
"please see the search related documentation in the User Manual. Default is " "please see the search related documentation in the User Manual. Default is "
"to do no filtering." "to do no filtering."
msgstr "" msgstr ""
"Filtrar los resultados de la consulta de búsqueda. Para el formato de la "
"consulta de búsqueda consulte la documentación relacionada con la búsqueda "
"en el Manual del usuario. El valor predeterminado es a no hacer el filtrado."
#: /home/kovid/work/calibre/src/calibre/library/cli.py:101 #: /home/kovid/work/calibre/src/calibre/library/cli.py:101
msgid "Invalid fields. Available fields:" msgid "Invalid fields. Available fields:"
@ -2891,12 +2896,20 @@ msgid ""
"directories, see\n" "directories, see\n"
"the directory related options below. \n" "the directory related options below. \n"
msgstr "" msgstr ""
"%prog add [options] file1 file2 file3 ...\n"
"\n"
"Añadir los archivos especificados como libros a la base de datos. También "
"puede especificar\n"
"directorios, consulte las opciones relacionadas a los directorios más abajo. "
"\n"
#: /home/kovid/work/calibre/src/calibre/library/cli.py:204 #: /home/kovid/work/calibre/src/calibre/library/cli.py:204
msgid "" msgid ""
"Assume that each directory has only a single logical book and that all files " "Assume that each directory has only a single logical book and that all files "
"in it are different e-book formats of that book" "in it are different e-book formats of that book"
msgstr "" msgstr ""
"Supongamos que cada directorio tiene un solo libro lógico y que todos los "
"archivos en este directorio son diferentes formatos de este libro"
#: /home/kovid/work/calibre/src/calibre/library/cli.py:206 #: /home/kovid/work/calibre/src/calibre/library/cli.py:206
msgid "Process directories recursively" msgid "Process directories recursively"
@ -2922,6 +2935,12 @@ msgid ""
"separated list of id numbers (you can get id numbers by using the list " "separated list of id numbers (you can get id numbers by using the list "
"command). For example, 23,34,57-85\n" "command). For example, 23,34,57-85\n"
msgstr "" msgstr ""
"%prog remove ids\n"
"\n"
"Eliminar los libros identificados por ID de la base de datos. ID debe ser "
"una lista separada por comas de números de identificación (se puede obtener "
"números de identificación utilizando el commando \"list\"). Por ejemplo, "
"23,34,57-85\n"
#: /home/kovid/work/calibre/src/calibre/library/cli.py:243 #: /home/kovid/work/calibre/src/calibre/library/cli.py:243
msgid "You must specify at least one book to remove" msgid "You must specify at least one book to remove"
@ -2953,6 +2972,13 @@ msgid ""
"by using the list command. fmt should be a file extension like LRF or TXT or " "by using the list command. fmt should be a file extension like LRF or TXT or "
"EPUB. If the logical book does not have fmt available, do nothing.\n" "EPUB. If the logical book does not have fmt available, do nothing.\n"
msgstr "" msgstr ""
"\n"
"%prog remove_format [options] id fmt\n"
"\n"
"Eliminar el formato fmt del libro lógico identificado por id. Usted puede "
"obtener id utilizando el comando \"list\". fmt debe ser una extensión de "
"archivo como LRF o TXT o EPUB. Si el libro lógico no tiene fmt disponible, "
"no hacer nada.\n"
#: /home/kovid/work/calibre/src/calibre/library/cli.py:300 #: /home/kovid/work/calibre/src/calibre/library/cli.py:300
msgid "You must specify an id and a format" msgid "You must specify an id and a format"
@ -2976,11 +3002,11 @@ msgstr "Trabajo detenido por el usuario"
#: /home/kovid/work/calibre/src/calibre/utils/fontconfig.py:124 #: /home/kovid/work/calibre/src/calibre/utils/fontconfig.py:124
msgid "Could not initialize the fontconfig library" msgid "Could not initialize the fontconfig library"
msgstr "" msgstr "No se ha podido inicializar la biblioteca fontconfig"
#: /home/kovid/work/calibre/src/calibre/utils/sftp.py:53 #: /home/kovid/work/calibre/src/calibre/utils/sftp.py:53
msgid "URL must have the scheme sftp" msgid "URL must have the scheme sftp"
msgstr "" msgstr "La URL debe tener el régimen de sftp"
#: /home/kovid/work/calibre/src/calibre/utils/sftp.py:57 #: /home/kovid/work/calibre/src/calibre/utils/sftp.py:57
msgid "host must be of the form user@hostname" msgid "host must be of the form user@hostname"
@ -2988,11 +3014,11 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/utils/sftp.py:68 #: /home/kovid/work/calibre/src/calibre/utils/sftp.py:68
msgid "Failed to negotiate SSH session: " msgid "Failed to negotiate SSH session: "
msgstr "" msgstr "No se ha podido negociar período de sesiones SSH: "
#: /home/kovid/work/calibre/src/calibre/utils/sftp.py:71 #: /home/kovid/work/calibre/src/calibre/utils/sftp.py:71
msgid "Failed to authenticate with server: %s" msgid "Failed to authenticate with server: %s"
msgstr "" msgstr "No se ha podido autenticar con el servidor: %s"
#: /home/kovid/work/calibre/src/calibre/web/feeds/__init__.py:56 #: /home/kovid/work/calibre/src/calibre/web/feeds/__init__.py:56
#: /home/kovid/work/calibre/src/calibre/web/feeds/__init__.py:77 #: /home/kovid/work/calibre/src/calibre/web/feeds/__init__.py:77

View File

@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2008-06-14 07:16+0000\n" "X-Launchpad-Export-Date: 2008-06-15 22:20+0000\n"
"X-Generator: Launchpad (build Unknown)\n" "X-Generator: Launchpad (build Unknown)\n"
"Generated-By: pygettext.py 1.5\n" "Generated-By: pygettext.py 1.5\n"

View File

@ -15,7 +15,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2008-06-14 07:16+0000\n" "X-Launchpad-Export-Date: 2008-06-15 22:20+0000\n"
"X-Generator: Launchpad (build Unknown)\n" "X-Generator: Launchpad (build Unknown)\n"
"Generated-By: pygettext.py 1.5\n" "Generated-By: pygettext.py 1.5\n"

View File

@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2008-06-14 07:16+0000\n" "X-Launchpad-Export-Date: 2008-06-15 22:20+0000\n"
"X-Generator: Launchpad (build Unknown)\n" "X-Generator: Launchpad (build Unknown)\n"
"Generated-By: pygettext.py 1.5\n" "Generated-By: pygettext.py 1.5\n"

View File

@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2008-06-14 07:16+0000\n" "X-Launchpad-Export-Date: 2008-06-15 22:20+0000\n"
"X-Generator: Launchpad (build Unknown)\n" "X-Generator: Launchpad (build Unknown)\n"
#~ msgid "" #~ msgid ""

View File

@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2008-06-14 07:16+0000\n" "X-Launchpad-Export-Date: 2008-06-15 22:20+0000\n"
"X-Generator: Launchpad (build Unknown)\n" "X-Generator: Launchpad (build Unknown)\n"
"Generated-By: pygettext.py 1.5\n" "Generated-By: pygettext.py 1.5\n"

View File

@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2008-06-14 07:16+0000\n" "X-Launchpad-Export-Date: 2008-06-15 22:20+0000\n"
"X-Generator: Launchpad (build Unknown)\n" "X-Generator: Launchpad (build Unknown)\n"
"Generated-By: pygettext.py 1.5\n" "Generated-By: pygettext.py 1.5\n"

View File

@ -109,6 +109,8 @@ class Feed(object):
if id in self.added_articles: if id in self.added_articles:
return return
published = item.get('date_parsed', time.gmtime()) published = item.get('date_parsed', time.gmtime())
if not published:
published = time.gmtime()
self.id_counter += 1 self.id_counter += 1
self.added_articles.append(id) self.added_articles.append(id)

234
upload.py
View File

@ -1,13 +1,19 @@
#!/usr/bin/python #!/usr/bin/python
import sys, os, shutil, time, tempfile, socket import sys, os, shutil, time, tempfile, socket, fcntl, struct
sys.path.append('src') sys.path.append('src')
import subprocess import subprocess
from subprocess import check_call as _check_call from subprocess import check_call as _check_call
from functools import partial from functools import partial
#from pyvix.vix import Host, VIX_SERVICEPROVIDER_VMWARE_WORKSTATION #from pyvix.vix import Host, VIX_SERVICEPROVIDER_VMWARE_WORKSTATION
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) def get_ip_address(ifname):
s.connect(('google.com', 0)) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
HOST=s.getsockname()[0] return socket.inet_ntoa(fcntl.ioctl(
s.fileno(),
0x8915, # SIOCGIFADDR
struct.pack('256s', ifname[:15])
)[20:24])
HOST=get_ip_address('eth0')
PROJECT=os.path.basename(os.getcwd()) PROJECT=os.path.basename(os.getcwd())
from calibre import __version__, __appname__ from calibre import __version__, __appname__
@ -21,11 +27,12 @@ TXT2LRF = "src/calibre/ebooks/lrf/txt/demo"
BUILD_SCRIPT ='''\ BUILD_SCRIPT ='''\
#!/bin/bash #!/bin/bash
cd ~/build && \ cd ~/build && \
rsync -avz --exclude docs --exclude .bzr --exclude .build --exclude build --exclude dist --exclude "*.pyc" --exclude "*.pyo" rsync://%(host)s/work/%(project)s . && \ rsync -avz --exclude src/calibre/plugins --exclude docs --exclude .bzr --exclude .build --exclude build --exclude dist --exclude "*.pyc" --exclude "*.pyo" rsync://%(host)s/work/%(project)s . && \
cd %(project)s && \ cd %(project)s && \
mkdir -p build dist && \ mkdir -p build dist src/calibre/plugins && \
%%s && \
rm -rf build/* dist/* && \ rm -rf build/* dist/* && \
python %%s %%s %%s
'''%dict(host=HOST, project=PROJECT) '''%dict(host=HOST, project=PROJECT)
check_call = partial(_check_call, shell=True) check_call = partial(_check_call, shell=True)
#h = Host(hostType=VIX_SERVICEPROVIDER_VMWARE_WORKSTATION) #h = Host(hostType=VIX_SERVICEPROVIDER_VMWARE_WORKSTATION)
@ -41,22 +48,24 @@ def installer_name(ext):
return 'dist/%s-%s.%s'%(__appname__, __version__, ext) return 'dist/%s-%s.%s'%(__appname__, __version__, ext)
return 'dist/%s-%s-i686.%s'%(__appname__, __version__, ext) return 'dist/%s-%s-i686.%s'%(__appname__, __version__, ext)
def start_vm(vm, ssh_host, build_script, sleep): def start_vm(vm, ssh_host, build_script, sleep=75):
vmware = ('vmware', '-q', '-x', '-n', vm) vmware = ('vmware', '-q', '-x', '-n', vm)
subprocess.Popen(vmware) subprocess.Popen(vmware)
t = tempfile.NamedTemporaryFile(suffix='.sh') t = tempfile.NamedTemporaryFile(suffix='.sh')
t.write(build_script) t.write(build_script)
t.flush() t.flush()
print 'Waiting for VM to startup' print 'Waiting for VM to startup'
time.sleep(sleep) while subprocess.call('ping -q -c1 '+ssh_host, shell=True, stdout=open('/dev/null', 'w')) != 0:
time.sleep(5)
time.sleep(20)
print 'Trying to SSH into VM' print 'Trying to SSH into VM'
subprocess.check_call(('scp', t.name, ssh_host+':build-'+PROJECT)) subprocess.check_call(('scp', t.name, ssh_host+':build-'+PROJECT))
subprocess.check_call('ssh -t %s bash build-%s'%(ssh_host, PROJECT), shell=True)
def build_windows(): def build_windows():
installer = installer_name('exe') installer = installer_name('exe')
vm = '/vmware/Windows XP/Windows XP Professional.vmx' vm = '/vmware/Windows XP/Windows XP Professional.vmx'
start_vm(vm, 'windows', BUILD_SCRIPT%'windows_installer.py', 75) start_vm(vm, 'windows', BUILD_SCRIPT%('python setup.py develop', 'python','windows_installer.py'))
subprocess.check_call(('ssh', 'windows', '/bin/bash', '~/build-'+PROJECT))
subprocess.check_call(('scp', 'windows:build/%s/dist/*.exe'%PROJECT, 'dist')) subprocess.check_call(('scp', 'windows:build/%s/dist/*.exe'%PROJECT, 'dist'))
if not os.path.exists(installer): if not os.path.exists(installer):
raise Exception('Failed to build installer '+installer) raise Exception('Failed to build installer '+installer)
@ -66,160 +75,24 @@ def build_windows():
def build_osx(): def build_osx():
installer = installer_name('dmg') installer = installer_name('dmg')
vm = '/vmware/Mac OSX/Mac OSX.vmx' vm = '/vmware/Mac OSX/Mac OSX.vmx'
vmware = ('vmware', '-q', '-x', '-n', vm) python = '/Library/Frameworks/Python.framework/Versions/Current/bin/python'
start_vm(vm, 'osx', BUILD_SCRIPT%'osx_installer.py', 120) start_vm(vm, 'osx', BUILD_SCRIPT%('sudo %s setup.py develop'%python, python, 'osx_installer.py'))
subprocess.check_call(('ssh', 'osx', '/bin/bash', '~/build-'+PROJECT)) subprocess.check_call(('scp', 'osx:build/%s/dist/*.dmg'%PROJECT, 'dist'))
subprocess.check_call(('scp', 'windows:build/%s/dist/*.dmg'%PROJECT, 'dist'))
if not os.path.exists(installer): if not os.path.exists(installer):
raise Exception('Failed to build installer '+installer) raise Exception('Failed to build installer '+installer)
subprocess.Popen(('ssh', 'osx', 'sudo', '/sbin/shutdown', '-h', 'now')) subprocess.Popen(('ssh', 'osx', 'sudo', '/sbin/shutdown', '-h', 'now'))
return os.path.basename(installer) return os.path.basename(installer)
def _build_linux():
cwd = os.getcwd()
tbz2 = os.path.join(cwd, installer_name('tar.bz2'))
SPEC="""\
import os
HOME = '%(home)s'
PYINSTALLER = os.path.expanduser('~/build/pyinstaller')
CALIBREPREFIX = HOME+'/work/%(project)s'
CLIT = '/usr/bin/clit'
PDFTOHTML = '/usr/bin/pdftohtml'
LIBUNRAR = '/usr/lib/libunrar.so'
QTDIR = '/usr/lib/qt4'
QTDLLS = ('QtCore', 'QtGui', 'QtNetwork', 'QtSvg', 'QtXml')
EXTRAS = ('/usr/lib/python2.5/site-packages/PIL', os.path.expanduser('~/ipython/IPython'))
import glob, sys, subprocess, tarfile
CALIBRESRC = os.path.join(CALIBREPREFIX, 'src')
CALIBREPLUGINS = os.path.join(CALIBRESRC, 'calibre', 'plugins')
subprocess.check_call(('/usr/bin/sudo', 'chown', '-R', 'kovid:users', glob.glob('/usr/lib/python*/site-packages/')[-1]))
subprocess.check_call('rm -rf %%(py)s/dist/* %%(py)s/build/*'%%dict(py=PYINSTALLER), shell=True)
loader = os.path.join('/tmp', 'calibre_installer_loader.py')
if not os.path.exists(loader):
open(loader, 'wb').write('''
import sys, os
sys.frozen_path = os.getcwd()
os.chdir(os.environ.get("ORIGWD", "."))
sys.path.insert(0, os.path.join(sys.frozen_path, "library.pyz"))
sys.path.insert(0, sys.frozen_path)
from PyQt4.QtCore import QCoreApplication
QCoreApplication.setLibraryPaths([sys.frozen_path, os.path.join(sys.frozen_path, "plugins")])
''')
excludes = ['gtk._gtk', 'gtk.glade', 'qt', 'matplotlib.nxutils', 'matplotlib._cntr',
'matplotlib.ttconv', 'matplotlib._image', 'matplotlib.ft2font',
'matplotlib._transforms', 'matplotlib._agg', 'matplotlib.backends._backend_agg',
'matplotlib.axes', 'matplotlib', 'matplotlib.pyparsing',
'TKinter', 'atk', 'gobject._gobject', 'pango', 'PIL', 'Image', 'IPython']
temp = ['keyword', 'codeop']
recipes = ['calibre', 'web', 'feeds', 'recipes']
prefix = '.'.join(recipes)+'.'
for f in glob.glob(os.path.join(CALIBRESRC, *(recipes+['*.py']))):
temp.append(prefix + os.path.basename(f).partition('.')[0])
hook = '/tmp/hook-calibre.py'
open(hook, 'wb').write('hiddenimports = %%s'%%repr(temp) + '\\n')
sys.path.insert(0, CALIBRESRC)
from calibre.linux import entry_points
executables, scripts = ['calibre_postinstall', 'parallel'], \
[os.path.join(CALIBRESRC, 'calibre', 'linux.py'), os.path.join(CALIBRESRC, 'calibre', 'parallel.py')]
for entry in entry_points['console_scripts'] + entry_points['gui_scripts']:
fields = entry.split('=')
executables.append(fields[0].strip())
scripts.append(os.path.join(CALIBRESRC, *map(lambda x: x.strip(), fields[1].split(':')[0].split('.')))+'.py')
recipes = Analysis(glob.glob(os.path.join(CALIBRESRC, 'calibre', 'web', 'feeds', 'recipes', '*.py')),
pathex=[CALIBRESRC], hookspath=[os.path.dirname(hook)], excludes=excludes)
analyses = [Analysis([os.path.join(HOMEPATH,'support/_mountzlib.py'), os.path.join(HOMEPATH,'support/useUnicode.py'), loader, script],
pathex=[PYINSTALLER, CALIBRESRC, CALIBREPLUGINS], excludes=excludes) for script in scripts]
pyz = TOC()
binaries = TOC()
for a in analyses:
pyz = a.pure + pyz
binaries = a.binaries + binaries
pyz = PYZ(pyz + recipes.pure, name='library.pyz')
built_executables = []
for script, exe, a in zip(scripts, executables, analyses):
built_executables.append(EXE(PYZ(TOC()),
a.scripts+[('O','','OPTION'),],
exclude_binaries=1,
name=os.path.join('buildcalibre', exe),
debug=False,
strip=True,
upx=False,
excludes=excludes,
console=1))
print 'Adding plugins...'
for f in glob.glob(os.path.join(CALIBREPLUGINS, '*.so')):
binaries += [(os.path.basename(f), f, 'BINARY')]
print 'Adding external programs...'
binaries += [('clit', CLIT, 'BINARY'), ('pdftohtml', PDFTOHTML, 'BINARY'),
('libunrar.so', LIBUNRAR, 'BINARY')]
qt = []
for dll in QTDLLS:
path = os.path.join(QTDIR, 'lib'+dll+'.so.4')
qt.append((os.path.basename(path), path, 'BINARY'))
binaries += 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 'codcs' in dirpath or 'sqldrivers' in dirpath : continue
f = os.path.join(dirpath, f)
plugins.append(('plugins/'+f.replace(plugdir, ''), f, 'BINARY'))
binaries += plugins
manifest = '/tmp/manifest'
open(manifest, 'wb').write('\\n'.join(executables))
from calibre import __version__
version = '/tmp/version'
open(version, 'wb').write(__version__)
coll = COLLECT(binaries, pyz, [('manifest', manifest, 'DATA'), ('version', version, 'DATA')],
*built_executables,
**dict(strip=True,
upx=False,
excludes=excludes,
name='dist'))
os.chdir(os.path.join(HOMEPATH, 'calibre', 'dist'))
for folder in EXTRAS:
subprocess.check_call('cp -rf %%s .'%%folder, shell=True)
print 'Building tarball...'
tf = tarfile.open('%(tarfile)s', 'w:bz2')
for f in os.listdir('.'):
tf.add(f)
"""%dict(home='/mnt/hgfs/giskard/', tarfile=tbz2, project=PROJECT)
os.chdir(os.path.expanduser('~/build/pyinstaller'))
open('calibre/calibre.spec', 'wb').write(SPEC)
try:
subprocess.check_call(('/usr/bin/python', '-O', 'Build.py', 'calibre/calibre.spec'))
finally:
os.chdir(cwd)
return os.path.basename(tbz2)
def build_linux(): def build_linux():
installer = installer_name('tar.bz2')
vm = '/vmware/linux/libprs500-gentoo.vmx' vm = '/vmware/linux/libprs500-gentoo.vmx'
vmware = ('vmware', '-q', '-x', '-n', vm) start_vm(vm, 'linux', BUILD_SCRIPT%('sudo python setup.py develop', 'python','linux_installer.py'))
subprocess.Popen(vmware) subprocess.check_call(('scp', 'linux:/tmp/%s'%os.path.basename(installer), 'dist'))
print 'Waiting for linux to boot up...' if not os.path.exists(installer):
time.sleep(75) raise Exception('Failed to build installer '+installer)
check_call('ssh linux make -C /mnt/hgfs/giskard/work/%s all egg linux_binary'%PROJECT) subprocess.Popen(('ssh', 'linux', 'sudo', '/sbin/poweroff'))
check_call('ssh linux sudo poweroff') return os.path.basename(installer)
def build_installers(): def build_installers():
return build_linux(), build_windows(), build_osx() return build_linux(), build_windows(), build_osx()
@ -267,18 +140,14 @@ def upload_user_manual():
finally: finally:
os.chdir(cwd) os.chdir(cwd)
def build_tarball(): def build_src_tarball():
cwd = os.getcwd()
check_call('bzr export dist/calibre-%s.tar.bz2'%__version__) check_call('bzr export dist/calibre-%s.tar.bz2'%__version__)
def upload_tarball(): def upload_src_tarball():
check_call('ssh divok rm -f %s/calibre-\*.tar.bz2'%DOWNLOADS) check_call('ssh divok rm -f %s/calibre-\*.tar.bz2'%DOWNLOADS)
check_call('scp dist/calibre-*.tar.bz2 divok:%s/'%DOWNLOADS) check_call('scp dist/calibre-*.tar.bz2 divok:%s/'%DOWNLOADS)
def stage_one():
def main():
upload = len(sys.argv) < 2
shutil.rmtree('build') shutil.rmtree('build')
os.mkdir('build') os.mkdir('build')
shutil.rmtree('docs') shutil.rmtree('docs')
@ -288,17 +157,32 @@ def main():
check_call('make', shell=True) check_call('make', shell=True)
tag_release() tag_release()
upload_demo() upload_demo()
def stage_two():
subprocess.check_call('rm -rf dist/*', shell=True)
build_installers() build_installers()
build_tarball() build_src_tarball()
if upload:
print 'Uploading installers...' def stage_three():
upload_installers() print 'Uploading installers...'
print 'Uploading to PyPI' upload_installers()
upload_tarball() print 'Uploading to PyPI'
upload_docs() upload_src_tarball()
upload_user_manual() upload_docs()
check_call('python setup.py register bdist_egg --exclude-source-files upload') upload_user_manual()
check_call('''rm -rf dist/* build/*''') check_call('python setup.py register bdist_egg --exclude-source-files upload')
check_call('''rm -rf dist/* build/*''')
def main(args=sys.argv):
print 'Starting stage one...'
stage_one()
print 'Starting stage two...'
stage_two()
print 'Starting stage three...'
stage_three()
print 'Finished'
return 0
if __name__ == '__main__': if __name__ == '__main__':
main() sys.exit(main())

View File

@ -1,7 +1,7 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
''' Create a windows installer ''' ''' Create a windows installer '''
import sys, re, os, shutil, subprocess import sys, re, os, shutil, subprocess, zipfile
from setup import VERSION, APPNAME, entry_points, scripts, basenames from setup import VERSION, APPNAME, entry_points, scripts, basenames
from distutils.core import setup from distutils.core import setup
from distutils.filelist import FileList from distutils.filelist import FileList
@ -18,7 +18,7 @@ if os.path.exists(PY2EXE_DIR):
class NSISInstaller(object): class NSISInstaller(object):
TEMPLATE = r''' TEMPLATE = r'''
; Do a Cyclic Redundancy Check to make sure the installer ; Do a Cyclic Redundancy Check to make sure the installer
; was not corrupted by the download. ; was not corrupted by the download.
CRCCheck on CRCCheck on
SetCompressor lzma SetCompressor lzma
@ -29,7 +29,7 @@ ShowUnInstDetails show
;Include Modern UI ;Include Modern UI
!include "MUI2.nsh" !include "MUI2.nsh"
!include "WinMessages.nsh" !include "WinMessages.nsh"
;------------------------------------------------------------------------------------------------------ ;------------------------------------------------------------------------------------------------------
;Variables ;Variables
Var STARTMENU_FOLDER Var STARTMENU_FOLDER
@ -63,7 +63,7 @@ Loop:
StrCmp "$R2" "$\r" RTrim StrCmp "$R2" "$\r" RTrim
StrCmp "$R2" ";" RTrim StrCmp "$R2" ";" RTrim
GoTo Done GoTo Done
RTrim: RTrim:
StrCpy $R1 "$R1" -1 StrCpy $R1 "$R1" -1
Goto Loop Goto Loop
Done: Done:
@ -82,7 +82,7 @@ FunctionEnd
; Call StrStr ; Call StrStr
; Pop $R0 ; Pop $R0
; ($R0 at this point is "ass string") ; ($R0 at this point is "ass string")
!macro StrStr un !macro StrStr un
Function ${un}StrStr Function ${un}StrStr
Exch $R1 ; st=haystack,old$R1, $R1=needle Exch $R1 ; st=haystack,old$R1, $R1=needle
@ -123,7 +123,7 @@ Function AddToPath
Push $3 Push $3
; don't add if the path doesn't exist ; don't add if the path doesn't exist
IfFileExists "$0\*.*" "" AddToPath_done IfFileExists "$0\*.*" "" AddToPath_done
ReadEnvStr $1 PATH ReadEnvStr $1 PATH
Push "$1;" Push "$1;"
Push "$0;" Push "$0;"
@ -146,7 +146,7 @@ Function AddToPath
Call StrStr Call StrStr
Pop $2 Pop $2
StrCmp $2 "" "" AddToPath_done StrCmp $2 "" "" AddToPath_done
ReadRegStr $1 ${WriteEnvStr_RegKey} "PATH" ReadRegStr $1 ${WriteEnvStr_RegKey} "PATH"
StrCmp $1 "" AddToPath_NTdoIt StrCmp $1 "" AddToPath_NTdoIt
Push $1 Push $1
@ -156,7 +156,7 @@ Function AddToPath
AddToPath_NTdoIt: AddToPath_NTdoIt:
WriteRegExpandStr ${WriteEnvStr_RegKey} "PATH" $0 WriteRegExpandStr ${WriteEnvStr_RegKey} "PATH" $0
SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000
AddToPath_done: AddToPath_done:
Pop $3 Pop $3
Pop $2 Pop $2
@ -172,9 +172,9 @@ Function un.RemoveFromPath
Push $4 Push $4
Push $5 Push $5
Push $6 Push $6
IntFmt $6 "%%c" 26 # DOS EOF IntFmt $6 "%%c" 26 # DOS EOF
ReadRegStr $1 ${WriteEnvStr_RegKey} "PATH" ReadRegStr $1 ${WriteEnvStr_RegKey} "PATH"
StrCpy $5 $1 1 -1 # copy last char StrCpy $5 $1 1 -1 # copy last char
StrCmp $5 ";" +2 # if last char != ; StrCmp $5 ";" +2 # if last char != ;
@ -192,14 +192,14 @@ Function un.RemoveFromPath
StrCpy $5 $1 -$4 # $5 is now the part before the path to remove StrCpy $5 $1 -$4 # $5 is now the part before the path to remove
StrCpy $6 $2 "" $3 # $6 is now the part after the path to remove StrCpy $6 $2 "" $3 # $6 is now the part after the path to remove
StrCpy $3 $5$6 StrCpy $3 $5$6
StrCpy $5 $3 1 -1 # copy last char StrCpy $5 $3 1 -1 # copy last char
StrCmp $5 ";" 0 +2 # if last char == ; StrCmp $5 ";" 0 +2 # if last char == ;
StrCpy $3 $3 -1 # remove last char StrCpy $3 $3 -1 # remove last char
WriteRegExpandStr ${WriteEnvStr_RegKey} "PATH" $3 WriteRegExpandStr ${WriteEnvStr_RegKey} "PATH" $3
SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000
unRemoveFromPath_done: unRemoveFromPath_done:
Pop $6 Pop $6
Pop $5 Pop $5
@ -219,13 +219,13 @@ FunctionEnd
;Default installation folder ;Default installation folder
InstallDir "$PROGRAMFILES\${PRODUCT_NAME}" InstallDir "$PROGRAMFILES\${PRODUCT_NAME}"
;Get installation folder from registry if available ;Get installation folder from registry if available
InstallDirRegKey HKCU "Software\${PRODUCT_NAME}" "" InstallDirRegKey HKCU "Software\${PRODUCT_NAME}" ""
;Vista redirects $SMPROGRAMS to all users without this ;Vista redirects $SMPROGRAMS to all users without this
RequestExecutionLevel admin RequestExecutionLevel admin
;------------------------------------------------------------------------------------------------------ ;------------------------------------------------------------------------------------------------------
;Interface Settings ;Interface Settings
@ -241,25 +241,25 @@ FunctionEnd
!insertmacro MUI_PAGE_COMPONENTS !insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_DIRECTORY
;Start Menu Folder Page Configuration ;Start Menu Folder Page Configuration
!define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKCU" !define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKCU"
!define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\${PRODUCT_NAME}" !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\${PRODUCT_NAME}"
!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder"
!insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER !insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER
!insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_INSTFILES
; Finish page with option to run program ; Finish page with option to run program
; Disabled as GUI requires PATH and working directory to be set correctly ; Disabled as GUI requires PATH and working directory to be set correctly
;!define MUI_FINISHPAGE_RUN "$INSTDIR\${PRODUCT_NAME}.exe" ;!define MUI_FINISHPAGE_RUN "$INSTDIR\${PRODUCT_NAME}.exe"
;!define MUI_FINISHPAGE_NOAUTOCLOSE ;!define MUI_FINISHPAGE_NOAUTOCLOSE
;!insertmacro MUI_PAGE_FINISH ;!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES !insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_UNPAGE_FINISH !insertmacro MUI_UNPAGE_FINISH
;------------------------------------------------------------------------------------------------------ ;------------------------------------------------------------------------------------------------------
;Languages ;Languages
!insertmacro MUI_LANGUAGE "English" !insertmacro MUI_LANGUAGE "English"
;------------------------------------------------------------------------------------------------------ ;------------------------------------------------------------------------------------------------------
;Installer Sections ;Installer Sections
@ -268,7 +268,7 @@ Function .onInit
; Prevent multiple instances of the installer from running ; Prevent multiple instances of the installer from running
System::Call 'kernel32::CreateMutexA(i 0, i 0, t "${PRODUCT_NAME}-setup") i .r1 ?e' System::Call 'kernel32::CreateMutexA(i 0, i 0, t "${PRODUCT_NAME}-setup") i .r1 ?e'
Pop $R0 Pop $R0
StrCmp $R0 0 +3 StrCmp $R0 0 +3
MessageBox MB_OK|MB_ICONEXCLAMATION "The installer is already running." MessageBox MB_OK|MB_ICONEXCLAMATION "The installer is already running."
Abort Abort
@ -279,24 +279,24 @@ FunctionEnd
Section "Main" "secmain" Section "Main" "secmain"
SetOutPath "$INSTDIR" SetOutPath "$INSTDIR"
;ADD YOUR OWN FILES HERE... ;ADD YOUR OWN FILES HERE...
File /r "${PY2EXE_DIR}\*" File /r "${PY2EXE_DIR}\*"
File "${CLIT}" File "${CLIT}"
File "${PDFTOHTML}" File "${PDFTOHTML}"
File /r "${FONTCONFIG}\*" File /r "${FONTCONFIG}\*"
SetOutPath "$INSTDIR\ImageMagick" SetOutPath "$INSTDIR\ImageMagick"
File /r "${IMAGEMAGICK}\*" File /r "${IMAGEMAGICK}\*"
SetOutPath "$SYSDIR" SetOutPath "$SYSDIR"
File "${LIBUNRAR_DIR}\unrar.dll" File "${LIBUNRAR_DIR}\unrar.dll"
DetailPrint " " DetailPrint " "
;Store installation folder ;Store installation folder
WriteRegStr HKCU "Software\${PRODUCT_NAME}" "" $INSTDIR WriteRegStr HKCU "Software\${PRODUCT_NAME}" "" $INSTDIR
;Create uninstaller ;Create uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe" WriteUninstaller "$INSTDIR\Uninstall.exe"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
@ -306,7 +306,7 @@ Section "Main" "secmain"
SetOutPath "$INSTDIR" SetOutPath "$INSTDIR"
!insertmacro MUI_STARTMENU_WRITE_BEGIN Application !insertmacro MUI_STARTMENU_WRITE_BEGIN Application
;Create shortcuts ;Create shortcuts
WriteIniStr "$INSTDIR\${PRODUCT_NAME}.url" "InternetShortcut" "URL" "${WEBSITE}" WriteIniStr "$INSTDIR\${PRODUCT_NAME}.url" "InternetShortcut" "URL" "${WEBSITE}"
CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER" CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER"
@ -317,11 +317,11 @@ Section "Main" "secmain"
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\calibre.exe" CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\calibre.exe"
!insertmacro MUI_STARTMENU_WRITE_END !insertmacro MUI_STARTMENU_WRITE_END
;Add the installation directory to PATH for the commandline tools ;Add the installation directory to PATH for the commandline tools
Push "$INSTDIR" Push "$INSTDIR"
Call AddToPath Call AddToPath
SectionEnd SectionEnd
Section /o "Device Drivers (only needed for PRS500)" "secdd" Section /o "Device Drivers (only needed for PRS500)" "secdd"
@ -338,13 +338,13 @@ Section /o "Device Drivers (only needed for PRS500)" "secdd"
File "${LIBUSB_DIR}\libusb0.sys" File "${LIBUSB_DIR}\libusb0.sys"
;File "${LIBUSB_DIR}\libusb0_x64.dll" ;File "${LIBUSB_DIR}\libusb0_x64.dll"
;File "${LIBUSB_DIR}\libusb0_x64.sys" ;File "${LIBUSB_DIR}\libusb0_x64.sys"
; Uninstall USB drivers ; Uninstall USB drivers
DetailPrint "Uninstalling any existing device drivers" DetailPrint "Uninstalling any existing device drivers"
ExecWait '"$INSTDIR\driver\devcon.exe" remove "USB\VID_054C&PID_029B"' $0 ExecWait '"$INSTDIR\driver\devcon.exe" remove "USB\VID_054C&PID_029B"' $0
DetailPrint "devcon returned exit code $0" DetailPrint "devcon returned exit code $0"
DetailPrint "Installing USB driver for prs500..." DetailPrint "Installing USB driver for prs500..."
ExecWait '"$INSTDIR\driver\devcon.exe" install "$INSTDIR\driver\prs500.inf" "USB\VID_054C&PID_029B"' $0 ExecWait '"$INSTDIR\driver\devcon.exe" install "$INSTDIR\driver\prs500.inf" "USB\VID_054C&PID_029B"' $0
DetailPrint "devcon returned exit code $0" DetailPrint "devcon returned exit code $0"
@ -353,10 +353,10 @@ Section /o "Device Drivers (only needed for PRS500)" "secdd"
Goto +2 Goto +2
MessageBox MB_OK '1. If you have the SONY Connect Reader software installed: $\nGoto Add Remove Programs and uninstall the entry "Windows Driver Package - Sony Corporation (PRSUSB)". $\n$\n2. If your reader is connected to the computer, disconnect and reconnect it now.' MessageBox MB_OK '1. If you have the SONY Connect Reader software installed: $\nGoto Add Remove Programs and uninstall the entry "Windows Driver Package - Sony Corporation (PRSUSB)". $\n$\n2. If your reader is connected to the computer, disconnect and reconnect it now.'
DetailPrint " " DetailPrint " "
SectionEnd SectionEnd
;------------------------------------------------------------------------------------------------------ ;------------------------------------------------------------------------------------------------------
@ -381,7 +381,7 @@ Section "un.DeviceDrivers"
SectionEnd SectionEnd
Section "Uninstall" Section "Uninstall"
;ADD YOUR OWN FILES HERE... ;ADD YOUR OWN FILES HERE...
RMDir /r "$INSTDIR" RMDir /r "$INSTDIR"
!insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP !insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP
@ -404,15 +404,15 @@ Section "Uninstall"
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
; Remove installation directory from PATH ; Remove installation directory from PATH
Push "$INSTDIR" Push "$INSTDIR"
Call un.RemoveFromPath Call un.RemoveFromPath
SectionEnd SectionEnd
''' '''
def __init__(self, name, py2exe_dir, output_dir): def __init__(self, name, py2exe_dir, output_dir):
self.installer = self.__class__.TEMPLATE % dict(name=name, py2exe_dir=py2exe_dir, self.installer = self.__class__.TEMPLATE % dict(name=name, py2exe_dir=py2exe_dir,
version=VERSION, version=VERSION,
outpath=os.path.abspath(output_dir)) outpath=os.path.abspath(output_dir))
def build(self): def build(self):
f = open('installer.nsi', 'w') f = open('installer.nsi', 'w')
path = f.name path = f.name
f.write(self.installer) f.write(self.installer)
@ -420,23 +420,23 @@ SectionEnd
try: try:
subprocess.check_call('"C:\Program Files\NSIS\makensis.exe" /V2 ' + path, shell=True) subprocess.check_call('"C:\Program Files\NSIS\makensis.exe" /V2 ' + path, shell=True)
except: except:
print path print path
else: else:
os.remove(path) os.remove(path)
class BuildEXE(build_exe): class BuildEXE(build_exe):
manifest_resource_id = 0 manifest_resource_id = 0
QT_PREFIX = r'C:\\Qt\\4.4.0' QT_PREFIX = r'C:\\Qt\\4.4.0'
MANIFEST_TEMPLATE = ''' MANIFEST_TEMPLATE = '''
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity version="%(version)s" <assemblyIdentity version="%(version)s"
processorArchitecture="x86" processorArchitecture="x86"
name="net.kovidgoyal.%(prog)s" name="net.kovidgoyal.%(prog)s"
type="win32" type="win32"
/> />
<description>Ebook management application</description> <description>Ebook management application</description>
<!-- Identify the application security requirements. --> <!-- Identify the application security requirements. -->
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security> <security>
@ -474,7 +474,7 @@ class BuildEXE(build_exe):
shutil.rmtree('.build', True) shutil.rmtree('.build', True)
finally: finally:
os.chdir(cwd) os.chdir(cwd)
def run(self): def run(self):
if not os.path.exists(self.dist_dir): if not os.path.exists(self.dist_dir):
os.makedirs(self.dist_dir) os.makedirs(self.dist_dir)
@ -493,7 +493,7 @@ class BuildEXE(build_exe):
shutil.copyfile(qtsvgdll, os.path.join(self.dist_dir, os.path.basename(qtsvgdll))) shutil.copyfile(qtsvgdll, os.path.join(self.dist_dir, os.path.basename(qtsvgdll)))
qtxmldll = os.path.join(os.path.dirname(qtsvgdll), 'QtXml4.dll') qtxmldll = os.path.join(os.path.dirname(qtsvgdll), 'QtXml4.dll')
print 'Adding', qtxmldll print 'Adding', qtxmldll
shutil.copyfile(qtxmldll, shutil.copyfile(qtxmldll,
os.path.join(self.dist_dir, os.path.basename(qtxmldll))) os.path.join(self.dist_dir, os.path.basename(qtxmldll)))
print 'Adding plugins...', print 'Adding plugins...',
qt_prefix = self.QT_PREFIX qt_prefix = self.QT_PREFIX
@ -503,50 +503,45 @@ class BuildEXE(build_exe):
for d in ('imageformats', 'codecs', 'iconengines'): for d in ('imageformats', 'codecs', 'iconengines'):
print d, print d,
imfd = os.path.join(plugdir, d) imfd = os.path.join(plugdir, d)
tg = os.path.join(self.dist_dir, d) tg = os.path.join(self.dist_dir, d)
if os.path.exists(tg): if os.path.exists(tg):
shutil.rmtree(tg) shutil.rmtree(tg)
shutil.copytree(imfd, tg) shutil.copytree(imfd, tg)
print
print 'Adding GUI main.py'
f = zipfile.ZipFile(os.path.join('build', 'py2exe', 'library.zip'), 'a', zipfile.ZIP_DEFLATED)
f.write('src\\calibre\\gui2\\main.py', 'calibre\\gui2\\main.py')
f.close()
print print
print print
print 'Building Installer' print 'Building Installer'
installer = NSISInstaller(APPNAME, self.dist_dir, 'dist') installer = NSISInstaller(APPNAME, self.dist_dir, 'dist')
installer.build() installer.build()
@classmethod @classmethod
def manifest(cls, prog): def manifest(cls, prog):
cls.manifest_resource_id += 1 cls.manifest_resource_id += 1
return (24, cls.manifest_resource_id, return (24, cls.manifest_resource_id,
cls.MANIFEST_TEMPLATE % dict(prog=prog, version=VERSION+'.0')) cls.MANIFEST_TEMPLATE % dict(prog=prog, version=VERSION+'.0'))
def main(): def main():
auto = '--auto' in sys.argv
if auto:
sys.argv.remove('--auto')
sys.argv[1:2] = ['py2exe'] sys.argv[1:2] = ['py2exe']
if '--verbose' not in sys.argv: console = [dict(dest_base=basenames['console'][i], script=scripts['console'][i])
sys.argv.append('--quiet') #py2exe produces too much output by default
subprocess.check_call('python setup.py develop', shell=True)
if auto and not os.path.exists('dist\\auto'):
print os.path.abspath('dist\\auto'), 'does not exist'
return 1
console = [dict(dest_base=basenames['console'][i], script=scripts['console'][i])
for i in range(len(scripts['console']))] for i in range(len(scripts['console']))]
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
setup( setup(
cmdclass = {'py2exe': BuildEXE}, cmdclass = {'py2exe': BuildEXE},
windows = [ windows = [
{'script' : scripts['gui'][0], {'script' : scripts['gui'][0],
'dest_base' : APPNAME, 'dest_base' : APPNAME,
'icon_resources' : [(1, 'icons/library.ico')], 'icon_resources' : [(1, 'icons/library.ico')],
'other_resources' : [BuildEXE.manifest(APPNAME)], 'other_resources' : [BuildEXE.manifest(APPNAME)],
}, },
{'script' : scripts['gui'][1], {'script' : scripts['gui'][1],
'dest_base' : 'lrfviewer', 'dest_base' : 'lrfviewer',
'icon_resources' : [(1, 'icons/viewer.ico')], 'icon_resources' : [(1, 'icons/viewer.ico')],
'other_resources' : [BuildEXE.manifest('lrfviewer')], 'other_resources' : [BuildEXE.manifest('lrfviewer')],
@ -557,24 +552,22 @@ def main():
'optimize' : 2, 'optimize' : 2,
'dist_dir' : PY2EXE_DIR, 'dist_dir' : PY2EXE_DIR,
'includes' : [ 'includes' : [
'sip', 'pkg_resources', 'PyQt4.QtSvg', 'sip', 'pkg_resources', 'PyQt4.QtSvg',
'mechanize', 'ClientForm', 'wmi', 'mechanize', 'ClientForm', 'wmi',
'win32file', 'pythoncom', 'rtf2xml', 'win32file', 'pythoncom', 'rtf2xml',
'lxml', 'lxml._elementpath', 'genshi', 'lxml', 'lxml._elementpath', 'genshi',
'path', 'pydoc', 'IPython.Extensions.*', 'path', 'pydoc', 'IPython.Extensions.*',
'calibre.web.feeds.recipes.*', 'pydoc', 'calibre.web.feeds.recipes.*', 'pydoc',
], ],
'packages' : ['PIL'], 'packages' : ['PIL'],
'excludes' : ["Tkconstants", "Tkinter", "tcl", 'excludes' : ["Tkconstants", "Tkinter", "tcl",
"_imagingtk", "ImageTk", "FixTk", "_imagingtk", "ImageTk", "FixTk",
'pydoc'], 'pydoc'],
'dll_excludes' : ['mswsock.dll'], 'dll_excludes' : ['mswsock.dll'],
}, },
}, },
) )
if auto:
subprocess.call(('shutdown', '-s', '-f', '-t', '01'))
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':