""" GraphQL API using strawberry, served via FastAPI. Primary API for MPR — all client interactions go through GraphQL. Uses core.db 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.schema.config import StrawberryConfig from strawberry.types import Info from core.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 core.db import list_assets return list_assets(status=status, search=search) @strawberry.field def asset(self, info: Info, id: UUID) -> Optional[MediaAssetType]: from core.db import get_asset try: return get_asset(id) except Exception: return None @strawberry.field def jobs( self, info: Info, status: Optional[str] = None, source_asset_id: Optional[UUID] = None, ) -> List[TranscodeJobType]: from core.db import list_jobs return list_jobs(status=status, source_asset_id=source_asset_id) @strawberry.field def job(self, info: Info, id: UUID) -> Optional[TranscodeJobType]: from core.db import get_job try: return get_job(id) except Exception: return None @strawberry.field def presets(self, info: Info) -> List[TranscodePresetType]: from core.db import list_presets return list_presets() @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 core.db import create_asset, get_asset_filenames objects = list_objects(BUCKET_IN, extensions=MEDIA_EXTS) existing = get_asset_filenames() registered = [] skipped = [] for obj in objects: if obj["filename"] in existing: skipped.append(obj["filename"]) continue try: create_asset( 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 core.db import create_job, get_asset, get_preset try: source = get_asset(input.source_asset_id) except Exception: raise Exception("Source asset not found") preset = None preset_snapshot = {} if input.preset_id: try: preset = get_preset(input.preset_id) preset_snapshot = { "name": preset.name, "container": preset.container, "video_codec": preset.video_codec, "audio_codec": preset.audio_codec, } except Exception: 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 = create_job( 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 core.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 core.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 core.db import get_job, update_job try: job = get_job(id) except Exception: raise Exception("Job not found") if job.status not in ("pending", "processing"): raise Exception(f"Cannot cancel job with status: {job.status}") return update_job(job, status="cancelled") @strawberry.mutation def retry_job(self, info: Info, id: UUID) -> TranscodeJobType: from core.db import get_job, update_job try: job = get_job(id) except Exception: raise Exception("Job not found") if job.status != "failed": raise Exception("Only failed jobs can be retried") return update_job(job, status="pending", progress=0, error_message=None) @strawberry.mutation def update_asset(self, info: Info, id: UUID, input: UpdateAssetInput) -> MediaAssetType: from core.db import get_asset, update_asset try: asset = get_asset(id) except Exception: raise Exception("Asset not found") fields = {} if input.comments is not None: fields["comments"] = input.comments if input.tags is not None: fields["tags"] = input.tags if fields: asset = update_asset(asset, **fields) return asset @strawberry.mutation def delete_asset(self, info: Info, id: UUID) -> DeleteResultType: from core.db import delete_asset, get_asset try: asset = get_asset(id) delete_asset(asset) return DeleteResultType(ok=True) except Exception: raise Exception("Asset not found") # --------------------------------------------------------------------------- # Schema # --------------------------------------------------------------------------- schema = strawberry.Schema( query=Query, mutation=Mutation, config=StrawberryConfig(auto_camel_case=False), )