gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit8751adb88751adb8tracker: report 032mre8751adb8/lib/details.hl

14.6 KB

  1. // lib/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 sync.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 (`withDetailsFields`, shared with the daily sync since tracker#18): title, overview → `tmdbSummary` (`summary` is the
  16. // creator's own text, never written), homepage, tagline, status, genres (by TMDB id, when
  17. // TMDB gives any), release/year/language when we have none, `adult`, the CAST (TMDB's first `castLimit`, in TMDB's order;
  18. // 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
  19. // list are kept after them). Then `detailsAt` (ms) = COMPLETE, never asked again (the daily sync keeps a FOLLOWED title fresh).
  20. // TMDB 404 → `detailsCheck = 'TMDB 404'` (never asked again; the page shows what it has). TMDB says ADULT → the flag is
  21. // stored, `detailsCheck = 'adult'`, no poster/seasons (mission 056: an adult title is never public, its poster not kept).
  22. //
  23. // tracker#33 (mission 029): INCOMPLETE is every title TMDB's details never went through (shows.hl isIncomplete) — also the old
  24. // tracker's migrated rows (5,697 on the live copy: never synced, 691 series without seasons); they are completed the same way.
  25. //
  26. // WHO CALLS IT (`completeStep`, again and again until `done`): the show page when an incomplete title is opened
  27. // (components/show.hl face `showComplete`, skeleton meanwhile) and the repair job in jobs.hl (`repairTick`, one step per tick
  28. // on the app's clock, like the adult backfill) for every incomplete title that exists when the app starts. Adult titles are
  29. // left alone by both.
  30. //
  31. // The cast and crew of the same answer: credits.hl (withCredits). Mission 028: the cast + crew parts moved there, `isIncomplete`
  32. // to shows.hl.
  33. import { now } from 'hl:time'
  34. import { env } from 'hl:proc'
  35. import { textOr, listOr } from './util.hl'
  36. import { showsTable, personsTable, kindOfTitle, isIncomplete, missingParts } from './shows.hl'
  37. import { tmdbGet, adultOf } from './tmdb.hl'
  38. import { syncShowWith, syncShowPart, persistSync } from './sync.hl'
  39. import { searchTitleChanged } from './search.hl'
  40. import { refreshCatalogShow } from './catalog.hl'
  41. import { genreIdsOfTmdb } from './people.hl'
  42. import { withCredits } from './credits.hl'
  43. import { hasCredits } from './credits-helpers.hl'
  44. // what the show page and the repair complete: an incomplete title that is not adult (mission 054/056: never public)
  45. static needsDetails = (&s) => { return isIncomplete(s) && s.adult != true && s.mergedInto == null }
  46. // every title the repair job has to do (read once when it starts — so a restart goes on with what is left). tracker#33: the ones
  47. // a page shows as reduced first (shows.hl missingParts: no seasons, poster, genres …), then the rest, each in table order
  48. static detailsRepairQueue = () => {
  49. first = []
  50. rest = []
  51. for (s of showsTable.find(null, null)) {
  52. if (needsDetails(s)) {
  53. if (missingParts(s).length > 0) { first.push(s.id) } else { rest.push(s.id) }
  54. }
  55. }
  56. for (id of rest) { first.push(id) }
  57. return first
  58. }
  59. // THE RECORD'S OWN FIELDS from a details answer (tracker#26 completion; tracker#18 the daily sync too — creator: "some shows changed
  60. // descriptions and titles and that did not update"): title (a series' `name`, a movie's `title`), TMDB's overview → `tmdbSummary`,
  61. // homepage, tagline, status, genres (by TMDB id), adult; type / release / year / language only when we have none. A TMDB value
  62. // that is empty never overwrites ours. `summary` is the creator's OWN text — never written here (the page shows summary >
  63. // tmdbSummary > tvmazeSummary, shows.hl summaryOf). The slug (`urlSegment`) never changes with the title.
  64. // mission 070 (conductor): the sync never DESTROYS what the old tracker's migration brought — before TMDB's value replaces a
  65. // migrated record's title / genres / homepage / tagline for the FIRST time, the old one is kept in `migratedTitle` /
  66. // `migratedGenres` / `migratedHomepage` / `migratedTagline` (kept, shown nowhere; set once, a later rename leaves it). A record is
  67. // migrated when its `oldId` is the old tracker's (not `tmdb-…`: search import, filmography fill). `status`, counts, ids and the
  68. // poster are TMDB's state, written by the sync since #9 — not kept.
  69. static isMigratedRecord = (&show) => { return show.oldId != null && ('' + show.oldId).slice(0, 5) != 'tmdb-' }
  70. static sameIdList = (&a, &b) => {
  71. x = listOr(a)
  72. y = listOr(b)
  73. if (x.length != y.length) { return false }
  74. let i = 0
  75. while (i < x.length) { if ('' + x[i] != '' + y[i]) { return false } i = i + 1 }
  76. return true
  77. }
  78. static withDetailsFields = (show, &d, &movie) => {
  79. x = show
  80. migrated = isMigratedRecord(show)
  81. adult = adultOf(d)
  82. if (adult != null) { x.adult = adult }
  83. let title = textOr(movie ? d.title : d.name)
  84. if (title != null) {
  85. if (migrated && x.migratedTitle == null && textOr(x.title) != null && x.title != title) { x.migratedTitle = x.title }
  86. x.title = title
  87. }
  88. if (textOr(d.overview) != null) { x.tmdbSummary = d.overview }
  89. if (textOr(d.homepage) != null) {
  90. if (migrated && x.migratedHomepage == null && textOr(x.homepage) != null && x.homepage != d.homepage) { x.migratedHomepage = x.homepage }
  91. x.homepage = d.homepage
  92. }
  93. if (textOr(d.tagline) != null) {
  94. if (migrated && x.migratedTagline == null && textOr(x.tagline) != null && x.tagline != d.tagline) { x.migratedTagline = x.tagline }
  95. x.tagline = d.tagline
  96. }
  97. if (textOr(d.status) != null) { x.status = d.status }
  98. tmdbGenreIds = []
  99. for (g of listOr(d.genres)) { if (g != null && g.id != null) { tmdbGenreIds.push(g.id) } }
  100. genres = genreIdsOfTmdb(tmdbGenreIds)
  101. if (genres.length > 0) {
  102. if (migrated && x.migratedGenres == null && listOr(x.genres).length > 0 && !sameIdList(x.genres, genres)) { x.migratedGenres = x.genres }
  103. x.genres = genres
  104. }
  105. if (textOr(x.type) == null) { x.type = movie ? 'movie' : 'series' }
  106. release = textOr(movie ? d.release_date : d.first_air_date)
  107. if (textOr(x.release) == null && release != null) {
  108. x.release = release
  109. if (release.length >= 4) { x.year = toNumber(release.slice(0, 4)) }
  110. }
  111. if (textOr(x.language) == null && textOr(d.original_language) != null) { x.language = d.original_language }
  112. // tracker#21 (merge, mission 070): TMDB's TV type + the kind from the genres just stored (the delta's light step and the
  113. // completion of #26 store it too, not only syncShow / the kind backfill)
  114. if (!movie && textOr(d.type) != null) {
  115. x.tmdbType = d.type
  116. x.kind = kindOfTitle(d.type, x.genres)
  117. }
  118. return x
  119. }
  120. // THE DAILY SYNC (jobs.hl syncTick, tools/sync-tmdb.hl; tracker#18): after sync.hl `syncShow` (seasons, episodes, poster,
  121. // ids, TVmaze) the record's own fields (withDetailsFields) and its cast + crew (#28) from the SAME details answer (`res.details`)
  122. // — no extra request. Answers { renamed (the title changed: the search re-indexes it), cast, crew, people, ms, error }
  123. static applySynced = (&showId, &d) => {
  124. out = { renamed = false cast = 0 crew = 0 people = 0 ms = 0 error = '' }
  125. t0 = now()
  126. show = showsTable.fetch(showId)
  127. if (show == null || d == null) { return out }
  128. movie = show.type == 'movie'
  129. before = show.title
  130. let x = withDetailsFields(show, d, movie)
  131. out.renamed = x.title != before
  132. if (x.adult != true && hasCredits(d, movie)) {
  133. w = withCredits(x, d, movie)
  134. x = w.show
  135. out.cast = w.cast
  136. out.crew = w.crew
  137. out.people = w.people
  138. }
  139. if (showsTable.update(x.id, x) == null) { out.error = x.title + ': record not stored: ' + showsTable.lastError() }
  140. out.ms = now() - t0
  141. return out
  142. }
  143. // 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
  144. // request at a time, every page waited): step 1 = the details request (fields + cast); then one step per PART of its seasons
  145. // (≤ `partEpisodes` episodes, ≤ 20 seasons = one TMDB request — sync.hl syncShowPart); the last step = ids, TVmaze, poster,
  146. // `detailsAt`. A movie is one step. Between two steps every other request is served.
  147. // show id → { d (TMDB's answer of step 1), parts ([[season numbers]]), pos, sum } while a series is under way — in memory: a
  148. // restart begins again with the details request; what is stored by then is matched by tmdbId, nothing is duplicated
  149. static jobs = {}
  150. // (TRACKER_DETAILS_PART_EPISODES: the gate sets 1 → one season per step)
  151. static partEpisodes = env('TRACKER_DETAILS_PART_EPISODES') != null ? toNumber(env('TRACKER_DETAILS_PART_EPISODES')) : 300
  152. 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 = '' } }
  153. static addSum = (sum, &r, &ms) => {
  154. o = sum
  155. o.steps = o.steps + 1
  156. o.requests = o.requests + r.requests
  157. o.tvmazeRequests = o.tvmazeRequests + r.tvmazeRequests
  158. o.newSeasons = o.newSeasons + r.newSeasons + r.tvmazeSeasons
  159. o.newEpisodes = o.newEpisodes + r.newEpisodes + r.tvmazeEpisodes
  160. o.posters = o.posters + r.posters
  161. o.ids = o.ids + r.ids
  162. o.ms = o.ms + ms
  163. if (r.error != '') { o.error = r.error }
  164. return o
  165. }
  166. // TMDB's season list → parts of ≤ partEpisodes episodes (a bigger season is a part of its own), ≤ 20 seasons each; which of
  167. // them are really synced (season 0, empty ones) syncShowPart decides
  168. static seasonParts = (&d) => {
  169. parts = []
  170. let cur = []
  171. let n = 0
  172. for (x of listOr(d.seasons)) {
  173. if (x != null && x.season_number != null) {
  174. let c = x.episode_count != null ? x.episode_count : 0
  175. if (cur.length > 0 && (n + c > partEpisodes || cur.length == 20)) {
  176. parts.push(cur)
  177. cur = []
  178. n = 0
  179. }
  180. cur.push(x.season_number)
  181. n = n + c
  182. }
  183. }
  184. if (cur.length > 0) { parts.push(cur) }
  185. return parts
  186. }
  187. // a step's answer: { skipped (nothing to do), done, requests / tvmazeRequests (THIS step's — what the pause counts), adult,
  188. // error ('' = none), retry (a passing failure: no answer, 429, 5xx), sum (the whole title's counts so far: steps, requests,
  189. // cast, newSeasons, newEpisodes, posters, ids, ms) }
  190. static stepOut = () => { return { skipped = false done = false requests = 0 tvmazeRequests = 0 adult = null error = '' retry = false sum = emptySum() } }
  191. // the last step: the title is complete
  192. static markComplete = (&showId, &sum) => {
  193. done = showsTable.fetch(showId)
  194. done.detailsAt = now()
  195. if (sum.error != '') { done.detailsError = sum.error }
  196. ok = showsTable.update(done.id, done) != null
  197. refreshCatalogShow(done.id)
  198. searchTitleChanged(done.id)
  199. return ok
  200. }
  201. // step 1: the details request — the record's fields, the cast; a movie also its ids + poster (done); a series' parts are planned
  202. static firstStep = (show) => {
  203. out = stepOut()
  204. t0 = now()
  205. movie = show.type == 'movie'
  206. out.requests = 1
  207. d = tmdbGet((movie ? '/movie/' : '/tv/') + show.tmdbId + '?append_to_response=external_ids,' + (movie ? 'credits' : 'aggregate_credits'))
  208. if (d.failed != null) {
  209. out.error = show.title + ': ' + d.failed
  210. if (d.status == 404) {
  211. show.detailsCheck = 'TMDB 404'
  212. showsTable.update(show.id, show)
  213. out.done = true
  214. } else { out.retry = true }
  215. return out
  216. }
  217. out.adult = adultOf(d)
  218. let x = withDetailsFields(show, d, movie)
  219. // mission 025: a movie has no season/episode counts (hasTvLeftovers)
  220. if (movie) { x.seasonsCount = null x.episodesCount = null }
  221. if (out.adult == true) {
  222. // never public: the flag, no poster, no seasons, no cast (mission 056)
  223. x.detailsCheck = 'adult'
  224. showsTable.update(x.id, x)
  225. refreshCatalogShow(x.id)
  226. searchTitleChanged(x.id)
  227. out.done = true
  228. return out
  229. }
  230. // tracker#28: the whole cast + the crew, from this same answer
  231. w = withCredits(x, d, movie)
  232. x = w.show
  233. if (showsTable.update(x.id, x) == null) {
  234. out.error = show.title + ': not updated: ' + showsTable.lastError()
  235. out.retry = true
  236. return out
  237. }
  238. sum = emptySum()
  239. sum.cast = w.cast
  240. sum.crew = w.crew
  241. sum.people = w.people
  242. if (movie) {
  243. // one request was enough: ids + poster now (syncMovie with this answer)
  244. r = syncShowWith(x.id, d)
  245. out.requests = r.requests
  246. out.sum = addSum(sum, r, now() - t0)
  247. if (r.error != '') { out.error = r.error }
  248. markComplete(x.id, out.sum)
  249. out.done = true
  250. return out
  251. }
  252. sum.steps = 1
  253. sum.requests = 1
  254. sum.ms = now() - t0
  255. // the next steps need the details, not the credits (a long series' aggregate_credits are ~1 MB, copied on every step)
  256. dd = d
  257. dd.aggregate_credits = null
  258. jobs[x.id] = { d = dd parts = seasonParts(d) pos = 0 sum = sum }
  259. out.sum = sum
  260. return out
  261. }
  262. // ONE step of completing this title (by our id) — called until `done` (the page's face, the repair job)
  263. static completeStep = (&showId) => {
  264. show = showsTable.fetch(showId)
  265. if (!needsDetails(show)) {
  266. jobs[showId] = null
  267. let out = stepOut()
  268. out.skipped = true
  269. out.done = true
  270. return out
  271. }
  272. job = jobs[showId]
  273. if (job == null) { return firstStep(show) }
  274. t0 = now()
  275. let out = stepOut()
  276. last = job.pos >= job.parts.length
  277. r = syncShowPart(showId, job.d, { seasons = last ? [] : job.parts[job.pos] finish = last })
  278. out.requests = r.requests
  279. out.tvmazeRequests = r.tvmazeRequests
  280. if (r.error != '') { out.error = r.error }
  281. j = job
  282. j.sum = addSum(j.sum, r, now() - t0)
  283. j.pos = j.pos + 1
  284. out.sum = j.sum
  285. if (!last) {
  286. jobs[showId] = j
  287. return out
  288. }
  289. jobs[showId] = null
  290. markComplete(showId, j.sum)
  291. out.done = true
  292. return out
  293. }
  294. // the line every completed title logs
  295. static completeLine = (&title, &sum) => {
  296. return '"' + title + '" steps=' + sum.steps + ' requests=' + sum.requests + ' tvmazeRequests=' + sum.tvmazeRequests + ' cast=' + sum.cast + ' newSeasons=' + sum.newSeasons
  297. + ' newEpisodes=' + sum.newEpisodes + ' posters=' + sum.posters + ' ids=' + sum.ids + ' ms=' + sum.ms + ' crew=' + sum.crew + ' people=' + sum.people + (sum.error != '' ? ' error: ' + sum.error : '')
  298. }
  299. // flush shows/seasons/episodes + people (new cast members)
  300. static persistDetails = () => {
  301. persistSync()
  302. personsTable.persist()
  303. return true
  304. }

Branches

Latest commits

  • 8751adb8tracker: report 032mre
  • 9bce1f65tracker mission 032: STATUS gate files + the hour-boundary flakemre
  • 718bfb89tracker#37 (mission 032): /people = everyone, last updated first (updatedAt stamped by the person fill; view built at boot, touched people first at once), photo + name tiles (person colour) with the /movies pagination, /people/<letter> removed; photo = our file, tmdbProfile, a cast/crew entry's profile (in-memory map at boot), else the new 'no photo' placeholder; new cast/crew/created_by people keep tmdbProfile; search people rows with the photo; /settings = the heading only; util.hl sortDesc starts from sorted runs (same result, 105k: 1.6 s -> 0.15 s); gates 369/0, 32/0, 52/0, check-theme 0; README/STATUS/LOGmre
  • 96ba683adeploy.sh: a gate without a 'passed,' line (check-theme) no longer ends the scriptmre
  • eb3b9205tracker: report 031mre
  • 9b5d2e89tracker mission 031: README (What it does, Test: four gates + the #32 checks, Files: theme/, new pages), STATUS (real copy, A/B load, how to repeat, open points), LOGmre
  • 39950e4ctracker#32 (mission 031): the WorldAPI theme (theme/ vendored verbatim from layouts.worldapi.org 85b5654; styles.hl inherits it: accent green-dark, type colours 1-6; own base/header rules, row lines, genre-pill and inverted-button frames removed, the season foldable keeps its line; check-theme 21 -> 0, 4th deploy gate; main actions class primary) and the #32 header (theme AppHeader/MainMenu/UserMenu/Sidebar/ContentFirst: desktop brand, search, Series|Shows|Movies|Genres|People, user icon with Unwatched..Settings, Logout; signed out the ident selector, phone the iD icon dropdown; phone menu in the sidebar overlay; marked entry by :has); /find -> /search/<q>, /genres, /people(/<letter>), /settings; main { ContentFirst { slot } } works around the hl:web one-line slot bug; gates 365/0, 32/0, 52/0, check-theme 0mre
  • a386dc92tracker: reports 029 + 030mre
  • 71e0fd7dtracker missions 029 + 030: README (What it does, Files, gate count), STATUS (real-copy numbers, how to repeat, open points), LOGmre
  • d36ea6eatracker#34 + #35 (mission 030): Follow directly under the poster, as wide as the poster (show.hl, styles.hl); the status pill next to a series' title — TVmaze's status (new tvmazeStatus, stored by the sync's TVmaze merge) else TMDB's, TVmaze Ended + TMDB Canceled = Canceled, inverted (filled, dark text, no border), green running / yellow pending / red canceled / muted ended (shows.hl statusOf); the daily delta asks TVmaze's status of an unfollowed series TVmaze's change list names (dailysync.hl syncRunStep, sync.hl syncTvmazeStatus); the status backfill after the details repair (backfill.hl, jobs.hl statusTick; resumable, 550 ms per TVmaze request); gates 354/0, 32/0, 52/0mre
  • 7d7d4487tracker#33 (mission 029): reduced titles — every title TMDB's details never went through this app (no detailsAt, no tmdbSync) is incomplete (shows.hl isIncomplete; the old tracker's migrated rows passed #26's test: 5,697 non-adult on the live copy, 691 series without seasons); the repair job does the visibly reduced first (shows.hl missingParts), the page completes one on open; a title TMDB has no poster for (The Remaining) shows the placeholder; tools/count-incomplete.hl; gate fixtures stand for synced titles (tmdbSync), tests/seed-reduced.hl + #33 checks; gates 347/0, 32/0, 52/0mre
  • 661c2592tracker: report 028mre
  • 27c916fatracker mission 028: README ("Code order", the new file map), STATUS (counts before/after, tests, how to repeat, open), LOGmre
  • d924f398tracker mission 028: comments name the new files (sync.hl, dailysync.hl, backfill.hl, credits.hl, jobs.hl, images.hl …); tools/ref-params.py + tools/lambda-audit.py also scan lib/ (they globbed the root only), lambda-audit counts a plain `x = p` alias like `let x = p`mre
  • 2e89b968tracker mission 028 (code order) 5/5 let: `let` only where a variable is reassigned — 667 never-reassigned lets became plain declarations (project.hl, lib/, components/, tools/, tests/); kept: 264 in loop bodies (a plain declaration there is 'Cannot reassign' on the 2nd pass), 234 reassigned, 27 whose name is also a member/outer/free name (a plain write would rebind it); tools/let-audit.py decides and fixes (README 'Code order'); tests/realdata-m028.{sh,mjs} = the page-output diff on a real copy; gates 342/0, 32/0, 52/0, real-copy pages identicalmre
  • 54796ff2tracker mission 028 (code order) 4/5 thin faces + last copies: the show page's check/follow faces call lib/watches.hl toggleWatched / toggleSeasonWatched (seasonAllWatched moved there) and lib/follows.hl toggleFollowed; both logins (header selector face, /login/callback) share lib/users.hl userOfCode; todayStr/listOf copies in components and the export readers copied into tools/migrate.hl + tools/old-short-ids.hl now once (lib/util.hl, lib/export.hl); gates 342/0, 32/0, 52/0; old-short-ids output byte-identical, migrate output identicalmre
  • 06b078e3tracker mission 028 (code order) 3/5 project.hl is the map: config, routes, wiring and a feature → file index (914 → 258 lines); the background jobs (daily sync run, backfills, details repair, credits job, merge, short ids, collection seed) moved unchanged into lib/jobs.hl (a class: their state is reassigned every step, a static cannot be; one instance made after the server), the login callback into lib/users.hl, poster/photo serving into lib/images.hl, the /shows/<slug> rule into lib/shows.hl showsMovedPath; route handlers are thin wrappers; gates 342/0, 32/0, 52/0, real-copy pages identicalmre
  • 94716fd2tracker mission 028 (code order) 2/5 util + topics: lib/util.hl holds envOr, storageDir, postersDir, profilesDir, newId, hexDigits, todayStr, dateOr, textOr, hasId, listOr, firstOf, sortDesc once (were copied into up to 5 files); tmdbsync.hl split into tmdb.hl (TMDB/TVmaze requests), sync.hl (one title's sync), sync-helpers.hl, backfill.hl; details.hl split into details.hl, credits.hl, credits-helpers.hl (isIncomplete to shows.hl); search-helpers.hl (words, query, ranking, slugs); collections.hl (the TMDB collection seed, out of franchises.hl); deltasync.hl renamed dailysync.hl; no behaviour change: gates 342/0, 32/0, 52/0, real-copy pages identicalmre
  • 186079b0tracker mission 028 (code order) 1/5 move: every root .hl except project.hl into lib/ (styles.hl into components/), import paths only; gates 342/0, 32/0, 52/0; real-copy pages identicalmre
  • 4f47f181tracker: report 027mre