Skip to content

Commit

Permalink
feat: Integrate click for cli and config-parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
janw committed Jan 7, 2024
1 parent ccf67d9 commit c43e6ee
Show file tree
Hide file tree
Showing 10 changed files with 542 additions and 357 deletions.
40 changes: 0 additions & 40 deletions podcast_archiver/__main__.py

This file was deleted.

84 changes: 0 additions & 84 deletions podcast_archiver/argparse.py

This file was deleted.

8 changes: 4 additions & 4 deletions podcast_archiver/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from podcast_archiver import constants

if TYPE_CHECKING:
from podcast_archiver.config import Settings
from podcast_archiver.settings import Settings


class PodcastArchiver:
Expand Down Expand Up @@ -196,7 +196,7 @@ def truncateLinkList(self, linklist, feed_info):
link = episode_dict["url"]
filename = self.linkToTargetFilename(link, feed_info)

if path.isfile(filename):
if filename and path.isfile(filename):
del linklist[index:]
if self.verbose > 1:
print(f" found existing episodes, {len(linklist)} new to process")
Expand All @@ -221,7 +221,7 @@ def parseFeedInfo(self, feedobj):
return None

def processPodcastLink(self, feed_next_page):
feed_info = None
feed_info = {}
linklist = []
while True:
if not (feedobj := self.getFeedObj(feed_next_page)):
Expand Down Expand Up @@ -255,7 +255,7 @@ def checkEpisodeExistsPreflight(self, link, *, feed_info, episode_dict):
if self.verbose > 1:
print("\tLocal filename:", filename)

if path.isfile(filename):
if filename and path.isfile(filename):
if self.verbose > 1:
print("\t✓ Already exists.")
return None
Expand Down
200 changes: 200 additions & 0 deletions podcast_archiver/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import pathlib
from typing import Any

import rich_click as click
from pydantic import ValidationError

from podcast_archiver import __version__ as version
from podcast_archiver.base import PodcastArchiver
from podcast_archiver.config import DEFAULT_SETTINGS, Settings
from podcast_archiver.constants import ENVVAR_PREFIX, PROG_NAME

click.rich_click.USE_RICH_MARKUP = True
click.rich_click.USE_MARKDOWN = True
click.rich_click.OPTIONS_PANEL_TITLE = "Miscellaneous Options"
click.rich_click.OPTION_GROUPS = {
PROG_NAME: [
{
"name": "Basic parameters",
"options": [
"--feed",
"--opml",
"--dir",
"--config",
],
},
{
"name": "Processing parameters",
"options": [
"--subdirs",
"--update",
"--slugify",
"--max-episodes",
"--date-prefix",
],
},
]
}


@click.command(
context_settings={
"auto_envvar_prefix": ENVVAR_PREFIX,
},
help="Archive all of your favorite podcasts",
)
@click.help_option("-h", "--help")
@click.option(
"-f",
"--feed",
multiple=True,
show_envvar=True,
help="Feed URLs to archive. Use repeatedly for multiple feeds.",
)
@click.option(
"-o",
"--opml",
multiple=True,
show_envvar=True,
help=(
"OPML files (as exported by many other podcatchers) containing feed URLs to archive. "
"Use repeatedly for multiple files."
),
)
@click.option(
"-d",
"--dir",
type=click.Path(
exists=False,
writable=True,
file_okay=False,
dir_okay=True,
resolve_path=True,
path_type=pathlib.Path,
),
show_default=True,
required=False,
default=DEFAULT_SETTINGS.archive_directory,
show_envvar=True,
help="Directory to which to download the podcast archive",
)
@click.option(
"-s",
"--subdirs",
type=bool,
default=DEFAULT_SETTINGS.create_subdirectories,
is_flag=True,
show_envvar=True,
help="Place downloaded podcasts in separate subdirectories per podcast (named with their title).",
)
@click.option(
"-u",
"--update",
type=bool,
default=DEFAULT_SETTINGS.update_archive,
is_flag=True,
show_envvar=True,
help=(
"Update the feeds with newly added episodes only. "
"Adding episodes ends with the first episode already present in the download directory."
),
)
@click.option(
"-p",
"--progress",
type=bool,
default=DEFAULT_SETTINGS.show_progress_bars,
is_flag=True,
show_envvar=True,
help="Show progress bars while downloading episodes.",
)
@click.option(
"-v",
"--verbose",
count=True,
default=DEFAULT_SETTINGS.verbose,
help="Increase the level of verbosity while downloading.",
)
@click.option(
"-S",
"--slugify",
type=bool,
default=DEFAULT_SETTINGS.slugify_paths,
is_flag=True,
show_envvar=True,
help="Format filenames in the most compatible way, replacing all special characters.",
)
@click.option(
"-m",
"--max-episodes",
type=int,
default=DEFAULT_SETTINGS.maximum_episode_count,
help=(
"Only download the given number of episodes per podcast feed. "
"Useful if you don't really need the entire backlog."
),
)
@click.option(
"--date-prefix",
type=bool,
default=DEFAULT_SETTINGS.add_date_prefix,
is_flag=True,
show_envvar=True,
help="Prefix episodes with their publishing date. Useful to ensure chronological ordering.",
)
@click.version_option(
version,
"-V",
"--version",
prog_name=PROG_NAME,
)
@click.option(
"--config-generate",
type=bool,
expose_value=False,
is_flag=True,
callback=Settings.click_callback_generate,
is_eager=True,
help="Emit an example YAML config file to stdout and exit.",
)
@click.option(
"-c",
"--config",
type=click.Path(
readable=True,
file_okay=True,
dir_okay=False,
resolve_path=True,
path_type=pathlib.Path,
),
expose_value=False,
default=pathlib.Path(click.get_app_dir(PROG_NAME)) / "config.yaml",
show_default=True,
callback=Settings.click_callback_load,
is_eager=True,
show_envvar=True,
help="Path to a config file. Command line arguments will take precedence.",
)
@click.pass_context
def main(ctx: click.RichContext, **kwargs: Any) -> int:
try:
config = Settings.model_validate(kwargs)

# Replicate click's `no_args_is_help` behavior but only when config file does not contain feeds/OPMLs
if not (config.feeds or config.opml_files):
click.echo(ctx.command.get_help(ctx))
return 0

pa = PodcastArchiver(config)
pa.run()
except KeyboardInterrupt as exc:
raise click.Abort("Interrupted by user") from exc
except FileNotFoundError as exc:
raise click.Abort(exc) from exc
except ValidationError as exc:
raise click.Abort(f"Invalid settings: {exc}") from exc
return 0


if __name__ == "__main__":
main.main(prog_name=PROG_NAME)
Loading

0 comments on commit c43e6ee

Please sign in to comment.