Files
Ske_Signage/app/routes/api.py
2025-07-16 11:19:31 +03:00

219 lines
7.5 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.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