Add edit_on_player_enabled feature for playlist content

- Added edit_on_player_enabled column to playlist_content table
- Updated playlist model to track edit enablement per content item
- Added UI checkbox on upload media page to enable/disable editing
- Added toggle column on manage playlist page for existing content
- Updated API endpoint to return edit_on_player_enabled flag to players
- Fixed docker-entrypoint.sh to use production config
- Supports PDF, Images, and PPTX content types
This commit is contained in:
DigiServer Developer
2025-12-05 21:31:04 +02:00
parent d395240dce
commit 8e43f2bd42
7 changed files with 172 additions and 13 deletions

View File

@@ -401,7 +401,8 @@ def get_cached_playlist(player_id: int) -> List[Dict]:
'duration': content._playlist_duration or content.duration or 10,
'position': content._playlist_position or idx,
'url': content_url, # Full URL for downloads
'description': content.description
'description': content.description,
'edit_on_player_enabled': getattr(content, '_playlist_edit_on_player_enabled', False)
})
return playlist_data

View File

@@ -396,6 +396,47 @@ def update_playlist_content_muted(playlist_id: int, content_id: int):
return jsonify({'success': False, 'message': str(e)}), 500
@content_bp.route('/playlist/<int:playlist_id>/update-edit-enabled/<int:content_id>', methods=['POST'])
@login_required
def update_playlist_content_edit_enabled(playlist_id: int, content_id: int):
"""Update content edit_on_player_enabled setting in playlist."""
playlist = Playlist.query.get_or_404(playlist_id)
try:
content = Content.query.get_or_404(content_id)
edit_enabled = request.form.get('edit_enabled', 'false').lower() == 'true'
from app.models.playlist import playlist_content
from sqlalchemy import update
# Update edit_on_player_enabled in association table
stmt = update(playlist_content).where(
(playlist_content.c.playlist_id == playlist_id) &
(playlist_content.c.content_id == content_id)
).values(edit_on_player_enabled=edit_enabled)
db.session.execute(stmt)
# Increment playlist version
playlist.increment_version()
db.session.commit()
cache.clear()
log_action('info', f'Updated edit_on_player_enabled={edit_enabled} for "{content.filename}" in playlist "{playlist.name}"')
return jsonify({
'success': True,
'message': 'Edit setting updated',
'edit_enabled': edit_enabled,
'version': playlist.version
})
except Exception as e:
db.session.rollback()
log_action('error', f'Error updating edit setting: {str(e)}')
return jsonify({'success': False, 'message': str(e)}), 500
@content_bp.route('/upload-media-page')
@login_required
def upload_media_page():
@@ -527,7 +568,7 @@ def process_pdf_file(filepath: str, filename: str) -> tuple[bool, str]:
def process_file_in_background(app, filepath: str, filename: str, file_ext: str,
duration: int, playlist_id: Optional[int], task_id: str):
duration: int, playlist_id: Optional[int], task_id: str, edit_on_player_enabled: bool = False):
"""Process large files (PDF, PPTX, Video) in background thread."""
with app.app_context():
try:
@@ -573,7 +614,8 @@ def process_file_in_background(app, filepath: str, filename: str, file_ext: str,
playlist_id=playlist_id,
content_id=page_content.id,
position=max_position,
duration=duration
duration=duration,
edit_on_player_enabled=edit_on_player_enabled
)
db.session.execute(stmt)
@@ -629,7 +671,8 @@ def process_file_in_background(app, filepath: str, filename: str, file_ext: str,
playlist_id=playlist_id,
content_id=slide_content.id,
position=max_position,
duration=duration
duration=duration,
edit_on_player_enabled=edit_on_player_enabled
)
db.session.execute(stmt)
@@ -677,7 +720,8 @@ def process_file_in_background(app, filepath: str, filename: str, file_ext: str,
playlist_id=playlist_id,
content_id=content.id,
position=max_position + 1,
duration=duration
duration=duration,
edit_on_player_enabled=edit_on_player_enabled
)
db.session.execute(stmt)
playlist.version += 1
@@ -949,6 +993,7 @@ def upload_media():
content_type = request.form.get('content_type', 'image')
duration = request.form.get('duration', type=int, default=10)
playlist_id = request.form.get('playlist_id', type=int)
edit_on_player_enabled = request.form.get('edit_on_player_enabled', '0') == '1'
if not files or files[0].filename == '':
flash('No files provided.', 'warning')
@@ -991,7 +1036,7 @@ def upload_media():
thread = threading.Thread(
target=process_file_in_background,
args=(current_app._get_current_object(), filepath, filename, file_ext, duration, playlist_id, task_id)
args=(current_app._get_current_object(), filepath, filename, file_ext, duration, playlist_id, task_id, edit_on_player_enabled)
)
thread.daemon = True
thread.start()
@@ -1054,7 +1099,8 @@ def upload_media():
playlist_id=playlist_id,
content_id=page_content.id,
position=max_position,
duration=duration
duration=duration,
edit_on_player_enabled=edit_on_player_enabled
)
db.session.execute(stmt)
@@ -1113,7 +1159,8 @@ def upload_media():
playlist_id=playlist_id,
content_id=slide_content.id,
position=max_position,
duration=duration
duration=duration,
edit_on_player_enabled=edit_on_player_enabled
)
db.session.execute(stmt)
@@ -1165,7 +1212,8 @@ def upload_media():
playlist_id=playlist_id,
content_id=content.id,
position=max_position + 1,
duration=duration
duration=duration,
edit_on_player_enabled=edit_on_player_enabled
)
db.session.execute(stmt)

View File

@@ -11,7 +11,8 @@ playlist_content = db.Table('playlist_content',
db.Column('content_id', db.Integer, db.ForeignKey('content.id', ondelete='CASCADE'), primary_key=True),
db.Column('position', db.Integer, default=0),
db.Column('duration', db.Integer, default=10),
db.Column('muted', db.Boolean, default=True)
db.Column('muted', db.Boolean, default=True),
db.Column('edit_on_player_enabled', db.Boolean, default=False)
)
@@ -78,7 +79,8 @@ class Playlist(db.Model):
stmt = select(playlist_content.c.content_id,
playlist_content.c.position,
playlist_content.c.duration,
playlist_content.c.muted).where(
playlist_content.c.muted,
playlist_content.c.edit_on_player_enabled).where(
playlist_content.c.playlist_id == self.id
).order_by(playlist_content.c.position)
@@ -91,6 +93,7 @@ class Playlist(db.Model):
content._playlist_position = row.position
content._playlist_duration = row.duration
content._playlist_muted = row.muted if len(row) > 3 else True
content._playlist_edit_on_player_enabled = row.edit_on_player_enabled if len(row) > 4 else False
ordered_content.append(content)
return ordered_content

View File

@@ -211,6 +211,7 @@
<th style="width: 100px;">Type</th>
<th style="width: 100px;">Duration</th>
<th style="width: 80px;">Audio</th>
<th style="width: 80px;">Edit</th>
<th style="width: 100px;">Actions</th>
</tr>
</thead>
@@ -247,6 +248,23 @@
<span style="color: #999;"></span>
{% endif %}
</td>
<td>
{% if content.content_type in ['image', 'pdf'] %}
<label class="audio-toggle">
<input type="checkbox"
class="edit-checkbox"
data-content-id="{{ content.id }}"
{{ 'checked' if content._playlist_edit_on_player_enabled else '' }}
onchange="toggleEdit({{ content.id }}, this.checked)">
<span class="audio-label">
<span class="audio-on">✏️</span>
<span class="audio-off">🔒</span>
</span>
</label>
{% else %}
<span style="color: #999;"></span>
{% endif %}
</td>
<td>
<form method="POST"
action="{{ url_for('content.remove_content_from_playlist', playlist_id=playlist.id, content_id=content.id) }}"
@@ -427,6 +445,37 @@ function toggleAudio(contentId, enabled) {
});
}
function toggleEdit(contentId, enabled) {
const playlistId = {{ playlist.id }};
const url = `/content/playlist/${playlistId}/update-edit-enabled/${contentId}`;
const formData = new FormData();
formData.append('edit_enabled', enabled ? 'true' : 'false');
fetch(url, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('Edit setting updated:', enabled ? 'Enabled' : 'Disabled');
} else {
alert('Error updating edit setting: ' + data.message);
// Revert checkbox on error
const checkbox = document.querySelector(`.edit-checkbox[data-content-id="${contentId}"]`);
if (checkbox) checkbox.checked = !enabled;
}
})
.catch(error => {
console.error('Error:', error);
alert('Error updating edit setting');
// Revert checkbox on error
const checkbox = document.querySelector(`.edit-checkbox[data-content-id="${contentId}"]`);
if (checkbox) checkbox.checked = !enabled;
});
}
function toggleSelectAll(checkbox) {
const checkboxes = document.querySelectorAll('.content-checkbox');
checkboxes.forEach(cb => {

View File

@@ -296,6 +296,17 @@
</small>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; cursor: pointer; user-select: none;">
<input type="checkbox" name="edit_on_player_enabled" id="edit_on_player_enabled"
value="1" style="margin-right: 8px; width: 18px; height: 18px; cursor: pointer;">
<span>Allow editing on player (PDF, Images, PPTX)</span>
</label>
<small style="color: #6c757d; display: block; margin-top: 4px; font-size: 11px;">
✏️ Enable local editing of this media on the player device
</small>
</div>
<!-- Upload Button -->
<button type="submit" class="btn-upload" id="upload-btn" disabled style="display: flex; align-items: center; justify-content: center; gap: 0.5rem; margin-top: 20px; padding: 12px 30px;">
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">

View File

@@ -8,14 +8,14 @@ mkdir -p /app/instance
mkdir -p /app/app/static/uploads
# Initialize database if it doesn't exist
if [ ! -f /app/instance/digiserver.db ]; then
if [ ! -f /app/instance/dashboard.db ]; then
echo "Initializing database..."
python -c "
from app.app import create_app
from app.extensions import db, bcrypt
from app.models import User
app = create_app()
app = create_app('production')
with app.app_context():
db.create_all()

View File

@@ -0,0 +1,47 @@
"""Migration: Add edit_on_player_enabled column to playlist_content table."""
import sqlite3
import os
DB_PATH = 'instance/dashboard.db'
def migrate():
"""Add edit_on_player_enabled column to playlist_content."""
if not os.path.exists(DB_PATH):
print(f"Database not found at {DB_PATH}")
return False
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
try:
# Check if column already exists
cursor.execute("PRAGMA table_info(playlist_content)")
columns = [col[1] for col in cursor.fetchall()]
if 'edit_on_player_enabled' in columns:
print("Column 'edit_on_player_enabled' already exists!")
return True
# Add the new column with default value False
print("Adding 'edit_on_player_enabled' column to playlist_content table...")
cursor.execute("""
ALTER TABLE playlist_content
ADD COLUMN edit_on_player_enabled BOOLEAN DEFAULT 0
""")
conn.commit()
print("✅ Migration completed successfully!")
print("Column 'edit_on_player_enabled' added with default value False (0)")
return True
except Exception as e:
conn.rollback()
print(f"❌ Migration failed: {e}")
return False
finally:
conn.close()
if __name__ == '__main__':
migrate()