diff options
| author | Joris Guyonvarch | 2025-12-26 18:41:26 +0100 |
|---|---|---|
| committer | Joris Guyonvarch | 2025-12-27 20:41:44 +0100 |
| commit | a110c200e86d2325af07167531fac0f61d9681a0 (patch) | |
| tree | 90e843f915a2e153ba735849afd83710d90560bf /cli | |
| parent | a26d92ad5055fa057647158eb79511e7b1841162 (diff) | |
Switch to GUI to manage the library
Allow to regroup the CLI and the view into one unique tool.
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 | 70 | ||||
| -rw-r--r-- | cli/new/reader.py | 55 | ||||
| -rw-r--r-- | cli/new/test_format.py | 25 | ||||
| -rw-r--r-- | cli/view/__init__.py | 0 | ||||
| -rw-r--r-- | cli/view/command.py | 16 |
11 files changed, 0 insertions, 355 deletions
diff --git a/cli/__init__.py b/cli/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/cli/__init__.py +++ /dev/null diff --git a/cli/library/__init__.py b/cli/library/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/cli/library/__init__.py +++ /dev/null diff --git a/cli/library/command.py b/cli/library/command.py deleted file mode 100644 index 1c4d20c..0000000 --- a/cli/library/command.py +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 01434fa..0000000 --- a/cli/main.py +++ /dev/null @@ -1,80 +0,0 @@ -# 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 deleted file mode 100644 index e69de29..0000000 --- a/cli/new/__init__.py +++ /dev/null diff --git a/cli/new/command.py b/cli/new/command.py deleted file mode 100644 index 17b686c..0000000 --- a/cli/new/command.py +++ /dev/null @@ -1,88 +0,0 @@ -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 book_source is not 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.list(authors)} - authorsSort = "{author_sort}" - genres = {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 book_source is not 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 book_source is not 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 deleted file mode 100644 index 7f66f44..0000000 --- a/cli/new/format.py +++ /dev/null @@ -1,70 +0,0 @@ -import pathlib -import re -import unicodedata - -def 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(r'\.\.\.', '…', s) - s = re.sub(r'\. \. \.', '…', s) - s = cleanup_double_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 :', s) - s = re.sub(r'([^ ])\?', r'\1 ?', s) - s = re.sub(r'([^ ])\!', r'\1 !', s) - s = re.sub(r'([^ ])»', r'\1 »', s) - s = re.sub(r'«([^ ])', r'« \1', s) - - elif lang == 'en': - s = re.sub('«', '“', s) - s = re.sub('»', '”', s) - - return s - -def cleanup_double_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 deleted file mode 100644 index eacd70b..0000000 --- a/cli/new/reader.py +++ /dev/null @@ -1,55 +0,0 @@ -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/new/test_format.py b/cli/new/test_format.py deleted file mode 100644 index d6269d0..0000000 --- a/cli/new/test_format.py +++ /dev/null @@ -1,25 +0,0 @@ -import cli.new.format as format - -def test_list(): - assert format.list([]) == '[]' - assert format.list(['a', 'b', 'c']) == '["a", "b", "c"]' - -def test_unaccent(): - assert format.unaccent('AuieTsrn') == 'AuieTsrn' - assert format.unaccent('âàéèêëîïôù') == 'aaeeeeiiou' - assert format.unaccent('ÂÀÉÈÊËÎÏÔÙ') == 'AAEEEEIIOU' - -def test_path_part(): - assert format.path_part('L’Homme à la béquille') == 'l-homme-a-la-bequille' - -def test_extension(): - assert format.extension('https://website.de/file.webp') == '.webp' - assert format.extension('/home/toto/extension-test/auie.notepad') == '.notepad' - -def test_cleaneup_quotes(): - assert format.cleanup_double_quotes('Bonjour, "ceci" où “cela”.', 'fr') == 'Bonjour, «ceci» où “cela”.' - assert format.cleanup_double_quotes('Hello, "this" or «that».', 'en') == 'Hello, “this” or «that».' - -def test_cleaneup_text(): - assert format.cleanup_text('l\'"est": ici... Là? OK! Yes !', 'fr') == 'l’« est » : ici… Là ? OK ! Yes !' - assert format.cleanup_text('Is it "ok" or «not»?', 'en') == 'Is it “ok” or “not”?' diff --git a/cli/view/__init__.py b/cli/view/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/cli/view/__init__.py +++ /dev/null diff --git a/cli/view/command.py b/cli/view/command.py deleted file mode 100644 index 72e44dd..0000000 --- a/cli/view/command.py +++ /dev/null @@ -1,16 +0,0 @@ -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(' ')) |
