M0: scaffold SPA + API monorepo

Backend (Django 6.1 + DRF):
- Token auth with a custom User model (register/login/logout/me)
- Library models (MediaItem, MediaLocation) and REST endpoints
- File list/detail with search, rating filter, sorting, pagination
- Multipart upload with optional rating/tags/notes
- Range-aware media serving (video seeking) and ffmpeg thumbnails
- scan_files management command for the watched folder

Frontend (React 19 + Vite + TypeScript):
- Catppuccin Mocha design tokens from the design docs
- App shell, token persistence, protected routes
- Library grid with filters, file detail with custom data editor
- Upload page with per-file progress via XHR
- Dev proxy to the Django API
This commit is contained in:
2026-09-17 07:56:47 -05:00
commit 555c25d77b
65 changed files with 5237 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
import os
from rest_framework import serializers
from .models import MediaItem, MediaLocation
from .services import VIDEO_EXTENSIONS
class MediaLocationSerializer(serializers.ModelSerializer):
class Meta:
model = MediaLocation
fields = ["id", "path", "rel_path", "mtime"]
class MediaItemSerializer(serializers.ModelSerializer):
tags = serializers.ListField(
child=serializers.CharField(max_length=100), required=False
)
rating = serializers.ChoiceField(choices=["", "s", "q", "e"], required=False)
locations = MediaLocationSerializer(many=True, read_only=True)
filename = serializers.SerializerMethodField()
extension = serializers.SerializerMethodField()
kind = serializers.SerializerMethodField()
class Meta:
model = MediaItem
fields = [
"md5",
"size",
"rating",
"tags",
"notes",
"filename",
"extension",
"kind",
"locations",
"created_at",
"updated_at",
]
read_only_fields = [
"md5",
"size",
"filename",
"extension",
"kind",
"locations",
"created_at",
"updated_at",
]
def _first_location(self, obj):
locations = list(obj.locations.all())
return locations[0] if locations else None
def get_filename(self, obj):
location = self._first_location(obj)
return os.path.basename(location.rel_path) if location else obj.md5
def get_extension(self, obj):
return os.path.splitext(self.get_filename(obj))[1].lstrip(".").lower()
def get_kind(self, obj):
return "video" if "." + self.get_extension(obj) in VIDEO_EXTENSIONS else "image"