From aca765c3d0ebb3aefcff3d0f3018006415977ff9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 1 May 2012 12:37:11 +0530 Subject: [PATCH] Command Line Interface for the Tweak Book feature --- src/calibre/debug.py | 16 ++- src/calibre/ebooks/mobi/reader/headers.py | 16 +++ src/calibre/ebooks/mobi/tweak.py | 106 ++++++------------ src/calibre/ebooks/tweak.py | 126 ++++++++++++++++++++++ 4 files changed, 183 insertions(+), 81 deletions(-) create mode 100644 src/calibre/ebooks/tweak.py diff --git a/src/calibre/debug.py b/src/calibre/debug.py index 3b04a431e7..f2ae5d8eaf 100644 --- a/src/calibre/debug.py +++ b/src/calibre/debug.py @@ -54,8 +54,14 @@ Run an embedded python interpreter. parser.add_option('-m', '--inspect-mobi', action='store_true', default=False, help='Inspect the MOBI file(s) at the specified path(s)') - parser.add_option('--tweak-kf8', default=None, - help='Tweak the KF8 file at the specified path') + parser.add_option('--tweak-book', default=None, + help='Tweak the book (exports the book as a collection of HTML ' + 'files and metadata, which you can edit using standard HTML ' + 'editing tools, and then rebuilds the file from the edited HTML. ' + 'Makes no additional changes to the HTML, unlike a full calibre ' + 'conversion). Note that this tool will try to open the ' + 'folder containing the HTML files in the editor pointed to by the' + ' EDITOR environment variable.') parser.add_option('--test-build', help='Test binary modules in build', action='store_true', default=False) @@ -242,9 +248,9 @@ def main(args=sys.argv): prints('Inspecting:', path) inspect_mobi(path) print - elif opts.tweak_kf8: - from calibre.ebooks.mobi.tweak import tweak - tweak(opts.tweak_kf8) + elif opts.tweak_book: + from calibre.ebooks.tweak import tweak + tweak(opts.tweak_book) elif opts.test_build: from calibre.test_build import test test() diff --git a/src/calibre/ebooks/mobi/reader/headers.py b/src/calibre/ebooks/mobi/reader/headers.py index 0162fddda7..a5ca4a7132 100644 --- a/src/calibre/ebooks/mobi/reader/headers.py +++ b/src/calibre/ebooks/mobi/reader/headers.py @@ -234,6 +234,22 @@ class MetadataHeader(BookHeader): else: self.exth = None + @property + def kf8_type(self): + if (self.mobi_version == 8 and getattr(self, 'skelidx', NULL_INDEX) != + NULL_INDEX): + return u'standalone' + + kf8_header_index = getattr(self.exth, 'kf8_header', None) + if kf8_header_index is None: + return None + try: + if self.section_data(kf8_header_index-1) == b'BOUNDARY': + return u'joint' + except: + pass + return None + def identity(self): self.stream.seek(60) ident = self.stream.read(8).upper() diff --git a/src/calibre/ebooks/mobi/tweak.py b/src/calibre/ebooks/mobi/tweak.py index a5a6c3cc78..4d21e71679 100644 --- a/src/calibre/ebooks/mobi/tweak.py +++ b/src/calibre/ebooks/mobi/tweak.py @@ -7,9 +7,9 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os, glob, sys, shlex, subprocess +import os, glob -from calibre import CurrentDir, as_unicode, prints +from calibre import CurrentDir from calibre.ebooks.mobi import MobiError from calibre.ebooks.mobi.reader.mobi6 import MobiReader from calibre.ebooks.mobi.reader.headers import MetadataHeader @@ -17,44 +17,46 @@ from calibre.utils.logging import default_log from calibre.ebooks import DRMError from calibre.ebooks.mobi.reader.mobi8 import Mobi8Reader from calibre.ebooks.conversion.plumber import Plumber, create_oebbook -from calibre.ptempfile import TemporaryDirectory -from calibre.constants import __appname__, iswindows from calibre.customize.ui import (plugin_for_input_format, plugin_for_output_format) class BadFormat(ValueError): pass -def explode(stream, dest, split_callback=lambda x:True): - raw = stream.read(3) - stream.seek(0) - if raw == b'TPZ': - raise BadFormat(_('This is not a MOBI file. It is a Topaz file.')) +def explode(path, dest, question=lambda x:True): + with open(path, 'rb') as stream: + raw = stream.read(3) + stream.seek(0) + if raw == b'TPZ': + raise BadFormat(_('This is not a MOBI file. It is a Topaz file.')) - try: - header = MetadataHeader(stream, default_log) - except MobiError: - raise BadFormat(_('This is not a MOBI file.')) + try: + header = MetadataHeader(stream, default_log) + except MobiError: + raise BadFormat(_('This is not a MOBI file.')) - if header.encryption_type != 0: - raise DRMError(_('This file is locked with DRM. It cannot be tweaked.')) + if header.encryption_type != 0: + raise DRMError(_('This file is locked with DRM. It cannot be tweaked.')) - stream.seek(0) - mr = MobiReader(stream, default_log, None, None) + kf8_type = header.kf8_type - if mr.kf8_type is None: - raise BadFormat('This MOBI file does not contain a KF8 format book') + if kf8_type is None: + raise BadFormat('This MOBI file does not contain a KF8 format book') - if mr.kf8_type == 'joint': - if not split_callback(_('This MOBI file contains both KF8 and ' - 'older Mobi6 data. Tweaking it will remove the Mobi6 data, which ' - ' means the file will not be usable on older Kindles. Are you ' - 'sure?')): - return None + if kf8_type == 'joint': + if not question(_('This MOBI file contains both KF8 and ' + 'older Mobi6 data. Tweaking it will remove the Mobi6 data, which ' + 'means the file will not be usable on older Kindles. Are you ' + 'sure?')): + return None - with CurrentDir(dest): - mr = Mobi8Reader(mr, default_log) - opf = os.path.abspath(mr()) + + stream.seek(0) + mr = MobiReader(stream, default_log, None, None) + + with CurrentDir(dest): + mr = Mobi8Reader(mr, default_log) + opf = os.path.abspath(mr()) return opf @@ -72,52 +74,4 @@ def rebuild(src_dir, dest_path): oeb = create_oebbook(default_log, opf, plumber.opts) outp.convert(oeb, dest_path, inp, plumber.opts, default_log) -def ask_question(msg): - prints(msg, end=' [y/N]: ') - sys.stdout.flush() - - if iswindows: - import msvcrt - ans = msvcrt.getch() - else: - import tty, termios - old_settings = termios.tcgetattr(sys.stdin.fileno()) - try: - tty.setraw(sys.stdin.fileno()) - ans = sys.stdin.read(1) - finally: - termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old_settings) - print() - return ans == b'y' - -def tweak(mobi_file): - with TemporaryDirectory('_tweak_'+os.path.basename(mobi_file)) as tdir: - with open(mobi_file, 'rb') as stream: - try: - opf = explode(stream, tdir, split_callback=ask_question) - except BadFormat as e: - prints(as_unicode(e), file=sys.stderr) - raise SystemExit(1) - if opf is None: - return - - ed = os.environ.get('EDITOR', None) - proceed = False - if ed is None: - prints('KF8 extracted to', tdir) - prints('Make your tweaks and once you are done,', __appname__, - 'will rebuild', mobi_file, 'from', tdir) - proceed = ask_question('Rebuild ' + mobi_file + '?') - else: - cmd = shlex.split(ed) - try: - subprocess.check_call(cmd + [tdir]) - except: - prints(ed, 'failed, aborting...') - raise SystemExit(1) - proceed = True - - if proceed: - rebuild(tdir, mobi_file) - prints(mobi_file, 'successfully tweaked') diff --git a/src/calibre/ebooks/tweak.py b/src/calibre/ebooks/tweak.py new file mode 100644 index 0000000000..ba3cf65a85 --- /dev/null +++ b/src/calibre/ebooks/tweak.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import sys, os, shlex, subprocess + +from calibre import prints, as_unicode, walk +from calibre.constants import iswindows, __appname__ +from calibre.ptempfile import TemporaryDirectory +from calibre.libunzip import extract as zipextract +from calibre.utils.zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED + +class Error(ValueError): + pass + +def ask_cli_question(msg): + prints(msg, end=' [y/N]: ') + sys.stdout.flush() + + if iswindows: + import msvcrt + ans = msvcrt.getch() + else: + import tty, termios + old_settings = termios.tcgetattr(sys.stdin.fileno()) + try: + tty.setraw(sys.stdin.fileno()) + try: + ans = sys.stdin.read(1) + except KeyboardInterrupt: + ans = b'' + finally: + termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old_settings) + print() + return ans == b'y' + +def mobi_exploder(path, tdir, question=lambda x:True): + from calibre.ebooks.mobi.tweak import explode, BadFormat + try: + return explode(path, tdir, question=question) + except BadFormat as e: + raise Error(as_unicode(e)) + +def zip_exploder(path, tdir, question=lambda x:True): + zipextract(path, tdir) + for f in walk(tdir): + if f.lower().endswith('.opf'): + return f + raise Error('Invalid book: Could not find .opf') + +def zip_rebuilder(tdir, path): + with ZipFile(path, 'w', compression=ZIP_DEFLATED) as zf: + # Write mimetype + mt = os.path.join(tdir, 'mimetype') + if os.path.exists(mt): + zf.write(mt, 'mimetype', compress_type=ZIP_STORED) + # Write everything else + exclude_files = {'.DS_Store', 'mimetype', 'iTunesMetadata.plist'} + for root, dirs, files in os.walk(tdir): + for fn in files: + if fn in exclude_files: + continue + absfn = os.path.join(root, fn) + zfn = os.path.relpath(absfn, tdir).replace(os.sep, '/') + zf.write(absfn, zfn) + +def get_tools(fmt): + fmt = fmt.lower() + + if fmt in {'mobi', 'azw', 'azw3'}: + from calibre.ebooks.mobi.tweak import rebuild + ans = mobi_exploder, rebuild + elif fmt in {'epub', 'htmlz'}: + ans = zip_exploder, zip_rebuilder + else: + ans = None, None + + return ans + +def tweak(ebook_file): + ''' Command line interface to the Tweak Book tool ''' + fmt = ebook_file.rpartition('.')[-1].lower() + exploder, rebuilder = get_tools(fmt) + if exploder is None: + prints('Cannot tweak %s files. Supported formats are: EPUB, HTMLZ, AZW3, MOBI' + , file=sys.stderr) + raise SystemExit(1) + + with TemporaryDirectory('_tweak_'+ + os.path.basename(ebook_file).rpartition('.')[0]) as tdir: + try: + opf = exploder(ebook_file, tdir, question=ask_cli_question) + except Error as e: + prints(as_unicode(e), file=sys.stderr) + raise SystemExit(1) + + if opf is None: + # The question was answered with No + return + + ed = os.environ.get('EDITOR', None) + proceed = False + if ed is None: + prints('Book extracted to', tdir) + prints('Make your tweaks and once you are done,', __appname__, + 'will rebuild', ebook_file, 'from', tdir) + print() + proceed = ask_cli_question('Rebuild ' + ebook_file + '?') + else: + cmd = shlex.split(ed) + try: + subprocess.check_call(cmd + [tdir]) + except: + prints(ed, 'failed, aborting...') + raise SystemExit(1) + proceed = True + + if proceed: + rebuilder(tdir, ebook_file) + prints(ebook_file, 'successfully tweaked') +