feat(credits): public CC-BY/BY-SA attribution colophon + CC BY-SA work license
CC BY / CC BY-SA footage needs its author + license shown to the viewer, and the installation alters every frame (a derivative). Per-clip license/source already shipped in clips.json but only surfaced in the dev panel and internal review pages — the public experience showed nothing, so the static benstull.art site was not attribution-compliant. Option A — add a manifest-driven credits/colophon (credits.html + credits.js): the page fetches the same clips the experience uses (baked clips.json in static, the live API otherwise) and renders one attribution block per clip (title, license, source), HTML-escaped, sorted, never silently dropping a clip. An "ⓘ Credits & licenses" link in the header (localised en/es/fr/ja) makes it reachable from the work — what CC's "reasonable to the medium" calls for. Audio is all PD/CC0, credited as a courtesy block. Option D1 — the colophon declares the altered work itself is offered under CC BY-SA 4.0 and states the footage is modified, satisfying the ShareAlike + "indicate changes" obligations for the BY-SA sources. Non-commercial. Ships in the static build (added to PUBLIC_ASSETS). Verified: 41/41 clips credited incl. all 7 CC-BY/BY-SA clips, 0 missing a license. Tests: node units for creditEntries/creditsListHtml + escaping + page structure/declaration; pytest build-output assertion; Playwright e2e (page lists attributions, header links through). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
"use strict";
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const C = require("../static/credits.js");
|
||||
|
||||
const CLIPS = [
|
||||
{ id: "reef_x", title: "Reef X", license: "CC-BY-SA 4.0 — credit Joe Mabel", source: "Wikimedia <reef>" },
|
||||
{ id: "cosmos", title: "Cosmic Cliffs", license: "CC-BY-class — credit NASA, ESA, CSA, STScI", source: "NASA SVS 31348" },
|
||||
{ id: "pd_only", title: "PD clip", license: "public-domain (US Gov, 17 U.S.C. §105)", source: "NASA" },
|
||||
];
|
||||
|
||||
test("creditEntries returns one entry per clip, preserving license/source", () => {
|
||||
const out = C.creditEntries(CLIPS);
|
||||
assert.equal(out.length, CLIPS.length, "no clip is silently dropped from the credits");
|
||||
const reef = out.find((e) => e.id === "reef_x");
|
||||
assert.equal(reef.license, "CC-BY-SA 4.0 — credit Joe Mabel");
|
||||
assert.equal(reef.source, "Wikimedia <reef>");
|
||||
assert.equal(reef.title, "Reef X");
|
||||
});
|
||||
|
||||
test("creditEntries is sorted by id for deterministic output", () => {
|
||||
const ids = C.creditEntries(CLIPS).map((e) => e.id);
|
||||
assert.deepEqual(ids, ["cosmos", "pd_only", "reef_x"]);
|
||||
});
|
||||
|
||||
test("creditEntries falls back title->id and tolerates missing fields", () => {
|
||||
const out = C.creditEntries([{ id: "bare" }]);
|
||||
assert.deepEqual(out, [{ id: "bare", title: "bare", license: "", source: "" }]);
|
||||
});
|
||||
|
||||
test("creditEntries handles null/empty input", () => {
|
||||
assert.deepEqual(C.creditEntries(null), []);
|
||||
assert.deepEqual(C.creditEntries([]), []);
|
||||
});
|
||||
|
||||
test("creditsListHtml renders every clip's attribution and escapes HTML", () => {
|
||||
const html = C.creditsListHtml(CLIPS);
|
||||
assert.match(html, /Cosmic Cliffs/);
|
||||
assert.match(html, /Joe Mabel/);
|
||||
// free-text source "Wikimedia <reef>" must be escaped, not injected as a tag
|
||||
assert.match(html, /Wikimedia <reef>/);
|
||||
assert.doesNotMatch(html, /<reef>/);
|
||||
// one block per clip
|
||||
const blocks = html.match(/class="credit"/g) || [];
|
||||
assert.equal(blocks.length, CLIPS.length);
|
||||
});
|
||||
|
||||
test("credits.html wires config.js + credits.js, the attributions container, and a back link", () => {
|
||||
const html = fs.readFileSync(path.join(__dirname, "../static/credits.html"), "utf8");
|
||||
assert.match(html, /<script src="config\.js">/); // static/live media-base + flag
|
||||
assert.match(html, /<script src="credits\.js">/);
|
||||
assert.match(html, /id="video-credits"/); // JS fills this
|
||||
assert.match(html, /href="index\.html"/); // back into the experience
|
||||
});
|
||||
|
||||
test("credits.html carries the CC BY-SA derivative declaration (Option D1)", () => {
|
||||
const html = fs.readFileSync(path.join(__dirname, "../static/credits.html"), "utf8");
|
||||
assert.match(html, /CC BY-SA 4\.0/); // the altered work's own license
|
||||
assert.match(html, /altered/i); // states the footage is modified
|
||||
});
|
||||
Reference in New Issue
Block a user