Last active
January 4, 2019 17:36
-
-
Save redxef/07d0db7a4e746d06da55c9f9312c38f7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
## | |
# @file epub-edit.py | |
# @author redxef | |
# @since 2019-01-04 | |
# | |
# @brief Edit metadata of epubs like i like it. | |
## | |
import sys | |
import os | |
import os.path | |
import re | |
import string | |
import shutil | |
import getopt | |
import zipfile | |
import tempfile | |
_SIMULATE_DIR = './book' | |
def mktmpdir(simulate=False): | |
global _SIMULATE_DIR | |
if simulate: | |
try: | |
os.mkdir(_SIMULATE_DIR) | |
except FileExistsError: | |
pass | |
return _SIMULATE_DIR | |
return tempfile.mkdtemp() | |
def rmtmpdir(d=_SIMULATE_DIR): | |
shutil.rmtree(d) | |
def unzip_file(zfile=None, workdir=None): | |
if zfile is None: | |
return | |
if workdir is None: | |
return | |
with zipfile.ZipFile(zfile, 'r') as z: | |
z.extractall(workdir) | |
def zip_dir(d=None, dest=None): | |
if d is None: | |
return | |
if dest is None: | |
return | |
name = shutil.make_archive(dest, 'zip', d) | |
os.rename(name, name[:-4]) | |
def read_file(opf=None): | |
if opf is None: | |
return '' | |
with open(opf, 'r') as f: | |
data = f.read() | |
return data | |
def get_root_file(workdir=None): | |
if workdir is None: | |
return | |
for f in os.listdir(path=workdir): | |
s = '{}/{}'.format(workdir, f) | |
if f == 'container.xml': | |
return s | |
if os.path.isdir(s): | |
return get_root_file(s) | |
return None | |
def get_content_opf(workdir=None): | |
if workdir is None: | |
return | |
text = read_file(get_root_file(workdir=workdir)) | |
r = re.compile('full-path=\"([^\"]*)\"') | |
result = r.search(text) | |
return '{}/{}'.format(workdir, result.group(1)) | |
def write_file(opf=None, data=None): | |
if opf is None: | |
return | |
if data is None: | |
return | |
with open(opf, 'w') as f: | |
f.write(data) | |
def replace_str(text=None, start=None, stop=None, replacement=None): | |
if text is None: | |
return | |
if start is None: | |
return | |
if stop is None: | |
return | |
if replacement is None: | |
return | |
start_idx = text.find(start) | |
stop_idx = text.find(stop, start_idx) | |
return '{}{}{}'.format(text[:start_idx], replacement, text[stop_idx+len(stop):]) | |
def set_creator(text=None, surname=None, name=None): | |
if text is None: | |
return | |
if surname is None: | |
return | |
if name is None: | |
return | |
text = replace_str(text=text, start='<dc:creator', stop='</dc:creator>', replacement= | |
'<dc:creator opf:role="aut" opf:file-as="{sname}, {name}">{name} {sname}</dc:creator>'.format(sname=surname, name=name)) | |
return text | |
def set_title(text=None, title=None): | |
if text is None: | |
return | |
if title is None: | |
return | |
text = replace_str(text=text, start='<meta name="calibre:title_sort"', stop='/>', replacement= | |
'<meta name="calibre:title_sort" content="{}"/>'.format(title)) | |
text = replace_str(text=text, start='<dc:title', stop='</dc:title>', replacement= | |
'<dc:title>{}</dc:title>'.format(title)) | |
return text | |
def sanitze_file_name_str(s): | |
s = s.lower() | |
s = s.replace(' ', '-') | |
s_ = '' | |
for i in range(len(s)): | |
if s[i] not in (string.ascii_lowercase + string.digits + '-_'): | |
continue | |
s_ += s[i] | |
return s_ | |
def generate_file_name(surname, name, series, title, series_nr, title_nr): | |
surname = sanitze_file_name_str(surname) | |
name = sanitze_file_name_str(name) | |
series = sanitze_file_name_str(series) | |
title = sanitze_file_name_str(title) | |
return '{lsurname}-{lname}---{series_nr:0>2d}-{title_nr:0>2d}---{lseries}---{ltitle}.epub'.format( | |
lsurname=surname, lname=name, series_nr=series_nr, title_nr=title_nr, lseries=series, ltitle=title) | |
def generate_title(surname, name, series, title, series_nr, title_nr): | |
return '{series} #{title_nr} {title}'.format(series=series, title_nr=title_nr, title=title) | |
def main(argc, argv): | |
debug = False | |
file = None | |
destination = None | |
surname = None | |
name = None | |
series = None | |
title = None | |
series_nr = None | |
title_nr = None | |
opts, args = getopt.getopt(argv[1:], "d:N:n:s:t:P:p:hD", | |
['destination=', 'surname=', 'name=', 'series=', 'title=', 'series-nr=', 'title-nr=', 'help', 'debug']) | |
for o, a in opts: | |
if o in ('-d', '--destination'): | |
destination = a | |
if o in ('-N', '--surname'): | |
surname = a | |
if o in ('-n', '--name'): | |
name = a | |
elif o in ('-s', '--series'): | |
series = a | |
elif o in ('-t', '--title'): | |
title = a | |
elif o in ('-P', '--series-nr'): | |
series_nr = int(a) | |
elif o in ('-p', '--title-nr'): | |
title_nr = int(a) | |
elif o in ('-h', '--help'): | |
print( | |
"""NAME | |
epub-edit - rewrite some metadata of ebooks (epub) and save them | |
SYNOPSIS | |
epub-edit [-d DESTINATION] [--destination DESTINATION] [-N SURNAME] | |
[--surname SURNAME] [-n NAME] [--name NAME] [-s SERIES] | |
[--series SERIES] [-t TITLE] [--title TITLE] [-P SERIES_NR] | |
[--series-nr SERIES_NR] [-p TITLE_NR] [--title-nr TITLE_NR] file | |
DESCRIPTION | |
epub-edit unpacks the specified file in a temporary directory and | |
modifies it's metadata. Afterwards it repacks the book and saves it | |
to the specified destination. If destination is a directory | |
it formats the name according to a builtin format string, otherwise | |
the destination is interpreted as file path. | |
OPTIONS | |
-d, --destination=DESTINATION | |
The location where to save the file. | |
-N, --surname=SURNAME | |
The surname of the author. | |
-n, --name=NAME | |
The name of the author. | |
-s, --series=SERIES | |
The name of the book series. | |
-t, --title=TITLE | |
The title of the book. | |
-P, --series-nr=SERIES_NR | |
The number of the series (maybe chronological or the order in | |
which books have been released by the author). | |
-p, --title-nr=TITLE_NR | |
The chronological number of the title in the series. | |
-h, --help | |
Displays this help dialog. | |
-D, --debug | |
Enables debug mode. | |
""" | |
) | |
return 1 | |
elif o in ('-D', '--debug'): | |
debug = True | |
if len(args) == 1: | |
file = args[0] | |
if file is None: | |
print('file missing, the argument should be the file') | |
return 1 | |
if destination is None: | |
print('no destination specified') | |
return 1 | |
if surname is None: | |
print('no surname specified') | |
return 1 | |
if name is None: | |
print('no name specified') | |
return 1 | |
if series is None: | |
print('np series specified') | |
return 1 | |
if title is None: | |
print('no title specified') | |
return 1 | |
if series_nr is None: | |
print('no series-nr specified') | |
return 1 | |
if title_nr is None: | |
print('no title-nr specified') | |
return 1 | |
if os.path.isdir(destination): | |
destination = '{}/{}'.format(destination, generate_file_name(surname, name, series, title, series_nr, title_nr)) | |
workdir = mktmpdir(simulate=debug) | |
unzip_file(zfile=file, workdir=workdir) | |
fcopf = get_content_opf(workdir) | |
scopf = read_file(fcopf) | |
scopf = set_creator(scopf, surname=surname, name=name) | |
scopf = set_title(scopf, generate_title(surname, name, series, title, series_nr, title_nr)) | |
# only modify the destination if something has changed | |
if read_file(fcopf) != scopf: | |
if debug: | |
print('new file:', file=sys.stderr) | |
print(scopf, file=sys.stderr) | |
print('old file', file=sys.stderr) | |
print(read_file(fcopf), file=sys.stderr) | |
write_file(fcopf, scopf) | |
zip_dir(workdir, destination) | |
else: | |
if not os.path.samefile(file, destination): | |
os.rename(file, destination) | |
rmtmpdir(workdir) | |
print(destination) | |
return 0 | |
if __name__ == '__main__': | |
sys.exit(main(len(sys.argv), sys.argv)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment