Compare commits
2 Commits
master
...
feature/19
| Author | SHA1 | Date | |
|---|---|---|---|
| ae47d476f3 | |||
| 2204a3a2c3 |
108
renamer.py
108
renamer.py
@ -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")
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user