d4d8d8a3b6
- Migrated monolithic main.py to feature-scoped routers in app/routers/ - Added GEMINI.md for project context and AI instructional guidelines - Updated README.md with a comprehensive modernization plan (SQL migration, robust scraping DSL, frontend modernization) - Improved authentication with cookie support and modular JS - Updated test suite and documentation
460 lines
16 KiB
Python
460 lines
16 KiB
Python
"""
|
|
Watchlist management routes for Ohm Stream Downloader API.
|
|
"""
|
|
|
|
import re
|
|
from typing import List, Optional
|
|
|
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
|
|
|
from app.download_manager import DownloadManager
|
|
from app.downloaders import get_downloader
|
|
from app.models import DownloadRequest
|
|
from app.models.auth import User
|
|
from app.models.watchlist import (
|
|
WatchlistItem,
|
|
WatchlistItemCreate,
|
|
WatchlistItemUpdate,
|
|
WatchlistSettings,
|
|
WatchlistStatus,
|
|
)
|
|
from app.routers.router_auth import get_current_user_from_token
|
|
|
|
router = APIRouter(prefix="/api/watchlist", tags=["watchlist"])
|
|
|
|
|
|
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,
|
|
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)
|
|
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))
|
|
|
|
|
|
@router.get("", response_model=List[WatchlistItem])
|
|
async def get_watchlist(
|
|
status: Optional[WatchlistStatus] = None,
|
|
current_user: User = Depends(get_current_user_from_token),
|
|
):
|
|
"""Get user's watchlist"""
|
|
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))
|
|
|
|
|
|
@router.get("/settings", response_model=WatchlistSettings)
|
|
async def get_watchlist_settings(
|
|
current_user: User = Depends(get_current_user_from_token),
|
|
):
|
|
"""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))
|
|
|
|
|
|
@router.put("/settings", response_model=WatchlistSettings)
|
|
async def update_watchlist_settings(
|
|
settings: WatchlistSettings,
|
|
current_user: User = Depends(get_current_user_from_token),
|
|
):
|
|
"""Update global watchlist settings"""
|
|
from main import auto_download_scheduler, watchlist_manager
|
|
|
|
try:
|
|
updated_settings = watchlist_manager.update_settings(settings)
|
|
if auto_download_scheduler.is_running():
|
|
auto_download_scheduler.restart()
|
|
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))
|
|
|
|
|
|
@router.get("/{item_id}", response_model=WatchlistItem)
|
|
async def get_watchlist_item(
|
|
item_id: str,
|
|
current_user: User = Depends(get_current_user_from_token),
|
|
):
|
|
"""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))
|
|
|
|
|
|
@router.put("/{item_id}", response_model=WatchlistItem)
|
|
async def update_watchlist_item(
|
|
item_id: str,
|
|
update_data: WatchlistItemUpdate,
|
|
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
|
|
|
|
logger = getLogger(__name__)
|
|
logger.error(f"Error updating watchlist item: {e}", exc_info=True)
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.delete("/{item_id}")
|
|
async def delete_watchlist_item(
|
|
item_id: str,
|
|
current_user: User = Depends(get_current_user_from_token),
|
|
):
|
|
"""Delete 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
|
|
|
|
logger = getLogger(__name__)
|
|
logger.error(f"Error deleting watchlist item: {e}", exc_info=True)
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@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,
|
|
background_tasks: BackgroundTasks,
|
|
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))
|
|
|
|
|
|
@router.post("/{item_id}/pause", response_model=WatchlistItem)
|
|
async def pause_watchlist_item(
|
|
item_id: str,
|
|
current_user: User = Depends(get_current_user_from_token),
|
|
):
|
|
"""Pause automatic downloading for a specific 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.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))
|