Command Line Interface for the Tweak Book feature

This commit is contained in:
Kovid Goyal 2012-05-01 12:37:11 +05:30
parent 953bf80981
commit aca765c3d0
4 changed files with 183 additions and 81 deletions

View File

@ -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()

View File

@ -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()

View File

@ -7,9 +7,9 @@ __license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__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')

126
src/calibre/ebooks/tweak.py Normal file
View File

@ -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 <kovid@kovidgoyal.net>'
__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')