268 lines
9.2 KiB
Python
268 lines
9.2 KiB
Python
"""
|
|
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/<int:content_id>/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/<int:player_id>/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/<int:player_id>/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/<int:content_id>/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
|