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:
@@ -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"
|
||||
Reference in New Issue
Block a user