#!/usr/bin/env python3 """ Watchlists build script. For each *-watchlist.md in this folder: - Parse title, optional first-load lightbox, and playlist entries - Generate a self-contained HTML page Then update watchlists-hub.html subtitle with current totals. Output filenames: strip '-watchlist' suffix, lowercase the stem, no hyphens/spaces. ContentAddictionArchive-watchlist.md → contentaddictionarchive.html AnalogFrontier-watchlist.md → analogfrontier.html etc. Run after adding or editing any *-watchlist.md file. """ import re from pathlib import Path from html import escape HERE = Path(__file__).parent HUB = HERE / "watchlists-hub.html" COLORS = ["teal", "green", "toucan"] # rotation order 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 # e.g. "AnalogFrontier-watchlist" stem = re.sub(r'-watchlist$', '', stem, flags=re.IGNORECASE) return stem.lower().replace('-', '').replace('_', '').replace(' ', '') def parse_md(path: Path) -> dict: text = path.read_text(encoding='utf-8') # Title: first # heading m = re.search(r'^#\s+(.+)$', text, re.MULTILINE) title = m.group(1).strip() if m else path.stem # Optional lightbox: >>on-first-load - lightbox - "text" m = re.search(r'>>on-first-load\s+-\s+lightbox\s+-\s+"([^"]+)"', text) lightbox = m.group(1).strip() if m else None # Playlist entries # - **Name \- YouTube** # - URL: https://www.youtube.com/playlist?list=PLxxxxxx playlists = [] for m in re.finditer( r'-\s+\*\*(.+?)\s*\\-\s*YouTube\*\*\s*\n\s+-\s+URL:\s+(https://[^\s]+)', text ): name = m.group(1).strip() url = m.group(2).strip() lid = re.search(r'[?&]list=([A-Za-z0-9_-]+)', url) if lid: playlists.append({'name': name, 'list_id': lid.group(1)}) return {'title': title, 'lightbox': lightbox, 'playlists': playlists} # ── HTML generation ─────────────────────────────────────────────────────────── def build_lightbox_html(text: str, slug: str) -> tuple[str, str]: """Return (lightbox_div_html, lightbox_js_html). Empty strings if no lightbox.""" if not text: return '', '' safe = escape(text) div = f""" """ js = f""" // Lightbox: show on first load const lightbox = document.getElementById('lightbox'); if (!localStorage.getItem('watchlist-{slug}-seen')) {{ lightbox.style.display = 'flex'; localStorage.setItem('watchlist-{slug}-seen', '1'); }} function closeLightbox() {{ lightbox.style.display = 'none'; }} lightbox.addEventListener('click', (e) => {{ if (e.target === lightbox) closeLightbox(); }}); document.getElementById('caption-close').addEventListener('click', closeLightbox); document.addEventListener('keydown', (e) => {{ if (e.key === 'Escape') closeLightbox(); }}); """ return div, js def build_panels_html(playlists: list) -> str: panels = [] for i, pl in enumerate(playlists): color = COLORS[i % len(COLORS)] num = f"PANEL {i+1:02d}" embed_url = f"https://www.youtube.com/embed/videoseries?list={pl['list_id']}" title_esc = escape(pl['name'].upper()) panels.append(f"""

{title_esc}

{num}
""") return '\n'.join(panels) def build_stagger_css(count: int) -> str: lines = [] for i in range(1, count + 1): delay = (i - 1) * 60 lines.append( f" .playlist-panel:nth-child({i}) {{ animation-delay: {delay}ms; }}" ) return '\n'.join(lines) def generate_html(data: dict, slug: str) -> str: title = data['title'] playlists = data['playlists'] count = len(playlists) title_upper = title.upper() count_label = f"{count} PLAYLIST{'S' if count != 1 else ''}" lightbox_div, lightbox_js = build_lightbox_html(data['lightbox'], slug) panels_html = build_panels_html(playlists) stagger_css = build_stagger_css(count) # Lightbox CSS block (only if there's a lightbox) lightbox_css = """ /* ── Lightbox ── */ #lightbox { position: fixed; inset: 0; background: rgba(4, 6, 11, 0.92); display: none; justify-content: center; align-items: center; z-index: 2000; padding: 2rem; cursor: pointer; } .caption-box { background: #080d10; border: 2px solid var(--wl-teal); border-top: 10px solid var(--wl-toucan); border-left: 6px solid var(--wl-green); max-width: 640px; width: 100%; max-height: 90vh; overflow-y: auto; padding: 2rem 2rem 1.5rem; position: relative; cursor: default; } .caption-close { position: absolute; top: 0.6rem; right: 0.75rem; background: none; border: none; color: var(--wl-teal); font-family: 'Rambla', sans-serif; font-size: 1.6rem; font-weight: 700; line-height: 1; cursor: pointer; padding: 0.1rem 0.3rem; transition: color 100ms ease; } .caption-close:hover { color: var(--wl-toucan); } .caption-text { font-style: italic; font-size: 1.05rem; color: var(--text-warm); margin-bottom: 1.25rem; line-height: 1.65; padding-right: 1.5rem; } .dismiss-hint { font-weight: 700; text-transform: uppercase; font-size: 0.62rem; letter-spacing: 0.18em; color: var(--muted); text-align: center; margin-top: 1.25rem; } """ if data['lightbox'] else '' return f""" {escape(title_upper)} | WATCHLISTS {lightbox_div}
{panels_html}
""" # ── Hub subtitle update ─────────────────────────────────────────────────────── def update_hub_subtitle(collection_count: int, playlist_total: int): hub_text = HUB.read_text(encoding='utf-8') subtitle = ( f'
' f'{collection_count} COLLECTION{"S" if collection_count != 1 else ""}' f' · CURATED VIDEO ARCHIVES' f' · {playlist_total} PLAYLIST{"S" if playlist_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('*-watchlist.md')) if not md_files: print("No *-watchlist.md files found.") return total_playlists = 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['playlists']) if count == 0: print(f" SKIP {md_path.name} — no playlists found") continue html = generate_html(data, slug) out.write_text(html, encoding='utf-8') total_playlists += count print(f" BUILD {out.name} — {data['title']} ({count} playlists)") update_hub_subtitle(len(md_files), total_playlists) print(f"\nUpdated hub subtitle — {len(md_files)} collections, {total_playlists} playlists total") if __name__ == '__main__': main()