Files
KrisandClaude Sonnet 5 4877e0a779 Initial commit: Aurora Moments self-hosted photo/video sharing platform
Django/DRF + PostgreSQL/Redis/Celery backend with accounts, library,
moments, and notifications apps; React/TypeScript/Vite frontend with
Instagram-style sidebar nav, feed, profiles, nested comment replies,
notifications, and photo/video upload with ffmpeg-generated posters.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-10 10:28:53 -05:00

124 lines
4.9 KiB
Python

from django.conf import settings
from django.db import models
from apps.core.models import Audience, TimeStampedModel
def library_upload_path(instance, filename):
return f"library/{instance.owner_id}/originals/{filename}"
def rendition_upload_path(instance, filename):
return f"library/{instance.library_item.owner_id}/renditions/{instance.kind}/{filename}"
class LibraryItem(TimeStampedModel):
"""A single photo or video backed up to a user's private library.
Nothing here is visible to anyone but the owner until it is attached to
a Moment or a shared Album."""
class MediaType(models.TextChoices):
PHOTO = "photo", "Photo"
VIDEO = "video", "Video"
class ProcessingStatus(models.TextChoices):
PENDING = "pending", "Pending"
PROCESSING = "processing", "Processing"
READY = "ready", "Ready"
FAILED = "failed", "Failed"
owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="library_items", on_delete=models.CASCADE)
file = models.FileField(upload_to=library_upload_path)
media_type = models.CharField(max_length=10, choices=MediaType.choices)
original_filename = models.CharField(max_length=255)
file_size = models.PositiveBigIntegerField(default=0)
width = models.PositiveIntegerField(null=True, blank=True)
height = models.PositiveIntegerField(null=True, blank=True)
duration_seconds = models.FloatField(null=True, blank=True)
captured_at = models.DateTimeField(null=True, blank=True)
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
camera_make = models.CharField(max_length=100, blank=True)
camera_model = models.CharField(max_length=100, blank=True)
checksum = models.CharField(max_length=64, db_index=True, help_text="sha256 of the original file, used for dedup")
processing_status = models.CharField(
max_length=12, choices=ProcessingStatus.choices, default=ProcessingStatus.PENDING
)
class Meta:
unique_together = ("owner", "checksum")
ordering = ["-captured_at", "-created_at"]
def __str__(self):
return f"{self.original_filename} ({self.owner})"
class MediaRendition(models.Model):
"""A derived, smaller version of a LibraryItem (thumbnail/feed/large),
generated asynchronously so the original never has to be served to the
browser just to render a grid."""
class Kind(models.TextChoices):
THUMBNAIL = "thumbnail", "Thumbnail"
FEED = "feed", "Feed"
LARGE = "large", "Large / Fullscreen"
library_item = models.ForeignKey(LibraryItem, related_name="renditions", on_delete=models.CASCADE)
kind = models.CharField(max_length=10, choices=Kind.choices)
file = models.FileField(upload_to=rendition_upload_path)
width = models.PositiveIntegerField()
height = models.PositiveIntegerField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ("library_item", "kind")
def __str__(self):
return f"{self.library_item_id} - {self.kind}"
class Album(TimeStampedModel):
owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="albums", on_delete=models.CASCADE)
name = models.CharField(max_length=150)
description = models.TextField(blank=True)
cover_item = models.ForeignKey(
LibraryItem, related_name="+", on_delete=models.SET_NULL, null=True, blank=True
)
audience = models.CharField(max_length=10, choices=Audience.choices, default=Audience.ONLY_ME)
def __str__(self):
return self.name
class AlbumItem(models.Model):
album = models.ForeignKey(Album, related_name="items", on_delete=models.CASCADE)
library_item = models.ForeignKey(LibraryItem, related_name="album_items", on_delete=models.CASCADE)
added_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="+", on_delete=models.CASCADE)
added_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ("album", "library_item")
ordering = ["-added_at"]
class AlbumCollaborator(models.Model):
class Role(models.TextChoices):
VIEWER = "viewer", "Viewer"
CONTRIBUTOR = "contributor", "Contributor"
ADMIN = "admin", "Admin"
class Status(models.TextChoices):
PENDING = "pending", "Pending"
ACCEPTED = "accepted", "Accepted"
album = models.ForeignKey(Album, related_name="collaborators", on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="album_invites", on_delete=models.CASCADE)
role = models.CharField(max_length=11, choices=Role.choices, default=Role.VIEWER)
invited_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="+", on_delete=models.CASCADE)
status = models.CharField(max_length=8, choices=Status.choices, default=Status.PENDING)
class Meta:
unique_together = ("album", "user")