diff options
author | Joris | 2023-09-17 12:23:47 +0200 |
---|---|---|
committer | Joris | 2023-09-17 12:23:47 +0200 |
commit | 1ebc55c72a1a17293bbf4ad86e0177a10a794750 (patch) | |
tree | 5fce0ea3a011ccbae85b0d3927f8ac33099585fb /cli | |
parent | c236facb4d4c277773c83f1a4ee85b48833d7e67 (diff) |
Make app packageable
Diffstat (limited to 'cli')
-rw-r--r-- | cli/__init__.py | 0 | ||||
-rw-r--r-- | cli/library/__init__.py | 0 | ||||
-rw-r--r-- | cli/library/command.py | 21 | ||||
-rw-r--r-- | cli/main.py | 80 | ||||
-rw-r--r-- | cli/new/__init__.py | 0 | ||||
-rw-r--r-- | cli/new/command.py | 88 | ||||
-rw-r--r-- | cli/new/format.py | 68 | ||||
-rw-r--r-- | cli/new/reader.py | 55 | ||||
-rw-r--r-- | cli/view/__init__.py | 0 | ||||
-rw-r--r-- | cli/view/command.py | 16 |
10 files changed, 328 insertions, 0 deletions
diff --git a/cli/__init__.py b/cli/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/cli/__init__.py diff --git a/cli/library/__init__.py b/cli/library/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/cli/library/__init__.py diff --git a/cli/library/command.py b/cli/library/command.py new file mode 100644 index 0000000..1c4d20c --- /dev/null +++ b/cli/library/command.py @@ -0,0 +1,21 @@ +import glob +import json +import os +import tomllib + +def run(books_library): + print(get(books_library)) + +def get(books_library): + metadatas = [] + + for path in glob.glob(f'{books_library}/**/metadata.toml', recursive=True): + with open(path, 'rb') as f: + directory = os.path.dirname(os.path.realpath(path)) + metadata = tomllib.load(f) + for p in glob.glob(f'{directory}/cover.*'): + metadata['cover'] = p + break + metadatas.append(metadata) + + return json.dumps(metadatas) diff --git a/cli/main.py b/cli/main.py new file mode 100644 index 0000000..01434fa --- /dev/null +++ b/cli/main.py @@ -0,0 +1,80 @@ +# Manage book library. +# +# Required dependencies: +# +# - python >= 3.11 +# - requests +# - pillow +# - ebook-convert CLI (from calibre) + +import cli.library.command +import cli.new.command +import cli.view.command +import os +import sys + +def main(bin_dir): + match sys.argv: + case [ _, 'new' ]: + books_library = get_books_library() + cli.new.command.run(books_library) + case [ _, 'new', book_source ]: + if os.path.isfile(book_source): + books_library = get_books_library() + cli.new.command.run(books_library, book_source) + else: + print_help(title=f'File not found: {book_source}.') + exit(1) + case [ _, 'library' ]: + books_library = get_books_library() + cli.library.command.run(books_library) + case [ _, 'view' ]: + books_library = get_books_library() + books_browser = get_env_var('BOOKS_BROWSER') + cli.view.command.run(books_library, books_browser, bin_dir) + case [ _, '--help' ]: + print_help() + case [ _, '-h' ]: + print_help() + case _: + print_help('Command not found.') + exit(1) + +def get_books_library(): + books_library = get_env_var('BOOKS_LIBRARY') + if os.path.isdir(books_library): + return books_library + else: + print_help(title=f'BOOKS_LIBRARY {books_library} not found.') + exit(1) + +def get_env_var(key): + value = os.getenv(key) + if value: + return value + else: + print_help(title=f'{key} environment variable is required.') + exit(1) + +def print_help(title='Manage book library'): + print(f"""{title} + +- Insert book entry with optional ebook file: + + $ python {sys.argv[0]} new [path-to-book] + +- Print library metadata as json: + + $ python {sys.argv[0]} library + +- View books in web page: + + $ python {sys.argv[0]} view + +Environment variables: + + BOOKS_LIBRARY: path to book library, + BOOKS_BROWSER: browser command executed to view the library.""") + +if __name__ == "__main__": + main() diff --git a/cli/new/__init__.py b/cli/new/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/cli/new/__init__.py diff --git a/cli/new/command.py b/cli/new/command.py new file mode 100644 index 0000000..9f5e5dc --- /dev/null +++ b/cli/new/command.py @@ -0,0 +1,88 @@ +import PIL.Image +import cli.new.format as format +import cli.new.reader as reader +import io +import os +import pathlib +import requests +import shutil +import subprocess + +def run(books_library, book_source=None): + + # Get data + + title = reader.required('Title') + subtitle = reader.optional('Subtitle') + authors = reader.non_empty_list('Authors') + author_sort = reader.required('Authors sorting') + genres = reader.non_empty_list('Genres') + year = reader.integer('Year') + lang = reader.choices('Lang', ['fr', 'en', 'de']) + summary = format.cleanup_text(reader.multi_line('Summary'), lang) + cover_url = reader.required('Cover url') + read = reader.choices('Read', ['Read', 'Unread', 'Reading', 'Stopped']) + + # Output paths + + author_path = format.path_part(author_sort) + title_path = format.path_part(title) + + output_dir = f'{books_library}/{author_path}/{title_path}' + metadata_path = f'{output_dir}/metadata.toml' + cover_path = f'{output_dir}/cover.webp' + + if not book_source is None: + ext = format.extension(book_source) + book_path = f'{output_dir}/book{ext}' + book_source_dir = os.path.dirname(os.path.realpath(book_source)) + book_source_new = f'{book_source_dir}/{author_path}-{title_path}.mobi' + + # Metadata + + metadata = f"""title = "{title}" + subtitle = "{subtitle}" + authors = {format.format_list(authors)} + authorsSort = "{author_sort}" + genres = {format.format_list(genres)} + date = {year} + summary = \"\"\" + {summary} + \"\"\" + read = "{read}" + """ + + # Ask for confirmation + + print(f'About to create:\n\n- {metadata_path}\n- {cover_path}') + if not book_source is None: + print(f'- {book_path}') + print(f'\nAnd moving:\n\n {book_source},\n -> {book_source_new}.') + print() + + reader.confirm('OK?') + + # Create files + + pathlib.Path(output_dir).mkdir(parents=True, exist_ok=True) + download_cover(cover_url, cover_path) + with open(metadata_path, 'w') as f: + f.write(metadata) + if not book_path is None: + shutil.copyfile(book_source, book_path) + if format.extension(book_source) in ['mobi', 'azw3']: + os.rename(book_source, book_source_new) + else: + subprocess.run(['ebook-convert', book_source, book_source_new]) + os.remove(book_source) + +# Download cover as WEBP +def download_cover(url, path): + response = requests.get(url, headers={ 'User-Agent': 'python-script' }) + image = PIL.Image.open(io.BytesIO(response.content)) + width, height = image.size + if width > 300: + image = image.resize((300, int(300 * height / width)), PIL.Image.LANCZOS) + image = image.convert('RGB') + image.save(path, 'WEBP', optimize=True, quality=85) + diff --git a/cli/new/format.py b/cli/new/format.py new file mode 100644 index 0000000..c004f82 --- /dev/null +++ b/cli/new/format.py @@ -0,0 +1,68 @@ +import pathlib +import re +import unicodedata + +def format_list(xs): + return '[ ' + ', '.join([f'"{x}"' for x in xs]) + ' ]' + +def path_part(name): + simplified = ''.join([alnum_or_space(c) for c in unaccent(name.lower())]) + return '-'.join(simplified.split()) + +def unaccent(s): + return ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn') + +def alnum_or_space(c): + if c.isalnum(): + return c + else: + return ' ' + +def extension(path): + return pathlib.Path(path).suffix + +def cleanup_text(s, lang): + s = re.sub('\'', '’', s) + s = re.sub('\.\.\.', '…', s) + s = re.sub('\. \. \.', '…', s) + s = cleanup_quotes(s, lang) + + if lang == 'fr': + s = re.sub('“', '«', s) + s = re.sub('”', '»', s) + + # Replace space by insecable spaces + s = re.sub(r' ([:?\!»])', r' \1', s) + s = re.sub('« ', '« ', s) + + # Add missing insecable spaces + s = re.sub(r'([^ ])([:?\!»])', r'\1 \2', s) + s = re.sub(r'«([^ ])', r'« \1', s) + + elif lang == 'en': + s = re.sub('«', '“', s) + s = re.sub('»', '”', s) + + return s + +def cleanup_quotes(s, lang): + res = '' + quoted = False + for c in s: + if c == '"': + if quoted: + quoted = False + if lang == 'fr': + res += '»' + elif lang == 'en': + res += '”' + else: + quoted = True + if lang == 'fr': + res += '«' + elif lang == 'en': + res += '“' + else: + res += c + return res + diff --git a/cli/new/reader.py b/cli/new/reader.py new file mode 100644 index 0000000..eacd70b --- /dev/null +++ b/cli/new/reader.py @@ -0,0 +1,55 @@ +def required(label): + value = input(f'{label}: ').strip() + if value: + print() + return value + else: + return required(label) + +def multi_line(label): + lines = '' + print(f'{label}, type [end] to finish:\n') + while True: + value = input() + if value.strip() == '[end]': + break + elif value.strip(): + lines += f'{value.strip()}\n' + print() + return lines.strip() + +def optional(label): + value = input(f'{label} (optional): ').strip() + print() + return value + +def non_empty_list(label): + value = input(f'{label} (separated by commas): ') + values = [x.strip() for x in value.split(',') if x.strip()] + if len(values) > 0: + print() + return values + else: + return non_empty_list(label) + +def integer(label): + value = input(f'{label}: ').strip() + if value.isdigit(): + print() + return int(value) + else: + return integer(label) + +def choices(label, xs): + pp_choices = '/'.join(xs) + value = input(f'{label} [{pp_choices}] ') + if value in xs: + print() + return value + else: + return choices(label, xs) + +def confirm(message): + if choices(message, ['y', 'n']) == 'n': + print('\nStopping.') + exit(1) diff --git a/cli/view/__init__.py b/cli/view/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/cli/view/__init__.py diff --git a/cli/view/command.py b/cli/view/command.py new file mode 100644 index 0000000..72e44dd --- /dev/null +++ b/cli/view/command.py @@ -0,0 +1,16 @@ +import cli.library.command +import shutil +import subprocess +import subprocess +import tempfile +import time + +def run(books_library, books_browser, bin_dir): + tmp_dir = tempfile.mkdtemp() + shutil.copytree(f'{bin_dir}/library/public', tmp_dir, dirs_exist_ok=True) + subprocess.run(['chmod', 'u+w', tmp_dir]) + with open(f'{tmp_dir}/books.js', 'w') as f: + json = cli.library.command.get(books_library) + f.write(f'const bookLibrary = {json}') + browser_cmd = f'{books_browser} {tmp_dir}/index.html' + subprocess.run(browser_cmd.split(' ')) |