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