summaryrefslogtreecommitdiff
path: root/routers/stats.py
diff options
context:
space:
mode:
Diffstat (limited to 'routers/stats.py')
-rw-r--r--routers/stats.py126
1 files changed, 126 insertions, 0 deletions
diff --git a/routers/stats.py b/routers/stats.py
new file mode 100644
index 0000000..78e9621
--- /dev/null
+++ b/routers/stats.py
@@ -0,0 +1,126 @@
+from collections import Counter
+from datetime import date, timedelta
+
+from fastapi import APIRouter, Depends, Request
+from fastapi.templating import Jinja2Templates
+from sqlalchemy.orm import Session
+
+from database import get_db
+from models import Film
+from services.countries import (
+ ISO_NUMERIC_TO_COUNTRY_NAME,
+ country_name_to_iso_numeric,
+ split_country_names,
+)
+from services.film_people import split_credit_names
+
+router = APIRouter(tags=["stats"])
+templates = Jinja2Templates(directory="templates")
+
+
+def _build_stats_payload(films: list[Film]) -> dict:
+ countries = Counter()
+ country_codes = Counter()
+ directors = Counter()
+ star_counts = Counter({0: 0, 1: 0, 2: 0, 3: 0})
+ months = Counter()
+ days = Counter()
+ watched_with = Counter()
+
+ for film in films:
+ country_names = split_country_names(film.country)
+ countries.update(country_names)
+ for country in country_names:
+ iso_numeric = country_name_to_iso_numeric(country)
+ if iso_numeric is not None:
+ country_codes[iso_numeric] += 1
+
+ directors.update(split_credit_names(film.director))
+
+ stars = film.stars if film.stars in {0, 1, 2, 3} else 0
+ star_counts[stars] += 1
+
+ if film.date_watched:
+ months[film.date_watched.strftime("%Y-%m")] += 1
+ days[film.date_watched.isoformat()] += 1
+
+ companions = split_credit_names(film.watched_with)
+ if companions:
+ watched_with.update(companions)
+ else:
+ watched_with["solo"] += 1
+
+ total_watched = len(films)
+ rewatched = sum(1 for film in films if film.rewatch or film.rewatch_count > 0)
+
+ today = date.today()
+ start_day = today - timedelta(days=364)
+ trailing_days = []
+ cursor = start_day
+ while cursor <= today:
+ trailing_days.append({"date": cursor.isoformat(), "count": days[cursor.isoformat()]})
+ cursor += timedelta(days=1)
+
+ return {
+ "scope": {
+ "shelf": "diary",
+ "requires_date_watched": True,
+ },
+ "total_watched": total_watched,
+ "films_per_country": [
+ {"country": country, "count": count}
+ for country, count in sorted(countries.items(), key=lambda item: (-item[1], item[0]))
+ ],
+ "films_per_country_codes": [
+ {"code": code, "count": count}
+ for code, count in sorted(country_codes.items(), key=lambda item: (-item[1], item[0]))
+ ],
+ "country_labels_by_code": {
+ str(code): ISO_NUMERIC_TO_COUNTRY_NAME.get(code, str(code)) for code in country_codes
+ },
+ "most_watched_directors": [
+ {"director": director, "count": count}
+ for director, count in sorted(directors.items(), key=lambda item: (-item[1], item[0]))
+ ],
+ "star_distribution": [{"stars": stars, "count": star_counts[stars]} for stars in (0, 1, 2, 3)],
+ "films_per_month": [
+ {"month": month, "count": count}
+ for month, count in sorted(months.items())
+ ],
+ "films_per_day": [
+ {"date": watched_date, "count": count}
+ for watched_date, count in sorted(days.items())
+ ],
+ "films_per_day_365": trailing_days,
+ "rewatch_rate": {
+ "rewatched": rewatched,
+ "total_watched": total_watched,
+ "rate": round(rewatched / total_watched, 4) if total_watched else 0,
+ },
+ "watched_with_breakdown": [
+ {"watched_with": watched_with_value, "count": count}
+ for watched_with_value, count in sorted(watched_with.items(), key=lambda item: (-item[1], item[0]))
+ ],
+ }
+
+
+def _diary_films(db: Session) -> list[Film]:
+ return (
+ db.query(Film)
+ .filter(Film.shelf == "diary", Film.date_watched.is_not(None))
+ .all()
+ )
+
+
+@router.get("/stats")
+def stats_page(request: Request):
+ return templates.TemplateResponse(
+ request=request,
+ name="stats.html",
+ context={"request": request, "active_page": "stats"},
+ )
+
+
+@router.get("/stats/data")
+def stats_data(db: Session = Depends(get_db)):
+ return _build_stats_payload(_diary_films(db))