diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.py | 15 | ||||
| -rw-r--r-- | src/new/__init__.py | 0 | ||||
| -rw-r--r-- | src/new/command.py | 94 | ||||
| -rw-r--r-- | src/new/format.py | 76 | ||||
| -rw-r--r-- | src/new/reader.py | 55 | 
5 files changed, 240 insertions, 0 deletions
diff --git a/src/main.py b/src/main.py index 618cc5a..1f07785 100644 --- a/src/main.py +++ b/src/main.py @@ -12,10 +12,15 @@ import os  import library.command  import view.command +import new.command  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  @@ -38,6 +43,16 @@ def get_book_library():  def main():      match sys.argv: +        case [ _, 'new' ]: +            book_library = get_book_library() +            new.command.run(book_library) +        case [ _, 'new', book_source ]: +            if os.path.isfile(book_source): +                book_library = get_book_library() +                new.command.run(book_library, book_source) +            else: +                print_help(title=f'File not found: {book_source}.') +                exit(1)          case [ _, 'library' ]:              book_library = get_book_library()              library.command.run(book_library) diff --git a/src/new/__init__.py b/src/new/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/new/__init__.py diff --git a/src/new/command.py b/src/new/command.py new file mode 100644 index 0000000..fe706c2 --- /dev/null +++ b/src/new/command.py @@ -0,0 +1,94 @@ +import PIL.Image +import io +import os +import pathlib +import re +import requests +import shutil +import subprocess +import sys +import unicodedata +import urllib.request + +import new.reader as reader +import new.format as format + +def run(book_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) + +    library_path = '/home/joris/documents/books' +    output_dir = f'{library_path}/{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/src/new/format.py b/src/new/format.py new file mode 100644 index 0000000..a712544 --- /dev/null +++ b/src/new/format.py @@ -0,0 +1,76 @@ +import PIL.Image +import io +import os +import pathlib +import re +import requests +import shutil +import subprocess +import sys +import unicodedata +import urllib.request + +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/src/new/reader.py b/src/new/reader.py new file mode 100644 index 0000000..eacd70b --- /dev/null +++ b/src/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)  | 
