219 lines
7.6 KiB
Python
219 lines
7.6 KiB
Python
"""
|
|
YouTube Publish Router
|
|
Handles video upload/publishing to YouTube via the Data API v3.
|
|
Uses stored OAuth credentials for authentication.
|
|
"""
|
|
|
|
from typing import Optional, List
|
|
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, Query
|
|
from pydantic import BaseModel, Field
|
|
from loguru import logger
|
|
|
|
from middleware.auth_middleware import get_current_user
|
|
from services.youtube.youtube_oauth_service import YouTubeOAuthService
|
|
from services.youtube.youtube_publish_service import YouTubePublishService
|
|
from .oauth_router import get_oauth_service
|
|
from .task_manager import task_manager
|
|
|
|
router = APIRouter(prefix="/youtube/publish", tags=["youtube-publish"])
|
|
|
|
|
|
class PublishRequest(BaseModel):
|
|
token_id: int = Field(..., description="YouTube OAuth token row ID (which channel to publish to)")
|
|
video_source: str = Field(..., description="URL or local file path to the video")
|
|
title: str = Field(..., min_length=1, max_length=100, description="Video title (max 100 chars)")
|
|
description: str = Field("", description="Video description")
|
|
tags: List[str] = Field(default_factory=list, description="Video tags")
|
|
privacy_status: str = Field("unlisted", pattern="^(public|private|unlisted)$", description="Privacy status")
|
|
category_id: str = Field("22", description="YouTube category ID (default: People & Blogs)")
|
|
made_for_kids: bool = Field(False, description="Whether content is made for children")
|
|
|
|
|
|
class PublishResponse(BaseModel):
|
|
success: bool
|
|
task_id: Optional[str] = None
|
|
video_id: Optional[str] = None
|
|
video_url: Optional[str] = None
|
|
error: Optional[str] = None
|
|
message: str = ""
|
|
|
|
|
|
def get_publish_service(
|
|
oauth_service: YouTubeOAuthService = Depends(get_oauth_service),
|
|
) -> YouTubePublishService:
|
|
return YouTubePublishService(oauth_service)
|
|
|
|
|
|
@router.post("", response_model=PublishResponse)
|
|
def start_publish(
|
|
request: PublishRequest,
|
|
background_tasks: BackgroundTasks,
|
|
user: dict = Depends(get_current_user),
|
|
publish_service: YouTubePublishService = Depends(get_publish_service),
|
|
):
|
|
"""Start publishing a video to YouTube as a background task."""
|
|
try:
|
|
user_id = user.get("id")
|
|
if not user_id:
|
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
|
|
# Verify token belongs to user
|
|
oauth_service = publish_service.oauth_service
|
|
status = oauth_service.get_connection_status(user_id)
|
|
tokens = [c for c in status.get("channels", []) if c["token_id"] == request.token_id and c["is_active"]]
|
|
if not tokens:
|
|
raise HTTPException(status_code=400, detail="Invalid or inactive token_id")
|
|
|
|
# Create background task
|
|
task_id = task_manager.create_task("youtube_publish")
|
|
logger.info(
|
|
f"YouTube publish: created task {task_id} for user {user_id}, "
|
|
f"title='{request.title[:50]}', channel={tokens[0].get('channel_name', 'unknown')}"
|
|
)
|
|
|
|
background_tasks.add_task(
|
|
_execute_publish_task,
|
|
task_id=task_id,
|
|
user_id=user_id,
|
|
token_id=request.token_id,
|
|
video_source=request.video_source,
|
|
title=request.title,
|
|
description=request.description,
|
|
tags=request.tags,
|
|
privacy_status=request.privacy_status,
|
|
category_id=request.category_id,
|
|
made_for_kids=request.made_for_kids,
|
|
publish_service=publish_service,
|
|
)
|
|
|
|
return PublishResponse(
|
|
success=True,
|
|
task_id=task_id,
|
|
message="Publishing to YouTube started. Poll task_id for progress.",
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"YouTube publish: error starting task: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/{task_id}", response_model=PublishResponse)
|
|
def get_publish_status(
|
|
task_id: str,
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Check the status of a YouTube publish task."""
|
|
try:
|
|
user_id = user.get("id")
|
|
if not user_id:
|
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
|
|
task_status = task_manager.get_task_status(task_id)
|
|
if not task_status:
|
|
return PublishResponse(
|
|
success=False,
|
|
error="Task not found",
|
|
message="Publish task not found (may have expired).",
|
|
)
|
|
|
|
status = task_status.get("status", "unknown")
|
|
result = task_status.get("result") or {}
|
|
error = task_status.get("error")
|
|
|
|
if status == "completed":
|
|
return PublishResponse(
|
|
success=True,
|
|
task_id=task_id,
|
|
video_id=result.get("video_id"),
|
|
video_url=result.get("video_url"),
|
|
message=task_status.get("message", "Published successfully"),
|
|
)
|
|
elif status == "failed":
|
|
return PublishResponse(
|
|
success=False,
|
|
task_id=task_id,
|
|
error=error or result.get("error", "Publish failed"),
|
|
message=task_status.get("message", "Publish failed"),
|
|
)
|
|
else:
|
|
return PublishResponse(
|
|
success=False,
|
|
task_id=task_id,
|
|
message=task_status.get("message", "Publishing in progress..."),
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"YouTube publish: status check error: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
def _execute_publish_task(
|
|
task_id: str,
|
|
user_id: str,
|
|
token_id: int,
|
|
video_source: str,
|
|
title: str,
|
|
description: str,
|
|
tags: List[str],
|
|
privacy_status: str,
|
|
category_id: str,
|
|
made_for_kids: bool,
|
|
publish_service: YouTubePublishService,
|
|
):
|
|
"""Background task to execute video publish."""
|
|
logger.info(f"YouTube publish: background task {task_id} starting for user {user_id}")
|
|
|
|
try:
|
|
task_manager.update_task_status(
|
|
task_id, "processing", progress=10.0, message="Preparing video for upload..."
|
|
)
|
|
|
|
result = publish_service.publish_video(
|
|
user_id=user_id,
|
|
token_id=token_id,
|
|
video_source=video_source,
|
|
title=title,
|
|
description=description,
|
|
tags=tags,
|
|
privacy_status=privacy_status,
|
|
category_id=category_id,
|
|
made_for_kids=made_for_kids,
|
|
)
|
|
|
|
if result.get("success"):
|
|
task_manager.update_task_status(
|
|
task_id,
|
|
"completed",
|
|
progress=100.0,
|
|
message=f"Published successfully: {result.get('video_url', '')}",
|
|
result=result,
|
|
)
|
|
logger.info(
|
|
f"YouTube publish: task {task_id} completed — "
|
|
f"video_id={result.get('video_id')}, url={result.get('video_url')}"
|
|
)
|
|
else:
|
|
error_msg = result.get("error", "Unknown publish error")
|
|
logger.error(f"YouTube publish: task {task_id} failed: {error_msg}")
|
|
task_manager.update_task_status(
|
|
task_id,
|
|
"failed",
|
|
error=error_msg,
|
|
message="Publish failed",
|
|
result=result,
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"YouTube publish: background task {task_id} error: {e}")
|
|
task_manager.update_task_status(
|
|
task_id,
|
|
"failed",
|
|
error=str(e),
|
|
message="Publish error",
|
|
result={"error": str(e)},
|
|
)
|