""" API routes for player clients """ from flask import Blueprint, request, jsonify, url_for from app.models.player import Player from app.models.content import Content from app.extensions import bcrypt, db bp = Blueprint('api', __name__) @bp.route('/playlists', methods=['GET']) def get_playlists(): """Get playlist for a player""" hostname = request.args.get('hostname') quickconnect_code = request.args.get('quickconnect_code') # Validate parameters if not hostname or not quickconnect_code: return jsonify({'error': 'Hostname and quick connect code are required'}), 400 # Find player and verify credentials player = Player.query.filter_by(hostname=hostname).first() if not player or not player.verify_quickconnect_code(quickconnect_code): return jsonify({'error': 'Invalid hostname or quick connect code'}), 404 # Update last seen player.last_seen = db.func.current_timestamp() db.session.commit() # Get content based on player's group status if player.is_locked_to_group: # Player is locked to a group - get shared content group = player.locked_to_group player_ids = [p.id for p in group.players] # Get unique content by filename (first occurrence) content_query = ( db.session.query( Content.file_name, db.func.min(Content.id).label('id'), db.func.min(Content.duration).label('duration'), db.func.min(Content.position).label('position'), db.func.min(Content.content_type).label('content_type') ) .filter(Content.player_id.in_(player_ids)) .group_by(Content.file_name) ) content = db.session.query(Content).filter( Content.id.in_([c.id for c in content_query]) ).order_by(Content.position).all() else: # Individual player content content = Content.query.filter_by(player_id=player.id).order_by(Content.position).all() # Build playlist playlist = [] for media in content: playlist.append({ 'file_name': media.file_name, 'url': url_for('content.media', filename=media.file_name, _external=True), 'duration': media.duration, 'content_type': media.content_type, 'position': media.position }) return jsonify({ 'playlist': playlist, 'playlist_version': player.playlist_version, 'hashed_quickconnect': player.quickconnect_password, 'player_id': player.id, 'player_name': player.username }) @bp.route('/playlist_version', methods=['GET']) def get_playlist_version(): """Get playlist version for a player (for checking updates)""" hostname = request.args.get('hostname') quickconnect_code = request.args.get('quickconnect_code') # Validate parameters if not hostname or not quickconnect_code: return jsonify({'error': 'Hostname and quick connect code are required'}), 400 # Find player and verify credentials player = Player.query.filter_by(hostname=hostname).first() if not player or not player.verify_quickconnect_code(quickconnect_code): return jsonify({'error': 'Invalid hostname or quick connect code'}), 404 # Update last seen player.last_seen = db.func.current_timestamp() db.session.commit() return jsonify({ 'playlist_version': player.playlist_version, 'hashed_quickconnect': player.quickconnect_password }) @bp.route('/player_status', methods=['POST']) def update_player_status(): """Update player status (heartbeat)""" data = request.get_json() if not data: return jsonify({'error': 'JSON data required'}), 400 hostname = data.get('hostname') quickconnect_code = data.get('quickconnect_code') if not hostname or not quickconnect_code: return jsonify({'error': 'Hostname and quick connect code are required'}), 400 # Find player and verify credentials player = Player.query.filter_by(hostname=hostname).first() if not player or not player.verify_quickconnect_code(quickconnect_code): return jsonify({'error': 'Invalid hostname or quick connect code'}), 404 # Update player status player.last_seen = db.func.current_timestamp() player.is_active = True # Optional: Update additional status info if provided if 'status' in data: # Could store additional status information in the future pass db.session.commit() return jsonify({ 'success': True, 'playlist_version': player.playlist_version, 'message': 'Status updated successfully' }) @bp.route('/health', methods=['GET']) def health_check(): """Health check endpoint""" return jsonify({ 'status': 'healthy', 'service': 'SKE Digital Signage Server', 'version': '2.0.0' }) @bp.route('/content//remove-from-player', methods=['POST']) def remove_content_from_player(content_id): """Remove content from a specific player""" from flask_login import login_required, current_user # Require authentication for this operation if not current_user.is_authenticated: return jsonify({'error': 'Authentication required'}), 401 data = request.get_json() if not data or 'player_id' not in data: return jsonify({'error': 'Player ID required'}), 400 player_id = data.get('player_id') # Find the content item content = Content.query.filter_by(id=content_id, player_id=player_id).first() if not content: return jsonify({'error': 'Content not found for this player'}), 404 # Remove the content try: db.session.delete(content) db.session.commit() return jsonify({ 'success': True, 'message': f'Content {content.file_name} removed from player' }) except Exception as e: db.session.rollback() return jsonify({'error': f'Failed to remove content: {str(e)}'}), 500 @bp.route('/player//heartbeat', methods=['POST']) def player_heartbeat(player_id): """Update player heartbeat/last seen timestamp""" try: player = Player.query.get_or_404(player_id) player.last_seen = db.func.current_timestamp() db.session.commit() return jsonify({ 'success': True, 'timestamp': player.last_seen.isoformat() if player.last_seen else None }) except Exception as e: db.session.rollback() return jsonify({'error': f'Failed to update heartbeat: {str(e)}'}), 500 @bp.route('/player//content', methods=['GET']) def get_player_content_status(player_id): """Get player content status for checking updates""" try: player = Player.query.get_or_404(player_id) content_count = Content.query.filter_by(player_id=player_id).count() return jsonify({ 'player_id': player_id, 'playlist_version': player.playlist_version, 'content_count': content_count, 'updated': False # Could implement version checking logic here }) except Exception as e: return jsonify({'error': f'Failed to get content status: {str(e)}'}), 500 @bp.route('/content//edit', methods=['POST']) def edit_content_duration(content_id): """Edit content duration""" from flask_login import current_user # Require authentication for this operation if not current_user.is_authenticated: return jsonify({'error': 'Authentication required'}), 401 data = request.get_json() if not data or 'duration' not in data: return jsonify({'error': 'Duration required'}), 400 new_duration = data.get('duration') # Validate duration try: new_duration = int(new_duration) if new_duration < 1 or new_duration > 300: return jsonify({'error': 'Duration must be between 1 and 300 seconds'}), 400 except (ValueError, TypeError): return jsonify({'error': 'Invalid duration value'}), 400 # Find the content item content = Content.query.get(content_id) if not content: return jsonify({'error': 'Content not found'}), 404 # Update the content duration try: old_duration = content.duration content.duration = new_duration # Update player's playlist version to trigger refresh player = Player.query.get(content.player_id) if player: player.increment_playlist_version() db.session.commit() return jsonify({ 'success': True, 'message': f'Content duration updated from {old_duration}s to {new_duration}s', 'new_duration': new_duration }) except Exception as e: db.session.rollback() return jsonify({'error': f'Failed to update content: {str(e)}'}), 500 @bp.errorhandler(404) def api_not_found(error): """API 404 handler""" return jsonify({'error': 'API endpoint not found'}), 404 @bp.errorhandler(500) def api_internal_error(error): """API 500 handler""" return jsonify({'error': 'Internal server error'}), 500