refactor: migrate main.py to modular routers and add project roadmap
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

- 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
This commit is contained in:
root
2026-03-24 10:12:04 +00:00
parent 1b5d7f9238
commit d4d8d8a3b6
42 changed files with 4518 additions and 2426 deletions
+459
View File
@@ -0,0 +1,459 @@
"""
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))