Compare commits
5 Commits
ca021cc3f7
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| b4cc8770a7 | |||
| 15e7324144 | |||
| 152a974533 | |||
| 22ee2b58ad | |||
| d88547e310 |
+63
-7
@@ -299,12 +299,24 @@ def update_voting_config(
|
|||||||
status_code=400,
|
status_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
was_active = config.is_active
|
||||||
|
new_active = is_active == "on"
|
||||||
|
|
||||||
config.start_time = start_time
|
config.start_time = start_time
|
||||||
config.end_time = end_time
|
config.end_time = end_time
|
||||||
config.is_active = is_active == "on"
|
config.is_active = new_active
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
votes_reset = False
|
||||||
|
if new_active and not was_active:
|
||||||
|
db.query(Vote).delete()
|
||||||
|
db.commit()
|
||||||
|
votes_reset = True
|
||||||
|
|
||||||
results = _vote_results(db)
|
results = _vote_results(db)
|
||||||
|
success_msg = "Configuración guardada"
|
||||||
|
if votes_reset:
|
||||||
|
success_msg = "Configuración guardada · Votos reiniciados para la nueva ventana"
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"admin/voting.html",
|
"admin/voting.html",
|
||||||
{
|
{
|
||||||
@@ -314,7 +326,7 @@ def update_voting_config(
|
|||||||
"total_votes": sum(r["votes"] for r in results),
|
"total_votes": sum(r["votes"] for r in results),
|
||||||
"server_time": now_str,
|
"server_time": now_str,
|
||||||
"error": None,
|
"error": None,
|
||||||
"success": "Configuración guardada",
|
"success": success_msg,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -328,19 +340,63 @@ def reset_votes(request: Request, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/voting/play-winner")
|
@router.post("/voting/play-winner")
|
||||||
def play_winner(request: Request, db: Session = Depends(get_db)):
|
def play_winner(
|
||||||
|
request: Request,
|
||||||
|
device_id: str = Form(default=""),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
_require_admin(request)
|
_require_admin(request)
|
||||||
results = _vote_results(db)
|
results = _vote_results(db)
|
||||||
|
config = _get_or_create_config(db)
|
||||||
|
now_str = datetime.now().strftime("%H:%M:%S")
|
||||||
|
|
||||||
|
def _error(msg: str):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"admin/voting.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"config": config,
|
||||||
|
"results": results,
|
||||||
|
"total_votes": sum(r["votes"] for r in results),
|
||||||
|
"server_time": now_str,
|
||||||
|
"error": msg,
|
||||||
|
"success": None,
|
||||||
|
},
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
if not results or results[0]["votes"] == 0:
|
if not results or results[0]["votes"] == 0:
|
||||||
raise HTTPException(status_code=400, detail="No hay votos registrados")
|
return _error("No hay votos registrados")
|
||||||
|
|
||||||
winner = results[0]["playlist"]
|
winner = results[0]["playlist"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sp = spotify.get_client()
|
sp = spotify.get_client()
|
||||||
sp.start_playback(context_uri=f"spotify:playlist:{winner.spotify_id}")
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
|
if device_id:
|
||||||
|
devices = sp.devices().get("devices", [])
|
||||||
|
device = next((d for d in devices if d["id"] == device_id), None)
|
||||||
|
if not device:
|
||||||
|
return _error("El dispositivo seleccionado ya no está disponible")
|
||||||
|
if device.get("is_restricted"):
|
||||||
|
return _error(
|
||||||
|
f"El dispositivo «{device['name']}» no permite reproducción remota"
|
||||||
|
)
|
||||||
|
|
||||||
|
kwargs: dict = {}
|
||||||
|
if device_id:
|
||||||
|
kwargs["device_id"] = device_id
|
||||||
|
if winner.spotify_type == "track":
|
||||||
|
kwargs["uris"] = [f"spotify:track:{winner.spotify_id}"]
|
||||||
|
else:
|
||||||
|
kwargs["context_uri"] = f"spotify:{winner.spotify_type}:{winner.spotify_id}"
|
||||||
|
|
||||||
|
sp.start_playback(**kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
return _error(f"Error al reproducir: {e}")
|
||||||
|
|
||||||
|
db.query(Vote).delete()
|
||||||
|
db.commit()
|
||||||
return RedirectResponse(url="/admin/voting", status_code=303)
|
return RedirectResponse(url="/admin/voting", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Selector de dispositivo -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>Dispositivo de reproducción</h2>
|
||||||
|
<div class="device-row">
|
||||||
|
<select id="device-select" class="input" onchange="setDevice(this.value)" style="flex:1">
|
||||||
|
<option value="">Cargando dispositivos...</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn-sm" onclick="loadDevices()">↻ Actualizar</button>
|
||||||
|
</div>
|
||||||
|
<div id="device-msg" style="font-size:.8rem;color:var(--text-muted);margin-top:.4rem"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Resultados -->
|
<!-- Resultados -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="results-header">
|
<div class="results-header">
|
||||||
@@ -73,6 +85,7 @@
|
|||||||
<div style="display:flex;gap:.5rem;align-items:center">
|
<div style="display:flex;gap:.5rem;align-items:center">
|
||||||
{% if results and results[0].votes > 0 %}
|
{% if results and results[0].votes > 0 %}
|
||||||
<form method="post" action="/admin/voting/play-winner">
|
<form method="post" action="/admin/voting/play-winner">
|
||||||
|
<input type="hidden" id="play-winner-device-id" name="device_id" value="">
|
||||||
<button type="submit" class="btn-primary btn-sm">
|
<button type="submit" class="btn-primary btn-sm">
|
||||||
▶ Reproducir ganador
|
▶ Reproducir ganador
|
||||||
</button>
|
</button>
|
||||||
@@ -121,7 +134,58 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function _syncWinnerDevice(deviceId) {
|
||||||
|
const input = document.getElementById('play-winner-device-id');
|
||||||
|
if (input) input.value = deviceId || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDevices() {
|
||||||
|
const sel = document.getElementById('device-select');
|
||||||
|
const msg = document.getElementById('device-msg');
|
||||||
|
sel.innerHTML = '<option value="">Cargando...</option>';
|
||||||
|
msg.textContent = '';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/player/devices');
|
||||||
|
const devices = await res.json();
|
||||||
|
if (!devices.length) {
|
||||||
|
sel.innerHTML = '<option value="">Sin dispositivos activos</option>';
|
||||||
|
_syncWinnerDevice('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sel.innerHTML = devices.map(d =>
|
||||||
|
`<option value="${d.id}"${d.is_active ? ' selected' : ''}>${d.name} (${d.type})${d.is_restricted ? ' — sin control remoto' : ''}</option>`
|
||||||
|
).join('');
|
||||||
|
const active = devices.find(d => d.is_active);
|
||||||
|
_syncWinnerDevice(active ? active.id : devices[0].id);
|
||||||
|
} catch (_) {
|
||||||
|
sel.innerHTML = '<option value="">Error al cargar dispositivos</option>';
|
||||||
|
_syncWinnerDevice('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setDevice(deviceId) {
|
||||||
|
if (!deviceId) return;
|
||||||
|
const msg = document.getElementById('device-msg');
|
||||||
|
_syncWinnerDevice(deviceId);
|
||||||
|
try {
|
||||||
|
await fetch('/player/device', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ device_id: deviceId }),
|
||||||
|
});
|
||||||
|
msg.textContent = 'Dispositivo actualizado.';
|
||||||
|
setTimeout(() => msg.textContent = '', 2500);
|
||||||
|
} catch (_) {
|
||||||
|
msg.textContent = 'Error al cambiar dispositivo.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDevices();
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.device-row { display: flex; align-items: center; gap: .5rem; }
|
||||||
.voting-form { display: flex; flex-direction: column; gap: 1rem; }
|
.voting-form { display: flex; flex-direction: column; gap: 1rem; }
|
||||||
.time-row { display: flex; align-items: flex-end; gap: 1rem; }
|
.time-row { display: flex; align-items: flex-end; gap: 1rem; }
|
||||||
.time-field { display: flex; flex-direction: column; gap: .3rem; flex: 1; }
|
.time-field { display: flex; flex-direction: column; gap: .3rem; flex: 1; }
|
||||||
|
|||||||
@@ -44,17 +44,6 @@
|
|||||||
<span class="volume-icon">🔊</span>
|
<span class="volume-icon">🔊</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Selector de dispositivo -->
|
|
||||||
<div class="device-section">
|
|
||||||
<label class="section-label">Dispositivo</label>
|
|
||||||
<div class="device-row">
|
|
||||||
<select id="device-select" class="select" onchange="setDevice(this.value)">
|
|
||||||
<option value="">Cargando dispositivos...</option>
|
|
||||||
</select>
|
|
||||||
<button class="btn-sm" onclick="loadDevices()">↻</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Playlists / Votación -->
|
<!-- Playlists / Votación -->
|
||||||
<div class="playlist-section">
|
<div class="playlist-section">
|
||||||
<div class="playlist-section-header">
|
<div class="playlist-section-header">
|
||||||
@@ -88,7 +77,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
let isPlaying = false;
|
let isPlaying = false;
|
||||||
let currentDeviceId = null;
|
|
||||||
let votingOpen = false;
|
let votingOpen = false;
|
||||||
let myVotedPlaylistId = null;
|
let myVotedPlaylistId = null;
|
||||||
let cooldownRemaining = 0;
|
let cooldownRemaining = 0;
|
||||||
@@ -128,8 +116,7 @@ async function fetchCurrent() {
|
|||||||
|
|
||||||
async function togglePlay() {
|
async function togglePlay() {
|
||||||
const endpoint = isPlaying ? '/player/pause' : '/player/play';
|
const endpoint = isPlaying ? '/player/pause' : '/player/play';
|
||||||
const body = (!isPlaying && currentDeviceId) ? JSON.stringify({ device_id: currentDeviceId }) : '{}';
|
await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||||||
await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body });
|
|
||||||
setTimeout(fetchCurrent, 300);
|
setTimeout(fetchCurrent, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,29 +142,6 @@ function setVolume(val) {
|
|||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadDevices() {
|
|
||||||
const res = await fetch('/player/devices');
|
|
||||||
const devices = await res.json();
|
|
||||||
const sel = document.getElementById('device-select');
|
|
||||||
sel.innerHTML = devices.length === 0
|
|
||||||
? '<option value="">Sin dispositivos activos</option>'
|
|
||||||
: devices.map(d => `<option value="${d.id}">${d.name} (${d.type})</option>`).join('');
|
|
||||||
if (devices.length > 0) {
|
|
||||||
currentDeviceId = devices[0].id;
|
|
||||||
sel.value = currentDeviceId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDevice(deviceId) {
|
|
||||||
if (!deviceId) return;
|
|
||||||
currentDeviceId = deviceId;
|
|
||||||
fetch('/player/device', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ device_id: deviceId }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function playItem(spotifyId, spotifyType) {
|
async function playItem(spotifyId, spotifyType) {
|
||||||
const uri = `spotify:${spotifyType}:${spotifyId}`;
|
const uri = `spotify:${spotifyType}:${spotifyId}`;
|
||||||
const body = {};
|
const body = {};
|
||||||
@@ -186,7 +150,6 @@ async function playItem(spotifyId, spotifyType) {
|
|||||||
} else {
|
} else {
|
||||||
body.context_uri = uri;
|
body.context_uri = uri;
|
||||||
}
|
}
|
||||||
if (currentDeviceId) body.device_id = currentDeviceId;
|
|
||||||
await fetch('/player/play', {
|
await fetch('/player/play', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -395,7 +358,6 @@ function closeTracksModal(event) {
|
|||||||
|
|
||||||
async function playTrackFromModal(trackId) {
|
async function playTrackFromModal(trackId) {
|
||||||
const body = { uris: [`spotify:track:${trackId}`] };
|
const body = { uris: [`spotify:track:${trackId}`] };
|
||||||
if (currentDeviceId) body.device_id = currentDeviceId;
|
|
||||||
await fetch('/player/play', {
|
await fetch('/player/play', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -410,7 +372,6 @@ document.addEventListener('keydown', e => {
|
|||||||
if (e.key === 'Escape') closeTracksModal();
|
if (e.key === 'Escape') closeTracksModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
loadDevices();
|
|
||||||
fetchCurrent();
|
fetchCurrent();
|
||||||
fetchVotingStatus();
|
fetchVotingStatus();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user