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 argparse
import collections import collections
import enum
import logging import logging
import os import os
import os.path import os.path
import pprint
import re import re
import sys import sys
@ -42,12 +44,48 @@ PATTERNS = (
("unknown", r".*") ("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") _lg = logging.getLogger("spqr.movie-renamer")
def main(): def main():
parser = argparse.ArgumentParser(description="Rename media files.") 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") help="path to the media file/directory")
parser.add_argument("-v", "--verbose", action="store_true", default=False, parser.add_argument("-v", "--verbose", action="store_true", default=False,
help="verbose output") help="verbose output")
@ -56,54 +94,61 @@ def main():
loglevel = logging.DEBUG if args.verbose else logging.INFO loglevel = logging.DEBUG if args.verbose else logging.INFO
logging.basicConfig(level=loglevel) logging.basicConfig(level=loglevel)
if os.path.isdir(args.target): process_path(args.action, args.target)
process_dir(args.target)
else:
process_file(args.target)
return 0 return 0
def process_dir(dir_path): def process_path(action: CliAction, path):
for fname in os.listdir(dir_path):
fpath = os.path.join(dir_path, fname)
process_file(fpath)
def process_file(fpath):
# process only files # process only files
if not os.path.isfile(fpath): if os.path.isdir(path):
_lg.debug("Not a file: %s", fpath) for child_path in sorted(os.listdir(path)):
return process_path(action, os.path.join(path, child_path))
# split filepath to dir path, title, and extension # 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) title, ext = os.path.splitext(fname)
ext = ext[1:] ext = ext[1:]
if ext not in PROCESSED_FILETYPES: if ext not in PROCESSED_FILETYPES:
_lg.debug("Extension is not supported: %s", fpath) _lg.debug("Extension is not supported: %s", path)
return return
parsed_title = parse_title(title) 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 = [k for k, _ in PATTERNS]
chunk_order = ["name"] + chunk_order chunk_order = ["name"] + chunk_order
episode_idx = chunk_order.index("episode") + 1 ep_idx = chunk_order.index("episode") + 1
chunk_order = chunk_order[:episode_idx] + ["episode_name"] + chunk_order[episode_idx:] chunk_order = chunk_order[:ep_idx] + ["episode_name"] + chunk_order[ep_idx:]
result = [] result = []
for chunk_type in chunk_order: for chunk_type in chunk_order:
if not parsed_title.get(chunk_type, []): if not parsed_title.get(chunk_type, []):
continue continue
result.append(".".join(parsed_title[chunk_type])) result.append(".".join(parsed_title[chunk_type]))
result.append(ext)
result = ".".join(result) result = ".".join(result)
return result
if result != fname:
_lg.warning("%s -> %s", fname, result)
def _get_parsed_title_dict(chunk_list, chunk_map): def _get_parsed_title_dict(chunk_list, chunk_map):
""" Get {chunk_type: [chunk_value_1, ..., chunk_value_n]} dictionary. """
p_title = collections.defaultdict(list) p_title = collections.defaultdict(list)
for idx, chunk in enumerate(chunk_list): for idx, chunk in enumerate(chunk_list):
chunk_type = chunk_map[idx] 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): 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 is_changed = False
p_title = _get_parsed_title_dict(chunk_values, chunk_map) p_title = _get_parsed_title_dict(chunk_values, chunk_map)
if len(p_title["unknown"]) < 2: if len(p_title["unknown"]) < 2:
@ -165,14 +210,14 @@ def parse_title(title):
# remove non-word chunks (like single hyphens), but leave ampersands (&) # remove non-word chunks (like single hyphens), but leave ampersands (&)
chunk_values = list(filter(lambda ch: re.search(r"(\w|&)+", ch), chunk_values)) chunk_values = list(filter(lambda ch: re.search(r"(\w|&)+", ch), chunk_values))
chunk_map = [] # list of chunk_types
# parse each chunk # parse each chunk
chunk_map = []
for ch_value in chunk_values: for ch_value in chunk_values:
chunk_map.append(guess_part(ch_value)) chunk_map.append(guess_part(ch_value))
_, chunk_values, chunk_map = _guess_combined(chunk_values, chunk_map) _, 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) p_title = _get_parsed_title_dict(chunk_values, chunk_map)
is_changed = False is_changed = False
if p_title.get("unknown"): if p_title.get("unknown"):
@ -230,11 +275,12 @@ def parse_title(title):
return dict(p_title) return dict(p_title)
def guess_part(fname_part): def guess_part(chunk_value):
for pat_type, pattern in PATTERNS: """ Return chunk type for given chunk value. """
for chunk_type, pattern in PATTERNS:
full_match_pat = r"^" + pattern + r"$" full_match_pat = r"^" + pattern + r"$"
if re.match(full_match_pat, fname_part, flags=re.I): if re.match(full_match_pat, chunk_value, flags=re.I):
return pat_type return chunk_type
raise RuntimeError("unhandled pattern type") raise RuntimeError("unhandled pattern type")