"""Collect all informations about the current job in a class."""
import configparser
import os
import time
import typing
from .args import ArgsDefault
from .message import Message
class Timer:
begin: float = 0
end: float = 0
def start(self) -> None:
self.begin = time.time()
def stop(self) -> None:
self.end = time.time()
def result(self) -> str:
return "{:.1f}s".format(self.end - self.begin)
class Counter:
def __init__(self) -> None:
self._counters: typing.Dict[str, int] = {}
def reset(self) -> None:
self._counters = {}
def count(self, counter: str) -> None:
"""Add one to number identified by a string.
:param counter: A string to identify the counter
:return: None
"""
if counter in self._counters:
self._counters[counter] += 1
else:
self._counters[counter] = 1
def get(self, counter: str) -> int:
"""Get the counter identify by a string.
:param str counter: A string to identify the counter
:return: The counter as a number
:rtype: int
"""
if counter in self._counters:
return self._counters[counter]
else:
return 0
def result(self) -> str:
out: typing.List[str] = []
for counter, value in sorted(self._counters.items()):
out.append(counter + "=" + str(value))
if out:
return " ".join(out)
else:
return "Nothing to count!"
class Statistic:
counter = Counter()
timer = Timer()
IniDataTypes = typing.Literal["boolean", "integer", "string"]
[docs]
class Config:
"""The class ``Config`` is used to combine the two sources of settings
(command line arguments and INI configuration file). The command line
arguments override the values of the configuration file. This class is to
be inherited by subclasses. Each subclass corresponds to a section of the
INI configuration file. All settings are saved as private properties with
leading underscore. The subclass provide for each private property a
getter method (@property)"""
_job: "Job"
def __init__(
self, job: "Job", section: str, options: typing.Dict[str, IniDataTypes]
):
self._job = job
for key, data_type in options.items():
value = None
if getattr(job.args, key) is not None:
value = getattr(job.args, key)
elif job.config:
for config in job.config:
result = self.__get_value_from_config(
config, section, key, data_type
)
if result is not None:
value = result
if value is not None:
setattr(self, "_" + key, value)
def __get_value_from_config(
self,
config: configparser.ConfigParser,
section: str,
key: str,
data_type: IniDataTypes,
) -> typing.Optional[typing.Any]:
try:
if data_type == "boolean":
return config.getboolean(section, key)
elif data_type == "integer":
return config.getint(section, key)
else:
return config.get(section, key)
except (configparser.NoOptionError, configparser.NoSectionError):
pass
return None
[docs]
class SelectionConfig(Config):
_source: typing.Optional[str]
_target: typing.Optional[str]
_source_as_target: typing.Optional[bool]
@property
def source(self) -> str:
"""The source path as an absolute path. It maybe a directory or a
file."""
source: str
if hasattr(self, "_source") and self._source:
source = self._source
else:
source = "."
return os.path.abspath(source)
@property
def target(self) -> typing.Optional[str]:
"""The path of the target as an absolute path. It is always a
directory.
"""
target: typing.Union[None, str] = None
if hasattr(self, "_target"):
target = self._target
if self.source_as_target:
if os.path.isdir(self.source):
return os.path.abspath(self.source)
else:
return os.path.abspath(os.path.dirname(self.source))
elif target:
return os.path.abspath(target)
else:
return os.getcwd()
@property
def source_as_target(self) -> bool:
if hasattr(self, "_source_as_target") and isinstance(
self._source_as_target, bool
):
return self._source_as_target
return False
MoveAction = typing.Literal["move", "copy", "no_rename"]
CleaningAction = typing.Literal["backup", "delete", "do_nothing"]
[docs]
class RenameConfig(Config):
_backup_folder: typing.Optional[str]
_best_format: typing.Optional[bool]
_dry_run: typing.Optional[bool]
_move_action: typing.Optional[MoveAction]
_cleaning_action: typing.Optional[CleaningAction]
@property
def backup_folder(self) -> str:
if hasattr(self, "_backup_folder") and isinstance(self._backup_folder, str):
return self._backup_folder
return os.path.join(os.getcwd(), "_audiorename_backups")
@property
def best_format(self) -> bool:
if hasattr(self, "_best_format") and isinstance(self._best_format, bool):
return self._best_format
return True
@property
def dry_run(self) -> bool:
if hasattr(self, "_dry_run") and isinstance(self._dry_run, bool):
return self._dry_run
return False
@property
def move_action(self) -> MoveAction:
if hasattr(self, "_move_action") and self._move_action in [
"move",
"copy",
"no_rename",
]:
return self._move_action
return "move"
@property
def cleaning_action(self) -> CleaningAction:
if hasattr(self, "_cleaning_action") and self._cleaning_action in [
"backup",
"delete",
"do_nothing",
]:
return self._cleaning_action
return "do_nothing"
[docs]
class FiltersConfig(Config):
_album_complete: typing.Optional[bool]
_album_min: typing.Optional[int]
_extension: typing.Optional[str]
_genre_classical: typing.Optional[str]
_field_skip: typing.Optional[str]
@property
def album_complete(self) -> bool:
if hasattr(self, "_album_complete") and isinstance(self._album_complete, bool):
return self._album_complete
return False
@property
def album_min(self) -> typing.Optional[int]:
if hasattr(self, "_album_min") and isinstance(self._album_min, int):
return self._album_min
return None
@property
def extension(self) -> typing.List[str]:
extension: str
if hasattr(self, "_extension") and isinstance(self._extension, str):
extension = self._extension
else:
extension = "mp3,m4a,flac,wma"
return extension.split(",")
@property
def genre_classical(self) -> typing.List[str]:
genre_classical: str
if hasattr(self, "_genre_classical") and isinstance(self._genre_classical, str):
genre_classical = self._genre_classical
else:
genre_classical = ","
return list(filter(str.strip, genre_classical.lower().split(",")))
@property
def field_skip(self) -> typing.Optional[str]:
if hasattr(self, "_field_skip") and isinstance(self._field_skip, str):
return self._field_skip
return None
[docs]
class TemplateSettingsConfig(Config):
_classical: typing.Optional[bool]
_shell_friendly: typing.Optional[bool]
_no_soundtrack: typing.Optional[bool]
@property
def classical(self) -> bool:
if hasattr(self, "_classical") and isinstance(self._classical, bool):
return self._classical
return False
@property
def shell_friendly(self) -> bool:
if hasattr(self, "_shell_friendly") and isinstance(self._shell_friendly, bool):
return self._shell_friendly
return False
@property
def no_soundtrack(self) -> bool:
if hasattr(self, "_no_soundtrack") and isinstance(self._no_soundtrack, bool):
return self._no_soundtrack
return False
[docs]
class PathTemplatesConfig(Config):
"""A class to store the selected or configured path templates. This class
can be accessed under the attibute path_templates of the Job class."""
_default_template: typing.Optional[str]
_compilation_template: typing.Optional[str]
_soundtrack_template: typing.Optional[str]
_classical_template: typing.Optional[str]
@property
def _is_classical(self) -> bool:
return self._job.template_settings.classical
@property
def default(self) -> str:
"""Get the default path template."""
if self._is_classical:
return self.classical
if hasattr(self, "_default_template") and isinstance(
self._default_template, str
):
return self._default_template
return (
"$ar_initial_artist/"
"%shorten{$ar_combined_artist_sort}/"
"%shorten{$ar_combined_album}"
"%ifdefnotempty{ar_combined_year,_${ar_combined_year}}/"
"${ar_combined_disctrack}_%shorten{$title}"
)
@property
def compilation(self) -> str:
"""Get the path template for compilations."""
if self._is_classical:
return self.classical
if hasattr(self, "_compilation_template") and isinstance(
self._compilation_template, str
):
return self._compilation_template
return (
"_compilations/"
"$ar_initial_album/"
"%shorten{$ar_combined_album}"
"%ifdefnotempty{ar_combined_year,_${ar_combined_year}}/"
"${ar_combined_disctrack}_%shorten{$title}"
)
@property
def soundtrack(self) -> str:
"""Get the path template for soundtracks."""
if self._is_classical:
return self.classical
if self._job.template_settings.no_soundtrack:
return self.default
if hasattr(self, "_soundtrack_template") and isinstance(
self._soundtrack_template, str
):
return self._soundtrack_template
return (
"_soundtrack/"
"$ar_initial_album/"
"%shorten{$ar_combined_album}"
"%ifdefnotempty{ar_combined_year,_${ar_combined_year}}/"
"${ar_combined_disctrack}_${artist}_%shorten{$title}"
)
@property
def classical(self) -> str:
"""Get the path template for classical music."""
if hasattr(self, "_classical_template") and isinstance(
self._classical_template, str
):
return self._classical_template
return (
"$ar_initial_composer/$ar_combined_composer/"
"%shorten{$ar_combined_work_top,48}"
"_[%shorten{$ar_classical_performer,32}]/"
"${ar_combined_disctrack}_%shorten{$ar_classical_title,64}"
"%ifdefnotempty{acoustid_id,_%shorten{$acoustid_id,8}}"
)
[docs]
class CliOutputConfig(Config):
_color: typing.Optional[bool]
_debug: typing.Optional[bool]
_job_info: typing.Optional[bool]
_mb_track_listing: typing.Optional[bool]
_one_line: typing.Optional[bool]
_stats: typing.Optional[bool]
_verbose: typing.Optional[bool]
@property
def color(self) -> bool:
if hasattr(self, "_color") and isinstance(self._color, bool):
return self._color
return True
@property
def debug(self) -> bool:
if hasattr(self, "_debug") and isinstance(self._debug, bool):
return self._debug
return False
@property
def job_info(self) -> bool:
if hasattr(self, "_job_info") and isinstance(self._job_info, bool):
return self._job_info
return False
@property
def mb_track_listing(self) -> bool:
if hasattr(self, "_mb_track_listing") and isinstance(
self._mb_track_listing, bool
):
return self._mb_track_listing
return False
@property
def one_line(self) -> bool:
if hasattr(self, "_one_line") and isinstance(self._one_line, bool):
return self._one_line
return False
@property
def stats(self) -> bool:
if hasattr(self, "_stats") and isinstance(self._stats, bool):
return self._stats
return False
@property
def verbose(self) -> bool:
if hasattr(self, "_verbose") and isinstance(self._verbose, bool):
return self._verbose
return False
[docs]
class Job:
"""Holds informations of one job which can handle multiple files.
A jobs represents one call of the program on the command line. This class
unifies and processes the data of the `argparse` and the `configparser`
call. It groups the `argparse` and the `configparser` key-value pairs into
parent properties. The properties of this class for example can be used to
display easily an overview message of the job.
"""
stats = Statistic()
args: ArgsDefault
config: typing.Optional[typing.List[configparser.ConfigParser]] = None
def __init__(self, args: ArgsDefault):
self.args = args
if args.config is not None:
self.config = self.__read_config(args.config)
self.msg = Message(self)
def __read_config(
self, file_paths: typing.List[str]
) -> typing.List[configparser.ConfigParser]:
configs: typing.List[configparser.ConfigParser] = []
for file_path in file_paths:
config = configparser.ConfigParser()
config.read(file_path)
configs.append(config)
return configs
@property
def selection(self) -> SelectionConfig:
return SelectionConfig(
self,
"selection",
{"source": "string", "target": "string", "source_as_target": "boolean"},
)
@property
def rename(self) -> RenameConfig:
return RenameConfig(
self,
"rename",
{
"backup_folder": "string",
"best_format": "boolean",
"dry_run": "boolean",
"move_action": "string",
"cleaning_action": "string",
},
)
@property
def filters(self) -> FiltersConfig:
return FiltersConfig(
self,
"filters",
{
"album_complete": "boolean",
"album_min": "integer",
"extension": "string",
"genre_classical": "string",
"field_skip": "string",
},
)
@property
def template_settings(self) -> TemplateSettingsConfig:
return TemplateSettingsConfig(
self,
"template_settings",
{
"classical": "boolean",
"shell_friendly": "boolean",
"no_soundtrack": "boolean",
},
)
@property
def path_templates(self) -> PathTemplatesConfig:
return PathTemplatesConfig(
self,
"path_templates",
{
"default_template": "string",
"compilation_template": "string",
"soundtrack_template": "string",
"classical_template": "string",
},
)
@property
def cli_output(self) -> CliOutputConfig:
return CliOutputConfig(
self,
"cli_output",
{
"color": "boolean",
"debug": "boolean",
"job_info": "boolean",
"mb_track_listing": "boolean",
"one_line": "boolean",
"stats": "boolean",
"verbose": "boolean",
},
)
@property
def metadata_actions(self) -> MetadataActionsConfig:
return MetadataActionsConfig(
self,
"metadata_actions",
{
"enrich_metadata": "boolean",
"remap_classical": "boolean",
},
)