def load(self): arg_loader = ArgLoader(*self.specifications()) try: arguments = arg_loader.load() except RuntimeError as e: raise MnamerException(e) config_path = arguments.get("config_path", crawl_out(".mnamer-v2.json")) config = json_loads(str(config_path)) if config_path else {} if not self.config_ignore and not arguments.get("config_ignore"): self.bulk_apply(config) if arguments: self.bulk_apply(arguments)
class Settings: # positional attributes ---------------------------------------------------- targets: List[Path] = dataclasses.field( default_factory=lambda: [], metadata=ArgSpec( flags=["targets"], group=SettingsType.POSITIONAL, help="[TARGET,...]: media file file path(s) to process", nargs="*", )(), ) # parameter attributes ----------------------------------------------------- batch: bool = dataclasses.field( default=False, metadata=ArgSpec( action="store_true", dest="batch", flags=["-b", "--batch"], group=SettingsType.PARAMETER, help= "-b, --batch: process automatically without interactive prompts", )(), ) lower: bool = dataclasses.field( default=False, metadata=ArgSpec( action="store_true", flags=["-l", "--lower"], group=SettingsType.PARAMETER, help="-l, --lower: rename files using lowercase characters", )(), ) recurse: bool = dataclasses.field( default=False, metadata=ArgSpec( action="store_true", flags=["-r", "--recurse"], group=SettingsType.PARAMETER, help="-r, --recurse: search for files within nested directories", )(), ) scene: bool = dataclasses.field( default=False, metadata=ArgSpec( action="store_true", flags=["-s", "--scene"], group=SettingsType.PARAMETER, help="-s, --scene: use dots in place of alphanumeric chars", )(), ) verbose: bool = dataclasses.field( default=False, metadata=ArgSpec( action="store_true", flags=["-v", "--verbose"], group=SettingsType.PARAMETER, help="-v, --verbose: increase output verbosity", )(), ) hits: int = dataclasses.field( default=5, metadata=ArgSpec( flags=["--hits"], group=SettingsType.PARAMETER, help= "--hits=<NUMBER>: limit the maximum number of hits for each query", type=int, )(), ) ignore: List[str] = dataclasses.field( default_factory=lambda: [".*sample.*", "^RARBG.*"], metadata=ArgSpec( flags=["--ignore"], group=SettingsType.PARAMETER, help= "--ignore=<PATTERN,...>: ignore files matching these regular expressions", nargs="+", )(), ) mask: List[str] = dataclasses.field( default_factory=lambda: ["avi", "m4v", "mp4", "mkv", "ts", "wmv"], metadata=ArgSpec( flags=["--mask"], group=SettingsType.PARAMETER, help="--mask=<EXTENSION,...>: only process given file types", nargs="+", )(), ) no_cache: bool = dataclasses.field( default=False, metadata=ArgSpec( action="store_true", dest="no_cache", flags=["--nocache", "--no_cache", "--no-cache"], group=SettingsType.PARAMETER, help="--nocache: disable and clear request cache", )(), ) no_guess: bool = dataclasses.field( default=False, metadata=ArgSpec( action="store_true", dest="no_guess", flags=["--noguess", "--no_guess", "--no-guess"], group=SettingsType.PARAMETER, help= "--noguess: disable best guess; e.g. when no matches or network down", )(), ) no_replace: bool = dataclasses.field( default=False, metadata=ArgSpec( action="store_true", dest="no_replace", flags=["--noreplace", "--no_replace", "--no-replace"], group=SettingsType.PARAMETER, help="--noreplace: prevent relocation if it would overwrite a file", )(), ) no_style: bool = dataclasses.field( default=False, metadata=ArgSpec( action="store_true", dest="no_style", flags=["--nostyle", "--no_style", "--no-style"], group=SettingsType.PARAMETER, help= "--nostyle: print to stdout without using colour or unicode chars", )(), ) movie_api: Union[ProviderType, str] = dataclasses.field( default=ProviderType.TMDB, metadata=ArgSpec( choices=[ProviderType.TMDB.value, ProviderType.OMDB.value], dest="movie_api", flags=["--movie_api", "--movie-api"], group=SettingsType.PARAMETER, help="--movie-api={*tmdb,omdb}: set movie api provider", )(), ) movie_directory: Optional[Path] = dataclasses.field( default=None, metadata=ArgSpec( dest="movie_directory", flags=["--movie_directory", "--movie-directory"], group=SettingsType.PARAMETER, help="--movie-directory: set movie relocation directory", )(), ) movie_format: str = dataclasses.field( default="{name} ({year}){extension}", metadata=ArgSpec( dest="movie_format", flags=["--movie_format", "--movie-format"], group=SettingsType.PARAMETER, help="--movie-format: set movie renaming format specification", )(), ) episode_api: Union[ProviderType, str] = dataclasses.field( default=ProviderType.TVMAZE, metadata=ArgSpec( choices=[ProviderType.TVDB.value, ProviderType.TVMAZE.value], dest="episode_api", flags=["--episode_api", "--episode-api"], group=SettingsType.PARAMETER, help="--episode-api={tvdb,*tvmaze}: set episode api provider", )(), ) episode_directory: Path = dataclasses.field( default=None, metadata=ArgSpec( dest="episode_directory", flags=["--episode_directory", "--episode-directory"], group=SettingsType.PARAMETER, help="--episode-directory: set episode relocation directory", )(), ) episode_format: str = dataclasses.field( default="{series} - S{season:02}E{episode:02} - {title}{extension}", metadata=ArgSpec( dest="episode_format", flags=["--episode_format", "--episode-format"], group=SettingsType.PARAMETER, help="--episode-format: set episode renaming format specification", )(), ) # directive attributes ----------------------------------------------------- version: bool = dataclasses.field( default=False, metadata=ArgSpec( action="store_true", flags=["-V", "--version"], group=SettingsType.DIRECTIVE, help="-V, --version: display the running mnamer version number", )(), ) config_dump: bool = dataclasses.field( default=False, metadata=ArgSpec( action="store_true", dest="config_dump", flags=["--config_dump", "--config-dump"], group=SettingsType.DIRECTIVE, help= "--config-dump: prints current config JSON to stdout then exits", )(), ) config_ignore: bool = dataclasses.field( default=False, metadata=ArgSpec( action="store_true", dest="config_ignore", flags=["--config-ignore", "--config_ignore"], group=SettingsType.DIRECTIVE, help="--config-ignore: skips loading config file for session", )(), ) id_imdb: str = dataclasses.field( default=None, metadata=ArgSpec( flags=["--id-imdb", "--id_imdb"], group=SettingsType.DIRECTIVE, help="--id-imdb=<ID>: specify an IMDb movie id override", )(), ) id_tmdb: str = dataclasses.field( default=None, metadata=ArgSpec( flags=["--id-tmdb", "--id_tmdb"], group=SettingsType.DIRECTIVE, help="--id-tmdb=<ID>: specify a TMDb movie id override", )(), ) id_tvdb: str = dataclasses.field( default=None, metadata=ArgSpec( flags=["--id-tvdb", "--id_tvdb"], group=SettingsType.DIRECTIVE, help="--id-tvdb=<ID>: specify a TVDb series id override", )(), ) id_tvmaze: str = dataclasses.field( default=None, metadata=ArgSpec( flags=["--id-tvmaze", "--id_tvmaze"], group=SettingsType.DIRECTIVE, help="--id-tvmaze=<ID>: specify a TvMaze series id override", )(), ) media: Optional[Union[MediaType, str]] = dataclasses.field( default=None, metadata=ArgSpec( choices=[MediaType.EPISODE.value, MediaType.MOVIE.value], flags=["--media"], group=SettingsType.DIRECTIVE, help="--media={movie,episode}: override media detection", )(), ) test: bool = dataclasses.field( default=False, metadata=ArgSpec( action="store_true", flags=["--test"], group=SettingsType.DIRECTIVE, help="--test: mocks the renaming and moving of files", )(), ) # config-only attributes --------------------------------------------------- api_key_omdb: str = dataclasses.field( default=None, metadata=ArgSpec(group=SettingsType.CONFIGURATION)(), ) api_key_tmdb: str = dataclasses.field( default=None, metadata=ArgSpec(group=SettingsType.CONFIGURATION)(), ) api_key_tvdb: str = dataclasses.field( default=None, metadata=ArgSpec(group=SettingsType.CONFIGURATION)(), ) api_key_tvmaze: str = dataclasses.field( default=None, metadata=ArgSpec(group=SettingsType.CONFIGURATION)(), ) replacements: Dict[str, str] = dataclasses.field( default_factory=lambda: { "&": "and", "@": "at", ":": "", ";": "," }, metadata=ArgSpec(group=SettingsType.CONFIGURATION)(), ) # init-var attributes ------------------------------------------------------ load_configuration: dataclasses.InitVar[bool] = False load_arguments: dataclasses.InitVar[bool] = False configuration_path: dataclasses.InitVar[Optional[Path]] = crawl_out( ".mnamer-v2.json") def __post_init__( self, load_configuration: bool, load_arguments: bool, configuration_path: Optional[Path], ): self._arg_data = {} self._config_data = {} # load cli arguments self._load_arguments(load_arguments) self._bulk_apply(self._arg_data) # load configuration if configuration_path and load_configuration and not self.config_ignore: self._load_configuration(configuration_path) self._bulk_apply(self._config_data) @classmethod def _attribute_metadata(cls) -> Dict[str, ArgSpec]: return { f.name: ArgSpec(**f.metadata) for f in dataclasses.fields(cls) if f.metadata } @classmethod def _serializable_fields(cls): return { field.name for field in dataclasses.fields(cls) if field.metadata.get("group") in {SettingsType.PARAMETER, SettingsType.CONFIGURATION} } def __setattr__(self, key: str, value: Any): converter = { "episode_api": ProviderType, "episode_directory": Path, "mask": normalize_extensions, "media": MediaType, "movie_api": ProviderType, "movie_directory": Path, "targets": lambda targets: [Path(target) for target in targets], }.get(key) if value is not None and converter: value = converter(value) super().__setattr__(key, value) @property def as_dict(self): return dataclasses.asdict(self) @property def as_json(self): payload = {} # transform values into primitive JSON-serializable types for k, v in self.as_dict.items(): if k not in self._serializable_fields(): continue if hasattr(v, "value"): payload[k] = v.value elif isinstance(v, Path): payload[k] = str(v.resolve()) else: payload[k] = v return json.dumps( payload, allow_nan=False, check_circular=False, ensure_ascii=True, indent=4, skipkeys=True, sort_keys=True, ) def _bulk_apply(self, data: Dict[str, Any]): [setattr(self, k, v) for k, v in data.items() if v] def _load_arguments(self, load_arguments: bool): arg_parser = ArgParser() groups = {SettingsType.DIRECTIVE} if load_arguments: groups |= {SettingsType.PARAMETER, SettingsType.POSITIONAL} for spec in self._attribute_metadata().values(): if spec.group in groups: arg_parser.add_spec(spec) try: arguments = arg_parser.parse_args() except MnamerException: if load_arguments: raise else: self._arg_data = vars(arguments) def _load_configuration(self, path: Union[Path, str]): path = Template(str(path)).substitute(environ) with open(path, mode="r") as file_pointer: data = json.loads(file_pointer.read()) for key in data: if key not in self._attribute_metadata(): raise MnamerException(f"invalid setting: {key}") self._config_data = data def api_for(self, media_type: MediaType) -> ProviderType: """Returns the ProviderType for a given media type.""" return getattr(self, f"{media_type.value}_api") def api_key_for(self, provider_type: ProviderType) -> Optional[str]: """Returns the API key for a provider type.""" return getattr(self, f"api_key_{provider_type.value}")