From b3271a0dede43f60c19014652850d4f6321ee11a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 20 Jan 2009 12:29:39 -0800 Subject: [PATCH 01/22] Remove comments, etc. from Newsweek download --- src/calibre/web/feeds/recipes/recipe_newsweek.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/calibre/web/feeds/recipes/recipe_newsweek.py b/src/calibre/web/feeds/recipes/recipe_newsweek.py index 9ad551c469..b4de7c5762 100644 --- a/src/calibre/web/feeds/recipes/recipe_newsweek.py +++ b/src/calibre/web/feeds/recipes/recipe_newsweek.py @@ -19,9 +19,16 @@ class Newsweek(BasicNewsRecipe): remove_tags = [ dict(name=['script', 'noscript']), - dict(name='div', attrs={'class':['ad', 'SocialLinks', 'SocialLinksDiv', 'channel', 'bot', 'nav', 'top', 'EmailArticleBlock']}), + dict(name='div', attrs={'class':['ad', 'SocialLinks', 'SocialLinksDiv', + 'channel', 'bot', 'nav', 'top', + 'EmailArticleBlock', + 'comments-and-social-links-wrapper', + 'inline-social-links-wrapper', + 'inline-social-links', + ]}), dict(name='div', attrs={'class':re.compile('box')}), - dict(id=['ToolBox', 'EmailMain', 'EmailArticle', ]) + dict(id=['ToolBox', 'EmailMain', 'EmailArticle', 'comment-box', + 'nw-comments']) ] recursions = 1 From 3ffa6bb88f52d3f93084024cf4faa8b5f86f12d4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 20 Jan 2009 12:41:39 -0800 Subject: [PATCH 02/22] Implement feeds2mobi --- src/calibre/__init__.py | 1 + src/calibre/ebooks/epub/from_html.py | 6 ++- src/calibre/ebooks/mobi/from_any.py | 9 ++-- src/calibre/ebooks/mobi/from_feeds.py | 74 +++++++++++++++++++++++++++ src/calibre/ebooks/mobi/writer.py | 62 +++++++++++----------- src/calibre/ebooks/oeb/stylizer.py | 7 ++- src/calibre/linux.py | 7 ++- src/calibre/web/feeds/main.py | 2 + src/calibre/web/feeds/news.py | 8 ++- 9 files changed, 136 insertions(+), 40 deletions(-) create mode 100644 src/calibre/ebooks/mobi/from_feeds.py diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index e8d4b61ce1..e11dec8688 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -20,6 +20,7 @@ import mechanize mimetypes.add_type('application/epub+zip', '.epub') mimetypes.add_type('text/x-sony-bbeb+xml', '.lrs') mimetypes.add_type('application/x-sony-bbeb', '.lrf') +mimetypes.add_type('application/x-dtbncx+xml', '.ncx') def to_unicode(raw, encoding='utf-8', errors='strict'): if isinstance(raw, unicode): diff --git a/src/calibre/ebooks/epub/from_html.py b/src/calibre/ebooks/epub/from_html.py index 664ac21de9..d61dc0051a 100644 --- a/src/calibre/ebooks/epub/from_html.py +++ b/src/calibre/ebooks/epub/from_html.py @@ -160,7 +160,11 @@ class HTMLProcessor(Processor, Rationalizer): br.text = u'\u00a0' if self.opts.profile.remove_object_tags: - for tag in self.root.xpath('//object|//embed'): + for tag in self.root.xpath('//embed'): + tag.getparent().remove(tag) + for tag in self.root.xpath('//object'): + if tag.get('type', '').lower().strip() in ('image/svg+xml',): + continue tag.getparent().remove(tag) def save(self): diff --git a/src/calibre/ebooks/mobi/from_any.py b/src/calibre/ebooks/mobi/from_any.py index 9af2e5fe68..5a60549cad 100644 --- a/src/calibre/ebooks/mobi/from_any.py +++ b/src/calibre/ebooks/mobi/from_any.py @@ -14,15 +14,18 @@ import sys, os, glob, logging from calibre.ebooks.epub.from_any import any2epub, formats, USAGE from calibre.ebooks.epub import config as common_config from calibre.ptempfile import TemporaryDirectory -from calibre.ebooks.mobi.writer import oeb2mobi, add_mobi_options +from calibre.ebooks.mobi.writer import oeb2mobi, config as mobi_config def config(defaults=None): - return common_config(defaults=defaults, name='mobi') + c = common_config(defaults=defaults, name='mobi') + c.remove_opt('profile') + mobic = mobi_config(defaults=defaults) + c.update(mobic) + return c def option_parser(usage=USAGE): usage = usage % ('Mobipocket', formats()) parser = config().option_parser(usage=usage) - add_mobi_options(parser) return parser def any2mobi(opts, path): diff --git a/src/calibre/ebooks/mobi/from_feeds.py b/src/calibre/ebooks/mobi/from_feeds.py new file mode 100644 index 0000000000..205550f730 --- /dev/null +++ b/src/calibre/ebooks/mobi/from_feeds.py @@ -0,0 +1,74 @@ +from __future__ import with_statement +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' + +''' +Convert feeds to MOBI ebook +''' + +import sys, glob, os +from calibre.web.feeds.main import config as feeds2disk_config, USAGE, run_recipe +from calibre.ebooks.mobi.writer import config as oeb2mobi_config, oeb2mobi +from calibre.ptempfile import TemporaryDirectory +from calibre import strftime, sanitize_file_name + +def config(defaults=None): + c = feeds2disk_config(defaults=defaults) + c.remove('lrf') + c.remove('epub') + c.remove('mobi') + c.remove('output_dir') + c.update(oeb2mobi_config(defaults=defaults)) + c.remove('encoding') + c.remove('source_profile') + c.add_opt('output', ['-o', '--output'], default=None, + help=_('Output file. Default is derived from input filename.')) + return c + +def option_parser(): + c = config() + return c.option_parser(usage=USAGE) + +def convert(opts, recipe_arg, notification=None): + opts.lrf = False + opts.epub = False + opts.mobi = True + if opts.debug: + opts.verbose = 2 + parser = option_parser() + with TemporaryDirectory('_feeds2mobi') as tdir: + opts.output_dir = tdir + recipe = run_recipe(opts, recipe_arg, parser, notification=notification) + c = config() + recipe_opts = c.parse_string(recipe.oeb2mobi_options) + c.smart_update(recipe_opts, opts) + opts = recipe_opts + opf = glob.glob(os.path.join(tdir, '*.opf')) + if not opf: + raise Exception('Downloading of recipe: %s failed'%recipe_arg) + opf = opf[0] + + if opts.output is None: + fname = recipe.title + strftime(recipe.timefmt) + '.mobi' + opts.output = os.path.join(os.getcwd(), sanitize_file_name(fname)) + + print 'Generating MOBI...' + opts.encoding = 'utf-8' + opts.source_profile = 'Browser' + oeb2mobi(opts, opf) + + +def main(args=sys.argv, notification=None, handler=None): + parser = option_parser() + opts, args = parser.parse_args(args) + if len(args) != 2 and opts.feeds is None: + parser.print_help() + return 1 + recipe_arg = args[1] if len(args) > 1 else None + convert(opts, recipe_arg, notification=notification) + + return 0 + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py index 3be283fa0a..1d9152c446 100644 --- a/src/calibre/ebooks/mobi/writer.py +++ b/src/calibre/ebooks/mobi/writer.py @@ -34,8 +34,7 @@ from calibre.ebooks.mobi.palmdoc import compress_doc from calibre.ebooks.mobi.langcodes import iana2mobi from calibre.ebooks.mobi.mobiml import MBP_NS, MBP, MobiMLizer from calibre.customize.ui import run_plugins_on_postprocess -from calibre.utils.config import OptionParser -from optparse import OptionGroup +from calibre.utils.config import Config, StringConfig # TODO: # - Allow override CSS (?) @@ -502,44 +501,45 @@ class MobiWriter(object): self._write(record) -def add_mobi_options(parser): - profiles = Context.PROFILES.keys() - profiles.sort() - profiles = ', '.join(profiles) - group = OptionGroup(parser, _('Mobipocket'), - _('Mobipocket-specific options.')) - group.add_option( - '-c', '--compress', default=False, action='store_true', - help=_('Compress file text using PalmDOC compression. ' +def config(defaults=None): + desc = _('Options to control the conversion to MOBI') + _profiles = list(sorted(Context.PROFILES.keys())) + if defaults is None: + c = Config('mobi', desc) + else: + c = StringConfig(defaults, desc) + + mobi = c.add_group('mobipocket', _('Mobipocket-specific options.')) + mobi('compress', ['--compress'], default=False, + help=_('Compress file text using PalmDOC compression. ' 'Results in smaller files, but takes a long time to run.')) - group.add_option( - '-r', '--rescale-images', default=False, action='store_true', + mobi('rescale_images', ['--rescale-images'], default=False, help=_('Modify images to meet Palm device size limitations.')) - group.add_option( - '--toc-title', default=None, action='store', - help=_('Title for any generated in-line table of contents.')) - parser.add_option_group(group) - group = OptionGroup(parser, _('Profiles'), _('Device renderer profiles. ' - 'Affects conversion of default font sizes and rasterization ' - 'resolution. Valid profiles are: %s.') % profiles) - group.add_option( - '--source-profile', default='Browser', metavar='PROFILE', - help=_("Source renderer profile. Default is 'Browser'.")) - group.add_option( - '--dest-profile', default='CybookG3', metavar='PROFILE', - help=_("Destination renderer profile. Default is 'CybookG3'.")) - parser.add_option_group(group) - return - + mobi('toc_title', ['--toc-title'], default=None, + help=_('Title for any generated in-line table of contents.')) + profiles = c.add_group('profiles', _('Device renderer profiles. ' + 'Affects conversion of font sizes, image rescaling and rasterization ' + 'of tables. Valid profiles are: %s.') % ', '.join(_profiles)) + profiles('source_profile', ['--source-profile'], + default='Browser', choices=_profiles, + help=_("Source renderer profile. Default is %default.")) + profiles('dest_profile', ['--dest-profile'], + default='CybookG3', choices=_profiles, + help=_("Destination renderer profile. Default is %default.")) + c.add_opt('encoding', ['--encoding'], default=None, + help=_('Character encoding for HTML files. Default is to auto detect.')) + return c + + def option_parser(): - parser = OptionParser(usage=_('%prog [options] OPFFILE')) + c = config() + parser = c.option_parser(usage='%prog '+_('[options]')+' file.opf') parser.add_option( '-o', '--output', default=None, help=_('Output file. Default is derived from input filename.')) parser.add_option( '-v', '--verbose', default=0, action='count', help=_('Useful for debugging.')) - add_mobi_options(parser) return parser def oeb2mobi(opts, inpath): diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 8668d89975..6549c8eccd 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -223,8 +223,11 @@ class Stylizer(object): for key in composition: style[key] = 'inherit' else: - primitives = [v.cssText for v in cssvalue] - primitites.reverse() + try: + primitives = [v.cssText for v in cssvalue] + except TypeError: + primitives = [cssvalue.cssText] + primitives.reverse() value = primitives.pop() for key in composition: if cssproperties.cssvalues[key](value): diff --git a/src/calibre/linux.py b/src/calibre/linux.py index be5864033a..acd7e0b1bd 100644 --- a/src/calibre/linux.py +++ b/src/calibre/linux.py @@ -40,6 +40,7 @@ entry_points = { 'calibre-server = calibre.library.server:main', 'feeds2lrf = calibre.ebooks.lrf.feeds.convert_from:main', 'feeds2epub = calibre.ebooks.epub.from_feeds:main', + 'feeds2mobi = calibre.ebooks.mobi.from_feeds:main', 'web2lrf = calibre.ebooks.lrf.web.convert_from:main', 'pdf2lrf = calibre.ebooks.lrf.pdf.convert_from:main', 'mobi2lrf = calibre.ebooks.lrf.mobi.convert_from:main', @@ -189,6 +190,7 @@ def setup_completion(fatal_errors): from calibre.ebooks.html import option_parser as html2oeb from calibre.ebooks.odt.to_oeb import option_parser as odt2oeb from calibre.ebooks.epub.from_feeds import option_parser as feeds2epub + from calibre.ebooks.mobi.from_feeds import option_parser as feeds2mobi from calibre.ebooks.epub.from_any import option_parser as any2epub from calibre.ebooks.lit.from_any import option_parser as any2lit from calibre.ebooks.epub.from_comic import option_parser as comic2epub @@ -219,7 +221,7 @@ def setup_completion(fatal_errors): f.write(opts_and_exts('any2epub', any2epub, any_formats)) f.write(opts_and_exts('any2lit', any2lit, any_formats)) f.write(opts_and_exts('any2mobi', any2mobi, any_formats)) - f.write(opts_and_exts('oeb2mobi', oeb2mobi, ['mobi', 'prc'])) + f.write(opts_and_exts('oeb2mobi', oeb2mobi, ['opf'])) f.write(opts_and_exts('lrf2lrs', lrf2lrsop, ['lrf'])) f.write(opts_and_exts('lrf-meta', metaop, ['lrf'])) f.write(opts_and_exts('rtf-meta', metaop, ['rtf'])) @@ -239,7 +241,8 @@ def setup_completion(fatal_errors): f.write(opts_and_exts('comic2pdf', comic2epub, ['cbz', 'cbr'])) f.write(opts_and_words('feeds2disk', feeds2disk, feed_titles)) f.write(opts_and_words('feeds2lrf', feeds2lrf, feed_titles)) - f.write(opts_and_words('feeds2lrf', feeds2epub, feed_titles)) + f.write(opts_and_words('feeds2epub', feeds2epub, feed_titles)) + f.write(opts_and_words('feeds2mobi', feeds2mobi, feed_titles)) f.write(opts_and_exts('html2epub', html2epub, ['html', 'htm', 'xhtm', 'xhtml', 'opf'])) f.write(opts_and_exts('html2oeb', html2oeb, ['html', 'htm', 'xhtm', 'xhtml'])) f.write(opts_and_exts('odt2oeb', odt2oeb, ['odt'])) diff --git a/src/calibre/web/feeds/main.py b/src/calibre/web/feeds/main.py index ebc2c937ed..faa132bef4 100644 --- a/src/calibre/web/feeds/main.py +++ b/src/calibre/web/feeds/main.py @@ -45,6 +45,8 @@ If you specify this option, any argument to %prog is ignored and a default recip help='Optimize fetching for subsequent conversion to LRF.') c.add_opt('epub', ['--epub'], default=False, action='store_true', help='Optimize fetching for subsequent conversion to EPUB.') + c.add_opt('mobi', ['--mobi'], default=False, action='store_true', + help='Optimize fetching for subsequent conversion to MOBI.') c.add_opt('recursions', ['--recursions'], default=0, help=_('Number of levels of links to follow on webpages that are linked to from feeds. Defaul %default')) c.add_opt('output_dir', ['--output-dir'], default='.', diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index 6da6383210..bc7b2e62bf 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -20,7 +20,7 @@ from PyQt4.QtWebKit import QWebPage from calibre import browser, __appname__, iswindows, LoggingInterface, \ strftime, __version__, preferred_encoding from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString, CData, Tag -from calibre.ebooks.metadata.opf import OPFCreator +from calibre.ebooks.metadata.opf2 import OPFCreator from calibre.ebooks.lrf import entity_to_unicode from calibre.ebooks.metadata.toc import TOC from calibre.ebooks.metadata import MetaInformation @@ -152,6 +152,8 @@ class BasicNewsRecipe(object, LoggingInterface): #: Options to pass to html2epub to customize generation of EPUB ebooks. html2epub_options = '' + #: Options to pass to oeb2mobi to customize generation of MOBI ebooks. + oeb2mobi_options = '' #: List of tags to be removed. Specified tags are removed from downloaded HTML. #: A tag is specified as a dictionary of the form:: @@ -876,6 +878,7 @@ class BasicNewsRecipe(object, LoggingInterface): manifest = [os.path.join(dir, 'feed_%d'%i) for i in range(len(feeds))] manifest.append(os.path.join(dir, 'index.html')) + manifest.append(os.path.join(dir, 'index.ncx')) cpath = getattr(self, 'cover_path', None) if cpath is None: pf = PersistentTemporaryFile('_recipe_cover.jpg') @@ -885,6 +888,9 @@ class BasicNewsRecipe(object, LoggingInterface): opf.cover = cpath manifest.append(cpath) opf.create_manifest_from_files_in(manifest) + for mani in opf.manifest: + if mani.path.endswith('.ncx'): + mani.id = 'ncx' entries = ['index.html'] toc = TOC(base_path=dir) From 1fa831703d4264786684ad28adb37507e9b6f281 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 20 Jan 2009 13:56:27 -0800 Subject: [PATCH 03/22] IGN:... --- src/calibre/devices/usbms/device.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index ef2800b1cc..f5c176bcdb 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -188,8 +188,8 @@ class Device(_Device): if not drives: raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.') % self.__class__.__name__) - self._main_prefix = drives['main'] if 'main' in names.keys() else None - self._card_prefix = drives['card'] if 'card' in names.keys() else None + self._main_prefix = drives.get('main', None) + self._card_prefix = drives.get('card', None) @classmethod def get_osx_mountpoints(self, raw=None): @@ -197,7 +197,8 @@ class Device(_Device): ioreg = '/usr/sbin/ioreg' if not os.access(ioreg, os.X_OK): ioreg = 'ioreg' - raw = subprocess.Popen((ioreg+' -w 0 -S -c IOMedia').split(), stdout=subprocess.PIPE).stdout.read() + raw = subprocess.Popen((ioreg+' -w 0 -S -c IOMedia').split(), + stdout=subprocess.PIPE).stdout.read() lines = raw.splitlines() names = {} From 276d58e1d976239eb436ddc99465ffe21e2753a9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 20 Jan 2009 14:23:40 -0800 Subject: [PATCH 04/22] IGN:Various usbms cleanups --- src/calibre/devices/cybookg3/driver.py | 1 + src/calibre/devices/usbms/device.py | 4 ++-- src/calibre/devices/usbms/driver.py | 16 +++++++++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/cybookg3/driver.py b/src/calibre/devices/cybookg3/driver.py index ea6376df0d..4ab6dc297b 100644 --- a/src/calibre/devices/cybookg3/driver.py +++ b/src/calibre/devices/cybookg3/driver.py @@ -9,6 +9,7 @@ from itertools import cycle from calibre.devices.usbms.driver import USBMS import calibre.devices.cybookg3.t2b as t2b +from calibre.devices.errors import FreeSpaceError class CYBOOKG3(USBMS): # Ordered list of supported formats diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index f5c176bcdb..d85fdef29a 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -3,10 +3,10 @@ __copyright__ = '2009, John Schember ' ''' Generic device driver. This is not a complete stand alone driver. It is intended to be subclassed with the relevant parts implemented for a particular -device. This class handles devive detection. +device. This class handles device detection. ''' -import os, subprocess, time +import os, subprocess, time, re from calibre.devices.interface import Device as _Device from calibre.devices.errors import DeviceError diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 75f04219e7..eb02408aef 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -11,9 +11,23 @@ from itertools import cycle from calibre.devices.usbms.device import Device from calibre.devices.usbms.books import BookList, Book -from calibre.devices.errors import FreeSpaceError +from calibre.devices.errors import FreeSpaceError, PathError from calibre.devices.mime import MIME_MAP +class File(object): + def __init__(self, path): + stats = os.stat(path) + self.is_dir = os.path.isdir(path) + self.is_readonly = not os.access(path, os.W_OK) + self.ctime = stats.st_ctime + self.wtime = stats.st_mtime + self.size = stats.st_size + if path.endswith(os.sep): + path = path[:-1] + self.path = path + self.name = os.path.basename(path) + + class USBMS(Device): FORMATS = [] EBOOK_DIR_MAIN = '' From f91028fed0be345189d9e179c9eceb1c7a6098c4 Mon Sep 17 00:00:00 2001 From: John Schember Date: Tue, 20 Jan 2009 18:28:58 -0500 Subject: [PATCH 05/22] Fix typos --- src/calibre/devices/usbms/device.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index ef2800b1cc..c9a9c2a942 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -6,7 +6,7 @@ intended to be subclassed with the relevant parts implemented for a particular device. This class handles devive detection. ''' -import os, subprocess, time +import os, re, subprocess, time from calibre.devices.interface import Device as _Device from calibre.devices.errors import DeviceError @@ -188,8 +188,8 @@ class Device(_Device): if not drives: raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.') % self.__class__.__name__) - self._main_prefix = drives['main'] if 'main' in names.keys() else None - self._card_prefix = drives['card'] if 'card' in names.keys() else None + self._main_prefix = drives['main'] if 'main' in drives.keys() else None + self._card_prefix = drives['card'] if 'card' in drives.keys() else None @classmethod def get_osx_mountpoints(self, raw=None): From eba26565b8b9eb60e7bcbe9db36536b285f3545d Mon Sep 17 00:00:00 2001 From: John Schember Date: Tue, 20 Jan 2009 18:33:49 -0500 Subject: [PATCH 06/22] Fix errors found by pylint --- src/calibre/devices/cybookg3/driver.py | 1 + src/calibre/devices/usbms/driver.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/cybookg3/driver.py b/src/calibre/devices/cybookg3/driver.py index ea6376df0d..66e6097371 100644 --- a/src/calibre/devices/cybookg3/driver.py +++ b/src/calibre/devices/cybookg3/driver.py @@ -7,6 +7,7 @@ Device driver for Bookeen's Cybook Gen 3 import os, shutil from itertools import cycle +from calibre.devices.errors import FreeSpaceError from calibre.devices.usbms.driver import USBMS import calibre.devices.cybookg3.t2b as t2b diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 75f04219e7..1edf406d0b 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -11,9 +11,22 @@ from itertools import cycle from calibre.devices.usbms.device import Device from calibre.devices.usbms.books import BookList, Book -from calibre.devices.errors import FreeSpaceError +from calibre.devices.errors import FreeSpaceError, PathError from calibre.devices.mime import MIME_MAP +class File(object): + def __init__(self, path): + stats = os.stat(path) + self.is_dir = os.path.isdir(path) + self.is_readonly = not os.access(path, os.W_OK) + self.ctime = stats.st_ctime + self.wtime = stats.st_mtime + self.size = stats.st_size + if path.endswith(os.sep): + path = path[:-1] + self.path = path + self.name = os.path.basename(path) + class USBMS(Device): FORMATS = [] EBOOK_DIR_MAIN = '' From 68fd289b9f28151b73aa116efeb26cb38dbe05e0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 20 Jan 2009 16:34:42 -0800 Subject: [PATCH 07/22] IGN:... --- .pydevproject | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.pydevproject b/.pydevproject index fba36e6fb7..b6d22db5e1 100644 --- a/.pydevproject +++ b/.pydevproject @@ -2,8 +2,9 @@ -python 2.5 +python 2.6 /calibre/src +Default From bf50a850f4f5315f4a9d3b0b9bff38f62bc5de51 Mon Sep 17 00:00:00 2001 From: "Marshall T. Vandegrift" Date: Tue, 20 Jan 2009 20:29:55 -0500 Subject: [PATCH 08/22] More dynamic translation!: - Translate //guide/reference/@title attributes - Let pygettext find the strings for such translation --- setup.py | 2 +- src/calibre/ebooks/oeb/base.py | 43 ++++++++++++++------ src/calibre/ebooks/oeb/transforms/htmltoc.py | 6 ++- src/calibre/startup.py | 4 ++ 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/setup.py b/setup.py index 2f0c5aad26..6bc4109277 100644 --- a/setup.py +++ b/setup.py @@ -121,7 +121,7 @@ if __name__ == '__main__': buf = cStringIO.StringIO() print 'Creating translations template' tempdir = tempfile.mkdtemp() - pygettext(buf, ['-p', tempdir]+files) + pygettext(buf, ['-k', '__', '-p', tempdir]+files) src = buf.getvalue() pot = os.path.join(tempdir, 'calibre.pot') f = open(pot, 'wb') diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index c167151a5f..c44ffcd8d9 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -535,27 +535,36 @@ class Spine(object): class Guide(object): class Reference(object): - _TYPES_TITLES = [('cover', 'Cover'), ('title-page', 'Title Page'), - ('toc', 'Table of Contents'), ('index', 'Index'), - ('glossary', 'Glossary'), ('acknowledgements', 'Acknowledgements'), - ('bibliography', 'Bibliography'), ('colophon', 'Colophon'), - ('copyright-page', 'Copyright'), ('dedication', 'Dedication'), - ('epigraph', 'Epigraph'), ('foreword', 'Foreword'), - ('loi', 'List of Illustrations'), ('lot', 'List of Tables'), - ('notes', 'Notes'), ('preface', 'Preface'), - ('text', 'Main Text')] + _TYPES_TITLES = [('cover', __('Cover')), + ('title-page', __('Title Page')), + ('toc', __('Table of Contents')), + ('index', __('Index')), + ('glossary', __('Glossary')), + ('acknowledgements', __('Acknowledgements')), + ('bibliography', __('Bibliography')), + ('colophon', __('Colophon')), + ('copyright-page', __('Copyright')), + ('dedication', __('Dedication')), + ('epigraph', __('Epigraph')), + ('foreword', __('Foreword')), + ('loi', __('List of Illustrations')), + ('lot', __('List of Tables')), + ('notes', __('Notes')), + ('preface', __('Preface')), + ('text', __('Main Text'))] TYPES = set(t for t, _ in _TYPES_TITLES) TITLES = dict(_TYPES_TITLES) ORDER = dict((t, i) for (t, _), i in izip(_TYPES_TITLES, count(0))) - def __init__(self, type, title, href): + def __init__(self, oeb, type, title, href): + self.oeb = oeb if type.lower() in self.TYPES: type = type.lower() elif type not in self.TYPES and \ not type.startswith('other.'): type = 'other.' + type - if not title: - title = self.TITLES.get(type, None) + if not title and type in self.TITLES: + title = oeb.translate(self.TITLES[type]) self.type = type self.title = title self.href = urlnormalize(href) @@ -574,13 +583,21 @@ class Guide(object): if not isinstance(other, Guide.Reference): return NotImplemented return cmp(self._order, other._order) + + def item(): + def fget(self): + path, frag = urldefrag(self.href) + hrefs = self.oeb.manifest.hrefs + return hrefs.get(path, None) + return property(fget=fget) + item = item() def __init__(self, oeb): self.oeb = oeb self.refs = {} def add(self, type, title, href): - ref = self.Reference(type, title, href) + ref = self.Reference(self.oeb, type, title, href) self.refs[type] = ref return ref diff --git a/src/calibre/ebooks/oeb/transforms/htmltoc.py b/src/calibre/ebooks/oeb/transforms/htmltoc.py index 7da7df17e9..5508b58ec3 100644 --- a/src/calibre/ebooks/oeb/transforms/htmltoc.py +++ b/src/calibre/ebooks/oeb/transforms/htmltoc.py @@ -13,6 +13,10 @@ from calibre.ebooks.oeb.base import XML, XHTML, XHTML_NS from calibre.ebooks.oeb.base import XHTML_MIME, CSS_MIME from calibre.ebooks.oeb.base import element +__all__ = ['HTMLTOCAdder'] + +DEFAULT_TITLE = __('Table of Contents') + STYLE_CSS = { 'nested': """ .calibre_toc_header { @@ -52,7 +56,7 @@ class HTMLTOCAdder(object): if 'toc' in oeb.guide: return oeb.logger.info('Generating in-line TOC...') - title = self.title or oeb.translate('Table of Contents') + title = self.title or oeb.translate(DEFAULT_TITLE) style = self.style if style not in STYLE_CSS: oeb.logger.error('Unknown TOC style %r' % style) diff --git a/src/calibre/startup.py b/src/calibre/startup.py index eebec7d784..6cf155792e 100644 --- a/src/calibre/startup.py +++ b/src/calibre/startup.py @@ -13,6 +13,10 @@ from gettext import GNUTranslations import __builtin__ __builtin__.__dict__['_'] = lambda s: s +# For strings which belong in the translation tables, but which shouldn't be +# immediately translated to the environment language +__builtin__.__dict__['__'] = lambda s: s + from calibre.constants import iswindows, preferred_encoding, plugins from calibre.utils.config import prefs from calibre.translations.msgfmt import make From 02e4e537e46e3f93e6a1044c7f4bfb647993dfc6 Mon Sep 17 00:00:00 2001 From: John Schember Date: Tue, 20 Jan 2009 20:54:36 -0500 Subject: [PATCH 09/22] pylint clean ups --- src/calibre/devices/cybookg3/driver.py | 4 ++-- src/calibre/devices/usbms/device.py | 5 ++--- src/calibre/devices/usbms/driver.py | 7 ++++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/calibre/devices/cybookg3/driver.py b/src/calibre/devices/cybookg3/driver.py index 66e6097371..2415950d85 100644 --- a/src/calibre/devices/cybookg3/driver.py +++ b/src/calibre/devices/cybookg3/driver.py @@ -51,10 +51,10 @@ class CYBOOKG3(USBMS): return size return os.path.getsize(obj) - sizes = map(get_size, files) + sizes = [get_size(f) for f in files] size = sum(sizes) - if on_card and size > self.free_space()[2] - 1024*1024: + 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")) diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index c9a9c2a942..3c6c660e2b 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -191,7 +191,6 @@ class Device(_Device): self._main_prefix = drives['main'] if 'main' in drives.keys() else None self._card_prefix = drives['card'] if 'card' in drives.keys() else None - @classmethod def get_osx_mountpoints(self, raw=None): if raw is None: ioreg = '/usr/sbin/ioreg' @@ -226,11 +225,11 @@ class Device(_Device): dev_pat = r'/dev/%s(\w*)\s+on\s+([^\(]+)\s+' if 'main' not in names.keys(): raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.')%self.__class__.__name__) - main_pat = dev_pat%names['main'] + main_pat = dev_pat % names['main'] self._main_prefix = re.search(main_pat, mount).group(2) + os.sep card_pat = names['card'] if 'card' in names.keys() else None if card_pat is not None: - card_pat = dev_pat%card_pat + card_pat = dev_pat % card_pat self._card_prefix = re.search(card_pat, mount).group(2) + os.sep def open_linux(self): diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 1edf406d0b..db527c04ea 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -34,7 +34,7 @@ class USBMS(Device): SUPPORTS_SUB_DIRS = False def __init__(self, key='-1', log_packets=False, report_progress=None): - pass + Device.__init__(self, key, log_packets, report_progress) def get_device_information(self, end_session=True): """ @@ -54,7 +54,8 @@ class USBMS(Device): # Get all books in all directories under the root ebook_dir directory for path, dirs, files in os.walk(os.path.join(prefix, ebook_dir)): - # Filter out anything that isn't in the list of supported ebook types + # Filter out anything that isn't in the list of supported ebook + # types for book_type in self.FORMATS: for filename in fnmatch.filter(files, '*.%s' % (book_type)): title, author, mime = self.__class__.extract_book_metadata_by_filename(filename) @@ -80,7 +81,7 @@ class USBMS(Device): return size return os.path.getsize(obj) - sizes = map(get_size, files) + sizes = [get_size(f) for f in files] size = sum(sizes) if on_card and size > self.free_space()[2] - 1024*1024: From d0986bbe8ad24d82feab3b2415294a1a88fb4bb6 Mon Sep 17 00:00:00 2001 From: John Schember Date: Tue, 20 Jan 2009 21:13:00 -0500 Subject: [PATCH 10/22] Do not add books to the list of books on device if they are already in the list. Fixes book showing up multiple times if it it sent to the device multiple times. Only one istance of the book will be on the device in this case. --- src/calibre/devices/usbms/books.py | 3 +++ src/calibre/devices/usbms/driver.py | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index 3943ef94f4..fffed41549 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -18,6 +18,9 @@ class Book(object): self.thumbnail = None self.tags = [] + def __eq__(self, other): + return self.path == other.path + @apply def title_sorter(): doc = '''String to sort the title. If absent, title is returned''' diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index db527c04ea..7529890470 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -109,7 +109,7 @@ class USBMS(Device): if not os.path.exists(newpath): os.makedirs(newpath) - filepath = os.path.join(newpath, names.next()) + filepath = os.path.join(newpath, names.next()) paths.append(filepath) if hasattr(infile, 'read'): @@ -132,7 +132,10 @@ class USBMS(Device): on_card = 1 if location[1] else 0 title, author, mime = cls.extract_book_metadata_by_filename(os.path.basename(path)) - booklists[on_card].append(Book(path, title, author, mime)) + book = Book(path, title, author, mime) + + if not book in booklists[on_card]: + booklists[on_card].append(book) def delete_books(self, paths, end_session=True): for path in paths: From 7abdacff582945d9a9e04dc6a4aeffca036e02c2 Mon Sep 17 00:00:00 2001 From: "Marshall T. Vandegrift" Date: Tue, 20 Jan 2009 22:49:30 -0500 Subject: [PATCH 11/22] Forced default encoding: - Support for forced default encoding of HTML content of OEBs - oeb2mobi command-line option to set the encoding --- src/calibre/ebooks/mobi/writer.py | 2 +- src/calibre/ebooks/oeb/base.py | 28 +++++++++++++++++++--------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py index 1d9152c446..fdafd2e08b 100644 --- a/src/calibre/ebooks/mobi/writer.py +++ b/src/calibre/ebooks/mobi/writer.py @@ -560,7 +560,7 @@ def oeb2mobi(opts, inpath): compression = PALMDOC if opts.compress else UNCOMPRESSED imagemax = PALM_MAX_IMAGE_SIZE if opts.rescale_images else None context = Context(source, dest) - oeb = OEBBook(inpath, logger=logger) + oeb = OEBBook(inpath, logger=logger, encoding=opts.encoding) tocadder = HTMLTOCAdder(title=opts.toc_title) tocadder.transform(oeb, context) mangler = CaseMangler() diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index c44ffcd8d9..c2d30eb2c3 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -90,6 +90,9 @@ def prefixname(name, nsrmap): return barename(name) return ':'.join((prefix, barename(name))) +def XPath(expr): + return etree.XPath(expr, namespaces=XPNSMAP) + def xpath(elem, expr): return elem.xpath(expr, namespaces=XPNSMAP) @@ -292,15 +295,19 @@ class Metadata(object): class Manifest(object): class Item(object): NUM_RE = re.compile('^(.*)([0-9][0-9.]*)(?=[.]|$)') + META_XP = XPath('/h:html/h:head/h:meta[@http-equiv="Content-Type"]') - def __init__(self, id, href, media_type, + def __init__(self, oeb, id, href, media_type, fallback=None, loader=str, data=None): + self.oeb = oeb self.id = id self.href = self.path = urlnormalize(href) self.media_type = media_type self.fallback = fallback self.spine_position = None self.linear = True + if loader is None and data is None: + loader = oeb.container.read self._loader = loader self._data = data @@ -309,16 +316,20 @@ class Manifest(object): % (self.id, self.href, self.media_type) def _force_xhtml(self, data): + if self.oeb.encoding is not None: + data = data.decode(self.oeb.encoding, 'replace') try: data = etree.fromstring(data, parser=XML_PARSER) except etree.XMLSyntaxError: - data = html.fromstring(data, parser=XML_PARSER) + data = html.fromstring(data) data = etree.tostring(data, encoding=unicode) data = etree.fromstring(data, parser=XML_PARSER) if namespace(data.tag) != XHTML_NS: data.attrib['xmlns'] = XHTML_NS - data = etree.tostring(data) + data = etree.tostring(data, encoding=unicode) data = etree.fromstring(data, parser=XML_PARSER) + for meta in self.META_XP(data): + meta.getparent().remove(meta) return data def data(): @@ -395,9 +406,8 @@ class Manifest(object): self.hrefs = {} def add(self, id, href, media_type, fallback=None, loader=None, data=None): - loader = loader or self.oeb.container.read item = self.Item( - id, href, media_type, fallback, loader, data) + self.oeb, id, href, media_type, fallback, loader, data) self.ids[item.id] = item self.hrefs[item.href] = item return item @@ -607,9 +617,7 @@ class Guide(object): __iter__ = iterkeys def values(self): - values = list(self.refs.values()) - values.sort() - return values + return sorted(self.refs.values()) def items(self): for type, ref in self.refs.items(): @@ -713,11 +721,13 @@ class TOC(object): class OEBBook(object): - def __init__(self, opfpath=None, container=None, logger=FauxLogger()): + def __init__(self, opfpath=None, container=None, encoding=None, + logger=FauxLogger()): if opfpath and not container: container = DirContainer(os.path.dirname(opfpath)) opfpath = os.path.basename(opfpath) self.container = container + self.encoding = encoding self.logger = logger if opfpath or container: opf = self._read_opf(opfpath) From 7e00391f593116e4ce61f3c31fb1f33ddc799280 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 20 Jan 2009 23:22:14 -0800 Subject: [PATCH 12/22] Fix conversion error when book list is filtered before conversion completes --- src/calibre/library/database2.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index da41dba57f..76f244a456 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -224,9 +224,17 @@ class ResultCache(SearchQueryParser): return False def refresh_ids(self, conn, ids): + ''' + Refresh the data in the cache for books identified by ids. + Returns a list of affected rows or None if the rows are filtered. + ''' for id in ids: self._data[id] = conn.get('SELECT * from meta WHERE id=?', (id,))[0] - return map(self.row, ids) + try: + return map(self.row, ids) + except ValueError: + pass + return None def books_added(self, ids, conn): if not ids: From 00180c7f273ab7b7e5c20fb389fb41620aff4dc4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 20 Jan 2009 23:49:25 -0800 Subject: [PATCH 13/22] Add MOBI as an output format to the GUI. calibre now fully supports MOBI output, except for conversion of comics. --- src/calibre/ebooks/mobi/from_any.py | 2 +- src/calibre/gui2/dialogs/epub.py | 30 +++++-- src/calibre/gui2/dialogs/epub.ui | 126 +++++++++++++++++++--------- src/calibre/gui2/dialogs/mobi.py | 20 +++++ src/calibre/gui2/main.py | 5 +- src/calibre/gui2/tools.py | 70 +++++++++------- src/calibre/parallel.py | 10 ++- 7 files changed, 177 insertions(+), 86 deletions(-) create mode 100644 src/calibre/gui2/dialogs/mobi.py diff --git a/src/calibre/ebooks/mobi/from_any.py b/src/calibre/ebooks/mobi/from_any.py index 5a60549cad..5607690e21 100644 --- a/src/calibre/ebooks/mobi/from_any.py +++ b/src/calibre/ebooks/mobi/from_any.py @@ -28,7 +28,7 @@ def option_parser(usage=USAGE): parser = config().option_parser(usage=usage) return parser -def any2mobi(opts, path): +def any2mobi(opts, path, notification=None): ext = os.path.splitext(path)[1] if not ext: raise ValueError('Unknown file type: '+path) diff --git a/src/calibre/gui2/dialogs/epub.py b/src/calibre/gui2/dialogs/epub.py index 8bd5ef9331..af5622169a 100644 --- a/src/calibre/gui2/dialogs/epub.py +++ b/src/calibre/gui2/dialogs/epub.py @@ -15,7 +15,7 @@ from lxml.etree import XPath from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.gui2.dialogs.epub_ui import Ui_Dialog from calibre.gui2 import error_dialog, choose_images, pixmap_to_data -from calibre.ebooks.epub.from_any import SOURCE_FORMATS, config +from calibre.ebooks.epub.from_any import SOURCE_FORMATS, config as epubconfig from calibre.ebooks.metadata import MetaInformation from calibre.ptempfile import PersistentTemporaryFile from calibre.ebooks.metadata.opf import OPFCreator @@ -24,9 +24,12 @@ from calibre.ebooks.metadata import authors_to_string, string_to_authors class Config(QDialog, Ui_Dialog): - def __init__(self, parent, db, row=None): + OUTPUT = 'EPUB' + + def __init__(self, parent, db, row=None, config=epubconfig): QDialog.__init__(self, parent) self.setupUi(self) + self.hide_controls() self.connect(self.category_list, SIGNAL('itemEntered(QListWidgetItem *)'), self.show_category_help) self.connect(self.cover_button, SIGNAL("clicked()"), self.select_cover) @@ -38,7 +41,7 @@ class Config(QDialog, Ui_Dialog): if row is not None: self.id = db.id(row) base = config().as_string() + '\n\n' - defaults = self.db.conversion_options(self.id, 'epub') + defaults = self.db.conversion_options(self.id, self.OUTPUT.lower()) defaults = base + (defaults if defaults else '') self.config = config(defaults=defaults) else: @@ -47,9 +50,18 @@ class Config(QDialog, Ui_Dialog): self.get_source_format() self.category_list.setCurrentRow(0) if self.row is None: - self.setWindowTitle(_('Bulk convert to EPUB')) + self.setWindowTitle(_('Bulk convert to ')+self.OUTPUT) else: - self.setWindowTitle(_(u'Convert %s to EPUB')%unicode(self.title.text())) + self.setWindowTitle((_(u'Convert %s to ')%unicode(self.title.text()))+self.OUTPUT) + + def hide_controls(self): + self.source_profile_label.setVisible(False) + self.opt_source_profile.setVisible(False) + self.dest_profile_label.setVisible(False) + self.opt_dest_profile.setVisible(False) + self.opt_toc_title.setVisible(False) + self.toc_title_label.setVisible(False) + self.opt_rescale_images.setVisible(False) def initialize(self): self.__w = [] @@ -81,8 +93,8 @@ class Config(QDialog, Ui_Dialog): def show_category_help(self, item): text = unicode(item.text()) help = { - _('Metadata') : _('Specify metadata such as title and author for the book.\n\nMetadata will be updated in the database as well as the generated EPUB file.'), - _('Look & Feel') : _('Adjust the look of the generated EPUB file by specifying things like font sizes.'), + _('Metadata') : _('Specify metadata such as title and author for the book.\n\nMetadata will be updated in the database as well as the generated %s file.')%self.OUTPUT, + _('Look & Feel') : _('Adjust the look of the generated ebook by specifying things like font sizes.'), _('Page Setup') : _('Specify the page layout settings like margins.'), _('Chapter Detection') : _('Fine tune the detection of chapter and section headings.'), } @@ -195,7 +207,7 @@ class Config(QDialog, Ui_Dialog): elif isinstance(g, QCheckBox): self.config.set(pref.name, bool(g.isChecked())) if self.row is not None: - self.db.set_conversion_options(self.id, 'epub', self.config.src) + self.db.set_conversion_options(self.id, self.OUTPUT.lower(), self.config.src) def initialize_options(self): @@ -235,7 +247,7 @@ class Config(QDialog, Ui_Dialog): elif len(choices) == 1: self.source_format = choices[0] else: - d = ChooseFormatDialog(self.parent(), _('Choose the format to convert to EPUB'), choices) + d = ChooseFormatDialog(self.parent(), _('Choose the format to convert to ')+self.OUTPUT, choices) if d.exec_() == QDialog.Accepted: self.source_format = d.format() diff --git a/src/calibre/gui2/dialogs/epub.ui b/src/calibre/gui2/dialogs/epub.ui index f19ed7ed1a..7e3ad344ce 100644 --- a/src/calibre/gui2/dialogs/epub.ui +++ b/src/calibre/gui2/dialogs/epub.ui @@ -89,36 +89,6 @@ Book Cover - - - - - - - - - :/images/book.svg - - - true - - - Qt::AlignCenter - - - - - - - - - Use cover from &source file - - - true - - - @@ -170,6 +140,36 @@ + + + + Use cover from &source file + + + true + + + + + + + + + + + + :/images/book.svg + + + true + + + Qt::AlignCenter + + + + + opt_prefer_metadata_cover @@ -456,6 +456,13 @@ + + + + &Rescale images + + + @@ -475,7 +482,7 @@ - + &Profile: @@ -494,7 +501,7 @@ - + &Left Margin: @@ -504,7 +511,7 @@ - + pt @@ -517,7 +524,7 @@ - + &Right Margin: @@ -527,7 +534,7 @@ - + pt @@ -540,7 +547,7 @@ - + &Top Margin: @@ -550,7 +557,7 @@ - + pt @@ -563,7 +570,7 @@ - + &Bottom Margin: @@ -573,7 +580,7 @@ - + pt @@ -586,13 +593,39 @@ - + Do not &split on page breaks + + + + &Source profile: + + + opt_source_profile + + + + + + + + + + &Destination profile: + + + opt_dest_profile + + + + + + @@ -721,6 +754,19 @@ p, li { white-space: pre-wrap; } + + + + + + + &Title for generated TOC + + + opt_toc_title + + + diff --git a/src/calibre/gui2/dialogs/mobi.py b/src/calibre/gui2/dialogs/mobi.py new file mode 100644 index 0000000000..4019950c23 --- /dev/null +++ b/src/calibre/gui2/dialogs/mobi.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' + +from calibre.gui2.dialogs.epub import Config as _Config +from calibre.ebooks.mobi.from_any import config as mobiconfig + +class Config(_Config): + + OUTPUT = 'MOBI' + + def __init__(self, parent, db, row=None): + _Config.__init__(self, parent, db, row=row, config=mobiconfig) + + def hide_controls(self): + self.profile_label.setVisible(False) + self.opt_profile.setVisible(False) + self.opt_dont_split_on_page_breaks.setVisible(False) + self.opt_preserve_tag_structure.setVisible(False) \ No newline at end of file diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index b2d06a0502..a7c4c47add 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -25,7 +25,6 @@ from calibre.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \ max_available_height, config, info_dialog, \ available_width from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror -from calibre.library.database import LibraryDatabase from calibre.gui2.dialogs.scheduler import Scheduler from calibre.gui2.update import CheckForUpdates from calibre.gui2.dialogs.progress import ProgressDialog @@ -131,14 +130,14 @@ class Main(MainWindow, Ui_MainWindow): QObject.connect(self.stack, SIGNAL('currentChanged(int)'), self.location_view.location_changed) - self.output_formats = sorted(['EPUB', 'LRF']) + self.output_formats = sorted(['EPUB', 'MOBI', 'LRF']) for f in self.output_formats: self.output_format.addItem(f) self.output_format.setCurrentIndex(self.output_formats.index(prefs['output_format'])) def change_output_format(x): of = unicode(x).strip() if of != prefs['output_format']: - if of in ('EPUB', 'LIT'): + if of not in ('LRF',): warning_dialog(self, 'Warning', '

%s support is still in beta. If you find bugs, please report them by opening a ticket.'%of).exec_() prefs.set('output_format', of) diff --git a/src/calibre/gui2/tools.py b/src/calibre/gui2/tools.py index c00fbfe8e3..aca2da74e2 100644 --- a/src/calibre/gui2/tools.py +++ b/src/calibre/gui2/tools.py @@ -12,6 +12,7 @@ from PyQt4.Qt import QDialog from calibre.utils.config import prefs from calibre.gui2.dialogs.lrf_single import LRFSingleDialog, LRFBulkDialog from calibre.gui2.dialogs.epub import Config as EPUBConvert +from calibre.gui2.dialogs.mobi import Config as MOBIConvert import calibre.gui2.dialogs.comicconf as ComicConf from calibre.gui2 import warning_dialog from calibre.ptempfile import PersistentTemporaryFile @@ -19,14 +20,20 @@ from calibre.ebooks.lrf import preferred_source_formats as LRF_PREFERRED_SOURCE_ from calibre.ebooks.metadata.opf import OPFCreator from calibre.ebooks.epub.from_any import SOURCE_FORMATS as EPUB_PREFERRED_SOURCE_FORMATS -def convert_single_epub(parent, db, comics, others): +def get_dialog(fmt): + return { + 'epub':EPUBConvert, + 'mobi':MOBIConvert, + }[fmt] + +def convert_single(fmt, parent, db, comics, others): changed = False jobs = [] others_ids = [db.id(row) for row in others] comics_ids = [db.id(row) for row in comics] for row, row_id in zip(others, others_ids): temp_files = [] - d = EPUBConvert(parent, db, row) + d = get_dialog(fmt)(parent, db, row) if d.source_format is not None: d.exec_() if d.result() == QDialog.Accepted: @@ -35,7 +42,7 @@ def convert_single_epub(parent, db, comics, others): pt = PersistentTemporaryFile('.'+d.source_format.lower()) pt.write(data) pt.close() - of = PersistentTemporaryFile('.epub') + of = PersistentTemporaryFile('.'+fmt) of.close() opts.output = of.name opts.from_opf = d.opf_file.name @@ -45,8 +52,8 @@ def convert_single_epub(parent, db, comics, others): temp_files.append(d.cover_file) opts.cover = d.cover_file.name temp_files.extend([d.opf_file, pt, of]) - jobs.append(('any2epub', args, _('Convert book: ')+d.mi.title, - 'EPUB', row_id, temp_files)) + jobs.append(('any2'+fmt, args, _('Convert book: ')+d.mi.title, + fmt.upper(), row_id, temp_files)) changed = True for row, row_id in zip(comics, comics_ids): @@ -61,24 +68,24 @@ def convert_single_epub(parent, db, comics, others): if defaults is not None: db.set_conversion_options(db.id(row), 'comic', defaults) if opts is None: continue - for fmt in ['cbz', 'cbr']: + for _fmt in ['cbz', 'cbr']: try: - data = db.format(row, fmt.upper()) + data = db.format(row, _fmt.upper()) if data is not None: break except: continue - pt = PersistentTemporaryFile('.'+fmt) + pt = PersistentTemporaryFile('.'+_fmt) pt.write(data) pt.close() - of = PersistentTemporaryFile('.epub') + of = PersistentTemporaryFile('.'+fmt) of.close() opts.output = of.name opts.verbose = 2 args = [pt.name, opts] changed = True - jobs.append(('comic2epub', args, _('Convert comic: ')+opts.title, - 'EPUB', row_id, [pt, of])) + jobs.append(('comic2'+fmt, args, _('Convert comic: ')+opts.title, + fmt.upper(), row_id, [pt, of])) return jobs, changed @@ -146,9 +153,9 @@ def convert_single_lrf(parent, db, comics, others): return jobs, changed -def convert_bulk_epub(parent, db, comics, others): +def convert_bulk(fmt, parent, db, comics, others): if others: - d = EPUBConvert(parent, db) + d = get_dialog(fmt)(parent, db) if d.exec_() != QDialog.Accepted: others = [] else: @@ -169,9 +176,9 @@ def convert_bulk_epub(parent, db, comics, others): row_id = db.id(row) if row in others: data = None - for fmt in EPUB_PREFERRED_SOURCE_FORMATS: + for _fmt in EPUB_PREFERRED_SOURCE_FORMATS: try: - data = db.format(row, fmt.upper()) + data = db.format(row, _fmt.upper()) if data is not None: break except: @@ -185,10 +192,10 @@ def convert_bulk_epub(parent, db, comics, others): opf_file = PersistentTemporaryFile('.opf') opf.render(opf_file) opf_file.close() - pt = PersistentTemporaryFile('.'+fmt.lower()) + pt = PersistentTemporaryFile('.'+_fmt.lower()) pt.write(data) pt.close() - of = PersistentTemporaryFile('.epub') + of = PersistentTemporaryFile('.'+fmt) of.close() cover = db.cover(row) cf = None @@ -203,7 +210,7 @@ def convert_bulk_epub(parent, db, comics, others): desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title)) temp_files = [cf] if cf is not None else [] temp_files.extend([opf_file, pt, of]) - jobs.append(('any2epub', args, desc, 'EPUB', row_id, temp_files)) + jobs.append(('any2'+fmt, args, desc, fmt.upper(), row_id, temp_files)) else: options = comic_opts.copy() mi = db.get_metadata(row) @@ -212,24 +219,24 @@ def convert_bulk_epub(parent, db, comics, others): if mi.authors: options.author = ','.join(mi.authors) data = None - for fmt in ['cbz', 'cbr']: + for _fmt in ['cbz', 'cbr']: try: - data = db.format(row, fmt.upper()) + data = db.format(row, _fmt.upper()) if data is not None: break except: continue - pt = PersistentTemporaryFile('.'+fmt.lower()) + pt = PersistentTemporaryFile('.'+_fmt.lower()) pt.write(data) pt.close() - of = PersistentTemporaryFile('.epub') + of = PersistentTemporaryFile('.'+fmt) of.close() setattr(options, 'output', of.name) options.verbose = 1 args = [pt.name, options] desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title)) - jobs.append(('comic2epub', args, desc, 'EPUB', row_id, [pt, of])) + jobs.append(('comic2'+fmt, args, desc, fmt.upper(), row_id, [pt, of])) if bad_rows: res = [] @@ -345,15 +352,14 @@ def set_conversion_defaults_lrf(comic, parent, db): else: LRFSingleDialog(parent, None, None).exec_() -def set_conversion_defaults_epub(comic, parent, db): +def _set_conversion_defaults(dialog, comic, parent, db): if comic: ComicConf.set_conversion_defaults(parent) else: - d = EPUBConvert(parent, db) + d = dialog(parent, db) d.setWindowTitle(_('Set conversion defaults')) d.exec_() - def _fetch_news(data, fmt): pt = PersistentTemporaryFile(suffix='_feeds2%s.%s'%(fmt.lower(), fmt.lower())) pt.close() @@ -385,22 +391,22 @@ def convert_single_ebook(*args): fmt = prefs['output_format'].lower() if fmt == 'lrf': return convert_single_lrf(*args) - elif fmt == 'epub': - return convert_single_epub(*args) + elif fmt in ('epub', 'mobi'): + return convert_single(fmt, *args) def convert_bulk_ebooks(*args): fmt = prefs['output_format'].lower() if fmt == 'lrf': return convert_bulk_lrf(*args) - elif fmt == 'epub': - return convert_bulk_epub(*args) + elif fmt in ('epub', 'mobi'): + return convert_bulk(fmt, *args) def set_conversion_defaults(comic, parent, db): fmt = prefs['output_format'].lower() if fmt == 'lrf': return set_conversion_defaults_lrf(comic, parent, db) - elif fmt == 'epub': - return set_conversion_defaults_epub(comic, parent, db) + elif fmt in ('epub', 'mobi'): + return _set_conversion_defaults(get_dialog(fmt), comic, parent, db) def fetch_news(data): fmt = prefs['output_format'].lower() diff --git a/src/calibre/parallel.py b/src/calibre/parallel.py index 6cbe1c96e4..fa9284ce46 100644 --- a/src/calibre/parallel.py +++ b/src/calibre/parallel.py @@ -67,7 +67,15 @@ PARALLEL_FUNCS = { 'comic2epub' : ('calibre.ebooks.epub.from_comic', 'convert', {}, 'notification'), - + + 'any2mobi' : + ('calibre.ebooks.mobi.from_any', 'any2mobi', {}, None), + + 'feeds2mobi' : + ('calibre.ebooks.mobi.from_feeds', 'main', {}, 'notification'), + + 'comic2mobi' : + ('calibre.ebooks.mobi.from_comic', 'convert', {}, 'notification'), } From e32744eb6d0ae3a41c3c818d4e32146ba4c8f644 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 21 Jan 2009 10:06:09 -0800 Subject: [PATCH 14/22] Fix #1669 (xkcd recipe improvement: bubble help) --- src/calibre/web/feeds/recipes/recipe_xkcd.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/calibre/web/feeds/recipes/recipe_xkcd.py b/src/calibre/web/feeds/recipes/recipe_xkcd.py index f76cf5614e..35a65ab948 100644 --- a/src/calibre/web/feeds/recipes/recipe_xkcd.py +++ b/src/calibre/web/feeds/recipes/recipe_xkcd.py @@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal ' Fetch xkcd. ''' -import time +import time, re from calibre.web.feeds.news import BasicNewsRecipe class XkcdCom(BasicNewsRecipe): @@ -17,6 +17,11 @@ class XkcdCom(BasicNewsRecipe): keep_only_tags = [dict(id='middleContent')] remove_tags = [dict(name='ul'), dict(name='h3'), dict(name='br')] no_stylesheets = True + # turn image bubblehelp into a paragraph + preprocess_regexps = [ + (re.compile(r'()'), + lambda m: '%s%s

%s

' % (m.group(1), m.group(3), m.group(2))) + ] def parse_index(self): INDEX = 'http://xkcd.com/archive/' From c8a9846b0e4a4e284228ad80d49d0dbfca81297b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 21 Jan 2009 12:17:54 -0800 Subject: [PATCH 15/22] IGN:... --- src/calibre/gui2/library.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index 8a737fd608..2f426e3766 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -162,7 +162,8 @@ class BooksModel(QAbstractTableModel): def refresh_ids(self, ids, current_row=-1): rows = self.db.refresh_ids(ids) - self.refresh_rows(rows, current_row=current_row) + if rows: + self.refresh_rows(rows, current_row=current_row) def refresh_rows(self, rows, current_row=-1): for row in rows: From f35e9376e17a968ee1cc028d3217917f687e8502 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 21 Jan 2009 12:43:22 -0800 Subject: [PATCH 16/22] LIT Input:Workaround for LIT files created by ReaderWorks that have quoted ampersands in their filenames. Fixes #1627 (Failed to convert LIT to EPUB) --- src/calibre/ebooks/epub/from_any.py | 35 +++++++++++++++++++++++++---- src/calibre/ebooks/metadata/opf2.py | 5 +++-- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/calibre/ebooks/epub/from_any.py b/src/calibre/ebooks/epub/from_any.py index 51fdcc4e6a..e81821ed53 100644 --- a/src/calibre/ebooks/epub/from_any.py +++ b/src/calibre/ebooks/epub/from_any.py @@ -16,7 +16,7 @@ from calibre.ebooks.epub import config as common_config, process_encryption from calibre.ebooks.epub.from_html import convert as html2epub, find_html_index from calibre.ptempfile import TemporaryDirectory from calibre.ebooks.metadata import MetaInformation -from calibre.ebooks.metadata.opf2 import OPFCreator +from calibre.ebooks.metadata.opf2 import OPFCreator, OPF from calibre.utils.zipfile import ZipFile from calibre.customize.ui import run_plugins_on_preprocess @@ -25,9 +25,36 @@ def lit2opf(path, tdir, opts): print 'Exploding LIT file:', path reader = LitReader(path) reader.extract_content(tdir, False) - for f in walk(tdir): - if f.lower().endswith('.opf'): - return f + opf = None + for opf in walk(tdir): + if opf.lower().endswith('.opf'): + break + if not opf.endswith('.opf'): + opf = None + if opf is not None: # Check for url-quoted filenames + _opf = OPF(opf, os.path.dirname(opf)) + replacements = [] + for item in _opf.itermanifest(): + href = item.get('href', '') + path = os.path.join(os.path.dirname(opf), *(href.split('/'))) + if not os.path.exists(path) and os.path.exists(path.replace('&', '%26')): + npath = path + path = path.replace('&', '%26') + replacements.append((path, npath)) + if replacements: + print 'Fixing quoted filenames...' + for path, npath in replacements: + if os.path.exists(path): + os.rename(path, npath) + for f in walk(tdir): + with open(f, 'r+b') as f: + raw = f.read() + for path, npath in replacements: + raw = raw.replace(os.path.basename(path), os.path.basename(npath)) + f.seek(0) + f.truncate() + f.write(raw) + return opf def mobi2opf(path, tdir, opts): from calibre.ebooks.mobi.reader import MobiReader diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index 08ed9aacd1..ed0340e4a8 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -435,7 +435,7 @@ class OPF(object): rating = MetadataField('rating', is_dc=False, formatter=int) - def __init__(self, stream, basedir=os.getcwdu()): + def __init__(self, stream, basedir=os.getcwdu(), unquote_urls=True): if not hasattr(stream, 'read'): stream = open(stream, 'rb') self.basedir = self.base_dir = basedir @@ -446,7 +446,8 @@ class OPF(object): if not self.metadata: raise ValueError('Malformed OPF file: No element') self.metadata = self.metadata[0] - self.unquote_urls() + if unquote_urls: + self.unquote_urls() self.manifest = Manifest() m = self.manifest_path(self.root) if m: From 8af820ef49f16ed1b2c343d6c9043e2bc0535379 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 21 Jan 2009 13:20:02 -0800 Subject: [PATCH 17/22] Fix #1658 (Not able to view some PDF files) --- src/calibre/library/static/gui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/library/static/gui.js b/src/calibre/library/static/gui.js index 17f6f7f8fd..c158f3a46a 100644 --- a/src/calibre/library/static/gui.js +++ b/src/calibre/library/static/gui.js @@ -40,7 +40,7 @@ function create_table_headers() { function format_url(format, id, title) { - return 'get/'+format.toLowerCase() + '/'+title + '_' + id+'.'+format.toLowerCase(); + return 'get/'+format.toLowerCase() + '/'+title.replace('#', '') + '_' + id+'.'+format.toLowerCase(); } function render_book(book) { From 1e46683819aebbaaffec3222a4e606bbea96aae3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 21 Jan 2009 13:28:04 -0800 Subject: [PATCH 18/22] IGN:Content server now properly encodes generated URIs based on book metadata --- src/calibre/library/static/gui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/library/static/gui.js b/src/calibre/library/static/gui.js index c158f3a46a..edefdbc5ef 100644 --- a/src/calibre/library/static/gui.js +++ b/src/calibre/library/static/gui.js @@ -40,7 +40,7 @@ function create_table_headers() { function format_url(format, id, title) { - return 'get/'+format.toLowerCase() + '/'+title.replace('#', '') + '_' + id+'.'+format.toLowerCase(); + return 'get/'+format.toLowerCase() + '/'+encodeURIComponent(title) + '_' + id+'.'+format.toLowerCase(); } function render_book(book) { From 223c36f20b98f605d2b1462eb804b65ddfbdd29f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 21 Jan 2009 15:48:09 -0800 Subject: [PATCH 19/22] IGN:... --- src/calibre/library/__init__.py | 2 +- src/calibre/translations/dynamic.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/calibre/library/__init__.py b/src/calibre/library/__init__.py index 77d02e1354..6cec55c471 100644 --- a/src/calibre/library/__init__.py +++ b/src/calibre/library/__init__.py @@ -29,5 +29,5 @@ def server_config(defaults=None): c.add_opt('develop', ['--develop'], default=False, help='Development mode. Server automatically restarts on file changes and serves code files (html, css, js) from the file system instead of calibre\'s resource system.') c.add_opt('max_cover', ['--max-cover'], default='600x800', - help=_('The maximum size for displayed covers')) + help=_('The maximum size for displayed covers. Default is %default.')) return c diff --git a/src/calibre/translations/dynamic.py b/src/calibre/translations/dynamic.py index 1c9f53e960..6131a84c8f 100644 --- a/src/calibre/translations/dynamic.py +++ b/src/calibre/translations/dynamic.py @@ -5,9 +5,8 @@ Dynamic language lookup of translations for user-visible strings. __license__ = 'GPL v3' __copyright__ = '2008, Marshall T. Vandegrift ' -import sys from cStringIO import StringIO -from gettext import GNUTranslations, NullTranslations +from gettext import GNUTranslations from calibre.translations.compiled import translations __all__ = ['translate'] @@ -23,5 +22,5 @@ def translate(lang, text): trans = GNUTranslations(buf) _CACHE[lang] = trans if trans is None: - return _(text) + return getattr(__builtins__, '_', lambda x: x)(text) return trans.ugettext(text) From ccd7aaa5c9206c5272a457733a04a412d28ded53 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 21 Jan 2009 18:18:48 -0800 Subject: [PATCH 20/22] Improve metadata reading from LIT files --- src/calibre/ebooks/metadata/lit.py | 41 +++++++++++++----------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/calibre/ebooks/metadata/lit.py b/src/calibre/ebooks/metadata/lit.py index 825fe45cf4..c38450c64c 100644 --- a/src/calibre/ebooks/metadata/lit.py +++ b/src/calibre/ebooks/metadata/lit.py @@ -6,33 +6,28 @@ Support for reading the metadata from a LIT file. import sys, cStringIO, os -from calibre import relpath from calibre.ebooks.metadata import MetaInformation -from calibre.ebooks.metadata.opf import OPFReader +from calibre.ebooks.metadata.opf2 import OPF from calibre.ebooks.lit.reader import LitReader def get_metadata(stream): - try: - litfile = LitReader(stream) - src = litfile.meta.encode('utf-8') - mi = OPFReader(cStringIO.StringIO(src), dir=os.getcwd()) - cover_url, cover_item = mi.cover, None - if cover_url: - cover_url = relpath(cover_url, os.getcwd()) - for item in litfile.manifest.values(): - if item.path == cover_url: - cover_item = item.internal - if cover_item is not None: - ext = cover_url.rpartition('.')[-1] - if not ext: - ext = 'jpg' - else: - ext = ext.lower() - cd = litfile.get_file('/data/' + cover_item) - mi.cover_data = (ext, cd) if cd else (None, None) - except: - title = stream.name if hasattr(stream, 'name') and stream.name else 'Unknown' - mi = MetaInformation(title, ['Unknown']) + litfile = LitReader(stream) + src = litfile.meta.encode('utf-8') + opf = OPF(cStringIO.StringIO(src), os.getcwd()) + mi = MetaInformation(opf) + covers = [] + for item in opf.iterguide(): + if 'cover' not in item.get('type', '').lower(): + continue + href = item.get('href', '') + candidates = [href, href.replace('&', '%26')] + for item in litfile.manifest.values(): + if item.path in candidates: + covers.append(item.internal) + break + covers = [litfile.get_file('/data/' + i) for i in covers] + covers.sort(cmp=lambda x, y:cmp(len(x), len(y))) + mi.cover_data = ('jpg', covers[-1]) return mi def main(args=sys.argv): From a33aa9140adb90675a8113ced4b53b51d5d27c4b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 21 Jan 2009 19:31:21 -0800 Subject: [PATCH 21/22] Fix calibredb add not refreshing book list in GUI --- src/calibre/gui2/library.py | 12 +++++++++++- src/calibre/gui2/main.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index 2f426e3766..0e426aaf42 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -262,7 +262,17 @@ class BooksModel(QAbstractTableModel): self.reset() self.sorted_on = (self.column_map[col], order) - + + def refresh(self, reset=True): + try: + col = self.column_map.index(self.sorted_on[0]) + except: + col = 0 + self.db.refresh(field=self.column_map[col], + ascending=self.sorted_on[1]==Qt.AscendingOrder) + if reset: + self.reset() + def resort(self, reset=True): try: col = self.column_map.index(self.sorted_on[0]) diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index a7c4c47add..ba59908c9f 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -478,7 +478,7 @@ class Main(MainWindow, Ui_MainWindow): self.raise_() self.activateWindow() elif msg.startswith('refreshdb:'): - self.library_view.model().resort() + self.library_view.model().refresh() self.library_view.model().research() else: print msg From 79e1e261af6cf3fa2a00024d59ff71eb55b235a6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 21 Jan 2009 21:32:34 -0800 Subject: [PATCH 22/22] Create mobi-meta command line tool and make cover extraction from MOBI files more efficient --- src/calibre/ebooks/metadata/mobi.py | 29 +++++++++++++++++++ src/calibre/ebooks/mobi/reader.py | 44 ++++++++++++----------------- src/calibre/linux.py | 3 +- 3 files changed, 49 insertions(+), 27 deletions(-) create mode 100644 src/calibre/ebooks/metadata/mobi.py diff --git a/src/calibre/ebooks/metadata/mobi.py b/src/calibre/ebooks/metadata/mobi.py new file mode 100644 index 0000000000..933cbbdaed --- /dev/null +++ b/src/calibre/ebooks/metadata/mobi.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' + +''' +''' + +import sys, os + +from calibre.ebooks.mobi.reader import get_metadata + +def main(args=sys.argv): + if len(args) != 2: + print >>sys.stderr, 'Usage: %s file.mobi' % args[0] + return 1 + fname = args[1] + mi = get_metadata(open(fname, 'rb')) + print unicode(mi) + if mi.cover_data[1]: + cover = os.path.abspath( + '.'.join((os.path.splitext(os.path.basename(fname))[0], + mi.cover_data[0].lower()))) + open(cover, 'wb').write(mi.cover_data[1]) + print _('Cover saved to'), cover + return 0 + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index 5d2edd3fe0..51e184a420 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -311,7 +311,7 @@ class MobiReader(object): opf.cover = 'images/%05d.jpg'%(self.book_header.exth.cover_offset+1) manifest = [(htmlfile, 'text/x-oeb1-document')] bp = os.path.dirname(htmlfile) - for i in self.image_names: + for i in getattr(self, 'image_names', []): manifest.append((os.path.join(bp, 'images/', i), 'image/jpg')) opf.create_manifest(manifest) @@ -451,7 +451,7 @@ class MobiReader(object): image_index += 1 try: im = PILImage.open(buf) - except IOError, e: + except IOError: continue path = os.path.join(output_dir, '%05d.jpg'%image_index) @@ -476,31 +476,23 @@ def get_metadata(stream): if mr.book_header.exth is None: mi = MetaInformation(mr.name, [_('Unknown')]) else: - tdir = tempfile.mkdtemp('_mobi_meta', __appname__+'_') - atexit.register(shutil.rmtree, tdir) - #print tdir - mr.extract_images([], tdir) mi = mr.create_opf('dummy.html') - if mi.cover: - cover = os.path.join(tdir, mi.cover) - if not os.access(cover, os.R_OK): - fname = os.path.basename(cover) - match = re.match(r'(\d+)(.+)', fname) - if match: - num, ext = int(match.group(1), 10), match.group(2) - while num > 0: - num -= 1 - candidate = os.path.join(os.path.dirname(cover), '%05d%s'%(num, ext)) - if os.access(candidate, os.R_OK): - cover = candidate - break - if os.access(cover, os.R_OK): - mi.cover_data = ('JPEG', open(os.path.join(tdir, cover), 'rb').read()) - else: - path = os.path.join(tdir, 'images', '00001.jpg') - if os.access(path, os.R_OK): - mi.cover_data = ('JPEG', open(path, 'rb').read()) - return mi + try: + if hasattr(mr.book_header.exth, 'cover_offset'): + cover_index = mr.book_header.first_image_index + mr.book_header.exth.cover_offset + data = mr.sections[cover_index][0] + else: + data = mr.sections[mr.book_header.first_image_index][0] + buf = cStringIO.StringIO(data) + im = PILImage.open(buf) + obuf = cStringIO.StringIO() + im.convert('RGBA').save(obuf, format='JPEG') + mi.cover_data = ('jpg', obuf.getvalue()) + except: + import traceback + traceback.print_exc() + return mi + def option_parser(): from calibre.utils.config import OptionParser diff --git a/src/calibre/linux.py b/src/calibre/linux.py index acd7e0b1bd..a05a7ea7a8 100644 --- a/src/calibre/linux.py +++ b/src/calibre/linux.py @@ -26,6 +26,7 @@ entry_points = { 'opf-meta = calibre.ebooks.metadata.opf2:main', 'odt-meta = calibre.ebooks.metadata.odt:main', 'epub-meta = calibre.ebooks.metadata.epub:main', + 'mobi-meta = calibre.ebooks.metadata.mobi:main', 'txt2lrf = calibre.ebooks.lrf.txt.convert_from:main', 'html2lrf = calibre.ebooks.lrf.html.convert_from:main', 'html2oeb = calibre.ebooks.html:main', @@ -423,7 +424,7 @@ def install_man_pages(fatal_errors): if prog in ('prs500', 'pdf-meta', 'epub-meta', 'lit-meta', 'markdown-calibre', 'calibre-debug', 'fb2-meta', 'calibre-fontconfig', 'calibre-parallel', 'odt-meta', - 'rb-meta', 'imp-meta'): + 'rb-meta', 'imp-meta', 'mobi-meta'): continue help2man = ('help2man', prog, '--name', 'part of %s'%__appname__,