Files
ozan 6e92841352 Fix generation JSON/Lyria errors, add Winamp player, and ship Echoes of the Sahel.
Harden DeepSeek JSON parsing with retry, pre-sanitize Lyria prompts, and instrumental fallback. Add pure HTML Winamp skin at /winamp with playlist export support.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 16:38:52 +01:00

269 lines
12 KiB
HTML

<!--
title: Live Ozan Radio
date: 2026-06-07
author: Ozan
source: live-radio
description: Auto-play AI radio — desert dub and Anadolu psych. Streams saved Lyria tracks from Git LFS on tinqs.com.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Live Ozan Radio</title>
<meta name="description" content="Personal AI radio — auto-play desert dub and Anadolu psych from tinqs.com">
<style>
:root {
--bg: #0a0a0f;
--panel: #14141f;
--accent: #ff6b35;
--accent2: #7b5cff;
--text: #f0f0f5;
--muted: #8888a0;
--glow: rgba(255, 107, 53, 0.35);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
font-family: "Segoe UI", system-ui, sans-serif;
background: var(--bg);
color: var(--text);
display: flex;
align-items: center;
justify-content: center;
background-image:
radial-gradient(ellipse 80% 50% at 50% -10%, var(--glow), transparent),
radial-gradient(ellipse 60% 40% at 100% 100%, rgba(123,92,255,0.15), transparent);
}
.shell {
width: min(440px, 92vw);
background: var(--panel);
border: 1px solid #2a2a3a;
border-radius: 20px;
padding: 2rem;
box-shadow: 0 24px 80px rgba(0,0,0,0.5);
text-align: center;
}
.badge {
display: inline-block;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent);
border: 1px solid var(--accent);
border-radius: 4px;
padding: 0.2rem 0.5rem;
margin-bottom: 0.75rem;
}
h1 { font-size: 1.6rem; margin-bottom: 0.35rem; }
.tagline { color: var(--muted); font-size: 0.85rem; line-height: 1.45; margin-bottom: 1.25rem; }
.viz {
height: 56px;
display: flex;
align-items: flex-end;
justify-content: center;
gap: 4px;
margin-bottom: 1rem;
}
.bar {
width: 6px;
background: linear-gradient(to top, var(--accent), var(--accent2));
border-radius: 3px;
animation: bounce 0.8s ease-in-out infinite alternate;
}
.bar:nth-child(1) { height: 20px; animation-delay: 0s; }
.bar:nth-child(2) { height: 36px; animation-delay: 0.1s; }
.bar:nth-child(3) { height: 52px; animation-delay: 0.2s; }
.bar:nth-child(4) { height: 40px; animation-delay: 0.15s; }
.bar:nth-child(5) { height: 28px; animation-delay: 0.05s; }
.viz.paused .bar { animation-play-state: paused; opacity: 0.35; }
@keyframes bounce {
from { transform: scaleY(0.4); opacity: 0.6; }
to { transform: scaleY(1); opacity: 1; }
}
.title { font-size: 1.15rem; font-weight: 600; margin-bottom: 0.35rem; min-height: 1.4em; }
.mood, .dj { font-size: 0.85rem; color: var(--muted); margin-bottom: 0.45rem; min-height: 1.2em; }
.dj { font-style: italic; color: #b0b0c8; }
audio { width: 100%; margin: 0.75rem 0; border-radius: 8px; }
.status { font-size: 0.75rem; color: var(--muted); min-height: 1.2em; }
.overlay {
position: fixed;
inset: 0;
background: rgba(10, 10, 15, 0.92);
display: none;
align-items: center;
justify-content: center;
z-index: 10;
cursor: pointer;
}
.overlay.show { display: flex; }
.overlay button {
background: linear-gradient(135deg, var(--accent), #ff8f65);
color: #fff;
border: none;
border-radius: 14px;
padding: 1rem 1.5rem;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
}
.links { margin-top: 1.25rem; font-size: 0.72rem; color: #555568; line-height: 1.6; }
.links a { color: var(--accent); text-decoration: none; }
</style>
</head>
<body>
<div class="overlay" id="overlay" aria-hidden="true">
<button type="button" id="startBtn">▶ Start Live Ozan Radio</button>
</div>
<div class="shell">
<div class="badge" id="badge">● Live</div>
<h1>Ozan Radio</h1>
<p class="tagline">No catalog tracks — AI-composed desert dub &amp; Anadolu psych, hosted on tinqs.com.</p>
<div class="viz" id="viz">
<div class="bar"></div><div class="bar"></div><div class="bar"></div>
<div class="bar"></div><div class="bar"></div>
</div>
<div class="title" id="trackTitle">Loading…</div>
<div class="mood" id="trackMood"></div>
<div class="dj" id="trackDj"></div>
<audio id="player" preload="auto" autoplay playsinline></audio>
<div class="status" id="status">Connecting…</div>
<div class="links">
Full DJ + Lyria dashboard (local): <code>python -m ozan_radio serve</code><br>
<a href="https://tinqs.com/tinqs/live-radio" target="_blank" rel="noopener">Repo on Git Studio</a>
· <a href="player.html">Player shell</a> (needs API)
</div>
</div>
<script>
/*__PLAYLIST__*/
{"station": "Live Ozan Radio", "tagline": "Techno-ethnic AI radio — no catalog tracks.", "media_base": "https://tinqs.com/tinqs/live-radio/media/branch/main/songs/", "share_url": "https://tinqs.com/tinqs/live-radio/src/branch/main/gateway/index.html", "winamp_url": "https://tinqs.com/tinqs/live-radio/src/branch/main/gateway/winamp.html", "updated": "2026-06-07T15:38:41.264287+00:00", "tracks": [{"id": "82595273", "title": "Caravan of the Blue Hour", "mood": "hypnotic, warm, spacious", "dj_line": "Late-night dub caravan from the Sahel — let the bass carry you across the dunes.", "file": "82595273_Caravan_of_the_Blue_Hour.mp3", "url": "https://tinqs.com/tinqs/live-radio/media/branch/main/songs/82595273_Caravan_of_the_Blue_Hour.mp3", "rating": "keeper", "shuffle_weight": 1.1, "genres": [], "categories": []}, {"id": "eeebb429", "title": "Desert Mirage", "mood": "hypnotic, warm, late-night desert dub", "dj_line": "Sending you a dust-kissed groove from the edge of the dunes — let the delays carry you through the night.", "file": "eeebb429_Desert_Mirage.mp3", "url": "https://tinqs.com/tinqs/live-radio/media/branch/main/songs/eeebb429_Desert_Mirage.mp3", "rating": "keeper", "shuffle_weight": 1.0, "genres": [], "categories": []}, {"id": "1c1d7b8a", "title": "Sahara's Saz", "mood": "Hypnotic desert dub with Anadolu warmth", "dj_line": "Blowing sand and saz strings — a slow burn across the dunes, right here on Live Ozan.", "file": "1c1d7b8a_Sahara_s_Saz.mp3", "url": "https://tinqs.com/tinqs/live-radio/media/branch/main/songs/1c1d7b8a_Sahara_s_Saz.mp3", "rating": "love", "shuffle_weight": 1.5, "genres": [], "categories": []}, {"id": "aee4994a", "title": "Nomad's Saz", "mood": "hypnotic desert dub with Turkish soul", "dj_line": "From the Sahara to Anatolia — here's a caravan of warm analog dub.", "file": "aee4994a_Nomad_s_Saz.mp3", "url": "https://tinqs.com/tinqs/live-radio/media/branch/main/songs/aee4994a_Nomad_s_Saz.mp3", "rating": "keeper", "shuffle_weight": 1.0, "genres": [], "categories": []}, {"id": "839aa313", "title": "Caravan of the Night", "mood": "hypnotic desert dub, late-night caravan, warm and spacious", "dj_line": "From the Sahel to the Anatolian plateau, let the caravan carry you through the night — this one's for the wanderers.", "file": "839aa313_Caravan_of_the_Night.mp3", "url": "https://tinqs.com/tinqs/live-radio/media/branch/main/songs/839aa313_Caravan_of_the_Night.mp3", "rating": "keeper", "shuffle_weight": 0.75, "genres": [], "categories": []}, {"id": "ce7ae31e", "title": "Chac's Dub", "mood": "ceremonial, deep, hypnotic", "dj_line": "From the temple steps to the dance floor — here's a Mesoamerican ceremonial dub built for the blue hour.", "file": "ce7ae31e_Chac_s_Dub.mp3", "url": "https://tinqs.com/tinqs/live-radio/media/branch/main/songs/ce7ae31e_Chac_s_Dub.mp3", "rating": "keeper", "shuffle_weight": 1.25, "genres": ["ceremonial-dub", "mesoamerican-electronica", "world-dub"], "categories": ["ceremonial-world", "dub-space", "vocal-ethnic"]}, {"id": "71cfdfea", "title": "Frostbite Dub", "mood": "cold cinematic gothic with sub bass warmth", "dj_line": "From the frozen steppe to the warm sub — Nordic ether dub for the only lovers left alive.", "file": "71cfdfea_Frostbite_Dub.mp3", "url": "https://tinqs.com/tinqs/live-radio/media/branch/main/songs/71cfdfea_Frostbite_Dub.mp3", "rating": "keeper", "shuffle_weight": 1.25, "genres": ["gregorian-ether", "nordic-dub", "cinematic-gothic"], "categories": ["cinematic-gothic", "vocal-ethnic", "dub-space"]}, {"id": "74646b3e", "title": "Echoes of the Sahel", "mood": "warm, dusty, spacious, hypnotic", "dj_line": "From the desert edge to the dub chamber — here's a late-night caravan drenched in spring reverb.", "file": "74646b3e_Echoes_of_the_Sahel.mp3", "url": "https://tinqs.com/tinqs/live-radio/media/branch/main/songs/74646b3e_Echoes_of_the_Sahel.mp3", "rating": "unrated", "shuffle_weight": 1.0, "genres": [], "categories": []}]}
/*__PLAYLIST__*/
const player = document.getElementById('player');
const viz = document.getElementById('viz');
const trackTitle = document.getElementById('trackTitle');
const trackMood = document.getElementById('trackMood');
const trackDj = document.getElementById('trackDj');
const statusEl = document.getElementById('status');
const overlay = document.getElementById('overlay');
const startBtn = document.getElementById('startBtn');
let playlist = null;
let queue = [];
let index = 0;
let started = false;
function parseEmbeddedPlaylist() {
const scripts = document.getElementsByTagName('script');
for (const s of scripts) {
const text = s.textContent || '';
const m = text.match(/\/\*__PLAYLIST__\*\/\s*([\s\S]*?)\s*\/\*__PLAYLIST__\*\//);
if (m) {
try { return JSON.parse(m[1]); } catch (_) {}
}
}
return null;
}
function shuffle(arr) {
const a = arr.slice();
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
function showTrack(t) {
trackTitle.textContent = t.title || 'Untitled';
trackMood.textContent = t.mood ? `Mood: ${t.mood}` : '';
trackDj.textContent = t.dj_line ? `"${t.dj_line}"` : '';
statusEl.textContent = `Playing · ${t.id || ''}`;
}
function playCurrent() {
if (!queue.length) {
trackTitle.textContent = 'No tracks yet';
statusEl.textContent = 'Push songs to main — see README';
return;
}
const t = queue[index];
showTrack(t);
const url = t.url || (playlist.media_base + t.file);
if (player.src !== url) player.src = url;
return player.play();
}
function nextTrack() {
if (!queue.length) return;
index = (index + 1) % queue.length;
playCurrent().catch(showOverlay);
}
function showOverlay() {
overlay.classList.add('show');
overlay.setAttribute('aria-hidden', 'false');
statusEl.textContent = 'Tap to start audio (browser policy)';
}
function hideOverlay() {
overlay.classList.remove('show');
overlay.setAttribute('aria-hidden', 'true');
}
function startRadio() {
if (started) return;
started = true;
hideOverlay();
playCurrent().catch(showOverlay);
}
async function init() {
playlist = playlist || parseEmbeddedPlaylist();
if (!playlist || !playlist.tracks || !playlist.tracks.length) {
try {
const res = await fetch('playlist.json');
if (res.ok) playlist = await res.json();
} catch (_) {}
}
if (!playlist || !playlist.tracks || !playlist.tracks.length) {
trackTitle.textContent = 'Library empty';
statusEl.textContent = 'Generate tracks locally, export, push to main';
return;
}
queue = shuffle(playlist.tracks);
index = 0;
document.title = playlist.station || 'Live Ozan Radio';
player.addEventListener('ended', nextTrack);
player.addEventListener('play', () => viz.classList.remove('paused'));
player.addEventListener('pause', () => viz.classList.add('paused'));
const attempt = playCurrent();
if (attempt && typeof attempt.then === 'function') {
attempt.then(() => { started = true; hideOverlay(); }).catch(showOverlay);
} else {
showOverlay();
}
}
overlay.addEventListener('click', startRadio);
startBtn.addEventListener('click', (e) => { e.stopPropagation(); startRadio(); });
document.addEventListener('keydown', (e) => {
if (e.key === ' ' || e.key === 'Enter') startRadio();
});
init();
</script>
</body>
</html>