just-download/backend/routers/youtube.py

130 lines
3.5 KiB
Python

"""
/api/youtube — video, audio, thumbnail, subtitles, channel art
"""
import asyncio
import os
from fastapi import APIRouter, HTTPException, BackgroundTasks
from fastapi.responses import FileResponse
from pydantic import BaseModel
from typing import Optional
from utils import clean_yt_url
from services.youtube_service import (
download_video,
download_audio,
download_thumbnail,
download_subtitles,
download_channel_art,
)
router = APIRouter()
class VideoRequest(BaseModel):
url: str
resolution: str = "best"
format: str = "mp4"
start_time: Optional[str] = None
end_time: Optional[str] = None
class AudioRequest(BaseModel):
url: str
quality: str = "192"
format: str = "mp3"
class SimpleRequest(BaseModel):
url: str
class SubtitleRequest(BaseModel):
url: str
lang: str = "en"
class ChannelRequest(BaseModel):
channel_url: str # youtube.com/@handle or youtube.com/channel/UCxxxxxx
async def _delete_later(path: str, delay: int = 3600):
await asyncio.sleep(delay)
try:
os.unlink(path)
except Exception:
pass
@router.post("/video")
async def video(req: VideoRequest, bg: BackgroundTasks):
url = clean_yt_url(req.url.strip())
try:
path, filename = await download_video(
url=url,
resolution=req.resolution,
fmt=req.format,
start_time=req.start_time,
end_time=req.end_time,
)
except Exception as e:
raise HTTPException(500, detail=str(e))
bg.add_task(_delete_later, path)
return FileResponse(path, filename=filename, media_type="video/mp4")
@router.post("/audio")
async def audio(req: AudioRequest, bg: BackgroundTasks):
url = clean_yt_url(req.url.strip())
try:
path, filename = await download_audio(url=url, quality=req.quality, fmt=req.format)
except Exception as e:
raise HTTPException(500, detail=str(e))
bg.add_task(_delete_later, path)
return FileResponse(path, filename=filename, media_type="audio/mpeg")
@router.post("/thumbnail")
async def thumbnail(req: SimpleRequest, bg: BackgroundTasks):
url = clean_yt_url(req.url.strip())
try:
path, filename = await download_thumbnail(url)
except Exception as e:
raise HTTPException(500, detail=str(e))
bg.add_task(_delete_later, path)
return FileResponse(path, filename=filename, media_type="image/jpeg")
@router.post("/subtitles")
async def subtitles(req: SubtitleRequest, bg: BackgroundTasks):
url = clean_yt_url(req.url.strip())
try:
path, filename = await download_subtitles(url, req.lang)
except Exception as e:
raise HTTPException(500, detail=str(e))
bg.add_task(_delete_later, path)
return FileResponse(path, filename=filename, media_type="text/plain")
@router.post("/channel-art")
async def channel_art(req: ChannelRequest, bg: BackgroundTasks):
"""
Downloads avatar (JPG) and banner (JPG) for a YouTube channel.
Returns JSON — frontend opens each asset URL to trigger download.
{
"assets": [
{"type": "avatar", "filename": "...", "url": "/files/..."},
{"type": "banner", "filename": "...", "url": "/files/..."}
]
}
"""
try:
assets = await download_channel_art(req.channel_url.strip())
except Exception as e:
raise HTTPException(500, detail=str(e))
for asset in assets:
bg.add_task(_delete_later, f"downloads/{asset['filename']}")
return {"assets": assets}