from pathlib import Path
from urllib.parse import unquote_plus
from fastapi import FastAPI, Request, HTTPException, Form
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
import config
import db
BASE_DIR = Path(__file__).parent
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
app = FastAPI(title=config.APP_TITLE)
@app.on_event("startup")
def on_startup():
db.init_db()
def _render_search_page(request: Request, q: str = "", sort: str = "score"):
results = []
q = unquote_plus(q or "").strip()
sort = (sort or "score").strip().lower()
search_info = {"query": q, "expanded_terms": [], "total_count": 0, "shown_count": 0, "per_term_counts": [], "has_more": False, "limit": 50, "total_matches": 0, "returned_count": 0, "sort": sort}
if q:
results, search_info = db.search_videos(q, limit=50, sort=sort)
search_info["sort"] = sort
return templates.TemplateResponse(request, "index.html", {
"app_title": config.APP_TITLE,
"app_tagline": config.APP_TAGLINE,
"source_count": db.count_sources(),
"item_count": db.count_items(),
"last_import": db.get_last_import_stats(),
"query": q,
"results": results,
"search_info": search_info,
"sort": search_info.get("sort", sort),
})
@app.get("/", response_class=HTMLResponse)
def index(request: Request, q: str = "", sort: str = "score"):
return _render_search_page(request, q, sort)
@app.get("/search", response_class=HTMLResponse)
def search(request: Request, q: str = "", sort: str = "score"):
return _render_search_page(request, q, sort)
@app.get("/videos/{source_id}", response_class=HTMLResponse)
def video_detail_page(request: Request, source_id: int):
source = db.get_source(source_id)
if source is None:
raise HTTPException(status_code=404, detail="Video nicht gefunden")
return templates.TemplateResponse(request, "video_detail.html", {
"app_title": config.APP_TITLE,
"source": source,
})
@app.get("/health")
def health():
return {"status": "ok", "app": "content-extraction"}
@app.get("/sources", response_class=HTMLResponse)
def sources_page(
request: Request,
q: str = "",
channel: str = "",
language: str = "",
transcript_status: str = "",
is_high_value: str = "",
has_artifact_path: str = "",
has_item: str = "",
):
filters = {
"q": q, "channel": channel, "language": language,
"transcript_status": transcript_status, "is_high_value": is_high_value,
"has_artifact_path": has_artifact_path, "has_item": has_item,
}
sources, total = db.list_sources(filters)
dropdowns = db.get_distinct_source_values()
return templates.TemplateResponse(request, "sources.html", {
"app_title": config.APP_TITLE,
"sources": sources,
"total": total,
"max_results": db.MAX_RESULTS,
"filters": filters,
"channels": dropdowns["channels"],
"languages": dropdowns["languages"],
"transcript_statuses": dropdowns["transcript_statuses"],
})
@app.post("/admin/import-sources")
def admin_import_sources():
"""Manuell angestossener, read-only Import aus YouTube Research.
Keine automatische Ausfuehrung -- nur ueber diesen Endpunkt/Button."""
stats = db.import_sources_from_youtube_research()
return {"import": stats}
@app.get("/admin/import-status")
def admin_import_status():
return {"last_import": db.get_last_import_stats()}
@app.post("/sources/{source_id}/create-item")
def create_item(source_id: int):
item, created = db.create_item_from_source(source_id)
if item is None:
raise HTTPException(status_code=404, detail="Quelle nicht gefunden")
return {"item": item, "created": created}
@app.get("/items", response_class=HTMLResponse)
def items_page(
request: Request,
q: str = "",
content_type: str = "",
status: str = "",
imitation_score: str = "",
effort: str = "",
feasibility: str = "",
expected_value: str = "",
):
filters = {
"q": q, "content_type": content_type, "status": status,
"imitation_score": imitation_score, "effort": effort,
"feasibility": feasibility, "expected_value": expected_value,
}
items, total = db.list_items(filters)
return templates.TemplateResponse(request, "items.html", {
"app_title": config.APP_TITLE,
"items": items,
"total": total,
"max_results": db.MAX_RESULTS,
"filters": filters,
"content_types": db.CONTENT_TYPES,
"status_values": db.STATUS_VALUES,
})
@app.get("/items/{item_id}", response_class=HTMLResponse)
def item_detail_page(request: Request, item_id: int):
item = db.get_item_detail(item_id)
if item is None:
raise HTTPException(status_code=404, detail="Kandidat nicht gefunden")
return templates.TemplateResponse(request, "item_detail.html", {
"app_title": config.APP_TITLE,
"item": item,
"content_types": db.CONTENT_TYPES,
"status_values": db.STATUS_VALUES,
"level_values": db.LEVEL_VALUES,
})
@app.post("/items/{item_id}/rating")
def save_rating(
item_id: int,
content_type: str = Form(None),
status: str = Form(None),
nachahmbarkeit: int = Form(None),
aufwand: str = Form(None),
umsetzbarkeit: str = Form(None),
erwarteter_nutzen: str = Form(None),
rationale: str = Form(None),
):
try:
item = db.save_item_rating(
item_id,
content_type=content_type or None,
status=status or None,
nachahmbarkeit=nachahmbarkeit,
aufwand=aufwand or None,
umsetzbarkeit=umsetzbarkeit or None,
erwarteter_nutzen=erwarteter_nutzen or None,
rationale=rationale or None,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
if item is None:
raise HTTPException(status_code=404, detail="Kandidat nicht gefunden")
return {"item": item}
@app.get("/matrix", response_class=HTMLResponse)
def matrix_page(request: Request):
"""Read-only Auswertungsansicht ueber vorhandene ce_items/ce_ratings.
Erzeugt keine neuen Daten, keine automatische Bewertung."""
return templates.TemplateResponse(request, "matrix.html", {
"app_title": config.APP_TITLE,
"rows": db.list_content_matrix(),
})