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

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'],
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',

View File

@ -1,7 +1,7 @@
''' E-book management software'''
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
__version__ = '0.4.70'
__version__ = '0.4.72'
__docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid at kovidgoyal.net>"
__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):

View File

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

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

View File

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

View File

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

View File

@ -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 <font face="serif">%s</font> 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 <b>edit</b> me<br><br>"))
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 <b>edit</b> me<br><br>")
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)

View File

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

View File

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

View File

@ -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', '.']

View File

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

View File

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

View File

@ -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 = []

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?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

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

View File

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

View File

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

View File

@ -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 <Unknown>\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</a>. "
"Visit the download page?"
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
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

234
upload.py
View File

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

View File

@ -1,7 +1,7 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
''' 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 = '''
<?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"
processorArchitecture="x86"
name="net.kovidgoyal.%(prog)s"
type="win32"
/>
<description>Ebook management application</description>
/>
<description>Ebook management application</description>
<!-- Identify the application security requirements. -->
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
@ -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__':