Spaces:
Runtime error
Runtime error
| import json | |
| from datetime import datetime, timezone | |
| from fastapi import APIRouter, HTTPException, Request, Query | |
| from pydantic import BaseModel | |
| from typing import Optional | |
| from database import fetch_one, fetch_all, execute | |
| from auth import get_current_user | |
| from config import TMDB_IMAGE_BASE, DEV_MODE | |
| from ratelimit import limiter | |
| router = APIRouter() | |
| async def _resolve_user(request: Request) -> dict: | |
| user_data = await get_current_user(request) | |
| user = await fetch_one("SELECT * FROM users WHERE user_id = ?", [str(user_data["id"])]) | |
| if not user: | |
| raise HTTPException(status_code=404, detail="User not found") | |
| return user | |
| class ProfileUpdate(BaseModel): | |
| first_name: Optional[str] = None | |
| last_name: Optional[str] = None | |
| username: Optional[str] = None | |
| photo_url: Optional[str] = None | |
| class RatingRequest(BaseModel): | |
| tmdb_id: int | |
| media_type: str | |
| rating: int | |
| review: Optional[str] = None | |
| class AssetActivate(BaseModel): | |
| asset_item_id: str | |
| product_id: str | |
| class ListAdd(BaseModel): | |
| tmdb_id: int | |
| media_type: str | |
| title: str | |
| poster_url: Optional[str] = None | |
| class ListRemove(BaseModel): | |
| tmdb_id: int | |
| media_type: str | |
| async def get_my_profile(request: Request): | |
| user = await _resolve_user(request) | |
| watch_count = await fetch_one("SELECT COUNT(*) as cnt FROM watch_history WHERE user_id = ?", [str(user["id"])]) | |
| series_completed = await fetch_one( | |
| "SELECT COUNT(DISTINCT tmdb_id) as cnt FROM watch_history WHERE user_id = ? AND media_type='tv' AND completed=1", | |
| [str(user["id"])], | |
| ) | |
| friends_count = await fetch_one( | |
| "SELECT COUNT(*) as cnt FROM friendships WHERE (user_id = ? OR friend_id = ?) AND status='accepted'", | |
| [str(user["user_id"]), str(user["user_id"])], | |
| ) | |
| return { | |
| "ok": True, | |
| "data": { | |
| "id": user["id"], | |
| "user_id": user["user_id"], | |
| "username": user.get("username", ""), | |
| "first_name": user.get("first_name", ""), | |
| "last_name": user.get("last_name", ""), | |
| "photo_url": user.get("photo_url", ""), | |
| "language_code": user.get("language_code", "en"), | |
| "is_premium": bool(user.get("is_premium")), | |
| "stars_balance": user.get("stars_balance", 0), | |
| "kernels_balance": user.get("kernels_balance", 0), | |
| "total_watch_time": user.get("total_watch_time", 0), | |
| "last_active_at": user.get("last_active_at", ""), | |
| "watch_count": watch_count["cnt"] if watch_count else 0, | |
| "series_completed": series_completed["cnt"] if series_completed else 0, | |
| "friends_count": friends_count["cnt"] if friends_count else 0, | |
| }, | |
| } | |
| async def update_profile(body: ProfileUpdate, request: Request): | |
| user = await _resolve_user(request) | |
| updates = {} | |
| if body.first_name is not None: | |
| updates["first_name"] = body.first_name | |
| if body.last_name is not None: | |
| updates["last_name"] = body.last_name | |
| if body.username is not None: | |
| updates["username"] = body.username | |
| if body.photo_url is not None: | |
| updates["photo_url"] = body.photo_url | |
| if updates: | |
| set_clause = ", ".join(f"{k}=?" for k in updates) | |
| vals = [str(v) for v in updates.values()] + [str(user["user_id"])] | |
| await execute(f"UPDATE users SET {set_clause} WHERE user_id=?", vals) | |
| return {"ok": True, "data": {"updated": list(updates.keys())}} | |
| # Ratings | |
| async def create_rating(body: RatingRequest, request: Request): | |
| user = await _resolve_user(request) | |
| if body.rating < 1 or body.rating > 10: | |
| raise HTTPException(status_code=400, detail="Rating must be 1-10") | |
| await execute( | |
| """INSERT INTO ratings (user_id, tmdb_id, media_type, rating, review, updated_at) | |
| VALUES (?, ?, ?, ?, ?, datetime('now')) | |
| ON CONFLICT(user_id, tmdb_id, media_type) | |
| DO UPDATE SET rating=?, review=?, updated_at=datetime('now')""", | |
| [ | |
| str(user["id"]), str(body.tmdb_id), body.media_type, | |
| str(body.rating), body.review or "", | |
| str(body.rating), body.review or "", | |
| ], | |
| ) | |
| return {"ok": True, "data": {"saved": True}} | |
| async def get_my_ratings(request: Request): | |
| user = await _resolve_user(request) | |
| rows = await fetch_all( | |
| "SELECT * FROM ratings WHERE user_id = ? ORDER BY updated_at DESC", [str(user["id"])] | |
| ) | |
| return {"ok": True, "data": rows} | |
| # Assets | |
| async def get_my_assets(request: Request): | |
| user = await _resolve_user(request) | |
| rows = await fetch_all( | |
| """SELECT ua.*, ai.name_en, ai.image_url, ai.rarity, ai.type as asset_type_name | |
| FROM user_assets ua | |
| LEFT JOIN asset_items ai ON ua.asset_id = ai.id | |
| WHERE ua.user_id = ? ORDER BY ua.acquired_at DESC""", | |
| [str(user["id"])], | |
| ) | |
| return {"ok": True, "data": rows} | |
| async def activate_asset(body: AssetActivate, request: Request): | |
| user = await _resolve_user(request) | |
| await execute( | |
| "UPDATE user_assets SET is_active=0 WHERE user_id=?", | |
| [str(user["id"])], | |
| ) | |
| await execute( | |
| "UPDATE user_assets SET is_active=1 WHERE user_id=? AND asset_item_id=? AND product_id=?", | |
| [str(user["id"]), body.asset_item_id, body.product_id], | |
| ) | |
| return {"ok": True, "data": {"activated": True}} | |
| # Wallet | |
| async def get_wallet(request: Request): | |
| user = await _resolve_user(request) | |
| return { | |
| "ok": True, | |
| "data": { | |
| "stars": user.get("stars_balance", 0), | |
| "kernels": user.get("kernels_balance", 0), | |
| "points": user.get("points", 0), | |
| }, | |
| } | |
| async def get_kernels(request: Request): | |
| user = await _resolve_user(request) | |
| return { | |
| "ok": True, | |
| "data": { | |
| "balance": user.get("kernels_balance", 0), | |
| "total_earned": user.get("total_earned_kernels", 0), | |
| "total_spent": user.get("total_spent_kernels", 0), | |
| }, | |
| } | |
| # Daily Streak | |
| async def get_daily_streak(request: Request): | |
| user = await _resolve_user(request) | |
| streak = await fetch_one("SELECT * FROM daily_streaks WHERE user_id = ?", [str(user["id"])]) | |
| if not streak: | |
| return {"ok": True, "data": {"streak": 0, "can_claim": True, "days": []}} | |
| last_claim = streak.get("last_claim_date", "") | |
| today = datetime.now(timezone.utc).strftime("%Y-%m-%d") | |
| can_claim = last_claim != today | |
| return { | |
| "ok": True, | |
| "data": { | |
| "streak": streak.get("streak_count", 0), | |
| "last_claim": last_claim or "", | |
| "can_claim": can_claim or DEV_MODE, | |
| "today_claimed": not can_claim, | |
| "reward_today": 10, | |
| "next_reward": 10, | |
| }, | |
| } | |
| async def claim_daily_streak(request: Request): | |
| user = await _resolve_user(request) | |
| today = datetime.now(timezone.utc).strftime("%Y-%m-%d") | |
| streak = await fetch_one("SELECT * FROM daily_streaks WHERE user_id = ?", [str(user["id"])]) | |
| if streak: | |
| last_claim = streak.get("last_claim_date", "") | |
| if last_claim == today and not DEV_MODE: | |
| raise HTTPException(status_code=400, detail="Already claimed today") | |
| yesterday = datetime.now(timezone.utc).timestamp() - 86400 | |
| yesterday_str = datetime.fromtimestamp(yesterday, tz=timezone.utc).strftime("%Y-%m-%d") | |
| if last_claim == yesterday_str or not last_claim: | |
| new_count = (streak.get("streak_count", 0) or 0) + 1 | |
| else: | |
| new_count = 1 | |
| await execute( | |
| "UPDATE daily_streaks SET streak_count=?, last_claim_date=? WHERE user_id=?", | |
| [str(new_count), today, str(user["id"])], | |
| ) | |
| else: | |
| new_count = 1 | |
| await execute( | |
| "INSERT INTO daily_streaks (user_id, streak_count, last_claim_date) VALUES (?, 1, ?)", | |
| [str(user["id"]), today], | |
| ) | |
| await execute("UPDATE users SET stars_balance = stars_balance + 10 WHERE id = ?", [str(user["id"])]) | |
| return {"ok": True, "data": {"claimed": True, "streak": new_count, "reward": 10}} | |
| # Earnings | |
| async def get_earnings(request: Request): | |
| user = await _resolve_user(request) | |
| return {"ok": True, "data": {"kernels": user.get("kernels_balance", 0), "history": []}} | |
| # Watch List | |
| async def get_my_list(request: Request): | |
| user = await _resolve_user(request) | |
| rows = await fetch_all( | |
| "SELECT * FROM user_list WHERE user_id = ? ORDER BY added_at DESC", [str(user["id"])] | |
| ) | |
| return {"ok": True, "data": rows} | |
| async def add_to_list(body: ListAdd, request: Request): | |
| user = await _resolve_user(request) | |
| await execute( | |
| """INSERT OR IGNORE INTO user_list (user_id, tmdb_id, media_type, title, poster_url) | |
| VALUES (?, ?, ?, ?, ?)""", | |
| [str(user["id"]), str(body.tmdb_id), body.media_type, body.title, body.poster_url or ""], | |
| ) | |
| return {"ok": True, "data": {"added": True}} | |
| async def remove_from_list(body: ListRemove, request: Request): | |
| user = await _resolve_user(request) | |
| await execute( | |
| "DELETE FROM user_list WHERE user_id=? AND tmdb_id=? AND media_type=?", | |
| [str(user["id"]), str(body.tmdb_id), body.media_type], | |
| ) | |
| return {"ok": True, "data": {"removed": True}} | |
| # Watch History | |
| async def get_watch_history(request: Request, page: int = Query(1, ge=1)): | |
| user = await _resolve_user(request) | |
| offset = (page - 1) * 20 | |
| rows = await fetch_all( | |
| f"""SELECT wh.*, a.title, a.poster_path, a.quality, a.year | |
| FROM watch_history wh | |
| LEFT JOIN (SELECT DISTINCT media_id, title, poster_path, quality, year FROM archives) a | |
| ON wh.tmdb_id = a.media_id | |
| WHERE wh.user_id = ? | |
| ORDER BY wh.last_watched_at DESC LIMIT 20 OFFSET {offset}""", | |
| [str(user["id"])], | |
| ) | |
| for r in rows: | |
| pp = r.get("poster_path", "") | |
| if pp and not pp.startswith("http"): | |
| r["poster_url"] = f"{TMDB_IMAGE_BASE}/w500{pp}" if pp.startswith("/") else pp | |
| else: | |
| r["poster_url"] = pp or "" | |
| return {"ok": True, "data": rows} | |
| # Purchases | |
| async def get_purchases(request: Request): | |
| user = await _resolve_user(request) | |
| rows = await fetch_all( | |
| """SELECT p.*, pr.name_en, pr.name_ar, pr.image_url, pr.category | |
| FROM purchases p | |
| LEFT JOIN products pr ON p.product_id = pr.id | |
| WHERE p.user_id = ? ORDER BY p.purchased_at DESC""", | |
| [str(user["id"])], | |
| ) | |
| return {"ok": True, "data": rows} | |
| # User profile (other users) | |
| async def get_user_profile(user_id: int): | |
| user = await fetch_one("SELECT * FROM users WHERE user_id = ?", [str(user_id)]) | |
| if not user: | |
| raise HTTPException(status_code=404, detail="User not found") | |
| watch_stats = await fetch_one( | |
| "SELECT COUNT(*) as watch_count, COALESCE(SUM(duration_seconds), 0) as total_watch_time FROM watch_history WHERE user_id=?", | |
| [str(user["id"])], | |
| ) | |
| total_watch_time = watch_stats["total_watch_time"] if watch_stats else 0 | |
| watch_count = watch_stats["watch_count"] if watch_stats else 0 | |
| return { | |
| "ok": True, | |
| "data": { | |
| "user_id": user["user_id"], | |
| "username": user.get("username", ""), | |
| "first_name": user.get("first_name", ""), | |
| "last_name": user.get("last_name", ""), | |
| "photo_url": user.get("photo_url", ""), | |
| "is_premium": bool(user.get("is_premium")), | |
| "total_watch_time": total_watch_time, | |
| "points": user.get("points", 0), | |
| "stats": { | |
| "total_watch_time": total_watch_time, | |
| "watch_count": watch_count, | |
| "points": user.get("points", 0), | |
| "kernels_balance": user.get("kernels_balance", 0), | |
| }, | |
| }, | |
| } | |