feat: frontend modernization with HTMX, Alpine.js and Plyr (Phase 3)
CI / Test (Python 3.11) (push) Has been cancelled
CI / Test (Python 3.12) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Type Check (push) Has been cancelled
CI / Summary (push) Has been cancelled

- 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
This commit is contained in:
root
2026-03-24 11:10:22 +00:00
parent 2b4cc617cb
commit 5c7116557d
17 changed files with 584 additions and 690 deletions
+1 -5
View File
@@ -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",
+19 -21
View File
@@ -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,
+50 -97
View File
@@ -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"}
+68 -343
View File
@@ -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)