feat: Add AI file renamer
This commit is contained in:
1 parent
f3526b60e5
commit
b913274f87
1 file changed
+124
Executable
+124
@@ -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))
|
||||
Reference in new issue
Block a user