Add Music section: hub, template, build pipeline, and homepage wire

Music/build.py parses *-music.md files and generates collection pages supporting
YouTube video embeds (lazy IntersectionObserver) and FileBrowser audio cards with
floating media player (prev/next/auto-advance). Music star node wired in index.html.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-28 15:23:37 +02:00
parent aec3300e40
commit 1e776bee83
6 changed files with 2367 additions and 1 deletions

703
Music/music-template.html Normal file
View File

@@ -0,0 +1,703 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Old Music and Music Videos — Singular Particular Space</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Rancho&family=Rambla:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
<style>
:root {
--bg-void: #04060b;
--text-warm: #e8d5b8;
--mu-red: #8b2020;
--mu-orange: #ff7f3f;
--mu-red-glow: rgba(139, 32, 32, 0.25);
--mu-orange-glow: rgba(255, 127, 63, 0.25);
--transition: 100ms ease;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
border-radius: 0 !important;
}
body {
background-color: var(--bg-void);
color: var(--text-warm);
font-family: 'Rambla', sans-serif;
line-height: 1.6;
margin: 0;
padding-top: 64px;
padding-bottom: 0;
}
body.player-active {
padding-bottom: 64px;
}
/* --- Header --- */
.sticky-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: var(--bg-void);
border-bottom: 2px solid var(--mu-red);
padding: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.header-title {
font-family: 'Rancho', cursive;
font-size: 1.5rem;
color: var(--mu-orange);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 1rem;
}
.header-nav {
display: flex;
align-items: center;
gap: 1.5rem;
}
.nav-link {
font-weight: 700;
text-transform: uppercase;
font-size: 0.85rem;
color: var(--mu-orange);
text-decoration: none;
transition: var(--transition);
}
.nav-link:hover {
color: #ff9f6f;
}
.count {
font-weight: 700;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.1em;
color: rgba(232, 213, 184, 0.5);
}
.menu-toggle {
display: none;
background: none;
border: none;
color: var(--mu-orange);
font-size: 1.5rem;
cursor: pointer;
}
/* --- Main Layout --- */
main {
max-width: 900px;
margin: 3rem auto;
padding: 0 1rem;
}
.section-title {
font-family: 'Rancho', cursive;
font-size: 3rem;
color: var(--mu-orange);
margin-bottom: 2rem;
text-align: center;
}
/* --- Cards --- */
.card {
margin-bottom: 3rem;
border: 2px solid var(--mu-red);
background: rgba(139, 32, 32, 0.05);
transition: var(--transition);
}
.card:hover {
border-color: var(--mu-orange);
box-shadow: 0 0 8px var(--mu-orange-glow);
}
.card-header {
padding: 1rem;
border-bottom: 1px solid var(--mu-red);
}
.card-title {
font-family: 'Rancho', cursive;
font-size: 1.8rem;
color: var(--mu-orange);
}
.video-container {
position: relative;
padding-bottom: 56.25%; /* 16:9 */
height: 0;
overflow: hidden;
background: #000;
}
.video-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
/* --- Audio Card --- */
.audio-card {
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.audio-controls {
display: flex;
align-items: center;
gap: 1.5rem;
}
.play-btn {
background: none;
border: 2px solid var(--mu-orange);
color: var(--mu-orange);
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
cursor: pointer;
transition: var(--transition);
}
.play-btn:hover {
background: var(--mu-orange);
color: var(--bg-void);
}
.audio-info {
flex-grow: 1;
}
.download-link {
font-weight: 700;
text-transform: uppercase;
font-size: 0.75rem;
color: var(--mu-orange);
text-decoration: none;
transition: var(--transition);
}
.download-link:hover {
text-decoration: underline;
}
.progress-container {
height: 4px;
background: var(--mu-red);
width: 100%;
position: relative;
cursor: pointer;
}
.progress-bar {
height: 100%;
background: var(--mu-orange);
width: 0%;
}
.duration-meta {
font-weight: 700;
text-transform: uppercase;
font-size: 0.7rem;
color: rgba(232, 213, 184, 0.5);
margin-top: 0.25rem;
display: flex;
justify-content: space-between;
}
/* --- Lightbox --- */
#lightbox {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(4, 6, 11, 0.9);
display: none; /* JS toggles flex */
align-items: center;
justify-content: center;
z-index: 1000;
padding: 2rem;
}
.lightbox-content {
background: #0c0a08;
border: 2px solid var(--mu-red);
border-top: 8px solid var(--mu-orange);
max-width: 600px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
padding: 2.5rem;
position: relative;
}
.lightbox-close {
position: absolute;
top: 0.5rem;
right: 0.75rem;
font-size: 2rem;
color: var(--mu-orange);
cursor: pointer;
line-height: 1;
}
.lightbox-quote {
font-style: italic;
font-size: 1.1rem;
color: var(--text-warm);
margin-bottom: 1.5rem;
}
.lightbox-attribution {
font-weight: 700;
text-transform: uppercase;
color: var(--mu-orange);
text-align: right;
}
.lightbox-hint {
display: block;
text-align: center;
margin-top: 2rem;
font-weight: 700;
text-transform: uppercase;
font-size: 0.65rem;
letter-spacing: 0.1em;
color: rgba(232, 213, 184, 0.4);
}
/* --- Floating Player --- */
.floating-player {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 64px;
background: var(--bg-void);
border-top: 2px solid var(--mu-red);
z-index: 500;
display: none; /* JS toggles flex */
align-items: center;
padding: 0 1rem;
gap: 1rem;
}
.floating-player.active {
display: flex;
}
.fp-info {
flex: 1;
min-width: 0;
}
.fp-title {
font-family: 'Rancho', cursive;
font-size: 1.2rem;
color: var(--mu-orange);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fp-controls {
display: flex;
align-items: center;
gap: 1rem;
}
.fp-btn {
background: none;
border: none;
color: var(--mu-orange);
font-family: 'Rambla', sans-serif;
font-weight: 700;
font-size: 1rem;
cursor: pointer;
}
.fp-progress-wrap {
position: absolute;
top: -2px;
left: 0;
width: 100%;
height: 2px;
}
/* --- Mobile --- */
@media (max-width: 600px) {
.menu-toggle { display: block; }
.header-nav {
display: none;
position: absolute;
top: 100%;
right: 0;
background: var(--bg-void);
border: 2px solid var(--mu-red);
border-top: none;
flex-direction: column;
padding: 1rem;
gap: 1rem;
width: 200px;
}
.header-nav.open { display: flex; }
.section-title { font-size: 2.2rem; }
}
</style>
</head>
<body>
<div id="lightbox">
<div class="lightbox-content">
<span class="lightbox-close">&times;</span>
<p class="lightbox-quote">
I much prefer making music live. But here are some old recordings. Oh and some music videos that I made for better recording artists than me at the time. At time of writing, there are inklings of thinkings about returning to making music. I'm considering it. We'll see how it plays out.
</p>
<p class="lightbox-attribution">— JL</p>
<span class="lightbox-hint">CLICK ANYWHERE TO DISMISS</span>
</div>
</div>
<header class="sticky-header">
<div class="header-title">OLD MUSIC AND MUSIC VIDEOS — THE ARCHIVE</div>
<button class="menu-toggle"></button>
<nav class="header-nav">
<a href="music-hub.html" class="nav-link">← MUSIC</a>
<span class="count">14 TRACKS</span>
</nav>
</header>
<main>
<h1 class="section-title">THE ARCHIVE</h1>
<!-- Audio Card -->
<div class="card" id="track-1">
<div class="card-header">
<h2 class="card-title">Dammit (Download — Free)</h2>
</div>
<div class="audio-card">
<div class="audio-controls">
<button class="play-btn" data-track-id="1" data-src="https://files.exopraxist.org/api/public/dl/KeQTyi9r" data-title="Dammit (Blink 182 Cover)"></button>
<div class="audio-info">
<a href="https://files.exopraxist.org/share/KeQTyi9r" target="_blank" class="download-link">⬇ DOWNLOAD</a>
<div class="progress-container">
<div class="progress-bar"></div>
</div>
<div class="duration-meta">
<span class="current-time">0:00</span>
<span class="total-time">--:--</span>
</div>
</div>
</div>
</div>
</div>
<!-- Video Cards -->
<div class="card">
<div class="card-header"><h2 class="card-title">Sixteen by Melanie Kerr — A Music Video</h2></div>
<div class="video-container">
<iframe data-src="https://www.youtube.com/embed/zmwv8NXMO6E" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">Good Girl by Melanie Kerr — A Music Video</h2></div>
<div class="video-container">
<iframe data-src="https://www.youtube.com/embed/M4KCZr7Xx7A" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">042 Letter to My Sunrise (Demo Tape)</h2></div>
<div class="video-container">
<iframe data-src="https://www.youtube.com/embed/lbhFd6I_CII" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">038 Trouble Done Bore Me Down</h2></div>
<div class="video-container">
<iframe data-src="https://www.youtube.com/embed/fpm0nfMjm7M" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">036 That's All Right Mama</h2></div>
<div class="video-container">
<iframe data-src="https://www.youtube.com/embed/N_8WL1GhW0s" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">035 Psycho Bch</h2></div>
<div class="video-container">
<iframe data-src="https://www.youtube.com/embed/fsE03yW7M9Y" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">034 On the Curious Creatures Called People</h2></div>
<div class="video-container">
<iframe data-src="https://www.youtube.com/embed/JJtnygYcs5o" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">033 Ode to Truth Kind of</h2></div>
<div class="video-container">
<iframe data-src="https://www.youtube.com/embed/QVQl2uyUBGg" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">032 iYeza</h2></div>
<div class="video-container">
<iframe data-src="https://www.youtube.com/embed/1rN4C828vR4" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">031 Dammit (Blink 182 Cover)</h2></div>
<div class="video-container">
<iframe data-src="https://www.youtube.com/embed/YTZqMbIuGKk" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">030 An Irish Airman Forsees His Death</h2></div>
<div class="video-container">
<iframe data-src="https://www.youtube.com/embed/wOe-KTlWX1Y" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">Trouble Done Bore Me Down</h2></div>
<div class="video-container">
<iframe data-src="https://www.youtube.com/embed/BWJrOOYqRuM" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">Shortstraw — Cold Shoulder (Music Video 720p)</h2></div>
<div class="video-container">
<iframe data-src="https://www.youtube.com/embed/pIGIW3h9LpU" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
</div>
</main>
<!-- Floating Player -->
<div class="floating-player">
<div class="fp-progress-wrap">
<div class="progress-container">
<div class="progress-bar"></div>
</div>
</div>
<div class="fp-info">
<div class="fp-title">No Track Selected</div>
</div>
<div class="fp-controls">
<button class="fp-btn" id="fp-prev">◀◀</button>
<button class="fp-btn" id="fp-play"></button>
<button class="fp-btn" id="fp-next">▶▶</button>
</div>
</div>
<audio id="main-audio"></audio>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- Lightbox ---
const lightbox = document.getElementById('lightbox');
if (!localStorage.getItem('music-oldmusicandmvs-seen')) {
lightbox.style.display = 'flex';
localStorage.setItem('music-oldmusicandmvs-seen', '1');
}
const closeLightbox = () => { lightbox.style.display = 'none'; };
lightbox.addEventListener('click', (e) => {
if (e.target === lightbox) closeLightbox();
});
document.querySelector('.lightbox-close').addEventListener('click', closeLightbox);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeLightbox();
});
// --- Mobile Menu ---
const menuToggle = document.querySelector('.menu-toggle');
const nav = document.querySelector('.header-nav');
menuToggle.addEventListener('click', (e) => {
e.stopPropagation();
nav.classList.toggle('open');
});
document.addEventListener('click', (e) => {
if (!nav.contains(e.target) && e.target !== menuToggle) {
nav.classList.remove('open');
}
});
// --- Lazy Load Iframes ---
const iframeObserver = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const iframe = entry.target;
if (iframe.dataset.src) {
iframe.src = iframe.dataset.src;
iframe.removeAttribute('data-src');
obs.unobserve(iframe);
}
}
});
}, { rootMargin: '200px' });
document.querySelectorAll('iframe').forEach(iframe => {
iframeObserver.observe(iframe);
});
// --- Audio Logic ---
const audio = document.getElementById('main-audio');
const floatingPlayer = document.querySelector('.floating-player');
const fpPlayBtn = document.getElementById('fp-play');
const fpTitle = document.querySelector('.fp-title');
const fpProgressBar = floatingPlayer.querySelector('.progress-bar');
let currentTrackBtn = null;
const updateUI = () => {
const isPlaying = !audio.paused;
const icon = isPlaying ? '▐▐' : '▶';
if (currentTrackBtn) currentTrackBtn.textContent = icon;
fpPlayBtn.textContent = icon;
if (isPlaying) {
floatingPlayer.classList.add('active');
document.body.classList.add('player-active');
}
};
const formatTime = (time) => {
const min = Math.floor(time / 60);
const sec = Math.floor(time % 60);
return `${min}:${sec.toString().padStart(2, '0')}`;
};
document.querySelectorAll('.play-btn').forEach(btn => {
btn.addEventListener('click', () => {
const src = btn.dataset.src;
const title = btn.dataset.title;
if (audio.src === src) {
if (audio.paused) audio.play();
else audio.pause();
} else {
// Reset previous btn if any
if (currentTrackBtn) currentTrackBtn.textContent = '▶';
audio.src = src;
fpTitle.textContent = title;
currentTrackBtn = btn;
audio.play();
}
updateUI();
});
});
fpPlayBtn.addEventListener('click', () => {
if (audio.paused) audio.play();
else audio.pause();
updateUI();
});
audio.addEventListener('timeupdate', () => {
const percent = (audio.currentTime / audio.duration) * 100;
document.querySelectorAll('.progress-bar').forEach(bar => {
bar.style.width = percent + '%';
});
// Update local time display if card is visible
if (currentTrackBtn) {
const card = currentTrackBtn.closest('.audio-card');
card.querySelector('.current-time').textContent = formatTime(audio.currentTime);
}
});
audio.addEventListener('loadedmetadata', () => {
if (currentTrackBtn) {
const card = currentTrackBtn.closest('.audio-card');
card.querySelector('.total-time').textContent = formatTime(audio.duration);
}
});
// Sync floating player button on native play/pause events
audio.addEventListener('play', updateUI);
audio.addEventListener('pause', updateUI);
audio.addEventListener('ended', () => {
const allBtns = [...document.querySelectorAll('.play-btn')];
const idx = allBtns.indexOf(currentTrackBtn);
const next = allBtns[idx + 1];
if (next) {
next.click();
} else {
audio.pause();
if (currentTrackBtn) currentTrackBtn.textContent = '▶';
fpPlayBtn.textContent = '▶';
floatingPlayer.classList.remove('active');
document.body.classList.remove('player-active');
}
});
document.getElementById('fp-prev').addEventListener('click', () => {
const allBtns = [...document.querySelectorAll('.play-btn')];
const idx = allBtns.indexOf(currentTrackBtn);
const prev = allBtns[idx - 1];
if (prev) prev.click();
});
document.getElementById('fp-next').addEventListener('click', () => {
const allBtns = [...document.querySelectorAll('.play-btn')];
const idx = allBtns.indexOf(currentTrackBtn);
const next = allBtns[idx + 1];
if (next) next.click();
});
// Seek logic
document.querySelectorAll('.progress-container').forEach(container => {
container.addEventListener('click', (e) => {
const rect = container.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
audio.currentTime = pos * audio.duration;
});
});
});
</script>
</body>
</html>