mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Command Line Interface for the Tweak Book feature
This commit is contained in:
parent
953bf80981
commit
aca765c3d0
@ -54,8 +54,14 @@ Run an embedded python interpreter.
|
|||||||
parser.add_option('-m', '--inspect-mobi', action='store_true',
|
parser.add_option('-m', '--inspect-mobi', action='store_true',
|
||||||
default=False,
|
default=False,
|
||||||
help='Inspect the MOBI file(s) at the specified path(s)')
|
help='Inspect the MOBI file(s) at the specified path(s)')
|
||||||
parser.add_option('--tweak-kf8', default=None,
|
parser.add_option('--tweak-book', default=None,
|
||||||
help='Tweak the KF8 file at the specified path')
|
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',
|
parser.add_option('--test-build', help='Test binary modules in build',
|
||||||
action='store_true', default=False)
|
action='store_true', default=False)
|
||||||
@ -242,9 +248,9 @@ def main(args=sys.argv):
|
|||||||
prints('Inspecting:', path)
|
prints('Inspecting:', path)
|
||||||
inspect_mobi(path)
|
inspect_mobi(path)
|
||||||
print
|
print
|
||||||
elif opts.tweak_kf8:
|
elif opts.tweak_book:
|
||||||
from calibre.ebooks.mobi.tweak import tweak
|
from calibre.ebooks.tweak import tweak
|
||||||
tweak(opts.tweak_kf8)
|
tweak(opts.tweak_book)
|
||||||
elif opts.test_build:
|
elif opts.test_build:
|
||||||
from calibre.test_build import test
|
from calibre.test_build import test
|
||||||
test()
|
test()
|
||||||
|
@ -234,6 +234,22 @@ class MetadataHeader(BookHeader):
|
|||||||
else:
|
else:
|
||||||
self.exth = None
|
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):
|
def identity(self):
|
||||||
self.stream.seek(60)
|
self.stream.seek(60)
|
||||||
ident = self.stream.read(8).upper()
|
ident = self.stream.read(8).upper()
|
||||||
|
@ -7,9 +7,9 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__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 import MobiError
|
||||||
from calibre.ebooks.mobi.reader.mobi6 import MobiReader
|
from calibre.ebooks.mobi.reader.mobi6 import MobiReader
|
||||||
from calibre.ebooks.mobi.reader.headers import MetadataHeader
|
from calibre.ebooks.mobi.reader.headers import MetadataHeader
|
||||||
@ -17,15 +17,14 @@ from calibre.utils.logging import default_log
|
|||||||
from calibre.ebooks import DRMError
|
from calibre.ebooks import DRMError
|
||||||
from calibre.ebooks.mobi.reader.mobi8 import Mobi8Reader
|
from calibre.ebooks.mobi.reader.mobi8 import Mobi8Reader
|
||||||
from calibre.ebooks.conversion.plumber import Plumber, create_oebbook
|
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,
|
from calibre.customize.ui import (plugin_for_input_format,
|
||||||
plugin_for_output_format)
|
plugin_for_output_format)
|
||||||
|
|
||||||
class BadFormat(ValueError):
|
class BadFormat(ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def explode(stream, dest, split_callback=lambda x:True):
|
def explode(path, dest, question=lambda x:True):
|
||||||
|
with open(path, 'rb') as stream:
|
||||||
raw = stream.read(3)
|
raw = stream.read(3)
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
if raw == b'TPZ':
|
if raw == b'TPZ':
|
||||||
@ -39,19 +38,22 @@ def explode(stream, dest, split_callback=lambda x:True):
|
|||||||
if header.encryption_type != 0:
|
if header.encryption_type != 0:
|
||||||
raise DRMError(_('This file is locked with DRM. It cannot be tweaked.'))
|
raise DRMError(_('This file is locked with DRM. It cannot be tweaked.'))
|
||||||
|
|
||||||
stream.seek(0)
|
kf8_type = header.kf8_type
|
||||||
mr = MobiReader(stream, default_log, None, None)
|
|
||||||
|
|
||||||
if mr.kf8_type is None:
|
if kf8_type is None:
|
||||||
raise BadFormat('This MOBI file does not contain a KF8 format book')
|
raise BadFormat('This MOBI file does not contain a KF8 format book')
|
||||||
|
|
||||||
if mr.kf8_type == 'joint':
|
if kf8_type == 'joint':
|
||||||
if not split_callback(_('This MOBI file contains both KF8 and '
|
if not question(_('This MOBI file contains both KF8 and '
|
||||||
'older Mobi6 data. Tweaking it will remove the Mobi6 data, which '
|
'older Mobi6 data. Tweaking it will remove the Mobi6 data, which '
|
||||||
'means the file will not be usable on older Kindles. Are you '
|
'means the file will not be usable on older Kindles. Are you '
|
||||||
'sure?')):
|
'sure?')):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
stream.seek(0)
|
||||||
|
mr = MobiReader(stream, default_log, None, None)
|
||||||
|
|
||||||
with CurrentDir(dest):
|
with CurrentDir(dest):
|
||||||
mr = Mobi8Reader(mr, default_log)
|
mr = Mobi8Reader(mr, default_log)
|
||||||
opf = os.path.abspath(mr())
|
opf = os.path.abspath(mr())
|
||||||
@ -72,52 +74,4 @@ def rebuild(src_dir, dest_path):
|
|||||||
oeb = create_oebbook(default_log, opf, plumber.opts)
|
oeb = create_oebbook(default_log, opf, plumber.opts)
|
||||||
outp.convert(oeb, dest_path, inp, plumber.opts, default_log)
|
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
126
src/calibre/ebooks/tweak.py
Normal 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')
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user