feat: add weblink playlists, SSH player deployment, Caddy HTTPS, build player page
- Content model: add url column + is_weblink() for web page content items - Player model: add deployment tracking fields (status, timestamps, message) - Content blueprint: add add_weblink and add_weblink_to_playlist routes; weblinks auto-deleted when removed from playlist - Players blueprint: add SSH deploy mode in add_player; weblink-aware playlist - API blueprint: weblink URL served directly in playlist response; add /api/deploy/test-ssh and /api/deploy/player endpoints - Admin blueprint: add build-player page (clone from Gitea, write base config); replace nginx status card with Caddy status on HTTPS config page - caddy_manager: rewritten to generate proper HTTPS/internal-CA Caddyfiles and reload Caddy via admin API (/load) for live config updates - ssh_deploy, background_tasks, player_build: new utils for SSH deployment - background_tasks: push Flask app context into background thread so DB updates after deployment complete correctly - ssh_deploy: robust install script detection with passwordless sudo injection (uses SSH credentials, cleaned up after install) - Dockerfile: add git, sshpass, openssh-client, rsync - docker-compose: switch nginx to Caddy on ports 80/443; add port 5000 for dev - Templates: add_player deploy mode UI, weblink form in playlist/upload pages, build_player admin page, Caddy status on HTTPS config page - Migrations: add_url_to_content, add_deployment_fields_to_player - app.py: call db.create_all() on startup for schema bootstrap - config.py: add PLAYER_CODE_DIR and PLAYER_REPO_URL settings
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""Players blueprint for player management and display."""
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, current_app
|
||||
from flask_login import login_required
|
||||
from werkzeug.security import generate_password_hash
|
||||
import secrets
|
||||
@@ -41,7 +41,7 @@ def list():
|
||||
@players_bp.route('/add', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def add_player():
|
||||
"""Add a new player."""
|
||||
"""Add a new player with optional SSH deployment."""
|
||||
if request.method == 'GET':
|
||||
playlists = Playlist.query.filter_by(is_active=True).order_by(Playlist.name).all()
|
||||
return render_template('players/add_player.html', playlists=playlists)
|
||||
@@ -55,6 +55,13 @@ def add_player():
|
||||
orientation = request.form.get('orientation', 'Landscape')
|
||||
playlist_id = request.form.get('playlist_id', '').strip()
|
||||
|
||||
# Get SSH deployment info if provided
|
||||
ssh_hostname = request.form.get('ssh_hostname', '').strip()
|
||||
ssh_username = request.form.get('ssh_username', '').strip()
|
||||
ssh_password = request.form.get('ssh_password', '').strip()
|
||||
ssh_port = int(request.form.get('ssh_port', '22')) if request.form.get('ssh_port') else 22
|
||||
deploy_player = request.form.get('deploy_player', '').strip()
|
||||
|
||||
# Validation
|
||||
if not name or len(name) < 3:
|
||||
flash('Player name must be at least 3 characters long.', 'warning')
|
||||
@@ -102,14 +109,82 @@ def add_player():
|
||||
|
||||
log_action('info', f'Player "{name}" (hostname: {hostname}) created')
|
||||
|
||||
# If deployment requested and SSH credentials provided, trigger background deployment
|
||||
deployment_initiated = False
|
||||
if deploy_player and ssh_hostname and ssh_username and ssh_password:
|
||||
try:
|
||||
from app.utils.background_tasks import background_player_deployment, run_background_task
|
||||
|
||||
# Determine the server address the player should contact.
|
||||
from flask import request as flask_request
|
||||
from app.models.https_config import HTTPSConfig
|
||||
|
||||
server_url = None
|
||||
try:
|
||||
import os
|
||||
from app.utils.player_build import get_player_server_settings, BUILD_META_FILENAME
|
||||
meta_path = os.path.join(current_app.instance_path, BUILD_META_FILENAME)
|
||||
build_srv = get_player_server_settings(meta_path)
|
||||
if build_srv:
|
||||
scheme = 'https' if build_srv['use_https'] else 'http'
|
||||
server_url = f"{scheme}://{build_srv['server_ip']}:{build_srv['port']}"
|
||||
except Exception:
|
||||
server_url = None
|
||||
|
||||
if not server_url:
|
||||
https_cfg = HTTPSConfig.get_config()
|
||||
if https_cfg and https_cfg.https_enabled and (https_cfg.domain or https_cfg.ip_address):
|
||||
host = https_cfg.domain or https_cfg.ip_address
|
||||
cfg_port = https_cfg.port or 443
|
||||
server_url = f"https://{host}:{cfg_port}"
|
||||
else:
|
||||
host = flask_request.host
|
||||
hostname_only = host.split(':')[0]
|
||||
if hostname_only in ('localhost', '127.0.0.1', '') or hostname_only.startswith('127.'):
|
||||
from app.utils.ssh_deploy import detect_server_ip
|
||||
detected_ip = detect_server_ip()
|
||||
if detected_ip:
|
||||
port_part = host.split(':', 1)[1] if ':' in host else ''
|
||||
host = f"{detected_ip}:{port_part}" if port_part else detected_ip
|
||||
server_url = f"{flask_request.scheme}://{host}"
|
||||
|
||||
# Generate API key for player authentication
|
||||
import hashlib
|
||||
api_key = hashlib.sha256(f'{name}:{hostname}'.encode()).hexdigest()[:32]
|
||||
|
||||
# Start deployment in background thread
|
||||
run_background_task(
|
||||
background_player_deployment,
|
||||
hostname=ssh_hostname,
|
||||
username=ssh_username,
|
||||
password=ssh_password,
|
||||
player_name=name,
|
||||
player_id=new_player.id,
|
||||
port=ssh_port,
|
||||
server_url=server_url,
|
||||
server_api_key=api_key,
|
||||
player_hostname=hostname,
|
||||
quickconnect_code=quickconnect_code,
|
||||
orientation=orientation
|
||||
)
|
||||
deployment_initiated = True
|
||||
log_action('info', f'Background deployment initiated for player "{name}" on {ssh_hostname}')
|
||||
except Exception as deploy_err:
|
||||
log_action('error', f'Failed to initiate background deployment for player "{name}": {str(deploy_err)}')
|
||||
|
||||
# Flash detailed success message
|
||||
success_msg = f'''
|
||||
Player "{name}" created successfully!<br>
|
||||
<strong>Auth Code:</strong> {auth_code}<br>
|
||||
<strong>Auth Code:</strong> <code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px;">{auth_code}</code><br>
|
||||
<strong>Hostname:</strong> {hostname}<br>
|
||||
<strong>Quick Connect:</strong> {quickconnect_code}<br>
|
||||
<small>Configure the player with these credentials in app_config.json</small>
|
||||
'''
|
||||
|
||||
if deployment_initiated:
|
||||
success_msg += f'<strong style="color: #0275d8;">⌛ Deployment in Progress</strong> Deploying to {ssh_hostname} in background...<br>'
|
||||
success_msg += '<small>Check player status to see deployment completion</small><br>'
|
||||
|
||||
success_msg += '<small>Configure the player with these credentials in app_config.json</small>'
|
||||
flash(success_msg, 'success')
|
||||
|
||||
return redirect(url_for('players.list'))
|
||||
@@ -426,9 +501,14 @@ def get_player_playlist(player_id: int) -> List[dict]:
|
||||
# Build playlist
|
||||
playlist = []
|
||||
for content in ordered_content:
|
||||
# For weblinks, serve the actual URL directly; for files, serve static path
|
||||
if content.content_type == 'weblink' and content.url:
|
||||
item_url = content.url
|
||||
else:
|
||||
item_url = url_for('static', filename=f'uploads/{content.filename}')
|
||||
playlist.append({
|
||||
'id': content.id,
|
||||
'url': url_for('static', filename=f'uploads/{content.filename}'),
|
||||
'url': item_url,
|
||||
'type': content.content_type,
|
||||
'duration': getattr(content, '_playlist_duration', content.duration or 10),
|
||||
'position': getattr(content, '_playlist_position', 0),
|
||||
|
||||
Reference in New Issue
Block a user