gitoriaLog in with ident

tracker

All repositories: gitoria

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

11.9 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. import { now } from 'hl:time'
  27. import { env } from 'hl:proc'
  28. import { showsTable, personsTable } from './shows.hl'
  29. import { tmdbGet, textOr, adultOf, syncShowWith, syncShowPart, persistSync } from './tmdbsync.hl'
  30. import { searchTitleChanged } from './search.hl'
  31. import { refreshCatalogShow } from './catalog.hl'
  32. import { castPersonId, genreIdsOfTmdb } from './people.hl'
  33. // how many of TMDB's cast a title keeps (TMDB's own order = billing; a movie can list 100+, each would be a new person)
  34. static castLimit = 20
  35. static listOr = (v) => { return v == null ? [] : v }
  36. // INCOMPLETE = a filmography title (`minimal`) or an import whose sync never finished (`imported`, no `tmdbSync`), not
  37. // completed (`detailsAt`) and not given up on (`detailsCheck`)
  38. static isIncomplete = (s) => {
  39. return s != null && s.tmdbId != null && s.detailsAt == null && s.detailsCheck == null && (s.minimal == true || (s.imported != null && s.tmdbSync == null))
  40. }
  41. // what the show page and the repair complete: an incomplete title that is not adult (mission 054/056: never public)
  42. static needsDetails = (s) => { return isIncomplete(s) && s.adult != true }
  43. // 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)
  44. static detailsRepairQueue = () => {
  45. let out = []
  46. for (s of showsTable.find(null, null)) { if (needsDetails(s)) { out.push(s.id) } }
  47. return out
  48. }
  49. // a series' roles ([{ character, episode_count }]) as one text: "Mother / Lamia"
  50. static rolesText = (roles) => {
  51. let out = ''
  52. for (r of listOr(roles)) {
  53. let c = r != null ? textOr(r.character) : null
  54. if (c != null && !(' / ' + out + ' / ').includes(' / ' + c + ' / ')) { out = out == '' ? c : out + ' / ' + c }
  55. }
  56. return out
  57. }
  58. // TMDB's cast → [{ tmdbId, name, character, gender }], TMDB's order, at most castLimit, adult people left out
  59. static castOf = (d, movie) => {
  60. let src = movie ? (d.credits != null ? d.credits.cast : null) : (d.aggregate_credits != null ? d.aggregate_credits.cast : null)
  61. let out = []
  62. for (c of listOr(src)) {
  63. if (out.length < castLimit && c != null && c.id != null && hlTypeName(c.id) == 'Number' && textOr(c.name) != null && c.adult != true) {
  64. let role = movie ? textOr(c.character) : rolesText(c.roles)
  65. out.push({ tmdbId = c.id name = c.name character = role != null ? role : '' gender = c.gender })
  66. }
  67. }
  68. return out
  69. }
  70. // the title's cast: TMDB's (linked to our people, new ones added) first, then the entries we had that TMDB does not list
  71. static mergedCast = (show, tmdbCast) => {
  72. let out = []
  73. let have = {}
  74. let showKey = show.id
  75. for (c of tmdbCast) {
  76. let pid = castPersonId(c, showKey)
  77. if (pid != null && have[pid] != true) {
  78. have[pid] = true
  79. out.push({ person = pid character = c.character actor = c.name })
  80. }
  81. }
  82. for (c of listOr(show.cast)) {
  83. if (c != null && c.person != null && have[c.person] != true) {
  84. have[c.person] = true
  85. out.push(c)
  86. }
  87. }
  88. return out
  89. }
  90. // 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
  91. // request at a time, every page waited): step 1 = the details request (fields + cast); then one step per PART of its seasons
  92. // (≤ `partEpisodes` episodes, ≤ 20 seasons = one TMDB request — tmdbsync.hl syncShowPart); the last step = ids, TVmaze, poster,
  93. // `detailsAt`. A movie is one step. Between two steps every other request is served.
  94. // show id → { d (TMDB's answer of step 1), parts ([[season numbers]]), pos, sum } while a series is under way — in memory: a
  95. // restart begins again with the details request; what is stored by then is matched by tmdbId, nothing is duplicated
  96. static jobs = {}
  97. // (TRACKER_DETAILS_PART_EPISODES: the gate sets 1 → one season per step)
  98. static partEpisodes = env('TRACKER_DETAILS_PART_EPISODES') != null ? toNumber(env('TRACKER_DETAILS_PART_EPISODES')) : 300
  99. static emptySum = () => { return { steps = 0 requests = 0 tvmazeRequests = 0 cast = 0 newSeasons = 0 newEpisodes = 0 posters = 0 ids = 0 ms = 0 error = '' } }
  100. static addSum = (sum, r, ms) => {
  101. let o = sum
  102. o.steps = o.steps + 1
  103. o.requests = o.requests + r.requests
  104. o.tvmazeRequests = o.tvmazeRequests + r.tvmazeRequests
  105. o.newSeasons = o.newSeasons + r.newSeasons + r.tvmazeSeasons
  106. o.newEpisodes = o.newEpisodes + r.newEpisodes + r.tvmazeEpisodes
  107. o.posters = o.posters + r.posters
  108. o.ids = o.ids + r.ids
  109. o.ms = o.ms + ms
  110. if (r.error != '') { o.error = r.error }
  111. return o
  112. }
  113. // TMDB's season list → parts of ≤ partEpisodes episodes (a bigger season is a part of its own), ≤ 20 seasons each; which of
  114. // them are really synced (season 0, empty ones) syncShowPart decides
  115. static seasonParts = (d) => {
  116. let parts = []
  117. let cur = []
  118. let n = 0
  119. for (x of listOr(d.seasons)) {
  120. if (x != null && x.season_number != null) {
  121. let c = x.episode_count != null ? x.episode_count : 0
  122. if (cur.length > 0 && (n + c > partEpisodes || cur.length == 20)) {
  123. parts.push(cur)
  124. cur = []
  125. n = 0
  126. }
  127. cur.push(x.season_number)
  128. n = n + c
  129. }
  130. }
  131. if (cur.length > 0) { parts.push(cur) }
  132. return parts
  133. }
  134. // a step's answer: { skipped (nothing to do), done, requests / tvmazeRequests (THIS step's — what the pause counts), adult,
  135. // error ('' = none), retry (a passing failure: no answer, 429, 5xx), sum (the whole title's counts so far: steps, requests,
  136. // cast, newSeasons, newEpisodes, posters, ids, ms) }
  137. static stepOut = () => { return { skipped = false done = false requests = 0 tvmazeRequests = 0 adult = null error = '' retry = false sum = emptySum() } }
  138. // the last step: the title is complete
  139. static markComplete = (showId, sum) => {
  140. let done = showsTable.fetch(showId)
  141. done.detailsAt = now()
  142. if (sum.error != '') { done.detailsError = sum.error }
  143. let ok = showsTable.update(done.id, done) != null
  144. refreshCatalogShow(done.id)
  145. searchTitleChanged(done.id)
  146. return ok
  147. }
  148. // step 1: the details request — the record's fields, the cast; a movie also its ids + poster (done); a series' parts are planned
  149. static firstStep = (show) => {
  150. let out = stepOut()
  151. let t0 = now()
  152. let movie = show.type == 'movie'
  153. out.requests = 1
  154. let d = tmdbGet((movie ? '/movie/' : '/tv/') + show.tmdbId + '?append_to_response=external_ids,' + (movie ? 'credits' : 'aggregate_credits'))
  155. if (d.failed != null) {
  156. out.error = show.title + ': ' + d.failed
  157. if (d.status == 404) {
  158. show.detailsCheck = 'TMDB 404'
  159. showsTable.update(show.id, show)
  160. out.done = true
  161. } else { out.retry = true }
  162. return out
  163. }
  164. let x = show
  165. out.adult = adultOf(d)
  166. if (out.adult != null) { x.adult = out.adult }
  167. let overview = textOr(d.overview)
  168. if (overview != null) {
  169. x.tmdbSummary = overview
  170. if (textOr(x.summary) == null) { x.summary = overview }
  171. }
  172. if (textOr(d.homepage) != null) { x.homepage = d.homepage }
  173. if (textOr(d.tagline) != null) { x.tagline = d.tagline }
  174. if (textOr(d.status) != null) { x.status = d.status }
  175. let tmdbGenreIds = []
  176. for (g of listOr(d.genres)) { if (g != null && g.id != null) { tmdbGenreIds.push(g.id) } }
  177. let genres = genreIdsOfTmdb(tmdbGenreIds)
  178. if (genres.length > 0) { x.genres = genres }
  179. let release = textOr(movie ? d.release_date : d.first_air_date)
  180. if (textOr(x.release) == null && release != null) {
  181. x.release = release
  182. if (release.length >= 4) { x.year = toNumber(release.slice(0, 4)) }
  183. }
  184. if (textOr(x.language) == null && textOr(d.original_language) != null) { x.language = d.original_language }
  185. if (out.adult == true) {
  186. // never public: the flag, no poster, no seasons, no cast (mission 056)
  187. x.detailsCheck = 'adult'
  188. showsTable.update(x.id, x)
  189. refreshCatalogShow(x.id)
  190. searchTitleChanged(x.id)
  191. out.done = true
  192. return out
  193. }
  194. let tmdbCast = castOf(d, movie)
  195. x.cast = mergedCast(x, tmdbCast)
  196. if (showsTable.update(x.id, x) == null) {
  197. out.error = show.title + ': not updated: ' + showsTable.lastError()
  198. out.retry = true
  199. return out
  200. }
  201. let sum = emptySum()
  202. sum.cast = tmdbCast.length
  203. if (movie) {
  204. // one request was enough: ids + poster now (syncMovie with this answer)
  205. let r = syncShowWith(x.id, d)
  206. out.requests = r.requests
  207. out.sum = addSum(sum, r, now() - t0)
  208. if (r.error != '') { out.error = r.error }
  209. markComplete(x.id, out.sum)
  210. out.done = true
  211. return out
  212. }
  213. sum.steps = 1
  214. sum.requests = 1
  215. sum.ms = now() - t0
  216. jobs[x.id] = { d = d parts = seasonParts(d) pos = 0 sum = sum }
  217. out.sum = sum
  218. return out
  219. }
  220. // ONE step of completing this title (by our id) — called until `done` (the page's face, the repair job)
  221. static completeStep = (showId) => {
  222. let show = showsTable.fetch(showId)
  223. if (!needsDetails(show)) {
  224. jobs[showId] = null
  225. let out = stepOut()
  226. out.skipped = true
  227. out.done = true
  228. return out
  229. }
  230. let job = jobs[showId]
  231. if (job == null) { return firstStep(show) }
  232. let t0 = now()
  233. let out = stepOut()
  234. let last = job.pos >= job.parts.length
  235. let r = syncShowPart(showId, job.d, { seasons = last ? [] : job.parts[job.pos] finish = last })
  236. out.requests = r.requests
  237. out.tvmazeRequests = r.tvmazeRequests
  238. if (r.error != '') { out.error = r.error }
  239. let j = job
  240. j.sum = addSum(j.sum, r, now() - t0)
  241. j.pos = j.pos + 1
  242. out.sum = j.sum
  243. if (!last) {
  244. jobs[showId] = j
  245. return out
  246. }
  247. jobs[showId] = null
  248. markComplete(showId, j.sum)
  249. out.done = true
  250. return out
  251. }
  252. // the line every completed title logs
  253. static completeLine = (title, sum) => {
  254. return '"' + title + '" steps=' + sum.steps + ' requests=' + sum.requests + ' tvmazeRequests=' + sum.tvmazeRequests + ' cast=' + sum.cast + ' newSeasons=' + sum.newSeasons
  255. + ' newEpisodes=' + sum.newEpisodes + ' posters=' + sum.posters + ' ids=' + sum.ids + ' ms=' + sum.ms + (sum.error != '' ? ' error: ' + sum.error : '')
  256. }
  257. // flush shows/seasons/episodes + people (new cast members)
  258. static persistDetails = () => {
  259. persistSync()
  260. personsTable.persist()
  261. return true
  262. }

Branches

Latest commits

  • 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
  • 31b758aatracker#10: installable app (manifest, service worker, offline shell), own icon + faviconmre