gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit84e1b3e184e1b3e1tracker: reports 025 + 026mre84e1b3e1/people.hl

19.9 KB

  1. // people.hl — THE PERSON PAGE's data (tracker.worldapi.org#16, components/person.hl `/person/<slug>`): a person by slug,
  2. // their facts and filmography, and the ONE-TIME fetch of their whole filmography from TMDB on the first visit. Creator:
  3. // "the movies come from artist scrolls. when one clicks on an artist that has no dataset yet the old version fetched that
  4. // artist and its entire bibilogrqphy so its profile was complete".
  5. //
  6. // personBySlug(slug) the person (slug = `urlSegment`) — a slug → id map built once (15k people, one scan)
  7. // personInfoOf(p) name, photo URL, born/died lines, bio
  8. // filmographyOf(p) their credits (`p.shows`): poster, title, year, role, kind — newest first, adult titles hidden
  9. // personFillStep(id) ONE step of the fetch, called again and again by the page until it answers `done`:
  10. // 1st call: GET <TMDB>/person/<tmdbId>?append_to_response=combined_credits (ONE request: details +
  11. // every cast/crew credit) + the photo (w185, like the posters); the person's facts are updated;
  12. // every next call stores the next `fillBatch` titles; the last one links the credits and marks
  13. // the person complete (`filmographyAt`, ms). hl:web serves one request at a time — between two
  14. // steps every other request is served, so a person with 300 credits never holds the server.
  15. //
  16. // COMPLETE = `filmographyAt` set: never fetched again (the daily sync may refresh followed people later — not built).
  17. // Migrated people are NOT complete (153 of them carry the old tracker's credits — kept, the fetch adds the rest).
  18. // A title we don't have → a MINIMAL show record (title, year, type, tmdbId, `adult`, overview, genres by TMDB id, language;
  19. // no seasons, no poster file; `minimal = true` — details.hl completes it when it is opened or by the repair job, tracker#26), shaped like search.hl importTitle's
  20. // (`oldId tmdb-<tv|movie>-<id>`, a free slug), indexed for the search and put into the public lists (catalog.hl).
  21. // The person's credits (`shows`, the migrated field): [{ show, character, posterPath, adult }] — `character` is the role
  22. // text (the characters, then crew jobs like "Director", " / " between them); `posterPath` = TMDB's, so the page can show
  23. // TMDB's small poster (w92, like search's web rows) until we have the file. ADULT (TMDB's flag, mission 054): a row is
  24. // shown only for a PUBLIC title (shows.hl `isPublicTitle`: `adult == false`, unknown hidden like the public lists) whose
  25. // credit is not adult either (mission 056 merge).
  26. // ONE PROCESS PER STORAGE (hybriel#21); new rows get self-assigned 16-hex ids (hybriel#113), like tmdbsync.hl.
  27. import { now } from 'hl:time'
  28. import { fetch } from 'hl:fetch'
  29. import { randomBytes } from 'hl:crypto'
  30. import { writeFile, mkDir, exists } from 'hl:fs'
  31. import { personsTable, showsTable, genresTable, storageDir, showById, showYear, posterUrlOf, isPublicTitle, titlePath, typeWordOf, typeClassOf, liveShowOf } from './shows.hl'
  32. import { tmdbGet, tmdbImageBase, textOr } from './tmdbsync.hl'
  33. import { titleIdByTmdb, indexShow, slugOf, slugBaseOf, indexPerson } from './search.hl'
  34. import { refreshCatalogShow, sortDesc } from './catalog.hl'
  35. import { claimShortId } from './shortids.hl'
  36. static profilesDir = storageDir + '/profiles'
  37. // titles stored per step (real data: see STATUS.md ticket #16 for the per-step time)
  38. static fillBatch = 40
  39. static listOr = (&v) => { return v == null ? [] : v }
  40. // ---- the person -------------------------------------------------------------------------------------------------
  41. static slugIndex = { built = false ids = {} }
  42. static ensureSlugIndex = () => {
  43. if (!slugIndex.built) {
  44. let m = {}
  45. for (p of personsTable.find(null, null)) { if (p.urlSegment != null) { m[p.urlSegment] = p.id } }
  46. slugIndex.ids = m
  47. slugIndex.built = true
  48. }
  49. return true
  50. }
  51. static personBySlug = (&slug) => {
  52. if (slug == null || hlTypeName(slug) != 'String' || slug == '') { return null }
  53. ensureSlugIndex()
  54. let id = slugIndex.ids[slug]
  55. return id == null ? null : personsTable.fetch(id)
  56. }
  57. static isComplete = (&p) => { return p.filmographyAt != null }
  58. // the page asks TMDB for this person: not complete yet, and TMDB knows them
  59. static needsFill = (&p) => { return !isComplete(p) && p.tmdbId != null }
  60. // the photo: /profiles/<oldId>.<ext> (project.hl profileRoute) — only when the file is there (the migrated `image` = 'jpg'
  61. // of 129 people never came with a file)
  62. static profileName = (&p) => {
  63. if (p.oldId == null || p.image == null || p.image == '') { return null }
  64. return p.oldId + '.' + p.image
  65. }
  66. static photoUrlOf = (&p) => {
  67. let name = profileName(p)
  68. return name != null && exists(profilesDir + '/' + name) ? '/profiles/' + name : ''
  69. }
  70. static monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
  71. // '1986-03-04' → '4 March 1986'; a bare year or anything else as it is
  72. static dateText = (&d) => {
  73. let t = textOr(d)
  74. if (t == null) { return null }
  75. if (t.length < 10) { return t }
  76. let m = toNumber(t.slice(5, 7))
  77. let day = toNumber(t.slice(8, 10))
  78. if (m == null || day == null || m < 1 || m > 12) { return t }
  79. return day + ' ' + monthNames[m - 1] + ' ' + t.slice(0, 4)
  80. }
  81. // { name, photoUrl, born, died, bio } — every field a plain string ('' = none)
  82. static personInfoOf = (&p) => {
  83. let born = dateText(p.birthDate)
  84. let place = textOr(p.birthPlace)
  85. let bornText = ''
  86. if (born != null && place != null) { bornText = 'Born ' + born + ' in ' + place } else if (born != null) { bornText = 'Born ' + born } else if (place != null) { bornText = 'Born in ' + place }
  87. let died = dateText(p.deathDate)
  88. return { name = p.name != null ? p.name : '' photoUrl = photoUrlOf(p) born = bornText died = died != null ? 'Died ' + died : ''
  89. bio = textOr(p.biography) != null ? p.biography : '' }
  90. }
  91. // ---- the filmography --------------------------------------------------------------------------------------------
  92. // the credits, one row per title (a title twice — the migrated credit and TMDB's — is ONE row, roles merged), newest
  93. // first by the title's release ('YYYY-MM-DD', else its year), undated last; only public titles (no adult/unknown)
  94. static filmographyOf = (&p) => {
  95. let byShow = {}
  96. let order = []
  97. for (c of listOr(p.shows)) {
  98. if (c != null && c.show != null) {
  99. let role = textOr(c.character)
  100. let prev = byShow[c.show]
  101. if (prev == null) {
  102. byShow[c.show] = { role = role != null ? role : '' posterPath = textOr(c.posterPath) adult = c.adult == true }
  103. order.push(c.show)
  104. } else {
  105. let x = prev
  106. if (role != null && !(' / ' + x.role + ' / ').includes(' / ' + role + ' / ')) { x.role = x.role == '' ? role : x.role + ' / ' + role }
  107. if (x.posterPath == null) { x.posterPath = textOr(c.posterPath) }
  108. if (c.adult == true) { x.adult = true }
  109. byShow[c.show] = x
  110. }
  111. }
  112. }
  113. let list = []
  114. let listed = {}
  115. for (sid of order) {
  116. let c = byShow[sid]
  117. // tracker#31: a credit on a merged tombstone is the keeper's (merge.hl repoints them; a keeper listed once)
  118. let s = liveShowOf(showById(sid))
  119. if (isPublicTitle(s) && !c.adult && listed[s.id] != true) {
  120. listed[s.id] = true
  121. let y = showYear(s)
  122. let release = textOr(s.release)
  123. let key = release != null && release.length >= 4 ? release : (y != null ? y : '')
  124. let poster = posterUrlOf(s)
  125. if (poster == '/posters/none' && c.posterPath != null) { poster = tmdbImageBase + '/w92' + c.posterPath }
  126. list.push({ key = key id = s.id title = s.title year = y != null ? y : '' href = titlePath(s) posterUrl = poster
  127. role = c.role typeLabel = typeWordOf(s) typeClass = typeClassOf(s) })
  128. }
  129. }
  130. let out = []
  131. let undated = []
  132. for (r of sortDesc(list)) { if (r.key == '') { undated.push(r) } else { out.push(r) } }
  133. for (r of undated) { out.push(r) }
  134. return out
  135. }
  136. // ---- the fetch ------------------------------------------------------------------------------------------------
  137. // person id → { items, pos, credits, added, requests, started, steps } while a fetch is under way (in memory: a restart
  138. // loses it — the titles stored so far are found again by TMDB id, nothing is duplicated)
  139. static jobs = {}
  140. // TMDB genre id → ours (our genres carry TMDB's id)
  141. static genreMap = { built = false ids = {} }
  142. static genreIdsOfTmdb = (&tmdbIds) => {
  143. if (!genreMap.built) {
  144. let m = {}
  145. for (g of genresTable.find(null, null)) { if (g.tmdbId != null) { m['' + g.tmdbId] = g.id } }
  146. genreMap.ids = m
  147. genreMap.built = true
  148. }
  149. let out = []
  150. for (t of listOr(tmdbIds)) {
  151. let id = genreMap.ids['' + t]
  152. if (id != null && !out.includes(id)) { out.push(id) }
  153. }
  154. return out
  155. }
  156. // TMDB's combined_credits → one item per title (cast AND crew of the same title merged), TV + movies only
  157. static creditItems = (&cc) => {
  158. let byKey = {}
  159. let order = []
  160. let all = []
  161. if (cc != null) {
  162. for (c of listOr(cc.cast)) { all.push({ credit = c role = textOr(c.character) }) }
  163. for (c of listOr(cc.crew)) { all.push({ credit = c role = textOr(c.job) }) }
  164. }
  165. for (a of all) {
  166. let c = a.credit
  167. let kind = c.media_type
  168. if ((kind == 'tv' || kind == 'movie') && c.id != null && hlTypeName(c.id) == 'Number') {
  169. let k = kind + ':' + c.id
  170. let known = byKey[k]
  171. if (known == null) {
  172. let name = kind == 'tv' ? textOr(c.name) : textOr(c.title)
  173. if (name != null) {
  174. byKey[k] = { kind = kind tmdbId = c.id title = name release = textOr(kind == 'tv' ? c.first_air_date : c.release_date)
  175. overview = textOr(c.overview) language = textOr(c.original_language) genreIds = listOr(c.genre_ids)
  176. posterPath = textOr(c.poster_path) adult = c.adult == true role = a.role != null ? a.role : '' }
  177. order.push(k)
  178. }
  179. } else if (a.role != null && !(' / ' + known.role + ' / ').includes(' / ' + a.role + ' / ')) {
  180. let x = known
  181. x.role = x.role == '' ? a.role : x.role + ' / ' + a.role
  182. byKey[k] = x
  183. }
  184. }
  185. }
  186. let out = []
  187. for (k of order) { out.push(byKey[k]) }
  188. return out
  189. }
  190. // the photo: w185 → storage/mpackdb/profiles/<oldId>.<ext> — { requests, image, tmdbProfile, failed }
  191. static syncProfile = (&p, profilePath) => {
  192. let out = { requests = 0 image = p.image tmdbProfile = p.tmdbProfile failed = null }
  193. if (textOr(profilePath) == null || p.oldId == null) { return out }
  194. let parts = profilePath.split('.')
  195. let ext = parts[parts.length - 1].toLowerCase()
  196. if (ext != 'jpg' && ext != 'jpeg' && ext != 'png' && ext != 'webp') { return out }
  197. let file = profilesDir + '/' + p.oldId + '.' + ext
  198. if (exists(file) && p.tmdbProfile == profilePath && p.image == ext) { return out }
  199. out.requests = 1
  200. let r = fetch(tmdbImageBase + '/w185' + profilePath, { headers = { 'user-agent' = 'tracker.worldapi.org (person page)' } timeoutMs = 20000 })
  201. if (r == null || r.status != 200 || r.body == null || r.body.length == 0) {
  202. out.failed = 'photo ' + profilePath + ': ' + (r == null ? 'no answer' : 'HTTP ' + r.status)
  203. return out
  204. }
  205. mkDir(profilesDir)
  206. writeFile(file, toBytes(r.body), 420)
  207. out.image = ext
  208. out.tmdbProfile = profilePath
  209. return out
  210. }
  211. // step 1: TMDB's person + credits; the facts (a TMDB value that is empty never overwrites ours) and the photo
  212. static startFill = (p) => {
  213. let t0 = now()
  214. let d = tmdbGet('/person/' + p.tmdbId + '?append_to_response=combined_credits')
  215. if (d.failed != null) { return { error = 'TMDB: ' + d.failed } }
  216. let items = creditItems(d.combined_credits)
  217. let photo = syncProfile(p, d.profile_path)
  218. let x = p
  219. if (textOr(d.biography) != null) { x.biography = d.biography }
  220. if (textOr(d.birthday) != null) { x.birthDate = d.birthday }
  221. if (textOr(d.deathday) != null) { x.deathDate = d.deathday }
  222. if (textOr(d.place_of_birth) != null) { x.birthPlace = d.place_of_birth }
  223. if (textOr(x.imdbId) == null && textOr(d.imdb_id) != null) { x.imdbId = d.imdb_id }
  224. if (d.adult != null) { x.adult = d.adult == true }
  225. x.image = photo.image
  226. x.tmdbProfile = photo.tmdbProfile
  227. if (personsTable.update(x.id, x) == null) { return { error = 'could not store the person: ' + personsTable.lastError() } }
  228. if (photo.failed != null) { console.log('person fill: ' + p.name + ': ' + photo.failed) }
  229. jobs[p.id] = { items = items pos = 0 credits = [] added = 0 requests = 1 + photo.requests started = t0 steps = 1 tmdbMs = now() - t0 }
  230. return { done = false pos = 0 total = items.length }
  231. }
  232. // a title we don't have: the minimal record (shape: search.hl importTitle) → its id, or null
  233. static addMinimalTitle = (&cr, &pid, pname) => {
  234. let kindType = cr.kind == 'tv' ? 'series' : 'movie'
  235. let rel = cr.release
  236. let yr = rel != null && rel.length >= 4 ? toNumber(rel.slice(0, 4)) : null
  237. let slug = slugOf(cr.title, cr.tmdbId)
  238. let record = {
  239. id = randomBytes(8, 'hex') title = cr.title summary = null tmdbSummary = cr.overview tmdbId = cr.tmdbId
  240. language = cr.language type = kindType release = rel year = yr image = null
  241. urlSegment = slug episodesCount = null homepage = null imdbId = null seasonsCount = null
  242. status = null tagline = null tvdbId = null tvmzId = null genres = genreIdsOfTmdb(cr.genreIds)
  243. cast = [{ person = pid character = cr.role actor = pname }] seasons = [] oldId = 'tmdb-' + cr.kind + '-' + cr.tmdbId
  244. adult = cr.adult imported = now() minimal = true
  245. }
  246. // mission 060: its short id (shortids.hl — unique across shows and persons)
  247. record.shortId = claimShortId('show', record.id, record.oldId)
  248. if (showsTable.put(record) == null) {
  249. console.log('person fill: "' + cr.title + '" not stored: ' + showsTable.lastError())
  250. return null
  251. }
  252. indexShow(record)
  253. refreshCatalogShow(record.id)
  254. return record.id
  255. }
  256. // every next step: the next `fillBatch` titles; the last one links the credits and marks the person complete
  257. static fillStep = (p, jobIn) => {
  258. let job = jobIn
  259. let total = job.items.length
  260. let end = job.pos + fillBatch
  261. if (end > total) { end = total }
  262. let credits = job.credits
  263. let added = job.added
  264. let pid = p.id
  265. let pname = p.name
  266. let i = job.pos
  267. while (i < end) {
  268. let item = job.items[i]
  269. let sid = titleIdByTmdb(item.kind == 'tv' ? 'series' : 'movie', item.tmdbId)
  270. if (sid == null) {
  271. sid = addMinimalTitle(item, pid, pname)
  272. if (sid != null) { added = added + 1 }
  273. }
  274. if (sid != null) { credits.push({ show = sid character = item.role posterPath = item.posterPath adult = item.adult }) }
  275. i = i + 1
  276. }
  277. job.pos = end
  278. job.credits = credits
  279. job.added = added
  280. job.steps = job.steps + 1
  281. jobs[pid] = job
  282. if (end < total) { return { done = false pos = end total = total } }
  283. // done: the credits (+ the migrated ones TMDB no longer lists), complete
  284. let have = {}
  285. let all = []
  286. for (c of credits) { have[c.show] = true all.push(c) }
  287. for (c of listOr(p.shows)) { if (c != null && c.show != null && have[c.show] != true) { all.push(c) } }
  288. let x = p
  289. x.shows = all
  290. x.filmographyAt = now()
  291. if (personsTable.update(x.id, x) == null) { return { error = 'could not store the filmography: ' + personsTable.lastError() } }
  292. personsTable.persist()
  293. showsTable.persist()
  294. jobs[pid] = null
  295. console.log('person fill: ' + pname + ' (tmdb ' + p.tmdbId + ') credits=' + total + ' added=' + added + ' requests=' + job.requests + ' steps=' + job.steps + ' tmdbMs=' + job.tmdbMs + ' ms=' + (now() - job.started))
  296. return { done = true pos = total total = total added = added }
  297. }
  298. // one step for the person with this id: { done, pos, total } or { error }
  299. static personFillStep = (&personId) => {
  300. let p = personsTable.fetch(personId)
  301. if (p == null) { return { error = 'no such person' } }
  302. if (isComplete(p)) { return { done = true } }
  303. if (p.tmdbId == null) { return { error = 'TMDB does not know this person' } }
  304. let job = jobs[personId]
  305. if (job == null) { return startFill(p) }
  306. return fillStep(p, job)
  307. }
  308. // ---- people from a title's cast (tracker#26, details.hl completeStep) ------------------------------------------
  309. // TMDB person id → our person id, built once (15k people, one scan), kept up to date by castPersonId
  310. // (flat statics, written key by key like search.hl's haveTmdb: `let m = …ids` would copy the whole map — plain data
  311. // deep-copies on assignment)
  312. static personTmdbBuilt = { yes = false }
  313. static personTmdbIds = {}
  314. static personIdByTmdb = (&tmdbId) => {
  315. if (!personTmdbBuilt.yes) {
  316. for (p of personsTable.find(null, null)) { if (p.tmdbId != null) { personTmdbIds['' + p.tmdbId] = p.id } }
  317. personTmdbBuilt.yes = true
  318. }
  319. return personTmdbIds['' + tmdbId]
  320. }
  321. // a free person slug: "Maya-Rudolph", taken → "-2", … (the migrated people's form)
  322. static personSlugOf = (&name, &tmdbId) => {
  323. ensureSlugIndex()
  324. let base = slugBaseOf(name, 'person-' + tmdbId)
  325. if (slugIndex.ids[base] == null) { return base }
  326. let n = 2
  327. while (slugIndex.ids[base + '-' + n] != null) { n = n + 1 }
  328. return base + '-' + n
  329. }
  330. // how many people castPersonId added since the start (details.hl counts a title's new people by the difference, tracker#28)
  331. static castPeopleAdded = { n = 0 }
  332. static castPeopleCount = () => { return castPeopleAdded.n }
  333. // our person for one cast entry of TMDB ({ tmdbId, name, character, gender }) — the one we have by TMDB id, else a NEW
  334. // person (name, tmdbId, a free slug, this one credit; not complete: their page fetches the rest on the first visit like any
  335. // migrated person). Answers the id, or null when it could not be stored. Also a CREW person (tracker#28: `character` = the
  336. // job, "Director").
  337. static castPersonId = (&c, &showId) => {
  338. let have = personIdByTmdb(c.tmdbId)
  339. if (have != null) { return have }
  340. let record = { id = randomBytes(8, 'hex') name = c.name tmdbId = c.tmdbId urlSegment = personSlugOf(c.name, c.tmdbId) oldId = 'tmdb-person-' + c.tmdbId
  341. gender = c.gender image = null tmdbProfile = null adult = false birthDate = null birthPlace = null deathDate = null imdbId = null
  342. shows = showId != null ? [{ show = showId character = c.character }] : [] added = now() }
  343. // mission 062 (tracker#27 for #26/#28): every person the app creates gets its short id at once (shortids.hl)
  344. record.shortId = claimShortId('person', record.id, record.oldId)
  345. if (personsTable.put(record) == null) {
  346. console.log('details: person "' + c.name + '" not stored: ' + personsTable.lastError())
  347. return null
  348. }
  349. personTmdbIds['' + c.tmdbId] = record.id
  350. slugIndex.ids[record.urlSegment] = record.id
  351. indexPerson(record)
  352. castPeopleAdded.n = castPeopleAdded.n + 1
  353. return record.id
  354. }
  355. // ---- GUEST STARS (mission 062): a series' guests are NOT person records — details.hl stores them on the title ({ tmdbId, actor,
  356. // character, profile, episodes, guest }, `person` only when we have them already). 160k people (most of them one-episode guests)
  357. // cost 3.3 GB of RSS at boot (w061). A guest's link is `/person/tmdb/<tmdbId>?show=<show id>` (project.hl guestRoute): the FIRST
  358. // open creates the person (name + role from that title's guest entry; without one — an old link — TMDB's /person/<id>), with its
  359. // short id, then the person page fills it like any other.
  360. static guestOf = (&showId, &tmdbId) => {
  361. let s = showId != null && hlTypeName(showId) == 'String' && showId != '' ? showsTable.fetch(showId) : null
  362. if (s == null) { return null }
  363. for (c of listOr(s.cast)) { if (c != null && c.tmdbId == tmdbId && textOr(c.actor) != null) { return c } }
  364. return null
  365. }
  366. // our person for this TMDB person (created now when we have none) → { id, created } or { error }
  367. static guestPersonId = (tmdbId, showId) => {
  368. let have = personIdByTmdb(tmdbId)
  369. if (have != null) { return { id = have created = false } }
  370. let g = guestOf(showId, tmdbId)
  371. let c = null
  372. let inShow = null
  373. if (g != null) {
  374. c = { tmdbId = tmdbId name = g.actor character = g.character gender = null }
  375. inShow = showId
  376. } else {
  377. let d = tmdbGet('/person/' + tmdbId)
  378. if (d.failed != null) { return { error = 'TMDB: ' + d.failed } }
  379. if (textOr(d.name) == null || d.adult == true) { return { error = 'no such person' } }
  380. c = { tmdbId = tmdbId name = d.name character = null gender = d.gender }
  381. }
  382. let pid = castPersonId(c, inShow)
  383. if (pid == null) { return { error = 'could not store the person' } }
  384. personsTable.persist()
  385. console.log('guest opened: ' + c.name + ' (tmdb ' + tmdbId + ') → new person ' + pid + (inShow != null ? ' from show ' + inShow : ' from TMDB'))
  386. return { id = pid created = true }
  387. }

Branches

Latest commits

  • 84e1b3e1tracker: reports 025 + 026mre
  • dc40d859tracker#31 (mission 026): duplicate titles merged — the 68 type+tmdbId pairs held by 157 records were the old tracker's (all migrated); merge.hl repair job (own clock, before the TMDB jobs) keeps one keeper per title (follows/watches > old short id > oldest), moves follows, watches, seasons, cast, credits, timelines, tombstones the rest (mergedInto, never deleted), slugs + short ids 301 to the keeper; stray seasons merged into their listed twin (Reacher S3 watches) or linked when watched; search import re-checks before its put; deploy.sh waits up to 90 s for 200; real copy 68 -> 0 dup ids, az5b2 follows/watches equal; gates 342/0, 32/0, 52/0mre
  • 2667da05tracker#30 (mission 025): /my/ pages from slim cached title cards, episode rows and watch sets (after the jobs /my/series 1.8 s -> 0.06 s, /my/unwatched 4.1 -> 0.18 s); timeline page shows its name once; franchise widget under the poster/title; movies with TV leftovers (First Contact) go through the details repair; tools/count-tmdb-ids.hl; gates 327/0, 52/0, 32/0mre
  • 3909810dantcolony#40: mission references in README/STATUS/docs point to the moved missionsmre
  • e63b1d28antcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
  • 1cda451dtracker: Hybriel master 190aa11d (#127 both shapes, GC correctness fc838894) — conductor adopts despite /my/series 2.4x after jobs (memory 8.0 → 1.7 GB boot); see reports/071mre
  • c1fa2f2etracker (mission 071): Hybriel master 190aa11d measured on the real copy vs the live binary 8590df63 — NOT adopted (after the first-start jobs /my/series 2.4x slower, /series 1.6x, RSS swings 7.4-12.2 GB; fresh it is flat at 1.7-2.2 GB and /my/unwatched faster), vendor stays 8efba065, candidate kept in .scratch/w071/vendor-190aa11d; tests/kinds.mjs: collection seed off (its TMDB request broke check 1 in 1 of 4 runs); tests/realdata-071.sh + realdata-071-bench.mjs + tools/realdata-071-table.py; README + STATUS (numbers, how to repeat); gates 325/0, 32/0, 50/0mre
  • 8081350atracker docs (mission 070): README (summary, Config HL_GC_BYTES — kept at Hybriel's default, the 256 MiB setting is taken out of docker-compose.yml again: the jobs grew to 12+ GB with it too, see STATUS), Test (three gates), Deploy (first start ~50 min: kinds then seed, restart once after collections done, memory numbers), Vendored Hybriel 8efba065 + #48 audit, Files; STATUS mission 070 entry (merges, migrated counts, lambda audit, gates, RSS old vs new, how to repeat, open points); docs/kinds.md + docs/franchises.md job order; tests/realdata-070-*.sh, tools/count-migrated.hl, tools/ref-params.py, tools/lambda-audit.pymre
  • 1ad19c8ctracker: re-vendor Hybriel master 8efba065 (#126 GC by bytes, #48 lambda parameters copy) (mission 070): bin/hybriel sha256 50361e95…, plugins core crypto data fetch fs http http1 mpackdb proc smtp time web; lambda audit: 13 lambdas change a passed record/list (11 through a local alias), no caller relies on it — unchanged; 319 read-only lambda parameters get & (no copy per call: /my/schedule 2.35 → 0.46 s, /my/unwatched 13.7 → 5.7 s on the real copy); /my/unwatched one merge sort instead of n² inserts; docker-compose HL_GC_BYTES=268435456; deploy.sh runs kinds.mjs + franchises.mjs too (default ports 8700–8710); tests/realdata-070.mjs; gates browser 325/0, kinds 32/0, franchises 50/0mre
  • 46b21389tracker (mission 070, conductor): the sync never destroys migrated data — the one-time summary step MOVES a copied summary to migratedSummary (marker summaries-moved.txt) instead of clearing it; a migrated record's first title/genres/homepage/tagline TMDB replaces → migratedTitle/migratedGenres/migratedHomepage/migratedTagline, a migrated season's/episode's title/summary → migratedTitle/migratedSummary (set once); tests/peek-shows.hl prints them; gate 325/0mre
  • 749019b0Merge t19 (tracker#19 franchises + timelines) into main (mission 070): conflicts README/STATUS/show.hl/project.hl/browser.mjs/faketmdb.mjs, both sides kept; franchise/timeline pages get #20's typed heading (Franchise | …, Timeline | …), their title links via titlePath (/movies|/series|/shows); the collection seed waits for repair, kinds and credits too; franchises.mjs URLs + 2 new checks; gates browser 323/0, kinds 32/0, franchises 50/0mre
  • daf49feaMerge t20 (tracker#20 typed headings + #21 series/shows split) into main (mission 070): conflicts README/STATUS/show.hl/project.hl/search.hl/components/search.hl/browser.mjs/faketmdb.mjs, both sides kept; clock order backfill → repair → kinds → credits; withDetailsFields stores tmdbType + kind; gates: browser.mjs URLs → /series|/movies, typed h1 selectors; kinds.mjs repair/credits off, fixture name = seed name; browser 323/0, kinds 32/0mre
  • 20e09d89Merge t18 (tracker#18 delta sync) into main (mission 070): conflicts README/STATUS/show.hl/project.hl/browser.mjs, both sides kept; pageShowOf summary = summaryOfmre
  • f83571c3tracker#18 (mission 067): daily sync by change lists — TMDB /tv|movie/changes (since the stored day, paged) + TVmaze /updates/shows → only our changed titles (followed: full step, unfollowed: light step — changed seasons, no TVmaze), full walk on first run / gap > 14 days / failed list; show record refreshed (title, tmdbSummary, tagline, status, genres …; renamed titles re-indexed); summary = the creator's own text (page: summary > tmdbSummary > tvmazeSummary), one-time clear of copied summaries (9,647 on the live copy); gate 261, tests/realdata-018*.mjs, README + STATUSmre
  • 1ed5457etracker#20 + #21 (mission 068): typed headings "<Type> | <name>" in type colours; TV titles split into Series (/series) and Shows (/shows) by TMDB type + Reality/Talk/News genres — kind stored by sync/import/adult backfill + new kind backfill (resumes), /movies/<slug>, /shows/<slug> of a series/movie → 301, /my/series + /my/shows, home 5 tiles + 3 rows, search/filmography labels; gates kinds 32 + browser 266, tests/realdata-068.mjs, tools/count-kinds.hl, docs/kinds.md, README + STATUSmre
  • f2b00674Merge t26 (tracker#26 + #28) into main (mission 062): short ids for every new person (castPersonId, guest route), guest stars stored on the title and created as people only when opened (/person/tmdb/<id>?show=<id> → 302), lean watch/follow clicks (showRow a small object, cast/crew from the slug, watches cached per user, face rows only after a season toggle); gate 311, tests/realdata-062.mjs, README + STATUSmre
  • 0553b51ftracker#19 (mission 066): franchises and timelines — tables, /franchises, /franchises/<slug>, /timelines/<slug> (Timeline | Release sort, series by last episode), the Prequel | Timeline | Sequel widget with the franchise above, the creator's editor, TMDB collection seed in the app (resumes, paced); gate tests/franchises.mjs 48/0 + browser.mjs 266/0, tests/realdata-066.mjs, docs/franchises.md, README + STATUSmre
  • fa1f9dfatracker#29 (mission 063): unwatched check muted grey outline + check (accent only on hover), watched stays solid — no code regression, the accent outline read as ticked; gate checks real checks visibly (computed style + screenshot pixel) on /my/unwatched, show, movie, /my/movies; gate 266, tests/realdata-063.mjs, README + STATUSmre
  • 10bb3f93tracker#28 (mission 061): full cast (all seasons, main cast by episodes, guest stars) + crew (created by, directed by, written by, screenplay, story, music) — stored by the details completion, the daily sync, the search import (one details request) and a background credits job (resumes, RSS limit); show page collapsed after 20 with client-side Show all; showBySlug via a slug map; gate 249, tests/realdata-028.mjs, README + STATUSmre
  • d8b12d67tracker#27 (mission 060): short ids for movies, series and persons — old 702 kept (data/old-short-ids.json), new random [a-z0-9]{5} unique across both, claimed at creation, background backfill (resumes), shown under poster/photo, /<shortId> → 301; gate 259, tests/realdata-060*, README + STATUSmre