"""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'' @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//original_. 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//original_; 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