Merge upstream changes.

This commit is contained in:
Marshall T. Vandegrift 2009-01-03 12:27:55 -05:00
commit 6e3d8aee52
156 changed files with 12370 additions and 7194 deletions

View File

@ -380,12 +380,15 @@ if __name__ == '__main__':
class build(_build): class build(_build):
sub_commands = \ sub_commands = [
[
('resources', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()), ('resources', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
('translations', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()), ('translations', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
('gui', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()), ('gui', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
] + _build.sub_commands ('build_ext', lambda self: True),
('build_py', lambda self: True),
('build_clib', _build.has_c_libraries),
('build_scripts', _build.has_scripts),
]
entry_points['console_scripts'].append('calibre_postinstall = calibre.linux:post_install') entry_points['console_scripts'].append('calibre_postinstall = calibre.linux:post_install')
ext_modules = [ ext_modules = [

View File

@ -13,7 +13,8 @@ from calibre.startup import plugins, winutil, winutilerror
from calibre.constants import iswindows, isosx, islinux, isfrozen, \ from calibre.constants import iswindows, isosx, islinux, isfrozen, \
terminal_controller, preferred_encoding, \ terminal_controller, preferred_encoding, \
__appname__, __version__, __author__, \ __appname__, __version__, __author__, \
win32event, win32api, winerror, fcntl win32event, win32api, winerror, fcntl, \
filesystem_encoding
import mechanize import mechanize
mimetypes.add_type('application/epub+zip', '.epub') mimetypes.add_type('application/epub+zip', '.epub')
@ -41,6 +42,28 @@ def osx_version():
return int(m.group(1)), int(m.group(2)), int(m.group(3)) return int(m.group(1)), int(m.group(2)), int(m.group(3))
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+\[\]/]')
def sanitize_file_name(name, substitute='_', as_unicode=False):
'''
Sanitize the filename `name`. All invalid characters are replaced by `substitute`.
The set of invalid characters is the union of the invalid characters in Windows,
OS X and Linux. Also removes leading an trailing whitespace.
**WARNING:** This function also replaces path separators, so only pass file names
and not full paths to it.
*NOTE:* This function always returns byte strings, not unicode objects. The byte strings
are encoded in the filesystem encoding of the platform, or UTF-8.
'''
if isinstance(name, unicode):
name = name.encode(filesystem_encoding, 'ignore')
one = _filename_sanitize.sub(substitute, name)
one = re.sub(r'\s', ' ', one).strip()
one = re.sub(r'^\.+$', '_', one)
if as_unicode:
one = one.decode(filesystem_encoding)
return one
class CommandLineError(Exception): class CommandLineError(Exception):
pass pass
@ -201,13 +224,6 @@ class CurrentDir(object):
def __exit__(self, *args): def __exit__(self, *args):
os.chdir(self.cwd) os.chdir(self.cwd)
def sanitize_file_name(name):
'''
Remove characters that are illegal in filenames from name.
Also remove path separators. All illegal characters are replaced by
underscores.
'''
return re.sub(r'\s', ' ', re.sub(r'[\xae"\'\|\~\:\?\\\/]|^-', '_', name.strip()))
def detect_ncpus(): def detect_ncpus():
"""Detects the number of effective CPUs in the system""" """Detects the number of effective CPUs in the system"""
@ -366,6 +382,12 @@ def strftime(fmt, t=time.localtime()):
return plugins['winutil'][0].strftime(fmt, t) return plugins['winutil'][0].strftime(fmt, t)
return time.strftime(fmt, t).decode(preferred_encoding, 'replace') return time.strftime(fmt, t).decode(preferred_encoding, 'replace')
def my_unichr(num):
try:
return unichr(num)
except ValueError:
return u'?'
def entity_to_unicode(match, exceptions=[], encoding='cp1252'): def entity_to_unicode(match, exceptions=[], encoding='cp1252'):
''' '''
@param match: A match object such that '&'+match.group(1)';' is the entity. @param match: A match object such that '&'+match.group(1)';' is the entity.
@ -381,7 +403,7 @@ def entity_to_unicode(match, exceptions=[], encoding='cp1252'):
if ent.startswith(u'#x'): if ent.startswith(u'#x'):
num = int(ent[2:], 16) num = int(ent[2:], 16)
if encoding is None or num > 255: if encoding is None or num > 255:
return unichr(num) return my_unichr(num)
return chr(num).decode(encoding) return chr(num).decode(encoding)
if ent.startswith(u'#'): if ent.startswith(u'#'):
try: try:
@ -389,13 +411,13 @@ def entity_to_unicode(match, exceptions=[], encoding='cp1252'):
except ValueError: except ValueError:
return '&'+ent+';' return '&'+ent+';'
if encoding is None or num > 255: if encoding is None or num > 255:
return unichr(num) return my_unichr(num)
try: try:
return chr(num).decode(encoding) return chr(num).decode(encoding)
except UnicodeDecodeError: except UnicodeDecodeError:
return unichr(num) return my_unichr(num)
try: try:
return unichr(name2codepoint[ent]) return my_unichr(name2codepoint[ent])
except KeyError: except KeyError:
return '&'+ent+';' return '&'+ent+';'

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__appname__ = 'calibre' __appname__ = 'calibre'
__version__ = '0.4.121' __version__ = '0.4.124'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
''' '''
Various run time constants. Various run time constants.
@ -29,6 +29,10 @@ winerror = __import__('winerror') if iswindows else None
win32api = __import__('win32api') if iswindows else None win32api = __import__('win32api') if iswindows else None
fcntl = None if iswindows else __import__('fcntl') fcntl = None if iswindows else __import__('fcntl')
filesystem_encoding = sys.getfilesystemencoding()
if filesystem_encoding is None: filesystem_encoding = 'utf-8'
################################################################################ ################################################################################
plugins = None plugins = None
if plugins is None: if plugins is None:

View File

@ -185,6 +185,20 @@ def add_plugin(path_to_zip_file):
initialize_plugins() initialize_plugins()
return plugin return plugin
def remove_plugin(plugin_or_name):
name = getattr(plugin_or_name, 'name', plugin_or_name)
plugins = config['plugins']
removed = False
if name in plugins.keys():
removed = True
zfp = plugins[name]
if os.path.exists(zfp):
os.remove(zfp)
plugins.pop(name)
config['plugins'] = plugins
initialize_plugins()
return removed
def is_disabled(plugin): def is_disabled(plugin):
return plugin.name in config['disabled_plugins'] return plugin.name in config['disabled_plugins']
@ -237,6 +251,8 @@ def option_parser():
''')) '''))
parser.add_option('-a', '--add-plugin', default=None, parser.add_option('-a', '--add-plugin', default=None,
help=_('Add a plugin by specifying the path to the zip file containing it.')) help=_('Add a plugin by specifying the path to the zip file containing it.'))
parser.add_option('-r', '--remove-plugin', default=None,
help=_('Remove a custom plugin by name. Has no effect on builtin plugins'))
parser.add_option('--customize-plugin', default=None, parser.add_option('--customize-plugin', default=None,
help=_('Customize plugin. Specify name of plugin and customization string separated by a comma.')) help=_('Customize plugin. Specify name of plugin and customization string separated by a comma.'))
parser.add_option('-l', '--list-plugins', default=False, action='store_true', parser.add_option('-l', '--list-plugins', default=False, action='store_true',
@ -267,6 +283,11 @@ def main(args=sys.argv):
if opts.add_plugin is not None: if opts.add_plugin is not None:
plugin = add_plugin(opts.add_plugin) plugin = add_plugin(opts.add_plugin)
print 'Plugin added:', plugin.name, plugin.version print 'Plugin added:', plugin.name, plugin.version
if opts.remove_plugin is not None:
if remove_plugin(opts.remove_plugin):
print 'Plugin removed'
else:
print 'No custom pluginnamed', opts.remove_plugin
if opts.customize_plugin is not None: if opts.customize_plugin is not None:
name, custom = opts.customize_plugin.split(',') name, custom = opts.customize_plugin.split(',')
plugin = find_plugin(name.strip()) plugin = find_plugin(name.strip())

View File

@ -1,5 +1,6 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
''' '''
Device drivers. Device drivers.
''' '''
@ -8,8 +9,9 @@ def devices():
from calibre.devices.prs500.driver import PRS500 from calibre.devices.prs500.driver import PRS500
from calibre.devices.prs505.driver import PRS505 from calibre.devices.prs505.driver import PRS505
from calibre.devices.prs700.driver import PRS700 from calibre.devices.prs700.driver import PRS700
from calibre.devices.cybookg3.driver import CYBOOKG3
#from calibre.devices.kindle.driver import KINDLE #from calibre.devices.kindle.driver import KINDLE
return (PRS500, PRS505, PRS700) return (PRS500, PRS505, PRS700, CYBOOKG3)
import time import time

View File

View File

@ -0,0 +1,77 @@
__license__ = 'GPL v3'
__copyright__ = '2009, John Schember <john at nachtimwald.com'
'''
'''
import os, fnmatch, time
from calibre.devices.interface import BookList as _BookList
EBOOK_DIR = "eBooks"
EBOOK_TYPES = ['mobi', 'prc', 'html', 'pdf', 'rtf', 'txt']
class Book(object):
def __init__(self, path, title, authors):
self.title = title
self.authors = authors
self.size = os.path.getsize(path)
self.datetime = time.gmtime(os.path.getctime(path))
self.path = path
self.thumbnail = None
self.tags = []
@apply
def thumbnail():
return None
def __str__(self):
""" Return a utf-8 encoded string with title author and path information """
return self.title.encode('utf-8') + " by " + \
self.authors.encode('utf-8') + " at " + self.path.encode('utf-8')
class BookList(_BookList):
def __init__(self, mountpath):
self._mountpath = mountpath
_BookList.__init__(self)
self.return_books(mountpath)
def return_books(self, mountpath):
# Get all books in all directories under the root EBOOK_DIR directory
for path, dirs, files in os.walk(os.path.join(mountpath, EBOOK_DIR)):
# Filter out anything that isn't in the list of supported ebook types
for book_type in EBOOK_TYPES:
for filename in fnmatch.filter(files, '*.%s' % (book_type)):
# Calibre uses a specific format for file names. They take the form
# title_-_author_number.extention We want to see if the file name is
# in this format.
if fnmatch.fnmatchcase(filename, '*_-_*.*'):
# Get the title and author from the file name
title, sep, author = filename.rpartition('_-_')
author, sep, ext = author.rpartition('_')
book_title = title.replace('_', ' ')
book_author = author.replace('_', ' ')
# if the filename did not match just set the title to
# the filename without the extension
else:
book_title = os.path.splitext(filename)[0].replace('_', ' ')
book_path = os.path.join(path, filename)
self.append(Book(book_path, book_title, book_author))
def add_book(self, path, title):
self.append(Book(path, title, ""))
def remove_book(self, path):
for book in self:
if path.endswith(book.path):
self.remove(book)
break
def supports_tags(self):
''' Return True if the the device supports tags (collections) for this book list. '''
return False
def set_tags(self, book, tags):
pass

View File

@ -0,0 +1,291 @@
__license__ = 'GPL v3'
__copyright__ = '2009, John Schember <john at nachtimwald.com'
'''
Device driver for Bookeen's Cybook Gen 3
'''
import os, fnmatch, shutil, time
from itertools import cycle
from calibre.devices.interface import Device
from calibre.devices.errors import DeviceError, FreeSpaceError
from calibre.devices.cybookg3.books import BookList, EBOOK_DIR, EBOOK_TYPES
from calibre import iswindows, islinux, isosx, __appname__
class CYBOOKG3(Device):
# Ordered list of supported formats
FORMATS = EBOOK_TYPES
VENDOR_ID = 0x0bda
PRODUCT_ID = 0x0703
BCD = 0x110
#THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device
MAIN_MEMORY_VOLUME_LABEL = 'Cybook Gen 3 Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'Cybook Gen 3 Storage Card'
FDI_TEMPLATE = \
'''
<device>
<match key="info.category" string="volume">
<match key="@info.parent:@info.parent:@info.parent:@info.parent:usb.vendor_id" int="%(vendor_id)s">
<match key="@info.parent:@info.parent:@info.parent:@info.parent:usb.product_id" int="%(product_id)s">
<match key="@info.parent:@info.parent:@info.parent:@info.parent:usb.device_revision_bcd" int="%(bcd)s">
<match key="volume.is_partition" bool="false">
<merge key="volume.label" type="string">%(main_memory)s</merge>
<merge key="%(app)s.mainvolume" type="string">%(deviceclass)s</merge>
</match>
</match>
</match>
</match>
</match>
</device>
<device>
<match key="info.category" string="volume">
<match key="@info.parent:@info.parent:@info.parent:@info.parent:usb.vendor_id" int="%(vendor_id)s">
<match key="@info.parent:@info.parent:@info.parent:@info.parent:usb.product_id" int="%(product_id)s">
<match key="@info.parent:@info.parent:@info.parent:@info.parent:usb.device_revision_bcd" int="%(bcd)s">
<match key="volume.is_partition" bool="true">
<merge key="volume.label" type="string">%(storage_card)s</merge>
<merge key="%(app)s.cardvolume" type="string">%(deviceclass)s</merge>
</match>
</match>
</match>
</match>
</match>
</device>
'''
def __init__(self, key='-1', log_packets=False, report_progress=None) :
self._main_prefix = self._card_prefix = None
@classmethod
def get_fdi(cls):
return cls.FDI_TEMPLATE%dict(
app=__appname__,
deviceclass=cls.__name__,
vendor_id=hex(cls.VENDOR_ID),
product_id=hex(cls.PRODUCT_ID),
bcd=hex(cls.BCD),
main_memory=cls.MAIN_MEMORY_VOLUME_LABEL,
storage_card=cls.STORAGE_CARD_VOLUME_LABEL,
)
def set_progress_reporter(self, report_progress):
self.report_progress = report_progress
def get_device_information(self, end_session=True):
"""
Ask device for device information. See L{DeviceInfoQuery}.
@return: (device name, device version, software version on device, mime type)
"""
return (self.__class__.__name__, '', '', '')
def card_prefix(self, end_session=True):
return self._card_prefix
@classmethod
def _windows_space(cls, prefix):
if prefix is None:
return 0, 0
win32file = __import__('win32file', globals(), locals(), [], -1)
try:
sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \
win32file.GetDiskFreeSpace(prefix[:-1])
except Exception, err:
if getattr(err, 'args', [None])[0] == 21: # Disk not ready
time.sleep(3)
sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \
win32file.GetDiskFreeSpace(prefix[:-1])
else: raise
mult = sectors_per_cluster * bytes_per_sector
return total_clusters * mult, free_clusters * mult
def total_space(self, end_session=True):
msz = csz = 0
print self._main_prefix
if not iswindows:
if self._main_prefix is not None:
stats = os.statvfs(self._main_prefix)
msz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree)
if self._card_prefix is not None:
stats = os.statvfs(self._card_prefix)
csz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree)
else:
msz = self._windows_space(self._main_prefix)[0]
csz = self._windows_space(self._card_prefix)[0]
return (msz, 0, csz)
def free_space(self, end_session=True):
msz = csz = 0
if not iswindows:
if self._main_prefix is not None:
stats = os.statvfs(self._main_prefix)
msz = stats.f_frsize * stats.f_bavail
if self._card_prefix is not None:
stats = os.statvfs(self._card_prefix)
csz = stats.f_frsize * stats.f_bavail
else:
msz = self._windows_space(self._main_prefix)[1]
csz = self._windows_space(self._card_prefix)[1]
return (msz, 0, csz)
def books(self, oncard=False, end_session=True):
if oncard and self._card_prefix is None:
return []
prefix = self._card_prefix if oncard else self._main_prefix
bl = BookList(prefix)
return bl
def upload_books(self, files, names, on_card=False, end_session=True):
if on_card and not self._card_prefix:
raise ValueError(_('The reader has no storage card connected.'))
if not on_card:
path = os.path.join(self._main_prefix, EBOOK_DIR)
else:
path = os.path.join(self._card_prefix, EBOOK_DIR)
sizes = map(os.path.getsize, files)
size = sum(sizes)
if on_card and size > self.free_space()[2] - 1024*1024:
raise FreeSpaceError("There is insufficient free space "+\
"on the storage card")
if not on_card and size > self.free_space()[0] - 2*1024*1024:
raise FreeSpaceError("There is insufficient free space " +\
"in main memory")
paths = []
names = iter(names)
for infile in files:
filepath = os.path.join(path, names.next())
paths.append(filepath)
shutil.copy2(infile, filepath)
return zip(paths, cycle([on_card]))
@classmethod
def add_books_to_metadata(cls, locations, metadata, booklists):
for location in locations:
path = location[0]
on_card = 1 if location[1] else 0
booklists[on_card].add_book(path, os.path.basename(path))
def delete_books(self, paths, end_session=True):
for path in paths:
if os.path.exists(path):
# Delete the ebook
os.unlink(path)
filepath, ext = os.path.splitext(path)
basepath, filename = os.path.split(filepath)
# Delete the ebook auxiliary file
if os.path.exists(filepath + '.mbp'):
os.unlink(filepath + '.mbp')
# Delete the thumbnails file auto generated for the ebook
for p, d, files in os.walk(basepath):
for filen in fnmatch.filter(files, filename + "*.t2b"):
os.unlink(os.path.join(p, filen))
@classmethod
def remove_books_from_metadata(cls, paths, booklists):
for path in paths:
for bl in booklists:
bl.remove_book(path)
def sync_booklists(self, booklists, end_session=True):
# There is no meta data on the device to update. The device is treated
# as a mass storage device and does not use a meta data xml file like
# the Sony Readers.
pass
def get_file(self, path, outfile, end_session=True):
path = self.munge_path(path)
src = open(path, 'rb')
shutil.copyfileobj(src, outfile, 10*1024*1024)
def munge_path(self, path):
if path.startswith('/') and not (path.startswith(self._main_prefix) or \
(self._card_prefix and path.startswith(self._card_prefix))):
path = self._main_prefix + path[1:]
elif path.startswith('card:'):
path = path.replace('card:', self._card_prefix[:-1])
return path
def open_windows(self):
raise NotImplementedError()
def open_osx(self):
raise NotImplementedError()
def open_linux(self):
import dbus
bus = dbus.SystemBus()
hm = dbus.Interface(bus.get_object("org.freedesktop.Hal", "/org/freedesktop/Hal/Manager"), "org.freedesktop.Hal.Manager")
def conditional_mount(dev):
mmo = bus.get_object("org.freedesktop.Hal", dev)
label = mmo.GetPropertyString('volume.label', dbus_interface='org.freedesktop.Hal.Device')
is_mounted = mmo.GetPropertyString('volume.is_mounted', dbus_interface='org.freedesktop.Hal.Device')
mount_point = mmo.GetPropertyString('volume.mount_point', dbus_interface='org.freedesktop.Hal.Device')
fstype = mmo.GetPropertyString('volume.fstype', dbus_interface='org.freedesktop.Hal.Device')
if is_mounted:
return str(mount_point)
mmo.Mount(label, fstype, ['umask=077', 'uid='+str(os.getuid()), 'sync'],
dbus_interface='org.freedesktop.Hal.Device.Volume')
return os.path.normpath('/media/'+label)+'/'
mm = hm.FindDeviceStringMatch(__appname__+'.mainvolume', self.__class__.__name__)
if not mm:
raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.')%(self.__class__.__name__,))
self._main_prefix = None
for dev in mm:
try:
self._main_prefix = conditional_mount(dev)+os.sep
break
except dbus.exceptions.DBusException:
continue
if not self._main_prefix:
raise DeviceError('Could not open device for reading. Try a reboot.')
self._card_prefix = None
cards = hm.FindDeviceStringMatch(__appname__+'.cardvolume', self.__class__.__name__)
for dev in cards:
try:
self._card_prefix = conditional_mount(dev)+os.sep
break
except:
import traceback
print traceback
continue
def open(self):
time.sleep(5)
self._main_prefix = self._card_prefix = None
if islinux:
try:
self.open_linux()
except DeviceError:
time.sleep(3)
self.open_linux()
if iswindows:
try:
self.open_windows()
except DeviceError:
time.sleep(3)
self.open_windows()
if isosx:
try:
self.open_osx()
except DeviceError:
time.sleep(3)
self.open_osx()

View File

@ -39,6 +39,18 @@ class Device(object):
'''Return the FDI description of this device for HAL on linux.''' '''Return the FDI description of this device for HAL on linux.'''
return '' return ''
def open(self):
'''
Perform any device specific initialization. Called after the device is
detected but before any other functions that communicate with the device.
For example: For devices that present themselves as USB Mass storage
devices, this method would be responsible for mounting the device or
if the device has been automounted, for finding out where it has been
mounted. The driver for the PRS505 has a implementation of this function
that should serve as a good example for USB Mass storage devices.
'''
raise NotImplementedError()
def set_progress_reporter(self, report_progress): def set_progress_reporter(self, report_progress):
''' '''
@param report_progress: Function that is called with a % progress @param report_progress: Function that is called with a % progress

View File

@ -55,7 +55,7 @@ content = functools.partial(os.path.join, u'content')
def remove_bad_link(element, attribute, link, pos): def remove_bad_link(element, attribute, link, pos):
if attribute is not None: if attribute is not None:
if element.tag in ['link', 'img']: if element.tag in ['link']:
element.getparent().remove(element) element.getparent().remove(element)
else: else:
element.set(attribute, '') element.set(attribute, '')
@ -123,6 +123,10 @@ class HTMLProcessor(Processor, Rationalizer):
if opts.verbose > 2: if opts.verbose > 2:
self.debug_tree('nocss') self.debug_tree('nocss')
if hasattr(self.body, 'xpath'):
for script in list(self.body.xpath('descendant::script')):
script.getparent().remove(script)
def convert_image(self, img): def convert_image(self, img):
rpath = img.get('src', '') rpath = img.get('src', '')
path = os.path.join(os.path.dirname(self.save_path()), *rpath.split('/')) path = os.path.join(os.path.dirname(self.save_path()), *rpath.split('/'))

View File

@ -314,10 +314,22 @@ def opf_traverse(opf_reader, verbose=0, encoding=None):
convert_entities = functools.partial(entity_to_unicode, exceptions=['quot', 'apos', 'lt', 'gt', 'amp']) convert_entities = functools.partial(entity_to_unicode, exceptions=['quot', 'apos', 'lt', 'gt', 'amp'])
_span_pat = re.compile('<span.*?</span>', re.DOTALL|re.IGNORECASE)
def sanitize_head(match):
x = match.group(1)
x = _span_pat.sub('', x)
return '<head>\n'+x+'\n</head>'
class PreProcessor(object): class PreProcessor(object):
PREPROCESS = [ PREPROCESS = [
# Some idiotic HTML generators (Frontpage I'm looking at you)
# Put all sorts of crap into <head>. This messes up lxml
(re.compile(r'<head[^>]*>(.*?)</head>', re.IGNORECASE|re.DOTALL),
sanitize_head),
# Convert all entities, since lxml doesn't handle them well # Convert all entities, since lxml doesn't handle them well
(re.compile(r'&(\S+?);'), convert_entities), (re.compile(r'&(\S+?);'), convert_entities),
] ]
# Fix pdftohtml markup # Fix pdftohtml markup
@ -776,7 +788,10 @@ class Processor(Parser):
size = '3' size = '3'
if size and size.strip() and size.strip()[0] in ('+', '-'): if size and size.strip() and size.strip()[0] in ('+', '-'):
size = 3 + float(size) # Hack assumes basefont=3 size = 3 + float(size) # Hack assumes basefont=3
setting = 'font-size: %d%%;'%int((float(size)/3) * 100) try:
setting = 'font-size: %d%%;'%int((float(size)/3) * 100)
except ValueError:
setting = ''
face = font.attrib.pop('face', None) face = font.attrib.pop('face', None)
if face is not None: if face is not None:
setting += 'font-face:%s;'%face setting += 'font-face:%s;'%face
@ -809,7 +824,7 @@ class Processor(Parser):
css = '\n'.join(['.%s {%s;}'%(cn, setting) for \ css = '\n'.join(['.%s {%s;}'%(cn, setting) for \
setting, cn in cache.items()]) setting, cn in cache.items()])
sheet = self.css_parser.parseString(self.preprocess_css(css)) sheet = self.css_parser.parseString(self.preprocess_css(css.replace(';;}', ';}')))
for rule in sheet: for rule in sheet:
self.stylesheet.add(rule) self.stylesheet.add(rule)
css = '' css = ''
@ -875,7 +890,7 @@ def option_parser():
%prog [options] file.html|opf %prog [options] file.html|opf
Follow all links in an HTML file and collect them into the specified directory. Follow all links in an HTML file and collect them into the specified directory.
Also collects any references resources like images, stylesheets, scripts, etc. Also collects any resources like images, stylesheets, scripts, etc.
If an OPF file is specified instead, the list of files in its <spine> element If an OPF file is specified instead, the list of files in its <spine> element
is used. is used.
''')) '''))

View File

@ -108,6 +108,8 @@ def option_parser(usage, gui_mode=False):
help=_('Add a header to all the pages with title and author.')) help=_('Add a header to all the pages with title and author.'))
laf.add_option('--headerformat', default="%t by %a", dest='headerformat', type='string', laf.add_option('--headerformat', default="%t by %a", dest='headerformat', type='string',
help=_('Set the format of the header. %a is replaced by the author and %t by the title. Default is %default')) help=_('Set the format of the header. %a is replaced by the author and %t by the title. Default is %default'))
laf.add_option('--header-separation', default=0, type='int',
help=_('Add extra spacing below the header. Default is %default px.'))
laf.add_option('--override-css', default=None, dest='_override_css', type='string', laf.add_option('--override-css', default=None, dest='_override_css', type='string',
help=_('Override the CSS. Can be either a path to a CSS stylesheet or a string. If it is a string it is interpreted as CSS.')) help=_('Override the CSS. Can be either a path to a CSS stylesheet or a string. If it is a string it is interpreted as CSS.'))
laf.add_option('--use-spine', default=False, dest='use_spine', action='store_true', laf.add_option('--use-spine', default=False, dest='use_spine', action='store_true',
@ -260,10 +262,11 @@ def Book(options, logger, font_delta=0, header=None,
hb.append(header) hb.append(header)
hdr.PutObj(hb) hdr.PutObj(hb)
ps['headheight'] = profile.header_height ps['headheight'] = profile.header_height
ps['headsep'] = options.header_separation
ps['header'] = hdr ps['header'] = hdr
ps['topmargin'] = 0 ps['topmargin'] = 0
ps['textheight'] = profile.screen_height - (options.bottom_margin + ps['topmargin']) \ ps['textheight'] = profile.screen_height - (options.bottom_margin + ps['topmargin']) \
- ps['headheight'] - profile.fudge - ps['headheight'] - ps['headsep'] - profile.fudge
fontsize = int(10*profile.font_size+font_delta*20) fontsize = int(10*profile.font_size+font_delta*20)
baselineskip = fontsize + 20 baselineskip = fontsize + 20

View File

@ -2,14 +2,14 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys, shutil, logging import os, sys, shutil, logging
from tempfile import mkdtemp
from calibre.ebooks.lrf import option_parser as lrf_option_parser from calibre.ebooks.lrf import option_parser as lrf_option_parser
from calibre.ebooks import ConversionError, DRMError from calibre.ebooks import ConversionError, DRMError
from calibre.ebooks.lrf.html.convert_from import process_file as html_process_file from calibre.ebooks.lrf.html.convert_from import process_file as html_process_file
from calibre.ebooks.metadata.opf import OPF from calibre.ebooks.metadata.opf import OPF
from calibre.ebooks.metadata.epub import OCFDirReader from calibre.ebooks.metadata.epub import OCFDirReader
from calibre.utils.zipfile import ZipFile from calibre.utils.zipfile import ZipFile
from calibre import __appname__, setup_cli_handlers from calibre import setup_cli_handlers
from calibre.ptempfile import PersistentTemporaryDirectory
def option_parser(): def option_parser():
@ -22,17 +22,16 @@ _('''Usage: %prog [options] mybook.epub
def generate_html(pathtoepub, logger): def generate_html(pathtoepub, logger):
if not os.access(pathtoepub, os.R_OK): if not os.access(pathtoepub, os.R_OK):
raise ConversionError, 'Cannot read from ' + pathtoepub raise ConversionError('Cannot read from ' + pathtoepub)
tdir = mkdtemp(prefix=__appname__+'_') tdir = PersistentTemporaryDirectory('_epub2lrf')
os.rmdir(tdir) #os.rmdir(tdir)
try: try:
ZipFile(pathtoepub).extractall(tdir) ZipFile(pathtoepub).extractall(tdir)
if os.path.exists(os.path.join(tdir, 'META-INF', 'encryption.xml')):
raise DRMError(os.path.basename(pathtoepub))
except: except:
if os.path.exists(tdir) and os.path.isdir(tdir):
shutil.rmtree(tdir)
raise ConversionError, '.epub extraction failed' raise ConversionError, '.epub extraction failed'
if os.path.exists(os.path.join(tdir, 'META-INF', 'encryption.xml')):
raise DRMError(os.path.basename(pathtoepub))
return tdir return tdir
def process_file(path, options, logger=None): def process_file(path, options, logger=None):

View File

@ -12,24 +12,61 @@ except ImportError:
''' '''
Default fonts used in the PRS500 Default fonts used in the PRS500
''' '''
from calibre.ebooks.lrf.fonts.prs500 import tt0003m_, tt0011m_, tt0419m_
SYSTEM_FONT_PATH = '/usr/share/fonts/truetype/ttf-liberation/'
FONT_MAP = { FONT_MAP = {
'Swis721 BT Roman' : tt0003m_, 'Swis721 BT Roman' : 'tt0003m_',
'Dutch801 Rm BT Roman' : tt0011m_, 'Dutch801 Rm BT Roman' : 'tt0011m_',
'Courier10 BT Roman' : tt0419m_, 'Courier10 BT Roman' : 'tt0419m_',
} }
LIBERATION_FONT_MAP = {
'Swis721 BT Roman' : 'LiberationSans_Regular',
'Dutch801 Rm BT Roman' : 'LiberationSerif_Regular',
'Courier10 BT Roman' : 'LiberationMono_Regular',
}
SYSTEM_FONT_MAP = {}
for key, val in LIBERATION_FONT_MAP.items():
SYSTEM_FONT_MAP[key] = SYSTEM_FONT_PATH + val.replace('_', '-') + '.ttf'
FONT_FILE_MAP = {} FONT_FILE_MAP = {}
def get_font_path(name): def get_font_path(name):
if FONT_FILE_MAP.has_key(name) and os.access(FONT_FILE_MAP[name].name, os.R_OK): if FONT_FILE_MAP.has_key(name) and os.access(FONT_FILE_MAP[name].name, os.R_OK):
return FONT_FILE_MAP[name].name return FONT_FILE_MAP[name].name
p = PersistentTemporaryFile('.ttf', 'font_')
p.write(FONT_MAP[name].font_data)
p.close()
FONT_FILE_MAP[name] = p
return p.name
# translate font into file name
fname = FONT_MAP[name]
# first, check configuration in /etc/
etc_file = os.path.join(os.path.sep, 'etc', 'calibre', 'fonts', fname + '.ttf')
if os.access(etc_file, os.R_OK):
return etc_file
# then, try calibre shipped ones
try:
try:
font_mod = __import__('calibre.ebooks.lrf.fonts.prs500', {}, {},
[fname], -1)
except ImportError:
font_mod = __import__('calibre.ebooks.lrf.fonts.liberation', {}, {},
[LIBERATION_FONT_MAP[name]], -1)
p = PersistentTemporaryFile('.ttf', 'font_')
p.write(getattr(font_mod, fname).font_data)
p.close()
FONT_FILE_MAP[name] = p
return p.name
except ImportError:
pass
# finally, try system default ones
if SYSTEM_FONT_MAP.has_key(name) and os.access(SYSTEM_FONT_MAP[name], os.R_OK):
return SYSTEM_FONT_MAP[name]
# not found
raise SystemError, 'font %s (in file %s) not installed' % (name, fname)
def get_font(name, size, encoding='unic'): def get_font(name, size, encoding='unic'):
''' '''

View File

@ -1906,6 +1906,8 @@ def process_file(path, options, logger=None):
fpb = re.compile(options.force_page_break, re.IGNORECASE) if options.force_page_break else \ fpb = re.compile(options.force_page_break, re.IGNORECASE) if options.force_page_break else \
re.compile('$') re.compile('$')
cq = options.chapter_attr.split(',') cq = options.chapter_attr.split(',')
if len(cq) < 3:
raise ValueError('The --chapter-attr setting must have 2 commas.')
options.chapter_attr = [re.compile(cq[0], re.IGNORECASE), cq[1], options.chapter_attr = [re.compile(cq[0], re.IGNORECASE), cq[1],
re.compile(cq[2], re.IGNORECASE)] re.compile(cq[2], re.IGNORECASE)]
options.force_page_break = fpb options.force_page_break = fpb
@ -1933,7 +1935,7 @@ def process_file(path, options, logger=None):
oname = os.path.abspath(os.path.expanduser(oname)) oname = os.path.abspath(os.path.expanduser(oname))
conv.writeto(oname, lrs=options.lrs) conv.writeto(oname, lrs=options.lrs)
run_plugins_on_postprocess(oname, 'lrf') run_plugins_on_postprocess(oname, 'lrf')
logger.info('Output written to %s', oname) conv.log_info('Output written to %s', oname)
conv.cleanup() conv.cleanup()
return oname return oname
@ -1980,17 +1982,7 @@ def try_opf(path, options, logger):
PILImage.open(cover) PILImage.open(cover)
options.cover = cover options.cover = cover
except: except:
for prefix in opf.possible_cover_prefixes(): pass
if options.cover:
break
for suffix in ['.jpg', '.jpeg', '.gif', '.png', '.bmp']:
cpath = os.path.join(os.path.dirname(path), prefix+suffix)
try:
PILImage.open(cpath)
options.cover = cpath
break
except:
continue
if not getattr(options, 'cover', None) and orig_cover is not None: if not getattr(options, 'cover', None) and orig_cover is not None:
options.cover = orig_cover options.cover = orig_cover
if getattr(opf, 'spine', False): if getattr(opf, 'spine', False):

View File

@ -77,7 +77,7 @@ class LRFDocument(LRFMetaFile):
for obj in self.image_map.values() + self.font_map.values(): for obj in self.image_map.values() + self.font_map.values():
open(obj.file, 'wb').write(obj.stream) open(obj.file, 'wb').write(obj.stream)
def to_xml(self): def to_xml(self, write_files=True):
bookinfo = u'<BookInformation>\n<Info version="1.1">\n<BookInfo>\n' bookinfo = u'<BookInformation>\n<Info version="1.1">\n<BookInfo>\n'
bookinfo += u'<Title reading="%s">%s</Title>\n'%(self.metadata.title_reading, self.metadata.title) bookinfo += u'<Title reading="%s">%s</Title>\n'%(self.metadata.title_reading, self.metadata.title)
bookinfo += u'<Author reading="%s">%s</Author>\n'%(self.metadata.author_reading, self.metadata.author) bookinfo += u'<Author reading="%s">%s</Author>\n'%(self.metadata.author_reading, self.metadata.author)
@ -89,9 +89,10 @@ class LRFDocument(LRFMetaFile):
bookinfo += u'<FreeText reading="">%s</FreeText>\n</BookInfo>\n<DocInfo>\n'%(self.metadata.free_text,) bookinfo += u'<FreeText reading="">%s</FreeText>\n</BookInfo>\n<DocInfo>\n'%(self.metadata.free_text,)
th = self.doc_info.thumbnail th = self.doc_info.thumbnail
if th: if th:
prefix = sanitize_file_name(self.metadata.title) prefix = sanitize_file_name(self.metadata.title, as_unicode=True)
bookinfo += u'<CThumbnail file="%s" />\n'%(prefix+'_thumbnail.'+self.doc_info.thumbnail_extension,) bookinfo += u'<CThumbnail file="%s" />\n'%(prefix+'_thumbnail.'+self.doc_info.thumbnail_extension,)
open(prefix+'_thumbnail.'+self.doc_info.thumbnail_extension, 'wb').write(th) if write_files:
open(prefix+'_thumbnail.'+self.doc_info.thumbnail_extension, 'wb').write(th)
bookinfo += u'<Language reading="">%s</Language>\n'%(self.doc_info.language,) bookinfo += u'<Language reading="">%s</Language>\n'%(self.doc_info.language,)
bookinfo += u'<Creator reading="">%s</Creator>\n'%(self.doc_info.creator,) bookinfo += u'<Creator reading="">%s</Creator>\n'%(self.doc_info.creator,)
bookinfo += u'<Producer reading="">%s</Producer>\n'%(self.doc_info.producer,) bookinfo += u'<Producer reading="">%s</Producer>\n'%(self.doc_info.producer,)
@ -127,12 +128,16 @@ class LRFDocument(LRFMetaFile):
objects += unicode(obj) objects += unicode(obj)
styles += '</Style>\n' styles += '</Style>\n'
objects += '</Objects>\n' objects += '</Objects>\n'
self.write_files() if write_files:
self.write_files()
return '<BBeBXylog version="1.0">\n' + bookinfo + pages + styles + objects + '</BBeBXylog>' return '<BBeBXylog version="1.0">\n' + bookinfo + pages + styles + objects + '</BBeBXylog>'
def option_parser(): def option_parser():
parser = OptionParser(usage=_('%prog book.lrf\nConvert an LRF file into an LRS (XML UTF-8 encoded) file')) parser = OptionParser(usage=_('%prog book.lrf\nConvert an LRF file into an LRS (XML UTF-8 encoded) file'))
parser.add_option('--output', '-o', default=None, help=_('Output LRS file'), dest='out') parser.add_option('--output', '-o', default=None, help=_('Output LRS file'), dest='out')
parser.add_option('--dont-output-resources', default=True, action='store_false',
help=_('Do not save embedded image and font files to disk'),
dest='output_resources')
parser.add_option('--verbose', default=False, action='store_true', dest='verbose') parser.add_option('--verbose', default=False, action='store_true', dest='verbose')
return parser return parser
@ -154,7 +159,7 @@ def main(args=sys.argv, logger=None):
d = LRFDocument(open(args[1], 'rb')) d = LRFDocument(open(args[1], 'rb'))
d.parse() d.parse()
logger.info(_('Creating XML...')) logger.info(_('Creating XML...'))
o.write(d.to_xml()) o.write(d.to_xml(write_files=opts.output_resources))
logger.info(_('LRS written to ')+opts.out) logger.info(_('LRS written to ')+opts.out)
return 0 return 0

View File

@ -603,12 +603,18 @@ Show/edit the metadata in an LRF file.\n\n'''),
parser.add_option("--get-thumbnail", action="store_true", \ parser.add_option("--get-thumbnail", action="store_true", \
dest="get_thumbnail", default=False, \ dest="get_thumbnail", default=False, \
help=_("Extract thumbnail from LRF file")) help=_("Extract thumbnail from LRF file"))
parser.add_option('--publisher', default=None, help=_('Set the publisher'))
parser.add_option('--classification', default=None, help=_('Set the book classification'))
parser.add_option('--creator', default=None, help=_('Set the book creator'))
parser.add_option('--producer', default=None, help=_('Set the book producer'))
parser.add_option('--get-cover', action='store_true', default=False, parser.add_option('--get-cover', action='store_true', default=False,
help=_('Extract cover from LRF file. Note that the LRF format has no defined cover, so we use some heuristics to guess the cover.')) help=_('Extract cover from LRF file. Note that the LRF format has no defined cover, so we use some heuristics to guess the cover.'))
parser.add_option('--bookid', action='store', type='string', default=None, parser.add_option('--bookid', action='store', type='string', default=None,
dest='book_id', help=_('Set book ID')) dest='book_id', help=_('Set book ID'))
parser.add_option("-p", "--page", action="store", type="string", \ # The SumPage element specifies the number of "View"s (visible pages for the BookSetting element conditions) of the content.
dest="page", help=_("Don't know what this is for")) # Basically, the total pages per the page size, font size, etc. when the LRF is first created. Since this will change as the book is reflowed, it is probably not worth using.
#parser.add_option("-p", "--page", action="store", type="string", \
# dest="page", help=_("Don't know what this is for"))
return parser return parser
@ -624,6 +630,8 @@ def set_metadata(stream, mi):
lrf.free_text = mi.comments lrf.free_text = mi.comments
if mi.author_sort: if mi.author_sort:
lrf.author_reading = mi.author_sort lrf.author_reading = mi.author_sort
if mi.publisher:
lrf.publisher = mi.publisher
def main(args=sys.argv): def main(args=sys.argv):
@ -644,10 +652,16 @@ def main(args=sys.argv):
lrf.author_reading = options.author_reading lrf.author_reading = options.author_reading
if options.author: if options.author:
lrf.author = options.author lrf.author = options.author
if options.publisher:
lrf.publisher = options.publisher
if options.classification:
lrf.classification = options.classification
if options.category: if options.category:
lrf.category = options.category lrf.category = options.category
if options.page: if options.creator:
lrf.page = options.page lrf.creator = options.creator
if options.producer:
lrf.producer = options.producer
if options.thumbnail: if options.thumbnail:
path = os.path.expanduser(os.path.expandvars(options.thumbnail)) path = os.path.expanduser(os.path.expandvars(options.thumbnail))
f = open(path, "rb") f = open(path, "rb")

View File

@ -10,9 +10,10 @@ Try to read metadata from an HTML file.
import re import re
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.chardet import xml_to_unicode
def get_metadata(stream): def get_metadata(stream):
src = stream.read() src = xml_to_unicode(stream.read())[0]
# Title # Title
title = None title = None

View File

@ -418,7 +418,8 @@ class OPF(object):
tags_path = XPath('descendant::*[re:match(name(), "subject", "i")]') tags_path = XPath('descendant::*[re:match(name(), "subject", "i")]')
isbn_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+ isbn_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+
'(re:match(@scheme, "isbn", "i") or re:match(@opf:scheme, "isbn", "i"))]') '(re:match(@scheme, "isbn", "i") or re:match(@opf:scheme, "isbn", "i"))]')
application_id_path= XPath('descendant::*[re:match(name(), "identifier", "i") and '+ identifier_path = XPath('descendant::*[re:match(name(), "identifier", "i")]')
application_id_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+
'(re:match(@opf:scheme, "calibre|libprs500", "i") or re:match(@scheme, "calibre|libprs500", "i"))]') '(re:match(@opf:scheme, "calibre|libprs500", "i") or re:match(@scheme, "calibre|libprs500", "i"))]')
manifest_path = XPath('descendant::*[re:match(name(), "manifest", "i")]/*[re:match(name(), "item", "i")]') manifest_path = XPath('descendant::*[re:match(name(), "manifest", "i")]/*[re:match(name(), "item", "i")]')
manifest_ppath = XPath('descendant::*[re:match(name(), "manifest", "i")]') manifest_ppath = XPath('descendant::*[re:match(name(), "manifest", "i")]')
@ -719,6 +720,27 @@ class OPF(object):
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def guess_cover(self):
'''
Try to guess a cover. Needed for some old/badly formed OPF files.
'''
if self.base_dir and os.path.exists(self.base_dir):
for item in self.identifier_path(self.metadata):
scheme = None
for key in item.attrib.keys():
if key.endswith('scheme'):
scheme = item.get(key)
break
if scheme is None:
continue
if item.text:
prefix = item.text.replace('-', '')
for suffix in ['.jpg', '.jpeg', '.gif', '.png', '.bmp']:
cpath = os.access(os.path.join(self.base_dir, prefix+suffix), os.R_OK)
if os.access(os.path.join(self.base_dir, prefix+suffix), os.R_OK):
return cpath
@apply @apply
def cover(): def cover():
@ -728,6 +750,10 @@ class OPF(object):
for item in self.guide: for item in self.guide:
if item.type.lower() == t: if item.type.lower() == t:
return item.path return item.path
try:
return self.guess_cover()
except:
pass
def fset(self, path): def fset(self, path):
if self.guide is not None: if self.guide is not None:

View File

@ -22,6 +22,7 @@ from calibre.ebooks.mobi.langcodes import main_language, sub_language
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf import OPFCreator from calibre.ebooks.metadata.opf import OPFCreator
from calibre.ebooks.metadata.toc import TOC from calibre.ebooks.metadata.toc import TOC
from calibre import sanitize_file_name
class EXTHHeader(object): class EXTHHeader(object):
@ -200,7 +201,8 @@ class MobiReader(object):
guide = soup.find('guide') guide = soup.find('guide')
for elem in soup.findAll(['metadata', 'guide']): for elem in soup.findAll(['metadata', 'guide']):
elem.extract() elem.extract()
htmlfile = os.path.join(output_dir, self.name+'.html') htmlfile = os.path.join(output_dir,
sanitize_file_name(self.name)+'.html')
try: try:
for ref in guide.findAll('reference', href=True): for ref in guide.findAll('reference', href=True):
ref['href'] = os.path.basename(htmlfile)+ref['href'] ref['href'] = os.path.basename(htmlfile)+ref['href']
@ -232,6 +234,15 @@ class MobiReader(object):
def cleanup_soup(self, soup): def cleanup_soup(self, soup):
if self.verbose: if self.verbose:
print 'Replacing height, width and align attributes' print 'Replacing height, width and align attributes'
size_map = {
'xx-small' : '0.5',
'x-small' : '1',
'small' : '2',
'medium' : '3',
'large' : '4',
'x-large' : '5',
'xx-large' : '6',
}
for tag in soup.recursiveChildGenerator(): for tag in soup.recursiveChildGenerator():
if not isinstance(tag, Tag): continue if not isinstance(tag, Tag): continue
styles = [] styles = []
@ -257,6 +268,15 @@ class MobiReader(object):
if styles: if styles:
tag['style'] = '; '.join(styles) tag['style'] = '; '.join(styles)
if tag.name.lower() == 'font':
sz = tag.get('size', '')
try:
float(sz)
except ValueError:
sz = sz.lower()
if sz in size_map.keys():
tag['size'] = size_map[sz]
def create_opf(self, htmlfile, guide=None): def create_opf(self, htmlfile, guide=None):
mi = self.book_header.exth.mi mi = self.book_header.exth.mi
opf = OPFCreator(os.path.dirname(htmlfile), mi) opf = OPFCreator(os.path.dirname(htmlfile), mi)
@ -289,7 +309,8 @@ class MobiReader(object):
except: except:
text = '' text = ''
text = ent_pat.sub(entity_to_unicode, text) text = ent_pat.sub(entity_to_unicode, text)
tocobj.add_item(toc.partition('#')[0], a['href'][1:], text) if a['href'].startswith('#'):
tocobj.add_item(toc.partition('#')[0], a['href'][1:], text)
if tocobj is not None: if tocobj is not None:
opf.set_toc(tocobj) opf.set_toc(tocobj)

View File

@ -20,7 +20,7 @@ from calibre.ebooks.epub.iterator import is_supported
from calibre.library import server_config from calibre.library import server_config
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \ from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \
disable_plugin, customize_plugin, \ disable_plugin, customize_plugin, \
plugin_customization, add_plugin plugin_customization, add_plugin, remove_plugin
class PluginModel(QAbstractItemModel): class PluginModel(QAbstractItemModel):
@ -186,7 +186,6 @@ class ConfigDialog(QDialog, Ui_Dialog):
single_format = config['save_to_disk_single_format'] single_format = config['save_to_disk_single_format']
self.single_format.setCurrentIndex(BOOK_EXTENSIONS.index(single_format)) self.single_format.setCurrentIndex(BOOK_EXTENSIONS.index(single_format))
self.cover_browse.setValue(config['cover_flow_queue_length']) self.cover_browse.setValue(config['cover_flow_queue_length'])
self.confirm_delete.setChecked(config['confirm_delete'])
from calibre.translations.compiled import translations from calibre.translations.compiled import translations
from calibre.translations import language_codes from calibre.translations import language_codes
from calibre.startup import get_lang from calibre.startup import get_lang
@ -242,6 +241,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.plugin_view.setModel(self._plugin_model) self.plugin_view.setModel(self._plugin_model)
self.connect(self.toggle_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='toggle')) self.connect(self.toggle_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='toggle'))
self.connect(self.customize_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='customize')) self.connect(self.customize_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='customize'))
self.connect(self.remove_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='remove'))
self.connect(self.button_plugin_browse, SIGNAL('clicked()'), self.find_plugin) self.connect(self.button_plugin_browse, SIGNAL('clicked()'), self.find_plugin)
self.connect(self.button_plugin_add, SIGNAL('clicked()'), self.add_plugin) self.connect(self.button_plugin_add, SIGNAL('clicked()'), self.add_plugin)
@ -287,6 +287,13 @@ class ConfigDialog(QDialog, Ui_Dialog):
if ok: if ok:
customize_plugin(plugin, unicode(text)) customize_plugin(plugin, unicode(text))
self._plugin_model.refresh_plugin(plugin) self._plugin_model.refresh_plugin(plugin)
if op == 'remove':
if remove_plugin(plugin):
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_()
def up_column(self): def up_column(self):
@ -385,7 +392,6 @@ class ConfigDialog(QDialog, Ui_Dialog):
config['column_map'] = cols config['column_map'] = cols
config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()] config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()]
config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked()) config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked())
config['confirm_delete'] = bool(self.confirm_delete.isChecked())
pattern = self.filename_pattern.commit() pattern = self.filename_pattern.commit()
prefs['filename_pattern'] = pattern prefs['filename_pattern'] = pattern
p = {0:'normal', 1:'high', 2:'low'}[self.priority.currentIndex()] p = {0:'normal', 1:'high', 2:'low'}[self.priority.currentIndex()]

View File

@ -127,17 +127,10 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="confirm_delete" >
<property name="text" >
<string>Ask for &amp;confirmation before deleting files</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QCheckBox" name="pdf_metadata" > <widget class="QCheckBox" name="pdf_metadata" >
<property name="toolTip" > <property name="toolTip" >
<string>If you disable this setting, metadatas is guessed from the filename instead. This can be configured in the Advanced section.</string> <string>If you disable this setting, metadata is guessed from the filename instead. This can be configured in the Advanced section.</string>
</property> </property>
<property name="text" > <property name="text" >
<string>Read &amp;metadata from files</string> <string>Read &amp;metadata from files</string>
@ -811,6 +804,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="remove_plugin" >
<property name="text" >
<string>&amp;Remove plugin</string>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
<item> <item>

View File

@ -0,0 +1,31 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
from calibre.gui2 import dynamic
from calibre.gui2.dialogs.confirm_delete_ui import Ui_Dialog
from PyQt4.Qt import QDialog, SIGNAL
def _config_name(name):
return name + '_again'
class Dialog(QDialog, Ui_Dialog):
def __init__(self, msg, name, parent):
QDialog.__init__(self, parent)
self.setupUi(self)
self.msg.setText(msg)
self.name = name
self.connect(self.again, SIGNAL('stateChanged(int)'), self.toggle)
def toggle(self, x):
dynamic[_config_name(self.name)] = self.again.isChecked()
def confirm(msg, name, parent=None):
if not dynamic.get(_config_name(name), True):
return True
d = Dialog(msg, name, parent)
return d.exec_() == d.Accepted

View File

@ -0,0 +1,100 @@
<ui version="4.0" >
<class>Dialog</class>
<widget class="QDialog" name="Dialog" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>439</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle" >
<string>Are you sure?</string>
</property>
<property name="windowIcon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/dialog_warning.svg</normaloff>:/images/dialog_warning.svg</iconset>
</property>
<layout class="QGridLayout" name="gridLayout" >
<item row="0" column="0" >
<layout class="QHBoxLayout" name="horizontalLayout" >
<item>
<widget class="QLabel" name="label" >
<property name="pixmap" >
<pixmap resource="../images.qrc" >:/images/dialog_warning.svg</pixmap>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="msg" >
<property name="text" >
<string>TextLabel</string>
</property>
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0" >
<widget class="QCheckBox" name="again" >
<property name="text" >
<string>&amp;Show this warning again</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0" >
<widget class="QDialogButtonBox" name="buttonBox" >
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons" >
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../images.qrc" />
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel" >
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel" >
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel" >
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel" >
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -6,7 +6,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>866</width> <width>866</width>
<height>671</height> <height>679</height>
</rect> </rect>
</property> </property>
<property name="windowTitle" > <property name="windowTitle" >
@ -115,7 +115,7 @@
<item row="0" column="0" > <item row="0" column="0" >
<widget class="QStackedWidget" name="stack" > <widget class="QStackedWidget" name="stack" >
<property name="currentIndex" > <property name="currentIndex" >
<number>0</number> <number>1</number>
</property> </property>
<widget class="QWidget" name="metadata_page" > <widget class="QWidget" name="metadata_page" >
<layout class="QHBoxLayout" > <layout class="QHBoxLayout" >
@ -652,7 +652,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0" > <item row="2" column="0" >
<widget class="QLabel" name="label_10" > <widget class="QLabel" name="label_10" >
<property name="text" > <property name="text" >
<string>&amp;Header format:</string> <string>&amp;Header format:</string>
@ -662,9 +662,26 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1" > <item row="2" column="1" >
<widget class="QLineEdit" name="gui_headerformat" /> <widget class="QLineEdit" name="gui_headerformat" />
</item> </item>
<item row="1" column="1" >
<widget class="QSpinBox" name="gui_header_separation" >
<property name="suffix" >
<string> px</string>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="QLabel" name="label_29" >
<property name="text" >
<string>Header &amp;separation:</string>
</property>
<property name="buddy" >
<cstring>gui_header_separation</cstring>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -985,8 +1002,8 @@
<string>&lt;!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <string>&lt;!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
&lt;html>&lt;head>&lt;meta name="qrichtext" content="1" />&lt;style type="text/css"> &lt;html>&lt;head>&lt;meta name="qrichtext" content="1" />&lt;style type="text/css">
p, li { white-space: pre-wrap; } p, li { white-space: pre-wrap; }
&lt;/style>&lt;/head>&lt;body style=" font-family:'DejaVu Sans'; font-size:10pt; font-weight:400; font-style:normal;"> &lt;/style>&lt;/head>&lt;body style=" font-family:'Sans Serif'; font-size:10pt; font-weight:400; font-style:normal;">
&lt;p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">&lt;/p>&lt;/body>&lt;/html></string> &lt;p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'DejaVu Sans';">&lt;/p>&lt;/body>&lt;/html></string>
</property> </property>
</widget> </widget>
</item> </item>
@ -1078,12 +1095,12 @@ p, li { white-space: pre-wrap; }
<slot>setDisabled(bool)</slot> <slot>setDisabled(bool)</slot>
<hints> <hints>
<hint type="sourcelabel" > <hint type="sourcelabel" >
<x>650</x> <x>308</x>
<y>122</y> <y>74</y>
</hint> </hint>
<hint type="destinationlabel" > <hint type="destinationlabel" >
<x>788</x> <x>308</x>
<y>140</y> <y>74</y>
</hint> </hint>
</hints> </hints>
</connection> </connection>
@ -1094,12 +1111,12 @@ p, li { white-space: pre-wrap; }
<slot>setDisabled(bool)</slot> <slot>setDisabled(bool)</slot>
<hints> <hints>
<hint type="sourcelabel" > <hint type="sourcelabel" >
<x>543</x> <x>308</x>
<y>122</y> <y>74</y>
</hint> </hint>
<hint type="destinationlabel" > <hint type="destinationlabel" >
<x>544</x> <x>308</x>
<y>211</y> <y>74</y>
</hint> </hint>
</hints> </hints>
</connection> </connection>
@ -1110,12 +1127,12 @@ p, li { white-space: pre-wrap; }
<slot>setEnabled(bool)</slot> <slot>setEnabled(bool)</slot>
<hints> <hints>
<hint type="sourcelabel" > <hint type="sourcelabel" >
<x>298</x> <x>308</x>
<y>398</y> <y>74</y>
</hint> </hint>
<hint type="destinationlabel" > <hint type="destinationlabel" >
<x>660</x> <x>308</x>
<y>435</y> <y>74</y>
</hint> </hint>
</hints> </hints>
</connection> </connection>
@ -1126,12 +1143,12 @@ p, li { white-space: pre-wrap; }
<slot>setEnabled(bool)</slot> <slot>setEnabled(bool)</slot>
<hints> <hints>
<hint type="sourcelabel" > <hint type="sourcelabel" >
<x>330</x> <x>345</x>
<y>367</y> <y>363</y>
</hint> </hint>
<hint type="destinationlabel" > <hint type="destinationlabel" >
<x>823</x> <x>837</x>
<y>372</y> <y>435</y>
</hint> </hint>
</hints> </hints>
</connection> </connection>
@ -1142,12 +1159,28 @@ p, li { white-space: pre-wrap; }
<slot>setDisabled(bool)</slot> <slot>setDisabled(bool)</slot>
<hints> <hints>
<hint type="sourcelabel" > <hint type="sourcelabel" >
<x>344</x> <x>308</x>
<y>107</y> <y>74</y>
</hint> </hint>
<hint type="destinationlabel" > <hint type="destinationlabel" >
<x>489</x> <x>308</x>
<y>465</y> <y>74</y>
</hint>
</hints>
</connection>
<connection>
<sender>gui_header</sender>
<signal>toggled(bool)</signal>
<receiver>gui_header_separation</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel" >
<x>261</x>
<y>346</y>
</hint>
<hint type="destinationlabel" >
<x>379</x>
<y>378</y>
</hint> </hint>
</hints> </hints>
</connection> </connection>

View File

@ -7,7 +7,7 @@ add/remove formats
import os import os
from PyQt4.QtCore import SIGNAL, QObject, QCoreApplication, Qt from PyQt4.QtCore import SIGNAL, QObject, QCoreApplication, Qt
from PyQt4.QtGui import QPixmap, QListWidgetItem, QErrorMessage, QDialog from PyQt4.QtGui import QPixmap, QListWidgetItem, QErrorMessage, QDialog, QCompleter
from calibre.gui2 import qstring_to_unicode, error_dialog, file_icon_provider, \ from calibre.gui2 import qstring_to_unicode, error_dialog, file_icon_provider, \
@ -20,6 +20,7 @@ from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata import authors_to_sort_string, string_to_authors, authors_to_string from calibre.ebooks.metadata import authors_to_sort_string, string_to_authors, authors_to_string
from calibre.ebooks.metadata.library_thing import login, cover_from_isbn, LibraryThingError from calibre.ebooks.metadata.library_thing import login, cover_from_isbn, LibraryThingError
from calibre import islinux from calibre import islinux
from calibre.ebooks.metadata.meta import get_metadata
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.customize.ui import run_plugins_on_import from calibre.customize.ui import run_plugins_on_import
@ -32,6 +33,13 @@ class Format(QListWidgetItem):
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext), QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
text, parent, QListWidgetItem.UserType) text, parent, QListWidgetItem.UserType)
class AuthorCompleter(QCompleter):
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(QDialog, Ui_MetadataSingleDialog): class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
def do_reset_cover(self, *args): def do_reset_cover(self, *args):
@ -102,6 +110,39 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
self.formats.takeItem(row.row()) self.formats.takeItem(row.row())
self.formats_changed = True self.formats_changed = True
def set_cover(self):
row = self.formats.currentRow()
fmt = self.formats.item(row)
ext = fmt.ext.lower()
if fmt.path is None:
stream = self.db.format(self.row, ext, as_file=True)
else:
stream = open(fmt.path, 'r+b')
try:
mi = get_metadata(stream, ext)
except:
error_dialog(self, _('Could not read metadata'),
_('Could not read metadata from %s format')%ext).exec_()
return
cdata = None
if mi.cover and os.access(mi.cover, os.R_OK):
cdata = open(mi.cover).read()
elif mi.cover_data[1] is not None:
cdata = mi.cover_data[1]
if cdata is None:
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'),
_('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): def sync_formats(self):
old_extensions, new_extensions, paths = set(), set(), {} old_extensions, new_extensions, paths = set(), set(), {}
for row in range(self.formats.count()): for row in range(self.formats.count()):
@ -137,6 +178,8 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
self.cover_changed = False self.cover_changed = False
self.cpixmap = None self.cpixmap = None
self.cover.setAcceptDrops(True) self.cover.setAcceptDrops(True)
self._author_completer = AuthorCompleter(self.db)
self.authors.setCompleter(self._author_completer)
self.connect(self.cover, SIGNAL('cover_changed()'), self.cover_dropped) self.connect(self.cover, SIGNAL('cover_changed()'), self.cover_dropped)
QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), \ QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), \
self.select_cover) self.select_cover)
@ -155,6 +198,7 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
self.remove_unused_series) self.remove_unused_series)
QObject.connect(self.auto_author_sort, SIGNAL('clicked()'), QObject.connect(self.auto_author_sort, SIGNAL('clicked()'),
self.deduce_author_sort) self.deduce_author_sort)
self.connect(self.button_set_cover, SIGNAL('clicked()'), self.set_cover)
self.connect(self.reset_cover, SIGNAL('clicked()'), self.do_reset_cover) self.connect(self.reset_cover, SIGNAL('clicked()'), self.do_reset_cover)
self.connect(self.swap_button, SIGNAL('clicked()'), self.swap_title_author) self.connect(self.swap_button, SIGNAL('clicked()'), self.swap_title_author)
self.timeout = float(prefs['network_timeout']) self.timeout = float(prefs['network_timeout'])
@ -171,8 +215,6 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
self.authors.setText('') self.authors.setText('')
aus = self.db.author_sort(row) aus = self.db.author_sort(row)
self.author_sort.setText(aus if aus else '') self.author_sort.setText(aus if aus else '')
pub = self.db.publisher(row)
self.publisher.setText(pub if pub else '')
tags = self.db.tags(row) tags = self.db.tags(row)
self.tags.setText(tags if tags else '') self.tags.setText(tags if tags else '')
rating = self.db.rating(row) rating = self.db.rating(row)
@ -191,7 +233,8 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
size = self.db.sizeof_format(row, ext) size = self.db.sizeof_format(row, ext)
Format(self.formats, ext, size) Format(self.formats, ext, size)
self.initialize_series()
self.initialize_series_and_publisher()
self.series_index.setValue(self.db.series_index(row)) self.series_index.setValue(self.db.series_index(row))
QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.enable_series_index) QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.enable_series_index)
@ -224,7 +267,7 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
def cover_dropped(self): def cover_dropped(self):
self.cover_changed = True self.cover_changed = True
def initialize_series(self): def initialize_series_and_publisher(self):
all_series = self.db.all_series() all_series = self.db.all_series()
all_series.sort(cmp=lambda x, y : cmp(x[1], y[1])) all_series.sort(cmp=lambda x, y : cmp(x[1], y[1]))
series_id = self.db.series_id(self.row) series_id = self.db.series_id(self.row)
@ -248,6 +291,22 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
l.invalidate() l.invalidate()
l.activate() l.activate()
all_publishers = self.db.all_publishers()
all_publishers.sort(cmp=lambda x, y : cmp(x[1], y[1]))
publisher_id = self.db.publisher_id(self.row)
idx, c = None, 0
for i in all_publishers:
id, name = i
if id == publisher_id:
idx = c
self.publisher.addItem(name)
c += 1
self.publisher.setEditText('')
if idx is not None:
self.publisher.setCurrentIndex(idx)
self.layout().activate() self.layout().activate()
def edit_tags(self): def edit_tags(self):
@ -302,7 +361,7 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
isbn = qstring_to_unicode(self.isbn.text()) isbn = qstring_to_unicode(self.isbn.text())
title = qstring_to_unicode(self.title.text()) title = qstring_to_unicode(self.title.text())
author = string_to_authors(unicode(self.authors.text()))[0] author = string_to_authors(unicode(self.authors.text()))[0]
publisher = qstring_to_unicode(self.publisher.text()) publisher = qstring_to_unicode(self.publisher.currentText())
if isbn or title or author or publisher: if isbn or title or author or publisher:
d = FetchMetadata(self, isbn, title, author, publisher, self.timeout) d = FetchMetadata(self, isbn, title, author, publisher, self.timeout)
d.exec_() d.exec_()
@ -312,7 +371,7 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
self.title.setText(book.title) self.title.setText(book.title)
self.authors.setText(authors_to_string(book.authors)) self.authors.setText(authors_to_string(book.authors))
if book.author_sort: self.author_sort.setText(book.author_sort) if book.author_sort: self.author_sort.setText(book.author_sort)
if book.publisher: self.publisher.setText(book.publisher) if book.publisher: self.publisher.setEditText(book.publisher)
if book.isbn: self.isbn.setText(book.isbn) if book.isbn: self.isbn.setText(book.isbn)
summ = book.comments summ = book.comments
if summ: if summ:
@ -351,7 +410,7 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
self.db.set_author_sort(self.id, aus, notify=False) self.db.set_author_sort(self.id, aus, notify=False)
self.db.set_isbn(self.id, qstring_to_unicode(self.isbn.text()), notify=False) self.db.set_isbn(self.id, qstring_to_unicode(self.isbn.text()), notify=False)
self.db.set_rating(self.id, 2*self.rating.value(), notify=False) self.db.set_rating(self.id, 2*self.rating.value(), notify=False)
self.db.set_publisher(self.id, qstring_to_unicode(self.publisher.text()), notify=False) self.db.set_publisher(self.id, qstring_to_unicode(self.publisher.currentText()), notify=False)
self.db.set_tags(self.id, qstring_to_unicode(self.tags.text()).split(','), notify=False) self.db.set_tags(self.id, qstring_to_unicode(self.tags.text()).split(','), notify=False)
self.db.set_series(self.id, qstring_to_unicode(self.series.currentText()), notify=False) self.db.set_series(self.id, qstring_to_unicode(self.series.currentText()), notify=False)
self.db.set_series_index(self.id, self.series_index.value(), notify=False) self.db.set_series_index(self.id, self.series_index.value(), notify=False)

View File

@ -95,13 +95,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1" >
<widget class="QLineEdit" name="authors" >
<property name="toolTip" >
<string>Change the author(s) of this book. Multiple authors should be separated by an &amp;. If the author name contains an &amp;, use &amp;&amp; to represent it.</string>
</property>
</widget>
</item>
<item row="2" column="0" > <item row="2" column="0" >
<widget class="QLabel" name="label_8" > <widget class="QLabel" name="label_8" >
<property name="text" > <property name="text" >
@ -111,7 +104,7 @@
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property> </property>
<property name="buddy" > <property name="buddy" >
<cstring>authors</cstring> <cstring>author_sort</cstring>
</property> </property>
</widget> </widget>
</item> </item>
@ -185,13 +178,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1" colspan="2" >
<widget class="QLineEdit" name="publisher" >
<property name="toolTip" >
<string>Change the publisher of this book</string>
</property>
</widget>
</item>
<item row="5" column="0" > <item row="5" column="0" >
<widget class="QLabel" name="label_4" > <widget class="QLabel" name="label_4" >
<property name="text" > <property name="text" >
@ -330,6 +316,16 @@
<item row="8" column="1" colspan="2" > <item row="8" column="1" colspan="2" >
<widget class="QLineEdit" name="isbn" /> <widget class="QLineEdit" name="isbn" />
</item> </item>
<item row="4" column="1" >
<widget class="QComboBox" name="publisher" >
<property name="editable" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1" >
<widget class="QLineEdit" name="authors" />
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -370,7 +366,7 @@
<layout class="QVBoxLayout" name="verticalLayout" > <layout class="QVBoxLayout" name="verticalLayout" >
<item> <item>
<layout class="QGridLayout" name="gridLayout" > <layout class="QGridLayout" name="gridLayout" >
<item rowspan="2" row="0" column="0" > <item rowspan="3" row="0" column="0" >
<widget class="QListWidget" name="formats" > <widget class="QListWidget" name="formats" >
<property name="sizePolicy" > <property name="sizePolicy" >
<sizepolicy vsizetype="Minimum" hsizetype="Minimum" > <sizepolicy vsizetype="Minimum" hsizetype="Minimum" >
@ -412,7 +408,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1" > <item row="2" column="1" >
<widget class="QToolButton" name="remove_format_button" > <widget class="QToolButton" name="remove_format_button" >
<property name="toolTip" > <property name="toolTip" >
<string>Remove the selected formats for this book from the database.</string> <string>Remove the selected formats for this book from the database.</string>
@ -432,6 +428,26 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1" >
<widget class="QToolButton" name="button_set_cover" >
<property name="toolTip" >
<string>Set the cover for the book from the selected format</string>
</property>
<property name="text" >
<string>...</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/book.svg</normaloff>:/images/book.svg</iconset>
</property>
<property name="iconSize" >
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
</layout> </layout>
@ -595,8 +611,8 @@
<slot>accept()</slot> <slot>accept()</slot>
<hints> <hints>
<hint type="sourcelabel" > <hint type="sourcelabel" >
<x>257</x> <x>261</x>
<y>646</y> <y>710</y>
</hint> </hint>
<hint type="destinationlabel" > <hint type="destinationlabel" >
<x>157</x> <x>157</x>
@ -611,8 +627,8 @@
<slot>reject()</slot> <slot>reject()</slot>
<hints> <hints>
<hint type="sourcelabel" > <hint type="sourcelabel" >
<x>325</x> <x>329</x>
<y>646</y> <y>710</y>
</hint> </hint>
<hint type="destinationlabel" > <hint type="destinationlabel" >
<x>286</x> <x>286</x>

View File

@ -155,10 +155,11 @@ class RecipeModel(QAbstractListModel, SearchQueryParser):
return recipe return recipe
elif role == Qt.DecorationRole: elif role == Qt.DecorationRole:
icon = self.default_icon icon = self.default_icon
icon_path = (':/images/news/%s.png'%recipe.id).replace('recipe_', '')
if not recipe.builtin: if not recipe.builtin:
icon = self.custom_icon icon = self.custom_icon
elif QFile(':/images/news/%s.png'%recipe.id).exists(): elif QFile().exists(icon_path):
icon = QIcon(':/images/news/%s.png'%recipe.id) icon = QIcon(icon_path)
return QVariant(icon) return QVariant(icon)
return NONE return NONE

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 B

View File

@ -128,7 +128,10 @@ class JobManager(QAbstractTableModel):
self.emit(SIGNAL('layoutChanged()')) self.emit(SIGNAL('layoutChanged()'))
def _status_update(self, job): def _status_update(self, job):
row = self.jobs.index(job) try:
row = self.jobs.index(job)
except ValueError: # Job has been stopped
return
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'),
self.index(row, 0), self.index(row, 3)) self.index(row, 0), self.index(row, 3))

View File

@ -15,7 +15,8 @@ from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
from calibre import strftime from calibre import strftime
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.library.database2 import FIELD_MAP from calibre.library.database2 import FIELD_MAP
from calibre.gui2 import NONE, TableView, qstring_to_unicode, config from calibre.gui2 import NONE, TableView, qstring_to_unicode, config, \
error_dialog
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
class LibraryDelegate(QItemDelegate): class LibraryDelegate(QItemDelegate):
@ -657,6 +658,8 @@ class DeviceBooksView(BooksView):
self.rating_delegate = None self.rating_delegate = None
for i in range(10): for i in range(10):
self.setItemDelegateForColumn(i, self.itemDelegate()) self.setItemDelegateForColumn(i, self.itemDelegate())
self.setDragDropMode(self.NoDragDrop)
self.setAcceptDrops(False)
def resizeColumnsToContents(self): def resizeColumnsToContents(self):
QTableView.resizeColumnsToContents(self) QTableView.resizeColumnsToContents(self)
@ -668,6 +671,10 @@ class DeviceBooksView(BooksView):
def sortByColumn(self, col, order): def sortByColumn(self, col, order):
TableView.sortByColumn(self, col, order) TableView.sortByColumn(self, col, order)
def dropEvent(self, *args):
error_dialog(self, _('Not allowed'),
_('Dropping onto a device is not supported. First add the book to the calibre library.')).exec_()
class OnDeviceSearch(SearchQueryParser): class OnDeviceSearch(SearchQueryParser):
def __init__(self, model): def __init__(self, model):

View File

@ -3,11 +3,11 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys, textwrap, collections, traceback, time import os, sys, textwrap, collections, traceback, time
from xml.parsers.expat import ExpatError from xml.parsers.expat import ExpatError
from functools import partial from functools import partial
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer from PyQt4.Qt import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer, \
from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \ QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \
QToolButton, QDialog, QDesktopServices, QFileDialog, \ QToolButton, QDialog, QDesktopServices, QFileDialog, \
QSystemTrayIcon, QApplication, QKeySequence, QAction, \ QSystemTrayIcon, QApplication, QKeySequence, QAction, \
QProgressDialog QProgressDialog, QMessageBox
from PyQt4.QtSvg import QSvgRenderer from PyQt4.QtSvg import QSvgRenderer
from calibre import __version__, __appname__, islinux, sanitize_file_name, \ from calibre import __version__, __appname__, islinux, sanitize_file_name, \
@ -49,6 +49,7 @@ from calibre.library.database2 import LibraryDatabase2, CoverCache
from calibre.parallel import JobKilled from calibre.parallel import JobKilled
from calibre.utils.filenames import ascii_filename from calibre.utils.filenames import ascii_filename
from calibre.gui2.widgets import WarningDialog from calibre.gui2.widgets import WarningDialog
from calibre.gui2.dialogs.confirm_delete import confirm
class Main(MainWindow, Ui_MainWindow): class Main(MainWindow, Ui_MainWindow):
@ -187,8 +188,8 @@ class Main(MainWindow, Ui_MainWindow):
self.metadata_menu = md self.metadata_menu = md
self.add_menu = QMenu() self.add_menu = QMenu()
self.add_menu.addAction(_('Add books from a single directory')) self.add_menu.addAction(_('Add books from a single directory'))
self.add_menu.addAction(_('Add books recursively (One book per directory, assumes every ebook file is the same book in a different format)')) self.add_menu.addAction(_('Add books from directories, including sub-directories (One book per directory, assumes every ebook file is the same book in a different format)'))
self.add_menu.addAction(_('Add books recursively (Multiple books per directory, assumes every ebook file is a different book)')) self.add_menu.addAction(_('Add books from directories, including sub directories (Multiple books per directory, assumes every ebook file is a different book)'))
self.action_add.setMenu(self.add_menu) self.action_add.setMenu(self.add_menu)
QObject.connect(self.action_add, SIGNAL("triggered(bool)"), self.add_books) QObject.connect(self.action_add, SIGNAL("triggered(bool)"), self.add_books)
QObject.connect(self.add_menu.actions()[0], SIGNAL("triggered(bool)"), self.add_books) QObject.connect(self.add_menu.actions()[0], SIGNAL("triggered(bool)"), self.add_books)
@ -307,7 +308,6 @@ class Main(MainWindow, Ui_MainWindow):
db = LibraryDatabase2(self.library_path) db = LibraryDatabase2(self.library_path)
self.library_view.set_database(db) self.library_view.set_database(db)
if self.olddb is not None: if self.olddb is not None:
from PyQt4.QtGui import QProgressDialog
pd = QProgressDialog('', '', 0, 100, self) pd = QProgressDialog('', '', 0, 100, self)
pd.setWindowModality(Qt.ApplicationModal) pd.setWindowModality(Qt.ApplicationModal)
pd.setCancelButton(None) pd.setCancelButton(None)
@ -703,6 +703,7 @@ class Main(MainWindow, Ui_MainWindow):
self.upload_books(paths, list(map(sanitize_file_name, names)), infos, on_card=on_card) self.upload_books(paths, list(map(sanitize_file_name, names)), infos, on_card=on_card)
finally: finally:
progress.setValue(len(paths)) progress.setValue(len(paths))
progress.close()
def upload_books(self, files, names, metadata, on_card=False, memory=None): def upload_books(self, files, names, metadata, on_card=False, memory=None):
''' '''
@ -758,13 +759,9 @@ class Main(MainWindow, Ui_MainWindow):
rows = view.selectionModel().selectedRows() rows = view.selectionModel().selectedRows()
if not rows or len(rows) == 0: if not rows or len(rows) == 0:
return return
if config['confirm_delete']:
d = question_dialog(self, _('Confirm delete'),
_('Are you sure you want to delete these %d books?')%len(rows))
if d.exec_() != QMessageBox.Yes:
return
if self.stack.currentIndex() == 0: if self.stack.currentIndex() == 0:
if not confirm('<p>'+_('The selected books will be <b>permanently deleted</b> and the files removed from your computer. Are you sure?')+'</p>', 'library_delete_books', self):
return
view.model().delete_books(rows) view.model().delete_books(rows)
else: else:
view = self.memory_view if self.stack.currentIndex() == 1 else self.card_view view = self.memory_view if self.stack.currentIndex() == 1 else self.card_view
@ -801,6 +798,7 @@ class Main(MainWindow, Ui_MainWindow):
Edit metadata of selected books in library. Edit metadata of selected books in library.
''' '''
rows = self.library_view.selectionModel().selectedRows() rows = self.library_view.selectionModel().selectedRows()
previous = self.library_view.currentIndex()
if not rows or len(rows) == 0: if not rows or len(rows) == 0:
d = error_dialog(self, _('Cannot edit metadata'), _('No books selected')) d = error_dialog(self, _('Cannot edit metadata'), _('No books selected'))
d.exec_() d.exec_()
@ -817,6 +815,9 @@ class Main(MainWindow, Ui_MainWindow):
self.library_view.model().db, self.library_view.model().db,
accepted_callback=accepted) accepted_callback=accepted)
d.exec_() d.exec_()
if rows:
current = self.library_view.currentIndex()
self.library_view.model().current_changed(current, previous)
def edit_bulk_metadata(self, checked): def edit_bulk_metadata(self, checked):
''' '''
@ -1046,6 +1047,8 @@ class Main(MainWindow, Ui_MainWindow):
def convert_single(self, checked): def convert_single(self, checked):
r = self.get_books_for_conversion() r = self.get_books_for_conversion()
if r is None: return if r is None: return
previous = self.library_view.currentIndex()
rows = [x.row() for x in self.library_view.selectionModel().selectedRows()]
comics, others = r comics, others = r
jobs, changed = convert_single_ebook(self, self.library_view.model().db, comics, others) jobs, changed = convert_single_ebook(self, self.library_view.model().db, comics, others)
for func, args, desc, fmt, id, temp_files in jobs: for func, args, desc, fmt, id, temp_files in jobs:
@ -1054,8 +1057,9 @@ class Main(MainWindow, Ui_MainWindow):
self.conversion_jobs[job] = (temp_files, fmt, id) self.conversion_jobs[job] = (temp_files, fmt, id)
if changed: if changed:
self.library_view.model().resort(reset=False) self.library_view.model().refresh_rows(rows)
self.library_view.model().research() current = self.library_view.currentIndex()
self.library_view.model().current_changed(current, previous)
def book_converted(self, job): def book_converted(self, job):
temp_files, fmt, book_id = self.conversion_jobs.pop(job) temp_files, fmt, book_id = self.conversion_jobs.pop(job)
@ -1074,6 +1078,9 @@ class Main(MainWindow, Ui_MainWindow):
os.remove(f.name) os.remove(f.name)
except: except:
pass pass
if self.current_view() is self.library_view:
current = self.library_view.currentIndex()
self.library_view.model().current_changed(current, QModelIndex())
#############################View book###################################### #############################View book######################################
@ -1206,7 +1213,6 @@ class Main(MainWindow, Ui_MainWindow):
newloc = d.database_location newloc = d.database_location
if not os.path.exists(os.path.join(newloc, 'metadata.db')): if not os.path.exists(os.path.join(newloc, 'metadata.db')):
if os.access(self.library_path, os.R_OK): if os.access(self.library_path, os.R_OK):
from PyQt4.QtGui import QProgressDialog
pd = QProgressDialog('', '', 0, 100, self) pd = QProgressDialog('', '', 0, 100, self)
pd.setWindowModality(Qt.ApplicationModal) pd.setWindowModality(Qt.ApplicationModal)
pd.setCancelButton(None) pd.setCancelButton(None)

View File

@ -199,7 +199,10 @@ class StatusBar(QStatusBar):
ret = QStatusBar.showMessage(self, msg, timeout) ret = QStatusBar.showMessage(self, msg, timeout)
if self.systray is not None: if self.systray is not None:
if isosx and isinstance(msg, unicode): if isosx and isinstance(msg, unicode):
msg = msg.encode(preferred_encoding) try:
msg = msg.encode(preferred_encoding)
except UnicodeEncodeError:
msg = msg.encode('utf-8')
self.systray.showMessage('calibre', msg, self.systray.Information, 10000) self.systray.showMessage('calibre', msg, self.systray.Information, 10000)
return ret return ret

View File

@ -523,7 +523,10 @@ class DocumentView(QWebView):
self.manager.previous_document() self.manager.previous_document()
event.accept() event.accept()
return return
return QWebView.wheelEvent(self, event) ret = QWebView.wheelEvent(self, event)
if self.manager is not None:
self.manager.scrolled(self.scroll_fraction)
return ret
def keyPressEvent(self, event): def keyPressEvent(self, event):
key = event.key() key = event.key()

View File

@ -244,6 +244,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
lambda x:self.find(unicode(self.search.text()), True, repeat=True)) lambda x:self.find(unicode(self.search.text()), True, repeat=True))
self.connect(self.action_full_screen, SIGNAL('triggered(bool)'), self.connect(self.action_full_screen, SIGNAL('triggered(bool)'),
self.toggle_fullscreen) self.toggle_fullscreen)
self.action_full_screen.setShortcuts([Qt.Key_F11, Qt.CTRL+Qt.SHIFT+Qt.Key_F])
self.connect(self.action_back, SIGNAL('triggered(bool)'), self.back) self.connect(self.action_back, SIGNAL('triggered(bool)'), self.back)
self.connect(self.action_bookmark, SIGNAL('triggered(bool)'), self.bookmark) self.connect(self.action_bookmark, SIGNAL('triggered(bool)'), self.bookmark)
self.connect(self.action_forward, SIGNAL('triggered(bool)'), self.forward) self.connect(self.action_forward, SIGNAL('triggered(bool)'), self.forward)
@ -511,7 +512,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.metadata.show_opf(self.iterator.opf) self.metadata.show_opf(self.iterator.opf)
title = self.iterator.opf.title title = self.iterator.opf.title
if not title: if not title:
title = os.path.splitext(os.path.basename(pathtoebook)) title = os.path.splitext(os.path.basename(pathtoebook))[0]
self.action_table_of_contents.setDisabled(not self.iterator.toc) self.action_table_of_contents.setDisabled(not self.iterator.toc)
if self.iterator.toc: if self.iterator.toc:
self.toc_model = TOC(self.iterator.toc) self.toc_model = TOC(self.iterator.toc)

View File

@ -76,13 +76,23 @@ STANZA_TEMPLATE='''\
<entry> <entry>
<title>${record['title']}</title> <title>${record['title']}</title>
<id>urn:calibre:${record['id']}</id> <id>urn:calibre:${record['id']}</id>
<author><name>${record['authors']}</name></author> <author><name>${record['author_sort']}</name></author>
<updated>${record['timestamp'].strftime('%Y-%m-%dT%H:%M:%SZ')}</updated> <updated>${record['timestamp'].strftime('%Y-%m-%dT%H:%M:%SZ')}</updated>
<link type="application/epub+zip" href="${quote(record['fmt_epub'].replace(sep, '/')).replace('http%3A', 'http:')}" /> <link type="application/epub+zip" href="${quote(record['fmt_epub'].replace(sep, '/')).replace('http%3A', 'http:')}" />
<link py:if="record['cover']" rel="x-stanza-cover-image" type="image/png" href="${quote(record['cover'].replace(sep, '/')).replace('http%3A', 'http:')}" /> <link py:if="record['cover']" rel="x-stanza-cover-image" type="image/png" href="${quote(record['cover'].replace(sep, '/')).replace('http%3A', 'http:')}" />
<link py:if="record['cover']" rel="x-stanza-cover-image-thumbnail" type="image/png" href="${quote(record['cover'].replace(sep, '/')).replace('http%3A', 'http:')}" /> <link py:if="record['cover']" rel="x-stanza-cover-image-thumbnail" type="image/png" href="${quote(record['cover'].replace(sep, '/')).replace('http%3A', 'http:')}" />
<content type="xhtml"> <content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">${record['comments']}</div> <div xmlns="http://www.w3.org/1999/xhtml">
<py:for each="f in ('authors','publisher','rating','tags','series', 'isbn')">
<py:if test="record[f]">
${f.capitalize()}:${unicode(', '.join(record[f]) if f=='tags' else record[f])}<br/>
</py:if>
</py:for>
<py:if test="record['comments']">
<br/>
${record['comments']}
</py:if>
</div>
</content> </content>
</entry> </entry>
</py:for> </py:for>

View File

@ -944,6 +944,10 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
return self.conn.get('SELECT publisher FROM meta WHERE id=?', (index,), all=False) return self.conn.get('SELECT publisher FROM meta WHERE id=?', (index,), all=False)
return self.data[index][3] return self.data[index][3]
def publisher_id(self, index, index_is_id=False):
id = index if index_is_id else self.id(index)
return self.conn.get('SELECT publisher from books_publishers_link WHERE book=?', (id,), all=False)
def rating(self, index, index_is_id=False): def rating(self, index, index_is_id=False):
if index_is_id: if index_is_id:
return self.conn.get('SELECT rating FROM meta WHERE id=?', (index,), all=False) return self.conn.get('SELECT rating FROM meta WHERE id=?', (index,), all=False)
@ -1042,6 +1046,14 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
return [ (i[0], i[1]) for i in \ return [ (i[0], i[1]) for i in \
self.conn.get('SELECT id, name FROM series')] self.conn.get('SELECT id, name FROM series')]
def all_authors(self):
return [ (i[0], i[1]) for i in \
self.conn.get('SELECT id, name FROM authors')]
def all_publishers(self):
return [ (i[0], i[1]) for i in \
self.conn.get('SELECT id, name FROM publishers')]
def all_tags(self): def all_tags(self):
return [i[0].strip() for i in self.conn.get('SELECT name FROM tags') if i[0].strip()] return [i[0].strip() for i in self.conn.get('SELECT name FROM tags') if i[0].strip()]

View File

@ -21,13 +21,12 @@ from calibre.library.sqlite import connect, IntegrityError
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
from calibre.ebooks.metadata import string_to_authors, authors_to_string from calibre.ebooks.metadata import string_to_authors, authors_to_string
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.constants import preferred_encoding, iswindows, isosx from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.customize.ui import run_plugins_on_import from calibre.customize.ui import run_plugins_on_import
from calibre import sanitize_file_name
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
filesystem_encoding = sys.getfilesystemencoding()
if filesystem_encoding is None: filesystem_encoding = 'utf-8'
iscaseinsensitive = iswindows or isosx iscaseinsensitive = iswindows or isosx
def normpath(x): def normpath(x):
@ -37,23 +36,6 @@ def normpath(x):
x = x.lower() x = x.lower()
return x return x
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+\[\]/]')
def sanitize_file_name(name, substitute='_'):
'''
Sanitize the filename `name`. All invalid characters are replaced by `substitute`.
The set of invalid characters is the union of the invalid characters in Windows,
OS X and Linux. Also removes leading an trailing whitespace.
**WARNING:** This function also replaces path separators, so only pass file names
and not full paths to it.
*NOTE:* This function always returns byte strings, not unicode objects. The byte strings
are encoded in the filesystem encoding of the platform, or UTF-8.
'''
if isinstance(name, unicode):
name = name.encode(filesystem_encoding, 'ignore')
one = _filename_sanitize.sub(substitute, name)
one = re.sub(r'\s', ' ', one).strip()
return re.sub(r'^\.+$', '_', one)
FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'publisher':3, 'rating':4, 'timestamp':5, FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'publisher':3, 'rating':4, 'timestamp':5,
'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10, 'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10,
@ -489,8 +471,8 @@ class LibraryDatabase2(LibraryDatabase):
authors = self.authors(id, index_is_id=True) authors = self.authors(id, index_is_id=True)
if not authors: if not authors:
authors = _('Unknown') authors = _('Unknown')
author = sanitize_file_name(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding) author = sanitize_file_name(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding, 'replace')
title = sanitize_file_name(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding) title = sanitize_file_name(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding, 'replace')
name = title + ' - ' + author name = title + ' - ' + author
return name return name
@ -1118,8 +1100,13 @@ class LibraryDatabase2(LibraryDatabase):
continue continue
series_index = 1 if mi.series_index is None else mi.series_index series_index = 1 if mi.series_index is None else mi.series_index
aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors) aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
title = mi.title
if isinstance(aus, str):
aus = aus.decode(preferred_encoding, 'replace')
if isinstance(title, str):
title = title.decode(preferred_encoding)
obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)', obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)',
(mi.title, uri, series_index, aus)) (title, uri, series_index, aus))
id = obj.lastrowid id = obj.lastrowid
self.data.books_added([id], self.conn) self.data.books_added([id], self.conn)
ids.append(id) ids.append(id)

View File

@ -2,10 +2,10 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
''' Post installation script for linux ''' ''' Post installation script for linux '''
import sys, os, re, shutil import sys, os, re, shutil
from subprocess import check_call, call from subprocess import check_call, call
from calibre import __version__, __appname__ from tempfile import NamedTemporaryFile
from calibre import __version__, __appname__
from calibre.devices import devices from calibre.devices import devices
DEVICES = devices() DEVICES = devices()
@ -401,9 +401,9 @@ def install_man_pages(fatal_errors):
import subprocess import subprocess
print 'Installing MAN pages...' print 'Installing MAN pages...'
manpath = '/usr/share/man/man1' manpath = '/usr/share/man/man1'
f = open_file('/tmp/man_extra', 'wb') f = NamedTemporaryFile()
f.write('[see also]\nhttp://%s.kovidgoyal.net\n'%__appname__) f.write('[see also]\nhttp://%s.kovidgoyal.net\n'%__appname__)
f.close() f.flush()
manifest = [] manifest = []
os.environ['PATH'] += ':'+os.path.expanduser('~/bin') os.environ['PATH'] += ':'+os.path.expanduser('~/bin')
for src in entry_points['console_scripts']: for src in entry_points['console_scripts']:

View File

@ -143,6 +143,15 @@ Where are the book files stored?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When you first run |app|, it will ask you for a folder in which to store your books. Whenever you add a book to |app|, it will copy the book into that folder. Books in the folder are nicely arranged into sub-folders by Author and Title. Metadata about the books is stored in the file ``metadata.db`` (which is a sqlite database). When you first run |app|, it will ask you for a folder in which to store your books. Whenever you add a book to |app|, it will copy the book into that folder. Books in the folder are nicely arranged into sub-folders by Author and Title. Metadata about the books is stored in the file ``metadata.db`` (which is a sqlite database).
Why doesn't |app| let me store books in my own directory structure?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The whole point if |app|'s library management features is that they provide an interface for locating books that is *much* more efficient than any possible directory scheme you could come up with for your collection. Indeed, once you become comfortable using |app|'s interface to find, sort and browse your collection, you wont ever feel the need to hunt through the files on your disk to find a book again. By managing books in its own directory struture of Author -> Title -> Book files, |app| is able to achieve a high level of reliability and standardization.
Why doesn't |app| have a column for foo?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|app| is designed to have columns for the most frequently and widely used fields. If it does not have a coulmn for your favorite field, you can always add a tag to the book for that piece of information. |app| also supports a general purpose "comments" fields for longer items.
Content From The Web Content From The Web
--------------------- ---------------------

View File

@ -133,7 +133,7 @@ to the recipe. Finally, lets replace some of the :term:`CSS` that we disabled ea
With these additions, our recipe has become "production quality", indeed it is very close to the actual recipe used by |app| for the *BBC*, shown below: With these additions, our recipe has become "production quality", indeed it is very close to the actual recipe used by |app| for the *BBC*, shown below:
.. literalinclude:: ../web/feeds/recipes/bbc.py .. literalinclude:: ../web/feeds/recipes/recipe_bbc.py
This :term:`recipe` explores only the tip of the iceberg when it comes to the power of |app|. To explore more of the abilities of |app| we'll examine a more complex real life example in the next section. This :term:`recipe` explores only the tip of the iceberg when it comes to the power of |app|. To explore more of the abilities of |app| we'll examine a more complex real life example in the next section.

View File

@ -31,7 +31,7 @@ from threading import RLock, Thread, Event
from math import ceil from math import ceil
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre import iswindows, detect_ncpus, isosx from calibre import iswindows, detect_ncpus, isosx, preferred_encoding
from calibre.utils.config import prefs from calibre.utils.config import prefs
DEBUG = False DEBUG = False
@ -615,7 +615,9 @@ class Job(object):
self.log = unicode(self.log, 'utf-8', 'replace') self.log = unicode(self.log, 'utf-8', 'replace')
ans.extend(self.log.split('\n')) ans.extend(self.log.split('\n'))
return '<br>'.join(ans) ans = [x.decode(preferred_encoding, 'replace') if isinstance(x, 'str') else x for x in ans]
return u'<br>'.join(ans)
class ParallelJob(Job): class ParallelJob(Job):

View File

@ -21,7 +21,7 @@ from lxml import etree
def range_for_month(year, month): def range_for_month(year, month):
ty, tm = date.today().year, date.today().month ty, tm = date.today().year, date.today().month
min = date(year=year, month=month, day=1) min = max = date(year=year, month=month, day=1)
x = date.today().day if ty == year and tm == month else 31 x = date.today().day if ty == year and tm == month else 31
while x > 1: while x > 1:
try: try:
@ -102,6 +102,8 @@ class Stats:
def get_deviation(self, amounts): def get_deviation(self, amounts):
l = float(len(amounts)) l = float(len(amounts))
if l == 0:
return 0
mean = sum(amounts)/l mean = sum(amounts)/l
return sqrt( sum([i**2 for i in amounts])/l - mean**2 ) return sqrt( sum([i**2 for i in amounts])/l - mean**2 )
@ -199,7 +201,7 @@ class Server(object):
x = list(range(days-1, -1, -1)) x = list(range(days-1, -1, -1))
y = stats.daily_totals y = stats.daily_totals
ax.plot(x, y)#, align='center', width=20, color='g') ax.plot(x, y)#, align='center', width=20, color='g')
ax.set_xlabel('Day') ax.set_xlabel('Days ago')
ax.set_ylabel('Income ($)') ax.set_ylabel('Income ($)')
ax.hlines([stats.daily_average], 0, days-1) ax.hlines([stats.daily_average], 0, days-1)
ax.set_xlim([0, days-1]) ax.set_xlim([0, days-1])

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

View File

@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2008-12-25 21:21+0000\n" "X-Launchpad-Export-Date: 2008-12-30 05:49+0000\n"
"X-Generator: Launchpad (build Unknown)\n" "X-Generator: Launchpad (build Unknown)\n"
"X-Poedit-Country: RUSSIAN FEDERATION\n" "X-Poedit-Country: RUSSIAN FEDERATION\n"
"X-Poedit-Language: Russian\n" "X-Poedit-Language: Russian\n"

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

View File

@ -1,60 +1,25 @@
/* crypto/des/spr.h */ /*--[crypto/des/spr.h]---------------------------------------------------------
/* Copyright (C) 1995-1997 Eric Young (eay@cryptsoft.com) | Copyright (C) 2002 Dan A. Jackson
* All rights reserved. |
* | This file is part of the "openclit" library for processing .LIT files.
* This package is an SSL implementation written |
* by Eric Young (eay@cryptsoft.com). | "Openclit" is free software; you can redistribute it and/or modify
* The implementation was written so as to conform with Netscapes SSL. | it under the terms of the GNU General Public License as published by
* | the Free Software Foundation; either version 2 of the License, or
* This library is free for commercial and non-commercial use as long as | (at your option) any later version.
* the following conditions are aheared to. The following conditions |
* apply to all code found in this distribution, be it the RC4, RSA, | This program is distributed in the hope that it will be useful,
* lhash, DES, etc., code; not just the SSL code. The SSL documentation | but WITHOUT ANY WARRANTY; without even the implied warranty of
* included with this distribution is covered by the same copyright terms | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* except that the holder is Tim Hudson (tjh@cryptsoft.com). | GNU General Public License for more details.
* |
* Copyright remains Eric Young's, and as such any Copyright notices in | You should have received a copy of the GNU General Public License
* the code are not to be removed. | along with this program; if not, write to the Free Software
* If this package is used in a product, Eric Young should be given attribution | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
* as the author of the parts of the library used. |
* This can be in the form of a textual message at program startup or | The GNU General Public License may also be available at the following
* in documentation (online or textual) provided with the package. | URL: http://www.gnu.org/licenses/gpl.html
* */
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* "This product includes cryptographic software written by
* Eric Young (eay@cryptsoft.com)"
* The word 'cryptographic' can be left out if the rouines from the library
* being used are not cryptographic related :-).
* 4. If you include any Windows specific code (or a derivative thereof) from
* the apps directory (application code) you must include an acknowledgement:
* "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
*
* THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* The licence and distribution terms for any publically available version or
* derivative of this code cannot be changed. i.e. this code cannot simply be
* copied and put under another distribution licence
* [including the GNU Public Licence.]
*/
static unsigned long SP1[64] = { static unsigned long SP1[64] = {
0x02080800L, 0x00080000L, 0x02000002L, 0x02080802L, 0x02080800L, 0x00080000L, 0x02000002L, 0x02080802L,

View File

@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
''' '''
Builtin recipes. Builtin recipes.
''' '''
recipe_modules = [ recipe_modules = ['recipe_' + r for r in (
'newsweek', 'atlantic', 'economist', 'portfolio', 'newsweek', 'atlantic', 'economist', 'portfolio',
'nytimes', 'usatoday', 'outlook_india', 'bbc', 'greader', 'wsj', 'nytimes', 'usatoday', 'outlook_india', 'bbc', 'greader', 'wsj',
'wired', 'globe_and_mail', 'smh', 'espn', 'business_week', 'wired', 'globe_and_mail', 'smh', 'espn', 'business_week',
@ -19,8 +19,9 @@ recipe_modules = [
'clarin', 'financial_times', 'heise', 'le_monde', 'harpers', 'science_aas', 'clarin', 'financial_times', 'heise', 'le_monde', 'harpers', 'science_aas',
'science_news', 'the_nation', 'lrb', 'harpers_full', 'liberation', 'science_news', 'the_nation', 'lrb', 'harpers_full', 'liberation',
'linux_magazine', 'telegraph_uk', 'utne', 'sciencedaily', 'forbes', 'linux_magazine', 'telegraph_uk', 'utne', 'sciencedaily', 'forbes',
'time_magazine', 'endgadget', 'fudzilla', 'time_magazine', 'endgadget', 'fudzilla', 'nspm_int', 'nspm', 'pescanik',
] 'spiegel_int', 'themarketticker', 'tomshardware',
)]
import re, imp, inspect, time, os import re, imp, inspect, time, os
from calibre.web.feeds.news import BasicNewsRecipe, CustomIndexRecipe, AutomaticNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe, CustomIndexRecipe, AutomaticNewsRecipe

View File

@ -1,40 +0,0 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Darko Miletic <darko.miletic at gmail.com>'
'''
b92.net
'''
import string,re
from calibre.web.feeds.news import BasicNewsRecipe
class B92(BasicNewsRecipe):
title = u'B92'
__author__ = 'Darko Miletic'
description = 'Dnevne vesti iz Srbije i sveta'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
remove_tags_after = dict(name='div', attrs={'class':'gas'})
remove_tags = [
dict(name='div' , attrs={'class':'interaction clearfix' })
,dict(name='div' , attrs={'class':'gas' })
,dict(name='ul' , attrs={'class':'comment-nav' })
,dict(name='table', attrs={'class':'pages-navigation-form'})
]
feeds = [
(u'Vesti' , u'http://www.b92.net/info/rss/vesti.xml' )
,(u'Kultura' , u'http://www.b92.net/info/rss/kultura.xml' )
,(u'Automobili', u'http://www.b92.net/info/rss/automobili.xml')
,(u'Zivot' , u'http://www.b92.net/info/rss/zivot.xml' )
,(u'Tehnopolis', u'http://www.b92.net/info/rss/tehnopolis.xml')
,(u'Biz' , u'http://www.b92.net/info/rss/biz.xml' )
]
def print_version(self, url):
return url + '&version=print'

View File

@ -1,34 +0,0 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Darko Miletic <darko.miletic at gmail.com>'
'''
danas.rs
'''
import string,re
from calibre.web.feeds.news import BasicNewsRecipe
class Danas(BasicNewsRecipe):
title = u'Danas'
__author__ = 'Darko Miletic'
description = 'Vesti'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
timefmt = ' [%A, %d %B, %Y]'
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [ dict(name='div', attrs={'id':'left'}) ]
remove_tags_after = dict(name='div', attrs={'id':'comments'})
remove_tags = [
dict(name='div', attrs={'class':'width_1_4'})
,dict(name='div', attrs={'class':'metaClanka'})
,dict(name='div', attrs={'id':'comments'})
,dict(name='div', attrs={'class':'baner'})
]
feeds = [ (u'Vesti', u'http://www.danas.rs/rss/rss.asp')]

View File

@ -0,0 +1,45 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Darko Miletic <darko.miletic at gmail.com>'
'''
b92.net
'''
import string,re
from calibre.web.feeds.news import BasicNewsRecipe
class B92(BasicNewsRecipe):
title = u'B92'
__author__ = 'Darko Miletic'
description = 'Dnevne vesti iz Srbije i sveta'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
cover_url = 'http://static.b92.net/images/fp/logo.gif'
keep_only_tags = [ dict(name='div', attrs={'class':'sama_vest'}) ]
html2lrf_options = [
'--comment', description
, '--base-font-size', '10'
, '--category', 'news, Serbia'
, '--publisher', 'B92'
]
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
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

View File

@ -11,12 +11,18 @@ from calibre.web.feeds.news import BasicNewsRecipe
class Blic(BasicNewsRecipe): class Blic(BasicNewsRecipe):
title = u'Blic' title = u'Blic'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic'
description = 'Vesti' description = 'Blic.rs online verzija najtiraznije novine u Srbiji donosi najnovije vesti iz Srbije i sveta, komentare, politicke analize, poslovne i ekonomske vesti, vesti iz regiona, intervjue, informacije iz kulture, reportaze, pokriva sve sportske dogadjaje, detaljan tv program, nagradne igre, zabavu, fenomenalni Blic strip, dnevni horoskop, arhivu svih dogadjaja'
oldest_article = 7 oldest_article = 7
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
timefmt = ' [%A, %d %B, %Y]' cover_url = 'http://www.blic.rs/resources/images/header_back_tile.png'
html2lrf_options = [
'--comment', description
, '--base-font-size', '10'
, '--category', 'news, Serbia'
, '--publisher', 'Blic'
]
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')] preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]

View File

@ -9,14 +9,19 @@ clarin.com
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class Clarin(BasicNewsRecipe): class Clarin(BasicNewsRecipe):
title = u'Clarin' title = 'Clarin'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic'
description = 'Noticias de Argentina y mundo' description = 'Noticias de Argentina y mundo'
oldest_article = 2 oldest_article = 2
max_articles_per_feed = 100 max_articles_per_feed = 100
use_embedded_content = False use_embedded_content = False
simultaneous_downloads = 1 cover_url = 'http://www.clarin.com/shared/v10/img/Hd/lg_Clarin.gif'
delay = 1 html2lrf_options = [
'--comment', description
, '--base-font-size', '10'
, '--category', 'news, Argentina'
, '--publisher', 'Grupo Clarin'
]
remove_tags = [ remove_tags = [
dict(name='a' , attrs={'class':'Imp' }) dict(name='a' , attrs={'class':'Imp' })

View File

@ -0,0 +1,41 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Darko Miletic <darko.miletic at gmail.com>'
'''
danas.rs
'''
import string,re
from calibre.web.feeds.news import BasicNewsRecipe
class Danas(BasicNewsRecipe):
title = 'Danas'
__author__ = 'Darko Miletic'
description = 'Dnevne novine sa vestima iz sveta, politike, ekonomije, kulture, sporta, Beograda, Novog Sada i cele Srbije.'
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
cover_url = 'http://www.danas.rs/images/basic/danas.gif'
html2lrf_options = [
'--comment', description
, '--base-font-size', '10'
, '--category', 'news, Serbia'
, '--publisher', 'Danas'
]
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [ dict(name='div', attrs={'id':'left'}) ]
remove_tags = [
dict(name='div', attrs={'class':'width_1_4' })
,dict(name='div', attrs={'class':'metaClanka' })
,dict(name='div', attrs={'id':'comments' })
,dict(name='div', attrs={'class':'baner' })
,dict(name='div', attrs={'class':'slikaClanka'})
]
feeds = [(u'Vesti', u'http://www.danas.rs/rss/rss.asp')]
def print_version(self, url):
return url + '&action=print'

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