← 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 &lt;story-id&gt;'))
    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">&ldquo;{e(m.get("caption"))}&rdquo;</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")