diff --git a/.assets/podcast-archiver-help.svg b/.assets/podcast-archiver-help.svg index d3e7d6e..6634099 100644 --- a/.assets/podcast-archiver-help.svg +++ b/.assets/podcast-archiver-help.svg @@ -1,4 +1,4 @@ - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + - + - + - - $ podcast-archiver --help - -Usage:podcast-archiver [OPTIONS]                                                                                                           - - Archive all of your favorite podcasts                                                                                                       - -╭─ Basic parameters ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ---feed-fTEXT       Feed URLs to archive. Use repeatedly for multiple feeds.                                                         -[env var: PODCAST_ARCHIVER_FEED]                                                                                ---opml-oTEXT       OPML files containing feed URLs to archive. OPML files can be exported from a variety of podcatchers. Use        -                          repeatedly for multiple files.                                                                                   -[env var: PODCAST_ARCHIVER_OPML]                                                                                ---dir-dDIRECTORY  Directory to which to download the podcast archive. By default, the archive will be created in the current       -                          working directory  ('.').                                                                                        -[env var: PODCAST_ARCHIVER_DIR]                                                                                 -[default: .]                                                                                                    ---config-cFILE       Path to a config file. Command line arguments will take precedence.                                              -[env var: PODCAST_ARCHIVER_CONFIG]                                                                              -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Output parameters ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ---filename-template-FTEXT  Template to be used when generating filenames. Available template variables are: 'episode.title,           -                                'episode.subtitle, 'episode.published_time, 'episode.original_filename, 'show.title, 'show.subtitle,       -                                'show.author, 'show.language', and 'ext' (the filename extension)                                          -[env var: PODCAST_ARCHIVER_FILENAME_TEMPLATE]                                                             -[default: {show.title}/{episode.published_time:%Y-%m-%d} - {episode.title}.{ext}]                         ---write-info-json  Write episode metadata to a .info.json file next to the media file itself.                                 -[env var: PODCAST_ARCHIVER_WRITE_INFO_JSON]                                                               ---slugify-S  Format filenames in the most compatible way, replacing all special characters.                             -[env var: PODCAST_ARCHIVER_SLUGIFY]                                                                       -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Processing parameters ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ---update-u  Update the feeds with newly added episodes only. Adding episodes ends with the first episode already         -                              present in the download directory.                                                                           -[env var: PODCAST_ARCHIVER_UPDATE]                                                                          ---max-episodes-mINTEGER  Only download the given number of episodes per podcast feed. Useful if you don't really need the entire      -                              backlog.                                                                                                     -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Miscellaneous Options ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ ---help-h  Show this message and exit.                                                                         ---quiet-q  Print only minimal progress information. Errors will always be emitted.                             -[env var: PODCAST_ARCHIVER_QUIET]                                                                  ---concurrency-CINTEGER        Maximum number of simultaneous downloads.                                                           -[env var: PODCAST_ARCHIVER_CONCURRENCY]                                                            ---debug-partial  Download only the first 1048576 bytes of episodes for debugging purposes.                           -[env var: PODCAST_ARCHIVER_DEBUG_PARTIAL]                                                          ---verbose-vINTEGER RANGE  Increase the level of verbosity while downloading.                                                  -[env var: PODCAST_ARCHIVER_VERBOSE]                                                                ---version-V  Show the version and exit.                                                                          ---config-generate  Emit an example YAML config file to stdout and exit.                                                -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ - + + $ podcast-archiver --help + +Usage:podcast-archiver [OPTIONS]                                                                                                           + + Archive all of your favorite podcasts                                                                                                       + +╭─ Basic parameters ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +--feed-fTEXT       Feed URLs to archive. Use repeatedly for multiple feeds.                                                         +[env var: PODCAST_ARCHIVER_FEED]                                                                                +--opml-oTEXT       OPML files containing feed URLs to archive. OPML files can be exported from a variety of podcatchers. Use        +                          repeatedly for multiple files.                                                                                   +[env var: PODCAST_ARCHIVER_OPML]                                                                                +--dir-dDIRECTORY  Directory to which to download the podcast archive. By default, the archive will be created in the current       +                          working directory  ('.').                                                                                        +[env var: PODCAST_ARCHIVER_DIR]                                                                                 +[default: .]                                                                                                    +--config-cFILE       Path to a config file. Command line arguments will take precedence.                                              +[env var: PODCAST_ARCHIVER_CONFIG]                                                                              +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Output parameters ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +--filename-template-FTEXT  Template to be used when generating filenames. Available template variables are: 'episode.title,           +                                'episode.subtitle, 'episode.published_time, 'episode.original_filename, 'show.title, 'show.subtitle,       +                                'show.author, 'show.language', and 'ext' (the filename extension)                                          +[env var: PODCAST_ARCHIVER_FILENAME_TEMPLATE]                                                             +[default: {show.title}/{episode.published_time:%Y-%m-%d} - {episode.title}.{ext}]                         +--write-info-json  Write episode metadata to a .info.json file next to the media file itself.                                 +[env var: PODCAST_ARCHIVER_WRITE_INFO_JSON]                                                               +--slugify-S  Format filenames in the most compatible way, replacing all special characters.                             +[env var: PODCAST_ARCHIVER_SLUGIFY]                                                                       +--dry-run-n  Do not download anything.                                                                                  +[env var: PODCAST_ARCHIVER_DRY_RUN]                                                                       +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Processing parameters ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +--update-u  Update the feeds with newly added episodes only. Adding episodes ends with the first episode already         +                              present in the download directory.                                                                           +[env var: PODCAST_ARCHIVER_UPDATE]                                                                          +--max-episodes-mINTEGER  Only download the given number of episodes per podcast feed. Useful if you don't really need the entire      +                              backlog.                                                                                                     +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Miscellaneous Options ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +--help-h  Show this message and exit.                                                                         +--quiet-q  Print only minimal progress information. Errors will always be emitted.                             +[env var: PODCAST_ARCHIVER_QUIET]                                                                  +--concurrency-CINTEGER        Maximum number of simultaneous downloads.                                                           +[env var: PODCAST_ARCHIVER_CONCURRENCY]                                                            +--debug-partial  Download only the first 1048576 bytes of episodes for debugging purposes.                           +[env var: PODCAST_ARCHIVER_DEBUG_PARTIAL]                                                          +--verbose-vINTEGER RANGE  Increase the level of verbosity while downloading.                                                  +[env var: PODCAST_ARCHIVER_VERBOSE]                                                                +--version-V  Show the version and exit.                                                                          +--config-generate  Emit an example YAML config file to stdout and exit.                                                +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + diff --git a/podcast_archiver/cli.py b/podcast_archiver/cli.py index bbaaf62..f813f41 100644 --- a/podcast_archiver/cli.py +++ b/podcast_archiver/cli.py @@ -33,6 +33,7 @@ "--filename-template", "--write-info-json", "--slugify", + "--dry-run", ], }, { @@ -181,6 +182,15 @@ def generate_default_config(ctx: click.Context, param: click.Parameter, value: b show_envvar=True, help=Settings.model_fields["quiet"].description, ) +@click.option( + "-n", + "--dry-run", + type=bool, + default=DEFAULT_SETTINGS.dry_run, + is_flag=True, + show_envvar=True, + help=Settings.model_fields["dry_run"].description, +) @click.option( "-C", "--concurrency", diff --git a/podcast_archiver/config.py b/podcast_archiver/config.py index 9f2abf4..df9e04c 100644 --- a/podcast_archiver/config.py +++ b/podcast_archiver/config.py @@ -81,6 +81,12 @@ class Settings(BaseModel): description="Print only minimal progress information. Errors will always be emitted.", ) + dry_run: bool = Field( + default=False, + alias="dry_run", + description="Do not download anything.", + ) + verbose: int = Field( default=0, alias="verbose", diff --git a/podcast_archiver/console.py b/podcast_archiver/console.py index a9463af..f8e36f7 100644 --- a/podcast_archiver/console.py +++ b/podcast_archiver/console.py @@ -1,3 +1,62 @@ -from rich.console import Console +from typing import TYPE_CHECKING + +from rich import progress, table +from rich.console import Console, RenderableType console = Console() + +if TYPE_CHECKING: + _MixinBase = progress.ProgressColumn +else: + _MixinBase = object + + +class HideableColumnMixin(_MixinBase): + def __call__(self, task: progress.Task) -> RenderableType: + speed = task.finished_speed or task.speed + if speed is None: + return "" + return super().__call__(task) + + +class HideableTimeRemainingColumn(HideableColumnMixin, progress.TimeRemainingColumn): + pass + + +class HideableTransferSpeedColumn(HideableColumnMixin, progress.TransferSpeedColumn): + pass + + +COMMON_PROGRESS_COLUMNS: list[progress.ProgressColumn] = [ + progress.SpinnerColumn(finished_text="[bar.finished]✔[/]"), + progress.TextColumn( + "{task.fields[date]:%Y-%m-%d}", + style="blue", + table_column=table.Column(width=len("2023-09-24")), + ), + progress.TextColumn( + "{task.description}", + style="progress.description", + table_column=table.Column(no_wrap=True, ratio=2), + ), + progress.BarColumn(bar_width=25), + progress.TaskProgressColumn(), + progress.DownloadColumn(), +] + +TRANSFER_COLUMNS: list[progress.ProgressColumn] = [ + HideableTimeRemainingColumn(), + HideableTransferSpeedColumn(), +] + + +def get_progress(console: Console, dry_run: bool, disable: bool) -> progress.Progress: + cols = COMMON_PROGRESS_COLUMNS + if dry_run: + cols += [progress.TextColumn("[bright_black]\\[dry-run][/]")] + else: + cols += TRANSFER_COLUMNS + + prog = progress.Progress(*cols, console=console, expand=True, disable=disable) + prog.live.vertical_overflow = "visible" + return prog diff --git a/podcast_archiver/download.py b/podcast_archiver/download.py index 101bc1f..97f7285 100644 --- a/podcast_archiver/download.py +++ b/podcast_archiver/download.py @@ -60,8 +60,9 @@ def __call__(self) -> DownloadResult: return DownloadResult.FAILED def run(self) -> DownloadResult: - self.target.parent.mkdir(parents=True, exist_ok=True) - self.write_info_json() + if not self.settings.dry_run: + self.target.parent.mkdir(parents=True, exist_ok=True) + self.write_info_json() if result := self.preflight_check(): return result @@ -75,6 +76,11 @@ def run(self) -> DownloadResult: total_size = int(response.headers.get("content-length", "0")) self.update_progress(total=total_size) + if self.settings.dry_run: + logger.info("Dry-run download of %s", self.target) + self.update_progress(total=total_size, completed=total_size) + return DownloadResult.COMPLETED_SUCCESSFULLY + with atomic_write(self.target, mode="wb") as fp: receive_complete = self.receive_data(fp, response) diff --git a/podcast_archiver/processor.py b/podcast_archiver/processor.py index f7add5c..82a32b8 100644 --- a/podcast_archiver/processor.py +++ b/podcast_archiver/processor.py @@ -9,23 +9,12 @@ from rich import progress as rich_progress from podcast_archiver.config import Settings -from podcast_archiver.console import console +from podcast_archiver.console import console, get_progress from podcast_archiver.download import DownloadJob from podcast_archiver.enums import DownloadResult, QueueCompletionType from podcast_archiver.logging import logger from podcast_archiver.models import Feed -PROGRESS_COLUMNS = ( - rich_progress.SpinnerColumn(finished_text="[bar.finished]✔[/]"), - rich_progress.TextColumn("[blue]{task.fields[date]:%Y-%m-%d}"), - rich_progress.TextColumn("[progress.description]{task.description}"), - rich_progress.BarColumn(bar_width=25), - rich_progress.TaskProgressColumn(), - rich_progress.TimeRemainingColumn(), - rich_progress.DownloadColumn(), - rich_progress.TransferSpeedColumn(), -) - @dataclass class ProcessingResult: @@ -44,12 +33,11 @@ class FeedProcessor: def __init__(self, settings: Settings) -> None: self.settings = settings self.pool_executor = ThreadPoolExecutor(max_workers=self.settings.concurrency) - self.progress = rich_progress.Progress( - *PROGRESS_COLUMNS, + self.progress = get_progress( console=console, disable=settings.verbose > 1 or settings.quiet, + dry_run=settings.dry_run, ) - # self.progress.live.vertical_overflow = "visible" self.stop_event = Event() def process(self, url: AnyHttpUrl) -> ProcessingResult: