updated view and playlist management
This commit is contained in:
33
add_muted_column.py
Normal file
33
add_muted_column.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Add muted column to playlist_content table."""
|
||||||
|
from app.app import create_app
|
||||||
|
from app.extensions import db
|
||||||
|
|
||||||
|
def add_muted_column():
|
||||||
|
"""Add muted column to playlist_content association table."""
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
try:
|
||||||
|
# Check if column already exists
|
||||||
|
result = db.session.execute(db.text("PRAGMA table_info(playlist_content)")).fetchall()
|
||||||
|
columns = [row[1] for row in result]
|
||||||
|
|
||||||
|
if 'muted' in columns:
|
||||||
|
print("ℹ️ Column 'muted' already exists in playlist_content table")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add muted column with default value True (muted by default)
|
||||||
|
db.session.execute(db.text("""
|
||||||
|
ALTER TABLE playlist_content
|
||||||
|
ADD COLUMN muted BOOLEAN DEFAULT TRUE
|
||||||
|
"""))
|
||||||
|
db.session.commit()
|
||||||
|
print("✅ Successfully added 'muted' column to playlist_content table")
|
||||||
|
print(" Default: TRUE (videos will be muted by default)")
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"❌ Error adding column: {e}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
add_muted_column()
|
||||||
@@ -235,6 +235,47 @@ def reorder_playlist_content(playlist_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-muted/<int:content_id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def update_playlist_content_muted(playlist_id: int, content_id: int):
|
||||||
|
"""Update content muted setting in playlist."""
|
||||||
|
playlist = Playlist.query.get_or_404(playlist_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = Content.query.get_or_404(content_id)
|
||||||
|
muted = request.form.get('muted', 'true').lower() == 'true'
|
||||||
|
|
||||||
|
from app.models.playlist import playlist_content
|
||||||
|
from sqlalchemy import update
|
||||||
|
|
||||||
|
# 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 playlist "{playlist.name}"')
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
@content_bp.route('/upload-media-page')
|
@content_bp.route('/upload-media-page')
|
||||||
@login_required
|
@login_required
|
||||||
def upload_media_page():
|
def upload_media_page():
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ def get_player_playlist(player_id: int) -> List[dict]:
|
|||||||
'type': content.content_type,
|
'type': content.content_type,
|
||||||
'duration': getattr(content, '_playlist_duration', content.duration or 10),
|
'duration': getattr(content, '_playlist_duration', content.duration or 10),
|
||||||
'position': getattr(content, '_playlist_position', 0),
|
'position': getattr(content, '_playlist_position', 0),
|
||||||
|
'muted': getattr(content, '_playlist_muted', True),
|
||||||
'filename': content.filename
|
'filename': content.filename
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -239,6 +239,49 @@ def update_duration(player_id: int, content_id: int):
|
|||||||
return jsonify({'success': False, 'message': str(e)}), 500
|
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'])
|
@playlist_bp.route('/<int:player_id>/clear', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def clear_playlist(player_id: int):
|
def clear_playlist(player_id: int):
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ playlist_content = db.Table('playlist_content',
|
|||||||
db.Column('playlist_id', db.Integer, db.ForeignKey('playlist.id', ondelete='CASCADE'), primary_key=True),
|
db.Column('playlist_id', db.Integer, db.ForeignKey('playlist.id', ondelete='CASCADE'), primary_key=True),
|
||||||
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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -76,7 +77,8 @@ class Playlist(db.Model):
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
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).where(
|
playlist_content.c.duration,
|
||||||
|
playlist_content.c.muted).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)
|
||||||
|
|
||||||
@@ -88,6 +90,7 @@ class Playlist(db.Model):
|
|||||||
if content:
|
if content:
|
||||||
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
|
||||||
ordered_content.append(content)
|
ordered_content.append(content)
|
||||||
|
|
||||||
return ordered_content
|
return ordered_content
|
||||||
|
|||||||
@@ -88,6 +88,72 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Audio toggle styles */
|
||||||
|
.audio-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-checkbox {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-label {
|
||||||
|
font-size: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-checkbox + .audio-label .audio-on {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-checkbox + .audio-label .audio-off {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-checkbox:checked + .audio-label .audio-on {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-checkbox:checked + .audio-label .audio-off {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-label:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support */
|
||||||
|
body.dark-mode .playlist-table th {
|
||||||
|
background: #1a202c;
|
||||||
|
color: #cbd5e0;
|
||||||
|
border-bottom-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .playlist-table td {
|
||||||
|
border-bottom-color: #4a5568;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .draggable-row:hover {
|
||||||
|
background: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .drag-handle {
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .content-item {
|
||||||
|
background: #1a202c;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .available-content {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="container" style="max-width: 1400px;">
|
<div class="container" style="max-width: 1400px;">
|
||||||
@@ -136,6 +202,7 @@
|
|||||||
<th>Filename</th>
|
<th>Filename</th>
|
||||||
<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: 100px;">Actions</th>
|
<th style="width: 100px;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -152,6 +219,23 @@
|
|||||||
{% else %}📁 Other{% endif %}
|
{% else %}📁 Other{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ content._playlist_duration or content.duration }}s</td>
|
<td>{{ content._playlist_duration or content.duration }}s</td>
|
||||||
|
<td>
|
||||||
|
{% if content.content_type == 'video' %}
|
||||||
|
<label class="audio-toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="audio-checkbox"
|
||||||
|
data-content-id="{{ content.id }}"
|
||||||
|
{{ 'checked' if not content._playlist_muted else '' }}
|
||||||
|
onchange="toggleAudio({{ 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) }}"
|
||||||
@@ -299,6 +383,38 @@ function saveOrder() {
|
|||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleAudio(contentId, enabled) {
|
||||||
|
const muted = !enabled; // Checkbox is "enabled audio", but backend stores "muted"
|
||||||
|
const playlistId = {{ playlist.id }};
|
||||||
|
const url = `/content/playlist/${playlistId}/update-muted/${contentId}`;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('muted', muted ? 'true' : 'false');
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
console.log('Audio setting updated:', enabled ? 'Enabled' : 'Muted');
|
||||||
|
} else {
|
||||||
|
alert('Error updating audio setting: ' + data.message);
|
||||||
|
// Revert checkbox on error
|
||||||
|
const checkbox = document.querySelector(`.audio-checkbox[data-content-id="${contentId}"]`);
|
||||||
|
if (checkbox) checkbox.checked = !enabled;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error updating audio setting');
|
||||||
|
// Revert checkbox on error
|
||||||
|
const checkbox = document.querySelector(`.audio-checkbox[data-content-id="${contentId}"]`);
|
||||||
|
if (checkbox) checkbox.checked = !enabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
<div id="player-container">
|
<div id="player-container">
|
||||||
<div class="loading" id="loading">Loading playlist...</div>
|
<div class="loading" id="loading">Loading playlist...</div>
|
||||||
<img id="media-display" alt="Content">
|
<img id="media-display" alt="Content">
|
||||||
<video id="video-display" style="display: none; max-width: 100%; max-height: 100%; width: 100%; height: 100%; object-fit: contain;"></video>
|
<video id="video-display" muted autoplay playsinline style="display: none; max-width: 100%; max-height: 100%; width: 100%; height: 100%; object-fit: contain;"></video>
|
||||||
<div class="no-content" id="no-content" style="display: none;">
|
<div class="no-content" id="no-content" style="display: none;">
|
||||||
<p>💭 No content in playlist</p>
|
<p>💭 No content in playlist</p>
|
||||||
<p style="font-size: 16px; margin-top: 10px; opacity: 0.7;">Add content to the playlist to preview</p>
|
<p style="font-size: 16px; margin-top: 10px; opacity: 0.7;">Add content to the playlist to preview</p>
|
||||||
@@ -149,6 +149,7 @@
|
|||||||
|
|
||||||
if (item.type === 'video') {
|
if (item.type === 'video') {
|
||||||
videoDisplay.src = item.url;
|
videoDisplay.src = item.url;
|
||||||
|
videoDisplay.muted = item.muted !== false; // Muted unless explicitly set to false
|
||||||
videoDisplay.style.display = 'block';
|
videoDisplay.style.display = 'block';
|
||||||
videoDisplay.play();
|
videoDisplay.play();
|
||||||
|
|
||||||
|
|||||||
@@ -369,6 +369,43 @@
|
|||||||
background: #5a1e1e;
|
background: #5a1e1e;
|
||||||
color: #ef5350;
|
color: #ef5350;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Audio toggle styles */
|
||||||
|
.audio-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-checkbox {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-label {
|
||||||
|
font-size: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-checkbox + .audio-label .audio-on {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-checkbox + .audio-label .audio-off {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-checkbox:checked + .audio-label .audio-on {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-checkbox:checked + .audio-label .audio-off {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-label:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="playlist-container">
|
<div class="playlist-container">
|
||||||
@@ -433,6 +470,7 @@
|
|||||||
<th>Filename</th>
|
<th>Filename</th>
|
||||||
<th style="width: 100px;">Type</th>
|
<th style="width: 100px;">Type</th>
|
||||||
<th style="width: 120px;">Duration (s)</th>
|
<th style="width: 120px;">Duration (s)</th>
|
||||||
|
<th style="width: 80px;">Audio</th>
|
||||||
<th style="width: 100px;">Size</th>
|
<th style="width: 100px;">Size</th>
|
||||||
<th style="width: 150px;">Actions</th>
|
<th style="width: 150px;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -479,6 +517,24 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if content.content_type == 'video' %}
|
||||||
|
<label class="audio-toggle" onclick="event.stopPropagation()">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="audio-checkbox"
|
||||||
|
data-content-id="{{ content.id }}"
|
||||||
|
{{ 'checked' if not content._playlist_muted else '' }}
|
||||||
|
onchange="toggleAudio({{ content.id }}, this.checked)"
|
||||||
|
onclick="event.stopPropagation()">
|
||||||
|
<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>{{ "%.2f"|format(content.file_size_mb) }} MB</td>
|
<td>{{ "%.2f"|format(content.file_size_mb) }} MB</td>
|
||||||
<td>
|
<td>
|
||||||
<form method="POST"
|
<form method="POST"
|
||||||
@@ -764,6 +820,38 @@ function updateTotalDuration() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleAudio(contentId, enabled) {
|
||||||
|
const muted = !enabled; // Checkbox is "enabled audio", but backend stores "muted"
|
||||||
|
const playerId = {{ player.id }};
|
||||||
|
const url = `/playlist/${playerId}/update-muted/${contentId}`;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('muted', muted ? 'true' : 'false');
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
console.log('Audio setting updated:', enabled ? 'Enabled' : 'Muted');
|
||||||
|
} else {
|
||||||
|
alert('Error updating audio setting: ' + data.message);
|
||||||
|
// Revert checkbox on error
|
||||||
|
const checkbox = document.querySelector(`.audio-checkbox[data-content-id="${contentId}"]`);
|
||||||
|
if (checkbox) checkbox.checked = !enabled;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error updating audio setting');
|
||||||
|
// Revert checkbox on error
|
||||||
|
const checkbox = document.querySelector(`.audio-checkbox[data-content-id="${contentId}"]`);
|
||||||
|
if (checkbox) checkbox.checked = !enabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user