287 lines
9.0 KiB
Python
287 lines
9.0 KiB
Python
"""
|
|
GraphQL API using strawberry, served via FastAPI.
|
|
|
|
Primary API for MPR — all client interactions go through GraphQL.
|
|
Uses Django ORM directly for data access.
|
|
Types are generated from schema/ via modelgen — see api/schema/graphql.py.
|
|
"""
|
|
|
|
import os
|
|
from typing import List, Optional
|
|
from uuid import UUID
|
|
|
|
import strawberry
|
|
from strawberry.types import Info
|
|
|
|
from api.schema.graphql import (
|
|
CreateJobInput,
|
|
DeleteResultType,
|
|
MediaAssetType,
|
|
ScanResultType,
|
|
SystemStatusType,
|
|
TranscodeJobType,
|
|
TranscodePresetType,
|
|
UpdateAssetInput,
|
|
)
|
|
from core.storage import BUCKET_IN, list_objects
|
|
|
|
VIDEO_EXTS = {".mp4", ".mkv", ".avi", ".mov", ".webm", ".flv", ".wmv", ".m4v"}
|
|
AUDIO_EXTS = {".mp3", ".wav", ".flac", ".aac", ".ogg", ".m4a"}
|
|
MEDIA_EXTS = VIDEO_EXTS | AUDIO_EXTS
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Queries
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@strawberry.type
|
|
class Query:
|
|
@strawberry.field
|
|
def assets(
|
|
self,
|
|
info: Info,
|
|
status: Optional[str] = None,
|
|
search: Optional[str] = None,
|
|
) -> List[MediaAssetType]:
|
|
from mpr.media_assets.models import MediaAsset
|
|
|
|
qs = MediaAsset.objects.all()
|
|
if status:
|
|
qs = qs.filter(status=status)
|
|
if search:
|
|
qs = qs.filter(filename__icontains=search)
|
|
return list(qs)
|
|
|
|
@strawberry.field
|
|
def asset(self, info: Info, id: UUID) -> Optional[MediaAssetType]:
|
|
from mpr.media_assets.models import MediaAsset
|
|
|
|
try:
|
|
return MediaAsset.objects.get(id=id)
|
|
except MediaAsset.DoesNotExist:
|
|
return None
|
|
|
|
@strawberry.field
|
|
def jobs(
|
|
self,
|
|
info: Info,
|
|
status: Optional[str] = None,
|
|
source_asset_id: Optional[UUID] = None,
|
|
) -> List[TranscodeJobType]:
|
|
from mpr.media_assets.models import TranscodeJob
|
|
|
|
qs = TranscodeJob.objects.all()
|
|
if status:
|
|
qs = qs.filter(status=status)
|
|
if source_asset_id:
|
|
qs = qs.filter(source_asset_id=source_asset_id)
|
|
return list(qs)
|
|
|
|
@strawberry.field
|
|
def job(self, info: Info, id: UUID) -> Optional[TranscodeJobType]:
|
|
from mpr.media_assets.models import TranscodeJob
|
|
|
|
try:
|
|
return TranscodeJob.objects.get(id=id)
|
|
except TranscodeJob.DoesNotExist:
|
|
return None
|
|
|
|
@strawberry.field
|
|
def presets(self, info: Info) -> List[TranscodePresetType]:
|
|
from mpr.media_assets.models import TranscodePreset
|
|
|
|
return list(TranscodePreset.objects.all())
|
|
|
|
@strawberry.field
|
|
def system_status(self, info: Info) -> SystemStatusType:
|
|
return SystemStatusType(status="ok", version="0.1.0")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mutations
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@strawberry.type
|
|
class Mutation:
|
|
@strawberry.mutation
|
|
def scan_media_folder(self, info: Info) -> ScanResultType:
|
|
from mpr.media_assets.models import MediaAsset
|
|
|
|
objects = list_objects(BUCKET_IN, extensions=MEDIA_EXTS)
|
|
existing = set(MediaAsset.objects.values_list("filename", flat=True))
|
|
|
|
registered = []
|
|
skipped = []
|
|
|
|
for obj in objects:
|
|
if obj["filename"] in existing:
|
|
skipped.append(obj["filename"])
|
|
continue
|
|
try:
|
|
MediaAsset.objects.create(
|
|
filename=obj["filename"],
|
|
file_path=obj["key"],
|
|
file_size=obj["size"],
|
|
)
|
|
registered.append(obj["filename"])
|
|
except Exception:
|
|
pass
|
|
|
|
return ScanResultType(
|
|
found=len(objects),
|
|
registered=len(registered),
|
|
skipped=len(skipped),
|
|
files=registered,
|
|
)
|
|
|
|
@strawberry.mutation
|
|
def create_job(self, info: Info, input: CreateJobInput) -> TranscodeJobType:
|
|
from pathlib import Path
|
|
|
|
from mpr.media_assets.models import MediaAsset, TranscodeJob, TranscodePreset
|
|
|
|
try:
|
|
source = MediaAsset.objects.get(id=input.source_asset_id)
|
|
except MediaAsset.DoesNotExist:
|
|
raise Exception("Source asset not found")
|
|
|
|
preset = None
|
|
preset_snapshot = {}
|
|
if input.preset_id:
|
|
try:
|
|
preset = TranscodePreset.objects.get(id=input.preset_id)
|
|
preset_snapshot = {
|
|
"name": preset.name,
|
|
"container": preset.container,
|
|
"video_codec": preset.video_codec,
|
|
"audio_codec": preset.audio_codec,
|
|
}
|
|
except TranscodePreset.DoesNotExist:
|
|
raise Exception("Preset not found")
|
|
|
|
if not preset and not input.trim_start and not input.trim_end:
|
|
raise Exception("Must specify preset_id or trim_start/trim_end")
|
|
|
|
output_filename = input.output_filename
|
|
if not output_filename:
|
|
stem = Path(source.filename).stem
|
|
ext = preset_snapshot.get("container", "mp4") if preset else "mp4"
|
|
output_filename = f"{stem}_output.{ext}"
|
|
|
|
job = TranscodeJob.objects.create(
|
|
source_asset_id=source.id,
|
|
preset_id=preset.id if preset else None,
|
|
preset_snapshot=preset_snapshot,
|
|
trim_start=input.trim_start,
|
|
trim_end=input.trim_end,
|
|
output_filename=output_filename,
|
|
output_path=output_filename,
|
|
priority=input.priority or 0,
|
|
)
|
|
|
|
executor_mode = os.environ.get("MPR_EXECUTOR", "local")
|
|
if executor_mode in ("lambda", "gcp"):
|
|
from task.executor import get_executor
|
|
|
|
get_executor().run(
|
|
job_id=str(job.id),
|
|
source_path=source.file_path,
|
|
output_path=output_filename,
|
|
preset=preset_snapshot or None,
|
|
trim_start=input.trim_start,
|
|
trim_end=input.trim_end,
|
|
duration=source.duration,
|
|
)
|
|
else:
|
|
from task.tasks import run_transcode_job
|
|
|
|
result = run_transcode_job.delay(
|
|
job_id=str(job.id),
|
|
source_key=source.file_path,
|
|
output_key=output_filename,
|
|
preset=preset_snapshot or None,
|
|
trim_start=input.trim_start,
|
|
trim_end=input.trim_end,
|
|
duration=source.duration,
|
|
)
|
|
job.celery_task_id = result.id
|
|
job.save(update_fields=["celery_task_id"])
|
|
|
|
return job
|
|
|
|
@strawberry.mutation
|
|
def cancel_job(self, info: Info, id: UUID) -> TranscodeJobType:
|
|
from mpr.media_assets.models import TranscodeJob
|
|
|
|
try:
|
|
job = TranscodeJob.objects.get(id=id)
|
|
except TranscodeJob.DoesNotExist:
|
|
raise Exception("Job not found")
|
|
|
|
if job.status not in ("pending", "processing"):
|
|
raise Exception(f"Cannot cancel job with status: {job.status}")
|
|
|
|
job.status = "cancelled"
|
|
job.save(update_fields=["status"])
|
|
return job
|
|
|
|
@strawberry.mutation
|
|
def retry_job(self, info: Info, id: UUID) -> TranscodeJobType:
|
|
from mpr.media_assets.models import TranscodeJob
|
|
|
|
try:
|
|
job = TranscodeJob.objects.get(id=id)
|
|
except TranscodeJob.DoesNotExist:
|
|
raise Exception("Job not found")
|
|
|
|
if job.status != "failed":
|
|
raise Exception("Only failed jobs can be retried")
|
|
|
|
job.status = "pending"
|
|
job.progress = 0
|
|
job.error_message = None
|
|
job.save(update_fields=["status", "progress", "error_message"])
|
|
return job
|
|
|
|
@strawberry.mutation
|
|
def update_asset(self, info: Info, id: UUID, input: UpdateAssetInput) -> MediaAssetType:
|
|
from mpr.media_assets.models import MediaAsset
|
|
|
|
try:
|
|
asset = MediaAsset.objects.get(id=id)
|
|
except MediaAsset.DoesNotExist:
|
|
raise Exception("Asset not found")
|
|
|
|
update_fields = []
|
|
if input.comments is not None:
|
|
asset.comments = input.comments
|
|
update_fields.append("comments")
|
|
if input.tags is not None:
|
|
asset.tags = input.tags
|
|
update_fields.append("tags")
|
|
|
|
if update_fields:
|
|
asset.save(update_fields=update_fields)
|
|
|
|
return asset
|
|
|
|
@strawberry.mutation
|
|
def delete_asset(self, info: Info, id: UUID) -> DeleteResultType:
|
|
from mpr.media_assets.models import MediaAsset
|
|
|
|
try:
|
|
asset = MediaAsset.objects.get(id=id)
|
|
asset.delete()
|
|
return DeleteResultType(ok=True)
|
|
except MediaAsset.DoesNotExist:
|
|
raise Exception("Asset not found")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Schema
|
|
# ---------------------------------------------------------------------------
|
|
|
|
schema = strawberry.Schema(query=Query, mutation=Mutation)
|