54465f751c
- Serie-detail side: sæt under serie + enkelt/bulk opret - Billedhentning: URL→lokal download med source_url til re-sync - Advarselsbadge (⚠) på kort med eksterne URLs (ingen streaming) - Sync/Verify endpoints + knapper i admin UI - Brugerstyring: opret/slet (self-delete beskyttet) - Dark mode flash fix: render-blokerende script i <head> - Persistent volume til billeder (pokedexter_images)
615 lines
26 KiB
Python
615 lines
26 KiB
Python
import csv
|
|
import io
|
|
import os
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile, File, Form, status
|
|
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import selectinload
|
|
from pydantic import BaseModel
|
|
|
|
from ..database import get_db
|
|
from ..models import User, Serie, Set, Card
|
|
from ..auth import require_admin, hash_password
|
|
|
|
router = APIRouter()
|
|
templates = Jinja2Templates(directory="app/templates")
|
|
|
|
UPLOAD_DIR = Path("app/static/images")
|
|
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
# ─── Hjælpefunktioner ───────────────────────────────────────────────
|
|
|
|
async def download_image_from_url(url: str, serie_name: str, set_code: str, number: str) -> str | None:
|
|
"""Download billede fra URL og gem lokalt. Returnerer relativ sti eller None ved fejl."""
|
|
if not url or not url.startswith(("http://", "https://")):
|
|
return url # Allerede lokal sti eller None
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
|
|
resp = await client.get(url)
|
|
resp.raise_for_status()
|
|
|
|
# Bestem filtype ud fra content-type
|
|
ct = resp.headers.get("content-type", "")
|
|
ext_map = {"image/jpeg": ".jpg", "image/png": ".png",
|
|
"image/gif": ".gif", "image/webp": ".webp"}
|
|
ext = ext_map.get(ct.split(";")[0].strip(), ".jpg")
|
|
|
|
serie_folder = serie_name.replace(" ", "_").replace("/", "_")
|
|
folder = UPLOAD_DIR / serie_folder / set_code
|
|
folder.mkdir(parents=True, exist_ok=True)
|
|
|
|
filename = f"{number}{ext}"
|
|
filepath = folder / filename
|
|
filepath.write_bytes(resp.content)
|
|
|
|
return f"images/{serie_folder}/{set_code}/{filename}"
|
|
except Exception as e:
|
|
print(f"Kunne ikke downloade billede {url}: {e}")
|
|
return None # Ingen fallback til URL — offline only
|
|
|
|
|
|
# ─── Pydantic schemas ───────────────────────────────────────────────
|
|
|
|
class SerieCreate(BaseModel):
|
|
name: str
|
|
description: str | None = None
|
|
|
|
class SetCreate(BaseModel):
|
|
code: str
|
|
name: str
|
|
release_date: str | None = None
|
|
description: str | None = None
|
|
|
|
class CardCreate(BaseModel):
|
|
number: str
|
|
name: str
|
|
image: str | None = None # URL
|
|
|
|
class NewUserForm(BaseModel):
|
|
username: str
|
|
password: str
|
|
role: str = "user"
|
|
|
|
|
|
# ─── Admin HTML pages ───────────────────────────────────────────────
|
|
|
|
@router.get("/admin", response_class=HTMLResponse)
|
|
async def admin_dashboard(request: Request, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin)):
|
|
result = await db.execute(select(User).order_by(User.username))
|
|
users = result.scalars().all()
|
|
return templates.TemplateResponse("admin/dashboard.html", {
|
|
"request": request, "users": users
|
|
})
|
|
|
|
|
|
@router.get("/admin/series", response_class=HTMLResponse)
|
|
async def admin_series(request: Request, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin)):
|
|
result = await db.execute(select(Serie).options(selectinload(Serie.sets)).order_by(Serie.name))
|
|
series = result.scalars().all()
|
|
return templates.TemplateResponse("admin/series.html", {"request": request, "series": series})
|
|
|
|
|
|
@router.get("/admin/series/{serie_name}", response_class=HTMLResponse)
|
|
async def admin_series_detail(request: Request, serie_name: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin)):
|
|
result = await db.execute(
|
|
select(Serie).where(Serie.name == serie_name).options(
|
|
selectinload(Serie.sets).selectinload(Set.cards)
|
|
)
|
|
)
|
|
serie = result.scalar_one_or_none()
|
|
if not serie:
|
|
return templates.TemplateResponse("404.html", {"request": request}, status_code=404)
|
|
|
|
# Hent serier til dropdown (til CSV import)
|
|
series_result = await db.execute(select(Serie).order_by(Serie.name))
|
|
all_series = series_result.scalars().all()
|
|
|
|
return templates.TemplateResponse("admin/serie_detail.html", {
|
|
"request": request, "serie": serie, "all_series": all_series
|
|
})
|
|
|
|
|
|
@router.get("/admin/sets", response_class=HTMLResponse)
|
|
async def admin_sets_page(request: Request, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin)):
|
|
result = await db.execute(
|
|
select(Set).options(selectinload(Set.serie), selectinload(Set.cards)).order_by(Set.code)
|
|
)
|
|
sets = result.scalars().all()
|
|
series_result = await db.execute(select(Serie).order_by(Serie.name))
|
|
series = series_result.scalars().all()
|
|
return templates.TemplateResponse("admin/sets.html", {"request": request, "sets": sets, "series": series})
|
|
|
|
|
|
@router.get("/admin/cards/{set_code}", response_class=HTMLResponse)
|
|
async def admin_cards_page(request: Request, set_code: str, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin)):
|
|
result = await db.execute(
|
|
select(Set).where(Set.code == set_code.upper()).options(
|
|
selectinload(Set.cards), selectinload(Set.serie)
|
|
)
|
|
)
|
|
s = result.scalar_one_or_none()
|
|
if not s:
|
|
return templates.TemplateResponse("404.html", {"request": request}, status_code=404)
|
|
return templates.TemplateResponse("admin/cards.html", {"request": request, "set": s})
|
|
|
|
# ─── Users (admin) ─────────────────────────────────────────────────
|
|
|
|
@router.post("/api/admin/users")
|
|
async def create_user(data: NewUserForm, db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
exists = await db.execute(select(User).where(User.username == data.username))
|
|
if exists.scalar_one_or_none():
|
|
raise HTTPException(status_code=400, detail="Bruger findes allerede")
|
|
user = User(username=data.username, password_hash=hash_password(data.password), role=data.role)
|
|
db.add(user)
|
|
await db.commit()
|
|
return {"ok": True, "username": user.username, "role": user.role}
|
|
|
|
@router.get("/api/admin/users")
|
|
async def list_users(db: AsyncSession = Depends(get_db), _: User = Depends(require_admin)):
|
|
result = await db.execute(select(User).order_by(User.username))
|
|
return [{"id": u.id, "username": u.username, "role": u.role, "created_at": str(u.created_at)}
|
|
for u in result.scalars().all()]
|
|
|
|
@router.delete("/api/admin/users/{user_id}")
|
|
async def delete_user(user_id: int, db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_admin)):
|
|
result = await db.execute(select(User).where(User.id == user_id))
|
|
user = result.scalar_one_or_none()
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="Bruger ikke fundet")
|
|
if user.id == current_user.id:
|
|
raise HTTPException(status_code=400, detail="Du kan ikke slette dig selv")
|
|
await db.delete(user)
|
|
await db.commit()
|
|
return {"ok": True}
|
|
|
|
|
|
# ─── API: Series ────────────────────────────────────────────────────
|
|
|
|
@router.post("/api/admin/series")
|
|
async def create_serie(data: SerieCreate, db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
result = await db.execute(select(Serie).where(Serie.name == data.name))
|
|
if result.scalar_one_or_none():
|
|
raise HTTPException(status_code=400, detail="Serie findes allerede")
|
|
serie = Serie(name=data.name, description=data.description)
|
|
db.add(serie)
|
|
await db.commit()
|
|
return {"ok": True, "id": serie.id, "name": serie.name}
|
|
|
|
@router.get("/api/admin/series")
|
|
async def list_series(db: AsyncSession = Depends(get_db), _: User = Depends(require_admin)):
|
|
result = await db.execute(select(Serie).options(selectinload(Serie.sets)).order_by(Serie.name))
|
|
return [{"id": s.id, "name": s.name, "description": s.description or "",
|
|
"set_count": len(s.sets)} for s in result.scalars().all()]
|
|
|
|
@router.delete("/api/admin/series/{serie_id}")
|
|
async def delete_serie(serie_id: int, db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
result = await db.execute(select(Serie).where(Serie.id == serie_id))
|
|
s = result.scalar_one_or_none()
|
|
if not s:
|
|
raise HTTPException(status_code=404, detail="Serie ikke fundet")
|
|
await db.delete(s)
|
|
await db.commit()
|
|
return {"ok": True}
|
|
|
|
@router.post("/api/admin/series/import")
|
|
async def import_series(file: UploadFile = File(...), db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
content = await file.read()
|
|
text = content.decode("utf-8-sig")
|
|
reader = csv.DictReader(io.StringIO(text))
|
|
created = 0
|
|
errors = []
|
|
for i, row in enumerate(reader, start=2):
|
|
name = row.get("name", "").strip()
|
|
if not name:
|
|
errors.append(f"Linje {i}: mangler navn")
|
|
continue
|
|
try:
|
|
exists = await db.execute(select(Serie).where(Serie.name == name))
|
|
if exists.scalar_one_or_none():
|
|
errors.append(f"Linje {i}: '{name}' findes allerede")
|
|
continue
|
|
serie = Serie(name=name, description=row.get("description", "").strip() or None)
|
|
db.add(serie)
|
|
created += 1
|
|
except Exception as e:
|
|
errors.append(f"Linje {i}: {str(e)}")
|
|
await db.commit()
|
|
return {"ok": True, "created": created, "errors": errors}
|
|
|
|
|
|
# ─── API: Sets ──────────────────────────────────────────────────────
|
|
|
|
@router.post("/api/admin/series/{serie_name}/sets")
|
|
async def create_set(serie_name: str, data: SetCreate, db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
result = await db.execute(select(Serie).where(Serie.name == serie_name))
|
|
serie = result.scalar_one_or_none()
|
|
if not serie:
|
|
raise HTTPException(status_code=404, detail="Serie ikke fundet")
|
|
exists = await db.execute(select(Set).where(Set.code == data.code.upper()))
|
|
if exists.scalar_one_or_none():
|
|
raise HTTPException(status_code=400, detail="Set-kode findes allerede")
|
|
s = Set(serie_id=serie.id, code=data.code.upper(), name=data.name,
|
|
release_date=data.release_date, description=data.description)
|
|
db.add(s)
|
|
await db.commit()
|
|
return {"ok": True, "id": s.id, "code": s.code, "name": s.name}
|
|
|
|
@router.get("/api/admin/series/{serie_name}/sets")
|
|
async def list_sets(serie_name: str, db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
result = await db.execute(
|
|
select(Serie).where(Serie.name == serie_name).options(selectinload(Serie.sets))
|
|
)
|
|
serie = result.scalar_one_or_none()
|
|
if not serie:
|
|
raise HTTPException(status_code=404, detail="Serie ikke fundet")
|
|
return [{"id": s.id, "code": s.code, "name": s.name, "card_count": len(s.cards)}
|
|
for s in serie.sets]
|
|
|
|
@router.post("/api/admin/series/{serie_name}/sets/import")
|
|
async def import_sets_for_serie(serie_name: str, file: UploadFile = File(...),
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
"""CSV-import af sets til en specifik serie — 'serie'-kolonne ignoreres/overskrives."""
|
|
result = await db.execute(select(Serie).where(Serie.name == serie_name))
|
|
serie = result.scalar_one_or_none()
|
|
if not serie:
|
|
raise HTTPException(status_code=404, detail="Serie ikke fundet")
|
|
|
|
content = await file.read()
|
|
text = content.decode("utf-8-sig")
|
|
reader = csv.DictReader(io.StringIO(text))
|
|
created = 0
|
|
errors = []
|
|
for i, row in enumerate(reader, start=2):
|
|
code = row.get("code", "").strip().upper()
|
|
name = row.get("name", "").strip()
|
|
if not code or not name:
|
|
errors.append(f"Linje {i}: mangler kode eller navn")
|
|
continue
|
|
try:
|
|
exists = await db.execute(select(Set).where(Set.code == code))
|
|
if exists.scalar_one_or_none():
|
|
errors.append(f"Linje {i}: kode '{code}' findes allerede")
|
|
continue
|
|
s = Set(serie_id=serie.id, code=code, name=name,
|
|
release_date=row.get("release_date", "").strip() or None,
|
|
description=row.get("description", "").strip() or None)
|
|
db.add(s)
|
|
created += 1
|
|
except Exception as e:
|
|
errors.append(f"Linje {i}: {str(e)}")
|
|
await db.commit()
|
|
return {"ok": True, "created": created, "errors": errors}
|
|
|
|
@router.get("/api/admin/sets")
|
|
async def list_all_sets(db: AsyncSession = Depends(get_db), _: User = Depends(require_admin)):
|
|
result = await db.execute(
|
|
select(Set).options(selectinload(Set.serie), selectinload(Set.cards)).order_by(Set.code)
|
|
)
|
|
sets = result.scalars().all()
|
|
return [{"id": s.id, "code": s.code, "name": s.name, "serie": s.serie.name,
|
|
"card_count": len(s.cards), "release_date": s.release_date or ""}
|
|
for s in sets]
|
|
|
|
@router.delete("/api/admin/sets/{set_id}")
|
|
async def delete_set(set_id: int, db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
result = await db.execute(select(Set).where(Set.id == set_id))
|
|
s = result.scalar_one_or_none()
|
|
if not s:
|
|
raise HTTPException(status_code=404, detail="Set ikke fundet")
|
|
await db.delete(s)
|
|
await db.commit()
|
|
return {"ok": True}
|
|
|
|
@router.post("/api/admin/sets/import")
|
|
async def import_sets(file: UploadFile = File(...), db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
content = await file.read()
|
|
text = content.decode("utf-8-sig")
|
|
reader = csv.DictReader(io.StringIO(text))
|
|
created = 0
|
|
errors = []
|
|
for i, row in enumerate(reader, start=2):
|
|
code = row.get("code", "").strip().upper()
|
|
name = row.get("name", "").strip()
|
|
serie_name = row.get("serie", "").strip()
|
|
if not code or not name or not serie_name:
|
|
errors.append(f"Linje {i}: mangler kode, navn eller serie")
|
|
continue
|
|
try:
|
|
serie = await db.execute(select(Serie).where(Serie.name == serie_name))
|
|
serie = serie.scalar_one_or_none()
|
|
if not serie:
|
|
errors.append(f"Linje {i}: serie '{serie_name}' ikke fundet")
|
|
continue
|
|
exists = await db.execute(select(Set).where(Set.code == code))
|
|
if exists.scalar_one_or_none():
|
|
errors.append(f"Linje {i}: kode '{code}' findes allerede")
|
|
continue
|
|
s = Set(serie_id=serie.id, code=code, name=name,
|
|
release_date=row.get("release_date", "").strip() or None,
|
|
description=row.get("description", "").strip() or None)
|
|
db.add(s)
|
|
created += 1
|
|
except Exception as e:
|
|
errors.append(f"Linje {i}: {str(e)}")
|
|
await db.commit()
|
|
return {"ok": True, "created": created, "errors": errors}
|
|
|
|
|
|
# ─── API: Cards ─────────────────────────────────────────────────────
|
|
|
|
@router.post("/api/admin/sets/{set_code}/cards")
|
|
async def create_card(set_code: str, data: CardCreate, db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
result = await db.execute(
|
|
select(Set).where(Set.code == set_code.upper()).options(selectinload(Set.serie))
|
|
)
|
|
s = result.scalar_one_or_none()
|
|
if not s:
|
|
raise HTTPException(status_code=404, detail="Set ikke fundet")
|
|
|
|
card = Card(set_id=s.id, number=data.number, name=data.name)
|
|
if data.image:
|
|
card.source_url = data.image # Gem original URL til evt. re-sync
|
|
local_path = await download_image_from_url(data.image, s.serie.name, s.code, data.number)
|
|
card.image_path = local_path or data.image # Behold URL hvis download fejler
|
|
db.add(card)
|
|
await db.commit()
|
|
return {"ok": True, "id": card.id, "full_code": card.full_code, "name": card.name}
|
|
|
|
@router.get("/api/admin/sets/{set_code}/cards")
|
|
async def list_cards(set_code: str, db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
result = await db.execute(
|
|
select(Set).where(Set.code == set_code.upper()).options(
|
|
selectinload(Set.cards), selectinload(Set.serie)
|
|
)
|
|
)
|
|
s = result.scalar_one_or_none()
|
|
if not s:
|
|
raise HTTPException(status_code=404, detail="Set ikke fundet")
|
|
return [{"id": c.id, "number": c.number, "name": c.name, "full_code": c.full_code,
|
|
"image_url": c.image_url} for c in s.cards]
|
|
|
|
@router.delete("/api/admin/cards/{card_id}")
|
|
async def delete_card(card_id: int, db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
result = await db.execute(select(Card).where(Card.id == card_id))
|
|
card = result.scalar_one_or_none()
|
|
if not card:
|
|
raise HTTPException(status_code=404, detail="Kort ikke fundet")
|
|
# Slet billedfil hvis lokal
|
|
if card.image_path and not card.image_path.startswith(("http://", "https://")):
|
|
fp = Path("app/static") / card.image_path
|
|
if fp.exists():
|
|
fp.unlink()
|
|
await db.delete(card)
|
|
await db.commit()
|
|
return {"ok": True}
|
|
|
|
@router.post("/api/admin/cards/import")
|
|
async def import_cards(file: UploadFile = File(...), db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
content = await file.read()
|
|
text = content.decode("utf-8-sig")
|
|
reader = csv.DictReader(io.StringIO(text))
|
|
created = 0
|
|
errors = []
|
|
for i, row in enumerate(reader, start=2):
|
|
set_code = row.get("set_code", "").strip().upper()
|
|
number = row.get("number", "").strip()
|
|
name = row.get("name", "").strip()
|
|
if not set_code or not number or not name:
|
|
errors.append(f"Linje {i}: mangler set_code, number eller name")
|
|
continue
|
|
try:
|
|
result = await db.execute(
|
|
select(Set).where(Set.code == set_code).options(selectinload(Set.serie))
|
|
)
|
|
s = result.scalar_one_or_none()
|
|
if not s:
|
|
errors.append(f"Linje {i}: set '{set_code}' ikke fundet")
|
|
continue
|
|
raw_url = row.get("image", "").strip() or None
|
|
if raw_url:
|
|
local_path = await download_image_from_url(raw_url, s.serie.name, s.code, number)
|
|
image = local_path or raw_url # Behold URL hvis download fejler
|
|
else:
|
|
image = None
|
|
card = Card(set_id=s.id, number=number, name=name, image_path=image, source_url=raw_url)
|
|
db.add(card)
|
|
created += 1
|
|
except Exception as e:
|
|
errors.append(f"Linje {i}: {str(e)}")
|
|
await db.commit()
|
|
return {"ok": True, "created": created, "errors": errors}
|
|
|
|
@router.post("/api/admin/cards/{card_id}/upload")
|
|
async def upload_card_image(card_id: int, file: UploadFile = File(...),
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
result = await db.execute(
|
|
select(Card).where(Card.id == card_id).options(
|
|
selectinload(Card.set).selectinload(Set.serie)
|
|
)
|
|
)
|
|
card = result.scalar_one_or_none()
|
|
if not card:
|
|
raise HTTPException(status_code=404, detail="Kort ikke fundet")
|
|
|
|
# Bestem filtype
|
|
ext = os.path.splitext(file.filename or "image.png")[1].lower()
|
|
if ext not in (".jpg", ".jpeg", ".png", ".gif", ".webp"):
|
|
raise HTTPException(status_code=400, detail="Kun .jpg, .png, .gif, .webp tilladt")
|
|
|
|
serie_name = card.set.serie.name.replace(" ", "_").replace("/", "_")
|
|
set_code = card.set.code
|
|
number = card.number
|
|
folder = UPLOAD_DIR / serie_name / set_code
|
|
folder.mkdir(parents=True, exist_ok=True)
|
|
|
|
filename = f"{number}{ext}"
|
|
filepath = folder / filename
|
|
content = await file.read()
|
|
filepath.write_bytes(content)
|
|
|
|
# Gem relativ sti
|
|
rel_path = f"images/{serie_name}/{set_code}/{filename}"
|
|
card.image_path = rel_path
|
|
await db.commit()
|
|
|
|
return {"ok": True, "image_url": f"/static/{rel_path}", "path": rel_path}
|
|
|
|
|
|
# ─── Synkronisér billeder ──────────────────────────────────────────
|
|
|
|
@router.post("/api/admin/sets/{set_code}/sync-images")
|
|
async def sync_set_images(set_code: str, db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
"""Download alle eksterne billeder for et bestemt sæt til lokal lagring."""
|
|
result = await db.execute(
|
|
select(Set).where(Set.code == set_code.upper()).options(
|
|
selectinload(Set.cards), selectinload(Set.serie)
|
|
)
|
|
)
|
|
s = result.scalar_one_or_none()
|
|
if not s:
|
|
raise HTTPException(status_code=404, detail="Set ikke fundet")
|
|
|
|
downloaded = 0
|
|
skipped = 0
|
|
errors = []
|
|
for card in s.cards:
|
|
if not card.image_path or not card.image_path.startswith(("http://", "https://")):
|
|
skipped += 1
|
|
continue
|
|
try:
|
|
if not card.source_url:
|
|
card.source_url = card.image_path # Gem URL hvis ikke allerede gemt
|
|
rel_path = await download_image_from_url(card.image_path, s.serie.name, s.code, card.number)
|
|
if rel_path:
|
|
card.image_path = rel_path
|
|
downloaded += 1
|
|
else:
|
|
errors.append(f"{card.full_code}: download fejlede")
|
|
except Exception as e:
|
|
errors.append(f"{card.full_code}: {str(e)}")
|
|
|
|
await db.commit()
|
|
return {"ok": True, "set": s.code, "downloaded": downloaded, "skipped": skipped, "errors": errors}
|
|
|
|
|
|
@router.post("/api/admin/sync-all-images")
|
|
async def sync_all_images(db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
"""Download ALLE eksterne billeder på tværs af alle sæt."""
|
|
result = await db.execute(
|
|
select(Card).options(selectinload(Card.set).selectinload(Set.serie))
|
|
)
|
|
cards = result.scalars().all()
|
|
|
|
downloaded = 0
|
|
skipped = 0
|
|
errors = []
|
|
for card in cards:
|
|
if not card.image_path or not card.image_path.startswith(("http://", "https://")):
|
|
skipped += 1
|
|
continue
|
|
try:
|
|
serie_name = card.set.serie.name
|
|
set_code = card.set.code
|
|
if not card.source_url:
|
|
card.source_url = card.image_path # Gem URL hvis ikke allerede gemt
|
|
rel_path = await download_image_from_url(card.image_path, serie_name, set_code, card.number)
|
|
if rel_path:
|
|
card.image_path = rel_path
|
|
downloaded += 1
|
|
else:
|
|
errors.append(f"{card.full_code}: download fejlede")
|
|
except Exception as e:
|
|
errors.append(f"{card.full_code}: {str(e)}")
|
|
|
|
await db.commit()
|
|
return {"ok": True, "downloaded": downloaded, "skipped": skipped, "errors": errors}
|
|
|
|
|
|
@router.post("/api/admin/verify-images")
|
|
async def verify_images(force: bool = False, db: AsyncSession = Depends(get_db),
|
|
_: User = Depends(require_admin)):
|
|
"""Tjek at alle billedfiler findes på disken. Re-download fra source_url hvis mangler."""
|
|
result = await db.execute(
|
|
select(Card).where(Card.source_url.isnot(None)).options(
|
|
selectinload(Card.set).selectinload(Set.serie)
|
|
)
|
|
)
|
|
cards = result.scalars().all()
|
|
|
|
restored = 0
|
|
skipped = 0
|
|
errors = []
|
|
for card in cards:
|
|
# Bestem om filen findes lokalt
|
|
is_local = card.image_path and not card.image_path.startswith(("http://", "https://"))
|
|
file_exists = False
|
|
if is_local:
|
|
fp = Path("app/static") / card.image_path
|
|
file_exists = fp.exists()
|
|
|
|
if not force and is_local and file_exists:
|
|
skipped += 1
|
|
continue
|
|
|
|
# Mangler eller force — prøv at downloade fra source_url
|
|
try:
|
|
serie_name = card.set.serie.name
|
|
set_code = card.set.code
|
|
rel_path = await download_image_from_url(card.source_url, serie_name, set_code, card.number)
|
|
if rel_path:
|
|
card.image_path = rel_path
|
|
restored += 1
|
|
else:
|
|
errors.append(f"{card.full_code}: download fra source_url fejlede")
|
|
except Exception as e:
|
|
errors.append(f"{card.full_code}: {str(e)}")
|
|
|
|
await db.commit()
|
|
return {"ok": True, "restored": restored, "skipped": skipped, "errors": errors}
|
|
|
|
|
|
# ─── CSV Templates ──────────────────────────────────────────────────
|
|
|
|
@router.get("/api/admin/templates/series.csv", response_class=PlainTextResponse)
|
|
async def series_csv_template(_: User = Depends(require_admin)):
|
|
return "name,description\nSword & Shield,The Sword & Shield series\n"
|
|
|
|
@router.get("/api/admin/templates/sets.csv", response_class=PlainTextResponse)
|
|
async def sets_csv_template(_: User = Depends(require_admin)):
|
|
return "serie,code,name,release_date,description\nSword & Shield,EVS,Evolving Skies,2021-08-27,\n"
|
|
|
|
@router.get("/api/admin/templates/cards.csv", response_class=PlainTextResponse)
|
|
async def cards_csv_template(_: User = Depends(require_admin)):
|
|
return "set_code,number,name,image\nEVS,050,Raichu,\n"
|