aboutsummaryrefslogtreecommitdiff
path: root/cli
diff options
context:
space:
mode:
Diffstat (limited to 'cli')
-rw-r--r--cli/__init__.py0
-rw-r--r--cli/library/__init__.py0
-rw-r--r--cli/library/command.py21
-rw-r--r--cli/main.py80
-rw-r--r--cli/new/__init__.py0
-rw-r--r--cli/new/command.py88
-rw-r--r--cli/new/format.py70
-rw-r--r--cli/new/reader.py55
-rw-r--r--cli/new/test_format.py25
-rw-r--r--cli/view/__init__.py0
-rw-r--r--cli/view/command.py16
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(' '))