django and fastapi apps

This commit is contained in:
2026-02-03 12:20:40 -03:00
parent d31a3ed612
commit 67573713bd
54 changed files with 3272 additions and 11 deletions

3
mpr/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ("celery_app",)

16
mpr/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for mpr project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mpr.settings')
application = get_asgi_application()

9
mpr/celery.py Normal file
View File

@@ -0,0 +1,9 @@
import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mpr.settings")
app = Celery("mpr")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()

View File

174
mpr/media_assets/admin.py Normal file
View File

@@ -0,0 +1,174 @@
from django.contrib import admin
from .models import MediaAsset, TranscodeJob, TranscodePreset
@admin.register(MediaAsset)
class MediaAssetAdmin(admin.ModelAdmin):
list_display = [
"filename",
"status",
"duration_display",
"resolution",
"created_at",
]
list_filter = ["status", "video_codec", "audio_codec"]
search_fields = ["filename", "file_path", "comments"]
readonly_fields = ["id", "created_at", "updated_at", "properties"]
fieldsets = [
(None, {"fields": ["id", "filename", "file_path", "status", "error_message"]}),
(
"Media Info",
{
"fields": [
"file_size",
"duration",
"video_codec",
"audio_codec",
"width",
"height",
"framerate",
"bitrate",
]
},
),
("Annotations", {"fields": ["comments", "tags"]}),
(
"Metadata",
{
"classes": ["collapse"],
"fields": ["properties", "created_at", "updated_at"],
},
),
]
def duration_display(self, obj):
if obj.duration:
mins, secs = divmod(int(obj.duration), 60)
hours, mins = divmod(mins, 60)
if hours:
return f"{hours}:{mins:02d}:{secs:02d}"
return f"{mins}:{secs:02d}"
return "-"
duration_display.short_description = "Duration"
def resolution(self, obj):
if obj.width and obj.height:
return f"{obj.width}x{obj.height}"
return "-"
@admin.register(TranscodePreset)
class TranscodePresetAdmin(admin.ModelAdmin):
list_display = ["name", "container", "video_codec", "audio_codec", "is_builtin"]
list_filter = ["is_builtin", "container", "video_codec"]
search_fields = ["name", "description"]
readonly_fields = ["id", "created_at", "updated_at"]
fieldsets = [
(None, {"fields": ["id", "name", "description", "is_builtin"]}),
("Output", {"fields": ["container"]}),
(
"Video",
{
"fields": [
"video_codec",
"video_bitrate",
"video_crf",
"video_preset",
"resolution",
"framerate",
]
},
),
(
"Audio",
{
"fields": [
"audio_codec",
"audio_bitrate",
"audio_channels",
"audio_samplerate",
]
},
),
(
"Advanced",
{
"classes": ["collapse"],
"fields": ["extra_args", "created_at", "updated_at"],
},
),
]
@admin.register(TranscodeJob)
class TranscodeJobAdmin(admin.ModelAdmin):
list_display = [
"id_short",
"source_asset",
"preset",
"status",
"progress_display",
"created_at",
]
list_filter = ["status", "preset"]
search_fields = ["source_asset__filename", "output_filename"]
readonly_fields = [
"id",
"created_at",
"started_at",
"completed_at",
"progress",
"current_frame",
"current_time",
"speed",
"celery_task_id",
"preset_snapshot",
]
raw_id_fields = ["source_asset", "preset", "output_asset"]
fieldsets = [
(None, {"fields": ["id", "source_asset", "status", "error_message"]}),
(
"Configuration",
{
"fields": [
"preset",
"preset_snapshot",
"trim_start",
"trim_end",
"priority",
]
},
),
("Output", {"fields": ["output_filename", "output_path", "output_asset"]}),
(
"Progress",
{"fields": ["progress", "current_frame", "current_time", "speed"]},
),
(
"Worker",
{
"classes": ["collapse"],
"fields": [
"celery_task_id",
"created_at",
"started_at",
"completed_at",
],
},
),
]
def id_short(self, obj):
return str(obj.id)[:8]
id_short.short_description = "ID"
def progress_display(self, obj):
return f"{obj.progress:.1f}%"
progress_display.short_description = "Progress"

7
mpr/media_assets/apps.py Normal file
View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class MediaAssetsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "mpr.media_assets"
verbose_name = "Media Assets"

View File

View File

@@ -0,0 +1,54 @@
# Import builtin presets from schema
import sys
from pathlib import Path
from django.core.management.base import BaseCommand
from mpr.media_assets.models import TranscodePreset
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent.parent.parent))
from schema.models import BUILTIN_PRESETS
class Command(BaseCommand):
help = "Load builtin transcode presets"
def handle(self, *args, **options):
created_count = 0
updated_count = 0
for preset_data in BUILTIN_PRESETS:
name = preset_data["name"]
defaults = {
"description": preset_data.get("description", ""),
"is_builtin": True,
"container": preset_data.get("container", "mp4"),
"video_codec": preset_data.get("video_codec", "libx264"),
"video_bitrate": preset_data.get("video_bitrate"),
"video_crf": preset_data.get("video_crf"),
"video_preset": preset_data.get("video_preset"),
"resolution": preset_data.get("resolution"),
"framerate": preset_data.get("framerate"),
"audio_codec": preset_data.get("audio_codec", "aac"),
"audio_bitrate": preset_data.get("audio_bitrate"),
"audio_channels": preset_data.get("audio_channels"),
"audio_samplerate": preset_data.get("audio_samplerate"),
"extra_args": preset_data.get("extra_args", []),
}
preset, created = TranscodePreset.objects.update_or_create(
name=name, defaults=defaults
)
if created:
created_count += 1
self.stdout.write(self.style.SUCCESS(f"Created: {name}"))
else:
updated_count += 1
self.stdout.write(f"Updated: {name}")
self.stdout.write(
self.style.SUCCESS(
f"Done: {created_count} created, {updated_count} updated"
)
)

View File

@@ -0,0 +1,98 @@
# Generated by Django 6.0.1 on 2026-02-01 15:13
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='TranscodePreset',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('description', models.TextField(blank=True, default='')),
('is_builtin', models.BooleanField(default=False)),
('container', models.CharField(default='mp4', max_length=20)),
('video_codec', models.CharField(default='libx264', max_length=50)),
('video_bitrate', models.CharField(blank=True, max_length=20, null=True)),
('video_crf', models.IntegerField(blank=True, null=True)),
('video_preset', models.CharField(blank=True, max_length=20, null=True)),
('resolution', models.CharField(blank=True, max_length=20, null=True)),
('framerate', models.FloatField(blank=True, null=True)),
('audio_codec', models.CharField(default='aac', max_length=50)),
('audio_bitrate', models.CharField(blank=True, max_length=20, null=True)),
('audio_channels', models.IntegerField(blank=True, null=True)),
('audio_samplerate', models.IntegerField(blank=True, null=True)),
('extra_args', models.JSONField(blank=True, default=list)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='MediaAsset',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('filename', models.CharField(max_length=500)),
('file_path', models.CharField(max_length=1000)),
('status', models.CharField(choices=[('pending', 'Pending Probe'), ('ready', 'Ready'), ('error', 'Error')], default='pending', max_length=20)),
('error_message', models.TextField(blank=True, null=True)),
('file_size', models.BigIntegerField(blank=True, null=True)),
('duration', models.FloatField(blank=True, null=True)),
('video_codec', models.CharField(blank=True, max_length=50, null=True)),
('audio_codec', models.CharField(blank=True, max_length=50, null=True)),
('width', models.IntegerField(blank=True, null=True)),
('height', models.IntegerField(blank=True, null=True)),
('framerate', models.FloatField(blank=True, null=True)),
('bitrate', models.BigIntegerField(blank=True, null=True)),
('properties', models.JSONField(blank=True, default=dict)),
('comments', models.TextField(blank=True, default='')),
('tags', models.JSONField(blank=True, default=list)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['status'], name='media_asset_status_9ea2f2_idx'), models.Index(fields=['created_at'], name='media_asset_created_368039_idx')],
},
),
migrations.CreateModel(
name='TranscodeJob',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('preset_snapshot', models.JSONField(blank=True, default=dict)),
('trim_start', models.FloatField(blank=True, null=True)),
('trim_end', models.FloatField(blank=True, null=True)),
('output_filename', models.CharField(max_length=500)),
('output_path', models.CharField(blank=True, max_length=1000, null=True)),
('status', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed'), ('cancelled', 'Cancelled')], default='pending', max_length=20)),
('progress', models.FloatField(default=0.0)),
('current_frame', models.IntegerField(blank=True, null=True)),
('current_time', models.FloatField(blank=True, null=True)),
('speed', models.CharField(blank=True, max_length=20, null=True)),
('error_message', models.TextField(blank=True, null=True)),
('celery_task_id', models.CharField(blank=True, max_length=100, null=True)),
('priority', models.IntegerField(default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
('started_at', models.DateTimeField(blank=True, null=True)),
('completed_at', models.DateTimeField(blank=True, null=True)),
('output_asset', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_jobs', to='media_assets.mediaasset')),
('source_asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transcode_jobs', to='media_assets.mediaasset')),
('preset', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='jobs', to='media_assets.transcodepreset')),
],
options={
'ordering': ['priority', 'created_at'],
'indexes': [models.Index(fields=['status', 'priority'], name='media_asset_status_e6ac18_idx'), models.Index(fields=['created_at'], name='media_asset_created_ba3a46_idx'), models.Index(fields=['celery_task_id'], name='media_asset_celery__81a88e_idx')],
},
),
]

View File

110
mpr/media_assets/models.py Normal file
View File

@@ -0,0 +1,110 @@
"""
Django ORM Models - GENERATED FILE
Do not edit directly. Modify schema/models/*.py and run:
python schema/generate.py --django
"""
import uuid
from django.db import models
class MediaAsset(models.Model):
"""A video/audio file registered in the system."""
class Status(models.TextChoices):
PENDING = "pending", "Pending"
READY = "ready", "Ready"
ERROR = "error", "Error"
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
filename = models.CharField(max_length=500)
file_path = models.CharField(max_length=1000)
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
error_message = models.TextField(blank=True, default='')
file_size = models.BigIntegerField(null=True, blank=True)
duration = models.FloatField(null=True, blank=True, default=None)
video_codec = models.CharField(max_length=255, null=True, blank=True)
audio_codec = models.CharField(max_length=255, null=True, blank=True)
width = models.IntegerField(null=True, blank=True, default=None)
height = models.IntegerField(null=True, blank=True, default=None)
framerate = models.FloatField(null=True, blank=True, default=None)
bitrate = models.BigIntegerField(null=True, blank=True)
properties = models.JSONField(default=dict, blank=True)
comments = models.TextField(blank=True, default='')
tags = models.JSONField(default=list, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return self.filename
class TranscodePreset(models.Model):
"""A reusable transcoding configuration (like Handbrake presets)."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=255)
description = models.TextField(blank=True, default='')
is_builtin = models.BooleanField(default=False)
container = models.CharField(max_length=255)
video_codec = models.CharField(max_length=255)
video_bitrate = models.CharField(max_length=255, null=True, blank=True)
video_crf = models.IntegerField(null=True, blank=True, default=None)
video_preset = models.CharField(max_length=255, null=True, blank=True)
resolution = models.CharField(max_length=255, null=True, blank=True)
framerate = models.FloatField(null=True, blank=True, default=None)
audio_codec = models.CharField(max_length=255)
audio_bitrate = models.CharField(max_length=255, null=True, blank=True)
audio_channels = models.IntegerField(null=True, blank=True, default=None)
audio_samplerate = models.IntegerField(null=True, blank=True, default=None)
extra_args = models.JSONField(default=list, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return self.name
class TranscodeJob(models.Model):
"""A transcoding or trimming job in the queue."""
class Status(models.TextChoices):
PENDING = "pending", "Pending"
PROCESSING = "processing", "Processing"
COMPLETED = "completed", "Completed"
FAILED = "failed", "Failed"
CANCELLED = "cancelled", "Cancelled"
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
source_asset_id = models.UUIDField()
preset_id = models.UUIDField(null=True, blank=True)
preset_snapshot = models.JSONField(default=dict, blank=True)
trim_start = models.FloatField(null=True, blank=True, default=None)
trim_end = models.FloatField(null=True, blank=True, default=None)
output_filename = models.CharField(max_length=500)
output_path = models.CharField(max_length=1000, null=True, blank=True)
output_asset_id = models.UUIDField(null=True, blank=True)
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
progress = models.FloatField(default=0.0)
current_frame = models.IntegerField(null=True, blank=True, default=None)
current_time = models.FloatField(null=True, blank=True, default=None)
speed = models.CharField(max_length=255, null=True, blank=True)
error_message = models.TextField(blank=True, default='')
celery_task_id = models.CharField(max_length=255, null=True, blank=True)
priority = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
started_at = models.DateTimeField(null=True, blank=True)
completed_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return str(self.id)

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

103
mpr/settings.py Normal file
View File

@@ -0,0 +1,103 @@
"""
Django settings for mpr project.
"""
import os
from pathlib import Path
import environ
BASE_DIR = Path(__file__).resolve().parent.parent
env = environ.Env(
DEBUG=(bool, False),
SECRET_KEY=(str, "dev-secret-key-change-in-production"),
)
environ.Env.read_env(BASE_DIR / ".env")
SECRET_KEY = env("SECRET_KEY")
DEBUG = env("DEBUG")
ALLOWED_HOSTS = ["*"]
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"mpr.media_assets",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "mpr.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "mpr.wsgi.application"
# Database
DATABASE_URL = env("DATABASE_URL", default="sqlite:///db.sqlite3")
if DATABASE_URL.startswith("postgresql"):
DATABASES = {"default": env.db("DATABASE_URL")}
else:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Celery
REDIS_URL = env("REDIS_URL", default="redis://localhost:6379/0")
CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json"

22
mpr/urls.py Normal file
View File

@@ -0,0 +1,22 @@
"""
URL configuration for mpr project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/6.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]

16
mpr/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for mpr project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mpr.settings')
application = get_wsgi_application()