diff --git a/Music/OldMusicandMVs-music.md b/Music/OldMusicandMVs-music.md index 953c73b..231eba5 100644 --- a/Music/OldMusicandMVs-music.md +++ b/Music/OldMusicandMVs-music.md @@ -45,3 +45,6 @@ - **Shortstraw \- Cold Shoulder \[Music Video 720p\] \- YouTube** - URL: https://www.youtube.com/watch?v=pIGIW3h9LpU +- **Dammit - Dwonlaod \ - Free On File Browser** + - URL: https://files.exopraxist.org/share/KeQTyi9r + diff --git a/Music/build.py b/Music/build.py new file mode 100644 index 0000000..9242cde --- /dev/null +++ b/Music/build.py @@ -0,0 +1,769 @@ +#!/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() diff --git a/Music/music-hub.html b/Music/music-hub.html new file mode 100644 index 0000000..5a74a06 --- /dev/null +++ b/Music/music-hub.html @@ -0,0 +1,186 @@ + + + + + + Music Hub — Singular Particular Space + + + + + + +
+ ← SPACE +

MUSIC

+ 1 COLLECTION · PERSONAL ARCHIVE · 14 TRACKS +
+ +
+ + +
+

Old Music and Music Videos

+
+
+ 14 ENTRIES +

An archive of old recordings and music videos directed by JL.

+
+
+ + + +
+

Live Sessions

+
+
+ 0 ENTRIES +

Upcoming recordings from live performances and studio sessions.

+
+
+ + + +
+

Experimental Tapes

+
+
+ 0 ENTRIES +

Found sounds, field recordings, and sonic experiments.

+
+
+
+ + diff --git a/Music/music-template.html b/Music/music-template.html new file mode 100644 index 0000000..fe6db87 --- /dev/null +++ b/Music/music-template.html @@ -0,0 +1,703 @@ + + + + + + Old Music and Music Videos — Singular Particular Space + + + + + + + + + + + +
+

THE ARCHIVE

+ + +
+
+

Dammit (Download — Free)

+
+
+
+ +
+ ⬇ DOWNLOAD +
+
+
+
+ 0:00 + --:-- +
+
+
+
+
+ + +
+

Sixteen by Melanie Kerr — A Music Video

+
+ +
+
+ +
+

Good Girl by Melanie Kerr — A Music Video

+
+ +
+
+ +
+

042 Letter to My Sunrise (Demo Tape)

+
+ +
+
+ +
+

038 Trouble Done Bore Me Down

+
+ +
+
+ +
+

036 That's All Right Mama

+
+ +
+
+ +
+

035 Psycho Bch

+
+ +
+
+ +
+

034 On the Curious Creatures Called People

+
+ +
+
+ +
+

033 Ode to Truth Kind of

+
+ +
+
+ +
+

032 iYeza

+
+ +
+
+ +
+

031 Dammit (Blink 182 Cover)

+
+ +
+
+ +
+

030 An Irish Airman Forsees His Death

+
+ +
+
+ +
+

Trouble Done Bore Me Down

+
+ +
+
+ +
+

Shortstraw — Cold Shoulder (Music Video 720p)

+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+
No Track Selected
+
+
+ + + +
+
+ + + + + + diff --git a/Music/oldmusicandmvs.html b/Music/oldmusicandmvs.html new file mode 100644 index 0000000..4caf875 --- /dev/null +++ b/Music/oldmusicandmvs.html @@ -0,0 +1,705 @@ + + + + + + Old Music and Music Videos - The Archive | MUSIC + + + + + + + + + + +
+

OLD MUSIC AND MUSIC VIDEOS - THE ARCHIVE

+ +
+

Sixteen by Melanie Kerr

+
+ +
+
+ +
+

Good Girl by Melanie Kerr

+
+ +
+
+ +
+

042 Letter to My Sunrise \(Demo Tape\)

+
+ +
+
+ +
+

038 Trouble Done Bore Me Down

+
+ +
+
+ +
+

036 That's All Right Mama

+
+ +
+
+ +
+

035 Psycho Bch

+
+ +
+
+ +
+

034 On the Curious Creatures Called People

+
+ +
+
+ +
+

033 Ode to Truth Kind of

+
+ +
+
+ +
+

032 iYeza

+
+ +
+
+ +
+

031 Dammit Blink 182 Cover

+
+ +
+
+ +
+

030 An Irish Airman Forsees his Death

+
+ +
+
+ +
+

Trouble Done Bore Me Down

+
+ +
+
+ +
+

Shortstraw

+
+ +
+
+ +
+
+

Dammit - Dwonlaod \ - Free On File Browser

+
+
+
+ +
+ ⬇ DOWNLOAD +
+
+
+
+ 0:00 + --:-- +
+
+
+
+
+
+ +
+
+
No Track Selected
+
+ + + +
+
+ + + + diff --git a/index.html b/index.html index 4e76e21..81acab7 100644 --- a/index.html +++ b/index.html @@ -691,7 +691,7 @@ body::after { var STARS = [ { id: 'writings', label: 'Writings', sign: '文', x: 25, y: 22, href: '' }, { id: 'videos', label: 'Videos', sign: '映', x: 68, y: 18, href: 'Videos/index.html' }, - { id: 'music', label: 'Music', sign: '♬', x: 12, y: 44, href: '' }, + { id: 'music', label: 'Music', sign: '♬', x: 12, y: 44, href: 'Music/music-hub.html' }, { id: 'images', label: 'Images', sign: '絵', x: 55, y: 35, href: 'Images/images.html' }, { id: 'playlists', label: 'Playlists', sign: '≡', x: 78, y: 50, href: 'Playlists/playlists.html' }, { id: 'watchlists', label: 'Watchlists', sign: '視', x: 22, y: 66, href: 'Watchlists/watchlists-hub.html' },