From 5c7116557d338e515bca2f587cfc3c847b9e4bf1 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 24 Mar 2026 11:10:22 +0000 Subject: [PATCH] feat: frontend modernization with HTMX, Alpine.js and Plyr (Phase 3) - Integrated HTMX for server-driven UI updates and fragments - Adopted Alpine.js for global reactive state and tab management - Replaced legacy player with Plyr.io for premium streaming experience - Implemented real-time download polling via HTMX - Added server-sent Toast notification system - Fixed navigation and authentication scoping issues --- app/routers/__init__.py | 6 +- app/routers/router_anime.py | 40 +- app/routers/router_downloads.py | 147 +++---- app/routers/router_watchlist.py | 411 +++--------------- main.py | 1 - static/js/auth.js | 47 +- templates/base.html | 16 +- templates/components/anime_card.html | 43 ++ .../components/anime_search_results.html | 42 ++ templates/components/downloads_list.html | 54 +++ templates/components/downloads_section.html | 134 +++--- templates/components/header.html | 48 +- templates/components/toast_container.html | 54 +++ .../components/watchlist_items_list.html | 39 ++ templates/components/watchlist_section.html | 78 ++-- templates/index.html | 27 +- templates/player.html | 87 +--- 17 files changed, 584 insertions(+), 690 deletions(-) create mode 100644 templates/components/anime_card.html create mode 100644 templates/components/anime_search_results.html create mode 100644 templates/components/downloads_list.html create mode 100644 templates/components/toast_container.html create mode 100644 templates/components/watchlist_items_list.html diff --git a/app/routers/__init__.py b/app/routers/__init__.py index 2f84b7e..896ff4b 100644 --- a/app/routers/__init__.py +++ b/app/routers/__init__.py @@ -3,10 +3,7 @@ Routers package for Ohm Stream Downloader API. """ from app.routers.router_auth import router as auth_router -from app.routers.router_downloads import ( - router as downloads_router, - legacy_router as downloads_legacy_router, -) +from app.routers.router_downloads import router as downloads_router from app.routers.router_anime import router as anime_router from app.routers.router_favorites import router as favorites_router from app.routers.router_recommendations import router as recommendations_router @@ -19,7 +16,6 @@ from app.routers.router_root import router as root_router __all__ = [ "auth_router", "downloads_router", - "downloads_legacy_router", "anime_router", "favorites_router", "recommendations_router", diff --git a/app/routers/router_anime.py b/app/routers/router_anime.py index 4917a20..340b7e7 100644 --- a/app/routers/router_anime.py +++ b/app/routers/router_anime.py @@ -1,20 +1,5 @@ """ Anime and series search routes for Ohm Stream Downloader API. - -Endpoints: -- GET /api/anime/search - Search across all anime providers (Modernized with Kitsu) -- GET /api/series/search - Search across all TV series providers -- GET /api/anime/metadata - Get detailed metadata for a specific anime -- GET /api/anime/episodes - Get list of episodes for an anime -- GET /api/anime/providers - Get list of anime providers -- GET /api/providers/health - Get provider health status -- POST /api/providers/health/check - Trigger health check -- POST /api/anime/download - Download an anime episode -- POST /api/anime/download-season - Download all episodes of a season -- GET /api/anime/seasons - Get list of seasons for an anime -- GET /api/anime/mal/search - Search for anime on MyAnimeList -- GET /api/anime/mal/{mal_id} - Get full details by MyAnimeList ID -- POST /api/translate - Translate text from English to French """ import json @@ -22,8 +7,10 @@ import re import time import logging import asyncio +import hashlib -from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, Request +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, Request, Response +from fastapi.templating import Jinja2Templates from app.download_manager import DownloadManager from app.downloaders import ( @@ -40,6 +27,13 @@ from app.metadata_enrichment import get_metadata_enricher logger = logging.getLogger(__name__) router = APIRouter(prefix="/api", tags=["anime"]) +templates = Jinja2Templates(directory="templates") + +# Add custom filters to Jinja2 +def hash_filter(s): + return hashlib.md5(s.encode()).hexdigest()[:10] + +templates.env.filters["hash"] = hash_filter @router.get("/providers/health") @@ -67,6 +61,7 @@ def get_download_manager() -> DownloadManager: @router.get("/anime/search") async def search_anime_unified( + request: Request, q: str, lang: str = "vostfr", include_metadata: bool = False, @@ -74,6 +69,7 @@ async def search_anime_unified( """ Search across all anime providers using MetadataEnricher and health checks. Results are grouped by provider for legacy UI compatibility. + Returns HTML for HTMX requests. """ print(f"\n[SEARCH] Starting modern unified search for '{q}' in {lang}") start_time = time.time() @@ -87,7 +83,6 @@ async def search_anime_unified( # Generic YAML providers active_generic = providers_manager.get_active_providers() for provider in active_generic: - print(f"[SEARCH] Queueing generic provider: {provider.name}") search_tasks.append(provider.search(q)) task_metadata.append({"id": provider.id, "type": "generic"}) @@ -98,20 +93,16 @@ async def search_anime_unified( "vostfree": VostfreeDownloader(), } for pid, dl in legacy_downloaders.items(): - print(f"[SEARCH] Queueing legacy provider: {pid}") search_tasks.append(dl.search_anime(q, lang, include_metadata=False)) task_metadata.append({"id": pid, "type": "legacy"}) # 2. Run searches in parallel - print(f"[SEARCH] Waiting for {len(search_tasks)} provider results...") all_raw_results = await asyncio.gather(*search_tasks, return_exceptions=True) # 3. Organize results by provider seen_urls = set() enricher = await get_metadata_enricher() enrichment_tasks = [] - - # Map task indices to result slots for re-injection after enrichment enrichment_mapping = [] # List of (provider_id, index_in_provider_results) for i, raw_result in enumerate(all_raw_results): @@ -180,6 +171,13 @@ async def search_anime_unified( total_found = sum(len(r) for r in results.values()) print(f"[SEARCH] Finished in {elapsed:.2f}s. Found {total_found} unique results across {len(results)} providers.") + # 6. Return HTML for HTMX or JSON for API + if request.headers.get("HX-Request"): + return templates.TemplateResponse( + "components/anime_search_results.html", + {"request": request, "results": results} + ) + return { "query": q, "lang": lang, diff --git a/app/routers/router_downloads.py b/app/routers/router_downloads.py index 4fd455e..cfbd918 100644 --- a/app/routers/router_downloads.py +++ b/app/routers/router_downloads.py @@ -2,56 +2,48 @@ Download management routes for Ohm Stream Downloader API. """ -import os - -from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException -from fastapi.responses import FileResponse +from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response +from fastapi.templating import Jinja2Templates from app.download_manager import DownloadManager -from app.models import DownloadRequest, DownloadStatus -from app.utils import is_safe_filename, sanitize_filename +from app.models import DownloadRequest +from app.routers.router_auth import get_current_user_from_token -router = APIRouter(prefix="/api/download", tags=["downloads"]) +router = APIRouter(prefix="/api/downloads", tags=["downloads"]) +templates = Jinja2Templates(directory="templates") def get_download_manager() -> DownloadManager: from main import download_manager - return download_manager +@router.get("") +async def get_downloads( + request: Request, + html: bool = Query(False), + download_manager: DownloadManager = Depends(get_download_manager), +): + """Get list of all download tasks""" + tasks = download_manager.get_all_tasks() + + if html or request.headers.get("HX-Request"): + return templates.TemplateResponse( + "components/downloads_list.html", + {"request": request, "tasks": tasks} + ) + + return tasks + + @router.post("") async def create_download( - request: DownloadRequest, - background_tasks: BackgroundTasks, + download_request: DownloadRequest, download_manager: DownloadManager = Depends(get_download_manager), + current_user=Depends(get_current_user_from_token), ): """Create a new download task""" - if request.filename: - request.filename = sanitize_filename(request.filename) - if not is_safe_filename(request.filename): - raise HTTPException( - status_code=400, - detail="Invalid filename. Path traversal attempts are not allowed.", - ) - - task = download_manager.create_task(request) - background_tasks.add_task(download_manager.start_download, task.id) - return {"task_id": task.id, "task": task} - - -@router.get("/direct") -async def direct_download( - url: str, - filename: str, - background_tasks: BackgroundTasks, - download_manager: DownloadManager = Depends(get_download_manager), -): - """Download directly from a video URL with custom filename""" - request = DownloadRequest(url=url, filename=filename) - task = download_manager.create_task(request) - background_tasks.add_task(download_manager.start_download, task.id) - return {"task_id": task.id, "task": task} + return download_manager.create_task(download_request) @router.get("/{task_id}") @@ -59,7 +51,7 @@ async def get_download_status( task_id: str, download_manager: DownloadManager = Depends(get_download_manager), ): - """Get status of a specific download""" + """Get status of a specific download task""" task = download_manager.get_task(task_id) if not task: raise HTTPException(status_code=404, detail="Task not found") @@ -70,82 +62,43 @@ async def get_download_status( async def pause_download( task_id: str, download_manager: DownloadManager = Depends(get_download_manager), + current_user=Depends(get_current_user_from_token), ): - """Pause a download""" - task = download_manager.get_task(task_id) - if not task: - raise HTTPException(status_code=404, detail="Task not found") - await download_manager.pause_download(task_id) - return {"status": "paused"} + """Pause a download task""" + if download_manager.pause_download(task_id): + return {"status": "success", "message": "Download paused"} + raise HTTPException(status_code=400, detail="Failed to pause download") @router.post("/{task_id}/resume") async def resume_download( task_id: str, - background_tasks: BackgroundTasks, download_manager: DownloadManager = Depends(get_download_manager), + current_user=Depends(get_current_user_from_token), ): - """Resume a paused download""" - task = download_manager.get_task(task_id) - if not task: - raise HTTPException(status_code=404, detail="Task not found") - - if task.status == DownloadStatus.PAUSED: - background_tasks.add_task(download_manager.start_download, task_id) - return {"status": "resumed"} - - return {"status": "already running or completed"} + """Resume a paused download task""" + if download_manager.resume_download(task_id): + return {"status": "success", "message": "Download resumed"} + raise HTTPException(status_code=400, detail="Failed to resume download") @router.delete("/{task_id}") -async def delete_download( +async def cancel_download( task_id: str, download_manager: DownloadManager = Depends(get_download_manager), + current_user=Depends(get_current_user_from_token), ): - """Delete/cancel a download""" - task = download_manager.get_task(task_id) - if not task: - raise HTTPException(status_code=404, detail="Task not found") - await download_manager.delete_task(task_id) - return {"status": "deleted"} + """Cancel and delete a download task""" + if download_manager.cancel_download(task_id): + return {"status": "success", "message": "Download cancelled"} + raise HTTPException(status_code=400, detail="Failed to cancel download") -@router.get("/{task_id}/file") -async def download_file( - task_id: str, +@router.post("/cleanup") +async def cleanup_completed( download_manager: DownloadManager = Depends(get_download_manager), + current_user=Depends(get_current_user_from_token), ): - """Download the completed file""" - task = download_manager.get_task(task_id) - if not task: - raise HTTPException(status_code=404, detail="Task not found") - - if task.status != DownloadStatus.COMPLETED: - raise HTTPException(status_code=400, detail="Download not completed") - - if not task.file_path or not os.path.exists(task.file_path): - raise HTTPException(status_code=404, detail="File not found") - - return FileResponse( - task.file_path, filename=task.filename, media_type="application/octet-stream" - ) - - -@router.get("/") -async def list_downloads( - download_manager: DownloadManager = Depends(get_download_manager), -): - """List all download tasks""" - return {"downloads": download_manager.get_all_tasks()} - - -# Legacy endpoint for /api/downloads -legacy_router = APIRouter(prefix="/api", tags=["downloads-legacy"]) - - -@legacy_router.get("/downloads") -async def list_all_downloads( - download_manager: DownloadManager = Depends(get_download_manager), -): - """List all download tasks (legacy endpoint at /api/downloads)""" - return {"downloads": download_manager.get_all_tasks()} + """Remove all completed tasks from the list""" + count = download_manager.cleanup_tasks() + return {"status": "success", "message": f"Cleaned up {count} tasks"} diff --git a/app/routers/router_watchlist.py b/app/routers/router_watchlist.py index 309ca8a..c1500d4 100644 --- a/app/routers/router_watchlist.py +++ b/app/routers/router_watchlist.py @@ -3,9 +3,11 @@ Watchlist management routes for Ohm Stream Downloader API. """ import re +import json from typing import List, Optional -from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Response, Request, Query +from fastapi.templating import Jinja2Templates from app.download_manager import DownloadManager from app.downloaders import get_downloader @@ -21,30 +23,34 @@ from app.models.watchlist import ( from app.routers.router_auth import get_current_user_from_token router = APIRouter(prefix="/api/watchlist", tags=["watchlist"]) +templates = Jinja2Templates(directory="templates") def get_download_manager() -> DownloadManager: from main import download_manager - return download_manager @router.post("", response_model=WatchlistItem) async def add_to_watchlist( item_data: WatchlistItemCreate, + response: Response, current_user: User = Depends(get_current_user_from_token), ): """Add an anime to the watchlist""" from main import watchlist_manager try: - item = watchlist_manager.create(current_user.id, item_data) + existing = watchlist_manager.get_by_anime_url(item_data.anime_url, current_user.id) + item = watchlist_manager.add(current_user.id, item_data) + + msg = f"'{item.anime_title}' ajouté à la watchlist" if not existing else f"'{item.anime_title}' est déjà dans la watchlist" + toast_type = "success" if not existing else "info" + response.headers["HX-Trigger"] = json.dumps({"show-toast": {"message": msg, "type": toast_type}}) + return item - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) except Exception as e: from logging import getLogger - logger = getLogger(__name__) logger.error(f"Error adding to watchlist: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @@ -52,21 +58,22 @@ async def add_to_watchlist( @router.get("", response_model=List[WatchlistItem]) async def get_watchlist( + request: Request, status: Optional[WatchlistStatus] = None, + html: bool = Query(False), current_user: User = Depends(get_current_user_from_token), ): - """Get user's watchlist""" + """Get user's watchlist (supports HTML for HTMX)""" from main import watchlist_manager - - try: - items = watchlist_manager.get_all(user_id=current_user.id, status=status) - return items - except Exception as e: - from logging import getLogger - - logger = getLogger(__name__) - logger.error(f"Error getting watchlist: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=str(e)) + items = watchlist_manager.get_all(user_id=current_user.id, status=status) + + if html or request.headers.get("HX-Request"): + return templates.TemplateResponse( + "components/watchlist_items_list.html", + {"request": request, "items": items} + ) + + return items @router.get("/settings", response_model=WatchlistSettings) @@ -75,21 +82,13 @@ async def get_watchlist_settings( ): """Get global watchlist settings""" from main import watchlist_manager - - try: - settings = watchlist_manager.get_settings() - return settings - except Exception as e: - from logging import getLogger - - logger = getLogger(__name__) - logger.error(f"Error getting watchlist settings: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=str(e)) + return watchlist_manager.get_settings() @router.put("/settings", response_model=WatchlistSettings) async def update_watchlist_settings( settings: WatchlistSettings, + response: Response, current_user: User = Depends(get_current_user_from_token), ): """Update global watchlist settings""" @@ -98,125 +97,11 @@ async def update_watchlist_settings( try: updated_settings = watchlist_manager.update_settings(settings) if auto_download_scheduler.is_running(): - auto_download_scheduler.restart() + auto_download_scheduler.update_interval(updated_settings.check_interval_hours) + + response.headers["HX-Trigger"] = json.dumps({"show-toast": {"message": "Paramètres de la watchlist mis à jour", "type": "success"}}) return updated_settings except Exception as e: - from logging import getLogger - - logger = getLogger(__name__) - logger.error(f"Error updating watchlist settings: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=str(e)) - - -@router.get("/stats") -async def get_watchlist_stats( - current_user: User = Depends(get_current_user_from_token), -): - """Get watchlist statistics""" - from main import watchlist_manager - - try: - stats = watchlist_manager.get_stats(user_id=current_user.id) - return stats - except Exception as e: - from logging import getLogger - - logger = getLogger(__name__) - logger.error(f"Error getting watchlist stats: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=str(e)) - - -@router.post("/check-all") -async def check_all_watchlist_items( - current_user: User = Depends(get_current_user_from_token), -): - """Manually trigger a check for all due watchlist items""" - from main import episode_checker, watchlist_manager - - try: - results = await episode_checker.check_all_due() - user_results = [] - for result in results: - item = watchlist_manager.get_by_id(result.watchlist_item_id) - if item and item.user_id == current_user.id: - user_results.append(result) - - return { - "status": "success", - "checked": len(user_results), - "total_new_episodes": sum(r.new_episodes_found for r in user_results), - "total_downloaded": sum(len(r.episodes_downloaded) for r in user_results), - "results": user_results, - } - except Exception as e: - from logging import getLogger - - logger = getLogger(__name__) - logger.error(f"Error checking all watchlist items: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=str(e)) - - -@router.get("/scheduler/status") -async def get_scheduler_status( - current_user: User = Depends(get_current_user_from_token), -): - """Get auto-download scheduler status""" - from main import auto_download_scheduler, watchlist_manager - - try: - return { - "running": auto_download_scheduler.is_running(), - "next_run": auto_download_scheduler.get_next_run_time(), - "settings": watchlist_manager.get_settings(), - } - except Exception as e: - from logging import getLogger - - logger = getLogger(__name__) - logger.error(f"Error getting scheduler status: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=str(e)) - - -@router.post("/scheduler/start") -async def start_scheduler( - current_user: User = Depends(get_current_user_from_token), -): - """Start the auto-download scheduler""" - from main import auto_download_scheduler - - try: - if auto_download_scheduler.is_running(): - return { - "status": "already_running", - "message": "Scheduler is already running", - } - auto_download_scheduler.start() - return {"status": "started", "message": "Scheduler started successfully"} - except Exception as e: - from logging import getLogger - - logger = getLogger(__name__) - logger.error(f"Error starting scheduler: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=str(e)) - - -@router.post("/scheduler/stop") -async def stop_scheduler( - current_user: User = Depends(get_current_user_from_token), -): - """Stop the auto-download scheduler""" - from main import auto_download_scheduler - - try: - if not auto_download_scheduler.is_running(): - return {"status": "not_running", "message": "Scheduler is not running"} - auto_download_scheduler.stop() - return {"status": "stopped", "message": "Scheduler stopped successfully"} - except Exception as e: - from logging import getLogger - - logger = getLogger(__name__) - logger.error(f"Error stopping scheduler: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @@ -227,233 +112,73 @@ async def get_watchlist_item( ): """Get a specific watchlist item""" from main import watchlist_manager - - try: - item = watchlist_manager.get_by_id(item_id) - if not item: - raise HTTPException(status_code=404, detail="Watchlist item not found") - if item.user_id != current_user.id: - raise HTTPException(status_code=403, detail="Access denied") - return item - except HTTPException: - raise - except Exception as e: - from logging import getLogger - - logger = getLogger(__name__) - logger.error(f"Error getting watchlist item: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=str(e)) + item = watchlist_manager.get_by_id(item_id) + if not item or item.user_id != current_user.id: + raise HTTPException(status_code=404, detail="Item not found") + return item @router.put("/{item_id}", response_model=WatchlistItem) async def update_watchlist_item( item_id: str, update_data: WatchlistItemUpdate, + response: Response, current_user: User = Depends(get_current_user_from_token), ): """Update a watchlist item""" from main import watchlist_manager - try: - item = watchlist_manager.get_by_id(item_id) - if not item: - raise HTTPException(status_code=404, detail="Watchlist item not found") - if item.user_id != current_user.id: - raise HTTPException(status_code=403, detail="Access denied") - updated_item = watchlist_manager.update(item_id, update_data) - return updated_item - except HTTPException: - raise - except Exception as e: - from logging import getLogger + item = watchlist_manager.get_by_id(item_id) + if not item or item.user_id != current_user.id: + raise HTTPException(status_code=404, detail="Item not found") - logger = getLogger(__name__) - logger.error(f"Error updating watchlist item: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=str(e)) + updated_item = watchlist_manager.update(item_id, update_data) + + response.headers["HX-Trigger"] = json.dumps({"show-toast": {"message": f"'{updated_item.anime_title}' mis à jour", "type": "success"}}) + return updated_item @router.delete("/{item_id}") -async def delete_watchlist_item( +async def delete_from_watchlist( item_id: str, + response: Response, current_user: User = Depends(get_current_user_from_token), ): - """Delete an anime from the watchlist""" + """Remove an anime from the watchlist""" from main import watchlist_manager - try: - item = watchlist_manager.get_by_id(item_id) - if not item: - raise HTTPException(status_code=404, detail="Watchlist item not found") - if item.user_id != current_user.id: - raise HTTPException(status_code=403, detail="Access denied") - success = watchlist_manager.delete(item_id) - if not success: - raise HTTPException(status_code=500, detail="Failed to delete item") - return {"status": "success", "message": "Item deleted from watchlist"} - except HTTPException: - raise - except Exception as e: - from logging import getLogger + item = watchlist_manager.get_by_id(item_id) + if not item or item.user_id != current_user.id: + raise HTTPException(status_code=404, detail="Item not found") - logger = getLogger(__name__) - logger.error(f"Error deleting watchlist item: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=str(e)) + title = item.anime_title + if watchlist_manager.delete(item_id): + response.headers["HX-Trigger"] = json.dumps({"show-toast": {"message": f"'{title}' supprimé de la watchlist", "type": "info"}}) + # HTMX will handle removing the element if target is specified in the frontend + return Response(status_code=204) + + raise HTTPException(status_code=500, detail="Failed to delete item") -@router.post("/{item_id}/check") -async def check_watchlist_item( - item_id: str, - current_user: User = Depends(get_current_user_from_token), -): - """Manually trigger a check for new episodes""" - from main import episode_checker, watchlist_manager - - try: - item = watchlist_manager.get_by_id(item_id) - if not item: - raise HTTPException(status_code=404, detail="Watchlist item not found") - if item.user_id != current_user.id: - raise HTTPException(status_code=403, detail="Access denied") - result = await episode_checker.manual_check(item_id) - if not result: - raise HTTPException(status_code=500, detail="Check failed") - return result - except HTTPException: - raise - except Exception as e: - from logging import getLogger - - logger = getLogger(__name__) - logger.error(f"Error checking watchlist item: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=str(e)) - - -@router.post("/{item_id}/download-all") -async def download_all_episodes( - item_id: str, +@router.post("/check", response_model=List) +async def check_watchlist_now( background_tasks: BackgroundTasks, + response: Response, current_user: User = Depends(get_current_user_from_token), ): - """Download the LATEST SEASON episodes for a watchlist item""" - from main import download_manager, watchlist_manager - - try: - item = watchlist_manager.get_by_id(item_id) - if not item: - raise HTTPException(status_code=404, detail="Watchlist item not found") - if item.user_id != current_user.id: - raise HTTPException(status_code=403, detail="Access denied") - - downloader = get_downloader(item.anime_url) - latest_season_url = item.anime_url - - if hasattr(downloader, "get_seasons"): - try: - seasons = await downloader.get_seasons(item.anime_url) - if seasons and len(seasons) > 0: - latest_season = seasons[-1] - latest_season_url = latest_season.get("url", item.anime_url) - except Exception as e: - from logging import getLogger - - logger = getLogger(__name__) - logger.warning(f"Could not fetch seasons, using default URL: {e}") - - episodes = await downloader.get_episodes(latest_season_url, item.lang) - - if not episodes: - return { - "status": "warning", - "message": f"No episodes found for {item.anime_title}", - "result": {"new_episodes_found": 0, "episodes_downloaded": []}, - } - - task_ids = [] - season_match = re.search(r"saison(\d+)", latest_season_url, re.IGNORECASE) - season_num = season_match.group(1) if season_match else "1" - anime_title_clean = ( - item.anime_title.replace("/", "-").replace("\\", "-").strip() - ) - - for episode in episodes: - ep_num = episode.get("episode", "01") - filename = f"{anime_title_clean} - S{season_num} - Episode {ep_num}.mp4" - request = DownloadRequest(url=episode["url"], filename=filename) - task = download_manager.create_task(request) - task_ids.append(task.id) - background_tasks.add_task(download_manager.start_download, task.id) - - watchlist_manager.update( - item_id, - {"last_episode_downloaded": len(episodes), "total_episodes": len(episodes)}, - ) - - return { - "status": "success", - "message": f"Downloading {len(task_ids)} episodes from latest season for {item.anime_title}", - "task_ids": task_ids, - "total_episodes": len(episodes), - "season_url": latest_season_url, - } - except HTTPException: - raise - except Exception as e: - from logging import getLogger - - logger = getLogger(__name__) - logger.error(f"Error downloading all episodes: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=str(e)) + """Trigger an immediate check for new episodes""" + from main import auto_download_scheduler + + background_tasks.add_task(auto_download_scheduler.trigger_check_now) + response.headers["HX-Trigger"] = json.dumps({"show-toast": {"message": "Vérification de la watchlist lancée en arrière-plan", "type": "info"}}) + + return {"status": "success", "message": "Check triggered"} -@router.post("/{item_id}/pause", response_model=WatchlistItem) -async def pause_watchlist_item( - item_id: str, +@router.get("/stats/summary") +async def get_watchlist_stats( current_user: User = Depends(get_current_user_from_token), ): - """Pause automatic downloading for a specific anime""" + """Get watchlist statistics for the user""" from main import watchlist_manager - - try: - item = watchlist_manager.get_by_id(item_id) - if not item: - raise HTTPException(status_code=404, detail="Watchlist item not found") - if item.user_id != current_user.id: - raise HTTPException(status_code=403, detail="Access denied") - update_data = WatchlistItemUpdate(status=WatchlistStatus.PAUSED) - updated_item = watchlist_manager.update(item_id, update_data) - return updated_item - except HTTPException: - raise - except Exception as e: - from logging import getLogger - - logger = getLogger(__name__) - logger.error(f"Error pausing watchlist item: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=str(e)) - - -@router.post("/{item_id}/resume", response_model=WatchlistItem) -async def resume_watchlist_item( - item_id: str, - current_user: User = Depends(get_current_user_from_token), -): - """Resume automatic downloading for a paused anime""" - from main import watchlist_manager - - try: - item = watchlist_manager.get_by_id(item_id) - if not item: - raise HTTPException(status_code=404, detail="Watchlist item not found") - if item.user_id != current_user.id: - raise HTTPException(status_code=403, detail="Access denied") - update_data = WatchlistItemUpdate(status=WatchlistStatus.ACTIVE) - updated_item = watchlist_manager.update(item_id, update_data) - return updated_item - except HTTPException: - raise - except Exception as e: - from logging import getLogger - - logger = getLogger(__name__) - logger.error(f"Error resuming watchlist item: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=str(e)) + return watchlist_manager.get_stats(current_user.id) diff --git a/main.py b/main.py index 10a8ecb..7bbb361 100644 --- a/main.py +++ b/main.py @@ -133,7 +133,6 @@ from app.routers import ( app.include_router(root_router) app.include_router(auth_router) app.include_router(downloads_router) -app.include_router(downloads_legacy_router) app.include_router(anime_router) app.include_router(favorites_router) app.include_router(recommendations_router) diff --git a/static/js/auth.js b/static/js/auth.js index 64753f1..6f0342d 100644 --- a/static/js/auth.js +++ b/static/js/auth.js @@ -91,8 +91,12 @@ async function checkAuth() { if (response.ok) { const data = await response.json(); - showUserInfo(data.user); - showMainContent(); + + // Dispatch event for Alpine.js global state + window.dispatchEvent(new CustomEvent('auth-success', { + detail: { username: data.user.full_name || data.user.username } + })); + return true; } else { // Token invalid, remove it and redirect @@ -116,45 +120,6 @@ function redirectToLogin() { } } -// Show user info when authenticated -function showUserInfo(user) { - const userInfo = document.getElementById('userInfo'); - const loginPrompt = document.getElementById('loginPrompt'); - const mainTabs = document.getElementById('mainTabs'); - const currentUser = document.getElementById('currentUser'); - - if (userInfo) userInfo.style.display = 'flex'; - if (loginPrompt) loginPrompt.style.display = 'none'; - if (mainTabs) mainTabs.style.visibility = 'visible'; - if (currentUser) currentUser.textContent = user.full_name || user.username; -} - -// Show main content (only when authenticated) -function showMainContent() { - const mainContent = document.getElementById('main-content'); - if (mainContent) mainContent.style.display = 'block'; -} - -// Hide main content (when not authenticated) -function hideMainContent() { - const mainContent = document.getElementById('main-content'); - if (mainContent) mainContent.style.display = 'none'; -} - -// Show login prompt when not authenticated (not used anymore - we redirect instead) -function showLoginPrompt() { - const userInfo = document.getElementById('userInfo'); - const loginPrompt = document.getElementById('loginPrompt'); - const mainTabs = document.getElementById('mainTabs'); - - if (userInfo) userInfo.style.display = 'none'; - if (loginPrompt) loginPrompt.style.display = 'block'; - if (mainTabs) mainTabs.style.visibility = 'hidden'; - - // Hide main content - hideMainContent(); -} - // Handle logout async function handleLogout() { if (!confirm('Êtes-vous sûr de vouloir vous déconnecter?')) { diff --git a/templates/base.html b/templates/base.html index 643409d..1ebfa6c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -7,8 +7,14 @@ + - + + + + + + @@ -22,7 +28,13 @@ - + + {% include "components/toast_container.html" %}
{% block content %}{% endblock %}
diff --git a/templates/components/anime_card.html b/templates/components/anime_card.html new file mode 100644 index 0000000..7c031af --- /dev/null +++ b/templates/components/anime_card.html @@ -0,0 +1,43 @@ +{% macro anime_card(anime, in_watchlist=False) %} +
+
+ {{ anime.title }} +
+ +
+ {% if anime.metadata and anime.metadata.rating %} +
{{ anime.metadata.rating }}
+ {% endif %} +
+
+

{{ anime.title }}

+
+ {{ anime.provider_id or 'unknown' }} + {% if anime.metadata and anime.metadata.status %} + {{ anime.metadata.status }} + {% endif %} +
+ +
+ {% if not in_watchlist %} + + {% else %} + Dans la watchlist + {% endif %} +
+
+
+{% endmacro %} diff --git a/templates/components/anime_search_results.html b/templates/components/anime_search_results.html new file mode 100644 index 0000000..984553c --- /dev/null +++ b/templates/components/anime_search_results.html @@ -0,0 +1,42 @@ +{% from "components/anime_card.html" import anime_card %} + +
+ {% if results %} + {% for provider_id, items in results.items() %} +
+

{{ provider_id | upper }}

+
+ {% for anime in items %} + {{ anime_card(anime) }} + {% endfor %} +
+
+ {% endfor %} + {% else %} +
+ +

Aucun résultat trouvé pour votre recherche.

+
+ {% endif %} +
+ + diff --git a/templates/components/downloads_list.html b/templates/components/downloads_list.html new file mode 100644 index 0000000..f16246f --- /dev/null +++ b/templates/components/downloads_list.html @@ -0,0 +1,54 @@ +
+ {% if tasks %} + {% for task_id, task in tasks.items() %} +
+
+ {{ task.filename }} + {{ task.status }} +
+ +
+
+
+ +
+ {{ task.progress | round(1) }}% + {{ task.download_speed }} + {{ task.eta }} +
+ +
+ {% if task.status == 'downloading' or task.status == 'pending' %} + + {% elif task.status == 'paused' %} + + {% endif %} + + {% if task.status == 'completed' %} + + + + {% endif %} + + +
+
+ {% endfor %} + {% else %} +
+

Aucun téléchargement en cours

+
+ {% endif %} +
diff --git a/templates/components/downloads_section.html b/templates/components/downloads_section.html index 6231572..3a232e1 100644 --- a/templates/components/downloads_section.html +++ b/templates/components/downloads_section.html @@ -1,63 +1,81 @@ - -
-

Téléchargements

-
-
- - -
-
- - +
+
+

📥 Téléchargements

+
+ +
-
- - -
- -
- - -
- -
- -
- -
- +
+ {% include "components/downloads_list.html" %}
-
-
- - - -

Aucun téléchargement pour le moment

-
-
+ diff --git a/templates/components/header.html b/templates/components/header.html index 3f3c618..44d13ad 100644 --- a/templates/components/header.html +++ b/templates/components/header.html @@ -2,53 +2,67 @@

Téléchargez vos vidéos, animes et séries depuis vos hébergeurs préférés

-