Pull from trunk

This commit is contained in:
Kovid Goyal 2009-03-28 12:18:03 -07:00
commit 83dedd68fc
177 changed files with 44667 additions and 18758 deletions

View File

@ -29,3 +29,4 @@ src/cssutils/scripts/
src/cssutils/css/.svn/ src/cssutils/css/.svn/
src/cssutils/stylesheets/.svn/ src/cssutils/stylesheets/.svn/
src/odf/.svn src/odf/.svn
tags

View File

@ -9,7 +9,7 @@ Create linux binary.
''' '''
def freeze(): def freeze():
import glob, sys, subprocess, tarfile, os, re, textwrap, shutil, cStringIO, bz2, codecs import glob, sys, tarfile, os, textwrap, shutil
from contextlib import closing from contextlib import closing
from cx_Freeze import Executable, setup from cx_Freeze import Executable, setup
from calibre.constants import __version__, __appname__ from calibre.constants import __version__, __appname__
@ -18,13 +18,13 @@ def freeze():
from calibre.web.feeds.recipes import recipe_modules from calibre.web.feeds.recipes import recipe_modules
from calibre.ebooks.lrf.fonts import FONT_MAP from calibre.ebooks.lrf.fonts import FONT_MAP
import calibre import calibre
QTDIR = '/usr/lib/qt4' QTDIR = '/usr/lib/qt4'
QTDLLS = ('QtCore', 'QtGui', 'QtNetwork', 'QtSvg', 'QtXml', 'QtWebKit') QTDLLS = ('QtCore', 'QtGui', 'QtNetwork', 'QtSvg', 'QtXml', 'QtWebKit')
binary_excludes = ['libGLcore*', 'libGL*', 'libnvidia*'] binary_excludes = ['libGLcore*', 'libGL*', 'libnvidia*']
binary_includes = [ binary_includes = [
'/usr/bin/pdftohtml', '/usr/bin/pdftohtml',
'/usr/lib/libunrar.so', '/usr/lib/libunrar.so',
@ -41,65 +41,66 @@ def freeze():
'/usr/lib/libxslt.so.1', '/usr/lib/libxslt.so.1',
'/usr/lib/libxslt.so.1', '/usr/lib/libxslt.so.1',
'/usr/lib/libgthread-2.0.so.0', '/usr/lib/libgthread-2.0.so.0',
'/usr/lib/libglib-2.0.so.0',
'/usr/lib/gcc/i686-pc-linux-gnu/4.3.3/libstdc++.so.6', '/usr/lib/gcc/i686-pc-linux-gnu/4.3.3/libstdc++.so.6',
'/usr/lib/libpng12.so.0', '/usr/lib/libpng12.so.0',
'/usr/lib/libexslt.so.0', '/usr/lib/libexslt.so.0',
'/usr/lib/libMagickWand.so', '/usr/lib/libMagickWand.so',
'/usr/lib/libMagickCore.so', '/usr/lib/libMagickCore.so',
] ]
binary_includes += [os.path.join(QTDIR, 'lib%s.so.4'%x) for x in QTDLLS] binary_includes += [os.path.join(QTDIR, 'lib%s.so.4'%x) for x in QTDLLS]
d = os.path.dirname d = os.path.dirname
CALIBRESRC = d(d(d(os.path.abspath(calibre.__file__)))) CALIBRESRC = d(d(d(os.path.abspath(calibre.__file__))))
CALIBREPLUGINS = os.path.join(CALIBRESRC, 'src', 'calibre', 'plugins') CALIBREPLUGINS = os.path.join(CALIBRESRC, 'src', 'calibre', 'plugins')
FREEZE_DIR = os.path.join(CALIBRESRC, 'build', 'cx_freeze') FREEZE_DIR = os.path.join(CALIBRESRC, 'build', 'cx_freeze')
DIST_DIR = os.path.join(CALIBRESRC, 'dist') DIST_DIR = os.path.join(CALIBRESRC, 'dist')
os.chdir(CALIBRESRC) os.chdir(CALIBRESRC)
print 'Freezing calibre located at', CALIBRESRC print 'Freezing calibre located at', CALIBRESRC
sys.path.insert(0, os.path.join(CALIBRESRC, 'src')) sys.path.insert(0, os.path.join(CALIBRESRC, 'src'))
entry_points = entry_points['console_scripts'] + entry_points['gui_scripts'] entry_points = entry_points['console_scripts'] + entry_points['gui_scripts']
entry_points = ['calibre_postinstall=calibre.linux:binary_install', entry_points = ['calibre_postinstall=calibre.linux:binary_install',
'calibre-parallel=calibre.parallel:main'] + entry_points 'calibre-parallel=calibre.parallel:main'] + entry_points
executables = {} executables = {}
for ep in entry_points: for ep in entry_points:
executables[ep.split('=')[0].strip()] = (ep.split('=')[1].split(':')[0].strip(), executables[ep.split('=')[0].strip()] = (ep.split('=')[1].split(':')[0].strip(),
ep.split(':')[-1].strip()) ep.split(':')[-1].strip())
if os.path.exists(FREEZE_DIR): if os.path.exists(FREEZE_DIR):
shutil.rmtree(FREEZE_DIR) shutil.rmtree(FREEZE_DIR)
os.makedirs(FREEZE_DIR) os.makedirs(FREEZE_DIR)
if not os.path.exists(DIST_DIR): if not os.path.exists(DIST_DIR):
os.makedirs(DIST_DIR) os.makedirs(DIST_DIR)
includes = [x[0] for x in executables.values()] includes = [x[0] for x in executables.values()]
includes += ['calibre.ebooks.lrf.fonts.prs500.'+x for x in FONT_MAP.values()] includes += ['calibre.ebooks.lrf.fonts.prs500.'+x for x in FONT_MAP.values()]
includes += ['email.iterators', 'email.generator']
excludes = ['matplotlib', "Tkconstants", "Tkinter", "tcl", "_imagingtk",
"ImageTk", "FixTk", 'wx', 'PyQt4.QtAssistant', 'PyQt4.QtOpenGL.so',
excludes = ['matplotlib', "Tkconstants", "Tkinter", "tcl", "_imagingtk",
"ImageTk", "FixTk", 'wx', 'PyQt4.QtAssistant', 'PyQt4.QtOpenGL.so',
'PyQt4.QtScript.so', 'PyQt4.QtSql.so', 'PyQt4.QtTest.so', 'qt', 'PyQt4.QtScript.so', 'PyQt4.QtSql.so', 'PyQt4.QtTest.so', 'qt',
'glib', 'gobject'] 'glib', 'gobject']
packages = ['calibre', 'encodings', 'cherrypy', 'cssutils', 'xdg', packages = ['calibre', 'encodings', 'cherrypy', 'cssutils', 'xdg',
'dateutil'] 'dateutil', 'dns', 'email']
includes += ['calibre.web.feeds.recipes.'+r for r in recipe_modules] includes += ['calibre.web.feeds.recipes.'+r for r in recipe_modules]
LOADER = '/tmp/loader.py' LOADER = '/tmp/loader.py'
open(LOADER, 'wb').write('# This script is never actually used.\nimport sys') open(LOADER, 'wb').write('# This script is never actually used.\nimport sys')
INIT_SCRIPT = '/tmp/init.py' INIT_SCRIPT = '/tmp/init.py'
open(INIT_SCRIPT, 'wb').write(textwrap.dedent(''' open(INIT_SCRIPT, 'wb').write(textwrap.dedent('''
## Load calibre module specified in the environment variable CALIBRE_CX_EXE ## Load calibre module specified in the environment variable CALIBRE_CX_EXE
## Also restrict sys.path to the executables' directory and add the ## Also restrict sys.path to the executables' directory and add the
## executables directory to LD_LIBRARY_PATH ## executables directory to LD_LIBRARY_PATH
import encodings import encodings
import os import os
import sys import sys
@ -107,26 +108,26 @@ def freeze():
import zipimport import zipimport
import locale import locale
import codecs import codecs
enc = locale.getdefaultlocale()[1] enc = locale.getdefaultlocale()[1]
if not enc: if not enc:
enc = locale.nl_langinfo(locale.CODESET) enc = locale.nl_langinfo(locale.CODESET)
enc = codecs.lookup(enc if enc else 'UTF-8').name enc = codecs.lookup(enc if enc else 'UTF-8').name
sys.setdefaultencoding(enc) sys.setdefaultencoding(enc)
paths = os.environ.get('LD_LIBRARY_PATH', '').split(os.pathsep) paths = os.environ.get('LD_LIBRARY_PATH', '').split(os.pathsep)
if DIR_NAME not in paths or not sys.getfilesystemencoding(): if DIR_NAME not in paths or not sys.getfilesystemencoding():
paths.insert(0, DIR_NAME) paths.insert(0, DIR_NAME)
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths) os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths)
os.environ['PYTHONIOENCODING'] = enc os.environ['PYTHONIOENCODING'] = enc
os.execv(sys.executable, sys.argv) os.execv(sys.executable, sys.argv)
sys.path = sys.path[:3] sys.path = sys.path[:3]
sys.frozen = True sys.frozen = True
sys.frozen_path = DIR_NAME sys.frozen_path = DIR_NAME
executables = %(executables)s executables = %(executables)s
exe = os.environ.get('CALIBRE_CX_EXE', False) exe = os.environ.get('CALIBRE_CX_EXE', False)
ret = 1 ret = 1
if not exe: if not exe:
@ -141,7 +142,7 @@ def freeze():
module = __import__(module, fromlist=[1]) module = __import__(module, fromlist=[1])
func = getattr(module, func) func = getattr(module, func)
ret = func() ret = func()
module = sys.modules.get("threading") module = sys.modules.get("threading")
if module is not None: if module is not None:
module._shutdown() module._shutdown()
@ -162,35 +163,35 @@ def freeze():
'init_script' : INIT_SCRIPT, 'init_script' : INIT_SCRIPT,
'copy_dependent_files' : True, 'copy_dependent_files' : True,
'create_shared_zip' : False, 'create_shared_zip' : False,
} }
} }
) )
def copy_binary(src, dest_dir): def copy_binary(src, dest_dir):
dest = os.path.join(dest_dir, os.path.basename(src)) dest = os.path.join(dest_dir, os.path.basename(src))
if not os.path.exists(dest_dir): if not os.path.exists(dest_dir):
os.makedirs(dest_dir) os.makedirs(dest_dir)
shutil.copyfile(os.path.realpath(src), dest) shutil.copyfile(os.path.realpath(src), dest)
shutil.copymode(os.path.realpath(src), dest) shutil.copymode(os.path.realpath(src), dest)
for f in binary_includes: for f in binary_includes:
copy_binary(f, FREEZE_DIR) copy_binary(f, FREEZE_DIR)
for pat in binary_excludes: for pat in binary_excludes:
matches = glob.glob(os.path.join(FREEZE_DIR, pat)) matches = glob.glob(os.path.join(FREEZE_DIR, pat))
for f in matches: for f in matches:
os.remove(f) os.remove(f)
print 'Adding calibre plugins...' print 'Adding calibre plugins...'
os.makedirs(os.path.join(FREEZE_DIR, 'plugins')) os.makedirs(os.path.join(FREEZE_DIR, 'plugins'))
for f in glob.glob(os.path.join(CALIBREPLUGINS, '*.so')): for f in glob.glob(os.path.join(CALIBREPLUGINS, '*.so')):
copy_binary(f, os.path.join(FREEZE_DIR, 'plugins')) copy_binary(f, os.path.join(FREEZE_DIR, 'plugins'))
print 'Adding Qt plugins...' print 'Adding Qt plugins...'
plugdir = os.path.join(QTDIR, 'plugins') plugdir = os.path.join(QTDIR, 'plugins')
for dirpath, dirnames, filenames in os.walk(plugdir): for dirpath, dirnames, filenames in os.walk(plugdir):
for f in filenames: for f in filenames:
if not f.endswith('.so') or 'designer' in dirpath or 'codecs' in dirpath or 'sqldrivers' in dirpath: if not f.endswith('.so') or 'designer' in dirpath or 'codecs' in dirpath or 'sqldrivers' in dirpath:
continue continue
f = os.path.join(dirpath, f) f = os.path.join(dirpath, f)
dest_dir = dirpath.replace(plugdir, os.path.join(FREEZE_DIR, 'qtplugins')) dest_dir = dirpath.replace(plugdir, os.path.join(FREEZE_DIR, 'qtplugins'))
@ -198,7 +199,7 @@ def freeze():
print 'Creating launchers' print 'Creating launchers'
for exe in executables: for exe in executables:
path = os.path.join(FREEZE_DIR, exe) path = os.path.join(FREEZE_DIR, exe)
open(path, 'wb').write(textwrap.dedent('''\ open(path, 'wb').write(textwrap.dedent('''\
#!/bin/sh #!/bin/sh
export CALIBRE_CX_EXE=%s export CALIBRE_CX_EXE=%s
@ -209,15 +210,15 @@ def freeze():
$loader "$@" $loader "$@"
''')%exe) ''')%exe)
os.chmod(path, 0755) os.chmod(path, 0755)
exes = list(executables.keys()) exes = list(executables.keys())
exes.remove('calibre_postinstall') exes.remove('calibre_postinstall')
exes.remove('calibre-parallel') exes.remove('calibre-parallel')
open(os.path.join(FREEZE_DIR, 'manifest'), 'wb').write('\n'.join(exes)) open(os.path.join(FREEZE_DIR, 'manifest'), 'wb').write('\n'.join(exes))
print 'Creating archive...' print 'Creating archive...'
dist = open(os.path.join(DIST_DIR, 'calibre-%s-i686.tar.bz2'%__version__), 'wb') dist = open(os.path.join(DIST_DIR, 'calibre-%s-i686.tar.bz2'%__version__), 'wb')
with closing(tarfile.open(fileobj=dist, mode='w:bz2', with closing(tarfile.open(fileobj=dist, mode='w:bz2',
format=tarfile.PAX_FORMAT)) as tf: format=tarfile.PAX_FORMAT)) as tf:
for f in walk(FREEZE_DIR): for f in walk(FREEZE_DIR):
name = f.replace(FREEZE_DIR, '')[1:] name = f.replace(FREEZE_DIR, '')[1:]

View File

@ -57,7 +57,7 @@ r'''
def _check_symlinks_prescript(): def _check_symlinks_prescript():
import os, tempfile, traceback, sys import os, tempfile, traceback, sys
from Authorization import Authorization, kAuthorizationFlagDestroyRights from Authorization import Authorization, kAuthorizationFlagDestroyRights
AUTHTOOL="""#!%(sp)s AUTHTOOL="""#!%(sp)s
import os import os
scripts = %(sp)s scripts = %(sp)s
@ -71,13 +71,13 @@ for s, l in zip(scripts, links):
os.symlink(s, l) os.symlink(s, l)
os.umask(omask) os.umask(omask)
""" """
dest_path = %(dest_path)s dest_path = %(dest_path)s
resources_path = os.environ['RESOURCEPATH'] resources_path = os.environ['RESOURCEPATH']
scripts = %(scripts)s scripts = %(scripts)s
links = [os.path.join(dest_path, i) for i in scripts] links = [os.path.join(dest_path, i) for i in scripts]
scripts = [os.path.join(resources_path, 'loaders', i) for i in scripts] scripts = [os.path.join(resources_path, 'loaders', i) for i in scripts]
bad = False bad = False
for s, l in zip(scripts, links): for s, l in zip(scripts, links):
if os.path.exists(l) and os.path.exists(os.path.realpath(l)): if os.path.exists(l) and os.path.exists(os.path.realpath(l)):
@ -111,22 +111,22 @@ _check_symlinks_prescript()
packages=self.packages, packages=self.packages,
excludes=self.excludes, excludes=self.excludes,
debug=debug) debug=debug)
@classmethod @classmethod
def makedmg(cls, d, volname, def makedmg(cls, d, volname,
destdir='dist', destdir='dist',
internet_enable=True, internet_enable=True,
format='UDBZ'): format='UDBZ'):
''' Copy a directory d into a dmg named volname ''' ''' Copy a directory d into a dmg named volname '''
dmg = os.path.join(destdir, volname+'.dmg') dmg = os.path.join(destdir, volname+'.dmg')
if os.path.exists(dmg): if os.path.exists(dmg):
os.unlink(dmg) os.unlink(dmg)
subprocess.check_call(['/usr/bin/hdiutil', 'create', '-srcfolder', os.path.abspath(d), subprocess.check_call(['/usr/bin/hdiutil', 'create', '-srcfolder', os.path.abspath(d),
'-volname', volname, '-format', format, dmg]) '-volname', volname, '-format', format, dmg])
if internet_enable: if internet_enable:
subprocess.check_call(['/usr/bin/hdiutil', 'internet-enable', '-yes', dmg]) subprocess.check_call(['/usr/bin/hdiutil', 'internet-enable', '-yes', dmg])
return dmg return dmg
@classmethod @classmethod
def qt_dependencies(cls, path): def qt_dependencies(cls, path):
pipe = subprocess.Popen('/usr/bin/otool -L '+path, shell=True, stdout=subprocess.PIPE).stdout pipe = subprocess.Popen('/usr/bin/otool -L '+path, shell=True, stdout=subprocess.PIPE).stdout
@ -134,12 +134,12 @@ _check_symlinks_prescript()
for l in pipe.readlines(): for l in pipe.readlines():
match = re.search(r'(.*)\(', l) match = re.search(r'(.*)\(', l)
if not match: if not match:
continue continue
lib = match.group(1).strip() lib = match.group(1).strip()
if lib.startswith(BuildAPP.QT_PREFIX): if lib.startswith(BuildAPP.QT_PREFIX):
deps.append(lib) deps.append(lib)
return deps return deps
@classmethod @classmethod
def fix_qt_dependencies(cls, path, deps): def fix_qt_dependencies(cls, path, deps):
fp = '@executable_path/../Frameworks/' fp = '@executable_path/../Frameworks/'
@ -155,8 +155,8 @@ _check_symlinks_prescript()
newpath = fp + '%s.framework/Versions/Current/%s'%(module, module) newpath = fp + '%s.framework/Versions/Current/%s'%(module, module)
cmd = ' '.join(['/usr/bin/install_name_tool', '-change', dep, newpath, path]) cmd = ' '.join(['/usr/bin/install_name_tool', '-change', dep, newpath, path])
subprocess.check_call(cmd, shell=True) subprocess.check_call(cmd, shell=True)
def add_qt_plugins(self): def add_qt_plugins(self):
macos_dir = os.path.join(self.dist_dir, APPNAME + '.app', 'Contents', 'MacOS') macos_dir = os.path.join(self.dist_dir, APPNAME + '.app', 'Contents', 'MacOS')
for root, dirs, files in os.walk(BuildAPP.QT_PREFIX+'/plugins'): for root, dirs, files in os.walk(BuildAPP.QT_PREFIX+'/plugins'):
@ -172,14 +172,14 @@ _check_symlinks_prescript()
shutil.copymode(path, target) shutil.copymode(path, target)
deps = BuildAPP.qt_dependencies(target) deps = BuildAPP.qt_dependencies(target)
BuildAPP.fix_qt_dependencies(target, deps) BuildAPP.fix_qt_dependencies(target, deps)
#deps = BuildAPP.qt_dependencies(path) #deps = BuildAPP.qt_dependencies(path)
def fix_python_dependencies(self, files): def fix_python_dependencies(self, files):
for f in files: for f in files:
subprocess.check_call(['/usr/bin/install_name_tool', '-change', '/Library/Frameworks/Python.framework/Versions/2.6/Python', '@executable_path/../Frameworks/Python.framework/Versions/2.6/Python', f]) subprocess.check_call(['/usr/bin/install_name_tool', '-change', '/Library/Frameworks/Python.framework/Versions/2.6/Python', '@executable_path/../Frameworks/Python.framework/Versions/2.6/Python', f])
def fix_misc_dependencies(self, files): def fix_misc_dependencies(self, files):
for path in files: for path in files:
frameworks_dir = os.path.join(self.dist_dir, APPNAME + '.app', 'Contents', 'Frameworks') frameworks_dir = os.path.join(self.dist_dir, APPNAME + '.app', 'Contents', 'Frameworks')
@ -195,15 +195,15 @@ _check_symlinks_prescript()
if os.path.exists(bundle): if os.path.exists(bundle):
subprocess.check_call(['/usr/bin/install_name_tool', '-change', dep, subprocess.check_call(['/usr/bin/install_name_tool', '-change', dep,
'@executable_path/../Frameworks/'+name, path]) '@executable_path/../Frameworks/'+name, path])
def add_plugins(self): def add_plugins(self):
self.add_qt_plugins() self.add_qt_plugins()
frameworks_dir = os.path.join(self.dist_dir, APPNAME + '.app', 'Contents', 'Frameworks') frameworks_dir = os.path.join(self.dist_dir, APPNAME + '.app', 'Contents', 'Frameworks')
plugins_dir = os.path.join(frameworks_dir, 'plugins') plugins_dir = os.path.join(frameworks_dir, 'plugins')
if not os.path.exists(plugins_dir): if not os.path.exists(plugins_dir):
os.mkdir(plugins_dir) os.mkdir(plugins_dir)
maps = {} maps = {}
for f in glob.glob('src/calibre/plugins/*'): for f in glob.glob('src/calibre/plugins/*'):
tgt = plugins_dir tgt = plugins_dir
@ -217,8 +217,8 @@ _check_symlinks_prescript()
deps.append(dst) deps.append(dst)
self.fix_python_dependencies(deps) self.fix_python_dependencies(deps)
self.fix_misc_dependencies(deps) self.fix_misc_dependencies(deps)
def run(self): def run(self):
py2app.run(self) py2app.run(self)
resource_dir = os.path.join(self.dist_dir, resource_dir = os.path.join(self.dist_dir,
@ -242,8 +242,8 @@ _check_symlinks_prescript()
os.chmod(path, stat.S_IXUSR|stat.S_IXGRP|stat.S_IXOTH|stat.S_IREAD\ os.chmod(path, stat.S_IXUSR|stat.S_IXGRP|stat.S_IXOTH|stat.S_IREAD\
|stat.S_IWUSR|stat.S_IROTH|stat.S_IRGRP) |stat.S_IWUSR|stat.S_IROTH|stat.S_IRGRP)
self.add_plugins() self.add_plugins()
print print
print 'Adding pdftohtml' print 'Adding pdftohtml'
os.link(os.path.expanduser('~/pdftohtml'), os.path.join(frameworks_dir, 'pdftohtml')) os.link(os.path.expanduser('~/pdftohtml'), os.path.join(frameworks_dir, 'pdftohtml'))
@ -259,21 +259,21 @@ _check_symlinks_prescript()
if os.path.exists(dst): if os.path.exists(dst):
shutil.rmtree(dst) shutil.rmtree(dst)
shutil.copytree('/usr/local/etc/fonts', dst, symlinks=False) shutil.copytree('/usr/local/etc/fonts', dst, symlinks=False)
print print
print 'Adding IPython' print 'Adding IPython'
dst = os.path.join(resource_dir, 'lib', 'python2.6', 'IPython') dst = os.path.join(resource_dir, 'lib', 'python2.6', 'IPython')
if os.path.exists(dst): shutil.rmtree(dst) if os.path.exists(dst): shutil.rmtree(dst)
shutil.copytree(os.path.expanduser('~/build/ipython/IPython'), dst) shutil.copytree(os.path.expanduser('~/build/ipython/IPython'), dst)
print print
print 'Adding ImageMagick' print 'Adding ImageMagick'
dest = os.path.join(frameworks_dir, 'ImageMagick') dest = os.path.join(frameworks_dir, 'ImageMagick')
if os.path.exists(dest): if os.path.exists(dest):
sutil.rmtree(dest) shutil.rmtree(dest)
shutil.copytree(os.path.expanduser('~/ImageMagick'), dest, True) shutil.copytree(os.path.expanduser('~/ImageMagick'), dest, True)
shutil.copyfile('/usr/local/lib/libpng12.0.dylib', os.path.join(dest, 'lib', 'libpng12.0.dylib')) shutil.copyfile('/usr/local/lib/libpng12.0.dylib', os.path.join(dest, 'lib', 'libpng12.0.dylib'))
print print
print 'Installing prescipt' print 'Installing prescipt'
sf = [os.path.basename(s) for s in all_names] sf = [os.path.basename(s) for s in all_names]
@ -293,13 +293,13 @@ sys.frameworks_dir = os.path.join(os.path.dirname(os.environ['RESOURCEPATH']), '
print >>f, 'import sys, os' print >>f, 'import sys, os'
f.write(src) f.write(src)
f.close() f.close()
print print
print 'Adding main scripts to site-packages' print 'Adding main scripts to site-packages'
f = zipfile.ZipFile(os.path.join(self.dist_dir, APPNAME+'.app', 'Contents', 'Resources', 'lib', 'python'+sys.version[:3], 'site-packages.zip'), 'a', zipfile.ZIP_DEFLATED) f = zipfile.ZipFile(os.path.join(self.dist_dir, APPNAME+'.app', 'Contents', 'Resources', 'lib', 'python'+sys.version[:3], 'site-packages.zip'), 'a', zipfile.ZIP_DEFLATED)
for script in scripts['gui']+scripts['console']: for script in scripts['gui']+scripts['console']:
f.write(script, script.partition('/')[-1]) f.write(script, script.partition('/')[-1])
f.close() f.close()
print print
print 'Creating console.app' print 'Creating console.app'
contents_dir = os.path.dirname(resource_dir) contents_dir = os.path.dirname(resource_dir)
cc_dir = os.path.join(contents_dir, 'console.app', 'Contents') cc_dir = os.path.join(contents_dir, 'console.app', 'Contents')
@ -312,7 +312,7 @@ sys.frameworks_dir = os.path.join(os.path.dirname(os.environ['RESOURCEPATH']), '
plist['LSUIElement'] = '1' plist['LSUIElement'] = '1'
plistlib.writePlist(plist, os.path.join(cc_dir, x)) plistlib.writePlist(plist, os.path.join(cc_dir, x))
else: else:
os.symlink(os.path.join('../..', x), os.symlink(os.path.join('../..', x),
os.path.join(cc_dir, x)) os.path.join(cc_dir, x))
print print
print 'Building disk image' print 'Building disk image'
@ -343,9 +343,10 @@ def main():
'calibre.ebooks.lrf.any.*', 'calibre.ebooks.lrf.feeds.*', 'calibre.ebooks.lrf.any.*', 'calibre.ebooks.lrf.feeds.*',
'keyword', 'codeop', 'pydoc', 'readline', 'keyword', 'codeop', 'pydoc', 'readline',
'BeautifulSoup', 'calibre.ebooks.lrf.fonts.prs500.*', 'BeautifulSoup', 'calibre.ebooks.lrf.fonts.prs500.*',
'dateutil', 'dateutil', 'email.iterators',
'email.generator',
], ],
'packages' : ['PIL', 'Authorization', 'lxml'], 'packages' : ['PIL', 'Authorization', 'lxml', 'dns'],
'excludes' : ['IPython'], 'excludes' : ['IPython'],
'plist' : { 'CFBundleGetInfoString' : '''calibre, an E-book management application.''' 'plist' : { 'CFBundleGetInfoString' : '''calibre, an E-book management application.'''
''' Visit http://calibre.kovidgoyal.net for details.''', ''' Visit http://calibre.kovidgoyal.net for details.''',

View File

@ -273,7 +273,6 @@ File ::C49805D2-C0B8-01C4-DF6F-674D9C0BFD15 -name IM_MOD_RL_viff_.dll -parent 8E
File ::1B9F2F00-20A5-B207-5A80-8F75470286AD -name txt2lrf.exe.local -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40 File ::1B9F2F00-20A5-B207-5A80-8F75470286AD -name txt2lrf.exe.local -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::826F1915-9F97-59DD-6637-3EEC0744A79C -name IM_MOD_RL_ps2_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40 File ::826F1915-9F97-59DD-6637-3EEC0744A79C -name IM_MOD_RL_ps2_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::519A6618-8A1F-93A5-93B4-6EEF5A4A3DE9 -name comic2pdf.exe -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40 File ::519A6618-8A1F-93A5-93B4-6EEF5A4A3DE9 -name comic2pdf.exe -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::B0CEAA35-52BF-0DE0-BAC7-7B23157E29BD -name isbndb.exe -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::A5F23791-BCDC-A997-4941-5D1F2F227E6D -name type.xml -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40 File ::A5F23791-BCDC-A997-4941-5D1F2F227E6D -name type.xml -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::0A1C107A-C0AA-3ED6-4F37-A6894386DCBE -name IM_MOD_RL_ps3_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40 File ::0A1C107A-C0AA-3ED6-4F37-A6894386DCBE -name IM_MOD_RL_ps3_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::EEBA64E7-6509-EBAF-3E23-1A203216F39A -name epub2lrf.exe -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40 File ::EEBA64E7-6509-EBAF-3E23-1A203216F39A -name epub2lrf.exe -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
@ -284,7 +283,6 @@ File ::EA37C1C2-57BB-4E7A-C004-0010D79142C2 -name IM_MOD_RL_fits_.dll -parent 8E
File ::05F5C10D-6988-F1F4-A486-86C96DB20302 -name pywintypes26.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40 File ::05F5C10D-6988-F1F4-A486-86C96DB20302 -name pywintypes26.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::0137A2B1-EB94-EB26-7295-0C7CD941A1DF -name IM_MOD_RL_histogram_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40 File ::0137A2B1-EB94-EB26-7295-0C7CD941A1DF -name IM_MOD_RL_histogram_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::7F199A1F-4FA4-2ABA-DED3-36ECF3C089CA -name epub2lrf.exe.local -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40 File ::7F199A1F-4FA4-2ABA-DED3-36ECF3C089CA -name epub2lrf.exe.local -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::F9F112C9-B61B-E041-1A9D-47641B047135 -name isbndb.exe.local -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::CF6398D8-2140-53CF-1DA6-421A82E92621 -name any2epub.exe -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40 File ::CF6398D8-2140-53CF-1DA6-421A82E92621 -name any2epub.exe -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::8DFA6C69-360D-FA63-7FF9-860E3DB00B19 -name any2lrf.exe.local -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40 File ::8DFA6C69-360D-FA63-7FF9-860E3DB00B19 -name any2lrf.exe.local -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::5BB7579D-9183-412C-81F8-B411B07C57B3 -name IM_MOD_RL_pnm_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40 File ::5BB7579D-9183-412C-81F8-B411B07C57B3 -name IM_MOD_RL_pnm_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
@ -544,6 +542,8 @@ File ::325F545D-30A8-08DA-74F0-AC1244F6C1D9 -name IM_MOD_RL_vid_.dll -parent 8E5
File ::24238371-77D0-0A8F-35D1-498A5FCC1B0D -name IM_MOD_RL_rla_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40 File ::24238371-77D0-0A8F-35D1-498A5FCC1B0D -name IM_MOD_RL_rla_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::6F5D62F3-5E63-0753-364C-01CAAF1002E0 -name IM_MOD_RL_magick_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40 File ::6F5D62F3-5E63-0753-364C-01CAAF1002E0 -name IM_MOD_RL_magick_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::9FDAC308-5D4F-A865-A09A-9FBF48162A47 -name IM_MOD_RL_djvu_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40 File ::9FDAC308-5D4F-A865-A09A-9FBF48162A47 -name IM_MOD_RL_djvu_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::5D748040-5973-EFF1-41FC-B424636C642E -name fetch-ebook-metadata.exe.local -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::8B8655B8-3823-AA02-1CDA-02F5AD4677C0 -name fetch-ebook-metadata.exe -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
Component ::F6829AB7-9F66-4CEE-CA0E-21F54C6D3609 -setup Install -active Yes -platforms {AIX-ppc FreeBSD-4-x86 FreeBSD-x86 HPUX-hppa Linux-x86 Solaris-sparc Windows} -name Main -parent Components Component ::F6829AB7-9F66-4CEE-CA0E-21F54C6D3609 -setup Install -active Yes -platforms {AIX-ppc FreeBSD-4-x86 FreeBSD-x86 HPUX-hppa Linux-x86 Solaris-sparc Windows} -name Main -parent Components
SetupType ::D9ADE41C-B744-690C-2CED-CF826BF03D2E -setup Install -active Yes -platforms {AIX-ppc FreeBSD-4-x86 FreeBSD-x86 HPUX-hppa Linux-x86 Solaris-sparc Windows} -name Typical -parent SetupTypes SetupType ::D9ADE41C-B744-690C-2CED-CF826BF03D2E -setup Install -active Yes -platforms {AIX-ppc FreeBSD-4-x86 FreeBSD-x86 HPUX-hppa Linux-x86 Solaris-sparc Windows} -name Typical -parent SetupTypes

View File

@ -14,12 +14,11 @@ IMAGEMAGICK_DIR = 'C:\\ImageMagick'
FONTCONFIG_DIR = 'C:\\fontconfig' FONTCONFIG_DIR = 'C:\\fontconfig'
VC90 = r'C:\VC90.CRT' VC90 = r'C:\VC90.CRT'
import sys, os, py2exe, shutil, zipfile, glob, subprocess, re import sys, os, py2exe, shutil, zipfile, glob, re
from distutils.core import setup from distutils.core import setup
from distutils.filelist import FileList
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
sys.path.insert(0, BASE_DIR) sys.path.insert(0, BASE_DIR)
from setup import VERSION, APPNAME, entry_points, scripts, basenames from setup import VERSION, APPNAME, scripts, basenames
sys.path.remove(BASE_DIR) sys.path.remove(BASE_DIR)
ICONS = [os.path.abspath(os.path.join(BASE_DIR, 'icons', i)) for i in ('library.ico', 'viewer.ico')] ICONS = [os.path.abspath(os.path.join(BASE_DIR, 'icons', i)) for i in ('library.ico', 'viewer.ico')]
@ -33,7 +32,7 @@ WINVER = VERSION+'.0'
PY2EXE_DIR = os.path.join(BASE_DIR, 'build','py2exe') PY2EXE_DIR = os.path.join(BASE_DIR, 'build','py2exe')
class BuildEXE(py2exe.build_exe.py2exe): class BuildEXE(py2exe.build_exe.py2exe):
def run(self): def run(self):
py2exe.build_exe.py2exe.run(self) py2exe.build_exe.py2exe.run(self)
print 'Adding plugins...' print 'Adding plugins...'
@ -61,19 +60,19 @@ class BuildEXE(py2exe.build_exe.py2exe):
if os.path.exists(tg): if os.path.exists(tg):
shutil.rmtree(tg) shutil.rmtree(tg)
shutil.copytree(imfd, tg) shutil.copytree(imfd, tg)
print print
print 'Adding main scripts' print 'Adding main scripts'
f = zipfile.ZipFile(os.path.join(PY2EXE_DIR, 'library.zip'), 'a', zipfile.ZIP_DEFLATED) f = zipfile.ZipFile(os.path.join(PY2EXE_DIR, 'library.zip'), 'a', zipfile.ZIP_DEFLATED)
for i in scripts['console'] + scripts['gui']: for i in scripts['console'] + scripts['gui']:
f.write(i, i.partition('\\')[-1]) f.write(i, i.partition('\\')[-1])
f.close() f.close()
print print
print 'Copying icons' print 'Copying icons'
for icon in ICONS: for icon in ICONS:
shutil.copyfile(icon, os.path.join(PY2EXE_DIR, os.path.basename(icon))) shutil.copyfile(icon, os.path.join(PY2EXE_DIR, os.path.basename(icon)))
print print
print 'Adding third party dependencies' print 'Adding third party dependencies'
print '\tAdding devcon' print '\tAdding devcon'
@ -96,18 +95,18 @@ class BuildEXE(py2exe.build_exe.py2exe):
shutil.copytree(f, tgt) shutil.copytree(f, tgt)
else: else:
shutil.copyfile(f, tgt) shutil.copyfile(f, tgt)
print print
print 'Doing DLL redirection' # See http://msdn.microsoft.com/en-us/library/ms682600(VS.85).aspx print 'Doing DLL redirection' # See http://msdn.microsoft.com/en-us/library/ms682600(VS.85).aspx
for f in glob.glob(os.path.join(PY2EXE_DIR, '*.exe')): for f in glob.glob(os.path.join(PY2EXE_DIR, '*.exe')):
open(f + '.local', 'w').write('\n') open(f + '.local', 'w').write('\n')
print print
print 'Adding Windows runtime dependencies...' print 'Adding Windows runtime dependencies...'
for f in glob.glob(os.path.join(VC90, '*')): for f in glob.glob(os.path.join(VC90, '*')):
shutil.copyfile(f, os.path.join(PY2EXE_DIR, os.path.basename(f))) shutil.copyfile(f, os.path.join(PY2EXE_DIR, os.path.basename(f)))
def exe_factory(dest_base, script, icon_resources=None): def exe_factory(dest_base, script, icon_resources=None):
exe = { exe = {
'dest_base' : dest_base, 'dest_base' : dest_base,
@ -144,7 +143,9 @@ def main(args=sys.argv):
'includes' : [ 'includes' : [
'sip', 'pkg_resources', 'PyQt4.QtSvg', 'sip', 'pkg_resources', 'PyQt4.QtSvg',
'mechanize', 'ClientForm', 'wmi', 'mechanize', 'ClientForm', 'wmi',
'win32file', 'pythoncom', 'win32file', 'pythoncom',
'email.iterators',
'email.generator',
'win32process', 'win32api', 'msvcrt', 'win32process', 'win32api', 'msvcrt',
'win32event', 'calibre.ebooks.lrf.any.*', 'win32event', 'calibre.ebooks.lrf.any.*',
'calibre.ebooks.lrf.feeds.*', 'calibre.ebooks.lrf.feeds.*',
@ -155,14 +156,14 @@ def main(args=sys.argv):
'PyQt4.QtWebKit', 'PyQt4.QtNetwork', 'PyQt4.QtWebKit', 'PyQt4.QtNetwork',
], ],
'packages' : ['PIL', 'lxml', 'cherrypy', 'packages' : ['PIL', 'lxml', 'cherrypy',
'dateutil'], 'dateutil', 'dns'],
'excludes' : ["Tkconstants", "Tkinter", "tcl", 'excludes' : ["Tkconstants", "Tkinter", "tcl",
"_imagingtk", "ImageTk", "FixTk" "_imagingtk", "ImageTk", "FixTk"
], ],
'dll_excludes' : ['mswsock.dll'], 'dll_excludes' : ['mswsock.dll'],
}, },
}, },
) )
return 0 return 0

16
session.vim Normal file
View File

@ -0,0 +1,16 @@
" Project wide builtins
let g:pyflakes_builtins += ["dynamic_property"]
python << EOFPY
import os
import vipy
source_file = vipy.vipy.eval('expand("<sfile>")')
project_dir = os.path.dirname(source_file)
src_dir = os.path.abspath(os.path.join(project_dir, 'src'))
base_dir = os.path.join(src_dir, 'calibre')
vipy.session.initialize(project_name='calibre', src_dir=src_dir,
project_dir=project_dir, base_dir=base_dir)
EOFPY

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__appname__ = 'calibre' __appname__ = 'calibre'
__version__ = '0.5.2' __version__ = '0.5.3'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
''' '''
Various run time constants. Various run time constants.

View File

@ -17,22 +17,25 @@ def option_parser():
Run an embedded python interpreter. Run an embedded python interpreter.
''') ''')
parser.add_option('--update-module', help='Update the specified module in the frozen library. '+ parser.add_option('--update-module',
'Module specifications are of the form full.name.of.module,path_to_module.py', default=None help='Update the specified module in the frozen library. '+
'Module specifications are of the form full.name.of.module,path_to_module.py',
default=None
) )
parser.add_option('-c', '--command', help='Run python code.', default=None) parser.add_option('-c', '--command', help='Run python code.', default=None)
parser.add_option('-e', '--exec-file', default=None, help='Run the python code in file.') parser.add_option('-e', '--exec-file', default=None, help='Run the python code in file.')
parser.add_option('-d', '--debug-device-driver', default=False, action='store_true', parser.add_option('-d', '--debug-device-driver', default=False, action='store_true',
help='Debug the specified device driver.') help='Debug the specified device driver.')
parser.add_option('-g', '--gui', default=False, action='store_true', parser.add_option('-g', '--gui', default=False, action='store_true',
help='Run the GUI',) help='Run the GUI',)
parser.add_option('--migrate', action='store_true', default=False, parser.add_option('--migrate', action='store_true', default=False,
help='Migrate old database. Needs two arguments. Path to library1.db and path to new library folder.') help='Migrate old database. Needs two arguments. Path '
'to library1.db and path to new library folder.')
return parser return parser
def update_zipfile(zipfile, mod, path): def update_zipfile(zipfile, mod, path):
if 'win32' in sys.platform: if 'win32' in sys.platform:
print 'WARNING: On Windows Vista using this option may cause windows to put library.zip into the Virtual Store (typically located in c:\Users\username\AppData\Local\VirtualStore). If it does this you must delete it from there after you\'re done debugging).' print 'WARNING: On Windows Vista using this option may cause windows to put library.zip into the Virtual Store (typically located in c:\Users\username\AppData\Local\VirtualStore). If it does this you must delete it from there after you\'re done debugging).'
pat = re.compile(mod.replace('.', '/')+r'\.py[co]*') pat = re.compile(mod.replace('.', '/')+r'\.py[co]*')
name = mod.replace('.', '/') + os.path.splitext(path)[-1] name = mod.replace('.', '/') + os.path.splitext(path)[-1]
update(zipfile, [pat], [path], [name]) update(zipfile, [pat], [path], [name])
@ -46,8 +49,8 @@ def update_module(mod, path):
zp = os.path.join(os.path.dirname(sys.executable), 'library.zip') zp = os.path.join(os.path.dirname(sys.executable), 'library.zip')
elif isosx: elif isosx:
zp = os.path.join(os.path.dirname(getattr(sys, 'frameworks_dir')), zp = os.path.join(os.path.dirname(getattr(sys, 'frameworks_dir')),
'Resources', 'lib', 'Resources', 'lib',
'python'+'.'.join(map(str, sys.version_info[:2])), 'python'+'.'.join(map(str, sys.version_info[:2])),
'site-packages.zip') 'site-packages.zip')
else: else:
zp = os.path.join(getattr(sys, 'frozen_path'), 'loader.zip') zp = os.path.join(getattr(sys, 'frozen_path'), 'loader.zip')
@ -71,23 +74,23 @@ def migrate(old, new):
self.max = max self.max = max
def setValue(self, val): def setValue(self, val):
self.update(float(val)/getattr(self, 'max', 1)) self.update(float(val)/getattr(self, 'max', 1))
db = LibraryDatabase(old) db = LibraryDatabase(old)
db2 = LibraryDatabase2(new) db2 = LibraryDatabase2(new)
db2.migrate_old(db, Dummy(terminal_controller, 'Migrating database...')) db2.migrate_old(db, Dummy(terminal_controller, 'Migrating database...'))
prefs['library_path'] = os.path.abspath(new) prefs['library_path'] = os.path.abspath(new)
print 'Database migrated to', os.path.abspath(new) print 'Database migrated to', os.path.abspath(new)
def debug_device_driver(): def debug_device_driver():
from calibre.devices.scanner import DeviceScanner from calibre.devices.scanner import DeviceScanner
s = DeviceScanner() s = DeviceScanner()
s.scan() s.scan()
print 'USB devices on system:', repr(s.devices) print 'USB devices on system:', repr(s.devices)
if iswindows: if iswindows:
wmi = __import__('wmi', globals(), locals(), [], -1) wmi = __import__('wmi', globals(), locals(), [], -1)
drives = [] drives = []
print 'Drives detected:' print 'Drives detected:'
print '\t', '(ID, Partitions, Drive letter)' print '\t', '(ID, Partitions, Drive letter)'
for drive in wmi.WMI().Win32_DiskDrive(): for drive in wmi.WMI().Win32_DiskDrive():
if drive.Partitions == 0: if drive.Partitions == 0:
continue continue
@ -111,7 +114,7 @@ def debug_device_driver():
d.open() d.open()
print 'Total space:', d.total_space() print 'Total space:', d.total_space()
break break
def main(args=sys.argv): def main(args=sys.argv):
opts, args = option_parser().parse_args(args) opts, args = option_parser().parse_args(args)

View File

@ -24,6 +24,8 @@ class Device(object):
# it can be a list of the BCD numbers of all devices supported by this driver. # it can be a list of the BCD numbers of all devices supported by this driver.
BCD = None BCD = None
THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device
# Whether the metadata on books can be set via the GUI.
CAN_SET_METADATA = True
def __init__(self, key='-1', log_packets=False, report_progress=None) : def __init__(self, key='-1', log_packets=False, report_progress=None) :
""" """

View File

@ -4,32 +4,32 @@ __copyright__ = '2009, John Schember <john at nachtimwald.com>'
Device driver for Amazon's Kindle Device driver for Amazon's Kindle
''' '''
import os, re import os, re, sys
from calibre.devices.usbms.driver import USBMS, metadata_from_formats from calibre.devices.usbms.driver import USBMS, metadata_from_formats
class KINDLE(USBMS): class KINDLE(USBMS):
# Ordered list of supported formats # Ordered list of supported formats
FORMATS = ['azw', 'mobi', 'prc', 'azw1', 'tpz', 'txt'] FORMATS = ['azw', 'mobi', 'prc', 'azw1', 'tpz', 'txt']
VENDOR_ID = [0x1949] VENDOR_ID = [0x1949]
PRODUCT_ID = [0x0001] PRODUCT_ID = [0x0001]
BCD = [0x399] BCD = [0x399]
VENDOR_NAME = 'KINDLE' VENDOR_NAME = 'KINDLE'
WINDOWS_MAIN_MEM = 'INTERNAL_STORAGE' WINDOWS_MAIN_MEM = 'INTERNAL_STORAGE'
WINDOWS_CARD_MEM = 'CARD_STORAGE' WINDOWS_CARD_MEM = 'CARD_STORAGE'
OSX_MAIN_MEM = 'Kindle Internal Storage Media' OSX_MAIN_MEM = 'Kindle Internal Storage Media'
OSX_CARD_MEM = 'Kindle Card Storage Media' OSX_CARD_MEM = 'Kindle Card Storage Media'
MAIN_MEMORY_VOLUME_LABEL = 'Kindle Main Memory' MAIN_MEMORY_VOLUME_LABEL = 'Kindle Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'Kindle Storage Card' STORAGE_CARD_VOLUME_LABEL = 'Kindle Storage Card'
EBOOK_DIR_MAIN = "documents" EBOOK_DIR_MAIN = "documents"
EBOOK_DIR_CARD = "documents" EBOOK_DIR_CARD = "documents"
SUPPORTS_SUB_DIRS = True SUPPORTS_SUB_DIRS = True
WIRELESS_FILE_NAME_PATTERN = re.compile( WIRELESS_FILE_NAME_PATTERN = re.compile(
r'(?P<title>[^-]+)-asin_(?P<asin>[a-zA-Z\d]{10,})-type_(?P<type>\w{4})-v_(?P<index>\d+).*') r'(?P<title>[^-]+)-asin_(?P<asin>[a-zA-Z\d]{10,})-type_(?P<type>\w{4})-v_(?P<index>\d+).*')
@ -37,13 +37,13 @@ class KINDLE(USBMS):
for path in paths: for path in paths:
if os.path.exists(path): if os.path.exists(path):
os.unlink(path) os.unlink(path)
filepath = os.path.splitext(path)[0] filepath = os.path.splitext(path)[0]
# Delete the ebook auxiliary file # Delete the ebook auxiliary file
if os.path.exists(filepath + '.mbp'): if os.path.exists(filepath + '.mbp'):
os.unlink(filepath + '.mbp') os.unlink(filepath + '.mbp')
@classmethod @classmethod
def metadata_from_path(cls, path): def metadata_from_path(cls, path):
mi = metadata_from_formats([path]) mi = metadata_from_formats([path])
@ -51,10 +51,13 @@ class KINDLE(USBMS):
match = cls.WIRELESS_FILE_NAME_PATTERN.match(os.path.basename(path)) match = cls.WIRELESS_FILE_NAME_PATTERN.match(os.path.basename(path))
if match is not None: if match is not None:
mi.title = match.group('title') mi.title = match.group('title')
if not isinstance(mi.title, unicode):
mi.title = mi.title.decode(sys.getfilesystemencoding(),
'replace')
return mi return mi
class KINDLE2(KINDLE): class KINDLE2(KINDLE):
PRODUCT_ID = [0x0002] PRODUCT_ID = [0x0002]
BCD = [0x0100] BCD = [0x0100]

View File

@ -22,17 +22,17 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
### Usage Type Data ### Usage Type Data
### wMaxPacketSize 0x0040 1x 64 bytes ### wMaxPacketSize 0x0040 1x 64 bytes
### bInterval 0 ### bInterval 0
###
### ###
### Endpoint 0x81 is device->host and endpoint 0x02 is host->device. ###
### Endpoint 0x81 is device->host and endpoint 0x02 is host->device.
### You can establish Stream pipes to/from these endpoints for Bulk transfers. ### You can establish Stream pipes to/from these endpoints for Bulk transfers.
### Has two configurations 1 is the USB charging config 2 is the self-powered ### Has two configurations 1 is the USB charging config 2 is the self-powered
### config. I think config management is automatic. Endpoints are the same ### config. I think config management is automatic. Endpoints are the same
""" """
Contains the logic for communication with the device (a SONY PRS-500). Contains the logic for communication with the device (a SONY PRS-500).
The public interface of class L{PRS500} defines the The public interface of class L{PRS500} defines the
methods for performing various tasks. methods for performing various tasks.
""" """
import sys, os import sys, os
from tempfile import TemporaryFile from tempfile import TemporaryFile
@ -49,12 +49,12 @@ from calibre.devices.prs500.books import BookList, fix_ids
from calibre import __author__, __appname__ from calibre import __author__, __appname__
# Protocol versions this driver has been tested with # Protocol versions this driver has been tested with
KNOWN_USB_PROTOCOL_VERSIONS = [0x3030303030303130L] KNOWN_USB_PROTOCOL_VERSIONS = [0x3030303030303130L]
class File(object): class File(object):
""" """
Wrapper that allows easy access to all information about files/directories Wrapper that allows easy access to all information about files/directories
""" """
def __init__(self, _file): def __init__(self, _file):
self.is_dir = _file[1].is_dir #: True if self is a directory self.is_dir = _file[1].is_dir #: True if self is a directory
@ -63,9 +63,9 @@ class File(object):
self.ctime = _file[1].ctime #: Creation time of self as a epoch self.ctime = _file[1].ctime #: Creation time of self as a epoch
self.wtime = _file[1].wtime #: Creation time of self as an epoch self.wtime = _file[1].wtime #: Creation time of self as an epoch
path = _file[0] path = _file[0]
if path.endswith("/"): if path.endswith("/"):
path = path[:-1] path = path[:-1]
self.path = path #: Path to self self.path = path #: Path to self
self.name = path[path.rfind("/")+1:].rstrip() #: Name of self self.name = path[path.rfind("/")+1:].rstrip() #: Name of self
def __repr__(self): def __repr__(self):
@ -80,7 +80,7 @@ class PRS500(Device):
""" """
Implements the backend for communication with the SONY Reader. Implements the backend for communication with the SONY Reader.
Each method decorated by C{safe} performs a task. Each method decorated by C{safe} performs a task.
""" """
VENDOR_ID = 0x054c #: SONY Vendor Id VENDOR_ID = 0x054c #: SONY Vendor Id
@ -92,33 +92,33 @@ class PRS500(Device):
BULK_IN_EP = 0x81 #: Endpoint for Bulk reads BULK_IN_EP = 0x81 #: Endpoint for Bulk reads
BULK_OUT_EP = 0x02 #: Endpoint for Bulk writes BULK_OUT_EP = 0x02 #: Endpoint for Bulk writes
# Location of media.xml file on device # Location of media.xml file on device
MEDIA_XML = "/Data/database/cache/media.xml" MEDIA_XML = "/Data/database/cache/media.xml"
# Location of cache.xml on storage card in device # Location of cache.xml on storage card in device
CACHE_XML = "/Sony Reader/database/cache.xml" CACHE_XML = "/Sony Reader/database/cache.xml"
# Ordered list of supported formats # Ordered list of supported formats
FORMATS = ["lrf", "lrx", "rtf", "pdf", "txt"] FORMATS = ["lrf", "lrx", "rtf", "pdf", "txt"]
# Height for thumbnails of books/images on the device # Height for thumbnails of books/images on the device
THUMBNAIL_HEIGHT = 68 THUMBNAIL_HEIGHT = 68
# Directory on card to which books are copied # Directory on card to which books are copied
CARD_PATH_PREFIX = __appname__ CARD_PATH_PREFIX = __appname__
_packet_number = 0 #: Keep track of the packet number for packet tracing _packet_number = 0 #: Keep track of the packet number for packet tracing
def log_packet(self, packet, header, stream=sys.stderr): def log_packet(self, packet, header, stream=sys.stderr):
""" """
Log C{packet} to stream C{stream}. Log C{packet} to stream C{stream}.
Header should be a small word describing the type of packet. Header should be a small word describing the type of packet.
""" """
self._packet_number += 1 self._packet_number += 1
print >> stream, str(self._packet_number), header, "Type:", \ print >> stream, str(self._packet_number), header, "Type:", \
packet.__class__.__name__ packet.__class__.__name__
print >> stream, packet print >> stream, packet
print >> stream, "--" print >> stream, "--"
@classmethod @classmethod
def validate_response(cls, res, _type=0x00, number=0x00): def validate_response(cls, res, _type=0x00, number=0x00):
""" """
Raise a ProtocolError if the type and number of C{res} Raise a ProtocolError if the type and number of C{res}
is not the same as C{type} and C{number}. is not the same as C{type} and C{number}.
""" """
if _type != res.type or number != res.rnumber: if _type != res.type or number != res.rnumber:
raise ProtocolError("Inavlid response.\ntype: expected=" + \ raise ProtocolError("Inavlid response.\ntype: expected=" + \
@ -127,31 +127,31 @@ class PRS500(Device):
" actual="+hex(res.rnumber)) " actual="+hex(res.rnumber))
@classmethod @classmethod
def signature(cls): def signature(cls):
""" Return a two element tuple (vendor id, product id) """ """ Return a two element tuple (vendor id, product id) """
return (cls.VENDOR_ID, cls.PRODUCT_ID ) return (cls.VENDOR_ID, cls.PRODUCT_ID )
def safe(func): def safe(func):
""" """
Decorator that wraps a call to C{func} to ensure that Decorator that wraps a call to C{func} to ensure that
exceptions are handled correctly. It also calls L{open} to claim exceptions are handled correctly. It also calls L{open} to claim
the interface and initialize the Reader if needed. the interface and initialize the Reader if needed.
As a convenience, C{safe} automatically sends the a As a convenience, C{safe} automatically sends the a
L{EndSession} after calling func, unless func has L{EndSession} after calling func, unless func has
a keyword argument named C{end_session} set to C{False}. a keyword argument named C{end_session} set to C{False}.
An L{ArgumentError} will cause the L{EndSession} command to An L{ArgumentError} will cause the L{EndSession} command to
be sent to the device, unless end_session is set to C{False}. be sent to the device, unless end_session is set to C{False}.
An L{usb.USBError} will cause the library to release control of the An L{usb.USBError} will cause the library to release control of the
USB interface via a call to L{close}. USB interface via a call to L{close}.
""" """
@wraps(func) @wraps(func)
def run_session(*args, **kwargs): def run_session(*args, **kwargs):
dev = args[0] dev = args[0]
res = None res = None
try: try:
if not dev.handle: if not dev.handle:
dev.open() dev.open()
if not dev.in_session: if not dev.in_session:
dev.send_validated_command(BeginEndSession(end=False)) dev.send_validated_command(BeginEndSession(end=False))
@ -161,19 +161,19 @@ class PRS500(Device):
if not kwargs.has_key("end_session") or kwargs["end_session"]: if not kwargs.has_key("end_session") or kwargs["end_session"]:
dev.send_validated_command(BeginEndSession(end=True)) dev.send_validated_command(BeginEndSession(end=True))
dev.in_session = False dev.in_session = False
raise raise
except USBError, err: except USBError, err:
if "No such device" in str(err): if "No such device" in str(err):
raise DeviceError() raise DeviceError()
elif "Connection timed out" in str(err): elif "Connection timed out" in str(err):
dev.close() dev.close()
raise TimeoutError(func.__name__) raise TimeoutError(func.__name__)
elif "Protocol error" in str(err): elif "Protocol error" in str(err):
dev.close() dev.close()
raise ProtocolError("There was an unknown error in the"+\ raise ProtocolError("There was an unknown error in the"+\
" protocol. Contact " + __author__) " protocol. Contact " + __author__)
dev.close() dev.close()
raise raise
if not kwargs.has_key("end_session") or kwargs["end_session"]: if not kwargs.has_key("end_session") or kwargs["end_session"]:
dev.send_validated_command(BeginEndSession(end=True)) dev.send_validated_command(BeginEndSession(end=True))
dev.in_session = False dev.in_session = False
@ -182,15 +182,15 @@ class PRS500(Device):
return run_session return run_session
def __init__(self, key='-1', log_packets=False, report_progress=None) : def __init__(self, key='-1', log_packets=False, report_progress=None) :
""" """
@param key: The key to unlock the device @param key: The key to unlock the device
@param log_packets: If true the packet stream to/from the device is logged @param log_packets: If true the packet stream to/from the device is logged
@param report_progress: Function that is called with a % progress @param report_progress: Function that is called with a % progress
(number between 0 and 100) for various tasks (number between 0 and 100) for various tasks
If it is called with -1 that means that the If it is called with -1 that means that the
task does not have any progress information task does not have any progress information
""" """
self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID) self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID)
# Handle that is used to communicate with device. Setup in L{open} # Handle that is used to communicate with device. Setup in L{open}
self.handle = None self.handle = None
self.in_session = False self.in_session = False
@ -204,14 +204,14 @@ class PRS500(Device):
def reconnect(self): def reconnect(self):
""" Only recreates the device node and deleted the connection handle """ """ Only recreates the device node and deleted the connection handle """
self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID) self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID)
self.handle = None self.handle = None
@classmethod @classmethod
def is_connected(cls, helper=None): def is_connected(cls, helper=None):
""" """
This method checks to see whether the device is physically connected. This method checks to see whether the device is physically connected.
It does not return any information about the validity of the It does not return any information about the validity of the
software connection. You may need to call L{reconnect} if you keep software connection. You may need to call L{reconnect} if you keep
getting L{DeviceError}. getting L{DeviceError}.
""" """
@ -222,15 +222,15 @@ class PRS500(Device):
def set_progress_reporter(self, report_progress): def set_progress_reporter(self, report_progress):
self.report_progress = report_progress self.report_progress = report_progress
def open(self) : def open(self) :
""" """
Claim an interface on the device for communication. Claim an interface on the device for communication.
Requires write privileges to the device file. Requires write privileges to the device file.
Also initialize the device. Also initialize the device.
See the source code for the sequence of initialization commands. See the source code for the sequence of initialization commands.
""" """
self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID) self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID)
if not self.device: if not self.device:
raise DeviceError() raise DeviceError()
configs = self.device.configurations configs = self.device.configurations
@ -238,7 +238,7 @@ class PRS500(Device):
self.handle = self.device.open() self.handle = self.device.open()
config = configs[0] config = configs[0]
try: try:
self.handle.set_configuration(configs[0]) self.handle.set_configuration(configs[0])
except USBError: except USBError:
self.handle.set_configuration(configs[1]) self.handle.set_configuration(configs[1])
config = configs[1] config = configs[1]
@ -250,13 +250,13 @@ class PRS500(Device):
else: else:
red, wed = ed2, ed1 red, wed = ed2, ed1
self.bulk_read_max_packet_size = red.MaxPacketSize self.bulk_read_max_packet_size = red.MaxPacketSize
self.bulk_write_max_packet_size = wed.MaxPacketSize self.bulk_write_max_packet_size = wed.MaxPacketSize
self.handle.claim_interface(self.INTERFACE_ID) self.handle.claim_interface(self.INTERFACE_ID)
except USBError, err: except USBError, err:
raise DeviceBusy(str(err)) raise DeviceBusy(str(err))
# Large timeout as device may still be initializing # Large timeout as device may still be initializing
res = self.send_validated_command(GetUSBProtocolVersion(), timeout=20000) res = self.send_validated_command(GetUSBProtocolVersion(), timeout=20000)
if res.code != 0: if res.code != 0:
raise ProtocolError("Unable to get USB Protocol version.") raise ProtocolError("Unable to get USB Protocol version.")
version = self._bulk_read(24, data_type=USBProtocolVersion)[0].version version = self._bulk_read(24, data_type=USBProtocolVersion)[0].version
if version not in KNOWN_USB_PROTOCOL_VERSIONS: if version not in KNOWN_USB_PROTOCOL_VERSIONS:
@ -265,16 +265,16 @@ class PRS500(Device):
res = self.send_validated_command(SetBulkSize(\ res = self.send_validated_command(SetBulkSize(\
chunk_size = 512*self.bulk_read_max_packet_size, \ chunk_size = 512*self.bulk_read_max_packet_size, \
unknown = 2)) unknown = 2))
if res.code != 0: if res.code != 0:
raise ProtocolError("Unable to set bulk size.") raise ProtocolError("Unable to set bulk size.")
res = self.send_validated_command(UnlockDevice(key=self.key))#0x312d)) res = self.send_validated_command(UnlockDevice(key=self.key))#0x312d))
if res.code != 0: if res.code != 0:
raise DeviceLocked() raise DeviceLocked()
res = self.send_validated_command(SetTime()) res = self.send_validated_command(SetTime())
if res.code != 0: if res.code != 0:
raise ProtocolError("Could not set time on device") raise ProtocolError("Could not set time on device")
def close(self): def close(self):
""" Release device interface """ """ Release device interface """
try: try:
self.handle.reset() self.handle.reset()
@ -285,16 +285,16 @@ class PRS500(Device):
self.in_session = False self.in_session = False
def _send_command(self, command, response_type=Response, timeout=1000): def _send_command(self, command, response_type=Response, timeout=1000):
""" """
Send L{command<Command>} to device and return its L{response<Response>}. Send L{command<Command>} to device and return its L{response<Response>}.
@param command: an object of type Command or one of its derived classes @param command: an object of type Command or one of its derived classes
@param response_type: an object of type 'type'. The return packet @param response_type: an object of type 'type'. The return packet
from the device is returned as an object of type response_type. from the device is returned as an object of type response_type.
@param timeout: The time to wait for a response from the @param timeout: The time to wait for a response from the
device, in milliseconds. If there is no response, a L{usb.USBError} is raised. device, in milliseconds. If there is no response, a L{usb.USBError} is raised.
""" """
if self.log_packets: if self.log_packets:
self.log_packet(command, "Command") self.log_packet(command, "Command")
bytes_sent = self.handle.control_msg(0x40, 0x80, command) bytes_sent = self.handle.control_msg(0x40, 0x80, command)
if bytes_sent != len(command): if bytes_sent != len(command):
@ -302,19 +302,19 @@ class PRS500(Device):
+ str(command)) + str(command))
response = response_type(self.handle.control_msg(0xc0, 0x81, \ response = response_type(self.handle.control_msg(0xc0, 0x81, \
Response.SIZE, timeout=timeout)) Response.SIZE, timeout=timeout))
if self.log_packets: if self.log_packets:
self.log_packet(response, "Response") self.log_packet(response, "Response")
return response return response
def send_validated_command(self, command, cnumber=None, \ def send_validated_command(self, command, cnumber=None, \
response_type=Response, timeout=1000): response_type=Response, timeout=1000):
""" """
Wrapper around L{_send_command} that checks if the Wrapper around L{_send_command} that checks if the
C{Response.rnumber == cnumber or C{Response.rnumber == cnumber or
command.number if cnumber==None}. Also check that command.number if cnumber==None}. Also check that
C{Response.type == Command.type}. C{Response.type == Command.type}.
""" """
if cnumber == None: if cnumber == None:
cnumber = command.number cnumber = command.number
res = self._send_command(command, response_type=response_type, \ res = self._send_command(command, response_type=response_type, \
timeout=timeout) timeout=timeout)
@ -322,18 +322,18 @@ class PRS500(Device):
return res return res
def _bulk_write(self, data, packet_size=0x1000): def _bulk_write(self, data, packet_size=0x1000):
""" """
Send data to device via a bulk transfer. Send data to device via a bulk transfer.
@type data: Any listable type supporting __getslice__ @type data: Any listable type supporting __getslice__
@param packet_size: Size of packets to be sent to device. @param packet_size: Size of packets to be sent to device.
C{data} is broken up into packets to be sent to device. C{data} is broken up into packets to be sent to device.
""" """
def bulk_write_packet(packet): def bulk_write_packet(packet):
self.handle.bulk_write(self.BULK_OUT_EP, packet) self.handle.bulk_write(self.BULK_OUT_EP, packet)
if self.log_packets: if self.log_packets:
self.log_packet(Answer(packet), "Answer h->d") self.log_packet(Answer(packet), "Answer h->d")
bytes_left = len(data) bytes_left = len(data)
if bytes_left + 16 <= packet_size: if bytes_left + 16 <= packet_size:
packet_size = bytes_left +16 packet_size = bytes_left +16
first_packet = Answer(bytes_left+16) first_packet = Answer(bytes_left+16)
@ -355,11 +355,11 @@ class PRS500(Device):
pos = endpos pos = endpos
res = Response(self.handle.control_msg(0xc0, 0x81, Response.SIZE, \ res = Response(self.handle.control_msg(0xc0, 0x81, Response.SIZE, \
timeout=5000)) timeout=5000))
if self.log_packets: if self.log_packets:
self.log_packet(res, "Response") self.log_packet(res, "Response")
if res.rnumber != 0x10005 or res.code != 0: if res.rnumber != 0x10005 or res.code != 0:
raise ProtocolError("Sending via Bulk Transfer failed with response:\n"\ raise ProtocolError("Sending via Bulk Transfer failed with response:\n"\
+str(res)) +str(res))
if res.data_size != len(data): if res.data_size != len(data):
raise ProtocolError("Unable to transfer all data to device. "+\ raise ProtocolError("Unable to transfer all data to device. "+\
"Response packet:\n"\ "Response packet:\n"\
@ -368,12 +368,12 @@ class PRS500(Device):
def _bulk_read(self, bytes, command_number=0x00, packet_size=0x1000, \ def _bulk_read(self, bytes, command_number=0x00, packet_size=0x1000, \
data_type=Answer): data_type=Answer):
""" """
Read in C{bytes} bytes via a bulk transfer in Read in C{bytes} bytes via a bulk transfer in
packets of size S{<=} C{packet_size} packets of size S{<=} C{packet_size}
@param data_type: an object of type type. @param data_type: an object of type type.
The data packet is returned as an object of type C{data_type}. The data packet is returned as an object of type C{data_type}.
@return: A list of packets read from the device. @return: A list of packets read from the device.
Each packet is of type data_type Each packet is of type data_type
""" """
msize = self.bulk_read_max_packet_size msize = self.bulk_read_max_packet_size
@ -392,7 +392,7 @@ class PRS500(Device):
bytes_left = bytes bytes_left = bytes
packets = [] packets = []
while bytes_left > 0: while bytes_left > 0:
if packet_size > bytes_left: if packet_size > bytes_left:
packet_size = bytes_left packet_size = bytes_left
packet = bulk_read_packet(data_type=data_type, size=packet_size) packet = bulk_read_packet(data_type=data_type, size=packet_size)
bytes_left -= len(packet) bytes_left -= len(packet)
@ -404,8 +404,8 @@ class PRS500(Device):
@safe @safe
def get_device_information(self, end_session=True): def get_device_information(self, end_session=True):
""" """
Ask device for device information. See L{DeviceInfoQuery}. Ask device for device information. See L{DeviceInfoQuery}.
@return: (device name, device version, software version on device, mime type) @return: (device name, device version, software version on device, mime type)
""" """
size = self.send_validated_command(DeviceInfoQuery()).data[2] + 16 size = self.send_validated_command(DeviceInfoQuery()).data[2] + 16
@ -416,21 +416,21 @@ class PRS500(Device):
@safe @safe
def path_properties(self, path, end_session=True): def path_properties(self, path, end_session=True):
""" """
Send command asking device for properties of C{path}. Send command asking device for properties of C{path}.
Return L{FileProperties}. Return L{FileProperties}.
""" """
res = self.send_validated_command(PathQuery(path), \ res = self.send_validated_command(PathQuery(path), \
response_type=ListResponse) response_type=ListResponse)
data = self._bulk_read(0x28, data_type=FileProperties, \ data = self._bulk_read(0x28, data_type=FileProperties, \
command_number=PathQuery.NUMBER)[0] command_number=PathQuery.NUMBER)[0]
if path.endswith('/') and path != '/': if path.endswith('/') and path != '/':
path = path[:-1] path = path[:-1]
if res.path_not_found : if res.path_not_found :
raise PathError(path + " does not exist on device") raise PathError(path + " does not exist on device")
if res.is_invalid: if res.is_invalid:
raise PathError(path + " is not a valid path") raise PathError(path + " is not a valid path")
if res.is_unmounted: if res.is_unmounted:
raise PathError(path + " is not mounted") raise PathError(path + " is not mounted")
if res.permission_denied: if res.permission_denied:
raise PathError('Permission denied for: ' + path + '\nYou can only '+\ raise PathError('Permission denied for: ' + path + '\nYou can only '+\
@ -443,20 +443,20 @@ class PRS500(Device):
@safe @safe
def get_file(self, path, outfile, end_session=True): def get_file(self, path, outfile, end_session=True):
""" """
Read the file at path on the device and write it to outfile. Read the file at path on the device and write it to outfile.
The data is fetched in chunks of size S{<=} 32K. Each chunk is The data is fetched in chunks of size S{<=} 32K. Each chunk is
made of packets of size S{<=} 4K. See L{FileOpen}, made of packets of size S{<=} 4K. See L{FileOpen},
L{FileRead} and L{FileClose} for details on the command packets used. L{FileRead} and L{FileClose} for details on the command packets used.
@param outfile: file object like C{sys.stdout} or the result of an C{open} call @param outfile: file object like C{sys.stdout} or the result of an C{open} call
""" """
if path.endswith("/"): if path.endswith("/"):
path = path[:-1] # We only copy files path = path[:-1] # We only copy files
cp = self.card_prefix(False) cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '') path = path.replace('card:/', cp if cp else '')
_file = self.path_properties(path, end_session=False) _file = self.path_properties(path, end_session=False)
if _file.is_dir: if _file.is_dir:
raise PathError("Cannot read as " + path + " is a directory") raise PathError("Cannot read as " + path + " is a directory")
bytes = _file.file_size bytes = _file.file_size
res = self.send_validated_command(FileOpen(path)) res = self.send_validated_command(FileOpen(path))
@ -464,12 +464,12 @@ class PRS500(Device):
raise PathError("Unable to open " + path + \ raise PathError("Unable to open " + path + \
" for reading. Response code: " + hex(res.code)) " for reading. Response code: " + hex(res.code))
_id = self._bulk_read(20, data_type=IdAnswer, \ _id = self._bulk_read(20, data_type=IdAnswer, \
command_number=FileOpen.NUMBER)[0].id command_number=FileOpen.NUMBER)[0].id
# The first 16 bytes from the device are meta information on the packet stream # The first 16 bytes from the device are meta information on the packet stream
bytes_left, chunk_size = bytes, 512 * self.bulk_read_max_packet_size -16 bytes_left, chunk_size = bytes, 512 * self.bulk_read_max_packet_size -16
packet_size, pos = 64 * self.bulk_read_max_packet_size, 0 packet_size, pos = 64 * self.bulk_read_max_packet_size, 0
while bytes_left > 0: while bytes_left > 0:
if chunk_size > bytes_left: if chunk_size > bytes_left:
chunk_size = bytes_left chunk_size = bytes_left
res = self.send_validated_command(FileIO(_id, pos, chunk_size)) res = self.send_validated_command(FileIO(_id, pos, chunk_size))
if res.code != 0: if res.code != 0:
@ -477,21 +477,21 @@ class PRS500(Device):
raise ProtocolError("Error while reading from " + path + \ raise ProtocolError("Error while reading from " + path + \
". Response code: " + hex(res.code)) ". Response code: " + hex(res.code))
packets = self._bulk_read(chunk_size+16, \ packets = self._bulk_read(chunk_size+16, \
command_number=FileIO.RNUMBER, packet_size=packet_size) command_number=FileIO.RNUMBER, packet_size=packet_size)
try: try:
outfile.write("".join(map(chr, packets[0][16:]))) outfile.write("".join(map(chr, packets[0][16:])))
for i in range(1, len(packets)): for i in range(1, len(packets)):
outfile.write("".join(map(chr, packets[i]))) outfile.write("".join(map(chr, packets[i])))
except IOError, err: except IOError, err:
self.send_validated_command(FileClose(_id)) self.send_validated_command(FileClose(_id))
raise ArgumentError("File get operation failed. " + \ raise ArgumentError("File get operation failed. " + \
"Could not write to local location: " + str(err)) "Could not write to local location: " + str(err))
bytes_left -= chunk_size bytes_left -= chunk_size
pos += chunk_size pos += chunk_size
if self.report_progress: if self.report_progress:
self.report_progress(int(100*((1.*pos)/bytes))) self.report_progress(int(100*((1.*pos)/bytes)))
self.send_validated_command(FileClose(_id)) self.send_validated_command(FileClose(_id))
# Not going to check response code to see if close was successful # Not going to check response code to see if close was successful
# as there's not much we can do if it wasnt # as there's not much we can do if it wasnt
@safe @safe
@ -503,26 +503,26 @@ class PRS500(Device):
@type path: string @type path: string
@param path: The path to list @param path: The path to list
@type recurse: boolean @type recurse: boolean
@param recurse: If true do a recursive listing @param recurse: If true do a recursive listing
@return: A list of tuples. The first element of each tuple is a path. @return: A list of tuples. The first element of each tuple is a path.
The second element is a list of L{Files<File>}. The second element is a list of L{Files<File>}.
The path is the path we are listing, the C{Files} are the The path is the path we are listing, the C{Files} are the
files/directories in that path. If it is a recursive list, then the first files/directories in that path. If it is a recursive list, then the first
element will be (C{path}, children), the next will be element will be (C{path}, children), the next will be
(child, its children) and so on. If it is not recursive the length of the (child, its children) and so on. If it is not recursive the length of the
outermost list will be 1. outermost list will be 1.
""" """
def _list(path): def _list(path):
""" Do a non recursive listsing of path """ """ Do a non recursive listsing of path """
if not path.endswith("/"): if not path.endswith("/"):
path += "/" # Initially assume path is a directory path += "/" # Initially assume path is a directory
cp = self.card_prefix(False) cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '') path = path.replace('card:/', cp if cp else '')
files = [] files = []
candidate = self.path_properties(path, end_session=False) candidate = self.path_properties(path, end_session=False)
if not candidate.is_dir: if not candidate.is_dir:
path = path[:-1] path = path[:-1]
data = self.path_properties(path, end_session=False) data = self.path_properties(path, end_session=False)
files = [ File((path, data)) ] files = [ File((path, data)) ]
else: else:
# Get query ID used to ask for next element in list # Get query ID used to ask for next element in list
@ -536,20 +536,20 @@ class PRS500(Device):
next = DirRead(_id) next = DirRead(_id)
items = [] items = []
while True: while True:
res = self.send_validated_command(next, response_type=ListResponse) res = self.send_validated_command(next, response_type=ListResponse)
size = res.data_size + 16 size = res.data_size + 16
data = self._bulk_read(size, data_type=ListAnswer, \ data = self._bulk_read(size, data_type=ListAnswer, \
command_number=DirRead.NUMBER)[0] command_number=DirRead.NUMBER)[0]
# path_not_found seems to happen if the usb server # path_not_found seems to happen if the usb server
# doesn't have the permissions to access the directory # doesn't have the permissions to access the directory
if res.is_eol or res.path_not_found: if res.is_eol or res.path_not_found:
break break
elif res.code != 0: elif res.code != 0:
raise ProtocolError("Unknown error occured while "+\ raise ProtocolError("Unknown error occured while "+\
"reading contents of directory " + path + \ "reading contents of directory " + path + \
". Response code: " + hex(res.code)) ". Response code: " + hex(res.code))
items.append(data.name) items.append(data.name)
self.send_validated_command(DirClose(_id)) self.send_validated_command(DirClose(_id))
# Ignore res.code as we cant do anything if close fails # Ignore res.code as we cant do anything if close fails
for item in items: for item in items:
ipath = path + item ipath = path + item
@ -568,23 +568,23 @@ class PRS500(Device):
@safe @safe
def total_space(self, end_session=True): def total_space(self, end_session=True):
""" """
Get total space available on the mountpoints: Get total space available on the mountpoints:
1. Main memory 1. Main memory
2. Memory Stick 2. Memory Stick
3. SD Card 3. SD Card
@return: A 3 element list with total space in bytes of (1, 2, 3) @return: A 3 element list with total space in bytes of (1, 2, 3)
""" """
data = [] data = []
for path in ("/Data/", "a:/", "b:/"): for path in ("/Data/", "a:/", "b:/"):
# Timeout needs to be increased as it takes time to read card # Timeout needs to be increased as it takes time to read card
res = self.send_validated_command(TotalSpaceQuery(path), \ res = self.send_validated_command(TotalSpaceQuery(path), \
timeout=5000) timeout=5000)
buffer_size = 16 + res.data[2] buffer_size = 16 + res.data[2]
pkt = self._bulk_read(buffer_size, data_type=TotalSpaceAnswer, \ pkt = self._bulk_read(buffer_size, data_type=TotalSpaceAnswer, \
command_number=TotalSpaceQuery.NUMBER)[0] command_number=TotalSpaceQuery.NUMBER)[0]
data.append( pkt.total ) data.append( pkt.total )
return data return data
@safe @safe
@ -600,26 +600,26 @@ class PRS500(Device):
return path return path
except PathError: except PathError:
return None return None
@safe @safe
def free_space(self, end_session=True): def free_space(self, end_session=True):
""" """
Get free space available on the mountpoints: Get free space available on the mountpoints:
1. Main memory 1. Main memory
2. Memory Stick 2. Memory Stick
3. SD Card 3. SD Card
@return: A 3 element list with free space in bytes of (1, 2, 3) @return: A 3 element list with free space in bytes of (1, 2, 3)
""" """
data = [] data = []
for path in ("/", "a:/", "b:/"): for path in ("/", "a:/", "b:/"):
# Timeout needs to be increased as it takes time to read card # Timeout needs to be increased as it takes time to read card
self.send_validated_command(FreeSpaceQuery(path), \ self.send_validated_command(FreeSpaceQuery(path), \
timeout=5000) timeout=5000)
pkt = self._bulk_read(FreeSpaceAnswer.SIZE, \ pkt = self._bulk_read(FreeSpaceAnswer.SIZE, \
data_type=FreeSpaceAnswer, \ data_type=FreeSpaceAnswer, \
command_number=FreeSpaceQuery.NUMBER)[0] command_number=FreeSpaceQuery.NUMBER)[0]
data.append( pkt.free ) data.append( pkt.free )
return data return data
def _exists(self, path): def _exists(self, path):
@ -628,21 +628,21 @@ class PRS500(Device):
try: try:
dest = self.path_properties(path, end_session=False) dest = self.path_properties(path, end_session=False)
except PathError, err: except PathError, err:
if "does not exist" in str(err) or "not mounted" in str(err): if "does not exist" in str(err) or "not mounted" in str(err):
return (False, None) return (False, None)
else: raise else: raise
return (True, dest) return (True, dest)
@safe @safe
def touch(self, path, end_session=True): def touch(self, path, end_session=True):
""" """
Create a file at path Create a file at path
@todo: Update file modification time if it exists. @todo: Update file modification time if it exists.
Opening the file in write mode and then closing it doesn't work. Opening the file in write mode and then closing it doesn't work.
""" """
cp = self.card_prefix(False) cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '') path = path.replace('card:/', cp if cp else '')
if path.endswith("/") and len(path) > 1: if path.endswith("/") and len(path) > 1:
path = path[:-1] path = path[:-1]
exists, _file = self._exists(path) exists, _file = self._exists(path)
if exists and _file.is_dir: if exists and _file.is_dir:
@ -651,18 +651,18 @@ class PRS500(Device):
res = self.send_validated_command(FileCreate(path)) res = self.send_validated_command(FileCreate(path))
if res.code != 0: if res.code != 0:
raise PathError("Could not create file " + path + \ raise PathError("Could not create file " + path + \
". Response code: " + str(hex(res.code))) ". Response code: " + str(hex(res.code)))
@safe @safe
def put_file(self, infile, path, replace_file=False, end_session=True): def put_file(self, infile, path, replace_file=False, end_session=True):
""" """
Put infile onto the devoce at path Put infile onto the devoce at path
@param infile: An open file object. infile must have a name attribute. @param infile: An open file object. infile must have a name attribute.
If you are using a StringIO object set its name attribute manually. If you are using a StringIO object set its name attribute manually.
@param path: The path on the device at which to put infile. @param path: The path on the device at which to put infile.
It should point to an existing directory. It should point to an existing directory.
@param replace_file: If True and path points to a file that already exists, it is replaced @param replace_file: If True and path points to a file that already exists, it is replaced
""" """
pos = infile.tell() pos = infile.tell()
infile.seek(0, 2) infile.seek(0, 2)
bytes = infile.tell() - pos bytes = infile.tell() - pos
@ -673,12 +673,12 @@ class PRS500(Device):
exists, dest = self._exists(path) exists, dest = self._exists(path)
if exists: if exists:
if dest.is_dir: if dest.is_dir:
if not path.endswith("/"): if not path.endswith("/"):
path += "/" path += "/"
path += os.path.basename(infile.name) path += os.path.basename(infile.name)
return self.put_file(infile, path, replace_file=replace_file, end_session=False) return self.put_file(infile, path, replace_file=replace_file, end_session=False)
else: else:
if not replace_file: if not replace_file:
raise PathError("Cannot write to " + \ raise PathError("Cannot write to " + \
path + " as it already exists", path=path) path + " as it already exists", path=path)
_file = self.path_properties(path, end_session=False) _file = self.path_properties(path, end_session=False)
@ -693,7 +693,7 @@ class PRS500(Device):
raise ProtocolError("Unable to open " + path + \ raise ProtocolError("Unable to open " + path + \
" for writing. Response code: " + hex(res.code)) " for writing. Response code: " + hex(res.code))
_id = self._bulk_read(20, data_type=IdAnswer, \ _id = self._bulk_read(20, data_type=IdAnswer, \
command_number=FileOpen.NUMBER)[0].id command_number=FileOpen.NUMBER)[0].id
while data_left: while data_left:
data = array('B') data = array('B')
@ -704,7 +704,7 @@ class PRS500(Device):
data.fromstring(ind) data.fromstring(ind)
if len(ind) < chunk_size: if len(ind) < chunk_size:
raise EOFError raise EOFError
except EOFError: except EOFError:
data_left = False data_left = False
res = self.send_validated_command(FileIO(_id, pos, len(data), \ res = self.send_validated_command(FileIO(_id, pos, len(data), \
mode=FileIO.WNUMBER)) mode=FileIO.WNUMBER))
@ -715,7 +715,7 @@ class PRS500(Device):
pos += len(data) pos += len(data)
if self.report_progress: if self.report_progress:
self.report_progress( int(100*(pos-start_pos)/(1.*bytes)) ) self.report_progress( int(100*(pos-start_pos)/(1.*bytes)) )
self.send_validated_command(FileClose(_id)) self.send_validated_command(FileClose(_id))
# Ignore res.code as cant do anything if close fails # Ignore res.code as cant do anything if close fails
_file = self.path_properties(path, end_session=False) _file = self.path_properties(path, end_session=False)
if _file.file_size != pos: if _file.file_size != pos:
@ -727,7 +727,7 @@ class PRS500(Device):
def del_file(self, path, end_session=True): def del_file(self, path, end_session=True):
""" Delete C{path} from device iff path is a file """ """ Delete C{path} from device iff path is a file """
data = self.path_properties(path, end_session=False) data = self.path_properties(path, end_session=False)
if data.is_dir: if data.is_dir:
raise PathError("Cannot delete directories") raise PathError("Cannot delete directories")
res = self.send_validated_command(FileDelete(path), \ res = self.send_validated_command(FileDelete(path), \
response_type=ListResponse) response_type=ListResponse)
@ -741,7 +741,7 @@ class PRS500(Device):
if path.startswith('card:/'): if path.startswith('card:/'):
cp = self.card_prefix(False) cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '') path = path.replace('card:/', cp if cp else '')
if not path.endswith("/"): if not path.endswith("/"):
path += "/" path += "/"
error_prefix = "Cannot create directory " + path error_prefix = "Cannot create directory " + path
res = self.send_validated_command(DirCreate(path)).data[0] res = self.send_validated_command(DirCreate(path)).data[0]
@ -764,8 +764,8 @@ class PRS500(Device):
if not dir.is_dir: if not dir.is_dir:
self.del_file(path, end_session=False) self.del_file(path, end_session=False)
else: else:
if not path.endswith("/"): if not path.endswith("/"):
path += "/" path += "/"
res = self.send_validated_command(DirDelete(path)) res = self.send_validated_command(DirDelete(path))
if res.code == PathResponseCodes.HAS_CHILDREN: if res.code == PathResponseCodes.HAS_CHILDREN:
raise PathError("Cannot delete directory " + path + \ raise PathError("Cannot delete directory " + path + \
@ -778,24 +778,24 @@ class PRS500(Device):
def card(self, end_session=True): def card(self, end_session=True):
""" Return path prefix to installed card or None """ """ Return path prefix to installed card or None """
card = None card = None
if self._exists("a:/")[0]: if self._exists("a:/")[0]:
card = "a:" card = "a:"
if self._exists("b:/")[0]: if self._exists("b:/")[0]:
card = "b:" card = "b:"
return card return card
@safe @safe
def books(self, oncard=False, end_session=True): def books(self, oncard=False, end_session=True):
""" """
Return a list of ebooks on the device. Return a list of ebooks on the device.
@param oncard: If True return a list of ebooks on the storage card, @param oncard: If True return a list of ebooks on the storage card,
otherwise return list of ebooks in main memory of device otherwise return list of ebooks in main memory of device
@return: L{BookList} @return: L{BookList}
""" """
root = "/Data/media/" root = "/Data/media/"
tfile = TemporaryFile() tfile = TemporaryFile()
if oncard: if oncard:
try: try:
self.get_file("a:"+self.CACHE_XML, tfile, end_session=False) self.get_file("a:"+self.CACHE_XML, tfile, end_session=False)
root = "a:/" root = "a:/"
@ -804,9 +804,9 @@ class PRS500(Device):
self.get_file("b:"+self.CACHE_XML, tfile, end_session=False) self.get_file("b:"+self.CACHE_XML, tfile, end_session=False)
root = "b:/" root = "b:/"
except PathError: pass except PathError: pass
if tfile.tell() == 0: if tfile.tell() == 0:
tfile = None tfile = None
else: else:
self.get_file(self.MEDIA_XML, tfile, end_session=False) self.get_file(self.MEDIA_XML, tfile, end_session=False)
bl = BookList(root=root, sfile=tfile) bl = BookList(root=root, sfile=tfile)
paths = bl.purge_corrupted_files() paths = bl.purge_corrupted_files()
@ -822,26 +822,26 @@ class PRS500(Device):
""" """
Remove the books specified by paths from the device. The metadata Remove the books specified by paths from the device. The metadata
cache on the device should also be updated. cache on the device should also be updated.
""" """
for path in paths: for path in paths:
self.del_file(path, end_session=False) self.del_file(path, end_session=False)
fix_ids(booklists[0], booklists[1]) fix_ids(booklists[0], booklists[1])
self.sync_booklists(booklists, end_session=False) self.sync_booklists(booklists, end_session=False)
@safe @safe
def sync_booklists(self, booklists, end_session=True): def sync_booklists(self, booklists, end_session=True):
''' '''
Upload bookslists to device. Upload bookslists to device.
@param booklists: A tuple containing the result of calls to @param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)). (L{books}(oncard=False), L{books}(oncard=True)).
''' '''
fix_ids(*booklists) fix_ids(*booklists)
self.upload_book_list(booklists[0], end_session=False) self.upload_book_list(booklists[0], end_session=False)
if booklists[1].root: if booklists[1].root:
self.upload_book_list(booklists[1], end_session=False) self.upload_book_list(booklists[1], end_session=False)
@safe @safe
def upload_books(self, files, names, on_card=False, end_session=True, def upload_books(self, files, names, on_card=False, end_session=True,
metadata=None): metadata=None):
card = self.card(end_session=False) card = self.card(end_session=False)
prefix = card + '/' + self.CARD_PATH_PREFIX +'/' if on_card else '/Data/media/books/' prefix = card + '/' + self.CARD_PATH_PREFIX +'/' if on_card else '/Data/media/books/'
@ -856,21 +856,21 @@ class PRS500(Device):
space = self.free_space(end_session=False) space = self.free_space(end_session=False)
mspace = space[0] mspace = space[0]
cspace = space[1] if space[1] >= space[2] else space[2] cspace = space[1] if space[1] >= space[2] else space[2]
if on_card and size > cspace - 1024*1024: if on_card and size > cspace - 1024*1024:
raise FreeSpaceError("There is insufficient free space "+\ raise FreeSpaceError("There is insufficient free space "+\
"on the storage card") "on the storage card")
if not on_card and size > mspace - 2*1024*1024: if not on_card and size > mspace - 2*1024*1024:
raise FreeSpaceError("There is insufficient free space " +\ raise FreeSpaceError("There is insufficient free space " +\
"in main memory") "in main memory")
for infile in infiles: for infile in infiles:
infile.seek(0) infile.seek(0)
name = names.next() name = names.next()
paths.append(prefix+name) paths.append(prefix+name)
self.put_file(infile, paths[-1], replace_file=True, end_session=False) self.put_file(infile, paths[-1], replace_file=True, end_session=False)
ctimes.append(self.path_properties(paths[-1], end_session=False).ctime) ctimes.append(self.path_properties(paths[-1], end_session=False).ctime)
return zip(paths, sizes, ctimes) return zip(paths, sizes, ctimes)
@classmethod @classmethod
def add_books_to_metadata(cls, locations, metadata, booklists): def add_books_to_metadata(cls, locations, metadata, booklists):
metadata = iter(metadata) metadata = iter(metadata)
@ -882,35 +882,35 @@ class PRS500(Device):
name = (cls.CARD_PATH_PREFIX+'/' if on_card else 'books/') + name name = (cls.CARD_PATH_PREFIX+'/' if on_card else 'books/') + name
booklists[on_card].add_book(info, name, *location[1:]) booklists[on_card].add_book(info, name, *location[1:])
fix_ids(*booklists) fix_ids(*booklists)
@safe @safe
def delete_books(self, paths, end_session=True): def delete_books(self, paths, end_session=True):
for path in paths: for path in paths:
self.del_file(path, end_session=False) self.del_file(path, end_session=False)
@classmethod @classmethod
def remove_books_from_metadata(cls, paths, booklists): def remove_books_from_metadata(cls, paths, booklists):
for path in paths: for path in paths:
on_card = 1 if path[1] == ':' else 0 on_card = 1 if path[1] == ':' else 0
booklists[on_card].remove_book(path) booklists[on_card].remove_book(path)
fix_ids(*booklists) fix_ids(*booklists)
@safe @safe
def add_book(self, infile, name, info, booklists, oncard=False, \ def add_book(self, infile, name, info, booklists, oncard=False, \
sync_booklists=False, end_session=True): sync_booklists=False, end_session=True):
""" """
Add a book to the device. If oncard is True then the book is copied Add a book to the device. If oncard is True then the book is copied
to the card rather than main memory. to the card rather than main memory.
@param infile: The source file, should be opened in "rb" mode @param infile: The source file, should be opened in "rb" mode
@param name: The name of the book file when uploaded to the @param name: The name of the book file when uploaded to the
device. The extension of name must be one of device. The extension of name must be one of
the supported formats for this device. the supported formats for this device.
@param info: A dictionary that must have the keys "title", "authors", "cover". @param info: A dictionary that must have the keys "title", "authors", "cover".
C{info["cover"]} should be a three element tuple (width, height, data) C{info["cover"]} should be a three element tuple (width, height, data)
where data is the image data in JPEG format as a string where data is the image data in JPEG format as a string
@param booklists: A tuple containing the result of calls to @param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)). (L{books}(oncard=False), L{books}(oncard=True)).
""" """
infile.seek(0, 2) infile.seek(0, 2)
size = infile.tell() size = infile.tell()
@ -922,11 +922,11 @@ class PRS500(Device):
if oncard and size > cspace - 1024*1024: if oncard and size > cspace - 1024*1024:
raise FreeSpaceError("There is insufficient free space "+\ raise FreeSpaceError("There is insufficient free space "+\
"on the storage card") "on the storage card")
if not oncard and size > mspace - 1024*1024: if not oncard and size > mspace - 1024*1024:
raise FreeSpaceError("There is insufficient free space " +\ raise FreeSpaceError("There is insufficient free space " +\
"in main memory") "in main memory")
prefix = "/Data/media/" prefix = "/Data/media/"
if oncard: if oncard:
prefix = card + "/" prefix = card + "/"
else: name = "books/"+name else: name = "books/"+name
path = prefix + name path = prefix + name
@ -943,12 +943,12 @@ class PRS500(Device):
path = self.MEDIA_XML path = self.MEDIA_XML
if not booklist.prefix: if not booklist.prefix:
card = self.card(end_session=True) card = self.card(end_session=True)
if not card: if not card:
raise ArgumentError("Cannot upload list to card as "+\ raise ArgumentError("Cannot upload list to card as "+\
"card is not present") "card is not present")
path = card + self.CACHE_XML path = card + self.CACHE_XML
f = StringIO() f = StringIO()
booklist.write(f) booklist.write(f)
f.seek(0) f.seek(0)
self.put_file(f, path, replace_file=True, end_session=False) self.put_file(f, path, replace_file=True, end_session=False)
f.close() f.close()

View File

@ -35,6 +35,7 @@ class USBMS(Device):
EBOOK_DIR_MAIN = '' EBOOK_DIR_MAIN = ''
EBOOK_DIR_CARD = '' EBOOK_DIR_CARD = ''
SUPPORTS_SUB_DIRS = False SUPPORTS_SUB_DIRS = False
CAN_SET_METADATA = False
def __init__(self, key='-1', log_packets=False, report_progress=None): def __init__(self, key='-1', log_packets=False, report_progress=None):
Device.__init__(self, key=key, log_packets=log_packets, Device.__init__(self, key=key, log_packets=log_packets,

View File

@ -12,7 +12,7 @@ from PyQt4.Qt import QFontDatabase
from calibre.ebooks.epub.from_any import MAP from calibre.ebooks.epub.from_any import MAP
from calibre.ebooks.epub.from_html import TITLEPAGE from calibre.ebooks.epub.from_html import TITLEPAGE
from calibre.ebooks.epub import config from calibre.ebooks.epub import config
from calibre.ebooks.metadata.opf2 import OPF from calibre.ebooks.metadata.opf2 import OPF
from calibre.ptempfile import TemporaryDirectory from calibre.ptempfile import TemporaryDirectory
from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.chardet import xml_to_unicode
@ -31,13 +31,15 @@ def character_count(html):
return count return count
class UnsupportedFormatError(Exception): class UnsupportedFormatError(Exception):
def __init__(self, fmt): def __init__(self, fmt):
Exception.__init__(self, _('%s format books are not supported')%fmt.upper()) Exception.__init__(self, _('%s format books are not supported')%fmt.upper())
class SpineItem(unicode): class SpineItem(unicode):
def __new__(cls, *args): def __new__(cls, *args):
args = list(args)
args[0] = args[0].partition('#')[0]
obj = super(SpineItem, cls).__new__(cls, *args) obj = super(SpineItem, cls).__new__(cls, *args)
path = args[0] path = args[0]
raw = open(path, 'rb').read() raw = open(path, 'rb').read()
@ -63,10 +65,11 @@ def is_supported(path):
return ext in list(MAP.keys())+['html', 'opf'] return ext in list(MAP.keys())+['html', 'opf']
class EbookIterator(object): class EbookIterator(object):
CHARACTERS_PER_PAGE = 1000 CHARACTERS_PER_PAGE = 1000
def __init__(self, pathtoebook): def __init__(self, pathtoebook):
pathtoebook = pathtoebook.strip()
self.pathtoebook = os.path.abspath(pathtoebook) self.pathtoebook = os.path.abspath(pathtoebook)
self.config = DynamicConfig(name='iterator') self.config = DynamicConfig(name='iterator')
ext = os.path.splitext(pathtoebook)[1].replace('.', '').lower() ext = os.path.splitext(pathtoebook)[1].replace('.', '').lower()
@ -77,17 +80,17 @@ class EbookIterator(object):
if ext not in map.keys(): if ext not in map.keys():
raise UnsupportedFormatError(ext) raise UnsupportedFormatError(ext)
self.to_opf = map[ext] self.to_opf = map[ext]
def search(self, text, index): def search(self, text, index):
text = text.lower() text = text.lower()
for i, path in enumerate(self.spine): for i, path in enumerate(self.spine):
if i > index: if i > index:
if text in open(path, 'rb').read().decode(path.encoding).lower(): if text in open(path, 'rb').read().decode(path.encoding).lower():
return i return i
def find_embedded_fonts(self): def find_embedded_fonts(self):
''' '''
This will become unnecessary once Qt WebKit supports the @font-face rule. This will become unnecessary once Qt WebKit supports the @font-face rule.
''' '''
for item in self.opf.manifest: for item in self.opf.manifest:
if item.mime_type and 'css' in item.mime_type.lower(): if item.mime_type and 'css' in item.mime_type.lower():
@ -108,7 +111,7 @@ class EbookIterator(object):
print 'WARNING: Family aliasing not supported:', block print 'WARNING: Family aliasing not supported:', block
else: else:
print 'Loaded embedded font:', repr(family) print 'Loaded embedded font:', repr(family)
def __enter__(self): def __enter__(self):
self._tdir = TemporaryDirectory('_ebook_iter') self._tdir = TemporaryDirectory('_ebook_iter')
self.base = self._tdir.__enter__() self.base = self._tdir.__enter__()
@ -116,50 +119,50 @@ class EbookIterator(object):
self.pathtoopf = self.to_opf(self.pathtoebook, self.base, opts) self.pathtoopf = self.to_opf(self.pathtoebook, self.base, opts)
self.opf = OPF(self.pathtoopf, os.path.dirname(self.pathtoopf)) self.opf = OPF(self.pathtoopf, os.path.dirname(self.pathtoopf))
self.spine = [SpineItem(i.path) for i in self.opf.spine] self.spine = [SpineItem(i.path) for i in self.opf.spine]
cover = self.opf.cover cover = self.opf.cover
if os.path.splitext(self.pathtoebook)[1].lower() in \ if os.path.splitext(self.pathtoebook)[1].lower() in \
('.lit', '.mobi', '.prc') and cover: ('.lit', '.mobi', '.prc') and cover:
cfile = os.path.join(os.path.dirname(self.spine[0]), 'calibre_ei_cover.html') cfile = os.path.join(os.path.dirname(self.spine[0]), 'calibre_ei_cover.html')
open(cfile, 'wb').write(TITLEPAGE%cover) open(cfile, 'wb').write(TITLEPAGE%cover)
self.spine[0:0] = [SpineItem(cfile)] self.spine[0:0] = [SpineItem(cfile)]
if self.opf.path_to_html_toc is not None and \ if self.opf.path_to_html_toc is not None and \
self.opf.path_to_html_toc not in self.spine: self.opf.path_to_html_toc not in self.spine:
self.spine.append(SpineItem(self.opf.path_to_html_toc)) self.spine.append(SpineItem(self.opf.path_to_html_toc))
sizes = [i.character_count for i in self.spine] sizes = [i.character_count for i in self.spine]
self.pages = [math.ceil(i/float(self.CHARACTERS_PER_PAGE)) for i in sizes] self.pages = [math.ceil(i/float(self.CHARACTERS_PER_PAGE)) for i in sizes]
for p, s in zip(self.pages, self.spine): for p, s in zip(self.pages, self.spine):
s.pages = p s.pages = p
start = 1 start = 1
for s in self.spine: for s in self.spine:
s.start_page = start s.start_page = start
start += s.pages start += s.pages
s.max_page = s.start_page + s.pages - 1 s.max_page = s.start_page + s.pages - 1
self.toc = self.opf.toc self.toc = self.opf.toc
self.find_embedded_fonts() self.find_embedded_fonts()
self.read_bookmarks() self.read_bookmarks()
return self return self
def parse_bookmarks(self, raw): def parse_bookmarks(self, raw):
for line in raw.splitlines(): for line in raw.splitlines():
if line.count('^') > 0: if line.count('^') > 0:
tokens = line.rpartition('^') tokens = line.rpartition('^')
title, ref = tokens[0], tokens[2] title, ref = tokens[0], tokens[2]
self.bookmarks.append((title, ref)) self.bookmarks.append((title, ref))
def serialize_bookmarks(self, bookmarks): def serialize_bookmarks(self, bookmarks):
dat = [] dat = []
for title, bm in bookmarks: for title, bm in bookmarks:
dat.append(u'%s^%s'%(title, bm)) dat.append(u'%s^%s'%(title, bm))
return (u'\n'.join(dat) +'\n').encode('utf-8') return (u'\n'.join(dat) +'\n').encode('utf-8')
def read_bookmarks(self): def read_bookmarks(self):
self.bookmarks = [] self.bookmarks = []
bmfile = os.path.join(self.base, 'META-INF', 'calibre_bookmarks.txt') bmfile = os.path.join(self.base, 'META-INF', 'calibre_bookmarks.txt')
@ -170,8 +173,8 @@ class EbookIterator(object):
saved = self.config['bookmarks_'+self.pathtoebook] saved = self.config['bookmarks_'+self.pathtoebook]
if saved: if saved:
raw = saved raw = saved
self.parse_bookmarks(raw) self.parse_bookmarks(raw)
def save_bookmarks(self, bookmarks=None): def save_bookmarks(self, bookmarks=None):
if bookmarks is None: if bookmarks is None:
bookmarks = self.bookmarks bookmarks = self.bookmarks
@ -190,7 +193,7 @@ class EbookIterator(object):
zipf.writestr('META-INF/calibre_bookmarks.txt', dat) zipf.writestr('META-INF/calibre_bookmarks.txt', dat)
else: else:
self.config['bookmarks_'+self.pathtoebook] = dat self.config['bookmarks_'+self.pathtoebook] = dat
def add_bookmark(self, bm): def add_bookmark(self, bm):
dups = [] dups = []
for x in self.bookmarks: for x in self.bookmarks:
@ -200,9 +203,9 @@ class EbookIterator(object):
self.bookmarks.remove(x) self.bookmarks.remove(x)
self.bookmarks.append(bm) self.bookmarks.append(bm)
self.save_bookmarks() self.save_bookmarks()
def set_bookmarks(self, bookmarks): def set_bookmarks(self, bookmarks):
self.bookmarks = bookmarks self.bookmarks = bookmarks
def __exit__(self, *args): def __exit__(self, *args):
self._tdir.__exit__(*args) self._tdir.__exit__(*args)

View File

@ -354,7 +354,10 @@ class PreProcessor(object):
(re.compile(r'-\n\r?'), lambda match: ''), (re.compile(r'-\n\r?'), lambda match: ''),
# Remove gray background # Remove gray background
(re.compile(r'<BODY[^<>]+>'), lambda match : '<BODY>') (re.compile(r'<BODY[^<>]+>'), lambda match : '<BODY>'),
# Remove non breaking spaces
(re.compile(ur'\u00a0'), lambda match : ' '),
] ]

View File

@ -11,7 +11,7 @@ from calibre.utils.config import OptionParser
class FetchGoogle(Thread): class FetchGoogle(Thread):
name = 'Google Books' name = 'Google Books'
def __init__(self, title, author, publisher, isbn, verbose): def __init__(self, title, author, publisher, isbn, verbose):
self.title = title self.title = title
self.verbose = verbose self.verbose = verbose
@ -21,17 +21,17 @@ class FetchGoogle(Thread):
Thread.__init__(self, None) Thread.__init__(self, None)
self.daemon = True self.daemon = True
self.exception, self.tb = None, None self.exception, self.tb = None, None
def run(self): def run(self):
from calibre.ebooks.metadata.google_books import search from calibre.ebooks.metadata.google_books import search
try: try:
self.results = search(self.title, self.author, self.publisher, self.results = search(self.title, self.author, self.publisher,
self.isbn, max_results=10, self.isbn, max_results=10,
verbose=self.verbose) verbose=self.verbose)
except Exception, e: except Exception, e:
self.results = [] self.results = []
self.exception = e self.exception = e
self.tb = traceback.format_exc() self.tb = traceback.format_exc()
class FetchISBNDB(Thread): class FetchISBNDB(Thread):
@ -46,19 +46,21 @@ class FetchISBNDB(Thread):
self.daemon = True self.daemon = True
self.exception, self.tb = None, None self.exception, self.tb = None, None
self.key = key self.key = key
def run(self): def run(self):
from calibre.ebooks.metadata.isbndb import option_parser, create_books from calibre.ebooks.metadata.isbndb import option_parser, create_books
args = ['isbndb'] args = ['isbndb']
if self.isbn: if self.isbn:
args.extend(['--isbn', self.isbn]) args.extend(['--isbn', self.isbn])
else: else:
if self.title: if self.title:
args.extend(['--title', self.title]) args.extend(['--title', self.title])
if self.author: if self.author:
args.extend(['--author', self.author]) args.extend(['--author', self.author])
if self.publisher: if self.publisher:
args.extend(['--publisher', self.publisher]) args.extend(['--publisher', self.publisher])
if self.verbose:
args.extend(['--verbose'])
args.append(self.key) args.append(self.key)
try: try:
opts, args = option_parser().parse_args(args) opts, args = option_parser().parse_args(args)
@ -75,7 +77,7 @@ def result_index(source, result):
if x.isbn == result.isbn: if x.isbn == result.isbn:
return i return i
return -1 return -1
def merge_results(one, two): def merge_results(one, two):
for x in two: for x in two:
idx = result_index(one, x) idx = result_index(one, x)
@ -90,42 +92,42 @@ def search(title=None, author=None, publisher=None, isbn=None, isbndb_key=None,
isbn is None) isbn is None)
fetchers = [FetchGoogle(title, author, publisher, isbn, verbose)] fetchers = [FetchGoogle(title, author, publisher, isbn, verbose)]
if isbndb_key: if isbndb_key:
fetchers.append(FetchISBNDB(title, author, publisher, isbn, verbose, fetchers.append(FetchISBNDB(title, author, publisher, isbn, verbose,
isbndb_key)) isbndb_key))
for fetcher in fetchers: for fetcher in fetchers:
fetcher.start() fetcher.start()
for fetcher in fetchers: for fetcher in fetchers:
fetcher.join() fetcher.join()
for fetcher in fetchers[1:]: for fetcher in fetchers[1:]:
merge_results(fetchers[0].results, fetcher.results) merge_results(fetchers[0].results, fetcher.results)
results = sorted(fetchers[0].results, cmp=lambda x, y : cmp( results = sorted(fetchers[0].results, cmp=lambda x, y : cmp(
(x.comments.strip() if x.comments else ''), (x.comments.strip() if x.comments else ''),
(y.comments.strip() if y.comments else '') (y.comments.strip() if y.comments else '')
), reverse=True) ), reverse=True)
return results, [(x.name, x.exception, x.tb) for x in fetchers] return results, [(x.name, x.exception, x.tb) for x in fetchers]
def option_parser(): def option_parser():
parser = OptionParser(textwrap.dedent( parser = OptionParser(textwrap.dedent(
'''\ '''\
%prog [options] %prog [options]
Fetch book metadata from online sources. You must specify at least one Fetch book metadata from online sources. You must specify at least one
of title, author, publisher or ISBN. If you specify ISBN, the others of title, author, publisher or ISBN. If you specify ISBN, the others
are ignored. are ignored.
''' '''
)) ))
parser.add_option('-t', '--title', help='Book title') parser.add_option('-t', '--title', help='Book title')
parser.add_option('-a', '--author', help='Book author(s)') parser.add_option('-a', '--author', help='Book author(s)')
parser.add_option('-p', '--publisher', help='Book publisher') parser.add_option('-p', '--publisher', help='Book publisher')
parser.add_option('-i', '--isbn', help='Book ISBN') parser.add_option('-i', '--isbn', help='Book ISBN')
parser.add_option('-m', '--max-results', default=10, parser.add_option('-m', '--max-results', default=10,
help='Maximum number of results to fetch') help='Maximum number of results to fetch')
parser.add_option('-k', '--isbndb-key', parser.add_option('-k', '--isbndb-key',
help=('The access key for your ISBNDB.com account. ' help=('The access key for your ISBNDB.com account. '
'Only needed if you want to search isbndb.com')) 'Only needed if you want to search isbndb.com'))
parser.add_option('-v', '--verbose', default=0, action='count', parser.add_option('-v', '--verbose', default=0, action='count',
@ -135,19 +137,19 @@ def option_parser():
def main(args=sys.argv): def main(args=sys.argv):
parser = option_parser() parser = option_parser()
opts, args = parser.parse_args(args) opts, args = parser.parse_args(args)
results, exceptions = search(opts.title, opts.author, opts.publisher, results, exceptions = search(opts.title, opts.author, opts.publisher,
opts.isbn, opts.isbndb_key, opts.verbose) opts.isbn, opts.isbndb_key, opts.verbose)
for result in results: for result in results:
print unicode(result).encode(preferred_encoding) print unicode(result).encode(preferred_encoding)
print print
for name, exception, tb in exceptions: for name, exception, tb in exceptions:
if exception is not None: if exception is not None:
print 'WARNING: Fetching from', name, 'failed with error:' print 'WARNING: Fetching from', name, 'failed with error:'
print exception print exception
print tb print tb
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(main()) sys.exit(main())

View File

@ -42,9 +42,9 @@ def report(verbose):
class Query(object): class Query(object):
BASE_URL = 'http://books.google.com/books/feeds/volumes?' BASE_URL = 'http://books.google.com/books/feeds/volumes?'
def __init__(self, title=None, author=None, publisher=None, isbn=None, def __init__(self, title=None, author=None, publisher=None, isbn=None,
max_results=20, min_viewability='none', start_index=1): max_results=20, min_viewability='none', start_index=1):
assert not(title is None and author is None and publisher is None and \ assert not(title is None and author is None and publisher is None and \
@ -60,17 +60,19 @@ class Query(object):
if title is not None: if title is not None:
q += build_term('title', title.split()) q += build_term('title', title.split())
if author is not None: if author is not None:
q += build_term('author', author.split()) q += ('+' if q else '')+build_term('author', author.split())
if publisher is not None: if publisher is not None:
q += build_term('publisher', publisher.split()) q += ('+' if q else '')+build_term('publisher', publisher.split())
if isinstance(q, unicode):
q = q.encode('utf-8')
self.url = self.BASE_URL+urlencode({ self.url = self.BASE_URL+urlencode({
'q':q, 'q':q,
'max-results':max_results, 'max-results':max_results,
'start-index':start_index, 'start-index':start_index,
'min-viewability':min_viewability, 'min-viewability':min_viewability,
}) })
def __call__(self, browser, verbose): def __call__(self, browser, verbose):
if verbose: if verbose:
print 'Query:', self.url print 'Query:', self.url
@ -85,7 +87,7 @@ class Query(object):
class ResultList(list): class ResultList(list):
def get_description(self, entry, verbose): def get_description(self, entry, verbose):
try: try:
desc = description(entry) desc = description(entry)
@ -93,7 +95,7 @@ class ResultList(list):
return 'SUMMARY:\n'+desc[0].text return 'SUMMARY:\n'+desc[0].text
except: except:
report(verbose) report(verbose)
def get_language(self, entry, verbose): def get_language(self, entry, verbose):
try: try:
l = language(entry) l = language(entry)
@ -101,27 +103,27 @@ class ResultList(list):
return l[0].text return l[0].text
except: except:
report(verbose) report(verbose)
def get_title(self, entry): def get_title(self, entry):
candidates = [x.text for x in title(entry)] candidates = [x.text for x in title(entry)]
candidates.sort(cmp=lambda x,y: cmp(len(x), len(y)), reverse=True) candidates.sort(cmp=lambda x,y: cmp(len(x), len(y)), reverse=True)
return candidates[0] return candidates[0]
def get_authors(self, entry): def get_authors(self, entry):
m = creator(entry) m = creator(entry)
if not m: if not m:
m = [] m = []
m = [x.text for x in m] m = [x.text for x in m]
return m return m
def get_author_sort(self, entry, verbose): def get_author_sort(self, entry, verbose):
for x in creator(entry): for x in creator(entry):
for key, val in x.attrib.items(): for key, val in x.attrib.items():
if key.endswith('file-as'): if key.endswith('file-as'):
return val return val
def get_identifiers(self, entry, mi): def get_identifiers(self, entry, mi):
isbns = [] isbns = []
for x in identifier(entry): for x in identifier(entry):
@ -131,7 +133,7 @@ class ResultList(list):
isbns.append(t[5:]) isbns.append(t[5:])
if isbns: if isbns:
mi.isbn = sorted(isbns, cmp=lambda x,y:cmp(len(x), len(y)))[-1] mi.isbn = sorted(isbns, cmp=lambda x,y:cmp(len(x), len(y)))[-1]
def get_tags(self, entry, verbose): def get_tags(self, entry, verbose):
try: try:
tags = [x.text for x in subject(entry)] tags = [x.text for x in subject(entry)]
@ -139,14 +141,14 @@ class ResultList(list):
report(verbose) report(verbose)
tags = [] tags = []
return tags return tags
def get_publisher(self, entry, verbose): def get_publisher(self, entry, verbose):
try: try:
pub = publisher(entry)[0].text pub = publisher(entry)[0].text
except: except:
pub = None pub = None
return pub return pub
def get_date(self, entry, verbose): def get_date(self, entry, verbose):
try: try:
d = date(entry) d = date(entry)
@ -158,7 +160,7 @@ class ResultList(list):
report(verbose) report(verbose)
d = None d = None
return d return d
def populate(self, entries, browser, verbose=False): def populate(self, entries, browser, verbose=False):
for x in entries: for x in entries:
try: try:
@ -175,7 +177,7 @@ class ResultList(list):
if verbose: if verbose:
print 'Failed to get all details for an entry' print 'Failed to get all details for an entry'
print e print e
mi.author_sort = self.get_author_sort(x, verbose) mi.author_sort = self.get_author_sort(x, verbose)
mi.comments = self.get_description(x, verbose) mi.comments = self.get_description(x, verbose)
self.get_identifiers(x, mi) self.get_identifiers(x, mi)
mi.tags = self.get_tags(x, verbose) mi.tags = self.get_tags(x, verbose)
@ -190,14 +192,14 @@ def search(title=None, author=None, publisher=None, isbn=None,
br = browser() br = browser()
start, entries = 1, [] start, entries = 1, []
while start > 0 and len(entries) <= max_results: while start > 0 and len(entries) <= max_results:
new, start = Query(title=title, author=author, publisher=publisher, new, start = Query(title=title, author=author, publisher=publisher,
isbn=isbn, min_viewability=min_viewability)(br, verbose) isbn=isbn, min_viewability=min_viewability)(br, verbose)
if not new: if not new:
break break
entries.extend(new) entries.extend(new)
entries = entries[:max_results] entries = entries[:max_results]
ans = ResultList() ans = ResultList()
ans.populate(entries, br, verbose) ans.populate(entries, br, verbose)
return ans return ans
@ -206,18 +208,18 @@ def option_parser():
parser = OptionParser(textwrap.dedent( parser = OptionParser(textwrap.dedent(
'''\ '''\
%prog [options] %prog [options]
Fetch book metadata from Google. You must specify one of title, author, Fetch book metadata from Google. You must specify one of title, author,
publisher or ISBN. If you specify ISBN the others are ignored. Will publisher or ISBN. If you specify ISBN the others are ignored. Will
fetch a maximum of 100 matches, so you should make your query as fetch a maximum of 100 matches, so you should make your query as
specific as possible. specific as possible.
''' '''
)) ))
parser.add_option('-t', '--title', help='Book title') parser.add_option('-t', '--title', help='Book title')
parser.add_option('-a', '--author', help='Book author(s)') parser.add_option('-a', '--author', help='Book author(s)')
parser.add_option('-p', '--publisher', help='Book publisher') parser.add_option('-p', '--publisher', help='Book publisher')
parser.add_option('-i', '--isbn', help='Book ISBN') parser.add_option('-i', '--isbn', help='Book ISBN')
parser.add_option('-m', '--max-results', default=10, parser.add_option('-m', '--max-results', default=10,
help='Maximum number of results to fetch') help='Maximum number of results to fetch')
parser.add_option('-v', '--verbose', default=0, action='count', parser.add_option('-v', '--verbose', default=0, action='count',
help='Be more verbose about errors') help='Be more verbose about errors')
@ -236,6 +238,6 @@ def main(args=sys.argv):
for result in results: for result in results:
print unicode(result).encode(preferred_encoding) print unicode(result).encode(preferred_encoding)
print print
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(main()) sys.exit(main())

View File

@ -8,7 +8,7 @@ import sys, re, socket
from urllib import urlopen, quote from urllib import urlopen, quote
from calibre.utils.config import OptionParser from calibre.utils.config import OptionParser
from calibre.ebooks.metadata import MetaInformation, authors_to_sort_string from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
BASE_URL = 'http://isbndb.com/api/books.xml?access_key=%(key)s&page_number=1&results=subjects,authors,texts&' BASE_URL = 'http://isbndb.com/api/books.xml?access_key=%(key)s&page_number=1&results=subjects,authors,texts&'
@ -28,7 +28,8 @@ def fetch_metadata(url, max=100, timeout=5.):
raw = urlopen(url).read() raw = urlopen(url).read()
except Exception, err: except Exception, err:
raise ISBNDBError('Could not fetch ISBNDB metadata. Error: '+str(err)) raise ISBNDBError('Could not fetch ISBNDB metadata. Error: '+str(err))
soup = BeautifulStoneSoup(raw) soup = BeautifulStoneSoup(raw,
convertEntities=BeautifulStoneSoup.XML_ENTITIES)
book_list = soup.find('booklist') book_list = soup.find('booklist')
if book_list is None: if book_list is None:
errmsg = soup.find('errormessage').string errmsg = soup.find('errormessage').string
@ -41,13 +42,13 @@ def fetch_metadata(url, max=100, timeout=5.):
return books return books
finally: finally:
socket.setdefaulttimeout(timeout) socket.setdefaulttimeout(timeout)
class ISBNDBMetadata(MetaInformation): class ISBNDBMetadata(MetaInformation):
def __init__(self, book): def __init__(self, book):
MetaInformation.__init__(self, None, []) MetaInformation.__init__(self, None, [])
self.isbn = book['isbn'] self.isbn = book['isbn']
self.title = book.find('titlelong').string self.title = book.find('titlelong').string
if not self.title: if not self.title:
@ -59,7 +60,7 @@ class ISBNDBMetadata(MetaInformation):
for au in temp: for au in temp:
if not au: continue if not au: continue
self.authors.extend([a.strip() for a in au.split('&amp;')]) self.authors.extend([a.strip() for a in au.split('&amp;')])
try: try:
self.author_sort = book.find('authors').find('person').string self.author_sort = book.find('authors').find('person').string
if self.authors and self.author_sort == self.authors[0]: if self.authors and self.author_sort == self.authors[0]:
@ -67,12 +68,12 @@ class ISBNDBMetadata(MetaInformation):
except: except:
pass pass
self.publisher = book.find('publishertext').string self.publisher = book.find('publishertext').string
summ = book.find('summary') summ = book.find('summary')
if summ and hasattr(summ, 'string') and summ.string: if summ and hasattr(summ, 'string') and summ.string:
self.comments = 'SUMMARY:\n'+summ.string self.comments = 'SUMMARY:\n'+summ.string
def build_isbn(base_url, opts): def build_isbn(base_url, opts):
return base_url + 'index1=isbn&value1='+opts.isbn return base_url + 'index1=isbn&value1='+opts.isbn
@ -85,11 +86,11 @@ def build_combined(base_url, opts):
query = query.strip() query = query.strip()
if len(query) == 0: if len(query) == 0:
raise ISBNDBError('You must specify at least one of --author, --title or --publisher') raise ISBNDBError('You must specify at least one of --author, --title or --publisher')
query = re.sub(r'\s+', '+', query) query = re.sub(r'\s+', '+', query)
if isinstance(query, unicode): if isinstance(query, unicode):
query = query.encode('utf-8') query = query.encode('utf-8')
return base_url+'index1=combined&value1='+quote(query, '+') return base_url+'index1=combined&value1='+quote(query, '+')
def option_parser(): def option_parser():
@ -97,7 +98,7 @@ def option_parser():
_(''' _('''
%prog [options] key %prog [options] key
Fetch metadata for books from isndb.com. You can specify either the Fetch metadata for books from isndb.com. You can specify either the
books ISBN ID or its title and author. If you specify the title and author, books ISBN ID or its title and author. If you specify the title and author,
then more than one book may be returned. then more than one book may be returned.
@ -112,11 +113,11 @@ key is the account key you generate after signing up for a free account from isb
default=None, help=_('The title of the book to search for.')) default=None, help=_('The title of the book to search for.'))
parser.add_option('-p', '--publisher', default=None, dest='publisher', parser.add_option('-p', '--publisher', default=None, dest='publisher',
help=_('The publisher of the book to search for.')) help=_('The publisher of the book to search for.'))
parser.add_option('-v', '--verbose', default=False, parser.add_option('-v', '--verbose', default=False,
action='store_true', help=_('Verbose processing')) action='store_true', help=_('Verbose processing'))
return parser return parser
def create_books(opts, args, timeout=5.): def create_books(opts, args, timeout=5.):
base_url = BASE_URL%dict(key=args[1]) base_url = BASE_URL%dict(key=args[1])
@ -124,10 +125,10 @@ def create_books(opts, args, timeout=5.):
url = build_isbn(base_url, opts) url = build_isbn(base_url, opts)
else: else:
url = build_combined(base_url, opts) url = build_combined(base_url, opts)
if opts.verbose: if opts.verbose:
print ('ISBNDB query: '+url) print ('ISBNDB query: '+url)
return [ISBNDBMetadata(book) for book in fetch_metadata(url, timeout=timeout)] return [ISBNDBMetadata(book) for book in fetch_metadata(url, timeout=timeout)]
def main(args=sys.argv): def main(args=sys.argv):
@ -137,10 +138,10 @@ def main(args=sys.argv):
parser.print_help() parser.print_help()
print ('You must supply the isbndb.com key') print ('You must supply the isbndb.com key')
return 1 return 1
for book in create_books(opts, args): for book in create_books(opts, args):
print unicode(book).encode('utf-8') print unicode(book).encode('utf-8')
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -9,7 +9,7 @@ lxml based OPF parser.
import sys, unittest, functools, os, mimetypes, uuid, glob, cStringIO import sys, unittest, functools, os, mimetypes, uuid, glob, cStringIO
from urllib import unquote from urllib import unquote
from urlparse import urlparse from urlparse import urlparse, urldefrag
from lxml import etree from lxml import etree
from dateutil import parser from dateutil import parser
@ -444,7 +444,7 @@ class OPF(object):
if not hasattr(stream, 'read'): if not hasattr(stream, 'read'):
stream = open(stream, 'rb') stream = open(stream, 'rb')
self.basedir = self.base_dir = basedir self.basedir = self.base_dir = basedir
self.path_to_html_toc = None self.path_to_html_toc = self.html_toc_fragment = None
raw, self.encoding = xml_to_unicode(stream.read(), strip_encoding_pats=True, resolve_entities=True) raw, self.encoding = xml_to_unicode(stream.read(), strip_encoding_pats=True, resolve_entities=True)
raw = raw[raw.find('<'):] raw = raw[raw.find('<'):]
self.root = etree.fromstring(raw, self.PARSER) self.root = etree.fromstring(raw, self.PARSER)
@ -496,7 +496,8 @@ class OPF(object):
if f: if f:
self.toc.read_ncx_toc(f[0]) self.toc.read_ncx_toc(f[0])
else: else:
self.path_to_html_toc = toc self.path_to_html_toc, self.html_toc_fragment = \
toc.partition('#')[0], toc.partition('#')[-1]
self.toc.read_html_toc(toc) self.toc.read_html_toc(toc)
except: except:
pass pass
@ -627,7 +628,7 @@ class OPF(object):
attrib = {'{%s}role'%self.NAMESPACES['opf']: 'aut'} attrib = {'{%s}role'%self.NAMESPACES['opf']: 'aut'}
elem = self.create_metadata_element('creator', attrib=attrib) elem = self.create_metadata_element('creator', attrib=attrib)
self.set_text(elem, author.strip()) self.set_text(elem, author.strip())
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
@dynamic_property @dynamic_property

View File

@ -9,6 +9,7 @@ import struct, os, cStringIO, re, functools
try: try:
from PIL import Image as PILImage from PIL import Image as PILImage
PILImage
except ImportError: except ImportError:
import Image as PILImage import Image as PILImage
@ -27,7 +28,7 @@ from calibre.ebooks.metadata.toc import TOC
from calibre import sanitize_file_name from calibre import sanitize_file_name
class EXTHHeader(object): class EXTHHeader(object):
def __init__(self, raw, codec, title): def __init__(self, raw, codec, title):
self.doctype = raw[:4] self.doctype = raw[:4]
self.length, self.num_items = struct.unpack('>LL', raw[4:12]) self.length, self.num_items = struct.unpack('>LL', raw[4:12])
@ -36,7 +37,7 @@ class EXTHHeader(object):
self.mi = MetaInformation(_('Unknown'), [_('Unknown')]) self.mi = MetaInformation(_('Unknown'), [_('Unknown')])
self.has_fake_cover = True self.has_fake_cover = True
left = self.num_items left = self.num_items
while left > 0: while left > 0:
left -= 1 left -= 1
id, size = struct.unpack('>LL', raw[pos:pos+8]) id, size = struct.unpack('>LL', raw[pos:pos+8])
@ -45,18 +46,20 @@ class EXTHHeader(object):
if id >= 100 and id < 200: if id >= 100 and id < 200:
self.process_metadata(id, content, codec) self.process_metadata(id, content, codec)
elif id == 203: elif id == 203:
self.has_fake_cover = bool(struct.unpack('>L', content)[0]) self.has_fake_cover = bool(struct.unpack('>L', content)[0])
elif id == 201: elif id == 201:
co, = struct.unpack('>L', content) co, = struct.unpack('>L', content)
if co < 1e7: if co < 1e7:
self.cover_offset = co self.cover_offset = co
elif id == 202: elif id == 202:
self.thumbnail_offset, = struct.unpack('>L', content) self.thumbnail_offset, = struct.unpack('>L', content)
elif id == 503 and (not title or title == _('Unknown')):
title = content
#else: #else:
# print 'unknown record', id, repr(content) # print 'unknown record', id, repr(content)
if title: if title:
self.mi.title = title self.mi.title = title
def process_metadata(self, id, content, codec): def process_metadata(self, id, content, codec):
if id == 100: if id == 100:
if self.mi.authors == [_('Unknown')]: if self.mi.authors == [_('Unknown')]:
@ -73,11 +76,11 @@ class EXTHHeader(object):
self.mi.tags = [] self.mi.tags = []
self.mi.tags.append(content.decode(codec, 'ignore')) self.mi.tags.append(content.decode(codec, 'ignore'))
#else: #else:
# print 'unhandled metadata record', id, repr(content), codec # print 'unhandled metadata record', id, repr(content), codec
class BookHeader(object): class BookHeader(object):
def __init__(self, raw, ident, user_encoding, log): def __init__(self, raw, ident, user_encoding, log):
self.log = log self.log = log
self.compression_type = raw[:2] self.compression_type = raw[:2]
@ -99,8 +102,8 @@ class BookHeader(object):
self.doctype = raw[16:20] self.doctype = raw[16:20]
self.length, self.type, self.codepage, self.unique_id, \ self.length, self.type, self.codepage, self.unique_id, \
self.version = struct.unpack('>LLLLL', raw[20:40]) self.version = struct.unpack('>LLLLL', raw[20:40])
try: try:
self.codec = { self.codec = {
1252 : 'cp1252', 1252 : 'cp1252',
@ -110,15 +113,14 @@ class BookHeader(object):
self.codec = 'cp1252' if user_encoding is None else user_encoding self.codec = 'cp1252' if user_encoding is None else user_encoding
log.warn('Unknown codepage %d. Assuming %s'%(self.codepage, log.warn('Unknown codepage %d. Assuming %s'%(self.codepage,
self.codec)) self.codec))
if ident == 'TEXTREAD' or self.length < 0xE4 or 0xE8 < self.length: if ident == 'TEXTREAD' or self.length < 0xE4 or 0xE8 < self.length:
self.extra_flags = 0 self.extra_flags = 0
else: else:
self.extra_flags, = struct.unpack('>H', raw[0xF2:0xF4]) self.extra_flags, = struct.unpack('>H', raw[0xF2:0xF4])
if self.compression_type == 'DH': if self.compression_type == 'DH':
self.huff_offset, self.huff_number = struct.unpack('>LL', raw[0x70:0x78]) self.huff_offset, self.huff_number = struct.unpack('>LL', raw[0x70:0x78])
toff, tlen = struct.unpack('>II', raw[0x54:0x5c]) toff, tlen = struct.unpack('>II', raw[0x54:0x5c])
tend = toff + tlen tend = toff + tlen
self.title = raw[toff:tend] if tend < len(raw) else _('Unknown') self.title = raw[toff:tend] if tend < len(raw) else _('Unknown')
@ -129,7 +131,7 @@ class BookHeader(object):
self.sublanguage = sub_language.get(sublangid, 'NEUTRAL') self.sublanguage = sub_language.get(sublangid, 'NEUTRAL')
self.mobi_version = struct.unpack('>I', raw[0x68:0x6c])[0] self.mobi_version = struct.unpack('>I', raw[0x68:0x6c])[0]
self.first_image_index = struct.unpack('>L', raw[0x6c:0x6c+4])[0] self.first_image_index = struct.unpack('>L', raw[0x6c:0x6c+4])[0]
self.exth_flag, = struct.unpack('>L', raw[0x80:0x84]) self.exth_flag, = struct.unpack('>L', raw[0x80:0x84])
self.exth = None self.exth = None
if not isinstance(self.title, unicode): if not isinstance(self.title, unicode):
@ -138,54 +140,54 @@ class BookHeader(object):
self.exth = EXTHHeader(raw[16+self.length:], self.codec, self.title) self.exth = EXTHHeader(raw[16+self.length:], self.codec, self.title)
self.exth.mi.uid = self.unique_id self.exth.mi.uid = self.unique_id
self.exth.mi.language = self.language self.exth.mi.language = self.language
class MobiReader(object): class MobiReader(object):
PAGE_BREAK_PAT = re.compile(r'(<[/]{0,1}mbp:pagebreak\s*[/]{0,1}>)+', re.IGNORECASE) PAGE_BREAK_PAT = re.compile(r'(<[/]{0,1}mbp:pagebreak\s*[/]{0,1}>)+', re.IGNORECASE)
IMAGE_ATTRS = ('lowrecindex', 'recindex', 'hirecindex') IMAGE_ATTRS = ('lowrecindex', 'recindex', 'hirecindex')
def __init__(self, filename_or_stream, log, user_encoding=None, debug=None): def __init__(self, filename_or_stream, log, user_encoding=None, debug=None):
self.log = log self.log = log
self.debug = debug self.debug = debug
self.embedded_mi = None self.embedded_mi = None
self.base_css_rules = ''' self.base_css_rules = '''
blockquote { margin: 0em 0em 0em 1.25em; text-align: justify } blockquote { margin: 0em 0em 0em 1.25em; text-align: justify }
p { margin: 0em; text-align: justify } p { margin: 0em; text-align: justify }
.bold { font-weight: bold } .bold { font-weight: bold }
.italic { font-style: italic } .italic { font-style: italic }
.mbp_pagebreak { .mbp_pagebreak {
page-break-after: always; margin: 0; display: block page-break-after: always; margin: 0; display: block
} }
''' '''
self.tag_css_rules = [] self.tag_css_rules = []
if hasattr(filename_or_stream, 'read'): if hasattr(filename_or_stream, 'read'):
stream = filename_or_stream stream = filename_or_stream
stream.seek(0) stream.seek(0)
else: else:
stream = open(filename_or_stream, 'rb') stream = open(filename_or_stream, 'rb')
raw = stream.read() raw = stream.read()
self.header = raw[0:72] self.header = raw[0:72]
self.name = self.header[:32].replace('\x00', '') self.name = self.header[:32].replace('\x00', '')
self.num_sections, = struct.unpack('>H', raw[76:78]) self.num_sections, = struct.unpack('>H', raw[76:78])
self.ident = self.header[0x3C:0x3C+8].upper() self.ident = self.header[0x3C:0x3C+8].upper()
if self.ident not in ['BOOKMOBI', 'TEXTREAD']: if self.ident not in ['BOOKMOBI', 'TEXTREAD']:
raise MobiError('Unknown book type: %s'%self.ident) raise MobiError('Unknown book type: %s'%self.ident)
self.sections = [] self.sections = []
self.section_headers = [] self.section_headers = []
for i in range(self.num_sections): for i in range(self.num_sections):
offset, a1, a2, a3, a4 = struct.unpack('>LBBBB', raw[78+i*8:78+i*8+8]) offset, a1, a2, a3, a4 = struct.unpack('>LBBBB', raw[78+i*8:78+i*8+8])
flags, val = a1, a2<<16 | a3<<8 | a4 flags, val = a1, a2<<16 | a3<<8 | a4
self.section_headers.append((offset, flags, val)) self.section_headers.append((offset, flags, val))
def section(section_number): def section(section_number):
if section_number == self.num_sections - 1: if section_number == self.num_sections - 1:
end_off = len(raw) end_off = len(raw)
@ -193,20 +195,20 @@ class MobiReader(object):
end_off = self.section_headers[section_number + 1][0] end_off = self.section_headers[section_number + 1][0]
off = self.section_headers[section_number][0] off = self.section_headers[section_number][0]
return raw[off:end_off] return raw[off:end_off]
for i in range(self.num_sections): for i in range(self.num_sections):
self.sections.append((section(i), self.section_headers[i])) self.sections.append((section(i), self.section_headers[i]))
self.book_header = BookHeader(self.sections[0][0], self.ident, self.book_header = BookHeader(self.sections[0][0], self.ident,
user_encoding, self.log) user_encoding, self.log)
self.name = self.name.decode(self.book_header.codec, 'replace') self.name = self.name.decode(self.book_header.codec, 'replace')
def extract_content(self, output_dir, parse_cache): def extract_content(self, output_dir, parse_cache):
output_dir = os.path.abspath(output_dir) output_dir = os.path.abspath(output_dir)
if self.book_header.encryption_type != 0: if self.book_header.encryption_type != 0:
raise DRMError(self.name) raise DRMError(self.name)
processed_records = self.extract_text() processed_records = self.extract_text()
if self.debug is not None: if self.debug is not None:
self.parse_cache['calibre_raw_mobi_markup'] = self.mobi_html self.parse_cache['calibre_raw_mobi_markup'] = self.mobi_html
@ -215,14 +217,14 @@ class MobiReader(object):
'ignore') 'ignore')
for pat in ENCODING_PATS: for pat in ENCODING_PATS:
self.processed_html = pat.sub('', self.processed_html) self.processed_html = pat.sub('', self.processed_html)
e2u = functools.partial(entity_to_unicode, e2u = functools.partial(entity_to_unicode,
exceptions=['lt', 'gt', 'amp', 'apos', 'quot']) exceptions=['lt', 'gt', 'amp', 'apos', 'quot'])
self.processed_html = re.sub(r'&(\S+?);', e2u, self.processed_html = re.sub(r'&(\S+?);', e2u,
self.processed_html) self.processed_html)
self.extract_images(processed_records, output_dir) self.extract_images(processed_records, output_dir)
self.replace_page_breaks() self.replace_page_breaks()
self.cleanup_html() self.cleanup_html()
if self.processed_html.startswith('<body'): if self.processed_html.startswith('<body'):
self.processed_html = '<html><head></head>'+self.processed_html+'</html>' self.processed_html = '<html><head></head>'+self.processed_html+'</html>'
self.processed_html = \ self.processed_html = \
@ -230,7 +232,7 @@ class MobiReader(object):
'\n<head>\n' '\n<head>\n'
'\t<link type="text/css" href="styles.css" />\n', '\t<link type="text/css" href="styles.css" />\n',
self.processed_html) self.processed_html)
self.log.debug('Parsing HTML...') self.log.debug('Parsing HTML...')
root = html.fromstring(self.processed_html) root = html.fromstring(self.processed_html)
self.upshift_markup(root) self.upshift_markup(root)
@ -241,7 +243,7 @@ class MobiReader(object):
self.read_embedded_metadata(root, metadata_elems[0], guide) self.read_embedded_metadata(root, metadata_elems[0], guide)
for elem in guides + metadata_elems: for elem in guides + metadata_elems:
elem.getparent().remove(elem) elem.getparent().remove(elem)
htmlfile = os.path.join(output_dir, htmlfile = os.path.join(output_dir,
sanitize_file_name(self.name)+'.html') sanitize_file_name(self.name)+'.html')
try: try:
for ref in guide.xpath('descendant::reference'): for ref in guide.xpath('descendant::reference'):
@ -254,19 +256,30 @@ class MobiReader(object):
self.log.debug('Creating OPF...') self.log.debug('Creating OPF...')
ncx = cStringIO.StringIO() ncx = cStringIO.StringIO()
opf = self.create_opf(htmlfile, guide, root) opf = self.create_opf(htmlfile, guide, root)
self.created_opf_path = os.path.splitext(htmlfile)[0]+'.opf' self.created_opf_path = os.path.splitext(htmlfile)[0]+'.opf'
opf.render(open(self.created_opf_path, 'wb'), ncx) opf.render(open(self.created_opf_path, 'wb'), ncx)
ncx = ncx.getvalue() ncx = ncx.getvalue()
if ncx: if ncx:
open(os.path.splitext(htmlfile)[0]+'.ncx', 'wb').write(ncx) open(os.path.splitext(htmlfile)[0]+'.ncx', 'wb').write(ncx)
with open('styles.css', 'wb') as s: with open('styles.css', 'wb') as s:
s.write(self.base_css_rules+'\n\n') s.write(self.base_css_rules+'\n\n')
for rule in self.tag_css_rules: for rule in self.tag_css_rules:
if isinstance(rule, unicode): if isinstance(rule, unicode):
rule = rule.encode('utf-8') rule = rule.encode('utf-8')
s.write(rule+'\n\n') s.write(rule+'\n\n')
if self.book_header.exth is not None or self.embedded_mi is not None:
if self.verbose:
print 'Creating OPF...'
ncx = cStringIO.StringIO()
opf = self.create_opf(htmlfile, guide, root)
opf.render(open(os.path.splitext(htmlfile)[0]+'.opf', 'wb'), ncx)
ncx = ncx.getvalue()
if ncx:
open(os.path.splitext(htmlfile)[0]+'.ncx', 'wb').write(ncx)
def read_embedded_metadata(self, root, elem, guide): def read_embedded_metadata(self, root, elem, guide):
raw = '<package>'+html.tostring(elem, encoding='utf-8')+'</package>' raw = '<package>'+html.tostring(elem, encoding='utf-8')+'</package>'
stream = cStringIO.StringIO(raw) stream = cStringIO.StringIO(raw)
@ -291,7 +304,7 @@ class MobiReader(object):
elem.getparent().remove(elem) elem.getparent().remove(elem)
break break
break break
def cleanup_html(self): def cleanup_html(self):
self.log.debug('Cleaning up HTML...') self.log.debug('Cleaning up HTML...')
self.processed_html = re.sub(r'<div height="0(pt|px|ex|em|%){0,1}"></div>', '', self.processed_html) self.processed_html = re.sub(r'<div height="0(pt|px|ex|em|%){0,1}"></div>', '', self.processed_html)
@ -299,7 +312,7 @@ class MobiReader(object):
self.processed_html = '<html><p>'+self.processed_html.replace('\n\n', '<p>')+'</html>' self.processed_html = '<html><p>'+self.processed_html.replace('\n\n', '<p>')+'</html>'
self.processed_html = self.processed_html.replace('\r\n', '\n') self.processed_html = self.processed_html.replace('\r\n', '\n')
self.processed_html = self.processed_html.replace('> <', '>\n<') self.processed_html = self.processed_html.replace('> <', '>\n<')
def upshift_markup(self, root): def upshift_markup(self, root):
self.log.debug('Converting style information to CSS...') self.log.debug('Converting style information to CSS...')
size_map = { size_map = {
@ -366,22 +379,22 @@ class MobiReader(object):
elif tag.tag == 'pre': elif tag.tag == 'pre':
if not tag.text: if not tag.text:
tag.tag = 'div' tag.tag = 'div'
if 'filepos-id' in attrib: if 'filepos-id' in attrib:
attrib['id'] = attrib.pop('filepos-id') attrib['id'] = attrib.pop('filepos-id')
if 'name' in attrib and attrib['name'] != attrib['id']:
attrib['name'] = attrib['id']
if 'filepos' in attrib: if 'filepos' in attrib:
filepos = attrib.pop('filepos') filepos = attrib.pop('filepos')
try: try:
attrib['href'] = "#filepos%d" % int(filepos) attrib['href'] = "#filepos%d" % int(filepos)
except ValueError: except ValueError:
pass pass
if styles: if styles:
attrib['id'] = attrib.get('id', 'calibre_mr_gid%d'%i) attrib['id'] = attrib.get('id', 'calibre_mr_gid%d'%i)
self.tag_css_rules.append('#%s {%s}'%(attrib['id'], self.tag_css_rules.append('#%s {%s}'%(attrib['id'],
'; '.join(styles))) '; '.join(styles)))
def create_opf(self, htmlfile, guide=None, root=None): def create_opf(self, htmlfile, guide=None, root=None):
mi = getattr(self.book_header.exth, 'mi', self.embedded_mi) mi = getattr(self.book_header.exth, 'mi', self.embedded_mi)
if mi is None: if mi is None:
@ -402,7 +415,7 @@ class MobiReader(object):
bp = os.path.dirname(htmlfile) bp = os.path.dirname(htmlfile)
for i in getattr(self, 'image_names', []): for i in getattr(self, 'image_names', []):
manifest.append((os.path.join(bp, 'images/', i), 'image/jpeg')) manifest.append((os.path.join(bp, 'images/', i), 'image/jpeg'))
opf.create_manifest(manifest) opf.create_manifest(manifest)
opf.create_spine([os.path.basename(htmlfile)]) opf.create_spine([os.path.basename(htmlfile)])
toc = None toc = None
@ -431,16 +444,16 @@ class MobiReader(object):
except: except:
text = '' text = ''
text = ent_pat.sub(entity_to_unicode, text) text = ent_pat.sub(entity_to_unicode, text)
tocobj.add_item(toc.partition('#')[0], href[1:], tocobj.add_item(toc.partition('#')[0], href[1:],
text) text)
if reached and x.get('class', None) == 'mbp_pagebreak': if reached and x.get('class', None) == 'mbp_pagebreak':
break break
if tocobj is not None: if tocobj is not None:
opf.set_toc(tocobj) opf.set_toc(tocobj)
return opf return opf
def sizeof_trailing_entries(self, data): def sizeof_trailing_entries(self, data):
def sizeof_trailing_entry(ptr, psize): def sizeof_trailing_entry(ptr, psize):
bitpos, result = 0, 0 bitpos, result = 0, 0
@ -451,7 +464,7 @@ class MobiReader(object):
psize -= 1 psize -= 1
if (v & 0x80) != 0 or (bitpos >= 28) or (psize == 0): if (v & 0x80) != 0 or (bitpos >= 28) or (psize == 0):
return result return result
num = 0 num = 0
size = len(data) size = len(data)
flags = self.book_header.extra_flags >> 1 flags = self.book_header.extra_flags >> 1
@ -467,27 +480,27 @@ class MobiReader(object):
data = self.sections[index][0] data = self.sections[index][0]
trail_size = self.sizeof_trailing_entries(data) trail_size = self.sizeof_trailing_entries(data)
return data[:len(data)-trail_size] return data[:len(data)-trail_size]
def extract_text(self): def extract_text(self):
self.log.debug('Extracting text...') self.log.debug('Extracting text...')
text_sections = [self.text_section(i) for i in range(1, self.book_header.records+1)] text_sections = [self.text_section(i) for i in range(1, self.book_header.records+1)]
processed_records = list(range(0, self.book_header.records+1)) processed_records = list(range(0, self.book_header.records+1))
self.mobi_html = '' self.mobi_html = ''
if self.book_header.compression_type == 'DH': if self.book_header.compression_type == 'DH':
huffs = [self.sections[i][0] for i in huffs = [self.sections[i][0] for i in
range(self.book_header.huff_offset, range(self.book_header.huff_offset,
self.book_header.huff_offset+self.book_header.huff_number)] self.book_header.huff_offset+self.book_header.huff_number)]
processed_records += list(range(self.book_header.huff_offset, processed_records += list(range(self.book_header.huff_offset,
self.book_header.huff_offset+self.book_header.huff_number)) self.book_header.huff_offset+self.book_header.huff_number))
huff = HuffReader(huffs) huff = HuffReader(huffs)
self.mobi_html = huff.decompress(text_sections) self.mobi_html = huff.decompress(text_sections)
elif self.book_header.compression_type == '\x00\x02': elif self.book_header.compression_type == '\x00\x02':
for section in text_sections: for section in text_sections:
self.mobi_html += decompress_doc(section) self.mobi_html += decompress_doc(section)
elif self.book_header.compression_type == '\x00\x01': elif self.book_header.compression_type == '\x00\x01':
self.mobi_html = ''.join(text_sections) self.mobi_html = ''.join(text_sections)
else: else:
@ -495,13 +508,13 @@ class MobiReader(object):
if self.book_header.ancient and '<html' not in self.mobi_html[:300].lower(): if self.book_header.ancient and '<html' not in self.mobi_html[:300].lower():
self.mobi_html = self.mobi_html.replace('\r ', '\n\n ') self.mobi_html = self.mobi_html.replace('\r ', '\n\n ')
return processed_records return processed_records
def replace_page_breaks(self): def replace_page_breaks(self):
self.processed_html = self.PAGE_BREAK_PAT.sub( self.processed_html = self.PAGE_BREAK_PAT.sub(
'<div class="mbp_pagebreak" />', '<div class="mbp_pagebreak" />',
self.processed_html) self.processed_html)
def add_anchors(self): def add_anchors(self):
self.log.debug('Adding anchors...') self.log.debug('Adding anchors...')
positions = set([]) positions = set([])
@ -530,8 +543,8 @@ class MobiReader(object):
self.processed_html += self.mobi_html[pos:end] + (anchor % oend) self.processed_html += self.mobi_html[pos:end] + (anchor % oend)
pos = end pos = end
self.processed_html += self.mobi_html[pos:] self.processed_html += self.mobi_html[pos:]
def extract_images(self, processed_records, output_dir): def extract_images(self, processed_records, output_dir):
self.log.debug('Extracting images...') self.log.debug('Extracting images...')
output_dir = os.path.abspath(os.path.join(output_dir, 'images')) output_dir = os.path.abspath(os.path.join(output_dir, 'images'))
@ -541,7 +554,7 @@ class MobiReader(object):
self.image_names = [] self.image_names = []
start = getattr(self.book_header, 'first_image_index', -1) start = getattr(self.book_header, 'first_image_index', -1)
if start > self.num_sections or start < 0: if start > self.num_sections or start < 0:
# BAEN PRC files have bad headers # BAEN PRC files have bad headers
start=0 start=0
for i in range(start, self.num_sections): for i in range(start, self.num_sections):
if i in processed_records: if i in processed_records:
@ -551,10 +564,10 @@ class MobiReader(object):
buf = cStringIO.StringIO(data) buf = cStringIO.StringIO(data)
image_index += 1 image_index += 1
try: try:
im = PILImage.open(buf) im = PILImage.open(buf)
except IOError: except IOError:
continue continue
path = os.path.join(output_dir, '%05d.jpg'%image_index) path = os.path.join(output_dir, '%05d.jpg'%image_index)
self.image_names.append(os.path.basename(path)) self.image_names.append(os.path.basename(path))
im.convert('RGB').save(open(path, 'wb'), format='JPEG') im.convert('RGB').save(open(path, 'wb'), format='JPEG')
@ -583,3 +596,4 @@ def get_metadata(stream):
log.exception() log.exception()
return mi return mi

View File

@ -132,7 +132,7 @@ def rescale_image(data, maxsizeb, dimen=None):
class Serializer(object): class Serializer(object):
NSRMAP = {'': None, XML_NS: 'xml', XHTML_NS: '', MBP_NS: 'mbp'} NSRMAP = {'': None, XML_NS: 'xml', XHTML_NS: '', MBP_NS: 'mbp'}
def __init__(self, oeb, images): def __init__(self, oeb, images):
self.oeb = oeb self.oeb = oeb
self.images = images self.images = images
@ -192,7 +192,7 @@ class Serializer(object):
self.href_offsets[href].append(buffer.tell()) self.href_offsets[href].append(buffer.tell())
buffer.write('0000000000') buffer.write('0000000000')
return True return True
def serialize_body(self): def serialize_body(self):
buffer = self.buffer buffer = self.buffer
self.anchor_offset = buffer.tell() self.anchor_offset = buffer.tell()
@ -305,7 +305,7 @@ class MobiFlattener(object):
fbase=fbase, fkey=fkey, unfloat=True, untable=True) fbase=fbase, fkey=fkey, unfloat=True, untable=True)
return flattener(oeb, context) return flattener(oeb, context)
class MobiWriter(object): class MobiWriter(object):
COLLAPSE_RE = re.compile(r'[ \t\r\n\v]+') COLLAPSE_RE = re.compile(r'[ \t\r\n\v]+')
@ -313,7 +313,7 @@ class MobiWriter(object):
TRANSFORMS = [HTMLTOCAdder, CaseMangler, MobiFlattener(), SVGRasterizer, TRANSFORMS = [HTMLTOCAdder, CaseMangler, MobiFlattener(), SVGRasterizer,
ManifestTrimmer, MobiMLizer] ManifestTrimmer, MobiMLizer]
def __init__(self, compression=None, imagemax=None, def __init__(self, compression=None, imagemax=None,
prefer_author_sort=False): prefer_author_sort=False):
self._compression = compression or UNCOMPRESSED self._compression = compression or UNCOMPRESSED
@ -329,7 +329,7 @@ class MobiWriter(object):
mobi('compress', ['--compress'], default=False, mobi('compress', ['--compress'], default=False,
help=_('Compress file text using PalmDOC compression. ' help=_('Compress file text using PalmDOC compression. '
'Results in smaller files, but takes a long time to run.')) 'Results in smaller files, but takes a long time to run.'))
mobi('rescale_images', ['--rescale-images'], default=False, mobi('rescale_images', ['--rescale-images'], default=False,
help=_('Modify images to meet Palm device size limitations.')) help=_('Modify images to meet Palm device size limitations.'))
mobi('prefer_author_sort', ['--prefer-author-sort'], default=False, mobi('prefer_author_sort', ['--prefer-author-sort'], default=False,
help=_('When present, use the author sorting information for ' help=_('When present, use the author sorting information for '
@ -344,17 +344,17 @@ class MobiWriter(object):
prefer_author_sort = opts.prefer_author_sort prefer_author_sort = opts.prefer_author_sort
return cls(compression=compression, imagemax=imagemax, return cls(compression=compression, imagemax=imagemax,
prefer_author_sort=prefer_author_sort) prefer_author_sort=prefer_author_sort)
def __call__(self, oeb, path): def __call__(self, oeb, path):
if hasattr(path, 'write'): if hasattr(path, 'write'):
return self._dump_stream(oeb, path) return self._dump_stream(oeb, path)
with open(path, 'w+b') as stream: with open(path, 'w+b') as stream:
return self._dump_stream(oeb, stream) return self._dump_stream(oeb, stream)
def _write(self, *data): def _write(self, *data):
for datum in data: for datum in data:
self._stream.write(datum) self._stream.write(datum)
def _tell(self): def _tell(self):
return self._stream.tell() return self._stream.tell()
@ -409,7 +409,7 @@ class MobiWriter(object):
overlap = text.read(extra) overlap = text.read(extra)
text.seek(npos) text.seek(npos)
return data, overlap return data, overlap
def _generate_text(self): def _generate_text(self):
self._oeb.logger.info('Serializing markup content...') self._oeb.logger.info('Serializing markup content...')
serializer = Serializer(self._oeb, self._images) serializer = Serializer(self._oeb, self._images)
@ -450,7 +450,7 @@ class MobiWriter(object):
offset += RECORD_SIZE offset += RECORD_SIZE
data, overlap = self._read_text_record(text) data, overlap = self._read_text_record(text)
self._text_nrecords = nrecords self._text_nrecords = nrecords
def _generate_images(self): def _generate_images(self):
self._oeb.logger.info('Serializing images...') self._oeb.logger.info('Serializing images...')
images = [(index, href) for href, index in self._images.items()] images = [(index, href) for href, index in self._images.items()]
@ -463,7 +463,7 @@ class MobiWriter(object):
self._oeb.logger.warn('Bad image file %r' % item.href) self._oeb.logger.warn('Bad image file %r' % item.href)
continue continue
self._records.append(data) self._records.append(data)
def _generate_record0(self): def _generate_record0(self):
metadata = self._oeb.metadata metadata = self._oeb.metadata
exth = self._build_exth() exth = self._build_exth()
@ -555,7 +555,7 @@ class MobiWriter(object):
self._images[href] = index self._images[href] = index
self._records.append(data) self._records.append(data)
return index return index
def _write_header(self): def _write_header(self):
title = str(self._oeb.metadata.title[0]) title = str(self._oeb.metadata.title[0])
title = re.sub('[^-A-Za-z0-9]+', '_', title)[:32] title = re.sub('[^-A-Za-z0-9]+', '_', title)[:32]
@ -582,7 +582,7 @@ def config(defaults=None):
c = Config('mobi', desc) c = Config('mobi', desc)
else: else:
c = StringConfig(defaults, desc) c = StringConfig(defaults, desc)
profiles = c.add_group('profiles', _('Device renderer profiles. ' profiles = c.add_group('profiles', _('Device renderer profiles. '
'Affects conversion of font sizes, image rescaling and rasterization ' 'Affects conversion of font sizes, image rescaling and rasterization '
'of tables. Valid profiles are: %s.') % ', '.join(_profiles)) 'of tables. Valid profiles are: %s.') % ', '.join(_profiles))
@ -595,13 +595,13 @@ def config(defaults=None):
c.add_opt('encoding', ['--encoding'], default=None, c.add_opt('encoding', ['--encoding'], default=None,
help=_('Character encoding for HTML files. Default is to auto detect.')) help=_('Character encoding for HTML files. Default is to auto detect.'))
return c return c
def option_parser(): def option_parser():
c = config() c = config()
parser = c.option_parser(usage='%prog '+_('[options]')+' file.opf') parser = c.option_parser(usage='%prog '+_('[options]')+' file.opf')
parser.add_option( parser.add_option(
'-o', '--output', default=None, '-o', '--output', default=None,
help=_('Output file. Default is derived from input filename.')) help=_('Output file. Default is derived from input filename.'))
parser.add_option( parser.add_option(
'-v', '--verbose', default=0, action='count', '-v', '--verbose', default=0, action='count',
@ -647,7 +647,7 @@ def oeb2mobi(opts, inpath):
writer.dump(oeb, outpath) writer.dump(oeb, outpath)
run_plugins_on_postprocess(outpath, 'mobi') run_plugins_on_postprocess(outpath, 'mobi')
logger.info(_('Output written to ') + outpath) logger.info(_('Output written to ') + outpath)
def main(argv=sys.argv): def main(argv=sys.argv):
parser = option_parser() parser = option_parser()
opts, args = parser.parse_args(argv[1:]) opts, args = parser.parse_args(argv[1:])

View File

@ -17,7 +17,6 @@ import logging
from lxml import etree, html from lxml import etree, html
import calibre import calibre
from cssutils import CSSParser from cssutils import CSSParser
from cssutils.css import CSSStyleSheet
from calibre.translations.dynamic import translate from calibre.translations.dynamic import translate
from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
@ -239,7 +238,7 @@ class DirContainer(object):
for path in self.namelist(): for path in self.namelist():
ext = os.path.splitext(path)[1].lower() ext = os.path.splitext(path)[1].lower()
if ext == '.opf': if ext == '.opf':
self.opfname = fname self.opfname = path
return return
self.opfname = None self.opfname = None
@ -280,7 +279,7 @@ class Metadata(object):
syntax. Return an empty list for any terms with no currently associated syntax. Return an empty list for any terms with no currently associated
metadata items. metadata items.
""" """
DC_TERMS = set(['contributor', 'coverage', 'creator', 'date', DC_TERMS = set(['contributor', 'coverage', 'creator', 'date',
'description', 'format', 'identifier', 'language', 'description', 'format', 'identifier', 'language',
'publisher', 'relation', 'rights', 'source', 'publisher', 'relation', 'rights', 'source',
@ -289,7 +288,10 @@ class Metadata(object):
OPF_ATTRS = {'role': OPF('role'), 'file-as': OPF('file-as'), OPF_ATTRS = {'role': OPF('role'), 'file-as': OPF('file-as'),
'scheme': OPF('scheme'), 'event': OPF('event'), 'scheme': OPF('scheme'), 'event': OPF('event'),
'type': XSI('type'), 'lang': XML('lang'), 'id': 'id'} 'type': XSI('type'), 'lang': XML('lang'), 'id': 'id'}
OPF1_NSMAP = {'dc': DC11_NS, 'oebpackage': OPF1_NS}
OPF2_NSMAP = {'opf': OPF2_NS, 'dc': DC11_NS, 'dcterms': DCTERMS_NS,
'xsi': XSI_NS, 'calibre': CALIBRE_NS}
class Item(object): class Item(object):
"""An item of OEB data model metadata. """An item of OEB data model metadata.
@ -305,13 +307,13 @@ class Metadata(object):
""" """
class Attribute(object): class Attribute(object):
"""Smart accessor for allowed OEB metadata item attributes.""" """Smart accessor for allowed OEB metadata item attributes."""
def __init__(self, attr, allowed=None): def __init__(self, attr, allowed=None):
if not callable(attr): if not callable(attr):
attr_, attr = attr, lambda term: attr_ attr_, attr = attr, lambda term: attr_
self.attr = attr self.attr = attr
self.allowed = allowed self.allowed = allowed
def term_attr(self, obj): def term_attr(self, obj):
term = obj.term term = obj.term
if namespace(term) != DC11_NS: if namespace(term) != DC11_NS:
@ -322,14 +324,14 @@ class Metadata(object):
'attribute %r not valid for metadata term %r' \ 'attribute %r not valid for metadata term %r' \
% (self.attr(term), barename(obj.term))) % (self.attr(term), barename(obj.term)))
return self.attr(term) return self.attr(term)
def __get__(self, obj, cls): def __get__(self, obj, cls):
if obj is None: return None if obj is None: return None
return obj.attrib.get(self.term_attr(obj), '') return obj.attrib.get(self.term_attr(obj), '')
def __set__(self, obj, value): def __set__(self, obj, value):
obj.attrib[self.term_attr(obj)] = value obj.attrib[self.term_attr(obj)] = value
def __init__(self, term, value, attrib={}, nsmap={}, **kwargs): def __init__(self, term, value, attrib={}, nsmap={}, **kwargs):
self.attrib = attrib = dict(attrib) self.attrib = attrib = dict(attrib)
self.nsmap = nsmap = dict(nsmap) self.nsmap = nsmap = dict(nsmap)
@ -369,7 +371,7 @@ class Metadata(object):
def fset(self, value): def fset(self, value):
self.value = value self.value = value
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
scheme = Attribute(lambda term: 'scheme' if \ scheme = Attribute(lambda term: 'scheme' if \
term == OPF('meta') else OPF('scheme'), term == OPF('meta') else OPF('scheme'),
[DC('identifier'), OPF('meta')]) [DC('identifier'), OPF('meta')])
@ -384,19 +386,19 @@ class Metadata(object):
DC('relation'), DC('rights'), DC('relation'), DC('rights'),
DC('source'), DC('subject'), DC('source'), DC('subject'),
OPF('meta')]) OPF('meta')])
def __getitem__(self, key): def __getitem__(self, key):
return self.attrib[key] return self.attrib[key]
def __setitem__(self, key, value): def __setitem__(self, key, value):
self.attrib[key] = value self.attrib[key] = value
def __contains__(self, key): def __contains__(self, key):
return key in self.attrib return key in self.attrib
def get(self, key, default=None): def get(self, key, default=None):
return self.attrib.get(key, default) return self.attrib.get(key, default)
def __repr__(self): def __repr__(self):
return 'Item(term=%r, value=%r, attrib=%r)' \ return 'Item(term=%r, value=%r, attrib=%r)' \
% (barename(self.term), self.value, self.attrib) % (barename(self.term), self.value, self.attrib)
@ -422,7 +424,7 @@ class Metadata(object):
elem.attrib['name'] = prefixname(self.term, nsrmap) elem.attrib['name'] = prefixname(self.term, nsrmap)
elem.attrib['content'] = prefixname(self.value, nsrmap) elem.attrib['content'] = prefixname(self.value, nsrmap)
return elem return elem
def to_opf2(self, parent=None, nsrmap={}): def to_opf2(self, parent=None, nsrmap={}):
attrib = {} attrib = {}
for key, value in self.attrib.items(): for key, value in self.attrib.items():
@ -435,7 +437,7 @@ class Metadata(object):
elem.attrib['name'] = prefixname(self.term, nsrmap) elem.attrib['name'] = prefixname(self.term, nsrmap)
elem.attrib['content'] = prefixname(self.value, nsrmap) elem.attrib['content'] = prefixname(self.value, nsrmap)
return elem return elem
def __init__(self, oeb): def __init__(self, oeb):
self.oeb = oeb self.oeb = oeb
self.items = defaultdict(list) self.items = defaultdict(list)
@ -470,7 +472,7 @@ class Metadata(object):
nsmap.update(item.nsmap) nsmap.update(item.nsmap)
return nsmap return nsmap
return property(fget=fget) return property(fget=fget)
@dynamic_property @dynamic_property
def _opf1_nsmap(self): def _opf1_nsmap(self):
def fget(self): def fget(self):
@ -480,7 +482,7 @@ class Metadata(object):
del nsmap[key] del nsmap[key]
return nsmap return nsmap
return property(fget=fget) return property(fget=fget)
@dynamic_property @dynamic_property
def _opf2_nsmap(self): def _opf2_nsmap(self):
def fget(self): def fget(self):
@ -488,7 +490,7 @@ class Metadata(object):
nsmap.update(OPF2_NSMAP) nsmap.update(OPF2_NSMAP)
return nsmap return nsmap
return property(fget=fget) return property(fget=fget)
def to_opf1(self, parent=None): def to_opf1(self, parent=None):
nsmap = self._opf1_nsmap nsmap = self._opf1_nsmap
nsrmap = dict((value, key) for key, value in nsmap.items()) nsrmap = dict((value, key) for key, value in nsmap.items())
@ -502,7 +504,7 @@ class Metadata(object):
chaptertour = self.Item('ms-chaptertour', 'chaptertour') chaptertour = self.Item('ms-chaptertour', 'chaptertour')
chaptertour.to_opf1(dcmeta, xmeta, nsrmap=nsrmap) chaptertour.to_opf1(dcmeta, xmeta, nsrmap=nsrmap)
return elem return elem
def to_opf2(self, parent=None): def to_opf2(self, parent=None):
nsmap = self._opf2_nsmap nsmap = self._opf2_nsmap
nsrmap = dict((value, key) for key, value in nsmap.items()) nsrmap = dict((value, key) for key, value in nsmap.items())
@ -528,7 +530,7 @@ class Manifest(object):
:attr:`hrefs`: A dictionary in which the keys are the internal paths of the :attr:`hrefs`: A dictionary in which the keys are the internal paths of the
manifest items and the values are the items themselves. manifest items and the values are the items themselves.
""" """
class Item(object): class Item(object):
"""An OEB data model book content file. """An OEB data model book content file.
@ -548,10 +550,10 @@ class Manifest(object):
which are not (such as footnotes). Meaningless for items which which are not (such as footnotes). Meaningless for items which
have a :attr:`spine_position` of `None`. have a :attr:`spine_position` of `None`.
""" """
NUM_RE = re.compile('^(.*)([0-9][0-9.]*)(?=[.]|$)') NUM_RE = re.compile('^(.*)([0-9][0-9.]*)(?=[.]|$)')
META_XP = XPath('/h:html/h:head/h:meta[@http-equiv="Content-Type"]') META_XP = XPath('/h:html/h:head/h:meta[@http-equiv="Content-Type"]')
def __init__(self, oeb, id, href, media_type, def __init__(self, oeb, id, href, media_type,
fallback=None, loader=str, data=None): fallback=None, loader=str, data=None):
self.oeb = oeb self.oeb = oeb
@ -609,7 +611,11 @@ class Manifest(object):
elif not namespace(data.tag): elif not namespace(data.tag):
data.attrib['xmlns'] = XHTML_NS data.attrib['xmlns'] = XHTML_NS
data = etree.tostring(data, encoding=unicode) data = etree.tostring(data, encoding=unicode)
data = etree.fromstring(data) try:
data = etree.fromstring(data)
except:
data=data.replace(':=', '=').replace(':>', '>')
data = etree.fromstring(data)
elif namespace(data.tag) != XHTML_NS: elif namespace(data.tag) != XHTML_NS:
# OEB_DOC_NS, but possibly others # OEB_DOC_NS, but possibly others
ns = namespace(data.tag) ns = namespace(data.tag)
@ -659,7 +665,7 @@ class Manifest(object):
data = parser.parseString(data, href=self.href) data = parser.parseString(data, href=self.href)
data.namespaces['h'] = XHTML_NS data.namespaces['h'] = XHTML_NS
return data return data
def _fetch_css(self, path): def _fetch_css(self, path):
hrefs = self.oeb.manifest.hrefs hrefs = self.oeb.manifest.hrefs
if path not in hrefs: if path not in hrefs:
@ -671,7 +677,7 @@ class Manifest(object):
return (None, None) return (None, None)
data = item.data.cssText data = item.data.cssText
return ('utf-8', data) return ('utf-8', data)
@dynamic_property @dynamic_property
def data(self): def data(self):
doc = """Provides MIME type sensitive access to the manifest doc = """Provides MIME type sensitive access to the manifest
@ -707,7 +713,7 @@ class Manifest(object):
def fdel(self): def fdel(self):
self._data = None self._data = None
return property(fget, fset, fdel, doc=doc) return property(fget, fset, fdel, doc=doc)
def __str__(self): def __str__(self):
data = self.data data = self.data
if isinstance(data, etree._Element): if isinstance(data, etree._Element):
@ -715,13 +721,13 @@ class Manifest(object):
if isinstance(data, unicode): if isinstance(data, unicode):
return data.encode('utf-8') return data.encode('utf-8')
return str(data) return str(data)
def __eq__(self, other): def __eq__(self, other):
return id(self) == id(other) return id(self) == id(other)
def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(other) return not self.__eq__(other)
def __cmp__(self, other): def __cmp__(self, other):
result = cmp(self.spine_position, other.spine_position) result = cmp(self.spine_position, other.spine_position)
if result != 0: if result != 0:
@ -735,7 +741,7 @@ class Manifest(object):
onum = float(omatch.group(2)) if omatch else 0.0 onum = float(omatch.group(2)) if omatch else 0.0
okey = (oref, onum, other.id) okey = (oref, onum, other.id)
return cmp(skey, okey) return cmp(skey, okey)
def relhref(self, href): def relhref(self, href):
"""Convert the URL provided in :param:`href` from a book-absolute """Convert the URL provided in :param:`href` from a book-absolute
reference to a reference relative to this manifest item. reference to a reference relative to this manifest item.
@ -756,7 +762,7 @@ class Manifest(object):
if frag: if frag:
relhref = '#'.join((relhref, frag)) relhref = '#'.join((relhref, frag))
return relhref return relhref
def abshref(self, href): def abshref(self, href):
"""Convert the URL provided in :param:`href` from a reference """Convert the URL provided in :param:`href` from a reference
relative to this manifest item to a book-absolute reference. relative to this manifest item to a book-absolute reference.
@ -772,13 +778,13 @@ class Manifest(object):
href = os.path.join(dirname, href) href = os.path.join(dirname, href)
href = os.path.normpath(href).replace('\\', '/') href = os.path.normpath(href).replace('\\', '/')
return href return href
def __init__(self, oeb): def __init__(self, oeb):
self.oeb = oeb self.oeb = oeb
self.items = set() self.items = set()
self.ids = {} self.ids = {}
self.hrefs = {} self.hrefs = {}
def add(self, id, href, media_type, fallback=None, loader=None, data=None): def add(self, id, href, media_type, fallback=None, loader=None, data=None):
"""Add a new item to the book manifest. """Add a new item to the book manifest.
@ -795,7 +801,7 @@ class Manifest(object):
self.ids[item.id] = item self.ids[item.id] = item
self.hrefs[item.href] = item self.hrefs[item.href] = item
return item return item
def remove(self, item): def remove(self, item):
"""Removes :param:`item` from the manifest.""" """Removes :param:`item` from the manifest."""
if item in self.ids: if item in self.ids:
@ -805,7 +811,7 @@ class Manifest(object):
self.items.remove(item) self.items.remove(item)
if item in self.oeb.spine: if item in self.oeb.spine:
self.oeb.spine.remove(item) self.oeb.spine.remove(item)
def generate(self, id=None, href=None): def generate(self, id=None, href=None):
"""Generate a new unique identifier and/or internal path for use in """Generate a new unique identifier and/or internal path for use in
creating a new manifest item, using the provided :param:`id` and/or creating a new manifest item, using the provided :param:`id` and/or
@ -833,13 +839,13 @@ class Manifest(object):
def __iter__(self): def __iter__(self):
for item in self.items: for item in self.items:
yield item yield item
def values(self): def values(self):
return list(self.items) return list(self.items)
def __contains__(self, item): def __contains__(self, item):
return item in self.items return item in self.items
def to_opf1(self, parent=None): def to_opf1(self, parent=None):
elem = element(parent, 'manifest') elem = element(parent, 'manifest')
for item in self.items: for item in self.items:
@ -854,7 +860,7 @@ class Manifest(object):
attrib['fallback'] = item.fallback attrib['fallback'] = item.fallback
element(elem, 'item', attrib=attrib) element(elem, 'item', attrib=attrib)
return elem return elem
def to_opf2(self, parent=None): def to_opf2(self, parent=None):
elem = element(parent, OPF('manifest')) elem = element(parent, OPF('manifest'))
for item in self.items: for item in self.items:
@ -891,14 +897,14 @@ class Spine(object):
elif linear in ('no', 'false'): elif linear in ('no', 'false'):
linear = False linear = False
return linear return linear
def add(self, item, linear=None): def add(self, item, linear=None):
"""Append :param:`item` to the end of the `Spine`.""" """Append :param:`item` to the end of the `Spine`."""
item.linear = self._linear(linear) item.linear = self._linear(linear)
item.spine_position = len(self.items) item.spine_position = len(self.items)
self.items.append(item) self.items.append(item)
return item return item
def insert(self, index, item, linear): def insert(self, index, item, linear):
"""Insert :param:`item` at position :param:`index` in the `Spine`.""" """Insert :param:`item` at position :param:`index` in the `Spine`."""
item.linear = self._linear(linear) item.linear = self._linear(linear)
@ -907,7 +913,7 @@ class Spine(object):
for i in xrange(index, len(self.items)): for i in xrange(index, len(self.items)):
self.items[i].spine_position = i self.items[i].spine_position = i
return item return item
def remove(self, item): def remove(self, item):
"""Remove :param:`item` from the `Spine`.""" """Remove :param:`item` from the `Spine`."""
index = item.spine_position index = item.spine_position
@ -915,7 +921,7 @@ class Spine(object):
for i in xrange(index, len(self.items)): for i in xrange(index, len(self.items)):
self.items[i].spine_position = i self.items[i].spine_position = i
item.spine_position = None item.spine_position = None
def __iter__(self): def __iter__(self):
for item in self.items: for item in self.items:
yield item yield item
@ -953,7 +959,7 @@ class Guide(object):
Provides dictionary-like access, in which the keys are the OEB reference Provides dictionary-like access, in which the keys are the OEB reference
type identifiers and the values are `Reference` objects. type identifiers and the values are `Reference` objects.
""" """
class Reference(object): class Reference(object):
"""Reference to a standard book section. """Reference to a standard book section.
@ -985,7 +991,7 @@ class Guide(object):
TYPES = set(t for t, _ in _TYPES_TITLES) TYPES = set(t for t, _ in _TYPES_TITLES)
TITLES = dict(_TYPES_TITLES) TITLES = dict(_TYPES_TITLES)
ORDER = dict((t, i) for i, (t, _) in enumerate(_TYPES_TITLES)) ORDER = dict((t, i) for i, (t, _) in enumerate(_TYPES_TITLES))
def __init__(self, oeb, type, title, href): def __init__(self, oeb, type, title, href):
self.oeb = oeb self.oeb = oeb
if type.lower() in self.TYPES: if type.lower() in self.TYPES:
@ -998,22 +1004,22 @@ class Guide(object):
self.type = type self.type = type
self.title = title self.title = title
self.href = urlnormalize(href) self.href = urlnormalize(href)
def __repr__(self): def __repr__(self):
return 'Reference(type=%r, title=%r, href=%r)' \ return 'Reference(type=%r, title=%r, href=%r)' \
% (self.type, self.title, self.href) % (self.type, self.title, self.href)
@dynamic_property @dynamic_property
def _order(self): def _order(self):
def fget(self): def fget(self):
return self.ORDER.get(self.type, self.type) return self.ORDER.get(self.type, self.type)
return property(fget=fget) return property(fget=fget)
def __cmp__(self, other): def __cmp__(self, other):
if not isinstance(other, Guide.Reference): if not isinstance(other, Guide.Reference):
return NotImplemented return NotImplemented
return cmp(self._order, other._order) return cmp(self._order, other._order)
@dynamic_property @dynamic_property
def item(self): def item(self):
doc = """The manifest item associated with this reference.""" doc = """The manifest item associated with this reference."""
@ -1022,41 +1028,41 @@ class Guide(object):
hrefs = self.oeb.manifest.hrefs hrefs = self.oeb.manifest.hrefs
return hrefs.get(path, None) return hrefs.get(path, None)
return property(fget=fget, doc=doc) return property(fget=fget, doc=doc)
def __init__(self, oeb): def __init__(self, oeb):
self.oeb = oeb self.oeb = oeb
self.refs = {} self.refs = {}
def add(self, type, title, href): def add(self, type, title, href):
"""Add a new reference to the `Guide`.""" """Add a new reference to the `Guide`."""
ref = self.Reference(self.oeb, type, title, href) ref = self.Reference(self.oeb, type, title, href)
self.refs[type] = ref self.refs[type] = ref
return ref return ref
def iterkeys(self): def iterkeys(self):
for type in self.refs: for type in self.refs:
yield type yield type
__iter__ = iterkeys __iter__ = iterkeys
def values(self): def values(self):
return sorted(self.refs.values()) return sorted(self.refs.values())
def items(self): def items(self):
for type, ref in self.refs.items(): for type, ref in self.refs.items():
yield type, ref yield type, ref
def __getitem__(self, key): def __getitem__(self, key):
return self.refs[key] return self.refs[key]
def __delitem__(self, key): def __delitem__(self, key):
del self.refs[key] del self.refs[key]
def __contains__(self, key): def __contains__(self, key):
return key in self.refs return key in self.refs
def __len__(self): def __len__(self):
return len(self.refs) return len(self.refs)
def to_opf1(self, parent=None): def to_opf1(self, parent=None):
elem = element(parent, 'guide') elem = element(parent, 'guide')
for ref in self.refs.values(): for ref in self.refs.values():
@ -1065,7 +1071,7 @@ class Guide(object):
attrib['title'] = ref.title attrib['title'] = ref.title
element(elem, 'reference', attrib=attrib) element(elem, 'reference', attrib=attrib)
return elem return elem
def to_opf2(self, parent=None): def to_opf2(self, parent=None):
elem = element(parent, OPF('guide')) elem = element(parent, OPF('guide'))
for ref in self.refs.values(): for ref in self.refs.values():
@ -1095,7 +1101,7 @@ class TOC(object):
self.klass = klass self.klass = klass
self.id = id self.id = id
self.nodes = [] self.nodes = []
def add(self, title, href, klass=None, id=None): def add(self, title, href, klass=None, id=None):
"""Create and return a new sub-node of this node.""" """Create and return a new sub-node of this node."""
node = TOC(title, href, klass, id) node = TOC(title, href, klass, id)
@ -1108,18 +1114,18 @@ class TOC(object):
for child in self.nodes: for child in self.nodes:
for node in child.iter(): for node in child.iter():
yield node yield node
def iterdescendants(self): def iterdescendants(self):
"""Iterate over all descendant nodes in depth-first order.""" """Iterate over all descendant nodes in depth-first order."""
for child in self.nodes: for child in self.nodes:
for node in child.iter(): for node in child.iter():
yield node yield node
def __iter__(self): def __iter__(self):
"""Iterate over all immediate child nodes.""" """Iterate over all immediate child nodes."""
for node in self.nodes: for node in self.nodes:
yield node yield node
def __getitem__(self, index): def __getitem__(self, index):
return self.nodes[index] return self.nodes[index]
@ -1134,7 +1140,7 @@ class TOC(object):
prev.nodes.append(node) prev.nodes.append(node)
else: else:
prev = node prev = node
def depth(self): def depth(self):
"""The maximum depth of the navigation tree rooted at this node.""" """The maximum depth of the navigation tree rooted at this node."""
try: try:
@ -1148,7 +1154,7 @@ class TOC(object):
'title': node.title, 'href': node.href}) 'title': node.title, 'href': node.href})
node.to_opf1(tour) node.to_opf1(tour)
return tour return tour
def to_ncx(self, parent): def to_ncx(self, parent):
for node in self.nodes: for node in self.nodes:
id = node.id or unicode(uuid.uuid4()) id = node.id or unicode(uuid.uuid4())
@ -1169,7 +1175,7 @@ class PageList(object):
Provides list-like access to the pages. Provides list-like access to the pages.
""" """
class Page(object): class Page(object):
"""Represents a mapping between a page name and a position within """Represents a mapping between a page name and a position within
the book content. the book content.
@ -1187,17 +1193,17 @@ class PageList(object):
:attr:`id`: Optional unique identifier for this page. :attr:`id`: Optional unique identifier for this page.
""" """
TYPES = set(['front', 'normal', 'special']) TYPES = set(['front', 'normal', 'special'])
def __init__(self, name, href, type='normal', klass=None, id=None): def __init__(self, name, href, type='normal', klass=None, id=None):
self.name = unicode(name) self.name = unicode(name)
self.href = urlnormalize(href) self.href = urlnormalize(href)
self.type = type if type in self.TYPES else 'normal' self.type = type if type in self.TYPES else 'normal'
self.id = id self.id = id
self.klass = klass self.klass = klass
def __init__(self): def __init__(self):
self.pages = [] self.pages = []
def add(self, name, href, type='normal', klass=None, id=None): def add(self, name, href, type='normal', klass=None, id=None):
"""Create a new page and add it to the `PageList`.""" """Create a new page and add it to the `PageList`."""
page = self.Page(name, href, type, klass, id) page = self.Page(name, href, type, klass, id)
@ -1206,11 +1212,11 @@ class PageList(object):
def __len__(self): def __len__(self):
return len(self.pages) return len(self.pages)
def __iter__(self): def __iter__(self):
for page in self.pages: for page in self.pages:
yield page yield page
def __getitem__(self, index): def __getitem__(self, index):
return self.pages[index] return self.pages[index]
@ -1219,7 +1225,7 @@ class PageList(object):
def remove(self, page): def remove(self, page):
return self.pages.remove(page) return self.pages.remove(page)
def to_ncx(self, parent=None): def to_ncx(self, parent=None):
plist = element(parent, NCX('pageList'), id=str(uuid.uuid4())) plist = element(parent, NCX('pageList'), id=str(uuid.uuid4()))
values = dict((t, count(1)) for t in ('front', 'normal', 'special')) values = dict((t, count(1)) for t in ('front', 'normal', 'special'))
@ -1235,7 +1241,7 @@ class PageList(object):
element(label, NCX('text')).text = page.name element(label, NCX('text')).text = page.name
element(ptarget, NCX('content'), src=page.href) element(ptarget, NCX('content'), src=page.href)
return plist return plist
def to_page_map(self): def to_page_map(self):
pmap = etree.Element(OPF('page-map'), nsmap={None: OPF2_NS}) pmap = etree.Element(OPF('page-map'), nsmap={None: OPF2_NS})
for page in self.pages: for page in self.pages:
@ -1245,11 +1251,14 @@ class PageList(object):
class OEBBook(object): class OEBBook(object):
"""Representation of a book in the IDPF OEB data model.""" """Representation of a book in the IDPF OEB data model."""
def __init__(self, logger, parse_cache={}, encoding='utf-8', COVER_SVG_XP = XPath('h:body//svg:svg[position() = 1]')
COVER_OBJECT_XP = XPath('h:body//h:object[@data][position() = 1]')
def __init__(self, logger, parse_cache={}, encoding='utf-8',
pretty_print=False): pretty_print=False):
"""Create empty book. Optional arguments: """Create empty book. Optional arguments:
:param parse_cache: A cache of parsed XHTML/CSS. Keys are absolute :param parse_cache: A cache of parsed XHTML/CSS. Keys are absolute
paths to te cached files and values are lxml root objects and paths to te cached files and values are lxml root objects and
cssutils stylesheets. cssutils stylesheets.
@ -1260,10 +1269,10 @@ class OEBBook(object):
:param:`logger`: A Log object to use for logging all messages :param:`logger`: A Log object to use for logging all messages
related to the processing of this book. It is accessible related to the processing of this book. It is accessible
via the instance data members :attr:`logger,log`. via the instance data members :attr:`logger,log`.
It provides the following public instance data members for It provides the following public instance data members for
accessing various parts of the OEB data model: accessing various parts of the OEB data model:
:attr:`metadata`: Metadata such as title, author name(s), etc. :attr:`metadata`: Metadata such as title, author name(s), etc.
:attr:`manifest`: Manifest of all files included in the book, :attr:`manifest`: Manifest of all files included in the book,
including MIME types and fallback information. including MIME types and fallback information.
@ -1275,6 +1284,7 @@ class OEBBook(object):
:attr:`pages`: List of "pages," such as indexed to a print edition of :attr:`pages`: List of "pages," such as indexed to a print edition of
the same text. the same text.
""" """
self.encoding = encoding self.encoding = encoding
self.pretty_print = pretty_print self.pretty_print = pretty_print
self.logger = self.log = logger self.logger = self.log = logger
@ -1294,13 +1304,13 @@ class OEBBook(object):
encoding = opts.encoding encoding = opts.encoding
pretty_print = opts.pretty_print pretty_print = opts.pretty_print
return cls(encoding=encoding, pretty_print=pretty_print) return cls(encoding=encoding, pretty_print=pretty_print)
def translate(self, text): def translate(self, text):
"""Translate :param:`text` into the book's primary language.""" """Translate :param:`text` into the book's primary language."""
lang = str(self.metadata.language[0]) lang = str(self.metadata.language[0])
lang = lang.split('-', 1)[0].lower() lang = lang.split('-', 1)[0].lower()
return translate(lang, text) return translate(lang, text)
def decode(self, data): def decode(self, data):
"""Automatically decode :param:`data` into a `unicode` object.""" """Automatically decode :param:`data` into a `unicode` object."""
if isinstance(data, unicode): if isinstance(data, unicode):
@ -1323,7 +1333,7 @@ class OEBBook(object):
data = data.replace('\r\n', '\n') data = data.replace('\r\n', '\n')
data = data.replace('\r', '\n') data = data.replace('\r', '\n')
return data return data
def to_opf1(self): def to_opf1(self):
"""Produce OPF 1.2 representing the book's metadata and structure. """Produce OPF 1.2 representing the book's metadata and structure.
@ -1370,7 +1380,7 @@ class OEBBook(object):
order = playorder.get(href, 0) order = playorder.get(href, 0)
elem.attrib['playOrder'] = str(order) elem.attrib['playOrder'] = str(order)
return return
def _to_ncx(self): def _to_ncx(self):
lang = unicode(self.metadata.language[0]) lang = unicode(self.metadata.language[0])
ncx = etree.Element(NCX('ncx'), ncx = etree.Element(NCX('ncx'),
@ -1399,10 +1409,10 @@ class OEBBook(object):
maxpnum.attrib['content'] = str(value) maxpnum.attrib['content'] = str(value)
self._update_playorder(ncx) self._update_playorder(ncx)
return ncx return ncx
def to_opf2(self, page_map=False): def to_opf2(self, page_map=False):
"""Produce OPF 2.0 representing the book's metadata and structure. """Produce OPF 2.0 representing the book's metadata and structure.
Returns a dictionary in which the keys are MIME types and the values Returns a dictionary in which the keys are MIME types and the values
are tuples of (default) filenames and lxml.etree element structures. are tuples of (default) filenames and lxml.etree element structures.
""" """

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,15 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
""" The GUI """ """ The GUI """
import sys, os, re, StringIO, traceback, time import os
from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \ from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
QByteArray, QLocale, QUrl, QTranslator, QCoreApplication, \ QByteArray, QUrl, QTranslator, QCoreApplication
QModelIndex
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \ from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
QIcon, QTableView, QDialogButtonBox, QApplication, QDialog QIcon, QTableView, QApplication, QDialog
ORG_NAME = 'KovidsBrain' ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500' APP_UID = 'libprs500'
from calibre import __author__, islinux, iswindows, isosx from calibre import islinux, iswindows
from calibre.startup import get_lang from calibre.startup import get_lang
from calibre.utils.config import Config, ConfigProxy, dynamic from calibre.utils.config import Config, ConfigProxy, dynamic
import calibre.resources as resources import calibre.resources as resources
@ -32,7 +31,7 @@ def _config():
help=_('The format to use when saving single files to disk')) help=_('The format to use when saving single files to disk'))
c.add_opt('confirm_delete', default=False, c.add_opt('confirm_delete', default=False,
help=_('Confirm before deleting')) help=_('Confirm before deleting'))
c.add_opt('toolbar_icon_size', default=QSize(48, 48), c.add_opt('toolbar_icon_size', default=QSize(48, 48),
help=_('Toolbar icon size')) # value QVariant.toSize help=_('Toolbar icon size')) # value QVariant.toSize
c.add_opt('show_text_in_toolbar', default=True, c.add_opt('show_text_in_toolbar', default=True,
help=_('Show button labels in the toolbar')) help=_('Show button labels in the toolbar'))
@ -57,16 +56,19 @@ def _config():
c.add_opt('autolaunch_server', default=False, help=_('Automatically launch content server on application startup')) c.add_opt('autolaunch_server', default=False, help=_('Automatically launch content server on application startup'))
c.add_opt('oldest_news', default=60, help=_('Oldest news kept in database')) c.add_opt('oldest_news', default=60, help=_('Oldest news kept in database'))
c.add_opt('systray_icon', default=True, help=_('Show system tray icon')) c.add_opt('systray_icon', default=True, help=_('Show system tray icon'))
c.add_opt('upload_news_to_device', default=True, c.add_opt('upload_news_to_device', default=True,
help=_('Upload downloaded news to device')) help=_('Upload downloaded news to device'))
c.add_opt('delete_news_from_library_on_upload', default=False, c.add_opt('delete_news_from_library_on_upload', default=False,
help=_('Delete books from library after uploading to device')) help=_('Delete books from library after uploading to device'))
c.add_opt('separate_cover_flow', default=False, c.add_opt('separate_cover_flow', default=False,
help=_('Show the cover flow in a separate window instead of in the main calibre window')) help=_('Show the cover flow in a separate window instead of in the main calibre window'))
c.add_opt('disable_tray_notification', default=False, c.add_opt('disable_tray_notification', default=False,
help=_('Disable notifications from the system tray icon')) help=_('Disable notifications from the system tray icon'))
c.add_opt('default_send_to_device_action', default=None,
help=_('Default action to perform when send to device button is '
'clicked'))
return ConfigProxy(c) return ConfigProxy(c)
config = _config() config = _config()
# Turn off DeprecationWarnings in windows GUI # Turn off DeprecationWarnings in windows GUI
if iswindows: if iswindows:
@ -139,16 +141,16 @@ def human_readable(size):
class Dispatcher(QObject): class Dispatcher(QObject):
'''Convenience class to ensure that a function call always happens in the GUI thread''' '''Convenience class to ensure that a function call always happens in the GUI thread'''
SIGNAL = SIGNAL('dispatcher(PyQt_PyObject,PyQt_PyObject)')
def __init__(self, func): def __init__(self, func):
QObject.__init__(self) QObject.__init__(self)
self.func = func self.func = func
self.connect(self, SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject)'), self.connect(self, self.SIGNAL, self.dispatch, Qt.QueuedConnection)
self.dispatch, Qt.QueuedConnection)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
self.emit(SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject)'), args, kwargs) self.emit(self.SIGNAL, args, kwargs)
def dispatch(self, args, kwargs): def dispatch(self, args, kwargs):
self.func(*args, **kwargs) self.func(*args, **kwargs)
@ -157,29 +159,29 @@ class GetMetadata(QObject):
Convenience class to ensure that metadata readers are used only in the Convenience class to ensure that metadata readers are used only in the
GUI thread. Must be instantiated in the GUI thread. GUI thread. Must be instantiated in the GUI thread.
''' '''
def __init__(self): def __init__(self):
QObject.__init__(self) QObject.__init__(self)
self.connect(self, SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), self.connect(self, SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self._get_metadata, Qt.QueuedConnection) self._get_metadata, Qt.QueuedConnection)
self.connect(self, SIGNAL('idispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), self.connect(self, SIGNAL('idispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self._from_formats, Qt.QueuedConnection) self._from_formats, Qt.QueuedConnection)
def __call__(self, id, *args, **kwargs): def __call__(self, id, *args, **kwargs):
self.emit(SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), self.emit(SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
id, args, kwargs) id, args, kwargs)
def from_formats(self, id, *args, **kwargs): def from_formats(self, id, *args, **kwargs):
self.emit(SIGNAL('idispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), self.emit(SIGNAL('idispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
id, args, kwargs) id, args, kwargs)
def _from_formats(self, id, args, kwargs): def _from_formats(self, id, args, kwargs):
try: try:
mi = metadata_from_formats(*args, **kwargs) mi = metadata_from_formats(*args, **kwargs)
except: except:
mi = MetaInformation('', [_('Unknown')]) mi = MetaInformation('', [_('Unknown')])
self.emit(SIGNAL('metadataf(PyQt_PyObject, PyQt_PyObject)'), id, mi) self.emit(SIGNAL('metadataf(PyQt_PyObject, PyQt_PyObject)'), id, mi)
def _get_metadata(self, id, args, kwargs): def _get_metadata(self, id, args, kwargs):
try: try:
mi = get_metadata(*args, **kwargs) mi = get_metadata(*args, **kwargs)
@ -191,27 +193,27 @@ class TableView(QTableView):
def __init__(self, parent): def __init__(self, parent):
QTableView.__init__(self, parent) QTableView.__init__(self, parent)
self.read_settings() self.read_settings()
def read_settings(self): def read_settings(self):
self.cw = dynamic[self.__class__.__name__+'column widths'] self.cw = dynamic[self.__class__.__name__+'column widths']
def write_settings(self): def write_settings(self):
dynamic[self.__class__.__name__+'column widths'] = \ dynamic[self.__class__.__name__+'column widths'] = \
tuple([int(self.columnWidth(i)) for i in range(self.model().columnCount(None))]) tuple([int(self.columnWidth(i)) for i in range(self.model().columnCount(None))])
def restore_column_widths(self): def restore_column_widths(self):
if self.cw and len(self.cw): if self.cw and len(self.cw):
for i in range(len(self.cw)): for i in range(len(self.cw)):
self.setColumnWidth(i, self.cw[i]) self.setColumnWidth(i, self.cw[i])
return True return True
class FileIconProvider(QFileIconProvider): class FileIconProvider(QFileIconProvider):
ICONS = { ICONS = {
'default' : 'unknown', 'default' : 'unknown',
'dir' : 'dir', 'dir' : 'dir',
'zero' : 'zero', 'zero' : 'zero',
'jpeg' : 'jpeg', 'jpeg' : 'jpeg',
'jpg' : 'jpeg', 'jpg' : 'jpeg',
'gif' : 'gif', 'gif' : 'gif',
@ -234,7 +236,7 @@ class FileIconProvider(QFileIconProvider):
'mobi' : 'mobi', 'mobi' : 'mobi',
'epub' : 'epub', 'epub' : 'epub',
} }
def __init__(self): def __init__(self):
QFileIconProvider.__init__(self) QFileIconProvider.__init__(self)
self.icons = {} self.icons = {}
@ -242,14 +244,14 @@ class FileIconProvider(QFileIconProvider):
self.icons[key] = ':/images/mimetypes/'+self.__class__.ICONS[key]+'.svg' self.icons[key] = ':/images/mimetypes/'+self.__class__.ICONS[key]+'.svg'
for i in ('dir', 'default', 'zero'): for i in ('dir', 'default', 'zero'):
self.icons[i] = QIcon(self.icons[i]) self.icons[i] = QIcon(self.icons[i])
def key_from_ext(self, ext): def key_from_ext(self, ext):
key = ext if ext in self.icons.keys() else 'default' key = ext if ext in self.icons.keys() else 'default'
if key == 'default' and ext.count('.') > 0: if key == 'default' and ext.count('.') > 0:
ext = ext.rpartition('.')[2] ext = ext.rpartition('.')[2]
key = ext if ext in self.icons.keys() else 'default' key = ext if ext in self.icons.keys() else 'default'
return key return key
def cached_icon(self, key): def cached_icon(self, key):
candidate = self.icons[key] candidate = self.icons[key]
if isinstance(candidate, QIcon): if isinstance(candidate, QIcon):
@ -257,11 +259,11 @@ class FileIconProvider(QFileIconProvider):
icon = QIcon(candidate) icon = QIcon(candidate)
self.icons[key] = icon self.icons[key] = icon
return icon return icon
def icon_from_ext(self, ext): def icon_from_ext(self, ext):
key = self.key_from_ext(ext.lower() if ext else '') key = self.key_from_ext(ext.lower() if ext else '')
return self.cached_icon(key) return self.cached_icon(key)
def load_icon(self, fileinfo): def load_icon(self, fileinfo):
key = 'default' key = 'default'
icons = self.icons icons = self.icons
@ -275,7 +277,7 @@ class FileIconProvider(QFileIconProvider):
ext = qstring_to_unicode(fileinfo.completeSuffix()).lower() ext = qstring_to_unicode(fileinfo.completeSuffix()).lower()
key = self.key_from_ext(ext) key = self.key_from_ext(ext)
return self.cached_icon(key) return self.cached_icon(key)
def icon(self, arg): def icon(self, arg):
if isinstance(arg, QFileInfo): if isinstance(arg, QFileInfo):
return self.load_icon(arg) return self.load_icon(arg)
@ -284,13 +286,13 @@ class FileIconProvider(QFileIconProvider):
if arg == QFileIconProvider.File: if arg == QFileIconProvider.File:
return self.icons['default'] return self.icons['default']
return QFileIconProvider.icon(self, arg) return QFileIconProvider.icon(self, arg)
_file_icon_provider = None _file_icon_provider = None
def initialize_file_icon_provider(): def initialize_file_icon_provider():
global _file_icon_provider global _file_icon_provider
if _file_icon_provider is None: if _file_icon_provider is None:
_file_icon_provider = FileIconProvider() _file_icon_provider = FileIconProvider()
def file_icon_provider(): def file_icon_provider():
global _file_icon_provider global _file_icon_provider
return _file_icon_provider return _file_icon_provider
@ -299,13 +301,13 @@ _sidebar_directories = []
def set_sidebar_directories(dirs): def set_sidebar_directories(dirs):
global _sidebar_directories global _sidebar_directories
if dirs is None: if dirs is None:
dirs = config['frequently_used_directories'] dirs = config['frequently_used_directories']
_sidebar_directories = [QUrl.fromLocalFile(i) for i in dirs] _sidebar_directories = [QUrl.fromLocalFile(i) for i in dirs]
class FileDialog(QObject): class FileDialog(QObject):
def __init__(self, title='Choose Files', def __init__(self, title='Choose Files',
filters=[], filters=[],
add_all_files_filter=True, add_all_files_filter=True,
parent=None, parent=None,
modal = True, modal = True,
name = '', name = '',
@ -321,16 +323,16 @@ class FileDialog(QObject):
ftext += '%s (%s);;'%(text, ' '.join(extensions)) ftext += '%s (%s);;'%(text, ' '.join(extensions))
if add_all_files_filter or not ftext: if add_all_files_filter or not ftext:
ftext += 'All files (*)' ftext += 'All files (*)'
self.dialog_name = name if name else 'dialog_' + title self.dialog_name = name if name else 'dialog_' + title
self.selected_files = None self.selected_files = None
self.fd = None self.fd = None
if islinux: if islinux:
self.fd = QFileDialog(parent) self.fd = QFileDialog(parent)
self.fd.setFileMode(mode) self.fd.setFileMode(mode)
self.fd.setIconProvider(_file_icon_provider) self.fd.setIconProvider(_file_icon_provider)
self.fd.setModal(modal) self.fd.setModal(modal)
self.fd.setNameFilter(ftext) self.fd.setNameFilter(ftext)
self.fd.setWindowTitle(title) self.fd.setWindowTitle(title)
state = dynamic[self.dialog_name] state = dynamic[self.dialog_name]
@ -347,7 +349,7 @@ class FileDialog(QObject):
f = qstring_to_unicode( f = qstring_to_unicode(
QFileDialog.getSaveFileName(parent, title, dir, ftext, "")) QFileDialog.getSaveFileName(parent, title, dir, ftext, ""))
if os.path.exists(f): if os.path.exists(f):
self.selected_files.append(f) self.selected_files.append(f)
elif mode == QFileDialog.ExistingFile: elif mode == QFileDialog.ExistingFile:
f = qstring_to_unicode( f = qstring_to_unicode(
QFileDialog.getOpenFileName(parent, title, dir, ftext, "")) QFileDialog.getOpenFileName(parent, title, dir, ftext, ""))
@ -367,44 +369,44 @@ class FileDialog(QObject):
if self.selected_files: if self.selected_files:
self.selected_files = [qstring_to_unicode(q) for q in self.selected_files] self.selected_files = [qstring_to_unicode(q) for q in self.selected_files]
dynamic[self.dialog_name] = os.path.dirname(self.selected_files[0]) dynamic[self.dialog_name] = os.path.dirname(self.selected_files[0])
self.accepted = bool(self.selected_files) self.accepted = bool(self.selected_files)
def get_files(self): def get_files(self):
if islinux and self.fd.result() != self.fd.Accepted: if islinux and self.fd.result() != self.fd.Accepted:
return tuple() return tuple()
if self.selected_files is None: if self.selected_files is None:
return tuple(os.path.abspath(qstring_to_unicode(i)) for i in self.fd.selectedFiles()) return tuple(os.path.abspath(qstring_to_unicode(i)) for i in self.fd.selectedFiles())
return tuple(self.selected_files) return tuple(self.selected_files)
def save_dir(self): def save_dir(self):
if self.fd: if self.fd:
dynamic[self.dialog_name] = self.fd.saveState() dynamic[self.dialog_name] = self.fd.saveState()
def choose_dir(window, name, title): def choose_dir(window, name, title):
fd = FileDialog(title, [], False, window, name=name, fd = FileDialog(title, [], False, window, name=name,
mode=QFileDialog.DirectoryOnly) mode=QFileDialog.DirectoryOnly)
dir = fd.get_files() dir = fd.get_files()
if dir: if dir:
return dir[0] return dir[0]
def choose_files(window, name, title, def choose_files(window, name, title,
filters=[], all_files=True, select_only_single_file=False): filters=[], all_files=True, select_only_single_file=False):
''' '''
Ask user to choose a bunch of files. Ask user to choose a bunch of files.
@param name: Unique dialog name used to store the opened directory @param name: Unique dialog name used to store the opened directory
@param title: Title to show in dialogs titlebar @param title: Title to show in dialogs titlebar
@param filters: list of allowable extensions. Each element of the list @param filters: list of allowable extensions. Each element of the list
must be a 2-tuple with first element a string describing must be a 2-tuple with first element a string describing
the type of files to be filtered and second element a list the type of files to be filtered and second element a list
of extensions. of extensions.
@param all_files: If True add All files to filters. @param all_files: If True add All files to filters.
@param select_only_single_file: If True only one file can be selected @param select_only_single_file: If True only one file can be selected
''' '''
mode = QFileDialog.ExistingFile if select_only_single_file else QFileDialog.ExistingFiles mode = QFileDialog.ExistingFile if select_only_single_file else QFileDialog.ExistingFiles
fd = FileDialog(title=title, name=name, filters=filters, fd = FileDialog(title=title, name=name, filters=filters,
parent=window, add_all_files_filter=all_files, mode=mode, parent=window, add_all_files_filter=all_files, mode=mode,
) )
if fd.accepted: if fd.accepted:
@ -413,8 +415,8 @@ def choose_files(window, name, title,
def choose_images(window, name, title, select_only_single_file=True): def choose_images(window, name, title, select_only_single_file=True):
mode = QFileDialog.ExistingFile if select_only_single_file else QFileDialog.ExistingFiles mode = QFileDialog.ExistingFile if select_only_single_file else QFileDialog.ExistingFiles
fd = FileDialog(title=title, name=name, fd = FileDialog(title=title, name=name,
filters=[('Images', ['png', 'gif', 'jpeg', 'jpg', 'svg'])], filters=[('Images', ['png', 'gif', 'jpeg', 'jpg', 'svg'])],
parent=window, add_all_files_filter=False, mode=mode, parent=window, add_all_files_filter=False, mode=mode,
) )
if fd.accepted: if fd.accepted:
@ -432,7 +434,7 @@ def pixmap_to_data(pixmap, format='JPEG'):
return str(ba.data()) return str(ba.data())
class ResizableDialog(QDialog): class ResizableDialog(QDialog):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
QDialog.__init__(self, *args) QDialog.__init__(self, *args)
self.setupUi(self) self.setupUi(self)
@ -444,14 +446,15 @@ class ResizableDialog(QDialog):
nh = min(self.height(), nh) nh = min(self.height(), nh)
nw = min(self.width(), nw) nw = min(self.width(), nw)
self.resize(nw, nh) self.resize(nw, nh)
try: try:
from calibre.utils.single_qt_application import SingleApplication from calibre.utils.single_qt_application import SingleApplication
SingleApplication
except: except:
SingleApplication = None SingleApplication = None
class Application(QApplication): class Application(QApplication):
def __init__(self, args): def __init__(self, args):
qargs = [i.encode('utf-8') if isinstance(i, unicode) else i for i in args] qargs = [i.encode('utf-8') if isinstance(i, unicode) else i for i in args]
QApplication.__init__(self, qargs) QApplication.__init__(self, qargs)
@ -462,6 +465,6 @@ class Application(QApplication):
if data: if data:
self.translator.loadFromData(data) self.translator.loadFromData(data)
self.installTranslator(self.translator) self.installTranslator(self.translator)

View File

@ -1,19 +1,36 @@
from __future__ import with_statement
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, traceback, Queue, time import os, traceback, Queue, time, socket
from threading import Thread from threading import Thread, RLock
from itertools import repeat
from functools import partial
from binascii import unhexlify
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \
Qt
from calibre.devices import devices from calibre.devices import devices
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.parallel import Job from calibre.parallel import Job
from calibre.devices.scanner import DeviceScanner from calibre.devices.scanner import DeviceScanner
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
pixmap_to_data, warning_dialog
from calibre.ebooks.metadata import authors_to_string
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
from calibre.devices.interface import Device
from calibre import sanitize_file_name, preferred_encoding
from calibre.utils.filenames import ascii_filename
from calibre.devices.errors import FreeSpaceError
from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
config as email_config
class DeviceJob(Job): class DeviceJob(Job):
def __init__(self, func, *args, **kwargs): def __init__(self, func, *args, **kwargs):
Job.__init__(self, *args, **kwargs) Job.__init__(self, *args, **kwargs)
self.func = func self.func = func
def run(self): def run(self):
self.start_work() self.start_work()
try: try:
@ -23,16 +40,12 @@ class DeviceJob(Job):
self.traceback = traceback.format_exc() self.traceback = traceback.format_exc()
finally: finally:
self.job_done() self.job_done()
class DeviceManager(Thread): class DeviceManager(Thread):
'''
Worker thread that polls the USB ports for devices. Emits the
signal connected(PyQt_PyObject, PyQt_PyObject) on connection and
disconnection events.
'''
def __init__(self, connected_slot, job_manager, sleep_time=2): def __init__(self, connected_slot, job_manager, sleep_time=2):
''' '''
@param sleep_time: Time to sleep between device probes in millisecs @param sleep_time: Time to sleep between device probes in millisecs
@type sleep_time: integer @type sleep_time: integer
''' '''
@ -48,7 +61,7 @@ class DeviceManager(Thread):
self.job_manager = job_manager self.job_manager = job_manager
self.current_job = None self.current_job = None
self.scanner = DeviceScanner() self.scanner = DeviceScanner()
def detect_device(self): def detect_device(self):
self.scanner.scan() self.scanner.scan()
for device in self.devices: for device in self.devices:
@ -63,8 +76,8 @@ class DeviceManager(Thread):
except: except:
print 'Unable to open device' print 'Unable to open device'
traceback.print_exc() traceback.print_exc()
finally: finally:
device[1] = True device[1] = True
elif not connected and device[1]: elif not connected and device[1]:
while True: while True:
try: try:
@ -75,14 +88,14 @@ class DeviceManager(Thread):
self.device = None self.device = None
self.connected_slot(False) self.connected_slot(False)
device[1] ^= True device[1] ^= True
def next(self): def next(self):
if not self.jobs.empty(): if not self.jobs.empty():
try: try:
return self.jobs.get_nowait() return self.jobs.get_nowait()
except Queue.Empty: except Queue.Empty:
pass pass
def run(self): def run(self):
while self.keep_going: while self.keep_going:
self.detect_device() self.detect_device()
@ -94,75 +107,80 @@ class DeviceManager(Thread):
self.current_job.run() self.current_job.run()
self.current_job = None self.current_job = None
else: else:
break break
time.sleep(self.sleep_time) time.sleep(self.sleep_time)
def create_job(self, func, done, description, args=[], kwargs={}): def create_job(self, func, done, description, args=[], kwargs={}):
job = DeviceJob(func, done, self.job_manager, job = DeviceJob(func, done, self.job_manager,
args=args, kwargs=kwargs, description=description) args=args, kwargs=kwargs, description=description)
self.job_manager.add_job(job) self.job_manager.add_job(job)
self.jobs.put(job) self.jobs.put(job)
return job return job
def has_card(self):
try:
return bool(self.device.card_prefix())
except:
return False
def _get_device_information(self): def _get_device_information(self):
info = self.device.get_device_information(end_session=False) info = self.device.get_device_information(end_session=False)
info = [i.replace('\x00', '').replace('\x01', '') for i in info] info = [i.replace('\x00', '').replace('\x01', '') for i in info]
cp = self.device.card_prefix(end_session=False) cp = self.device.card_prefix(end_session=False)
fs = self.device.free_space() fs = self.device.free_space()
return info, cp, fs return info, cp, fs
def get_device_information(self, done): def get_device_information(self, done):
'''Get device information and free space on device''' '''Get device information and free space on device'''
return self.create_job(self._get_device_information, done, return self.create_job(self._get_device_information, done,
description=_('Get device information')) description=_('Get device information'))
def _books(self): def _books(self):
'''Get metadata from device''' '''Get metadata from device'''
mainlist = self.device.books(oncard=False, end_session=False) mainlist = self.device.books(oncard=False, end_session=False)
cardlist = self.device.books(oncard=True) cardlist = self.device.books(oncard=True)
return (mainlist, cardlist) return (mainlist, cardlist)
def books(self, done): def books(self, done):
'''Return callable that returns the list of books on device as two booklists''' '''Return callable that returns the list of books on device as two booklists'''
return self.create_job(self._books, done, description=_('Get list of books on device')) return self.create_job(self._books, done, description=_('Get list of books on device'))
def _sync_booklists(self, booklists): def _sync_booklists(self, booklists):
'''Sync metadata to device''' '''Sync metadata to device'''
self.device.sync_booklists(booklists, end_session=False) self.device.sync_booklists(booklists, end_session=False)
return self.device.card_prefix(end_session=False), self.device.free_space() return self.device.card_prefix(end_session=False), self.device.free_space()
def sync_booklists(self, done, booklists): def sync_booklists(self, done, booklists):
return self.create_job(self._sync_booklists, done, args=[booklists], return self.create_job(self._sync_booklists, done, args=[booklists],
description=_('Send metadata to device')) description=_('Send metadata to device'))
def _upload_books(self, files, names, on_card=False, metadata=None): def _upload_books(self, files, names, on_card=False, metadata=None):
'''Upload books to device: ''' '''Upload books to device: '''
return self.device.upload_books(files, names, on_card, return self.device.upload_books(files, names, on_card,
metadata=metadata, end_session=False) metadata=metadata, end_session=False)
def upload_books(self, done, files, names, on_card=False, titles=None, def upload_books(self, done, files, names, on_card=False, titles=None,
metadata=None): metadata=None):
desc = _('Upload %d books to device')%len(names) desc = _('Upload %d books to device')%len(names)
if titles: if titles:
desc += u':' + u', '.join(titles) desc += u':' + u', '.join(titles)
return self.create_job(self._upload_books, done, args=[files, names], return self.create_job(self._upload_books, done, args=[files, names],
kwargs={'on_card':on_card,'metadata':metadata}, description=desc) kwargs={'on_card':on_card,'metadata':metadata}, description=desc)
def add_books_to_metadata(self, locations, metadata, booklists): def add_books_to_metadata(self, locations, metadata, booklists):
self.device.add_books_to_metadata(locations, metadata, booklists) self.device.add_books_to_metadata(locations, metadata, booklists)
def _delete_books(self, paths): def _delete_books(self, paths):
'''Remove books from device''' '''Remove books from device'''
self.device.delete_books(paths, end_session=True) self.device.delete_books(paths, end_session=True)
def delete_books(self, done, paths): def delete_books(self, done, paths):
return self.create_job(self._delete_books, done, args=[paths], return self.create_job(self._delete_books, done, args=[paths],
description=_('Delete books from device')) description=_('Delete books from device'))
def remove_books_from_metadata(self, paths, booklists): def remove_books_from_metadata(self, paths, booklists):
self.device.remove_books_from_metadata(paths, booklists) self.device.remove_books_from_metadata(paths, booklists)
def _save_books(self, paths, target): def _save_books(self, paths, target):
'''Copy books from device to disk''' '''Copy books from device to disk'''
for path in paths: for path in paths:
@ -170,18 +188,502 @@ class DeviceManager(Thread):
f = open(os.path.join(target, name), 'wb') f = open(os.path.join(target, name), 'wb')
self.device.get_file(path, f) self.device.get_file(path, f)
f.close() f.close()
def save_books(self, done, paths, target): def save_books(self, done, paths, target):
return self.create_job(self._save_books, done, args=[paths, target], return self.create_job(self._save_books, done, args=[paths, target],
description=_('Download books from device')) description=_('Download books from device'))
def _view_book(self, path, target): def _view_book(self, path, target):
f = open(target, 'wb') f = open(target, 'wb')
self.device.get_file(path, f) self.device.get_file(path, f)
f.close() f.close()
return target return target
def view_book(self, done, path, target): def view_book(self, done, path, target):
return self.create_job(self._view_book, done, args=[path, target], return self.create_job(self._view_book, done, args=[path, target],
description=_('View book on device')) description=_('View book on device'))
class DeviceAction(QAction):
def __init__(self, dest, delete, specific, icon_path, text, parent=None):
if delete:
text += ' ' + _('and delete from library')
QAction.__init__(self, QIcon(icon_path), text, parent)
self.dest = dest
self.delete = delete
self.specific = specific
self.connect(self, SIGNAL('triggered(bool)'),
lambda x : self.emit(SIGNAL('a_s(QAction)'), self))
def __repr__(self):
return self.__class__.__name__ + ':%s:%s:%s'%(self.dest, self.delete,
self.specific)
class DeviceMenu(QMenu):
def __init__(self, parent=None):
QMenu.__init__(self, parent)
self.group = QActionGroup(self)
self.actions = []
self._memory = []
self.set_default_menu = self.addMenu(_('Set default send to device'
' action'))
opts = email_config().parse()
default_account = None
if opts.accounts:
self.email_to_menu = self.addMenu(_('Email to')+'...')
keys = sorted(opts.accounts.keys())
for account in keys:
formats, auto, default = opts.accounts[account]
dest = 'mail:'+account+';'+formats
if default:
default_account = (dest, False, False, ':/images/mail.svg',
_('Email to')+' '+account)
action1 = DeviceAction(dest, False, False, ':/images/mail.svg',
_('Email to')+' '+account, self)
action2 = DeviceAction(dest, True, False, ':/images/mail.svg',
_('Email to')+' '+account, self)
map(self.email_to_menu.addAction, (action1, action2))
map(self._memory.append, (action1, action2))
self.email_to_menu.addSeparator()
self.connect(action1, SIGNAL('a_s(QAction)'),
self.action_triggered)
self.connect(action2, SIGNAL('a_s(QAction)'),
self.action_triggered)
_actions = [
('main:', False, False, ':/images/reader.svg',
_('Send to main memory')),
('card:0', False, False, ':/images/sd.svg',
_('Send to storage card')),
'-----',
('main:', True, False, ':/images/reader.svg',
_('Send to main memory')),
('card:0', True, False, ':/images/sd.svg',
_('Send to storage card')),
'-----',
('main:', False, True, ':/images/reader.svg',
_('Send specific format to main memory')),
('card:0', False, True, ':/images/sd.svg',
_('Send specific format to storage card')),
]
if default_account is not None:
_actions.insert(2, default_account)
_actions.insert(6, list(default_account))
_actions[6][1] = True
for round in (0, 1):
for dest, delete, specific, icon, text in _actions:
if dest == '-':
(self.set_default_menu if round else self).addSeparator()
continue
action = DeviceAction(dest, delete, specific, icon, text, self)
self._memory.append(action)
if round == 1:
action.setCheckable(True)
action.setText(action.text())
self.group.addAction(action)
self.set_default_menu.addAction(action)
else:
self.connect(action, SIGNAL('a_s(QAction)'),
self.action_triggered)
self.actions.append(action)
self.addAction(action)
da = config['default_send_to_device_action']
done = False
for action in self.group.actions():
if repr(action) == da:
action.setChecked(True)
done = True
break
if not done:
action = list(self.group.actions())[0]
action.setChecked(True)
config['default_send_to_device_action'] = repr(action)
self.connect(self.group, SIGNAL('triggered(QAction*)'),
self.change_default_action)
self.enable_device_actions(False)
if opts.accounts:
self.addSeparator()
self.addMenu(self.email_to_menu)
def change_default_action(self, action):
config['default_send_to_device_action'] = repr(action)
action.setChecked(True)
def action_triggered(self, action):
self.emit(SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
action.dest, action.delete, action.specific)
def trigger_default(self, *args):
r = config['default_send_to_device_action']
for action in self.actions:
if repr(action) == r:
self.action_triggered(action)
break
def enable_device_actions(self, enable):
for action in self.actions:
if action.dest[:4] in ('main', 'card'):
action.setEnabled(enable)
class Emailer(Thread):
def __init__(self, timeout=60):
Thread.__init__(self)
self.setDaemon(True)
self.job_lock = RLock()
self.jobs = []
self._run = True
self.timeout = timeout
def run(self):
while self._run:
job = None
with self.job_lock:
if self.jobs:
job = self.jobs[0]
self.jobs = self.jobs[1:]
if job is not None:
self._send_mails(*job)
time.sleep(1)
def stop(self):
self._run = False
def send_mails(self, jobnames, callback, attachments, to_s, subjects,
texts, attachment_names):
job = (jobnames, callback, attachments, to_s, subjects, texts,
attachment_names)
with self.job_lock:
self.jobs.append(job)
def _send_mails(self, jobnames, callback, attachments,
to_s, subjects, texts, attachment_names):
opts = email_config().parse()
opts.verbose = 3 if os.environ.get('CALIBRE_DEBUG_EMAIL', False) else 0
from_ = opts.from_
if not from_:
from_ = 'calibre <calibre@'+socket.getfqdn()+'>'
results = []
for i, jobname in enumerate(jobnames):
try:
msg = compose_mail(from_, to_s[i], texts[i], subjects[i],
open(attachments[i], 'rb'),
attachment_name = attachment_names[i])
efrom, eto = map(extract_email_address, (from_, to_s[i]))
eto = [eto]
sendmail(msg, efrom, eto, localhost=None,
verbose=opts.verbose,
timeout=self.timeout, relay=opts.relay_host,
username=opts.relay_username,
password=unhexlify(opts.relay_password), port=opts.relay_port,
encryption=opts.encryption)
results.append([jobname, None, None])
except Exception, e:
results.append([jobname, e, traceback.format_exc()])
callback(results)
class DeviceGUI(object):
def dispatch_sync_event(self, dest, delete, specific):
rows = self.library_view.selectionModel().selectedRows()
if not rows or len(rows) == 0:
error_dialog(self, _('No books'), _('No books')+' '+\
_('selected to send')).exec_()
return
fmt = None
if specific:
d = ChooseFormatDialog(self, _('Choose format to send to device'),
self.device_manager.device_class.FORMATS)
d.exec_()
fmt = d.format().lower()
dest, sub_dest = dest.split(':')
if dest in ('main', 'card'):
if not self.device_connected or not self.device_manager:
error_dialog(self, _('No device'),
_('Cannot send: No device is connected')).exec_()
return
on_card = dest == 'card'
if on_card and not self.device_manager.has_card():
error_dialog(self, _('No card'),
_('Cannot send: Device has no storage card')).exec_()
return
self.sync_to_device(on_card, delete, fmt)
elif dest == 'mail':
to, fmts = sub_dest.split(';')
fmts = [x.strip().lower() for x in fmts.split(',')]
self.send_by_mail(to, fmts, delete)
def send_by_mail(self, to, fmts, delete_from_library):
rows = self.library_view.selectionModel().selectedRows()
if not rows or len(rows) == 0:
return
ids = iter(self.library_view.model().id(r) for r in rows)
full_metadata = self.library_view.model().get_metadata(
rows, full_metadata=True)[-1]
files = self.library_view.model().get_preferred_formats(rows,
fmts, paths=True, set_metadata=True)
files = [getattr(f, 'name', None) for f in files]
bad, remove_ids, jobnames = [], [], []
texts, subjects, attachments, attachment_names = [], [], [], []
for f, mi, id in zip(files, full_metadata, ids):
t = mi.title
if not t:
t = _('Unknown')
if f is None:
bad.append(t)
else:
remove_ids.append(id)
jobnames.append(u'%s:%s'%(id, t))
attachments.append(f)
subjects.append(_('E-book:')+ ' '+t)
a = authors_to_string(mi.authors if mi.authors else \
[_('Unknown')])
texts.append(_('Attached, you will find the e-book') + \
'\n\n' + t + '\n\t' + _('by') + ' ' + a + '\n\n' + \
_('in the %s format.') %
os.path.splitext(f)[1][1:].upper())
prefix = sanitize_file_name(t+' - '+a)
if not isinstance(prefix, unicode):
prefix = prefix.decode(preferred_encoding, 'replace')
attachment_names.append(prefix + os.path.splitext(f)[1])
remove = remove_ids if delete_from_library else []
to_s = list(repeat(to, len(attachments)))
if attachments:
self.emailer.send_mails(jobnames,
Dispatcher(partial(self.emails_sent, remove=remove)),
attachments, to_s, subjects, texts, attachment_names)
self.status_bar.showMessage(_('Sending email to')+' '+to, 3000)
if bad:
bad = '\n'.join('<li>%s</li>'%(i,) for i in bad)
d = warning_dialog(self, _('No suitable formats'),
'<p>'+ _('Could not email the following books '
'as no suitable formats were found:<br><ul>%s</ul>')%(bad,))
d.exec_()
def emails_sent(self, results, remove=[]):
errors, good = [], []
for jobname, exception, tb in results:
id = jobname.partition(':')[0]
title = jobname.partition(':')[-1]
if exception is not None:
errors.append([title, exception, tb])
else:
good.append(title)
if errors:
errors = '\n'.join([
'<li><b>%s</b><br>%s<br>%s<br></li>' %
(title, e, tb.replace('\n', '<br>')) for \
title, e, tb in errors
])
ConversionErrorDialog(self, _('Failed to email books'),
'<p>'+_('Failed to email the following books:')+\
'<ul>%s</ul>'%errors,
show=True)
else:
self.status_bar.showMessage(_('Sent by email:') + ', '.join(good),
5000)
def cover_to_thumbnail(self, data):
p = QPixmap()
p.loadFromData(data)
if not p.isNull():
ht = self.device_manager.device_class.THUMBNAIL_HEIGHT \
if self.device_manager else Device.THUMBNAIL_HEIGHT
p = p.scaledToHeight(ht, Qt.SmoothTransformation)
return (p.width(), p.height(), pixmap_to_data(p))
def email_news(self, id):
opts = email_config().parse()
accounts = [(account, [x.strip().lower() for x in x[0].split(',')])
for account, x in opts.accounts.items() if x[1]]
sent_mails = []
for account, fmts in accounts:
files = self.library_view.model().\
get_preferred_formats_from_ids([id], fmts)
files = [f.name for f in files if f is not None]
if not files:
continue
attachment = files[0]
mi = self.library_view.model().db.get_metadata(id,
index_is_id=True)
to_s = [account]
subjects = [_('News:')+' '+mi.title]
texts = [_('Attached is the')+' '+mi.title]
attachment_names = [mi.title+os.path.splitext(attachment)[1]]
attachments = [attachment]
jobnames = ['%s:%s'%(id, mi.title)]
remove = [id] if config['delete_news_from_library_on_upload']\
else []
self.emailer.send_mails(jobnames,
Dispatcher(partial(self.emails_sent, remove=remove)),
attachments, to_s, subjects, texts, attachment_names)
sent_mails.append(to_s[0])
if sent_mails:
self.status_bar.showMessage(_('Sent news to')+' '+\
', '.join(sent_mails), 3000)
def sync_news(self):
if self.device_connected:
ids = list(dynamic.get('news_to_be_synced', set([])))
ids = [id for id in ids if self.library_view.model().db.has_id(id)]
files = self.library_view.model().get_preferred_formats_from_ids(
ids, self.device_manager.device_class.FORMATS)
files = [f for f in files if f is not None]
if not files:
dynamic.set('news_to_be_synced', set([]))
return
metadata = self.library_view.model().get_metadata(ids,
rows_are_ids=True)
names = []
for mi in metadata:
prefix = sanitize_file_name(mi['title'])
if not isinstance(prefix, unicode):
prefix = prefix.decode(preferred_encoding, 'replace')
prefix = ascii_filename(prefix)
names.append('%s_%d%s'%(prefix, id,
os.path.splitext(f.name)[1]))
cdata = mi['cover']
if cdata:
mi['cover'] = self.cover_to_thumbnail(cdata)
dynamic.set('news_to_be_synced', set([]))
if config['upload_news_to_device'] and files:
remove = ids if \
config['delete_news_from_library_on_upload'] else []
on_card = self.location_view.model().free[0] < \
self.location_view.model().free[1]
self.upload_books(files, names, metadata,
on_card=on_card,
memory=[[f.name for f in files], remove])
self.status_bar.showMessage(_('Sending news to device.'), 5000)
def sync_to_device(self, on_card, delete_from_library,
specific_format=None):
rows = self.library_view.selectionModel().selectedRows()
if not self.device_manager or not rows or len(rows) == 0:
return
ids = iter(self.library_view.model().id(r) for r in rows)
metadata = self.library_view.model().get_metadata(rows)
for mi in metadata:
cdata = mi['cover']
if cdata:
mi['cover'] = self.cover_to_thumbnail(cdata)
metadata = iter(metadata)
_files = self.library_view.model().get_preferred_formats(rows,
self.device_manager.device_class.FORMATS,
paths=True, set_metadata=True,
specific_format=specific_format)
files = [getattr(f, 'name', None) for f in _files]
bad, good, gf, names, remove_ids = [], [], [], [], []
for f in files:
mi = metadata.next()
id = ids.next()
if f is None:
bad.append(mi['title'])
else:
remove_ids.append(id)
good.append(mi)
gf.append(f)
t = mi['title']
if not t:
t = _('Unknown')
a = mi['authors']
if not a:
a = _('Unknown')
prefix = sanitize_file_name(t+' - '+a)
if not isinstance(prefix, unicode):
prefix = prefix.decode(preferred_encoding, 'replace')
prefix = ascii_filename(prefix)
names.append('%s_%d%s'%(prefix, id, os.path.splitext(f)[1]))
remove = remove_ids if delete_from_library else []
self.upload_books(gf, names, good, on_card, memory=(_files, remove))
self.status_bar.showMessage(_('Sending books to device.'), 5000)
if bad:
bad = '\n'.join('<li>%s</li>'%(i,) for i in bad)
d = warning_dialog(self, _('No suitable formats'),
_('Could not upload the following books to the device, '
'as no suitable formats were found:<br><ul>%s</ul>')%(bad,))
d.exec_()
def upload_booklists(self):
'''
Upload metadata to device.
'''
self.device_manager.sync_booklists(Dispatcher(self.metadata_synced),
self.booklists())
def metadata_synced(self, job):
'''
Called once metadata has been uploaded.
'''
if job.exception is not None:
self.device_job_exception(job)
return
cp, fs = job.result
self.location_view.model().update_devices(cp, fs)
def upload_books(self, files, names, metadata, on_card=False, memory=None):
'''
Upload books to device.
:param files: List of either paths to files or file like objects
'''
titles = [i['title'] for i in metadata]
job = self.device_manager.upload_books(
Dispatcher(self.books_uploaded),
files, names, on_card=on_card,
metadata=metadata, titles=titles
)
self.upload_memory[job] = (metadata, on_card, memory, files)
def books_uploaded(self, job):
'''
Called once books have been uploaded.
'''
metadata, on_card, memory, files = self.upload_memory.pop(job)
if job.exception is not None:
if isinstance(job.exception, FreeSpaceError):
where = 'in main memory.' if 'memory' in str(job.exception) \
else 'on the storage card.'
titles = '\n'.join(['<li>'+mi['title']+'</li>' \
for mi in metadata])
d = error_dialog(self, _('No space on device'),
_('<p>Cannot upload books to device there '
'is no more free space available ')+where+
'</p>\n<ul>%s</ul>'%(titles,))
d.exec_()
else:
self.device_job_exception(job)
return
self.device_manager.add_books_to_metadata(job.result,
metadata, self.booklists())
self.upload_booklists()
view = self.card_view if on_card else self.memory_view
view.model().resort(reset=False)
view.model().research()
for f in files:
getattr(f, 'close', lambda : True)()
if memory and memory[1]:
self.library_view.model().delete_books_by_id(memory[1])

View File

@ -1,12 +1,13 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, re, time, textwrap import os, re, time, textwrap
from binascii import hexlify, unhexlify
from PyQt4.Qt import QDialog, QMessageBox, QListWidgetItem, QIcon, \ from PyQt4.Qt import QDialog, QMessageBox, QListWidgetItem, QIcon, \
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \ QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
QStringListModel, QAbstractItemModel, \ QStringListModel, QAbstractItemModel, QFont, \
SIGNAL, QTimer, Qt, QSize, QVariant, QUrl, \ SIGNAL, QTimer, Qt, QSize, QVariant, QUrl, \
QModelIndex, QInputDialog QModelIndex, QInputDialog, QAbstractTableModel
from calibre.constants import islinux, iswindows from calibre.constants import islinux, iswindows
from calibre.gui2.dialogs.config_ui import Ui_Dialog from calibre.gui2.dialogs.config_ui import Ui_Dialog
@ -21,14 +22,15 @@ from calibre.library import server_config
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \ from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \
disable_plugin, customize_plugin, \ disable_plugin, customize_plugin, \
plugin_customization, add_plugin, remove_plugin plugin_customization, add_plugin, remove_plugin
from calibre.utils.smtp import config as smtp_prefs
class PluginModel(QAbstractItemModel): class PluginModel(QAbstractItemModel):
def __init__(self, *args): def __init__(self, *args):
QAbstractItemModel.__init__(self, *args) QAbstractItemModel.__init__(self, *args)
self.icon = QVariant(QIcon(':/images/plugins.svg')) self.icon = QVariant(QIcon(':/images/plugins.svg'))
self.populate() self.populate()
def populate(self): def populate(self):
self._data = {} self._data = {}
for plugin in initialized_plugins(): for plugin in initialized_plugins():
@ -37,21 +39,21 @@ class PluginModel(QAbstractItemModel):
else: else:
self._data[plugin.type].append(plugin) self._data[plugin.type].append(plugin)
self.categories = sorted(self._data.keys()) self.categories = sorted(self._data.keys())
def index(self, row, column, parent): def index(self, row, column, parent):
if not self.hasIndex(row, column, parent): if not self.hasIndex(row, column, parent):
return QModelIndex() return QModelIndex()
if parent.isValid(): if parent.isValid():
return self.createIndex(row, column, parent.row()) return self.createIndex(row, column, parent.row())
else: else:
return self.createIndex(row, column, -1) return self.createIndex(row, column, -1)
def parent(self, index): def parent(self, index):
if not index.isValid() or index.internalId() == -1: if not index.isValid() or index.internalId() == -1:
return QModelIndex() return QModelIndex()
return self.createIndex(index.internalId(), 0, -1) return self.createIndex(index.internalId(), 0, -1)
def rowCount(self, parent): def rowCount(self, parent):
if not parent.isValid(): if not parent.isValid():
return len(self.categories) return len(self.categories)
@ -59,14 +61,14 @@ class PluginModel(QAbstractItemModel):
category = self.categories[parent.row()] category = self.categories[parent.row()]
return len(self._data[category]) return len(self._data[category])
return 0 return 0
def columnCount(self, parent): def columnCount(self, parent):
return 1 return 1
def index_to_plugin(self, index): def index_to_plugin(self, index):
category = self.categories[index.parent().row()] category = self.categories[index.parent().row()]
return self._data[category][index.row()] return self._data[category][index.row()]
def plugin_to_index(self, plugin): def plugin_to_index(self, plugin):
for i, category in enumerate(self.categories): for i, category in enumerate(self.categories):
parent = self.index(i, 0, QModelIndex()) parent = self.index(i, 0, QModelIndex())
@ -74,13 +76,13 @@ class PluginModel(QAbstractItemModel):
if plugin == p: if plugin == p:
return self.index(j, 0, parent) return self.index(j, 0, parent)
return QModelIndex() return QModelIndex()
def refresh_plugin(self, plugin, rescan=False): def refresh_plugin(self, plugin, rescan=False):
if rescan: if rescan:
self.populate() self.populate()
idx = self.plugin_to_index(plugin) idx = self.plugin_to_index(plugin)
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), idx, idx) self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), idx, idx)
def flags(self, index): def flags(self, index):
if not index.isValid(): if not index.isValid():
return 0 return 0
@ -90,7 +92,7 @@ class PluginModel(QAbstractItemModel):
if not is_disabled(self.data(index, Qt.UserRole)): if not is_disabled(self.data(index, Qt.UserRole)):
flags |= Qt.ItemIsEnabled flags |= Qt.ItemIsEnabled
return flags return flags
def data(self, index, role): def data(self, index, role):
if not index.isValid(): if not index.isValid():
return NONE return NONE
@ -113,25 +115,140 @@ class PluginModel(QAbstractItemModel):
if role == Qt.UserRole: if role == Qt.UserRole:
return plugin return plugin
return NONE return NONE
class CategoryModel(QStringListModel): class CategoryModel(QStringListModel):
def __init__(self, *args): def __init__(self, *args):
QStringListModel.__init__(self, *args) QStringListModel.__init__(self, *args)
self.setStringList([_('General'), _('Interface'), _('Advanced'), self.setStringList([_('General'), _('Interface'), _('Email\nDelivery'),
_('Content\nServer'), _('Plugins')]) _('Advanced'), _('Content\nServer'), _('Plugins')])
self.icons = list(map(QVariant, map(QIcon, self.icons = list(map(QVariant, map(QIcon,
[':/images/dialog_information.svg', ':/images/lookfeel.svg', [':/images/dialog_information.svg', ':/images/lookfeel.svg',
':/images/view.svg', ':/images/network-server.svg', ':/images/mail.svg', ':/images/view.svg',
':/images/plugins.svg']))) ':/images/network-server.svg', ':/images/plugins.svg'])))
def data(self, index, role): def data(self, index, role):
if role == Qt.DecorationRole: if role == Qt.DecorationRole:
return self.icons[index.row()] return self.icons[index.row()]
return QStringListModel.data(self, index, role) return QStringListModel.data(self, index, role)
class EmailAccounts(QAbstractTableModel):
def __init__(self, accounts):
QAbstractTableModel.__init__(self)
self.accounts = accounts
self.account_order = sorted(self.accounts.keys())
self.headers = map(QVariant, [_('Email'), _('Formats'), _('Auto send')])
self.default_font = QFont()
self.default_font.setBold(True)
self.default_font = QVariant(self.default_font)
self.tooltips =[NONE] + map(QVariant,
[_('Formats to email. The first matching format will be sent.'),
'<p>'+_('If checked, downloaded news will be automatically '
'mailed <br>to this email address '
'(provided it is in one of the listed formats).')])
def rowCount(self, *args):
return len(self.account_order)
def columnCount(self, *args):
return 3
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return self.headers[section]
return NONE
def data(self, index, role):
row, col = index.row(), index.column()
if row < 0 or row >= self.rowCount():
return NONE
account = self.account_order[row]
if role == Qt.UserRole:
return (account, self.accounts[account])
if role == Qt.ToolTipRole:
return self.tooltips[col]
if role == Qt.DisplayRole:
if col == 0:
return QVariant(account)
if col == 1:
return QVariant(self.accounts[account][0])
if role == Qt.FontRole and self.accounts[account][2]:
return self.default_font
if role == Qt.CheckStateRole and col == 2:
return QVariant(Qt.Checked if self.accounts[account][1] else Qt.Unchecked)
return NONE
def flags(self, index):
if index.column() == 2:
return QAbstractTableModel.flags(self, index)|Qt.ItemIsUserCheckable
else:
return QAbstractTableModel.flags(self, index)|Qt.ItemIsEditable
def setData(self, index, value, role):
if not index.isValid():
return False
row, col = index.row(), index.column()
account = self.account_order[row]
if col == 2:
self.accounts[account][1] ^= True
elif col == 1:
self.accounts[account][0] = unicode(value.toString()).upper()
else:
na = unicode(value.toString())
from email.utils import parseaddr
addr = parseaddr(na)[-1]
if not addr:
return False
self.accounts[na] = self.accounts.pop(account)
self.account_order[row] = na
if '@kindle.com' in addr:
self.accounts[na][0] = 'AZW, MOBI, TPZ, PRC, AZW1'
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
self.index(index.row(), 0), self.index(index.row(), 2))
return True
def make_default(self, index):
if index.isValid():
row = index.row()
for x in self.accounts.values():
x[2] = False
self.accounts[self.account_order[row]][2] = True
self.reset()
def add(self):
x = _('new email address')
y = x
c = 0
while y in self.accounts:
c += 1
y = x + str(c)
self.accounts[y] = ['MOBI, EPUB', True,
len(self.account_order) == 0]
self.account_order = sorted(self.accounts.keys())
self.reset()
return self.index(self.account_order.index(y), 0)
def remove(self, index):
if index.isValid():
row = self.index.row()
account = self.account_order[row]
self.accounts.pop(account)
self.account_order = sorted(self.accounts.keys())
has_default = False
for account in self.account_order:
if self.accounts[account][2]:
has_default = True
break
if not has_default and self.account_order:
self.accounts[self.account_order[0]][2] = True
self.reset()
class ConfigDialog(QDialog, Ui_Dialog): class ConfigDialog(QDialog, Ui_Dialog):
@ -141,9 +258,9 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)} self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)}
self.setupUi(self) self.setupUi(self)
self._category_model = CategoryModel() self._category_model = CategoryModel()
self.connect(self.category_view, SIGNAL('activated(QModelIndex)'), lambda i: self.stackedWidget.setCurrentIndex(i.row())) self.category_view.currentChanged = \
self.connect(self.category_view, SIGNAL('clicked(QModelIndex)'), lambda i: self.stackedWidget.setCurrentIndex(i.row())) lambda n, p: self.stackedWidget.setCurrentIndex(n.row())
self.category_view.setModel(self._category_model) self.category_view.setModel(self._category_model)
self.db = db self.db = db
self.server = server self.server = server
@ -151,7 +268,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.location.setText(path if path else '') self.location.setText(path if path else '')
self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse) self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse)
self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact) self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact)
dirs = config['frequently_used_directories'] dirs = config['frequently_used_directories']
rn = config['use_roman_numerals_for_series_number'] rn = config['use_roman_numerals_for_series_number']
self.timeout.setValue(prefs['network_timeout']) self.timeout.setValue(prefs['network_timeout'])
@ -162,20 +279,20 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.connect(self.remove_button, SIGNAL('clicked(bool)'), self.remove_dir) self.connect(self.remove_button, SIGNAL('clicked(bool)'), self.remove_dir)
if not islinux: if not islinux:
self.dirs_box.setVisible(False) self.dirs_box.setVisible(False)
column_map = config['column_map'] column_map = config['column_map']
for col in column_map + [i for i in ALL_COLUMNS if i not in column_map]: for col in column_map + [i for i in ALL_COLUMNS if i not in column_map]:
item = QListWidgetItem(BooksModel.headers[col], self.columns) item = QListWidgetItem(BooksModel.headers[col], self.columns)
item.setData(Qt.UserRole, QVariant(col)) item.setData(Qt.UserRole, QVariant(col))
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable) item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
item.setCheckState(Qt.Checked if col in column_map else Qt.Unchecked) item.setCheckState(Qt.Checked if col in column_map else Qt.Unchecked)
self.connect(self.column_up, SIGNAL('clicked()'), self.up_column) self.connect(self.column_up, SIGNAL('clicked()'), self.up_column)
self.connect(self.column_down, SIGNAL('clicked()'), self.down_column) self.connect(self.column_down, SIGNAL('clicked()'), self.down_column)
self.filename_pattern = FilenamePattern(self) self.filename_pattern = FilenamePattern(self)
self.metadata_box.layout().insertWidget(0, self.filename_pattern) self.metadata_box.layout().insertWidget(0, self.filename_pattern)
icons = config['toolbar_icon_size'] icons = config['toolbar_icon_size']
self.toolbar_button_size.setCurrentIndex(0 if icons == self.ICON_SIZES[0] else 1 if icons == self.ICON_SIZES[1] else 2) self.toolbar_button_size.setCurrentIndex(0 if icons == self.ICON_SIZES[0] else 1 if icons == self.ICON_SIZES[1] else 2)
self.show_toolbar_text.setChecked(config['show_text_in_toolbar']) self.show_toolbar_text.setChecked(config['show_text_in_toolbar'])
@ -183,7 +300,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.book_exts = sorted(BOOK_EXTENSIONS) self.book_exts = sorted(BOOK_EXTENSIONS)
for ext in self.book_exts: for ext in self.book_exts:
self.single_format.addItem(ext.upper(), QVariant(ext)) self.single_format.addItem(ext.upper(), QVariant(ext))
single_format = config['save_to_disk_single_format'] single_format = config['save_to_disk_single_format']
self.single_format.setCurrentIndex(self.book_exts.index(single_format)) self.single_format.setCurrentIndex(self.book_exts.index(single_format))
self.cover_browse.setValue(config['cover_flow_queue_length']) self.cover_browse.setValue(config['cover_flow_queue_length'])
@ -204,9 +321,9 @@ class ConfigDialog(QDialog, Ui_Dialog):
items.sort(cmp=lambda x, y: cmp(x[1], y[1])) items.sort(cmp=lambda x, y: cmp(x[1], y[1]))
for item in items: for item in items:
self.language.addItem(item[1], QVariant(item[0])) self.language.addItem(item[1], QVariant(item[0]))
self.pdf_metadata.setChecked(prefs['read_file_metadata']) self.pdf_metadata.setChecked(prefs['read_file_metadata'])
added_html = False added_html = False
for ext in self.book_exts: for ext in self.book_exts:
ext = ext.lower() ext = ext.lower()
@ -242,7 +359,6 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.priority.setCurrentIndex(p) self.priority.setCurrentIndex(p)
self.priority.setVisible(iswindows) self.priority.setVisible(iswindows)
self.priority_label.setVisible(iswindows) self.priority_label.setVisible(iswindows)
self.category_view.setCurrentIndex(self._category_model.index(0))
self._plugin_model = PluginModel() self._plugin_model = PluginModel()
self.plugin_view.setModel(self._plugin_model) self.plugin_view.setModel(self._plugin_model)
self.connect(self.toggle_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='toggle')) self.connect(self.toggle_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='toggle'))
@ -251,7 +367,77 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.connect(self.button_plugin_browse, SIGNAL('clicked()'), self.find_plugin) self.connect(self.button_plugin_browse, SIGNAL('clicked()'), self.find_plugin)
self.connect(self.button_plugin_add, SIGNAL('clicked()'), self.add_plugin) self.connect(self.button_plugin_add, SIGNAL('clicked()'), self.add_plugin)
self.separate_cover_flow.setChecked(config['separate_cover_flow']) self.separate_cover_flow.setChecked(config['separate_cover_flow'])
self.setup_email_page()
self.category_view.setCurrentIndex(self.category_view.model().index(0))
def setup_email_page(self):
opts = smtp_prefs().parse()
if opts.from_:
self.email_from.setText(opts.from_)
self._email_accounts = EmailAccounts(opts.accounts)
self.email_view.setModel(self._email_accounts)
if opts.relay_host:
self.relay_host.setText(opts.relay_host)
self.relay_port.setValue(opts.relay_port)
if opts.relay_username:
self.relay_username.setText(opts.relay_username)
if opts.relay_password:
self.relay_password.setText(unhexlify(opts.relay_password))
(self.relay_tls if opts.encryption == 'TLS' else self.relay_ssl).setChecked(True)
self.connect(self.relay_use_gmail, SIGNAL('clicked(bool)'),
self.create_gmail_relay)
self.connect(self.relay_show_password, SIGNAL('stateChanged(int)'),
lambda
state:self.relay_password.setEchoMode(self.relay_password.Password if
state == 0 else self.relay_password.Normal))
self.connect(self.email_add, SIGNAL('clicked(bool)'),
self.add_email_account)
self.connect(self.email_make_default, SIGNAL('clicked(bool)'),
lambda c: self._email_accounts.make_default(self.email_view.currentIndex()))
self.email_view.resizeColumnsToContents()
def add_email_account(self, checked):
index = self._email_accounts.add()
self.email_view.setCurrentIndex(index)
self.email_view.resizeColumnsToContents()
self.email_view.edit(index)
def create_gmail_relay(self, *args):
self.relay_username.setText('@gmail.com')
self.relay_password.setText('')
self.relay_host.setText('smtp.gmail.com')
self.relay_port.setValue(587)
self.relay_tls.setChecked(True)
info_dialog(self, _('Finish gmail setup'),
_('Dont forget to enter your gmail username and password')).exec_()
self.relay_username.setFocus(Qt.OtherFocusReason)
self.relay_username.setCursorPosition(0)
def set_email_settings(self):
from_ = unicode(self.email_from.text()).strip()
if self._email_accounts.accounts and not from_:
error_dialog(self, _('Bad configuration'),
_('You must set the From email address')).exec_()
return False
username = unicode(self.relay_username.text()).strip()
password = unicode(self.relay_password.text()).strip()
host = unicode(self.relay_host.text()).strip()
if host and not (username and password):
error_dialog(self, _('Bad configuration'),
_('You must set the username and password for '
'the mail server.')).exec_()
return False
conf = smtp_prefs()
conf.set('from_', from_)
conf.set('accounts', self._email_accounts.accounts)
conf.set('relay_host', host if host else None)
conf.set('relay_port', self.relay_port.value())
conf.set('relay_username', username if username else None)
conf.set('relay_password', hexlify(password))
conf.set('encryption', 'TLS' if self.relay_tls.isChecked() else 'SSL')
return True
def add_plugin(self): def add_plugin(self):
path = unicode(self.plugin_path.text()) path = unicode(self.plugin_path.text())
if path and os.access(path, os.R_OK) and path.lower().endswith('.zip'): if path and os.access(path, os.R_OK) and path.lower().endswith('.zip'):
@ -259,22 +445,22 @@ class ConfigDialog(QDialog, Ui_Dialog):
self._plugin_model.populate() self._plugin_model.populate()
self._plugin_model.reset() self._plugin_model.reset()
else: else:
error_dialog(self, _('No valid plugin path'), error_dialog(self, _('No valid plugin path'),
_('%s is not a valid plugin path')%path).exec_() _('%s is not a valid plugin path')%path).exec_()
def find_plugin(self): def find_plugin(self):
path = choose_files(self, 'choose plugin dialog', _('Choose plugin'), path = choose_files(self, 'choose plugin dialog', _('Choose plugin'),
filters=[('Plugins', ['zip'])], all_files=False, filters=[('Plugins', ['zip'])], all_files=False,
select_only_single_file=True) select_only_single_file=True)
if path: if path:
self.plugin_path.setText(path[0]) self.plugin_path.setText(path[0])
def modify_plugin(self, op=''): def modify_plugin(self, op=''):
index = self.plugin_view.currentIndex() index = self.plugin_view.currentIndex()
if index.isValid(): if index.isValid():
plugin = self._plugin_model.index_to_plugin(index) plugin = self._plugin_model.index_to_plugin(index)
if not plugin.can_be_disabled: if not plugin.can_be_disabled:
error_dialog(self,_('Plugin cannot be disabled'), error_dialog(self,_('Plugin cannot be disabled'),
_('The plugin: %s cannot be disabled')%plugin.name).exec_() _('The plugin: %s cannot be disabled')%plugin.name).exec_()
return return
if op == 'toggle': if op == 'toggle':
@ -286,7 +472,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
if op == 'customize': if op == 'customize':
if not plugin.is_customizable(): if not plugin.is_customizable():
info_dialog(self, _('Plugin not customizable'), info_dialog(self, _('Plugin not customizable'),
_('Plugin: %s does not need customization')%plugin.name).exec_() _('Plugin: %s does not need customization')%plugin.name).exec_()
return return
help = plugin.customization_help() help = plugin.customization_help()
text, ok = QInputDialog.getText(self, _('Customize %s')%plugin.name, text, ok = QInputDialog.getText(self, _('Customize %s')%plugin.name,
@ -299,22 +485,23 @@ class ConfigDialog(QDialog, Ui_Dialog):
self._plugin_model.populate() self._plugin_model.populate()
self._plugin_model.reset() self._plugin_model.reset()
else: else:
error_dialog(self, _('Cannot remove builtin plugin'), error_dialog(self, _('Cannot remove builtin plugin'),
plugin.name + _(' cannot be removed. It is a builtin plugin. Try disabling it instead.')).exec_() plugin.name + _(' cannot be removed. It is a '
'builtin plugin. Try disabling it instead.')).exec_()
def up_column(self): def up_column(self):
idx = self.columns.currentRow() idx = self.columns.currentRow()
if idx > 0: if idx > 0:
self.columns.insertItem(idx-1, self.columns.takeItem(idx)) self.columns.insertItem(idx-1, self.columns.takeItem(idx))
self.columns.setCurrentRow(idx-1) self.columns.setCurrentRow(idx-1)
def down_column(self): def down_column(self):
idx = self.columns.currentRow() idx = self.columns.currentRow()
if idx < self.columns.count()-1: if idx < self.columns.count()-1:
self.columns.insertItem(idx+1, self.columns.takeItem(idx)) self.columns.insertItem(idx+1, self.columns.takeItem(idx))
self.columns.setCurrentRow(idx+1) self.columns.setCurrentRow(idx+1)
def view_server_logs(self): def view_server_logs(self):
from calibre.library.server import log_access_file, log_error_file from calibre.library.server import log_access_file, log_error_file
d = QDialog(self) d = QDialog(self)
@ -336,7 +523,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
except IOError: except IOError:
el.setPlainText('No access log found') el.setPlainText('No access log found')
d.show() d.show()
def set_server_options(self): def set_server_options(self):
c = server_config() c = server_config()
c.set('port', self.port.value()) c.set('port', self.port.value())
@ -345,7 +532,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
if not p: if not p:
p = None p = None
c.set('password', p) c.set('password', p)
def start_server(self): def start_server(self):
self.set_server_options() self.set_server_options()
from calibre.library.server import start_threaded_server from calibre.library.server import start_threaded_server
@ -353,13 +540,13 @@ class ConfigDialog(QDialog, Ui_Dialog):
while not self.server.is_running and self.server.exception is None: while not self.server.is_running and self.server.exception is None:
time.sleep(1) time.sleep(1)
if self.server.exception is not None: if self.server.exception is not None:
error_dialog(self, _('Failed to start content server'), error_dialog(self, _('Failed to start content server'),
unicode(self.server.exception)).exec_() unicode(self.server.exception)).exec_()
return return
self.start.setEnabled(False) self.start.setEnabled(False)
self.test.setEnabled(True) self.test.setEnabled(True)
self.stop.setEnabled(True) self.stop.setEnabled(True)
def stop_server(self): def stop_server(self):
from calibre.library.server import stop_threaded_server from calibre.library.server import stop_threaded_server
stop_threaded_server(self.server) stop_threaded_server(self.server)
@ -367,16 +554,17 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.start.setEnabled(True) self.start.setEnabled(True)
self.test.setEnabled(False) self.test.setEnabled(False)
self.stop.setEnabled(False) self.stop.setEnabled(False)
def test_server(self): def test_server(self):
QDesktopServices.openUrl(QUrl('http://127.0.0.1:'+str(self.port.value()))) QDesktopServices.openUrl(QUrl('http://127.0.0.1:'+str(self.port.value())))
def compact(self, toggled): def compact(self, toggled):
d = Vacuum(self, self.db) d = Vacuum(self, self.db)
d.exec_() d.exec_()
def browse(self): def browse(self):
dir = choose_dir(self, 'database location dialog', 'Select database location') dir = choose_dir(self, 'database location dialog',
_('Select database location'))
if dir: if dir:
self.location.setText(dir) self.location.setText(dir)
@ -393,7 +581,10 @@ class ConfigDialog(QDialog, Ui_Dialog):
def accept(self): def accept(self):
mcs = unicode(self.max_cover_size.text()).strip() mcs = unicode(self.max_cover_size.text()).strip()
if not re.match(r'\d+x\d+', mcs): if not re.match(r'\d+x\d+', mcs):
error_dialog(self, _('Invalid size'), _('The size %s is invalid. must be of the form widthxheight')%mcs).exec_() error_dialog(self, _('Invalid size'),
_('The size %s is invalid. must be of the form widthxheight')%mcs).exec_()
return
if not self.set_email_settings():
return return
config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked()) config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
config['new_version_notification'] = bool(self.new_version_notification.isChecked()) config['new_version_notification'] = bool(self.new_version_notification.isChecked())
@ -429,18 +620,21 @@ class ConfigDialog(QDialog, Ui_Dialog):
if self.viewer.item(i).checkState() == Qt.Checked: if self.viewer.item(i).checkState() == Qt.Checked:
fmts.append(str(self.viewer.item(i).text())) fmts.append(str(self.viewer.item(i).text()))
config['internally_viewed_formats'] = fmts config['internally_viewed_formats'] = fmts
if not path or not os.path.exists(path) or not os.path.isdir(path): if not path or not os.path.exists(path) or not os.path.isdir(path):
d = error_dialog(self, _('Invalid database location'), d = error_dialog(self, _('Invalid database location'),
_('Invalid database location ')+path+_('<br>Must be a directory.')) _('Invalid database location ')+path+
_('<br>Must be a directory.'))
d.exec_() d.exec_()
elif not os.access(path, os.W_OK): elif not os.access(path, os.W_OK):
d = error_dialog(self, _('Invalid database location'), d = error_dialog(self, _('Invalid database location'),
_('Invalid database location.<br>Cannot write to ')+path) _('Invalid database location.<br>Cannot write to ')+path)
d.exec_() d.exec_()
else: else:
self.database_location = os.path.abspath(path) self.database_location = os.path.abspath(path)
self.directories = [qstring_to_unicode(self.directory_list.item(i).text()) for i in range(self.directory_list.count())] self.directories = [
qstring_to_unicode(self.directory_list.item(i).text()) for i in \
range(self.directory_list.count())]
config['frequently_used_directories'] = self.directories config['frequently_used_directories'] = self.directories
QDialog.accept(self) QDialog.accept(self)
@ -448,7 +642,8 @@ class Vacuum(QMessageBox):
def __init__(self, parent, db): def __init__(self, parent, db):
self.db = db self.db = db
QMessageBox.__init__(self, QMessageBox.Information, _('Compacting...'), _('Compacting database. This may take a while.'), QMessageBox.__init__(self, QMessageBox.Information, _('Compacting...'),
_('Compacting database. This may take a while.'),
QMessageBox.NoButton, parent) QMessageBox.NoButton, parent)
QTimer.singleShot(200, self.vacuum) QTimer.singleShot(200, self.vacuum)
@ -456,3 +651,11 @@ class Vacuum(QMessageBox):
self.db.vacuum() self.db.vacuum()
self.accept() self.accept()
if __name__ == '__main__':
from calibre.library.database2 import LibraryDatabase2
from PyQt4.Qt import QApplication
app = QApplication([])
d=ConfigDialog(None, LibraryDatabase2('/tmp'))
d.category_view.setCurrentIndex(d.category_view.model().index(2))
d.show()
app.exec_()

View File

@ -6,7 +6,7 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>755</width> <width>789</width>
<height>557</height> <height>557</height>
</rect> </rect>
</property> </property>
@ -437,12 +437,6 @@
</widget> </widget>
</item> </item>
</layout> </layout>
<zorder>toolbar_button_size</zorder>
<zorder>label_4</zorder>
<zorder>show_toolbar_text</zorder>
<zorder>columns</zorder>
<zorder></zorder>
<zorder>groupBox_3</zorder>
</widget> </widget>
</item> </item>
<item> <item>
@ -507,7 +501,6 @@
</layout> </layout>
</item> </item>
</layout> </layout>
<zorder>columns</zorder>
</widget> </widget>
</item> </item>
<item> <item>
@ -534,16 +527,287 @@
</layout> </layout>
<zorder>roman_numerals</zorder> <zorder>roman_numerals</zorder>
<zorder>groupBox_2</zorder> <zorder>groupBox_2</zorder>
<zorder>groupBox</zorder>
<zorder>systray_icon</zorder> <zorder>systray_icon</zorder>
<zorder>sync_news</zorder> <zorder>sync_news</zorder>
<zorder>delete_news</zorder> <zorder>delete_news</zorder>
<zorder>separate_cover_flow</zorder> <zorder>separate_cover_flow</zorder>
<zorder>systray_notifications</zorder> <zorder>systray_notifications</zorder>
<zorder>groupBox_3</zorder>
<zorder></zorder> <zorder></zorder>
<zorder></zorder> <zorder></zorder>
</widget> </widget>
<widget class="QWidget" name="page_6" >
<layout class="QVBoxLayout" name="verticalLayout_9" >
<item>
<widget class="QLabel" name="label_22" >
<property name="text" >
<string>calibre can send your books to you (or your reader) by email</string>
</property>
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9" >
<item>
<widget class="QLabel" name="label_15" >
<property name="text" >
<string>Send email &amp;from:</string>
</property>
<property name="buddy" >
<cstring>email_from</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="email_from" >
<property name="toolTip" >
<string>&lt;p>This is what will be present in the From: field of emails sent by calibre.&lt;br> Set it to your email address</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8" >
<item>
<widget class="QTableView" name="email_view" >
<property name="selectionMode" >
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_8" >
<item>
<widget class="QToolButton" name="email_add" >
<property name="toolTip" >
<string>Add an email address to which to send books</string>
</property>
<property name="text" >
<string>&amp;Add email</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>
</property>
<property name="iconSize" >
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolButtonStyle" >
<enum>Qt::ToolButtonTextUnderIcon</enum>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="email_make_default" >
<property name="text" >
<string>Make &amp;default</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="email_remove" >
<property name="text" >
<string>&amp;Remove email</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/minus.svg</normaloff>:/images/minus.svg</iconset>
</property>
<property name="iconSize" >
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolButtonStyle" >
<enum>Qt::ToolButtonTextUnderIcon</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_10" >
<item>
<widget class="QGroupBox" name="groupBox_5" >
<property name="toolTip" >
<string>&lt;p>A mail server is useful if the service you are sending mail to only accepts email from well know mail services.</string>
</property>
<property name="title" >
<string>Mail &amp;Server</string>
</property>
<layout class="QGridLayout" name="gridLayout_3" >
<item row="0" column="0" colspan="4" >
<widget class="QLabel" name="label_16" >
<property name="text" >
<string>calibre can &lt;b>optionally&lt;/b> use a server to send mail</string>
</property>
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="QLabel" name="label_17" >
<property name="text" >
<string>&amp;Hostname:</string>
</property>
<property name="buddy" >
<cstring>relay_host</cstring>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2" >
<widget class="QLineEdit" name="relay_host" >
<property name="toolTip" >
<string>The hostname of your mail server. For e.g. smtp.gmail.com</string>
</property>
</widget>
</item>
<item row="1" column="3" >
<layout class="QHBoxLayout" name="horizontalLayout_11" >
<item>
<widget class="QLabel" name="label_18" >
<property name="text" >
<string>&amp;Port:</string>
</property>
<property name="buddy" >
<cstring>relay_port</cstring>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="relay_port" >
<property name="toolTip" >
<string>The port your mail server listens for connections on. The default is 25</string>
</property>
<property name="minimum" >
<number>1</number>
</property>
<property name="maximum" >
<number>65555</number>
</property>
<property name="value" >
<number>25</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0" >
<widget class="QLabel" name="label_19" >
<property name="text" >
<string>&amp;Username:</string>
</property>
<property name="buddy" >
<cstring>relay_username</cstring>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2" >
<widget class="QLineEdit" name="relay_username" >
<property name="toolTip" >
<string>Your username on the mail server</string>
</property>
</widget>
</item>
<item row="3" column="0" >
<widget class="QLabel" name="label_20" >
<property name="text" >
<string>&amp;Password:</string>
</property>
<property name="buddy" >
<cstring>relay_password</cstring>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2" >
<widget class="QLineEdit" name="relay_password" >
<property name="toolTip" >
<string>Your password on the mail server</string>
</property>
<property name="echoMode" >
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="3" column="3" >
<widget class="QCheckBox" name="relay_show_password" >
<property name="text" >
<string>&amp;Show</string>
</property>
</widget>
</item>
<item row="4" column="0" >
<widget class="QLabel" name="label_21" >
<property name="text" >
<string>&amp;Encryption:</string>
</property>
<property name="buddy" >
<cstring>relay_tls</cstring>
</property>
</widget>
</item>
<item row="4" column="1" >
<widget class="QRadioButton" name="relay_tls" >
<property name="toolTip" >
<string>Use TLS encryption when connecting to the mail server. This is the most common.</string>
</property>
<property name="text" >
<string>&amp;TLS</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="2" colspan="2" >
<widget class="QRadioButton" name="relay_ssl" >
<property name="toolTip" >
<string>Use SSL encryption when connecting to the mail server.</string>
</property>
<property name="text" >
<string>&amp;SSL</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QToolButton" name="relay_use_gmail" >
<property name="text" >
<string>Use Gmail</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/gmail_logo.png</normaloff>:/images/gmail_logo.png</iconset>
</property>
<property name="iconSize" >
<size>
<width>48</width>
<height>48</height>
</size>
</property>
<property name="toolButtonStyle" >
<enum>Qt::ToolButtonTextUnderIcon</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2" > <widget class="QWidget" name="page_2" >
<layout class="QVBoxLayout" > <layout class="QVBoxLayout" >
<item> <item>

View File

@ -6,14 +6,14 @@ __docformat__ = 'restructuredtext en'
''' '''
The GUI for conversion to EPUB. The GUI for conversion to EPUB.
''' '''
import os import os, uuid
from PyQt4.Qt import QDialog, QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit, \ from PyQt4.Qt import QDialog, QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit, \
QTextEdit, QCheckBox, Qt, QPixmap, QIcon, QListWidgetItem, SIGNAL QTextEdit, QCheckBox, Qt, QPixmap, QIcon, QListWidgetItem, SIGNAL
from lxml.etree import XPath from lxml.etree import XPath
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.gui2.dialogs.epub_ui import Ui_Dialog from calibre.gui2.dialogs.epub_ui import Ui_Dialog
from calibre.gui2 import error_dialog, choose_images, pixmap_to_data, ResizableDialog from calibre.gui2 import error_dialog, choose_images, pixmap_to_data, ResizableDialog
from calibre.ebooks.epub.from_any import SOURCE_FORMATS, config as epubconfig from calibre.ebooks.epub.from_any import SOURCE_FORMATS, config as epubconfig
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
@ -23,16 +23,16 @@ from calibre.ebooks.metadata import authors_to_string, string_to_authors
class Config(ResizableDialog, Ui_Dialog): class Config(ResizableDialog, Ui_Dialog):
OUTPUT = 'EPUB' OUTPUT = 'EPUB'
def __init__(self, parent, db, row=None, config=epubconfig): def __init__(self, parent, db, row=None, config=epubconfig):
ResizableDialog.__init__(self, parent) ResizableDialog.__init__(self, parent)
self.hide_controls() self.hide_controls()
self.connect(self.category_list, SIGNAL('itemEntered(QListWidgetItem *)'), self.connect(self.category_list, SIGNAL('itemEntered(QListWidgetItem *)'),
self.show_category_help) self.show_category_help)
self.connect(self.cover_button, SIGNAL("clicked()"), self.select_cover) self.connect(self.cover_button, SIGNAL("clicked()"), self.select_cover)
self.cover_changed = False self.cover_changed = False
self.db = db self.db = db
self.id = None self.id = None
@ -52,7 +52,7 @@ class Config(ResizableDialog, Ui_Dialog):
self.setWindowTitle(_('Bulk convert to ')+self.OUTPUT) self.setWindowTitle(_('Bulk convert to ')+self.OUTPUT)
else: else:
self.setWindowTitle((_(u'Convert %s to ')%unicode(self.title.text()))+self.OUTPUT) self.setWindowTitle((_(u'Convert %s to ')%unicode(self.title.text()))+self.OUTPUT)
def hide_controls(self): def hide_controls(self):
self.source_profile_label.setVisible(False) self.source_profile_label.setVisible(False)
self.opt_source_profile.setVisible(False) self.opt_source_profile.setVisible(False)
@ -63,7 +63,7 @@ class Config(ResizableDialog, Ui_Dialog):
self.opt_rescale_images.setVisible(False) self.opt_rescale_images.setVisible(False)
self.opt_ignore_tables.setVisible(False) self.opt_ignore_tables.setVisible(False)
self.opt_prefer_author_sort.setVisible(False) self.opt_prefer_author_sort.setVisible(False)
def initialize(self): def initialize(self):
self.__w = [] self.__w = []
self.__w.append(QIcon(':/images/dialog_information.svg')) self.__w.append(QIcon(':/images/dialog_information.svg'))
@ -76,11 +76,11 @@ class Config(ResizableDialog, Ui_Dialog):
self.item4 = QListWidgetItem(self.__w[-1], _('Chapter Detection').replace(' ','\n'), self.category_list) self.item4 = QListWidgetItem(self.__w[-1], _('Chapter Detection').replace(' ','\n'), self.category_list)
self.setup_tooltips() self.setup_tooltips()
self.initialize_options() self.initialize_options()
def set_help(self, msg): def set_help(self, msg):
if msg and getattr(msg, 'strip', lambda:True)(): if msg and getattr(msg, 'strip', lambda:True)():
self.help_view.setPlainText(msg) self.help_view.setPlainText(msg)
def setup_tooltips(self): def setup_tooltips(self):
for opt in self.config.option_set.preferences: for opt in self.config.option_set.preferences:
g = getattr(self, 'opt_'+opt.name, False) g = getattr(self, 'opt_'+opt.name, False)
@ -90,19 +90,19 @@ class Config(ResizableDialog, Ui_Dialog):
g.setToolTip(help.replace('<', '&lt;').replace('>', '&gt;')) g.setToolTip(help.replace('<', '&lt;').replace('>', '&gt;'))
g.setWhatsThis(help.replace('<', '&lt;').replace('>', '&gt;')) g.setWhatsThis(help.replace('<', '&lt;').replace('>', '&gt;'))
g.__class__.enterEvent = lambda obj, event: self.set_help(getattr(obj, '_help', obj.toolTip())) g.__class__.enterEvent = lambda obj, event: self.set_help(getattr(obj, '_help', obj.toolTip()))
def show_category_help(self, item): def show_category_help(self, item):
text = unicode(item.text()) text = unicode(item.text())
help = { help = {
_('Metadata') : _('Specify metadata such as title and author for the book.\n\nMetadata will be updated in the database as well as the generated %s file.')%self.OUTPUT, _('Metadata') : _('Specify metadata such as title and author for the book.\n\nMetadata will be updated in the database as well as the generated %s file.')%self.OUTPUT,
_('Look & Feel') : _('Adjust the look of the generated ebook by specifying things like font sizes.'), _('Look & Feel') : _('Adjust the look of the generated ebook by specifying things like font sizes.'),
_('Page Setup') : _('Specify the page layout settings like margins.'), _('Page Setup') : _('Specify the page layout settings like margins.'),
_('Chapter Detection') : _('Fine tune the detection of chapter and section headings.'), _('Chapter Detection') : _('Fine tune the detection of chapter and section headings.'),
} }
self.set_help(help[text.replace('\n', ' ')]) self.set_help(help[text.replace('\n', ' ')])
def select_cover(self): def select_cover(self):
files = choose_images(self, 'change cover dialog', files = choose_images(self, 'change cover dialog',
_('Choose cover for ') + unicode(self.title.text())) _('Choose cover for ') + unicode(self.title.text()))
if not files: if not files:
return return
@ -110,7 +110,7 @@ class Config(ResizableDialog, Ui_Dialog):
if _file: if _file:
_file = os.path.abspath(_file) _file = os.path.abspath(_file)
if not os.access(_file, os.R_OK): if not os.access(_file, os.R_OK):
d = error_dialog(self.window, _('Cannot read'), d = error_dialog(self.window, _('Cannot read'),
_('You do not have permission to read the file: ') + _file) _('You do not have permission to read the file: ') + _file)
d.exec_() d.exec_()
return return
@ -118,7 +118,7 @@ class Config(ResizableDialog, Ui_Dialog):
try: try:
cf = open(_file, "rb") cf = open(_file, "rb")
cover = cf.read() cover = cf.read()
except IOError, e: except IOError, e:
d = error_dialog(self.window, _('Error reading file'), d = error_dialog(self.window, _('Error reading file'),
_("<p>There was an error reading from file: <br /><b>") + _file + "</b></p><br />"+str(e)) _("<p>There was an error reading from file: <br /><b>") + _file + "</b></p><br />"+str(e))
d.exec_() d.exec_()
@ -134,14 +134,14 @@ class Config(ResizableDialog, Ui_Dialog):
self.cover.setPixmap(pix) self.cover.setPixmap(pix)
self.cover_changed = True self.cover_changed = True
self.cpixmap = pix self.cpixmap = pix
def initialize_metadata_options(self): def initialize_metadata_options(self):
all_series = self.db.all_series() all_series = self.db.all_series()
all_series.sort(cmp=lambda x, y : cmp(x[1], y[1])) all_series.sort(cmp=lambda x, y : cmp(x[1], y[1]))
for series in all_series: for series in all_series:
self.series.addItem(series[1]) self.series.addItem(series[1])
self.series.setCurrentIndex(-1) self.series.setCurrentIndex(-1)
if self.row is not None: if self.row is not None:
mi = self.db.get_metadata(self.id, index_is_id=True) mi = self.db.get_metadata(self.id, index_is_id=True)
self.title.setText(mi.title) self.title.setText(mi.title)
@ -157,14 +157,14 @@ class Config(ResizableDialog, Ui_Dialog):
self.series.setCurrentIndex(self.series.findText(mi.series)) self.series.setCurrentIndex(self.series.findText(mi.series))
if mi.series_index is not None: if mi.series_index is not None:
self.series_index.setValue(mi.series_index) self.series_index.setValue(mi.series_index)
cover = self.db.cover(self.id, index_is_id=True) cover = self.db.cover(self.id, index_is_id=True)
if cover: if cover:
pm = QPixmap() pm = QPixmap()
pm.loadFromData(cover) pm.loadFromData(cover)
if not pm.isNull(): if not pm.isNull():
self.cover.setPixmap(pm) self.cover.setPixmap(pm)
def get_title_and_authors(self): def get_title_and_authors(self):
title = unicode(self.title.text()).strip() title = unicode(self.title.text()).strip()
if not title: if not title:
@ -172,7 +172,7 @@ class Config(ResizableDialog, Ui_Dialog):
authors = unicode(self.author.text()).strip() authors = unicode(self.author.text()).strip()
authors = string_to_authors(authors) if authors else [_('Unknown')] authors = string_to_authors(authors) if authors else [_('Unknown')]
return title, authors return title, authors
def get_metadata(self): def get_metadata(self):
title, authors = self.get_title_and_authors() title, authors = self.get_title_and_authors()
mi = MetaInformation(title, authors) mi = MetaInformation(title, authors)
@ -191,9 +191,9 @@ class Config(ResizableDialog, Ui_Dialog):
tags = [t.strip() for t in unicode(self.tags.text()).split(',')] tags = [t.strip() for t in unicode(self.tags.text()).split(',')]
if tags: if tags:
mi.tags = tags mi.tags = tags
return mi return mi
def read_settings(self): def read_settings(self):
for pref in self.config.option_set.preferences: for pref in self.config.option_set.preferences:
g = getattr(self, 'opt_'+pref.name, False) g = getattr(self, 'opt_'+pref.name, False)
@ -209,9 +209,9 @@ class Config(ResizableDialog, Ui_Dialog):
elif isinstance(g, QCheckBox): elif isinstance(g, QCheckBox):
self.config.set(pref.name, bool(g.isChecked())) self.config.set(pref.name, bool(g.isChecked()))
if self.row is not None: if self.row is not None:
self.db.set_conversion_options(self.id, self.OUTPUT.lower(), self.config.src) self.db.set_conversion_options(self.id, self.OUTPUT.lower(), self.config.src)
def initialize_options(self): def initialize_options(self):
self.initialize_metadata_options() self.initialize_metadata_options()
values = self.config.parse() values = self.config.parse()
@ -232,16 +232,16 @@ class Config(ResizableDialog, Ui_Dialog):
g.setCurrentIndex(g.findText(val)) g.setCurrentIndex(g.findText(val))
elif isinstance(g, QCheckBox): elif isinstance(g, QCheckBox):
g.setCheckState(Qt.Checked if bool(val) else Qt.Unchecked) g.setCheckState(Qt.Checked if bool(val) else Qt.Unchecked)
def get_source_format(self): def get_source_format(self):
self.source_format = None self.source_format = None
if self.row is not None: if self.row is not None:
temp = self.db.formats(self.id, index_is_id=True) temp = self.db.formats(self.id, index_is_id=True)
if not temp: if not temp:
error_dialog(self.parent(), _('Cannot convert'), error_dialog(self.parent(), _('Cannot convert'),
_('This book has no available formats')).exec_() _('This book has no available formats')).exec_()
available_formats = [f.upper().strip() for f in temp.split(',')] available_formats = [f.upper().strip() for f in temp.split(',')]
choices = [fmt.upper() for fmt in SOURCE_FORMATS if fmt.upper() in available_formats] choices = [fmt.upper() for fmt in SOURCE_FORMATS if fmt.upper() in available_formats]
if not choices: if not choices:
@ -253,7 +253,7 @@ class Config(ResizableDialog, Ui_Dialog):
d = ChooseFormatDialog(self.parent(), _('Choose the format to convert to ')+self.OUTPUT, choices) d = ChooseFormatDialog(self.parent(), _('Choose the format to convert to ')+self.OUTPUT, choices)
if d.exec_() == QDialog.Accepted: if d.exec_() == QDialog.Accepted:
self.source_format = d.format() self.source_format = d.format()
def accept(self): def accept(self):
for opt in ('chapter', 'level1_toc', 'level2_toc', 'level3_toc', 'page', for opt in ('chapter', 'level1_toc', 'level2_toc', 'level3_toc', 'page',
'page_names'): 'page_names'):
@ -263,7 +263,7 @@ class Config(ResizableDialog, Ui_Dialog):
XPath(text,namespaces={'re':'http://exslt.org/regular-expressions'}) XPath(text,namespaces={'re':'http://exslt.org/regular-expressions'})
except Exception, err: except Exception, err:
error_dialog(self, _('Invalid XPath expression'), error_dialog(self, _('Invalid XPath expression'),
_('The expression %s is invalid. Error: %s')%(text, err) _('The expression %s is invalid. Error: %s')%(text, err)
).exec_() ).exec_()
return return
mi = self.get_metadata() mi = self.get_metadata()
@ -272,6 +272,7 @@ class Config(ResizableDialog, Ui_Dialog):
if self.row is not None: if self.row is not None:
self.db.set_metadata(self.id, mi) self.db.set_metadata(self.id, mi)
self.mi = self.db.get_metadata(self.id, index_is_id=True) self.mi = self.db.get_metadata(self.id, index_is_id=True)
self.mi.application_id = uuid.uuid4()
opf = OPFCreator(os.getcwdu(), self.mi) opf = OPFCreator(os.getcwdu(), self.mi)
self.opf_file = PersistentTemporaryFile('.opf') self.opf_file = PersistentTemporaryFile('.opf')
opf.render(self.opf_file) opf.render(self.opf_file)
@ -286,5 +287,5 @@ class Config(ResizableDialog, Ui_Dialog):
self.cover_file = cf self.cover_file = cf
self.opts = self.config.parse() self.opts = self.config.parse()
QDialog.accept(self) QDialog.accept(self)

View File

@ -8,14 +8,15 @@ import time
from PyQt4.QtCore import Qt, QObject, SIGNAL, QVariant, QThread, \ from PyQt4.QtCore import Qt, QObject, SIGNAL, QVariant, QThread, \
QAbstractTableModel, QCoreApplication, QTimer QAbstractTableModel, QCoreApplication, QTimer
from PyQt4.QtGui import QDialog, QItemSelectionModel, QWidget, QLabel, QMovie from PyQt4.QtGui import QDialog, QItemSelectionModel
from calibre.gui2.dialogs.fetch_metadata_ui import Ui_FetchMetadata from calibre.gui2.dialogs.fetch_metadata_ui import Ui_FetchMetadata
from calibre.gui2 import error_dialog, NONE, info_dialog, warning_dialog from calibre.gui2 import error_dialog, NONE, info_dialog
from calibre.gui2.widgets import ProgressIndicator
from calibre.utils.config import prefs from calibre.utils.config import prefs
class Fetcher(QThread): class Fetcher(QThread):
def __init__(self, title, author, publisher, isbn, key): def __init__(self, title, author, publisher, isbn, key):
QThread.__init__(self) QThread.__init__(self)
self.title = title self.title = title
@ -23,81 +24,47 @@ class Fetcher(QThread):
self.publisher = publisher self.publisher = publisher
self.isbn = isbn self.isbn = isbn
self.key = key self.key = key
def run(self): def run(self):
from calibre.ebooks.metadata.fetch import search from calibre.ebooks.metadata.fetch import search
self.results, self.exceptions = search(self.title, self.author, self.results, self.exceptions = search(self.title, self.author,
self.publisher, self.isbn, self.publisher, self.isbn,
self.key if self.key else None) self.key if self.key else None)
class ProgressIndicator(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
self.setGeometry(0, 0, 300, 350)
self.movie = QMovie(':/images/jobs-animated.mng')
self.ml = QLabel(self)
self.ml.setMovie(self.movie)
self.movie.start()
self.movie.setPaused(True)
self.status = QLabel(self)
self.status.setWordWrap(True)
self.status.setAlignment(Qt.AlignHCenter|Qt.AlignTop)
self.status.font().setBold(True)
self.status.font().setPointSize(self.font().pointSize()+6)
self.setVisible(False)
def start(self, msg=''):
view = self.parent()
pwidth, pheight = view.size().width(), view.size().height()
self.resize(pwidth, min(pheight, 250))
self.move(0, (pheight-self.size().height())/2.)
self.ml.resize(self.ml.sizeHint())
self.ml.move(int((self.size().width()-self.ml.size().width())/2.), 0)
self.status.resize(self.size().width(), self.size().height()-self.ml.size().height()-10)
self.status.move(0, self.ml.size().height()+10)
self.status.setText(msg)
self.setVisible(True)
self.movie.setPaused(False)
def stop(self):
if self.movie.state() == self.movie.Running:
self.movie.setPaused(True)
self.setVisible(False)
class Matches(QAbstractTableModel): class Matches(QAbstractTableModel):
def __init__(self, matches): def __init__(self, matches):
self.matches = matches self.matches = matches
self.matches.sort(cmp=lambda b, a: \ self.matches.sort(cmp=lambda b, a: \
cmp(len(a.comments if a.comments else ''), cmp(len(a.comments if a.comments else ''),
len(b.comments if b.comments else ''))) len(b.comments if b.comments else '')))
QAbstractTableModel.__init__(self) QAbstractTableModel.__init__(self)
def rowCount(self, *args): def rowCount(self, *args):
return len(self.matches) return len(self.matches)
def columnCount(self, *args): def columnCount(self, *args):
return 5 return 5
def headerData(self, section, orientation, role): def headerData(self, section, orientation, role):
if role != Qt.DisplayRole: if role != Qt.DisplayRole:
return NONE return NONE
text = "" text = ""
if orientation == Qt.Horizontal: if orientation == Qt.Horizontal:
if section == 0: text = _("Title") if section == 0: text = _("Title")
elif section == 1: text = _("Author(s)") elif section == 1: text = _("Author(s)")
elif section == 2: text = _("Author Sort") elif section == 2: text = _("Author Sort")
elif section == 3: text = _("Publisher") elif section == 3: text = _("Publisher")
elif section == 4: text = _("ISBN") elif section == 4: text = _("ISBN")
return QVariant(text) return QVariant(text)
else: else:
return QVariant(section+1) return QVariant(section+1)
def summary(self, row): def summary(self, row):
return self.matches[row].comments return self.matches[row].comments
def data(self, index, role): def data(self, index, role):
row, col = index.row(), index.column() row, col = index.row(), index.column()
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
@ -119,39 +86,41 @@ class Matches(QAbstractTableModel):
return NONE return NONE
class FetchMetadata(QDialog, Ui_FetchMetadata): class FetchMetadata(QDialog, Ui_FetchMetadata):
def __init__(self, parent, isbn, title, author, publisher, timeout): def __init__(self, parent, isbn, title, author, publisher, timeout):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
Ui_FetchMetadata.__init__(self) Ui_FetchMetadata.__init__(self)
self.setupUi(self) self.setupUi(self)
self.pi = ProgressIndicator(self) self.pi = ProgressIndicator(self)
self.timeout = timeout self.timeout = timeout
QObject.connect(self.fetch, SIGNAL('clicked()'), self.fetch_metadata) QObject.connect(self.fetch, SIGNAL('clicked()'), self.fetch_metadata)
self.key.setText(prefs['isbndb_com_key']) self.key.setText(prefs['isbndb_com_key'])
self.setWindowTitle(title if title else _('Unknown')) self.setWindowTitle(title if title else _('Unknown'))
self.isbn = isbn self.isbn = isbn
self.title = title self.title = title
self.author = author.strip() self.author = author.strip()
self.publisher = publisher self.publisher = publisher
self.previous_row = None self.previous_row = None
self.warning.setVisible(False)
self.connect(self.matches, SIGNAL('activated(QModelIndex)'), self.chosen) self.connect(self.matches, SIGNAL('activated(QModelIndex)'), self.chosen)
self.connect(self.matches, SIGNAL('entered(QModelIndex)'), self.connect(self.matches, SIGNAL('entered(QModelIndex)'),
lambda index:self.matches.setCurrentIndex(index)) self.show_summary)
self.matches.setMouseTracking(True) self.matches.setMouseTracking(True)
self.fetch_metadata() self.fetch_metadata()
def show_summary(self, current, previous): def show_summary(self, current, *args):
row = current.row() row = current.row()
if row != self.previous_row: if row != self.previous_row:
summ = self.model.summary(row) summ = self.model.summary(row)
self.summary.setText(summ if summ else '') self.summary.setText(summ if summ else '')
self.previous_row = row self.previous_row = row
def fetch_metadata(self): def fetch_metadata(self):
self.warning.setVisible(False)
key = str(self.key.text()) key = str(self.key.text())
if key: if key:
prefs['isbndb_com_key'] = key prefs['isbndb_com_key'] = key
@ -173,8 +142,8 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
self._hangcheck = QTimer(self) self._hangcheck = QTimer(self)
self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck) self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck)
self.start_time = time.time() self.start_time = time.time()
self._hangcheck.start() self._hangcheck.start(100)
def hangcheck(self): def hangcheck(self):
if not (self.fetcher.isFinished() or time.time() - self.start_time > 75): if not (self.fetcher.isFinished() or time.time() - self.start_time > 75):
return return
@ -191,22 +160,22 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
self.fetcher.exceptions if x[1] is not None] self.fetcher.exceptions if x[1] is not None]
if warnings: if warnings:
warnings='<br>'.join(['<b>%s</b>: %s'%(name, exc) for name,exc in warnings]) warnings='<br>'.join(['<b>%s</b>: %s'%(name, exc) for name,exc in warnings])
warning_dialog(self, _('Warning'), self.warning.setText('<p><b>'+ _('Warning')+':</b>'+\
'<p>'+_('Could not fetch metadata from:')+\ _('Could not fetch metadata from:')+\
'<br><br>'+warnings+'</p>').exec_() '<br>'+warnings+'</p>')
self.warning.setVisible(True)
if self.model.rowCount() < 1: if self.model.rowCount() < 1:
info_dialog(self, _('No metadata found'), info_dialog(self, _('No metadata found'),
_('No metadata found, try adjusting the title and author ' _('No metadata found, try adjusting the title and author '
'or the ISBN key.')).exec_() 'or the ISBN key.')).exec_()
self.reject()
return return
self.matches.setModel(self.model) self.matches.setModel(self.model)
QObject.connect(self.matches.selectionModel(), QObject.connect(self.matches.selectionModel(),
SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
self.show_summary) self.show_summary)
self.model.reset() self.model.reset()
self.matches.selectionModel().select(self.model.index(0, 0), self.matches.selectionModel().select(self.model.index(0, 0),
QItemSelectionModel.Select | QItemSelectionModel.Rows) QItemSelectionModel.Select | QItemSelectionModel.Rows)
self.matches.setCurrentIndex(self.model.index(0, 0)) self.matches.setCurrentIndex(self.model.index(0, 0))
finally: finally:
@ -214,14 +183,24 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
self.unsetCursor() self.unsetCursor()
self.matches.resizeColumnsToContents() self.matches.resizeColumnsToContents()
self.pi.stop() self.pi.stop()
def terminate(self):
if hasattr(self, 'fetcher') and self.fetcher.isRunning():
self.fetcher.terminate()
def __enter__(self, *args):
return self
def __exit__(self, *args):
self.terminate()
def selected_book(self): def selected_book(self):
try: try:
return self.matches.model().matches[self.matches.currentIndex().row()] return self.matches.model().matches[self.matches.currentIndex().row()]
except: except:
return None return None
def chosen(self, index): def chosen(self, index):
self.matches.setCurrentIndex(index) self.matches.setCurrentIndex(index)
self.accept() self.accept()

View File

@ -23,7 +23,7 @@
<item> <item>
<widget class="QLabel" name="tlabel" > <widget class="QLabel" name="tlabel" >
<property name="text" > <property name="text" >
<string>&lt;p>calibre can find metadata for your books from two locations: &lt;b>Google Books&lt;/b> and &lt;b>isbndb.com&lt;/b>. &lt;p>To use isbndb.com you must sign up for a &lt;a href="http://www.isbndb.com">free account&lt;/a> and exter you access key below.</string> <string>&lt;p>calibre can find metadata for your books from two locations: &lt;b>Google Books&lt;/b> and &lt;b>isbndb.com&lt;/b>. &lt;p>To use isbndb.com you must sign up for a &lt;a href="http://www.isbndb.com">free account&lt;/a> and enter your access key below.</string>
</property> </property>
<property name="alignment" > <property name="alignment" >
<set>Qt::AlignCenter</set> <set>Qt::AlignCenter</set>
@ -60,6 +60,16 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<widget class="QLabel" name="warning" >
<property name="text" >
<string/>
</property>
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
<item> <item>
<widget class="QGroupBox" name="groupBox" > <widget class="QGroupBox" name="groupBox" >
<property name="title" > <property name="title" >

View File

@ -10,10 +10,10 @@ from calibre.gui2.dialogs.jobs_ui import Ui_JobsDialog
from calibre import __appname__ from calibre import __appname__
class ProgressBarDelegate(QAbstractItemDelegate): class ProgressBarDelegate(QAbstractItemDelegate):
def sizeHint(self, option, index): def sizeHint(self, option, index):
return QSize(120, 30) return QSize(120, 30)
def paint(self, painter, option, index): def paint(self, painter, option, index):
opts = QStyleOptionProgressBarV2() opts = QStyleOptionProgressBarV2()
opts.rect = option.rect opts.rect = option.rect
@ -44,20 +44,23 @@ class JobsDialog(QDialog, Ui_JobsDialog):
self.jobs_view.model().kill_job) self.jobs_view.model().kill_job)
self.pb_delegate = ProgressBarDelegate(self) self.pb_delegate = ProgressBarDelegate(self)
self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate) self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate)
self.running_time_timer = QTimer(self) self.running_time_timer = QTimer(self)
self.connect(self.running_time_timer, SIGNAL('timeout()'), self.update_running_time) self.connect(self.running_time_timer, SIGNAL('timeout()'), self.update_running_time)
self.running_time_timer.start(1000) self.running_time_timer.start(1000)
def update_running_time(self, *args): def update_running_time(self, *args):
self.model.running_time_updated() try:
self.model.running_time_updated()
except: # Raises random exceptions on OS X
pass
def kill_job(self): def kill_job(self):
for index in self.jobs_view.selectedIndexes(): for index in self.jobs_view.selectedIndexes():
row = index.row() row = index.row()
self.model.kill_job(row, self) self.model.kill_job(row, self)
return return
def closeEvent(self, e): def closeEvent(self, e):
self.jobs_view.write_settings() self.jobs_view.write_settings()
e.accept() e.accept()

View File

@ -70,14 +70,14 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
pub = qstring_to_unicode(self.publisher.text()) pub = qstring_to_unicode(self.publisher.text())
if pub: if pub:
self.db.set_publisher(id, pub, notify=False) self.db.set_publisher(id, pub, notify=False)
tags = qstring_to_unicode(self.tags.text()).strip()
if tags:
tags = map(lambda x: x.strip(), tags.split(','))
self.db.set_tags(id, tags, append=True, notify=False)
remove_tags = qstring_to_unicode(self.remove_tags.text()).strip() remove_tags = qstring_to_unicode(self.remove_tags.text()).strip()
if remove_tags: if remove_tags:
remove_tags = [i.strip() for i in remove_tags.split(',')] remove_tags = [i.strip() for i in remove_tags.split(',')]
self.db.unapply_tags(id, remove_tags, notify=False) self.db.unapply_tags(id, remove_tags, notify=False)
tags = qstring_to_unicode(self.tags.text()).strip()
if tags:
tags = map(lambda x: x.strip(), tags.split(','))
self.db.set_tags(id, tags, append=True, notify=False)
if self.write_series: if self.write_series:
self.db.set_series(id, qstring_to_unicode(self.series.currentText()), notify=False) self.db.set_series(id, qstring_to_unicode(self.series.currentText()), notify=False)

View File

@ -1,12 +1,13 @@
from __future__ import with_statement
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
''' '''
The dialog used to edit meta information for a book as well as The dialog used to edit meta information for a book as well as
add/remove formats add/remove formats
''' '''
import os import os, time, traceback
from PyQt4.QtCore import SIGNAL, QObject, QCoreApplication, Qt from PyQt4.QtCore import SIGNAL, QObject, QCoreApplication, Qt, QTimer, QThread
from PyQt4.QtGui import QPixmap, QListWidgetItem, QErrorMessage, QDialog, QCompleter from PyQt4.QtGui import QPixmap, QListWidgetItem, QErrorMessage, QDialog, QCompleter
@ -16,39 +17,60 @@ from calibre.gui2.dialogs.metadata_single_ui import Ui_MetadataSingleDialog
from calibre.gui2.dialogs.fetch_metadata import FetchMetadata from calibre.gui2.dialogs.fetch_metadata import FetchMetadata
from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.gui2.dialogs.password import PasswordDialog from calibre.gui2.dialogs.password import PasswordDialog
from calibre.gui2.widgets import ProgressIndicator
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata import authors_to_sort_string, string_to_authors, authors_to_string from calibre.ebooks.metadata import authors_to_sort_string, string_to_authors, authors_to_string
from calibre.ebooks.metadata.library_thing import login, cover_from_isbn, LibraryThingError from calibre.ebooks.metadata.library_thing import login, cover_from_isbn
from calibre import islinux from calibre import islinux
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.customize.ui import run_plugins_on_import from calibre.customize.ui import run_plugins_on_import
class CoverFetcher(QThread):
def __init__(self, username, password, isbn, timeout):
self.username = username
self.password = password
self.timeout = timeout
self.isbn = isbn
QThread.__init__(self)
self.exception = self.traceback = self.cover_data = None
def run(self):
try:
login(self.username, self.password, force=False)
self.cover_data = cover_from_isbn(self.isbn, timeout=self.timeout)[0]
except Exception, e:
self.exception = e
self.traceback = traceback.format_exc()
class Format(QListWidgetItem): class Format(QListWidgetItem):
def __init__(self, parent, ext, size, path=None): def __init__(self, parent, ext, size, path=None):
self.path = path self.path = path
self.ext = ext self.ext = ext
self.size = float(size)/(1024*1024) self.size = float(size)/(1024*1024)
text = '%s (%.2f MB)'%(self.ext.upper(), self.size) text = '%s (%.2f MB)'%(self.ext.upper(), self.size)
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext), QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
text, parent, QListWidgetItem.UserType) text, parent, QListWidgetItem.UserType)
class AuthorCompleter(QCompleter): class AuthorCompleter(QCompleter):
def __init__(self, db): def __init__(self, db):
all_authors = db.all_authors() all_authors = db.all_authors()
all_authors.sort(cmp=lambda x, y : cmp(x[1], y[1])) all_authors.sort(cmp=lambda x, y : cmp(x[1], y[1]))
QCompleter.__init__(self, [x[1] for x in all_authors]) QCompleter.__init__(self, [x[1] for x in all_authors])
class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
def do_reset_cover(self, *args): def do_reset_cover(self, *args):
pix = QPixmap(':/images/book.svg') pix = QPixmap(':/images/book.svg')
self.cover.setPixmap(pix) self.cover.setPixmap(pix)
self.cover_changed = True self.cover_changed = True
def select_cover(self, checked): def select_cover(self, checked):
files = choose_images(self, 'change cover dialog', files = choose_images(self, 'change cover dialog',
u'Choose cover for ' + qstring_to_unicode(self.title.text())) u'Choose cover for ' + qstring_to_unicode(self.title.text()))
if not files: if not files:
return return
@ -56,7 +78,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if _file: if _file:
_file = os.path.abspath(_file) _file = os.path.abspath(_file)
if not os.access(_file, os.R_OK): if not os.access(_file, os.R_OK):
d = error_dialog(self.window, _('Cannot read'), d = error_dialog(self.window, _('Cannot read'),
_('You do not have permission to read the file: ') + _file) _('You do not have permission to read the file: ') + _file)
d.exec_() d.exec_()
return return
@ -64,7 +86,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
try: try:
cf = open(_file, "rb") cf = open(_file, "rb")
cover = cf.read() cover = cf.read()
except IOError, e: except IOError, e:
d = error_dialog(self.window, _('Error reading file'), d = error_dialog(self.window, _('Error reading file'),
_("<p>There was an error reading from file: <br /><b>") + _file + "</b></p><br />"+str(e)) _("<p>There was an error reading from file: <br /><b>") + _file + "</b></p><br />"+str(e))
d.exec_() d.exec_()
@ -78,15 +100,15 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.cover_path.setText(_file) self.cover_path.setText(_file)
self.cover.setPixmap(pix) self.cover.setPixmap(pix)
self.cover_changed = True self.cover_changed = True
self.cpixmap = pix self.cpixmap = pix
def add_format(self, x): def add_format(self, x):
files = choose_files(self, 'add formats dialog', files = choose_files(self, 'add formats dialog',
"Choose formats for " + qstring_to_unicode((self.title.text())), "Choose formats for " + qstring_to_unicode((self.title.text())),
[('Books', BOOK_EXTENSIONS)]) [('Books', BOOK_EXTENSIONS)])
if not files: if not files:
return return
for _file in files: for _file in files:
_file = os.path.abspath(_file) _file = os.path.abspath(_file)
if not os.access(_file, os.R_OK): if not os.access(_file, os.R_OK):
@ -103,13 +125,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
break break
Format(self.formats, ext, size, path=_file) Format(self.formats, ext, size, path=_file)
self.formats_changed = True self.formats_changed = True
def remove_format(self, x): def remove_format(self, x):
rows = self.formats.selectionModel().selectedRows(0) rows = self.formats.selectionModel().selectedRows(0)
for row in rows: for row in rows:
self.formats.takeItem(row.row()) self.formats.takeItem(row.row())
self.formats_changed = True self.formats_changed = True
def set_cover(self): def set_cover(self):
row = self.formats.currentRow() row = self.formats.currentRow()
fmt = self.formats.item(row) fmt = self.formats.item(row)
@ -134,19 +156,19 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
elif mi.cover_data[1] is not None: elif mi.cover_data[1] is not None:
cdata = mi.cover_data[1] cdata = mi.cover_data[1]
if cdata is None: if cdata is None:
error_dialog(self, _('Could not read cover'), error_dialog(self, _('Could not read cover'),
_('Could not read cover from %s format')%ext).exec_() _('Could not read cover from %s format')%ext).exec_()
return return
pix = QPixmap() pix = QPixmap()
pix.loadFromData(cdata) pix.loadFromData(cdata)
if pix.isNull(): if pix.isNull():
error_dialog(self, _('Could not read cover'), error_dialog(self, _('Could not read cover'),
_('The cover in the %s format is invalid')%ext).exec_() _('The cover in the %s format is invalid')%ext).exec_()
return return
self.cover.setPixmap(pix) self.cover.setPixmap(pix)
self.cover_changed = True self.cover_changed = True
self.cpixmap = pix self.cpixmap = pix
def sync_formats(self): def sync_formats(self):
old_extensions, new_extensions, paths = set(), set(), {} old_extensions, new_extensions, paths = set(), set(), {}
for row in range(self.formats.count()): for row in range(self.formats.count()):
@ -166,12 +188,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
for ext in db_extensions: for ext in db_extensions:
if ext not in extensions: if ext not in extensions:
self.db.remove_format(self.row, ext, notify=False) self.db.remove_format(self.row, ext, notify=False)
def __init__(self, window, row, db, accepted_callback=None): def __init__(self, window, row, db, accepted_callback=None):
ResizableDialog.__init__(self, window) ResizableDialog.__init__(self, window)
self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter) self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter)
self.splitter.setStretchFactor(100, 1) self.splitter.setStretchFactor(100, 1)
self.db = db self.db = db
self.pi = ProgressIndicator(self)
self.accepted_callback = accepted_callback self.accepted_callback = accepted_callback
self.id = db.id(row) self.id = db.id(row)
self.row = row self.row = row
@ -189,12 +212,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.add_format) self.add_format)
QObject.connect(self.remove_format_button, SIGNAL("clicked(bool)"), \ QObject.connect(self.remove_format_button, SIGNAL("clicked(bool)"), \
self.remove_format) self.remove_format)
QObject.connect(self.fetch_metadata_button, SIGNAL('clicked()'), QObject.connect(self.fetch_metadata_button, SIGNAL('clicked()'),
self.fetch_metadata) self.fetch_metadata)
QObject.connect(self.fetch_cover_button, SIGNAL('clicked()'), QObject.connect(self.fetch_cover_button, SIGNAL('clicked()'),
self.fetch_cover) self.fetch_cover)
QObject.connect(self.tag_editor_button, SIGNAL('clicked()'), QObject.connect(self.tag_editor_button, SIGNAL('clicked()'),
self.edit_tags) self.edit_tags)
QObject.connect(self.remove_series_button, SIGNAL('clicked()'), QObject.connect(self.remove_series_button, SIGNAL('clicked()'),
self.remove_unused_series) self.remove_unused_series)
@ -220,28 +243,28 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
tags = self.db.tags(row) tags = self.db.tags(row)
self.tags.setText(tags if tags else '') self.tags.setText(tags if tags else '')
rating = self.db.rating(row) rating = self.db.rating(row)
if rating > 0: if rating > 0:
self.rating.setValue(int(rating/2.)) self.rating.setValue(int(rating/2.))
comments = self.db.comments(row) comments = self.db.comments(row)
self.comments.setPlainText(comments if comments else '') self.comments.setPlainText(comments if comments else '')
cover = self.db.cover(row) cover = self.db.cover(row)
exts = self.db.formats(row) exts = self.db.formats(row)
if exts: if exts:
exts = exts.split(',') exts = exts.split(',')
for ext in exts: for ext in exts:
if not ext: if not ext:
ext = '' ext = ''
size = self.db.sizeof_format(row, ext) size = self.db.sizeof_format(row, ext)
Format(self.formats, ext, size) Format(self.formats, ext, size)
self.initialize_series_and_publisher() self.initialize_series_and_publisher()
self.series_index.setValue(self.db.series_index(row)) self.series_index.setValue(self.db.series_index(row))
QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.enable_series_index) QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.enable_series_index)
QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.enable_series_index) QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.enable_series_index)
QObject.connect(self.password_button, SIGNAL('clicked()'), self.change_password) QObject.connect(self.password_button, SIGNAL('clicked()'), self.change_password)
self.show() self.show()
height_of_rest = self.frameGeometry().height() - self.cover.height() height_of_rest = self.frameGeometry().height() - self.cover.height()
@ -252,14 +275,14 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if cover: if cover:
pm = QPixmap() pm = QPixmap()
pm.loadFromData(cover) pm.loadFromData(cover)
if not pm.isNull(): if not pm.isNull():
self.cover.setPixmap(pm) self.cover.setPixmap(pm)
def deduce_author_sort(self): def deduce_author_sort(self):
au = unicode(self.authors.text()) au = unicode(self.authors.text())
authors = string_to_authors(au) authors = string_to_authors(au)
self.author_sort.setText(authors_to_sort_string(authors)) self.author_sort.setText(authors_to_sort_string(authors))
def swap_title_author(self): def swap_title_author(self):
title = self.title.text() title = self.title.text()
self.title.setText(self.authors.text()) self.title.setText(self.authors.text())
@ -268,7 +291,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
def cover_dropped(self): def cover_dropped(self):
self.cover_changed = True self.cover_changed = True
def initialize_series(self): def initialize_series(self):
all_series = self.db.all_series() all_series = self.db.all_series()
all_series.sort(cmp=lambda x, y : cmp(x[1], y[1])) all_series.sort(cmp=lambda x, y : cmp(x[1], y[1]))
@ -280,19 +303,19 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
idx = c idx = c
self.series.addItem(name) self.series.addItem(name)
c += 1 c += 1
self.series.lineEdit().setText('') self.series.lineEdit().setText('')
if idx is not None: if idx is not None:
self.series.setCurrentIndex(idx) self.series.setCurrentIndex(idx)
self.enable_series_index() self.enable_series_index()
pl = self.series.parentWidget().layout() pl = self.series.parentWidget().layout()
for i in range(pl.count()): for i in range(pl.count()):
l = pl.itemAt(i).layout() l = pl.itemAt(i).layout()
if l: if l:
l.invalidate() l.invalidate()
l.activate() l.activate()
def initialize_series_and_publisher(self): def initialize_series_and_publisher(self):
self.initialize_series() self.initialize_series()
all_publishers = self.db.all_publishers() all_publishers = self.db.all_publishers()
@ -305,73 +328,93 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
idx = c idx = c
self.publisher.addItem(name) self.publisher.addItem(name)
c += 1 c += 1
self.publisher.setEditText('') self.publisher.setEditText('')
if idx is not None: if idx is not None:
self.publisher.setCurrentIndex(idx) self.publisher.setCurrentIndex(idx)
self.layout().activate() self.layout().activate()
def edit_tags(self): def edit_tags(self):
d = TagEditor(self, self.db, self.row) d = TagEditor(self, self.db, self.row)
d.exec_() d.exec_()
if d.result() == QDialog.Accepted: if d.result() == QDialog.Accepted:
tag_string = ', '.join(d.tags) tag_string = ', '.join(d.tags)
self.tags.setText(tag_string) self.tags.setText(tag_string)
def lt_password_dialog(self): def lt_password_dialog(self):
return PasswordDialog(self, 'LibraryThing account', return PasswordDialog(self, 'LibraryThing account',
_('<p>Enter your username and password for <b>LibraryThing.com</b>. <br/>If you do not have one, you can <a href=\'http://www.librarything.com\'>register</a> for free!.</p>')) _('<p>Enter your username and password for <b>LibraryThing.com</b>. <br/>If you do not have one, you can <a href=\'http://www.librarything.com\'>register</a> for free!.</p>'))
def change_password(self): def change_password(self):
d = self.lt_password_dialog() d = self.lt_password_dialog()
d.exec_() d.exec_()
def fetch_cover(self): def fetch_cover(self):
isbn = qstring_to_unicode(self.isbn.text()) isbn = qstring_to_unicode(self.isbn.text())
if isbn: if isbn:
d = self.lt_password_dialog() d = self.lt_password_dialog()
if not d.username() or not d.password(): if not d.username() or not d.password():
d.exec_() d.exec_()
if d.result() != PasswordDialog.Accepted: if d.result() != PasswordDialog.Accepted:
return return
self.fetch_cover_button.setEnabled(False) self.fetch_cover_button.setEnabled(False)
self.setCursor(Qt.WaitCursor) self.setCursor(Qt.WaitCursor)
QCoreApplication.instance().processEvents() self.cover_fetcher = CoverFetcher(d.username(), d.password(), isbn,
try: self.timeout)
login(d.username(), d.password(), force=False) self.cover_fetcher.start()
cover_data = cover_from_isbn(isbn, timeout=self.timeout)[0] self._hangcheck = QTimer(self)
self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck)
pix = QPixmap() self.cf_start_time = time.time()
pix.loadFromData(cover_data) self.pi.start(_('Downloading cover...'))
if pix.isNull(): self._hangcheck.start(100)
error_dialog(self.window, _('Bad cover'),
_('The cover is not a valid picture')).exec_()
else:
self.cover.setPixmap(pix)
self.cover_changed = True
self.cpixmap = pix
except LibraryThingError, err:
error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+repr(err)).exec_()
finally:
self.fetch_cover_button.setEnabled(True)
self.unsetCursor()
else: else:
error_dialog(self, _('Cannot fetch cover'), error_dialog(self, _('Cannot fetch cover'),
_('You must specify the ISBN identifier for this book.')).exec_() _('You must specify the ISBN identifier for this book.')).exec_()
def hangcheck(self):
if not (self.cover_fetcher.isFinished() or time.time()-self.cf_start_time > 150):
return
self._hangcheck.stop()
try:
if self.cover_fetcher.isRunning():
self.cover_fetcher.terminate()
error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+
_('The download timed out.')).exec_()
return
if self.cover_fetcher.exception is not None:
err = self.cover_fetcher.exception
error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+repr(err)).exec_()
return
pix = QPixmap()
pix.loadFromData(self.cover_fetcher.cover_data)
if pix.isNull():
error_dialog(self.window, _('Bad cover'),
_('The cover is not a valid picture')).exec_()
else:
self.cover.setPixmap(pix)
self.cover_changed = True
self.cpixmap = pix
finally:
self.fetch_cover_button.setEnabled(True)
self.unsetCursor()
self.pi.stop()
def fetch_metadata(self): def fetch_metadata(self):
isbn = qstring_to_unicode(self.isbn.text()) isbn = qstring_to_unicode(self.isbn.text())
title = qstring_to_unicode(self.title.text()) title = qstring_to_unicode(self.title.text())
author = string_to_authors(unicode(self.authors.text()))[0] author = string_to_authors(unicode(self.authors.text()))[0]
publisher = qstring_to_unicode(self.publisher.currentText()) publisher = qstring_to_unicode(self.publisher.currentText())
if isbn or title or author or publisher: if isbn or title or author or publisher:
d = FetchMetadata(self, isbn, title, author, publisher, self.timeout) d = FetchMetadata(self, isbn, title, author, publisher, self.timeout)
d.exec_() with d:
d.exec_()
if d.result() == QDialog.Accepted: if d.result() == QDialog.Accepted:
book = d.selected_book() book = d.selected_book()
if book: if book:
@ -387,11 +430,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
prefix += '\n' prefix += '\n'
self.comments.setText(prefix + summ) self.comments.setText(prefix + summ)
else: else:
error_dialog(self, 'Cannot fetch metadata', 'You must specify at least one of ISBN, Title, Authors or Publisher') error_dialog(self, _('Cannot fetch metadata'),
_('You must specify at least one of ISBN, Title, '
'Authors or Publisher'))
def enable_series_index(self, *args): def enable_series_index(self, *args):
self.series_index.setEnabled(True) self.series_index.setEnabled(True)
def remove_unused_series(self): def remove_unused_series(self):
self.db.remove_unused_series() self.db.remove_unused_series()
idx = qstring_to_unicode(self.series.currentText()) idx = qstring_to_unicode(self.series.currentText())
@ -402,15 +447,15 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if qstring_to_unicode(self.series.itemText(i)) == idx: if qstring_to_unicode(self.series.itemText(i)) == idx:
self.series.setCurrentIndex(i) self.series.setCurrentIndex(i)
break break
def accept(self): def accept(self):
if self.formats_changed: if self.formats_changed:
self.sync_formats() self.sync_formats()
title = qstring_to_unicode(self.title.text()) title = qstring_to_unicode(self.title.text())
self.db.set_title(self.id, title, notify=False) self.db.set_title(self.id, title, notify=False)
au = unicode(self.authors.text()) au = unicode(self.authors.text())
if au: if au:
self.db.set_authors(self.id, string_to_authors(au), notify=False) self.db.set_authors(self.id, string_to_authors(au), notify=False)
aus = qstring_to_unicode(self.author_sort.text()) aus = qstring_to_unicode(self.author_sort.text())
if aus: if aus:

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,270 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:i="http://ns.adobe.com/AdobeIllustrator/10.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
id="svg3007"
sodipodi:version="0.32"
inkscape:version="0.45.1"
version="1.0"
sodipodi:docbase="/home/david/Documents/Projects/KDE/Oxygen/kdelibs/scalable/actions"
sodipodi:docname="mail.svgz"
inkscape:output_extension="org.inkscape.output.svgz.inkscape"
inkscape:export-filename="/home/david/Documents/Projects/KDE/Oxygen/kdelibs/scalable/actions/mail.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs3009">
<linearGradient
id="polygon3293_1_"
gradientUnits="userSpaceOnUse"
x1="615.5"
y1="-584.6021"
x2="615.5"
y2="-595.8521"
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)">
<stop
offset="0"
style="stop-color:#6193CF"
id="stop2997" />
<stop
offset="1"
style="stop-color:#EEEEEE"
id="stop2999" />
</linearGradient>
<linearGradient
id="polygon3286_1_"
gradientUnits="userSpaceOnUse"
x1="615.5"
y1="-589.8511"
x2="615.5"
y2="-580.6011"
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)">
<stop
offset="0"
style="stop-color:#6193CF"
id="stop2991" />
<stop
offset="1"
style="stop-color:#D1DFF1"
id="stop2993" />
</linearGradient>
<linearGradient
id="rect3244_1_"
gradientUnits="userSpaceOnUse"
x1="59.9995"
y1="4"
x2="59.9995"
y2="72.0005"
gradientTransform="matrix(1,0,0,1.0588235,0,-0.2352941)">
<stop
offset="0"
style="stop-color:#A4C0E4"
id="stop2983" />
<stop
offset="0.25"
style="stop-color:#D1DFF1"
id="stop2985" />
<stop
offset="0.85"
style="stop-color:#FFFFFF"
id="stop2987" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#rect3244_1_"
id="linearGradient2212"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,1.0588235,0,-0.2352941)"
x1="59.9995"
y1="4"
x2="59.9995"
y2="72.0005" />
<linearGradient
inkscape:collect="always"
xlink:href="#polygon3286_1_"
id="linearGradient2214"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)"
x1="615.5"
y1="-589.8511"
x2="615.5"
y2="-580.6011" />
<linearGradient
inkscape:collect="always"
xlink:href="#polygon3293_1_"
id="linearGradient2216"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)"
x1="615.5"
y1="-584.6021"
x2="615.5"
y2="-595.8521" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
gridtolerance="10000"
guidetolerance="10"
objecttolerance="10"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="64"
inkscape:cy="64"
inkscape:document-units="px"
inkscape:current-layer="g2972"
width="128px"
height="128px"
inkscape:showpageshadow="false"
inkscape:window-width="794"
inkscape:window-height="731"
inkscape:window-x="0"
inkscape:window-y="0"
showgrid="true"
gridspacingx="4px"
gridspacingy="4px"
gridempspacing="2"
showborder="false" />
<metadata
id="metadata3012">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<switch
id="switch2966"
transform="translate(4,12)">
<foreignObject
requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/"
x="0"
y="0"
width="1"
height="1"
id="foreignObject2968">
<i:pgfRef
xlink:href="#adobe_illustrator_pgf" />
</foreignObject>
<g
i:extraneous="self"
id="g2970">
<g
id="g2972">
<linearGradient
id="linearGradient3033"
gradientUnits="userSpaceOnUse"
x1="59.9995"
y1="4"
x2="59.9995"
y2="72.000504">
<stop
offset="0"
style="stop-color:#A4C0E4"
id="stop3035" />
<stop
offset="0.25"
style="stop-color:#D1DFF1"
id="stop3037" />
<stop
offset="0.85"
style="stop-color:#FFFFFF"
id="stop3039" />
</linearGradient>
<linearGradient
id="linearGradient3042"
gradientUnits="userSpaceOnUse"
x1="615.5"
y1="-589.85107"
x2="615.5"
y2="-580.60107"
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)">
<stop
offset="0"
style="stop-color:#6193CF"
id="stop3044" />
<stop
offset="1"
style="stop-color:#D1DFF1"
id="stop3046" />
</linearGradient>
<linearGradient
id="linearGradient3049"
gradientUnits="userSpaceOnUse"
x1="615.5"
y1="-584.60211"
x2="615.5"
y2="-595.85211"
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)">
<stop
offset="0"
style="stop-color:#6193CF"
id="stop3051" />
<stop
offset="1"
style="stop-color:#EEEEEE"
id="stop3053" />
</linearGradient>
<g
id="g2202"
transform="translate(0,8)">
<path
style="opacity:0.1"
id="path2974"
d="M 4,0 C 1.794,0 0,1.8884211 0,4.2105263 L 0,75.789474 C 0,78.111579 1.794,80 4,80 L 116,80 C 118.206,80 120,78.111579 120,75.789474 L 120,4.2105263 C 120,1.8884211 118.206,0 116,0 L 4,0 z " />
<path
style="opacity:0.15"
id="path2976"
d="M 4,1 C 2.346,1 1,2.4187568 1,4.1621622 L 1,75.837838 C 1,77.581243 2.346,79 4,79 L 116,79 C 117.654,79 119,77.581243 119,75.837838 L 119,4.1621622 C 119,2.4187568 117.654,1 116,1 L 4,1 z " />
<path
style="opacity:0.2"
id="path2978"
d="M 4,2 C 2.897,2 2,2.9468333 2,4.1111111 L 2,75.888889 C 2,77.053167 2.897,78 4,78 L 116,78 C 117.103,78 118,77.053167 118,75.888889 L 118,4.1111111 C 118,2.9468333 117.103,2 116,2 L 4,2 z " />
<path
style="opacity:0.25"
id="path2980"
d="M 4,3 C 3.448,3 3,3.4736 3,4.0571428 L 3,75.942857 C 3,76.527457 3.448,77 4,77 L 116,77 C 116.553,77 117,76.527457 117,75.942857 L 117,4.0571428 C 117,3.4736 116.553,3 116,3 L 4,3 z " />
<rect
style="fill:url(#linearGradient2212)"
height="72"
width="112"
y="4"
x="4"
id="rect3244_9_" />
<polygon
style="fill:url(#linearGradient2214)"
points="4,8 4,12 60,45 116,12 116,8 60,41 4,8 "
id="polygon3286_9_" />
<polygon
style="fill:url(#linearGradient2216)"
points="116,69 116,65 59.997,24 4,65 4,69 59.997,28 116,69 "
id="polygon3293_9_" />
<polygon
style="fill:#ffffff"
id="polygon3002"
points="4,8 60.004,40.967 116,8 116,4 4,4 4,8 " />
</g>
</g>
</g>
</switch>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

View File

@ -18,27 +18,27 @@ from calibre.gui2.dialogs.job_view_ui import Ui_Dialog
NONE = QVariant() NONE = QVariant()
class JobManager(QAbstractTableModel): class JobManager(QAbstractTableModel):
def __init__(self): def __init__(self):
QAbstractTableModel.__init__(self) QAbstractTableModel.__init__(self)
self.wait_icon = QVariant(QIcon(':/images/jobs.svg')) self.wait_icon = QVariant(QIcon(':/images/jobs.svg'))
self.running_icon = QVariant(QIcon(':/images/exec.svg')) self.running_icon = QVariant(QIcon(':/images/exec.svg'))
self.error_icon = QVariant(QIcon(':/images/dialog_error.svg')) self.error_icon = QVariant(QIcon(':/images/dialog_error.svg'))
self.done_icon = QVariant(QIcon(':/images/ok.svg')) self.done_icon = QVariant(QIcon(':/images/ok.svg'))
self.jobs = [] self.jobs = []
self.server = Server() self.server = Server()
self.add_job = Dispatcher(self._add_job) self.add_job = Dispatcher(self._add_job)
self.status_update = Dispatcher(self._status_update) self.status_update = Dispatcher(self._status_update)
self.start_work = Dispatcher(self._start_work) self.start_work = Dispatcher(self._start_work)
self.job_done = Dispatcher(self._job_done) self.job_done = Dispatcher(self._job_done)
def columnCount(self, parent=QModelIndex()): def columnCount(self, parent=QModelIndex()):
return 4 return 4
def rowCount(self, parent=QModelIndex()): def rowCount(self, parent=QModelIndex()):
return len(self.jobs) return len(self.jobs)
def headerData(self, section, orientation, role): def headerData(self, section, orientation, role):
if role != Qt.DisplayRole: if role != Qt.DisplayRole:
return NONE return NONE
@ -50,14 +50,14 @@ class JobManager(QAbstractTableModel):
return QVariant(text) return QVariant(text)
else: else:
return QVariant(section+1) return QVariant(section+1)
def data(self, index, role): def data(self, index, role):
try: try:
if role not in (Qt.DisplayRole, Qt.DecorationRole): if role not in (Qt.DisplayRole, Qt.DecorationRole):
return NONE return NONE
row, col = index.row(), index.column() row, col = index.row(), index.column()
job = self.jobs[row] job = self.jobs[row]
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
if col == 0: if col == 0:
desc = job.description desc = job.description
@ -102,31 +102,31 @@ class JobManager(QAbstractTableModel):
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return NONE return NONE
def _add_job(self, job): def _add_job(self, job):
self.emit(SIGNAL('layoutAboutToBeChanged()')) self.emit(SIGNAL('layoutAboutToBeChanged()'))
self.jobs.append(job) self.jobs.append(job)
self.jobs.sort() self.jobs.sort()
self.emit(SIGNAL('job_added(int)'), self.rowCount()) self.emit(SIGNAL('job_added(int)'), self.rowCount())
self.emit(SIGNAL('layoutChanged()')) self.emit(SIGNAL('layoutChanged()'))
def done_jobs(self): def done_jobs(self):
return [j for j in self.jobs if j.status() in ['DONE', 'ERROR']] return [j for j in self.jobs if j.status() in ['DONE', 'ERROR']]
def row_to_job(self, row): def row_to_job(self, row):
return self.jobs[row] return self.jobs[row]
def _start_work(self, job): def _start_work(self, job):
self.emit(SIGNAL('layoutAboutToBeChanged()')) self.emit(SIGNAL('layoutAboutToBeChanged()'))
self.jobs.sort() self.jobs.sort()
self.emit(SIGNAL('layoutChanged()')) self.emit(SIGNAL('layoutChanged()'))
def _job_done(self, job): def _job_done(self, job):
self.emit(SIGNAL('layoutAboutToBeChanged()')) self.emit(SIGNAL('layoutAboutToBeChanged()'))
self.jobs.sort() self.jobs.sort()
self.emit(SIGNAL('job_done(int)'), len(self.jobs) - len(self.done_jobs())) self.emit(SIGNAL('job_done(int)'), len(self.jobs) - len(self.done_jobs()))
self.emit(SIGNAL('layoutChanged()')) self.emit(SIGNAL('layoutChanged()'))
def _status_update(self, job): def _status_update(self, job):
try: try:
row = self.jobs.index(job) row = self.jobs.index(job)
@ -134,38 +134,38 @@ class JobManager(QAbstractTableModel):
return return
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'),
self.index(row, 0), self.index(row, 3)) self.index(row, 0), self.index(row, 3))
def running_time_updated(self): def running_time_updated(self, *args):
for job in self.jobs: for job in self.jobs:
if not job.is_running: if not job.is_running:
continue continue
row = self.jobs.index(job) row = self.jobs.index(job)
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'),
self.index(row, 3), self.index(row, 3)) self.index(row, 3), self.index(row, 3))
def has_device_jobs(self): def has_device_jobs(self):
for job in self.jobs: for job in self.jobs:
if job.is_running and isinstance(job, DeviceJob): if job.is_running and isinstance(job, DeviceJob):
return True return True
return False return False
def has_jobs(self): def has_jobs(self):
for job in self.jobs: for job in self.jobs:
if job.is_running: if job.is_running:
return True return True
return False return False
def run_job(self, done, func, args=[], kwargs={}, def run_job(self, done, func, args=[], kwargs={},
description=None): description=None):
job = ParallelJob(func, done, self, args=args, kwargs=kwargs, job = ParallelJob(func, done, self, args=args, kwargs=kwargs,
description=description) description=description)
self.server.add_job(job) self.server.add_job(job)
return job return job
def output(self, job): def output(self, job):
self.emit(SIGNAL('output_received()')) self.emit(SIGNAL('output_received()'))
def kill_job(self, row, view): def kill_job(self, row, view):
job = self.jobs[row] job = self.jobs[row]
if isinstance(job, DeviceJob): if isinstance(job, DeviceJob):
@ -183,20 +183,20 @@ class JobManager(QAbstractTableModel):
self.server.kill(job) self.server.kill(job)
def terminate_all_jobs(self): def terminate_all_jobs(self):
pass pass
class DetailView(QDialog, Ui_Dialog): class DetailView(QDialog, Ui_Dialog):
def __init__(self, parent, job): def __init__(self, parent, job):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.setupUi(self) self.setupUi(self)
self.setWindowTitle(job.description) self.setWindowTitle(job.description)
self.job = job self.job = job
self.update() self.update()
def update(self): def update(self):
self.log.setPlainText(self.job.console_text()) self.log.setPlainText(self.job.console_text())
vbar = self.log.verticalScrollBar() vbar = self.log.verticalScrollBar()

View File

@ -86,15 +86,15 @@ class LibraryDelegate(QItemDelegate):
return sb return sb
class DateDelegate(QStyledItemDelegate): class DateDelegate(QStyledItemDelegate):
def displayText(self, val, locale): def displayText(self, val, locale):
d = val.toDate() d = val.toDate()
return d.toString('dd MMM yyyy') return d.toString('dd MMM yyyy')
def createEditor(self, parent, option, index): def createEditor(self, parent, option, index):
qde = QStyledItemDelegate.createEditor(self, parent, option, index) qde = QStyledItemDelegate.createEditor(self, parent, option, index)
qde.setDisplayFormat('MM/dd/yyyy') qde.setDisplayFormat('MM/dd/yyyy')
qde.setMinimumDate(QDate(-4000,1,1)) qde.setMinimumDate(QDate(101,1,1))
qde.setCalendarPopup(True) qde.setCalendarPopup(True)
return qde return qde
@ -103,7 +103,7 @@ class BooksModel(QAbstractTableModel):
[1000,900,500,400,100,90,50,40,10,9,5,4,1], [1000,900,500,400,100,90,50,40,10,9,5,4,1],
["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"] ["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"]
) )
headers = { headers = {
'title' : _("Title"), 'title' : _("Title"),
'authors' : _("Author(s)"), 'authors' : _("Author(s)"),
@ -114,7 +114,7 @@ class BooksModel(QAbstractTableModel):
'tags' : _("Tags"), 'tags' : _("Tags"),
'series' : _("Series"), 'series' : _("Series"),
} }
@classmethod @classmethod
def roman(cls, num): def roman(cls, num):
if num <= 0 or num >= 4000 or int(num) != num: if num <= 0 or num >= 4000 or int(num) != num:
@ -130,7 +130,7 @@ class BooksModel(QAbstractTableModel):
QAbstractTableModel.__init__(self, parent) QAbstractTableModel.__init__(self, parent)
self.db = None self.db = None
self.column_map = config['column_map'] self.column_map = config['column_map']
self.editable_cols = ['title', 'authors', 'rating', 'publisher', self.editable_cols = ['title', 'authors', 'rating', 'publisher',
'tags', 'series', 'timestamp'] 'tags', 'series', 'timestamp']
self.default_image = QImage(':/images/book.svg') self.default_image = QImage(':/images/book.svg')
self.sorted_on = ('timestamp', Qt.AscendingOrder) self.sorted_on = ('timestamp', Qt.AscendingOrder)
@ -157,10 +157,10 @@ class BooksModel(QAbstractTableModel):
tidx = self.column_map.index('timestamp') tidx = self.column_map.index('timestamp')
except ValueError: except ValueError:
tidx = -1 tidx = -1
self.emit(SIGNAL('columns_sorted(int,int)'), idx, tidx) self.emit(SIGNAL('columns_sorted(int,int)'), idx, tidx)
def set_database(self, db): def set_database(self, db):
self.db = db self.db = db
self.build_data_convertors() self.build_data_convertors()
@ -169,7 +169,7 @@ class BooksModel(QAbstractTableModel):
rows = self.db.refresh_ids(ids) rows = self.db.refresh_ids(ids)
if rows: if rows:
self.refresh_rows(rows, current_row=current_row) self.refresh_rows(rows, current_row=current_row)
def refresh_rows(self, rows, current_row=-1): def refresh_rows(self, rows, current_row=-1):
for row in rows: for row in rows:
if self.cover_cache: if self.cover_cache:
@ -191,12 +191,12 @@ class BooksModel(QAbstractTableModel):
add_duplicates=add_duplicates) add_duplicates=add_duplicates)
self.count_changed() self.count_changed()
return ret return ret
def add_news(self, path, recipe): def add_news(self, path, recipe):
ret = self.db.add_news(path, recipe) ret = self.db.add_news(path, recipe)
self.count_changed() self.count_changed()
return ret return ret
def count_changed(self, *args): def count_changed(self, *args):
self.emit(SIGNAL('count_changed(int)'), self.db.count()) self.emit(SIGNAL('count_changed(int)'), self.db.count())
@ -208,12 +208,12 @@ class BooksModel(QAbstractTableModel):
callback=None): callback=None):
rows = [row.row() for row in rows] rows = [row.row() for row in rows]
if single_format is None: if single_format is None:
return self.db.export_to_dir(path, rows, return self.db.export_to_dir(path, rows,
self.sorted_on[0] == 'authors', self.sorted_on[0] == 'authors',
single_dir=single_dir, single_dir=single_dir,
callback=callback) callback=callback)
else: else:
return self.db.export_single_format_to_dir(path, rows, return self.db.export_single_format_to_dir(path, rows,
single_format, single_format,
callback=callback) callback=callback)
@ -225,8 +225,8 @@ class BooksModel(QAbstractTableModel):
self.count_changed() self.count_changed()
self.clear_caches() self.clear_caches()
self.reset() self.reset()
def delete_books_by_id(self, ids): def delete_books_by_id(self, ids):
for id in ids: for id in ids:
try: try:
@ -263,18 +263,18 @@ class BooksModel(QAbstractTableModel):
self.clear_caches() self.clear_caches()
self.reset() self.reset()
self.sorted_on = (self.column_map[col], order) self.sorted_on = (self.column_map[col], order)
def refresh(self, reset=True): def refresh(self, reset=True):
try: try:
col = self.column_map.index(self.sorted_on[0]) col = self.column_map.index(self.sorted_on[0])
except: except:
col = 0 col = 0
self.db.refresh(field=self.column_map[col], self.db.refresh(field=self.column_map[col],
ascending=self.sorted_on[1]==Qt.AscendingOrder) ascending=self.sorted_on[1]==Qt.AscendingOrder)
if reset: if reset:
self.reset() self.reset()
def resort(self, reset=True): def resort(self, reset=True):
try: try:
col = self.column_map.index(self.sorted_on[0]) col = self.column_map.index(self.sorted_on[0])
@ -412,14 +412,14 @@ class BooksModel(QAbstractTableModel):
if format is None: if format is None:
ans.append(format) ans.append(format)
else: else:
f = self.db.format(id, format, index_is_id=True, as_file=True, f = self.db.format(id, format, index_is_id=True, as_file=True,
mode=mode) mode=mode)
ans.append(f) ans.append(f)
return ans return ans
def get_preferred_formats(self, rows, formats, paths=False, def get_preferred_formats(self, rows, formats, paths=False,
set_metadata=False, specific_format=None): set_metadata=False, specific_format=None):
ans = [] ans = []
need_auto = [] need_auto = []
@ -431,7 +431,7 @@ class BooksModel(QAbstractTableModel):
if not fmts: if not fmts:
fmts = '' fmts = ''
db_formats = set(fmts.lower().split(',')) db_formats = set(fmts.lower().split(','))
available_formats = set([f.lower() for f in formats]) available_formats = set([f.lower() for f in formats])
u = available_formats.intersection(db_formats) u = available_formats.intersection(db_formats)
for f in formats: for f in formats:
if f.lower() in u: if f.lower() in u:
@ -471,7 +471,7 @@ class BooksModel(QAbstractTableModel):
data = self.db.cover(row_number) data = self.db.cover(row_number)
except IndexError: # Happens if database has not yet been refreshed except IndexError: # Happens if database has not yet been refreshed
pass pass
if not data: if not data:
return self.default_image return self.default_image
img = QImage() img = QImage()
@ -481,7 +481,7 @@ class BooksModel(QAbstractTableModel):
return img return img
def build_data_convertors(self): def build_data_convertors(self):
tidx = FIELD_MAP['title'] tidx = FIELD_MAP['title']
aidx = FIELD_MAP['authors'] aidx = FIELD_MAP['authors']
sidx = FIELD_MAP['size'] sidx = FIELD_MAP['size']
@ -491,44 +491,44 @@ class BooksModel(QAbstractTableModel):
srdx = FIELD_MAP['series'] srdx = FIELD_MAP['series']
tgdx = FIELD_MAP['tags'] tgdx = FIELD_MAP['tags']
siix = FIELD_MAP['series_index'] siix = FIELD_MAP['series_index']
def authors(r): def authors(r):
au = self.db.data[r][aidx] au = self.db.data[r][aidx]
if au: if au:
au = [a.strip().replace('|', ',') for a in au.split(',')] au = [a.strip().replace('|', ',') for a in au.split(',')]
return ' & '.join(au) return ' & '.join(au)
def timestamp(r): def timestamp(r):
dt = self.db.data[r][tmdx] dt = self.db.data[r][tmdx]
if dt: if dt:
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight) dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
return QDate(dt.year, dt.month, dt.day) return QDate(dt.year, dt.month, dt.day)
def rating(r): def rating(r):
r = self.db.data[r][ridx] r = self.db.data[r][ridx]
r = r/2 if r else 0 r = r/2 if r else 0
return r return r
def publisher(r): def publisher(r):
pub = self.db.data[r][pidx] pub = self.db.data[r][pidx]
if pub: if pub:
return pub return pub
def tags(r): def tags(r):
tags = self.db.data[r][tgdx] tags = self.db.data[r][tgdx]
if tags: if tags:
return ', '.join(tags.split(',')) return ', '.join(tags.split(','))
def series(r): def series(r):
series = self.db.data[r][srdx] series = self.db.data[r][srdx]
if series: if series:
return series + ' [%d]'%self.db.data[r][siix] return series + ' [%d]'%self.db.data[r][siix]
def size(r): def size(r):
size = self.db.data[r][sidx] size = self.db.data[r][sidx]
if size: if size:
return '%.1f'%(float(size)/(1024*1024)) return '%.1f'%(float(size)/(1024*1024))
self.dc = { self.dc = {
'title' : lambda r : self.db.data[r][tidx], 'title' : lambda r : self.db.data[r][tidx],
'authors' : authors, 'authors' : authors,
@ -537,7 +537,7 @@ class BooksModel(QAbstractTableModel):
'rating' : rating, 'rating' : rating,
'publisher': publisher, 'publisher': publisher,
'tags' : tags, 'tags' : tags,
'series' : series, 'series' : series,
} }
def data(self, index, role): def data(self, index, role):
@ -575,7 +575,7 @@ class BooksModel(QAbstractTableModel):
val = int(value.toInt()[0]) if column == 'rating' else \ val = int(value.toInt()[0]) if column == 'rating' else \
value.toDate() if column == 'timestamp' else \ value.toDate() if column == 'timestamp' else \
unicode(value.toString()) unicode(value.toString())
id = self.db.id(row) id = self.db.id(row)
if column == 'rating': if column == 'rating':
val = 0 if val < 0 else 5 if val > 5 else val val = 0 if val < 0 else 5 if val > 5 else val
val *= 2 val *= 2
@ -600,7 +600,7 @@ class BooksModel(QAbstractTableModel):
index, index) index, index)
if column == self.sorted_on[0]: if column == self.sorted_on[0]:
self.resort() self.resort()
return True return True
class BooksView(TableView): class BooksView(TableView):
@ -634,7 +634,7 @@ class BooksView(TableView):
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'), QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
self._model.current_changed) self._model.current_changed)
self.connect(self._model, SIGNAL('columns_sorted(int, int)'), self.columns_sorted, Qt.QueuedConnection) self.connect(self._model, SIGNAL('columns_sorted(int, int)'), self.columns_sorted, Qt.QueuedConnection)
def columns_sorted(self, rating_col, timestamp_col): def columns_sorted(self, rating_col, timestamp_col):
for i in range(self.model().columnCount(None)): for i in range(self.model().columnCount(None)):
if self.itemDelegateForColumn(i) == self.rating_delegate: if self.itemDelegateForColumn(i) == self.rating_delegate:
@ -643,8 +643,8 @@ class BooksView(TableView):
self.setItemDelegateForColumn(rating_col, self.rating_delegate) self.setItemDelegateForColumn(rating_col, self.rating_delegate)
if timestamp_col > -1: if timestamp_col > -1:
self.setItemDelegateForColumn(timestamp_col, self.timestamp_delegate) self.setItemDelegateForColumn(timestamp_col, self.timestamp_delegate)
def set_context_menu(self, edit_metadata, send_to_device, convert, view, def set_context_menu(self, edit_metadata, send_to_device, convert, view,
save, open_folder, book_details, similar_menu=None): save, open_folder, book_details, similar_menu=None):
self.setContextMenuPolicy(Qt.DefaultContextMenu) self.setContextMenuPolicy(Qt.DefaultContextMenu)
self.context_menu = QMenu(self) self.context_menu = QMenu(self)
@ -662,18 +662,18 @@ class BooksView(TableView):
self.context_menu.addAction(book_details) self.context_menu.addAction(book_details)
if similar_menu is not None: if similar_menu is not None:
self.context_menu.addMenu(similar_menu) self.context_menu.addMenu(similar_menu)
def contextMenuEvent(self, event): def contextMenuEvent(self, event):
self.context_menu.popup(event.globalPos()) self.context_menu.popup(event.globalPos())
event.accept() event.accept()
def sortByColumn(self, colname, order): def sortByColumn(self, colname, order):
try: try:
idx = self._model.column_map.index(colname) idx = self._model.column_map.index(colname)
except ValueError: except ValueError:
idx = 0 idx = 0
TableView.sortByColumn(self, idx, order) TableView.sortByColumn(self, idx, order)
@classmethod @classmethod
def paths_from_event(cls, event): def paths_from_event(cls, event):
''' '''
@ -708,6 +708,9 @@ class BooksView(TableView):
def close(self): def close(self):
self._model.close() self._model.close()
def set_editable(self, editable):
self._model.set_editable(editable)
def connect_to_search_box(self, sb): def connect_to_search_box(self, sb):
QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'),
@ -736,23 +739,23 @@ class DeviceBooksView(BooksView):
def connect_dirtied_signal(self, slot): def connect_dirtied_signal(self, slot):
QObject.connect(self._model, SIGNAL('booklist_dirtied()'), slot) QObject.connect(self._model, SIGNAL('booklist_dirtied()'), slot)
def sortByColumn(self, col, order): def sortByColumn(self, col, order):
TableView.sortByColumn(self, col, order) TableView.sortByColumn(self, col, order)
def dropEvent(self, *args): def dropEvent(self, *args):
error_dialog(self, _('Not allowed'), error_dialog(self, _('Not allowed'),
_('Dropping onto a device is not supported. First add the book to the calibre library.')).exec_() _('Dropping onto a device is not supported. First add the book to the calibre library.')).exec_()
class OnDeviceSearch(SearchQueryParser): class OnDeviceSearch(SearchQueryParser):
def __init__(self, model): def __init__(self, model):
SearchQueryParser.__init__(self) SearchQueryParser.__init__(self)
self.model = model self.model = model
def universal_set(self): def universal_set(self):
return set(range(0, len(self.model.db))) return set(range(0, len(self.model.db)))
def get_matches(self, location, query): def get_matches(self, location, query):
location = location.lower().strip() location = location.lower().strip()
query = query.lower().strip() query = query.lower().strip()
@ -773,7 +776,7 @@ class OnDeviceSearch(SearchQueryParser):
matches.add(i) matches.add(i)
break break
return matches return matches
class DeviceBooksModel(BooksModel): class DeviceBooksModel(BooksModel):
@ -785,7 +788,7 @@ class DeviceBooksModel(BooksModel):
self.unknown = str(self.trUtf8('Unknown')) self.unknown = str(self.trUtf8('Unknown'))
self.marked_for_deletion = {} self.marked_for_deletion = {}
self.search_engine = OnDeviceSearch(self) self.search_engine = OnDeviceSearch(self)
self.editable = True
def mark_for_deletion(self, job, rows): def mark_for_deletion(self, job, rows):
self.marked_for_deletion[job] = self.indices(rows) self.marked_for_deletion[job] = self.indices(rows)
@ -793,7 +796,6 @@ class DeviceBooksModel(BooksModel):
indices = self.row_indices(row) indices = self.row_indices(row)
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1]) self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
def deletion_done(self, job, succeeded=True): def deletion_done(self, job, succeeded=True):
if not self.marked_for_deletion.has_key(job): if not self.marked_for_deletion.has_key(job):
return return
@ -818,7 +820,7 @@ class DeviceBooksModel(BooksModel):
if self.map[index.row()] in self.indices_to_be_deleted(): if self.map[index.row()] in self.indices_to_be_deleted():
return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python
flags = QAbstractTableModel.flags(self, index) flags = QAbstractTableModel.flags(self, index)
if index.isValid(): if index.isValid() and self.editable:
if index.column() in [0, 1] or (index.column() == 4 and self.db.supports_tags()): if index.column() in [0, 1] or (index.column() == 4 and self.db.supports_tags()):
flags |= Qt.ItemIsEditable flags |= Qt.ItemIsEditable
return flags return flags
@ -837,10 +839,10 @@ class DeviceBooksModel(BooksModel):
if reset: if reset:
self.reset() self.reset()
self.last_search = text self.last_search = text
def resort(self, reset): def resort(self, reset):
self.sort(self.sorted_on[0], self.sorted_on[1], reset=reset) self.sort(self.sorted_on[0], self.sorted_on[1], reset=reset)
def sort(self, col, order, reset=True): def sort(self, col, order, reset=True):
descending = order != Qt.AscendingOrder descending = order != Qt.AscendingOrder
def strcmp(attr): def strcmp(attr):
@ -959,7 +961,7 @@ class DeviceBooksModel(BooksModel):
return QVariant('Marked for deletion') return QVariant('Marked for deletion')
col = index.column() col = index.column()
if col in [0, 1] or (col == 4 and self.db.supports_tags()): if col in [0, 1] or (col == 4 and self.db.supports_tags()):
return QVariant("Double click to <b>edit</b> me<br><br>") return QVariant(_("Double click to <b>edit</b> me<br><br>"))
return NONE return NONE
def headerData(self, section, orientation, role): def headerData(self, section, orientation, role):
@ -999,6 +1001,10 @@ class DeviceBooksModel(BooksModel):
self.sort(col, self.sorted_on[1]) self.sort(col, self.sorted_on[1])
done = True done = True
return done return done
def set_editable(self, editable):
self.editable = editable
class SearchBox(QLineEdit): class SearchBox(QLineEdit):
@ -1062,11 +1068,11 @@ class SearchBox(QLineEdit):
if not all: if not all:
ans = '[' + ans + ']' ans = '[' + ans + ']'
self.set_search_string(ans) self.set_search_string(ans)
def search_from_tags(self, tags, all): def search_from_tags(self, tags, all):
joiner = ' and ' if all else ' or ' joiner = ' and ' if all else ' or '
self.set_search_string(joiner.join(tags)) self.set_search_string(joiner.join(tags))
def set_search_string(self, txt): def set_search_string(self, txt):
self.normalize_state() self.normalize_state()
self.setText(txt) self.setText(txt)

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <ui version="4.0" >
<ui version="4.0">
<author>Kovid Goyal</author> <author>Kovid Goyal</author>
<class>MainWindow</class> <class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow"> <widget class="QMainWindow" name="MainWindow" >
<property name="geometry"> <property name="geometry" >
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
@ -11,140 +10,149 @@
<height>822</height> <height>822</height>
</rect> </rect>
</property> </property>
<property name="sizePolicy"> <property name="sizePolicy" >
<sizepolicy hsizetype="Preferred" vsizetype="Preferred"> <sizepolicy vsizetype="Preferred" hsizetype="Preferred" >
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="contextMenuPolicy"> <property name="contextMenuPolicy" >
<enum>Qt::NoContextMenu</enum> <enum>Qt::NoContextMenu</enum>
</property> </property>
<property name="windowTitle"> <property name="windowTitle" >
<string>__appname__</string> <string>__appname__</string>
</property> </property>
<property name="windowIcon"> <property name="windowIcon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/library</normaloff>:/library</iconset> <normaloff>:/library</normaloff>:/library</iconset>
</property> </property>
<widget class="QWidget" name="centralwidget"> <widget class="QWidget" name="centralwidget" >
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout" >
<item row="0" column="0"> <item row="0" column="0" >
<layout class="QHBoxLayout" name="horizontalLayout_3"> <layout class="QHBoxLayout" name="horizontalLayout_3" >
<item> <item>
<widget class="LocationView" name="location_view"> <widget class="LocationView" name="location_view" >
<property name="sizePolicy"> <property name="sizePolicy" >
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="maximumSize"> <property name="maximumSize" >
<size> <size>
<width>16777215</width> <width>16777215</width>
<height>100</height> <height>100</height>
</size> </size>
</property> </property>
<property name="verticalScrollBarPolicy"> <property name="verticalScrollBarPolicy" >
<enum>Qt::ScrollBarAlwaysOff</enum> <enum>Qt::ScrollBarAlwaysOff</enum>
</property> </property>
<property name="horizontalScrollBarPolicy"> <property name="horizontalScrollBarPolicy" >
<enum>Qt::ScrollBarAsNeeded</enum> <enum>Qt::ScrollBarAsNeeded</enum>
</property> </property>
<property name="tabKeyNavigation"> <property name="editTriggers" >
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="tabKeyNavigation" >
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="showDropIndicator" stdset="0"> <property name="showDropIndicator" stdset="0" >
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="iconSize"> <property name="selectionMode" >
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="iconSize" >
<size> <size>
<width>40</width> <width>40</width>
<height>40</height> <height>40</height>
</size> </size>
</property> </property>
<property name="movement"> <property name="movement" >
<enum>QListView::Static</enum> <enum>QListView::Static</enum>
</property> </property>
<property name="flow"> <property name="flow" >
<enum>QListView::LeftToRight</enum> <enum>QListView::LeftToRight</enum>
</property> </property>
<property name="gridSize"> <property name="gridSize" >
<size> <size>
<width>175</width> <width>175</width>
<height>90</height> <height>90</height>
</size> </size>
</property> </property>
<property name="viewMode"> <property name="viewMode" >
<enum>QListView::ListMode</enum> <enum>QListView::ListMode</enum>
</property> </property>
<property name="wordWrap"> <property name="wordWrap" >
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QToolButton" name="donate_button"> <widget class="QToolButton" name="donate_button" >
<property name="cursor"> <property name="cursor" >
<cursorShape>PointingHandCursor</cursorShape> <cursorShape>PointingHandCursor</cursorShape>
</property> </property>
<property name="text"> <property name="text" >
<string>...</string> <string>...</string>
</property> </property>
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/donate.svg</normaloff>:/images/donate.svg</iconset> <normaloff>:/images/donate.svg</normaloff>:/images/donate.svg</iconset>
</property> </property>
<property name="iconSize"> <property name="iconSize" >
<size> <size>
<width>64</width> <width>64</width>
<height>64</height> <height>64</height>
</size> </size>
</property> </property>
<property name="autoRaise"> <property name="autoRaise" >
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QVBoxLayout" name="verticalLayout_3"> <layout class="QVBoxLayout" name="verticalLayout_3" >
<item> <item>
<widget class="QLabel" name="vanity"> <widget class="QLabel" name="vanity" >
<property name="sizePolicy"> <property name="sizePolicy" >
<sizepolicy hsizetype="Preferred" vsizetype="Preferred"> <sizepolicy vsizetype="Preferred" hsizetype="Preferred" >
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="maximumSize"> <property name="maximumSize" >
<size> <size>
<width>16777215</width> <width>16777215</width>
<height>90</height> <height>90</height>
</size> </size>
</property> </property>
<property name="text"> <property name="text" >
<string/> <string/>
</property> </property>
<property name="textFormat"> <property name="textFormat" >
<enum>Qt::RichText</enum> <enum>Qt::RichText</enum>
</property> </property>
<property name="openExternalLinks"> <property name="openExternalLinks" >
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_2"> <layout class="QHBoxLayout" name="horizontalLayout_2" >
<item> <item>
<widget class="QLabel" name="label_2"> <widget class="QLabel" name="label_2" >
<property name="text"> <property name="text" >
<string>Output:</string> <string>Output:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QComboBox" name="output_format"> <widget class="QComboBox" name="output_format" >
<property name="toolTip"> <property name="toolTip" >
<string>Set the output format that is used when converting ebooks and downloading news</string> <string>Set the output format that is used when converting ebooks and downloading news</string>
</property> </property>
</widget> </widget>
@ -155,99 +163,99 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="1" column="0"> <item row="1" column="0" >
<layout class="QHBoxLayout"> <layout class="QHBoxLayout" >
<property name="spacing"> <property name="spacing" >
<number>6</number> <number>6</number>
</property> </property>
<property name="margin"> <property name="margin" >
<number>0</number> <number>0</number>
</property> </property>
<item> <item>
<widget class="QToolButton" name="advanced_search_button"> <widget class="QToolButton" name="advanced_search_button" >
<property name="toolTip"> <property name="toolTip" >
<string>Advanced search</string> <string>Advanced search</string>
</property> </property>
<property name="text"> <property name="text" >
<string>...</string> <string>...</string>
</property> </property>
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/search.svg</normaloff>:/images/search.svg</iconset> <normaloff>:/images/search.svg</normaloff>:/images/search.svg</iconset>
</property> </property>
<property name="shortcut"> <property name="shortcut" >
<string>Alt+S</string> <string>Alt+S</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="label"> <widget class="QLabel" name="label" >
<property name="text"> <property name="text" >
<string>&amp;Search:</string> <string>&amp;Search:</string>
</property> </property>
<property name="buddy"> <property name="buddy" >
<cstring>search</cstring> <cstring>search</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="SearchBox" name="search"> <widget class="SearchBox" name="search" >
<property name="enabled"> <property name="enabled" >
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="sizePolicy"> <property name="sizePolicy" >
<sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
<horstretch>1</horstretch> <horstretch>1</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="acceptDrops"> <property name="acceptDrops" >
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="toolTip"> <property name="toolTip" >
<string>Search the list of books by title or author&lt;br&gt;&lt;br&gt;Words separated by spaces are ANDed</string> <string>Search the list of books by title or author&lt;br>&lt;br>Words separated by spaces are ANDed</string>
</property> </property>
<property name="whatsThis"> <property name="whatsThis" >
<string>Search the list of books by title, author, publisher, tags and comments&lt;br&gt;&lt;br&gt;Words separated by spaces are ANDed</string> <string>Search the list of books by title, author, publisher, tags and comments&lt;br>&lt;br>Words separated by spaces are ANDed</string>
</property> </property>
<property name="autoFillBackground"> <property name="autoFillBackground" >
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="text"> <property name="text" >
<string/> <string/>
</property> </property>
<property name="frame"> <property name="frame" >
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QToolButton" name="clear_button"> <widget class="QToolButton" name="clear_button" >
<property name="toolTip"> <property name="toolTip" >
<string>Reset Quick Search</string> <string>Reset Quick Search</string>
</property> </property>
<property name="text"> <property name="text" >
<string>...</string> <string>...</string>
</property> </property>
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/clear_left.svg</normaloff>:/images/clear_left.svg</iconset> <normaloff>:/images/clear_left.svg</normaloff>:/images/clear_left.svg</iconset>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="Line" name="line"> <widget class="Line" name="line" >
<property name="orientation"> <property name="orientation" >
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<spacer> <spacer>
<property name="orientation"> <property name="orientation" >
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0" >
<size> <size>
<width>20</width> <width>20</width>
<height>20</height> <height>20</height>
@ -256,77 +264,77 @@
</spacer> </spacer>
</item> </item>
<item> <item>
<widget class="QToolButton" name="config_button"> <widget class="QToolButton" name="config_button" >
<property name="toolTip"> <property name="toolTip" >
<string>Configuration</string> <string>Configuration</string>
</property> </property>
<property name="text"> <property name="text" >
<string>...</string> <string>...</string>
</property> </property>
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/config.svg</normaloff>:/images/config.svg</iconset> <normaloff>:/images/config.svg</normaloff>:/images/config.svg</iconset>
</property> </property>
</widget> </widget>
</item> </item>
</layout> </layout>
</item> </item>
<item row="2" column="0"> <item row="2" column="0" >
<widget class="QStackedWidget" name="stack"> <widget class="QStackedWidget" name="stack" >
<property name="sizePolicy"> <property name="sizePolicy" >
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>100</horstretch> <horstretch>100</horstretch>
<verstretch>100</verstretch> <verstretch>100</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="currentIndex"> <property name="currentIndex" >
<number>0</number> <number>0</number>
</property> </property>
<widget class="QWidget" name="library"> <widget class="QWidget" name="library" >
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2" >
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout" >
<item> <item>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout" >
<item> <item>
<widget class="QRadioButton" name="match_any"> <widget class="QRadioButton" name="match_any" >
<property name="text"> <property name="text" >
<string>Match any</string> <string>Match any</string>
</property> </property>
<property name="checked"> <property name="checked" >
<bool>false</bool> <bool>false</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QRadioButton" name="match_all"> <widget class="QRadioButton" name="match_all" >
<property name="text"> <property name="text" >
<string>Match all</string> <string>Match all</string>
</property> </property>
<property name="checked"> <property name="checked" >
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="popularity"> <widget class="QCheckBox" name="popularity" >
<property name="text"> <property name="text" >
<string>Sort by &amp;popularity</string> <string>Sort by &amp;popularity</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="TagsView" name="tags_view"> <widget class="TagsView" name="tags_view" >
<property name="tabKeyNavigation"> <property name="tabKeyNavigation" >
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="alternatingRowColors"> <property name="alternatingRowColors" >
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="animated"> <property name="animated" >
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="headerHidden"> <property name="headerHidden" >
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
@ -334,35 +342,35 @@
</layout> </layout>
</item> </item>
<item> <item>
<widget class="BooksView" name="library_view"> <widget class="BooksView" name="library_view" >
<property name="sizePolicy"> <property name="sizePolicy" >
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>100</horstretch> <horstretch>100</horstretch>
<verstretch>10</verstretch> <verstretch>10</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="acceptDrops"> <property name="acceptDrops" >
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="dragEnabled"> <property name="dragEnabled" >
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="dragDropOverwriteMode"> <property name="dragDropOverwriteMode" >
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="dragDropMode"> <property name="dragDropMode" >
<enum>QAbstractItemView::DragDrop</enum> <enum>QAbstractItemView::DragDrop</enum>
</property> </property>
<property name="alternatingRowColors"> <property name="alternatingRowColors" >
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="selectionBehavior"> <property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum> <enum>QAbstractItemView::SelectRows</enum>
</property> </property>
<property name="showGrid"> <property name="showGrid" >
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="wordWrap"> <property name="wordWrap" >
<bool>false</bool> <bool>false</bool>
</property> </property>
</widget> </widget>
@ -371,76 +379,76 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="main_memory"> <widget class="QWidget" name="main_memory" >
<layout class="QGridLayout"> <layout class="QGridLayout" >
<item row="0" column="0"> <item row="0" column="0" >
<widget class="DeviceBooksView" name="memory_view"> <widget class="DeviceBooksView" name="memory_view" >
<property name="sizePolicy"> <property name="sizePolicy" >
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>100</horstretch> <horstretch>100</horstretch>
<verstretch>10</verstretch> <verstretch>10</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="acceptDrops"> <property name="acceptDrops" >
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="dragEnabled"> <property name="dragEnabled" >
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="dragDropOverwriteMode"> <property name="dragDropOverwriteMode" >
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="dragDropMode"> <property name="dragDropMode" >
<enum>QAbstractItemView::DragDrop</enum> <enum>QAbstractItemView::DragDrop</enum>
</property> </property>
<property name="alternatingRowColors"> <property name="alternatingRowColors" >
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="selectionBehavior"> <property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum> <enum>QAbstractItemView::SelectRows</enum>
</property> </property>
<property name="showGrid"> <property name="showGrid" >
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="wordWrap"> <property name="wordWrap" >
<bool>false</bool> <bool>false</bool>
</property> </property>
</widget> </widget>
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="page"> <widget class="QWidget" name="page" >
<layout class="QGridLayout"> <layout class="QGridLayout" >
<item row="0" column="0"> <item row="0" column="0" >
<widget class="DeviceBooksView" name="card_view"> <widget class="DeviceBooksView" name="card_view" >
<property name="sizePolicy"> <property name="sizePolicy" >
<sizepolicy hsizetype="Preferred" vsizetype="Expanding"> <sizepolicy vsizetype="Expanding" hsizetype="Preferred" >
<horstretch>10</horstretch> <horstretch>10</horstretch>
<verstretch>10</verstretch> <verstretch>10</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="acceptDrops"> <property name="acceptDrops" >
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="dragEnabled"> <property name="dragEnabled" >
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="dragDropOverwriteMode"> <property name="dragDropOverwriteMode" >
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="dragDropMode"> <property name="dragDropMode" >
<enum>QAbstractItemView::DragDrop</enum> <enum>QAbstractItemView::DragDrop</enum>
</property> </property>
<property name="alternatingRowColors"> <property name="alternatingRowColors" >
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="selectionBehavior"> <property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum> <enum>QAbstractItemView::SelectRows</enum>
</property> </property>
<property name="showGrid"> <property name="showGrid" >
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="wordWrap"> <property name="wordWrap" >
<bool>false</bool> <bool>false</bool>
</property> </property>
</widget> </widget>
@ -451,234 +459,225 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QToolBar" name="tool_bar"> <widget class="QToolBar" name="tool_bar" >
<property name="minimumSize"> <property name="minimumSize" >
<size> <size>
<width>0</width> <width>0</width>
<height>0</height> <height>0</height>
</size> </size>
</property> </property>
<property name="contextMenuPolicy"> <property name="contextMenuPolicy" >
<enum>Qt::PreventContextMenu</enum> <enum>Qt::PreventContextMenu</enum>
</property> </property>
<property name="movable"> <property name="movable" >
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="orientation"> <property name="orientation" >
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
</property> </property>
<property name="iconSize"> <property name="iconSize" >
<size> <size>
<width>48</width> <width>48</width>
<height>48</height> <height>48</height>
</size> </size>
</property> </property>
<property name="toolButtonStyle"> <property name="toolButtonStyle" >
<enum>Qt::ToolButtonTextUnderIcon</enum> <enum>Qt::ToolButtonTextUnderIcon</enum>
</property> </property>
<attribute name="toolBarArea"> <attribute name="toolBarArea" >
<enum>TopToolBarArea</enum> <enum>TopToolBarArea</enum>
</attribute> </attribute>
<attribute name="toolBarBreak"> <attribute name="toolBarBreak" >
<bool>false</bool> <bool>false</bool>
</attribute> </attribute>
<addaction name="action_add"/> <addaction name="action_add" />
<addaction name="action_edit"/> <addaction name="action_edit" />
<addaction name="action_convert"/> <addaction name="action_convert" />
<addaction name="action_view"/> <addaction name="action_view" />
<addaction name="action_news"/> <addaction name="action_news" />
<addaction name="separator"/> <addaction name="separator" />
<addaction name="action_sync"/> <addaction name="action_sync" />
<addaction name="action_save"/> <addaction name="action_save" />
<addaction name="action_del"/> <addaction name="action_del" />
<addaction name="separator"/> <addaction name="separator" />
<addaction name="action_preferences"/> <addaction name="action_preferences" />
</widget> </widget>
<widget class="QStatusBar" name="statusBar"> <widget class="QStatusBar" name="statusBar" >
<property name="mouseTracking"> <property name="mouseTracking" >
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
<action name="action_add"> <action name="action_add" >
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/add_book.svg</normaloff>:/images/add_book.svg</iconset> <normaloff>:/images/add_book.svg</normaloff>:/images/add_book.svg</iconset>
</property> </property>
<property name="text"> <property name="text" >
<string>Add books</string> <string>Add books</string>
</property> </property>
<property name="shortcut"> <property name="shortcut" >
<string>A</string> <string>A</string>
</property> </property>
<property name="autoRepeat"> <property name="autoRepeat" >
<bool>false</bool> <bool>false</bool>
</property> </property>
</action> </action>
<action name="action_del"> <action name="action_del" >
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/trash.svg</normaloff>:/images/trash.svg</iconset> <normaloff>:/images/trash.svg</normaloff>:/images/trash.svg</iconset>
</property> </property>
<property name="text"> <property name="text" >
<string>Remove books</string> <string>Remove books</string>
</property> </property>
<property name="toolTip"> <property name="toolTip" >
<string>Remove books</string> <string>Remove books</string>
</property> </property>
<property name="shortcut"> <property name="shortcut" >
<string>Del</string> <string>Del</string>
</property> </property>
</action> </action>
<action name="action_edit"> <action name="action_edit" >
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/edit_input.svg</normaloff>:/images/edit_input.svg</iconset> <normaloff>:/images/edit_input.svg</normaloff>:/images/edit_input.svg</iconset>
</property> </property>
<property name="text"> <property name="text" >
<string>Edit meta information</string> <string>Edit meta information</string>
</property> </property>
<property name="shortcut"> <property name="shortcut" >
<string>E</string> <string>E</string>
</property> </property>
<property name="autoRepeat"> <property name="autoRepeat" >
<bool>false</bool> <bool>false</bool>
</property> </property>
</action> </action>
<action name="action_sync"> <action name="action_sync" >
<property name="enabled"> <property name="enabled" >
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/sync.svg</normaloff>:/images/sync.svg</iconset> <normaloff>:/images/sync.svg</normaloff>:/images/sync.svg</iconset>
</property> </property>
<property name="text"> <property name="text" >
<string>Send to device</string> <string>Send to device</string>
</property> </property>
</action> </action>
<action name="action_save"> <action name="action_save" >
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/save.svg</normaloff>:/images/save.svg</iconset> <normaloff>:/images/save.svg</normaloff>:/images/save.svg</iconset>
</property> </property>
<property name="text"> <property name="text" >
<string>Save to disk</string> <string>Save to disk</string>
</property> </property>
<property name="shortcut"> <property name="shortcut" >
<string>S</string> <string>S</string>
</property> </property>
</action> </action>
<action name="action_news"> <action name="action_news" >
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/news.svg</normaloff>:/images/news.svg</iconset> <normaloff>:/images/news.svg</normaloff>:/images/news.svg</iconset>
</property> </property>
<property name="text"> <property name="text" >
<string>Fetch news</string> <string>Fetch news</string>
</property> </property>
<property name="shortcut"> <property name="shortcut" >
<string>F</string> <string>F</string>
</property> </property>
</action> </action>
<action name="action_convert"> <action name="action_convert" >
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/convert.svg</normaloff>:/images/convert.svg</iconset> <normaloff>:/images/convert.svg</normaloff>:/images/convert.svg</iconset>
</property> </property>
<property name="text"> <property name="text" >
<string>Convert E-books</string> <string>Convert E-books</string>
</property> </property>
<property name="shortcut"> <property name="shortcut" >
<string>C</string> <string>C</string>
</property> </property>
</action> </action>
<action name="action_view"> <action name="action_view" >
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/view.svg</normaloff>:/images/view.svg</iconset> <normaloff>:/images/view.svg</normaloff>:/images/view.svg</iconset>
</property> </property>
<property name="text"> <property name="text" >
<string>View</string> <string>View</string>
</property> </property>
<property name="shortcut"> <property name="shortcut" >
<string>V</string> <string>V</string>
</property> </property>
</action> </action>
<action name="action_open_containing_folder"> <action name="action_open_containing_folder" >
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset> <normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset>
</property> </property>
<property name="text"> <property name="text" >
<string>Open containing folder</string> <string>Open containing folder</string>
</property> </property>
</action> </action>
<action name="action_show_book_details"> <action name="action_show_book_details" >
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/dialog_information.svg</normaloff>:/images/dialog_information.svg</iconset> <normaloff>:/images/dialog_information.svg</normaloff>:/images/dialog_information.svg</iconset>
</property> </property>
<property name="text"> <property name="text" >
<string>Show book details</string> <string>Show book details</string>
</property> </property>
</action> </action>
<action name="action_books_by_same_author"> <action name="action_books_by_same_author" >
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/user_profile.svg</normaloff>:/images/user_profile.svg</iconset> <normaloff>:/images/user_profile.svg</normaloff>:/images/user_profile.svg</iconset>
</property> </property>
<property name="text"> <property name="text" >
<string>Books by same author</string> <string>Books by same author</string>
</property> </property>
</action> </action>
<action name="action_books_in_this_series"> <action name="action_books_in_this_series" >
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/books_in_series.svg</normaloff>:/images/books_in_series.svg</iconset> <normaloff>:/images/books_in_series.svg</normaloff>:/images/books_in_series.svg</iconset>
</property> </property>
<property name="text"> <property name="text" >
<string>Books in this series</string> <string>Books in this series</string>
</property> </property>
</action> </action>
<action name="action_books_by_this_publisher"> <action name="action_books_by_this_publisher" >
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/publisher.png</normaloff>:/images/publisher.png</iconset> <normaloff>:/images/publisher.png</normaloff>:/images/publisher.png</iconset>
</property> </property>
<property name="text"> <property name="text" >
<string>Books by this publisher</string> <string>Books by this publisher</string>
</property> </property>
</action> </action>
<action name="action_books_with_the_same_tags"> <action name="action_books_with_the_same_tags" >
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/tags.svg</normaloff>:/images/tags.svg</iconset> <normaloff>:/images/tags.svg</normaloff>:/images/tags.svg</iconset>
</property> </property>
<property name="text"> <property name="text" >
<string>Books with the same tags</string> <string>Books with the same tags</string>
</property> </property>
</action> </action>
<action name="action_send_specific_format_to_device"> <action name="action_preferences" >
<property name="icon"> <property name="icon" >
<iconset resource="images.qrc"> <iconset resource="images.qrc" >
<normaloff>:/images/book.svg</normaloff>:/images/book.svg</iconset>
</property>
<property name="text">
<string>Send specific format to device</string>
</property>
</action>
<action name="action_preferences">
<property name="icon">
<iconset resource="images.qrc">
<normaloff>:/images/config.svg</normaloff>:/images/config.svg</iconset> <normaloff>:/images/config.svg</normaloff>:/images/config.svg</iconset>
</property> </property>
<property name="text"> <property name="text" >
<string>Preferences</string> <string>Preferences</string>
</property> </property>
<property name="toolTip"> <property name="toolTip" >
<string>Configure calibre</string> <string>Configure calibre</string>
</property> </property>
<property name="shortcut"> <property name="shortcut" >
<string>Ctrl+P</string> <string>Ctrl+P</string>
</property> </property>
</action> </action>
@ -711,7 +710,7 @@
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<resources> <resources>
<include location="images.qrc"/> <include location="images.qrc" />
</resources> </resources>
<connections> <connections>
<connection> <connection>
@ -720,11 +719,11 @@
<receiver>search</receiver> <receiver>search</receiver>
<slot>clear()</slot> <slot>clear()</slot>
<hints> <hints>
<hint type="sourcelabel"> <hint type="sourcelabel" >
<x>787</x> <x>787</x>
<y>215</y> <y>215</y>
</hint> </hint>
<hint type="destinationlabel"> <hint type="destinationlabel" >
<x>755</x> <x>755</x>
<y>213</y> <y>213</y>
</hint> </hint>

View File

@ -7,9 +7,9 @@ import re, os, traceback
from PyQt4.QtGui import QListView, QIcon, QFont, QLabel, QListWidget, \ from PyQt4.QtGui import QListView, QIcon, QFont, QLabel, QListWidget, \
QListWidgetItem, QTextCharFormat, QApplication, \ QListWidgetItem, QTextCharFormat, QApplication, \
QSyntaxHighlighter, QCursor, QColor, QWidget, QDialog, \ QSyntaxHighlighter, QCursor, QColor, QWidget, QDialog, \
QPixmap QPixmap, QMovie, QPalette
from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, SIGNAL, \ from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, SIGNAL, \
QObject, QRegExp, QString, QSettings, QSize QRegExp, QSettings, QSize, QModelIndex
from calibre.gui2.jobs2 import DetailView from calibre.gui2.jobs2 import DetailView
from calibre.gui2 import human_readable, NONE, TableView, \ from calibre.gui2 import human_readable, NONE, TableView, \
@ -21,8 +21,44 @@ from calibre.ebooks.metadata.meta import metadata_from_filename
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.gui2.dialogs.warning_ui import Ui_Dialog as Ui_WarningDialog from calibre.gui2.dialogs.warning_ui import Ui_Dialog as Ui_WarningDialog
class ProgressIndicator(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
self.setGeometry(0, 0, 300, 350)
self.movie = QMovie(':/images/jobs-animated.mng')
self.ml = QLabel(self)
self.ml.setMovie(self.movie)
self.movie.start()
self.movie.setPaused(True)
self.status = QLabel(self)
self.status.setWordWrap(True)
self.status.setAlignment(Qt.AlignHCenter|Qt.AlignTop)
self.status.font().setBold(True)
self.status.font().setPointSize(self.font().pointSize()+6)
self.setVisible(False)
def start(self, msg=''):
view = self.parent()
pwidth, pheight = view.size().width(), view.size().height()
self.resize(pwidth, min(pheight, 250))
self.move(0, (pheight-self.size().height())/2.)
self.ml.resize(self.ml.sizeHint())
self.ml.move(int((self.size().width()-self.ml.size().width())/2.), 0)
self.status.resize(self.size().width(), self.size().height()-self.ml.size().height()-10)
self.status.move(0, self.ml.size().height()+10)
self.status.setText(msg)
self.setVisible(True)
self.movie.setPaused(False)
def stop(self):
if self.movie.state() == self.movie.Running:
self.movie.setPaused(True)
self.setVisible(False)
class WarningDialog(QDialog, Ui_WarningDialog): class WarningDialog(QDialog, Ui_WarningDialog):
def __init__(self, title, msg, details, parent=None): def __init__(self, title, msg, details, parent=None):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.setupUi(self) self.setupUi(self)
@ -31,20 +67,20 @@ class WarningDialog(QDialog, Ui_WarningDialog):
self.details.setText(details) self.details.setText(details)
class FilenamePattern(QWidget, Ui_Form): class FilenamePattern(QWidget, Ui_Form):
def __init__(self, parent): def __init__(self, parent):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
self.setupUi(self) self.setupUi(self)
self.connect(self.test_button, SIGNAL('clicked()'), self.do_test) self.connect(self.test_button, SIGNAL('clicked()'), self.do_test)
self.connect(self.re, SIGNAL('returnPressed()'), self.do_test) self.connect(self.re, SIGNAL('returnPressed()'), self.do_test)
self.re.setText(prefs['filename_pattern']) self.re.setText(prefs['filename_pattern'])
def do_test(self): def do_test(self):
try: try:
pat = self.pattern() pat = self.pattern()
except Exception, err: except Exception, err:
error_dialog(self, _('Invalid regular expression'), error_dialog(self, _('Invalid regular expression'),
_('Invalid regular expression: %s')%err).exec_() _('Invalid regular expression: %s')%err).exec_()
return return
mi = metadata_from_filename(qstring_to_unicode(self.filename.text()), pat) mi = metadata_from_filename(qstring_to_unicode(self.filename.text()), pat)
@ -56,49 +92,49 @@ class FilenamePattern(QWidget, Ui_Form):
self.authors.setText(', '.join(mi.authors)) self.authors.setText(', '.join(mi.authors))
else: else:
self.authors.setText(_('No match')) self.authors.setText(_('No match'))
if mi.series: if mi.series:
self.series.setText(mi.series) self.series.setText(mi.series)
else: else:
self.series.setText(_('No match')) self.series.setText(_('No match'))
if mi.series_index is not None: if mi.series_index is not None:
self.series_index.setText(str(mi.series_index)) self.series_index.setText(str(mi.series_index))
else: else:
self.series_index.setText(_('No match')) self.series_index.setText(_('No match'))
self.isbn.setText(_('No match') if mi.isbn is None else str(mi.isbn)) self.isbn.setText(_('No match') if mi.isbn is None else str(mi.isbn))
def pattern(self): def pattern(self):
pat = qstring_to_unicode(self.re.text()) pat = qstring_to_unicode(self.re.text())
return re.compile(pat) return re.compile(pat)
def commit(self): def commit(self):
pat = self.pattern().pattern pat = self.pattern().pattern
prefs['filename_pattern'] = pat prefs['filename_pattern'] = pat
return pat return pat
class ImageView(QLabel): class ImageView(QLabel):
MAX_WIDTH = 400 MAX_WIDTH = 400
MAX_HEIGHT = 300 MAX_HEIGHT = 300
DROPABBLE_EXTENSIONS = ('jpg', 'jpeg', 'gif', 'png', 'bmp') DROPABBLE_EXTENSIONS = ('jpg', 'jpeg', 'gif', 'png', 'bmp')
@classmethod @classmethod
def paths_from_event(cls, event): def paths_from_event(cls, event):
''' '''
Accept a drop event and return a list of paths that can be read from Accept a drop event and return a list of paths that can be read from
and represent files with extensions. and represent files with extensions.
''' '''
if event.mimeData().hasFormat('text/uri-list'): if event.mimeData().hasFormat('text/uri-list'):
urls = [qstring_to_unicode(u.toLocalFile()) for u in event.mimeData().urls()] urls = [qstring_to_unicode(u.toLocalFile()) for u in event.mimeData().urls()]
urls = [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)] urls = [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
return [u for u in urls if os.path.splitext(u)[1][1:].lower() in cls.DROPABBLE_EXTENSIONS] return [u for u in urls if os.path.splitext(u)[1][1:].lower() in cls.DROPABBLE_EXTENSIONS]
def dragEnterEvent(self, event): def dragEnterEvent(self, event):
if int(event.possibleActions() & Qt.CopyAction) + \ if int(event.possibleActions() & Qt.CopyAction) + \
int(event.possibleActions() & Qt.MoveAction) == 0: int(event.possibleActions() & Qt.MoveAction) == 0:
@ -106,7 +142,7 @@ class ImageView(QLabel):
paths = self.paths_from_event(event) paths = self.paths_from_event(event)
if paths: if paths:
event.acceptProposedAction() event.acceptProposedAction()
def dropEvent(self, event): def dropEvent(self, event):
paths = self.paths_from_event(event) paths = self.paths_from_event(event)
event.setDropAction(Qt.CopyAction) event.setDropAction(Qt.CopyAction)
@ -118,19 +154,19 @@ class ImageView(QLabel):
event.accept() event.accept()
self.emit(SIGNAL('cover_changed()'), paths, Qt.QueuedConnection) self.emit(SIGNAL('cover_changed()'), paths, Qt.QueuedConnection)
break break
def dragMoveEvent(self, event): def dragMoveEvent(self, event):
event.acceptProposedAction() event.acceptProposedAction()
def setPixmap(self, pixmap): def setPixmap(self, pixmap):
QLabel.setPixmap(self, pixmap) QLabel.setPixmap(self, pixmap)
width, height = fit_image(pixmap.width(), pixmap.height(), self.MAX_WIDTH, self.MAX_HEIGHT)[1:] width, height = fit_image(pixmap.width(), pixmap.height(), self.MAX_WIDTH, self.MAX_HEIGHT)[1:]
self.setMaximumWidth(width) self.setMaximumWidth(width)
self.setMaximumHeight(height) self.setMaximumHeight(height)
class LocationModel(QAbstractListModel): class LocationModel(QAbstractListModel):
def __init__(self, parent): def __init__(self, parent):
QAbstractListModel.__init__(self, parent) QAbstractListModel.__init__(self, parent)
self.icons = [QVariant(QIcon(':/library')), self.icons = [QVariant(QIcon(':/library')),
@ -146,19 +182,19 @@ class LocationModel(QAbstractListModel):
_('Click to see the list of books available on your computer'), _('Click to see the list of books available on your computer'),
_('Click to see the list of books in the main memory of your reader'), _('Click to see the list of books in the main memory of your reader'),
_('Click to see the list of books on the storage card in your reader') _('Click to see the list of books on the storage card in your reader')
] ]
def rowCount(self, parent): def rowCount(self, parent):
return 1 + sum([1 for i in self.free if i >= 0]) return 1 + sum([1 for i in self.free if i >= 0])
def data(self, index, role): def data(self, index, role):
row = index.row() row = index.row()
data = NONE data = NONE
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
text = self.text[row]%(human_readable(self.free[row-1])) if row > 0 \ text = self.text[row]%(human_readable(self.free[row-1])) if row > 0 \
else self.text[row]%self.count else self.text[row]%self.count
data = QVariant(text) data = QVariant(text)
elif role == Qt.DecorationRole: elif role == Qt.DecorationRole:
data = self.icons[row] data = self.icons[row]
elif role == Qt.ToolTipRole: elif role == Qt.ToolTipRole:
data = QVariant(self.tooltips[row]) data = QVariant(self.tooltips[row])
@ -168,60 +204,70 @@ class LocationModel(QAbstractListModel):
font = QFont('monospace') font = QFont('monospace')
font.setBold(row == self.highlight_row) font.setBold(row == self.highlight_row)
data = QVariant(font) data = QVariant(font)
elif role == Qt.ForegroundRole and row == self.highlight_row:
return QVariant(QApplication.palette().brush(
QPalette.HighlightedText))
elif role == Qt.BackgroundRole and row == self.highlight_row:
return QVariant(QApplication.palette().brush(
QPalette.Highlight))
return data return data
def headerData(self, section, orientation, role): def headerData(self, section, orientation, role):
return NONE return NONE
def update_devices(self, cp=None, fs=[-1, -1, -1]): def update_devices(self, cp=None, fs=[-1, -1, -1]):
self.free[0] = fs[0] self.free[0] = fs[0]
self.free[1] = max(fs[1:]) self.free[1] = max(fs[1:])
if cp == None: if cp == None:
self.free[1] = -1 self.free[1] = -1
self.reset()
def location_changed(self, row):
self.highlight_row = row
self.reset() self.reset()
def location_changed(self, row):
self.highlight_row = row
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
self.index(0), self.index(self.rowCount(QModelIndex())-1))
class LocationView(QListView): class LocationView(QListView):
def __init__(self, parent): def __init__(self, parent):
QListView.__init__(self, parent) QListView.__init__(self, parent)
self.setModel(LocationModel(self)) self.setModel(LocationModel(self))
self.reset() self.reset()
QObject.connect(self.selectionModel(), SIGNAL('currentChanged(QModelIndex, QModelIndex)'), self.current_changed) self.setCursor(Qt.PointingHandCursor)
self.setCursor(Qt.PointingHandCursor) self.currentChanged = self.current_changed
def count_changed(self, new_count): def count_changed(self, new_count):
self.model().count = new_count self.model().count = new_count
self.model().reset() self.model().reset()
def current_changed(self, current, previous): def current_changed(self, current, previous):
i = current.row() if current.isValid():
location = 'library' if i == 0 else 'main' if i == 1 else 'card' i = current.row()
self.emit(SIGNAL('location_selected(PyQt_PyObject)'), location) location = 'library' if i == 0 else 'main' if i == 1 else 'card'
self.emit(SIGNAL('location_selected(PyQt_PyObject)'), location)
self.model().location_changed(i)
def location_changed(self, row): def location_changed(self, row):
if 0 <= row and row <= 2: if 0 <= row and row <= 2:
self.model().location_changed(row) self.model().location_changed(row)
class JobsView(TableView): class JobsView(TableView):
def __init__(self, parent): def __init__(self, parent):
TableView.__init__(self, parent) TableView.__init__(self, parent)
self.connect(self, SIGNAL('doubleClicked(QModelIndex)'), self.show_details) self.connect(self, SIGNAL('doubleClicked(QModelIndex)'), self.show_details)
def show_details(self, index): def show_details(self, index):
row = index.row() row = index.row()
job = self.model().row_to_job(row) job = self.model().row_to_job(row)
d = DetailView(self, job) d = DetailView(self, job)
self.connect(self.model(), SIGNAL('output_received()'), d.update) self.connect(self.model(), SIGNAL('output_received()'), d.update)
d.exec_() d.exec_()
class FontFamilyModel(QAbstractListModel): class FontFamilyModel(QAbstractListModel):
def __init__(self, *args): def __init__(self, *args):
QAbstractListModel.__init__(self, *args) QAbstractListModel.__init__(self, *args)
try: try:
@ -232,10 +278,10 @@ class FontFamilyModel(QAbstractListModel):
traceback.print_exc() traceback.print_exc()
self.families.sort() self.families.sort()
self.families[:0] = ['None'] self.families[:0] = ['None']
def rowCount(self, *args): def rowCount(self, *args):
return len(self.families) return len(self.families)
def data(self, index, role): def data(self, index, role):
try: try:
family = self.families[index.row()] family = self.families[index.row()]
@ -247,52 +293,52 @@ class FontFamilyModel(QAbstractListModel):
if role == Qt.FontRole: if role == Qt.FontRole:
return QVariant(QFont(family)) return QVariant(QFont(family))
return NONE return NONE
def index_of(self, family): def index_of(self, family):
return self.families.index(family.strip()) return self.families.index(family.strip())
class BasicListItem(QListWidgetItem): class BasicListItem(QListWidgetItem):
def __init__(self, text, user_data=None): def __init__(self, text, user_data=None):
QListWidgetItem.__init__(self, text) QListWidgetItem.__init__(self, text)
self.user_data = user_data self.user_data = user_data
def __eq__(self, other): def __eq__(self, other):
if hasattr(other, 'text'): if hasattr(other, 'text'):
return self.text() == other.text() return self.text() == other.text()
return False return False
class BasicList(QListWidget): class BasicList(QListWidget):
def add_item(self, text, user_data=None, replace=False): def add_item(self, text, user_data=None, replace=False):
item = BasicListItem(text, user_data) item = BasicListItem(text, user_data)
for oitem in self.items(): for oitem in self.items():
if oitem == item: if oitem == item:
if replace: if replace:
self.takeItem(self.row(oitem)) self.takeItem(self.row(oitem))
else: else:
raise ValueError('Item already in list') raise ValueError('Item already in list')
self.addItem(item) self.addItem(item)
def remove_selected_items(self, *args): def remove_selected_items(self, *args):
for item in self.selectedItems(): for item in self.selectedItems():
self.takeItem(self.row(item)) self.takeItem(self.row(item))
def items(self): def items(self):
for i in range(self.count()): for i in range(self.count()):
yield self.item(i) yield self.item(i)
class PythonHighlighter(QSyntaxHighlighter): class PythonHighlighter(QSyntaxHighlighter):
Rules = [] Rules = []
Formats = {} Formats = {}
Config = {} Config = {}
KEYWORDS = ["and", "as", "assert", "break", "class", "continue", "def", KEYWORDS = ["and", "as", "assert", "break", "class", "continue", "def",
"del", "elif", "else", "except", "exec", "finally", "for", "from", "del", "elif", "else", "except", "exec", "finally", "for", "from",
"global", "if", "import", "in", "is", "lambda", "not", "or", "global", "if", "import", "in", "is", "lambda", "not", "or",
@ -308,7 +354,7 @@ class PythonHighlighter(QSyntaxHighlighter):
"open", "ord", "pow", "property", "range", "reduce", "repr", "open", "ord", "pow", "property", "range", "reduce", "repr",
"reversed", "round", "set", "setattr", "slice", "sorted", "reversed", "round", "set", "setattr", "slice", "sorted",
"staticmethod", "str", "sum", "super", "tuple", "type", "unichr", "staticmethod", "str", "sum", "super", "tuple", "type", "unichr",
"unicode", "vars", "xrange", "zip"] "unicode", "vars", "xrange", "zip"]
CONSTANTS = ["False", "True", "None", "NotImplemented", "Ellipsis"] CONSTANTS = ["False", "True", "None", "NotImplemented", "Ellipsis"]
@ -317,7 +363,7 @@ class PythonHighlighter(QSyntaxHighlighter):
super(PythonHighlighter, self).__init__(parent) super(PythonHighlighter, self).__init__(parent)
if not self.Config: if not self.Config:
self.loadConfig() self.loadConfig()
self.initializeFormats() self.initializeFormats()
@ -356,7 +402,7 @@ class PythonHighlighter(QSyntaxHighlighter):
if value.isEmpty(): if value.isEmpty():
value = default value = default
Config[name] = value Config[name] = value
for name in ("window", "shell"): for name in ("window", "shell"):
Config["%swidth" % name] = settings.value("%swidth" % name, Config["%swidth" % name] = settings.value("%swidth" % name,
QVariant(QApplication.desktop() \ QVariant(QApplication.desktop() \
@ -385,9 +431,9 @@ class PythonHighlighter(QSyntaxHighlighter):
sys.stdout = codecs.getwriter("UTF8")(sys.stdout)""") sys.stdout = codecs.getwriter("UTF8")(sys.stdout)""")
setDefaultString("newfile", """\ setDefaultString("newfile", """\
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import division from __future__ import division
import sys import sys
""") """)
Config["backupsuffix"] = settings.value("backupsuffix", Config["backupsuffix"] = settings.value("backupsuffix",
@ -399,7 +445,7 @@ class PythonHighlighter(QSyntaxHighlighter):
QVariant(150)).toInt()[0] QVariant(150)).toInt()[0]
Config["maxlinestoscan"] = settings.value("maxlinestoscan", Config["maxlinestoscan"] = settings.value("maxlinestoscan",
QVariant(5000)).toInt()[0] QVariant(5000)).toInt()[0]
Config["pythondocpath"] = settings.value("pythondocpath", Config["pythondocpath"] = settings.value("pythondocpath",
QVariant("http://docs.python.org")).toString() QVariant("http://docs.python.org")).toString()
Config["autohidefinddialog"] = settings.value("autohidefinddialog", Config["autohidefinddialog"] = settings.value("autohidefinddialog",
QVariant(True)).toBool() QVariant(True)).toBool()
@ -512,7 +558,7 @@ class PythonHighlighter(QSyntaxHighlighter):
if i == -1: if i == -1:
i = text.length() i = text.length()
self.setCurrentBlockState(state) self.setCurrentBlockState(state)
self.setFormat(0, i + 3, self.setFormat(0, i + 3,
PythonHighlighter.Formats["string"]) PythonHighlighter.Formats["string"])
elif i > -1: elif i > -1:
self.setCurrentBlockState(state) self.setCurrentBlockState(state)

View File

@ -25,7 +25,8 @@ if iswindows:
else: else:
Structure = _Structure Structure = _Structure
if hasattr(sys, 'frozen') and iswindows: if hasattr(sys, 'frozen') and iswindows:
_libunrar = cdll.LoadLibrary(os.path.join(os.path.dirname(sys.executable), 'unrar.dll')) _libunrar = cdll.LoadLibrary(os.path.join(os.path.dirname(sys.executable),
'unrar.dll'))
_libunrar = load_library(_librar_name, cdll) _libunrar = load_library(_librar_name, cdll)
RAR_OM_LIST = 0 RAR_OM_LIST = 0
@ -95,7 +96,7 @@ class RARHeaderDataEx(Structure):
# Define a callback function # Define a callback function
#CALLBACK_FUNC = CFUNCTYPE(c_int, c_uint, c_long, c_char_p, c_long) #CALLBACK_FUNC = CFUNCTYPE(c_int, c_uint, c_long, c_char_p, c_long)
#def py_callback_func(msg, user_data, p1, p2): #def py_callback_func(msg, user_data, p1, p2):
# return 0 # return 0
#callback_func = CALLBACK_FUNC(py_callback_func) #callback_func = CALLBACK_FUNC(py_callback_func)
@ -123,7 +124,7 @@ def _interpret_open_error(code, path):
elif code == ERAR_EOPEN: elif code == ERAR_EOPEN:
msg = 'Cannot open ' + path msg = 'Cannot open ' + path
return msg return msg
def _interpret_process_file_error(code): def _interpret_process_file_error(code):
msg = 'Unknown Error' msg = 'Unknown Error'
if code == ERAR_UNKNOWN_FORMAT: if code == ERAR_UNKNOWN_FORMAT:
@ -145,7 +146,7 @@ def _interpret_process_file_error(code):
elif code == ERAR_MISSING_PASSWORD: elif code == ERAR_MISSING_PASSWORD:
msg = 'Password is required.' msg = 'Password is required.'
return msg return msg
def get_archive_info(flags): def get_archive_info(flags):
ios = StringIO() ios = StringIO()
print >>ios, 'Volume:\t\t', 'yes' if (flags & 1) else 'no' print >>ios, 'Volume:\t\t', 'yes' if (flags & 1) else 'no'
@ -162,7 +163,7 @@ def get_archive_info(flags):
def extract(path, dir): def extract(path, dir):
""" """
Extract archive C{filename} into directory C{dir} Extract archive C{filename} into directory C{dir}
""" """
open_archive_data = RAROpenArchiveDataEx(ArcName=path, OpenMode=RAR_OM_EXTRACT, CmtBuf=None) open_archive_data = RAROpenArchiveDataEx(ArcName=path, OpenMode=RAR_OM_EXTRACT, CmtBuf=None)
arc_data = _libunrar.RAROpenArchiveEx(byref(open_archive_data)) arc_data = _libunrar.RAROpenArchiveEx(byref(open_archive_data))
cwd = os.getcwd() cwd = os.getcwd()
@ -173,7 +174,7 @@ def extract(path, dir):
if open_archive_data.OpenResult != 0: if open_archive_data.OpenResult != 0:
raise UnRARException(_interpret_open_error(open_archive_data.OpenResult, path)) raise UnRARException(_interpret_open_error(open_archive_data.OpenResult, path))
print 'Archive:', path print 'Archive:', path
#print get_archive_info(open_archive_data.Flags) #print get_archive_info(open_archive_data.Flags)
header_data = RARHeaderDataEx(CmtBuf=None) header_data = RARHeaderDataEx(CmtBuf=None)
#_libunrar.RARSetCallback(arc_data, callback_func, mode) #_libunrar.RARSetCallback(arc_data, callback_func, mode)
while True: while True:
@ -240,5 +241,5 @@ def extract_member(path, match=re.compile(r'\.(jpg|jpeg|gif|png)\s*$', re.I), na
open(os.path.join(dir, *header_data.FileNameW.split('/')), 'rb').read() open(os.path.join(dir, *header_data.FileNameW.split('/')), 'rb').read()
finally: finally:
_libunrar.RARCloseArchive(arc_data) _libunrar.RARCloseArchive(arc_data)

View File

@ -41,6 +41,8 @@ entry_points = {
'calibre-customize = calibre.customize.ui:main', 'calibre-customize = calibre.customize.ui:main',
'pdftrim = calibre.ebooks.pdf.pdftrim:main' , 'pdftrim = calibre.ebooks.pdf.pdftrim:main' ,
'fetch-ebook-metadata = calibre.ebooks.metadata.fetch:main', 'fetch-ebook-metadata = calibre.ebooks.metadata.fetch:main',
'calibre-smtp = calibre.utils.smtp:main',
], ],
'gui_scripts' : [ 'gui_scripts' : [
__appname__+' = calibre.gui2.main:main', __appname__+' = calibre.gui2.main:main',
@ -160,6 +162,7 @@ def setup_completion(fatal_errors):
from calibre.ebooks.epub.from_comic import option_parser as comic2epub from calibre.ebooks.epub.from_comic import option_parser as comic2epub
from calibre.ebooks.metadata.fetch import option_parser as fem_op from calibre.ebooks.metadata.fetch import option_parser as fem_op
from calibre.gui2.main import option_parser as guiop from calibre.gui2.main import option_parser as guiop
from calibre.utils.smtp import option_parser as smtp_op
any_formats = ['epub', 'htm', 'html', 'xhtml', 'xhtm', 'rar', 'zip', any_formats = ['epub', 'htm', 'html', 'xhtml', 'xhtm', 'rar', 'zip',
'txt', 'lit', 'rtf', 'pdf', 'prc', 'mobi', 'fb2', 'odt'] 'txt', 'lit', 'rtf', 'pdf', 'prc', 'mobi', 'fb2', 'odt']
f = open_file('/etc/bash_completion.d/libprs500') f = open_file('/etc/bash_completion.d/libprs500')
@ -194,6 +197,7 @@ def setup_completion(fatal_errors):
f.write(opts_and_words('feeds2epub', feeds2epub, feed_titles)) f.write(opts_and_words('feeds2epub', feeds2epub, feed_titles))
f.write(opts_and_words('feeds2mobi', feeds2mobi, feed_titles)) f.write(opts_and_words('feeds2mobi', feeds2mobi, feed_titles))
f.write(opts_and_words('fetch-ebook-metadata', fem_op, [])) f.write(opts_and_words('fetch-ebook-metadata', fem_op, []))
f.write(opts_and_words('calibre-smtp', smtp_op, []))
f.write(''' f.write('''
_prs500_ls() _prs500_ls()
{ {
@ -403,7 +407,7 @@ def post_install():
if opts.save_manifest_to: if opts.save_manifest_to:
open(opts.save_manifest_to, 'wb').write('\n'.join(manifest)+'\n') open(opts.save_manifest_to, 'wb').write('\n'.join(manifest)+'\n')
from calibre.utils.config import config_dir from calibre.utils.config import config_dir
if os.path.exists(config_dir): if os.path.exists(config_dir):
os.chdir(config_dir) os.chdir(config_dir)
@ -516,7 +520,7 @@ def setup_desktop_integration(fatal_errors):
check_call('xdg-icon-resource install --size 128 calibre-gui.png calibre-gui', shell=True) check_call('xdg-icon-resource install --size 128 calibre-gui.png calibre-gui', shell=True)
render_svg(QFile(':/images/viewer.svg'), os.path.join(tdir, 'calibre-viewer.png')) render_svg(QFile(':/images/viewer.svg'), os.path.join(tdir, 'calibre-viewer.png'))
check_call('xdg-icon-resource install --size 128 calibre-viewer.png calibre-viewer', shell=True) check_call('xdg-icon-resource install --size 128 calibre-viewer.png calibre-viewer', shell=True)
f = open('calibre-lrfviewer.desktop', 'wb') f = open('calibre-lrfviewer.desktop', 'wb')
f.write(VIEWER) f.write(VIEWER)
f.close() f.close()

View File

@ -45,7 +45,7 @@ PARALLEL_FUNCS = {
('calibre.gui2.lrf_renderer.main', 'main', {}, None), ('calibre.gui2.lrf_renderer.main', 'main', {}, None),
'ebook-viewer' : 'ebook-viewer' :
('calibre.gui2.viewer.main', 'main', {}, None), ('calibre.gui2.viewer.main', 'main', {}, None),
'feeds2lrf' : 'feeds2lrf' :
('calibre.ebooks.lrf.feeds.convert_from', 'main', {}, 'notification'), ('calibre.ebooks.lrf.feeds.convert_from', 'main', {}, 'notification'),

View File

@ -13,7 +13,7 @@ def cleanup(path):
try: try:
import os import os
if os.path.exists(path): if os.path.exists(path):
os.remove(path) os.remove(path)
except: except:
pass pass

View File

@ -5,7 +5,7 @@ import re, textwrap
DEPENDENCIES = [ DEPENDENCIES = [
#(Generic, version, gentoo, ubuntu, fedora) #(Generic, version, gentoo, ubuntu, fedora)
('python', '2.5', None, None, None), ('python', '2.6', None, None, None),
('setuptools', '0.6c5', 'setuptools', 'python-setuptools', 'python-setuptools-devel'), ('setuptools', '0.6c5', 'setuptools', 'python-setuptools', 'python-setuptools-devel'),
('Python Imaging Library', '1.1.6', 'imaging', 'python-imaging', 'python-imaging'), ('Python Imaging Library', '1.1.6', 'imaging', 'python-imaging', 'python-imaging'),
('libusb', '0.1.12', None, None, None), ('libusb', '0.1.12', None, None, None),
@ -18,11 +18,12 @@ DEPENDENCIES = [
('lxml', '2.1.5', 'lxml', 'python-lxml', 'python-lxml'), ('lxml', '2.1.5', 'lxml', 'python-lxml', 'python-lxml'),
('python-dateutil', '1.4.1', 'python-dateutil', 'python-dateutil', 'python-dateutil'), ('python-dateutil', '1.4.1', 'python-dateutil', 'python-dateutil', 'python-dateutil'),
('BeautifulSoup', '3.0.5', 'beautifulsoup', 'python-beautifulsoup', 'python-BeautifulSoup'), ('BeautifulSoup', '3.0.5', 'beautifulsoup', 'python-beautifulsoup', 'python-BeautifulSoup'),
('dnspython', '1.6.0', 'dnspython', 'dnspython', 'dnspython', 'dnspython'),
] ]
class CoolDistro: class CoolDistro:
def __init__(self, name, title, prefix=''): def __init__(self, name, title, prefix=''):
self.title = title self.title = title
url = prefix + '/chrome/dl/images/%s_logo.png' url = prefix + '/chrome/dl/images/%s_logo.png'
@ -38,7 +39,7 @@ def get_linux_data(version='1.0.0'):
('foresight', 'Foresight 2.1'), ('foresight', 'Foresight 2.1'),
('ubuntu', 'Ubuntu Jaunty Jackalope'), ('ubuntu', 'Ubuntu Jaunty Jackalope'),
]: ]:
data['supported'].append(CoolDistro(name, title, data['supported'].append(CoolDistro(name, title,
prefix='http://calibre.kovidgoyal.net')) prefix='http://calibre.kovidgoyal.net'))
data['dependencies'] = DEPENDENCIES data['dependencies'] = DEPENDENCIES
return data return data
@ -53,51 +54,51 @@ if __name__ == '__main__':
return MarkupTemplate(raw).generate(**get_linux_data()).render('xhtml') return MarkupTemplate(raw).generate(**get_linux_data()).render('xhtml')
index.exposed = True index.exposed = True
t = Test() t = Test()
t.index() t.index()
cherrypy.quickstart(t) cherrypy.quickstart(t)
else: else:
from pkg_resources import resource_filename from pkg_resources import resource_filename
from trac.core import Component, implements from trac.core import Component, implements
from trac.web.chrome import INavigationContributor, ITemplateProvider, add_stylesheet from trac.web.chrome import INavigationContributor, ITemplateProvider, add_stylesheet
from trac.web.main import IRequestHandler from trac.web.main import IRequestHandler
from trac.util import Markup from trac.util import Markup
DOWNLOAD_DIR = '/var/www/calibre.kovidgoyal.net/htdocs/downloads' DOWNLOAD_DIR = '/var/www/calibre.kovidgoyal.net/htdocs/downloads'
MOBILEREAD = 'https://dev.mobileread.com/dist/kovid/calibre/' MOBILEREAD = 'https://dev.mobileread.com/dist/kovid/calibre/'
class OS(dict): class OS(dict):
"""Dictionary with a default value for unknown keys.""" """Dictionary with a default value for unknown keys."""
def __init__(self, dict): def __init__(self, dict):
self.update(dict) self.update(dict)
if not dict.has_key('img'): if not dict.has_key('img'):
self['img'] = self['name'] self['img'] = self['name']
class Download(Component): class Download(Component):
implements(INavigationContributor, IRequestHandler, ITemplateProvider) implements(INavigationContributor, IRequestHandler, ITemplateProvider)
request_pat = re.compile(r'\/download$|\/download_\S+') request_pat = re.compile(r'\/download$|\/download_\S+')
# INavigationContributor methods # INavigationContributor methods
def get_active_navigation_item(self, req): def get_active_navigation_item(self, req):
return 'download' return 'download'
def get_navigation_items(self, req): def get_navigation_items(self, req):
yield 'mainnav', 'download', Markup('<a href="/download">Get %s</a>'%(__appname__,)) yield 'mainnav', 'download', Markup('<a href="/download">Get %s</a>'%(__appname__,))
def get_templates_dirs(self): def get_templates_dirs(self):
return [resource_filename(__name__, 'templates')] return [resource_filename(__name__, 'templates')]
def get_htdocs_dirs(self): def get_htdocs_dirs(self):
return [('dl', resource_filename(__name__, 'htdocs'))] return [('dl', resource_filename(__name__, 'htdocs'))]
# IRequestHandler methods # IRequestHandler methods
def match_request(self, req): def match_request(self, req):
return self.__class__.request_pat.match(req.path_info) return self.__class__.request_pat.match(req.path_info)
def process_request(self, req): def process_request(self, req):
add_stylesheet(req, 'dl/css/download.css') add_stylesheet(req, 'dl/css/download.css')
if req.path_info == '/download': if req.path_info == '/download':
@ -114,29 +115,29 @@ else:
return self.osx(req) return self.osx(req)
elif os == 'linux': elif os == 'linux':
return self.linux(req) return self.linux(req)
def top_level(self, req): def top_level(self, req):
operating_systems = [ operating_systems = [
OS({'name' : 'windows', 'title' : 'Windows'}), OS({'name' : 'windows', 'title' : 'Windows'}),
OS({'name' : 'osx', 'title' : 'OS X'}), OS({'name' : 'osx', 'title' : 'OS X'}),
OS({'name' : 'linux', 'title' : 'Linux'}), OS({'name' : 'linux', 'title' : 'Linux'}),
] ]
data = dict(title='Get ' + __appname__, data = dict(title='Get ' + __appname__,
operating_systems=operating_systems, width=200, operating_systems=operating_systems, width=200,
font_size='xx-large', top_level=True) font_size='xx-large', top_level=True)
return 'download.html', data, None return 'download.html', data, None
def version_from_filename(self): def version_from_filename(self):
try: try:
return open(DOWNLOAD_DIR+'/latest_version', 'rb').read().strip() return open(DOWNLOAD_DIR+'/latest_version', 'rb').read().strip()
except: except:
return '0.0.0' return '0.0.0'
def windows(self, req): def windows(self, req):
version = self.version_from_filename() version = self.version_from_filename()
file = '%s-%s.exe'%(__appname__, version,) file = '%s-%s.exe'%(__appname__, version,)
data = dict(version = version, name='windows', data = dict(version = version, name='windows',
installer_name='Windows installer', installer_name='Windows installer',
title='Download %s for windows'%(__appname__), title='Download %s for windows'%(__appname__),
compatibility='%s works on Windows XP and Windows Vista.'%(__appname__,), compatibility='%s works on Windows XP and Windows Vista.'%(__appname__,),
path=MOBILEREAD+file, app=__appname__, path=MOBILEREAD+file, app=__appname__,
@ -145,7 +146,7 @@ else:
<p>If you are using the <b>SONY PRS-500</b> and %(appname)s does not detect your reader, read on:</p> <p>If you are using the <b>SONY PRS-500</b> and %(appname)s does not detect your reader, read on:</p>
<blockquote> <blockquote>
<p> <p>
If you are using 64-bit windows, you're out of luck. If you are using 64-bit windows, you're out of luck.
</p> </p>
<p> <p>
There may be a conflict with the USB driver from SONY. In windows, you cannot install two drivers There may be a conflict with the USB driver from SONY. In windows, you cannot install two drivers
@ -159,7 +160,7 @@ else:
</ul> </ul>
You can uninstall a driver by right clicking on it and selecting uninstall. You can uninstall a driver by right clicking on it and selecting uninstall.
</li> </li>
<li>Once the drivers have been uninstalled, find the file prs500.inf (it will be in the <li>Once the drivers have been uninstalled, find the file prs500.inf (it will be in the
driver folder in the folder in which you installed %(appname)s. Right click on it and driver folder in the folder in which you installed %(appname)s. Right click on it and
select Install.</li> select Install.</li>
</ol> </ol>
@ -167,12 +168,12 @@ else:
</blockquote> </blockquote>
'''%dict(appname=__appname__))) '''%dict(appname=__appname__)))
return 'binary.html', data, None return 'binary.html', data, None
def osx(self, req): def osx(self, req):
version = self.version_from_filename() version = self.version_from_filename()
file = 'calibre-%s.dmg'%(version,) file = 'calibre-%s.dmg'%(version,)
data = dict(version = version, name='osx', data = dict(version = version, name='osx',
installer_name='OS X universal dmg', installer_name='OS X universal dmg',
title='Download %s for OS X'%(__appname__), title='Download %s for OS X'%(__appname__),
compatibility='%s works on OS X Tiger and above.'%(__appname__,), compatibility='%s works on OS X Tiger and above.'%(__appname__,),
path=MOBILEREAD+file, app=__appname__, path=MOBILEREAD+file, app=__appname__,
@ -180,57 +181,57 @@ else:
u''' u'''
<ol> <ol>
<li>Before trying to use the command line tools, you must run the app at least once. This will ask you for you password and then setup the symbolic links for the command line tools.</li> <li>Before trying to use the command line tools, you must run the app at least once. This will ask you for you password and then setup the symbolic links for the command line tools.</li>
<li>The app cannot be run from within the dmg. You must drag it to a folder on your filesystem (The Desktop, Applications, wherever).</li> <li>The app cannot be run from within the dmg. You must drag it to a folder on your filesystem (The Desktop, Applications, wherever).</li>
<li>In order for localization of the user interface in your language, select your language in the preferences (by pressing u\2318+P) and select your language.</li> <li>In order for localization of the user interface in your language, select your language in the preferences (by pressing u\2318+P) and select your language.</li>
</ol> </ol>
''')) '''))
return 'binary.html', data, None return 'binary.html', data, None
def linux(self, req): def linux(self, req):
data = get_linux_data(version=self.version_from_filename()) data = get_linux_data(version=self.version_from_filename())
return 'linux.html', data, None return 'linux.html', data, None
LINUX_INSTALLER = textwrap.dedent(r''' LINUX_INSTALLER = textwrap.dedent(r'''
import sys, os, shutil, tarfile, subprocess, tempfile, urllib2, re, stat import sys, os, shutil, tarfile, subprocess, tempfile, urllib2, re, stat
MOBILEREAD='https://dev.mobileread.com/dist/kovid/calibre/' MOBILEREAD='https://dev.mobileread.com/dist/kovid/calibre/'
class TerminalController: class TerminalController:
BOL = '' #: Move the cursor to the beginning of the line BOL = '' #: Move the cursor to the beginning of the line
UP = '' #: Move the cursor up one line UP = '' #: Move the cursor up one line
DOWN = '' #: Move the cursor down one line DOWN = '' #: Move the cursor down one line
LEFT = '' #: Move the cursor left one char LEFT = '' #: Move the cursor left one char
RIGHT = '' #: Move the cursor right one char RIGHT = '' #: Move the cursor right one char
# Deletion: # Deletion:
CLEAR_SCREEN = '' #: Clear the screen and move to home position CLEAR_SCREEN = '' #: Clear the screen and move to home position
CLEAR_EOL = '' #: Clear to the end of the line. CLEAR_EOL = '' #: Clear to the end of the line.
CLEAR_BOL = '' #: Clear to the beginning of the line. CLEAR_BOL = '' #: Clear to the beginning of the line.
CLEAR_EOS = '' #: Clear to the end of the screen CLEAR_EOS = '' #: Clear to the end of the screen
# Output modes: # Output modes:
BOLD = '' #: Turn on bold mode BOLD = '' #: Turn on bold mode
BLINK = '' #: Turn on blink mode BLINK = '' #: Turn on blink mode
DIM = '' #: Turn on half-bright mode DIM = '' #: Turn on half-bright mode
REVERSE = '' #: Turn on reverse-video mode REVERSE = '' #: Turn on reverse-video mode
NORMAL = '' #: Turn off all modes NORMAL = '' #: Turn off all modes
# Cursor display: # Cursor display:
HIDE_CURSOR = '' #: Make the cursor invisible HIDE_CURSOR = '' #: Make the cursor invisible
SHOW_CURSOR = '' #: Make the cursor visible SHOW_CURSOR = '' #: Make the cursor visible
# Terminal size: # Terminal size:
COLS = None #: Width of the terminal (None for unknown) COLS = None #: Width of the terminal (None for unknown)
LINES = None #: Height of the terminal (None for unknown) LINES = None #: Height of the terminal (None for unknown)
# Foreground colors: # Foreground colors:
BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = '' BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
# Background colors: # Background colors:
BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = '' BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = '' BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
_STRING_CAPABILITIES = """ _STRING_CAPABILITIES = """
BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1 BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1
CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold
@ -238,29 +239,29 @@ else:
HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split() HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split()
_COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split() _COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
_ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split() _ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
def __init__(self, term_stream=sys.stdout): def __init__(self, term_stream=sys.stdout):
# Curses isn't available on all platforms # Curses isn't available on all platforms
try: import curses try: import curses
except: return except: return
# If the stream isn't a tty, then assume it has no capabilities. # If the stream isn't a tty, then assume it has no capabilities.
if not hasattr(term_stream, 'isatty') or not term_stream.isatty(): return if not hasattr(term_stream, 'isatty') or not term_stream.isatty(): return
# Check the terminal type. If we fail, then assume that the # Check the terminal type. If we fail, then assume that the
# terminal has no capabilities. # terminal has no capabilities.
try: curses.setupterm() try: curses.setupterm()
except: return except: return
# Look up numeric capabilities. # Look up numeric capabilities.
self.COLS = curses.tigetnum('cols') self.COLS = curses.tigetnum('cols')
self.LINES = curses.tigetnum('lines') self.LINES = curses.tigetnum('lines')
# Look up string capabilities. # Look up string capabilities.
for capability in self._STRING_CAPABILITIES: for capability in self._STRING_CAPABILITIES:
(attrib, cap_name) = capability.split('=') (attrib, cap_name) = capability.split('=')
setattr(self, attrib, self._tigetstr(cap_name) or '') setattr(self, attrib, self._tigetstr(cap_name) or '')
# Colors # Colors
set_fg = self._tigetstr('setf') set_fg = self._tigetstr('setf')
if set_fg: if set_fg:
@ -278,7 +279,7 @@ else:
if set_bg_ansi: if set_bg_ansi:
for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS): for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
setattr(self, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '') setattr(self, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '')
def _tigetstr(self, cap_name): def _tigetstr(self, cap_name):
# String capabilities can include "delays" of the form "$<2>". # String capabilities can include "delays" of the form "$<2>".
# For any modern terminal, we should be able to just ignore # For any modern terminal, we should be able to just ignore
@ -286,19 +287,19 @@ else:
import curses import curses
cap = curses.tigetstr(cap_name) or '' cap = curses.tigetstr(cap_name) or ''
return re.sub(r'\$<\d+>[/*]?', '', cap) return re.sub(r'\$<\d+>[/*]?', '', cap)
def render(self, template): def render(self, template):
return re.sub(r'\$\$|\${\w+}', self._render_sub, template) return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
def _render_sub(self, match): def _render_sub(self, match):
s = match.group() s = match.group()
if s == '$$': return s if s == '$$': return s
else: return getattr(self, s[2:-1]) else: return getattr(self, s[2:-1])
class ProgressBar: class ProgressBar:
BAR = '%3d%% ${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}\n' BAR = '%3d%% ${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}\n'
HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n' HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n'
def __init__(self, term, header): def __init__(self, term, header):
self.term = term self.term = term
if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL): if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL):
@ -308,7 +309,7 @@ else:
self.bar = term.render(self.BAR) self.bar = term.render(self.BAR)
self.header = self.term.render(self.HEADER % header.center(self.width)) self.header = self.term.render(self.HEADER % header.center(self.width))
self.cleared = 1 #: true if we haven't drawn the bar yet. self.cleared = 1 #: true if we haven't drawn the bar yet.
def update(self, percent, message=''): def update(self, percent, message=''):
if isinstance(message, unicode): if isinstance(message, unicode):
message = message.encode('utf-8', 'ignore') message = message.encode('utf-8', 'ignore')
@ -322,14 +323,14 @@ else:
(self.bar % (100*percent, '='*n, '-'*(self.width-10-n))) + (self.bar % (100*percent, '='*n, '-'*(self.width-10-n))) +
self.term.CLEAR_EOL + msg) self.term.CLEAR_EOL + msg)
sys.stdout.flush() sys.stdout.flush()
def clear(self): def clear(self):
if not self.cleared: if not self.cleared:
sys.stdout.write(self.term.BOL + self.term.CLEAR_EOL + sys.stdout.write(self.term.BOL + self.term.CLEAR_EOL +
self.term.UP + self.term.CLEAR_EOL + self.term.UP + self.term.CLEAR_EOL +
self.term.UP + self.term.CLEAR_EOL) self.term.UP + self.term.CLEAR_EOL)
self.cleared = 1 self.cleared = 1
def download_tarball(): def download_tarball():
try: try:
pb = ProgressBar(TerminalController(sys.stdout), 'Downloading calibre...') pb = ProgressBar(TerminalController(sys.stdout), 'Downloading calibre...')
@ -354,14 +355,14 @@ else:
print '%d%%, '%int(percent*100), print '%d%%, '%int(percent*100),
f.seek(0) f.seek(0)
return f return f
def extract_tarball(tar, destdir): def extract_tarball(tar, destdir):
print 'Extracting application files...' print 'Extracting application files...'
if hasattr(tar, 'read'): if hasattr(tar, 'read'):
subprocess.check_call(['tar', 'xjf', tar.name, '-C', destdir]) subprocess.check_call(['tar', 'xjf', tar.name, '-C', destdir])
else: else:
subprocess.check_call(['tar', 'xjf', tar, '-C', destdir]) subprocess.check_call(['tar', 'xjf', tar, '-C', destdir])
def main(): def main():
defdir = '/opt/calibre' defdir = '/opt/calibre'
destdir = raw_input('Enter the installation directory for calibre (Its contents will be deleted!)[%s]: '%defdir).strip() destdir = raw_input('Enter the installation directory for calibre (Its contents will be deleted!)[%s]: '%defdir).strip()
@ -371,12 +372,12 @@ else:
if os.path.exists(destdir): if os.path.exists(destdir):
shutil.rmtree(destdir) shutil.rmtree(destdir)
os.makedirs(destdir) os.makedirs(destdir)
f = download_tarball() f = download_tarball()
print 'Extracting files to %s ...'%destdir print 'Extracting files to %s ...'%destdir
extract_tarball(f, destdir) extract_tarball(f, destdir)
pi = os.path.join(destdir, 'calibre_postinstall') pi = os.path.join(destdir, 'calibre_postinstall')
subprocess.call(pi, shell=True) subprocess.call(pi, shell=True)
return 0 return 0
''') ''')

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

238
src/calibre/utils/smtp.py Normal file
View File

@ -0,0 +1,238 @@
from __future__ import with_statement
__license__ = 'GPL 3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
'''
This module implements a simple commandline SMTP client that supports:
* Delivery via an SMTP relay with SSL or TLS
* Background delivery with failures being saved in a maildir mailbox
'''
import sys, traceback, os
from email import encoders
def create_mail(from_, to, subject, text=None, attachment_data=None,
attachment_type=None, attachment_name=None):
assert text or attachment_data
from email.mime.multipart import MIMEMultipart
outer = MIMEMultipart()
outer['Subject'] = subject
outer['To'] = to
outer['From'] = from_
outer.preamble = 'You will not see this in a MIME-aware mail reader.\n'
if text is not None:
from email.mime.text import MIMEText
msg = MIMEText(text)
outer.attach(msg)
if attachment_data is not None:
from email.mime.base import MIMEBase
assert attachment_data and attachment_name
try:
maintype, subtype = attachment_type.split('/', 1)
except AttributeError:
maintype, subtype = 'application', 'octet-stream'
msg = MIMEBase(maintype, subtype)
msg.set_payload(attachment_data)
encoders.encode_base64(msg)
msg.add_header('Content-Disposition', 'attachment',
filename=attachment_name)
outer.attach(msg)
return outer.as_string()
def get_mx(host):
import dns.resolver
answers = list(dns.resolver.query(host, 'MX'))
answers.sort(cmp=lambda x, y: cmp(int(x.preference), int(y.preference)))
return [str(x.exchange) for x in answers]
def sendmail_direct(from_, to, msg, timeout, localhost, verbose):
import smtplib
hosts = get_mx(to.split('@')[-1].strip())
timeout=None # Non blocking sockets sometimes don't work
s = smtplib.SMTP(timeout=timeout, local_hostname=localhost)
s.set_debuglevel(verbose)
if not hosts:
raise ValueError('No mail server found for address: %s'%to)
last_error = last_traceback = None
for host in hosts:
try:
s.connect(host, 25)
s.sendmail(from_, [to], msg)
return s.quit()
except Exception, e:
last_error, last_traceback = e, traceback.format_exc()
if last_error is not None:
print last_traceback
raise IOError('Failed to send mail: '+repr(last_error))
def sendmail(msg, from_, to, localhost=None, verbose=0, timeout=30,
relay=None, username=None, password=None, encryption='TLS',
port=-1):
if relay is None:
for x in to:
return sendmail_direct(from_, x, msg, timeout, localhost, verbose)
import smtplib
cls = smtplib.SMTP if encryption == 'TLS' else smtplib.SMTP_SSL
timeout = None # Non-blocking sockets sometimes don't work
port = int(port)
s = cls(timeout=timeout, local_hostname=localhost)
s.set_debuglevel(verbose)
if port < 0:
port = 25 if encryption == 'TLS' else 465
s.connect(relay, port)
if encryption == 'TLS':
s.starttls()
s.ehlo()
if username is not None and password is not None:
s.login(username, password)
s.sendmail(from_, to, msg)
return s.quit()
def option_parser():
try:
from calibre.utils.config import OptionParser
OptionParser
except ImportError:
from optparse import OptionParser
import textwrap
parser = OptionParser(textwrap.dedent('''\
%prog [options] [from to text]
Send mail using the SMTP protocol. %prog has two modes of operation. In the
compose mode you specify from to and text and these are used to build and
send an email message. In the filter mode, %prog reads a complete email
message from STDIN and sends it.
text is the body of the email message.
If text is not specified, a complete email message is read from STDIN.
from is the email address of the sender and to is the email address
of the recipient. When a complete email is read from STDIN, from and to
are only used in the SMTP negotiation, the message headers are not modified.
'''))
c=parser.add_option_group('COMPOSE MAIL',
'Options to compose an email. Ignored if text is not specified').add_option
c('-a', '--attachment', help='File to attach to the email')
c('-s', '--subject', help='Subject of the email')
parser.add_option('-l', '--localhost',
help=('Host name of localhost. Used when connecting '
'to SMTP server.'))
r=parser.add_option_group('SMTP RELAY',
'Options to use an SMTP relay server to send mail. '
'%prog will try to send the email directly unless --relay is '
'specified.').add_option
r('-r', '--relay', help=('An SMTP relay server to use to send mail.'))
r('-p', '--port', default=-1,
help='Port to connect to on relay server. Default is to use 465 if '
'encryption method is SSL and 25 otherwise.')
r('-u', '--username', help='Username for relay')
r('-p', '--password', help='Password for relay')
r('-e', '--encryption-method', default='TLS',
choices=['TLS', 'SSL'],
help='Encryption method to use when connecting to relay. Choices are '
'TLS and SSL. Default is TLS.')
parser.add_option('-o', '--outbox', help='Path to maildir folder to store '
'failed email messages in.')
parser.add_option('-f', '--fork', default=False, action='store_true',
help='Fork and deliver message in background. '
'If you use this option, you should also use --outbox '
'to handle delivery failures.')
parser.add_option('-t', '--timeout', help='Timeout for connection')
parser.add_option('-v', '--verbose', default=0, action='count',
help='Be more verbose')
return parser
def extract_email_address(raw):
from email.utils import parseaddr
return parseaddr(raw)[-1]
def compose_mail(from_, to, text, subject=None, attachment=None,
attachment_name=None):
attachment_type = attachment_data = None
if attachment is not None:
try:
from calibre import guess_type
guess_type
except ImportError:
from mimetypes import guess_type
attachment_data = attachment.read() if hasattr(attachment, 'read') \
else open(attachment, 'rb').read()
attachment_type = guess_type(getattr(attachment, 'name', attachment))[0]
if attachment_name is None:
attachment_name = os.path.basename(getattr(attachment,
'name', attachment))
subject = subject if subject else 'no subject'
return create_mail(from_, to, subject, text=text,
attachment_data=attachment_data, attachment_type=attachment_type,
attachment_name=attachment_name)
def main(args=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(args)
if len(args) > 1:
msg = compose_mail(args[1], args[2], args[3], subject=opts.subject,
attachment=opts.attachment)
from_, to = args[1:3]
efrom, eto = map(extract_email_address, (from_, to))
eto = [eto]
else:
msg = sys.stdin.read()
from email.parser import Parser
from email.utils import getaddresses
eml = Parser.parsestr(msg, headersonly=True)
tos = eml.get_all('to', [])
ccs = eml.get_all('cc', [])
eto = getaddresses(tos + ccs)
if not eto:
raise ValueError('Email from STDIN does not specify any recipients')
efrom = getaddresses(eml.get_all('from', []))
if not efrom:
raise ValueError('Email from STDIN does not specify a sender')
efrom = efrom[0]
outbox = None
if opts.outbox is not None:
outbox = os.path.abspath(os.path.expanduser(opts.outbox))
from mailbox import Maildir
outbox = Maildir(opts.outbox, factory=None)
if opts.fork:
if os.fork() != 0:
return 0
try:
sendmail(msg, efrom, eto, localhost=opts.localhost, verbose=opts.verbose,
timeout=opts.timeout, relay=opts.relay, username=opts.username,
password=opts.password, port=opts.port,
encryption=opts.encryption_method)
except:
if outbox is not None:
outbox.add(msg)
print 'Delivery failed. Message saved to', opts.outbox
raise
return 0
def config(defaults=None):
from calibre.utils.config import Config, StringConfig
desc = _('Control email delivery')
c = Config('smtp',desc) if defaults is None else StringConfig(defaults,desc)
c.add_opt('from_')
c.add_opt('accounts', default={})
c.add_opt('relay_host')
c.add_opt('relay_port', default=25)
c.add_opt('relay_username')
c.add_opt('relay_password')
c.add_opt('encryption', default='TLS', choices=['TLS', 'SSL'])
return c
if __name__ == '__main__':
sys.exit(main())

File diff suppressed because it is too large Load Diff

View File

@ -36,7 +36,7 @@ recipe_modules = ['recipe_' + r for r in (
'el_universal', 'mediapart', 'wikinews_en', 'ecogeek', 'daily_mail', 'el_universal', 'mediapart', 'wikinews_en', 'ecogeek', 'daily_mail',
'new_york_review_of_books_no_sub', 'politico', 'adventuregamers', 'new_york_review_of_books_no_sub', 'politico', 'adventuregamers',
'mondedurable', 'instapaper', 'dnevnik_cro', 'vecernji_list', 'mondedurable', 'instapaper', 'dnevnik_cro', 'vecernji_list',
'nacional_cro', '24sata', 'nacional_cro', '24sata', 'dnevni_avaz', 'glas_srpske', '24sata_rs',
)] )]
import re, imp, inspect, time, os import re, imp, inspect, time, os
@ -95,7 +95,7 @@ def compile_recipe(src):
src = 'from %s.ebooks.lrf.web.profiles import DefaultProfile, FullContentProfile\n'%__appname__ + src src = 'from %s.ebooks.lrf.web.profiles import DefaultProfile, FullContentProfile\n'%__appname__ + src
src = '# coding: utf-8\n' + src src = '# coding: utf-8\n' + src
src = 'from __future__ import with_statement\n' + src src = 'from __future__ import with_statement\n' + src
src = src.replace('from libprs500', 'from calibre').encode('utf-8') src = src.replace('from libprs500', 'from calibre').encode('utf-8')
f.write(src) f.write(src)
f.close() f.close()

View File

@ -0,0 +1,52 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
'''
24sata.rs
'''
import re
from calibre.web.feeds.recipes import BasicNewsRecipe
class Ser24Sata(BasicNewsRecipe):
title = '24 Sata - Sr'
__author__ = 'Darko Miletic'
description = '24 sata portal vesti iz Srbije'
publisher = 'Ringier d.o.o.'
category = 'news, politics, entertainment, Serbia'
oldest_article = 1
max_articles_per_feed = 100
no_stylesheets = True
encoding = 'utf-8'
use_embedded_content = False
remove_javascript = True
language = _('Serbian')
extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} body{font-family: serif1, serif} .article_description{font-family: serif1, serif}'
html2lrf_options = [
'--comment', description
, '--category', category
, '--publisher', publisher
, '--ignore-tables'
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True'
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
feeds = [(u'Vesti Dana', u'http://www.24sata.rs/rss.php')]
def preprocess_html(self, soup):
soup.html['xml:lang'] = 'sr-Latn-RS'
soup.html['lang'] = 'sr-Latn-RS'
mtag = '<meta http-equiv="Content-Language" content="sr-Latn-RS"/>\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'
soup.head.insert(0,mtag)
return soup
def print_version(self, url):
article, sep, rest = url.partition('#')
return article.replace('/show.php','/_print.php')

View File

@ -5,7 +5,6 @@ __copyright__ = '2008-2009, Darko Miletic <darko.miletic at gmail.com>'
''' '''
b92.net b92.net
''' '''
import re import re
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
@ -13,57 +12,53 @@ class B92(BasicNewsRecipe):
title = 'B92' title = 'B92'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic'
description = 'Dnevne vesti iz Srbije i sveta' description = 'Dnevne vesti iz Srbije i sveta'
oldest_article = 2 publisher = 'B92'
publisher = 'B92.net' category = 'news, politics, Serbia'
category = 'news, politics, Serbia' oldest_article = 1
max_articles_per_feed = 100 max_articles_per_feed = 100
remove_javascript = True
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
cover_url = 'http://static.b92.net/images/fp/logo.gif' remove_javascript = True
encoding = 'cp1250'
language = _('Serbian') language = _('Serbian')
extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: serif1, serif} .article_description{font-family: sans1, sans-serif}' extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} body{font-family: serif1, serif} .article_description{font-family: serif1, serif}'
html2lrf_options = [ html2lrf_options = [
'--comment' , description '--comment', description
, '--category' , category , '--category', category
, '--publisher', publisher , '--publisher', publisher
, '--ignore-tables' , '--ignore-tables'
] ]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True' html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True\noverride_css=" p {text-indent: 0em; margin-top: 0em; margin-bottom: 0.5em}"'
keep_only_tags = [ dict(name='div', attrs={'class':'sama_vest'}) ]
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')] preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [dict(name='table', attrs={'class':'maindocument'})]
remove_tags = [
dict(name='ul', attrs={'class':'comment-nav'})
,dict(name=['embed','link','base'] )
]
feeds = [ feeds = [
(u'Vesti', u'http://www.b92.net/info/rss/vesti.xml') (u'Vesti', u'http://www.b92.net/info/rss/vesti.xml')
,(u'Biz' , u'http://www.b92.net/info/rss/biz.xml' ) ,(u'Biz' , u'http://www.b92.net/info/rss/biz.xml' )
,(u'Zivot', u'http://www.b92.net/info/rss/zivot.xml')
,(u'Sport', u'http://www.b92.net/info/rss/sport.xml')
] ]
def print_version(self, url): def print_version(self, url):
main, sep, article_id = url.partition('nav_id=') return url + '&version=print'
rmain, rsep, rrest = main.partition('.php?')
mrmain , rsepp, nnt = rmain.rpartition('/')
mprmain, rrsep, news_type = mrmain.rpartition('/')
nurl = 'http://www.b92.net/mobilni/' + news_type + '/index.php?nav_id=' + article_id
brbiz, biz, bizrest = rmain.partition('/biz/')
if biz:
nurl = 'http://www.b92.net/mobilni/biz/index.php?nav_id=' + article_id
return nurl
def preprocess_html(self, soup): def preprocess_html(self, soup):
lng = 'sr-Latn-RS' del soup.body['onload']
soup.html['xml:lang'] = lng mtag = '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>'
soup.html['lang'] = lng soup.head.insert(0,mtag)
mtag = '<meta http-equiv="Content-Language" content="sr-Latn-RS"/>'
soup.head.insert(0,mtag)
for item in soup.findAll(style=True): for item in soup.findAll(style=True):
del item['style'] del item['style']
for item in soup.findAll(name='img',align=True): for item in soup.findAll(align=True):
del item['align'] del item['align']
item.insert(0,'<br /><br />') for item in soup.findAll('font'):
item.name='p'
if item.has_key('size'):
del item['size']
return soup return soup

View File

@ -0,0 +1,55 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
'''
dnevniavaz.ba
'''
import re
from calibre.web.feeds.recipes import BasicNewsRecipe
class DnevniAvaz(BasicNewsRecipe):
title = 'Dnevni Avaz'
__author__ = 'Darko Miletic'
description = 'Latest news from Bosnia'
publisher = 'Dnevni Avaz'
category = 'news, politics, Bosnia and Herzegovina'
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
encoding = 'utf-8'
use_embedded_content = False
remove_javascript = True
cover_url = 'http://www.dnevniavaz.ba/img/logo.gif'
lang = 'bs-BA'
language = _('Bosnian')
extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} body{font-family: serif1, serif} .article_description{font-family: serif1, serif}'
html2lrf_options = [
'--comment', description
, '--category', category
, '--publisher', publisher
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\noverride_css=" p {text-indent: 0em; margin-top: 0em; margin-bottom: 0.5em} img {margin-top: 0em; margin-bottom: 0.4em}"'
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [dict(name='div', attrs={'id':['fullarticle-title','fullarticle-leading','fullarticle-date','fullarticle-text','articleauthor']})]
remove_tags = [dict(name=['object','link','base'])]
feeds = [
(u'Najnovije' , u'http://www.dnevniavaz.ba/rss/novo' )
,(u'Najpopularnije', u'http://www.dnevniavaz.ba/rss/popularno')
]
def preprocess_html(self, soup):
soup.html['xml:lang'] = self.lang
soup.html['lang'] = self.lang
mtag = '<meta http-equiv="Content-Language" content="bs-BA"/>\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'
soup.head.insert(0,mtag)
return soup

View File

@ -1,29 +1,50 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008-2009, Kovid Goyal <kovid at kovidgoyal.net>, Darko Miletic <darko at gmail.com>'
''' '''
Profile to download FAZ.net Profile to download FAZ.net
''' '''
import re
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class FazNet(BasicNewsRecipe):
class FazNet(BasicNewsRecipe): title = 'FAZ NET'
__author__ = 'Kovid Goyal, Darko Miletic'
title = 'FAZ NET' description = 'Frankfurter Allgemeine Zeitung'
__author__ = 'Kovid Goyal' publisher = 'FAZ Electronic Media GmbH'
description = 'Frankfurter Allgemeine Zeitung' category = 'news, politics, Germany'
use_embedded_content = False use_embedded_content = False
language = _('German') language = _('German')
max_articles_per_feed = 30 max_articles_per_feed = 30
no_stylesheets = True
preprocess_regexps = [ encoding = 'utf-8'
(re.compile(r'Zum Thema</span>.*?</BODY>', re.IGNORECASE | re.DOTALL), remove_javascript = True
lambda match : ''),
] html2lrf_options = [
'--comment', description
, '--category', category
feeds = [ ('FAZ.NET', 'http://www.faz.net/s/Rub/Tpl~Epartner~SRss_.xml') ] , '--publisher', publisher
]
def print_version(self, url):
return url.replace('.html?rss_aktuell', '~Afor~Eprint.html') html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
keep_only_tags = [dict(name='div', attrs={'class':'Article'})]
remove_tags = [
dict(name=['object','link','embed','base'])
,dict(name='div', attrs={'class':['LinkBoxModulSmall','ModulVerlagsInfo']})
]
feeds = [ ('FAZ.NET', 'http://www.faz.net/s/Rub/Tpl~Epartner~SRss_.xml') ]
def print_version(self, url):
article, sep, rest = url.partition('?')
return article.replace('.html', '~Afor~Eprint.html')
def preprocess_html(self, soup):
mtag = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>'
soup.head.insert(0,mtag)
del soup.body['onload']
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -0,0 +1,96 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
'''
glassrpske.com
'''
import re
from calibre.web.feeds.recipes import BasicNewsRecipe
class GlasSrpske(BasicNewsRecipe):
title = 'Glas Srpske'
__author__ = 'Darko Miletic'
description = 'Latest news from republika srpska'
publisher = 'GLAS SRPSKE'
category = 'Novine, Dnevne novine, Vijesti, Novosti, Ekonomija, Sport, Crna Hronika, Banja Luka,, Republika Srpska, Bosna i Hercegovina'
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
encoding = 'utf-8'
use_embedded_content = False
remove_javascript = True
cover_url = 'http://www.glassrpske.com/var/slike/glassrpske-logo.png'
lang = 'sr-BA'
language = _('Serbian')
INDEX = 'http://www.glassrpske.com'
extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} body{font-family: serif1, serif} .article_description{font-family: serif1, serif}'
html2lrf_options = [
'--comment', description
, '--category', category
, '--publisher', publisher
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\noverride_css=" p {text-indent: 0em; margin-top: 0em; margin-bottom: 0.5em} img {margin-top: 0em; margin-bottom: 0.4em}"'
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [dict(name='div', attrs={'class':'gl_cv paragraf'})]
remove_tags = [dict(name=['object','link','base'])]
feeds = [
(u'Novosti' , u'http://www.glassrpske.com/vijest/2/novosti/lat/' )
,(u'Drustvo' , u'http://www.glassrpske.com/vijest/3/drustvo/lat/' )
,(u'Biznis' , u'http://www.glassrpske.com/vijest/4/ekonomija/lat/' )
,(u'Kroz RS' , u'http://www.glassrpske.com/vijest/5/krozrs/lat/' )
,(u'Hronika' , u'http://www.glassrpske.com/vijest/6/hronika/lat/' )
,(u'Srbija' , u'http://www.glassrpske.com/vijest/8/srbija/lat/' )
,(u'Region' , u'http://www.glassrpske.com/vijest/18/region/lat/' )
,(u'Svijet' , u'http://www.glassrpske.com/vijest/12/svijet/lat/' )
,(u'Kultura' , u'http://www.glassrpske.com/vijest/9/kultura/lat/' )
,(u'Banja Luka', u'http://www.glassrpske.com/vijest/10/banjaluka/lat/')
,(u'Jet Set' , u'http://www.glassrpske.com/vijest/11/jetset/lat/' )
,(u'Muzika' , u'http://www.glassrpske.com/vijest/19/muzika/lat/' )
,(u'Sport' , u'http://www.glassrpske.com/vijest/13/sport/lat/' )
,(u'Kolumne' , u'http://www.glassrpske.com/vijest/16/kolumne/lat/' )
,(u'Plus' , u'http://www.glassrpske.com/vijest/7/plus/lat/' )
]
def preprocess_html(self, soup):
soup.html['xml:lang'] = self.lang
soup.html['lang'] = self.lang
mtag = '<meta http-equiv="Content-Language" content="sr-BA"/>\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'
soup.head.insert(0,mtag)
return soup
def parse_index(self):
totalfeeds = []
lfeeds = self.get_feeds()
for feedobj in lfeeds:
feedtitle, feedurl = feedobj
self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl))
articles = []
soup = self.index_to_soup(feedurl)
for item in soup.findAll('div', attrs={'class':'gl_rub'}):
atag = item.find('a')
ptag = item.find('p')
datetag = item.find('span')
url = self.INDEX + atag['href']
title = self.tag_to_string(atag)
description = self.tag_to_string(ptag)
date,sep,rest = self.tag_to_string(ptag).partition('|')
articles.append({
'title' :title
,'date' :date
,'url' :url
,'description':description
})
totalfeeds.append((feedtitle, articles))
return totalfeeds

View File

@ -8,19 +8,20 @@ joelonsoftware.com
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class Joelonsoftware(BasicNewsRecipe): class Joelonsoftware(BasicNewsRecipe):
title = 'Joel on Software' title = 'Joel on Software'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic'
description = 'Painless Software Management' description = 'Painless Software Management'
language = _('English') language = _('English')
no_stylesheets = True no_stylesheets = True
use_embedded_content = True use_embedded_content = True
oldest_article = 60
cover_url = 'http://www.joelonsoftware.com/RssJoelOnSoftware.jpg' cover_url = 'http://www.joelonsoftware.com/RssJoelOnSoftware.jpg'
html2lrf_options = [ '--comment' , description html2lrf_options = [ '--comment' , description
, '--category' , 'blog,software,news' , '--category' , 'blog,software,news'
, '--author' , 'Joel Spolsky' , '--author' , 'Joel Spolsky'
] ]
feeds = [(u'Articles', u'http://www.joelonsoftware.com/rss.xml')] feeds = [(u'Articles', u'http://www.joelonsoftware.com/rss.xml')]

View File

@ -8,11 +8,11 @@ Fetch Spiegel Online.
import re import re
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import BeautifulSoup
class SpeigelOnline(BasicNewsRecipe): class SpeigelOnline(BasicNewsRecipe):
title = 'Spiegel Online' title = 'Spiegel Online'
description = 'Nachrichten des Magazins Der Spiegel' description = 'Nachrichten des Magazins Der Spiegel'
__author__ = 'Kovid Goyal' __author__ = 'Kovid Goyal'
use_embedded_content = False use_embedded_content = False
@ -22,17 +22,26 @@ class SpeigelOnline(BasicNewsRecipe):
no_stylesheets = True no_stylesheets = True
preprocess_regexps = \ preprocess_regexps = \
[ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
[ [
# Remove Zum Thema footer # Remove Zum Thema footer
(r'<div class="spArticleCredit.*?</body>', lambda match: '</body>'), (r'<div class="spArticleCredit.*?</body>', lambda match: '</body>'),
] ]
] ]
feeds= [ ('Spiegel Online', 'http://www.spiegel.de/schlagzeilen/rss/0,5291,,00.xml') ]
feeds= [ ('Spiegel Online', 'http://www.spiegel.de/schlagzeilen/rss/0,5291,,00.xml') ]
def print_version(self,url): def print_version(self,url):
tokens = url.split(',') tokens = url.split(',')
tokens[-2:-2] = ['druck|'] tokens[-2:-2] = ['druck|']
return ','.join(tokens).replace('|,','-') return ','.join(tokens).replace('|,','-')
def postprocess_html(self, soup, first_fetch):
if soup.contents[0].name == 'head':
x = BeautifulSoup('<html></html>')
for y in reversed(soup.contents):
x.contents[0].insert(0, y)
soup = x
return soup

View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

View File

@ -0,0 +1,85 @@
changes:
date: 2008-09-17
change: Enabled the ability to override the default template names.
date: 2008-08-26
change: Upgraded post_detail.html to now use new Django refactored comments. Sidenote: basic.remarks have been removed.
date: 2008-07-14
change: Removed get_query_set from Blog manager to fix a problem where saving a post marked as Draft would not save.
change: Added a get_previous_post and get_next_post method for front end template. These will not return Draft posts.
date: 2008-06-17
change: BlogPostFeed is now BlogPostsFeed and there is a new BlogPostsByCategory.
date: 2008-05-18
change: Converted everything to 4 space tabs and made a few other changes to comply with Python Style Guide.
date: 2008-04-23
change: Added an inline admin interface helper for choosing inlines to go into posts.
change: The inline app is now a dependancy of the blog.
date: 2008-04-22
change: Removed the 'render_inlines' filter from the Blog template tags. The tag is now in an app called inlines which can be used with any django app.
date: 2008-02-27
change: Added 'allow_comments' field to the Post model.
change: Removed 'Closed' choice from status field of Post model
date: 2008-02-18
fix: Fixed feed pointing to hardcoded url.
date: 2008-02-15
change: Internationalized models
date: 2008-02-04
change: Added 'get_links' template filter.
change: Templates: added a {% block content_title %}
date: 2008-02-02
change: Added a sitemap
date: 2008-01-30
change: Renamed 'do_inlines' filter to 'render_inlines'
date: 2008-01-29
change: BeautifulSoup is no longer a dependancy unless you want to use the do_inlines filter.
date: 2008-01-27
fix: removed 'tagging.register(Post)' from model. It was causing too many unnecessary SQL JOINS.
change: Changed the inlines tag to a filter. (Example: {{ object.text|do_inlines }})
date: 2008-01-22
change: Registered the Post model with the tagging app
date: 2008-01-19
change: Renamed the 'list' class to 'link_list'
date: 2008-01-09
change: Changed urls.py so you can have /posts/page/2/ or /posts/?page=2
date: 2008-01-07
change: Removed PublicPostManager in favor of ManagerWithPublished.
change: Made wrappers for generic views.
date: 2008-01-06
fix: In blog.py changed 'beautifulsoup' to 'BeautifulSoup'
date: 2007-12-31
change: Changed some syntax in managers.py to hopefully fix a bug.
change: Removed an inline template that didn't belong.
date: 2007-12-21
change: Added markup tag that formats inlines.
date: 2007-12-12
change: Cleaned up unit tests.
date: 2007-12-11
change: Add documentation to templatetags and views.
change: Smartened up the previous/next blog part of the post_detail.html template.
date: 2007-12-09
change: Added feed templates and wrapped up feeds.py.
change: Changed Post.live manager to Post.public
change: Added a search view along with templates

View File

@ -0,0 +1,18 @@
===========================================
Django Basic Blog
http://code.google.com/p/django-basic-apps/
===========================================
A simple blog application for Django projects.
To install this app, simply create a folder somewhere in
your PYTHONPATH named 'basic' and place the 'blog'
app inside. Then add 'basic.blog' to your projects
INSTALLED_APPS list in your settings.py file.
=== Dependancies ===
* Basic Inlines
* [http://www.djangoproject.com/documentation/add_ons/#comments Django Comments]
* [http://code.google.com/p/django-tagging Django Tagging]
* [http://www.djangoproject.com/documentation/add_ons/#markup Markup]
* [http://www.crummy.com/software/BeautifulSoup/ BeautifulSoup] - only if you want to use the [http://code.google.com/p/django-basic-blog/wiki/BlogInlinesProposal render_inlines] filter, otherwise it's not necessary.

View File

View File

@ -0,0 +1,17 @@
from django.contrib import admin
from calibre.www.apps.blog.models import *
class CategoryAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('title',)}
admin.site.register(Category, CategoryAdmin)
class PostAdmin(admin.ModelAdmin):
list_display = ('title', 'publish', 'status')
list_filter = ('publish', 'categories', 'status')
search_fields = ('title', 'body')
prepopulated_fields = {'slug': ('title',)}
admin.site.register(Post, PostAdmin)

View File

@ -0,0 +1,42 @@
from django.contrib.syndication.feeds import FeedDoesNotExist
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.sites.models import Site
from django.contrib.syndication.feeds import Feed
from django.core.urlresolvers import reverse
from calibre.www.apps.blog.models import Post, Category
class BlogPostsFeed(Feed):
_site = Site.objects.get_current()
title = '%s feed' % _site.name
description = '%s posts feed.' % _site.name
def link(self):
return reverse('blog_index')
def items(self):
return Post.objects.published()[:10]
def item_pubdate(self, obj):
return obj.publish
class BlogPostsByCategory(Feed):
_site = Site.objects.get_current()
title = '%s posts category feed' % _site.name
def get_object(self, bits):
if len(bits) != 1:
raise ObjectDoesNotExist
return Category.objects.get(slug__exact=bits[0])
def link(self, obj):
if not obj:
raise FeedDoesNotExist
return obj.get_absolute_url()
def description(self, obj):
return "Posts recently categorized as %s" % obj.title
def items(self, obj):
return obj.post_set.published()[:10]

View File

@ -0,0 +1,9 @@
from django.db.models import Manager
import datetime
class PublicManager(Manager):
"""Returns published posts that are not in the future."""
def published(self):
return self.get_query_set().filter(status__gte=2, publish__lte=datetime.datetime.now())

View File

@ -0,0 +1,80 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.db.models import permalink
from django.contrib.auth.models import User
from calibre.www.apps.tagging.fields import TagField
from calibre.www.apps.blog.managers import PublicManager
import calibre.www.apps.tagging as tagging
class Category(models.Model):
"""Category model."""
title = models.CharField(_('title'), max_length=100)
slug = models.SlugField(_('slug'), unique=True)
class Meta:
verbose_name = _('category')
verbose_name_plural = _('categories')
db_table = 'blog_categories'
ordering = ('title',)
class Admin:
pass
def __unicode__(self):
return u'%s' % self.title
@permalink
def get_absolute_url(self):
return ('blog_category_detail', None, {'slug': self.slug})
class Post(models.Model):
"""Post model."""
STATUS_CHOICES = (
(1, _('Draft')),
(2, _('Public')),
)
title = models.CharField(_('title'), max_length=200)
slug = models.SlugField(_('slug'), unique_for_date='publish')
author = models.ForeignKey(User, blank=True, null=True)
body = models.TextField(_('body'))
tease = models.TextField(_('tease'), blank=True)
status = models.IntegerField(_('status'), choices=STATUS_CHOICES, default=2)
allow_comments = models.BooleanField(_('allow comments'), default=True)
publish = models.DateTimeField(_('publish'))
created = models.DateTimeField(_('created'), auto_now_add=True)
modified = models.DateTimeField(_('modified'), auto_now=True)
categories = models.ManyToManyField(Category, blank=True)
tags = TagField()
objects = PublicManager()
class Meta:
verbose_name = _('post')
verbose_name_plural = _('posts')
db_table = 'blog_posts'
ordering = ('-publish',)
get_latest_by = 'publish'
class Admin:
list_display = ('title', 'publish', 'status')
list_filter = ('publish', 'categories', 'status')
search_fields = ('title', 'body')
def __unicode__(self):
return u'%s' % self.title
@permalink
def get_absolute_url(self):
return ('blog_detail', None, {
'year': self.publish.year,
'month': self.publish.strftime('%b').lower(),
'day': self.publish.day,
'slug': self.slug
})
def get_previous_post(self):
return self.get_previous_by_publish(status__gte=2)
def get_next_post(self):
return self.get_next_by_publish(status__gte=2)

View File

@ -0,0 +1,13 @@
from django.contrib.sitemaps import Sitemap
from calibre.www.apps.blog.models import Post
class BlogSitemap(Sitemap):
changefreq = "never"
priority = 0.5
def items(self):
return Post.objects.published()
def lastmod(self, obj):
return obj.publish

View File

@ -0,0 +1,56 @@
{% extends "admin/change_form.html" %}
{% block extrahead %}
{% load adminmedia inlines %}
{{ block.super }}
<script type="text/javascript">
function InlineInit() {
var body_div = document.getElementById('id_body').parentNode;
var content = ''
content += '{% get_inline_types as inline_list %}'
content += '<label>Body inlines:</label>'
content += '<strong>Inline type:</strong> '
content += '<select id="id_inline_content_type" onchange="document.getElementById(\'lookup_id_inline\').href = \'../../../\'+this.value+\'/\';" style="margin-right:20px;">'
content += ' <option>----------</option>'
content += ' {% for inline in inline_list %}'
content += ' <option value="{{ inline.content_type.app_label }}/{{ inline.content_type.model }}">{{ inline.content_type.app_label|capfirst }}: {{ inline.content_type.model|capfirst }}</option>'
content += ' {% endfor %}'
content += '</select> '
content += '<strong>Object:</strong> '
content += '<input type="text" class="vIntegerField" id="id_inline" size="10" /> '
content += '<a id="lookup_id_inline" href="#" class="related-lookup" onclick="if(document.getElementById(\'id_inline_content_type\').value != \'----------\') { return showRelatedObjectLookupPopup(this); }" style="margin-right:20px;"><img src="{% admin_media_prefix %}img/admin/selector-search.gif" width="16" height="16" alt="Loopup" /></a> '
content += '<strong>Class:</strong> '
content += '<select id="id_inline_class">'
content += ' <option value="small_left">Small left</option>'
content += ' <option value="small_right">Small right</option>'
content += ' <option value="medium_left">Medium left</option>'
content += ' <option value="medium_right">Medium right</option>'
content += ' <option value="large_left">Large left</option>'
content += ' <option value="large_right">Large right</option>'
content += ' <option value="full">Full</option>'
content += '</select>'
content += '<input type="button" value="Add" style="margin-left:10px;" onclick="return insertInline(document.getElementById(\'id_inline_content_type\').value, document.getElementById(\'id_inline\').value, document.getElementById(\'id_inline_class\').value)" />'
content += '<p class="help">Insert inlines into your body by choosing an inline type, then an object, then a class.</p>'
var div = document.createElement('div');
div.setAttribute('style', 'margin-top:10px;');
div.innerHTML = content;
body_div.insertBefore(div);
}
function insertInline(type, id, classname) {
if (type != '----------' && id != '') {
inline = '<inline type="'+type.replace('/', '.')+'" id="'+id+'" class="'+classname+'" />';
body = document.getElementById('id_body');
body.value = body.value + inline + '\n';
}
}
addEvent(window, 'load', InlineInit);
</script>
{% endblock %}

View File

@ -0,0 +1,21 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>{% block title %}{% endblock %}</title>
</head>
<body id="{% block body_id %}{% endblock %}">
<div id="body">
{% block body %}
<div>
{% block content_title %}{% endblock %}
</div>
<div class="content">
{% block content %}{% endblock %}
</div>
{% endblock %}
</div>
</body>
</html>

View File

@ -0,0 +1,4 @@
{% extends "base.html" %}
{% block body_class %}blog{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends "blog/base_blog.html" %}
{% block title %}Posts for {{ category.title }}{% endblock %}
{% block body_class %}{{ block.super }} category_detail{% endblock %}
{% block body_id %}category_{{ category.id }}{% endblock %}
{% block content_title %}
<h2>Posts for {{ category.title }}</h2>
{% endblock %}
{% block content %}
{% load markup %}
<div class="post_list">
{% for post in object_list %}
<div>
<h3 class="title"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
<p class="date">{{ post.publish|date:"Y F d" }}</p>
<p class="tease">{{ post.tease }}</p>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "blog/base_blog.html" %}
{% block title %}Post categories{% endblock %}
{% block body_class %}{{ block.super }} category_list{% endblock %}
{% block content_title %}
<h2>Post categories</h2>
{% endblock %}
{% block content %}
{% load markup %}
<ul class="link_list">
{% for category in object_list %}
<li><a href="{{ category.get_absolute_url }}">{{ category }}</a></li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "blog/base_blog.html" %}
{% block title %}Post archive for {{ day|date:"d F Y" }}{% endblock %}
{% block body_class %}{{ block.super }} post_archive_day{% endblock %}
{% block content_title %}
<h2>Post archive for {{ day|date:"d F Y" }}</h2>
{% endblock %}
{% block content %}
<div class="post_list">
{% for post in object_list %}
<div>
<h3 class="title"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
<p class="date">{{ post.publish|date:"Y F d" }}</p>
<p class="tease">{{ post.tease }}</p>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "blog/base_blog.html" %}
{% block title %}Post archive for {{ month|date:"F Y" }}{% endblock %}
{% block body_class %}{{ block.super }} post_archive_month{% endblock %}
{% block content_title %}
<h2>Post archive for {{ month|date:"F Y" }}</h2>
{% endblock %}
{% block content %}
<div class="post_list">
{% for post in object_list %}
<div>
<h3 class="title"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
<p class="date">{{ post.publish|date:"Y F d" }}</p>
<p class="tease">{{ post.tease }}</p>
</div>
{% endfor %}
</div>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More