feat: Add AI file renamer

This commit is contained in:
abdus committed 2026-10-04 08:20:50 +02:00
1 parent f3526b60e5
commit b913274f87
1 file changed
+124
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Rename files according to filename mappings returned by OpenRouter."""
import argparse
import json
import os
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
# Fill in the instructions that should guide the filename changes.
SYSTEM_PROMPT = """
Your task is to rename files. You will be given a JSON array of filenames.
You must return a JSON array of objects, each with "find" and "replace" fields, where "find" is the original filename and "replace" is the new filename.
Only include files that need to be renamed; do not include files that should remain unchanged.
Ensure that the new filenames are valid and do not contain any directory paths or invalid characters.
The output format is "$actors -- @$studio -- $title -- $date"
- $actors are separated by commas, and should be the names of the actors in the video.
- $studio is the name of the studio that produced the video. Use "Unknown" if the studio is not known.
- Not all files have title or studio or date, leave those bits out if they are not known.
- $date must be in YYYY-MM-DD format. PRT releases are usually in YY.MM.DD format, so you will need to convert that to YYYY-MM-DD.
Examples:
- 15.01.10_-_Aspen_Ora_-_Cock_Crazy_-_x265.mp4 -> Aspen Ora -- @Unknown -- Cock Crazy -- 2015-01-10.mp4
- [onlyfans.com] Lilijunex - Huge Dildo Machine Slams Lilijunex [2026-06-28 -> Lilijunex -- @OnlyFans -- Huge Dildo Machine Slams Lilijunex -- 2026-06-28.mp4
"""
OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
def send_request(payload: dict[str, Any], api_key: str) -> dict[str, Any]:
request = Request(
OPENROUTER_URL,
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urlopen(request, timeout=60) as response:
result = json.loads(response.read().decode("utf-8"))
except HTTPError as error:
detail = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"OpenRouter returned HTTP {error.code}: {detail}") from error
except URLError as error:
raise RuntimeError(f"Could not reach OpenRouter: {error.reason}") from error
return result
def get_renames(filenames: list[str], api_key: str, model: str) -> dict[str, str]:
payload = {
"model": model,
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": json.dumps(filenames)},
],
}
result = send_request(payload, api_key)
try:
content = result["choices"][0]["message"]["content"]
mappings = json.loads(content)
except (KeyError, IndexError, TypeError, json.JSONDecodeError) as error:
raise RuntimeError("OpenRouter response did not contain a JSON mapping array.") from error
if not isinstance(mappings, list):
raise RuntimeError("OpenRouter response must be a JSON array.")
for mapping in mappings:
if not isinstance(mapping, dict) or not isinstance(mapping.get("find"), str) or not isinstance(mapping.get("replace"), str):
raise RuntimeError('Each mapping must have string "find" and "replace" fields.')
return {mapping["find"]: mapping["replace"] for mapping in mappings}
def main() -> None:
parser = argparse.ArgumentParser(description="Ask OpenRouter for filename changes and rename the supplied files.")
parser.add_argument("paths", nargs="+", type=Path, help="one or more file paths")
parser.add_argument(
"--openrouter-api-key",
default=os.environ.get("OPENROUTER_API_KEY"),
help="OpenRouter API key (defaults to OPENROUTER_API_KEY)",
)
parser.add_argument(
"--openrouter-model",
default=os.environ.get("OPENROUTER_MODEL", "~deepseek/deepseek-flash-latest"),
help="OpenRouter model (defaults to OPENROUTER_MODEL)",
)
args = parser.parse_args()
for path in args.paths:
if not path.exists():
parser.error(f"file does not exist: {path}")
filenames = [path.name for path in args.paths]
by_name = get_renames(filenames, args.openrouter_api_key, args.openrouter_model)
planned = []
for path in args.paths:
replacement = by_name.get(path.name)
if replacement is not None and replacement != path.name:
if not replacement or replacement in {".", ".."} or "/" in replacement or "\\" in replacement:
raise RuntimeError(f"Invalid replacement filename: {replacement!r}")
planned.append((path, path.with_name(replacement)))
destinations = [destination for _, destination in planned]
if len(set(destinations)) != len(destinations):
raise RuntimeError("Multiple files would be renamed to the same path.")
for source, destination in planned:
if destination.exists() and destination != source:
raise RuntimeError(f"Destination already exists: {destination}")
for source, destination in planned:
source.rename(destination)
print(f"{source} -> {destination}")
if __name__ == "__main__":
try:
main()
except RuntimeError as error:
raise SystemExit(str(error))