Add thumbs up/down for creamfresh radio, hardcoded to that station
All checks were successful
Build and push Docker image / build (push) Successful in 14s
Test / test (push) Successful in 1m35s

Relays to the DJ's own /dj/feedback via the same server-side-auth
proxy pattern as the stream itself, so the browser never needs the
station's credentials. Buttons only show when the currently playing
station is creamfresh radio (matched by URL against a hardcoded
constant) -- no other station's backend does anything with a vote,
so no other station gets the buttons yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drq8o4HfFmwkXz2AgvaQ4p
This commit is contained in:
marwin 2026-09-04 11:38:34 +02:00
parent 9228906db8
commit 171560c71e
4 changed files with 72 additions and 0 deletions

View file

@ -20,4 +20,5 @@ urlpatterns = [
path('radio/focus/stats/', views.focus_stats, name='focus_stats'),
path('radio/stream-player/', views.stream_player, name='stream_player'),
path('radio/creamfresh-stream/', views.creamfresh_stream, name='creamfresh_stream'),
path('radio/creamfresh-feedback/', views.creamfresh_feedback, name='creamfresh_feedback'),
]

View file

@ -622,6 +622,33 @@ def creamfresh_stream(request):
return response
@csrf_exempt
@require_http_methods(['POST'])
def creamfresh_feedback(request):
"""Relays a thumbs up/down to the creamfresh DJ's own /dj/feedback --
same server-side-credentials reasoning as creamfresh_stream above."""
try:
body = json.loads(request.body)
except (json.JSONDecodeError, ValueError):
return JsonResponse({'error': 'invalid JSON'}, status=400)
vote = body.get('vote')
if vote not in ('up', 'down'):
return JsonResponse({'error': "vote must be 'up' or 'down'"}, status=400)
try:
upstream = requests.post(
'https://radio.creamfresh.xyz/dj/feedback',
auth=CREAMFRESH_AUTH,
json={'vote': vote},
timeout=15,
)
except requests.RequestException:
return JsonResponse({'error': 'upstream unreachable'}, status=502)
return JsonResponse(upstream.json(), status=upstream.status_code, safe=False)
def stream_player(request):
url = request.GET.get('url', '').strip()
name = request.GET.get('name', '').strip()

View file

@ -9,6 +9,10 @@
// State
// ---------------------------------------------------------------------------
// Hardcoded for now -- only creamfresh radio gets the vote buttons, since
// only its backend (the DJ) actually does anything with them.
const CREAMFRESH_RADIO_URL = 'https://diora.creamfresh.xyz/radio/creamfresh-stream/';
let currentStation = null; // { url, name, id } | null
let currentTrack = '';
let sseSource = null;
@ -176,6 +180,12 @@ function playStation(url, name, stationId) {
$('play-stop-btn').classList.add('playing');
$('save-station-btn').style.display = '';
const isCreamfresh = url === CREAMFRESH_RADIO_URL;
$('creamfresh-vote-up-btn').style.display = isCreamfresh ? '' : 'none';
$('creamfresh-vote-down-btn').style.display = isCreamfresh ? '' : 'none';
$('creamfresh-vote-up-btn').classList.remove('active');
$('creamfresh-vote-down-btn').classList.remove('active');
startMetadataSSE(url);
startPlaySession(name, url);
maybeShowDonationHint(url, name);
@ -220,6 +230,8 @@ function stopPlayback(clearStation = true) {
$('play-stop-btn').textContent = '▶ Play';
$('play-stop-btn').classList.remove('playing');
$('save-station-btn').style.display = 'none';
$('creamfresh-vote-up-btn').style.display = 'none';
$('creamfresh-vote-down-btn').style.display = 'none';
$('affiliate-section').style.display = 'none';
stopPlaySession();
@ -604,6 +616,36 @@ async function saveCurrentStation() {
await saveStation(data);
}
// ---------------------------------------------------------------------------
// creamfresh radio: DJ feedback (hardcoded to this one station, see
// CREAMFRESH_RADIO_URL above)
// ---------------------------------------------------------------------------
async function creamfreshVote(direction) {
const upBtn = $('creamfresh-vote-up-btn');
const downBtn = $('creamfresh-vote-down-btn');
upBtn.disabled = true;
downBtn.disabled = true;
try {
const res = await fetch('/radio/creamfresh-feedback/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken(),
},
body: JSON.stringify({ vote: direction }),
});
if (!res.ok) throw new Error(`feedback returned ${res.status}`);
upBtn.classList.toggle('active', direction === 'up');
downBtn.classList.toggle('active', direction === 'down');
} catch (err) {
console.warn('creamfresh vote failed', err);
} finally {
upBtn.disabled = false;
downBtn.disabled = false;
}
}
async function saveStation(station) {
try {
const res = await fetch('/radio/save/', {

View file

@ -17,6 +17,8 @@
<input type="number" id="volume-num" min="0" max="255" value="204" class="volume-num">
</label>
<button class="btn btn-save" id="save-station-btn" style="display:none;" onclick="saveCurrentStation()">&#9733; Save</button>
<button class="btn-icon" id="creamfresh-vote-up-btn" style="display:none;" onclick="creamfreshVote('up')" title="Gefällt mir">&#128077;</button>
<button class="btn-icon" id="creamfresh-vote-down-btn" style="display:none;" onclick="creamfreshVote('down')" title="Gefällt mir nicht">&#128078;</button>
<button class="btn-icon" id="dnd-btn" onclick="toggleDND()" title="Focus mode (hides UI, press Esc to exit)"></button>
<button class="btn-icon" id="focus-station-btn" onclick="openRadioSidebar()" title="Radio"></button>
</div>