Open HTTP streams in minimal standalone player tab
All checks were successful
Build and push Docker image / build (push) Successful in 14s
Test / test (push) Successful in 16s

Instead of trying a HTTPS upgrade (which fails for IP-based streams):
- playStation() detects http:// URL on https:// page, opens /radio/stream-player/
  with url, name, vol as query params, then returns — main stream is already
  stopped by the stopPlayback(false) call at the top of playStation()
- New view stream_player renders a standalone minimal player page
- Template: auto-plays on load, correct volume from URL param, volume changes
  synced back to localStorage so main window picks it up next time,
  live track metadata via SSE, tab title updates on track change,
  close-tab button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
marwin 2026-03-21 17:50:12 +01:00
parent 83304c197d
commit 85776390f6
4 changed files with 184 additions and 19 deletions

View file

@ -18,4 +18,5 @@ urlpatterns = [
path('radio/notes/<int:pk>/', views.save_station_notes, name='save_station_notes'),
path('radio/focus/record/', views.record_focus_session, name='record_focus_session'),
path('radio/focus/stats/', views.focus_stats, name='focus_stats'),
path('radio/stream-player/', views.stream_player, name='stream_player'),
]

View file

@ -1,4 +1,6 @@
import json
import socket
import ssl as ssl_module
import time
import urllib.parse
from datetime import datetime
@ -573,3 +575,18 @@ def import_m3u(request):
skipped += 1
return JsonResponse({'ok': True, 'added': added, 'skipped': skipped})
# ---------------------------------------------------------------------------
# Minimal HTTP stream player (standalone tab for mixed-content streams)
# ---------------------------------------------------------------------------
def stream_player(request):
url = request.GET.get('url', '').strip()
name = request.GET.get('name', '').strip()
vol = request.GET.get('vol', '204').strip()
try:
vol = max(0, min(255, int(vol)))
except ValueError:
vol = 204
return render(request, 'radio/stream_player.html', {'stream_url': url, 'stream_name': name, 'stream_vol': vol})

View file

@ -67,28 +67,18 @@ function escapeHtml(str) {
function playStation(url, name, stationId) {
stopPlayback(false);
// HTTP stream on HTTPS page → open minimal player in new tab, keep main window as home base
if (location.protocol === 'https:' && url.startsWith('http://')) {
const vol = localStorage.getItem('diora_volume') || '204';
const params = new URLSearchParams({ url, name, vol });
window.open('/radio/stream-player/?' + params, '_blank');
return;
}
currentStation = { url, name, id: stationId || null };
isPlaying = true;
// If page is HTTPS and stream is HTTP, try upgrading to HTTPS first.
// Browsers block mixed content (HTTP media on HTTPS pages).
const playUrl = (location.protocol === 'https:' && url.startsWith('http://'))
? url.replace('http://', 'https://')
: url;
audio.onerror = () => {
const wasUpgraded = playUrl !== url;
if (wasUpgraded) {
$('now-playing-track').textContent = 'Stream nicht erreichbar (HTTP-only, kein HTTPS-Fallback)';
} else {
$('now-playing-track').textContent = 'Stream konnte nicht geladen werden';
}
isPlaying = false;
$('play-stop-btn').textContent = '&#9654; Play';
$('play-stop-btn').classList.remove('playing');
};
audio.src = playUrl;
audio.src = url;
const volSlider = document.getElementById('volume');
if (volSlider) audio.volume = volSlider.value / 255;
audio.play().catch(() => {

View file

@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ stream_name|default:"Radio" }}</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, sans-serif;
background: #0d0d0d;
color: #e0e0e0;
min-height: 100svh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
padding: 24px;
}
#station-name {
font-size: 1.3rem;
font-weight: 700;
text-align: center;
}
#track-name {
font-size: 0.9rem;
color: #888;
text-align: center;
min-height: 1.2em;
}
.controls {
display: flex;
align-items: center;
gap: 12px;
}
#play-btn {
background: #e63946;
color: #fff;
border: none;
border-radius: 6px;
padding: 8px 20px;
font-size: 1rem;
cursor: pointer;
min-width: 90px;
}
#play-btn:hover { background: #c1121f; }
.vol-wrap {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
color: #888;
}
#vol-slider { width: 90px; accent-color: #e63946; }
#vol-num {
width: 42px;
background: #1a1a1a;
border: 1px solid #333;
color: #e0e0e0;
border-radius: 4px;
padding: 2px 4px;
font-size: 0.85rem;
text-align: center;
}
#back-btn {
background: none;
border: none;
color: #555;
font-size: 0.8rem;
cursor: pointer;
text-decoration: underline;
margin-top: 8px;
}
#back-btn:hover { color: #aaa; }
</style>
</head>
<body>
<div id="station-name">{{ stream_name|default:"Radio" }}</div>
<div id="track-name"></div>
<div class="controls">
<button id="play-btn">&#9654; Play</button>
<div class="vol-wrap">
<span>vol</span>
<input type="range" id="vol-slider" min="0" max="255" value="{{ stream_vol }}">
<input type="number" id="vol-num" min="0" max="255" value="{{ stream_vol }}">
</div>
</div>
<button id="back-btn" onclick="window.close()">&#8592; close tab</button>
<script>
const audio = new Audio();
let playing = false;
let sse = null;
const streamUrl = '{{ stream_url|escapejs }}';
const stationName = '{{ stream_name|escapejs }}';
// Volume
function setVol(v) {
v = Math.max(0, Math.min(255, Math.round(v)));
audio.volume = v / 255;
document.getElementById('vol-slider').value = v;
document.getElementById('vol-num').value = v;
try { localStorage.setItem('diora_volume', v); } catch (_) {}
}
const slider = document.getElementById('vol-slider');
const numIn = document.getElementById('vol-num');
slider.addEventListener('input', () => setVol(parseInt(slider.value, 10)));
numIn.addEventListener('change', () => setVol(parseInt(numIn.value, 10)));
// Play / Stop
const playBtn = document.getElementById('play-btn');
function startPlay() {
audio.src = streamUrl;
setVol(parseInt(slider.value, 10));
audio.play().catch(() => {});
playing = true;
playBtn.innerHTML = '&#9646;&#9646; Stop';
// SSE metadata
if (sse) sse.close();
sse = new EventSource('/radio/sse/?url=' + encodeURIComponent(streamUrl));
sse.onmessage = e => {
try {
const data = JSON.parse(e.data);
if (data.track) {
document.getElementById('track-name').textContent = data.track;
document.title = data.track + ' — ' + stationName;
}
} catch (_) {}
};
}
function stopPlay() {
audio.pause();
audio.src = '';
playing = false;
playBtn.innerHTML = '&#9654; Play';
document.getElementById('track-name').textContent = '';
document.title = stationName;
if (sse) { sse.close(); sse = null; }
}
playBtn.addEventListener('click', () => {
if (playing) stopPlay(); else startPlay();
});
// Auto-play on load
document.addEventListener('DOMContentLoaded', startPlay);
</script>
</body>
</html>