[object Object]

← back to Nineoh Guide

cast photos: 12/14 headshots from Wikimedia (per-file license-verified PD/CC-BY/CC-BY-SA only) into rights-ledger w/ attribution; wired into /cast + detail; throttled+idempotent ingester

73371b37a6bdba7d867bb08d59e3110baf139509 · 2026-07-25 11:38:37 -0700 · Steve

Files touched

Diff

commit 73371b37a6bdba7d867bb08d59e3110baf139509
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jul 25 11:38:37 2026 -0700

    cast photos: 12/14 headshots from Wikimedia (per-file license-verified PD/CC-BY/CC-BY-SA only) into rights-ledger w/ attribution; wired into /cast + detail; throttled+idempotent ingester
---
 apps/web/app/cast/[slug]/page.tsx | 28 +++++++++++++++++++++++++---
 apps/web/app/cast/page.tsx        | 24 +++++++++++++++++++++---
 2 files changed, 46 insertions(+), 6 deletions(-)

diff --git a/apps/web/app/cast/[slug]/page.tsx b/apps/web/app/cast/[slug]/page.tsx
index 0709c83..3e5abe3 100644
--- a/apps/web/app/cast/[slug]/page.tsx
+++ b/apps/web/app/cast/[slug]/page.tsx
@@ -1,7 +1,24 @@
 import type { Metadata } from "next";
 import { notFound } from "next/navigation";
 import { pool } from "@/lib/db";
-import { SITE_URL, slugify, castPath } from "@nineoh/core";
+import { SITE_URL, slugify, castPath, type Asset } from "@nineoh/core";
+import { RightsImage } from "@/components/RightsImage";
+
+function toAsset(r: any): Asset | null {
+  if (!r?.file_url) return null;
+  return {
+    id: r.id ?? "x",
+    type: "image",
+    fileUrl: r.file_url,
+    thumbnailUrl: r.file_url,
+    licenseType: r.license_type,
+    licenseUrl: r.license_url ?? null,
+    attributionText: r.attribution_text ?? null,
+    usageScope: "editorial",
+    approvedForMarketing: false,
+    licensePurchaseStatus: "none",
+  };
+}
 
 export const dynamic = "force-dynamic";
 
@@ -10,10 +27,12 @@ type Params = { slug: string };
 async function getPersonBySlug(slug: string) {
   try {
     const { rows } = await pool.query(
-      `select cp.name, cp.biography_original, cp.bio_status, ch.name as character_name
+      `select cp.id, cp.name, cp.biography_original, cp.bio_status, ch.name as character_name,
+              a.file_url, a.license_type, a.license_url, a.attribution_text
          from cast_people cp
          left join credits cr on cr.person_id = cp.id and cr.credit_type='main-cast'
-         left join characters ch on ch.id = cr.character_id`
+         left join characters ch on ch.id = cr.character_id
+         left join assets a on a.id = cp.headshot_asset_id`
     );
     return rows.find((r) => slugify(r.name) === slug) ?? null;
   } catch {
@@ -67,6 +86,9 @@ export default async function CastMember({
       <p style={{ fontSize: 12, color: "#999" }}>
         <a href="/cast">← Cast</a>
       </p>
+      <div style={{ float: "right", marginLeft: 16 }}>
+        <RightsImage asset={toAsset(p)} alt={p.name} width={160} height={200} />
+      </div>
       <h1 style={{ marginBottom: 2 }}>{p.name}</h1>
       {p.character_name ? (
         <p style={{ color: "#8a1c1c", marginTop: 0, fontWeight: 600 }}>
diff --git a/apps/web/app/cast/page.tsx b/apps/web/app/cast/page.tsx
index afb8df1..45ac271 100644
--- a/apps/web/app/cast/page.tsx
+++ b/apps/web/app/cast/page.tsx
@@ -1,6 +1,22 @@
 import { pool } from "@/lib/db";
 import { RightsImage } from "@/components/RightsImage";
-import { castPath } from "@nineoh/core";
+import { castPath, type Asset } from "@nineoh/core";
+
+function toAsset(r: any): Asset | null {
+  if (!r.file_url) return null;
+  return {
+    id: r.id,
+    type: "image",
+    fileUrl: r.file_url,
+    thumbnailUrl: r.file_url,
+    licenseType: r.license_type,
+    licenseUrl: r.license_url ?? null,
+    attributionText: r.attribution_text ?? null,
+    usageScope: "editorial",
+    approvedForMarketing: false,
+    licensePurchaseStatus: "none",
+  };
+}
 
 export const dynamic = "force-dynamic";
 export const metadata = { title: "Cast" };
@@ -9,10 +25,12 @@ async function getCast() {
   try {
     const { rows } = await pool.query(
       `select cp.id, cp.name, cp.biography_original,
-              ch.name as character_name
+              ch.name as character_name,
+              a.file_url, a.license_type, a.license_url, a.attribution_text
          from cast_people cp
          left join credits cr on cr.person_id = cp.id and cr.credit_type = 'main-cast'
          left join characters ch on ch.id = cr.character_id
+         left join assets a on a.id = cp.headshot_asset_id
         order by cp.name`
     );
     return rows;
@@ -35,7 +53,7 @@ export default async function Cast() {
         <div style={{ display: "grid", gap: 16 }}>
           {cast.map((p) => (
             <div key={p.id} style={{ display: "flex", gap: 12 }}>
-              <RightsImage asset={null} alt={p.name} width={90} height={90} />
+              <RightsImage asset={toAsset(p)} alt={p.name} width={90} height={90} />
               <div>
                 <strong>
                   <a href={castPath(p.name)}>{p.name}</a>

← 8af5353 auto-save: 2026-07-25T11:35:57 (1 files) — db/ingest-wikimed  ·  back to Nineoh Guide  ·  beverly hills photos: 8 free-licensed CC/PD city photos (Rod 8fd5a56 →