Compare commits

...

2 Commits

Author SHA1 Message Date
ae47d476f3 Add parse/rename CLI options 2022-02-09 18:08:06 +03:00
2204a3a2c3 Refactoring 2022-02-08 23:45:55 +03:00

View File

@ -2,9 +2,11 @@
import argparse
import collections
import enum
import logging
import os
import os.path
import pprint
import re
import sys
@ -42,12 +44,48 @@ PATTERNS = (
("unknown", r".*")
)
# noinspection PyInterpreter
class EnumAction(argparse.Action):
"""
Argparse action for handling Enums
"""
def __init__(self, **kwargs):
# Pop off the type value
enum_type = kwargs.pop("type", None)
# Ensure an Enum subclass is provided
if enum_type is None:
raise ValueError("type must be assigned an Enum when using EnumAction")
if not issubclass(enum_type, enum.Enum):
raise TypeError("type must be an Enum when using EnumAction")
# Generate choices from the Enum
kwargs.setdefault("choices", tuple(e.value for e in enum_type))
super(EnumAction, self).__init__(**kwargs)
self._enum = enum_type
def __call__(self, parser, namespace, values, option_string=None):
# Convert value back into an Enum
value = self._enum(values)
setattr(namespace, self.dest, value)
class CliAction(enum.Enum):
parse = "parse"
rename = "rename"
_lg = logging.getLogger("spqr.movie-renamer")
def main():
parser = argparse.ArgumentParser(description="Rename media files.")
parser.add_argument("target", type=str,
parser.add_argument("action", type=CliAction, action=EnumAction, metavar="ACTION",
help="what to do with media file/directory (%(choices)s)")
parser.add_argument("target", type=str, metavar="TARGET",
help="path to the media file/directory")
parser.add_argument("-v", "--verbose", action="store_true", default=False,
help="verbose output")
@ -56,54 +94,61 @@ def main():
loglevel = logging.DEBUG if args.verbose else logging.INFO
logging.basicConfig(level=loglevel)
if os.path.isdir(args.target):
process_dir(args.target)
else:
process_file(args.target)
process_path(args.action, args.target)
return 0
def process_dir(dir_path):
for fname in os.listdir(dir_path):
fpath = os.path.join(dir_path, fname)
process_file(fpath)
def process_file(fpath):
def process_path(action: CliAction, path):
# process only files
if not os.path.isfile(fpath):
_lg.debug("Not a file: %s", fpath)
return
if os.path.isdir(path):
for child_path in sorted(os.listdir(path)):
process_path(action, os.path.join(path, child_path))
# split filepath to dir path, title, and extension
dir_path, fname = os.path.split(fpath)
dir_path, fname = os.path.split(path)
title, ext = os.path.splitext(fname)
ext = ext[1:]
if ext not in PROCESSED_FILETYPES:
_lg.debug("Extension is not supported: %s", fpath)
_lg.debug("Extension is not supported: %s", path)
return
parsed_title = parse_title(title)
if action == CliAction.parse:
print_parsed_title(title, parsed_title)
return
# create file name from parsed chunks
if action == CliAction.rename:
pretty_title = generate_pretty_name(parsed_title)
pretty_title += ".%s" % ext
if pretty_title != fname:
_lg.warning("%s -> %s", fname, pretty_title)
return
def print_parsed_title(title, parsed):
print(title)
pprint.pprint(parsed, indent=4)
def generate_pretty_name(parsed_title):
""" Create file name from parsed chunks. """
chunk_order = [k for k, _ in PATTERNS]
chunk_order = ["name"] + chunk_order
episode_idx = chunk_order.index("episode") + 1
chunk_order = chunk_order[:episode_idx] + ["episode_name"] + chunk_order[episode_idx:]
ep_idx = chunk_order.index("episode") + 1
chunk_order = chunk_order[:ep_idx] + ["episode_name"] + chunk_order[ep_idx:]
result = []
for chunk_type in chunk_order:
if not parsed_title.get(chunk_type, []):
continue
result.append(".".join(parsed_title[chunk_type]))
result.append(ext)
result = ".".join(result)
if result != fname:
_lg.warning("%s -> %s", fname, result)
return result
def _get_parsed_title_dict(chunk_list, chunk_map):
""" Get {chunk_type: [chunk_value_1, ..., chunk_value_n]} dictionary. """
p_title = collections.defaultdict(list)
for idx, chunk in enumerate(chunk_list):
chunk_type = chunk_map[idx]
@ -112,7 +157,7 @@ def _get_parsed_title_dict(chunk_list, chunk_map):
def _guess_combined(chunk_values, chunk_map):
""" Try to combine unknown chunks in pairs and parse them """
""" Try to combine unknown chunks in pairs and parse them. """
is_changed = False
p_title = _get_parsed_title_dict(chunk_values, chunk_map)
if len(p_title["unknown"]) < 2:
@ -165,14 +210,14 @@ def parse_title(title):
# remove non-word chunks (like single hyphens), but leave ampersands (&)
chunk_values = list(filter(lambda ch: re.search(r"(\w|&)+", ch), chunk_values))
chunk_map = [] # list of chunk_types
# parse each chunk
chunk_map = []
for ch_value in chunk_values:
chunk_map.append(guess_part(ch_value))
_, chunk_values, chunk_map = _guess_combined(chunk_values, chunk_map)
# # try to parse unknown chunks, replacing all hyphens in them with dots
# try to parse unknown chunks, replacing all hyphens in them with dots
p_title = _get_parsed_title_dict(chunk_values, chunk_map)
is_changed = False
if p_title.get("unknown"):
@ -230,11 +275,12 @@ def parse_title(title):
return dict(p_title)
def guess_part(fname_part):
for pat_type, pattern in PATTERNS:
def guess_part(chunk_value):
""" Return chunk type for given chunk value. """
for chunk_type, pattern in PATTERNS:
full_match_pat = r"^" + pattern + r"$"
if re.match(full_match_pat, fname_part, flags=re.I):
return pat_type
if re.match(full_match_pat, chunk_value, flags=re.I):
return chunk_type
raise RuntimeError("unhandled pattern type")