Three findings, one thread: rule 4 says verify a URL before shipping it, and the check I wrote for it yesterday was measuring one file. 1. THE TABLES PAGE WAS SHIPPING DEAD FORM. people-group-resources.json — the "Resources by People Group" tab, which a reader clicks — carried 2,294 URL cells, of which 1,412 were jesusfilm.org/watch/?languageId=<id>. That form is IGNORED by the site: a real id and a bogus one render the same generic library page, which is why the map migrated onto Arclight shareUrls. The migration never reached this table, so the booleans beside those links were being refreshed while the links themselves went stale for a year of builds. 1,417 film and 882 scripture URLs re-derived from the index, which is authoritative for both and whose URLs are the verified ones. 2. TWO ARCLIGHT LANGUAGES, ONE ISO CODE — resolved, with evidence rather than a ruling. jos was SKIPPED here, and the note said resolving it meant deciding whether the code or the publisher's name was authoritative, "a data question for someone who can check it". Checked: media-languages?iso3=jos returns 7138 "Jordanian Sign Language" and 139080 "Lebanese Sign Language", and 7138's shareUrl redirects to .../jordanian-sign-language.html while a bogus arc.gt code 404s. So the code stays authoritative for the join and the NAME breaks the tie the code cannot. The old note also had a fact wrong — it called Lebanese "a different language (lbs)", but lbs is LIBYAN Sign Language and Lebanese has no code of its own, so this is two Levantine varieties sharing one code, not a mis-filing. Three Deaf groups now reach the JESUS Film in their own sign language through a link that resolves; 0 retired-form links remain in the index. 3. THE GATE'S SCOPE WAS THE BUG. It walked the pin index alone, which is how item 1 hid from it. It now reads every table the SURFACES read, taken from the surfaces, and resolves each URL against four records: the verification table, the org contacts, the SUN pages (proved by content against a catch-all control) and doxa.life's own listing for prayer pages, which cannot be proved by fetch because pray.doxa.life answers 200 for any slug. Provenance fields are excluded by path — a citation is not an offer. 8,445 links measured yesterday; 12,292 shipped links today, 0 unrecorded, 0 broken, 0 that no status code can prove. Reading every STAGED file instead reported 6,115 unrecorded and 53 broken, all of them correct behaviour: API endpoints in source fields, and build caches that still hold the 53 share URLs the verifier proved dead — which is exactly why the pin builder drops them. A gate that goes red on correct behaviour gets switched off.
162 lines
10 KiB
JavaScript
162 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* fetch-arclight-share-urls.mjs — get the REAL per-language JESUS Film deep link.
|
|
*
|
|
* THE DEFECT THIS FIXES. We shipped https://www.jesusfilm.org/watch/?languageId=<id> for
|
|
* 2,087 languages. MEASURED IN A BROWSER: the current site ignores that query parameter —
|
|
* a real id and a bogus id render the same generic library page. The film was real and
|
|
* the link did not carry anyone to it.
|
|
*
|
|
* Arclight's media-components endpoint returns a shareUrl per language, and that IS a
|
|
* deep link: /watch/jesus/german-standard. It discriminates, which the old URL never did —
|
|
* a real slug returns ~900KB carrying the media component id `1_jf-0-0` 369 times, a
|
|
* bogus slug returns a 33KB page carrying it 0 times.
|
|
*
|
|
* WHY FETCHED PER LANGUAGE AND NOT DERIVED FROM THE NAME. The slug looks derivable —
|
|
* "Pashto, Southern" -> pashto-southern — and 8 of 8 sampled names transformed correctly.
|
|
* Eight is not 2,087, and a wrong slug is not a broken link but a link to SOMEONE ELSE'S
|
|
* language, which is the failure this repo guards hardest against. The API is
|
|
* authoritative, so it is asked. Rate-limited, resumable, and cached so it is asked once.
|
|
*/
|
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
import { writeSnapshot } from './_snapshot-guard.mjs'
|
|
import { resolve, dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
const REPO = resolve(HERE, '../../..')
|
|
const ARC = resolve(REPO, 'data/tables/arclight-index.json')
|
|
const OUT = resolve(REPO, 'data/tables/arclight-share-urls.json')
|
|
const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
|
|
const JF = '1_jf-0-0'
|
|
|
|
/**
|
|
* ═══ TWO ARCLIGHT LANGUAGES, ONE ISO CODE — RESOLVED, WITH THE EVIDENCE ═════════════════════
|
|
*
|
|
* This code was SKIPPED, and the note here said resolving it meant deciding whether the ISO code
|
|
* or the publisher's name was authoritative — "a data question for someone who can check it".
|
|
* It has now been checked, against Arclight's own API rather than against an opinion:
|
|
*
|
|
* media-languages?iso3=jos returns TWO ids
|
|
* 7138 name "Jordanian Sign Language" JESUS film -> arc.gt/g8wmw
|
|
* 139080 name "Lebanese Sign Language" (the id the roster file lists under jos)
|
|
* arc.gt/g8wmw redirects to .../watch/jesus.html/jordanian-sign-language.html — the publisher's
|
|
* own slug names the language, and a bogus arc.gt code 404s, so it discriminates.
|
|
*
|
|
* ⚠ AND THE OLD NOTE WAS WRONG ON A FACT. It called Lebanese Sign Language "a different language
|
|
* (lbs)". ISO 639-3 lbs is LIBYAN Sign Language; Lebanese Sign Language has no code of its own.
|
|
* So this is not a mis-filing to be corrected upstream — it is two Levantine varieties sharing one
|
|
* code, which ISO does not separate. Nothing here can be "given back" to a Lebanese code, because
|
|
* there is none, and joining 139080 to our lbs would be a NAME match on the wrong country.
|
|
*
|
|
* THE RULE, therefore: where several Arclight ids share an ISO code, take the one whose own NAME
|
|
* agrees with the language this map names for that code, and record the other. The code stays
|
|
* authoritative for the JOIN; the name only breaks a tie the code cannot. Our three jos groups are
|
|
* Jordanian, so they get the Jordanian film and not the Lebanese variety.
|
|
*/
|
|
const OVERRIDE = {
|
|
jos: { id: 7138, why: 'two Arclight ids carry iso3 jos (7138 Jordanian, 139080 Lebanese); 7138 is the one whose name agrees with this map\'s language for the code, and its shareUrl slug says jordanian-sign-language' },
|
|
}
|
|
const DELAY = Number(process.env.DELAY_MS || 250)
|
|
|
|
const arc = JSON.parse(readFileSync(ARC, 'utf8')).codes
|
|
const prior = existsSync(OUT) ? JSON.parse(readFileSync(OUT, 'utf8')) : { codes: {} }
|
|
const out = { ...prior.codes }
|
|
const entries = Object.entries(arc)
|
|
let fetched = 0, cached = 0, missing = 0
|
|
|
|
for (let i = 0; i < entries.length; i++) {
|
|
const [code, v] = entries[i]
|
|
if (out[code] !== undefined) { cached++; continue }
|
|
/* An override replaces the ROSTER's id, not the result: the shareUrl is still fetched from the
|
|
API for that id, so a wrong override cannot invent a link. */
|
|
const id = OVERRIDE[code] ? OVERRIDE[code].id : v.id
|
|
try {
|
|
const r = await fetch(`https://api.arclight.org/v2/media-components/${JF}/languages/${id}`, { headers: { 'User-Agent': UA } })
|
|
if (r.ok) {
|
|
const d = await r.json()
|
|
// https, always: the API returns http:// and a plain-http link in a field context
|
|
// is a downgrade nobody asked for.
|
|
const u = String(d.shareUrl || '').replace(/^http:\/\//, 'https://')
|
|
if (u) { out[code] = u; fetched++ } else missing++
|
|
} else if (r.status !== 404) {
|
|
/* ONLY A 404 MEANS "THE JESUS COMPONENT HAS NO ENTRY FOR THIS LANGUAGE". A 429, a 500
|
|
* or a blip means we do not know yet, and the fallback below must not run on it.
|
|
* Measured: the first version treated ANY non-ok as "no JESUS Film" and wrote Magdalena
|
|
* over Jordanian Sign Language, which HAS the JESUS Film at arc.gt/g8wmw — a transient
|
|
* failure silently downgrading a language to a different film. Left undefined so the
|
|
* next run retries it. */
|
|
missing++
|
|
} else {
|
|
/* ═══ THE JESUS FILM COMPONENT DOES NOT CARRY EVERY LANGUAGE THE ROSTER COUNTS ═══
|
|
*
|
|
* counts.featureFilm >= 1 says a FEATURE exists in this language. It does not say the
|
|
* feature is JESUS. For five codes this endpoint 404s, and the build was falling back
|
|
* to the retired ?languageId= URL — unverifiable by fetch, and labelled "JESUS Film"
|
|
* on a language the JESUS component has no entry for. Measured 2026-09-25:
|
|
* ars Arabic, Najdi -> The Savior (8 groups, film was their ONLY resource)
|
|
* gjk Kachi Koli -> Magdalena — Director's Cut
|
|
* mzn Mazanderani -> Magdalena — Director's Cut
|
|
* lia Limba, West-Central -> NO component at all
|
|
* jos Jordanian Sign Language -> the JESUS Film after all, at arc.gt/g8wmw
|
|
*
|
|
* So ask what feature this language actually has. A real JESUS Film Project feature in
|
|
* someone's own language is worth shipping — dropping ars would have pushed 8 groups
|
|
* into the gap while a gospel film exists in their language — but it must be shipped
|
|
* UNDER ITS OWN NAME. This bundle already refuses to call Open Bible Stories Scripture;
|
|
* calling The Savior the JESUS Film is the same lie.
|
|
*
|
|
* Same publisher, so the doctrinal screen is unchanged: these are JESUS Film Project
|
|
* features, the catalogue the brief names explicitly. Nothing outside it is taken.
|
|
*
|
|
* `null` is recorded when no feature exists, and the pin build DROPS the film rather
|
|
* than falling back to a URL nobody can check. */
|
|
let alt = null
|
|
try {
|
|
const c = await fetch(`https://api.arclight.org/v2/media-components?languageIds=${id}&limit=25`, { headers: { 'User-Agent': UA } })
|
|
if (c.ok) {
|
|
const cj = await c.json()
|
|
const feature = (cj._embedded?.mediaComponents || []).find((m) => m.subType === 'featureFilm')
|
|
if (feature) {
|
|
const fr = await fetch(`https://api.arclight.org/v2/media-components/${feature.mediaComponentId}/languages/${id}`, { headers: { 'User-Agent': UA } })
|
|
if (fr.ok) {
|
|
const fd = await fr.json()
|
|
const fu = String(fd.shareUrl || '').replace(/^http:\/\//, 'https://')
|
|
if (fu) alt = { u: fu, t: feature.title || feature.mediaComponentId }
|
|
}
|
|
}
|
|
}
|
|
} catch { /* fall through to null */ }
|
|
out[code] = alt
|
|
if (alt) fetched++; else missing++
|
|
}
|
|
} catch { missing++ }
|
|
// The CHECKPOINT write is deliberately unguarded: this script resumes from the existing
|
|
// file as a cache, so `out` starts full and only grows. It cannot shrink, which is the
|
|
// thing the guard exists to catch. The FINAL write below is guarded like every other
|
|
// snapshot.
|
|
// CHECKPOINT-WRITE-OK
|
|
if (fetched % 100 === 0 && fetched) {
|
|
writeFileSync(OUT, JSON.stringify({ _note: 'Per-language JESUS Film deep links (shareUrl) from Arclight media-components. Partial write; rerun to resume.', generated: new Date().toISOString().slice(0, 10), codes: out }))
|
|
console.log(` ... ${i + 1}/${entries.length} fetched=${fetched} cached=${cached} missing=${missing}`)
|
|
}
|
|
await new Promise((r) => setTimeout(r, DELAY))
|
|
}
|
|
writeSnapshot(OUT, JSON.stringify({
|
|
_note: 'Per-language JESUS Film Project deep links (shareUrl) from Arclight media-components. A STRING is the JESUS Film (component 1_jf-0-0). An OBJECT {u,t} is a different JESUS Film Project feature in that language — the JESUS component has no entry for it — and t is the real title, which the surfaces must show instead of "JESUS Film". null means no feature exists and the film is dropped rather than linked to the retired ?languageId= URL, which nothing can verify.',
|
|
generated: new Date().toISOString().slice(0, 10),
|
|
/* COUNTED, NOT NAMED. withShareUrl used to be Object.keys(out).length, which counts the nulls
|
|
and the alternative-feature objects too — so a file holding one null reported every language
|
|
as having a shareUrl, and the previous run reported 2,087 of them while jos was absent
|
|
entirely. Each state is now counted as what it is. */
|
|
stats: {
|
|
languages: entries.length,
|
|
recorded: Object.keys(out).length,
|
|
jesusFilm: Object.values(out).filter((v) => typeof v === 'string').length,
|
|
otherFeature: Object.values(out).filter((v) => v && typeof v === 'object').length,
|
|
noFeature: Object.values(out).filter((v) => v === null).length,
|
|
notAnswered: missing,
|
|
},
|
|
codes: out,
|
|
}), (j) => Object.keys(j.codes || {}).length, 'Arclight shareUrl codes')
|
|
console.log(`DONE languages ${entries.length} JESUS film ${Object.values(out).filter((v) => typeof v === 'string').length} other feature ${Object.values(out).filter((v) => v && typeof v === 'object').length} no feature ${Object.values(out).filter((v) => v === null).length} not answered ${missing}`)
|