185 lines
5.7 KiB
Python
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")
|