"""Check/import a local collection. Rejected games never get queued. python -m app.import_library /games --check python -m app.import_library /games --import --owner admin """ import argparse import asyncio import json import sys from pathlib import Path from app.converter.compatibility import CompatibilityError, Issue from app.converter.preflight import inspect_archive, inspect_directory ARCHIVES = (".zip", ".7z", ".rar", ".tar", ".tar.gz", ".tgz", ".tar.bz2", ".tar.xz") def entries(root: Path): """An unpacked copy takes precedence over an archive with the identical name.""" directories = {p.name.casefold() for p in root.iterdir() if p.is_dir() and not p.name.startswith(".")} for source in sorted(root.iterdir(), key=lambda p: p.name.casefold()): if source.name.startswith(".") or source.is_symlink(): continue suffix = next((ext for ext in ARCHIVES if source.name.lower().endswith(ext)), None) if source.is_file() and suffix and source.name[:-len(suffix)].casefold() in directories: continue yield source def check(source: Path) -> dict: try: if source.is_dir(): return inspect_directory(source) if not source.name.lower().endswith(ARCHIVES): if source.suffix.lower() == ".exe" and source.stat().st_size >= 4: with source.open("rb") as handle: handle.seek(-4, 2) if handle.read(4) == b"GDPC": raise CompatibilityError("godot", [Issue("unsupported_engine", "В EXE обнаружен встроенный пакет Godot; его исполнение пока не поддерживается")]) raise CompatibilityError("unknown", [Issue("not_distribution", "Нужна папка или архив полного дистрибутива; отдельные EXE и RPA не импортируются")]) return inspect_archive(source, source.name) except CompatibilityError as exc: return exc.report() except Exception as exc: return {"compatible": False, "engine": "unknown", "error": str(exc), "issues": [{"code": "input_error", "message": str(exc), "location": source.name, "count": 1}]} async def import_one(source: Path, report: dict, username: str | None) -> dict: from sqlalchemy import select from app.db import SessionLocal from app.models import Game, GameVersion, Role, User from app.worker import convert async with SessionLocal() as db: stmt = select(User).join(Role).order_by(User.id) stmt = stmt.where(User.username == username) if username else stmt.where(Role.name == "admin") owner = await db.scalar(stmt.limit(1)) if owner is None: raise ValueError("Пользователь для импорта не найден") game = Game(title=source.name, engine=report["engine"], created_by_id=owner.id) db.add(game) await db.flush() version = GameVersion(game_id=game.id, owner_id=owner.id, name="Оригинал", upload_name=source.name, status="importing", report=report) db.add(version) await db.commit() game_id, version_id = game.id, version.id ok = await convert(version_id, source, source.name) return {"gameId": game_id, "versionId": version_id, "status": "ready" if ok else "failed"} async def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("root", type=Path) mode = parser.add_mutually_exclusive_group(required=True) mode.add_argument("--check", action="store_true", help="Только проверка; без изменения библиотеки") mode.add_argument("--import", dest="import_games", action="store_true", help="Импортировать совместимые игры") parser.add_argument("--owner") parser.add_argument("--output", type=Path, help="Записать полный JSON-отчёт") args = parser.parse_args() if not args.root.is_dir(): parser.error("root должен быть каталогом коллекции") results = [] for source in entries(args.root): print(json.dumps({"game": source.name, "status": "checking"}, ensure_ascii=False), flush=True) report = await asyncio.to_thread(check, source) result = {"game": source.name, **report} if args.import_games and report.get("compatible"): result.update(await import_one(source, report, args.owner)) results.append(result) print(json.dumps(result, ensure_ascii=False), flush=True) if args.output: args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8") if any(not result.get("compatible") or result.get("status") == "failed" for result in results): raise SystemExit(2) if __name__ == "__main__": sys.stdout.reconfigure(encoding="utf-8") asyncio.run(main())