Files
Server-scripts/rename_episodes.py
T

228 lines
6.5 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import re
import os
import sys
from pathlib import Path
from serverlibary import safe_rename,safe_delete
VIDEO_EXTENSIONS = {".mpg", ".avi", ".mkv", ".mp4", ".m4v", ".srt"}
UNWANTED_PATTERNS_POST = [
r"\d{2,3}k", r"MULTi", r"TV\ Series", r"[a-z]264", r"10bit", r"\[.*?\]",
r"320kbps", r"AC3", r"NF\.WEB", r"bdrip", r"RERIP",
r"DUAL-", r"LostFilm", r"readnfo", r"BluRay",
r"720p", r"1080p", r"WEB", r"AMZN", r"HDTV",
r"DVD", r"BDRip", r"HDRip", r"REPACK", r"XviD", r"ETHEL", f"DL ", f"HEVC"
]
UNWANTED_PATTERNS_PRE = [
r"www.UIndex.org", r"\[TorrentCounter\.to\]",
r"www.Torrenting.org", r"www.Torrenting.com"
]
GARBAGE_PATTERNS = [
r"Downloaded From.*", r"www\.YTS.*", r"YTS.*txt", r"YTS.*jp*g" , r".*\.nfo", r"YTSY.*", r"RARBG.*", r".*YIFY.*\.jpg", f"Torrent Downloaded From.*txt", "Screens"
]
DAMAGE_PATTERNS = [
(r"\.\.", "."),
(r"__", "_"),
(r"\s+", " "),
]
def iter_media_files(base: Path):
if base.is_file():
yield base
for path in base.rglob("*"):
if path.is_file() and path.suffix.lower() in VIDEO_EXTENSIONS:
yield path
def clean_name(name: Path, search=None, replace=None):
if name.is_file():
base, ext = os.path.splitext(name.name)
else:
base = name.name
ext = ""
#base = base.replace(" ", ".")
base = re.sub(r"s(\d{2})e", r"S\1E", base, flags=re.IGNORECASE)
for pattern in UNWANTED_PATTERNS_PRE:
base = re.sub(rf"{pattern}[^a-zA-Z0-9]*", "", base, flags=re.IGNORECASE)
base_new = base
for pattern in UNWANTED_PATTERNS_POST:
base_new = re.sub(rf"{pattern}.*$", "", base_new, flags=re.IGNORECASE)
if len(base_new) > 5:
base = base_new
if search and replace:
base = re.sub(search, replace, base)
# Remove all non-word characters at the end of the string
base = re.sub(r'[^A-Za-z0-9)]+$', '', base)
base = re.sub(r"\.+", ".", base)
base = re.sub(r" +", " ", base)
#base = re.sub(r" ", ".", base)
base = base.strip(".")
return base + ext
def capitalize_name(name: str):
return re.sub(
r"(^|[ ._\-])([a-z])",
lambda m: m.group(1) + m.group(2).upper(),
name.lower()
)
# -----------------------------
# Operations
# -----------------------------
def rename_files(base: Path, args):
for file in iter_media_files(base):
new_name = clean_name(file, args.search, args.replace)
if args.capitalize:
new_name = capitalize_name(new_name)
new_path = file.with_name(new_name)
safe_rename(file, new_path, args.dryrun, args.confirm)
def rename_directories(base: Path, args):
# reverse order → avoid renaming parent first
paths = [base] + list(base.rglob("*"))
new_base = base
if base.is_dir():
new_base = clean_name(base, args.search, args.replace)
if args.capitalize:
new_base = capitalize_name(new_base)
new_base = base.with_name(new_base)
for path in sorted(paths, reverse=True):
if not path.is_dir():
continue
new_name = clean_name(path, args.search, args.replace)
if args.capitalize:
new_name = capitalize_name(new_name)
new_path = path.with_name(new_name)
safe_rename(path, new_path, args.dryrun, args.confirm)
return new_base
def repair_names(base: Path, args):
for file in iter_media_files(base):
new_name = file.name
for pattern, repl in DAMAGE_PATTERNS:
new_name = re.sub(pattern, repl, new_name)
new_path = file.with_name(new_name)
safe_rename(file, new_path, args.dryrun, args.confirm)
def delete_garbage(base: Path, args):
for path in base.rglob("*"):
for pattern in GARBAGE_PATTERNS:
if re.match(pattern, path.name, re.IGNORECASE):
print(f"Deleting: {path}")
if not args.dryrun:
safe_delete(path)
# -----------------------------
# CLI
# -----------------------------
def parse_args():
parser = argparse.ArgumentParser(description="Media renamer tool")
parser.add_argument("-p","--path", default=".", help="Target directory")
parser.add_argument("-rep","--repair", action="store_true", help="Fix filenames")
parser.add_argument("-d","--delete", dest="delete", action="store_true", help="Delete junk files")
parser.add_argument("-cap","--capitalize", dest="capitalize", action="store_true", help="Capitalize names")
parser.add_argument("-r","--replace", nargs=2, metavar=("SEARCH", "REPLACE"))
parser.add_argument("-dr","--dryrun", action="store_true", help="Dry run")
parser.add_argument("-c","--confirm", action="store_true", help="Confirm each rename")
return parser.parse_args()
def main():
args = parse_args()
if args.replace:
args.search = args.replace
else:
args.search = args.replace = None
base = Path(args.path).resolve()
if not base.exists():
print("Invalid path")
return
home = Path.home()
if base == home or home in base.parents:
answer = input(
f"You are about to run on your home directory:\n"
f" {base}\n\n"
"This may process many files. Continue? [y/N]: "
).strip().lower()
if answer not in ("y", "yes"):
print("Aborted.")
sys.exit(1)
#video_files = []
#for file in iter_media_files(base):
# print(file)
# video_files.append(file)
#
#if len(video_files) == 0:
# print(f"Error: No media files found in {base}")
# sys.exit(1)
#
#for f in video_files:
# p = Path(f).resolve()
#
# # skip files not inside base
# try:
# rel = p.relative_to(base)
# except ValueError:
# continue
#
# # rel.parts includes directories + filename
# # depth 0: (file directly in base) -> len(parts) == 1
# # depth 1: base/sub/file -> len(parts) == 2
# if len(rel.parts) <= 2:
# raise ValueError(f"Invalid file location: {p} (inside base or depth 1 subdir)")
#
# 1. fix broken names first (optional but recommended)
if args.repair:
print(f"Repair names")
repair_names(base, args)
# 2. rename directories FIRST (important for path stability)
print(f"Rename directories: {base}")
base = rename_directories(base, args)
# 3. rename files AFTER
print(f"Rename files {base}")
rename_files(base, args)
# 4. cleanup
if args.delete:
delete_garbage(base, args)
if __name__ == "__main__":
main()