diff --git a/app/favorites.py b/app/favorites.py index cfd80be..0c7d4df 100644 --- a/app/favorites.py +++ b/app/favorites.py @@ -27,11 +27,15 @@ class FavoritesManager: url: str, provider: str, metadata: Optional[Dict] = None, - poster_url: Optional[str] = None + poster_url: Optional[str] = None, + user_id: str = "default" ) -> Dict: """Add an anime to favorites""" with Session(engine) as session: - statement = select(FavoriteTable).where(FavoriteTable.anime_id == anime_id) + statement = select(FavoriteTable).where( + FavoriteTable.anime_id == anime_id, + FavoriteTable.user_id == user_id + ) existing = session.exec(statement).first() if existing: @@ -53,17 +57,21 @@ class FavoritesManager: url=url, provider=provider, anime_metadata=metadata or {}, - poster_url=poster_url + poster_url=poster_url, + user_id=user_id ) session.add(fav) session.commit() session.refresh(fav) return self._to_dict(fav) - async def remove_favorite(self, anime_id: str) -> bool: + async def remove_favorite(self, anime_id: str, user_id: str = "default") -> bool: """Remove an anime from favorites""" with Session(engine) as session: - statement = select(FavoriteTable).where(FavoriteTable.anime_id == anime_id) + statement = select(FavoriteTable).where( + FavoriteTable.anime_id == anime_id, + FavoriteTable.user_id == user_id + ) existing = session.exec(statement).first() if existing: session.delete(existing) @@ -71,10 +79,13 @@ class FavoritesManager: return True return False - async def get_favorite(self, anime_id: str) -> Optional[Dict]: + async def get_favorite(self, anime_id: str, user_id: str = "default") -> Optional[Dict]: """Get a specific favorite by ID""" with Session(engine) as session: - statement = select(FavoriteTable).where(FavoriteTable.anime_id == anime_id) + statement = select(FavoriteTable).where( + FavoriteTable.anime_id == anime_id, + FavoriteTable.user_id == user_id + ) existing = session.exec(statement).first() if existing: return self._to_dict(existing) @@ -82,6 +93,7 @@ class FavoritesManager: async def list_favorites( self, + user_id: str = "default", sort_by: str = "created_at", order: str = "desc", filter_provider: Optional[str] = None, @@ -89,11 +101,11 @@ class FavoritesManager: ) -> List[Dict]: """List all favorites with optional sorting and filtering""" with Session(engine) as session: - statement = select(FavoriteTable) - + statement = select(FavoriteTable).where(FavoriteTable.user_id == user_id) + if filter_provider: statement = statement.where(FavoriteTable.provider == filter_provider) - + # SQLite JSON filtering for genres is complex, handle it in Python results = session.exec(statement).all() favorites = [self._to_dict(fav) for fav in results] @@ -123,10 +135,13 @@ class FavoritesManager: return favorites - async def is_favorite(self, anime_id: str) -> bool: + async def is_favorite(self, anime_id: str, user_id: str = "default") -> bool: """Check if an anime is in favorites""" with Session(engine) as session: - statement = select(FavoriteTable).where(FavoriteTable.anime_id == anime_id) + statement = select(FavoriteTable).where( + FavoriteTable.anime_id == anime_id, + FavoriteTable.user_id == user_id + ) return session.exec(statement).first() is not None async def toggle_favorite( @@ -136,21 +151,22 @@ class FavoritesManager: url: str, provider: str, metadata: Optional[Dict] = None, - poster_url: Optional[str] = None + poster_url: Optional[str] = None, + user_id: str = "default" ) -> Dict: """Toggle an anime in favorites (add if not exists, remove if exists)""" - is_fav = await self.is_favorite(anime_id) + is_fav = await self.is_favorite(anime_id, user_id=user_id) if is_fav: - await self.remove_favorite(anime_id) + await self.remove_favorite(anime_id, user_id=user_id) return {"action": "removed", "anime_id": anime_id} else: - fav = await self.add_favorite(anime_id, title, url, provider, metadata, poster_url) + fav = await self.add_favorite(anime_id, title, url, provider, metadata, poster_url, user_id=user_id) return {"action": "added", "anime_id": anime_id, "favorite": fav} - async def get_stats(self) -> Dict: + async def get_stats(self, user_id: str = "default") -> Dict: """Get statistics about favorites""" - favorites = await self.list_favorites() + favorites = await self.list_favorites(user_id=user_id) total = len(favorites) # Count by provider diff --git a/app/routers/router_downloads.py b/app/routers/router_downloads.py index d0381e3..daa830e 100644 --- a/app/routers/router_downloads.py +++ b/app/routers/router_downloads.py @@ -2,13 +2,15 @@ Download management routes for Ohm Stream Downloader API. """ +from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse from app.download_manager import DownloadManager from app.models import DownloadRequest -from app.routers.router_auth import get_current_user_from_token +from app.models.auth import User +from app.routers.router_auth import get_current_user_from_token, get_optional_user router = APIRouter(prefix="/api/downloads", tags=["downloads"]) templates = Jinja2Templates(directory="templates") @@ -24,20 +26,28 @@ async def get_downloads( request: Request, html: bool = Query(False), download_manager: DownloadManager = Depends(get_download_manager), + current_user: Optional[User] = Depends(get_optional_user), ): """Get list of all download tasks. Returns HTML for HTMX requests.""" - tasks = download_manager.get_all_tasks() - - # Strictly check for HTMX or explicit HTML flag is_htmx = request.headers.get("HX-Request") == "true" or request.headers.get("HX-Request") - + + if current_user is None and (html or is_htmx): + return templates.TemplateResponse( + "components/login_prompt.html", {"request": request} + ) + + if current_user is None: + raise HTTPException(status_code=401, detail="Authentication required") + + tasks = download_manager.get_all_tasks() + if html or is_htmx: print(f"[DOWNLOADS] HTML Request. Found {len(tasks)} tasks.") return templates.TemplateResponse( "components/downloads_list.html", {"request": request, "tasks": tasks} ) - + print(f"[DOWNLOADS] API Request. Returning JSON.") return {"downloads": tasks} @@ -56,8 +66,12 @@ async def create_download( async def get_download_status( task_id: str, download_manager: DownloadManager = Depends(get_download_manager), + current_user: Optional[User] = Depends(get_optional_user), ): """Get status of a specific download task""" + if current_user is None: + raise HTTPException(status_code=401, detail="Authentication required") + task = download_manager.get_task(task_id) if not task: raise HTTPException(status_code=404, detail="Task not found") diff --git a/app/routers/router_favorites.py b/app/routers/router_favorites.py index f0ee7a3..37f2e3c 100644 --- a/app/routers/router_favorites.py +++ b/app/routers/router_favorites.py @@ -2,24 +2,42 @@ Favorites management routes for Ohm Stream Downloader API. """ -from fastapi import APIRouter, HTTPException -from fastapi.requests import Request +from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response +from fastapi.templating import Jinja2Templates from app.favorites import get_favorites_manager +from app.models.auth import User +from app.routers.router_auth import get_current_user_from_token, get_optional_user router = APIRouter(prefix="/api/favorites", tags=["favorites"]) +templates = Jinja2Templates(directory="templates") @router.get("") async def list_favorites( + request: Request, sort_by: str = "created_at", order: str = "desc", - filter_provider: str = None, - filter_genre: str = None, + filter_provider: Optional[str] = None, + filter_genre: Optional[str] = None, + html: bool = Query(False), + current_user: Optional[User] = Depends(get_optional_user), ): """List all favorite anime with optional sorting and filtering""" + is_htmx = request.headers.get("HX-Request") + + if current_user is None and (html or is_htmx): + return templates.TemplateResponse( + "components/login_prompt.html", {"request": request} + ) + + if current_user is None: + raise HTTPException(status_code=401, detail="Authentication required") + fav_manager = get_favorites_manager() favorites = await fav_manager.list_favorites( + user_id=current_user.id, sort_by=sort_by, order=order, filter_provider=filter_provider, @@ -38,7 +56,11 @@ async def list_favorites( @router.post("") -async def add_favorite(request: Request): +async def add_favorite( + request: Request, + response: Response, + current_user: User = Depends(get_current_user_from_token), +): """Add an anime to favorites""" data = await request.json() @@ -51,6 +73,7 @@ async def add_favorite(request: Request): fav_manager = get_favorites_manager() favorite = await fav_manager.add_favorite( + user_id=current_user.id, anime_id=data["anime_id"], title=data["title"], url=data["url"], @@ -59,34 +82,45 @@ async def add_favorite(request: Request): poster_url=data.get("poster_url"), ) + response.headers["HX-Trigger"] = '{"show-toast": {"message": "' + favorite['title'] + ' ajouté aux favoris", "type": "success"}}' return {"status": "added", "favorite": favorite} @router.delete("/{anime_id}") -async def remove_favorite(anime_id: str): +async def remove_favorite( + anime_id: str, + response: Response, + current_user: User = Depends(get_current_user_from_token), +): """Remove an anime from favorites""" fav_manager = get_favorites_manager() - removed = await fav_manager.remove_favorite(anime_id) + removed = await fav_manager.remove_favorite(anime_id, user_id=current_user.id) if not removed: raise HTTPException(status_code=404, detail="Favorite not found") + response.headers["HX-Trigger"] = '{"show-toast": {"message": "Favori supprimé", "type": "info"}}' return {"status": "removed", "anime_id": anime_id} @router.get("/stats") -async def get_favorites_stats(): +async def get_favorites_stats( + current_user: User = Depends(get_current_user_from_token), +): """Get statistics about favorites""" fav_manager = get_favorites_manager() - stats = await fav_manager.get_stats() + stats = await fav_manager.get_stats(user_id=current_user.id) return stats @router.get("/{anime_id}") -async def get_favorite(anime_id: str): +async def get_favorite( + anime_id: str, + current_user: User = Depends(get_current_user_from_token), +): """Get details of a specific favorite anime""" fav_manager = get_favorites_manager() - favorite = await fav_manager.get_favorite(anime_id) + favorite = await fav_manager.get_favorite(anime_id, user_id=current_user.id) if not favorite: raise HTTPException(status_code=404, detail="Favorite not found") @@ -95,7 +129,11 @@ async def get_favorite(anime_id: str): @router.post("/toggle") -async def toggle_favorite(request: Request): +async def toggle_favorite( + request: Request, + response: Response, + current_user: User = Depends(get_current_user_from_token), +): """Toggle an anime in favorites""" data = await request.json() @@ -108,6 +146,7 @@ async def toggle_favorite(request: Request): fav_manager = get_favorites_manager() result = await fav_manager.toggle_favorite( + user_id=current_user.id, anime_id=data["anime_id"], title=data["title"], url=data["url"], @@ -116,4 +155,9 @@ async def toggle_favorite(request: Request): poster_url=data.get("poster_url"), ) + action = result.get("action", "unknown") + message = f"'{data['title']}' {'ajouté aux' if action == 'added' else 'retiré des'} favoris" + toast_type = "success" if action == "added" else "info" + + response.headers["HX-Trigger"] = f'{{"show-toast": {{"message": "{message}", "type": "{toast_type}"}}}}' return result diff --git a/app/routers/router_recommendations.py b/app/routers/router_recommendations.py index 2fdf0a7..43f9d5b 100644 --- a/app/routers/router_recommendations.py +++ b/app/routers/router_recommendations.py @@ -6,10 +6,12 @@ import hashlib from datetime import datetime from typing import Optional -from fastapi import APIRouter, Request, Query, HTTPException +from fastapi import APIRouter, Request, Query, HTTPException, Depends from fastapi.templating import Jinja2Templates from app.recommendation_engine import RecommendationEngine +from app.models.auth import User +from app.routers.router_auth import get_optional_user, get_current_user_from_token router = APIRouter(prefix="/api", tags=["recommendations"]) templates = Jinja2Templates(directory="templates") @@ -26,14 +28,25 @@ async def get_recommendations( request: Request, limit: int = 15, html: bool = Query(False), + current_user: Optional[User] = Depends(get_optional_user), ): """Get personalized anime recommendations based on download history""" + is_htmx = request.headers.get("HX-Request") + + if current_user is None and (html or is_htmx): + return templates.TemplateResponse( + "components/login_prompt.html", {"request": request} + ) + + if current_user is None: + raise HTTPException(status_code=401, detail="Authentication required") + engine = RecommendationEngine(download_dir="downloads") try: recommendations = await engine.get_personalized_recommendations(limit=limit) - if html or request.headers.get("HX-Request"): + if html or is_htmx: return templates.TemplateResponse( "components/recommendations_list.html", {"request": request, "recommendations": recommendations} @@ -140,7 +153,9 @@ async def get_top_anime( @router.get("/stats/downloads") -async def get_download_statistics(): +async def get_download_statistics( + current_user: User = Depends(get_current_user_from_token), +): """Get download statistics and preferences""" engine = RecommendationEngine(download_dir="downloads") diff --git a/app/routers/router_sonarr.py b/app/routers/router_sonarr.py index 45d6073..f9d27fc 100644 --- a/app/routers/router_sonarr.py +++ b/app/routers/router_sonarr.py @@ -68,14 +68,19 @@ async def test_sonarr_webhook(request: Request): @router.get("/sonarr/config") -async def get_sonarr_config(): +async def get_sonarr_config( + current_user: User = Depends(get_current_user_from_token), +): """Get Sonarr webhook configuration""" sonarr_handler = get_sonarr_handler() return sonarr_handler.get_config() @router.put("/sonarr/config") -async def update_sonarr_config(config: SonarrConfig): +async def update_sonarr_config( + config: SonarrConfig, + current_user: User = Depends(get_current_user_from_token), +): """Update Sonarr webhook configuration""" sonarr_handler = get_sonarr_handler() try: @@ -87,14 +92,19 @@ async def update_sonarr_config(config: SonarrConfig): @router.get("/sonarr/mappings") -async def get_sonarr_mappings(): +async def get_sonarr_mappings( + current_user: User = Depends(get_current_user_from_token), +): """Get all Sonarr to anime mappings""" sonarr_handler = get_sonarr_handler() return sonarr_handler.get_mappings() @router.get("/sonarr/mappings/{series_id}") -async def get_sonarr_mapping(series_id: int): +async def get_sonarr_mapping( + series_id: int, + current_user: User = Depends(get_current_user_from_token), +): """Get specific mapping by Sonarr series ID""" sonarr_handler = get_sonarr_handler() mapping = sonarr_handler.get_mapping(series_id) @@ -104,7 +114,10 @@ async def get_sonarr_mapping(series_id: int): @router.post("/sonarr/mappings") -async def create_sonarr_mapping(mapping: SonarrMapping): +async def create_sonarr_mapping( + mapping: SonarrMapping, + current_user: User = Depends(get_current_user_from_token), +): """Create or update a Sonarr to anime mapping""" sonarr_handler = get_sonarr_handler() try: @@ -116,7 +129,10 @@ async def create_sonarr_mapping(mapping: SonarrMapping): @router.delete("/sonarr/mappings/{series_id}") -async def delete_sonarr_mapping(series_id: int): +async def delete_sonarr_mapping( + series_id: int, + current_user: User = Depends(get_current_user_from_token), +): """Delete a Sonarr mapping""" sonarr_handler = get_sonarr_handler() success = sonarr_handler.delete_mapping(series_id) @@ -130,6 +146,7 @@ async def search_anime_for_sonarr( q: str = Query(..., description="Series title to search"), provider: str = Query("anime-sama", description="Anime provider to search"), lang: str = Query("vostfr", description="Language (vostfr, vf)"), + current_user: User = Depends(get_current_user_from_token), ): """Search for anime on providers to create Sonarr mappings""" sonarr_handler = get_sonarr_handler() @@ -152,6 +169,7 @@ async def get_anime_episodes( url: str = Query(..., description="Anime URL from provider"), provider: str = Query("anime-sama", description="Anime provider"), lang: str = Query("vostfr", description="Language (vostfr, vf)"), + current_user: User = Depends(get_current_user_from_token), ): """Get episode list for anime""" sonarr_handler = get_sonarr_handler() @@ -174,6 +192,7 @@ async def suggest_anime_mapping( sonarr_title: str = Query(..., description="Sonarr series title"), provider: str = Query("anime-sama", description="Anime provider"), lang: str = Query("vostfr", description="Language"), + current_user: User = Depends(get_current_user_from_token), ): """Suggest possible anime mappings based on Sonarr series title""" sonarr_handler = get_sonarr_handler() @@ -195,6 +214,7 @@ async def suggest_anime_mapping( async def trigger_sonarr_download( request: SonarrDownloadRequest, background_tasks: BackgroundTasks, + current_user: User = Depends(get_current_user_from_token), ): """Manually trigger a download based on Sonarr information""" from main import download_manager