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:
@@ -401,7 +401,8 @@ def get_cached_playlist(player_id: int) -> List[Dict]:
|
|||||||
'duration': content._playlist_duration or content.duration or 10,
|
'duration': content._playlist_duration or content.duration or 10,
|
||||||
'position': content._playlist_position or idx,
|
'position': content._playlist_position or idx,
|
||||||
'url': content_url, # Full URL for downloads
|
'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
|
return playlist_data
|
||||||
|
|||||||
@@ -396,6 +396,47 @@ def update_playlist_content_muted(playlist_id: int, content_id: int):
|
|||||||
return jsonify({'success': False, 'message': str(e)}), 500
|
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')
|
@content_bp.route('/upload-media-page')
|
||||||
@login_required
|
@login_required
|
||||||
def upload_media_page():
|
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,
|
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."""
|
"""Process large files (PDF, PPTX, Video) in background thread."""
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
try:
|
try:
|
||||||
@@ -573,7 +614,8 @@ def process_file_in_background(app, filepath: str, filename: str, file_ext: str,
|
|||||||
playlist_id=playlist_id,
|
playlist_id=playlist_id,
|
||||||
content_id=page_content.id,
|
content_id=page_content.id,
|
||||||
position=max_position,
|
position=max_position,
|
||||||
duration=duration
|
duration=duration,
|
||||||
|
edit_on_player_enabled=edit_on_player_enabled
|
||||||
)
|
)
|
||||||
db.session.execute(stmt)
|
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,
|
playlist_id=playlist_id,
|
||||||
content_id=slide_content.id,
|
content_id=slide_content.id,
|
||||||
position=max_position,
|
position=max_position,
|
||||||
duration=duration
|
duration=duration,
|
||||||
|
edit_on_player_enabled=edit_on_player_enabled
|
||||||
)
|
)
|
||||||
db.session.execute(stmt)
|
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,
|
playlist_id=playlist_id,
|
||||||
content_id=content.id,
|
content_id=content.id,
|
||||||
position=max_position + 1,
|
position=max_position + 1,
|
||||||
duration=duration
|
duration=duration,
|
||||||
|
edit_on_player_enabled=edit_on_player_enabled
|
||||||
)
|
)
|
||||||
db.session.execute(stmt)
|
db.session.execute(stmt)
|
||||||
playlist.version += 1
|
playlist.version += 1
|
||||||
@@ -949,6 +993,7 @@ def upload_media():
|
|||||||
content_type = request.form.get('content_type', 'image')
|
content_type = request.form.get('content_type', 'image')
|
||||||
duration = request.form.get('duration', type=int, default=10)
|
duration = request.form.get('duration', type=int, default=10)
|
||||||
playlist_id = request.form.get('playlist_id', type=int)
|
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 == '':
|
if not files or files[0].filename == '':
|
||||||
flash('No files provided.', 'warning')
|
flash('No files provided.', 'warning')
|
||||||
@@ -991,7 +1036,7 @@ def upload_media():
|
|||||||
|
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
target=process_file_in_background,
|
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.daemon = True
|
||||||
thread.start()
|
thread.start()
|
||||||
@@ -1054,7 +1099,8 @@ def upload_media():
|
|||||||
playlist_id=playlist_id,
|
playlist_id=playlist_id,
|
||||||
content_id=page_content.id,
|
content_id=page_content.id,
|
||||||
position=max_position,
|
position=max_position,
|
||||||
duration=duration
|
duration=duration,
|
||||||
|
edit_on_player_enabled=edit_on_player_enabled
|
||||||
)
|
)
|
||||||
db.session.execute(stmt)
|
db.session.execute(stmt)
|
||||||
|
|
||||||
@@ -1113,7 +1159,8 @@ def upload_media():
|
|||||||
playlist_id=playlist_id,
|
playlist_id=playlist_id,
|
||||||
content_id=slide_content.id,
|
content_id=slide_content.id,
|
||||||
position=max_position,
|
position=max_position,
|
||||||
duration=duration
|
duration=duration,
|
||||||
|
edit_on_player_enabled=edit_on_player_enabled
|
||||||
)
|
)
|
||||||
db.session.execute(stmt)
|
db.session.execute(stmt)
|
||||||
|
|
||||||
@@ -1165,7 +1212,8 @@ def upload_media():
|
|||||||
playlist_id=playlist_id,
|
playlist_id=playlist_id,
|
||||||
content_id=content.id,
|
content_id=content.id,
|
||||||
position=max_position + 1,
|
position=max_position + 1,
|
||||||
duration=duration
|
duration=duration,
|
||||||
|
edit_on_player_enabled=edit_on_player_enabled
|
||||||
)
|
)
|
||||||
db.session.execute(stmt)
|
db.session.execute(stmt)
|
||||||
|
|
||||||
|
|||||||
@@ -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('content_id', db.Integer, db.ForeignKey('content.id', ondelete='CASCADE'), primary_key=True),
|
||||||
db.Column('position', db.Integer, default=0),
|
db.Column('position', db.Integer, default=0),
|
||||||
db.Column('duration', db.Integer, default=10),
|
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,
|
stmt = select(playlist_content.c.content_id,
|
||||||
playlist_content.c.position,
|
playlist_content.c.position,
|
||||||
playlist_content.c.duration,
|
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
|
playlist_content.c.playlist_id == self.id
|
||||||
).order_by(playlist_content.c.position)
|
).order_by(playlist_content.c.position)
|
||||||
|
|
||||||
@@ -91,6 +93,7 @@ class Playlist(db.Model):
|
|||||||
content._playlist_position = row.position
|
content._playlist_position = row.position
|
||||||
content._playlist_duration = row.duration
|
content._playlist_duration = row.duration
|
||||||
content._playlist_muted = row.muted if len(row) > 3 else True
|
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)
|
ordered_content.append(content)
|
||||||
|
|
||||||
return ordered_content
|
return ordered_content
|
||||||
|
|||||||
@@ -211,6 +211,7 @@
|
|||||||
<th style="width: 100px;">Type</th>
|
<th style="width: 100px;">Type</th>
|
||||||
<th style="width: 100px;">Duration</th>
|
<th style="width: 100px;">Duration</th>
|
||||||
<th style="width: 80px;">Audio</th>
|
<th style="width: 80px;">Audio</th>
|
||||||
|
<th style="width: 80px;">Edit</th>
|
||||||
<th style="width: 100px;">Actions</th>
|
<th style="width: 100px;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -247,6 +248,23 @@
|
|||||||
<span style="color: #999;">—</span>
|
<span style="color: #999;">—</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</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>
|
<td>
|
||||||
<form method="POST"
|
<form method="POST"
|
||||||
action="{{ url_for('content.remove_content_from_playlist', playlist_id=playlist.id, content_id=content.id) }}"
|
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) {
|
function toggleSelectAll(checkbox) {
|
||||||
const checkboxes = document.querySelectorAll('.content-checkbox');
|
const checkboxes = document.querySelectorAll('.content-checkbox');
|
||||||
checkboxes.forEach(cb => {
|
checkboxes.forEach(cb => {
|
||||||
|
|||||||
@@ -296,6 +296,17 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</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 -->
|
<!-- 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;">
|
<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);">
|
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ mkdir -p /app/instance
|
|||||||
mkdir -p /app/app/static/uploads
|
mkdir -p /app/app/static/uploads
|
||||||
|
|
||||||
# Initialize database if it doesn't exist
|
# Initialize database if it doesn't exist
|
||||||
if [ ! -f /app/instance/digiserver.db ]; then
|
if [ ! -f /app/instance/dashboard.db ]; then
|
||||||
echo "Initializing database..."
|
echo "Initializing database..."
|
||||||
python -c "
|
python -c "
|
||||||
from app.app import create_app
|
from app.app import create_app
|
||||||
from app.extensions import db, bcrypt
|
from app.extensions import db, bcrypt
|
||||||
from app.models import User
|
from app.models import User
|
||||||
|
|
||||||
app = create_app()
|
app = create_app('production')
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|
||||||
|
|||||||
47
migrate_add_edit_enabled.py
Normal file
47
migrate_add_edit_enabled.py
Normal 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()
|
||||||
Reference in New Issue
Block a user