1document.addEventListener("DOMContentLoaded", function () { 2 const historyContainer = document.getElementById("song-history"); 3 const tableContainer = document.getElementById("playlist-container"); 4 const endpoint = "/api/played"; 5 const timeFormatter = new Intl.DateTimeFormat("de-DE", { 6 timeZone: "Europe/Berlin", 7 hour: "2-digit", 8 minute: "2-digit" 9 }); 10 const dateFormatter = new Intl.DateTimeFormat("de-DE", { 11 timeZone: "Europe/Berlin", 12 day: "2-digit", 13 month: "2-digit", 14 year: "numeric" 15 }); 16 17 function escapeHtml(value) { 18 return String(value) 19 .replace(/&/g, "&") 20 .replace(/</g, "<") 21 .replace(/>/g, ">") 22 .replace(/"/g, """) 23 .replace(/'/g, "'"); 24 } 25 26 function normalize(data) { 27 if (!Array.isArray(data)) return []; 28 return data 29 .filter((entry) => entry && entry.playedat && entry.title) 30 .sort((a, b) => Number(b.playedat) - Number(a.playedat)); 31 } 32 33 function renderCompact(entries) { 34 if (!historyContainer) return; 35 const body = historyContainer.querySelector("#body") || historyContainer; 36 if (!entries.length) { 37 body.textContent = "Playlist konnte nicht geladen werden."; 38 return; 39 } 40 41 const nowPlaying = escapeHtml(entries[0].title); 42 const rows = entries 43 .map((entry) => { 44 const date = new Date(Number(entry.playedat) * 1000); 45 return escapeHtml(timeFormatter.format(date)) + " - " + escapeHtml(entry.title); 46 }) 47 .join("<br>"); 48 49 body.innerHTML = "now playing: " + nowPlaying + "<br><br>" + rows; 50 } 51 52 function renderTable(entries) { 53 if (!tableContainer) return; 54 if (!entries.length) { 55 tableContainer.textContent = "Playlist konnte nicht geladen werden."; 56 return; 57 } 58 59 const rows = entries 60 .map((entry) => { 61 const date = new Date(Number(entry.playedat) * 1000); 62 return "<tr><td>" + dateFormatter.format(date) + "</td><td>" + 63 timeFormatter.format(date) + "</td><td>" + escapeHtml(entry.title) + "</td></tr>"; 64 }) 65 .join(""); 66 67 tableContainer.innerHTML = "<table class=\"playlist\"><thead><tr><th>Datum</th><th>Uhrzeit</th><th>Titel</th></tr></thead><tbody>" + rows + "</tbody></table>"; 68 } 69 70 async function loadHistory() { 71 try { 72 const response = await fetch(endpoint, { cache: "no-store" }); 73 if (!response.ok) throw new Error("HTTP " + response.status); 74 const entries = normalize(await response.json()); 75 renderCompact(entries); 76 renderTable(entries); 77 } catch (error) { 78 console.error(error); 79 renderCompact([]); 80 renderTable([]); 81 } 82 } 83 84 loadHistory(); 85 window.setInterval(loadHistory, 60000); 86});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.