- Implement Docker image-based deployment (Option 1) * Code immutable in image, no volume override * Eliminated init-data.sh manual step * Simplified deployment process - Unified persistence in data/ folder * Moved nginx.conf and nginx-custom-domains.conf to data/ * All runtime configs and data in single location * Clear separation: repo (source) vs data/ (runtime) - Archive legacy features * Groups blueprint and templates removed * Legacy playlist routes redirected to content area * Organized in old_code_documentation/ - Added network migration support * New migrate_network.sh script for IP changes * Regenerates SSL certs for new IP * Updates database configuration * Tested workflow: clone → deploy → migrate - Enhanced deploy.sh * Creates data directories * Copies nginx configs from repo to data/ * Validates file existence before deployment * Prevents incomplete deployments - Updated documentation * QUICK_DEPLOYMENT.md shows 4-step workflow * Complete deployment workflow documented * Migration procedures included - Production ready deployment workflow: 1. Clone & setup (.env configuration) 2. Deploy (./deploy.sh) 3. Migrate network (./migrate_network.sh if needed) 4. Normal operations (docker compose restart)
311 lines
11 KiB
Python
311 lines
11 KiB
Python
"""Playlist blueprint for managing player playlists."""
|
|
from flask import (Blueprint, render_template, request, redirect, url_for,
|
|
flash, jsonify, current_app)
|
|
from flask_login import login_required
|
|
from sqlalchemy import desc, update
|
|
import os
|
|
|
|
from app.extensions import db, cache
|
|
from app.models import Player, Content, Playlist
|
|
from app.models.playlist import playlist_content
|
|
from app.utils.logger import log_action
|
|
|
|
playlist_bp = Blueprint('playlist', __name__, url_prefix='/playlist')
|
|
|
|
|
|
@playlist_bp.route('/<int:player_id>')
|
|
@login_required
|
|
def manage_playlist(player_id: int):
|
|
"""Legacy route - redirect to new content management area."""
|
|
player = Player.query.get_or_404(player_id)
|
|
|
|
if player.playlist_id:
|
|
# Redirect to the new content management interface
|
|
return redirect(url_for('content.manage_playlist_content', playlist_id=player.playlist_id))
|
|
else:
|
|
# Player has no playlist assigned
|
|
flash('This player has no playlist assigned.', 'warning')
|
|
return redirect(url_for('players.manage_player', player_id=player_id))
|
|
|
|
|
|
@playlist_bp.route('/<int:player_id>/add', methods=['POST'])
|
|
@login_required
|
|
def add_to_playlist(player_id: int):
|
|
"""Add content to player's playlist."""
|
|
player = Player.query.get_or_404(player_id)
|
|
|
|
if not player.playlist_id:
|
|
flash('Player has no playlist assigned.', 'warning')
|
|
return redirect(url_for('playlist.manage_playlist', player_id=player_id))
|
|
|
|
try:
|
|
content_id = request.form.get('content_id', type=int)
|
|
duration = request.form.get('duration', type=int, default=10)
|
|
|
|
if not content_id:
|
|
flash('Please select content.', 'warning')
|
|
return redirect(url_for('playlist.manage_playlist', player_id=player_id))
|
|
|
|
content = Content.query.get_or_404(content_id)
|
|
playlist = Playlist.query.get(player.playlist_id)
|
|
|
|
# Get max position
|
|
from sqlalchemy import select, func
|
|
max_pos = db.session.execute(
|
|
select(func.max(playlist_content.c.position)).where(
|
|
playlist_content.c.playlist_id == playlist.id
|
|
)
|
|
).scalar() or 0
|
|
|
|
# Add to playlist_content association table
|
|
stmt = playlist_content.insert().values(
|
|
playlist_id=playlist.id,
|
|
content_id=content.id,
|
|
position=max_pos + 1,
|
|
duration=duration
|
|
)
|
|
db.session.execute(stmt)
|
|
|
|
# Increment playlist version
|
|
playlist.increment_version()
|
|
|
|
db.session.commit()
|
|
cache.clear()
|
|
|
|
log_action('info', f'Added "{content.filename}" to playlist for player "{player.name}"')
|
|
flash(f'Added "{content.filename}" to playlist.', 'success')
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
log_action('error', f'Error adding to playlist: {str(e)}')
|
|
flash('Error adding to playlist.', 'danger')
|
|
|
|
return redirect(url_for('playlist.manage_playlist', player_id=player_id))
|
|
|
|
|
|
@playlist_bp.route('/<int:player_id>/remove/<int:content_id>', methods=['POST'])
|
|
@login_required
|
|
def remove_from_playlist(player_id: int, content_id: int):
|
|
"""Remove content from player's playlist."""
|
|
player = Player.query.get_or_404(player_id)
|
|
|
|
if not player.playlist_id:
|
|
flash('Player has no playlist assigned.', 'danger')
|
|
return redirect(url_for('playlist.manage_playlist', player_id=player_id))
|
|
|
|
try:
|
|
content = Content.query.get_or_404(content_id)
|
|
playlist = Playlist.query.get(player.playlist_id)
|
|
filename = content.filename
|
|
|
|
# Remove from playlist_content association table
|
|
from sqlalchemy import delete
|
|
stmt = delete(playlist_content).where(
|
|
(playlist_content.c.playlist_id == playlist.id) &
|
|
(playlist_content.c.content_id == content_id)
|
|
)
|
|
db.session.execute(stmt)
|
|
|
|
# Reorder remaining content
|
|
from sqlalchemy import select
|
|
remaining = db.session.execute(
|
|
select(playlist_content.c.content_id, playlist_content.c.position).where(
|
|
playlist_content.c.playlist_id == playlist.id
|
|
).order_by(playlist_content.c.position)
|
|
).fetchall()
|
|
|
|
for idx, row in enumerate(remaining, start=1):
|
|
stmt = update(playlist_content).where(
|
|
(playlist_content.c.playlist_id == playlist.id) &
|
|
(playlist_content.c.content_id == row.content_id)
|
|
).values(position=idx)
|
|
db.session.execute(stmt)
|
|
|
|
# Increment playlist version
|
|
playlist.increment_version()
|
|
|
|
db.session.commit()
|
|
cache.clear()
|
|
|
|
log_action('info', f'Removed "{filename}" from playlist for player "{player.name}"')
|
|
flash(f'Removed "{filename}" from playlist.', 'success')
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
log_action('error', f'Error removing from playlist: {str(e)}')
|
|
flash('Error removing from playlist.', 'danger')
|
|
|
|
return redirect(url_for('playlist.manage_playlist', player_id=player_id))
|
|
|
|
|
|
@playlist_bp.route('/<int:player_id>/reorder', methods=['POST'])
|
|
@login_required
|
|
def reorder_playlist(player_id: int):
|
|
"""Reorder playlist items."""
|
|
player = Player.query.get_or_404(player_id)
|
|
|
|
if not player.playlist_id:
|
|
return jsonify({'success': False, 'message': 'Player has no playlist'}), 400
|
|
|
|
try:
|
|
playlist = Playlist.query.get(player.playlist_id)
|
|
|
|
# Get new order from JSON
|
|
data = request.get_json()
|
|
content_ids = data.get('content_ids', [])
|
|
|
|
if not content_ids:
|
|
return jsonify({'success': False, 'message': 'No content IDs provided'}), 400
|
|
|
|
# Update positions in association table
|
|
for idx, content_id in enumerate(content_ids, start=1):
|
|
stmt = update(playlist_content).where(
|
|
(playlist_content.c.playlist_id == playlist.id) &
|
|
(playlist_content.c.content_id == content_id)
|
|
).values(position=idx)
|
|
db.session.execute(stmt)
|
|
|
|
# Increment playlist version
|
|
playlist.increment_version()
|
|
|
|
db.session.commit()
|
|
cache.clear()
|
|
|
|
log_action('info', f'Reordered playlist for player "{player.name}" (version {playlist.version})')
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Playlist reordered successfully',
|
|
'version': playlist.version
|
|
})
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
log_action('error', f'Error reordering playlist: {str(e)}')
|
|
return jsonify({'success': False, 'message': str(e)}), 500
|
|
|
|
|
|
@playlist_bp.route('/<int:player_id>/update-duration/<int:content_id>', methods=['POST'])
|
|
@login_required
|
|
def update_duration(player_id: int, content_id: int):
|
|
"""Update content duration in playlist."""
|
|
player = Player.query.get_or_404(player_id)
|
|
|
|
if not player.playlist_id:
|
|
return jsonify({'success': False, 'message': 'Player has no playlist'}), 400
|
|
|
|
try:
|
|
playlist = Playlist.query.get(player.playlist_id)
|
|
content = Content.query.get_or_404(content_id)
|
|
|
|
duration = request.form.get('duration', type=int)
|
|
|
|
if not duration or duration < 1:
|
|
return jsonify({'success': False, 'message': 'Invalid duration'}), 400
|
|
|
|
# Update duration in association table
|
|
stmt = update(playlist_content).where(
|
|
(playlist_content.c.playlist_id == playlist.id) &
|
|
(playlist_content.c.content_id == content_id)
|
|
).values(duration=duration)
|
|
db.session.execute(stmt)
|
|
|
|
# Increment playlist version
|
|
playlist.increment_version()
|
|
|
|
db.session.commit()
|
|
cache.clear()
|
|
|
|
log_action('info', f'Updated duration for "{content.filename}" in player "{player.name}" playlist')
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Duration updated',
|
|
'version': playlist.version
|
|
})
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
log_action('error', f'Error updating duration: {str(e)}')
|
|
return jsonify({'success': False, 'message': str(e)}), 500
|
|
|
|
|
|
@playlist_bp.route('/<int:player_id>/update-muted/<int:content_id>', methods=['POST'])
|
|
@login_required
|
|
def update_muted(player_id: int, content_id: int):
|
|
"""Update content muted setting in playlist."""
|
|
player = Player.query.get_or_404(player_id)
|
|
|
|
if not player.playlist_id:
|
|
return jsonify({'success': False, 'message': 'Player has no playlist'}), 400
|
|
|
|
try:
|
|
playlist = Playlist.query.get(player.playlist_id)
|
|
content = Content.query.get_or_404(content_id)
|
|
|
|
muted = request.form.get('muted', 'true').lower() == 'true'
|
|
|
|
# Update muted in association table
|
|
stmt = update(playlist_content).where(
|
|
(playlist_content.c.playlist_id == playlist.id) &
|
|
(playlist_content.c.content_id == content_id)
|
|
).values(muted=muted)
|
|
db.session.execute(stmt)
|
|
|
|
# Increment playlist version
|
|
playlist.increment_version()
|
|
|
|
db.session.commit()
|
|
cache.clear()
|
|
|
|
log_action('info', f'Updated muted={muted} for "{content.filename}" in player "{player.name}" playlist')
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Audio setting updated',
|
|
'muted': muted,
|
|
'version': playlist.version
|
|
})
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
log_action('error', f'Error updating muted setting: {str(e)}')
|
|
return jsonify({'success': False, 'message': str(e)}), 500
|
|
|
|
|
|
@playlist_bp.route('/<int:player_id>/clear', methods=['POST'])
|
|
@login_required
|
|
def clear_playlist(player_id: int):
|
|
"""Clear all content from player's playlist."""
|
|
player = Player.query.get_or_404(player_id)
|
|
|
|
if not player.playlist_id:
|
|
flash('Player has no playlist assigned.', 'warning')
|
|
return redirect(url_for('playlist.manage_playlist', player_id=player_id))
|
|
|
|
try:
|
|
playlist = Playlist.query.get(player.playlist_id)
|
|
|
|
# Delete all content from playlist
|
|
from sqlalchemy import delete
|
|
stmt = delete(playlist_content).where(
|
|
playlist_content.c.playlist_id == playlist.id
|
|
)
|
|
db.session.execute(stmt)
|
|
|
|
# Increment playlist version
|
|
playlist.increment_version()
|
|
|
|
db.session.commit()
|
|
cache.clear()
|
|
|
|
log_action('info', f'Cleared playlist for player "{player.name}"')
|
|
flash('Playlist cleared successfully.', 'success')
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
log_action('error', f'Error clearing playlist: {str(e)}')
|
|
flash('Error clearing playlist.', 'danger')
|
|
|
|
return redirect(url_for('playlist.manage_playlist', player_id=player_id))
|