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/stylesheets/.svn/
src/odf/.svn
tags

View File

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

View File

@ -57,7 +57,7 @@ r'''
def _check_symlinks_prescript():
import os, tempfile, traceback, sys
from Authorization import Authorization, kAuthorizationFlagDestroyRights
AUTHTOOL="""#!%(sp)s
import os
scripts = %(sp)s
@ -71,13 +71,13 @@ for s, l in zip(scripts, links):
os.symlink(s, l)
os.umask(omask)
"""
dest_path = %(dest_path)s
resources_path = os.environ['RESOURCEPATH']
scripts = %(scripts)s
scripts = %(scripts)s
links = [os.path.join(dest_path, i) for i in scripts]
scripts = [os.path.join(resources_path, 'loaders', i) for i in scripts]
bad = False
for s, l in zip(scripts, links):
if os.path.exists(l) and os.path.exists(os.path.realpath(l)):
@ -111,22 +111,22 @@ _check_symlinks_prescript()
packages=self.packages,
excludes=self.excludes,
debug=debug)
@classmethod
def makedmg(cls, d, volname,
destdir='dist',
def makedmg(cls, d, volname,
destdir='dist',
internet_enable=True,
format='UDBZ'):
''' Copy a directory d into a dmg named volname '''
dmg = os.path.join(destdir, volname+'.dmg')
if os.path.exists(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])
if internet_enable:
subprocess.check_call(['/usr/bin/hdiutil', 'internet-enable', '-yes', dmg])
return dmg
@classmethod
def qt_dependencies(cls, path):
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():
match = re.search(r'(.*)\(', l)
if not match:
continue
continue
lib = match.group(1).strip()
if lib.startswith(BuildAPP.QT_PREFIX):
deps.append(lib)
return deps
@classmethod
def fix_qt_dependencies(cls, path, deps):
fp = '@executable_path/../Frameworks/'
@ -155,8 +155,8 @@ _check_symlinks_prescript()
newpath = fp + '%s.framework/Versions/Current/%s'%(module, module)
cmd = ' '.join(['/usr/bin/install_name_tool', '-change', dep, newpath, path])
subprocess.check_call(cmd, shell=True)
def add_qt_plugins(self):
macos_dir = os.path.join(self.dist_dir, APPNAME + '.app', 'Contents', 'MacOS')
for root, dirs, files in os.walk(BuildAPP.QT_PREFIX+'/plugins'):
@ -172,14 +172,14 @@ _check_symlinks_prescript()
shutil.copymode(path, target)
deps = BuildAPP.qt_dependencies(target)
BuildAPP.fix_qt_dependencies(target, deps)
#deps = BuildAPP.qt_dependencies(path)
def fix_python_dependencies(self, 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])
def fix_misc_dependencies(self, files):
for path in files:
frameworks_dir = os.path.join(self.dist_dir, APPNAME + '.app', 'Contents', 'Frameworks')
@ -195,15 +195,15 @@ _check_symlinks_prescript()
if os.path.exists(bundle):
subprocess.check_call(['/usr/bin/install_name_tool', '-change', dep,
'@executable_path/../Frameworks/'+name, path])
def add_plugins(self):
self.add_qt_plugins()
frameworks_dir = os.path.join(self.dist_dir, APPNAME + '.app', 'Contents', 'Frameworks')
plugins_dir = os.path.join(frameworks_dir, 'plugins')
if not os.path.exists(plugins_dir):
os.mkdir(plugins_dir)
maps = {}
for f in glob.glob('src/calibre/plugins/*'):
tgt = plugins_dir
@ -217,8 +217,8 @@ _check_symlinks_prescript()
deps.append(dst)
self.fix_python_dependencies(deps)
self.fix_misc_dependencies(deps)
def run(self):
py2app.run(self)
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\
|stat.S_IWUSR|stat.S_IROTH|stat.S_IRGRP)
self.add_plugins()
print
print 'Adding 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):
shutil.rmtree(dst)
shutil.copytree('/usr/local/etc/fonts', dst, symlinks=False)
print
print 'Adding IPython'
dst = os.path.join(resource_dir, 'lib', 'python2.6', 'IPython')
if os.path.exists(dst): shutil.rmtree(dst)
shutil.copytree(os.path.expanduser('~/build/ipython/IPython'), dst)
print
print
print 'Adding ImageMagick'
dest = os.path.join(frameworks_dir, 'ImageMagick')
if os.path.exists(dest):
sutil.rmtree(dest)
shutil.rmtree(dest)
shutil.copytree(os.path.expanduser('~/ImageMagick'), dest, True)
shutil.copyfile('/usr/local/lib/libpng12.0.dylib', os.path.join(dest, 'lib', 'libpng12.0.dylib'))
print
print 'Installing prescipt'
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'
f.write(src)
f.close()
print
print
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)
for script in scripts['gui']+scripts['console']:
f.write(script, script.partition('/')[-1])
f.close()
print
print
print 'Creating console.app'
contents_dir = os.path.dirname(resource_dir)
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'
plistlib.writePlist(plist, os.path.join(cc_dir, x))
else:
os.symlink(os.path.join('../..', x),
os.symlink(os.path.join('../..', x),
os.path.join(cc_dir, x))
print
print 'Building disk image'
@ -343,9 +343,10 @@ def main():
'calibre.ebooks.lrf.any.*', 'calibre.ebooks.lrf.feeds.*',
'keyword', 'codeop', 'pydoc', 'readline',
'BeautifulSoup', 'calibre.ebooks.lrf.fonts.prs500.*',
'dateutil',
'dateutil', 'email.iterators',
'email.generator',
],
'packages' : ['PIL', 'Authorization', 'lxml'],
'packages' : ['PIL', 'Authorization', 'lxml', 'dns'],
'excludes' : ['IPython'],
'plist' : { 'CFBundleGetInfoString' : '''calibre, an E-book management application.'''
''' 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 ::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 ::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 ::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
@ -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 ::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 ::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 ::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
@ -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 ::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 ::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
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'
VC90 = r'C:\VC90.CRT'
import sys, os, py2exe, shutil, zipfile, glob, subprocess, re
import sys, os, py2exe, shutil, zipfile, glob, re
from distutils.core import setup
from distutils.filelist import FileList
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
sys.path.insert(0, BASE_DIR)
from setup import VERSION, APPNAME, entry_points, scripts, basenames
from setup import VERSION, APPNAME, scripts, basenames
sys.path.remove(BASE_DIR)
ICONS = [os.path.abspath(os.path.join(BASE_DIR, 'icons', i)) for i in ('library.ico', 'viewer.ico')]
@ -33,7 +32,7 @@ WINVER = VERSION+'.0'
PY2EXE_DIR = os.path.join(BASE_DIR, 'build','py2exe')
class BuildEXE(py2exe.build_exe.py2exe):
def run(self):
py2exe.build_exe.py2exe.run(self)
print 'Adding plugins...'
@ -61,19 +60,19 @@ class BuildEXE(py2exe.build_exe.py2exe):
if os.path.exists(tg):
shutil.rmtree(tg)
shutil.copytree(imfd, tg)
print
print
print 'Adding main scripts'
f = zipfile.ZipFile(os.path.join(PY2EXE_DIR, 'library.zip'), 'a', zipfile.ZIP_DEFLATED)
for i in scripts['console'] + scripts['gui']:
f.write(i, i.partition('\\')[-1])
f.close()
print
print 'Copying icons'
for icon in ICONS:
shutil.copyfile(icon, os.path.join(PY2EXE_DIR, os.path.basename(icon)))
print
print 'Adding third party dependencies'
print '\tAdding devcon'
@ -96,18 +95,18 @@ class BuildEXE(py2exe.build_exe.py2exe):
shutil.copytree(f, tgt)
else:
shutil.copyfile(f, tgt)
print
print
print 'Doing DLL redirection' # See http://msdn.microsoft.com/en-us/library/ms682600(VS.85).aspx
for f in glob.glob(os.path.join(PY2EXE_DIR, '*.exe')):
open(f + '.local', 'w').write('\n')
print
print 'Adding Windows runtime dependencies...'
for f in glob.glob(os.path.join(VC90, '*')):
shutil.copyfile(f, os.path.join(PY2EXE_DIR, os.path.basename(f)))
def exe_factory(dest_base, script, icon_resources=None):
exe = {
'dest_base' : dest_base,
@ -144,7 +143,9 @@ def main(args=sys.argv):
'includes' : [
'sip', 'pkg_resources', 'PyQt4.QtSvg',
'mechanize', 'ClientForm', 'wmi',
'win32file', 'pythoncom',
'win32file', 'pythoncom',
'email.iterators',
'email.generator',
'win32process', 'win32api', 'msvcrt',
'win32event', 'calibre.ebooks.lrf.any.*',
'calibre.ebooks.lrf.feeds.*',
@ -155,14 +156,14 @@ def main(args=sys.argv):
'PyQt4.QtWebKit', 'PyQt4.QtNetwork',
],
'packages' : ['PIL', 'lxml', 'cherrypy',
'dateutil'],
'dateutil', 'dns'],
'excludes' : ["Tkconstants", "Tkinter", "tcl",
"_imagingtk", "ImageTk", "FixTk"
],
'dll_excludes' : ['mswsock.dll'],
},
},
)
return 0

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'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.5.2'
__version__ = '0.5.3'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
'''
Various run time constants.

View File

@ -17,22 +17,25 @@ def option_parser():
Run an embedded python interpreter.
''')
parser.add_option('--update-module', 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('--update-module',
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('-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.')
parser.add_option('-g', '--gui', default=False, action='store_true',
help='Run the GUI',)
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.')
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.')
return parser
def update_zipfile(zipfile, mod, path):
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]*')
name = mod.replace('.', '/') + os.path.splitext(path)[-1]
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')
elif isosx:
zp = os.path.join(os.path.dirname(getattr(sys, 'frameworks_dir')),
'Resources', 'lib',
'python'+'.'.join(map(str, sys.version_info[:2])),
'Resources', 'lib',
'python'+'.'.join(map(str, sys.version_info[:2])),
'site-packages.zip')
else:
zp = os.path.join(getattr(sys, 'frozen_path'), 'loader.zip')
@ -71,23 +74,23 @@ def migrate(old, new):
self.max = max
def setValue(self, val):
self.update(float(val)/getattr(self, 'max', 1))
db = LibraryDatabase(old)
db2 = LibraryDatabase2(new)
db2.migrate_old(db, Dummy(terminal_controller, 'Migrating database...'))
prefs['library_path'] = os.path.abspath(new)
print 'Database migrated to', os.path.abspath(new)
def debug_device_driver():
from calibre.devices.scanner import DeviceScanner
s = DeviceScanner()
s.scan()
print 'USB devices on system:', repr(s.devices)
if iswindows:
wmi = __import__('wmi', globals(), locals(), [], -1)
wmi = __import__('wmi', globals(), locals(), [], -1)
drives = []
print 'Drives detected:'
print '\t', '(ID, Partitions, Drive letter)'
print '\t', '(ID, Partitions, Drive letter)'
for drive in wmi.WMI().Win32_DiskDrive():
if drive.Partitions == 0:
continue
@ -111,7 +114,7 @@ def debug_device_driver():
d.open()
print 'Total space:', d.total_space()
break
def main(args=sys.argv):
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.
BCD = None
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) :
"""

View File

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

View File

@ -22,17 +22,17 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
### Usage Type Data
### wMaxPacketSize 0x0040 1x 64 bytes
### 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.
### 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
"""
Contains the logic for communication with the device (a SONY PRS-500).
The public interface of class L{PRS500} defines the
methods for performing various tasks.
The public interface of class L{PRS500} defines the
methods for performing various tasks.
"""
import sys, os
from tempfile import TemporaryFile
@ -49,12 +49,12 @@ from calibre.devices.prs500.books import BookList, fix_ids
from calibre import __author__, __appname__
# Protocol versions this driver has been tested with
KNOWN_USB_PROTOCOL_VERSIONS = [0x3030303030303130L]
KNOWN_USB_PROTOCOL_VERSIONS = [0x3030303030303130L]
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):
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.wtime = _file[1].wtime #: Creation time of self as an epoch
path = _file[0]
if path.endswith("/"):
if path.endswith("/"):
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
def __repr__(self):
@ -80,7 +80,7 @@ class PRS500(Device):
"""
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
@ -92,33 +92,33 @@ class PRS500(Device):
BULK_IN_EP = 0x81 #: Endpoint for Bulk reads
BULK_OUT_EP = 0x02 #: Endpoint for Bulk writes
# 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
CACHE_XML = "/Sony Reader/database/cache.xml"
CACHE_XML = "/Sony Reader/database/cache.xml"
# 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
THUMBNAIL_HEIGHT = 68
# Directory on card to which books are copied
CARD_PATH_PREFIX = __appname__
_packet_number = 0 #: Keep track of the packet number for packet tracing
def log_packet(self, packet, header, stream=sys.stderr):
"""
Log C{packet} to stream C{stream}.
Header should be a small word describing the type of packet.
"""
"""
Log C{packet} to stream C{stream}.
Header should be a small word describing the type of packet.
"""
self._packet_number += 1
print >> stream, str(self._packet_number), header, "Type:", \
packet.__class__.__name__
print >> stream, packet
print >> stream, "--"
@classmethod
def validate_response(cls, res, _type=0x00, number=0x00):
"""
Raise a ProtocolError if the type and number of C{res}
is not the same as C{type} and C{number}.
"""
Raise a ProtocolError if the type and number of C{res}
is not the same as C{type} and C{number}.
"""
if _type != res.type or number != res.rnumber:
raise ProtocolError("Inavlid response.\ntype: expected=" + \
@ -127,31 +127,31 @@ class PRS500(Device):
" actual="+hex(res.rnumber))
@classmethod
def signature(cls):
def signature(cls):
""" Return a two element tuple (vendor id, product id) """
return (cls.VENDOR_ID, cls.PRODUCT_ID )
def safe(func):
"""
Decorator that wraps a call to C{func} to ensure that
exceptions are handled correctly. It also calls L{open} to claim
"""
Decorator that wraps a call to C{func} to ensure that
exceptions are handled correctly. It also calls L{open} to claim
the interface and initialize the Reader if needed.
As a convenience, C{safe} automatically sends the a
L{EndSession} after calling func, unless func has
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}.
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}.
"""
@wraps(func)
def run_session(*args, **kwargs):
dev = args[0]
dev = args[0]
res = None
try:
if not dev.handle:
if not dev.handle:
dev.open()
if not dev.in_session:
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"]:
dev.send_validated_command(BeginEndSession(end=True))
dev.in_session = False
raise
raise
except USBError, err:
if "No such device" in str(err):
raise DeviceError()
elif "Connection timed out" in str(err):
elif "Connection timed out" in str(err):
dev.close()
raise TimeoutError(func.__name__)
elif "Protocol error" in str(err):
elif "Protocol error" in str(err):
dev.close()
raise ProtocolError("There was an unknown error in the"+\
" protocol. Contact " + __author__)
dev.close()
raise
raise
if not kwargs.has_key("end_session") or kwargs["end_session"]:
dev.send_validated_command(BeginEndSession(end=True))
dev.in_session = False
@ -182,15 +182,15 @@ class PRS500(Device):
return run_session
def __init__(self, key='-1', log_packets=False, report_progress=None) :
"""
"""
@param key: The key to unlock the device
@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 log_packets: If true the packet stream to/from the device is logged
@param report_progress: Function that is called with a % progress
(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
"""
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}
self.handle = None
self.in_session = False
@ -204,14 +204,14 @@ class PRS500(Device):
def reconnect(self):
""" 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
@classmethod
def is_connected(cls, helper=None):
"""
This method checks to see whether the device is physically connected.
It does not return any information about the validity of the
"""
This method checks to see whether the device is physically connected.
It does not return any information about the validity of the
software connection. You may need to call L{reconnect} if you keep
getting L{DeviceError}.
"""
@ -222,15 +222,15 @@ class PRS500(Device):
def set_progress_reporter(self, report_progress):
self.report_progress = report_progress
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.
Also initialize the device.
Also initialize the device.
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:
raise DeviceError()
configs = self.device.configurations
@ -238,7 +238,7 @@ class PRS500(Device):
self.handle = self.device.open()
config = configs[0]
try:
self.handle.set_configuration(configs[0])
self.handle.set_configuration(configs[0])
except USBError:
self.handle.set_configuration(configs[1])
config = configs[1]
@ -250,13 +250,13 @@ class PRS500(Device):
else:
red, wed = ed2, ed1
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)
except USBError, err:
raise DeviceBusy(str(err))
# Large timeout as device may still be initializing
res = self.send_validated_command(GetUSBProtocolVersion(), timeout=20000)
if res.code != 0:
res = self.send_validated_command(GetUSBProtocolVersion(), timeout=20000)
if res.code != 0:
raise ProtocolError("Unable to get USB Protocol version.")
version = self._bulk_read(24, data_type=USBProtocolVersion)[0].version
if version not in KNOWN_USB_PROTOCOL_VERSIONS:
@ -265,16 +265,16 @@ class PRS500(Device):
res = self.send_validated_command(SetBulkSize(\
chunk_size = 512*self.bulk_read_max_packet_size, \
unknown = 2))
if res.code != 0:
if res.code != 0:
raise ProtocolError("Unable to set bulk size.")
res = self.send_validated_command(UnlockDevice(key=self.key))#0x312d))
if res.code != 0:
res = self.send_validated_command(UnlockDevice(key=self.key))#0x312d))
if res.code != 0:
raise DeviceLocked()
res = self.send_validated_command(SetTime())
if res.code != 0:
raise ProtocolError("Could not set time on device")
def close(self):
def close(self):
""" Release device interface """
try:
self.handle.reset()
@ -285,16 +285,16 @@ class PRS500(Device):
self.in_session = False
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 response_type: an object of type 'type'. The return packet
from the device is returned as an object of type response_type.
@param timeout: The time to wait for a response from the
from the device is returned as an object of type response_type.
@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.
"""
if self.log_packets:
if self.log_packets:
self.log_packet(command, "Command")
bytes_sent = self.handle.control_msg(0x40, 0x80, command)
if bytes_sent != len(command):
@ -302,19 +302,19 @@ class PRS500(Device):
+ str(command))
response = response_type(self.handle.control_msg(0xc0, 0x81, \
Response.SIZE, timeout=timeout))
if self.log_packets:
if self.log_packets:
self.log_packet(response, "Response")
return response
def send_validated_command(self, command, cnumber=None, \
response_type=Response, timeout=1000):
"""
Wrapper around L{_send_command} that checks if the
C{Response.rnumber == cnumber or
"""
Wrapper around L{_send_command} that checks if the
C{Response.rnumber == cnumber or
command.number if cnumber==None}. Also check that
C{Response.type == Command.type}.
"""
if cnumber == None:
if cnumber == None:
cnumber = command.number
res = self._send_command(command, response_type=response_type, \
timeout=timeout)
@ -322,18 +322,18 @@ class PRS500(Device):
return res
def _bulk_write(self, data, packet_size=0x1000):
"""
"""
Send data to device via a bulk transfer.
@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.
"""
def bulk_write_packet(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")
bytes_left = len(data)
bytes_left = len(data)
if bytes_left + 16 <= packet_size:
packet_size = bytes_left +16
first_packet = Answer(bytes_left+16)
@ -355,11 +355,11 @@ class PRS500(Device):
pos = endpos
res = Response(self.handle.control_msg(0xc0, 0x81, Response.SIZE, \
timeout=5000))
if self.log_packets:
if self.log_packets:
self.log_packet(res, "Response")
if res.rnumber != 0x10005 or res.code != 0:
raise ProtocolError("Sending via Bulk Transfer failed with response:\n"\
+str(res))
+str(res))
if res.data_size != len(data):
raise ProtocolError("Unable to transfer all data to device. "+\
"Response packet:\n"\
@ -368,12 +368,12 @@ class PRS500(Device):
def _bulk_read(self, bytes, command_number=0x00, packet_size=0x1000, \
data_type=Answer):
"""
Read in C{bytes} bytes via a bulk transfer in
packets of size S{<=} C{packet_size}
@param data_type: an object of type type.
The data packet is returned as an object of type C{data_type}.
@return: A list of packets read from the device.
"""
Read in C{bytes} bytes via a bulk transfer in
packets of size S{<=} C{packet_size}
@param data_type: an object of type type.
The data packet is returned as an object of type C{data_type}.
@return: A list of packets read from the device.
Each packet is of type data_type
"""
msize = self.bulk_read_max_packet_size
@ -392,7 +392,7 @@ class PRS500(Device):
bytes_left = bytes
packets = []
while bytes_left > 0:
if packet_size > bytes_left:
if packet_size > bytes_left:
packet_size = bytes_left
packet = bulk_read_packet(data_type=data_type, size=packet_size)
bytes_left -= len(packet)
@ -404,8 +404,8 @@ class PRS500(Device):
@safe
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)
"""
size = self.send_validated_command(DeviceInfoQuery()).data[2] + 16
@ -416,21 +416,21 @@ class PRS500(Device):
@safe
def path_properties(self, path, end_session=True):
"""
Send command asking device for properties of C{path}.
Return L{FileProperties}.
"""
Send command asking device for properties of C{path}.
Return L{FileProperties}.
"""
res = self.send_validated_command(PathQuery(path), \
response_type=ListResponse)
data = self._bulk_read(0x28, data_type=FileProperties, \
command_number=PathQuery.NUMBER)[0]
if path.endswith('/') and path != '/':
if path.endswith('/') and path != '/':
path = path[:-1]
if res.path_not_found :
raise PathError(path + " does not exist on device")
if res.is_invalid:
if res.is_invalid:
raise PathError(path + " is not a valid path")
if res.is_unmounted:
if res.is_unmounted:
raise PathError(path + " is not mounted")
if res.permission_denied:
raise PathError('Permission denied for: ' + path + '\nYou can only '+\
@ -443,20 +443,20 @@ class PRS500(Device):
@safe
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},
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
"""
if path.endswith("/"):
if path.endswith("/"):
path = path[:-1] # We only copy files
cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '')
_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")
bytes = _file.file_size
res = self.send_validated_command(FileOpen(path))
@ -464,12 +464,12 @@ class PRS500(Device):
raise PathError("Unable to open " + path + \
" for reading. Response code: " + hex(res.code))
_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
bytes_left, chunk_size = bytes, 512 * self.bulk_read_max_packet_size -16
packet_size, pos = 64 * self.bulk_read_max_packet_size, 0
while bytes_left > 0:
if chunk_size > bytes_left:
while bytes_left > 0:
if chunk_size > bytes_left:
chunk_size = bytes_left
res = self.send_validated_command(FileIO(_id, pos, chunk_size))
if res.code != 0:
@ -477,21 +477,21 @@ class PRS500(Device):
raise ProtocolError("Error while reading from " + path + \
". Response code: " + hex(res.code))
packets = self._bulk_read(chunk_size+16, \
command_number=FileIO.RNUMBER, packet_size=packet_size)
command_number=FileIO.RNUMBER, packet_size=packet_size)
try:
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])))
except IOError, err:
self.send_validated_command(FileClose(_id))
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
pos += chunk_size
if self.report_progress:
if self.report_progress:
self.report_progress(int(100*((1.*pos)/bytes)))
self.send_validated_command(FileClose(_id))
# Not going to check response code to see if close was successful
self.send_validated_command(FileClose(_id))
# Not going to check response code to see if close was successful
# as there's not much we can do if it wasnt
@safe
@ -503,26 +503,26 @@ class PRS500(Device):
@type path: string
@param path: The path to list
@type recurse: boolean
@param recurse: If true do a recursive listing
@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 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
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
@param recurse: If true do a recursive listing
@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 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
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
outermost list will be 1.
"""
def _list(path):
def _list(path):
""" Do a non recursive listsing of path """
if not path.endswith("/"):
if not path.endswith("/"):
path += "/" # Initially assume path is a directory
cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '')
files = []
candidate = self.path_properties(path, end_session=False)
if not candidate.is_dir:
if not candidate.is_dir:
path = path[:-1]
data = self.path_properties(path, end_session=False)
data = self.path_properties(path, end_session=False)
files = [ File((path, data)) ]
else:
# Get query ID used to ask for next element in list
@ -536,20 +536,20 @@ class PRS500(Device):
next = DirRead(_id)
items = []
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
data = self._bulk_read(size, data_type=ListAnswer, \
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
if res.is_eol or res.path_not_found:
break
if res.is_eol or res.path_not_found:
break
elif res.code != 0:
raise ProtocolError("Unknown error occured while "+\
"reading contents of directory " + path + \
". Response code: " + hex(res.code))
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
for item in items:
ipath = path + item
@ -568,23 +568,23 @@ class PRS500(Device):
@safe
def total_space(self, end_session=True):
"""
"""
Get total space available on the mountpoints:
1. Main memory
2. Memory Stick
3. SD Card
@return: A 3 element list with total space in bytes of (1, 2, 3)
"""
"""
data = []
for path in ("/Data/", "a:/", "b:/"):
# Timeout needs to be increased as it takes time to read card
res = self.send_validated_command(TotalSpaceQuery(path), \
timeout=5000)
timeout=5000)
buffer_size = 16 + res.data[2]
pkt = self._bulk_read(buffer_size, data_type=TotalSpaceAnswer, \
command_number=TotalSpaceQuery.NUMBER)[0]
data.append( pkt.total )
data.append( pkt.total )
return data
@safe
@ -600,26 +600,26 @@ class PRS500(Device):
return path
except PathError:
return None
@safe
def free_space(self, end_session=True):
"""
"""
Get free space available on the mountpoints:
1. Main memory
2. Memory Stick
3. SD Card
@return: A 3 element list with free space in bytes of (1, 2, 3)
"""
"""
data = []
for path in ("/", "a:/", "b:/"):
# Timeout needs to be increased as it takes time to read card
self.send_validated_command(FreeSpaceQuery(path), \
timeout=5000)
timeout=5000)
pkt = self._bulk_read(FreeSpaceAnswer.SIZE, \
data_type=FreeSpaceAnswer, \
command_number=FreeSpaceQuery.NUMBER)[0]
data.append( pkt.free )
data.append( pkt.free )
return data
def _exists(self, path):
@ -628,21 +628,21 @@ class PRS500(Device):
try:
dest = self.path_properties(path, end_session=False)
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)
else: raise
else: raise
return (True, dest)
@safe
def touch(self, path, end_session=True):
"""
Create a file at path
@todo: Update file modification time if it exists.
"""
Create a file at path
@todo: Update file modification time if it exists.
Opening the file in write mode and then closing it doesn't work.
"""
cp = self.card_prefix(False)
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]
exists, _file = self._exists(path)
if exists and _file.is_dir:
@ -651,18 +651,18 @@ class PRS500(Device):
res = self.send_validated_command(FileCreate(path))
if res.code != 0:
raise PathError("Could not create file " + path + \
". Response code: " + str(hex(res.code)))
". Response code: " + str(hex(res.code)))
@safe
def put_file(self, infile, path, replace_file=False, end_session=True):
"""
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.
@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.
@param replace_file: If True and path points to a file that already exists, it is replaced
"""
"""
pos = infile.tell()
infile.seek(0, 2)
bytes = infile.tell() - pos
@ -673,12 +673,12 @@ class PRS500(Device):
exists, dest = self._exists(path)
if exists:
if dest.is_dir:
if not path.endswith("/"):
if not path.endswith("/"):
path += "/"
path += os.path.basename(infile.name)
return self.put_file(infile, path, replace_file=replace_file, end_session=False)
else:
if not replace_file:
if not replace_file:
raise PathError("Cannot write to " + \
path + " as it already exists", path=path)
_file = self.path_properties(path, end_session=False)
@ -693,7 +693,7 @@ class PRS500(Device):
raise ProtocolError("Unable to open " + path + \
" for writing. Response code: " + hex(res.code))
_id = self._bulk_read(20, data_type=IdAnswer, \
command_number=FileOpen.NUMBER)[0].id
command_number=FileOpen.NUMBER)[0].id
while data_left:
data = array('B')
@ -704,7 +704,7 @@ class PRS500(Device):
data.fromstring(ind)
if len(ind) < chunk_size:
raise EOFError
except EOFError:
except EOFError:
data_left = False
res = self.send_validated_command(FileIO(_id, pos, len(data), \
mode=FileIO.WNUMBER))
@ -715,7 +715,7 @@ class PRS500(Device):
pos += len(data)
if self.report_progress:
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
_file = self.path_properties(path, end_session=False)
if _file.file_size != pos:
@ -727,7 +727,7 @@ class PRS500(Device):
def del_file(self, path, end_session=True):
""" Delete C{path} from device iff path is a file """
data = self.path_properties(path, end_session=False)
if data.is_dir:
if data.is_dir:
raise PathError("Cannot delete directories")
res = self.send_validated_command(FileDelete(path), \
response_type=ListResponse)
@ -741,7 +741,7 @@ class PRS500(Device):
if path.startswith('card:/'):
cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '')
if not path.endswith("/"):
if not path.endswith("/"):
path += "/"
error_prefix = "Cannot create directory " + path
res = self.send_validated_command(DirCreate(path)).data[0]
@ -764,8 +764,8 @@ class PRS500(Device):
if not dir.is_dir:
self.del_file(path, end_session=False)
else:
if not path.endswith("/"):
path += "/"
if not path.endswith("/"):
path += "/"
res = self.send_validated_command(DirDelete(path))
if res.code == PathResponseCodes.HAS_CHILDREN:
raise PathError("Cannot delete directory " + path + \
@ -778,24 +778,24 @@ class PRS500(Device):
def card(self, end_session=True):
""" Return path prefix to installed card or None """
card = None
if self._exists("a:/")[0]:
if self._exists("a:/")[0]:
card = "a:"
if self._exists("b:/")[0]:
card = "b:"
if self._exists("b:/")[0]:
card = "b:"
return card
@safe
def books(self, oncard=False, end_session=True):
"""
"""
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
@return: L{BookList}
"""
"""
root = "/Data/media/"
tfile = TemporaryFile()
if oncard:
if oncard:
try:
self.get_file("a:"+self.CACHE_XML, tfile, end_session=False)
root = "a:/"
@ -804,9 +804,9 @@ class PRS500(Device):
self.get_file("b:"+self.CACHE_XML, tfile, end_session=False)
root = "b:/"
except PathError: pass
if tfile.tell() == 0:
if tfile.tell() == 0:
tfile = None
else:
else:
self.get_file(self.MEDIA_XML, tfile, end_session=False)
bl = BookList(root=root, sfile=tfile)
paths = bl.purge_corrupted_files()
@ -822,26 +822,26 @@ class PRS500(Device):
"""
Remove the books specified by paths from the device. The metadata
cache on the device should also be updated.
"""
"""
for path in paths:
self.del_file(path, end_session=False)
fix_ids(booklists[0], booklists[1])
self.sync_booklists(booklists, end_session=False)
@safe
def sync_booklists(self, booklists, end_session=True):
'''
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)).
'''
fix_ids(*booklists)
self.upload_book_list(booklists[0], end_session=False)
if booklists[1].root:
self.upload_book_list(booklists[1], end_session=False)
@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):
card = self.card(end_session=False)
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)
mspace = space[0]
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 "+\
"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 " +\
"in main memory")
for infile in infiles:
infile.seek(0)
infile.seek(0)
name = names.next()
paths.append(prefix+name)
self.put_file(infile, paths[-1], replace_file=True, end_session=False)
ctimes.append(self.path_properties(paths[-1], end_session=False).ctime)
return zip(paths, sizes, ctimes)
@classmethod
def add_books_to_metadata(cls, locations, metadata, booklists):
metadata = iter(metadata)
@ -882,35 +882,35 @@ class PRS500(Device):
name = (cls.CARD_PATH_PREFIX+'/' if on_card else 'books/') + name
booklists[on_card].add_book(info, name, *location[1:])
fix_ids(*booklists)
@safe
def delete_books(self, paths, end_session=True):
for path in paths:
self.del_file(path, end_session=False)
@classmethod
def remove_books_from_metadata(cls, paths, booklists):
for path in paths:
on_card = 1 if path[1] == ':' else 0
booklists[on_card].remove_book(path)
fix_ids(*booklists)
@safe
def add_book(self, infile, name, info, booklists, oncard=False, \
sync_booklists=False, end_session=True):
"""
Add a book to the device. If oncard is True then the book is copied
to the card rather than main memory.
Add a book to the device. If oncard is True then the book is copied
to the card rather than main memory.
@param infile: The source file, should be opened in "rb" mode
@param name: The name of the book file when uploaded to the
device. The extension of name must be one of
@param name: The name of the book file when uploaded to the
device. The extension of name must be one of
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)
where data is the image data in JPEG format as a string
@param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)).
@param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)).
"""
infile.seek(0, 2)
size = infile.tell()
@ -922,11 +922,11 @@ class PRS500(Device):
if oncard and size > cspace - 1024*1024:
raise FreeSpaceError("There is insufficient free space "+\
"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 " +\
"in main memory")
prefix = "/Data/media/"
if oncard:
if oncard:
prefix = card + "/"
else: name = "books/"+name
path = prefix + name
@ -943,12 +943,12 @@ class PRS500(Device):
path = self.MEDIA_XML
if not booklist.prefix:
card = self.card(end_session=True)
if not card:
if not card:
raise ArgumentError("Cannot upload list to card as "+\
"card is not present")
path = card + self.CACHE_XML
f = StringIO()
f = StringIO()
booklist.write(f)
f.seek(0)
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_CARD = ''
SUPPORTS_SUB_DIRS = False
CAN_SET_METADATA = False
def __init__(self, key='-1', log_packets=False, report_progress=None):
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_html import TITLEPAGE
from calibre.ebooks.epub import config
from calibre.ebooks.epub import config
from calibre.ebooks.metadata.opf2 import OPF
from calibre.ptempfile import TemporaryDirectory
from calibre.ebooks.chardet import xml_to_unicode
@ -31,13 +31,15 @@ def character_count(html):
return count
class UnsupportedFormatError(Exception):
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):
def __new__(cls, *args):
args = list(args)
args[0] = args[0].partition('#')[0]
obj = super(SpineItem, cls).__new__(cls, *args)
path = args[0]
raw = open(path, 'rb').read()
@ -63,10 +65,11 @@ def is_supported(path):
return ext in list(MAP.keys())+['html', 'opf']
class EbookIterator(object):
CHARACTERS_PER_PAGE = 1000
def __init__(self, pathtoebook):
pathtoebook = pathtoebook.strip()
self.pathtoebook = os.path.abspath(pathtoebook)
self.config = DynamicConfig(name='iterator')
ext = os.path.splitext(pathtoebook)[1].replace('.', '').lower()
@ -77,17 +80,17 @@ class EbookIterator(object):
if ext not in map.keys():
raise UnsupportedFormatError(ext)
self.to_opf = map[ext]
def search(self, text, index):
text = text.lower()
for i, path in enumerate(self.spine):
if i > index:
if text in open(path, 'rb').read().decode(path.encoding).lower():
return i
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:
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
else:
print 'Loaded embedded font:', repr(family)
def __enter__(self):
self._tdir = TemporaryDirectory('_ebook_iter')
self.base = self._tdir.__enter__()
@ -116,50 +119,50 @@ class EbookIterator(object):
self.pathtoopf = self.to_opf(self.pathtoebook, self.base, opts)
self.opf = OPF(self.pathtoopf, os.path.dirname(self.pathtoopf))
self.spine = [SpineItem(i.path) for i in self.opf.spine]
cover = self.opf.cover
if os.path.splitext(self.pathtoebook)[1].lower() in \
('.lit', '.mobi', '.prc') and cover:
cfile = os.path.join(os.path.dirname(self.spine[0]), 'calibre_ei_cover.html')
open(cfile, 'wb').write(TITLEPAGE%cover)
self.spine[0:0] = [SpineItem(cfile)]
if self.opf.path_to_html_toc is not None and \
self.opf.path_to_html_toc not in self.spine:
self.spine.append(SpineItem(self.opf.path_to_html_toc))
sizes = [i.character_count for i in self.spine]
self.pages = [math.ceil(i/float(self.CHARACTERS_PER_PAGE)) for i in sizes]
for p, s in zip(self.pages, self.spine):
s.pages = p
start = 1
for s in self.spine:
s.start_page = start
start += s.pages
s.max_page = s.start_page + s.pages - 1
self.toc = self.opf.toc
self.find_embedded_fonts()
self.read_bookmarks()
self.read_bookmarks()
return self
def parse_bookmarks(self, raw):
for line in raw.splitlines():
if line.count('^') > 0:
tokens = line.rpartition('^')
title, ref = tokens[0], tokens[2]
self.bookmarks.append((title, ref))
def serialize_bookmarks(self, bookmarks):
dat = []
for title, bm in bookmarks:
dat.append(u'%s^%s'%(title, bm))
return (u'\n'.join(dat) +'\n').encode('utf-8')
def read_bookmarks(self):
self.bookmarks = []
bmfile = os.path.join(self.base, 'META-INF', 'calibre_bookmarks.txt')
@ -170,8 +173,8 @@ class EbookIterator(object):
saved = self.config['bookmarks_'+self.pathtoebook]
if saved:
raw = saved
self.parse_bookmarks(raw)
self.parse_bookmarks(raw)
def save_bookmarks(self, bookmarks=None):
if bookmarks is None:
bookmarks = self.bookmarks
@ -190,7 +193,7 @@ class EbookIterator(object):
zipf.writestr('META-INF/calibre_bookmarks.txt', dat)
else:
self.config['bookmarks_'+self.pathtoebook] = dat
def add_bookmark(self, bm):
dups = []
for x in self.bookmarks:
@ -200,9 +203,9 @@ class EbookIterator(object):
self.bookmarks.remove(x)
self.bookmarks.append(bm)
self.save_bookmarks()
def set_bookmarks(self, bookmarks):
self.bookmarks = bookmarks
def __exit__(self, *args):
self._tdir.__exit__(*args)

View File

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

View File

@ -42,9 +42,9 @@ def report(verbose):
class Query(object):
BASE_URL = 'http://books.google.com/books/feeds/volumes?'
def __init__(self, title=None, author=None, publisher=None, isbn=None,
max_results=20, min_viewability='none', start_index=1):
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:
q += build_term('title', title.split())
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:
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({
'q':q,
'max-results':max_results,
'start-index':start_index,
'min-viewability':min_viewability,
})
def __call__(self, browser, verbose):
if verbose:
print 'Query:', self.url
@ -85,7 +87,7 @@ class Query(object):
class ResultList(list):
def get_description(self, entry, verbose):
try:
desc = description(entry)
@ -93,7 +95,7 @@ class ResultList(list):
return 'SUMMARY:\n'+desc[0].text
except:
report(verbose)
def get_language(self, entry, verbose):
try:
l = language(entry)
@ -101,27 +103,27 @@ class ResultList(list):
return l[0].text
except:
report(verbose)
def get_title(self, entry):
candidates = [x.text for x in title(entry)]
candidates.sort(cmp=lambda x,y: cmp(len(x), len(y)), reverse=True)
return candidates[0]
def get_authors(self, entry):
m = creator(entry)
if not m:
m = []
m = [x.text for x in m]
return m
def get_author_sort(self, entry, verbose):
for x in creator(entry):
for key, val in x.attrib.items():
if key.endswith('file-as'):
return val
def get_identifiers(self, entry, mi):
isbns = []
for x in identifier(entry):
@ -131,7 +133,7 @@ class ResultList(list):
isbns.append(t[5:])
if isbns:
mi.isbn = sorted(isbns, cmp=lambda x,y:cmp(len(x), len(y)))[-1]
def get_tags(self, entry, verbose):
try:
tags = [x.text for x in subject(entry)]
@ -139,14 +141,14 @@ class ResultList(list):
report(verbose)
tags = []
return tags
def get_publisher(self, entry, verbose):
try:
pub = publisher(entry)[0].text
except:
pub = None
return pub
def get_date(self, entry, verbose):
try:
d = date(entry)
@ -158,7 +160,7 @@ class ResultList(list):
report(verbose)
d = None
return d
def populate(self, entries, browser, verbose=False):
for x in entries:
try:
@ -175,7 +177,7 @@ class ResultList(list):
if verbose:
print 'Failed to get all details for an entry'
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)
self.get_identifiers(x, mi)
mi.tags = self.get_tags(x, verbose)
@ -190,14 +192,14 @@ def search(title=None, author=None, publisher=None, isbn=None,
br = browser()
start, entries = 1, []
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)
if not new:
break
entries.extend(new)
entries = entries[:max_results]
ans = ResultList()
ans.populate(entries, br, verbose)
return ans
@ -206,18 +208,18 @@ def option_parser():
parser = OptionParser(textwrap.dedent(
'''\
%prog [options]
Fetch book metadata from Google. You must specify one of title, author,
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
specific as possible.
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
specific as possible.
'''
))
parser.add_option('-t', '--title', help='Book title')
parser.add_option('-a', '--author', help='Book author(s)')
parser.add_option('-p', '--publisher', help='Book publisher')
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')
parser.add_option('-v', '--verbose', default=0, action='count',
help='Be more verbose about errors')
@ -236,6 +238,6 @@ def main(args=sys.argv):
for result in results:
print unicode(result).encode(preferred_encoding)
print
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 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
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()
except Exception, err:
raise ISBNDBError('Could not fetch ISBNDB metadata. Error: '+str(err))
soup = BeautifulStoneSoup(raw)
soup = BeautifulStoneSoup(raw,
convertEntities=BeautifulStoneSoup.XML_ENTITIES)
book_list = soup.find('booklist')
if book_list is None:
errmsg = soup.find('errormessage').string
@ -41,13 +42,13 @@ def fetch_metadata(url, max=100, timeout=5.):
return books
finally:
socket.setdefaulttimeout(timeout)
class ISBNDBMetadata(MetaInformation):
def __init__(self, book):
MetaInformation.__init__(self, None, [])
self.isbn = book['isbn']
self.title = book.find('titlelong').string
if not self.title:
@ -59,7 +60,7 @@ class ISBNDBMetadata(MetaInformation):
for au in temp:
if not au: continue
self.authors.extend([a.strip() for a in au.split('&amp;')])
try:
self.author_sort = book.find('authors').find('person').string
if self.authors and self.author_sort == self.authors[0]:
@ -67,12 +68,12 @@ class ISBNDBMetadata(MetaInformation):
except:
pass
self.publisher = book.find('publishertext').string
summ = book.find('summary')
if summ and hasattr(summ, 'string') and summ.string:
self.comments = 'SUMMARY:\n'+summ.string
def build_isbn(base_url, opts):
return base_url + 'index1=isbn&value1='+opts.isbn
@ -85,11 +86,11 @@ def build_combined(base_url, opts):
query = query.strip()
if len(query) == 0:
raise ISBNDBError('You must specify at least one of --author, --title or --publisher')
query = re.sub(r'\s+', '+', query)
if isinstance(query, unicode):
query = query.encode('utf-8')
return base_url+'index1=combined&value1='+quote(query, '+')
return base_url+'index1=combined&value1='+quote(query, '+')
def option_parser():
@ -97,7 +98,7 @@ def option_parser():
_('''
%prog [options] key
Fetch metadata for books from isndb.com. You can specify either the
Fetch metadata for books from isndb.com. You can specify either the
books ISBN ID or its title and author. If you specify the title and author,
then more than one book may be returned.
@ -112,11 +113,11 @@ key is the account key you generate after signing up for a free account from isb
default=None, help=_('The title of the book to search for.'))
parser.add_option('-p', '--publisher', default=None, dest='publisher',
help=_('The publisher of the book to search for.'))
parser.add_option('-v', '--verbose', default=False,
parser.add_option('-v', '--verbose', default=False,
action='store_true', help=_('Verbose processing'))
return parser
def create_books(opts, args, timeout=5.):
base_url = BASE_URL%dict(key=args[1])
@ -124,10 +125,10 @@ def create_books(opts, args, timeout=5.):
url = build_isbn(base_url, opts)
else:
url = build_combined(base_url, opts)
if opts.verbose:
print ('ISBNDB query: '+url)
return [ISBNDBMetadata(book) for book in fetch_metadata(url, timeout=timeout)]
def main(args=sys.argv):
@ -137,10 +138,10 @@ def main(args=sys.argv):
parser.print_help()
print ('You must supply the isbndb.com key')
return 1
for book in create_books(opts, args):
print unicode(book).encode('utf-8')
return 0
if __name__ == '__main__':

View File

@ -9,7 +9,7 @@ lxml based OPF parser.
import sys, unittest, functools, os, mimetypes, uuid, glob, cStringIO
from urllib import unquote
from urlparse import urlparse
from urlparse import urlparse, urldefrag
from lxml import etree
from dateutil import parser
@ -444,7 +444,7 @@ class OPF(object):
if not hasattr(stream, 'read'):
stream = open(stream, 'rb')
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 = raw[raw.find('<'):]
self.root = etree.fromstring(raw, self.PARSER)
@ -496,7 +496,8 @@ class OPF(object):
if f:
self.toc.read_ncx_toc(f[0])
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)
except:
pass
@ -627,7 +628,7 @@ class OPF(object):
attrib = {'{%s}role'%self.NAMESPACES['opf']: 'aut'}
elem = self.create_metadata_element('creator', attrib=attrib)
self.set_text(elem, author.strip())
return property(fget=fget, fset=fset)
@dynamic_property

View File

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

View File

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

View File

@ -17,7 +17,6 @@ import logging
from lxml import etree, html
import calibre
from cssutils import CSSParser
from cssutils.css import CSSStyleSheet
from calibre.translations.dynamic import translate
from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
@ -239,7 +238,7 @@ class DirContainer(object):
for path in self.namelist():
ext = os.path.splitext(path)[1].lower()
if ext == '.opf':
self.opfname = fname
self.opfname = path
return
self.opfname = None
@ -280,7 +279,7 @@ class Metadata(object):
syntax. Return an empty list for any terms with no currently associated
metadata items.
"""
DC_TERMS = set(['contributor', 'coverage', 'creator', 'date',
'description', 'format', 'identifier', 'language',
'publisher', 'relation', 'rights', 'source',
@ -289,7 +288,10 @@ class Metadata(object):
OPF_ATTRS = {'role': OPF('role'), 'file-as': OPF('file-as'),
'scheme': OPF('scheme'), 'event': OPF('event'),
'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):
"""An item of OEB data model metadata.
@ -305,13 +307,13 @@ class Metadata(object):
"""
class Attribute(object):
"""Smart accessor for allowed OEB metadata item attributes."""
def __init__(self, attr, allowed=None):
if not callable(attr):
attr_, attr = attr, lambda term: attr_
self.attr = attr
self.allowed = allowed
def term_attr(self, obj):
term = obj.term
if namespace(term) != DC11_NS:
@ -322,14 +324,14 @@ class Metadata(object):
'attribute %r not valid for metadata term %r' \
% (self.attr(term), barename(obj.term)))
return self.attr(term)
def __get__(self, obj, cls):
if obj is None: return None
return obj.attrib.get(self.term_attr(obj), '')
def __set__(self, obj, value):
obj.attrib[self.term_attr(obj)] = value
def __init__(self, term, value, attrib={}, nsmap={}, **kwargs):
self.attrib = attrib = dict(attrib)
self.nsmap = nsmap = dict(nsmap)
@ -369,7 +371,7 @@ class Metadata(object):
def fset(self, value):
self.value = value
return property(fget=fget, fset=fset)
scheme = Attribute(lambda term: 'scheme' if \
term == OPF('meta') else OPF('scheme'),
[DC('identifier'), OPF('meta')])
@ -384,19 +386,19 @@ class Metadata(object):
DC('relation'), DC('rights'),
DC('source'), DC('subject'),
OPF('meta')])
def __getitem__(self, key):
return self.attrib[key]
def __setitem__(self, key, value):
self.attrib[key] = value
def __contains__(self, key):
return key in self.attrib
def get(self, key, default=None):
return self.attrib.get(key, default)
def __repr__(self):
return 'Item(term=%r, value=%r, attrib=%r)' \
% (barename(self.term), self.value, self.attrib)
@ -422,7 +424,7 @@ class Metadata(object):
elem.attrib['name'] = prefixname(self.term, nsrmap)
elem.attrib['content'] = prefixname(self.value, nsrmap)
return elem
def to_opf2(self, parent=None, nsrmap={}):
attrib = {}
for key, value in self.attrib.items():
@ -435,7 +437,7 @@ class Metadata(object):
elem.attrib['name'] = prefixname(self.term, nsrmap)
elem.attrib['content'] = prefixname(self.value, nsrmap)
return elem
def __init__(self, oeb):
self.oeb = oeb
self.items = defaultdict(list)
@ -470,7 +472,7 @@ class Metadata(object):
nsmap.update(item.nsmap)
return nsmap
return property(fget=fget)
@dynamic_property
def _opf1_nsmap(self):
def fget(self):
@ -480,7 +482,7 @@ class Metadata(object):
del nsmap[key]
return nsmap
return property(fget=fget)
@dynamic_property
def _opf2_nsmap(self):
def fget(self):
@ -488,7 +490,7 @@ class Metadata(object):
nsmap.update(OPF2_NSMAP)
return nsmap
return property(fget=fget)
def to_opf1(self, parent=None):
nsmap = self._opf1_nsmap
nsrmap = dict((value, key) for key, value in nsmap.items())
@ -502,7 +504,7 @@ class Metadata(object):
chaptertour = self.Item('ms-chaptertour', 'chaptertour')
chaptertour.to_opf1(dcmeta, xmeta, nsrmap=nsrmap)
return elem
def to_opf2(self, parent=None):
nsmap = self._opf2_nsmap
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
manifest items and the values are the items themselves.
"""
class Item(object):
"""An OEB data model book content file.
@ -548,10 +550,10 @@ class Manifest(object):
which are not (such as footnotes). Meaningless for items which
have a :attr:`spine_position` of `None`.
"""
NUM_RE = re.compile('^(.*)([0-9][0-9.]*)(?=[.]|$)')
META_XP = XPath('/h:html/h:head/h:meta[@http-equiv="Content-Type"]')
def __init__(self, oeb, id, href, media_type,
fallback=None, loader=str, data=None):
self.oeb = oeb
@ -609,7 +611,11 @@ class Manifest(object):
elif not namespace(data.tag):
data.attrib['xmlns'] = XHTML_NS
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:
# OEB_DOC_NS, but possibly others
ns = namespace(data.tag)
@ -659,7 +665,7 @@ class Manifest(object):
data = parser.parseString(data, href=self.href)
data.namespaces['h'] = XHTML_NS
return data
def _fetch_css(self, path):
hrefs = self.oeb.manifest.hrefs
if path not in hrefs:
@ -671,7 +677,7 @@ class Manifest(object):
return (None, None)
data = item.data.cssText
return ('utf-8', data)
@dynamic_property
def data(self):
doc = """Provides MIME type sensitive access to the manifest
@ -707,7 +713,7 @@ class Manifest(object):
def fdel(self):
self._data = None
return property(fget, fset, fdel, doc=doc)
def __str__(self):
data = self.data
if isinstance(data, etree._Element):
@ -715,13 +721,13 @@ class Manifest(object):
if isinstance(data, unicode):
return data.encode('utf-8')
return str(data)
def __eq__(self, other):
return id(self) == id(other)
def __ne__(self, other):
return not self.__eq__(other)
def __cmp__(self, other):
result = cmp(self.spine_position, other.spine_position)
if result != 0:
@ -735,7 +741,7 @@ class Manifest(object):
onum = float(omatch.group(2)) if omatch else 0.0
okey = (oref, onum, other.id)
return cmp(skey, okey)
def relhref(self, href):
"""Convert the URL provided in :param:`href` from a book-absolute
reference to a reference relative to this manifest item.
@ -756,7 +762,7 @@ class Manifest(object):
if frag:
relhref = '#'.join((relhref, frag))
return relhref
def abshref(self, href):
"""Convert the URL provided in :param:`href` from a 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.normpath(href).replace('\\', '/')
return href
def __init__(self, oeb):
self.oeb = oeb
self.items = set()
self.ids = {}
self.hrefs = {}
def add(self, id, href, media_type, fallback=None, loader=None, data=None):
"""Add a new item to the book manifest.
@ -795,7 +801,7 @@ class Manifest(object):
self.ids[item.id] = item
self.hrefs[item.href] = item
return item
def remove(self, item):
"""Removes :param:`item` from the manifest."""
if item in self.ids:
@ -805,7 +811,7 @@ class Manifest(object):
self.items.remove(item)
if item in self.oeb.spine:
self.oeb.spine.remove(item)
def generate(self, id=None, href=None):
"""Generate a new unique identifier and/or internal path for use in
creating a new manifest item, using the provided :param:`id` and/or
@ -833,13 +839,13 @@ class Manifest(object):
def __iter__(self):
for item in self.items:
yield item
def values(self):
return list(self.items)
def __contains__(self, item):
return item in self.items
def to_opf1(self, parent=None):
elem = element(parent, 'manifest')
for item in self.items:
@ -854,7 +860,7 @@ class Manifest(object):
attrib['fallback'] = item.fallback
element(elem, 'item', attrib=attrib)
return elem
def to_opf2(self, parent=None):
elem = element(parent, OPF('manifest'))
for item in self.items:
@ -891,14 +897,14 @@ class Spine(object):
elif linear in ('no', 'false'):
linear = False
return linear
def add(self, item, linear=None):
"""Append :param:`item` to the end of the `Spine`."""
item.linear = self._linear(linear)
item.spine_position = len(self.items)
self.items.append(item)
return item
def insert(self, index, item, linear):
"""Insert :param:`item` at position :param:`index` in the `Spine`."""
item.linear = self._linear(linear)
@ -907,7 +913,7 @@ class Spine(object):
for i in xrange(index, len(self.items)):
self.items[i].spine_position = i
return item
def remove(self, item):
"""Remove :param:`item` from the `Spine`."""
index = item.spine_position
@ -915,7 +921,7 @@ class Spine(object):
for i in xrange(index, len(self.items)):
self.items[i].spine_position = i
item.spine_position = None
def __iter__(self):
for item in self.items:
yield item
@ -953,7 +959,7 @@ class Guide(object):
Provides dictionary-like access, in which the keys are the OEB reference
type identifiers and the values are `Reference` objects.
"""
class Reference(object):
"""Reference to a standard book section.
@ -985,7 +991,7 @@ class Guide(object):
TYPES = set(t for t, _ in _TYPES_TITLES)
TITLES = dict(_TYPES_TITLES)
ORDER = dict((t, i) for i, (t, _) in enumerate(_TYPES_TITLES))
def __init__(self, oeb, type, title, href):
self.oeb = oeb
if type.lower() in self.TYPES:
@ -998,22 +1004,22 @@ class Guide(object):
self.type = type
self.title = title
self.href = urlnormalize(href)
def __repr__(self):
return 'Reference(type=%r, title=%r, href=%r)' \
% (self.type, self.title, self.href)
@dynamic_property
def _order(self):
def fget(self):
return self.ORDER.get(self.type, self.type)
return property(fget=fget)
def __cmp__(self, other):
if not isinstance(other, Guide.Reference):
return NotImplemented
return cmp(self._order, other._order)
@dynamic_property
def item(self):
doc = """The manifest item associated with this reference."""
@ -1022,41 +1028,41 @@ class Guide(object):
hrefs = self.oeb.manifest.hrefs
return hrefs.get(path, None)
return property(fget=fget, doc=doc)
def __init__(self, oeb):
self.oeb = oeb
self.refs = {}
def add(self, type, title, href):
"""Add a new reference to the `Guide`."""
ref = self.Reference(self.oeb, type, title, href)
self.refs[type] = ref
return ref
def iterkeys(self):
for type in self.refs:
yield type
__iter__ = iterkeys
def values(self):
return sorted(self.refs.values())
def items(self):
for type, ref in self.refs.items():
yield type, ref
def __getitem__(self, key):
return self.refs[key]
def __delitem__(self, key):
del self.refs[key]
def __contains__(self, key):
return key in self.refs
def __len__(self):
return len(self.refs)
def to_opf1(self, parent=None):
elem = element(parent, 'guide')
for ref in self.refs.values():
@ -1065,7 +1071,7 @@ class Guide(object):
attrib['title'] = ref.title
element(elem, 'reference', attrib=attrib)
return elem
def to_opf2(self, parent=None):
elem = element(parent, OPF('guide'))
for ref in self.refs.values():
@ -1095,7 +1101,7 @@ class TOC(object):
self.klass = klass
self.id = id
self.nodes = []
def add(self, title, href, klass=None, id=None):
"""Create and return a new sub-node of this node."""
node = TOC(title, href, klass, id)
@ -1108,18 +1114,18 @@ class TOC(object):
for child in self.nodes:
for node in child.iter():
yield node
def iterdescendants(self):
"""Iterate over all descendant nodes in depth-first order."""
for child in self.nodes:
for node in child.iter():
yield node
def __iter__(self):
"""Iterate over all immediate child nodes."""
for node in self.nodes:
yield node
def __getitem__(self, index):
return self.nodes[index]
@ -1134,7 +1140,7 @@ class TOC(object):
prev.nodes.append(node)
else:
prev = node
def depth(self):
"""The maximum depth of the navigation tree rooted at this node."""
try:
@ -1148,7 +1154,7 @@ class TOC(object):
'title': node.title, 'href': node.href})
node.to_opf1(tour)
return tour
def to_ncx(self, parent):
for node in self.nodes:
id = node.id or unicode(uuid.uuid4())
@ -1169,7 +1175,7 @@ class PageList(object):
Provides list-like access to the pages.
"""
class Page(object):
"""Represents a mapping between a page name and a position within
the book content.
@ -1187,17 +1193,17 @@ class PageList(object):
:attr:`id`: Optional unique identifier for this page.
"""
TYPES = set(['front', 'normal', 'special'])
def __init__(self, name, href, type='normal', klass=None, id=None):
self.name = unicode(name)
self.href = urlnormalize(href)
self.type = type if type in self.TYPES else 'normal'
self.id = id
self.klass = klass
def __init__(self):
self.pages = []
def add(self, name, href, type='normal', klass=None, id=None):
"""Create a new page and add it to the `PageList`."""
page = self.Page(name, href, type, klass, id)
@ -1206,11 +1212,11 @@ class PageList(object):
def __len__(self):
return len(self.pages)
def __iter__(self):
for page in self.pages:
yield page
def __getitem__(self, index):
return self.pages[index]
@ -1219,7 +1225,7 @@ class PageList(object):
def remove(self, page):
return self.pages.remove(page)
def to_ncx(self, parent=None):
plist = element(parent, NCX('pageList'), id=str(uuid.uuid4()))
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(ptarget, NCX('content'), src=page.href)
return plist
def to_page_map(self):
pmap = etree.Element(OPF('page-map'), nsmap={None: OPF2_NS})
for page in self.pages:
@ -1245,11 +1251,14 @@ class PageList(object):
class OEBBook(object):
"""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):
"""Create empty book. Optional arguments:
:param parse_cache: A cache of parsed XHTML/CSS. Keys are absolute
paths to te cached files and values are lxml root objects and
cssutils stylesheets.
@ -1260,10 +1269,10 @@ class OEBBook(object):
:param:`logger`: A Log object to use for logging all messages
related to the processing of this book. It is accessible
via the instance data members :attr:`logger,log`.
It provides the following public instance data members for
accessing various parts of the OEB data model:
:attr:`metadata`: Metadata such as title, author name(s), etc.
:attr:`manifest`: Manifest of all files included in the book,
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
the same text.
"""
self.encoding = encoding
self.pretty_print = pretty_print
self.logger = self.log = logger
@ -1294,13 +1304,13 @@ class OEBBook(object):
encoding = opts.encoding
pretty_print = opts.pretty_print
return cls(encoding=encoding, pretty_print=pretty_print)
def translate(self, text):
"""Translate :param:`text` into the book's primary language."""
lang = str(self.metadata.language[0])
lang = lang.split('-', 1)[0].lower()
return translate(lang, text)
def decode(self, data):
"""Automatically decode :param:`data` into a `unicode` object."""
if isinstance(data, unicode):
@ -1323,7 +1333,7 @@ class OEBBook(object):
data = data.replace('\r\n', '\n')
data = data.replace('\r', '\n')
return data
def to_opf1(self):
"""Produce OPF 1.2 representing the book's metadata and structure.
@ -1370,7 +1380,7 @@ class OEBBook(object):
order = playorder.get(href, 0)
elem.attrib['playOrder'] = str(order)
return
def _to_ncx(self):
lang = unicode(self.metadata.language[0])
ncx = etree.Element(NCX('ncx'),
@ -1399,10 +1409,10 @@ class OEBBook(object):
maxpnum.attrib['content'] = str(value)
self._update_playorder(ncx)
return ncx
def to_opf2(self, page_map=False):
"""Produce OPF 2.0 representing the book's metadata and structure.
Returns a dictionary in which the keys are MIME types and the values
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'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
""" The GUI """
import sys, os, re, StringIO, traceback, time
import os
from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
QByteArray, QLocale, QUrl, QTranslator, QCoreApplication, \
QModelIndex
QByteArray, QUrl, QTranslator, QCoreApplication
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
QIcon, QTableView, QDialogButtonBox, QApplication, QDialog
QIcon, QTableView, QApplication, QDialog
ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500'
from calibre import __author__, islinux, iswindows, isosx
from calibre import islinux, iswindows
from calibre.startup import get_lang
from calibre.utils.config import Config, ConfigProxy, dynamic
import calibre.resources as resources
@ -32,7 +31,7 @@ def _config():
help=_('The format to use when saving single files to disk'))
c.add_opt('confirm_delete', default=False,
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
c.add_opt('show_text_in_toolbar', default=True,
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('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('upload_news_to_device', default=True,
c.add_opt('upload_news_to_device', default=True,
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'))
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'))
c.add_opt('disable_tray_notification', default=False,
c.add_opt('disable_tray_notification', default=False,
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)
config = _config()
# Turn off DeprecationWarnings in windows GUI
if iswindows:
@ -139,16 +141,16 @@ def human_readable(size):
class Dispatcher(QObject):
'''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):
QObject.__init__(self)
self.func = func
self.connect(self, SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject)'),
self.dispatch, Qt.QueuedConnection)
self.connect(self, self.SIGNAL, self.dispatch, Qt.QueuedConnection)
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):
self.func(*args, **kwargs)
@ -157,29 +159,29 @@ class GetMetadata(QObject):
Convenience class to ensure that metadata readers are used only in the
GUI thread. Must be instantiated in the GUI thread.
'''
def __init__(self):
QObject.__init__(self)
self.connect(self, SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self._get_metadata, Qt.QueuedConnection)
self.connect(self, SIGNAL('idispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self._from_formats, Qt.QueuedConnection)
def __call__(self, id, *args, **kwargs):
self.emit(SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
id, args, kwargs)
def from_formats(self, id, *args, **kwargs):
self.emit(SIGNAL('idispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
id, args, kwargs)
def _from_formats(self, id, args, kwargs):
try:
mi = metadata_from_formats(*args, **kwargs)
except:
mi = MetaInformation('', [_('Unknown')])
self.emit(SIGNAL('metadataf(PyQt_PyObject, PyQt_PyObject)'), id, mi)
def _get_metadata(self, id, args, kwargs):
try:
mi = get_metadata(*args, **kwargs)
@ -191,27 +193,27 @@ class TableView(QTableView):
def __init__(self, parent):
QTableView.__init__(self, parent)
self.read_settings()
def read_settings(self):
self.cw = dynamic[self.__class__.__name__+'column widths']
def write_settings(self):
dynamic[self.__class__.__name__+'column widths'] = \
tuple([int(self.columnWidth(i)) for i in range(self.model().columnCount(None))])
def restore_column_widths(self):
if self.cw and len(self.cw):
for i in range(len(self.cw)):
self.setColumnWidth(i, self.cw[i])
return True
class FileIconProvider(QFileIconProvider):
ICONS = {
'default' : 'unknown',
'dir' : 'dir',
'zero' : 'zero',
'jpeg' : 'jpeg',
'jpg' : 'jpeg',
'gif' : 'gif',
@ -234,7 +236,7 @@ class FileIconProvider(QFileIconProvider):
'mobi' : 'mobi',
'epub' : 'epub',
}
def __init__(self):
QFileIconProvider.__init__(self)
self.icons = {}
@ -242,14 +244,14 @@ class FileIconProvider(QFileIconProvider):
self.icons[key] = ':/images/mimetypes/'+self.__class__.ICONS[key]+'.svg'
for i in ('dir', 'default', 'zero'):
self.icons[i] = QIcon(self.icons[i])
def key_from_ext(self, ext):
key = ext if ext in self.icons.keys() else 'default'
if key == 'default' and ext.count('.') > 0:
ext = ext.rpartition('.')[2]
key = ext if ext in self.icons.keys() else 'default'
return key
def cached_icon(self, key):
candidate = self.icons[key]
if isinstance(candidate, QIcon):
@ -257,11 +259,11 @@ class FileIconProvider(QFileIconProvider):
icon = QIcon(candidate)
self.icons[key] = icon
return icon
def icon_from_ext(self, ext):
key = self.key_from_ext(ext.lower() if ext else '')
return self.cached_icon(key)
def load_icon(self, fileinfo):
key = 'default'
icons = self.icons
@ -275,7 +277,7 @@ class FileIconProvider(QFileIconProvider):
ext = qstring_to_unicode(fileinfo.completeSuffix()).lower()
key = self.key_from_ext(ext)
return self.cached_icon(key)
def icon(self, arg):
if isinstance(arg, QFileInfo):
return self.load_icon(arg)
@ -284,13 +286,13 @@ class FileIconProvider(QFileIconProvider):
if arg == QFileIconProvider.File:
return self.icons['default']
return QFileIconProvider.icon(self, arg)
_file_icon_provider = None
def initialize_file_icon_provider():
global _file_icon_provider
if _file_icon_provider is None:
_file_icon_provider = FileIconProvider()
def file_icon_provider():
global _file_icon_provider
return _file_icon_provider
@ -299,13 +301,13 @@ _sidebar_directories = []
def set_sidebar_directories(dirs):
global _sidebar_directories
if dirs is None:
dirs = config['frequently_used_directories']
dirs = config['frequently_used_directories']
_sidebar_directories = [QUrl.fromLocalFile(i) for i in dirs]
class FileDialog(QObject):
def __init__(self, title='Choose Files',
def __init__(self, title='Choose Files',
filters=[],
add_all_files_filter=True,
add_all_files_filter=True,
parent=None,
modal = True,
name = '',
@ -321,16 +323,16 @@ class FileDialog(QObject):
ftext += '%s (%s);;'%(text, ' '.join(extensions))
if add_all_files_filter or not ftext:
ftext += 'All files (*)'
self.dialog_name = name if name else 'dialog_' + title
self.selected_files = None
self.fd = None
if islinux:
self.fd = QFileDialog(parent)
self.fd.setFileMode(mode)
self.fd.setFileMode(mode)
self.fd.setIconProvider(_file_icon_provider)
self.fd.setModal(modal)
self.fd.setModal(modal)
self.fd.setNameFilter(ftext)
self.fd.setWindowTitle(title)
state = dynamic[self.dialog_name]
@ -347,7 +349,7 @@ class FileDialog(QObject):
f = qstring_to_unicode(
QFileDialog.getSaveFileName(parent, title, dir, ftext, ""))
if os.path.exists(f):
self.selected_files.append(f)
self.selected_files.append(f)
elif mode == QFileDialog.ExistingFile:
f = qstring_to_unicode(
QFileDialog.getOpenFileName(parent, title, dir, ftext, ""))
@ -367,44 +369,44 @@ class FileDialog(QObject):
if 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])
self.accepted = bool(self.selected_files)
self.accepted = bool(self.selected_files)
def get_files(self):
if islinux and self.fd.result() != self.fd.Accepted:
return tuple()
if self.selected_files is None:
return tuple()
if self.selected_files is None:
return tuple(os.path.abspath(qstring_to_unicode(i)) for i in self.fd.selectedFiles())
return tuple(self.selected_files)
def save_dir(self):
if self.fd:
dynamic[self.dialog_name] = self.fd.saveState()
def choose_dir(window, name, title):
fd = FileDialog(title, [], False, window, name=name,
fd = FileDialog(title, [], False, window, name=name,
mode=QFileDialog.DirectoryOnly)
dir = fd.get_files()
if dir:
return dir[0]
def choose_files(window, name, title,
def choose_files(window, name, title,
filters=[], all_files=True, select_only_single_file=False):
'''
Ask user to choose a bunch of files.
@param name: Unique dialog name used to store the opened directory
@param title: Title to show in dialogs titlebar
@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
of extensions.
of extensions.
@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
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,
)
if fd.accepted:
@ -413,8 +415,8 @@ def choose_files(window, name, title,
def choose_images(window, name, title, select_only_single_file=True):
mode = QFileDialog.ExistingFile if select_only_single_file else QFileDialog.ExistingFiles
fd = FileDialog(title=title, name=name,
filters=[('Images', ['png', 'gif', 'jpeg', 'jpg', 'svg'])],
fd = FileDialog(title=title, name=name,
filters=[('Images', ['png', 'gif', 'jpeg', 'jpg', 'svg'])],
parent=window, add_all_files_filter=False, mode=mode,
)
if fd.accepted:
@ -432,7 +434,7 @@ def pixmap_to_data(pixmap, format='JPEG'):
return str(ba.data())
class ResizableDialog(QDialog):
def __init__(self, *args, **kwargs):
QDialog.__init__(self, *args)
self.setupUi(self)
@ -444,14 +446,15 @@ class ResizableDialog(QDialog):
nh = min(self.height(), nh)
nw = min(self.width(), nw)
self.resize(nw, nh)
try:
from calibre.utils.single_qt_application import SingleApplication
SingleApplication
except:
SingleApplication = None
class Application(QApplication):
def __init__(self, args):
qargs = [i.encode('utf-8') if isinstance(i, unicode) else i for i in args]
QApplication.__init__(self, qargs)
@ -462,6 +465,6 @@ class Application(QApplication):
if data:
self.translator.loadFromData(data)
self.installTranslator(self.translator)

View File

@ -1,19 +1,36 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, traceback, Queue, time
from threading import Thread
import os, traceback, Queue, time, socket
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.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.parallel import Job
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):
def __init__(self, func, *args, **kwargs):
Job.__init__(self, *args, **kwargs)
self.func = func
def run(self):
self.start_work()
try:
@ -23,16 +40,12 @@ class DeviceJob(Job):
self.traceback = traceback.format_exc()
finally:
self.job_done()
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):
'''
'''
@param sleep_time: Time to sleep between device probes in millisecs
@type sleep_time: integer
'''
@ -48,7 +61,7 @@ class DeviceManager(Thread):
self.job_manager = job_manager
self.current_job = None
self.scanner = DeviceScanner()
def detect_device(self):
self.scanner.scan()
for device in self.devices:
@ -63,8 +76,8 @@ class DeviceManager(Thread):
except:
print 'Unable to open device'
traceback.print_exc()
finally:
device[1] = True
finally:
device[1] = True
elif not connected and device[1]:
while True:
try:
@ -75,14 +88,14 @@ class DeviceManager(Thread):
self.device = None
self.connected_slot(False)
device[1] ^= True
def next(self):
if not self.jobs.empty():
try:
return self.jobs.get_nowait()
except Queue.Empty:
pass
def run(self):
while self.keep_going:
self.detect_device()
@ -94,75 +107,80 @@ class DeviceManager(Thread):
self.current_job.run()
self.current_job = None
else:
break
break
time.sleep(self.sleep_time)
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)
self.job_manager.add_job(job)
self.jobs.put(job)
return job
def has_card(self):
try:
return bool(self.device.card_prefix())
except:
return False
def _get_device_information(self):
info = self.device.get_device_information(end_session=False)
info = [i.replace('\x00', '').replace('\x01', '') for i in info]
cp = self.device.card_prefix(end_session=False)
fs = self.device.free_space()
return info, cp, fs
def get_device_information(self, done):
'''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'))
def _books(self):
'''Get metadata from device'''
mainlist = self.device.books(oncard=False, end_session=False)
cardlist = self.device.books(oncard=True)
return (mainlist, cardlist)
def books(self, done):
'''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'))
def _sync_booklists(self, booklists):
'''Sync metadata to device'''
self.device.sync_booklists(booklists, end_session=False)
return self.device.card_prefix(end_session=False), self.device.free_space()
def sync_booklists(self, done, booklists):
return self.create_job(self._sync_booklists, done, args=[booklists],
description=_('Send metadata to device'))
def _upload_books(self, files, names, on_card=False, metadata=None):
'''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)
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):
desc = _('Upload %d books to device')%len(names)
if 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)
def add_books_to_metadata(self, locations, metadata, booklists):
self.device.add_books_to_metadata(locations, metadata, booklists)
def _delete_books(self, paths):
'''Remove books from device'''
self.device.delete_books(paths, end_session=True)
def delete_books(self, done, paths):
return self.create_job(self._delete_books, done, args=[paths],
description=_('Delete books from device'))
def remove_books_from_metadata(self, paths, booklists):
self.device.remove_books_from_metadata(paths, booklists)
def _save_books(self, paths, target):
'''Copy books from device to disk'''
for path in paths:
@ -170,18 +188,502 @@ class DeviceManager(Thread):
f = open(os.path.join(target, name), 'wb')
self.device.get_file(path, f)
f.close()
def save_books(self, done, paths, target):
return self.create_job(self._save_books, done, args=[paths, target],
description=_('Download books from device'))
def _view_book(self, path, target):
f = open(target, 'wb')
self.device.get_file(path, f)
f.close()
return target
def view_book(self, done, path, target):
return self.create_job(self._view_book, done, args=[path, target],
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'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, re, time, textwrap
from binascii import hexlify, unhexlify
from PyQt4.Qt import QDialog, QMessageBox, QListWidgetItem, QIcon, \
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
QStringListModel, QAbstractItemModel, \
QStringListModel, QAbstractItemModel, QFont, \
SIGNAL, QTimer, Qt, QSize, QVariant, QUrl, \
QModelIndex, QInputDialog
QModelIndex, QInputDialog, QAbstractTableModel
from calibre.constants import islinux, iswindows
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, \
disable_plugin, customize_plugin, \
plugin_customization, add_plugin, remove_plugin
from calibre.utils.smtp import config as smtp_prefs
class PluginModel(QAbstractItemModel):
def __init__(self, *args):
QAbstractItemModel.__init__(self, *args)
self.icon = QVariant(QIcon(':/images/plugins.svg'))
self.populate()
def populate(self):
self._data = {}
for plugin in initialized_plugins():
@ -37,21 +39,21 @@ class PluginModel(QAbstractItemModel):
else:
self._data[plugin.type].append(plugin)
self.categories = sorted(self._data.keys())
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QModelIndex()
if parent.isValid():
return self.createIndex(row, column, parent.row())
else:
return self.createIndex(row, column, -1)
def parent(self, index):
if not index.isValid() or index.internalId() == -1:
return QModelIndex()
return self.createIndex(index.internalId(), 0, -1)
def rowCount(self, parent):
if not parent.isValid():
return len(self.categories)
@ -59,14 +61,14 @@ class PluginModel(QAbstractItemModel):
category = self.categories[parent.row()]
return len(self._data[category])
return 0
def columnCount(self, parent):
return 1
def index_to_plugin(self, index):
category = self.categories[index.parent().row()]
return self._data[category][index.row()]
def plugin_to_index(self, plugin):
for i, category in enumerate(self.categories):
parent = self.index(i, 0, QModelIndex())
@ -74,13 +76,13 @@ class PluginModel(QAbstractItemModel):
if plugin == p:
return self.index(j, 0, parent)
return QModelIndex()
def refresh_plugin(self, plugin, rescan=False):
if rescan:
self.populate()
idx = self.plugin_to_index(plugin)
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), idx, idx)
def flags(self, index):
if not index.isValid():
return 0
@ -90,7 +92,7 @@ class PluginModel(QAbstractItemModel):
if not is_disabled(self.data(index, Qt.UserRole)):
flags |= Qt.ItemIsEnabled
return flags
def data(self, index, role):
if not index.isValid():
return NONE
@ -113,25 +115,140 @@ class PluginModel(QAbstractItemModel):
if role == Qt.UserRole:
return plugin
return NONE
class CategoryModel(QStringListModel):
def __init__(self, *args):
QStringListModel.__init__(self, *args)
self.setStringList([_('General'), _('Interface'), _('Advanced'),
_('Content\nServer'), _('Plugins')])
self.icons = list(map(QVariant, map(QIcon,
[':/images/dialog_information.svg', ':/images/lookfeel.svg',
':/images/view.svg', ':/images/network-server.svg',
':/images/plugins.svg'])))
self.setStringList([_('General'), _('Interface'), _('Email\nDelivery'),
_('Advanced'), _('Content\nServer'), _('Plugins')])
self.icons = list(map(QVariant, map(QIcon,
[':/images/dialog_information.svg', ':/images/lookfeel.svg',
':/images/mail.svg', ':/images/view.svg',
':/images/network-server.svg', ':/images/plugins.svg'])))
def data(self, index, role):
if role == Qt.DecorationRole:
return self.icons[index.row()]
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):
@ -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.setupUi(self)
self._category_model = CategoryModel()
self.connect(self.category_view, SIGNAL('activated(QModelIndex)'), lambda i: self.stackedWidget.setCurrentIndex(i.row()))
self.connect(self.category_view, SIGNAL('clicked(QModelIndex)'), lambda i: self.stackedWidget.setCurrentIndex(i.row()))
self.category_view.currentChanged = \
lambda n, p: self.stackedWidget.setCurrentIndex(n.row())
self.category_view.setModel(self._category_model)
self.db = db
self.server = server
@ -151,7 +268,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.location.setText(path if path else '')
self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse)
self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact)
dirs = config['frequently_used_directories']
rn = config['use_roman_numerals_for_series_number']
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)
if not islinux:
self.dirs_box.setVisible(False)
column_map = config['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.setData(Qt.UserRole, QVariant(col))
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
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_down, SIGNAL('clicked()'), self.down_column)
self.filename_pattern = FilenamePattern(self)
self.metadata_box.layout().insertWidget(0, self.filename_pattern)
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.show_toolbar_text.setChecked(config['show_text_in_toolbar'])
@ -183,7 +300,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.book_exts = sorted(BOOK_EXTENSIONS)
for ext in self.book_exts:
self.single_format.addItem(ext.upper(), QVariant(ext))
single_format = config['save_to_disk_single_format']
self.single_format.setCurrentIndex(self.book_exts.index(single_format))
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]))
for item in items:
self.language.addItem(item[1], QVariant(item[0]))
self.pdf_metadata.setChecked(prefs['read_file_metadata'])
added_html = False
for ext in self.book_exts:
ext = ext.lower()
@ -242,7 +359,6 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.priority.setCurrentIndex(p)
self.priority.setVisible(iswindows)
self.priority_label.setVisible(iswindows)
self.category_view.setCurrentIndex(self._category_model.index(0))
self._plugin_model = PluginModel()
self.plugin_view.setModel(self._plugin_model)
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_add, SIGNAL('clicked()'), self.add_plugin)
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):
path = unicode(self.plugin_path.text())
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.reset()
else:
error_dialog(self, _('No valid plugin path'),
error_dialog(self, _('No valid plugin path'),
_('%s is not a valid plugin path')%path).exec_()
def find_plugin(self):
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)
if path:
self.plugin_path.setText(path[0])
def modify_plugin(self, op=''):
index = self.plugin_view.currentIndex()
if index.isValid():
plugin = self._plugin_model.index_to_plugin(index)
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_()
return
if op == 'toggle':
@ -286,7 +472,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
if op == 'customize':
if not plugin.is_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
help = plugin.customization_help()
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.reset()
else:
error_dialog(self, _('Cannot remove builtin plugin'),
plugin.name + _(' cannot be removed. It is a builtin plugin. Try disabling it instead.')).exec_()
error_dialog(self, _('Cannot remove builtin plugin'),
plugin.name + _(' cannot be removed. It is a '
'builtin plugin. Try disabling it instead.')).exec_()
def up_column(self):
idx = self.columns.currentRow()
if idx > 0:
self.columns.insertItem(idx-1, self.columns.takeItem(idx))
self.columns.setCurrentRow(idx-1)
def down_column(self):
idx = self.columns.currentRow()
if idx < self.columns.count()-1:
self.columns.insertItem(idx+1, self.columns.takeItem(idx))
self.columns.setCurrentRow(idx+1)
def view_server_logs(self):
from calibre.library.server import log_access_file, log_error_file
d = QDialog(self)
@ -336,7 +523,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
except IOError:
el.setPlainText('No access log found')
d.show()
def set_server_options(self):
c = server_config()
c.set('port', self.port.value())
@ -345,7 +532,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
if not p:
p = None
c.set('password', p)
def start_server(self):
self.set_server_options()
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:
time.sleep(1)
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_()
return
self.start.setEnabled(False)
self.test.setEnabled(True)
self.stop.setEnabled(True)
def stop_server(self):
from calibre.library.server import stop_threaded_server
stop_threaded_server(self.server)
@ -367,16 +554,17 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.start.setEnabled(True)
self.test.setEnabled(False)
self.stop.setEnabled(False)
def test_server(self):
QDesktopServices.openUrl(QUrl('http://127.0.0.1:'+str(self.port.value())))
def compact(self, toggled):
d = Vacuum(self, self.db)
d.exec_()
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:
self.location.setText(dir)
@ -393,7 +581,10 @@ class ConfigDialog(QDialog, Ui_Dialog):
def accept(self):
mcs = unicode(self.max_cover_size.text()).strip()
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
config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.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:
fmts.append(str(self.viewer.item(i).text()))
config['internally_viewed_formats'] = fmts
if not path or not os.path.exists(path) or not os.path.isdir(path):
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_()
elif not os.access(path, os.W_OK):
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_()
else:
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
QDialog.accept(self)
@ -448,7 +642,8 @@ class Vacuum(QMessageBox):
def __init__(self, parent, 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)
QTimer.singleShot(200, self.vacuum)
@ -456,3 +651,11 @@ class Vacuum(QMessageBox):
self.db.vacuum()
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>
<x>0</x>
<y>0</y>
<width>755</width>
<width>789</width>
<height>557</height>
</rect>
</property>
@ -437,12 +437,6 @@
</widget>
</item>
</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>
</item>
<item>
@ -507,7 +501,6 @@
</layout>
</item>
</layout>
<zorder>columns</zorder>
</widget>
</item>
<item>
@ -534,16 +527,287 @@
</layout>
<zorder>roman_numerals</zorder>
<zorder>groupBox_2</zorder>
<zorder>groupBox</zorder>
<zorder>systray_icon</zorder>
<zorder>sync_news</zorder>
<zorder>delete_news</zorder>
<zorder>separate_cover_flow</zorder>
<zorder>systray_notifications</zorder>
<zorder>groupBox_3</zorder>
<zorder></zorder>
<zorder></zorder>
</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" >
<layout class="QVBoxLayout" >
<item>

View File

@ -6,14 +6,14 @@ __docformat__ = 'restructuredtext en'
'''
The GUI for conversion to EPUB.
'''
import os
import os, uuid
from PyQt4.Qt import QDialog, QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit, \
QTextEdit, QCheckBox, Qt, QPixmap, QIcon, QListWidgetItem, SIGNAL
from lxml.etree import XPath
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.ebooks.epub.from_any import SOURCE_FORMATS, config as epubconfig
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):
OUTPUT = 'EPUB'
def __init__(self, parent, db, row=None, config=epubconfig):
ResizableDialog.__init__(self, parent)
self.hide_controls()
self.connect(self.category_list, SIGNAL('itemEntered(QListWidgetItem *)'),
self.show_category_help)
self.connect(self.cover_button, SIGNAL("clicked()"), self.select_cover)
self.cover_changed = False
self.db = db
self.id = None
@ -52,7 +52,7 @@ class Config(ResizableDialog, Ui_Dialog):
self.setWindowTitle(_('Bulk convert to ')+self.OUTPUT)
else:
self.setWindowTitle((_(u'Convert %s to ')%unicode(self.title.text()))+self.OUTPUT)
def hide_controls(self):
self.source_profile_label.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_ignore_tables.setVisible(False)
self.opt_prefer_author_sort.setVisible(False)
def initialize(self):
self.__w = []
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.setup_tooltips()
self.initialize_options()
def set_help(self, msg):
if msg and getattr(msg, 'strip', lambda:True)():
self.help_view.setPlainText(msg)
def setup_tooltips(self):
for opt in self.config.option_set.preferences:
g = getattr(self, 'opt_'+opt.name, False)
@ -90,19 +90,19 @@ class Config(ResizableDialog, Ui_Dialog):
g.setToolTip(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()))
def show_category_help(self, item):
text = unicode(item.text())
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,
_('Look & Feel') : _('Adjust the look of the generated ebook by specifying things like font sizes.'),
_('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', ' ')])
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()))
if not files:
return
@ -110,7 +110,7 @@ class Config(ResizableDialog, Ui_Dialog):
if _file:
_file = os.path.abspath(_file)
if not os.access(_file, os.R_OK):
d = error_dialog(self.window, _('Cannot read'),
d = error_dialog(self.window, _('Cannot read'),
_('You do not have permission to read the file: ') + _file)
d.exec_()
return
@ -118,7 +118,7 @@ class Config(ResizableDialog, Ui_Dialog):
try:
cf = open(_file, "rb")
cover = cf.read()
except IOError, e:
except IOError, e:
d = error_dialog(self.window, _('Error reading file'),
_("<p>There was an error reading from file: <br /><b>") + _file + "</b></p><br />"+str(e))
d.exec_()
@ -134,14 +134,14 @@ class Config(ResizableDialog, Ui_Dialog):
self.cover.setPixmap(pix)
self.cover_changed = True
self.cpixmap = pix
def initialize_metadata_options(self):
all_series = self.db.all_series()
all_series.sort(cmp=lambda x, y : cmp(x[1], y[1]))
for series in all_series:
self.series.addItem(series[1])
self.series.setCurrentIndex(-1)
if self.row is not None:
mi = self.db.get_metadata(self.id, index_is_id=True)
self.title.setText(mi.title)
@ -157,14 +157,14 @@ class Config(ResizableDialog, Ui_Dialog):
self.series.setCurrentIndex(self.series.findText(mi.series))
if mi.series_index is not None:
self.series_index.setValue(mi.series_index)
cover = self.db.cover(self.id, index_is_id=True)
if cover:
pm = QPixmap()
pm.loadFromData(cover)
if not pm.isNull():
self.cover.setPixmap(pm)
if not pm.isNull():
self.cover.setPixmap(pm)
def get_title_and_authors(self):
title = unicode(self.title.text()).strip()
if not title:
@ -172,7 +172,7 @@ class Config(ResizableDialog, Ui_Dialog):
authors = unicode(self.author.text()).strip()
authors = string_to_authors(authors) if authors else [_('Unknown')]
return title, authors
def get_metadata(self):
title, authors = self.get_title_and_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(',')]
if tags:
mi.tags = tags
return mi
def read_settings(self):
for pref in self.config.option_set.preferences:
g = getattr(self, 'opt_'+pref.name, False)
@ -209,9 +209,9 @@ class Config(ResizableDialog, Ui_Dialog):
elif isinstance(g, QCheckBox):
self.config.set(pref.name, bool(g.isChecked()))
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):
self.initialize_metadata_options()
values = self.config.parse()
@ -232,16 +232,16 @@ class Config(ResizableDialog, Ui_Dialog):
g.setCurrentIndex(g.findText(val))
elif isinstance(g, QCheckBox):
g.setCheckState(Qt.Checked if bool(val) else Qt.Unchecked)
def get_source_format(self):
self.source_format = None
if self.row is not None:
temp = self.db.formats(self.id, index_is_id=True)
if not temp:
error_dialog(self.parent(), _('Cannot convert'),
error_dialog(self.parent(), _('Cannot convert'),
_('This book has no available formats')).exec_()
available_formats = [f.upper().strip() for f in temp.split(',')]
choices = [fmt.upper() for fmt in SOURCE_FORMATS if fmt.upper() in available_formats]
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)
if d.exec_() == QDialog.Accepted:
self.source_format = d.format()
def accept(self):
for opt in ('chapter', 'level1_toc', 'level2_toc', 'level3_toc', 'page',
'page_names'):
@ -263,7 +263,7 @@ class Config(ResizableDialog, Ui_Dialog):
XPath(text,namespaces={'re':'http://exslt.org/regular-expressions'})
except Exception, err:
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_()
return
mi = self.get_metadata()
@ -272,6 +272,7 @@ class Config(ResizableDialog, Ui_Dialog):
if self.row is not None:
self.db.set_metadata(self.id, mi)
self.mi = self.db.get_metadata(self.id, index_is_id=True)
self.mi.application_id = uuid.uuid4()
opf = OPFCreator(os.getcwdu(), self.mi)
self.opf_file = PersistentTemporaryFile('.opf')
opf.render(self.opf_file)
@ -286,5 +287,5 @@ class Config(ResizableDialog, Ui_Dialog):
self.cover_file = cf
self.opts = self.config.parse()
QDialog.accept(self)

View File

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

View File

@ -23,7 +23,7 @@
<item>
<widget class="QLabel" name="tlabel" >
<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 name="alignment" >
<set>Qt::AlignCenter</set>
@ -60,6 +60,16 @@
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="warning" >
<property name="text" >
<string/>
</property>
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox" >
<property name="title" >

View File

@ -10,10 +10,10 @@ from calibre.gui2.dialogs.jobs_ui import Ui_JobsDialog
from calibre import __appname__
class ProgressBarDelegate(QAbstractItemDelegate):
def sizeHint(self, option, index):
return QSize(120, 30)
def paint(self, painter, option, index):
opts = QStyleOptionProgressBarV2()
opts.rect = option.rect
@ -44,20 +44,23 @@ class JobsDialog(QDialog, Ui_JobsDialog):
self.jobs_view.model().kill_job)
self.pb_delegate = ProgressBarDelegate(self)
self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate)
self.running_time_timer = QTimer(self)
self.connect(self.running_time_timer, SIGNAL('timeout()'), self.update_running_time)
self.running_time_timer.start(1000)
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):
for index in self.jobs_view.selectedIndexes():
row = index.row()
self.model.kill_job(row, self)
return
def closeEvent(self, e):
self.jobs_view.write_settings()
e.accept()

View File

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

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

View File

@ -86,15 +86,15 @@ class LibraryDelegate(QItemDelegate):
return sb
class DateDelegate(QStyledItemDelegate):
def displayText(self, val, locale):
d = val.toDate()
return d.toString('dd MMM yyyy')
def createEditor(self, parent, option, index):
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
qde.setDisplayFormat('MM/dd/yyyy')
qde.setMinimumDate(QDate(-4000,1,1))
qde.setMinimumDate(QDate(101,1,1))
qde.setCalendarPopup(True)
return qde
@ -103,7 +103,7 @@ class BooksModel(QAbstractTableModel):
[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"]
)
headers = {
'title' : _("Title"),
'authors' : _("Author(s)"),
@ -114,7 +114,7 @@ class BooksModel(QAbstractTableModel):
'tags' : _("Tags"),
'series' : _("Series"),
}
@classmethod
def roman(cls, num):
if num <= 0 or num >= 4000 or int(num) != num:
@ -130,7 +130,7 @@ class BooksModel(QAbstractTableModel):
QAbstractTableModel.__init__(self, parent)
self.db = None
self.column_map = config['column_map']
self.editable_cols = ['title', 'authors', 'rating', 'publisher',
self.editable_cols = ['title', 'authors', 'rating', 'publisher',
'tags', 'series', 'timestamp']
self.default_image = QImage(':/images/book.svg')
self.sorted_on = ('timestamp', Qt.AscendingOrder)
@ -157,10 +157,10 @@ class BooksModel(QAbstractTableModel):
tidx = self.column_map.index('timestamp')
except ValueError:
tidx = -1
self.emit(SIGNAL('columns_sorted(int,int)'), idx, tidx)
def set_database(self, db):
self.db = db
self.build_data_convertors()
@ -169,7 +169,7 @@ class BooksModel(QAbstractTableModel):
rows = self.db.refresh_ids(ids)
if rows:
self.refresh_rows(rows, current_row=current_row)
def refresh_rows(self, rows, current_row=-1):
for row in rows:
if self.cover_cache:
@ -191,12 +191,12 @@ class BooksModel(QAbstractTableModel):
add_duplicates=add_duplicates)
self.count_changed()
return ret
def add_news(self, path, recipe):
ret = self.db.add_news(path, recipe)
self.count_changed()
return ret
def count_changed(self, *args):
self.emit(SIGNAL('count_changed(int)'), self.db.count())
@ -208,12 +208,12 @@ class BooksModel(QAbstractTableModel):
callback=None):
rows = [row.row() for row in rows]
if single_format is None:
return self.db.export_to_dir(path, rows,
self.sorted_on[0] == 'authors',
single_dir=single_dir,
return self.db.export_to_dir(path, rows,
self.sorted_on[0] == 'authors',
single_dir=single_dir,
callback=callback)
else:
return self.db.export_single_format_to_dir(path, rows,
return self.db.export_single_format_to_dir(path, rows,
single_format,
callback=callback)
@ -225,8 +225,8 @@ class BooksModel(QAbstractTableModel):
self.count_changed()
self.clear_caches()
self.reset()
def delete_books_by_id(self, ids):
for id in ids:
try:
@ -263,18 +263,18 @@ class BooksModel(QAbstractTableModel):
self.clear_caches()
self.reset()
self.sorted_on = (self.column_map[col], order)
def refresh(self, reset=True):
try:
col = self.column_map.index(self.sorted_on[0])
except:
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)
if reset:
self.reset()
def resort(self, reset=True):
try:
col = self.column_map.index(self.sorted_on[0])
@ -412,14 +412,14 @@ class BooksModel(QAbstractTableModel):
if format is None:
ans.append(format)
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)
ans.append(f)
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):
ans = []
need_auto = []
@ -431,7 +431,7 @@ class BooksModel(QAbstractTableModel):
if not fmts:
fmts = ''
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)
for f in formats:
if f.lower() in u:
@ -471,7 +471,7 @@ class BooksModel(QAbstractTableModel):
data = self.db.cover(row_number)
except IndexError: # Happens if database has not yet been refreshed
pass
if not data:
return self.default_image
img = QImage()
@ -481,7 +481,7 @@ class BooksModel(QAbstractTableModel):
return img
def build_data_convertors(self):
tidx = FIELD_MAP['title']
aidx = FIELD_MAP['authors']
sidx = FIELD_MAP['size']
@ -491,44 +491,44 @@ class BooksModel(QAbstractTableModel):
srdx = FIELD_MAP['series']
tgdx = FIELD_MAP['tags']
siix = FIELD_MAP['series_index']
def authors(r):
au = self.db.data[r][aidx]
if au:
au = [a.strip().replace('|', ',') for a in au.split(',')]
return ' & '.join(au)
def timestamp(r):
dt = self.db.data[r][tmdx]
if dt:
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
return QDate(dt.year, dt.month, dt.day)
def rating(r):
r = self.db.data[r][ridx]
r = r/2 if r else 0
return r
def publisher(r):
pub = self.db.data[r][pidx]
if pub:
return pub
def tags(r):
tags = self.db.data[r][tgdx]
if tags:
return ', '.join(tags.split(','))
def series(r):
series = self.db.data[r][srdx]
if series:
return series + ' [%d]'%self.db.data[r][siix]
def size(r):
size = self.db.data[r][sidx]
if size:
return '%.1f'%(float(size)/(1024*1024))
self.dc = {
'title' : lambda r : self.db.data[r][tidx],
'authors' : authors,
@ -537,7 +537,7 @@ class BooksModel(QAbstractTableModel):
'rating' : rating,
'publisher': publisher,
'tags' : tags,
'series' : series,
'series' : series,
}
def data(self, index, role):
@ -575,7 +575,7 @@ class BooksModel(QAbstractTableModel):
val = int(value.toInt()[0]) if column == 'rating' else \
value.toDate() if column == 'timestamp' else \
unicode(value.toString())
id = self.db.id(row)
id = self.db.id(row)
if column == 'rating':
val = 0 if val < 0 else 5 if val > 5 else val
val *= 2
@ -600,7 +600,7 @@ class BooksModel(QAbstractTableModel):
index, index)
if column == self.sorted_on[0]:
self.resort()
return True
class BooksView(TableView):
@ -634,7 +634,7 @@ class BooksView(TableView):
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
self._model.current_changed)
self.connect(self._model, SIGNAL('columns_sorted(int, int)'), self.columns_sorted, Qt.QueuedConnection)
def columns_sorted(self, rating_col, timestamp_col):
for i in range(self.model().columnCount(None)):
if self.itemDelegateForColumn(i) == self.rating_delegate:
@ -643,8 +643,8 @@ class BooksView(TableView):
self.setItemDelegateForColumn(rating_col, self.rating_delegate)
if timestamp_col > -1:
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):
self.setContextMenuPolicy(Qt.DefaultContextMenu)
self.context_menu = QMenu(self)
@ -662,18 +662,18 @@ class BooksView(TableView):
self.context_menu.addAction(book_details)
if similar_menu is not None:
self.context_menu.addMenu(similar_menu)
def contextMenuEvent(self, event):
self.context_menu.popup(event.globalPos())
event.accept()
def sortByColumn(self, colname, order):
try:
idx = self._model.column_map.index(colname)
except ValueError:
idx = 0
TableView.sortByColumn(self, idx, order)
@classmethod
def paths_from_event(cls, event):
'''
@ -708,6 +708,9 @@ class BooksView(TableView):
def close(self):
self._model.close()
def set_editable(self, editable):
self._model.set_editable(editable)
def connect_to_search_box(self, sb):
QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'),
@ -736,23 +739,23 @@ class DeviceBooksView(BooksView):
def connect_dirtied_signal(self, slot):
QObject.connect(self._model, SIGNAL('booklist_dirtied()'), slot)
def sortByColumn(self, col, order):
TableView.sortByColumn(self, col, order)
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_()
class OnDeviceSearch(SearchQueryParser):
def __init__(self, model):
SearchQueryParser.__init__(self)
self.model = model
def universal_set(self):
return set(range(0, len(self.model.db)))
def get_matches(self, location, query):
location = location.lower().strip()
query = query.lower().strip()
@ -773,7 +776,7 @@ class OnDeviceSearch(SearchQueryParser):
matches.add(i)
break
return matches
class DeviceBooksModel(BooksModel):
@ -785,7 +788,7 @@ class DeviceBooksModel(BooksModel):
self.unknown = str(self.trUtf8('Unknown'))
self.marked_for_deletion = {}
self.search_engine = OnDeviceSearch(self)
self.editable = True
def mark_for_deletion(self, job, rows):
self.marked_for_deletion[job] = self.indices(rows)
@ -793,7 +796,6 @@ class DeviceBooksModel(BooksModel):
indices = self.row_indices(row)
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
def deletion_done(self, job, succeeded=True):
if not self.marked_for_deletion.has_key(job):
return
@ -818,7 +820,7 @@ class DeviceBooksModel(BooksModel):
if self.map[index.row()] in self.indices_to_be_deleted():
return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python
flags = QAbstractTableModel.flags(self, index)
if index.isValid():
if index.isValid() and self.editable:
if index.column() in [0, 1] or (index.column() == 4 and self.db.supports_tags()):
flags |= Qt.ItemIsEditable
return flags
@ -837,10 +839,10 @@ class DeviceBooksModel(BooksModel):
if reset:
self.reset()
self.last_search = text
def resort(self, reset):
self.sort(self.sorted_on[0], self.sorted_on[1], reset=reset)
def sort(self, col, order, reset=True):
descending = order != Qt.AscendingOrder
def strcmp(attr):
@ -959,7 +961,7 @@ class DeviceBooksModel(BooksModel):
return QVariant('Marked for deletion')
col = index.column()
if col in [0, 1] or (col == 4 and self.db.supports_tags()):
return QVariant("Double click to <b>edit</b> me<br><br>")
return QVariant(_("Double click to <b>edit</b> me<br><br>"))
return NONE
def headerData(self, section, orientation, role):
@ -999,6 +1001,10 @@ class DeviceBooksModel(BooksModel):
self.sort(col, self.sorted_on[1])
done = True
return done
def set_editable(self, editable):
self.editable = editable
class SearchBox(QLineEdit):
@ -1062,11 +1068,11 @@ class SearchBox(QLineEdit):
if not all:
ans = '[' + ans + ']'
self.set_search_string(ans)
def search_from_tags(self, tags, all):
joiner = ' and ' if all else ' or '
self.set_search_string(joiner.join(tags))
def set_search_string(self, txt):
self.normalize_state()
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>
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<widget class="QMainWindow" name="MainWindow" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
@ -11,140 +10,149 @@
<height>822</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<property name="sizePolicy" >
<sizepolicy vsizetype="Preferred" hsizetype="Preferred" >
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="contextMenuPolicy">
<property name="contextMenuPolicy" >
<enum>Qt::NoContextMenu</enum>
</property>
<property name="windowTitle">
<property name="windowTitle" >
<string>__appname__</string>
</property>
<property name="windowIcon">
<iconset resource="images.qrc">
<property name="windowIcon" >
<iconset resource="images.qrc" >
<normaloff>:/library</normaloff>:/library</iconset>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<widget class="QWidget" name="centralwidget" >
<layout class="QGridLayout" name="gridLayout" >
<item row="0" column="0" >
<layout class="QHBoxLayout" name="horizontalLayout_3" >
<item>
<widget class="LocationView" name="location_view">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<widget class="LocationView" name="location_view" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>100</height>
</size>
</property>
<property name="verticalScrollBarPolicy">
<property name="verticalScrollBarPolicy" >
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<property name="horizontalScrollBarPolicy" >
<enum>Qt::ScrollBarAsNeeded</enum>
</property>
<property name="tabKeyNavigation">
<property name="editTriggers" >
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="tabKeyNavigation" >
<bool>true</bool>
</property>
<property name="showDropIndicator" stdset="0">
<property name="showDropIndicator" stdset="0" >
<bool>true</bool>
</property>
<property name="iconSize">
<property name="selectionMode" >
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="iconSize" >
<size>
<width>40</width>
<height>40</height>
</size>
</property>
<property name="movement">
<property name="movement" >
<enum>QListView::Static</enum>
</property>
<property name="flow">
<property name="flow" >
<enum>QListView::LeftToRight</enum>
</property>
<property name="gridSize">
<property name="gridSize" >
<size>
<width>175</width>
<height>90</height>
</size>
</property>
<property name="viewMode">
<property name="viewMode" >
<enum>QListView::ListMode</enum>
</property>
<property name="wordWrap">
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="donate_button">
<property name="cursor">
<widget class="QToolButton" name="donate_button" >
<property name="cursor" >
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="text">
<property name="text" >
<string>...</string>
</property>
<property name="icon">
<iconset resource="images.qrc">
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/donate.svg</normaloff>:/images/donate.svg</iconset>
</property>
<property name="iconSize">
<property name="iconSize" >
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="autoRaise">
<property name="autoRaise" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<layout class="QVBoxLayout" name="verticalLayout_3" >
<item>
<widget class="QLabel" name="vanity">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<widget class="QLabel" name="vanity" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Preferred" hsizetype="Preferred" >
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>90</height>
</size>
</property>
<property name="text">
<property name="text" >
<string/>
</property>
<property name="textFormat">
<property name="textFormat" >
<enum>Qt::RichText</enum>
</property>
<property name="openExternalLinks">
<property name="openExternalLinks" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<layout class="QHBoxLayout" name="horizontalLayout_2" >
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<widget class="QLabel" name="label_2" >
<property name="text" >
<string>Output:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="output_format">
<property name="toolTip">
<widget class="QComboBox" name="output_format" >
<property name="toolTip" >
<string>Set the output format that is used when converting ebooks and downloading news</string>
</property>
</widget>
@ -155,99 +163,99 @@
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout">
<property name="spacing">
<item row="1" column="0" >
<layout class="QHBoxLayout" >
<property name="spacing" >
<number>6</number>
</property>
<property name="margin">
<property name="margin" >
<number>0</number>
</property>
<item>
<widget class="QToolButton" name="advanced_search_button">
<property name="toolTip">
<widget class="QToolButton" name="advanced_search_button" >
<property name="toolTip" >
<string>Advanced search</string>
</property>
<property name="text">
<property name="text" >
<string>...</string>
</property>
<property name="icon">
<iconset resource="images.qrc">
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/search.svg</normaloff>:/images/search.svg</iconset>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>Alt+S</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<widget class="QLabel" name="label" >
<property name="text" >
<string>&amp;Search:</string>
</property>
<property name="buddy">
<property name="buddy" >
<cstring>search</cstring>
</property>
</widget>
</item>
<item>
<widget class="SearchBox" name="search">
<property name="enabled">
<widget class="SearchBox" name="search" >
<property name="enabled" >
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<property name="sizePolicy" >
<sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops">
<property name="acceptDrops" >
<bool>false</bool>
</property>
<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>
<property name="toolTip" >
<string>Search the list of books by title or author&lt;br>&lt;br>Words separated by spaces are ANDed</string>
</property>
<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>
<property name="whatsThis" >
<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 name="autoFillBackground">
<property name="autoFillBackground" >
<bool>false</bool>
</property>
<property name="text">
<property name="text" >
<string/>
</property>
<property name="frame">
<property name="frame" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="clear_button">
<property name="toolTip">
<widget class="QToolButton" name="clear_button" >
<property name="toolTip" >
<string>Reset Quick Search</string>
</property>
<property name="text">
<property name="text" >
<string>...</string>
</property>
<property name="icon">
<iconset resource="images.qrc">
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/clear_left.svg</normaloff>:/images/clear_left.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<widget class="Line" name="line" >
<property name="orientation" >
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<property name="sizeHint" stdset="0" >
<size>
<width>20</width>
<height>20</height>
@ -256,77 +264,77 @@
</spacer>
</item>
<item>
<widget class="QToolButton" name="config_button">
<property name="toolTip">
<widget class="QToolButton" name="config_button" >
<property name="toolTip" >
<string>Configuration</string>
</property>
<property name="text">
<property name="text" >
<string>...</string>
</property>
<property name="icon">
<iconset resource="images.qrc">
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/config.svg</normaloff>:/images/config.svg</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QStackedWidget" name="stack">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<item row="2" column="0" >
<widget class="QStackedWidget" name="stack" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>100</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<property name="currentIndex" >
<number>0</number>
</property>
<widget class="QWidget" name="library">
<layout class="QVBoxLayout" name="verticalLayout_2">
<widget class="QWidget" name="library" >
<layout class="QVBoxLayout" name="verticalLayout_2" >
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<layout class="QHBoxLayout" name="horizontalLayout" >
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<layout class="QVBoxLayout" name="verticalLayout" >
<item>
<widget class="QRadioButton" name="match_any">
<property name="text">
<widget class="QRadioButton" name="match_any" >
<property name="text" >
<string>Match any</string>
</property>
<property name="checked">
<property name="checked" >
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="match_all">
<property name="text">
<widget class="QRadioButton" name="match_all" >
<property name="text" >
<string>Match all</string>
</property>
<property name="checked">
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="popularity">
<property name="text">
<widget class="QCheckBox" name="popularity" >
<property name="text" >
<string>Sort by &amp;popularity</string>
</property>
</widget>
</item>
<item>
<widget class="TagsView" name="tags_view">
<property name="tabKeyNavigation">
<widget class="TagsView" name="tags_view" >
<property name="tabKeyNavigation" >
<bool>true</bool>
</property>
<property name="alternatingRowColors">
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="animated">
<property name="animated" >
<bool>true</bool>
</property>
<property name="headerHidden">
<property name="headerHidden" >
<bool>true</bool>
</property>
</widget>
@ -334,35 +342,35 @@
</layout>
</item>
<item>
<widget class="BooksView" name="library_view">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<widget class="BooksView" name="library_view" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>100</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops">
<property name="acceptDrops" >
<bool>true</bool>
</property>
<property name="dragEnabled">
<property name="dragEnabled" >
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode">
<property name="dragDropOverwriteMode" >
<bool>false</bool>
</property>
<property name="dragDropMode">
<property name="dragDropMode" >
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="alternatingRowColors">
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="selectionBehavior">
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid">
<property name="showGrid" >
<bool>false</bool>
</property>
<property name="wordWrap">
<property name="wordWrap" >
<bool>false</bool>
</property>
</widget>
@ -371,76 +379,76 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="main_memory">
<layout class="QGridLayout">
<item row="0" column="0">
<widget class="DeviceBooksView" name="memory_view">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<widget class="QWidget" name="main_memory" >
<layout class="QGridLayout" >
<item row="0" column="0" >
<widget class="DeviceBooksView" name="memory_view" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>100</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops">
<property name="acceptDrops" >
<bool>true</bool>
</property>
<property name="dragEnabled">
<property name="dragEnabled" >
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode">
<property name="dragDropOverwriteMode" >
<bool>false</bool>
</property>
<property name="dragDropMode">
<property name="dragDropMode" >
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="alternatingRowColors">
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="selectionBehavior">
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid">
<property name="showGrid" >
<bool>false</bool>
</property>
<property name="wordWrap">
<property name="wordWrap" >
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page">
<layout class="QGridLayout">
<item row="0" column="0">
<widget class="DeviceBooksView" name="card_view">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<widget class="QWidget" name="page" >
<layout class="QGridLayout" >
<item row="0" column="0" >
<widget class="DeviceBooksView" name="card_view" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Preferred" >
<horstretch>10</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops">
<property name="acceptDrops" >
<bool>true</bool>
</property>
<property name="dragEnabled">
<property name="dragEnabled" >
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode">
<property name="dragDropOverwriteMode" >
<bool>false</bool>
</property>
<property name="dragDropMode">
<property name="dragDropMode" >
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="alternatingRowColors">
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="selectionBehavior">
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid">
<property name="showGrid" >
<bool>false</bool>
</property>
<property name="wordWrap">
<property name="wordWrap" >
<bool>false</bool>
</property>
</widget>
@ -451,234 +459,225 @@
</item>
</layout>
</widget>
<widget class="QToolBar" name="tool_bar">
<property name="minimumSize">
<widget class="QToolBar" name="tool_bar" >
<property name="minimumSize" >
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="contextMenuPolicy">
<property name="contextMenuPolicy" >
<enum>Qt::PreventContextMenu</enum>
</property>
<property name="movable">
<property name="movable" >
<bool>false</bool>
</property>
<property name="orientation">
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="iconSize">
<property name="iconSize" >
<size>
<width>48</width>
<height>48</height>
</size>
</property>
<property name="toolButtonStyle">
<property name="toolButtonStyle" >
<enum>Qt::ToolButtonTextUnderIcon</enum>
</property>
<attribute name="toolBarArea">
<attribute name="toolBarArea" >
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<attribute name="toolBarBreak" >
<bool>false</bool>
</attribute>
<addaction name="action_add"/>
<addaction name="action_edit"/>
<addaction name="action_convert"/>
<addaction name="action_view"/>
<addaction name="action_news"/>
<addaction name="separator"/>
<addaction name="action_sync"/>
<addaction name="action_save"/>
<addaction name="action_del"/>
<addaction name="separator"/>
<addaction name="action_preferences"/>
<addaction name="action_add" />
<addaction name="action_edit" />
<addaction name="action_convert" />
<addaction name="action_view" />
<addaction name="action_news" />
<addaction name="separator" />
<addaction name="action_sync" />
<addaction name="action_save" />
<addaction name="action_del" />
<addaction name="separator" />
<addaction name="action_preferences" />
</widget>
<widget class="QStatusBar" name="statusBar">
<property name="mouseTracking">
<widget class="QStatusBar" name="statusBar" >
<property name="mouseTracking" >
<bool>true</bool>
</property>
</widget>
<action name="action_add">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_add" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/add_book.svg</normaloff>:/images/add_book.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Add books</string>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>A</string>
</property>
<property name="autoRepeat">
<property name="autoRepeat" >
<bool>false</bool>
</property>
</action>
<action name="action_del">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_del" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/trash.svg</normaloff>:/images/trash.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Remove books</string>
</property>
<property name="toolTip">
<property name="toolTip" >
<string>Remove books</string>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>Del</string>
</property>
</action>
<action name="action_edit">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_edit" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/edit_input.svg</normaloff>:/images/edit_input.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Edit meta information</string>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>E</string>
</property>
<property name="autoRepeat">
<property name="autoRepeat" >
<bool>false</bool>
</property>
</action>
<action name="action_sync">
<property name="enabled">
<action name="action_sync" >
<property name="enabled" >
<bool>false</bool>
</property>
<property name="icon">
<iconset resource="images.qrc">
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/sync.svg</normaloff>:/images/sync.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Send to device</string>
</property>
</action>
<action name="action_save">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_save" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/save.svg</normaloff>:/images/save.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Save to disk</string>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>S</string>
</property>
</action>
<action name="action_news">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_news" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/news.svg</normaloff>:/images/news.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Fetch news</string>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>F</string>
</property>
</action>
<action name="action_convert">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_convert" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/convert.svg</normaloff>:/images/convert.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Convert E-books</string>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>C</string>
</property>
</action>
<action name="action_view">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_view" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/view.svg</normaloff>:/images/view.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>View</string>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>V</string>
</property>
</action>
<action name="action_open_containing_folder">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_open_containing_folder" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Open containing folder</string>
</property>
</action>
<action name="action_show_book_details">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_show_book_details" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/dialog_information.svg</normaloff>:/images/dialog_information.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Show book details</string>
</property>
</action>
<action name="action_books_by_same_author">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_books_by_same_author" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/user_profile.svg</normaloff>:/images/user_profile.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Books by same author</string>
</property>
</action>
<action name="action_books_in_this_series">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_books_in_this_series" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/books_in_series.svg</normaloff>:/images/books_in_series.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Books in this series</string>
</property>
</action>
<action name="action_books_by_this_publisher">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_books_by_this_publisher" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/publisher.png</normaloff>:/images/publisher.png</iconset>
</property>
<property name="text">
<property name="text" >
<string>Books by this publisher</string>
</property>
</action>
<action name="action_books_with_the_same_tags">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_books_with_the_same_tags" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/tags.svg</normaloff>:/images/tags.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Books with the same tags</string>
</property>
</action>
<action name="action_send_specific_format_to_device">
<property name="icon">
<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">
<action name="action_preferences" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/config.svg</normaloff>:/images/config.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Preferences</string>
</property>
<property name="toolTip">
<property name="toolTip" >
<string>Configure calibre</string>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>Ctrl+P</string>
</property>
</action>
@ -711,7 +710,7 @@
</customwidget>
</customwidgets>
<resources>
<include location="images.qrc"/>
<include location="images.qrc" />
</resources>
<connections>
<connection>
@ -720,11 +719,11 @@
<receiver>search</receiver>
<slot>clear()</slot>
<hints>
<hint type="sourcelabel">
<hint type="sourcelabel" >
<x>787</x>
<y>215</y>
</hint>
<hint type="destinationlabel">
<hint type="destinationlabel" >
<x>755</x>
<y>213</y>
</hint>

View File

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

View File

@ -25,7 +25,8 @@ if iswindows:
else:
Structure = _Structure
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)
RAR_OM_LIST = 0
@ -95,7 +96,7 @@ class RARHeaderDataEx(Structure):
# Define a callback function
#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
#callback_func = CALLBACK_FUNC(py_callback_func)
@ -123,7 +124,7 @@ def _interpret_open_error(code, path):
elif code == ERAR_EOPEN:
msg = 'Cannot open ' + path
return msg
def _interpret_process_file_error(code):
msg = 'Unknown Error'
if code == ERAR_UNKNOWN_FORMAT:
@ -145,7 +146,7 @@ def _interpret_process_file_error(code):
elif code == ERAR_MISSING_PASSWORD:
msg = 'Password is required.'
return msg
def get_archive_info(flags):
ios = StringIO()
print >>ios, 'Volume:\t\t', 'yes' if (flags & 1) else 'no'
@ -162,7 +163,7 @@ def get_archive_info(flags):
def extract(path, dir):
"""
Extract archive C{filename} into directory C{dir}
"""
"""
open_archive_data = RAROpenArchiveDataEx(ArcName=path, OpenMode=RAR_OM_EXTRACT, CmtBuf=None)
arc_data = _libunrar.RAROpenArchiveEx(byref(open_archive_data))
cwd = os.getcwd()
@ -173,7 +174,7 @@ def extract(path, dir):
if open_archive_data.OpenResult != 0:
raise UnRARException(_interpret_open_error(open_archive_data.OpenResult, 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)
#_libunrar.RARSetCallback(arc_data, callback_func, mode)
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()
finally:
_libunrar.RARCloseArchive(arc_data)

View File

@ -41,6 +41,8 @@ entry_points = {
'calibre-customize = calibre.customize.ui:main',
'pdftrim = calibre.ebooks.pdf.pdftrim:main' ,
'fetch-ebook-metadata = calibre.ebooks.metadata.fetch:main',
'calibre-smtp = calibre.utils.smtp:main',
],
'gui_scripts' : [
__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.metadata.fetch import option_parser as fem_op
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',
'txt', 'lit', 'rtf', 'pdf', 'prc', 'mobi', 'fb2', 'odt']
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('feeds2mobi', feeds2mobi, feed_titles))
f.write(opts_and_words('fetch-ebook-metadata', fem_op, []))
f.write(opts_and_words('calibre-smtp', smtp_op, []))
f.write('''
_prs500_ls()
{
@ -403,7 +407,7 @@ def post_install():
if opts.save_manifest_to:
open(opts.save_manifest_to, 'wb').write('\n'.join(manifest)+'\n')
from calibre.utils.config import config_dir
if os.path.exists(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)
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)
f = open('calibre-lrfviewer.desktop', 'wb')
f.write(VIEWER)
f.close()

View File

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

View File

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

View File

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

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',
'new_york_review_of_books_no_sub', 'politico', 'adventuregamers',
'mondedurable', 'instapaper', 'dnevnik_cro', 'vecernji_list',
'nacional_cro', '24sata',
'nacional_cro', '24sata', 'dnevni_avaz', 'glas_srpske', '24sata_rs',
)]
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 = '# coding: utf-8\n' + src
src = 'from __future__ import with_statement\n' + src
src = src.replace('from libprs500', 'from calibre').encode('utf-8')
f.write(src)
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
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
@ -13,57 +12,53 @@ class B92(BasicNewsRecipe):
title = 'B92'
__author__ = 'Darko Miletic'
description = 'Dnevne vesti iz Srbije i sveta'
oldest_article = 2
publisher = 'B92.net'
category = 'news, politics, Serbia'
publisher = 'B92'
category = 'news, politics, Serbia'
oldest_article = 1
max_articles_per_feed = 100
remove_javascript = True
no_stylesheets = True
use_embedded_content = False
cover_url = 'http://static.b92.net/images/fp/logo.gif'
remove_javascript = True
encoding = 'cp1250'
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 = [
'--comment' , description
, '--category' , category
'--comment', description
, '--category', category
, '--publisher', publisher
, '--ignore-tables'
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True'
keep_only_tags = [ dict(name='div', attrs={'class':'sama_vest'}) ]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True\noverride_css=" p {text-indent: 0em; margin-top: 0em; margin-bottom: 0.5em}"'
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 = [
(u'Vesti', u'http://www.b92.net/info/rss/vesti.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):
main, sep, article_id = url.partition('nav_id=')
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
return url + '&version=print'
def preprocess_html(self, soup):
lng = 'sr-Latn-RS'
soup.html['xml:lang'] = lng
soup.html['lang'] = lng
mtag = '<meta http-equiv="Content-Language" content="sr-Latn-RS"/>'
soup.head.insert(0,mtag)
del soup.body['onload']
mtag = '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>'
soup.head.insert(0,mtag)
for item in soup.findAll(style=True):
del item['style']
for item in soup.findAll(name='img',align=True):
for item in soup.findAll(align=True):
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

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'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
Profile to download FAZ.net
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class FazNet(BasicNewsRecipe):
title = 'FAZ NET'
__author__ = 'Kovid Goyal'
description = 'Frankfurter Allgemeine Zeitung'
use_embedded_content = False
language = _('German')
max_articles_per_feed = 30
preprocess_regexps = [
(re.compile(r'Zum Thema</span>.*?</BODY>', re.IGNORECASE | re.DOTALL),
lambda match : ''),
]
feeds = [ ('FAZ.NET', 'http://www.faz.net/s/Rub/Tpl~Epartner~SRss_.xml') ]
def print_version(self, url):
return url.replace('.html?rss_aktuell', '~Afor~Eprint.html')
__license__ = 'GPL v3'
__copyright__ = '2008-2009, Kovid Goyal <kovid at kovidgoyal.net>, Darko Miletic <darko at gmail.com>'
'''
Profile to download FAZ.net
'''
from calibre.web.feeds.news import BasicNewsRecipe
class FazNet(BasicNewsRecipe):
title = 'FAZ NET'
__author__ = 'Kovid Goyal, Darko Miletic'
description = 'Frankfurter Allgemeine Zeitung'
publisher = 'FAZ Electronic Media GmbH'
category = 'news, politics, Germany'
use_embedded_content = False
language = _('German')
max_articles_per_feed = 30
no_stylesheets = True
encoding = 'utf-8'
remove_javascript = True
html2lrf_options = [
'--comment', description
, '--category', category
, '--publisher', publisher
]
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
class Joelonsoftware(BasicNewsRecipe):
title = 'Joel on Software'
__author__ = 'Darko Miletic'
description = 'Painless Software Management'
language = _('English')
no_stylesheets = True
use_embedded_content = True
oldest_article = 60
cover_url = 'http://www.joelonsoftware.com/RssJoelOnSoftware.jpg'
html2lrf_options = [ '--comment' , description
, '--category' , 'blog,software,news'
, '--author' , 'Joel Spolsky'
]
feeds = [(u'Articles', u'http://www.joelonsoftware.com/rss.xml')]

View File

@ -8,11 +8,11 @@ Fetch Spiegel Online.
import re
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import BeautifulSoup
class SpeigelOnline(BasicNewsRecipe):
title = 'Spiegel Online'
title = 'Spiegel Online'
description = 'Nachrichten des Magazins Der Spiegel'
__author__ = 'Kovid Goyal'
use_embedded_content = False
@ -22,17 +22,26 @@ class SpeigelOnline(BasicNewsRecipe):
no_stylesheets = True
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
(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):
tokens = url.split(',')
tokens[-2:-2] = ['druck|']
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