Goon — self-hosted aggregator for adult-content scene metadata. Indexes scenes from TPDB, StashDB, and 30+ public adult tube sites. Cross-source deduplication via perceptual hash + Levenshtein distance. FastAPI backend + APScheduler worker + React Native (Expo) mobile client. FOSS, ad-free, donation-funded. See README for details.
155 lines
5.1 KiB
Python
155 lines
5.1 KiB
Python
"""Bug reports — mobile FAB → POST /bug-reports → admin lista przez admin_html.
|
|
|
|
POST nie wymaga obecnego scene_id (user może raportować z FavoritesScreen,
|
|
SearchScreen itp.). Screenshot opcjonalny — niektóre ekrany nie warto kapturować.
|
|
|
|
Limit body 1.5MB (FastAPI default jest hojny, ale dla rozsądku ograniczamy).
|
|
Screenshot to PNG/JPEG z react-native-view-shot, base64 — typowe rozmiary:
|
|
- mały ekran scene-list: ~200-400KB
|
|
- duży scene-detail z thumbnail: ~600KB-1MB
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy import desc, func, select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.auth import require_api_key
|
|
from app.db import get_session
|
|
from app.models.bug_report import BugReport
|
|
from app.models.movie import Movie
|
|
from app.models.scene import Scene
|
|
|
|
router = APIRouter(tags=["bug-reports"], dependencies=[Depends(require_api_key)])
|
|
|
|
|
|
_MAX_SCREENSHOT_BYTES = 1_500_000 # raw base64 chars; ~1.1MB binary po dekodowaniu
|
|
|
|
|
|
class BugReportCreate(BaseModel):
|
|
message: str = Field(min_length=1, max_length=5000)
|
|
screen_name: str | None = Field(default=None, max_length=64)
|
|
app_version: str | None = Field(default=None, max_length=32)
|
|
scene_id: uuid.UUID | None = None
|
|
screenshot_b64: str | None = Field(default=None, max_length=_MAX_SCREENSHOT_BYTES)
|
|
|
|
|
|
class BugReportOut(BaseModel):
|
|
id: uuid.UUID
|
|
created_at: datetime
|
|
screen_name: str | None
|
|
app_version: str | None
|
|
scene_id: uuid.UUID | None
|
|
movie_id: uuid.UUID | None
|
|
message: str
|
|
has_screenshot: bool
|
|
resolved: bool
|
|
|
|
|
|
class BugReportListOut(BaseModel):
|
|
items: list[BugReportOut]
|
|
total: int
|
|
|
|
|
|
@router.post("/bug-reports", status_code=status.HTTP_201_CREATED)
|
|
def create_bug_report(
|
|
payload: BugReportCreate,
|
|
session: Annotated[Session, Depends(get_session)],
|
|
) -> dict[str, str]:
|
|
# Smart-route entity_id: mobile Player używa `sceneId` param zarówno dla
|
|
# scen jak i movies (legacy progress tracking hack). Bez tego INSERT FK
|
|
# violation crashował 500 (zgłoszone 2026-05-10). Sprawdź obie tabele.
|
|
scene_id: uuid.UUID | None = None
|
|
movie_id: uuid.UUID | None = None
|
|
if payload.scene_id is not None:
|
|
if session.get(Scene, payload.scene_id) is not None:
|
|
scene_id = payload.scene_id
|
|
elif session.get(Movie, payload.scene_id) is not None:
|
|
movie_id = payload.scene_id
|
|
# else: ID nie istnieje już nigdzie (deleted) — drop oba na null
|
|
|
|
br = BugReport(
|
|
id=uuid.uuid4(),
|
|
message=payload.message.strip(),
|
|
screen_name=payload.screen_name,
|
|
app_version=payload.app_version,
|
|
scene_id=scene_id,
|
|
movie_id=movie_id,
|
|
screenshot_b64=payload.screenshot_b64,
|
|
)
|
|
session.add(br)
|
|
session.commit()
|
|
return {"id": str(br.id)}
|
|
|
|
|
|
@router.get("/bug-reports", response_model=BugReportListOut)
|
|
def list_bug_reports(
|
|
session: Annotated[Session, Depends(get_session)],
|
|
limit: int = 50,
|
|
offset: int = 0,
|
|
include_resolved: bool = False,
|
|
) -> BugReportListOut:
|
|
q = select(BugReport).order_by(desc(BugReport.created_at))
|
|
cnt_q = select(func.count(BugReport.id))
|
|
if not include_resolved:
|
|
q = q.where(BugReport.resolved.is_(False))
|
|
cnt_q = cnt_q.where(BugReport.resolved.is_(False))
|
|
rows = session.scalars(q.limit(limit).offset(offset)).all()
|
|
total = session.scalar(cnt_q) or 0
|
|
items = [
|
|
BugReportOut(
|
|
id=r.id,
|
|
created_at=r.created_at,
|
|
screen_name=r.screen_name,
|
|
app_version=r.app_version,
|
|
scene_id=r.scene_id,
|
|
movie_id=r.movie_id,
|
|
message=r.message,
|
|
has_screenshot=bool(r.screenshot_b64),
|
|
resolved=r.resolved,
|
|
)
|
|
for r in rows
|
|
]
|
|
return BugReportListOut(items=items, total=total)
|
|
|
|
|
|
@router.get("/bug-reports/{bug_id}/screenshot")
|
|
def get_bug_report_screenshot(
|
|
bug_id: uuid.UUID,
|
|
session: Annotated[Session, Depends(get_session)],
|
|
) -> dict[str, str | None]:
|
|
"""Zwraca base64-encoded screenshot (jeśli jest) — admin UI go renderuje."""
|
|
br = session.get(BugReport, bug_id)
|
|
if br is None:
|
|
raise HTTPException(status_code=404, detail="not found")
|
|
return {"screenshot_b64": br.screenshot_b64}
|
|
|
|
|
|
@router.post("/bug-reports/{bug_id}/resolve")
|
|
def resolve_bug_report(
|
|
bug_id: uuid.UUID,
|
|
session: Annotated[Session, Depends(get_session)],
|
|
) -> dict[str, str]:
|
|
br = session.get(BugReport, bug_id)
|
|
if br is None:
|
|
raise HTTPException(status_code=404, detail="not found")
|
|
br.resolved = True
|
|
session.commit()
|
|
return {"status": "resolved"}
|
|
|
|
|
|
@router.delete("/bug-reports/{bug_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def delete_bug_report(
|
|
bug_id: uuid.UUID,
|
|
session: Annotated[Session, Depends(get_session)],
|
|
) -> None:
|
|
br = session.get(BugReport, bug_id)
|
|
if br is None:
|
|
raise HTTPException(status_code=404, detail="not found")
|
|
session.delete(br)
|
|
session.commit()
|