"""Routes for the workshop overlay type (add/remove items, manual rebuild, admin global refresh).""" from __future__ import annotations from flask import Blueprint, Response, redirect, render_template, request from sqlalchemy import delete as sa_delete from sqlalchemy import select from l4d2web.auth import current_user, require_admin, require_login from l4d2web.db import session_scope from l4d2web.models import ( Job, Overlay, OverlayWorkshopItem, WorkshopItem, ) from l4d2web.services import steam_workshop from l4d2web.services.job_worker import enqueue_build_overlay bp = Blueprint("workshop", __name__) def _check_workshop_overlay_access(overlay_id: int, user, db): overlay = db.scalar(select(Overlay).where(Overlay.id == overlay_id)) if overlay is None: return None, Response(status=404) if overlay.type != "workshop": return None, Response("not a workshop overlay", status=400) if overlay.user_id != user.id and not user.admin: return None, Response(status=403) return overlay, None def _render_item_table(overlay_id: int): with session_scope() as db: overlay = db.scalar(select(Overlay).where(Overlay.id == overlay_id)) items = db.scalars( select(WorkshopItem) .join( OverlayWorkshopItem, OverlayWorkshopItem.workshop_item_id == WorkshopItem.id, ) .where(OverlayWorkshopItem.overlay_id == overlay_id) .order_by(WorkshopItem.created_at) ).all() # Detach so attributes survive after the session closes. for item in items: db.expunge(item) if overlay is not None: db.expunge(overlay) return render_template( "_overlay_item_table.html", overlay=overlay, workshop_items=items, ) @bp.post("/overlays//items") @require_login def add_items(overlay_id: int) -> Response: user = current_user() assert user is not None raw_input = request.form.get("input", "").strip() mode = request.form.get("input_mode", "items") if not raw_input: return Response("missing input", status=400) try: ids = steam_workshop.parse_workshop_input(raw_input) except ValueError as exc: return Response(str(exc), status=400) if mode == "collection": if len(ids) != 1: return Response("collection mode expects exactly one id or url", status=400) try: ids = steam_workshop.resolve_collection(ids[0]) except Exception as exc: return Response(f"failed to resolve collection: {exc}", status=502) if not ids: return Response("collection has no items", status=400) try: metas = steam_workshop.fetch_metadata_batch(ids, mode="add") except steam_workshop.WorkshopValidationError as exc: return Response(str(exc), status=400) except Exception as exc: return Response(f"steam api error: {exc}", status=502) with session_scope() as db: overlay, err = _check_workshop_overlay_access(overlay_id, user, db) if err is not None: return err for meta in metas: wi = db.scalar(select(WorkshopItem).where(WorkshopItem.steam_id == meta.steam_id)) if wi is None: wi = WorkshopItem(steam_id=meta.steam_id) db.add(wi) wi.title = meta.title wi.filename = meta.filename wi.file_url = meta.file_url wi.file_size = meta.file_size wi.time_updated = meta.time_updated wi.preview_url = meta.preview_url wi.last_error = "" if meta.result == 1 else f"steam result {meta.result}" db.flush() existing = db.scalar( select(OverlayWorkshopItem).where( OverlayWorkshopItem.overlay_id == overlay_id, OverlayWorkshopItem.workshop_item_id == wi.id, ) ) if existing is None: db.add(OverlayWorkshopItem(overlay_id=overlay_id, workshop_item_id=wi.id)) enqueue_build_overlay(db, overlay_id=overlay_id, user_id=user.id) return _render_item_table(overlay_id) @bp.post("/overlays//items//delete") @require_login def remove_item(overlay_id: int, item_id: int) -> Response: user = current_user() assert user is not None with session_scope() as db: overlay, err = _check_workshop_overlay_access(overlay_id, user, db) if err is not None: return err result = db.execute( sa_delete(OverlayWorkshopItem).where( OverlayWorkshopItem.overlay_id == overlay_id, OverlayWorkshopItem.workshop_item_id == item_id, ) ) if result.rowcount == 0: return Response(status=404) enqueue_build_overlay(db, overlay_id=overlay_id, user_id=user.id) return _render_item_table(overlay_id) @bp.post("/admin/workshop/refresh") @require_admin def admin_refresh() -> Response: user = current_user() assert user is not None with session_scope() as db: db.add( Job( user_id=user.id, server_id=None, operation="refresh_workshop_items", state="queued", ) ) return redirect("/admin/jobs")