music-choice-azuracast/index2.html
2026-06-05 21:22:04 -04:00

545 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Music Choice</title>
<style>
* {
cursor: none !important;
}
@font-face {
font-family: 'Helvetica Neue Medium';
src: url('./HelveticaNeueMedium.ttf') format('woff2');
font-weight: normal;
font-style: normal;
}
*{
margin:0;
padding:0;
box-sizing:border-box;
font-family:Helvetica Neue Medium,sans-serif;
}
body{
background:#000;
color:#fff;
overflow:hidden;
}
#screen{
width:100vw;
height:100vh;
display:flex;
flex-direction:column;
}
#top{
flex:0;
display:flex;
}
#left{
flex:2;
position:relative;
overflow:hidden;
}
#left img{
width:100%;
height:100%;
object-fit:cover;
}
#right{
width:34%;
background:#111;
display:flex;
flex-direction:column;
}
.header{
background:#a946b0;
color:black;
padding:10px 30px;
font-weight:bold;
text-transform:uppercase;
font-size:30px;
}
.fact-box{
padding:15px;
font-size:1.5rem;
line-height:1.5;
}
#bottom{
background:#000;
border-top:0px solid #a946b0;
}
#genre{
background:#a946b0;
color:black;
padding:0px 15px;
font-weight:bold;
text-transform:uppercase;
font-size:2.0rem;
}
#nowPlaying{
display:flex;
align-items:center;
padding:15px;
gap:15px;
}
.songInfo{
display:flex;
flex-direction:column;
}
.artist{
font-size:1.5rem;
}
.title{
font-size:1.4rem;
margin-top:5px;
}
.channel{
color:#999;
margin-top:4px;
font-size: 1.3rem;
}
#left {
width: 1280px;
height: 600px;
overflow: hidden;
position: relative;
}
#slideImage {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: fill !important;
object-position: fill;
display: block;
padding-right: 5px;
background: #462233;
}
.logo {
width: 150px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
overflow: hidden;
#border-radius: 10px; /* optional rounded corners */
background: #462233;
}
.logo img {
width: 100%;
height: 100%;
object-fit: cover;
}
audio{
display:none;
}
</style>
</head>
<body>
<div id="screen">
<div id="top">
<div id="left">
<img id="slideImage"
src="#"
alt="">
</div>
<div id="right">
<div class="header">Did You Know?</div>
<div class="fact-box" id="factBox">
Loading...
</div>
</div>
</div>
<div id="bottom">
<div id="genre">Connecting...</div>
<div id="nowPlaying">
<div class="logo"><img src="logo.png" alt="Logo"></div>
<div class="songInfo">
<div class="artist" id="artist">
Loading...
</div>
<div class="title" id="title">
Connecting...
</div>
<div class="channel" id="channel">
Channel
</div>
</div>
</div>
</div>
</div>
<audio id="player" controls autoplay></audio>
<script>
const M3U_URL = "venith2.m3u";
const API =
"http://192.168.1.7:5000/api/nowplaying/veltron_radio_2";
/* -----------------------------
NOW PLAYING
------------------------------*/
async function updateNowPlaying(){
try{
const res = await fetch(API);
const data = await res.json();
const song = data.now_playing.song || {};
const station = data.station || {};
// 🔥 Use text as primary source (most reliable in your setup)
const rawText = song.text || "";
let artist = "";
let title = rawText;
if(rawText.includes(" - ")){
const parts = rawText.split(" - ");
artist = parts[0].trim();
title = parts.slice(1).join(" - ").trim();
}
// fallback cleanup
if(!artist){
artist = song.artist || "Unknown Artist";
}
title = title.replace(/\[.*?\]/g, "").trim();
document.getElementById("artist").textContent =
artist;
document.getElementById("title").textContent =
title || "Unknown Track";
document.getElementById("channel").textContent =
station.name || "Veltron Radio";
document.getElementById("genre").textContent =
data.now_playing.playlist || "Music Channel";
if(song.art){
document.getElementById("slideImage").src =
"./musicchoice.png";
}
}catch(err){
console.error("AzuraCast error:", err);
document.getElementById("artist").textContent =
"Connection Error";
document.getElementById("title").textContent =
"Cannot reach AzuraCast";
document.getElementById("channel").textContent =
"Offline";
}
}
/* -----------------------------
START
------------------------------*/
updateNowPlaying();
setInterval(updateNowPlaying, 15000);
</script>
<script>
/*
=========================================
PARSE M3U
=========================================
*/
async function loadM3U(){
const txt = await fetch(M3U_URL).then(r=>r.text());
const lines = txt.split("\n");
let name = "Music Channel";
let streamUrl = "";
for(let i=0;i<lines.length;i++){
if(lines[i].startsWith("#EXTINF")){
const parts = lines[i].split(",");
if(parts[1]) name = parts[1].trim();
}
if(
lines[i].startsWith("http://") ||
lines[i].startsWith("https://")
){
streamUrl = lines[i].trim();
break;
}
}
document.getElementById("channel").textContent = name;
if(streamUrl){
document.getElementById("player").src = streamUrl;
startMetadataPolling(streamUrl);
}
}
loadM3U();
</script>
<script>
const artistEl = document.getElementById("artist");
const factBox = document.getElementById("factBox");
const songFactCache = new Map();
let artistFacts = [];
let factIndexArtist = 0;
let factInterval = null;
let factsLoaded = false;
/* ---------------------------------------------------------
GENERIC FALLBACK FACTS
--------------------------------------------------------- */
function genericFallbackFacts(name) {
return [
`${name} is part of the global music library. Information for this artist is limited.`,
`Veltron Radio broadcasts from mars. Check us out at www.veltron.net`
];
}
/* ---------------------------------------------------------
SONG → ARTIST DETECTION
--------------------------------------------------------- */
function detectArtistFromSong(title) {
const lower = title.toLowerCase();
return null;
}
/* ---------------------------------------------------------
SOUNDTRACK / SCORE DETECTION
--------------------------------------------------------- */
function detectComposerFromTitle(title) {
const lower = title.toLowerCase();
if (lower.includes("ost") || lower.includes("soundtrack") || lower.includes("score") || lower.includes("from")) {
return null;
}
return null;
}
/* ---------------------------------------------------------
WIKIPEDIA LOOKUP (ARTIST-ONLY)
--------------------------------------------------------- */
async function getWikipediaSummary(name) {
const languages = ["en"];
const allowedTypes = ["bio", "artist", "musician", "music", "standard"];
for (let lang of languages) {
const url = `https://${lang}.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(name)}`;
try {
const res = await fetch(url);
if (!res.ok) continue;
const data = await res.json();
if (!allowedTypes.includes(data.type)) continue;
if (data.type === "standard") {
const text = (data.extract || "").toLowerCase();
const bandHints = [
"band", "musical group", "singer", "rapper", "musician",
"composer", "dj", "producer", "rock band", "punk band",
"pop group", "duo", "trio", "game"
];
const isBand = bandHints.some(h => text.includes(h));
if (!isBand) continue;
}
if (data.extract_html || data.extract) {
return data.extract_html || data.extract;
}
} catch (e) {
continue;
}
}
return null;
}
/* ---------------------------------------------------------
HTML → TEXT CLEANER
--------------------------------------------------------- */
function htmlToText(html) {
const div = document.createElement("div");
div.innerHTML = html;
return div.innerText;
}
/* ---------------------------------------------------------
FACT ROTATION
--------------------------------------------------------- */
function startArtistFactRotation() {
if (factInterval) clearInterval(factInterval);
if (artistFacts.length === 0) {
factBox.textContent = "No artist information available.";
return;
}
factIndexArtist = 0;
factBox.textContent = artistFacts[0];
factInterval = setInterval(() => {
factIndexArtist = (factIndexArtist + 1) % artistFacts.length;
factBox.textContent = artistFacts[factIndexArtist];
}, 8000);
}
/* ---------------------------------------------------------
MAIN FACT FETCHER
--------------------------------------------------------- */
async function updateFactBox() {
const name = artistEl.textContent.trim();
const title = document.getElementById("title").textContent.trim();
if (!name || name === "Loading..." || name === "Connecting...") return;
if (name === "Station Offline") {
artistFacts = genericFallbackFacts("This track");
startArtistFactRotation();
return;
}
// ✅ SONG-LEVEL CACHE CHECK
if (songFactCache.has(title)) {
artistFacts = songFactCache.get(title);
factsLoaded = true;
startArtistFactRotation();
return;
}
if (!factsLoaded) {
factBox.textContent = "Loading artist info...";
}
let lookupName = name;
if (name === "Unknown Artist" || name.trim() === "") {
lookupName = title;
}
const composer = detectComposerFromTitle(title);
if (composer) lookupName = composer;
const detectedArtist = detectArtistFromSong(title);
if (detectedArtist) lookupName = detectedArtist;
try {
let summaryHTML = await getWikipediaSummary(lookupName);
if (!summaryHTML) {
try {
const searchURL =
`https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(lookupName)}&format=json&origin=*`;
const searchRes = await fetch(searchURL);
const searchData = await searchRes.json();
if (searchData.query.search.length > 0) {
const bestMatch = searchData.query.search[0].title;
summaryHTML = await getWikipediaSummary(bestMatch);
}
} catch (e) {}
}
if (!summaryHTML) {
artistFacts = genericFallbackFacts(lookupName);
} else {
const clean = htmlToText(summaryHTML);
artistFacts = clean
.split(/\. |\.$/)
.map(s => s.trim())
.filter(s => s.length > 5)
.slice(0, 4)
.map(s => s + ".");
}
// ✅ STORE IN SONG CACHE
songFactCache.set(title, artistFacts);
factsLoaded = true;
startArtistFactRotation();
} catch (err) {
console.error(err);
artistFacts = genericFallbackFacts(name);
startArtistFactRotation();
}
}
/* ---------------------------------------------------------
WATCH ARTIST FIELD FOR CHANGES
--------------------------------------------------------- */
const observer = new MutationObserver(updateFactBox);
observer.observe(artistEl, { childList: true, subtree: true, characterData: true });
updateFactBox();
</script>
</body>
</html>