545 lines
12 KiB
HTML
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>
|