Add Google OAuth configuration and enhance API functionality
- Updated .env.example with WEB_GOOGLE_CLIENT_ID and WEB_GOOGLE_CLIENT_SECRET for Google OAuth. - Modified README.md to reflect changes in docker-compose for unified web and API deployment. - Enhanced auth.py to support NextAuth JWT session cookies. - Improved main.py with lifespan management and additional API endpoints. - Added mod_overview endpoint in mod.py for MOD panel statistics. - Updated docker-compose.yml for local API and web service configurations.
This commit is contained in:
+28
-1
@@ -70,7 +70,34 @@ async def _get_user_from_session_cookie(db: AsyncSession, request: Request) -> d
|
||||
{"token": token},
|
||||
)
|
||||
row = result.mappings().first()
|
||||
return dict(row) if row else None
|
||||
if row:
|
||||
return dict(row)
|
||||
|
||||
# Support NextAuth/Auth.js JWT session cookies when frontend runs in JWT mode.
|
||||
secret = _jwt_secret()
|
||||
if not secret:
|
||||
return None
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, secret, algorithms=["HS256"])
|
||||
except JWTError:
|
||||
return None
|
||||
|
||||
subject = payload.get("sub") or payload.get("id")
|
||||
if not isinstance(subject, str) or not subject:
|
||||
return None
|
||||
|
||||
role = payload.get("role")
|
||||
if isinstance(role, str) and role:
|
||||
return {
|
||||
"id": subject,
|
||||
"email": payload.get("email"),
|
||||
"name": payload.get("name"),
|
||||
"image": payload.get("picture") or payload.get("image"),
|
||||
"role": role,
|
||||
}
|
||||
|
||||
return await _get_user_by_id(db, subject)
|
||||
|
||||
|
||||
async def resolve_current_user(db: AsyncSession, request: Request) -> dict[str, Any] | None:
|
||||
|
||||
+431
-20
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import random
|
||||
import secrets
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
@@ -21,6 +22,25 @@ from app.config import settings
|
||||
from app.database import get_db_session, mongo_client, mongo_db
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
yield
|
||||
mongo_client.close()
|
||||
|
||||
|
||||
app = FastAPI(title=settings.app_name, lifespan=lifespan)
|
||||
|
||||
app.include_router(mod.router)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origin_list,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
def _new_id(prefix: str = "") -> str:
|
||||
token = uuid.uuid4().hex
|
||||
return f"{prefix}{token}" if prefix else token
|
||||
@@ -38,6 +58,376 @@ def _iso(value: Any) -> str | None:
|
||||
return str(value)
|
||||
|
||||
|
||||
def _shuffle_rows[T](rows: list[T]) -> list[T]:
|
||||
copied = list(rows)
|
||||
random.shuffle(copied)
|
||||
return copied
|
||||
|
||||
|
||||
def _collapse_series_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
picked_series: set[str] = set()
|
||||
output: list[dict[str, Any]] = []
|
||||
|
||||
for row in rows:
|
||||
series_id = row.get("seriesId")
|
||||
if not series_id:
|
||||
output.append(row)
|
||||
continue
|
||||
|
||||
if series_id in picked_series:
|
||||
continue
|
||||
|
||||
picked_series.add(series_id)
|
||||
output.append(row)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def _fill_unique_rows(rows: list[dict[str, Any]], fallback: list[dict[str, Any]], target: int) -> list[dict[str, Any]]:
|
||||
picked: set[str] = set()
|
||||
output: list[dict[str, Any]] = []
|
||||
|
||||
for row in [*rows, *fallback]:
|
||||
row_id = str(row.get("id") or "")
|
||||
if not row_id or row_id in picked:
|
||||
continue
|
||||
picked.add(row_id)
|
||||
output.append(row)
|
||||
if len(output) >= target:
|
||||
return output
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def _home_novel_from_row(row: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"id": row["id"],
|
||||
"slug": row["slug"],
|
||||
"title": row["title"],
|
||||
"authorName": row["authorName"],
|
||||
"coverColor": row.get("coverColor"),
|
||||
"coverUrl": row.get("coverUrl"),
|
||||
"rating": float(row.get("rating") or 0),
|
||||
"views": int(row.get("views") or 0),
|
||||
"totalChapters": int(row.get("totalChapters") or 0),
|
||||
"status": row.get("status") or "Đang ra",
|
||||
"description": row.get("description") or "",
|
||||
"bookmarkCount": int(row.get("bookmarkCount") or 0),
|
||||
"seriesId": row.get("seriesId"),
|
||||
"updatedAt": _iso(row.get("updatedAt")),
|
||||
}
|
||||
|
||||
|
||||
async def _fetch_home_ranking_rows(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
since: dt.date | None = None,
|
||||
take: int = 300,
|
||||
) -> list[dict[str, Any]]:
|
||||
where_sql = 'WHERE v.day >= :since' if since else ''
|
||||
params: dict[str, Any] = {"take": take}
|
||||
if since:
|
||||
params["since"] = since
|
||||
|
||||
rows = (
|
||||
await db.execute(
|
||||
text(
|
||||
'SELECT n.id, n.slug, n.title, n."authorName", n."coverColor", n."coverUrl", '
|
||||
'n.rating, n.views, n."totalChapters", n.status, n.description, n."bookmarkCount", '
|
||||
'n."seriesId", n."updatedAt", COALESCE(SUM(v.views), 0)::int AS aggregated_views '
|
||||
'FROM "NovelViewDaily" v '
|
||||
'JOIN "Novel" n ON n.id = v."novelId" '
|
||||
f'{where_sql} '
|
||||
'GROUP BY n.id '
|
||||
'ORDER BY aggregated_views DESC, n."updatedAt" DESC '
|
||||
'LIMIT :take'
|
||||
),
|
||||
params,
|
||||
)
|
||||
).mappings().all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": row["id"],
|
||||
"seriesId": row["seriesId"],
|
||||
"aggregatedViews": int(row.get("aggregated_views") or 0),
|
||||
"novel": _home_novel_from_row(dict(row)),
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
async def _fetch_home_popular_fallback(db: AsyncSession, *, take: int = 400) -> list[dict[str, Any]]:
|
||||
rows = (
|
||||
await db.execute(
|
||||
text(
|
||||
'SELECT id, slug, title, "authorName", "coverColor", "coverUrl", rating, views, '
|
||||
'"totalChapters", status, description, "bookmarkCount", "seriesId", "updatedAt" '
|
||||
'FROM "Novel" '
|
||||
'ORDER BY views DESC, "updatedAt" DESC '
|
||||
'LIMIT :take'
|
||||
),
|
||||
{"take": take},
|
||||
)
|
||||
).mappings().all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": row["id"],
|
||||
"seriesId": row["seriesId"],
|
||||
"aggregatedViews": int(row.get("views") or 0),
|
||||
"novel": _home_novel_from_row(dict(row)),
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
async def _fetch_home_random_pool(db: AsyncSession, *, take: int = 420) -> list[dict[str, Any]]:
|
||||
rows = (
|
||||
await db.execute(
|
||||
text(
|
||||
'SELECT id, slug, title, "authorName", "coverColor", "coverUrl", rating, views, '
|
||||
'"totalChapters", status, description, "bookmarkCount", "seriesId", "updatedAt" '
|
||||
'FROM "Novel" '
|
||||
'ORDER BY "updatedAt" DESC '
|
||||
'LIMIT :take'
|
||||
),
|
||||
{"take": take},
|
||||
)
|
||||
).mappings().all()
|
||||
|
||||
return [_home_novel_from_row(dict(row)) for row in rows]
|
||||
|
||||
|
||||
async def _fetch_home_manual_recommendations(db: AsyncSession) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
editor_docs = await mongo_db["editorrecommendations"].find({}).sort("createdAt", -1).limit(2000).to_list(2000)
|
||||
user_docs = await mongo_db["userrecommendations"].find({}).sort("createdAt", -1).limit(5000).to_list(5000)
|
||||
|
||||
novel_ids = list(
|
||||
{
|
||||
str(item.get("novelId"))
|
||||
for item in [*editor_docs, *user_docs]
|
||||
if item.get("novelId")
|
||||
}
|
||||
)
|
||||
editor_ids = list({str(item.get("editorId")) for item in editor_docs if item.get("editorId")})
|
||||
|
||||
if not novel_ids:
|
||||
return [], []
|
||||
|
||||
novel_rows = (
|
||||
await db.execute(
|
||||
text(
|
||||
'SELECT id, slug, title, "authorName", "coverUrl", rating '
|
||||
'FROM "Novel" '
|
||||
'WHERE id = ANY(:novel_ids)'
|
||||
),
|
||||
{"novel_ids": novel_ids},
|
||||
)
|
||||
).mappings().all()
|
||||
editor_rows = []
|
||||
if editor_ids:
|
||||
editor_rows = (
|
||||
await db.execute(
|
||||
text('SELECT id, name FROM "User" WHERE id = ANY(:editor_ids)'),
|
||||
{"editor_ids": editor_ids},
|
||||
)
|
||||
).mappings().all()
|
||||
|
||||
novel_map = {
|
||||
row["id"]: {
|
||||
"id": row["id"],
|
||||
"slug": row["slug"],
|
||||
"title": row["title"],
|
||||
"authorName": row["authorName"],
|
||||
"coverUrl": row.get("coverUrl"),
|
||||
"rating": float(row.get("rating") or 0),
|
||||
}
|
||||
for row in novel_rows
|
||||
}
|
||||
editor_map = {row["id"]: row.get("name") or "Biên tập viên" for row in editor_rows}
|
||||
|
||||
recommend_count_map: dict[str, int] = {}
|
||||
for doc in [*editor_docs, *user_docs]:
|
||||
novel_id = str(doc.get("novelId") or "")
|
||||
if not novel_id:
|
||||
continue
|
||||
recommend_count_map[novel_id] = recommend_count_map.get(novel_id, 0) + 1
|
||||
|
||||
top_items = [
|
||||
{"novel": novel_map[novel_id], "recommendCount": count}
|
||||
for novel_id, count in recommend_count_map.items()
|
||||
if novel_id in novel_map
|
||||
]
|
||||
top_items.sort(
|
||||
key=lambda item: (
|
||||
-item["recommendCount"],
|
||||
-float(item["novel"].get("rating") or 0),
|
||||
)
|
||||
)
|
||||
|
||||
editor_items: list[dict[str, Any]] = []
|
||||
for doc in editor_docs:
|
||||
novel_id = str(doc.get("novelId") or "")
|
||||
if novel_id not in novel_map:
|
||||
continue
|
||||
editor_items.append(
|
||||
{
|
||||
"novel": novel_map[novel_id],
|
||||
"editorName": editor_map.get(str(doc.get("editorId") or ""), "Biên tập viên"),
|
||||
"recommendCount": recommend_count_map.get(novel_id, 0),
|
||||
"createdAt": _iso(doc.get("createdAt")),
|
||||
}
|
||||
)
|
||||
|
||||
editor_items.sort(
|
||||
key=lambda item: (
|
||||
-item["recommendCount"],
|
||||
item["createdAt"] or "",
|
||||
),
|
||||
reverse=False,
|
||||
)
|
||||
editor_items.reverse()
|
||||
|
||||
for item in editor_items:
|
||||
item.pop("createdAt", None)
|
||||
|
||||
return top_items, editor_items
|
||||
|
||||
|
||||
async def _fetch_home_recent_comments(db: AsyncSession, *, take: int = 10) -> list[dict[str, Any]]:
|
||||
rows = (
|
||||
await db.execute(
|
||||
text(
|
||||
'SELECT c.id, c.content, c."createdAt", u.name AS username, n.slug AS novel_slug, n.title AS novel_title '
|
||||
'FROM "Comment" c '
|
||||
'JOIN "User" u ON u.id = c."userId" '
|
||||
'JOIN "Novel" n ON n.id = c."novelId" '
|
||||
'ORDER BY c."createdAt" DESC '
|
||||
'LIMIT :take'
|
||||
),
|
||||
{"take": take},
|
||||
)
|
||||
).mappings().all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": row["id"],
|
||||
"content": row["content"],
|
||||
"createdAt": _iso(row["createdAt"]),
|
||||
"user": {"name": row.get("username")},
|
||||
"novel": {"slug": row["novel_slug"], "title": row["novel_title"]},
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
async def _fetch_home_latest_novels(db: AsyncSession, *, take: int = 5) -> list[dict[str, Any]]:
|
||||
recent_chapters = await mongo_db["chapters"].find(
|
||||
{},
|
||||
{"novelId": 1, "number": 1, "title": 1, "createdAt": 1},
|
||||
).sort("_id", -1).limit(400).to_list(400)
|
||||
|
||||
latest_novel_ids: list[str] = []
|
||||
latest_seen_ids: set[str] = set()
|
||||
latest_chapter_map: dict[str, dict[str, Any]] = {}
|
||||
|
||||
for row in recent_chapters:
|
||||
novel_id = str(row.get("novelId") or "").strip()
|
||||
if not novel_id or novel_id in latest_seen_ids:
|
||||
continue
|
||||
latest_seen_ids.add(novel_id)
|
||||
latest_novel_ids.append(novel_id)
|
||||
latest_chapter_map[novel_id] = {
|
||||
"number": row.get("number"),
|
||||
"title": row.get("title"),
|
||||
"createdAt": _iso(row.get("createdAt")),
|
||||
}
|
||||
if len(latest_novel_ids) >= 500:
|
||||
break
|
||||
|
||||
if not latest_novel_ids:
|
||||
return []
|
||||
|
||||
novel_rows = (
|
||||
await db.execute(
|
||||
text(
|
||||
'SELECT id, slug, title, "authorName", "coverColor", "coverUrl", rating, views, '
|
||||
'"totalChapters", status, description, "bookmarkCount", "seriesId", "updatedAt" '
|
||||
'FROM "Novel" '
|
||||
'WHERE id = ANY(:novel_ids)'
|
||||
),
|
||||
{"novel_ids": latest_novel_ids},
|
||||
)
|
||||
).mappings().all()
|
||||
|
||||
novel_map = {row["id"]: _home_novel_from_row(dict(row)) for row in novel_rows}
|
||||
ordered = [novel_map[novel_id] for novel_id in latest_novel_ids if novel_id in novel_map]
|
||||
collapsed = _collapse_series_rows(ordered)[:take]
|
||||
|
||||
for novel in collapsed:
|
||||
novel["latestChapter"] = latest_chapter_map.get(novel["id"])
|
||||
|
||||
return collapsed
|
||||
|
||||
|
||||
def _to_hot_slide(row: dict[str, Any], source: str) -> dict[str, Any]:
|
||||
novel = row["novel"]
|
||||
return {
|
||||
"id": novel["id"],
|
||||
"slug": novel["slug"],
|
||||
"title": novel["title"],
|
||||
"authorName": novel["authorName"],
|
||||
"description": novel["description"],
|
||||
"coverUrl": novel["coverUrl"],
|
||||
"totalChapters": novel["totalChapters"],
|
||||
"rating": novel["rating"],
|
||||
"views": row["aggregatedViews"],
|
||||
"status": novel["status"],
|
||||
"hotSource": source,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/home")
|
||||
async def get_home_data(db: AsyncSession = Depends(get_db_session)):
|
||||
today = dt.datetime.now(dt.timezone.utc).date()
|
||||
week_start = today - dt.timedelta(days=7)
|
||||
month_start = today - dt.timedelta(days=30)
|
||||
|
||||
weekly_raw = await _fetch_home_ranking_rows(db, since=week_start, take=600)
|
||||
monthly_raw = await _fetch_home_ranking_rows(db, since=month_start, take=600)
|
||||
all_time_raw = await _fetch_home_ranking_rows(db, take=800)
|
||||
popular_fallback = await _fetch_home_popular_fallback(db, take=400)
|
||||
random_pool = await _fetch_home_random_pool(db, take=420)
|
||||
top_recommendations, editor_recommendations = await _fetch_home_manual_recommendations(db)
|
||||
recent_comments = await _fetch_home_recent_comments(db, take=10)
|
||||
latest_novels = await _fetch_home_latest_novels(db, take=5)
|
||||
|
||||
weekly_ranking = _fill_unique_rows(_collapse_series_rows(weekly_raw), _collapse_series_rows(popular_fallback), 5)
|
||||
monthly_ranking = _fill_unique_rows(_collapse_series_rows(monthly_raw), _collapse_series_rows(popular_fallback), 5)
|
||||
all_time_ranking = _fill_unique_rows(_collapse_series_rows(all_time_raw), _collapse_series_rows(popular_fallback), 5)
|
||||
|
||||
hot_primary = [_to_hot_slide(item, "week") for item in weekly_ranking[:5]] + [_to_hot_slide(item, "month") for item in monthly_ranking[:5]]
|
||||
hot_fallback = [_to_hot_slide(item, "all") for item in all_time_ranking[:8]]
|
||||
hot_slides = _fill_unique_rows(hot_primary, hot_fallback, 10)
|
||||
|
||||
used_hot_ids = {item["id"] for item in hot_slides}
|
||||
random_candidates = [item for item in _collapse_series_rows(_shuffle_rows(random_pool)) if item["id"] not in used_hot_ids]
|
||||
random_novels = _fill_unique_rows(random_candidates, _shuffle_rows(random_pool), 12)
|
||||
|
||||
return {
|
||||
"hotSlides": hot_slides,
|
||||
"randomNovels": random_novels,
|
||||
"recommendedByCountItems": top_recommendations,
|
||||
"editorRecommendedItems": editor_recommendations,
|
||||
"weeklyRanking": weekly_ranking,
|
||||
"monthlyRanking": monthly_ranking,
|
||||
"allTimeRanking": all_time_ranking,
|
||||
"latestNovels": latest_novels,
|
||||
"recentComments": recent_comments,
|
||||
}
|
||||
|
||||
|
||||
async def _load_bookmark_with_novel(db: AsyncSession, user_id: str, novel_id: str) -> dict[str, Any] | None:
|
||||
result = await db.execute(
|
||||
text(
|
||||
@@ -158,25 +548,6 @@ async def _update_reading_progress(
|
||||
return {"status": "updated", "bookmark": bookmark}
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
yield
|
||||
mongo_client.close()
|
||||
|
||||
|
||||
app = FastAPI(title=settings.app_name, lifespan=lifespan)
|
||||
|
||||
app.include_router(mod.router)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origin_list,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
async def healthcheck(db: AsyncSession = Depends(get_db_session)):
|
||||
db_ok = False
|
||||
@@ -460,7 +831,7 @@ async def get_novel_chapters(
|
||||
skip = (page - 1) * limit
|
||||
chapters_cursor = (
|
||||
mongo_db["chapters"]
|
||||
.find({"novelId": novel_id}, {"title": 1, "number": 1, "createdAt": 1, "volumeNumber": 1, "volumeTitle": 1, "volumeChapterNumber": 1})
|
||||
.find({"novelId": novel_id}, {"title": 1, "number": 1, "createdAt": 1, "views": 1, "volumeNumber": 1, "volumeTitle": 1, "volumeChapterNumber": 1})
|
||||
.sort("number", 1)
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
@@ -474,6 +845,7 @@ async def get_novel_chapters(
|
||||
"id": str(item.get("_id")),
|
||||
"number": item.get("number"),
|
||||
"title": item.get("title"),
|
||||
"views": item.get("views", 0),
|
||||
"volumeNumber": item.get("volumeNumber"),
|
||||
"volumeTitle": item.get("volumeTitle"),
|
||||
"volumeChapterNumber": item.get("volumeChapterNumber"),
|
||||
@@ -487,6 +859,41 @@ async def get_novel_chapters(
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/truyen/{novel_id}/chapters/by-number/{chapter_number}")
|
||||
async def get_chapter_by_number(novel_id: str, chapter_number: int):
|
||||
chapter = await mongo_db["chapters"].find_one({"novelId": novel_id, "number": chapter_number})
|
||||
if not chapter:
|
||||
raise HTTPException(status_code=404, detail="Chapter not found")
|
||||
|
||||
prev_chapter = await mongo_db["chapters"].find_one(
|
||||
{"novelId": novel_id, "number": chapter_number - 1},
|
||||
{"number": 1},
|
||||
)
|
||||
next_chapter = await mongo_db["chapters"].find_one(
|
||||
{"novelId": novel_id, "number": chapter_number + 1},
|
||||
{"number": 1},
|
||||
)
|
||||
max_chapter = await mongo_db["chapters"].count_documents({"novelId": novel_id})
|
||||
|
||||
await mongo_db["chapters"].update_one({"_id": chapter["_id"]}, {"$inc": {"views": 1}})
|
||||
|
||||
return {
|
||||
"id": str(chapter.get("_id")),
|
||||
"novelId": chapter.get("novelId"),
|
||||
"number": chapter.get("number"),
|
||||
"title": chapter.get("title"),
|
||||
"content": chapter.get("content"),
|
||||
"views": int(chapter.get("views") or 0) + 1,
|
||||
"volumeNumber": chapter.get("volumeNumber"),
|
||||
"volumeTitle": chapter.get("volumeTitle"),
|
||||
"volumeChapterNumber": chapter.get("volumeChapterNumber"),
|
||||
"createdAt": _iso(chapter.get("createdAt")),
|
||||
"prevChapterNumber": prev_chapter.get("number") if prev_chapter else None,
|
||||
"nextChapterNumber": next_chapter.get("number") if next_chapter else None,
|
||||
"maxChapter": max_chapter,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/chapters/{chapter_id}")
|
||||
async def get_chapter_detail(chapter_id: str):
|
||||
try:
|
||||
@@ -592,6 +999,7 @@ async def rate_novel(novel_id: str, payload: RatePayload, db: AsyncSession = Dep
|
||||
async def list_comments(
|
||||
novel_id: str,
|
||||
chapterId: str | None = None,
|
||||
scope: str | None = None,
|
||||
page: int = Query(default=1, ge=1),
|
||||
limit: int = Query(default=20, ge=1, le=50),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
@@ -600,6 +1008,9 @@ async def list_comments(
|
||||
if chapterId:
|
||||
where_sql = 'c."novelId" = :novel_id AND c."chapterId" = :chapter_id'
|
||||
params = {"novel_id": novel_id, "chapter_id": chapterId, "skip": skip, "limit": limit}
|
||||
elif scope == "chapter":
|
||||
where_sql = 'c."novelId" = :novel_id AND c."chapterId" IS NOT NULL'
|
||||
params = {"novel_id": novel_id, "skip": skip, "limit": limit}
|
||||
else:
|
||||
where_sql = 'c."novelId" = :novel_id AND c."chapterId" IS NULL'
|
||||
params = {"novel_id": novel_id, "skip": skip, "limit": limit}
|
||||
|
||||
+62
-1
@@ -150,7 +150,10 @@ async def dev_make_mod(
|
||||
|
||||
|
||||
@router.get("/mod/the-loai")
|
||||
async def mod_list_genres(db: AsyncSession = Depends(get_db_session)):
|
||||
async def mod_list_genres(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
user: dict = Depends(require_mod_user),
|
||||
):
|
||||
result = await db.execute(text('SELECT id, name, slug, description, icon FROM "Genre" ORDER BY name ASC'))
|
||||
return [dict(r) for r in result.mappings()]
|
||||
|
||||
@@ -376,6 +379,64 @@ async def _resolve_series_id(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/mod/overview")
|
||||
async def mod_overview(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
user: dict = Depends(require_mod_user),
|
||||
):
|
||||
if _is_admin(user):
|
||||
novel_count = (await db.execute(text('SELECT COUNT(*)::int FROM "Novel"'))).scalar() or 0
|
||||
total_views = (await db.execute(text('SELECT COALESCE(SUM(views), 0)::bigint FROM "Novel"'))).scalar() or 0
|
||||
comment_count = (await db.execute(text('SELECT COUNT(*)::int FROM "Comment"'))).scalar() or 0
|
||||
series_count = (await db.execute(text('SELECT COUNT(*)::int FROM "Series"'))).scalar() or 0
|
||||
else:
|
||||
novel_count = (
|
||||
await db.execute(
|
||||
text('SELECT COUNT(*)::int FROM "Novel" WHERE "uploaderId" = :uid OR "uploaderId" IS NULL'),
|
||||
{"uid": user["id"]},
|
||||
)
|
||||
).scalar() or 0
|
||||
total_views = (
|
||||
await db.execute(
|
||||
text('SELECT COALESCE(SUM(views), 0)::bigint FROM "Novel" WHERE "uploaderId" = :uid OR "uploaderId" IS NULL'),
|
||||
{"uid": user["id"]},
|
||||
)
|
||||
).scalar() or 0
|
||||
comment_count = (
|
||||
await db.execute(
|
||||
text(
|
||||
'SELECT COUNT(c.id)::int '
|
||||
'FROM "Comment" c '
|
||||
'JOIN "Novel" n ON n.id = c."novelId" '
|
||||
'WHERE n."uploaderId" = :uid OR n."uploaderId" IS NULL'
|
||||
),
|
||||
{"uid": user["id"]},
|
||||
)
|
||||
).scalar() or 0
|
||||
series_count = (
|
||||
await db.execute(
|
||||
text(
|
||||
'SELECT COUNT(s.id)::int '
|
||||
'FROM "Series" s '
|
||||
'WHERE EXISTS ('
|
||||
' SELECT 1 FROM "Novel" n '
|
||||
' WHERE n."seriesId" = s.id AND (n."uploaderId" = :uid OR n."uploaderId" IS NULL)'
|
||||
') OR NOT EXISTS ('
|
||||
' SELECT 1 FROM "Novel" n2 WHERE n2."seriesId" = s.id'
|
||||
')'
|
||||
),
|
||||
{"uid": user["id"]},
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
return {
|
||||
"novelCount": int(novel_count),
|
||||
"totalViews": int(total_views),
|
||||
"commentCount": int(comment_count),
|
||||
"seriesCount": int(series_count),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/mod/truyen")
|
||||
async def mod_list_novels(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
|
||||
Reference in New Issue
Block a user