Compare commits
7 Commits
aws-int
...
4e9d731cff
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e9d731cff | |||
| dbbaad5b94 | |||
| 2ac31083e5 | |||
| f481fa6cbe | |||
| cc1a1b9953 | |||
| da1ff62877 | |||
| 9cead74fb3 |
54
api/deps.py
54
api/deps.py
@@ -1,54 +0,0 @@
|
|||||||
"""
|
|
||||||
FastAPI dependencies.
|
|
||||||
|
|
||||||
Provides database sessions, settings, and common dependencies.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from functools import lru_cache
|
|
||||||
from typing import Generator
|
|
||||||
|
|
||||||
import django
|
|
||||||
from django.conf import settings as django_settings
|
|
||||||
|
|
||||||
# Initialize Django
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mpr.settings")
|
|
||||||
django.setup()
|
|
||||||
|
|
||||||
from mpr.media_assets.models import MediaAsset, TranscodeJob, TranscodePreset
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
|
||||||
def get_settings():
|
|
||||||
"""Get Django settings."""
|
|
||||||
return django_settings
|
|
||||||
|
|
||||||
|
|
||||||
def get_asset(asset_id: str) -> MediaAsset:
|
|
||||||
"""Get asset by ID or raise 404."""
|
|
||||||
from fastapi import HTTPException
|
|
||||||
|
|
||||||
try:
|
|
||||||
return MediaAsset.objects.get(id=asset_id)
|
|
||||||
except MediaAsset.DoesNotExist:
|
|
||||||
raise HTTPException(status_code=404, detail="Asset not found")
|
|
||||||
|
|
||||||
|
|
||||||
def get_preset(preset_id: str) -> TranscodePreset:
|
|
||||||
"""Get preset by ID or raise 404."""
|
|
||||||
from fastapi import HTTPException
|
|
||||||
|
|
||||||
try:
|
|
||||||
return TranscodePreset.objects.get(id=preset_id)
|
|
||||||
except TranscodePreset.DoesNotExist:
|
|
||||||
raise HTTPException(status_code=404, detail="Preset not found")
|
|
||||||
|
|
||||||
|
|
||||||
def get_job(job_id: str) -> TranscodeJob:
|
|
||||||
"""Get job by ID or raise 404."""
|
|
||||||
from fastapi import HTTPException
|
|
||||||
|
|
||||||
try:
|
|
||||||
return TranscodeJob.objects.get(id=job_id)
|
|
||||||
except TranscodeJob.DoesNotExist:
|
|
||||||
raise HTTPException(status_code=404, detail="Job not found")
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
GraphQL API using graphene, mounted on FastAPI/Starlette.
|
GraphQL API using graphene, mounted on FastAPI/Starlette.
|
||||||
|
|
||||||
Provides the same data as the REST API but via GraphQL queries and mutations.
|
Primary API for MPR — all client interactions go through GraphQL.
|
||||||
Uses Django ORM directly for data access.
|
Uses Django ORM directly for data access.
|
||||||
Types are generated from schema/ via modelgen — see api/schema/graphql.py.
|
Types are generated from schema/ via modelgen — see api/schema/graphql.py.
|
||||||
"""
|
"""
|
||||||
@@ -12,11 +12,13 @@ import graphene
|
|||||||
|
|
||||||
from api.schema.graphql import (
|
from api.schema.graphql import (
|
||||||
CreateJobInput,
|
CreateJobInput,
|
||||||
|
DeleteResultType,
|
||||||
MediaAssetType,
|
MediaAssetType,
|
||||||
ScanResultType,
|
ScanResultType,
|
||||||
SystemStatusType,
|
SystemStatusType,
|
||||||
TranscodeJobType,
|
TranscodeJobType,
|
||||||
TranscodePresetType,
|
TranscodePresetType,
|
||||||
|
UpdateAssetInput,
|
||||||
)
|
)
|
||||||
from core.storage import BUCKET_IN, list_objects
|
from core.storage import BUCKET_IN, list_objects
|
||||||
|
|
||||||
@@ -238,10 +240,83 @@ class CancelJob(graphene.Mutation):
|
|||||||
return job
|
return job
|
||||||
|
|
||||||
|
|
||||||
|
class RetryJob(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
id = graphene.UUID(required=True)
|
||||||
|
|
||||||
|
Output = TranscodeJobType
|
||||||
|
|
||||||
|
def mutate(self, info, id):
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAsset(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
id = graphene.UUID(required=True)
|
||||||
|
input = UpdateAssetInput(required=True)
|
||||||
|
|
||||||
|
Output = MediaAssetType
|
||||||
|
|
||||||
|
def mutate(self, info, id, input):
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteAsset(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
id = graphene.UUID(required=True)
|
||||||
|
|
||||||
|
Output = DeleteResultType
|
||||||
|
|
||||||
|
def mutate(self, info, id):
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
class Mutation(graphene.ObjectType):
|
class Mutation(graphene.ObjectType):
|
||||||
scan_media_folder = ScanMediaFolder.Field()
|
scan_media_folder = ScanMediaFolder.Field()
|
||||||
create_job = CreateJob.Field()
|
create_job = CreateJob.Field()
|
||||||
cancel_job = CancelJob.Field()
|
cancel_job = CancelJob.Field()
|
||||||
|
retry_job = RetryJob.Field()
|
||||||
|
update_asset = UpdateAsset.Field()
|
||||||
|
delete_asset = DeleteAsset.Field()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
61
api/main.py
61
api/main.py
@@ -1,11 +1,13 @@
|
|||||||
"""
|
"""
|
||||||
MPR FastAPI Application
|
MPR FastAPI Application
|
||||||
|
|
||||||
Main entry point for the REST API.
|
Serves GraphQL API and Lambda callback endpoint.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
# Add project root to path
|
# Add project root to path
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
@@ -17,16 +19,17 @@ import django
|
|||||||
|
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, Header, HTTPException
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from starlette_graphene3 import GraphQLApp, make_graphiql_handler
|
||||||
|
|
||||||
from api.graphql import schema as graphql_schema
|
from api.graphql import schema as graphql_schema
|
||||||
from api.routes import assets_router, jobs_router, presets_router, system_router
|
|
||||||
from starlette_graphene3 import GraphQLApp, make_graphiql_handler
|
CALLBACK_API_KEY = os.environ.get("CALLBACK_API_KEY", "")
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="MPR API",
|
title="MPR API",
|
||||||
description="Media Processor REST API",
|
description="Media Processor — GraphQL API",
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc",
|
redoc_url="/redoc",
|
||||||
@@ -41,12 +44,6 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Routes - all under /api prefix
|
|
||||||
app.include_router(system_router, prefix="/api")
|
|
||||||
app.include_router(assets_router, prefix="/api")
|
|
||||||
app.include_router(presets_router, prefix="/api")
|
|
||||||
app.include_router(jobs_router, prefix="/api")
|
|
||||||
|
|
||||||
# GraphQL
|
# GraphQL
|
||||||
app.mount("/graphql", GraphQLApp(schema=graphql_schema, on_get=make_graphiql_handler()))
|
app.mount("/graphql", GraphQLApp(schema=graphql_schema, on_get=make_graphiql_handler()))
|
||||||
|
|
||||||
@@ -57,5 +54,45 @@ def root():
|
|||||||
return {
|
return {
|
||||||
"name": "MPR API",
|
"name": "MPR API",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"docs": "/docs",
|
"graphql": "/graphql",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/jobs/{job_id}/callback")
|
||||||
|
def job_callback(
|
||||||
|
job_id: UUID,
|
||||||
|
payload: dict,
|
||||||
|
x_api_key: Optional[str] = Header(None),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Callback endpoint for Lambda to report job completion.
|
||||||
|
Protected by API key.
|
||||||
|
"""
|
||||||
|
if CALLBACK_API_KEY and x_api_key != CALLBACK_API_KEY:
|
||||||
|
raise HTTPException(status_code=403, detail="Invalid API key")
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from mpr.media_assets.models import TranscodeJob
|
||||||
|
|
||||||
|
try:
|
||||||
|
job = TranscodeJob.objects.get(id=job_id)
|
||||||
|
except TranscodeJob.DoesNotExist:
|
||||||
|
raise HTTPException(status_code=404, detail="Job not found")
|
||||||
|
|
||||||
|
status = payload.get("status", "failed")
|
||||||
|
job.status = status
|
||||||
|
job.progress = 100.0 if status == "completed" else job.progress
|
||||||
|
update_fields = ["status", "progress"]
|
||||||
|
|
||||||
|
if payload.get("error"):
|
||||||
|
job.error_message = payload["error"]
|
||||||
|
update_fields.append("error_message")
|
||||||
|
|
||||||
|
if status in ("completed", "failed"):
|
||||||
|
job.completed_at = timezone.now()
|
||||||
|
update_fields.append("completed_at")
|
||||||
|
|
||||||
|
job.save(update_fields=update_fields)
|
||||||
|
|
||||||
|
return {"ok": True}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
"""API Routes."""
|
|
||||||
|
|
||||||
from .assets import router as assets_router
|
|
||||||
from .jobs import router as jobs_router
|
|
||||||
from .presets import router as presets_router
|
|
||||||
from .system import router as system_router
|
|
||||||
|
|
||||||
__all__ = ["assets_router", "jobs_router", "presets_router", "system_router"]
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
"""
|
|
||||||
Asset endpoints - media file registration and metadata.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
||||||
|
|
||||||
from api.deps import get_asset
|
|
||||||
from api.schema import AssetCreate, AssetResponse, AssetUpdate
|
|
||||||
from core.storage import BUCKET_IN, list_objects
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/assets", tags=["assets"])
|
|
||||||
|
|
||||||
# Supported media extensions
|
|
||||||
VIDEO_EXTS = {".mp4", ".mkv", ".avi", ".mov", ".webm", ".flv", ".wmv", ".m4v"}
|
|
||||||
AUDIO_EXTS = {".mp3", ".wav", ".flac", ".aac", ".ogg", ".m4a"}
|
|
||||||
MEDIA_EXTS = VIDEO_EXTS | AUDIO_EXTS
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=AssetResponse, status_code=201)
|
|
||||||
def create_asset(data: AssetCreate):
|
|
||||||
"""Register a media file as an asset."""
|
|
||||||
from mpr.media_assets.models import MediaAsset
|
|
||||||
|
|
||||||
asset = MediaAsset.objects.create(
|
|
||||||
filename=data.filename or data.file_path.split("/")[-1],
|
|
||||||
file_path=data.file_path,
|
|
||||||
file_size=data.file_size,
|
|
||||||
)
|
|
||||||
return asset
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list[AssetResponse])
|
|
||||||
def list_assets(
|
|
||||||
status: Optional[str] = Query(None, description="Filter by status"),
|
|
||||||
limit: int = Query(50, ge=1, le=100),
|
|
||||||
offset: int = Query(0, ge=0),
|
|
||||||
):
|
|
||||||
"""List assets with optional filtering."""
|
|
||||||
from mpr.media_assets.models import MediaAsset
|
|
||||||
|
|
||||||
qs = MediaAsset.objects.all()
|
|
||||||
if status:
|
|
||||||
qs = qs.filter(status=status)
|
|
||||||
return list(qs[offset : offset + limit])
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{asset_id}", response_model=AssetResponse)
|
|
||||||
def get_asset_detail(asset_id: UUID, asset=Depends(get_asset)):
|
|
||||||
"""Get asset details."""
|
|
||||||
return asset
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{asset_id}", response_model=AssetResponse)
|
|
||||||
def update_asset(asset_id: UUID, data: AssetUpdate, asset=Depends(get_asset)):
|
|
||||||
"""Update asset metadata (comments, tags)."""
|
|
||||||
update_fields = []
|
|
||||||
|
|
||||||
if data.comments is not None:
|
|
||||||
asset.comments = data.comments
|
|
||||||
update_fields.append("comments")
|
|
||||||
|
|
||||||
if data.tags is not None:
|
|
||||||
asset.tags = data.tags
|
|
||||||
update_fields.append("tags")
|
|
||||||
|
|
||||||
if update_fields:
|
|
||||||
asset.save(update_fields=update_fields)
|
|
||||||
|
|
||||||
return asset
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{asset_id}", status_code=204)
|
|
||||||
def delete_asset(asset_id: UUID, asset=Depends(get_asset)):
|
|
||||||
"""Delete an asset."""
|
|
||||||
asset.delete()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/scan", response_model=dict)
|
|
||||||
def scan_media_folder():
|
|
||||||
"""
|
|
||||||
Scan the S3 media-in bucket for new video/audio files and register them as assets.
|
|
||||||
"""
|
|
||||||
from mpr.media_assets.models import MediaAsset
|
|
||||||
|
|
||||||
# List objects from S3 bucket
|
|
||||||
objects = list_objects(BUCKET_IN, extensions=MEDIA_EXTS)
|
|
||||||
|
|
||||||
# Get existing filenames to avoid duplicates
|
|
||||||
existing_filenames = set(MediaAsset.objects.values_list("filename", flat=True))
|
|
||||||
|
|
||||||
registered_files = []
|
|
||||||
skipped_files = []
|
|
||||||
|
|
||||||
for obj in objects:
|
|
||||||
if obj["filename"] in existing_filenames:
|
|
||||||
skipped_files.append(obj["filename"])
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
MediaAsset.objects.create(
|
|
||||||
filename=obj["filename"],
|
|
||||||
file_path=obj["key"],
|
|
||||||
file_size=obj["size"],
|
|
||||||
)
|
|
||||||
registered_files.append(obj["filename"])
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error registering {obj['filename']}: {e}")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"found": len(objects),
|
|
||||||
"registered": len(registered_files),
|
|
||||||
"skipped": len(skipped_files),
|
|
||||||
"files": registered_files,
|
|
||||||
}
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
"""
|
|
||||||
Job endpoints - transcode/trim job management.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query
|
|
||||||
|
|
||||||
from api.deps import get_asset, get_job, get_preset
|
|
||||||
from api.schema import JobCreate, JobResponse
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/jobs", tags=["jobs"])
|
|
||||||
|
|
||||||
CALLBACK_API_KEY = os.environ.get("CALLBACK_API_KEY", "")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=JobResponse, status_code=201)
|
|
||||||
def create_job(data: JobCreate):
|
|
||||||
"""
|
|
||||||
Create a transcode or trim job.
|
|
||||||
|
|
||||||
- With preset_id: Full transcode using preset settings
|
|
||||||
- Without preset_id but with trim_start/end: Trim only (stream copy)
|
|
||||||
"""
|
|
||||||
from mpr.media_assets.models import MediaAsset, TranscodeJob, TranscodePreset
|
|
||||||
|
|
||||||
# Get source asset
|
|
||||||
try:
|
|
||||||
source = MediaAsset.objects.get(id=data.source_asset_id)
|
|
||||||
except MediaAsset.DoesNotExist:
|
|
||||||
raise HTTPException(status_code=404, detail="Source asset not found")
|
|
||||||
|
|
||||||
# Get preset if specified
|
|
||||||
preset = None
|
|
||||||
preset_snapshot = {}
|
|
||||||
if data.preset_id:
|
|
||||||
try:
|
|
||||||
preset = TranscodePreset.objects.get(id=data.preset_id)
|
|
||||||
preset_snapshot = {
|
|
||||||
"name": preset.name,
|
|
||||||
"container": preset.container,
|
|
||||||
"video_codec": preset.video_codec,
|
|
||||||
"video_bitrate": preset.video_bitrate,
|
|
||||||
"video_crf": preset.video_crf,
|
|
||||||
"video_preset": preset.video_preset,
|
|
||||||
"resolution": preset.resolution,
|
|
||||||
"framerate": preset.framerate,
|
|
||||||
"audio_codec": preset.audio_codec,
|
|
||||||
"audio_bitrate": preset.audio_bitrate,
|
|
||||||
"audio_channels": preset.audio_channels,
|
|
||||||
"audio_samplerate": preset.audio_samplerate,
|
|
||||||
"extra_args": preset.extra_args,
|
|
||||||
}
|
|
||||||
except TranscodePreset.DoesNotExist:
|
|
||||||
raise HTTPException(status_code=404, detail="Preset not found")
|
|
||||||
|
|
||||||
# Validate trim-only job
|
|
||||||
if not preset and not data.trim_start and not data.trim_end:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400, detail="Must specify preset_id or trim_start/trim_end"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generate output filename - stored as S3 key in output bucket
|
|
||||||
output_filename = data.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}"
|
|
||||||
|
|
||||||
# Create job
|
|
||||||
job = TranscodeJob.objects.create(
|
|
||||||
source_asset_id=source.id,
|
|
||||||
preset_id=preset.id if preset else None,
|
|
||||||
preset_snapshot=preset_snapshot,
|
|
||||||
trim_start=data.trim_start,
|
|
||||||
trim_end=data.trim_end,
|
|
||||||
output_filename=output_filename,
|
|
||||||
output_path=output_filename, # S3 key in output bucket
|
|
||||||
priority=data.priority or 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Dispatch based on executor mode
|
|
||||||
executor_mode = os.environ.get("MPR_EXECUTOR", "local")
|
|
||||||
|
|
||||||
if executor_mode == "lambda":
|
|
||||||
_dispatch_lambda(job, source, preset_snapshot)
|
|
||||||
else:
|
|
||||||
_dispatch_celery(job, source, preset_snapshot)
|
|
||||||
|
|
||||||
return job
|
|
||||||
|
|
||||||
|
|
||||||
def _dispatch_celery(job, source, preset_snapshot):
|
|
||||||
"""Dispatch job to Celery worker."""
|
|
||||||
from task.tasks import run_transcode_job
|
|
||||||
|
|
||||||
result = run_transcode_job.delay(
|
|
||||||
job_id=str(job.id),
|
|
||||||
source_key=source.file_path,
|
|
||||||
output_key=job.output_filename,
|
|
||||||
preset=preset_snapshot or None,
|
|
||||||
trim_start=job.trim_start,
|
|
||||||
trim_end=job.trim_end,
|
|
||||||
duration=source.duration,
|
|
||||||
)
|
|
||||||
job.celery_task_id = result.id
|
|
||||||
job.save(update_fields=["celery_task_id"])
|
|
||||||
|
|
||||||
|
|
||||||
def _dispatch_lambda(job, source, preset_snapshot):
|
|
||||||
"""Dispatch job to AWS Step Functions."""
|
|
||||||
from task.executor import get_executor
|
|
||||||
|
|
||||||
executor = get_executor()
|
|
||||||
executor.run(
|
|
||||||
job_id=str(job.id),
|
|
||||||
source_path=source.file_path,
|
|
||||||
output_path=job.output_filename,
|
|
||||||
preset=preset_snapshot or None,
|
|
||||||
trim_start=job.trim_start,
|
|
||||||
trim_end=job.trim_end,
|
|
||||||
duration=source.duration,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{job_id}/callback")
|
|
||||||
def job_callback(
|
|
||||||
job_id: UUID,
|
|
||||||
payload: dict,
|
|
||||||
x_api_key: Optional[str] = Header(None),
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Callback endpoint for Lambda to report job completion.
|
|
||||||
Protected by API key.
|
|
||||||
"""
|
|
||||||
if CALLBACK_API_KEY and x_api_key != CALLBACK_API_KEY:
|
|
||||||
raise HTTPException(status_code=403, detail="Invalid API key")
|
|
||||||
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from mpr.media_assets.models import TranscodeJob
|
|
||||||
|
|
||||||
try:
|
|
||||||
job = TranscodeJob.objects.get(id=job_id)
|
|
||||||
except TranscodeJob.DoesNotExist:
|
|
||||||
raise HTTPException(status_code=404, detail="Job not found")
|
|
||||||
|
|
||||||
status = payload.get("status", "failed")
|
|
||||||
job.status = status
|
|
||||||
job.progress = 100.0 if status == "completed" else job.progress
|
|
||||||
update_fields = ["status", "progress"]
|
|
||||||
|
|
||||||
if payload.get("error"):
|
|
||||||
job.error_message = payload["error"]
|
|
||||||
update_fields.append("error_message")
|
|
||||||
|
|
||||||
if status == "completed":
|
|
||||||
job.completed_at = timezone.now()
|
|
||||||
update_fields.append("completed_at")
|
|
||||||
elif status == "failed":
|
|
||||||
job.completed_at = timezone.now()
|
|
||||||
update_fields.append("completed_at")
|
|
||||||
|
|
||||||
job.save(update_fields=update_fields)
|
|
||||||
|
|
||||||
return {"ok": True}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list[JobResponse])
|
|
||||||
def list_jobs(
|
|
||||||
status: Optional[str] = Query(None, description="Filter by status"),
|
|
||||||
source_asset_id: Optional[UUID] = Query(None),
|
|
||||||
limit: int = Query(50, ge=1, le=100),
|
|
||||||
offset: int = Query(0, ge=0),
|
|
||||||
):
|
|
||||||
"""List jobs with optional filtering."""
|
|
||||||
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[offset : offset + limit])
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{job_id}", response_model=JobResponse)
|
|
||||||
def get_job_detail(job_id: UUID, job=Depends(get_job)):
|
|
||||||
"""Get job details including progress."""
|
|
||||||
return job
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{job_id}/progress")
|
|
||||||
def get_job_progress(job_id: UUID, job=Depends(get_job)):
|
|
||||||
"""Get real-time job progress."""
|
|
||||||
return {
|
|
||||||
"job_id": str(job.id),
|
|
||||||
"status": job.status,
|
|
||||||
"progress": job.progress,
|
|
||||||
"current_frame": job.current_frame,
|
|
||||||
"current_time": job.current_time,
|
|
||||||
"speed": job.speed,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{job_id}/cancel", response_model=JobResponse)
|
|
||||||
def cancel_job(job_id: UUID, job=Depends(get_job)):
|
|
||||||
"""Cancel a pending or processing job."""
|
|
||||||
if job.status not in ("pending", "processing"):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400, detail=f"Cannot cancel job with status: {job.status}"
|
|
||||||
)
|
|
||||||
|
|
||||||
job.status = "cancelled"
|
|
||||||
job.save(update_fields=["status"])
|
|
||||||
return job
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{job_id}/retry", response_model=JobResponse)
|
|
||||||
def retry_job(job_id: UUID, job=Depends(get_job)):
|
|
||||||
"""Retry a failed job."""
|
|
||||||
if job.status != "failed":
|
|
||||||
raise HTTPException(status_code=400, detail="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
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
"""
|
|
||||||
Preset endpoints - transcode configuration templates.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
|
||||||
|
|
||||||
from api.deps import get_preset
|
|
||||||
from api.schema import PresetCreate, PresetResponse, PresetUpdate
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/presets", tags=["presets"])
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=PresetResponse, status_code=201)
|
|
||||||
def create_preset(data: PresetCreate):
|
|
||||||
"""Create a custom preset."""
|
|
||||||
from mpr.media_assets.models import TranscodePreset
|
|
||||||
|
|
||||||
preset = TranscodePreset.objects.create(
|
|
||||||
name=data.name,
|
|
||||||
description=data.description or "",
|
|
||||||
container=data.container or "mp4",
|
|
||||||
video_codec=data.video_codec or "libx264",
|
|
||||||
video_bitrate=data.video_bitrate,
|
|
||||||
video_crf=data.video_crf,
|
|
||||||
video_preset=data.video_preset,
|
|
||||||
resolution=data.resolution,
|
|
||||||
framerate=data.framerate,
|
|
||||||
audio_codec=data.audio_codec or "aac",
|
|
||||||
audio_bitrate=data.audio_bitrate,
|
|
||||||
audio_channels=data.audio_channels,
|
|
||||||
audio_samplerate=data.audio_samplerate,
|
|
||||||
extra_args=data.extra_args or [],
|
|
||||||
is_builtin=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
return preset
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list[PresetResponse])
|
|
||||||
def list_presets(include_builtin: bool = True):
|
|
||||||
"""List all presets."""
|
|
||||||
from mpr.media_assets.models import TranscodePreset
|
|
||||||
|
|
||||||
qs = TranscodePreset.objects.all()
|
|
||||||
|
|
||||||
if not include_builtin:
|
|
||||||
qs = qs.filter(is_builtin=False)
|
|
||||||
|
|
||||||
return list(qs)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{preset_id}", response_model=PresetResponse)
|
|
||||||
def get_preset_detail(preset_id: UUID, preset=Depends(get_preset)):
|
|
||||||
"""Get preset details."""
|
|
||||||
return preset
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{preset_id}", response_model=PresetResponse)
|
|
||||||
def update_preset(preset_id: UUID, data: PresetUpdate, preset=Depends(get_preset)):
|
|
||||||
"""Update a custom preset. Builtin presets cannot be modified."""
|
|
||||||
if preset.is_builtin:
|
|
||||||
raise HTTPException(status_code=403, detail="Cannot modify builtin preset")
|
|
||||||
|
|
||||||
update_fields = []
|
|
||||||
for field in [
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
"container",
|
|
||||||
"video_codec",
|
|
||||||
"video_bitrate",
|
|
||||||
"video_crf",
|
|
||||||
"video_preset",
|
|
||||||
"resolution",
|
|
||||||
"framerate",
|
|
||||||
"audio_codec",
|
|
||||||
"audio_bitrate",
|
|
||||||
"audio_channels",
|
|
||||||
"audio_samplerate",
|
|
||||||
"extra_args",
|
|
||||||
]:
|
|
||||||
value = getattr(data, field, None)
|
|
||||||
if value is not None:
|
|
||||||
setattr(preset, field, value)
|
|
||||||
update_fields.append(field)
|
|
||||||
|
|
||||||
if update_fields:
|
|
||||||
preset.save(update_fields=update_fields)
|
|
||||||
|
|
||||||
return preset
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{preset_id}", status_code=204)
|
|
||||||
def delete_preset(preset_id: UUID, preset=Depends(get_preset)):
|
|
||||||
"""Delete a custom preset. Builtin presets cannot be deleted."""
|
|
||||||
if preset.is_builtin:
|
|
||||||
raise HTTPException(status_code=403, detail="Cannot delete builtin preset")
|
|
||||||
|
|
||||||
preset.delete()
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
"""
|
|
||||||
System endpoints - health checks and FFmpeg capabilities.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from fastapi import APIRouter
|
|
||||||
|
|
||||||
from core.ffmpeg import get_decoders, get_encoders, get_formats
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/system", tags=["system"])
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/health")
|
|
||||||
def health_check():
|
|
||||||
"""Health check endpoint."""
|
|
||||||
return {"status": "healthy"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/status")
|
|
||||||
def system_status():
|
|
||||||
"""System status for UI."""
|
|
||||||
return {"status": "ok", "version": "0.1.0"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/worker")
|
|
||||||
def worker_status():
|
|
||||||
"""Worker status from gRPC."""
|
|
||||||
try:
|
|
||||||
from rpc.client import get_client
|
|
||||||
|
|
||||||
client = get_client()
|
|
||||||
status = client.get_worker_status()
|
|
||||||
if status:
|
|
||||||
return status
|
|
||||||
return {"available": False, "error": "No response from worker"}
|
|
||||||
except Exception as e:
|
|
||||||
return {"available": False, "error": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/ffmpeg/codecs")
|
|
||||||
def ffmpeg_codecs():
|
|
||||||
"""Get available FFmpeg encoders and decoders."""
|
|
||||||
return {
|
|
||||||
"encoders": get_encoders(),
|
|
||||||
"decoders": get_decoders(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/ffmpeg/formats")
|
|
||||||
def ffmpeg_formats():
|
|
||||||
"""Get available FFmpeg muxers and demuxers."""
|
|
||||||
return get_formats()
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
"""API Schemas - GENERATED FILE"""
|
|
||||||
|
|
||||||
from .base import BaseSchema
|
|
||||||
from .asset import AssetCreate, AssetUpdate, AssetResponse
|
|
||||||
from .asset import AssetStatus
|
|
||||||
from .preset import PresetCreate, PresetUpdate, PresetResponse
|
|
||||||
from .job import JobCreate, JobUpdate, JobResponse
|
|
||||||
from .job import JobStatus
|
|
||||||
|
|
||||||
__all__ = ["BaseSchema", "AssetCreate", "AssetUpdate", "AssetResponse", "AssetStatus", "PresetCreate", "PresetUpdate", "PresetResponse", "JobCreate", "JobUpdate", "JobResponse", "JobStatus"]
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
"""MediaAsset Schemas - GENERATED FILE"""
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Any, Dict, List, Optional
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from .base import BaseSchema
|
|
||||||
|
|
||||||
|
|
||||||
class AssetStatus(str, Enum):
|
|
||||||
PENDING = "pending"
|
|
||||||
READY = "ready"
|
|
||||||
ERROR = "error"
|
|
||||||
|
|
||||||
|
|
||||||
class AssetCreate(BaseSchema):
|
|
||||||
"""AssetCreate schema."""
|
|
||||||
filename: str
|
|
||||||
file_path: str
|
|
||||||
file_size: Optional[int] = None
|
|
||||||
duration: Optional[float] = None
|
|
||||||
video_codec: Optional[str] = None
|
|
||||||
audio_codec: Optional[str] = None
|
|
||||||
width: Optional[int] = None
|
|
||||||
height: Optional[int] = None
|
|
||||||
framerate: Optional[float] = None
|
|
||||||
bitrate: Optional[int] = None
|
|
||||||
properties: Dict[str, Any]
|
|
||||||
comments: str = ""
|
|
||||||
tags: List[str] = Field(default_factory=list)
|
|
||||||
|
|
||||||
class AssetUpdate(BaseSchema):
|
|
||||||
"""AssetUpdate schema."""
|
|
||||||
filename: Optional[str] = None
|
|
||||||
file_path: Optional[str] = None
|
|
||||||
status: Optional[AssetStatus] = None
|
|
||||||
error_message: Optional[str] = None
|
|
||||||
file_size: Optional[int] = None
|
|
||||||
duration: Optional[float] = None
|
|
||||||
video_codec: Optional[str] = None
|
|
||||||
audio_codec: Optional[str] = None
|
|
||||||
width: Optional[int] = None
|
|
||||||
height: Optional[int] = None
|
|
||||||
framerate: Optional[float] = None
|
|
||||||
bitrate: Optional[int] = None
|
|
||||||
properties: Optional[Dict[str, Any]] = None
|
|
||||||
comments: Optional[str] = None
|
|
||||||
tags: Optional[List[str]] = None
|
|
||||||
|
|
||||||
class AssetResponse(BaseSchema):
|
|
||||||
"""AssetResponse schema."""
|
|
||||||
id: UUID
|
|
||||||
filename: str
|
|
||||||
file_path: str
|
|
||||||
status: AssetStatus = "AssetStatus.PENDING"
|
|
||||||
error_message: Optional[str] = None
|
|
||||||
file_size: Optional[int] = None
|
|
||||||
duration: Optional[float] = None
|
|
||||||
video_codec: Optional[str] = None
|
|
||||||
audio_codec: Optional[str] = None
|
|
||||||
width: Optional[int] = None
|
|
||||||
height: Optional[int] = None
|
|
||||||
framerate: Optional[float] = None
|
|
||||||
bitrate: Optional[int] = None
|
|
||||||
properties: Dict[str, Any]
|
|
||||||
comments: str = ""
|
|
||||||
tags: List[str] = Field(default_factory=list)
|
|
||||||
created_at: Optional[datetime] = None
|
|
||||||
updated_at: Optional[datetime] = None
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
"""Pydantic Base Schema - GENERATED FILE"""
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
|
||||||
|
|
||||||
|
|
||||||
class BaseSchema(BaseModel):
|
|
||||||
"""Base schema with ORM mode."""
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
|
||||||
@@ -104,6 +104,13 @@ class CreateJobInput(graphene.InputObjectType):
|
|||||||
priority = graphene.Int(default_value=0)
|
priority = graphene.Int(default_value=0)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAssetInput(graphene.InputObjectType):
|
||||||
|
"""Request body for updating asset metadata."""
|
||||||
|
|
||||||
|
comments = graphene.String()
|
||||||
|
tags = graphene.List(graphene.String)
|
||||||
|
|
||||||
|
|
||||||
class SystemStatusType(graphene.ObjectType):
|
class SystemStatusType(graphene.ObjectType):
|
||||||
"""System status response."""
|
"""System status response."""
|
||||||
|
|
||||||
@@ -120,6 +127,12 @@ class ScanResultType(graphene.ObjectType):
|
|||||||
files = graphene.List(graphene.String)
|
files = graphene.List(graphene.String)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteResultType(graphene.ObjectType):
|
||||||
|
"""Result of a delete operation."""
|
||||||
|
|
||||||
|
ok = graphene.Boolean()
|
||||||
|
|
||||||
|
|
||||||
class WorkerStatusType(graphene.ObjectType):
|
class WorkerStatusType(graphene.ObjectType):
|
||||||
"""Worker health and capabilities."""
|
"""Worker health and capabilities."""
|
||||||
|
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
"""TranscodeJob Schemas - GENERATED FILE"""
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Any, Dict, List, Optional
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from .base import BaseSchema
|
|
||||||
|
|
||||||
|
|
||||||
class JobStatus(str, Enum):
|
|
||||||
PENDING = "pending"
|
|
||||||
PROCESSING = "processing"
|
|
||||||
COMPLETED = "completed"
|
|
||||||
FAILED = "failed"
|
|
||||||
CANCELLED = "cancelled"
|
|
||||||
|
|
||||||
|
|
||||||
class JobCreate(BaseSchema):
|
|
||||||
"""JobCreate schema."""
|
|
||||||
source_asset_id: UUID
|
|
||||||
preset_id: Optional[UUID] = None
|
|
||||||
preset_snapshot: Dict[str, Any]
|
|
||||||
trim_start: Optional[float] = None
|
|
||||||
trim_end: Optional[float] = None
|
|
||||||
output_filename: str = ""
|
|
||||||
output_path: Optional[str] = None
|
|
||||||
output_asset_id: Optional[UUID] = None
|
|
||||||
progress: float = 0.0
|
|
||||||
current_frame: Optional[int] = None
|
|
||||||
current_time: Optional[float] = None
|
|
||||||
speed: Optional[str] = None
|
|
||||||
celery_task_id: Optional[str] = None
|
|
||||||
execution_arn: Optional[str] = None
|
|
||||||
priority: int = 0
|
|
||||||
started_at: Optional[datetime] = None
|
|
||||||
completed_at: Optional[datetime] = None
|
|
||||||
|
|
||||||
class JobUpdate(BaseSchema):
|
|
||||||
"""JobUpdate schema."""
|
|
||||||
source_asset_id: Optional[UUID] = None
|
|
||||||
preset_id: Optional[UUID] = None
|
|
||||||
preset_snapshot: Optional[Dict[str, Any]] = None
|
|
||||||
trim_start: Optional[float] = None
|
|
||||||
trim_end: Optional[float] = None
|
|
||||||
output_filename: Optional[str] = None
|
|
||||||
output_path: Optional[str] = None
|
|
||||||
output_asset_id: Optional[UUID] = None
|
|
||||||
status: Optional[JobStatus] = None
|
|
||||||
progress: Optional[float] = None
|
|
||||||
current_frame: Optional[int] = None
|
|
||||||
current_time: Optional[float] = None
|
|
||||||
speed: Optional[str] = None
|
|
||||||
error_message: Optional[str] = None
|
|
||||||
celery_task_id: Optional[str] = None
|
|
||||||
execution_arn: Optional[str] = None
|
|
||||||
priority: Optional[int] = None
|
|
||||||
started_at: Optional[datetime] = None
|
|
||||||
completed_at: Optional[datetime] = None
|
|
||||||
|
|
||||||
class JobResponse(BaseSchema):
|
|
||||||
"""JobResponse schema."""
|
|
||||||
id: UUID
|
|
||||||
source_asset_id: UUID
|
|
||||||
preset_id: Optional[UUID] = None
|
|
||||||
preset_snapshot: Dict[str, Any]
|
|
||||||
trim_start: Optional[float] = None
|
|
||||||
trim_end: Optional[float] = None
|
|
||||||
output_filename: str = ""
|
|
||||||
output_path: Optional[str] = None
|
|
||||||
output_asset_id: Optional[UUID] = None
|
|
||||||
status: JobStatus = "JobStatus.PENDING"
|
|
||||||
progress: float = 0.0
|
|
||||||
current_frame: Optional[int] = None
|
|
||||||
current_time: Optional[float] = None
|
|
||||||
speed: Optional[str] = None
|
|
||||||
error_message: Optional[str] = None
|
|
||||||
celery_task_id: Optional[str] = None
|
|
||||||
execution_arn: Optional[str] = None
|
|
||||||
priority: int = 0
|
|
||||||
created_at: Optional[datetime] = None
|
|
||||||
started_at: Optional[datetime] = None
|
|
||||||
completed_at: Optional[datetime] = None
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
"""TranscodePreset Schemas - GENERATED FILE"""
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Any, Dict, List, Optional
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from .base import BaseSchema
|
|
||||||
|
|
||||||
|
|
||||||
class PresetCreate(BaseSchema):
|
|
||||||
"""PresetCreate schema."""
|
|
||||||
name: str
|
|
||||||
description: str = ""
|
|
||||||
is_builtin: bool = False
|
|
||||||
container: str = "mp4"
|
|
||||||
video_codec: str = "libx264"
|
|
||||||
video_bitrate: Optional[str] = None
|
|
||||||
video_crf: Optional[int] = None
|
|
||||||
video_preset: Optional[str] = None
|
|
||||||
resolution: Optional[str] = None
|
|
||||||
framerate: Optional[float] = None
|
|
||||||
audio_codec: str = "aac"
|
|
||||||
audio_bitrate: Optional[str] = None
|
|
||||||
audio_channels: Optional[int] = None
|
|
||||||
audio_samplerate: Optional[int] = None
|
|
||||||
extra_args: List[str] = Field(default_factory=list)
|
|
||||||
|
|
||||||
class PresetUpdate(BaseSchema):
|
|
||||||
"""PresetUpdate schema."""
|
|
||||||
name: Optional[str] = None
|
|
||||||
description: Optional[str] = None
|
|
||||||
is_builtin: Optional[bool] = None
|
|
||||||
container: Optional[str] = None
|
|
||||||
video_codec: Optional[str] = None
|
|
||||||
video_bitrate: Optional[str] = None
|
|
||||||
video_crf: Optional[int] = None
|
|
||||||
video_preset: Optional[str] = None
|
|
||||||
resolution: Optional[str] = None
|
|
||||||
framerate: Optional[float] = None
|
|
||||||
audio_codec: Optional[str] = None
|
|
||||||
audio_bitrate: Optional[str] = None
|
|
||||||
audio_channels: Optional[int] = None
|
|
||||||
audio_samplerate: Optional[int] = None
|
|
||||||
extra_args: Optional[List[str]] = None
|
|
||||||
|
|
||||||
class PresetResponse(BaseSchema):
|
|
||||||
"""PresetResponse schema."""
|
|
||||||
id: UUID
|
|
||||||
name: str
|
|
||||||
description: str = ""
|
|
||||||
is_builtin: bool = False
|
|
||||||
container: str = "mp4"
|
|
||||||
video_codec: str = "libx264"
|
|
||||||
video_bitrate: Optional[str] = None
|
|
||||||
video_crf: Optional[int] = None
|
|
||||||
video_preset: Optional[str] = None
|
|
||||||
resolution: Optional[str] = None
|
|
||||||
framerate: Optional[float] = None
|
|
||||||
audio_codec: str = "aac"
|
|
||||||
audio_bitrate: Optional[str] = None
|
|
||||||
audio_channels: Optional[int] = None
|
|
||||||
audio_samplerate: Optional[int] = None
|
|
||||||
extra_args: List[str] = Field(default_factory=list)
|
|
||||||
created_at: Optional[datetime] = None
|
|
||||||
updated_at: Optional[datetime] = None
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
digraph system_overview {
|
|
||||||
rankdir=TB
|
|
||||||
node [shape=box, style=rounded, fontname="Helvetica"]
|
|
||||||
edge [fontname="Helvetica", fontsize=10]
|
|
||||||
|
|
||||||
labelloc="t"
|
|
||||||
label="MPR - System Overview"
|
|
||||||
fontsize=16
|
|
||||||
fontname="Helvetica-Bold"
|
|
||||||
|
|
||||||
graph [splines=ortho, nodesep=0.8, ranksep=0.8]
|
|
||||||
|
|
||||||
// External
|
|
||||||
subgraph cluster_external {
|
|
||||||
label="External"
|
|
||||||
style=dashed
|
|
||||||
color=gray
|
|
||||||
|
|
||||||
browser [label="Browser\nmpr.local.ar / mpr.mcrn.ar", shape=ellipse]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nginx reverse proxy
|
|
||||||
subgraph cluster_proxy {
|
|
||||||
label="Reverse Proxy"
|
|
||||||
style=filled
|
|
||||||
fillcolor="#e8f4f8"
|
|
||||||
|
|
||||||
nginx [label="nginx\nport 80"]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Application layer
|
|
||||||
subgraph cluster_apps {
|
|
||||||
label="Application Layer"
|
|
||||||
style=filled
|
|
||||||
fillcolor="#f0f8e8"
|
|
||||||
|
|
||||||
django [label="Django\n/admin\nport 8701"]
|
|
||||||
fastapi [label="FastAPI\n/api + /graphql\nport 8702"]
|
|
||||||
timeline [label="Timeline UI\n/ui\nport 5173"]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Worker layer
|
|
||||||
subgraph cluster_workers {
|
|
||||||
label="Worker Layer"
|
|
||||||
style=filled
|
|
||||||
fillcolor="#fff8e8"
|
|
||||||
|
|
||||||
grpc_server [label="gRPC Server\nport 50051"]
|
|
||||||
celery [label="Celery Worker\n(local mode)"]
|
|
||||||
}
|
|
||||||
|
|
||||||
// AWS layer
|
|
||||||
subgraph cluster_aws {
|
|
||||||
label="AWS (lambda mode)"
|
|
||||||
style=filled
|
|
||||||
fillcolor="#fde8d0"
|
|
||||||
|
|
||||||
step_functions [label="Step Functions\nstate machine"]
|
|
||||||
lambda [label="Lambda\nFFmpeg container"]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data layer
|
|
||||||
subgraph cluster_data {
|
|
||||||
label="Data Layer"
|
|
||||||
style=filled
|
|
||||||
fillcolor="#f8e8f0"
|
|
||||||
|
|
||||||
postgres [label="PostgreSQL\nport 5436", shape=cylinder]
|
|
||||||
redis [label="Redis\nport 6381", shape=cylinder]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Storage
|
|
||||||
subgraph cluster_storage {
|
|
||||||
label="S3 Storage"
|
|
||||||
style=filled
|
|
||||||
fillcolor="#f0f0f0"
|
|
||||||
|
|
||||||
minio [label="MinIO (local)\nport 9000", shape=folder]
|
|
||||||
s3 [label="AWS S3 (cloud)", shape=folder, style="dashed,rounded"]
|
|
||||||
bucket_in [label="mpr-media-in", shape=note]
|
|
||||||
bucket_out [label="mpr-media-out", shape=note]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connections
|
|
||||||
browser -> nginx
|
|
||||||
|
|
||||||
nginx -> django [xlabel="/admin"]
|
|
||||||
nginx -> fastapi [xlabel="/api, /graphql"]
|
|
||||||
nginx -> timeline [xlabel="/ui"]
|
|
||||||
nginx -> minio [xlabel="/media/*"]
|
|
||||||
|
|
||||||
timeline -> fastapi [xlabel="REST API"]
|
|
||||||
|
|
||||||
fastapi -> postgres
|
|
||||||
fastapi -> grpc_server [xlabel="gRPC\nprogress"]
|
|
||||||
|
|
||||||
// Local mode
|
|
||||||
grpc_server -> celery [xlabel="task dispatch"]
|
|
||||||
celery -> redis [xlabel="queue"]
|
|
||||||
celery -> postgres [xlabel="job updates"]
|
|
||||||
celery -> minio [xlabel="S3 API\ndownload/upload"]
|
|
||||||
|
|
||||||
// Lambda mode
|
|
||||||
fastapi -> step_functions [xlabel="boto3\nstart_execution", style=dashed]
|
|
||||||
step_functions -> lambda [style=dashed]
|
|
||||||
lambda -> s3 [xlabel="download/upload", style=dashed]
|
|
||||||
lambda -> fastapi [xlabel="callback\nPOST /jobs/{id}/callback", style=dashed]
|
|
||||||
|
|
||||||
// Storage details
|
|
||||||
minio -> bucket_in [style=dotted, arrowhead=none]
|
|
||||||
minio -> bucket_out [style=dotted, arrowhead=none]
|
|
||||||
s3 -> bucket_in [style=dotted, arrowhead=none]
|
|
||||||
s3 -> bucket_out [style=dotted, arrowhead=none]
|
|
||||||
}
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
|
||||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<!-- Generated by graphviz version 14.1.2 (0)
|
|
||||||
-->
|
|
||||||
<!-- Title: system_overview Pages: 1 -->
|
|
||||||
<svg width="620pt" height="903pt"
|
|
||||||
viewBox="0.00 0.00 620.00 903.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 898.54)">
|
|
||||||
<title>system_overview</title>
|
|
||||||
<polygon fill="white" stroke="none" points="-4,4 -4,-898.54 616,-898.54 616,4 -4,4"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="306" y="-875.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">MPR - System Overview</text>
|
|
||||||
<g id="clust1" class="cluster">
|
|
||||||
<title>cluster_external</title>
|
|
||||||
<polygon fill="none" stroke="gray" stroke-dasharray="5,2" points="246,-755.44 246,-859.04 540,-859.04 540,-755.44 246,-755.44"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="393" y="-839.84" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">External</text>
|
|
||||||
</g>
|
|
||||||
<g id="clust2" class="cluster">
|
|
||||||
<title>cluster_proxy</title>
|
|
||||||
<polygon fill="#e8f4f8" stroke="black" points="320,-654.94 320,-740.94 466,-740.94 466,-654.94 320,-654.94"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="393" y="-721.74" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Reverse Proxy</text>
|
|
||||||
</g>
|
|
||||||
<g id="clust3" class="cluster">
|
|
||||||
<title>cluster_apps</title>
|
|
||||||
<polygon fill="#f0f8e8" stroke="black" points="278,-419.44 278,-640.44 532,-640.44 532,-419.44 278,-419.44"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="405" y="-621.24" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Application Layer</text>
|
|
||||||
</g>
|
|
||||||
<g id="clust4" class="cluster">
|
|
||||||
<title>cluster_workers</title>
|
|
||||||
<polygon fill="#fff8e8" stroke="black" points="142,-218.44 142,-404.94 280,-404.94 280,-218.44 142,-218.44"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="211" y="-385.74" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Worker Layer</text>
|
|
||||||
</g>
|
|
||||||
<g id="clust5" class="cluster">
|
|
||||||
<title>cluster_aws</title>
|
|
||||||
<polygon fill="#fde8d0" stroke="black" points="383,-218.44 383,-404.94 581,-404.94 581,-218.44 383,-218.44"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="482" y="-385.74" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AWS (lambda mode)</text>
|
|
||||||
</g>
|
|
||||||
<g id="clust6" class="cluster">
|
|
||||||
<title>cluster_data</title>
|
|
||||||
<polygon fill="#f8e8f0" stroke="black" points="8,-102 8,-203.94 263,-203.94 263,-102 8,-102"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="135.5" y="-184.74" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Data Layer</text>
|
|
||||||
</g>
|
|
||||||
<g id="clust7" class="cluster">
|
|
||||||
<title>cluster_storage</title>
|
|
||||||
<polygon fill="#f0f0f0" stroke="black" points="302,-8 302,-195.97 604,-195.97 604,-8 302,-8"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="453" y="-176.77" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">S3 Storage</text>
|
|
||||||
</g>
|
|
||||||
<!-- browser -->
|
|
||||||
<g id="node1" class="node">
|
|
||||||
<title>browser</title>
|
|
||||||
<ellipse fill="none" stroke="black" cx="393" cy="-793.49" rx="139.12" ry="30.05"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="393" y="-797.44" font-family="Helvetica,sans-Serif" font-size="14.00">Browser</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="393" y="-780.19" font-family="Helvetica,sans-Serif" font-size="14.00">mpr.local.ar / mpr.mcrn.ar</text>
|
|
||||||
</g>
|
|
||||||
<!-- nginx -->
|
|
||||||
<g id="node2" class="node">
|
|
||||||
<title>nginx</title>
|
|
||||||
<path fill="none" stroke="black" d="M414.5,-705.44C414.5,-705.44 371.5,-705.44 371.5,-705.44 365.5,-705.44 359.5,-699.44 359.5,-693.44 359.5,-693.44 359.5,-674.94 359.5,-674.94 359.5,-668.94 365.5,-662.94 371.5,-662.94 371.5,-662.94 414.5,-662.94 414.5,-662.94 420.5,-662.94 426.5,-668.94 426.5,-674.94 426.5,-674.94 426.5,-693.44 426.5,-693.44 426.5,-699.44 420.5,-705.44 414.5,-705.44"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="393" y="-688.14" font-family="Helvetica,sans-Serif" font-size="14.00">nginx</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="393" y="-670.89" font-family="Helvetica,sans-Serif" font-size="14.00">port 80</text>
|
|
||||||
</g>
|
|
||||||
<!-- browser->nginx -->
|
|
||||||
<g id="edge1" class="edge">
|
|
||||||
<title>browser->nginx</title>
|
|
||||||
<path fill="none" stroke="black" d="M393,-763.04C393,-763.04 393,-717.33 393,-717.33"/>
|
|
||||||
<polygon fill="black" stroke="black" points="396.5,-717.33 393,-707.33 389.5,-717.33 396.5,-717.33"/>
|
|
||||||
</g>
|
|
||||||
<!-- django -->
|
|
||||||
<g id="node3" class="node">
|
|
||||||
<title>django</title>
|
|
||||||
<path fill="none" stroke="black" d="M359.5,-604.94C359.5,-604.94 298.5,-604.94 298.5,-604.94 292.5,-604.94 286.5,-598.94 286.5,-592.94 286.5,-592.94 286.5,-557.19 286.5,-557.19 286.5,-551.19 292.5,-545.19 298.5,-545.19 298.5,-545.19 359.5,-545.19 359.5,-545.19 365.5,-545.19 371.5,-551.19 371.5,-557.19 371.5,-557.19 371.5,-592.94 371.5,-592.94 371.5,-598.94 365.5,-604.94 359.5,-604.94"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="329" y="-587.64" font-family="Helvetica,sans-Serif" font-size="14.00">Django</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="329" y="-570.39" font-family="Helvetica,sans-Serif" font-size="14.00">/admin</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="329" y="-553.14" font-family="Helvetica,sans-Serif" font-size="14.00">port 8701</text>
|
|
||||||
</g>
|
|
||||||
<!-- nginx->django -->
|
|
||||||
<g id="edge2" class="edge">
|
|
||||||
<title>nginx->django</title>
|
|
||||||
<path fill="none" stroke="black" d="M365.5,-662.63C365.5,-662.63 365.5,-616.77 365.5,-616.77"/>
|
|
||||||
<polygon fill="black" stroke="black" points="369,-616.77 365.5,-606.77 362,-616.77 369,-616.77"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="348.62" y="-642.95" font-family="Helvetica,sans-Serif" font-size="10.00">/admin</text>
|
|
||||||
</g>
|
|
||||||
<!-- fastapi -->
|
|
||||||
<g id="node4" class="node">
|
|
||||||
<title>fastapi</title>
|
|
||||||
<path fill="none" stroke="black" d="M395.5,-487.19C395.5,-487.19 298.5,-487.19 298.5,-487.19 292.5,-487.19 286.5,-481.19 286.5,-475.19 286.5,-475.19 286.5,-439.44 286.5,-439.44 286.5,-433.44 292.5,-427.44 298.5,-427.44 298.5,-427.44 395.5,-427.44 395.5,-427.44 401.5,-427.44 407.5,-433.44 407.5,-439.44 407.5,-439.44 407.5,-475.19 407.5,-475.19 407.5,-481.19 401.5,-487.19 395.5,-487.19"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="347" y="-469.89" font-family="Helvetica,sans-Serif" font-size="14.00">FastAPI</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="347" y="-452.64" font-family="Helvetica,sans-Serif" font-size="14.00">/api + /graphql</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="347" y="-435.39" font-family="Helvetica,sans-Serif" font-size="14.00">port 8702</text>
|
|
||||||
</g>
|
|
||||||
<!-- nginx->fastapi -->
|
|
||||||
<g id="edge3" class="edge">
|
|
||||||
<title>nginx->fastapi</title>
|
|
||||||
<path fill="none" stroke="black" d="M383.5,-662.84C383.5,-662.84 383.5,-498.82 383.5,-498.82"/>
|
|
||||||
<polygon fill="black" stroke="black" points="387,-498.82 383.5,-488.82 380,-498.82 387,-498.82"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="399.44" y="-571.33" font-family="Helvetica,sans-Serif" font-size="10.00">/api, /graphql</text>
|
|
||||||
</g>
|
|
||||||
<!-- timeline -->
|
|
||||||
<g id="node5" class="node">
|
|
||||||
<title>timeline</title>
|
|
||||||
<path fill="none" stroke="black" d="M512,-604.94C512,-604.94 442,-604.94 442,-604.94 436,-604.94 430,-598.94 430,-592.94 430,-592.94 430,-557.19 430,-557.19 430,-551.19 436,-545.19 442,-545.19 442,-545.19 512,-545.19 512,-545.19 518,-545.19 524,-551.19 524,-557.19 524,-557.19 524,-592.94 524,-592.94 524,-598.94 518,-604.94 512,-604.94"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="477" y="-587.64" font-family="Helvetica,sans-Serif" font-size="14.00">Timeline UI</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="477" y="-570.39" font-family="Helvetica,sans-Serif" font-size="14.00">/ui</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="477" y="-553.14" font-family="Helvetica,sans-Serif" font-size="14.00">port 5173</text>
|
|
||||||
</g>
|
|
||||||
<!-- nginx->timeline -->
|
|
||||||
<g id="edge4" class="edge">
|
|
||||||
<title>nginx->timeline</title>
|
|
||||||
<path fill="none" stroke="black" d="M422.62,-662.67C422.62,-633.49 422.62,-585 422.62,-585 422.62,-585 423.34,-585 423.34,-585"/>
|
|
||||||
<polygon fill="black" stroke="black" points="418.22,-588.5 428.22,-585 418.22,-581.5 418.22,-588.5"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="416.62" y="-613.98" font-family="Helvetica,sans-Serif" font-size="10.00">/ui</text>
|
|
||||||
</g>
|
|
||||||
<!-- minio -->
|
|
||||||
<g id="node12" class="node">
|
|
||||||
<title>minio</title>
|
|
||||||
<polygon fill="none" stroke="black" points="415.5,-160.47 412.5,-164.47 391.5,-164.47 388.5,-160.47 312.5,-160.47 312.5,-117.97 415.5,-117.97 415.5,-160.47"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="364" y="-143.17" font-family="Helvetica,sans-Serif" font-size="14.00">MinIO (local)</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="364" y="-125.92" font-family="Helvetica,sans-Serif" font-size="14.00">port 9000</text>
|
|
||||||
</g>
|
|
||||||
<!-- nginx->minio -->
|
|
||||||
<g id="edge5" class="edge">
|
|
||||||
<title>nginx->minio</title>
|
|
||||||
<path fill="none" stroke="black" d="M414.88,-662.68C414.88,-596.12 414.88,-398 414.88,-398 414.88,-398 344.17,-398 344.17,-398 344.17,-398 344.17,-172.35 344.17,-172.35"/>
|
|
||||||
<polygon fill="black" stroke="black" points="347.67,-172.35 344.17,-162.35 340.67,-172.35 347.67,-172.35"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="378.03" y="-401.25" font-family="Helvetica,sans-Serif" font-size="10.00">/media/*</text>
|
|
||||||
</g>
|
|
||||||
<!-- grpc_server -->
|
|
||||||
<g id="node6" class="node">
|
|
||||||
<title>grpc_server</title>
|
|
||||||
<path fill="none" stroke="black" d="M246.5,-369.44C246.5,-369.44 167.5,-369.44 167.5,-369.44 161.5,-369.44 155.5,-363.44 155.5,-357.44 155.5,-357.44 155.5,-338.94 155.5,-338.94 155.5,-332.94 161.5,-326.94 167.5,-326.94 167.5,-326.94 246.5,-326.94 246.5,-326.94 252.5,-326.94 258.5,-332.94 258.5,-338.94 258.5,-338.94 258.5,-357.44 258.5,-357.44 258.5,-363.44 252.5,-369.44 246.5,-369.44"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="207" y="-352.14" font-family="Helvetica,sans-Serif" font-size="14.00">gRPC Server</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="207" y="-334.89" font-family="Helvetica,sans-Serif" font-size="14.00">port 50051</text>
|
|
||||||
</g>
|
|
||||||
<!-- fastapi->grpc_server -->
|
|
||||||
<g id="edge8" class="edge">
|
|
||||||
<title>fastapi->grpc_server</title>
|
|
||||||
<path fill="none" stroke="black" d="M298.5,-427.06C298.5,-392.59 298.5,-341 298.5,-341 298.5,-341 270.41,-341 270.41,-341"/>
|
|
||||||
<polygon fill="black" stroke="black" points="270.41,-337.5 260.41,-341 270.41,-344.5 270.41,-337.5"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="319.5" y="-385.98" font-family="Helvetica,sans-Serif" font-size="10.00">gRPC</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="319.5" y="-373.23" font-family="Helvetica,sans-Serif" font-size="10.00">progress</text>
|
|
||||||
</g>
|
|
||||||
<!-- step_functions -->
|
|
||||||
<g id="node8" class="node">
|
|
||||||
<title>step_functions</title>
|
|
||||||
<path fill="none" stroke="black" d="M541.38,-369.44C541.38,-369.44 446.62,-369.44 446.62,-369.44 440.62,-369.44 434.62,-363.44 434.62,-357.44 434.62,-357.44 434.62,-338.94 434.62,-338.94 434.62,-332.94 440.62,-326.94 446.62,-326.94 446.62,-326.94 541.38,-326.94 541.38,-326.94 547.38,-326.94 553.38,-332.94 553.38,-338.94 553.38,-338.94 553.38,-357.44 553.38,-357.44 553.38,-363.44 547.38,-369.44 541.38,-369.44"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="494" y="-352.14" font-family="Helvetica,sans-Serif" font-size="14.00">Step Functions</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="494" y="-334.89" font-family="Helvetica,sans-Serif" font-size="14.00">state machine</text>
|
|
||||||
</g>
|
|
||||||
<!-- fastapi->step_functions -->
|
|
||||||
<g id="edge13" class="edge">
|
|
||||||
<title>fastapi->step_functions</title>
|
|
||||||
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M375.83,-427.17C375.83,-396.99 375.83,-355 375.83,-355 375.83,-355 422.71,-355 422.71,-355"/>
|
|
||||||
<polygon fill="black" stroke="black" points="422.71,-358.5 432.71,-355 422.71,-351.5 422.71,-358.5"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="338.33" y="-358.15" font-family="Helvetica,sans-Serif" font-size="10.00">boto3</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="338.33" y="-345.4" font-family="Helvetica,sans-Serif" font-size="10.00">start_execution</text>
|
|
||||||
</g>
|
|
||||||
<!-- postgres -->
|
|
||||||
<g id="node10" class="node">
|
|
||||||
<title>postgres</title>
|
|
||||||
<path fill="none" stroke="black" d="M111.75,-163.12C111.75,-166.06 90.35,-168.44 64,-168.44 37.65,-168.44 16.25,-166.06 16.25,-163.12 16.25,-163.12 16.25,-115.31 16.25,-115.31 16.25,-112.38 37.65,-110 64,-110 90.35,-110 111.75,-112.38 111.75,-115.31 111.75,-115.31 111.75,-163.12 111.75,-163.12"/>
|
|
||||||
<path fill="none" stroke="black" d="M111.75,-163.12C111.75,-160.19 90.35,-157.81 64,-157.81 37.65,-157.81 16.25,-160.19 16.25,-163.12"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="64" y="-143.17" font-family="Helvetica,sans-Serif" font-size="14.00">PostgreSQL</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="64" y="-125.92" font-family="Helvetica,sans-Serif" font-size="14.00">port 5436</text>
|
|
||||||
</g>
|
|
||||||
<!-- fastapi->postgres -->
|
|
||||||
<g id="edge7" class="edge">
|
|
||||||
<title>fastapi->postgres</title>
|
|
||||||
<path fill="none" stroke="black" d="M286.29,-457C203.13,-457 64,-457 64,-457 64,-457 64,-180.34 64,-180.34"/>
|
|
||||||
<polygon fill="black" stroke="black" points="67.5,-180.34 64,-170.34 60.5,-180.34 67.5,-180.34"/>
|
|
||||||
</g>
|
|
||||||
<!-- timeline->fastapi -->
|
|
||||||
<g id="edge6" class="edge">
|
|
||||||
<title>timeline->fastapi</title>
|
|
||||||
<path fill="none" stroke="black" d="M429.59,-565C411.66,-565 395.5,-565 395.5,-565 395.5,-565 395.5,-499.11 395.5,-499.11"/>
|
|
||||||
<polygon fill="black" stroke="black" points="399,-499.11 395.5,-489.11 392,-499.11 399,-499.11"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="406.38" y="-539.6" font-family="Helvetica,sans-Serif" font-size="10.00">REST API</text>
|
|
||||||
</g>
|
|
||||||
<!-- celery -->
|
|
||||||
<g id="node7" class="node">
|
|
||||||
<title>celery</title>
|
|
||||||
<path fill="none" stroke="black" d="M255.75,-268.94C255.75,-268.94 166.25,-268.94 166.25,-268.94 160.25,-268.94 154.25,-262.94 154.25,-256.94 154.25,-256.94 154.25,-238.44 154.25,-238.44 154.25,-232.44 160.25,-226.44 166.25,-226.44 166.25,-226.44 255.75,-226.44 255.75,-226.44 261.75,-226.44 267.75,-232.44 267.75,-238.44 267.75,-238.44 267.75,-256.94 267.75,-256.94 267.75,-262.94 261.75,-268.94 255.75,-268.94"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="211" y="-251.64" font-family="Helvetica,sans-Serif" font-size="14.00">Celery Worker</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="211" y="-234.39" font-family="Helvetica,sans-Serif" font-size="14.00">(local mode)</text>
|
|
||||||
</g>
|
|
||||||
<!-- grpc_server->celery -->
|
|
||||||
<g id="edge9" class="edge">
|
|
||||||
<title>grpc_server->celery</title>
|
|
||||||
<path fill="none" stroke="black" d="M207,-326.87C207,-326.87 207,-280.83 207,-280.83"/>
|
|
||||||
<polygon fill="black" stroke="black" points="210.5,-280.83 207,-270.83 203.5,-280.83 210.5,-280.83"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="174.38" y="-307.1" font-family="Helvetica,sans-Serif" font-size="10.00">task dispatch</text>
|
|
||||||
</g>
|
|
||||||
<!-- celery->postgres -->
|
|
||||||
<g id="edge11" class="edge">
|
|
||||||
<title>celery->postgres</title>
|
|
||||||
<path fill="none" stroke="black" d="M161.88,-225.95C161.88,-194.24 161.88,-139 161.88,-139 161.88,-139 123.59,-139 123.59,-139"/>
|
|
||||||
<polygon fill="black" stroke="black" points="123.59,-135.5 113.59,-139 123.59,-142.5 123.59,-135.5"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="133.38" y="-166.59" font-family="Helvetica,sans-Serif" font-size="10.00">job updates</text>
|
|
||||||
</g>
|
|
||||||
<!-- redis -->
|
|
||||||
<g id="node11" class="node">
|
|
||||||
<title>redis</title>
|
|
||||||
<path fill="none" stroke="black" d="M254.5,-163.12C254.5,-166.06 235.45,-168.44 212,-168.44 188.55,-168.44 169.5,-166.06 169.5,-163.12 169.5,-163.12 169.5,-115.31 169.5,-115.31 169.5,-112.38 188.55,-110 212,-110 235.45,-110 254.5,-112.38 254.5,-115.31 254.5,-115.31 254.5,-163.12 254.5,-163.12"/>
|
|
||||||
<path fill="none" stroke="black" d="M254.5,-163.12C254.5,-160.19 235.45,-157.81 212,-157.81 188.55,-157.81 169.5,-160.19 169.5,-163.12"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="212" y="-143.17" font-family="Helvetica,sans-Serif" font-size="14.00">Redis</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="212" y="-125.92" font-family="Helvetica,sans-Serif" font-size="14.00">port 6381</text>
|
|
||||||
</g>
|
|
||||||
<!-- celery->redis -->
|
|
||||||
<g id="edge10" class="edge">
|
|
||||||
<title>celery->redis</title>
|
|
||||||
<path fill="none" stroke="black" d="M212,-226C212,-226 212,-180.19 212,-180.19"/>
|
|
||||||
<polygon fill="black" stroke="black" points="215.5,-180.19 212,-170.19 208.5,-180.19 215.5,-180.19"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="197" y="-206.34" font-family="Helvetica,sans-Serif" font-size="10.00">queue</text>
|
|
||||||
</g>
|
|
||||||
<!-- celery->minio -->
|
|
||||||
<g id="edge12" class="edge">
|
|
||||||
<title>celery->minio</title>
|
|
||||||
<path fill="none" stroke="black" d="M261.12,-225.95C261.12,-194.24 261.12,-139 261.12,-139 261.12,-139 300.75,-139 300.75,-139"/>
|
|
||||||
<polygon fill="black" stroke="black" points="300.75,-142.5 310.75,-139 300.75,-135.5 300.75,-142.5"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="302.75" y="-178.67" font-family="Helvetica,sans-Serif" font-size="10.00">S3 API</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="302.75" y="-165.92" font-family="Helvetica,sans-Serif" font-size="10.00">download/upload</text>
|
|
||||||
</g>
|
|
||||||
<!-- lambda -->
|
|
||||||
<g id="node9" class="node">
|
|
||||||
<title>lambda</title>
|
|
||||||
<path fill="none" stroke="black" d="M541,-268.94C541,-268.94 423,-268.94 423,-268.94 417,-268.94 411,-262.94 411,-256.94 411,-256.94 411,-238.44 411,-238.44 411,-232.44 417,-226.44 423,-226.44 423,-226.44 541,-226.44 541,-226.44 547,-226.44 553,-232.44 553,-238.44 553,-238.44 553,-256.94 553,-256.94 553,-262.94 547,-268.94 541,-268.94"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="482" y="-251.64" font-family="Helvetica,sans-Serif" font-size="14.00">Lambda</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="482" y="-234.39" font-family="Helvetica,sans-Serif" font-size="14.00">FFmpeg container</text>
|
|
||||||
</g>
|
|
||||||
<!-- step_functions->lambda -->
|
|
||||||
<g id="edge14" class="edge">
|
|
||||||
<title>step_functions->lambda</title>
|
|
||||||
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M493.81,-326.87C493.81,-326.87 493.81,-280.83 493.81,-280.83"/>
|
|
||||||
<polygon fill="black" stroke="black" points="497.31,-280.83 493.81,-270.83 490.31,-280.83 497.31,-280.83"/>
|
|
||||||
</g>
|
|
||||||
<!-- lambda->fastapi -->
|
|
||||||
<g id="edge16" class="edge">
|
|
||||||
<title>lambda->fastapi</title>
|
|
||||||
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M418.75,-269.3C418.75,-322.78 418.75,-457 418.75,-457 418.75,-457 417.66,-457 417.66,-457"/>
|
|
||||||
<polygon fill="black" stroke="black" points="419.37,-453.5 409.37,-457 419.37,-460.5 419.37,-453.5"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="359.12" y="-379.69" font-family="Helvetica,sans-Serif" font-size="10.00">callback</text>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="359.12" y="-366.94" font-family="Helvetica,sans-Serif" font-size="10.00">POST /jobs/{id}/callback</text>
|
|
||||||
</g>
|
|
||||||
<!-- s3 -->
|
|
||||||
<g id="node13" class="node">
|
|
||||||
<title>s3</title>
|
|
||||||
<polygon fill="none" stroke="black" stroke-dasharray="5,2" points="596.25,-157.22 593.25,-161.22 572.25,-161.22 569.25,-157.22 473.75,-157.22 473.75,-121.22 596.25,-121.22 596.25,-157.22"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="535" y="-134.54" font-family="Helvetica,sans-Serif" font-size="14.00">AWS S3 (cloud)</text>
|
|
||||||
</g>
|
|
||||||
<!-- lambda->s3 -->
|
|
||||||
<g id="edge15" class="edge">
|
|
||||||
<title>lambda->s3</title>
|
|
||||||
<path fill="none" stroke="black" stroke-dasharray="5,2" d="M513.38,-226C513.38,-226 513.38,-169.14 513.38,-169.14"/>
|
|
||||||
<polygon fill="black" stroke="black" points="516.88,-169.14 513.38,-159.14 509.88,-169.14 516.88,-169.14"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="471.75" y="-200.82" font-family="Helvetica,sans-Serif" font-size="10.00">download/upload</text>
|
|
||||||
</g>
|
|
||||||
<!-- bucket_in -->
|
|
||||||
<g id="node14" class="node">
|
|
||||||
<title>bucket_in</title>
|
|
||||||
<polygon fill="none" stroke="black" points="413.5,-52 310.5,-52 310.5,-16 419.5,-16 419.5,-46 413.5,-52"/>
|
|
||||||
<polyline fill="none" stroke="black" points="413.5,-52 413.5,-46"/>
|
|
||||||
<polyline fill="none" stroke="black" points="419.5,-46 413.5,-46"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="365" y="-29.32" font-family="Helvetica,sans-Serif" font-size="14.00">mpr-media-in</text>
|
|
||||||
</g>
|
|
||||||
<!-- minio->bucket_in -->
|
|
||||||
<g id="edge17" class="edge">
|
|
||||||
<title>minio->bucket_in</title>
|
|
||||||
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M364,-117.67C364,-98.43 364,-70.56 364,-52.36"/>
|
|
||||||
</g>
|
|
||||||
<!-- bucket_out -->
|
|
||||||
<g id="node15" class="node">
|
|
||||||
<title>bucket_out</title>
|
|
||||||
<polygon fill="none" stroke="black" points="590.38,-52 477.62,-52 477.62,-16 596.38,-16 596.38,-46 590.38,-52"/>
|
|
||||||
<polyline fill="none" stroke="black" points="590.38,-52 590.38,-46"/>
|
|
||||||
<polyline fill="none" stroke="black" points="596.38,-46 590.38,-46"/>
|
|
||||||
<text xml:space="preserve" text-anchor="middle" x="537" y="-29.32" font-family="Helvetica,sans-Serif" font-size="14.00">mpr-media-out</text>
|
|
||||||
</g>
|
|
||||||
<!-- minio->bucket_out -->
|
|
||||||
<g id="edge18" class="edge">
|
|
||||||
<title>minio->bucket_out</title>
|
|
||||||
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M415.9,-145C428.08,-145 437.58,-145 437.58,-145 437.58,-145 437.58,-40 437.58,-40 437.58,-40 456.11,-40 477.16,-40"/>
|
|
||||||
</g>
|
|
||||||
<!-- s3->bucket_in -->
|
|
||||||
<g id="edge19" class="edge">
|
|
||||||
<title>s3->bucket_in</title>
|
|
||||||
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M473.27,-133C463.03,-133 455.67,-133 455.67,-133 455.67,-133 455.67,-28 455.67,-28 455.67,-28 438.93,-28 419.83,-28"/>
|
|
||||||
</g>
|
|
||||||
<!-- s3->bucket_out -->
|
|
||||||
<g id="edge20" class="edge">
|
|
||||||
<title>s3->bucket_out</title>
|
|
||||||
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M536.94,-120.89C536.94,-101.7 536.94,-71.72 536.94,-52.47"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 21 KiB |
94
docs/architecture/01a-local-architecture.dot
Normal file
94
docs/architecture/01a-local-architecture.dot
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
digraph local_architecture {
|
||||||
|
rankdir=TB
|
||||||
|
node [shape=box, style=rounded, fontname="Helvetica"]
|
||||||
|
edge [fontname="Helvetica", fontsize=10]
|
||||||
|
|
||||||
|
labelloc="t"
|
||||||
|
label="MPR - Local Architecture (Celery + MinIO)"
|
||||||
|
fontsize=16
|
||||||
|
fontname="Helvetica-Bold"
|
||||||
|
|
||||||
|
graph [splines=ortho, nodesep=0.8, ranksep=0.8]
|
||||||
|
|
||||||
|
// External
|
||||||
|
subgraph cluster_external {
|
||||||
|
label="External"
|
||||||
|
style=dashed
|
||||||
|
color=gray
|
||||||
|
|
||||||
|
browser [label="Browser\nmpr.local.ar", shape=ellipse]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nginx reverse proxy
|
||||||
|
subgraph cluster_proxy {
|
||||||
|
label="Reverse Proxy"
|
||||||
|
style=filled
|
||||||
|
fillcolor="#e8f4f8"
|
||||||
|
|
||||||
|
nginx [label="nginx\nport 80"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application layer
|
||||||
|
subgraph cluster_apps {
|
||||||
|
label="Application Layer"
|
||||||
|
style=filled
|
||||||
|
fillcolor="#f0f8e8"
|
||||||
|
|
||||||
|
django [label="Django Admin\n/admin\nport 8701"]
|
||||||
|
fastapi [label="GraphQL API\n/graphql\nport 8702"]
|
||||||
|
timeline [label="Timeline UI\n/\nport 5173"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Worker layer
|
||||||
|
subgraph cluster_workers {
|
||||||
|
label="Worker Layer"
|
||||||
|
style=filled
|
||||||
|
fillcolor="#fff8e8"
|
||||||
|
|
||||||
|
grpc_server [label="gRPC Server\nport 50051"]
|
||||||
|
celery [label="Celery Worker\nFFmpeg transcoding"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data layer
|
||||||
|
subgraph cluster_data {
|
||||||
|
label="Data Layer"
|
||||||
|
style=filled
|
||||||
|
fillcolor="#f8e8f0"
|
||||||
|
|
||||||
|
postgres [label="PostgreSQL\nport 5436", shape=cylinder]
|
||||||
|
redis [label="Redis\nCelery queue\nport 6381", shape=cylinder]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage
|
||||||
|
subgraph cluster_storage {
|
||||||
|
label="S3 Storage (MinIO)"
|
||||||
|
style=filled
|
||||||
|
fillcolor="#f0f0f0"
|
||||||
|
|
||||||
|
minio [label="MinIO\nS3-compatible API\nport 9000", shape=folder]
|
||||||
|
bucket_in [label="mpr-media-in/\ninput videos", shape=note]
|
||||||
|
bucket_out [label="mpr-media-out/\ntranscoded output", shape=note]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connections
|
||||||
|
browser -> nginx [label="HTTP"]
|
||||||
|
|
||||||
|
nginx -> django [xlabel="/admin"]
|
||||||
|
nginx -> fastapi [xlabel="/graphql"]
|
||||||
|
nginx -> timeline [xlabel="/"]
|
||||||
|
nginx -> minio [xlabel="/media/*"]
|
||||||
|
|
||||||
|
timeline -> fastapi [label="GraphQL"]
|
||||||
|
django -> postgres
|
||||||
|
|
||||||
|
fastapi -> postgres [label="read/write jobs"]
|
||||||
|
fastapi -> grpc_server [label="gRPC\nprogress updates"]
|
||||||
|
|
||||||
|
grpc_server -> celery [label="dispatch tasks"]
|
||||||
|
celery -> redis [label="task queue"]
|
||||||
|
celery -> postgres [label="update job status"]
|
||||||
|
celery -> minio [label="S3 API\ndownload input\nupload output"]
|
||||||
|
|
||||||
|
minio -> bucket_in [style=dotted, arrowhead=none]
|
||||||
|
minio -> bucket_out [style=dotted, arrowhead=none]
|
||||||
|
}
|
||||||
242
docs/architecture/01a-local-architecture.svg
Normal file
242
docs/architecture/01a-local-architecture.svg
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Generated by graphviz version 14.1.2 (0)
|
||||||
|
-->
|
||||||
|
<!-- Title: local_architecture Pages: 1 -->
|
||||||
|
<svg width="667pt" height="1095pt"
|
||||||
|
viewBox="0.00 0.00 667.00 1095.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1090.76)">
|
||||||
|
<title>local_architecture</title>
|
||||||
|
<polygon fill="white" stroke="none" points="-4,4 -4,-1090.76 663,-1090.76 663,4 -4,4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="329.5" y="-1067.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">MPR - Local Architecture (Celery + MinIO)</text>
|
||||||
|
<g id="clust1" class="cluster">
|
||||||
|
<title>cluster_external</title>
|
||||||
|
<polygon fill="none" stroke="gray" stroke-dasharray="5,2" points="270,-947.66 270,-1051.26 424,-1051.26 424,-947.66 270,-947.66"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="347" y="-1032.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">External</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust2" class="cluster">
|
||||||
|
<title>cluster_proxy</title>
|
||||||
|
<polygon fill="#e8f4f8" stroke="black" points="274,-819.91 274,-905.91 420,-905.91 420,-819.91 274,-819.91"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="347" y="-886.71" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Reverse Proxy</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust3" class="cluster">
|
||||||
|
<title>cluster_apps</title>
|
||||||
|
<polygon fill="#f0f8e8" stroke="black" points="19,-556.16 19,-789.91 301,-789.91 301,-556.16 19,-556.16"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="160" y="-770.71" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Application Layer</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust4" class="cluster">
|
||||||
|
<title>cluster_workers</title>
|
||||||
|
<polygon fill="#fff8e8" stroke="black" points="193,-302.41 193,-501.66 369,-501.66 369,-302.41 193,-302.41"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="281" y="-482.46" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Worker Layer</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust5" class="cluster">
|
||||||
|
<title>cluster_data</title>
|
||||||
|
<polygon fill="#f8e8f0" stroke="black" points="8,-109.5 8,-235.16 286,-235.16 286,-109.5 8,-109.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="147" y="-215.96" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Data Layer</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust6" class="cluster">
|
||||||
|
<title>cluster_storage</title>
|
||||||
|
<polygon fill="#f0f0f0" stroke="black" points="319,-8 319,-223.95 651,-223.95 651,-8 319,-8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="485" y="-204.75" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">S3 Storage (MinIO)</text>
|
||||||
|
</g>
|
||||||
|
<!-- browser -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>browser</title>
|
||||||
|
<ellipse fill="none" stroke="black" cx="347" cy="-985.71" rx="69.12" ry="30.05"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="347" y="-989.66" font-family="Helvetica,sans-Serif" font-size="14.00">Browser</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="347" y="-972.41" font-family="Helvetica,sans-Serif" font-size="14.00">mpr.local.ar</text>
|
||||||
|
</g>
|
||||||
|
<!-- nginx -->
|
||||||
|
<g id="node2" class="node">
|
||||||
|
<title>nginx</title>
|
||||||
|
<path fill="none" stroke="black" d="M368.5,-870.41C368.5,-870.41 325.5,-870.41 325.5,-870.41 319.5,-870.41 313.5,-864.41 313.5,-858.41 313.5,-858.41 313.5,-839.91 313.5,-839.91 313.5,-833.91 319.5,-827.91 325.5,-827.91 325.5,-827.91 368.5,-827.91 368.5,-827.91 374.5,-827.91 380.5,-833.91 380.5,-839.91 380.5,-839.91 380.5,-858.41 380.5,-858.41 380.5,-864.41 374.5,-870.41 368.5,-870.41"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="347" y="-853.11" font-family="Helvetica,sans-Serif" font-size="14.00">nginx</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="347" y="-835.86" font-family="Helvetica,sans-Serif" font-size="14.00">port 80</text>
|
||||||
|
</g>
|
||||||
|
<!-- browser->nginx -->
|
||||||
|
<g id="edge1" class="edge">
|
||||||
|
<title>browser->nginx</title>
|
||||||
|
<path fill="none" stroke="black" d="M347,-955.4C347,-955.4 347,-882.41 347,-882.41"/>
|
||||||
|
<polygon fill="black" stroke="black" points="350.5,-882.41 347,-872.41 343.5,-882.41 350.5,-882.41"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="359.75" y="-917.16" font-family="Helvetica,sans-Serif" font-size="10.00">HTTP</text>
|
||||||
|
</g>
|
||||||
|
<!-- django -->
|
||||||
|
<g id="node3" class="node">
|
||||||
|
<title>django</title>
|
||||||
|
<path fill="none" stroke="black" d="M128.75,-754.41C128.75,-754.41 39.25,-754.41 39.25,-754.41 33.25,-754.41 27.25,-748.41 27.25,-742.41 27.25,-742.41 27.25,-706.66 27.25,-706.66 27.25,-700.66 33.25,-694.66 39.25,-694.66 39.25,-694.66 128.75,-694.66 128.75,-694.66 134.75,-694.66 140.75,-700.66 140.75,-706.66 140.75,-706.66 140.75,-742.41 140.75,-742.41 140.75,-748.41 134.75,-754.41 128.75,-754.41"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="84" y="-737.11" font-family="Helvetica,sans-Serif" font-size="14.00">Django Admin</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="84" y="-719.86" font-family="Helvetica,sans-Serif" font-size="14.00">/admin</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="84" y="-702.61" font-family="Helvetica,sans-Serif" font-size="14.00">port 8701</text>
|
||||||
|
</g>
|
||||||
|
<!-- nginx->django -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<title>nginx->django</title>
|
||||||
|
<path fill="none" stroke="black" d="M313.16,-856C242.12,-856 84,-856 84,-856 84,-856 84,-766.21 84,-766.21"/>
|
||||||
|
<polygon fill="black" stroke="black" points="87.5,-766.21 84,-756.21 80.5,-766.21 87.5,-766.21"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="136.81" y="-859.25" font-family="Helvetica,sans-Serif" font-size="10.00">/admin</text>
|
||||||
|
</g>
|
||||||
|
<!-- fastapi -->
|
||||||
|
<g id="node4" class="node">
|
||||||
|
<title>fastapi</title>
|
||||||
|
<path fill="none" stroke="black" d="M281.25,-623.91C281.25,-623.91 200.75,-623.91 200.75,-623.91 194.75,-623.91 188.75,-617.91 188.75,-611.91 188.75,-611.91 188.75,-576.16 188.75,-576.16 188.75,-570.16 194.75,-564.16 200.75,-564.16 200.75,-564.16 281.25,-564.16 281.25,-564.16 287.25,-564.16 293.25,-570.16 293.25,-576.16 293.25,-576.16 293.25,-611.91 293.25,-611.91 293.25,-617.91 287.25,-623.91 281.25,-623.91"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="241" y="-606.61" font-family="Helvetica,sans-Serif" font-size="14.00">GraphQL API</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="241" y="-589.36" font-family="Helvetica,sans-Serif" font-size="14.00">/graphql</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="241" y="-572.11" font-family="Helvetica,sans-Serif" font-size="14.00">port 8702</text>
|
||||||
|
</g>
|
||||||
|
<!-- nginx->fastapi -->
|
||||||
|
<g id="edge3" class="edge">
|
||||||
|
<title>nginx->fastapi</title>
|
||||||
|
<path fill="none" stroke="black" d="M337.06,-827.84C337.06,-766.52 337.06,-594 337.06,-594 337.06,-594 305.04,-594 305.04,-594"/>
|
||||||
|
<polygon fill="black" stroke="black" points="305.04,-590.5 295.04,-594 305.04,-597.5 305.04,-590.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="317.19" y="-698.16" font-family="Helvetica,sans-Serif" font-size="10.00">/graphql</text>
|
||||||
|
</g>
|
||||||
|
<!-- timeline -->
|
||||||
|
<g id="node5" class="node">
|
||||||
|
<title>timeline</title>
|
||||||
|
<path fill="none" stroke="black" d="M281,-754.41C281,-754.41 211,-754.41 211,-754.41 205,-754.41 199,-748.41 199,-742.41 199,-742.41 199,-706.66 199,-706.66 199,-700.66 205,-694.66 211,-694.66 211,-694.66 281,-694.66 281,-694.66 287,-694.66 293,-700.66 293,-706.66 293,-706.66 293,-742.41 293,-742.41 293,-748.41 287,-754.41 281,-754.41"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="246" y="-737.11" font-family="Helvetica,sans-Serif" font-size="14.00">Timeline UI</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="246" y="-719.86" font-family="Helvetica,sans-Serif" font-size="14.00">/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="246" y="-702.61" font-family="Helvetica,sans-Serif" font-size="14.00">port 5173</text>
|
||||||
|
</g>
|
||||||
|
<!-- nginx->timeline -->
|
||||||
|
<g id="edge4" class="edge">
|
||||||
|
<title>nginx->timeline</title>
|
||||||
|
<path fill="none" stroke="black" d="M313.34,-842C298.97,-842 285.44,-842 285.44,-842 285.44,-842 285.44,-766.3 285.44,-766.3"/>
|
||||||
|
<polygon fill="black" stroke="black" points="288.94,-766.3 285.44,-756.3 281.94,-766.3 288.94,-766.3"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="283.94" y="-821.35" font-family="Helvetica,sans-Serif" font-size="10.00">/</text>
|
||||||
|
</g>
|
||||||
|
<!-- minio -->
|
||||||
|
<g id="node10" class="node">
|
||||||
|
<title>minio</title>
|
||||||
|
<polygon fill="none" stroke="black" points="486.38,-188.45 483.38,-192.45 462.38,-192.45 459.38,-188.45 343.62,-188.45 343.62,-128.7 486.38,-128.7 486.38,-188.45"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="415" y="-171.15" font-family="Helvetica,sans-Serif" font-size="14.00">MinIO</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="415" y="-153.9" font-family="Helvetica,sans-Serif" font-size="14.00">S3-compatible API</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="415" y="-136.65" font-family="Helvetica,sans-Serif" font-size="14.00">port 9000</text>
|
||||||
|
</g>
|
||||||
|
<!-- nginx->minio -->
|
||||||
|
<g id="edge5" class="edge">
|
||||||
|
<title>nginx->minio</title>
|
||||||
|
<path fill="none" stroke="black" d="M370.56,-827.73C370.56,-827.73 370.56,-200.13 370.56,-200.13"/>
|
||||||
|
<polygon fill="black" stroke="black" points="374.06,-200.13 370.56,-190.13 367.06,-200.13 374.06,-200.13"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="391.56" y="-517.18" font-family="Helvetica,sans-Serif" font-size="10.00">/media/*</text>
|
||||||
|
</g>
|
||||||
|
<!-- postgres -->
|
||||||
|
<g id="node8" class="node">
|
||||||
|
<title>postgres</title>
|
||||||
|
<path fill="none" stroke="black" d="M111.75,-182.48C111.75,-185.42 90.35,-187.8 64,-187.8 37.65,-187.8 16.25,-185.42 16.25,-182.48 16.25,-182.48 16.25,-134.67 16.25,-134.67 16.25,-131.74 37.65,-129.36 64,-129.36 90.35,-129.36 111.75,-131.74 111.75,-134.67 111.75,-134.67 111.75,-182.48 111.75,-182.48"/>
|
||||||
|
<path fill="none" stroke="black" d="M111.75,-182.48C111.75,-179.55 90.35,-177.17 64,-177.17 37.65,-177.17 16.25,-179.55 16.25,-182.48"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="64" y="-162.53" font-family="Helvetica,sans-Serif" font-size="14.00">PostgreSQL</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="64" y="-145.28" font-family="Helvetica,sans-Serif" font-size="14.00">port 5436</text>
|
||||||
|
</g>
|
||||||
|
<!-- django->postgres -->
|
||||||
|
<g id="edge7" class="edge">
|
||||||
|
<title>django->postgres</title>
|
||||||
|
<path fill="none" stroke="black" d="M48.38,-694.5C48.38,-694.5 48.38,-199.71 48.38,-199.71"/>
|
||||||
|
<polygon fill="black" stroke="black" points="51.88,-199.71 48.38,-189.71 44.88,-199.71 51.88,-199.71"/>
|
||||||
|
</g>
|
||||||
|
<!-- grpc_server -->
|
||||||
|
<g id="node6" class="node">
|
||||||
|
<title>grpc_server</title>
|
||||||
|
<path fill="none" stroke="black" d="M301.5,-466.16C301.5,-466.16 222.5,-466.16 222.5,-466.16 216.5,-466.16 210.5,-460.16 210.5,-454.16 210.5,-454.16 210.5,-435.66 210.5,-435.66 210.5,-429.66 216.5,-423.66 222.5,-423.66 222.5,-423.66 301.5,-423.66 301.5,-423.66 307.5,-423.66 313.5,-429.66 313.5,-435.66 313.5,-435.66 313.5,-454.16 313.5,-454.16 313.5,-460.16 307.5,-466.16 301.5,-466.16"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="262" y="-448.86" font-family="Helvetica,sans-Serif" font-size="14.00">gRPC Server</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="262" y="-431.61" font-family="Helvetica,sans-Serif" font-size="14.00">port 50051</text>
|
||||||
|
</g>
|
||||||
|
<!-- fastapi->grpc_server -->
|
||||||
|
<g id="edge9" class="edge">
|
||||||
|
<title>fastapi->grpc_server</title>
|
||||||
|
<path fill="none" stroke="black" d="M251.88,-563.85C251.88,-563.85 251.88,-477.88 251.88,-477.88"/>
|
||||||
|
<polygon fill="black" stroke="black" points="255.38,-477.88 251.88,-467.88 248.38,-477.88 255.38,-477.88"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="292" y="-525.66" font-family="Helvetica,sans-Serif" font-size="10.00">gRPC</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="292" y="-512.91" font-family="Helvetica,sans-Serif" font-size="10.00">progress updates</text>
|
||||||
|
</g>
|
||||||
|
<!-- fastapi->postgres -->
|
||||||
|
<g id="edge8" class="edge">
|
||||||
|
<title>fastapi->postgres</title>
|
||||||
|
<path fill="none" stroke="black" d="M188.61,-594C138.18,-594 69.5,-594 69.5,-594 69.5,-594 69.5,-199.68 69.5,-199.68"/>
|
||||||
|
<polygon fill="black" stroke="black" points="73,-199.68 69.5,-189.68 66,-199.68 73,-199.68"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="82.38" y="-385.16" font-family="Helvetica,sans-Serif" font-size="10.00">read/write jobs</text>
|
||||||
|
</g>
|
||||||
|
<!-- timeline->fastapi -->
|
||||||
|
<g id="edge6" class="edge">
|
||||||
|
<title>timeline->fastapi</title>
|
||||||
|
<path fill="none" stroke="black" d="M246,-694.26C246,-694.26 246,-635.65 246,-635.65"/>
|
||||||
|
<polygon fill="black" stroke="black" points="249.5,-635.65 246,-625.65 242.5,-635.65 249.5,-635.65"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="264" y="-656.16" font-family="Helvetica,sans-Serif" font-size="10.00">GraphQL</text>
|
||||||
|
</g>
|
||||||
|
<!-- celery -->
|
||||||
|
<g id="node7" class="node">
|
||||||
|
<title>celery</title>
|
||||||
|
<path fill="none" stroke="black" d="M348.62,-352.91C348.62,-352.91 213.38,-352.91 213.38,-352.91 207.38,-352.91 201.38,-346.91 201.38,-340.91 201.38,-340.91 201.38,-322.41 201.38,-322.41 201.38,-316.41 207.38,-310.41 213.38,-310.41 213.38,-310.41 348.62,-310.41 348.62,-310.41 354.62,-310.41 360.62,-316.41 360.62,-322.41 360.62,-322.41 360.62,-340.91 360.62,-340.91 360.62,-346.91 354.62,-352.91 348.62,-352.91"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="281" y="-335.61" font-family="Helvetica,sans-Serif" font-size="14.00">Celery Worker</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="281" y="-318.36" font-family="Helvetica,sans-Serif" font-size="14.00">FFmpeg transcoding</text>
|
||||||
|
</g>
|
||||||
|
<!-- grpc_server->celery -->
|
||||||
|
<g id="edge10" class="edge">
|
||||||
|
<title>grpc_server->celery</title>
|
||||||
|
<path fill="none" stroke="black" d="M262,-423.34C262,-423.34 262,-364.66 262,-364.66"/>
|
||||||
|
<polygon fill="black" stroke="black" points="265.5,-364.66 262,-354.66 258.5,-364.66 265.5,-364.66"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="305.25" y="-385.16" font-family="Helvetica,sans-Serif" font-size="10.00">dispatch tasks</text>
|
||||||
|
</g>
|
||||||
|
<!-- celery->postgres -->
|
||||||
|
<g id="edge12" class="edge">
|
||||||
|
<title>celery->postgres</title>
|
||||||
|
<path fill="none" stroke="black" d="M201.09,-332C148.99,-332 90.62,-332 90.62,-332 90.62,-332 90.62,-199.51 90.62,-199.51"/>
|
||||||
|
<polygon fill="black" stroke="black" points="94.13,-199.51 90.63,-189.51 87.13,-199.51 94.13,-199.51"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="181.38" y="-259.16" font-family="Helvetica,sans-Serif" font-size="10.00">update job status</text>
|
||||||
|
</g>
|
||||||
|
<!-- redis -->
|
||||||
|
<g id="node9" class="node">
|
||||||
|
<title>redis</title>
|
||||||
|
<path fill="none" stroke="black" d="M278.12,-192.19C278.12,-196.31 253.87,-199.66 224,-199.66 194.13,-199.66 169.88,-196.31 169.88,-192.19 169.88,-192.19 169.88,-124.97 169.88,-124.97 169.88,-120.85 194.13,-117.5 224,-117.5 253.87,-117.5 278.12,-120.85 278.12,-124.97 278.12,-124.97 278.12,-192.19 278.12,-192.19"/>
|
||||||
|
<path fill="none" stroke="black" d="M278.12,-192.19C278.12,-188.07 253.87,-184.72 224,-184.72 194.13,-184.72 169.88,-188.07 169.88,-192.19"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="224" y="-171.15" font-family="Helvetica,sans-Serif" font-size="14.00">Redis</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="224" y="-153.9" font-family="Helvetica,sans-Serif" font-size="14.00">Celery queue</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="224" y="-136.65" font-family="Helvetica,sans-Serif" font-size="14.00">port 6381</text>
|
||||||
|
</g>
|
||||||
|
<!-- celery->redis -->
|
||||||
|
<g id="edge11" class="edge">
|
||||||
|
<title>celery->redis</title>
|
||||||
|
<path fill="none" stroke="black" d="M239.75,-310.09C239.75,-310.09 239.75,-211.49 239.75,-211.49"/>
|
||||||
|
<polygon fill="black" stroke="black" points="243.25,-211.49 239.75,-201.49 236.25,-211.49 243.25,-211.49"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="314" y="-259.16" font-family="Helvetica,sans-Serif" font-size="10.00">task queue</text>
|
||||||
|
</g>
|
||||||
|
<!-- celery->minio -->
|
||||||
|
<g id="edge13" class="edge">
|
||||||
|
<title>celery->minio</title>
|
||||||
|
<path fill="none" stroke="black" d="M352.12,-310.09C352.12,-310.09 352.12,-200.39 352.12,-200.39"/>
|
||||||
|
<polygon fill="black" stroke="black" points="355.63,-200.39 352.13,-190.39 348.63,-200.39 355.63,-200.39"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="441.5" y="-271.91" font-family="Helvetica,sans-Serif" font-size="10.00">S3 API</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="441.5" y="-259.16" font-family="Helvetica,sans-Serif" font-size="10.00">download input</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="441.5" y="-246.41" font-family="Helvetica,sans-Serif" font-size="10.00">upload output</text>
|
||||||
|
</g>
|
||||||
|
<!-- bucket_in -->
|
||||||
|
<g id="node11" class="node">
|
||||||
|
<title>bucket_in</title>
|
||||||
|
<polygon fill="none" stroke="black" points="434.75,-58.5 327.25,-58.5 327.25,-16 440.75,-16 440.75,-52.5 434.75,-58.5"/>
|
||||||
|
<polyline fill="none" stroke="black" points="434.75,-58.5 434.75,-52.5"/>
|
||||||
|
<polyline fill="none" stroke="black" points="440.75,-52.5 434.75,-52.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="384" y="-41.2" font-family="Helvetica,sans-Serif" font-size="14.00">mpr-media-in/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="384" y="-23.95" font-family="Helvetica,sans-Serif" font-size="14.00">input videos</text>
|
||||||
|
</g>
|
||||||
|
<!-- minio->bucket_in -->
|
||||||
|
<g id="edge14" class="edge">
|
||||||
|
<title>minio->bucket_in</title>
|
||||||
|
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M392.19,-128.27C392.19,-106.66 392.19,-78.11 392.19,-58.79"/>
|
||||||
|
</g>
|
||||||
|
<!-- bucket_out -->
|
||||||
|
<g id="node12" class="node">
|
||||||
|
<title>bucket_out</title>
|
||||||
|
<polygon fill="none" stroke="black" points="637.12,-58.5 498.88,-58.5 498.88,-16 643.12,-16 643.12,-52.5 637.12,-58.5"/>
|
||||||
|
<polyline fill="none" stroke="black" points="637.12,-58.5 637.12,-52.5"/>
|
||||||
|
<polyline fill="none" stroke="black" points="643.12,-52.5 637.12,-52.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="571" y="-41.2" font-family="Helvetica,sans-Serif" font-size="14.00">mpr-media-out/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="571" y="-23.95" font-family="Helvetica,sans-Serif" font-size="14.00">transcoded output</text>
|
||||||
|
</g>
|
||||||
|
<!-- minio->bucket_out -->
|
||||||
|
<g id="edge15" class="edge">
|
||||||
|
<title>minio->bucket_out</title>
|
||||||
|
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M463.56,-128.21C463.56,-92.2 463.56,-37 463.56,-37 463.56,-37 479.15,-37 498.44,-37"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 18 KiB |
85
docs/architecture/01b-aws-architecture.dot
Normal file
85
docs/architecture/01b-aws-architecture.dot
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
digraph aws_architecture {
|
||||||
|
rankdir=TB
|
||||||
|
node [shape=box, style=rounded, fontname="Helvetica"]
|
||||||
|
edge [fontname="Helvetica", fontsize=10]
|
||||||
|
|
||||||
|
labelloc="t"
|
||||||
|
label="MPR - AWS Architecture (Lambda + Step Functions)"
|
||||||
|
fontsize=16
|
||||||
|
fontname="Helvetica-Bold"
|
||||||
|
|
||||||
|
graph [splines=ortho, nodesep=0.8, ranksep=0.8]
|
||||||
|
|
||||||
|
// External
|
||||||
|
subgraph cluster_external {
|
||||||
|
label="External"
|
||||||
|
style=dashed
|
||||||
|
color=gray
|
||||||
|
|
||||||
|
browser [label="Browser\nmpr.mcrn.ar", shape=ellipse]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nginx reverse proxy
|
||||||
|
subgraph cluster_proxy {
|
||||||
|
label="Reverse Proxy"
|
||||||
|
style=filled
|
||||||
|
fillcolor="#e8f4f8"
|
||||||
|
|
||||||
|
nginx [label="nginx\nport 80"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application layer
|
||||||
|
subgraph cluster_apps {
|
||||||
|
label="Application Layer"
|
||||||
|
style=filled
|
||||||
|
fillcolor="#f0f8e8"
|
||||||
|
|
||||||
|
django [label="Django Admin\n/admin\nport 8701"]
|
||||||
|
fastapi [label="GraphQL API\n/graphql\nport 8702"]
|
||||||
|
timeline [label="Timeline UI\n/\nport 5173"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data layer (still local)
|
||||||
|
subgraph cluster_data {
|
||||||
|
label="Data Layer"
|
||||||
|
style=filled
|
||||||
|
fillcolor="#f8e8f0"
|
||||||
|
|
||||||
|
postgres [label="PostgreSQL\nport 5436", shape=cylinder]
|
||||||
|
}
|
||||||
|
|
||||||
|
// AWS layer
|
||||||
|
subgraph cluster_aws {
|
||||||
|
label="AWS Cloud"
|
||||||
|
style=filled
|
||||||
|
fillcolor="#fde8d0"
|
||||||
|
|
||||||
|
step_functions [label="Step Functions\nOrchestration\nstate machine"]
|
||||||
|
lambda [label="Lambda Function\nFFmpeg container\ntranscoding"]
|
||||||
|
s3 [label="S3 Buckets", shape=folder]
|
||||||
|
bucket_in [label="mpr-media-in/\ninput videos", shape=note]
|
||||||
|
bucket_out [label="mpr-media-out/\ntranscoded output", shape=note]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connections
|
||||||
|
browser -> nginx [label="HTTP"]
|
||||||
|
|
||||||
|
nginx -> django [xlabel="/admin"]
|
||||||
|
nginx -> fastapi [xlabel="/graphql"]
|
||||||
|
nginx -> timeline [xlabel="/"]
|
||||||
|
|
||||||
|
timeline -> fastapi [label="GraphQL"]
|
||||||
|
django -> postgres
|
||||||
|
|
||||||
|
fastapi -> postgres [label="read/write jobs"]
|
||||||
|
fastapi -> step_functions [label="boto3\nstart_execution()\nexecution_arn"]
|
||||||
|
|
||||||
|
step_functions -> lambda [label="invoke with\njob parameters"]
|
||||||
|
lambda -> s3 [label="download input\nupload output"]
|
||||||
|
lambda -> fastapi [label="POST /jobs/{id}/callback\nupdate status"]
|
||||||
|
|
||||||
|
fastapi -> postgres [label="callback updates\njob status"]
|
||||||
|
|
||||||
|
s3 -> bucket_in [style=dotted, arrowhead=none]
|
||||||
|
s3 -> bucket_out [style=dotted, arrowhead=none]
|
||||||
|
}
|
||||||
224
docs/architecture/01b-aws-architecture.svg
Normal file
224
docs/architecture/01b-aws-architecture.svg
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Generated by graphviz version 14.1.2 (0)
|
||||||
|
-->
|
||||||
|
<!-- Title: aws_architecture Pages: 1 -->
|
||||||
|
<svg width="639pt" height="1081pt"
|
||||||
|
viewBox="0.00 0.00 639.00 1081.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1077.35)">
|
||||||
|
<title>aws_architecture</title>
|
||||||
|
<polygon fill="white" stroke="none" points="-4,4 -4,-1077.35 635.25,-1077.35 635.25,4 -4,4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="315.62" y="-1054.15" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">MPR - AWS Architecture (Lambda + Step Functions)</text>
|
||||||
|
<g id="clust1" class="cluster">
|
||||||
|
<title>cluster_external</title>
|
||||||
|
<polygon fill="none" stroke="gray" stroke-dasharray="5,2" points="155,-934.25 155,-1037.85 315,-1037.85 315,-934.25 155,-934.25"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="235" y="-1018.65" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">External</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust2" class="cluster">
|
||||||
|
<title>cluster_proxy</title>
|
||||||
|
<polygon fill="#e8f4f8" stroke="black" points="162,-806.5 162,-892.5 308,-892.5 308,-806.5 162,-806.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="235" y="-873.3" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Reverse Proxy</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust3" class="cluster">
|
||||||
|
<title>cluster_apps</title>
|
||||||
|
<polygon fill="#f0f8e8" stroke="black" points="8,-542.75 8,-776.5 290,-776.5 290,-542.75 8,-542.75"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="149" y="-757.3" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Application Layer</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust4" class="cluster">
|
||||||
|
<title>cluster_data</title>
|
||||||
|
<polygon fill="#f8e8f0" stroke="black" points="27,-372.91 27,-474.84 141,-474.84 141,-372.91 27,-372.91"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="84" y="-455.64" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Data Layer</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust5" class="cluster">
|
||||||
|
<title>cluster_aws</title>
|
||||||
|
<polygon fill="#fde8d0" stroke="black" points="264,-8 264,-475.5 596,-475.5 596,-8 264,-8"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="430" y="-456.3" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AWS Cloud</text>
|
||||||
|
</g>
|
||||||
|
<!-- browser -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>browser</title>
|
||||||
|
<ellipse fill="none" stroke="black" cx="235" cy="-972.3" rx="71.77" ry="30.05"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="235" y="-976.25" font-family="Helvetica,sans-Serif" font-size="14.00">Browser</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="235" y="-959" font-family="Helvetica,sans-Serif" font-size="14.00">mpr.mcrn.ar</text>
|
||||||
|
</g>
|
||||||
|
<!-- nginx -->
|
||||||
|
<g id="node2" class="node">
|
||||||
|
<title>nginx</title>
|
||||||
|
<path fill="none" stroke="black" d="M256.5,-857C256.5,-857 213.5,-857 213.5,-857 207.5,-857 201.5,-851 201.5,-845 201.5,-845 201.5,-826.5 201.5,-826.5 201.5,-820.5 207.5,-814.5 213.5,-814.5 213.5,-814.5 256.5,-814.5 256.5,-814.5 262.5,-814.5 268.5,-820.5 268.5,-826.5 268.5,-826.5 268.5,-845 268.5,-845 268.5,-851 262.5,-857 256.5,-857"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="235" y="-839.7" font-family="Helvetica,sans-Serif" font-size="14.00">nginx</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="235" y="-822.45" font-family="Helvetica,sans-Serif" font-size="14.00">port 80</text>
|
||||||
|
</g>
|
||||||
|
<!-- browser->nginx -->
|
||||||
|
<g id="edge1" class="edge">
|
||||||
|
<title>browser->nginx</title>
|
||||||
|
<path fill="none" stroke="black" d="M235,-942C235,-942 235,-869 235,-869"/>
|
||||||
|
<polygon fill="black" stroke="black" points="238.5,-869 235,-859 231.5,-869 238.5,-869"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="247.75" y="-903.75" font-family="Helvetica,sans-Serif" font-size="10.00">HTTP</text>
|
||||||
|
</g>
|
||||||
|
<!-- django -->
|
||||||
|
<g id="node3" class="node">
|
||||||
|
<title>django</title>
|
||||||
|
<path fill="none" stroke="black" d="M117.75,-741C117.75,-741 28.25,-741 28.25,-741 22.25,-741 16.25,-735 16.25,-729 16.25,-729 16.25,-693.25 16.25,-693.25 16.25,-687.25 22.25,-681.25 28.25,-681.25 28.25,-681.25 117.75,-681.25 117.75,-681.25 123.75,-681.25 129.75,-687.25 129.75,-693.25 129.75,-693.25 129.75,-729 129.75,-729 129.75,-735 123.75,-741 117.75,-741"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="73" y="-723.7" font-family="Helvetica,sans-Serif" font-size="14.00">Django Admin</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="73" y="-706.45" font-family="Helvetica,sans-Serif" font-size="14.00">/admin</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="73" y="-689.2" font-family="Helvetica,sans-Serif" font-size="14.00">port 8701</text>
|
||||||
|
</g>
|
||||||
|
<!-- nginx->django -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<title>nginx->django</title>
|
||||||
|
<path fill="none" stroke="black" d="M201.04,-843C153.54,-843 73,-843 73,-843 73,-843 73,-752.89 73,-752.89"/>
|
||||||
|
<polygon fill="black" stroke="black" points="76.5,-752.89 73,-742.89 69.5,-752.89 76.5,-752.89"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="75.09" y="-846.25" font-family="Helvetica,sans-Serif" font-size="10.00">/admin</text>
|
||||||
|
</g>
|
||||||
|
<!-- fastapi -->
|
||||||
|
<g id="node4" class="node">
|
||||||
|
<title>fastapi</title>
|
||||||
|
<path fill="none" stroke="black" d="M270.25,-610.5C270.25,-610.5 189.75,-610.5 189.75,-610.5 183.75,-610.5 177.75,-604.5 177.75,-598.5 177.75,-598.5 177.75,-562.75 177.75,-562.75 177.75,-556.75 183.75,-550.75 189.75,-550.75 189.75,-550.75 270.25,-550.75 270.25,-550.75 276.25,-550.75 282.25,-556.75 282.25,-562.75 282.25,-562.75 282.25,-598.5 282.25,-598.5 282.25,-604.5 276.25,-610.5 270.25,-610.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="230" y="-593.2" font-family="Helvetica,sans-Serif" font-size="14.00">GraphQL API</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="230" y="-575.95" font-family="Helvetica,sans-Serif" font-size="14.00">/graphql</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="230" y="-558.7" font-family="Helvetica,sans-Serif" font-size="14.00">port 8702</text>
|
||||||
|
</g>
|
||||||
|
<!-- nginx->fastapi -->
|
||||||
|
<g id="edge3" class="edge">
|
||||||
|
<title>nginx->fastapi</title>
|
||||||
|
<path fill="none" stroke="black" d="M201.11,-829C191.15,-829 182.88,-829 182.88,-829 182.88,-829 182.88,-622.1 182.88,-622.1"/>
|
||||||
|
<polygon fill="black" stroke="black" points="186.38,-622.1 182.88,-612.1 179.38,-622.1 186.38,-622.1"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="163" y="-737.91" font-family="Helvetica,sans-Serif" font-size="10.00">/graphql</text>
|
||||||
|
</g>
|
||||||
|
<!-- timeline -->
|
||||||
|
<g id="node5" class="node">
|
||||||
|
<title>timeline</title>
|
||||||
|
<path fill="none" stroke="black" d="M270,-741C270,-741 200,-741 200,-741 194,-741 188,-735 188,-729 188,-729 188,-693.25 188,-693.25 188,-687.25 194,-681.25 200,-681.25 200,-681.25 270,-681.25 270,-681.25 276,-681.25 282,-687.25 282,-693.25 282,-693.25 282,-729 282,-729 282,-735 276,-741 270,-741"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="235" y="-723.7" font-family="Helvetica,sans-Serif" font-size="14.00">Timeline UI</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="235" y="-706.45" font-family="Helvetica,sans-Serif" font-size="14.00">/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="235" y="-689.2" font-family="Helvetica,sans-Serif" font-size="14.00">port 5173</text>
|
||||||
|
</g>
|
||||||
|
<!-- nginx->timeline -->
|
||||||
|
<g id="edge4" class="edge">
|
||||||
|
<title>nginx->timeline</title>
|
||||||
|
<path fill="none" stroke="black" d="M235,-814.04C235,-814.04 235,-752.97 235,-752.97"/>
|
||||||
|
<polygon fill="black" stroke="black" points="238.5,-752.97 235,-742.97 231.5,-752.97 238.5,-752.97"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="233.5" y="-786.75" font-family="Helvetica,sans-Serif" font-size="10.00">/</text>
|
||||||
|
</g>
|
||||||
|
<!-- postgres -->
|
||||||
|
<g id="node6" class="node">
|
||||||
|
<title>postgres</title>
|
||||||
|
<path fill="none" stroke="black" d="M131.75,-434.03C131.75,-436.96 110.35,-439.34 84,-439.34 57.65,-439.34 36.25,-436.96 36.25,-434.03 36.25,-434.03 36.25,-386.22 36.25,-386.22 36.25,-383.29 57.65,-380.91 84,-380.91 110.35,-380.91 131.75,-383.29 131.75,-386.22 131.75,-386.22 131.75,-434.03 131.75,-434.03"/>
|
||||||
|
<path fill="none" stroke="black" d="M131.75,-434.03C131.75,-431.1 110.35,-428.72 84,-428.72 57.65,-428.72 36.25,-431.1 36.25,-434.03"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="84" y="-414.07" font-family="Helvetica,sans-Serif" font-size="14.00">PostgreSQL</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="84" y="-396.82" font-family="Helvetica,sans-Serif" font-size="14.00">port 5436</text>
|
||||||
|
</g>
|
||||||
|
<!-- django->postgres -->
|
||||||
|
<g id="edge6" class="edge">
|
||||||
|
<title>django->postgres</title>
|
||||||
|
<path fill="none" stroke="black" d="M83,-680.89C83,-680.89 83,-450.97 83,-450.97"/>
|
||||||
|
<polygon fill="black" stroke="black" points="86.5,-450.97 83,-440.97 79.5,-450.97 86.5,-450.97"/>
|
||||||
|
</g>
|
||||||
|
<!-- fastapi->postgres -->
|
||||||
|
<g id="edge7" class="edge">
|
||||||
|
<title>fastapi->postgres</title>
|
||||||
|
<path fill="none" stroke="black" d="M201.38,-550.41C201.38,-503.88 201.38,-420 201.38,-420 201.38,-420 143.59,-420 143.59,-420"/>
|
||||||
|
<polygon fill="black" stroke="black" points="143.59,-416.5 133.59,-420 143.59,-423.5 143.59,-416.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="266.38" y="-499.5" font-family="Helvetica,sans-Serif" font-size="10.00">read/write jobs</text>
|
||||||
|
</g>
|
||||||
|
<!-- fastapi->postgres -->
|
||||||
|
<g id="edge12" class="edge">
|
||||||
|
<title>fastapi->postgres</title>
|
||||||
|
<path fill="none" stroke="black" d="M225,-550.39C225,-498.97 225,-400 225,-400 225,-400 143.64,-400 143.64,-400"/>
|
||||||
|
<polygon fill="black" stroke="black" points="143.64,-396.5 133.64,-400 143.64,-403.5 143.64,-396.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="125.25" y="-505.88" font-family="Helvetica,sans-Serif" font-size="10.00">callback updates</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="125.25" y="-493.12" font-family="Helvetica,sans-Serif" font-size="10.00">job status</text>
|
||||||
|
</g>
|
||||||
|
<!-- step_functions -->
|
||||||
|
<g id="node7" class="node">
|
||||||
|
<title>step_functions</title>
|
||||||
|
<path fill="none" stroke="black" d="M384.38,-440C384.38,-440 289.62,-440 289.62,-440 283.62,-440 277.62,-434 277.62,-428 277.62,-428 277.62,-392.25 277.62,-392.25 277.62,-386.25 283.62,-380.25 289.62,-380.25 289.62,-380.25 384.38,-380.25 384.38,-380.25 390.38,-380.25 396.38,-386.25 396.38,-392.25 396.38,-392.25 396.38,-428 396.38,-428 396.38,-434 390.38,-440 384.38,-440"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="337" y="-422.7" font-family="Helvetica,sans-Serif" font-size="14.00">Step Functions</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="337" y="-405.45" font-family="Helvetica,sans-Serif" font-size="14.00">Orchestration</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="337" y="-388.2" font-family="Helvetica,sans-Serif" font-size="14.00">state machine</text>
|
||||||
|
</g>
|
||||||
|
<!-- fastapi->step_functions -->
|
||||||
|
<g id="edge8" class="edge">
|
||||||
|
<title>fastapi->step_functions</title>
|
||||||
|
<path fill="none" stroke="black" d="M282.68,-581C289.69,-581 294.51,-581 294.51,-581 294.51,-581 294.51,-451.79 294.51,-451.79"/>
|
||||||
|
<polygon fill="black" stroke="black" points="298.01,-451.79 294.51,-441.79 291.01,-451.79 298.01,-451.79"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="407.25" y="-512.25" font-family="Helvetica,sans-Serif" font-size="10.00">boto3</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="407.25" y="-499.5" font-family="Helvetica,sans-Serif" font-size="10.00">start_execution()</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="407.25" y="-486.75" font-family="Helvetica,sans-Serif" font-size="10.00">execution_arn</text>
|
||||||
|
</g>
|
||||||
|
<!-- timeline->fastapi -->
|
||||||
|
<g id="edge5" class="edge">
|
||||||
|
<title>timeline->fastapi</title>
|
||||||
|
<path fill="none" stroke="black" d="M235,-680.86C235,-680.86 235,-622.24 235,-622.24"/>
|
||||||
|
<polygon fill="black" stroke="black" points="238.5,-622.24 235,-612.24 231.5,-622.24 238.5,-622.24"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="253" y="-642.75" font-family="Helvetica,sans-Serif" font-size="10.00">GraphQL</text>
|
||||||
|
</g>
|
||||||
|
<!-- lambda -->
|
||||||
|
<g id="node8" class="node">
|
||||||
|
<title>lambda</title>
|
||||||
|
<path fill="none" stroke="black" d="M486,-296.75C486,-296.75 368,-296.75 368,-296.75 362,-296.75 356,-290.75 356,-284.75 356,-284.75 356,-249 356,-249 356,-243 362,-237 368,-237 368,-237 486,-237 486,-237 492,-237 498,-243 498,-249 498,-249 498,-284.75 498,-284.75 498,-290.75 492,-296.75 486,-296.75"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="427" y="-279.45" font-family="Helvetica,sans-Serif" font-size="14.00">Lambda Function</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="427" y="-262.2" font-family="Helvetica,sans-Serif" font-size="14.00">FFmpeg container</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="427" y="-244.95" font-family="Helvetica,sans-Serif" font-size="14.00">transcoding</text>
|
||||||
|
</g>
|
||||||
|
<!-- step_functions->lambda -->
|
||||||
|
<g id="edge9" class="edge">
|
||||||
|
<title>step_functions->lambda</title>
|
||||||
|
<path fill="none" stroke="black" d="M376.19,-380.1C376.19,-380.1 376.19,-308.38 376.19,-308.38"/>
|
||||||
|
<polygon fill="black" stroke="black" points="379.69,-308.38 376.19,-298.38 372.69,-308.38 379.69,-308.38"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="407.12" y="-341.75" font-family="Helvetica,sans-Serif" font-size="10.00">invoke with</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="407.12" y="-329" font-family="Helvetica,sans-Serif" font-size="10.00">job parameters</text>
|
||||||
|
</g>
|
||||||
|
<!-- lambda->fastapi -->
|
||||||
|
<g id="edge11" class="edge">
|
||||||
|
<title>lambda->fastapi</title>
|
||||||
|
<path fill="none" stroke="black" d="M355.73,-267C306.05,-267 248.62,-267 248.62,-267 248.62,-267 248.62,-538.75 248.62,-538.75"/>
|
||||||
|
<polygon fill="black" stroke="black" points="245.13,-538.75 248.63,-548.75 252.13,-538.75 245.13,-538.75"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="571.62" y="-413.38" font-family="Helvetica,sans-Serif" font-size="10.00">POST /jobs/{id}/callback</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="571.62" y="-400.62" font-family="Helvetica,sans-Serif" font-size="10.00">update status</text>
|
||||||
|
</g>
|
||||||
|
<!-- s3 -->
|
||||||
|
<g id="node9" class="node">
|
||||||
|
<title>s3</title>
|
||||||
|
<polygon fill="none" stroke="black" points="473.62,-153.5 470.62,-157.5 449.62,-157.5 446.62,-153.5 380.38,-153.5 380.38,-117.5 473.62,-117.5 473.62,-153.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="427" y="-130.82" font-family="Helvetica,sans-Serif" font-size="14.00">S3 Buckets</text>
|
||||||
|
</g>
|
||||||
|
<!-- lambda->s3 -->
|
||||||
|
<g id="edge10" class="edge">
|
||||||
|
<title>lambda->s3</title>
|
||||||
|
<path fill="none" stroke="black" d="M427,-236.73C427,-236.73 427,-165.27 427,-165.27"/>
|
||||||
|
<polygon fill="black" stroke="black" points="430.5,-165.27 427,-155.27 423.5,-165.27 430.5,-165.27"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="464.5" y="-198.5" font-family="Helvetica,sans-Serif" font-size="10.00">download input</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="464.5" y="-185.75" font-family="Helvetica,sans-Serif" font-size="10.00">upload output</text>
|
||||||
|
</g>
|
||||||
|
<!-- bucket_in -->
|
||||||
|
<g id="node10" class="node">
|
||||||
|
<title>bucket_in</title>
|
||||||
|
<polygon fill="none" stroke="black" points="379.75,-58.5 272.25,-58.5 272.25,-16 385.75,-16 385.75,-52.5 379.75,-58.5"/>
|
||||||
|
<polyline fill="none" stroke="black" points="379.75,-58.5 379.75,-52.5"/>
|
||||||
|
<polyline fill="none" stroke="black" points="385.75,-52.5 379.75,-52.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="329" y="-41.2" font-family="Helvetica,sans-Serif" font-size="14.00">mpr-media-in/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="329" y="-23.95" font-family="Helvetica,sans-Serif" font-size="14.00">input videos</text>
|
||||||
|
</g>
|
||||||
|
<!-- s3->bucket_in -->
|
||||||
|
<g id="edge13" class="edge">
|
||||||
|
<title>s3->bucket_in</title>
|
||||||
|
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M380.09,-136C373.1,-136 368.19,-136 368.19,-136 368.19,-136 368.19,-87.72 368.19,-58.68"/>
|
||||||
|
</g>
|
||||||
|
<!-- bucket_out -->
|
||||||
|
<g id="node11" class="node">
|
||||||
|
<title>bucket_out</title>
|
||||||
|
<polygon fill="none" stroke="black" points="582.12,-58.5 443.88,-58.5 443.88,-16 588.12,-16 588.12,-52.5 582.12,-58.5"/>
|
||||||
|
<polyline fill="none" stroke="black" points="582.12,-58.5 582.12,-52.5"/>
|
||||||
|
<polyline fill="none" stroke="black" points="588.12,-52.5 582.12,-52.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="516" y="-41.2" font-family="Helvetica,sans-Serif" font-size="14.00">mpr-media-out/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="516" y="-23.95" font-family="Helvetica,sans-Serif" font-size="14.00">transcoded output</text>
|
||||||
|
</g>
|
||||||
|
<!-- s3->bucket_out -->
|
||||||
|
<g id="edge14" class="edge">
|
||||||
|
<title>s3->bucket_out</title>
|
||||||
|
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M458.75,-117.02C458.75,-100.45 458.75,-76.15 458.75,-58.73"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 16 KiB |
@@ -17,16 +17,32 @@
|
|||||||
<a href="#overview">System Overview</a>
|
<a href="#overview">System Overview</a>
|
||||||
<a href="#data-model">Data Model</a>
|
<a href="#data-model">Data Model</a>
|
||||||
<a href="#job-flow">Job Flow</a>
|
<a href="#job-flow">Job Flow</a>
|
||||||
|
<a href="#media-storage">Media Storage</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<h2 id="overview">System Overview</h2>
|
<h2 id="overview">System Overview</h2>
|
||||||
<div class="diagram-container">
|
<div class="diagram-container">
|
||||||
<div class="diagram">
|
<div class="diagram">
|
||||||
<h3>Architecture</h3>
|
<h3>Local Architecture (Development)</h3>
|
||||||
<object type="image/svg+xml" data="01-system-overview.svg">
|
<object type="image/svg+xml" data="01a-local-architecture.svg">
|
||||||
<img src="01-system-overview.svg" alt="System Overview" />
|
<img
|
||||||
|
src="01a-local-architecture.svg"
|
||||||
|
alt="Local Architecture"
|
||||||
|
/>
|
||||||
</object>
|
</object>
|
||||||
<a href="01-system-overview.svg" target="_blank"
|
<a href="01a-local-architecture.svg" target="_blank"
|
||||||
|
>Open full size</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="diagram">
|
||||||
|
<h3>AWS Architecture (Production)</h3>
|
||||||
|
<object type="image/svg+xml" data="01b-aws-architecture.svg">
|
||||||
|
<img
|
||||||
|
src="01b-aws-architecture.svg"
|
||||||
|
alt="AWS Architecture"
|
||||||
|
/>
|
||||||
|
</object>
|
||||||
|
<a href="01b-aws-architecture.svg" target="_blank"
|
||||||
>Open full size</a
|
>Open full size</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,8 +57,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="color-box" style="background: #f0f8e8"></span>
|
<span class="color-box" style="background: #f0f8e8"></span>
|
||||||
Application Layer (Django Admin, FastAPI + GraphQL, Timeline
|
Application Layer (Django Admin, GraphQL API, Timeline UI)
|
||||||
UI)
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="color-box" style="background: #fff8e8"></span>
|
<span class="color-box" style="background: #fff8e8"></span>
|
||||||
@@ -141,24 +156,44 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>API Interfaces</h2>
|
<h2 id="media-storage">Media Storage</h2>
|
||||||
<pre><code># REST API
|
<div class="diagram-container">
|
||||||
http://mpr.local.ar/api/docs - Swagger UI
|
<p>
|
||||||
POST /api/assets/scan - Scan S3 bucket for media
|
MPR separates media into input and output paths for flexible
|
||||||
POST /api/jobs/ - Create transcode job
|
storage configuration.
|
||||||
POST /api/jobs/{id}/callback - Lambda completion callback
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="04-media-storage.md" target="_blank"
|
||||||
|
>View Media Storage Documentation →</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
# GraphQL (GraphiQL)
|
<h2>API (GraphQL)</h2>
|
||||||
http://mpr.local.ar/graphql - GraphiQL IDE
|
<pre><code># GraphiQL IDE
|
||||||
query { assets { id filename } }
|
http://mpr.local.ar/graphql
|
||||||
mutation { createJob(input: {...}) { id status } }
|
|
||||||
mutation { scanMediaFolder { found registered } }</code></pre>
|
# Queries
|
||||||
|
query { assets(status: "ready") { id filename duration } }
|
||||||
|
query { jobs(status: "processing") { id status progress } }
|
||||||
|
query { presets { id name container videoCodec } }
|
||||||
|
query { systemStatus { status version } }
|
||||||
|
|
||||||
|
# Mutations
|
||||||
|
mutation { scanMediaFolder { found registered skipped } }
|
||||||
|
mutation { createJob(input: { sourceAssetId: "...", presetId: "..." }) { id status } }
|
||||||
|
mutation { cancelJob(id: "...") { id status } }
|
||||||
|
mutation { retryJob(id: "...") { id status } }
|
||||||
|
mutation { updateAsset(id: "...", input: { comments: "..." }) { id comments } }
|
||||||
|
mutation { deleteAsset(id: "...") { ok } }
|
||||||
|
|
||||||
|
# Lambda callback (REST)
|
||||||
|
POST /api/jobs/{id}/callback - Lambda completion webhook</code></pre>
|
||||||
|
|
||||||
<h2>Access Points</h2>
|
<h2>Access Points</h2>
|
||||||
<pre><code># Local development
|
<pre><code># Local development
|
||||||
127.0.0.1 mpr.local.ar
|
127.0.0.1 mpr.local.ar
|
||||||
http://mpr.local.ar/admin - Django Admin
|
http://mpr.local.ar/admin - Django Admin
|
||||||
http://mpr.local.ar/api/docs - FastAPI Swagger
|
|
||||||
http://mpr.local.ar/graphql - GraphiQL
|
http://mpr.local.ar/graphql - GraphiQL
|
||||||
http://mpr.local.ar/ - Timeline UI
|
http://mpr.local.ar/ - Timeline UI
|
||||||
http://localhost:9001 - MinIO Console
|
http://localhost:9001 - MinIO Console
|
||||||
|
|||||||
259
docs/index.html
Normal file
259
docs/index.html
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>MPR - Architecture</title>
|
||||||
|
<link rel="stylesheet" href="architecture/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>MPR - Media Processor</h1>
|
||||||
|
<p>
|
||||||
|
Media transcoding platform with dual execution modes: local (Celery
|
||||||
|
+ MinIO) and cloud (AWS Step Functions + Lambda + S3).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<a href="#overview">System Overview</a>
|
||||||
|
<a href="#data-model">Data Model</a>
|
||||||
|
<a href="#job-flow">Job Flow</a>
|
||||||
|
<a href="#media-storage">Media Storage</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<h2 id="overview">System Overview</h2>
|
||||||
|
<div class="diagram-container">
|
||||||
|
<div class="diagram">
|
||||||
|
<h3>Local Architecture (Development)</h3>
|
||||||
|
<object
|
||||||
|
type="image/svg+xml"
|
||||||
|
data="architecture/01a-local-architecture.svg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="architecture/01a-local-architecture.svg"
|
||||||
|
alt="Local Architecture"
|
||||||
|
/>
|
||||||
|
</object>
|
||||||
|
<a
|
||||||
|
href="architecture/01a-local-architecture.svg"
|
||||||
|
target="_blank"
|
||||||
|
>Open full size</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="diagram">
|
||||||
|
<h3>AWS Architecture (Production)</h3>
|
||||||
|
<object
|
||||||
|
type="image/svg+xml"
|
||||||
|
data="architecture/01b-aws-architecture.svg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="architecture/01b-aws-architecture.svg"
|
||||||
|
alt="AWS Architecture"
|
||||||
|
/>
|
||||||
|
</object>
|
||||||
|
<a href="architecture/01b-aws-architecture.svg" target="_blank"
|
||||||
|
>Open full size</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<h3>Components</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span class="color-box" style="background: #e8f4f8"></span>
|
||||||
|
Reverse Proxy (nginx)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="color-box" style="background: #f0f8e8"></span>
|
||||||
|
Application Layer (Django Admin, GraphQL API, Timeline UI)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="color-box" style="background: #fff8e8"></span>
|
||||||
|
Worker Layer (Celery local mode)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="color-box" style="background: #fde8d0"></span>
|
||||||
|
AWS (Step Functions, Lambda - cloud mode)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="color-box" style="background: #f8e8f0"></span>
|
||||||
|
Data Layer (PostgreSQL, Redis)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="color-box" style="background: #f0f0f0"></span>
|
||||||
|
S3 Storage (MinIO local / AWS S3 cloud)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="data-model">Data Model</h2>
|
||||||
|
<div class="diagram-container">
|
||||||
|
<div class="diagram">
|
||||||
|
<h3>Entity Relationships</h3>
|
||||||
|
<object
|
||||||
|
type="image/svg+xml"
|
||||||
|
data="architecture/02-data-model.svg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="architecture/02-data-model.svg"
|
||||||
|
alt="Data Model"
|
||||||
|
/>
|
||||||
|
</object>
|
||||||
|
<a href="architecture/02-data-model.svg" target="_blank"
|
||||||
|
>Open full size</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<h3>Entities</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span class="color-box" style="background: #4a90d9"></span>
|
||||||
|
MediaAsset - Video/audio files with metadata
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="color-box" style="background: #50b050"></span>
|
||||||
|
TranscodePreset - Encoding configurations
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="color-box" style="background: #d9534f"></span>
|
||||||
|
TranscodeJob - Processing queue items
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="job-flow">Job Flow</h2>
|
||||||
|
<div class="diagram-container">
|
||||||
|
<div class="diagram">
|
||||||
|
<h3>Job Lifecycle</h3>
|
||||||
|
<object
|
||||||
|
type="image/svg+xml"
|
||||||
|
data="architecture/03-job-flow.svg"
|
||||||
|
>
|
||||||
|
<img src="architecture/03-job-flow.svg" alt="Job Flow" />
|
||||||
|
</object>
|
||||||
|
<a href="architecture/03-job-flow.svg" target="_blank"
|
||||||
|
>Open full size</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<h3>Job States</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span class="color-box" style="background: #ffc107"></span>
|
||||||
|
PENDING - Waiting in queue
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="color-box" style="background: #17a2b8"></span>
|
||||||
|
PROCESSING - Worker executing
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="color-box" style="background: #28a745"></span>
|
||||||
|
COMPLETED - Success
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="color-box" style="background: #dc3545"></span>
|
||||||
|
FAILED - Error occurred
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="color-box" style="background: #6c757d"></span>
|
||||||
|
CANCELLED - User cancelled
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="media-storage">Media Storage</h2>
|
||||||
|
<div class="diagram-container">
|
||||||
|
<p>
|
||||||
|
MPR separates media into <strong>input</strong> and
|
||||||
|
<strong>output</strong> paths, each independently configurable.
|
||||||
|
File paths are stored
|
||||||
|
<strong>relative to their respective root</strong> to ensure
|
||||||
|
portability between local development and cloud deployments (AWS
|
||||||
|
S3, etc.).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<h3>Input / Output Separation</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span class="color-box" style="background: #4a90d9"></span>
|
||||||
|
<code>MEDIA_IN</code> - Source media files to process
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="color-box" style="background: #50b050"></span>
|
||||||
|
<code>MEDIA_OUT</code> - Transcoded/trimmed output files
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Why Relative Paths?</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Portability: Same database works locally and in cloud</li>
|
||||||
|
<li>Flexibility: Easy to switch between storage backends</li>
|
||||||
|
<li>Simplicity: No need to update paths when migrating</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<h3>Local Development</h3>
|
||||||
|
<pre><code>MEDIA_IN=/app/media/in
|
||||||
|
MEDIA_OUT=/app/media/out
|
||||||
|
|
||||||
|
/app/media/
|
||||||
|
├── in/ # Source files
|
||||||
|
│ ├── video1.mp4
|
||||||
|
│ └── subfolder/video3.mp4
|
||||||
|
└── out/ # Transcoded output
|
||||||
|
└── video1_h264.mp4</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<h3>AWS/Cloud Deployment</h3>
|
||||||
|
<pre><code>MEDIA_IN=s3://source-bucket/media/
|
||||||
|
MEDIA_OUT=s3://output-bucket/transcoded/
|
||||||
|
MEDIA_BASE_URL=https://source-bucket.s3.amazonaws.com/media/</code></pre>
|
||||||
|
<p>
|
||||||
|
Database paths remain unchanged (already relative). Just upload
|
||||||
|
files to S3 and update environment variables.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<h3>API (GraphQL)</h3>
|
||||||
|
<p>
|
||||||
|
All client interactions go through GraphQL at
|
||||||
|
<code>/graphql</code>.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>scanMediaFolder</code> - Scan S3 bucket for media
|
||||||
|
files
|
||||||
|
</li>
|
||||||
|
<li><code>createJob</code> - Create transcode/trim job</li>
|
||||||
|
<li>
|
||||||
|
<code>cancelJob / retryJob</code> - Job lifecycle management
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>updateAsset / deleteAsset</code> - Asset management
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Supported File Types:</strong></p>
|
||||||
|
<p>
|
||||||
|
Video: mp4, mkv, avi, mov, webm, flv, wmv, m4v<br />
|
||||||
|
Audio: mp3, wav, flac, aac, ogg, m4a
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Access Points</h2>
|
||||||
|
<pre><code># Add to /etc/hosts
|
||||||
|
127.0.0.1 mpr.local.ar
|
||||||
|
|
||||||
|
# URLs
|
||||||
|
http://mpr.local.ar/admin - Django Admin
|
||||||
|
http://mpr.local.ar/graphql - GraphiQL IDE
|
||||||
|
http://mpr.local.ar/ - Timeline UI</code></pre>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
125
docs/media-storage.html
Normal file
125
docs/media-storage.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<h1>Media Storage Architecture</h1>
|
||||||
|
<h2>Overview</h2>
|
||||||
|
<p>MPR separates media into <strong>input</strong> and <strong>output</strong> paths, each independently configurable. File paths are stored <strong>relative to their respective root</strong> to ensure portability between local development and cloud deployments (AWS S3, etc.).</p>
|
||||||
|
<h2>Storage Strategy</h2>
|
||||||
|
<h3>Input / Output Separation</h3>
|
||||||
|
<p>| Path | Env Var | Purpose |
|
||||||
|
|------|---------|---------|
|
||||||
|
| <code>MEDIA_IN</code> | <code>/app/media/in</code> | Source media files to process |
|
||||||
|
| <code>MEDIA_OUT</code> | <code>/app/media/out</code> | Transcoded/trimmed output files |</p>
|
||||||
|
<p>These can point to different locations or even different servers/buckets in production.</p>
|
||||||
|
<h3>File Path Storage</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Database</strong>: Stores only the relative path (e.g., <code>videos/sample.mp4</code>)</li>
|
||||||
|
<li><strong>Input Root</strong>: Configurable via <code>MEDIA_IN</code> env var</li>
|
||||||
|
<li><strong>Output Root</strong>: Configurable via <code>MEDIA_OUT</code> env var</li>
|
||||||
|
<li><strong>Serving</strong>: Base URL configurable via <code>MEDIA_BASE_URL</code> env var</li>
|
||||||
|
</ul>
|
||||||
|
<h3>Why Relative Paths?</h3>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Portability</strong>: Same database works locally and in cloud</li>
|
||||||
|
<li><strong>Flexibility</strong>: Easy to switch between storage backends</li>
|
||||||
|
<li><strong>Simplicity</strong>: No need to update paths when migrating</li>
|
||||||
|
</ol>
|
||||||
|
<h2>Local Development</h2>
|
||||||
|
<h3>Configuration</h3>
|
||||||
|
<p><code>bash
|
||||||
|
MEDIA_IN=/app/media/in
|
||||||
|
MEDIA_OUT=/app/media/out</code></p>
|
||||||
|
<h3>File Structure</h3>
|
||||||
|
<p><code>/app/media/
|
||||||
|
├── in/ # Source files
|
||||||
|
│ ├── video1.mp4
|
||||||
|
│ ├── video2.mp4
|
||||||
|
│ └── subfolder/
|
||||||
|
│ └── video3.mp4
|
||||||
|
└── out/ # Transcoded output
|
||||||
|
├── video1_h264.mp4
|
||||||
|
└── video2_trimmed.mp4</code></p>
|
||||||
|
<h3>Database Storage</h3>
|
||||||
|
<p>```</p>
|
||||||
|
<h1>Source assets (scanned from media/in)</h1>
|
||||||
|
<p>filename: video1.mp4
|
||||||
|
file_path: video1.mp4</p>
|
||||||
|
<p>filename: video3.mp4
|
||||||
|
file_path: subfolder/video3.mp4
|
||||||
|
```</p>
|
||||||
|
<h3>URL Serving</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Nginx serves input via <code>location /media/in { alias /app/media/in; }</code></li>
|
||||||
|
<li>Nginx serves output via <code>location /media/out { alias /app/media/out; }</code></li>
|
||||||
|
<li>Frontend accesses: <code>http://mpr.local.ar/media/in/video1.mp4</code></li>
|
||||||
|
<li>Video player: <code><video src="/media/in/video1.mp4" /></code></li>
|
||||||
|
</ul>
|
||||||
|
<h2>AWS/Cloud Deployment</h2>
|
||||||
|
<h3>S3 Configuration</h3>
|
||||||
|
<p>```bash</p>
|
||||||
|
<h1>Input and output can be different buckets/paths</h1>
|
||||||
|
<p>MEDIA_IN=s3://source-bucket/media/
|
||||||
|
MEDIA_OUT=s3://output-bucket/transcoded/
|
||||||
|
MEDIA_BASE_URL=https://source-bucket.s3.amazonaws.com/media/
|
||||||
|
```</p>
|
||||||
|
<h3>S3 Structure</h3>
|
||||||
|
<p>```
|
||||||
|
s3://source-bucket/media/
|
||||||
|
├── video1.mp4
|
||||||
|
└── subfolder/
|
||||||
|
└── video3.mp4</p>
|
||||||
|
<p>s3://output-bucket/transcoded/
|
||||||
|
├── video1_h264.mp4
|
||||||
|
└── video2_trimmed.mp4
|
||||||
|
```</p>
|
||||||
|
<h3>Database Storage (Same!)</h3>
|
||||||
|
<p>```
|
||||||
|
filename: video1.mp4
|
||||||
|
file_path: video1.mp4</p>
|
||||||
|
<p>filename: video3.mp4
|
||||||
|
file_path: subfolder/video3.mp4
|
||||||
|
```</p>
|
||||||
|
<h2>API Endpoints</h2>
|
||||||
|
<h3>Scan Media Folder</h3>
|
||||||
|
<p><code>http
|
||||||
|
POST /api/assets/scan</code></p>
|
||||||
|
<p><strong>Behavior:</strong>
|
||||||
|
1. Recursively scans <code>MEDIA_IN</code> directory
|
||||||
|
2. Finds all video/audio files (mp4, mkv, avi, mov, mp3, wav, etc.)
|
||||||
|
3. Stores paths <strong>relative to MEDIA_IN</strong>
|
||||||
|
4. Skips already-registered files (by filename)
|
||||||
|
5. Returns summary: <code>{ found, registered, skipped, files }</code></p>
|
||||||
|
<h3>Create Job</h3>
|
||||||
|
<p>```http
|
||||||
|
POST /api/jobs/
|
||||||
|
Content-Type: application/json</p>
|
||||||
|
<p>{
|
||||||
|
"source_asset_id": "uuid",
|
||||||
|
"preset_id": "uuid",
|
||||||
|
"trim_start": 10.0,
|
||||||
|
"trim_end": 30.0
|
||||||
|
}
|
||||||
|
```</p>
|
||||||
|
<p><strong>Behavior:</strong>
|
||||||
|
- Server sets <code>output_path</code> using <code>MEDIA_OUT</code> + generated filename
|
||||||
|
- Output goes to the output directory, not alongside source files</p>
|
||||||
|
<h2>Migration Guide</h2>
|
||||||
|
<h3>Moving from Local to S3</h3>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<p><strong>Upload source files to S3:</strong>
|
||||||
|
<code>bash
|
||||||
|
aws s3 sync /app/media/in/ s3://source-bucket/media/
|
||||||
|
aws s3 sync /app/media/out/ s3://output-bucket/transcoded/</code></p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><strong>Update environment variables:</strong>
|
||||||
|
<code>bash
|
||||||
|
MEDIA_IN=s3://source-bucket/media/
|
||||||
|
MEDIA_OUT=s3://output-bucket/transcoded/
|
||||||
|
MEDIA_BASE_URL=https://source-bucket.s3.amazonaws.com/media/</code></p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p><strong>Database paths remain unchanged</strong> (already relative)</p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<h2>Supported File Types</h2>
|
||||||
|
<p><strong>Video:</strong> <code>.mp4</code>, <code>.mkv</code>, <code>.avi</code>, <code>.mov</code>, <code>.webm</code>, <code>.flv</code>, <code>.wmv</code>, <code>.m4v</code>
|
||||||
|
<strong>Audio:</strong> <code>.mp3</code>, <code>.wav</code>, <code>.flac</code>, <code>.aac</code>, <code>.ogg</code>, <code>.m4a</code></p>
|
||||||
@@ -6,16 +6,6 @@
|
|||||||
"output": "mpr/media_assets/models.py",
|
"output": "mpr/media_assets/models.py",
|
||||||
"include": ["dataclasses", "enums"]
|
"include": ["dataclasses", "enums"]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"target": "pydantic",
|
|
||||||
"output": "api/schema/",
|
|
||||||
"include": ["dataclasses", "enums"],
|
|
||||||
"name_map": {
|
|
||||||
"TranscodeJob": "Job",
|
|
||||||
"MediaAsset": "Asset",
|
|
||||||
"TranscodePreset": "Preset"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"target": "graphene",
|
"target": "graphene",
|
||||||
"output": "api/schema/graphql.py",
|
"output": "api/schema/graphql.py",
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ This module exports all dataclasses, enums, and constants that the generator
|
|||||||
should process. Add new models here to have them included in generation.
|
should process. Add new models here to have them included in generation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .api import CreateJobRequest, ScanResult, SystemStatus
|
from .api import (
|
||||||
|
CreateJobRequest,
|
||||||
|
DeleteResult,
|
||||||
|
ScanResult,
|
||||||
|
SystemStatus,
|
||||||
|
UpdateAssetRequest,
|
||||||
|
)
|
||||||
from .grpc import (
|
from .grpc import (
|
||||||
GRPC_SERVICE,
|
GRPC_SERVICE,
|
||||||
CancelRequest,
|
CancelRequest,
|
||||||
@@ -26,7 +32,14 @@ DATACLASSES = [MediaAsset, TranscodePreset, TranscodeJob]
|
|||||||
|
|
||||||
# API request/response models - generates TypeScript only (no Django)
|
# API request/response models - generates TypeScript only (no Django)
|
||||||
# WorkerStatus from grpc.py is reused here
|
# WorkerStatus from grpc.py is reused here
|
||||||
API_MODELS = [CreateJobRequest, SystemStatus, ScanResult, WorkerStatus]
|
API_MODELS = [
|
||||||
|
CreateJobRequest,
|
||||||
|
UpdateAssetRequest,
|
||||||
|
SystemStatus,
|
||||||
|
ScanResult,
|
||||||
|
DeleteResult,
|
||||||
|
WorkerStatus,
|
||||||
|
]
|
||||||
|
|
||||||
# Status enums - included in generated code
|
# Status enums - included in generated code
|
||||||
ENUMS = [AssetStatus, JobStatus]
|
ENUMS = [AssetStatus, JobStatus]
|
||||||
@@ -50,6 +63,8 @@ __all__ = [
|
|||||||
"TranscodeJob",
|
"TranscodeJob",
|
||||||
# API Models
|
# API Models
|
||||||
"CreateJobRequest",
|
"CreateJobRequest",
|
||||||
|
"UpdateAssetRequest",
|
||||||
|
"DeleteResult",
|
||||||
"ScanResult",
|
"ScanResult",
|
||||||
"SystemStatus",
|
"SystemStatus",
|
||||||
# Enums
|
# Enums
|
||||||
|
|||||||
@@ -40,4 +40,19 @@ class ScanResult:
|
|||||||
files: List[str] = field(default_factory=list)
|
files: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UpdateAssetRequest:
|
||||||
|
"""Request body for updating asset metadata."""
|
||||||
|
|
||||||
|
comments: Optional[str] = None
|
||||||
|
tags: Optional[List[str]] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DeleteResult:
|
||||||
|
"""Result of a delete operation."""
|
||||||
|
|
||||||
|
ok: bool = False
|
||||||
|
|
||||||
|
|
||||||
# Note: WorkerStatus is defined in grpc.py and reused here
|
# Note: WorkerStatus is defined in grpc.py and reused here
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ export interface CreateJobRequest {
|
|||||||
priority: number;
|
priority: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateAssetRequest {
|
||||||
|
comments: string | null;
|
||||||
|
tags: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SystemStatus {
|
export interface SystemStatus {
|
||||||
status: string;
|
status: string;
|
||||||
version: string;
|
version: string;
|
||||||
@@ -94,6 +99,10 @@ export interface ScanResult {
|
|||||||
files: string[];
|
files: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeleteResult {
|
||||||
|
ok: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WorkerStatus {
|
export interface WorkerStatus {
|
||||||
available: boolean;
|
available: boolean;
|
||||||
active_jobs: number;
|
active_jobs: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user