← back to Crazy News Channel Shadowman
daily-cartoons/build_review.py
76 lines
#!/usr/bin/env python3
"""Rebuild daily-cartoons/review.html (static, local, file://-safe) from queue/*/*/meta.json."""
import html, json, os
import common as C
def items():
out = []
if not os.path.isdir(C.QUEUE): return out
for d in sorted(os.listdir(C.QUEUE)):
dp = os.path.join(C.QUEUE, d)
if not os.path.isdir(dp): continue
for s in sorted(os.listdir(dp)):
mp = os.path.join(dp, s, "meta.json")
if os.path.exists(mp):
m = json.load(open(mp)); m["_rel"] = f"queue/{d}/{s}"
m["_has_clip"] = os.path.exists(os.path.join(dp, s, "clip.mp4"))
m["_has_poster"] = os.path.exists(os.path.join(dp, s, "poster.jpg"))
out.append(m)
return out
def card(m):
e = lambda x: html.escape(str(x or ""))
media = (f'<video controls preload="none" poster="{e(m["_rel"])}/poster.jpg" src="{e(m["_rel"])}/clip.mp4"></video>'
if m["_has_clip"] else (f'<img src="{e(m["_rel"])}/poster.jpg" alt="">' if m["_has_poster"] else '<div class="nomedia">no media</div>'))
story_id = m.get("story_id")
cmd = (f'python3 daily-cartoons/approve.py {m["slug"]} --date {m["date"]}' + (f' --story-id {story_id}' if story_id
else ' --story-id <story-id>'))
story_html = (f'<p class="src"><a href="{e(m.get("story_url"))}" target="_blank" rel="noopener noreferrer">📰 {e(m.get("story_title"))}</a>'
f' <span class="chip">{e(m.get("story_source"))}</span></p>' if m.get("story_url")
else '<p class="src no-story">no linked article — pre-TK-12237 item, pass --story-id to approve</p>')
prompts = f'<p><b>image:</b> {e(m.get("image_prompt"))}</p>'
if m.get("motion_prompt"):
prompts += f'<p><b>motion:</b> {e(m.get("motion_prompt"))}</p>'
return f'''<article class="card" data-created="{e(m.get("created_at"))}" data-title="{e(m.get("title"))}" data-cost="{m.get("cost",{}).get("total",0)}">
{media}
<div class="body">
<h2>{e(m.get("title"))}</h2>
<p class="cap">“{e(m.get("caption"))}”</p>
{story_html}
<div class="chips"><span class="chip when" title="{e(m.get("created_at"))}" data-iso="{e(m.get("created_at"))}">🕓 {e(m.get("created_at"))}</span>
<span class="chip">${m.get("cost",{}).get("total",0):.3f}</span><span class="chip st-{e(m.get("status"))}">{e(m.get("status"))}</span></div>
<details><summary>prompts</summary>{prompts}</details>
<code class="cmd" title="click to copy">{cmd}</code>
</div>
</article>'''
def build():
its = items()
its.sort(key=lambda m: m.get("created_at", ""), reverse=True)
total = sum(m.get("cost", {}).get("total", 0) for m in its)
# TK-12226: link ONLY the shared stylesheet (assets/style.css) — no
# per-page <style> block, no locally hardcoded palette/font/dark-mode
# media query. Layout lives in style.css under body.cartoon-queue-page.
page = f'''<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>P24 Cartoon Queue</title>
<link rel="stylesheet" href="../assets/style.css"></head><body class="cartoon-queue-page">
<header><div><h1>🖋️ P24 Daily Cartoon Queue</h1>
<p class="note">{len(its)} queued · total spend ${total:.3f} · LOCAL review only — approving copies into <code>cartoons/</code> locally; nothing deploys.</p></div>
<div class="ctrl"><label>Sort <select id="sort"><option value="new">Newest</option><option value="old">Oldest</option><option value="title">Title A→Z</option><option value="cost">Cost ↓</option></select></label>
<label>Size <input id="dens" type="range" min="220" max="640" step="20" value="340"></label></div></header>
<main id="grid">{"".join(card(m) for m in its) or "<p>Queue is empty — run daily-cartoons/run-daily.sh</p>"}</main>
<script>
const g=document.getElementById('grid'),S=document.getElementById('sort'),D=document.getElementById('dens');
const ls=(k,v)=>{{try{{return v===undefined?localStorage.getItem(k):localStorage.setItem(k,v)}}catch(e){{return null}}}};
document.querySelectorAll('.when').forEach(el=>{{const d=new Date(el.dataset.iso);if(!isNaN(d))el.textContent='🕓 '+d.toLocaleString(undefined,{{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}})}});
function sort(){{const c=[...g.querySelectorAll('.card')],v=S.value;c.sort((a,b)=>v==='title'?a.dataset.title.localeCompare(b.dataset.title):v==='cost'?b.dataset.cost-a.dataset.cost:v==='old'?a.dataset.created.localeCompare(b.dataset.created):b.dataset.created.localeCompare(a.dataset.created));c.forEach(x=>g.appendChild(x));ls('p24q-sort',v)}}
function dens(){{g.style.setProperty('--min',D.value+'px');ls('p24q-dens',D.value)}}
S.value=ls('p24q-sort')||'new';D.value=ls('p24q-dens')||340;S.onchange=sort;D.oninput=dens;sort();dens();
document.querySelectorAll('.cmd').forEach(el=>el.onclick=()=>navigator.clipboard&&navigator.clipboard.writeText(el.textContent));
</script></body></html>'''
open(os.path.join(C.HERE, "review.html"), "w").write(page)
return len(its)
if __name__ == "__main__":
print("review.html:", build(), "items")