#!/usr/bin/env python3 """ Music build script. For each *-music.md in this folder: - Parse title, optional first-load lightbox, and track entries - Two entry types detected by URL: YouTube → youtube.com/watch?v=VIDEO_ID → video embed card Audio → files.exopraxist.org/share/TOKEN → audio card + floating player - Generate a self-contained HTML page Then update music-hub.html subtitle with current totals. Output filenames: strip '-music' suffix, lowercase the stem. OldMusicandMVs-music.md → oldmusicandmvs.html Run after adding or editing any *-music.md file. """ import re from pathlib import Path from html import escape HERE = Path(__file__).parent HUB = HERE / "music-hub.html" ALLOW = ( "accelerometer; autoplay; clipboard-write; " "encrypted-media; gyroscope; picture-in-picture; web-share" ) # ── MD parsing ──────────────────────────────────────────────────────────────── def slug_from_path(path: Path) -> str: stem = path.stem stem = re.sub(r'-music$', '', stem, flags=re.IGNORECASE) return stem.lower().replace('-', '').replace('_', '').replace(' ', '') def parse_md(path: Path) -> dict: text = path.read_text(encoding='utf-8') m = re.search(r'^#\s+(.+)$', text, re.MULTILINE) title = m.group(1).strip() if m else path.stem m = re.search(r'>>on-first-load\s+-\s+lightbox\s+-\s+"([^"]+)"', text) lightbox = m.group(1).strip() if m else None entries = [] for m in re.finditer( r'-\s+\*\*(.+?)\*\*\s*\n\s+-\s+URL:\s+(https://[^\s]+)', text ): name = m.group(1).strip() # Strip trailing \- Source label from name (e.g. "Title \- YouTube") name = re.sub(r'\s*\\-\s*.+$', '', name).strip() url = m.group(2).strip() if 'youtube.com/watch' in url: vid = re.search(r'[?&]v=([A-Za-z0-9_-]+)', url) if vid: entries.append({'type': 'youtube', 'name': name, 'video_id': vid.group(1)}) elif 'files.exopraxist.org/share/' in url: token = url.rstrip('/').split('/')[-1] entries.append({'type': 'audio', 'name': name, 'share_url': url, 'direct_url': f"https://files.exopraxist.org/api/public/dl/{token}"}) return {'title': title, 'lightbox': lightbox, 'entries': entries} # ── HTML blocks ─────────────────────────────────────────────────────────────── def build_lightbox_html(text: str, slug: str) -> tuple[str, str]: if not text: return '', '' # Preserve — JL attribution if present at the end body = text attribution = '' m = re.search(r'\s*-\s*JL\s*$', text) if m: body = text[:m.start()].strip() attribution = '' safe_body = escape(body) div = f""" """ js = f""" const lightbox = document.getElementById('lightbox'); if (!localStorage.getItem('music-{slug}-seen')) {{ lightbox.style.display = 'flex'; localStorage.setItem('music-{slug}-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(); }}); """ return div, js def build_video_card(entry: dict) -> str: embed_url = f"https://www.youtube.com/embed/{entry['video_id']}" title_esc = escape(entry['name']) return f"""

{title_esc}

""" def build_audio_card(entry: dict, audio_index: int) -> str: title_esc = escape(entry['name']) direct_url = entry['direct_url'] share_url = entry['share_url'] return f"""

{title_esc}

⬇ DOWNLOAD
0:00 --:--
""" def build_entries_html(entries: list) -> str: parts = [] audio_idx = 1 for e in entries: if e['type'] == 'youtube': parts.append(build_video_card(e)) else: parts.append(build_audio_card(e, audio_idx)) audio_idx += 1 return '\n'.join(parts) def build_stagger_css(count: int) -> str: lines = [] for i in range(1, min(count, 12) + 1): lines.append( f" .card:nth-child({i}) {{ animation-delay: {(i-1)*50}ms; }}" ) return '\n'.join(lines) # ── Full page generation ────────────────────────────────────────────────────── def generate_html(data: dict, slug: str) -> str: title = data['title'] entries = data['entries'] count = len(entries) has_audio = any(e['type'] == 'audio' for e in entries) title_upper = title.upper() count_label = f"{count} TRACK{'S' if count != 1 else ''}" lightbox_div, lightbox_js = build_lightbox_html(data['lightbox'], slug) entries_html = build_entries_html(entries) stagger_css = build_stagger_css(count) lightbox_css = """ /* --- Lightbox --- */ #lightbox { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(4, 6, 11, 0.9); display: none; align-items: center; justify-content: center; z-index: 1000; padding: 2rem; cursor: pointer; } .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; cursor: default; } .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; padding-right: 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); } """ if data['lightbox'] else '' audio_css = """ /* --- Audio Card --- */ .audio-card { padding: 1.5rem; } .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; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; cursor: pointer; transition: var(--transition); font-family: inherit; } .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; display: inline-block; margin-bottom: 0.5rem; transition: var(--transition); } .download-link:hover { text-decoration: underline; } .progress-container { height: 4px; background: var(--mu-red); width: 100%; cursor: pointer; } .progress-bar { height: 100%; background: var(--mu-orange); width: 0%; transition: width 100ms linear; } .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; } /* --- 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; 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; transition: var(--transition); } .fp-btn:hover { color: var(--text-warm); } .fp-progress-wrap { position: absolute; top: -2px; left: 0; width: 100%; height: 2px; background: var(--mu-red); } .fp-progress-fill { height: 100%; background: var(--mu-orange); width: 0%; transition: width 100ms linear; } """ if has_audio else '' floating_player_html = """
No Track Selected
""" if has_audio else '' audio_js = """ // --- 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 fpFill = document.querySelector('.fp-progress-fill'); let currentTrackBtn = null; const formatTime = (t) => { const m = Math.floor(t / 60); const s = Math.floor(t % 60); return `${m}:${s.toString().padStart(2, '0')}`; }; const updateUI = () => { const playing = !audio.paused; const icon = playing ? '▐▐' : '▶'; if (currentTrackBtn) currentTrackBtn.textContent = icon; fpPlayBtn.textContent = icon; if (playing) { floatingPlayer.classList.add('active'); document.body.classList.add('player-active'); } }; document.querySelectorAll('.play-btn').forEach(btn => { btn.addEventListener('click', () => { const src = btn.dataset.src; const title = btn.dataset.title; if (audio.getAttribute('data-loaded') === src) { if (audio.paused) audio.play(); else audio.pause(); } else { if (currentTrackBtn) currentTrackBtn.textContent = '▶'; audio.src = src; audio.setAttribute('data-loaded', src); fpTitle.textContent = title; currentTrackBtn = btn; audio.play(); } }); }); fpPlayBtn.addEventListener('click', () => { if (audio.paused) audio.play(); else audio.pause(); }); audio.addEventListener('play', updateUI); audio.addEventListener('pause', updateUI); audio.addEventListener('timeupdate', () => { if (!audio.duration) return; const pct = (audio.currentTime / audio.duration) * 100; if (fpFill) fpFill.style.width = pct + '%'; if (currentTrackBtn) { const card = currentTrackBtn.closest('.audio-card'); const bar = card.querySelector('.progress-bar'); const cur = card.querySelector('.current-time'); if (bar) bar.style.width = pct + '%'; if (cur) cur.textContent = formatTime(audio.currentTime); } }); audio.addEventListener('loadedmetadata', () => { if (currentTrackBtn) { const card = currentTrackBtn.closest('.audio-card'); const tot = card.querySelector('.total-time'); if (tot) tot.textContent = formatTime(audio.duration); } }); 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 all = [...document.querySelectorAll('.play-btn')]; const prev = all[all.indexOf(currentTrackBtn) - 1]; if (prev) prev.click(); }); document.getElementById('fp-next').addEventListener('click', () => { const all = [...document.querySelectorAll('.play-btn')]; const next = all[all.indexOf(currentTrackBtn) + 1]; if (next) next.click(); }); document.querySelectorAll('.progress-container').forEach(bar => { bar.addEventListener('click', (e) => { const rect = bar.getBoundingClientRect(); audio.currentTime = ((e.clientX - rect.left) / rect.width) * audio.duration; }); }); """ if has_audio else '' return f""" {escape(title)} | MUSIC {lightbox_div}

{escape(title.upper())}

{entries_html}
{floating_player_html} """ # ── Hub subtitle update ─────────────────────────────────────────────────────── def update_hub_subtitle(collection_count: int, entry_total: int): hub_text = HUB.read_text(encoding='utf-8') subtitle = ( f'' f'{collection_count} COLLECTION{"S" if collection_count != 1 else ""}' f' · PERSONAL ARCHIVE' f' · {entry_total} TRACK{"S" if entry_total != 1 else ""}' f'' ) hub_text = re.sub( r'.*?', f'{subtitle}', hub_text, flags=re.DOTALL ) HUB.write_text(hub_text, encoding='utf-8') # ── Main ────────────────────────────────────────────────────────────────────── def main(): md_files = sorted(HERE.glob('*-music.md')) if not md_files: print("No *-music.md files found.") return total_entries = 0 for md_path in md_files: slug = slug_from_path(md_path) out = HERE / f"{slug}.html" data = parse_md(md_path) count = len(data['entries']) if count == 0: print(f" SKIP {md_path.name} — no entries found") continue audio_count = sum(1 for e in data['entries'] if e['type'] == 'audio') video_count = count - audio_count html = generate_html(data, slug) out.write_text(html, encoding='utf-8') total_entries += count print(f" BUILD {out.name} — {data['title']} ({video_count} videos, {audio_count} audio)") update_hub_subtitle(len(md_files), total_entries) print(f"\nUpdated hub — {len(md_files)} collections, {total_entries} tracks total") if __name__ == '__main__': main()