gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit10bb3f9310bb3f93tracker#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 + STATUSmre10bb3f93/details.hl

22.2 KB

  1. // details.hl — A TITLE'S FULL DETAILS (tracker.worldapi.org#26). Creator: "when i go to an artist and it loads the bibliography
  2. // those loaded movies and series miss most information and it stays that way when i click on them".
  3. //
  4. // The person page's fill (people.hl, #16) adds every title we don't have as a MINIMAL record — title, year, overview, genres,
  5. // type, tmdbId, adult, this one person as cast; no poster, no seasons, no external ids, no homepage/tagline/status
  6. // (`minimal = true`). A search import (search.hl importTitle) is full except for the cast. Here a minimal title gets what a
  7. // full import has, plus its cast:
  8. //
  9. // GET <TMDB>/movie/<tmdbId>?append_to_response=external_ids,credits a movie
  10. // GET <TMDB>/tv/<tmdbId>?append_to_response=external_ids,aggregate_credits a series (cast of ALL seasons, roles merged)
  11. // then tmdbsync.hl `syncShowPart(id, <that answer>, …)` — the daily sync's work, without asking for the details again, IN
  12. // STEPS (see `jobs` below): a series' seasons + episodes (`season/1,…`), then the poster (w342), missing imdb/tvdb ids,
  13. // TVmaze lookup + its episode merge (#15); a movie its imdb id + poster
  14. //
  15. // Stored: overview (`summary` only when ours is empty; `tmdbSummary`), homepage, tagline, status, genres (by TMDB id, when
  16. // TMDB gives any), release/year/language when we have none, `adult`, the CAST (TMDB's first `castLimit`, in TMDB's order;
  17. // a person we have by TMDB id is linked, a new one is added — people.hl castPersonId; cast entries we had and TMDB does not
  18. // list are kept after them). Then `detailsAt` (ms) = COMPLETE, never asked again (the daily sync keeps a FOLLOWED title fresh).
  19. // TMDB 404 → `detailsCheck = 'TMDB 404'` (never asked again; the page shows what it has). TMDB says ADULT → the flag is
  20. // stored, `detailsCheck = 'adult'`, no poster/seasons (mission 056: an adult title is never public, its poster not kept).
  21. //
  22. // WHO CALLS IT (`completeStep`, again and again until `done`): the show page when an incomplete title is opened
  23. // (components/show.hl face `showComplete`, skeleton meanwhile) and the repair job in project.hl (`repairTick`, one step per tick
  24. // on the app's clock, like the adult backfill) for every incomplete title that exists when the app starts. Adult titles are
  25. // left alone by both.
  26. //
  27. // CAST AND CREW (tracker.worldapi.org#28; creator: "you show quite few people, usually there are more especially at picard, and
  28. // directors and so"). From the SAME details answer (`credits` of a movie, `aggregate_credits` of a series = every season):
  29. // cast [{ person, character, actor, episodes, guest }] — a movie: TMDB's order (billing); a series: TMDB's aggregate order
  30. // (by episode count, most first; `character` = all roles " / "), `episodes` = the person's episodes, `guest` = fewer than
  31. // `mainMinOf` (half the series' episodes, at most 10, at least 1) → the page puts them after "Guest stars". At most
  32. // `castMax` (300: a talk show/soap lists 3,000+ one-episode guests — Grey's Anatomy 3,243, SNL 2,771 — each would be a
  33. // new person); adult people left out; entries we had and TMDB does not list are kept after them.
  34. // crew [{ person, name, job, episodes }] job = creator (a series' `created_by`) | director (Director) | writer (Writer) |
  35. // screenplay (Screenplay) | story (Story) | composer (Original Music Composer, Composer, Music) — one entry per person and
  36. // job, a series' by that job's episode count (most first), a movie's in TMDB's order; at most `crewMax` per job.
  37. // `creditsAt` (ms) = the credits are stored (by any path); `creditsCheck` = why the credits job gave up (TMDB 404, adult).
  38. // Every person is linked (people.hl castPersonId: the one we have by TMDB id, else a NEW minimal person — slug, search entry,
  39. // this one credit; their page fills on the first visit). WHO STORES THEM: the details step below (incomplete titles), the daily
  40. // sync and the search import (tmdbsync.hl asks for the credits in the details request it makes anyway → `applyCredits`), and
  41. // the CREDITS JOB (project.hl `creditsTick`, `creditsStep` here) for every other public title once — one request per title.
  42. import { now } from 'hl:time'
  43. import { env } from 'hl:proc'
  44. import { showsTable, personsTable } from './shows.hl'
  45. import { tmdbGet, textOr, adultOf, syncShowWith, syncShowPart, persistSync } from './tmdbsync.hl'
  46. import { searchTitleChanged } from './search.hl'
  47. import { refreshCatalogShow } from './catalog.hl'
  48. import { castPersonId, castPeopleCount, genreIdsOfTmdb } from './people.hl'
  49. // how many of TMDB's cast a title keeps (see the top; TRACKER_CAST_MAX) and how many people per crew job
  50. static castMax = env('TRACKER_CAST_MAX') != null ? toNumber(env('TRACKER_CAST_MAX')) : 300
  51. static crewMax = 30
  52. static listOr = (v) => { return v == null ? [] : v }
  53. // INCOMPLETE = a filmography title (`minimal`) or an import whose sync never finished (`imported`, no `tmdbSync`), not
  54. // completed (`detailsAt`) and not given up on (`detailsCheck`)
  55. static isIncomplete = (s) => {
  56. return s != null && s.tmdbId != null && s.detailsAt == null && s.detailsCheck == null && (s.minimal == true || (s.imported != null && s.tmdbSync == null))
  57. }
  58. // what the show page and the repair complete: an incomplete title that is not adult (mission 054/056: never public)
  59. static needsDetails = (s) => { return isIncomplete(s) && s.adult != true }
  60. // every title the repair job has to do, in table order (read once when it starts — so a restart goes on with what is left)
  61. static detailsRepairQueue = () => {
  62. let out = []
  63. for (s of showsTable.find(null, null)) { if (needsDetails(s)) { out.push(s.id) } }
  64. return out
  65. }
  66. // a series' roles ([{ character, episode_count }]) as one text: "Mother / Lamia". tracker#28 (real data): TMDB's characters are
  67. // themselves " / "-joined and repeat across seasons ("Data / Lore", "Data", "Yvette Picard (uncredited)") — split, " (uncredited)"
  68. // dropped, each name once, at most `rolesMax` (a voice actor of The Simpsons has 30+), then " / …"
  69. static rolesMax = 4
  70. static rolesText = (roles) => {
  71. let names = []
  72. let more = false
  73. for (r of listOr(roles)) {
  74. let c = r != null ? textOr(r.character) : null
  75. if (c != null) {
  76. for (part of c.split(' / ')) {
  77. let n = part.replaceAll(' (uncredited)', '').trim()
  78. if (n != '' && !names.includes(n)) { if (names.length < rolesMax) { names.push(n) } else { more = true } }
  79. }
  80. }
  81. }
  82. return names.join(' / ') + (more ? ' / …' : '')
  83. }
  84. // a series' episodes of one person: TMDB's total_episode_count, else the sum of the roles' / jobs' counts
  85. static episodesOf = (total, parts) => {
  86. if (total != null && hlTypeName(total) == 'Number') { return total }
  87. let n = 0
  88. for (r of listOr(parts)) { if (r != null && r.episode_count != null && hlTypeName(r.episode_count) == 'Number') { n = n + r.episode_count } }
  89. return n
  90. }
  91. static personOk = (c) => { return c != null && c.id != null && hlTypeName(c.id) == 'Number' && textOr(c.name) != null && c.adult != true }
  92. // TMDB's cast → [{ tmdbId, name, character, gender, episodes }], TMDB's order, at most castMax, adult people left out
  93. static castOf = (d, movie) => {
  94. let src = movie ? (d.credits != null ? d.credits.cast : null) : (d.aggregate_credits != null ? d.aggregate_credits.cast : null)
  95. let out = []
  96. for (c of listOr(src)) {
  97. if (out.length < castMax && personOk(c)) {
  98. let role = movie ? textOr(c.character) : rolesText(c.roles)
  99. out.push({ tmdbId = c.id name = c.name character = role != null ? role : '' gender = c.gender episodes = movie ? null : episodesOf(c.total_episode_count, c.roles) })
  100. }
  101. }
  102. return out
  103. }
  104. // a series' main cast is in at least this many episodes: half of the series' (rounded up), at most 10 (a season), at least 1
  105. static mainMinOf = (d) => {
  106. let n = d.number_of_episodes != null && hlTypeName(d.number_of_episodes) == 'Number' ? d.number_of_episodes : 0
  107. let half = (n - n % 2) / 2 + n % 2
  108. if (half > 10) { return 10 }
  109. return half < 1 ? 1 : half
  110. }
  111. // the crew jobs we show: TMDB job → ours (the page's labels: components/show.hl crewLabels)
  112. static crewJobOf = (job) => {
  113. if (job == 'Director') { return 'director' }
  114. if (job == 'Writer') { return 'writer' }
  115. if (job == 'Screenplay') { return 'screenplay' }
  116. if (job == 'Story') { return 'story' }
  117. if (job == 'Original Music Composer' || job == 'Composer' || job == 'Music') { return 'composer' }
  118. return null
  119. }
  120. static crewJobs = ['creator', 'director', 'writer', 'screenplay', 'story', 'composer']
  121. // keeps a job's list ordered by episodes (most first, equal ones in TMDB's order), at most crewMax (hand-written: no sort())
  122. static insertByEpisodes = (list, item) => {
  123. let out = []
  124. let inserted = false
  125. for (x of list) {
  126. if (!inserted && item.episodes > x.episodes) { out.push(item) inserted = true }
  127. if (out.length < crewMax) { out.push(x) }
  128. }
  129. if (!inserted && out.length < crewMax) { out.push(item) }
  130. return out
  131. }
  132. // TMDB's crew (+ a series' created_by) → [{ tmdbId, name, job, gender, episodes }], grouped by job in `crewJobs` order, one
  133. // entry per person and job
  134. static crewOf = (d, movie) => {
  135. let byJob = {}
  136. let seen = {}
  137. for (j of crewJobs) { byJob[j] = [] }
  138. if (!movie) {
  139. for (c of listOr(d.created_by)) {
  140. if (personOk(c) && seen['creator:' + c.id] != true && byJob['creator'].length < crewMax) {
  141. seen['creator:' + c.id] = true
  142. let l = byJob['creator']
  143. l.push({ tmdbId = c.id name = c.name job = 'creator' gender = c.gender episodes = 0 })
  144. byJob['creator'] = l
  145. }
  146. }
  147. }
  148. let src = movie ? (d.credits != null ? d.credits.crew : null) : (d.aggregate_credits != null ? d.aggregate_credits.crew : null)
  149. for (c of listOr(src)) {
  150. if (personOk(c)) {
  151. // a movie's crew entry has one `job`; a series' has `jobs` [{ job, episode_count }]
  152. let jobs = movie ? [{ job = c.job episode_count = 0 }] : listOr(c.jobs)
  153. for (jb of jobs) {
  154. let ours = jb != null ? crewJobOf(jb.job) : null
  155. if (ours != null && seen[ours + ':' + c.id] != true) {
  156. seen[ours + ':' + c.id] = true
  157. let n = movie ? 0 : episodesOf(jb.episode_count, null)
  158. byJob[ours] = insertByEpisodes(byJob[ours], { tmdbId = c.id name = c.name job = ours gender = c.gender episodes = n })
  159. }
  160. }
  161. }
  162. }
  163. let out = []
  164. for (j of crewJobs) { for (c of byJob[j]) { out.push(c) } }
  165. return out
  166. }
  167. // the title's cast: TMDB's (linked to our people, new ones added) first, then the entries we had that TMDB does not list.
  168. // `mainMin` = a series' main-cast episode count (mainMinOf), 0 for a movie (no guests)
  169. static mergedCast = (show, tmdbCast, mainMin) => {
  170. let out = []
  171. let have = {}
  172. let showKey = show.id
  173. for (c of tmdbCast) {
  174. let pid = castPersonId(c, showKey)
  175. if (pid != null && have[pid] != true) {
  176. have[pid] = true
  177. out.push({ person = pid character = c.character actor = c.name episodes = c.episodes guest = c.episodes != null && c.episodes < mainMin })
  178. }
  179. }
  180. for (c of listOr(show.cast)) {
  181. if (c != null && c.person != null && have[c.person] != true) {
  182. have[c.person] = true
  183. out.push(c)
  184. }
  185. }
  186. return out
  187. }
  188. static crewRoleOf = (job) => {
  189. if (job == 'creator') { return 'Creator' }
  190. if (job == 'director') { return 'Director' }
  191. if (job == 'writer') { return 'Writer' }
  192. if (job == 'screenplay') { return 'Screenplay' }
  193. if (job == 'story') { return 'Story' }
  194. return 'Music'
  195. }
  196. // the title's crew: TMDB's, linked to our people (a crew person new to us gets the job as their role) — replaces what we had
  197. static linkedCrew = (show, tmdbCrew) => {
  198. let out = []
  199. let showKey = show.id
  200. for (c of tmdbCrew) {
  201. let pid = castPersonId({ tmdbId = c.tmdbId name = c.name character = crewRoleOf(c.job) gender = c.gender }, showKey)
  202. if (pid != null) { out.push({ person = pid name = c.name job = c.job episodes = c.episodes }) }
  203. }
  204. return out
  205. }
  206. // the answer carries credits (a details request with `credits` / `aggregate_credits` appended)
  207. static hasCredits = (d, movie) => { return d != null && (movie ? d.credits != null : d.aggregate_credits != null) }
  208. // cast + crew of this record from a details answer → { show (the record with cast, crew, creditsAt), cast, crew, people (new) }
  209. static withCredits = (show, d, movie) => {
  210. let before = castPeopleCount()
  211. let tmdbCast = castOf(d, movie)
  212. let x = show
  213. x.cast = mergedCast(show, tmdbCast, movie ? 0 : mainMinOf(d))
  214. x.crew = linkedCrew(show, crewOf(d, movie))
  215. x.creditsAt = now()
  216. return { show = x cast = tmdbCast.length crew = x.crew.length people = castPeopleCount() - before }
  217. }
  218. // the daily sync and the search import: their details answer (tmdbsync.hl `res.details`) → the title's cast + crew. Nothing
  219. // for an adult title or an answer without credits. Answers { cast, crew, people, ms, error }
  220. static applyCredits = (showId, d) => {
  221. let out = { cast = 0 crew = 0 people = 0 ms = 0 error = '' }
  222. let t0 = now()
  223. let show = showsTable.fetch(showId)
  224. if (show == null || show.adult == true || adultOf(d) == true || !hasCredits(d, show.type == 'movie')) { return out }
  225. let w = withCredits(show, d, show.type == 'movie')
  226. if (showsTable.update(show.id, w.show) == null) { out.error = show.title + ': credits not stored: ' + showsTable.lastError() return out }
  227. out.cast = w.cast
  228. out.crew = w.crew
  229. out.people = w.people
  230. out.ms = now() - t0
  231. return out
  232. }
  233. // A SERIES IS DONE IN STEPS (real data: The Late Show, 22 seasons / 4,252 episodes, took 10.7 s as one step — hl:web serves one
  234. // request at a time, every page waited): step 1 = the details request (fields + cast); then one step per PART of its seasons
  235. // (≤ `partEpisodes` episodes, ≤ 20 seasons = one TMDB request — tmdbsync.hl syncShowPart); the last step = ids, TVmaze, poster,
  236. // `detailsAt`. A movie is one step. Between two steps every other request is served.
  237. // show id → { d (TMDB's answer of step 1), parts ([[season numbers]]), pos, sum } while a series is under way — in memory: a
  238. // restart begins again with the details request; what is stored by then is matched by tmdbId, nothing is duplicated
  239. static jobs = {}
  240. // (TRACKER_DETAILS_PART_EPISODES: the gate sets 1 → one season per step)
  241. static partEpisodes = env('TRACKER_DETAILS_PART_EPISODES') != null ? toNumber(env('TRACKER_DETAILS_PART_EPISODES')) : 300
  242. static emptySum = () => { return { steps = 0 requests = 0 tvmazeRequests = 0 cast = 0 crew = 0 people = 0 newSeasons = 0 newEpisodes = 0 posters = 0 ids = 0 ms = 0 error = '' } }
  243. static addSum = (sum, r, ms) => {
  244. let o = sum
  245. o.steps = o.steps + 1
  246. o.requests = o.requests + r.requests
  247. o.tvmazeRequests = o.tvmazeRequests + r.tvmazeRequests
  248. o.newSeasons = o.newSeasons + r.newSeasons + r.tvmazeSeasons
  249. o.newEpisodes = o.newEpisodes + r.newEpisodes + r.tvmazeEpisodes
  250. o.posters = o.posters + r.posters
  251. o.ids = o.ids + r.ids
  252. o.ms = o.ms + ms
  253. if (r.error != '') { o.error = r.error }
  254. return o
  255. }
  256. // TMDB's season list → parts of ≤ partEpisodes episodes (a bigger season is a part of its own), ≤ 20 seasons each; which of
  257. // them are really synced (season 0, empty ones) syncShowPart decides
  258. static seasonParts = (d) => {
  259. let parts = []
  260. let cur = []
  261. let n = 0
  262. for (x of listOr(d.seasons)) {
  263. if (x != null && x.season_number != null) {
  264. let c = x.episode_count != null ? x.episode_count : 0
  265. if (cur.length > 0 && (n + c > partEpisodes || cur.length == 20)) {
  266. parts.push(cur)
  267. cur = []
  268. n = 0
  269. }
  270. cur.push(x.season_number)
  271. n = n + c
  272. }
  273. }
  274. if (cur.length > 0) { parts.push(cur) }
  275. return parts
  276. }
  277. // a step's answer: { skipped (nothing to do), done, requests / tvmazeRequests (THIS step's — what the pause counts), adult,
  278. // error ('' = none), retry (a passing failure: no answer, 429, 5xx), sum (the whole title's counts so far: steps, requests,
  279. // cast, newSeasons, newEpisodes, posters, ids, ms) }
  280. static stepOut = () => { return { skipped = false done = false requests = 0 tvmazeRequests = 0 adult = null error = '' retry = false sum = emptySum() } }
  281. // the last step: the title is complete
  282. static markComplete = (showId, sum) => {
  283. let done = showsTable.fetch(showId)
  284. done.detailsAt = now()
  285. if (sum.error != '') { done.detailsError = sum.error }
  286. let ok = showsTable.update(done.id, done) != null
  287. refreshCatalogShow(done.id)
  288. searchTitleChanged(done.id)
  289. return ok
  290. }
  291. // step 1: the details request — the record's fields, the cast; a movie also its ids + poster (done); a series' parts are planned
  292. static firstStep = (show) => {
  293. let out = stepOut()
  294. let t0 = now()
  295. let movie = show.type == 'movie'
  296. out.requests = 1
  297. let d = tmdbGet((movie ? '/movie/' : '/tv/') + show.tmdbId + '?append_to_response=external_ids,' + (movie ? 'credits' : 'aggregate_credits'))
  298. if (d.failed != null) {
  299. out.error = show.title + ': ' + d.failed
  300. if (d.status == 404) {
  301. show.detailsCheck = 'TMDB 404'
  302. showsTable.update(show.id, show)
  303. out.done = true
  304. } else { out.retry = true }
  305. return out
  306. }
  307. let x = show
  308. out.adult = adultOf(d)
  309. if (out.adult != null) { x.adult = out.adult }
  310. let overview = textOr(d.overview)
  311. if (overview != null) {
  312. x.tmdbSummary = overview
  313. if (textOr(x.summary) == null) { x.summary = overview }
  314. }
  315. if (textOr(d.homepage) != null) { x.homepage = d.homepage }
  316. if (textOr(d.tagline) != null) { x.tagline = d.tagline }
  317. if (textOr(d.status) != null) { x.status = d.status }
  318. let tmdbGenreIds = []
  319. for (g of listOr(d.genres)) { if (g != null && g.id != null) { tmdbGenreIds.push(g.id) } }
  320. let genres = genreIdsOfTmdb(tmdbGenreIds)
  321. if (genres.length > 0) { x.genres = genres }
  322. let release = textOr(movie ? d.release_date : d.first_air_date)
  323. if (textOr(x.release) == null && release != null) {
  324. x.release = release
  325. if (release.length >= 4) { x.year = toNumber(release.slice(0, 4)) }
  326. }
  327. if (textOr(x.language) == null && textOr(d.original_language) != null) { x.language = d.original_language }
  328. if (out.adult == true) {
  329. // never public: the flag, no poster, no seasons, no cast (mission 056)
  330. x.detailsCheck = 'adult'
  331. showsTable.update(x.id, x)
  332. refreshCatalogShow(x.id)
  333. searchTitleChanged(x.id)
  334. out.done = true
  335. return out
  336. }
  337. // tracker#28: the whole cast + the crew, from this same answer
  338. let w = withCredits(x, d, movie)
  339. x = w.show
  340. if (showsTable.update(x.id, x) == null) {
  341. out.error = show.title + ': not updated: ' + showsTable.lastError()
  342. out.retry = true
  343. return out
  344. }
  345. let sum = emptySum()
  346. sum.cast = w.cast
  347. sum.crew = w.crew
  348. sum.people = w.people
  349. if (movie) {
  350. // one request was enough: ids + poster now (syncMovie with this answer)
  351. let r = syncShowWith(x.id, d)
  352. out.requests = r.requests
  353. out.sum = addSum(sum, r, now() - t0)
  354. if (r.error != '') { out.error = r.error }
  355. markComplete(x.id, out.sum)
  356. out.done = true
  357. return out
  358. }
  359. sum.steps = 1
  360. sum.requests = 1
  361. sum.ms = now() - t0
  362. // the next steps need the details, not the credits (a long series' aggregate_credits are ~1 MB, copied on every step)
  363. let dd = d
  364. dd.aggregate_credits = null
  365. jobs[x.id] = { d = dd parts = seasonParts(d) pos = 0 sum = sum }
  366. out.sum = sum
  367. return out
  368. }
  369. // ONE step of completing this title (by our id) — called until `done` (the page's face, the repair job)
  370. static completeStep = (showId) => {
  371. let show = showsTable.fetch(showId)
  372. if (!needsDetails(show)) {
  373. jobs[showId] = null
  374. let out = stepOut()
  375. out.skipped = true
  376. out.done = true
  377. return out
  378. }
  379. let job = jobs[showId]
  380. if (job == null) { return firstStep(show) }
  381. let t0 = now()
  382. let out = stepOut()
  383. let last = job.pos >= job.parts.length
  384. let r = syncShowPart(showId, job.d, { seasons = last ? [] : job.parts[job.pos] finish = last })
  385. out.requests = r.requests
  386. out.tvmazeRequests = r.tvmazeRequests
  387. if (r.error != '') { out.error = r.error }
  388. let j = job
  389. j.sum = addSum(j.sum, r, now() - t0)
  390. j.pos = j.pos + 1
  391. out.sum = j.sum
  392. if (!last) {
  393. jobs[showId] = j
  394. return out
  395. }
  396. jobs[showId] = null
  397. markComplete(showId, j.sum)
  398. out.done = true
  399. return out
  400. }
  401. // the line every completed title logs
  402. static completeLine = (title, sum) => {
  403. return '"' + title + '" steps=' + sum.steps + ' requests=' + sum.requests + ' tvmazeRequests=' + sum.tvmazeRequests + ' cast=' + sum.cast + ' newSeasons=' + sum.newSeasons
  404. + ' newEpisodes=' + sum.newEpisodes + ' posters=' + sum.posters + ' ids=' + sum.ids + ' ms=' + sum.ms + ' crew=' + sum.crew + ' people=' + sum.people + (sum.error != '' ? ' error: ' + sum.error : '')
  405. }
  406. // flush shows/seasons/episodes + people (new cast members)
  407. static persistDetails = () => {
  408. persistSync()
  409. personsTable.persist()
  410. return true
  411. }
  412. // the credits job touches only shows + people: the episodes' index files (~16 MB on the live data) are not rewritten
  413. static persistCredits = () => {
  414. showsTable.persist()
  415. personsTable.persist()
  416. return true
  417. }
  418. // ---- THE CREDITS JOB (tracker#28, project.hl `creditsTick`): every public title that has no credits yet, once --------------
  419. // (the incomplete ones get theirs from completeStep; adult and unknown-flag titles are never asked — not public)
  420. static needsCredits = (s) => { return s != null && s.tmdbId != null && s.adult == false && s.creditsAt == null && s.creditsCheck == null && !isIncomplete(s) }
  421. // in table order, read once when the job starts (a restart goes on with what is left)
  422. static creditsQueue = () => {
  423. let out = []
  424. for (s of showsTable.find(null, null)) { if (needsCredits(s)) { out.push(s.id) } }
  425. return out
  426. }
  427. // ONE title: one request (details + credits / aggregate_credits) → its cast + crew. Answers { skipped, requests, cast, crew,
  428. // people, ms, error, retry (no answer, 429, 5xx: ask again later) }. TMDB 404 / adult → `creditsCheck`, never asked again.
  429. static creditsStep = (showId) => {
  430. let out = { skipped = false requests = 0 cast = 0 crew = 0 people = 0 ms = 0 error = '' retry = false }
  431. let t0 = now()
  432. let show = showsTable.fetch(showId)
  433. if (!needsCredits(show)) { out.skipped = true return out }
  434. let movie = show.type == 'movie'
  435. out.requests = 1
  436. let d = tmdbGet((movie ? '/movie/' : '/tv/') + show.tmdbId + '?append_to_response=' + (movie ? 'credits' : 'aggregate_credits'))
  437. if (d.failed != null) {
  438. out.error = show.title + ': ' + d.failed
  439. if (d.status == 404) {
  440. show.creditsCheck = 'TMDB 404'
  441. showsTable.update(show.id, show)
  442. } else { out.retry = true }
  443. return out
  444. }
  445. if (adultOf(d) == true) {
  446. show.creditsCheck = 'adult'
  447. showsTable.update(show.id, show)
  448. return out
  449. }
  450. let w = withCredits(show, d, movie)
  451. if (showsTable.update(show.id, w.show) == null) {
  452. out.error = show.title + ': credits not stored: ' + showsTable.lastError()
  453. out.retry = true
  454. return out
  455. }
  456. out.cast = w.cast
  457. out.crew = w.crew
  458. out.people = w.people
  459. out.ms = now() - t0
  460. return out
  461. }

Branches

Latest commits

  • 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
  • 25a50bc4tracker#26 (mission 059): titles from a filmography are completed — on open (skeleton, step-wise face showComplete, no reload) and by the in-app details repair (resumes, TMDB-paced, series in parts); cast from TMDB credits; gate 231, tests/realdata-026.mjs, README + STATUSmre
  • 1704ec45tracker#17 (mission 057): season caret down/up, skeleton rows while a season loads, sessionless showSeasonEpisodes face (no page re-mount), client-only close; gate 214, tests/realdata-057.mjs, README + STATUSmre
  • f2fe3e36mission 056: README + STATUS (merge, fixes, Hybriel 8590df63, real-data check), tests/realdata-056.mjs, tools/check-public-slugs.hlmre
  • f40c250emission 056: re-vendor hybriel master 8590df63 (#121, #122); an adult title's page is Not found for non-followers; gate: leave the page before stopping the servermre
  • 2b7fdd6cmission 056: signed-out header one row on phones ("Log in", nowrap), backfill skips adult titles' posters, gate checksmre
  • 2c53d5efMerge branch 't16-person' (tracker#16 person pages) into main; filmography shows only public titles (054 adult flag), gate race fix (backfill start line)mre
  • c171227emission 054: hide adult/unknown titles from the public lists and the search; in-app adult-flag backfill (TMDB details + poster per title, resumes), gate + real-data proofmre
  • 139fafd8tracker#16: short bio (4 lines, click = all), real-data check script, README + STATUSmre
  • 93be9476tracker#16: person pages /person/<slug> with the filmography fetched from TMDB on the first visit (step by step), gatemre
  • 47a3cae6STATUS: mission 053 merge commit idsmre
  • dcc5eecaMerge branch 't14-search'mre
  • 03edc783Merge branch 't15-tvmaze'mre
  • 71b46345tracker#15: numbering check by date or title, placeholder titles in other languages, docs + real-data proofmre
  • 6bb2daf1tracker#13: homepage (tiles, intro, latest movies/shows), /shows, /movies/page/N, /my/movies; lists cached in memorymre
  • b8bd1157tracker#14: README + STATUS (search, real-data numbers, gate, merge notes)mre
  • 65c694a8tracker#14: search — header magnifier, /search/<text> (in-memory word-prefix index over titles + people), Fetch from web (TMDB search/multi, ours left out), Add = import via syncShow; gate +25 checks, real-data scriptmre
  • 34f2c15btracker#15: TVmaze merge in the sync (gaps only: new episodes/seasons, empty titles/air dates; numbering check), fake TVmaze episodes + gatemre
  • cbdc4ea7tracker#12: link icons TMDB/IMDb/TVDB/TVmaze; sync fills missing ids (TVmaze lookup); movies fetched via /movie/mre
  • b105bcd8tracker#11: Hybriel master ff51cf46 (checks no longer vanish), mobile-first styles, carets, follow button, sign-in modal, inverted check, orange castmre