Add immutable revisions, PGVN editor, asset replacement, merge and rebase workflows, authenticated media delivery and reachability GC. Harden the script interpreter, pin unrpyc and remove tracked build metadata. Include current compatibility checks, Naninovel import and regression tests.
101 lines
4.6 KiB
Python
101 lines
4.6 KiB
Python
"""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
|
|
from pathlib import Path
|
|
import tempfile
|
|
import sys
|
|
|
|
from app.converter.archive import ArchiveError, extract
|
|
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):
|
|
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())
|