feat: structure

This commit is contained in:
2026-04-06 23:30:19 +02:00
commit 46a0d7aba8
41 changed files with 3480 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
"""
Database models for Mulita
"""
from app.models.photos import Photo
from app.models.folders import Folder, SourceRoot
from app.models.tags import Tag, PhotoTag
from app.models.heaps import Heap, HeapPhoto
from app.models.embeddings import Embedding
__all__ = [
'Photo',
'Folder',
'SourceRoot',
'Tag',
'PhotoTag',
'Heap',
'HeapPhoto',
'Embedding'
]

View File

@@ -0,0 +1,17 @@
"""
Embedding model definition (placeholder for AI features)
"""
from sqlalchemy import Column, String, ForeignKey, LargeBinary
import uuid
from app.database import Base
class Embedding(Base):
"""
Placeholder table for future AI embeddings (CLIP, face recognition, etc.)
"""
__tablename__ = 'embeddings'
photo_id = Column(String, ForeignKey('photos.id', ondelete='CASCADE'), primary_key=True)
model = Column(String) # e.g., 'clip-vit-b32', 'face-recognition', etc.
vector = Column(LargeBinary) # raw float32 bytes for embedding vector

View File

@@ -0,0 +1,43 @@
"""
Folder and SourceRoot model definitions
"""
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey, Index
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
import uuid
from app.database import Base
class SourceRoot(Base):
__tablename__ = 'source_roots'
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
name = Column(String, nullable=False)
path = Column(String, unique=True, nullable=False)
is_active = Column(Boolean, default=True)
added_at = Column(DateTime, server_default=func.now())
# Relationships
folders = relationship("Folder", back_populates="source_root")
class Folder(Base):
__tablename__ = 'folders'
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
name = Column(String, nullable=False)
path = Column(String, unique=True, nullable=False)
parent_id = Column(String, ForeignKey('folders.id'))
source_root_id = Column(String, ForeignKey('source_roots.id'))
photo_count = Column(Integer, default=0)
last_scanned = Column(DateTime)
# Relationships
source_root = relationship("SourceRoot", back_populates="folders")
photos = relationship("Photo", backref="folder")
# Indexes
__table_args__ = (
Index('ix_folders_path', 'path'),
Index('ix_folders_parent_id', 'parent_id'),
Index('ix_folders_source_root_id', 'source_root_id'),
)

View File

@@ -0,0 +1,37 @@
"""
Heap model definitions
"""
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey, Table, Index
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
import uuid
from app.database import Base
# Association table for many-to-many relationship with additional fields
heap_photos = Table(
'heap_photos',
Base.metadata,
Column('heap_id', String, ForeignKey('heaps.id', ondelete='CASCADE'), primary_key=True),
Column('photo_id', String, ForeignKey('photos.id', ondelete='CASCADE'), primary_key=True),
Column('added_at', DateTime, server_default=func.now()),
Column('sort_order', Integer, default=0),
Index('ix_heap_photos_heap_id', 'heap_id'),
Index('ix_heap_photos_photo_id', 'photo_id'),
)
class Heap(Base):
__tablename__ = 'heaps'
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
name = Column(String, nullable=False)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, onupdate=func.now())
is_active = Column(Boolean, default=False) # For active heap feature
# Relationships
photos = relationship("Photo", secondary=heap_photos, backref="heaps")
class HeapPhoto:
"""Helper class for heap-photo associations (not a table model)"""
pass

View File

@@ -0,0 +1,75 @@
"""
Photo model definition
"""
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey, Text, Index
from sqlalchemy.sql import func
from datetime import datetime
import uuid
from app.database import Base
class Photo(Base):
__tablename__ = 'photos'
# Primary key
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
# File information
filepath = Column(String, unique=True, nullable=False)
filename = Column(String, nullable=False)
folder_id = Column(String, ForeignKey('folders.id'))
file_hash = Column(String, index=True) # SHA-256 hash for duplicate detection
# Media information
media_type = Column(String, nullable=False) # 'photo' | 'video' | 'raw' | 'heic'
original_format = Column(String) # 'CR3', 'NEF', 'HEIC', 'MP4', 'JPEG', etc.
width = Column(Integer)
height = Column(Integer)
file_size = Column(Integer)
# Timestamps
taken_at = Column(DateTime) # from EXIF DateTimeOriginal, fallback to file mtime
taken_at_source = Column(String) # 'exif' | 'filesystem' | 'manual'
added_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, onupdate=func.now())
# Trash status
is_trashed = Column(Boolean, default=False)
trashed_at = Column(DateTime)
# Thumbnail paths
thumb_small = Column(String) # path to 240px thumb
thumb_medium = Column(String) # path to 640px thumb
thumb_large = Column(String) # path to 1280px thumb
# Processing status
processing_status = Column(String, default='pending') # 'pending' | 'processing' | 'completed' | 'failed'
processing_error = Column(Text)
# Metadata
exif_json = Column(Text) # full EXIF/XMP blob as JSON
# User-editable fields
user_title = Column(String)
user_notes = Column(Text)
rating = Column(Integer, default=0) # 0-5 stars
color_label = Column(String) # 'red'|'orange'|'yellow'|'green'|'blue'|'purple'|NULL
is_picked = Column(Boolean, default=False)
is_rejected = Column(Boolean, default=False)
# Duplicate detection
is_duplicate = Column(Boolean, default=False)
# Live photo support
live_photo_video_id = Column(String, ForeignKey('photos.id'))
# Indexes for performance
__table_args__ = (
Index('ix_photos_taken_at', 'taken_at'),
Index('ix_photos_folder_id', 'folder_id'),
Index('ix_photos_is_trashed', 'is_trashed'),
Index('ix_photos_rating', 'rating'),
Index('ix_photos_color_label', 'color_label'),
Index('ix_photos_media_type', 'media_type'),
Index('ix_photos_processing_status', 'processing_status'),
)

View File

@@ -0,0 +1,32 @@
"""
Tag model definitions
"""
from sqlalchemy import Column, String, ForeignKey, Table, Index
from sqlalchemy.orm import relationship
import uuid
from app.database import Base
# Association table for many-to-many relationship
photo_tags = Table(
'photo_tags',
Base.metadata,
Column('photo_id', String, ForeignKey('photos.id', ondelete='CASCADE'), primary_key=True),
Column('tag_id', String, ForeignKey('tags.id', ondelete='CASCADE'), primary_key=True),
Index('ix_photo_tags_photo_id', 'photo_id'),
Index('ix_photo_tags_tag_id', 'tag_id'),
)
class Tag(Base):
__tablename__ = 'tags'
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
name = Column(String, unique=True, nullable=False, index=True)
color = Column(String) # Hex color code for UI display
# Relationships
photos = relationship("Photo", secondary=photo_tags, backref="tags")
class PhotoTag:
"""Helper class for photo-tag associations (not a table model)"""
pass