#!/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"""
×
{safe}
CLICK ANYWHERE TO DISMISS
"""
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"""
""")
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()