diff --git a/SongRequest/templates/index.html b/SongRequest/templates/index.html
index 8b44a17..f8fd2ed 100644
--- a/SongRequest/templates/index.html
+++ b/SongRequest/templates/index.html
@@ -338,6 +338,7 @@
border: 1px solid var(--border);
border-radius: var(--radius);
transition: all 0.2s ease;
+ position: relative;
}
.request-card:hover { border-color: #3a3a3a; }
@@ -422,7 +423,9 @@
.card-actions {
display: flex;
gap: 0.5rem;
- margin-top: 0.5rem;
+ position: absolute;
+ top: 0.625rem;
+ right: 0.625rem;
}
.btn {
@@ -541,6 +544,14 @@
display: flex;
flex-direction: column;
min-width: 0;
+ flex: 1;
+ }
+
+ .alt-actions {
+ display: flex;
+ gap: 0.375rem;
+ flex-shrink: 0;
+ margin-left: auto;
}
.alt-title {
diff --git a/SongRequest/web.py b/SongRequest/web.py
index 81d7615..e2bdaee 100644
--- a/SongRequest/web.py
+++ b/SongRequest/web.py
@@ -32,26 +32,35 @@ def _render_alternates(request_id: int, alternates_json: str, status: str) -> st
esc = html.escape
show_approve = status == "pending"
+ show_play = status in ("pending", "approved")
items = []
for idx, alt in enumerate(alts):
artwork = esc(alt.get("artwork_url", "").replace("600x600", "100x100"))
title = esc(alt.get("title", ""))
artist = esc(alt.get("artist", ""))
url = esc(alt.get("apple_music_url", ""))
- approve_btn = ""
+ buttons = []
if show_approve:
- approve_btn = (
+ buttons.append(
f''
)
+ if show_play:
+ buttons.append(
+ f''
+ )
+ actions_html = f'
{"".join(buttons)}
' if buttons else ""
items.append(
f''
f'

'
f'
{title}'
f'{artist}
'
- f'{approve_btn}
'
+ f'{actions_html}'
)
return (
''
@@ -104,8 +113,8 @@ def render_request_card(req: SongRequestModel) -> str:
{esc(req.album)}
Requested by {esc(req.requester_nick)} in {esc(req.channel)} at {created}
{esc(req.status)}
- {actions}
+ {actions}
{alternates_html}
"""
@@ -181,6 +190,7 @@ class WebServer:
app.router.add_get("/api/channels", self._handle_channels)
app.router.add_get("/api/requests", self._handle_api_get)
app.router.add_post("/api/requests/{request_id}/approve-alt/{alt_idx}", self._handle_approve_alt)
+ app.router.add_post("/api/requests/{request_id}/play-alt/{alt_idx}", self._handle_play_alt)
app.router.add_post("/api/requests/{request_id}/{action}", self._handle_api_action)
app.router.add_get("/api/export/markdown", self._handle_export_markdown)
app.router.add_get("/api/status", self._handle_get_status)
@@ -446,6 +456,46 @@ class WebServer:
return web.Response(text=card_html, content_type="text/html")
+ async def _handle_play_alt(self, request: web.Request) -> web.Response:
+ denied = self._require_auth(request)
+ if denied:
+ return denied
+
+ try:
+ request_id = int(request.match_info["request_id"])
+ alt_idx = int(request.match_info["alt_idx"])
+ except (ValueError, KeyError):
+ return web.Response(text="Bad request", status=400)
+
+ req = self._store.get(request_id)
+ if not req:
+ return web.Response(text="Request not found", status=404)
+
+ try:
+ alts = json.loads(req.alternates_json) if req.alternates_json else []
+ except (json.JSONDecodeError, TypeError):
+ alts = []
+
+ if alt_idx < 0 or alt_idx >= len(alts):
+ return web.Response(text="Invalid alternate index", status=400)
+
+ alt = alts[alt_idx]
+ self._store.swap_alternate(request_id, alt)
+ req = self._store.update_status(request_id, "played")
+ if not req:
+ return web.Response(text="Request not found", status=404)
+
+ card_html = render_request_card(req)
+ await self._broadcast("request-update", card_html)
+
+ if self._on_status_change:
+ try:
+ self._on_status_change(req)
+ except Exception:
+ log.exception("SongRequest: on_status_change callback failed")
+
+ return web.Response(text=card_html, content_type="text/html")
+
async def _handle_export_markdown(self, request: web.Request) -> web.Response:
denied = self._require_auth(request)
if denied: