Files
digiserver-v2/app/models/content.py
T

119 lines
4.5 KiB
Python

"""Content model for media files."""
from datetime import datetime
from typing import Optional, List
from app.extensions import db
class Content(db.Model):
"""Content model representing media files for display.
Attributes:
id: Primary key
filename: Original filename
content_type: Type of content (image, video, pdf, presentation, other)
duration: Default display duration in seconds
file_size: File size in bytes
description: Optional content description
uploaded_at: Upload timestamp
"""
__tablename__ = 'content'
id = db.Column(db.Integer, primary_key=True)
filename = db.Column(db.String(255), nullable=False, unique=True, index=True)
# The pristine original filename as uploaded. `filename` gets overwritten
# with an edited_media/... path when a player edits the content, so this
# column preserves the original so it can always be referenced/restored.
original_filename = db.Column(db.String(255), nullable=True, index=True)
content_type = db.Column(db.String(50), nullable=False, index=True)
duration = db.Column(db.Integer, default=10, nullable=True)
file_size = db.Column(db.BigInteger, nullable=True)
# For 'weblink' content this holds the web page URL to display on the player.
# NULL for file-based content (image/video/pdf).
url = db.Column(db.String(2048), nullable=True)
description = db.Column(db.Text, nullable=True)
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow,
nullable=False, index=True)
# Relationships - many-to-many with playlists
playlists = db.relationship('Playlist', secondary='playlist_content',
back_populates='contents', lazy='dynamic')
groups = db.relationship('Group', secondary='group_content',
back_populates='contents', lazy='dynamic')
def __repr__(self) -> str:
"""String representation of Content."""
return f'<Content {self.filename} (Type={self.content_type})>'
@property
def file_size_mb(self) -> float:
"""Get file size in megabytes."""
if self.file_size:
return round(self.file_size / (1024 * 1024), 2)
return 0.0
@property
def group_count(self) -> int:
"""Get number of groups containing this content."""
return self.groups.count()
@property
def original_display_name(self) -> str:
"""Name of the original (unedited) file for display purposes."""
return self.original_filename or self.filename
@property
def original_media_path(self) -> str:
"""Path (relative to uploads/) where the original unedited file lives.
When a player edits content the original file is archived under
edited_media/<id>/original_<name>. If never edited, the original IS the
current file.
"""
if self.original_filename and self.original_filename != self.filename:
return f"edited_media/{self.id}/original_{self.original_filename}"
return self.filename
@property
def current_media_path(self) -> str:
"""Path (relative to uploads/) of the current/latest version to display."""
return self.filename
def is_image(self) -> bool:
"""Check if content is an image."""
return self.content_type == 'image'
def is_video(self) -> bool:
"""Check if content is a video."""
return self.content_type == 'video'
def is_pdf(self) -> bool:
"""Check if content is a PDF."""
return self.content_type == 'pdf'
def is_weblink(self) -> bool:
"""Check if content is a web link (URL) rather than an uploaded file."""
return self.content_type == 'weblink'
@property
def has_player_edits(self) -> bool:
"""Whether this content has been edited on a player."""
return self.edits.count() > 0
@property
def original_display_name(self) -> str:
"""Display name of the original (unedited) file."""
return self.original_filename or self.filename
@property
def original_media_path(self) -> str:
"""uploads/-relative path of the pristine original file.
After a player edit the original is archived at
edited_media/<id>/original_<name>; before any edit it is simply the
current file.
"""
if self.original_filename and self.filename != self.original_filename:
return f"edited_media/{self.id}/original_{self.original_filename}"
return self.filename