Files
designloves2-itda/itda_server.py
T
2026-06-13 19:42:31 +09:00

185 lines
5.7 KiB
Python

import json
import mimetypes
import os
import re
import shutil
import time
from pathlib import Path
from aiohttp import web
from server import PromptServer
ROOT = Path(__file__).resolve().parent
ITDA_ROOT = ROOT / "ITDA"
PROJECTS_DIR = ITDA_ROOT / "projects"
MEDIA_DIR = ITDA_ROOT / "media"
CACHE_DIR = ITDA_ROOT / "cache"
WEB_DIR = ROOT / "web"
INPUT_ITDA = ROOT.parent.parent / "input" / "ITDA"
INPUT_SNAPSHOT = ROOT.parent.parent / "input" / "ITDA-SNAPSHOT"
OUTPUT_ITDA = ROOT.parent.parent / "output" / "ITDA"
VIDEO_EXT = {".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v"}
AUDIO_EXT = {".wav", ".mp3", ".flac", ".aac", ".m4a", ".ogg"}
IMAGE_EXT = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff"}
def ensure_dirs():
for p in [PROJECTS_DIR, MEDIA_DIR, CACHE_DIR, INPUT_ITDA, INPUT_SNAPSHOT, OUTPUT_ITDA]:
p.mkdir(parents=True, exist_ok=True)
def safe_name(name: str) -> str:
name = (name or "untitled").strip()
name = re.sub(r"[^a-zA-Z0-9가-힣._ -]+", "_", name)
name = name.strip(" .")
return name or "untitled"
def safe_project_path(project_name: str) -> Path:
project_name = safe_name(project_name)
path = PROJECTS_DIR / f"{project_name}.itda.json"
resolved = path.resolve()
if PROJECTS_DIR.resolve() not in resolved.parents and resolved != PROJECTS_DIR.resolve():
raise web.HTTPBadRequest(text="Invalid project name")
return resolved
def classify_media(path: Path) -> str:
ext = path.suffix.lower()
if ext in VIDEO_EXT:
return "video"
if ext in AUDIO_EXT:
return "audio"
if ext in IMAGE_EXT:
return "image"
return "unknown"
def media_record(path: Path, base: Path):
stat = path.stat()
mtype = classify_media(path)
rel = path.relative_to(base).as_posix()
return {
"id": f"media_{abs(hash(str(path.resolve())))}",
"name": path.name,
"type": mtype,
"path": str(path.resolve()),
"relativePath": rel,
"size": stat.st_size,
"modified": int(stat.st_mtime),
}
async def editor(_request):
ensure_dirs()
html = WEB_DIR / "itda_editor.html"
return web.FileResponse(html)
async def static_file(request):
filename = request.match_info.get("filename", "")
path = (WEB_DIR / filename).resolve()
if WEB_DIR.resolve() not in path.parents or not path.exists() or path.is_dir():
raise web.HTTPNotFound(text="Not found")
content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
return web.FileResponse(path, headers={"Content-Type": content_type})
async def health(_request):
ensure_dirs()
return web.json_response({"ok": True, "app": "ITDA", "version": "0.1.0-foundation"})
async def scan_media(_request):
ensure_dirs()
items = []
for base in [INPUT_ITDA, INPUT_SNAPSHOT, MEDIA_DIR]:
if not base.exists():
continue
for p in base.rglob("*"):
if p.is_file() and classify_media(p) != "unknown":
items.append(media_record(p, base))
items.sort(key=lambda x: (x["type"], x["name"].lower()))
return web.json_response({"items": items})
async def list_projects(_request):
ensure_dirs()
projects = []
for p in PROJECTS_DIR.glob("*.itda.json"):
stat = p.stat()
projects.append({"name": p.name.replace(".itda.json", ""), "path": str(p), "modified": int(stat.st_mtime)})
projects.sort(key=lambda x: x["modified"], reverse=True)
return web.json_response({"projects": projects})
async def load_project(request):
ensure_dirs()
name = request.match_info.get("name", "untitled")
path = safe_project_path(name)
if not path.exists():
return web.json_response(default_project(name))
return web.json_response(json.loads(path.read_text(encoding="utf-8")))
async def save_project(request):
ensure_dirs()
data = await request.json()
name = safe_name(data.get("name") or "untitled")
data["name"] = name
data["updatedAt"] = int(time.time())
data.setdefault("version", "0.1.0")
path = safe_project_path(name)
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
(MEDIA_DIR / name).mkdir(parents=True, exist_ok=True)
(CACHE_DIR / name).mkdir(parents=True, exist_ok=True)
return web.json_response({"ok": True, "name": name, "path": str(path)})
async def prerender_range(request):
ensure_dirs()
data = await request.json()
project = safe_name(data.get("project") or "untitled")
cache_dir = CACHE_DIR / project
cache_dir.mkdir(parents=True, exist_ok=True)
marker = cache_dir / "last_prerender_request.json"
marker.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
return web.json_response({"ok": True, "status": "queued-placeholder", "cacheDir": str(cache_dir)})
def default_project(name="untitled"):
now = int(time.time())
return {
"app": "ITDA",
"version": "0.1.0",
"name": safe_name(name),
"fps": 24,
"frame": 0,
"range": {"start": None, "end": None},
"snap": True,
"loop": False,
"previewMode": "single",
"previewMute": False,
"media": [],
"clips": [],
"groups": [],
"createdAt": now,
"updatedAt": now,
}
routes = PromptServer.instance.routes
routes.get("/itda/editor")(editor)
routes.get("/itda/web/{filename}")(static_file)
routes.get("/itda/health")(health)
routes.get("/itda/media/scan")(scan_media)
routes.get("/itda/projects")(list_projects)
routes.get("/itda/project/{name}")(load_project)
routes.post("/itda/project/save")(save_project)
routes.post("/itda/prerender")(prerender_range)
ensure_dirs()
print("[ITDA] v0.1 Foundation server loaded")