228 lines
6.5 KiB
Python
Executable File
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()
|