Initial commit.

This commit is contained in:
g_it 2026-03-31 11:43:01 +02:00
commit 2fa935016e
Signed by untrusted user who does not match committer: g_it
GPG key ID: A2B0A7C06A054627
11 changed files with 1410 additions and 0 deletions

0
app/routes/__init__.py Normal file
View file

76
app/routes/discovery.py Normal file
View file

@ -0,0 +1,76 @@
import sqlite3
from fastapi import APIRouter, Depends, Query
from app.database import get_db
from app.models import CategoryCount, CreatorCount, GenreCount
router = APIRouter(tags=["Discovery"])
@router.get(
"/categories",
response_model=list[CategoryCount],
summary="List all categories",
description="Returns every media category, along with a count of the number of reviews in that category."
)
def list_categories(db: sqlite3.Connection = Depends(get_db)):
rows = db.execute(
"SELECT category, COUNT(*) as count FROM reviews GROUP BY category ORDER BY count DESC"
).fetchall()
return [dict(row) for row in rows]
@router.get(
"/genres",
response_model=list[GenreCount],
summary="List all genres",
description="Returns every genre with review counts. Optionally filter to genres within a specific category.",
)
def list_genres(
category: str | None = Query(None, description="Only show genres in this category.", examples=["Movies"]),
db: sqlite3.Connection = Depends(get_db),
):
conditions = []
params = []
if category:
conditions.append("category = ?")
params.append(category)
where = ""
if conditions:
where = "WHERE " + " AND ".join(conditions)
rows = db.execute(
f"SELECT genre, COUNT(*) as count FROM reviews {where} GROUP BY genre ORDER BY count DESC",
params,
).fetchall()
return [dict(row) for row in rows]
@router.get(
"/creators",
response_model=list[CreatorCount],
summary="List all creators",
description="Returns every writer, director, or creator; with the number of works by them that has been reviewed. "
"Optionally filter by category.",
)
def list_creators(
category: str | None = Query(None, description="Only show creators in this category.", examples=["Books"]),
db: sqlite3.Connection = Depends(get_db),
):
conditions = []
params = []
if category:
conditions.append("category = ?")
params.append(category)
where = ""
if conditions:
where = "WHERE " + " AND ".join(conditions)
rows = db.execute(
f"SELECT creator, COUNT(*) as count FROM reviews {where} GROUP BY creator ORDER BY count DESC",
params,
).fetchall()
return [dict(row) for row in rows]

234
app/routes/reviews.py Normal file
View file

@ -0,0 +1,234 @@
import sqlite3
from fastapi import APIRouter, Depends, HTTPException, Query
from app.database import get_db
from app.models import Review, ReviewListResponse
router = APIRouter(prefix="/reviews", tags=["Reviews"])
@router.get(
"",
response_model=ReviewListResponse,
summary="List reviews",
description="Retrieve a paginated list of reviews with optional filters. All filters are combined with AND logic, where only reviews matching every specified filter are returned.",
)
def list_reviews(
category: str | None = Query(
None, description="Filter by media category (exact match).", examples=["Movies"]
),
creator: str | None = Query(
None, description="Filter by creator name (exact match).", examples=["Christopher Nolan"]
),
genre: str | None = Query(
None, description="Filter by genre (exact match).", examples=["Science Fiction"]
),
min_rating: float | None = Query(
None, description="Minimum rating (inclusive).", ge=-3.0, le=3.0, examples=[2.0]
),
max_rating: float | None = Query(
None, description="Maximum rating (inclusive).", ge=-3.0, le=3.0, examples=[3.0]
),
date_from: str | None = Query(
None, description="Earliest review date (inclusive, YYYY-MM-DD).", examples=["2020-01-01"]
),
date_to: str | None = Query(
None, description="Latest review date (inclusive, YYYY-MM-DD).", examples=["2025-12-31"]
),
year_from: int | None = Query(
None, description="Earliest release year (inclusive).", examples=[1990]
),
year_to: int | None = Query(
None, description="Latest release year (inclusive).", examples=[2000]
),
limit: int = Query(
20, description="Number of results per page.", ge=1, le=100
),
offset: int = Query(
0, description="Number of results to skip.", ge=0
),
db: sqlite3.Connection = Depends(get_db),
):
conditions = []
params = []
if category:
conditions.append("category = ?")
params.append(category)
if creator:
conditions.append("creator = ?")
params.append(creator)
if genre:
conditions.append("genre = ?")
params.append(genre)
if min_rating is not None:
conditions.append("rating >= ?")
params.append(min_rating)
if max_rating is not None:
conditions.append("rating <= ?")
params.append(max_rating)
if date_from:
conditions.append("date >= ?")
params.append(date_from)
if date_to:
conditions.append("date <= ?")
params.append(date_to)
if year_from is not None:
conditions.append("year >= ?")
params.append(year_from)
if year_to is not None:
conditions.append("year <= ?")
params.append(year_to)
where = ""
if conditions:
where = "WHERE " + " AND ".join(conditions)
total = db.execute(
f"SELECT COUNT(*) FROM reviews {where}", params
).fetchone()[0]
rows = db.execute(
f"SELECT * FROM reviews {where} ORDER BY date DESC LIMIT ? OFFSET ?",
params + [limit, offset],
).fetchall()
return ReviewListResponse(
total=total,
limit=limit,
offset=offset,
results=[dict(row) for row in rows],
)
@router.get(
"/random",
response_model=Review,
summary="Get a random review",
description="Returns a single randomly selected review. Optionally filter by category to get a random review from a specific media type.",
)
def random_review(
category: str | None = Query(
None, description="Limit random selection to this category.", examples=["Books"]
),
db: sqlite3.Connection = Depends(get_db),
):
conditions = []
params = []
if category:
conditions.append("category = ?")
params.append(category)
where = ""
if conditions:
where = "WHERE " + " AND ".join(conditions)
row = db.execute(
f"SELECT * FROM reviews {where} ORDER BY RANDOM() LIMIT 1", params
).fetchone()
if not row:
raise HTTPException(status_code=404, detail="No reviews found.")
return dict(row)
@router.get(
"/top",
response_model=list[Review],
summary="Get top-rated reviews",
description="Returns the highest-rated reviews, sorted by rating descending.",
)
def top_reviews(
category: str | None = Query(None, description="Limit to this category.", examples=["Movies"]),
year_from: int | None = Query(None, description="Earliest release year (inclusive).", examples=[1990]),
year_to: int | None = Query(None, description="Latest release year (inclusive).", examples=[2000]),
limit: int = Query(10, description="Number of results to return.", ge=1, le=50),
db: sqlite3.Connection = Depends(get_db),
):
conditions = []
params = []
if category:
conditions.append("category = ?")
params.append(category)
if year_from is not None:
conditions.append("year >= ?")
params.append(year_from)
if year_to is not None:
conditions.append("year <= ?")
params.append(year_to)
where = ""
if conditions:
where = "WHERE " + " AND ".join(conditions)
rows = db.execute(
f"SELECT * FROM reviews {where} ORDER BY rating DESC LIMIT ?",
params + [limit],
).fetchall()
return [dict(row) for row in rows]
@router.get(
"/bottom",
response_model=list[Review],
summary="Get lowest-rated reviews",
description="Returns the lowest-rated reviews, sorted by rating ascending. "
"These are the harshest takes in the collection.",
)
def bottom_reviews(
category: str | None = Query(None, description="Limit to this category.", examples=["Movies"]),
year_from: int | None = Query(None, description="Earliest release year (inclusive).", examples=[1990]),
year_to: int | None = Query(None, description="Latest release year (inclusive).", examples=[2000]),
limit: int = Query(10, description="Number of results to return.", ge=1, le=50),
db: sqlite3.Connection = Depends(get_db),
):
conditions = []
params = []
if category:
conditions.append("category = ?")
params.append(category)
if year_from is not None:
conditions.append("year >= ?")
params.append(year_from)
if year_to is not None:
conditions.append("year <= ?")
params.append(year_to)
where = ""
if conditions:
where = "WHERE " + " AND ".join(conditions)
rows = db.execute(
f"SELECT * FROM reviews {where} ORDER BY rating ASC LIMIT ?",
params + [limit],
).fetchall()
return [dict(row) for row in rows]
@router.get(
"/{category}/{title}/{creator}",
response_model=Review,
summary="Get a specific review",
description="Retrieve a single review by its unique combination of "
"category, title, and creator.",
)
def get_review(
category: str,
title: str,
creator: str,
db: sqlite3.Connection = Depends(get_db),
):
row = db.execute(
"SELECT * FROM reviews WHERE category = ? AND title = ? AND creator = ?",
[category, title, creator],
).fetchone()
if not row:
raise HTTPException(
status_code=404,
detail=f"No review found for '{title}' by {creator} in {category}.",
)
return dict(row)

152
app/routes/stats.py Normal file
View file

@ -0,0 +1,152 @@
import sqlite3
from fastapi import APIRouter, Depends, Query
from app.database import get_db
from app.models import CreatorStats, DecadeStats, GenreStats, OverviewStats, YearActivity
router = APIRouter(prefix="/stats", tags=["Statistics"])
@router.get(
"",
response_model=OverviewStats,
summary="Overall statistics",
description="High-level numbers: total reviews, breakdown by category, average rating, and date range of the reviews collection.",
)
def overview(db: sqlite3.Connection = Depends(get_db)):
total = db.execute("SELECT COUNT(*) FROM reviews").fetchone()[0]
categories = db.execute(
"SELECT category, COUNT(*) as count FROM reviews GROUP BY category ORDER BY count DESC"
).fetchall()
avg = db.execute("SELECT ROUND(AVG(rating), 2) FROM reviews").fetchone()[0]
earliest = db.execute("SELECT MIN(date) FROM reviews").fetchone()[0]
latest = db.execute("SELECT MAX(date) FROM reviews").fetchone()[0]
return OverviewStats(
total_reviews=total,
categories=[dict(row) for row in categories],
average_rating=avg,
earliest_review=earliest,
latest_review=latest,
)
@router.get(
"/creators",
response_model=list[CreatorStats],
summary="Creator rankings",
description="Writers, directors and creators ranked by number of reviewed works, with their average rating.",
)
def creator_stats(
category: str | None = Query(None, description="Limit to this category.", examples=["Movies"]),
limit: int = Query(20, description="Number of creators to return.", ge=1, le=100),
db: sqlite3.Connection = Depends(get_db),
):
conditions = []
params = []
if category:
conditions.append("category = ?")
params.append(category)
where = ""
if conditions:
where = "WHERE " + " AND ".join(conditions)
rows = db.execute(
f"SELECT creator, COUNT(*) as review_count, "
f"ROUND(AVG(rating), 2) as average_rating "
f"FROM reviews {where} "
f"GROUP BY creator ORDER BY review_count DESC LIMIT ?",
params + [limit],
).fetchall()
return [dict(row) for row in rows]
@router.get(
"/genres",
response_model=list[GenreStats],
summary="Genre breakdown",
description="Genres with review counts and average ratings. Optionally filtered by media category.",
)
def genre_stats(
category: str | None = Query(None, description="Limit to this category.", examples=["Movies"]),
db: sqlite3.Connection = Depends(get_db),
):
conditions = []
params = []
if category:
conditions.append("category = ?")
params.append(category)
where = ""
if conditions:
where = "WHERE " + " AND ".join(conditions)
rows = db.execute(
f"SELECT genre, COUNT(*) as review_count, "
f"ROUND(AVG(rating), 2) as average_rating "
f"FROM reviews {where} "
f"GROUP BY genre ORDER BY review_count DESC",
params,
).fetchall()
return [dict(row) for row in rows]
@router.get(
"/timeline",
response_model=list[YearActivity],
summary="Review timeline",
description="Reviews per year, showing how my reviewing activity changed over time.",
)
def timeline(db: sqlite3.Connection = Depends(get_db)):
rows = db.execute(
"SELECT SUBSTR(date, 1, 4) as year, COUNT(*) as review_count, "
"ROUND(AVG(rating), 2) as average_rating "
"FROM reviews GROUP BY year ORDER BY year ASC"
).fetchall()
return [dict(row) for row in rows]
@router.get(
"/decades",
response_model=list[DecadeStats],
summary="Reviews by decade of release",
description="Reviews grouped by the decade the work was released. Shows which eras of media appear most in the reviews collection.",
)
def decade_stats(
category: str | None = Query(None, description="Limit to this category.", examples=["Movies"]),
db: sqlite3.Connection = Depends(get_db),
):
conditions = ["year IS NOT NULL"]
params = []
if category:
conditions.append("category = ?")
params.append(category)
where = "WHERE " + " AND ".join(conditions)
rows = db.execute(
f"SELECT (year / 10 * 10) as decade_num, COUNT(*) as review_count, "
f"ROUND(AVG(rating), 2) as average_rating "
f"FROM reviews {where} "
f"GROUP BY decade_num ORDER BY decade_num ASC",
params,
).fetchall()
return [
DecadeStats(
decade=f"{row['decade_num']}s",
review_count=row["review_count"],
average_rating=row["average_rating"],
)
for row in rows
]