tubewatch/playlist-monitor/app/api/playlists.py

278 lines
8.7 KiB
Python

"""
Playlist API endpoints
"""
import logging
from typing import List, Optional
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel, HttpUrl
from sqlalchemy.orm import Session
from ..core.database import get_db
from ..core.config import settings
from ..models.playlist import PlaylistSubscription
from ..services.playlist_service import PlaylistService
logger = logging.getLogger(__name__)
router = APIRouter()
# Pydantic models for API requests/responses
class PlaylistCreate(BaseModel):
"""Playlist creation request model"""
url: HttpUrl
check_interval: int = settings.DEFAULT_CHECK_INTERVAL
start_point: Optional[str] = None # video_id or index
quality: str = settings.DEFAULT_QUALITY
format: str = settings.DEFAULT_FORMAT
folder: Optional[str] = None
enabled: bool = True
class PlaylistUpdate(BaseModel):
"""Playlist update request model"""
check_interval: Optional[int] = None
start_point: Optional[str] = None
quality: Optional[str] = None
format: Optional[str] = None
folder: Optional[str] = None
enabled: Optional[bool] = None
class PlaylistResponse(BaseModel):
"""Playlist response model"""
id: str
url: str
title: Optional[str]
check_interval: int
last_checked: Optional[datetime]
start_point: Optional[str]
quality: str
format: str
folder: Optional[str]
enabled: bool
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class PlaylistWithStats(PlaylistResponse):
"""Playlist response with statistics"""
stats: dict
videos: List[dict] = []
class PlaylistStats(BaseModel):
"""Playlist statistics"""
total: int
pending: int
downloading: int
completed: int
failed: int
skipped: int
@router.get("/", response_model=List[PlaylistResponse])
async def list_playlists(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
enabled: Optional[bool] = None,
db: Session = Depends(get_db)
):
"""List all playlists"""
try:
service = PlaylistService(db)
playlists = service.get_playlists(skip=skip, limit=limit, enabled=enabled)
return playlists
except Exception as e:
logger.error(f"Error listing playlists: {e}")
raise HTTPException(status_code=500, detail=f"Error listing playlists: {str(e)}")
@router.post("/", response_model=PlaylistResponse, status_code=status.HTTP_201_CREATED)
async def create_playlist(
playlist: PlaylistCreate,
db: Session = Depends(get_db)
):
"""Add a new playlist for monitoring"""
try:
service = PlaylistService(db)
new_playlist = await service.add_playlist(
url=str(playlist.url),
check_interval=playlist.check_interval,
start_point=playlist.start_point,
quality=playlist.quality,
format=playlist.format,
folder=playlist.folder,
enabled=playlist.enabled
)
return new_playlist
except ValueError as e:
logger.warning(f"Invalid playlist URL: {e}")
raise HTTPException(status_code=400, detail=f"Invalid playlist URL: {str(e)}")
except Exception as e:
logger.error(f"Error creating playlist: {e}")
raise HTTPException(status_code=500, detail=f"Error creating playlist: {str(e)}")
@router.get("/{playlist_id}", response_model=PlaylistWithStats)
async def get_playlist(
playlist_id: str,
include_videos: bool = Query(True),
video_status: Optional[str] = Query(None),
video_limit: int = Query(50, ge=1, le=500),
db: Session = Depends(get_db)
):
"""Get a specific playlist with details and statistics"""
try:
service = PlaylistService(db)
playlist = service.get_playlist(playlist_id)
if not playlist:
raise HTTPException(status_code=404, detail="Playlist not found")
# Get statistics
stats = service.get_playlist_stats(playlist_id)
# Get videos if requested
videos = []
if include_videos:
videos = service.get_playlist_videos(
playlist_id=playlist_id,
status=video_status,
limit=video_limit
)
return PlaylistWithStats(
**playlist.__dict__,
stats=stats,
videos=videos
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting playlist {playlist_id}: {e}")
raise HTTPException(status_code=500, detail=f"Error getting playlist: {str(e)}")
@router.put("/{playlist_id}", response_model=PlaylistResponse)
async def update_playlist(
playlist_id: str,
playlist_update: PlaylistUpdate,
db: Session = Depends(get_db)
):
"""Update a playlist"""
try:
service = PlaylistService(db)
# Get existing playlist
existing = service.get_playlist(playlist_id)
if not existing:
raise HTTPException(status_code=404, detail="Playlist not found")
# Update playlist
updated_playlist = service.update_playlist(
playlist_id=playlist_id,
**playlist_update.dict(exclude_unset=True)
)
return updated_playlist
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating playlist {playlist_id}: {e}")
raise HTTPException(status_code=500, detail=f"Error updating playlist: {str(e)}")
@router.delete("/{playlist_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_playlist(
playlist_id: str,
delete_videos: bool = Query(False, description="Also delete all associated video records"),
db: Session = Depends(get_db)
):
"""Delete a playlist"""
try:
service = PlaylistService(db)
# Check if playlist exists
existing = service.get_playlist(playlist_id)
if not existing:
raise HTTPException(status_code=404, detail="Playlist not found")
# Delete playlist
service.delete_playlist(playlist_id, delete_videos=delete_videos)
logger.info(f"Deleted playlist {playlist_id}")
except HTTPException:
raise
except Exception as e:
logger.error(f"Error deleting playlist {playlist_id}: {e}")
raise HTTPException(status_code=500, detail=f"Error deleting playlist: {str(e)}")
@router.post("/{playlist_id}/check", response_model=dict)
async def trigger_playlist_check(
playlist_id: str,
force: bool = Query(False, description="Force check even if recently checked"),
db: Session = Depends(get_db)
):
"""Manually trigger a playlist check"""
try:
service = PlaylistService(db)
# Check if playlist exists
existing = service.get_playlist(playlist_id)
if not existing:
raise HTTPException(status_code=404, detail="Playlist not found")
# Trigger check
new_videos = await service.check_playlist(playlist_id, force=force)
return {
"status": "ok",
"new_videos": new_videos,
"message": f"Playlist check completed. Found {new_videos} new videos."
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error checking playlist {playlist_id}: {e}")
raise HTTPException(status_code=500, detail=f"Error checking playlist: {str(e)}")
@router.post("/{playlist_id}/start-point", response_model=dict)
async def update_start_point(
playlist_id: str,
video_id: str,
db: Session = Depends(get_db)
):
"""Update the start point for a playlist"""
try:
service = PlaylistService(db)
# Check if playlist exists
existing = service.get_playlist(playlist_id)
if not existing:
raise HTTPException(status_code=404, detail="Playlist not found")
# Update start point
updated_count = service.update_start_point(playlist_id, video_id)
return {
"status": "ok",
"updated_videos": updated_count,
"message": f"Updated start point and marked {updated_count} videos as skipped."
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating start point for playlist {playlist_id}: {e}")
raise HTTPException(status_code=500, detail=f"Error updating start point: {str(e)}")