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
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user