Skip to content

Commit

Permalink
feat: Replace rich with tqdm for progress bars (janw#174)
Browse files Browse the repository at this point in the history
  • Loading branch information
janw authored Aug 17, 2024
1 parent 42a87dc commit 788c154
Show file tree
Hide file tree
Showing 14 changed files with 873 additions and 928 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
archive/
.ruff_cache/

.coverage
.coverage*
coverage.xml
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ A fast and simple command line client to archive all episodes from your favorite

Podcast Archiver takes the feed URLs of your favorite podcasts and downloads all available episodes for you—even those "hidden" in [paged feeds](https://podlove.org/paged-feeds/). You'll end up with a complete archive of your shows. The archiver also supports updating an existing archive, so that it lends itself to be set up as a cronjob.

⚠️ Podcast Archiver v1.0 completely changes the available command line options uses a new format for naming files (see [changing the filename format](#changing-the-filename-format) below). Using it on an existing pre-v1.0 folder structure will re-download all episodes unless you use an equivalent template. ⚠️

## Setup

Install via [pipx](https://pipx.pypa.io/stable/):
Expand Down
12 changes: 11 additions & 1 deletion cspell.config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
files:
- "**/*"
- '**/*'
useGitignore: true
ignorePaths:
- archive
Expand All @@ -8,8 +8,16 @@ ignorePaths:
- '*.xml'
- '*.opml'
- '*.svg'
- '*.mp3'
- '*.mp4'
- '*.m4a'
- LICENSE
allowCompoundWords: true
dictionaries:
- en_US
- python
- softwareTerms
- misc
words:
# podcasts-related
- archiver
Expand All @@ -35,9 +43,11 @@ words:
- pytest
- PYTHONUNBUFFERED
- pyyaml
- rprint
- subdirs
- tini
- tmpl
- tqdm
- venv
- virtualenv
- willhaus
5 changes: 3 additions & 2 deletions podcast_archiver/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import xml.etree.ElementTree as etree
from typing import TYPE_CHECKING

from podcast_archiver.console import console
from rich import print as rprint

from podcast_archiver.logging import logger
from podcast_archiver.processor import FeedProcessor

Expand Down Expand Up @@ -57,5 +58,5 @@ def run(self) -> int:
result = self.processor.process(url)
failures += result.failures

console.print("\n[bar.finished]Done.[/]\n")
rprint("\n[bar.finished]Done.[/]\n")
return failures
4 changes: 2 additions & 2 deletions podcast_archiver/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
from typing import TYPE_CHECKING, Any

import rich_click as click
from rich import get_console

from podcast_archiver import __version__ as version
from podcast_archiver import constants
from podcast_archiver.base import PodcastArchiver
from podcast_archiver.config import Settings, in_ci
from podcast_archiver.console import console
from podcast_archiver.exceptions import InvalidSettings
from podcast_archiver.logging import configure_logging

Expand Down Expand Up @@ -284,7 +284,7 @@ def generate_default_config(ctx: click.Context, param: click.Parameter, value: b
@click.pass_context
def main(ctx: click.RichContext, /, **kwargs: Any) -> int:
configure_logging(kwargs["verbose"])
console.quiet = kwargs["quiet"] or kwargs["verbose"] > 1
get_console().quiet = kwargs["quiet"] or kwargs["verbose"] > 1
try:
settings = Settings.load_from_dict(kwargs)

Expand Down
13 changes: 6 additions & 7 deletions podcast_archiver/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import pathlib
import sys
import textwrap
from datetime import datetime
from os import getenv
Expand Down Expand Up @@ -191,14 +192,12 @@ def generate_default_config(cls, file: IO[Text] | None = None) -> None:
f"{name}: {to_json(value).decode()}",
]

contents = "\n".join(lines).strip()
contents = "\n".join(lines).strip() + "\n"
if not file:
from podcast_archiver.console import console

console.print(contents, highlight=False)
return
with file:
file.write(contents + "\n")
sys.stdout.write(contents)
else:
with file:
file.write(contents)

def get_database(self) -> BaseDatabase:
if getenv("TESTING", "0").lower() in ("1", "true"):
Expand Down
100 changes: 0 additions & 100 deletions podcast_archiver/console.py

This file was deleted.

55 changes: 31 additions & 24 deletions podcast_archiver/download.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,36 @@
from __future__ import annotations

from contextlib import nullcontext
from threading import Event
from typing import IO, TYPE_CHECKING
from typing import IO, TYPE_CHECKING, NoReturn

from tqdm import tqdm
from tqdm.contrib.logging import logging_redirect_tqdm

from podcast_archiver import constants
from podcast_archiver.console import noop_callback
from podcast_archiver.enums import DownloadResult
from podcast_archiver.logging import logger
from podcast_archiver.session import session
from podcast_archiver.types import EpisodeResult, ProgressCallback
from podcast_archiver.types import EpisodeResult
from podcast_archiver.utils import atomic_write

if TYPE_CHECKING:
from pathlib import Path

from requests import Response
from rich import progress as rich_progress

from podcast_archiver.config import Settings
from podcast_archiver.models import Episode, FeedInfo


class DownloadJob:
episode: Episode
feed_info: FeedInfo
settings: Settings
target: Path
stop_event: Event

_debug_partial: bool
_write_info_json: bool

_progress: rich_progress.Progress | None = None
_task_id: rich_progress.TaskID | None = None
_no_progress: bool

def __init__(
self,
Expand All @@ -41,14 +39,14 @@ def __init__(
target: Path,
debug_partial: bool = False,
write_info_json: bool = False,
progress_callback: ProgressCallback = noop_callback,
no_progress: bool = False,
stop_event: Event | None = None,
) -> None:
self.episode = episode
self.target = target
self._debug_partial = debug_partial
self._write_info_json = write_info_json
self.progress_callback = progress_callback
self._no_progress = no_progress
self.stop_event = stop_event or Event()

def __repr__(self) -> str:
Expand Down Expand Up @@ -79,27 +77,36 @@ def run(self) -> EpisodeResult:
)
response.raise_for_status()
total_size = int(response.headers.get("content-length", "0"))
self.progress_callback(total=total_size)

with atomic_write(self.target, mode="wb") as fp:
receive_complete = self.receive_data(fp, response)

if not receive_complete:
self.target.unlink(missing_ok=True)
return EpisodeResult(self.episode, DownloadResult.ABORTED)

logger.info("Completed download of %s", self.target)
with (
logging_redirect_tqdm() if not self._no_progress else nullcontext(),
tqdm(
desc=f"{self.episode.title} ({self.episode.published_time:%Y-%m-%d})",
total=total_size,
unit_scale=True,
unit="B",
disable=self._no_progress,
) as progresser,
):
with atomic_write(self.target, mode="wb") as fp:
receive_complete = self.receive_data(fp, response, progresser=progresser)

if not receive_complete:
self.target.unlink(missing_ok=True)
return EpisodeResult(self.episode, DownloadResult.ABORTED)

logger.info("Completed download of %s", self.target)
return EpisodeResult(self.episode, DownloadResult.COMPLETED_SUCCESSFULLY)

@property
def infojsonfile(self) -> Path:
return self.target.with_suffix(".info.json")

def receive_data(self, fp: IO[str], response: Response) -> bool:
def receive_data(self, fp: IO[str], response: Response, progresser: tqdm[NoReturn]) -> bool:
total_written = 0
for chunk in response.iter_content(chunk_size=constants.DOWNLOAD_CHUNK_SIZE):
total_written += fp.write(chunk)
self.progress_callback(completed=total_written)
written = fp.write(chunk)
total_written += written
progresser.update(written)

if self._debug_partial and total_written >= constants.DEBUG_PARTIAL_SIZE:
logger.debug("Partial download completed.")
Expand Down
8 changes: 4 additions & 4 deletions podcast_archiver/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ class QueueCompletionType(StrEnum):


class DownloadResult(StrEnum):
ALREADY_EXISTS = "File already exists."
COMPLETED_SUCCESSFULLY = "Completed successfully."
FAILED = "Failed."
ABORTED = "Aborted."
ALREADY_EXISTS = "Exists"
COMPLETED_SUCCESSFULLY = "Completed"
FAILED = "Failed"
ABORTED = "Aborted"

def __str__(self) -> str:
return self.value
5 changes: 2 additions & 3 deletions podcast_archiver/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
import logging
import logging.config

from rich import get_console
from rich.logging import RichHandler

from podcast_archiver.console import console

logger = logging.getLogger("podcast_archiver")


Expand All @@ -28,7 +27,7 @@ def configure_logging(verbosity: int) -> None:
RichHandler(
log_time_format="[%X]",
markup=True,
rich_tracebacks=console.is_terminal,
rich_tracebacks=get_console().is_terminal,
tracebacks_suppress=[
"click",
],
Expand Down
Loading

0 comments on commit 788c154

Please sign in to comment.