Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --name-format and --direct options #22

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
__pycache__
.venv
19 changes: 9 additions & 10 deletions blinkist/book.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,18 @@

from .chapter import Chapter
from .common import api_request_web, download, request
from .config import BASE_URL, FILENAME_COVER, FILENAME_RAW, FILENAME_TEXT
from .config import BASE_URL, DEFAULT_FILENAME_COVER, DEFAULT_FILENAME_RAW, DEFAULT_FILENAME_TEXT
from .console import track


class Book:
def __init__(self, book_data: dict) -> None:
self.data = book_data

# pylint: disable=C0103
self.id = book_data['id']
self.language = book_data['language']
self.slug = book_data['slug']
self.title = book_data['title']
self.slug: str = book_data['slug']
self.title: str = book_data['title']
self.is_audio: bool = book_data['isAudio']

def __repr__(self) -> str:
Expand Down Expand Up @@ -57,7 +56,7 @@ def chapters(self) -> List[Chapter]:
]
return chapters

def download_cover(self, target_dir: Path) -> None:
def download_cover(self, target_dir: Path, file_name: str | None) -> None:
"""
Downloads the cover image to the given directory,
in the highest resolution available.
Expand All @@ -70,12 +69,12 @@ def download_cover(self, target_dir: Path) -> None:
# example: 'https://images.blinkist.io/images/books/617be9b56cee07000723559e/1_1/470.jpg' → 470
url = sorted(urls, key=lambda x: int(x.split('/')[-1].rstrip('.jpg')), reverse=True)[0]

file_path = target_dir / f"{FILENAME_COVER}.jpg"
file_path = target_dir / f"{file_name or DEFAULT_FILENAME_COVER}.jpg"

assert url.endswith('.jpg')
download(url, file_path)

def download_text_md(self, target_dir: Path) -> None:
def download_text_md(self, target_dir: Path, file_name: str | None) -> None:
"""
Downloads the text content as Markdown to the given directory.
"""
Expand Down Expand Up @@ -120,7 +119,7 @@ def md_section(level: int, title: str, text: str) -> str:

markdown_text = "\n\n\n".join(parts)

file_path = target_dir / f"{FILENAME_TEXT}.md"
file_path = target_dir / f"{file_name or DEFAULT_FILENAME_TEXT}.md"
file_path.write_text(markdown_text, encoding='utf-8')

def serialize(self) -> dict:
Expand All @@ -135,11 +134,11 @@ def serialize(self) -> dict:
],
}

def download_raw_yaml(self, target_dir: Path) -> None:
def download_raw_yaml(self, target_dir: Path, file_name: str | None) -> None:
"""
Downloads the raw YAML to the given directory.
"""
file_path = target_dir / f"{FILENAME_RAW}.yaml"
file_path = target_dir / f"{file_name or DEFAULT_FILENAME_RAW}.yaml"
file_path.write_text(
yaml.dump(
self.serialize(),
Expand Down
4 changes: 2 additions & 2 deletions blinkist/chapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ def serialize(self) -> dict:
"""
return self.data

def download_audio(self, target_dir: Path) -> None:
def download_audio(self, target_dir: Path, file_name: str | None) -> None:
if not self.data.get('signed_audio_url'):
# NOTE: In books where is_audio is true, every chapter should have audio, so this should never happen.
logging.warning(f'No audio for chapter {self.id}')
return

file_path = target_dir / f"chapter_{self.data['order_no']}.m4a"
file_path = target_dir / f"{f'{file_name} ' if file_name else ''}chapter_{self.data['order_no']}.m4a"

assert 'm4a' in self.data['signed_audio_url']
download(self.data['signed_audio_url'], file_path)
7 changes: 4 additions & 3 deletions blinkist/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

LANGUAGES = ['en', 'de']

FILENAME_COVER = "cover"
FILENAME_TEXT = "book"
FILENAME_RAW = "book"
# Default names for downloaded files if --name-format is not specified.
DEFAULT_FILENAME_COVER = "cover"
DEFAULT_FILENAME_TEXT = "book"
DEFAULT_FILENAME_RAW = "book"
55 changes: 42 additions & 13 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ def download_book(
book: Book,
language: str,
library_dir: Path,
name_format: str = None,
direct: bool = False,
# ---
yaml: bool = True,
markdown: bool = True,
Expand All @@ -43,11 +45,24 @@ def download_book(
# setup book directory
# book_dir = library_dir / f"{datetime.today().strftime('%Y-%m-%d')} – {book.slug}"
book_dir = library_dir / book.slug
if book_dir.exists() and not redownload:
if direct:
book_dir = library_dir
if book_dir.exists() and not redownload and not direct:
logging.info(f"Skipping “{book.title}” – already downloaded.")
# TODO: this doss not check if the download was complete! Can we do something about that
return
book_dir.mkdir(exist_ok=True) # We don't make parents in order to avoid user error.
if not direct:
book_dir.mkdir(exist_ok=True) # We don't make parents in order to avoid user error.

file_name = None
if name_format == "slug":
file_name = book.slug
elif name_format == "title":
file_name = book.title
elif name_format == "title-upper":
file_name = book.title.upper()
elif name_format == "id":
file_name = book.id

try:
# prefetch chapter_list and chapters for nicer progress info
Expand All @@ -60,36 +75,39 @@ def download_book(
# This comes first so we have all information saved as early as possible.
if yaml:
with status("Downloading raw YAML…"):
book.download_raw_yaml(book_dir)
book.download_raw_yaml(book_dir, file_name)

# download text (Markdown)
if markdown:
with status("Downloading text…"):
book.download_text_md(book_dir)
book.download_text_md(book_dir, file_name)

# download audio
if audio:
if book.is_audio:
for chapter in track(book.chapters, description="Downloading audio…"):
chapter.download_audio(book_dir)
chapter.download_audio(book_dir, file_name)
else:
logging.warning("This book has no audio.")

# download cover
if cover:
with status("Downloading cover…"):
book.download_cover(book_dir)
book.download_cover(book_dir, file_name)
except Exception as e:
logging.error(f"Error downloading “{book.title}”: {e}")

error_dir = book_dir.parent / f"{book.slug} – ERROR"
i = 0
while error_dir.exists() and any(error_dir.iterdir()):
i += 1
error_dir = book_dir.parent / f"{book.slug} – ERROR ({i})"
if not direct:
error_dir = book_dir.parent / f"{book.slug} – ERROR"
i = 0
while error_dir.exists() and any(error_dir.iterdir()):
i += 1
error_dir = book_dir.parent / f"{book.slug} – ERROR ({i})"

book_dir.replace(target=error_dir)
logging.warning(f"Renamed output directory to “{error_dir.relative_to(book_dir.parent)}”")
book_dir.replace(target=error_dir)
logging.warning(f"Renamed output directory to “{error_dir.relative_to(book_dir.parent)}”")
else:
logging.warning(f"Leaving output directory as “{book_dir.relative_to(book_dir.parent)}” because --direct was set.")

if continue_on_error:
logging.info("Continuing with next book… (--continue-on-error was set)")
Expand Down Expand Up @@ -124,10 +142,21 @@ def download_book(
@click.option('--yaml/--no-yaml', help="Save content as YAML", default=True)
# ▒▒ processed
@click.option('--markdown/--no-markdown', help="Save content as Markdown", default=True)
# ▒▒ output format
@click.option('--name-format', '-n', help='''Sets the format for output file names. By default no format is set, and generic names from config.py are used. Supported values:
- "slug": Book title slug (e.g. "the-4-hour-workweek")
- "title": Book title (e.g. "The 4-Hour Workweek")
- "title-upper": Book title in uppercase (e.g. "THE 4-HOUR WORKWEEK")
- "id": Book ID (e.g. "617be9b56cee07000723559e")''', type=str, default=None)
@click.option('--direct', help="Saves files directly in the parent folder, instead of creating a new folder for the book. Requires --file-format to be set.", is_flag=True, default=False)
def main(**kwargs):
languages_to_download = [kwargs['language']] if kwargs['language'] else LANGUAGES # default to all languages
books_to_download = set()

if kwargs['direct'] and not kwargs['name_format']:
logging.error("Error: --direct requires --name-format to be set.")
return

if kwargs['book_slug']:
books_to_download.add(Book.from_slug(kwargs['book_slug']))

Expand Down