diff --git a/Makefile b/Makefile index c9f7ca1db4..1abffcec66 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,10 @@ 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 clean : diff --git a/linux_installer.py b/linux_installer.py new file mode 100644 index 0000000000..315b2d8486 --- /dev/null +++ b/linux_installer.py @@ -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) diff --git a/osx_installer.py b/osx_installer.py index 79e363618f..cccb46ad93 100644 --- a/osx_installer.py +++ b/osx_installer.py @@ -99,8 +99,7 @@ _check_symlinks_prescript() includes=list(self.includes) + main_modules['console'], packages=self.packages, excludes=self.excludes, - debug=debug, - ) + debug=debug) @classmethod def makedmg(cls, d, volname, @@ -249,6 +248,11 @@ _check_symlinks_prescript() else: os.link(src, os.path.join(module_dir, dest)) 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' sf = [os.path.basename(s) for s in all_names] 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(): -# auto = '--auto' in sys.argv -# 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.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) sys.argv[1:2] = ['py2app'] setup( name = APPNAME, @@ -300,12 +298,12 @@ def main(): 'PyQt4.QtSvg', 'mechanize', 'ClientForm', 'usbobserver', 'genshi', 'calibre.web.feeds.recipes.*', - 'IPython.Extensions.*', 'pydoc'], + 'keyword', 'codeop', 'pydoc'], 'packages' : ['PIL', 'Authorization', 'rtf2xml', 'lxml'], - 'excludes' : [], + 'excludes' : ['IPython'], 'plist' : { 'CFBundleGetInfoString' : '''calibre, an E-book management application.''' ''' Visit http://calibre.kovidgoyal.net for details.''', - 'CFBundleIdentifier':'net.kovidgoyal.librs500', + 'CFBundleIdentifier':'net.kovidgoyal.calibre', 'CFBundleShortVersionString':VERSION, 'CFBundleVersion':APPNAME + ' ' + VERSION, 'LSMinimumSystemVersion':'10.4.3', diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 7c1656a614..14c1b5d13e 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -1,7 +1,7 @@ ''' E-book management software''' __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' -__version__ = '0.4.70' +__version__ = '0.4.72' __docformat__ = "epytext" __author__ = "Kovid Goyal " __appname__ = 'calibre' @@ -447,14 +447,13 @@ class Settings(QSettings): self.setValue(str(key), QVariant(QByteArray(val))) _settings = Settings() + if not _settings.get('rationalized'): __settings = Settings(name='calibre') dbpath = os.path.join(os.path.expanduser('~'), 'library1.db').decode(sys.getfilesystemencoding()) dbpath = unicode(__settings.value('database path', QVariant(QString.fromUtf8(dbpath.encode('utf-8')))).toString()) cmdline = __settings.value('LRF conversion defaults', QVariant(QByteArray(''))).toByteArray().data() - - _settings.set('database path', dbpath) if cmdline: cmdline = cPickle.loads(cmdline) @@ -464,6 +463,7 @@ if not _settings.get('rationalized'): os.unlink(unicode(__settings.fileName())) except: pass + _settings.set('database path', dbpath) _spat = re.compile(r'^the\s+|^a\s+|^an\s+', re.IGNORECASE) def english_sort(x, y): diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index 1a3c346210..0050e091d3 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -275,10 +275,10 @@ class PRS505(Device): if not iswindows: if self._main_prefix is not None: 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: stats = os.statvfs(self._card_prefix) - csz = stats.f_bsize * stats.f_bavail + csz = stats.f_frsize * stats.f_bavail else: msz = self._windows_space(self._main_prefix)[1] csz = self._windows_space(self._card_prefix)[1] diff --git a/src/calibre/gui2/dialogs/fetch_metadata.py b/src/calibre/gui2/dialogs/fetch_metadata.py index 9a64c8dc42..f905e72e81 100644 --- a/src/calibre/gui2/dialogs/fetch_metadata.py +++ b/src/calibre/gui2/dialogs/fetch_metadata.py @@ -12,7 +12,7 @@ from PyQt4.QtGui import QDialog, QItemSelectionModel from calibre.gui2.dialogs.fetch_metadata_ui import Ui_FetchMetadata from calibre.gui2 import error_dialog, NONE, info_dialog -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 class Matches(QAbstractTableModel): @@ -88,8 +88,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata): self.connect(self.matches, SIGNAL('activated(QModelIndex)'), self.chosen) key = str(self.key.text()) if key: - QTimer.singleShot(100, self.fetch.click) - + QTimer.singleShot(100, self.fetch_metadata) 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')) return else: - Settings().set('isbndb.com key', str(self.key.text())) + Settings().set('isbndb.com key', key) args = ['isbndb'] if self.isbn: @@ -121,36 +120,41 @@ class FetchMetadata(QDialog, Ui_FetchMetadata): self.fetch.setEnabled(False) self.setCursor(Qt.WaitCursor) QCoreApplication.instance().processEvents() - - args.append(key) - parser = option_parser() - opts, args = parser.parse_args(args) - - self.logger = logging.getLogger('Job #'+str(id)) - self.logger.setLevel(logging.DEBUG) - self.log_dest = cStringIO.StringIO() - handler = logging.StreamHandler(self.log_dest) - handler.setLevel(logging.DEBUG) - handler.setFormatter(logging.Formatter('[%(levelname)s] %(filename)s:%(lineno)s: %(message)s')) - self.logger.addHandler(handler) - - books = create_books(opts, args, self.logger, self.timeout) - - self.model = Matches(books) - if self.model.rowCount() < 1: - info_dialog(self, _('No metadata found'), _('No metadata found, try adjusting the title and author or the ISBN key.')).exec_() - self.reject() - - self.matches.setModel(self.model) - QObject.connect(self.matches.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'), - self.show_summary) - self.model.reset() - self.matches.selectionModel().select(self.model.index(0, 0), - QItemSelectionModel.Select | QItemSelectionModel.Rows) - self.matches.setCurrentIndex(self.model.index(0, 0)) - self.fetch.setEnabled(True) - self.unsetCursor() - self.matches.resizeColumnsToContents() + try: + args.append(key) + parser = option_parser() + opts, args = parser.parse_args(args) + + self.logger = logging.getLogger('Job #'+str(id)) + self.logger.setLevel(logging.DEBUG) + self.log_dest = cStringIO.StringIO() + handler = logging.StreamHandler(self.log_dest) + handler.setLevel(logging.DEBUG) + handler.setFormatter(logging.Formatter('[%(levelname)s] %(filename)s:%(lineno)s: %(message)s')) + self.logger.addHandler(handler) + + try: + books = create_books(opts, args, self.logger, self.timeout) + except ISBNDBError, err: + error_dialog(self, _('Error fetching metadata'), str(err)).exec_() + return + + self.model = Matches(books) + if self.model.rowCount() < 1: + info_dialog(self, _('No metadata found'), _('No metadata found, try adjusting the title and author or the ISBN key.')).exec_() + self.reject() + + self.matches.setModel(self.model) + QObject.connect(self.matches.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'), + self.show_summary) + self.model.reset() + self.matches.selectionModel().select(self.model.index(0, 0), + QItemSelectionModel.Select | QItemSelectionModel.Rows) + self.matches.setCurrentIndex(self.model.index(0, 0)) + finally: + self.fetch.setEnabled(True) + self.unsetCursor() + self.matches.resizeColumnsToContents() diff --git a/src/calibre/gui2/dialogs/fetch_metadata.ui b/src/calibre/gui2/dialogs/fetch_metadata.ui index 959fb18d89..e8b4252d8d 100644 --- a/src/calibre/gui2/dialogs/fetch_metadata.ui +++ b/src/calibre/gui2/dialogs/fetch_metadata.ui @@ -47,7 +47,7 @@ - &Access Key; + &Access Key: key diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index af2c97a3be..f4fce00482 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -6,7 +6,7 @@ add/remove formats ''' 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 diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index 28c760c74e..1efc04fb24 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -8,8 +8,8 @@ from math import cos, sin, pi from itertools import repeat from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \ QItemDelegate, QPainterPath, QLinearGradient, QBrush, \ - QPen, QStyle, QPainter, QLineEdit, QApplication, \ - QPalette, QImage + QPen, QStyle, QPainter, QLineEdit, \ + QPalette, QImage, QApplication from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \ QCoreApplication, SIGNAL, QObject, QSize, QModelIndex @@ -22,7 +22,7 @@ class LibraryDelegate(QItemDelegate): COLOR = QColor("blue") SIZE = 16 PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) - + def __init__(self, parent): QItemDelegate.__init__(self, parent) self.star_path = QPainterPath() @@ -41,10 +41,10 @@ class LibraryDelegate(QItemDelegate): def sizeHint(self, option, index): #num = index.model().data(index, Qt.DisplayRole).toInt()[0] return QSize(5*(self.SIZE), self.SIZE+4) - + def paint(self, painter, option, index): num = index.model().data(index, Qt.DisplayRole).toInt()[0] - def draw_star(): + def draw_star(): painter.save() painter.scale(self.factor, self.factor) painter.translate(50.0, 50.0) @@ -52,16 +52,18 @@ class LibraryDelegate(QItemDelegate): painter.translate(-50.0, -50.0) painter.drawPath(self.star_path) painter.restore() - + 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: - if option.state & QStyle.State_Selected: - painter.fillRect(option.rect, option.palette.highlight()) 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 - painter.setPen(self.PEN) - painter.setBrush(self.brush) + painter.setPen(self.PEN) + painter.setBrush(self.brush) painter.translate(x, y) i = 0 while i < num: @@ -71,7 +73,7 @@ class LibraryDelegate(QItemDelegate): except Exception, e: traceback.print_exc(e) painter.restore() - + def createEditor(self, parent, option, index): sb = QItemDelegate.createEditor(self, parent, option, index) sb.setMinimum(0) @@ -105,22 +107,22 @@ class BooksModel(QAbstractTableModel): self.read_config() self.buffer_size = buffer self.cover_cache = None - + def clear_caches(self): if self.cover_cache: self.cover_cache.clear_cache() - + def read_config(self): self.use_roman_numbers = Settings().get('use roman numerals for series number', True) - - + + def set_database(self, db): if isinstance(db, (QString, basestring)): if isinstance(db, QString): db = qstring_to_unicode(db) db = LibraryDatabase(os.path.expanduser(db)) self.db = db - + def refresh_ids(self, ids, current_row=-1): rows = self.db.refresh_ids(ids) for row in rows: @@ -132,24 +134,24 @@ class BooksModel(QAbstractTableModel): self.get_book_display_info(row)) self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), self.index(row, 0), self.index(row, self.columnCount(None)-1)) - + def close(self): self.db.close() self.db = None self.reset() - + def add_books(self, paths, formats, metadata, uris=[], add_duplicates=False): return self.db.add_books(paths, formats, metadata, uris, add_duplicates=add_duplicates) - + def row_indices(self, index): ''' Return list indices of all cells in index.row()''' return [ self.index(index.row(), c) for c in range(self.columnCount(None))] - + def save_to_disk(self, rows, path, single_dir=False): rows = [row.row() for row in rows] self.db.export_to_dir(path, rows, self.sorted_on[0] == 1, single_dir=single_dir) - + def delete_books(self, indices): ids = [ self.id(i) for i in indices ] for id in ids: @@ -159,10 +161,10 @@ class BooksModel(QAbstractTableModel): self.endRemoveRows() self.clear_caches() self.reset() - + def search_tokens(self, text): return text_to_tokens(text) - + def search(self, text, refinement, reset=True): tokens, OR = self.search_tokens(text) self.db.filter(tokens, refilter=refinement, OR=OR) @@ -170,7 +172,7 @@ class BooksModel(QAbstractTableModel): if reset: self.clear_caches() self.reset() - + def sort(self, col, order, reset=True): if not self.db: return @@ -179,30 +181,30 @@ class BooksModel(QAbstractTableModel): self.research() if reset: self.clear_caches() - self.reset() + self.reset() self.sorted_on = (col, order) - + def resort(self, reset=True): self.sort(*self.sorted_on, **dict(reset=reset)) - + def research(self, reset=True): self.search(self.last_search, False, reset=reset) - + def database_needs_migration(self): path = os.path.expanduser('~/library.db') return self.db.is_empty() and \ os.path.exists(path) and\ LibraryDatabase.sizeof_old_database(path) > 0 - + def columnCount(self, parent): return len(self.cols) - + def rowCount(self, parent): return self.db.rows() if self.db else 0 - + def count(self): return self.rowCount(None) - + def get_book_display_info(self, idx): data = {} cdata = self.cover(idx) @@ -229,9 +231,9 @@ class BooksModel(QAbstractTableModel): sidx = self.db.series_index(idx) sidx = self.__class__.roman(sidx) if self.use_roman_numbers else str(sidx) data[_('Series')] = _('Book %s of %s.')%(sidx, series) - + return data - + def set_cache(self, idx): l, r = 0, self.count()-1 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 = [self.db.id(i) for i in ids] self.cover_cache.set_cache(ids) - + def current_changed(self, current, previous, emit_signal=True): idx = current.row() self.set_cache(idx) @@ -253,7 +255,7 @@ class BooksModel(QAbstractTableModel): self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data) else: return data - + def get_book_info(self, index): data = self.current_changed(index, None, False) row = index.row() @@ -264,8 +266,8 @@ class BooksModel(QAbstractTableModel): au = ', '.join([a.strip() for a in au.split(',')]) data[_('Author(s)')] = au return data - - + + def get_metadata(self, rows): metadata = [] for row in rows: @@ -296,11 +298,11 @@ class BooksModel(QAbstractTableModel): 'comments': self.db.comments(row), } 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) return metadata - + def get_preferred_formats(self, rows, formats): ans = [] for row in (row.row() for row in rows): @@ -313,17 +315,17 @@ class BooksModel(QAbstractTableModel): pt = PersistentTemporaryFile(suffix='.'+format) pt.write(self.db.format(row, format)) pt.seek(0) - ans.append(pt) + ans.append(pt) else: ans.append(None) return ans - + def id(self, row): return self.db.id(row.row()) - + def title(self, row_number): return self.db.title(row_number) - + def cover(self, row_number): id = self.db.id(row_number) data = None @@ -342,15 +344,15 @@ class BooksModel(QAbstractTableModel): if img.isNull(): img = self.default_image return img - + 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() if col == 0: text = self.db.title(row) if text: return QVariant(text) - elif col == 1: + elif col == 1: au = self.db.authors(row) if au: au = au.split(',') @@ -364,18 +366,18 @@ class BooksModel(QAbstractTableModel): if dt: dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight) return QVariant(dt.strftime(BooksView.TIME_FMT).decode(preferred_encoding, 'replace')) - elif col == 4: + elif col == 4: r = self.db.rating(row) r = r/2 if r else 0 return QVariant(r) - elif col == 5: + elif col == 5: pub = self.db.publisher(row) - if pub: + if pub: return QVariant(pub) elif col == 6: tags = self.db.tags(row) if tags: - return QVariant(', '.join(tags.split(','))) + return QVariant(', '.join(tags.split(','))) elif col == 7: series = self.db.series(row) if series: @@ -383,16 +385,16 @@ class BooksModel(QAbstractTableModel): return NONE elif role == Qt.TextAlignmentRole and index.column() in [2, 3, 4]: 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: return QVariant(_("Double click to edit me

")) return NONE - - def headerData(self, section, orientation, role): + + def headerData(self, section, orientation, role): if role != Qt.DisplayRole: return NONE text = "" - if orientation == Qt.Horizontal: + if orientation == Qt.Horizontal: if section == 0: text = _("Title") elif section == 1: text = _("Author(s)") elif section == 2: text = _("Size (MB)") @@ -402,16 +404,16 @@ class BooksModel(QAbstractTableModel): elif section == 6: text = _("Tags") elif section == 7: text = _("Series") return QVariant(text) - else: + else: return QVariant(section+1) - + def flags(self, index): flags = QAbstractTableModel.flags(self, index) - if index.isValid(): - if index.column() in self.editable_cols: - flags |= Qt.ItemIsEditable + if index.isValid(): + if index.column() in self.editable_cols: + flags |= Qt.ItemIsEditable return flags - + def setData(self, index, value, role): done = False if role == Qt.EditRole: @@ -424,7 +426,7 @@ class BooksModel(QAbstractTableModel): val = 0 if val < 0 else 5 if val > 5 else val val *= 2 column = self.cols[col] - self.db.set(row, column, val) + self.db.set(row, column, val) self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \ index, index) if col == self.sorted_on[0]: @@ -435,17 +437,17 @@ class BooksModel(QAbstractTableModel): class BooksView(TableView): TIME_FMT = '%d %b %Y' wrapper = textwrap.TextWrapper(width=20) - + @classmethod - def wrap(cls, s, width=20): + def wrap(cls, s, width=20): cls.wrapper.width = width return cls.wrapper.fill(s) - + @classmethod def human_readable(cls, size, precision=1): """ Convert a size in bytes into megabytes """ return ('%.'+str(precision)+'f') % ((size/(1024.*1024.)),) - + def __init__(self, parent, modelcls=BooksModel): TableView.__init__(self, parent) self.display_parent = parent @@ -454,7 +456,7 @@ class BooksView(TableView): self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSortingEnabled(True) 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)'), self._model.current_changed) # 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) QObject.connect(self.model(), SIGNAL('modelReset()'), self.resizeRowsToContents) self.set_visible_columns() - + @classmethod 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. ''' if event.mimeData().hasFormat('text/uri-list'): 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)] - + def dragEnterEvent(self, event): if int(event.possibleActions() & Qt.CopyAction) + \ int(event.possibleActions() & Qt.MoveAction) == 0: return paths = self.paths_from_event(event) - + if paths: event.acceptProposedAction() - + def dragMoveEvent(self, event): event.acceptProposedAction() - + def dropEvent(self, event): paths = self.paths_from_event(event) event.setDropAction(Qt.CopyAction) 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): self._model.set_database(db) - + def close(self): self._model.close() - + def migrate_database(self): if self.model().database_needs_migration(): print 'Migrating database from pre 0.4.0 version' @@ -510,39 +512,39 @@ class BooksView(TableView): progress.setModal(True) progress.setValue(0) app = QCoreApplication.instance() - + def meter(count): progress.setValue(count) app.processEvents() progress.setWindowTitle('Upgrading database') progress.show() LibraryDatabase.import_old_database(path, self._model.db.conn, meter) - + 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) - + def connect_to_book_display(self, bd): QObject.connect(self._model, SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), bd) - + class DeviceBooksView(BooksView): - + def __init__(self, parent): BooksView.__init__(self, parent, DeviceBooksModel) self.columns_resized = False self.resize_on_select = False - + def resizeColumnsToContents(self): QTableView.resizeColumnsToContents(self) self.columns_resized = True - + def connect_dirtied_signal(self, slot): QObject.connect(self._model, SIGNAL('booklist_dirtied()'), slot) class DeviceBooksModel(BooksModel): - + def __init__(self, parent): BooksModel.__init__(self, parent) self.db = [] @@ -550,15 +552,15 @@ class DeviceBooksModel(BooksModel): self.sorted_map = [] self.unknown = str(self.trUtf8('Unknown')) self.marked_for_deletion = {} - - + + def mark_for_deletion(self, id, rows): self.marked_for_deletion[id] = self.indices(rows) for row in rows: indices = self.row_indices(row) self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1]) - - + + def deletion_done(self, id, succeeded=True): if not self.marked_for_deletion.has_key(id): return @@ -566,29 +568,29 @@ class DeviceBooksModel(BooksModel): for row in rows: if not succeeded: 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): self.map = list(range(0, len(self.db))) self.resort(False) self.research(True) - + def indices_to_be_deleted(self): ans = [] for v in self.marked_for_deletion.values(): ans.extend(v) return ans - + def flags(self, index): 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 flags = QAbstractTableModel.flags(self, index) - if index.isValid(): - if index.column() in [0, 1] or (index.column() == 4 and self.db.supports_tags()): - flags |= Qt.ItemIsEditable + if index.isValid(): + if index.column() in [0, 1] or (index.column() == 4 and self.db.supports_tags()): + flags |= Qt.ItemIsEditable return flags - - + + def search(self, text, refinement, reset=True): tokens, OR = self.search_tokens(text) base = self.map if refinement else self.sorted_map @@ -611,13 +613,13 @@ class DeviceBooksModel(BooksModel): break if add: result.append(i) - + self.map = result if reset: self.reset() self.last_search = text - + def sort(self, col, order, reset=True): descending = order != Qt.AscendingOrder def strcmp(attr): @@ -632,7 +634,7 @@ class DeviceBooksModel(BooksModel): x, y = x.strip().lower(), y.strip().lower() return cmp(x, y) return _strcmp - def datecmp(x, y): + def datecmp(x, y): x = self.db[x].datetime y = self.db[y].datetime 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) return cmp(x, y) 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) if len(self.map) == len(self.db): self.sorted_map = list(self.map) @@ -653,17 +655,17 @@ class DeviceBooksModel(BooksModel): self.sorted_on = (col, order) if reset: self.reset() - + def columnCount(self, parent): return 5 - + def rowCount(self, parent): return len(self.map) - + def set_database(self, db): self.db = db self.map = list(range(0, len(db))) - + def current_changed(self, current, previous): data = {} item = self.db[self.map[current.row()]] @@ -686,26 +688,26 @@ class DeviceBooksModel(BooksModel): data[_('Timestamp')] = dt.ctime() data[_('Tags')] = ', '.join(item.tags) self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data) - + def paths(self, rows): return [self.db[self.map[r.row()]].path for r in rows ] - + def indices(self, rows): ''' Return indices into underlying database from rows ''' return [ self.map[r.row()] for r in rows] - - + + 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() if col == 0: text = self.db[self.map[row]].title if not text: text = self.unknown return QVariant(text) - elif col == 1: + elif col == 1: au = self.db[self.map[row]].authors if not au: au = self.unknown @@ -726,40 +728,40 @@ class DeviceBooksModel(BooksModel): dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight) return QVariant(dt.strftime(BooksView.TIME_FMT)) elif col == 4: - tags = self.db[self.map[row]].tags + tags = self.db[self.map[row]].tags if tags: - return QVariant(', '.join(tags)) + return QVariant(', '.join(tags)) elif role == Qt.TextAlignmentRole and index.column() in [2, 3]: return QVariant(Qt.AlignRight | Qt.AlignVCenter) elif role == Qt.ToolTipRole and index.isValid(): if self.map[index.row()] in self.indices_to_be_deleted(): - return QVariant('Marked for deletion') + return QVariant('Marked for deletion') col = index.column() if col in [0, 1] or (col == 4 and self.db.supports_tags()): return QVariant("Double click to edit me

") return NONE - - def headerData(self, section, orientation, role): + + def headerData(self, section, orientation, role): if role != Qt.DisplayRole: return NONE text = "" - if orientation == Qt.Horizontal: + if orientation == Qt.Horizontal: if section == 0: text = _("Title") elif section == 1: text = _("Author(s)") elif section == 2: text = _("Size (MB)") elif section == 3: text = _("Date") elif section == 4: text = _("Tags") return QVariant(text) - else: + else: return QVariant(section+1) - + def setData(self, index, value, role): done = False if role == Qt.EditRole: row, col = index.row(), index.column() if col in [2, 3]: return False - val = qstring_to_unicode(value.toString()).strip() + val = qstring_to_unicode(value.toString()).strip() idx = self.map[row] if col == 0: self.db[idx].title = val @@ -778,9 +780,9 @@ class DeviceBooksModel(BooksModel): return done class SearchBox(QLineEdit): - + INTERVAL = 1000 #: Time to wait before emitting search signal - + def __init__(self, parent): QLineEdit.__init__(self, parent) self.help_text = _('Search (For Advanced Search click the button to the left)') @@ -792,40 +794,40 @@ class SearchBox(QLineEdit): self.timer = None self.clear_to_help() QObject.connect(self, SIGNAL('textEdited(QString)'), self.text_edited_slot) - - + + def normalize_state(self): self.setText('') self.setPalette(self.default_palette) - + def clear_to_help(self): self.setPalette(self.gray) self.setText(self.help_text) - self.home(False) + self.home(False) self.initial_state = True - + def clear(self): self.clear_to_help() self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), '', False) - - + + def keyPressEvent(self, event): if self.initial_state: self.normalize_state() self.initial_state = False QLineEdit.keyPressEvent(self, event) - + def mouseReleaseEvent(self, event): if self.initial_state: self.normalize_state() self.initial_state = False QLineEdit.mouseReleaseEvent(self, event) - + def text_edited_slot(self, text): text = qstring_to_unicode(text) if isinstance(text, QString) else unicode(text) self.prev_text = text self.timer = self.startTimer(self.__class__.INTERVAL) - + def timerEvent(self, event): self.killTimer(event.timerId()) if event.timerId() == self.timer: @@ -833,7 +835,7 @@ class SearchBox(QLineEdit): refinement = text.startswith(self.prev_search) and ':' not in text self.prev_search = text self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), text, refinement) - + def set_search_string(self, txt): self.normalize_state() self.setText(txt) diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index e5e2bb4b98..32523cc8a5 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -4,7 +4,7 @@ import os, sys, textwrap, collections, traceback, time from xml.parsers.expat import ExpatError from functools import partial 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, \ QToolButton, QDialog, QDesktopServices from PyQt4.QtSvg import QSvgRenderer @@ -1210,4 +1210,14 @@ def main(args=sys.argv): 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) diff --git a/src/calibre/gui2/main.ui b/src/calibre/gui2/main.ui index c53f89b3ec..e38b3833f6 100644 --- a/src/calibre/gui2/main.ui +++ b/src/calibre/gui2/main.ui @@ -27,9 +27,9 @@ 0 - 86 + 79 865 - 712 + 716 @@ -44,7 +44,7 @@ - + 0 0 @@ -52,19 +52,25 @@ 10000 - 100 + 110 Qt::ScrollBarAlwaysOff - Qt::ScrollBarAlwaysOff + Qt::ScrollBarAsNeeded + + + true + + + true - 32 - 32 + 40 + 40 @@ -77,14 +83,11 @@ false - 20 + 10 QListView::IconMode - - true - @@ -332,8 +335,8 @@ 0 0 - 847 - 553 + 857 + 552
@@ -380,7 +383,7 @@ 0 0 865 - 86 + 79 @@ -425,9 +428,9 @@ 0 - 798 + 795 865 - 24 + 27 diff --git a/src/calibre/gui2/pictureflow/PyQt/configure.py b/src/calibre/gui2/pictureflow/PyQt/configure.py index 04c7c8fe91..e24e9a8405 100644 --- a/src/calibre/gui2/pictureflow/PyQt/configure.py +++ b/src/calibre/gui2/pictureflow/PyQt/configure.py @@ -1,4 +1,4 @@ -import os, sys, glob +import os, sys, glob, shutil import sipconfig if os.environ.get('PYQT4PATH', None): print os.environ['PYQT4PATH'] @@ -37,7 +37,7 @@ makefile = pyqtconfig.QtGuiModuleMakefile ( # ".dll" extension on Windows). if 'linux' in sys.platform: 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 = ['.'] else: makefile.extra_lib_dirs = ['..\\..\\.build\\release', '../../.build', '.'] diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 408b8329e5..1f9de46c5b 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -7,9 +7,9 @@ import re, os from PyQt4.QtGui import QListView, QIcon, QFont, QLabel, QListWidget, \ QListWidgetItem, QTextCharFormat, QApplication, \ QSyntaxHighlighter, QCursor, QColor, QWidget, \ - QAbstractItemDelegate, QPixmap -from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, QRect, SIGNAL, \ - QObject, QRegExp, QRectF + QAbstractItemDelegate, QPixmap, QStyle, QFontMetrics +from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, SIGNAL, \ + QObject, QRegExp, QString from calibre.gui2.jobs import DetailView from calibre.gui2 import human_readable, NONE, TableView, qstring_to_unicode, error_dialog @@ -123,40 +123,43 @@ class LocationDelegate(QAbstractItemDelegate): def __init__(self): QAbstractItemDelegate.__init__(self) - self.icon_rect = QRect(0, 10, 150, 45) - self.buffer = 5 + self.pixmap = QPixmap(40, 40) + self.text = QString('Reader\n999.9 MB Available202') - def get_rects(self, index, option): - row = index.row() - irect = QRect(self.icon_rect) - irect.translate(row*(irect.width()+self.buffer), 0) - trect = irect.translated(0, irect.height()) - trect.adjust(0, 7, 0, 0) - return irect.adjusted(50, 0, -50, 0), trect + def rects(self, option): + style = QApplication.style() + font = QFont(option.font) + font.setBold(True) + irect = style.itemPixmapRect(option.rect, Qt.AlignHCenter|Qt.AlignTop, self.pixmap) + trect = style.itemTextRect(QFontMetrics(font), option.rect, + Qt.AlignHCenter|Qt.AlignTop, True, self.text) + trect.moveTop(irect.bottom()) + return irect, trect def sizeHint(self, option, index): - irect, trect = self.get_rects(index, option) + irect, trect = self.rects(option) return irect.united(trect).size() def paint(self, painter, option, index): - font = QFont() - 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() + style = QApplication.style() painter.save() - irect, trect = self.get_rects(index, option) - - mode = QIcon.Normal - if highlight: - font.setBold(True) - mode = QIcon.Active - + if hasattr(QStyle, 'CE_ItemViewItem'): + QApplication.style().drawControl(QStyle.CE_ItemViewItem, option, painter) + highlight = getattr(index.model(), 'highlight_row', -1) == index.row() + mode = QIcon.Active if highlight else QIcon.Normal + pixmap = QIcon(index.model().data(index, Qt.DecorationRole)).pixmap(self.pixmap.size()) + 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) - icon.paint(painter, irect, Qt.AlignHCenter|Qt.AlignTop, mode, QIcon.On) - painter.drawText(QRectF(trect), Qt.AlignTop|Qt.AlignHCenter, text) + style.drawItemText(painter, trect, Qt.AlignHCenter|Qt.AlignBottom, + option.palette, True, text) painter.restore() - + + class LocationModel(QAbstractListModel): def __init__(self, parent): QAbstractListModel.__init__(self, parent) diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index ec4788203c..2cca103a3f 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -15,6 +15,7 @@ from calibre.gui2 import SingleApplication from calibre.ebooks.metadata.meta import get_metadata from calibre.library.database2 import LibraryDatabase2 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']) @@ -304,9 +305,67 @@ do nothing. do_remove_format(get_db(dbpath, opts), id, fmt) 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): - commands = ('list', 'add', 'remove', 'add_format', 'remove_format') + commands = ('list', 'add', 'remove', 'add_format', 'remove_format', + 'show_metadata', 'set_metadata') parser = OptionParser(_( '''\ %%prog command [options] [arguments] diff --git a/src/calibre/library/database.py b/src/calibre/library/database.py index a09505b007..4171fb28ac 100644 --- a/src/calibre/library/database.py +++ b/src/calibre/library/database.py @@ -911,13 +911,19 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; def title(self, index, index_is_id=False): if not index_is_id: 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): ''' Authors as a comma separated list or None''' if not index_is_id: 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): 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): 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] def rating(self, index, index_is_id=False): 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] def timestamp(self, index, index_is_id=False): 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] def max_size(self, index, index_is_id=False): 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] 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} ''' + if mi.title: + self.set_title(id, mi.title) if not mi.authors: mi.authors = ['Unknown'] authors = [] @@ -1516,6 +1524,9 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; def has_book(self, mi): 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): root = os.path.abspath(root) duplicates = [] diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 34872cbdb3..9a12253b62 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -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? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -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 `_. +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 `_. I want some feature added to |app|. What can I do? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/calibre/translations/bg.po b/src/calibre/translations/bg.po index 6cf334ee75..a8d83a4ce0 100644 --- a/src/calibre/translations/bg.po +++ b/src/calibre/translations/bg.po @@ -13,7 +13,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\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" "Generated-By: pygettext.py 1.5\n" diff --git a/src/calibre/translations/ca.po b/src/calibre/translations/ca.po index cb5552d3fd..4f6ba82b26 100644 --- a/src/calibre/translations/ca.po +++ b/src/calibre/translations/ca.po @@ -17,7 +17,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\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" #~ msgid "" diff --git a/src/calibre/translations/de.po b/src/calibre/translations/de.po index 2b0290aaf7..8c4a6e9641 100644 --- a/src/calibre/translations/de.po +++ b/src/calibre/translations/de.po @@ -14,7 +14,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\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" "Generated-By: pygettext.py 1.5\n" diff --git a/src/calibre/translations/es.po b/src/calibre/translations/es.po index ea7b148db3..60e3560a54 100644 --- a/src/calibre/translations/es.po +++ b/src/calibre/translations/es.po @@ -11,13 +11,13 @@ msgstr "" "Project-Id-Version: es\n" "Report-Msgid-Bugs-To: \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 \n" "Language-Team: Spanish\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\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" #~ 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:174 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 msgid "Set the title. Default: filename." @@ -671,7 +671,6 @@ msgid "Creating XML..." msgstr "Creando XML..." #: /home/kovid/work/calibre/src/calibre/ebooks/lrf/lrfparser.py:156 -#, fuzzy msgid "LRS written to " 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" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:219 -#, fuzzy msgid "Free unused diskspace from the database" msgstr "Espacio de disco disponible de la base de datos" @@ -1896,18 +1894,16 @@ msgstr "" "actual" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:63 -#, fuzzy msgid "No recipe selected" msgstr "No hay ninguna receta seleccionada" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:69 -#, fuzzy 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 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:96 @@ -1941,9 +1937,8 @@ msgid "Already exists" msgstr "Ya existe" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:121 -#, fuzzy 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: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?" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:187 -#, fuzzy 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 msgid "Recipes" @@ -2651,6 +2645,9 @@ msgid "" "href=\"http://calibre.kovidgoyal.net/wiki/Changelog\">new features. " "Visit the download page?" msgstr "" +"%s se ha actualizado a la versión %s. Ver las nuevas " +"características. Visita la página de descarga?" #: /home/kovid/work/calibre/src/calibre/gui2/main.py:1155 msgid "Update available" @@ -2833,6 +2830,8 @@ msgid "" "Path to the calibre database. Default is to use the path stored in the " "settings." 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 msgid "" @@ -2840,6 +2839,9 @@ msgid "" "\n" "List the books available in the calibre database. \n" 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 msgid "" @@ -2866,6 +2868,9 @@ msgid "" "please see the search related documentation in the User Manual. Default is " "to do no filtering." 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 msgid "Invalid fields. Available fields:" @@ -2891,12 +2896,20 @@ msgid "" "directories, see\n" "the directory related options below. \n" 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 msgid "" "Assume that each directory has only a single logical book and that all files " "in it are different e-book formats of that book" 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 msgid "Process directories recursively" @@ -2922,6 +2935,12 @@ msgid "" "separated list of id numbers (you can get id numbers by using the list " "command). For example, 23,34,57-85\n" 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 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 " "EPUB. If the logical book does not have fmt available, do nothing.\n" 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 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 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 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 msgid "host must be of the form user@hostname" @@ -2988,11 +3014,11 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/utils/sftp.py:68 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 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:77 diff --git a/src/calibre/translations/fr.po b/src/calibre/translations/fr.po index ce9997db8d..7454c5abe2 100644 --- a/src/calibre/translations/fr.po +++ b/src/calibre/translations/fr.po @@ -13,7 +13,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\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" "Generated-By: pygettext.py 1.5\n" diff --git a/src/calibre/translations/it.po b/src/calibre/translations/it.po index 378142d18c..445a32c0e7 100644 --- a/src/calibre/translations/it.po +++ b/src/calibre/translations/it.po @@ -15,7 +15,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\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" "Generated-By: pygettext.py 1.5\n" diff --git a/src/calibre/translations/nds.po b/src/calibre/translations/nds.po index 6090676765..9e837a78bd 100644 --- a/src/calibre/translations/nds.po +++ b/src/calibre/translations/nds.po @@ -14,7 +14,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\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" "Generated-By: pygettext.py 1.5\n" diff --git a/src/calibre/translations/nl.po b/src/calibre/translations/nl.po index a901375f9e..e3ec255958 100644 --- a/src/calibre/translations/nl.po +++ b/src/calibre/translations/nl.po @@ -14,7 +14,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\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" #~ msgid "" diff --git a/src/calibre/translations/ru.po b/src/calibre/translations/ru.po index 348bb5492d..9701545c99 100644 --- a/src/calibre/translations/ru.po +++ b/src/calibre/translations/ru.po @@ -13,7 +13,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\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" "Generated-By: pygettext.py 1.5\n" diff --git a/src/calibre/translations/sl.po b/src/calibre/translations/sl.po index 6169f2dc89..43ea54d181 100644 --- a/src/calibre/translations/sl.po +++ b/src/calibre/translations/sl.po @@ -13,7 +13,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\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" "Generated-By: pygettext.py 1.5\n" diff --git a/src/calibre/web/feeds/__init__.py b/src/calibre/web/feeds/__init__.py index 0d869e36d2..003e9af318 100644 --- a/src/calibre/web/feeds/__init__.py +++ b/src/calibre/web/feeds/__init__.py @@ -109,6 +109,8 @@ class Feed(object): if id in self.added_articles: return published = item.get('date_parsed', time.gmtime()) + if not published: + published = time.gmtime() self.id_counter += 1 self.added_articles.append(id) diff --git a/upload.py b/upload.py index e3241c1417..3ad07aa28a 100644 --- a/upload.py +++ b/upload.py @@ -1,13 +1,19 @@ #!/usr/bin/python -import sys, os, shutil, time, tempfile, socket +import sys, os, shutil, time, tempfile, socket, fcntl, struct sys.path.append('src') import subprocess from subprocess import check_call as _check_call from functools import partial #from pyvix.vix import Host, VIX_SERVICEPROVIDER_VMWARE_WORKSTATION -s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -s.connect(('google.com', 0)) -HOST=s.getsockname()[0] +def get_ip_address(ifname): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + 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()) from calibre import __version__, __appname__ @@ -21,11 +27,12 @@ TXT2LRF = "src/calibre/ebooks/lrf/txt/demo" BUILD_SCRIPT ='''\ #!/bin/bash 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 && \ -mkdir -p build dist && \ +mkdir -p build dist src/calibre/plugins && \ +%%s && \ rm -rf build/* dist/* && \ -python %%s +%%s %%s '''%dict(host=HOST, project=PROJECT) check_call = partial(_check_call, shell=True) #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-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) subprocess.Popen(vmware) t = tempfile.NamedTemporaryFile(suffix='.sh') t.write(build_script) t.flush() 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' 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(): installer = installer_name('exe') vm = '/vmware/Windows XP/Windows XP Professional.vmx' - start_vm(vm, 'windows', BUILD_SCRIPT%'windows_installer.py', 75) - subprocess.check_call(('ssh', 'windows', '/bin/bash', '~/build-'+PROJECT)) + start_vm(vm, 'windows', BUILD_SCRIPT%('python setup.py develop', 'python','windows_installer.py')) subprocess.check_call(('scp', 'windows:build/%s/dist/*.exe'%PROJECT, 'dist')) if not os.path.exists(installer): raise Exception('Failed to build installer '+installer) @@ -66,160 +75,24 @@ def build_windows(): def build_osx(): installer = installer_name('dmg') vm = '/vmware/Mac OSX/Mac OSX.vmx' - vmware = ('vmware', '-q', '-x', '-n', vm) - start_vm(vm, 'osx', BUILD_SCRIPT%'osx_installer.py', 120) - subprocess.check_call(('ssh', 'osx', '/bin/bash', '~/build-'+PROJECT)) - subprocess.check_call(('scp', 'windows:build/%s/dist/*.dmg'%PROJECT, 'dist')) + python = '/Library/Frameworks/Python.framework/Versions/Current/bin/python' + start_vm(vm, 'osx', BUILD_SCRIPT%('sudo %s setup.py develop'%python, python, 'osx_installer.py')) + subprocess.check_call(('scp', 'osx:build/%s/dist/*.dmg'%PROJECT, 'dist')) if not os.path.exists(installer): raise Exception('Failed to build installer '+installer) subprocess.Popen(('ssh', 'osx', 'sudo', '/sbin/shutdown', '-h', 'now')) 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(): + installer = installer_name('tar.bz2') vm = '/vmware/linux/libprs500-gentoo.vmx' - vmware = ('vmware', '-q', '-x', '-n', vm) - subprocess.Popen(vmware) - print 'Waiting for linux to boot up...' - time.sleep(75) - check_call('ssh linux make -C /mnt/hgfs/giskard/work/%s all egg linux_binary'%PROJECT) - check_call('ssh linux sudo poweroff') + start_vm(vm, 'linux', BUILD_SCRIPT%('sudo python setup.py develop', 'python','linux_installer.py')) + subprocess.check_call(('scp', 'linux:/tmp/%s'%os.path.basename(installer), 'dist')) + if not os.path.exists(installer): + raise Exception('Failed to build installer '+installer) + subprocess.Popen(('ssh', 'linux', 'sudo', '/sbin/poweroff')) + return os.path.basename(installer) def build_installers(): return build_linux(), build_windows(), build_osx() @@ -267,18 +140,14 @@ def upload_user_manual(): finally: os.chdir(cwd) -def build_tarball(): - cwd = os.getcwd() +def build_src_tarball(): 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('scp dist/calibre-*.tar.bz2 divok:%s/'%DOWNLOADS) - - -def main(): - upload = len(sys.argv) < 2 +def stage_one(): shutil.rmtree('build') os.mkdir('build') shutil.rmtree('docs') @@ -288,17 +157,32 @@ def main(): check_call('make', shell=True) tag_release() upload_demo() + +def stage_two(): + subprocess.check_call('rm -rf dist/*', shell=True) build_installers() - build_tarball() - if upload: - print 'Uploading installers...' - upload_installers() - print 'Uploading to PyPI' - upload_tarball() - upload_docs() - upload_user_manual() - check_call('python setup.py register bdist_egg --exclude-source-files upload') - check_call('''rm -rf dist/* build/*''') + build_src_tarball() + +def stage_three(): + print 'Uploading installers...' + upload_installers() + print 'Uploading to PyPI' + upload_src_tarball() + upload_docs() + upload_user_manual() + 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__': - main() + sys.exit(main()) diff --git a/windows_installer.py b/windows_installer.py index 974caafb40..3748ace8e7 100644 --- a/windows_installer.py +++ b/windows_installer.py @@ -1,7 +1,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' ''' 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 distutils.core import setup from distutils.filelist import FileList @@ -18,7 +18,7 @@ if os.path.exists(PY2EXE_DIR): class NSISInstaller(object): TEMPLATE = r''' ; Do a Cyclic Redundancy Check to make sure the installer -; was not corrupted by the download. +; was not corrupted by the download. CRCCheck on SetCompressor lzma @@ -29,7 +29,7 @@ ShowUnInstDetails show ;Include Modern UI !include "MUI2.nsh" !include "WinMessages.nsh" - + ;------------------------------------------------------------------------------------------------------ ;Variables Var STARTMENU_FOLDER @@ -63,7 +63,7 @@ Loop: StrCmp "$R2" "$\r" RTrim StrCmp "$R2" ";" RTrim GoTo Done -RTrim: +RTrim: StrCpy $R1 "$R1" -1 Goto Loop Done: @@ -82,7 +82,7 @@ FunctionEnd ; Call StrStr ; Pop $R0 ; ($R0 at this point is "ass string") - + !macro StrStr un Function ${un}StrStr Exch $R1 ; st=haystack,old$R1, $R1=needle @@ -123,7 +123,7 @@ Function AddToPath Push $3 ; don't add if the path doesn't exist IfFileExists "$0\*.*" "" AddToPath_done - + ReadEnvStr $1 PATH Push "$1;" Push "$0;" @@ -146,7 +146,7 @@ Function AddToPath Call StrStr Pop $2 StrCmp $2 "" "" AddToPath_done - + ReadRegStr $1 ${WriteEnvStr_RegKey} "PATH" StrCmp $1 "" AddToPath_NTdoIt Push $1 @@ -156,7 +156,7 @@ Function AddToPath AddToPath_NTdoIt: WriteRegExpandStr ${WriteEnvStr_RegKey} "PATH" $0 SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 - + AddToPath_done: Pop $3 Pop $2 @@ -172,9 +172,9 @@ Function un.RemoveFromPath Push $4 Push $5 Push $6 - + IntFmt $6 "%%c" 26 # DOS EOF - + ReadRegStr $1 ${WriteEnvStr_RegKey} "PATH" StrCpy $5 $1 1 -1 # copy 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 $6 $2 "" $3 # $6 is now the part after the path to remove StrCpy $3 $5$6 - + StrCpy $5 $3 1 -1 # copy last char StrCmp $5 ";" 0 +2 # if last char == ; StrCpy $3 $3 -1 # remove last char - + WriteRegExpandStr ${WriteEnvStr_RegKey} "PATH" $3 SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 - + unRemoveFromPath_done: Pop $6 Pop $5 @@ -219,13 +219,13 @@ FunctionEnd ;Default installation folder InstallDir "$PROGRAMFILES\${PRODUCT_NAME}" - + ;Get installation folder from registry if available InstallDirRegKey HKCU "Software\${PRODUCT_NAME}" "" - + ;Vista redirects $SMPROGRAMS to all users without this RequestExecutionLevel admin - + ;------------------------------------------------------------------------------------------------------ ;Interface Settings @@ -241,25 +241,25 @@ FunctionEnd !insertmacro MUI_PAGE_COMPONENTS !insertmacro MUI_PAGE_DIRECTORY ;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_VALUENAME "Start Menu Folder" - + !insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER !insertmacro MUI_PAGE_INSTFILES - + ; Finish page with option to run program ; Disabled as GUI requires PATH and working directory to be set correctly ;!define MUI_FINISHPAGE_RUN "$INSTDIR\${PRODUCT_NAME}.exe" ;!define MUI_FINISHPAGE_NOAUTOCLOSE ;!insertmacro MUI_PAGE_FINISH - + !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES !insertmacro MUI_UNPAGE_FINISH ;------------------------------------------------------------------------------------------------------ ;Languages - + !insertmacro MUI_LANGUAGE "English" ;------------------------------------------------------------------------------------------------------ ;Installer Sections @@ -268,7 +268,7 @@ Function .onInit ; Prevent multiple instances of the installer from running System::Call 'kernel32::CreateMutexA(i 0, i 0, t "${PRODUCT_NAME}-setup") i .r1 ?e' Pop $R0 - + StrCmp $R0 0 +3 MessageBox MB_OK|MB_ICONEXCLAMATION "The installer is already running." Abort @@ -279,24 +279,24 @@ FunctionEnd Section "Main" "secmain" SetOutPath "$INSTDIR" - + ;ADD YOUR OWN FILES HERE... File /r "${PY2EXE_DIR}\*" File "${CLIT}" File "${PDFTOHTML}" File /r "${FONTCONFIG}\*" - + SetOutPath "$INSTDIR\ImageMagick" File /r "${IMAGEMAGICK}\*" - - + + SetOutPath "$SYSDIR" File "${LIBUNRAR_DIR}\unrar.dll" DetailPrint " " - + ;Store installation folder WriteRegStr HKCU "Software\${PRODUCT_NAME}" "" $INSTDIR - + ;Create uninstaller WriteUninstaller "$INSTDIR\Uninstall.exe" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ @@ -306,7 +306,7 @@ Section "Main" "secmain" SetOutPath "$INSTDIR" !insertmacro MUI_STARTMENU_WRITE_BEGIN Application - + ;Create shortcuts WriteIniStr "$INSTDIR\${PRODUCT_NAME}.url" "InternetShortcut" "URL" "${WEBSITE}" CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER" @@ -317,11 +317,11 @@ Section "Main" "secmain" CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\calibre.exe" !insertmacro MUI_STARTMENU_WRITE_END - + ;Add the installation directory to PATH for the commandline tools Push "$INSTDIR" Call AddToPath - + SectionEnd 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_x64.dll" ;File "${LIBUSB_DIR}\libusb0_x64.sys" - + ; Uninstall USB drivers DetailPrint "Uninstalling any existing device drivers" ExecWait '"$INSTDIR\driver\devcon.exe" remove "USB\VID_054C&PID_029B"' $0 DetailPrint "devcon returned exit code $0" - - + + DetailPrint "Installing USB driver for prs500..." ExecWait '"$INSTDIR\driver\devcon.exe" install "$INSTDIR\driver\prs500.inf" "USB\VID_054C&PID_029B"' $0 DetailPrint "devcon returned exit code $0" @@ -353,10 +353,10 @@ Section /o "Device Drivers (only needed for PRS500)" "secdd" 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.' DetailPrint " " - - - - + + + + SectionEnd ;------------------------------------------------------------------------------------------------------ @@ -381,7 +381,7 @@ Section "un.DeviceDrivers" SectionEnd Section "Uninstall" - + ;ADD YOUR OWN FILES HERE... RMDir /r "$INSTDIR" !insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP @@ -404,15 +404,15 @@ Section "Uninstall" DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" ; Remove installation directory from PATH Push "$INSTDIR" - Call un.RemoveFromPath + Call un.RemoveFromPath SectionEnd ''' def __init__(self, name, py2exe_dir, output_dir): self.installer = self.__class__.TEMPLATE % dict(name=name, py2exe_dir=py2exe_dir, - version=VERSION, + version=VERSION, outpath=os.path.abspath(output_dir)) - - def build(self): + + def build(self): f = open('installer.nsi', 'w') path = f.name f.write(self.installer) @@ -420,23 +420,23 @@ SectionEnd try: subprocess.check_call('"C:\Program Files\NSIS\makensis.exe" /V2 ' + path, shell=True) except: - print path + print path else: os.remove(path) class BuildEXE(build_exe): manifest_resource_id = 0 - QT_PREFIX = r'C:\\Qt\\4.4.0' + QT_PREFIX = r'C:\\Qt\\4.4.0' MANIFEST_TEMPLATE = ''' - + - Ebook management application + /> + Ebook management application @@ -474,7 +474,7 @@ class BuildEXE(build_exe): shutil.rmtree('.build', True) finally: os.chdir(cwd) - + def run(self): if not os.path.exists(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))) qtxmldll = os.path.join(os.path.dirname(qtsvgdll), 'QtXml4.dll') print 'Adding', qtxmldll - shutil.copyfile(qtxmldll, + shutil.copyfile(qtxmldll, os.path.join(self.dist_dir, os.path.basename(qtxmldll))) print 'Adding plugins...', qt_prefix = self.QT_PREFIX @@ -503,50 +503,45 @@ class BuildEXE(build_exe): for d in ('imageformats', 'codecs', 'iconengines'): print 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): shutil.rmtree(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 'Building Installer' installer = NSISInstaller(APPNAME, self.dist_dir, 'dist') installer.build() - + @classmethod def manifest(cls, prog): 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')) - + def main(): - auto = '--auto' in sys.argv - if auto: - sys.argv.remove('--auto') sys.argv[1:2] = ['py2exe'] - if '--verbose' not in sys.argv: - 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]) + console = [dict(dest_base=basenames['console'][i], script=scripts['console'][i]) for i in range(len(scripts['console']))] - + sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) setup( cmdclass = {'py2exe': BuildEXE}, windows = [ - {'script' : scripts['gui'][0], + {'script' : scripts['gui'][0], 'dest_base' : APPNAME, 'icon_resources' : [(1, 'icons/library.ico')], 'other_resources' : [BuildEXE.manifest(APPNAME)], }, - {'script' : scripts['gui'][1], + {'script' : scripts['gui'][1], 'dest_base' : 'lrfviewer', 'icon_resources' : [(1, 'icons/viewer.ico')], 'other_resources' : [BuildEXE.manifest('lrfviewer')], @@ -557,24 +552,22 @@ def main(): 'optimize' : 2, 'dist_dir' : PY2EXE_DIR, 'includes' : [ - 'sip', 'pkg_resources', 'PyQt4.QtSvg', - 'mechanize', 'ClientForm', 'wmi', - 'win32file', 'pythoncom', 'rtf2xml', + 'sip', 'pkg_resources', 'PyQt4.QtSvg', + 'mechanize', 'ClientForm', 'wmi', + 'win32file', 'pythoncom', 'rtf2xml', 'lxml', 'lxml._elementpath', 'genshi', 'path', 'pydoc', 'IPython.Extensions.*', 'calibre.web.feeds.recipes.*', 'pydoc', - ], + ], 'packages' : ['PIL'], - 'excludes' : ["Tkconstants", "Tkinter", "tcl", - "_imagingtk", "ImageTk", "FixTk", + 'excludes' : ["Tkconstants", "Tkinter", "tcl", + "_imagingtk", "ImageTk", "FixTk", 'pydoc'], 'dll_excludes' : ['mswsock.dll'], }, }, - + ) - if auto: - subprocess.call(('shutdown', '-s', '-f', '-t', '01')) return 0 if __name__ == '__main__':