From a110c200e86d2325af07167531fac0f61d9681a0 Mon Sep 17 00:00:00 2001 From: Joris Guyonvarch Date: Fri, 26 Dec 2025 18:41:26 +0100 Subject: Switch to GUI to manage the library Allow to regroup the CLI and the view into one unique tool. --- cli/new/__init__.py | 0 cli/new/command.py | 88 -------------------------------------------------- cli/new/format.py | 70 --------------------------------------- cli/new/reader.py | 55 ------------------------------- cli/new/test_format.py | 25 -------------- 5 files changed, 238 deletions(-) delete mode 100644 cli/new/__init__.py delete mode 100644 cli/new/command.py delete mode 100644 cli/new/format.py delete mode 100644 cli/new/reader.py delete mode 100644 cli/new/test_format.py (limited to 'cli/new') diff --git a/cli/new/__init__.py b/cli/new/__init__.py deleted file mode 100644 index e69de29..0000000 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”?' -- cgit v1.2.3