130 lines
3.5 KiB
Python
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}
|