Update uses of pathclass and a few command line argparsers.
This commit is contained in:
parent
6958f73cd9
commit
c23335733c
2 changed files with 154 additions and 108 deletions
|
@ -50,6 +50,8 @@ normalize:
|
||||||
Rename files and directories in the book to match a common structure.
|
Rename files and directories in the book to match a common structure.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To see the rest, try `epubfile --help`.
|
||||||
|
|
||||||
## Spec compliance
|
## Spec compliance
|
||||||
|
|
||||||
epubfile does not rigorously enforce the epub spec and you can create noncompliant books with it. Basic errors are checked, and I am open to issues and comments regarding ways to improve spec-compliance without adding significant size or complexity to the library. I am prioritizing simplicity and ease of use over perfection.
|
epubfile does not rigorously enforce the epub spec and you can create noncompliant books with it. Basic errors are checked, and I am open to issues and comments regarding ways to improve spec-compliance without adding significant size or complexity to the library. I am prioritizing simplicity and ease of use over perfection.
|
||||||
|
|
260
epubfile.py
260
epubfile.py
|
@ -14,6 +14,10 @@ import tinycss2
|
||||||
|
|
||||||
from voussoirkit import interactive
|
from voussoirkit import interactive
|
||||||
from voussoirkit import pathclass
|
from voussoirkit import pathclass
|
||||||
|
from voussoirkit import pipeable
|
||||||
|
from voussoirkit import vlogging
|
||||||
|
|
||||||
|
log = vlogging.get_logger(__name__, 'epubfile')
|
||||||
|
|
||||||
HTML_LINK_PROPERTIES = {
|
HTML_LINK_PROPERTIES = {
|
||||||
'a': ['href'],
|
'a': ['href'],
|
||||||
|
@ -70,10 +74,6 @@ OPF_TEMPLATE = '''
|
||||||
<package version="3.0" unique-identifier="BookId" xmlns="http://www.idpf.org/2007/opf">
|
<package version="3.0" unique-identifier="BookId" xmlns="http://www.idpf.org/2007/opf">
|
||||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
<dc:identifier id="BookId">{uuid}</dc:identifier>
|
<dc:identifier id="BookId">{uuid}</dc:identifier>
|
||||||
<dc:creator id="cre">author</dc:creator>
|
|
||||||
<meta scheme="marc:relators" refines="#cre" property="role">aut</meta>
|
|
||||||
<dc:title>title</dc:title>
|
|
||||||
<dc:language>und</dc:language>
|
|
||||||
</metadata>
|
</metadata>
|
||||||
<manifest>
|
<manifest>
|
||||||
<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
|
<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
|
||||||
|
@ -91,9 +91,6 @@ NCX_TEMPLATE = '''
|
||||||
<head>
|
<head>
|
||||||
<meta name="dtb:uid" content="{uuid}" />
|
<meta name="dtb:uid" content="{uuid}" />
|
||||||
</head>
|
</head>
|
||||||
<docTitle>
|
|
||||||
<text>{title}</text>
|
|
||||||
</docTitle>
|
|
||||||
<navMap>
|
<navMap>
|
||||||
{navpoints}
|
{navpoints}
|
||||||
</navMap>
|
</navMap>
|
||||||
|
@ -129,12 +126,12 @@ TEXT_TEMPLATE = '''
|
||||||
</html>
|
</html>
|
||||||
'''.strip()
|
'''.strip()
|
||||||
|
|
||||||
|
|
||||||
# EPUB COMPRESSION
|
# EPUB COMPRESSION
|
||||||
################################################################################
|
################################################################################
|
||||||
def compress_epub(directory, epub_filepath):
|
def compress_epub(directory, epub_filepath):
|
||||||
directory = pathclass.Path(directory)
|
directory = pathclass.Path(directory)
|
||||||
epub_filepath = pathclass.Path(epub_filepath)
|
epub_filepath = pathclass.Path(epub_filepath)
|
||||||
|
log.debug('Compressing %s to %s.', directory.absolute_path, epub_filepath.absolute_path)
|
||||||
|
|
||||||
if epub_filepath in directory:
|
if epub_filepath in directory:
|
||||||
raise ValueError('Epub inside its own directory')
|
raise ValueError('Epub inside its own directory')
|
||||||
|
@ -142,14 +139,14 @@ def compress_epub(directory, epub_filepath):
|
||||||
if epub_filepath.extension != 'epub':
|
if epub_filepath.extension != 'epub':
|
||||||
epub_filepath = epub_filepath.add_extension('epub')
|
epub_filepath = epub_filepath.add_extension('epub')
|
||||||
|
|
||||||
with zipfile.ZipFile(epub_filepath.absolute_path, 'w') as z:
|
with zipfile.ZipFile(epub_filepath, 'w') as z:
|
||||||
z.write(directory.with_child('mimetype').absolute_path, arcname='mimetype')
|
z.write(directory.with_child('mimetype'), arcname='mimetype')
|
||||||
for file in directory.walk():
|
for file in directory.walk():
|
||||||
if file in [directory.with_child('mimetype'), directory.with_child('sigil.cfg')]:
|
if file in [directory.with_child('mimetype'), directory.with_child('sigil.cfg')]:
|
||||||
continue
|
continue
|
||||||
z.write(
|
z.write(
|
||||||
file.absolute_path,
|
file,
|
||||||
arcname=file.relative_to(directory),
|
arcname=file.relative_to(directory).replace('\\', '/'),
|
||||||
compress_type=zipfile.ZIP_DEFLATED,
|
compress_type=zipfile.ZIP_DEFLATED,
|
||||||
)
|
)
|
||||||
return epub_filepath
|
return epub_filepath
|
||||||
|
@ -157,9 +154,10 @@ def compress_epub(directory, epub_filepath):
|
||||||
def extract_epub(epub_filepath, directory):
|
def extract_epub(epub_filepath, directory):
|
||||||
epub_filepath = pathclass.Path(epub_filepath)
|
epub_filepath = pathclass.Path(epub_filepath)
|
||||||
directory = pathclass.Path(directory)
|
directory = pathclass.Path(directory)
|
||||||
|
log.debug('Extracting %s to %s.', epub_filepath.absolute_path, directory.absolute_path)
|
||||||
|
|
||||||
with zipfile.ZipFile(epub_filepath.absolute_path, 'r') as z:
|
with zipfile.ZipFile(epub_filepath, 'r') as z:
|
||||||
z.extractall(directory.absolute_path)
|
z.extractall(directory)
|
||||||
|
|
||||||
# XHTML TOOLS
|
# XHTML TOOLS
|
||||||
################################################################################
|
################################################################################
|
||||||
|
@ -295,6 +293,11 @@ def make_spine_item(id):
|
||||||
# DECORATORS
|
# DECORATORS
|
||||||
################################################################################
|
################################################################################
|
||||||
def writes(method):
|
def writes(method):
|
||||||
|
'''
|
||||||
|
Indicates that the given method performs write operations to files inside
|
||||||
|
the book. The decorated method will raise ReadOnly if the book was opened
|
||||||
|
in read-only mode.
|
||||||
|
'''
|
||||||
@functools.wraps(method)
|
@functools.wraps(method)
|
||||||
def wrapped_method(self, *args, **kwargs):
|
def wrapped_method(self, *args, **kwargs):
|
||||||
if self.read_only:
|
if self.read_only:
|
||||||
|
@ -335,7 +338,6 @@ class NotInSpine(EpubfileException):
|
||||||
class ReadOnly(EpubfileException):
|
class ReadOnly(EpubfileException):
|
||||||
error_message = 'Can\'t do {} in read-only mode.'
|
error_message = 'Can\'t do {} in read-only mode.'
|
||||||
|
|
||||||
|
|
||||||
class Epub:
|
class Epub:
|
||||||
def __init__(self, epub_path, *, read_only=False):
|
def __init__(self, epub_path, *, read_only=False):
|
||||||
'''
|
'''
|
||||||
|
@ -366,7 +368,7 @@ class Epub:
|
||||||
|
|
||||||
def __init_from_dir(self, directory):
|
def __init_from_dir(self, directory):
|
||||||
self.is_zip = False
|
self.is_zip = False
|
||||||
self.root_directory = pathclass.Path(directory, force_sep='/')
|
self.root_directory = pathclass.Path(directory)
|
||||||
|
|
||||||
def __init_from_file_read_only(self, epub_path):
|
def __init_from_file_read_only(self, epub_path):
|
||||||
# It may appear that is_zip is a synonym for read_only, but don't forget
|
# It may appear that is_zip is a synonym for read_only, but don't forget
|
||||||
|
@ -374,8 +376,8 @@ class Epub:
|
||||||
# readonly dirs don't need a special init, all they have to do is
|
# readonly dirs don't need a special init, all they have to do is
|
||||||
# forbid writes.
|
# forbid writes.
|
||||||
self.is_zip = True
|
self.is_zip = True
|
||||||
self.root_directory = pathclass.Path(epub_path, force_sep='/')
|
self.root_directory = pathclass.Path(epub_path)
|
||||||
self.zip = zipfile.ZipFile(self.root_directory.absolute_path)
|
self.zip = zipfile.ZipFile(self.root_directory)
|
||||||
|
|
||||||
def __init_from_file(self, epub_path):
|
def __init_from_file(self, epub_path):
|
||||||
extract_to = tempfile.TemporaryDirectory(prefix='epubfile-')
|
extract_to = tempfile.TemporaryDirectory(prefix='epubfile-')
|
||||||
|
@ -411,9 +413,22 @@ class Epub:
|
||||||
If the book was opened as a read-only zip, we can read files out of
|
If the book was opened as a read-only zip, we can read files out of
|
||||||
the zip.
|
the zip.
|
||||||
'''
|
'''
|
||||||
p_path = self.root_directory.spawn(path)
|
# When reading from a zip, root_directory is the zip file itself.
|
||||||
if p_path in self.root_directory:
|
# So if the user is trying to read a filepath called
|
||||||
path = p_path.relative_to(self.root_directory, simple=True)
|
# D:\book.epub\dir1\file1.html, we need to convert it to the relative
|
||||||
|
# path dir1\file1.html
|
||||||
|
# But if they have already given us the relative path, we keep that.
|
||||||
|
normalized = path
|
||||||
|
if not isinstance(normalized, pathclass.Path):
|
||||||
|
normalized = pathclass.Path(normalized)
|
||||||
|
|
||||||
|
if normalized in self.root_directory:
|
||||||
|
# The given path was an absolute path including the epub.
|
||||||
|
path = normalized.relative_to(self.root_directory, simple=True)
|
||||||
|
else:
|
||||||
|
# The given path was either a relative path already inside the epub,
|
||||||
|
# or an absolute path somewhere totally wrong.
|
||||||
|
path = os.fspath(path)
|
||||||
|
|
||||||
# Zip files always use forward slash internally, even on Windows.
|
# Zip files always use forward slash internally, even on Windows.
|
||||||
path = path.replace('\\', '/')
|
path = path.replace('\\', '/')
|
||||||
|
@ -422,7 +437,7 @@ class Epub:
|
||||||
return self.zip.open(path, 'r')
|
return self.zip.open(path, 'r')
|
||||||
if mode == 'r':
|
if mode == 'r':
|
||||||
return io.TextIOWrapper(self.zip.open(path, 'r'), encoding)
|
return io.TextIOWrapper(self.zip.open(path, 'r'), encoding)
|
||||||
# At this time ZipFS is only used for read-only epubs anyway.
|
# At this time fopen_zip is only used for read-only epubs anyway.
|
||||||
if mode == 'wb':
|
if mode == 'wb':
|
||||||
return self.zip.open(path, 'w')
|
return self.zip.open(path, 'w')
|
||||||
if mode == 'w':
|
if mode == 'w':
|
||||||
|
@ -459,7 +474,7 @@ class Epub:
|
||||||
# Ensure we have a mimetype file.
|
# Ensure we have a mimetype file.
|
||||||
mimetype_file = self.root_directory.with_child('mimetype')
|
mimetype_file = self.root_directory.with_child('mimetype')
|
||||||
if not mimetype_file.exists:
|
if not mimetype_file.exists:
|
||||||
with self._fopen(mimetype_file.absolute_path, 'w', encoding='utf-8') as handle:
|
with self._fopen(mimetype_file, 'w', encoding='utf-8') as handle:
|
||||||
handle.write(MIMETYPE_FILE_TEMPLATE)
|
handle.write(MIMETYPE_FILE_TEMPLATE)
|
||||||
|
|
||||||
# Assert that all manifest items exist on disk.
|
# Assert that all manifest items exist on disk.
|
||||||
|
@ -493,7 +508,7 @@ class Epub:
|
||||||
writefile(root.join('mimetype'), MIMETYPE_FILE_TEMPLATE)
|
writefile(root.join('mimetype'), MIMETYPE_FILE_TEMPLATE)
|
||||||
writefile(root.join('META-INF/container.xml'), CONTAINER_XML_TEMPLATE)
|
writefile(root.join('META-INF/container.xml'), CONTAINER_XML_TEMPLATE)
|
||||||
writefile(root.join('OEBPS/content.opf'), OPF_TEMPLATE.format(uuid=uid))
|
writefile(root.join('OEBPS/content.opf'), OPF_TEMPLATE.format(uuid=uid))
|
||||||
writefile(root.join('OEBPS/toc.ncx'), NCX_TEMPLATE.format(uuid=uid, title='Unknown', navpoints=''))
|
writefile(root.join('OEBPS/toc.ncx'), NCX_TEMPLATE.format(uuid=uid, navpoints=''))
|
||||||
writefile(root.join('OEBPS/Text/nav.xhtml'), NAV_XHTML_TEMPLATE.format(toc_contents=''))
|
writefile(root.join('OEBPS/Text/nav.xhtml'), NAV_XHTML_TEMPLATE.format(toc_contents=''))
|
||||||
|
|
||||||
return cls(tempdir)
|
return cls(tempdir)
|
||||||
|
@ -518,15 +533,15 @@ class Epub:
|
||||||
|
|
||||||
def read_container_xml(self):
|
def read_container_xml(self):
|
||||||
container_xml_path = self.root_directory.join('META-INF/container.xml')
|
container_xml_path = self.root_directory.join('META-INF/container.xml')
|
||||||
container = self._fopen(container_xml_path.absolute_path, 'r', encoding='utf-8')
|
container = self._fopen(container_xml_path, 'r', encoding='utf-8')
|
||||||
# 'xml' and 'html.parser' seem about even here except that html.parser
|
# 'xml' and 'html.parser' seem about even here except that html.parser
|
||||||
# doesn't self-close.
|
# doesn't self-close.
|
||||||
container = bs4.BeautifulSoup(container, 'xml')
|
container = bs4.BeautifulSoup(container, 'xml')
|
||||||
return container
|
return container
|
||||||
|
|
||||||
def read_opf(self, rootfile):
|
def read_opf(self, rootfile):
|
||||||
rootfile = pathclass.Path(rootfile, force_sep='/')
|
rootfile = pathclass.Path(rootfile)
|
||||||
rootfile_xml = self._fopen(rootfile.absolute_path, 'r', encoding='utf-8').read()
|
rootfile_xml = self._fopen(rootfile, 'r', encoding='utf-8').read()
|
||||||
# 'html.parser' preserves namespacing the best, but unfortunately it
|
# 'html.parser' preserves namespacing the best, but unfortunately it
|
||||||
# botches the <meta> items because it wants them to be self-closing
|
# botches the <meta> items because it wants them to be self-closing
|
||||||
# and the string contents come out. We will fix in just a moment.
|
# and the string contents come out. We will fix in just a moment.
|
||||||
|
@ -554,12 +569,12 @@ class Epub:
|
||||||
if isinstance(container, bs4.BeautifulSoup):
|
if isinstance(container, bs4.BeautifulSoup):
|
||||||
container = str(container)
|
container = str(container)
|
||||||
container_xml_path = self.root_directory.join('META-INF/container.xml')
|
container_xml_path = self.root_directory.join('META-INF/container.xml')
|
||||||
container_xml = self._fopen(container_xml_path.absolute_path, 'w', encoding='utf-8')
|
container_xml = self._fopen(container_xml_path, 'w', encoding='utf-8')
|
||||||
container_xml.write(container)
|
container_xml.write(container)
|
||||||
|
|
||||||
@writes
|
@writes
|
||||||
def write_opf(self):
|
def write_opf(self):
|
||||||
with self._fopen(self.opf_filepath.absolute_path, 'w', encoding='utf-8') as rootfile:
|
with self._fopen(self.opf_filepath, 'w', encoding='utf-8') as rootfile:
|
||||||
rootfile.write(str(self.opf))
|
rootfile.write(str(self.opf))
|
||||||
|
|
||||||
# FILE OPERATIONS
|
# FILE OPERATIONS
|
||||||
|
@ -582,16 +597,16 @@ class Epub:
|
||||||
content = fix_xhtml(content)
|
content = fix_xhtml(content)
|
||||||
|
|
||||||
if isinstance(content, str):
|
if isinstance(content, str):
|
||||||
handle = self._fopen(filepath.absolute_path, 'w', encoding='utf-8')
|
handle = self._fopen(filepath, 'w', encoding='utf-8')
|
||||||
elif isinstance(content, bytes):
|
elif isinstance(content, bytes):
|
||||||
handle = self._fopen(filepath.absolute_path, 'wb')
|
handle = self._fopen(filepath, 'wb')
|
||||||
else:
|
else:
|
||||||
raise TypeError(f'content should be str or bytes, not {type(content)}.')
|
raise TypeError(f'content should be str or bytes, not {type(content)}.')
|
||||||
|
|
||||||
with handle:
|
with handle:
|
||||||
handle.write(content)
|
handle.write(content)
|
||||||
|
|
||||||
href = filepath.relative_to(self.opf_filepath.parent, simple=True)
|
href = filepath.relative_to(self.opf_filepath.parent, simple=True).replace('\\', '/')
|
||||||
href = urllib.parse.quote(href)
|
href = urllib.parse.quote(href)
|
||||||
|
|
||||||
manifest_item = make_manifest_item(id, href, mime)
|
manifest_item = make_manifest_item(id, href, mime)
|
||||||
|
@ -610,7 +625,7 @@ class Epub:
|
||||||
automatically generated.
|
automatically generated.
|
||||||
'''
|
'''
|
||||||
filepath = pathclass.Path(filepath)
|
filepath = pathclass.Path(filepath)
|
||||||
with self._fopen(filepath.absolute_path, 'rb') as handle:
|
with self._fopen(filepath, 'rb') as handle:
|
||||||
return self.add_file(
|
return self.add_file(
|
||||||
id=filepath.basename,
|
id=filepath.basename,
|
||||||
basename=filepath.basename,
|
basename=filepath.basename,
|
||||||
|
@ -626,7 +641,7 @@ class Epub:
|
||||||
spine_item = self.opf.spine.find('itemref', {'idref': id})
|
spine_item = self.opf.spine.find('itemref', {'idref': id})
|
||||||
if spine_item:
|
if spine_item:
|
||||||
spine_item.extract()
|
spine_item.extract()
|
||||||
os.remove(filepath.absolute_path)
|
os.remove(filepath)
|
||||||
|
|
||||||
def get_filepath(self, id):
|
def get_filepath(self, id):
|
||||||
href = self.get_manifest_item(id)['href']
|
href = self.get_manifest_item(id)['href']
|
||||||
|
@ -655,9 +670,9 @@ class Epub:
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_text:
|
if is_text:
|
||||||
handle = self._fopen(filepath.absolute_path, mode, encoding='utf-8')
|
handle = self._fopen(filepath, mode, encoding='utf-8')
|
||||||
else:
|
else:
|
||||||
handle = self._fopen(filepath.absolute_path, mode + 'b')
|
handle = self._fopen(filepath, mode + 'b')
|
||||||
|
|
||||||
return handle
|
return handle
|
||||||
|
|
||||||
|
@ -684,7 +699,7 @@ class Epub:
|
||||||
if not new_filepath.extension:
|
if not new_filepath.extension:
|
||||||
new_filepath = new_filepath.add_extension(old_filepath.extension)
|
new_filepath = new_filepath.add_extension(old_filepath.extension)
|
||||||
self.assert_file_not_exists(new_filepath)
|
self.assert_file_not_exists(new_filepath)
|
||||||
os.rename(old_filepath.absolute_path, new_filepath.absolute_path)
|
os.rename(old_filepath, new_filepath)
|
||||||
rename_map[old_filepath] = new_filepath
|
rename_map[old_filepath] = new_filepath
|
||||||
|
|
||||||
if fix_interlinking:
|
if fix_interlinking:
|
||||||
|
@ -826,7 +841,7 @@ class Epub:
|
||||||
|
|
||||||
current_meta = self.opf.metadata.find('meta', {'name': 'cover'})
|
current_meta = self.opf.metadata.find('meta', {'name': 'cover'})
|
||||||
if current_meta:
|
if current_meta:
|
||||||
current_meta[content] = id
|
current_meta['content'] = id
|
||||||
else:
|
else:
|
||||||
meta = make_meta_item(attrs={'name': 'cover', 'content': id})
|
meta = make_meta_item(attrs={'name': 'cover', 'content': id})
|
||||||
self.opf.metadata.append(meta)
|
self.opf.metadata.append(meta)
|
||||||
|
@ -838,7 +853,6 @@ class Epub:
|
||||||
if linear_only:
|
if linear_only:
|
||||||
items = [x for x in items if x.get('linear') != 'no']
|
items = [x for x in items if x.get('linear') != 'no']
|
||||||
return [x['idref'] for x in items]
|
return [x['idref'] for x in items]
|
||||||
return ids
|
|
||||||
|
|
||||||
@writes
|
@writes
|
||||||
def set_spine_order(self, ids):
|
def set_spine_order(self, ids):
|
||||||
|
@ -980,7 +994,7 @@ class Epub:
|
||||||
if new_filepath is None:
|
if new_filepath is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
link = link._replace(path=new_filepath.relative_to(relative_to, simple=True))
|
link = link._replace(path=new_filepath.relative_to(relative_to, simple=True).replace('\\', '/'))
|
||||||
link = link._replace(path=urllib.parse.quote(link.path))
|
link = link._replace(path=urllib.parse.quote(link.path))
|
||||||
|
|
||||||
return link.geturl()
|
return link.geturl()
|
||||||
|
@ -1236,10 +1250,10 @@ class Epub:
|
||||||
hash_anchor = f'#{header["id"]}'
|
hash_anchor = f'#{header["id"]}'
|
||||||
|
|
||||||
if nav_id:
|
if nav_id:
|
||||||
relative = file_path.relative_to(nav_filepath.parent, simple=True)
|
relative = file_path.relative_to(nav_filepath.parent, simple=True).replace('\\', '/')
|
||||||
toc_line['nav_anchor'] = f'{relative}{hash_anchor}'
|
toc_line['nav_anchor'] = f'{relative}{hash_anchor}'
|
||||||
if ncx_id:
|
if ncx_id:
|
||||||
relative = file_path.relative_to(ncx_filepath.parent, simple=True)
|
relative = file_path.relative_to(ncx_filepath.parent, simple=True).replace('\\', '/')
|
||||||
toc_line['ncx_anchor'] = f'{relative}{hash_anchor}'
|
toc_line['ncx_anchor'] = f'{relative}{hash_anchor}'
|
||||||
|
|
||||||
if current_list['level'] is None:
|
if current_list['level'] is None:
|
||||||
|
@ -1321,7 +1335,7 @@ class Epub:
|
||||||
# location of all all manifest item hrefs.
|
# location of all all manifest item hrefs.
|
||||||
manifest_items = self.get_manifest_items(soup=True)
|
manifest_items = self.get_manifest_items(soup=True)
|
||||||
old_filepaths = {item['id']: self.get_filepath(item['id']) for item in manifest_items}
|
old_filepaths = {item['id']: self.get_filepath(item['id']) for item in manifest_items}
|
||||||
old_ncx = self.get_ncx()
|
|
||||||
try:
|
try:
|
||||||
old_ncx_parent = self.get_filepath(self.get_ncx()).parent
|
old_ncx_parent = self.get_filepath(self.get_ncx()).parent
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -1332,10 +1346,10 @@ class Epub:
|
||||||
oebps.makedirs(exist_ok=True)
|
oebps.makedirs(exist_ok=True)
|
||||||
self.write_opf()
|
self.write_opf()
|
||||||
new_opf_path = oebps.with_child(self.opf_filepath.basename)
|
new_opf_path = oebps.with_child(self.opf_filepath.basename)
|
||||||
os.rename(self.opf_filepath.absolute_path, new_opf_path.absolute_path)
|
os.rename(self.opf_filepath, new_opf_path)
|
||||||
container = self.read_container_xml()
|
container = self.read_container_xml()
|
||||||
rootfile = container.find('rootfile', {'full-path': self.opf_filepath.basename})
|
rootfile = container.find('rootfile', {'full-path': self.opf_filepath.basename})
|
||||||
rootfile['full-path'] = new_opf_path.relative_to(self.root_directory, simple=True)
|
rootfile['full-path'] = new_opf_path.relative_to(self.root_directory, simple=True).replace('\\', '/')
|
||||||
self.write_container_xml(container)
|
self.write_container_xml(container)
|
||||||
self.opf_filepath = new_opf_path
|
self.opf_filepath = new_opf_path
|
||||||
|
|
||||||
|
@ -1348,15 +1362,15 @@ class Epub:
|
||||||
if directory.exists:
|
if directory.exists:
|
||||||
# On Windows, this will fix any incorrect casing.
|
# On Windows, this will fix any incorrect casing.
|
||||||
# On Linux it is inert.
|
# On Linux it is inert.
|
||||||
os.rename(directory.absolute_path, directory.absolute_path)
|
os.rename(directory, directory)
|
||||||
else:
|
else:
|
||||||
directory.makedirs()
|
directory.makedirs()
|
||||||
|
|
||||||
new_filepath = directory.with_child(old_filepath.basename)
|
new_filepath = directory.with_child(old_filepath.basename)
|
||||||
if new_filepath.absolute_path != old_filepath.absolute_path:
|
if new_filepath.absolute_path != old_filepath.absolute_path:
|
||||||
rename_map[old_filepath] = new_filepath
|
rename_map[old_filepath] = new_filepath
|
||||||
os.rename(old_filepath.absolute_path, new_filepath.absolute_path)
|
os.rename(old_filepath, new_filepath)
|
||||||
manifest_item['href'] = new_filepath.relative_to(self.opf_filepath.parent, simple=True)
|
manifest_item['href'] = new_filepath.relative_to(self.opf_filepath.parent, simple=True).replace('\\', '/')
|
||||||
|
|
||||||
self.fix_interlinking_opf(rename_map)
|
self.fix_interlinking_opf(rename_map)
|
||||||
for id in self.get_texts():
|
for id in self.get_texts():
|
||||||
|
@ -1372,7 +1386,6 @@ class Epub:
|
||||||
if item['href'] in ['toc.ncx', 'Misc/toc.ncx']:
|
if item['href'] in ['toc.ncx', 'Misc/toc.ncx']:
|
||||||
item['media-type'] = 'application/x-dtbncx+xml'
|
item['media-type'] = 'application/x-dtbncx+xml'
|
||||||
|
|
||||||
|
|
||||||
# COMMAND LINE TOOLS
|
# COMMAND LINE TOOLS
|
||||||
################################################################################
|
################################################################################
|
||||||
import argparse
|
import argparse
|
||||||
|
@ -1382,13 +1395,12 @@ import string
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from voussoirkit import betterhelp
|
from voussoirkit import betterhelp
|
||||||
from voussoirkit import winglob
|
|
||||||
|
|
||||||
DOCSTRING = '''
|
DOCSTRING = '''
|
||||||
Epubfile
|
Epubfile
|
||||||
========
|
========
|
||||||
|
|
||||||
A simple python .epub scripting tool.
|
A simple Python .epub scripting tool.
|
||||||
|
|
||||||
{addfile}
|
{addfile}
|
||||||
|
|
||||||
|
@ -1402,6 +1414,8 @@ A simple python .epub scripting tool.
|
||||||
|
|
||||||
{merge}
|
{merge}
|
||||||
|
|
||||||
|
{new}
|
||||||
|
|
||||||
{normalize}
|
{normalize}
|
||||||
|
|
||||||
{setfont}
|
{setfont}
|
||||||
|
@ -1454,8 +1468,12 @@ generate_toc:
|
||||||
|
|
||||||
holdit='''
|
holdit='''
|
||||||
holdit:
|
holdit:
|
||||||
Extract the book so that you can manually edit the files on disk, then save
|
Extract the book so that you can manually edit the files on disk, then
|
||||||
the changes back into the original file.
|
compress them back into the original file.
|
||||||
|
|
||||||
|
This is helpful when you want to do some file processing that is outside of
|
||||||
|
epubfile's scope. epubfile will save you the effort of extracting and
|
||||||
|
compressing the epub so you can focus on doing the file operations.
|
||||||
|
|
||||||
> epubfile.py holdit book.epub
|
> epubfile.py holdit book.epub
|
||||||
'''.strip(),
|
'''.strip(),
|
||||||
|
@ -1468,7 +1486,7 @@ merge:
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
--demote_headers:
|
--demote_headers:
|
||||||
All h1 in the book will be demoted to h2, and so forth. So that the
|
All h1 in the book will be demoted to h2, and so forth, so that the
|
||||||
headerfiles are the only h1s and the table of contents will generate
|
headerfiles are the only h1s and the table of contents will generate
|
||||||
with a good hierarchy.
|
with a good hierarchy.
|
||||||
|
|
||||||
|
@ -1479,7 +1497,7 @@ merge:
|
||||||
In the headerfile, the <h1> will start with the book's index, like
|
In the headerfile, the <h1> will start with the book's index, like
|
||||||
"01. First Book"
|
"01. First Book"
|
||||||
|
|
||||||
-y | --autoyes:
|
--yes:
|
||||||
Overwrite the output file without prompting.
|
Overwrite the output file without prompting.
|
||||||
'''.strip(),
|
'''.strip(),
|
||||||
|
|
||||||
|
@ -1490,7 +1508,7 @@ new:
|
||||||
> epubfile.py new book.epub <flags>
|
> epubfile.py new book.epub <flags>
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
-y | --autoyes:
|
--yes:
|
||||||
Overwrite the file without prompting.
|
Overwrite the file without prompting.
|
||||||
'''.strip(),
|
'''.strip(),
|
||||||
|
|
||||||
|
@ -1511,7 +1529,11 @@ setfont:
|
||||||
A stylesheet called epubfile_setfont.css will be created that sets
|
A stylesheet called epubfile_setfont.css will be created that sets
|
||||||
* { font-family: ... !important } with a font file of your choice.
|
* { font-family: ... !important } with a font file of your choice.
|
||||||
|
|
||||||
> epubfile.py setfont book.epub font.ttf
|
> epubfile.py setfont book.epub font.ttf <flags>
|
||||||
|
|
||||||
|
flags:
|
||||||
|
--yes:
|
||||||
|
Overwrite the epubfile_setfont.css without prompting.
|
||||||
'''.strip(),
|
'''.strip(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1525,7 +1547,7 @@ def addfile_argparse(args):
|
||||||
|
|
||||||
for pattern in args.files:
|
for pattern in args.files:
|
||||||
for file in pathclass.glob_files(pattern):
|
for file in pathclass.glob_files(pattern):
|
||||||
print(f'Adding file {file.absolute_path}.')
|
log.info('Adding file %s.', file.absolute_path)
|
||||||
try:
|
try:
|
||||||
book.easy_add_file(file)
|
book.easy_add_file(file)
|
||||||
except (IDExists, FileExists) as exc:
|
except (IDExists, FileExists) as exc:
|
||||||
|
@ -1538,6 +1560,7 @@ def addfile_argparse(args):
|
||||||
|
|
||||||
book.move_nav_to_end()
|
book.move_nav_to_end()
|
||||||
book.save(args.epub)
|
book.save(args.epub)
|
||||||
|
return 0
|
||||||
|
|
||||||
def covercomesfirst(book):
|
def covercomesfirst(book):
|
||||||
basenames = {i: book.get_filepath(i).basename for i in book.get_images()}
|
basenames = {i: book.get_filepath(i).basename for i in book.get_images()}
|
||||||
|
@ -1578,35 +1601,39 @@ def covercomesfirst(book):
|
||||||
book.rename_file(rename_map)
|
book.rename_file(rename_map)
|
||||||
|
|
||||||
def covercomesfirst_argparse(args):
|
def covercomesfirst_argparse(args):
|
||||||
epubs = [epub for pattern in args.epubs for epub in winglob.glob(pattern)]
|
epubs = pathclass.glob_many(args.epubs)
|
||||||
for epub in epubs:
|
for epub in epubs:
|
||||||
print(epub)
|
|
||||||
book = Epub(epub)
|
book = Epub(epub)
|
||||||
|
log.info('Moving %s\'s cover.', book)
|
||||||
covercomesfirst(book)
|
covercomesfirst(book)
|
||||||
book.save(epub)
|
book.save(epub)
|
||||||
|
pipeable.stdout(epub.absolute_path)
|
||||||
|
return 0
|
||||||
|
|
||||||
def exec_argparse(args):
|
def exec_argparse(args):
|
||||||
epubs = [epub for pattern in args.epubs for epub in winglob.glob(pattern)]
|
epubs = pathclass.glob_many(args.epubs)
|
||||||
for epub in epubs:
|
for epub in epubs:
|
||||||
print(epub)
|
|
||||||
book = Epub(epub)
|
book = Epub(epub)
|
||||||
exec(args.command)
|
exec(args.command)
|
||||||
book.save(epub)
|
book.save(epub)
|
||||||
|
pipeable.stdout(epub.absolute_path)
|
||||||
|
return 0
|
||||||
|
|
||||||
def generate_toc_argparse(args):
|
def generate_toc_argparse(args):
|
||||||
epubs = [epub for pattern in args.epubs for epub in winglob.glob(pattern)]
|
epubs = pathclass.glob_many(args.epubs)
|
||||||
books = []
|
books = []
|
||||||
for epub in epubs:
|
for epub in epubs:
|
||||||
book = Epub(epub)
|
book = Epub(epub)
|
||||||
book.generate_toc(max_level=int(args.max_level) if args.max_level else None)
|
book.generate_toc(max_level=int(args.max_level) if args.max_level else None)
|
||||||
book.save(epub)
|
book.save(epub)
|
||||||
|
return 0
|
||||||
|
|
||||||
def holdit_argparse(args):
|
def holdit_argparse(args):
|
||||||
epubs = [epub for pattern in args.epubs for epub in winglob.glob(pattern)]
|
epubs = pathclass.glob_many(args.epubs)
|
||||||
books = []
|
books = []
|
||||||
for epub in epubs:
|
for epub in epubs:
|
||||||
book = Epub(epub)
|
book = Epub(epub)
|
||||||
print(f'{epub} = {book.root_directory.absolute_path}')
|
pipeable.stderr(f'{epub} = {book.root_directory.absolute_path}')
|
||||||
books.append((epub, book))
|
books.append((epub, book))
|
||||||
|
|
||||||
input('Press Enter when ready.')
|
input('Press Enter when ready.')
|
||||||
|
@ -1615,10 +1642,11 @@ def holdit_argparse(args):
|
||||||
# So let's re-read it first.
|
# So let's re-read it first.
|
||||||
book.read_opf(book.opf_filepath)
|
book.read_opf(book.opf_filepath)
|
||||||
book.save(epub)
|
book.save(epub)
|
||||||
|
pipeable.stdout(epub.absolute_path)
|
||||||
|
return 0
|
||||||
|
|
||||||
def merge(
|
def merge(
|
||||||
input_filepaths,
|
input_filepaths,
|
||||||
output_filename,
|
|
||||||
demote_headers=False,
|
demote_headers=False,
|
||||||
do_headerfile=False,
|
do_headerfile=False,
|
||||||
number_headerfile=False,
|
number_headerfile=False,
|
||||||
|
@ -1631,7 +1659,7 @@ def merge(
|
||||||
|
|
||||||
# Number books from 1 for human sanity.
|
# Number books from 1 for human sanity.
|
||||||
for (index, input_filepath) in enumerate(input_filepaths, start=1):
|
for (index, input_filepath) in enumerate(input_filepaths, start=1):
|
||||||
print(f'Merging {input_filepath.absolute_path}.')
|
log.info('Merging %s.', input_filepath.absolute_path)
|
||||||
prefix = f'{rand_prefix}_{index:>0{index_length}}_{{}}'
|
prefix = f'{rand_prefix}_{index:>0{index_length}}_{{}}'
|
||||||
input_book = Epub(input_filepath)
|
input_book = Epub(input_filepath)
|
||||||
input_book.normalize_directory_structure()
|
input_book.normalize_directory_structure()
|
||||||
|
@ -1691,79 +1719,87 @@ def merge(
|
||||||
book.add_file(new_id, new_basename, content)
|
book.add_file(new_id, new_basename, content)
|
||||||
|
|
||||||
book.move_nav_to_end()
|
book.move_nav_to_end()
|
||||||
book.save(output_filename)
|
return book
|
||||||
|
|
||||||
def merge_argparse(args):
|
def merge_argparse(args):
|
||||||
if os.path.exists(args.output):
|
output = pathclass.Path(args.output)
|
||||||
|
|
||||||
|
if output.exists:
|
||||||
if not (args.autoyes or interactive.getpermission(f'Overwrite {args.output}?')):
|
if not (args.autoyes or interactive.getpermission(f'Overwrite {args.output}?')):
|
||||||
raise ValueError(f'{args.output} exists.')
|
raise ValueError(f'{args.output} exists.')
|
||||||
|
|
||||||
return merge(
|
book = merge(
|
||||||
input_filepaths=args.epubs,
|
input_filepaths=args.epubs,
|
||||||
output_filename=args.output,
|
|
||||||
demote_headers=args.demote_headers,
|
demote_headers=args.demote_headers,
|
||||||
do_headerfile=args.headerfile,
|
do_headerfile=args.headerfile,
|
||||||
number_headerfile=args.number_headerfile,
|
number_headerfile=args.number_headerfile,
|
||||||
)
|
)
|
||||||
|
book.save(output)
|
||||||
|
pipeable.stdout(output.absolute_path)
|
||||||
|
return 0
|
||||||
|
|
||||||
def new_book_argparse(args):
|
def new_book_argparse(args):
|
||||||
if os.path.exists(args.epub):
|
output = pathclass.Path(args.epub)
|
||||||
|
if output.exists:
|
||||||
if not (args.autoyes or interactive.getpermission(f'Overwrite {args.epub}?')):
|
if not (args.autoyes or interactive.getpermission(f'Overwrite {args.epub}?')):
|
||||||
raise ValueError(f'{args.epub} exists.')
|
raise ValueError(f'{output.absolute_path} exists.')
|
||||||
book = Epub.new()
|
book = Epub.new()
|
||||||
book.save(args.epub)
|
book.save(output)
|
||||||
|
pipeable.stdout(output.absolute_path)
|
||||||
|
return 0
|
||||||
|
|
||||||
def normalize_argparse(args):
|
def normalize_argparse(args):
|
||||||
epubs = [epub for pattern in args.epubs for epub in winglob.glob(pattern)]
|
epubs = pathclass.glob_many(args.epubs)
|
||||||
for epub in epubs:
|
for epub in epubs:
|
||||||
print(epub)
|
log.info('Normalizing %s.', epub.absolute_path)
|
||||||
book = Epub(epub)
|
book = Epub(epub)
|
||||||
book.normalize_opf()
|
book.normalize_opf()
|
||||||
book.normalize_directory_structure()
|
book.normalize_directory_structure()
|
||||||
book.move_nav_to_end()
|
book.move_nav_to_end()
|
||||||
book.save(epub)
|
book.save(epub)
|
||||||
|
pipeable.stdout(epub.absolute_path)
|
||||||
|
return 0
|
||||||
|
|
||||||
def setfont_argparse(args):
|
def setfont(book, new_font, autoyes=False):
|
||||||
book = Epub(args.epub)
|
|
||||||
|
|
||||||
css_id = 'epubfile_setfont'
|
css_id = 'epubfile_setfont'
|
||||||
css_basename = 'epubfile_setfont.css'
|
css_basename = 'epubfile_setfont.css'
|
||||||
|
|
||||||
|
new_font = pathclass.Path(new_font)
|
||||||
|
new_font.assert_is_file()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
book.assert_id_not_exists(css_id)
|
book.assert_id_not_exists(css_id)
|
||||||
except IDExists:
|
except IDExists:
|
||||||
if not interactive.getpermission(f'Overwrite {css_id}?'):
|
if not (autoyes or interactive.getpermission(f'Overwrite {css_id}?')):
|
||||||
return
|
return
|
||||||
book.delete_file(css_id)
|
book.delete_file(css_id)
|
||||||
|
|
||||||
font = pathclass.Path(args.font)
|
|
||||||
|
|
||||||
for existing_font in book.get_fonts():
|
for existing_font in book.get_fonts():
|
||||||
font_path = book.get_filepath(existing_font)
|
font_path = book.get_filepath(existing_font)
|
||||||
if font_path.basename == font.basename:
|
if font_path.basename == new_font.basename:
|
||||||
font_id = existing_font
|
font_id = existing_font
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
font_id = book.easy_add_file(font)
|
font_id = book.easy_add_file(new_font)
|
||||||
font_path = book.get_filepath(font_id)
|
font_path = book.get_filepath(font_id)
|
||||||
|
|
||||||
# The font_path may have come from an existing font in the book, so we have
|
# The font_path may have come from an existing font in the book, so we have
|
||||||
# no guarantees about its path layout. The css file, however, is definitely
|
# no guarantees about its path layout. The css file, however, is definitely
|
||||||
# going to be inside OEBPS/Styles since we're the ones creating it.
|
# going to be inside OEBPS/Styles since we're the ones creating it.
|
||||||
# So, we should be getting the correct number of .. in the relative path.
|
# So, we should be getting the correct number of .. in the relative path.
|
||||||
family = font_path.basename
|
family = font_path.replace_extension('').basename
|
||||||
relative = font_path.relative_to(book.opf_filepath.parent.with_child('Styles'))
|
relative = font_path.relative_to(book.opf_filepath.parent.with_child('Styles')).replace('\\', '/')
|
||||||
|
|
||||||
css = f'''
|
css = f'''
|
||||||
@font-face {{
|
@font-face {{
|
||||||
font-family: '{family}';
|
font-family: "{family}";
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
src: url("{relative}");
|
src: url("{relative}");
|
||||||
}}
|
}}
|
||||||
|
|
||||||
* {{
|
* {{
|
||||||
font-family: '{family}' !important;
|
font-family: "{family}" !important;
|
||||||
}}
|
}}
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -1776,69 +1812,77 @@ def setfont_argparse(args):
|
||||||
|
|
||||||
for text_id in book.get_texts():
|
for text_id in book.get_texts():
|
||||||
text_path = book.get_filepath(text_id)
|
text_path = book.get_filepath(text_id)
|
||||||
relative = css_path.relative_to(text_path)
|
|
||||||
soup = book.read_file(text_id, soup=True)
|
soup = book.read_file(text_id, soup=True)
|
||||||
head = soup.head
|
head = soup.head
|
||||||
if head.find('link', {'id': css_id}):
|
if head.find('link', {'id': css_id}):
|
||||||
continue
|
continue
|
||||||
link = soup.new_tag('link')
|
link = soup.new_tag('link')
|
||||||
link['id'] = css_id
|
link['id'] = css_id
|
||||||
link['href'] = css_path.relative_to(text_path.parent)
|
link['href'] = css_path.relative_to(text_path.parent).replace('\\', '/')
|
||||||
link['rel'] = 'stylesheet'
|
link['rel'] = 'stylesheet'
|
||||||
link['type'] = 'text/css'
|
link['type'] = 'text/css'
|
||||||
head.append(link)
|
head.append(link)
|
||||||
book.write_file(text_id, soup)
|
book.write_file(text_id, soup)
|
||||||
|
|
||||||
book.save(args.epub)
|
def setfont_argparse(args):
|
||||||
|
epubs = pathclass.glob_many(args.epubs)
|
||||||
|
for epub in epubs:
|
||||||
|
book = Epub(epub)
|
||||||
|
setfont(book, args.font, autoyes=args.autoyes)
|
||||||
|
book.save(epub)
|
||||||
|
pipeable.stdout(epub.absolute_path)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@vlogging.main_decorator
|
||||||
def main(argv):
|
def main(argv):
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
subparsers = parser.add_subparsers()
|
subparsers = parser.add_subparsers()
|
||||||
|
|
||||||
p_addfile = subparsers.add_parser('addfile')
|
p_addfile = subparsers.add_parser('addfile')
|
||||||
p_addfile.add_argument('epub')
|
p_addfile.add_argument('epub')
|
||||||
p_addfile.add_argument('files', nargs='+', default=[])
|
p_addfile.add_argument('files', nargs='+')
|
||||||
p_addfile.set_defaults(func=addfile_argparse)
|
p_addfile.set_defaults(func=addfile_argparse)
|
||||||
|
|
||||||
p_covercomesfirst = subparsers.add_parser('covercomesfirst')
|
p_covercomesfirst = subparsers.add_parser('covercomesfirst')
|
||||||
p_covercomesfirst.add_argument('epubs', nargs='+', default=[])
|
p_covercomesfirst.add_argument('epubs', nargs='+')
|
||||||
p_covercomesfirst.set_defaults(func=covercomesfirst_argparse)
|
p_covercomesfirst.set_defaults(func=covercomesfirst_argparse)
|
||||||
|
|
||||||
p_exec = subparsers.add_parser('exec')
|
p_exec = subparsers.add_parser('exec')
|
||||||
p_exec.add_argument('epubs', nargs='+', default=[])
|
p_exec.add_argument('epubs', nargs='+')
|
||||||
p_exec.add_argument('--command', dest='command', default=None, required=True)
|
p_exec.add_argument('--command', default=None, required=True)
|
||||||
p_exec.set_defaults(func=exec_argparse)
|
p_exec.set_defaults(func=exec_argparse)
|
||||||
|
|
||||||
p_generate_toc = subparsers.add_parser('generate_toc')
|
p_generate_toc = subparsers.add_parser('generate_toc')
|
||||||
p_generate_toc.add_argument('epubs', nargs='+', default=[])
|
p_generate_toc.add_argument('epubs', nargs='+')
|
||||||
p_generate_toc.add_argument('--max_level', '--max-level', dest='max_level', default=None)
|
p_generate_toc.add_argument('--max_level', '--max-level', default=None)
|
||||||
p_generate_toc.set_defaults(func=generate_toc_argparse)
|
p_generate_toc.set_defaults(func=generate_toc_argparse)
|
||||||
|
|
||||||
p_holdit = subparsers.add_parser('holdit')
|
p_holdit = subparsers.add_parser('holdit')
|
||||||
p_holdit.add_argument('epubs', nargs='+', default=[])
|
p_holdit.add_argument('epubs', nargs='+')
|
||||||
p_holdit.set_defaults(func=holdit_argparse)
|
p_holdit.set_defaults(func=holdit_argparse)
|
||||||
|
|
||||||
p_merge = subparsers.add_parser('merge')
|
p_merge = subparsers.add_parser('merge')
|
||||||
p_merge.add_argument('epubs', nargs='+', default=[])
|
p_merge.add_argument('epubs', nargs='+')
|
||||||
p_merge.add_argument('--output', dest='output', default=None, required=True)
|
p_merge.add_argument('--output', required=True)
|
||||||
p_merge.add_argument('--headerfile', dest='headerfile', action='store_true')
|
p_merge.add_argument('--headerfile', action='store_true')
|
||||||
p_merge.add_argument('--demote_headers', '--demote-headers', dest='demote_headers', action='store_true')
|
p_merge.add_argument('--demote_headers', '--demote-headers', action='store_true')
|
||||||
p_merge.add_argument('--number_headerfile', '--number-headerfile', dest='number_headerfile', action='store_true')
|
p_merge.add_argument('--number_headerfile', '--number-headerfile', action='store_true')
|
||||||
p_merge.add_argument('-y', '--autoyes', dest='autoyes', action='store_true')
|
p_merge.add_argument('-y', '--yes', '--autoyes', dest='autoyes', action='store_true')
|
||||||
p_merge.set_defaults(func=merge_argparse)
|
p_merge.set_defaults(func=merge_argparse)
|
||||||
|
|
||||||
p_new = subparsers.add_parser('new')
|
p_new = subparsers.add_parser('new')
|
||||||
p_new.add_argument('epub')
|
p_new.add_argument('epub')
|
||||||
p_new.add_argument('-y', '--autoyes', dest='autoyes', action='store_true')
|
p_new.add_argument('-y', '--yes', '--autoyes', dest='autoyes', action='store_true')
|
||||||
p_new.set_defaults(func=new_book_argparse)
|
p_new.set_defaults(func=new_book_argparse)
|
||||||
|
|
||||||
p_normalize = subparsers.add_parser('normalize')
|
p_normalize = subparsers.add_parser('normalize')
|
||||||
p_normalize.add_argument('epubs', nargs='+', default=[])
|
p_normalize.add_argument('epubs', nargs='+')
|
||||||
p_normalize.set_defaults(func=normalize_argparse)
|
p_normalize.set_defaults(func=normalize_argparse)
|
||||||
|
|
||||||
p_setfont = subparsers.add_parser('setfont')
|
p_setfont = subparsers.add_parser('setfont')
|
||||||
p_setfont.add_argument('epub')
|
p_setfont.add_argument('epubs', nargs='+')
|
||||||
p_setfont.add_argument('font')
|
p_setfont.add_argument('--font', required=True)
|
||||||
|
p_setfont.add_argument('--yes', dest='autoyes', action='store_true')
|
||||||
p_setfont.set_defaults(func=setfont_argparse)
|
p_setfont.set_defaults(func=setfont_argparse)
|
||||||
|
|
||||||
return betterhelp.subparser_main(
|
return betterhelp.subparser_main(
|
||||||
|
|
Loading…
Reference in a new issue