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