"""Player model for digital signage players.""" from datetime import datetime from typing import Optional, List from app.extensions import db class Player(db.Model): """Player model representing a digital signage device. Attributes: id: Primary key name: Display name for the player hostname: Unique hostname/identifier for the player location: Physical location description auth_code: Authentication code for API access (legacy) password_hash: Hashed password for player authentication quickconnect_code: Hashed quick connect code for easy pairing orientation: Display orientation (Landscape/Portrait) status: Current player status (online, offline, error) last_seen: Last activity timestamp playlist_version: Version number for playlist synchronization created_at: Player creation timestamp """ __tablename__ = 'player' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), nullable=False) hostname = db.Column(db.String(255), unique=True, nullable=False, index=True) location = db.Column(db.String(255), nullable=True) auth_code = db.Column(db.String(255), unique=True, nullable=False, index=True) password_hash = db.Column(db.String(255), nullable=False) quickconnect_code = db.Column(db.String(255), nullable=True) orientation = db.Column(db.String(16), default='Landscape', nullable=False) status = db.Column(db.String(50), default='offline', index=True) last_seen = db.Column(db.DateTime, nullable=True, index=True) last_heartbeat = db.Column(db.DateTime, nullable=True, index=True) created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) # Playlist assignment playlist_id = db.Column(db.Integer, db.ForeignKey('playlist.id', ondelete='SET NULL'), nullable=True, index=True) # Relationships playlist = db.relationship('Playlist', back_populates='players') feedback = db.relationship('PlayerFeedback', back_populates='player', cascade='all, delete-orphan', lazy='dynamic') def __repr__(self) -> str: """String representation of Player.""" return f'' @property def is_online(self) -> bool: """Check if player is online (seen in last 5 minutes).""" if not self.last_seen: return False delta = datetime.utcnow() - self.last_seen return delta.total_seconds() < 300 # 5 minutes def update_status(self, status: str) -> None: """Update player status and last seen timestamp. Args: status: New status value """ self.status = status self.last_seen = datetime.utcnow() def set_password(self, password: str) -> None: """Set player password with bcrypt hashing. Args: password: Plain text password """ from app.extensions import bcrypt self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8') def check_password(self, password: str) -> bool: """Verify player password. Args: password: Plain text password to check Returns: True if password matches, False otherwise """ from app.extensions import bcrypt return bcrypt.check_password_hash(self.password_hash, password) def set_quickconnect_code(self, code: str) -> None: """Set quick connect code with bcrypt hashing. Args: code: Plain text quick connect code """ from app.extensions import bcrypt self.quickconnect_code = bcrypt.generate_password_hash(code).decode('utf-8') def check_quickconnect_code(self, code: str) -> bool: """Verify quick connect code. Args: code: Plain text code to check Returns: True if code matches, False otherwise """ if not self.quickconnect_code: return False from app.extensions import bcrypt return bcrypt.check_password_hash(self.quickconnect_code, code) @staticmethod def authenticate(hostname: str, password: str = None, quickconnect_code: str = None) -> Optional['Player']: """Authenticate a player by hostname and password or quickconnect code. Args: hostname: Player hostname password: Player password (optional if using quickconnect) quickconnect_code: Quick connect code (optional if using password) Returns: Player instance if authentication successful, None otherwise """ player = Player.query.filter_by(hostname=hostname).first() if not player: return None # Try password authentication first if password and player.check_password(password): return player # Try quickconnect code authentication if quickconnect_code and player.check_quickconnect_code(quickconnect_code): return player return None