tracker
All repositories: gitoria
14.6 KB
// lib/details.hl — A TITLE'S FULL DETAILS (tracker.worldapi.org#26). Creator: "when i go to an artist and it loads the bibliography// those loaded movies and series miss most information and it stays that way when i click on them".//// The person page's fill (people.hl, #16) adds every title we don't have as a MINIMAL record — title, year, overview, genres,// type, tmdbId, adult, this one person as cast; no poster, no seasons, no external ids, no homepage/tagline/status// (`minimal = true`). A search import (search.hl importTitle) is full except for the cast. Here a minimal title gets what a// full import has, plus its cast://// GET <TMDB>/movie/<tmdbId>?append_to_response=external_ids,credits a movie// GET <TMDB>/tv/<tmdbId>?append_to_response=external_ids,aggregate_credits a series (cast of ALL seasons, roles merged)// then sync.hl `syncShowPart(id, <that answer>, …)` — the daily sync's work, without asking for the details again, IN// STEPS (see `jobs` below): a series' seasons + episodes (`season/1,…`), then the poster (w342), missing imdb/tvdb ids,// TVmaze lookup + its episode merge (#15); a movie its imdb id + poster//// Stored (`withDetailsFields`, shared with the daily sync since tracker#18): title, overview → `tmdbSummary` (`summary` is the// creator's own text, never written), homepage, tagline, status, genres (by TMDB id, when// TMDB gives any), release/year/language when we have none, `adult`, the CAST (TMDB's first `castLimit`, in TMDB's order;// 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// list are kept after them). Then `detailsAt` (ms) = COMPLETE, never asked again (the daily sync keeps a FOLLOWED title fresh).// TMDB 404 → `detailsCheck = 'TMDB 404'` (never asked again; the page shows what it has). TMDB says ADULT → the flag is// stored, `detailsCheck = 'adult'`, no poster/seasons (mission 056: an adult title is never public, its poster not kept).//// tracker#33 (mission 029): INCOMPLETE is every title TMDB's details never went through (shows.hl isIncomplete) — also the old// tracker's migrated rows (5,697 on the live copy: never synced, 691 series without seasons); they are completed the same way.//// WHO CALLS IT (`completeStep`, again and again until `done`): the show page when an incomplete title is opened// (components/show.hl face `showComplete`, skeleton meanwhile) and the repair job in jobs.hl (`repairTick`, one step per tick// on the app's clock, like the adult backfill) for every incomplete title that exists when the app starts. Adult titles are// left alone by both.//// The cast and crew of the same answer: credits.hl (withCredits). Mission 028: the cast + crew parts moved there, `isIncomplete`// to shows.hl.import { now } from 'hl:time'import { env } from 'hl:proc'import { textOr, listOr } from './util.hl'import { showsTable, personsTable, kindOfTitle, isIncomplete, missingParts } from './shows.hl'import { tmdbGet, adultOf } from './tmdb.hl'import { syncShowWith, syncShowPart, persistSync } from './sync.hl'import { searchTitleChanged } from './search.hl'import { refreshCatalogShow } from './catalog.hl'import { genreIdsOfTmdb } from './people.hl'import { withCredits } from './credits.hl'import { hasCredits } from './credits-helpers.hl'// what the show page and the repair complete: an incomplete title that is not adult (mission 054/056: never public)static needsDetails = (&s) => { return isIncomplete(s) && s.adult != true && s.mergedInto == null }// 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// a page shows as reduced first (shows.hl missingParts: no seasons, poster, genres …), then the rest, each in table orderstatic detailsRepairQueue = () => {first = []rest = []for (s of showsTable.find(null, null)) {if (needsDetails(s)) {if (missingParts(s).length > 0) { first.push(s.id) } else { rest.push(s.id) }}}for (id of rest) { first.push(id) }return first}// THE RECORD'S OWN FIELDS from a details answer (tracker#26 completion; tracker#18 the daily sync too — creator: "some shows changed// descriptions and titles and that did not update"): title (a series' `name`, a movie's `title`), TMDB's overview → `tmdbSummary`,// homepage, tagline, status, genres (by TMDB id), adult; type / release / year / language only when we have none. A TMDB value// that is empty never overwrites ours. `summary` is the creator's OWN text — never written here (the page shows summary >// tmdbSummary > tvmazeSummary, shows.hl summaryOf). The slug (`urlSegment`) never changes with the title.// mission 070 (conductor): the sync never DESTROYS what the old tracker's migration brought — before TMDB's value replaces a// migrated record's title / genres / homepage / tagline for the FIRST time, the old one is kept in `migratedTitle` /// `migratedGenres` / `migratedHomepage` / `migratedTagline` (kept, shown nowhere; set once, a later rename leaves it). A record is// migrated when its `oldId` is the old tracker's (not `tmdb-…`: search import, filmography fill). `status`, counts, ids and the// poster are TMDB's state, written by the sync since #9 — not kept.static isMigratedRecord = (&show) => { return show.oldId != null && ('' + show.oldId).slice(0, 5) != 'tmdb-' }static sameIdList = (&a, &b) => {x = listOr(a)y = listOr(b)if (x.length != y.length) { return false }let i = 0while (i < x.length) { if ('' + x[i] != '' + y[i]) { return false } i = i + 1 }return true}static withDetailsFields = (show, &d, &movie) => {x = showmigrated = isMigratedRecord(show)adult = adultOf(d)if (adult != null) { x.adult = adult }let title = textOr(movie ? d.title : d.name)if (title != null) {if (migrated && x.migratedTitle == null && textOr(x.title) != null && x.title != title) { x.migratedTitle = x.title }x.title = title}if (textOr(d.overview) != null) { x.tmdbSummary = d.overview }if (textOr(d.homepage) != null) {if (migrated && x.migratedHomepage == null && textOr(x.homepage) != null && x.homepage != d.homepage) { x.migratedHomepage = x.homepage }x.homepage = d.homepage}if (textOr(d.tagline) != null) {if (migrated && x.migratedTagline == null && textOr(x.tagline) != null && x.tagline != d.tagline) { x.migratedTagline = x.tagline }x.tagline = d.tagline}if (textOr(d.status) != null) { x.status = d.status }tmdbGenreIds = []for (g of listOr(d.genres)) { if (g != null && g.id != null) { tmdbGenreIds.push(g.id) } }genres = genreIdsOfTmdb(tmdbGenreIds)if (genres.length > 0) {if (migrated && x.migratedGenres == null && listOr(x.genres).length > 0 && !sameIdList(x.genres, genres)) { x.migratedGenres = x.genres }x.genres = genres}if (textOr(x.type) == null) { x.type = movie ? 'movie' : 'series' }release = textOr(movie ? d.release_date : d.first_air_date)if (textOr(x.release) == null && release != null) {x.release = releaseif (release.length >= 4) { x.year = toNumber(release.slice(0, 4)) }}if (textOr(x.language) == null && textOr(d.original_language) != null) { x.language = d.original_language }// tracker#21 (merge, mission 070): TMDB's TV type + the kind from the genres just stored (the delta's light step and the// completion of #26 store it too, not only syncShow / the kind backfill)if (!movie && textOr(d.type) != null) {x.tmdbType = d.typex.kind = kindOfTitle(d.type, x.genres)}return x}// THE DAILY SYNC (jobs.hl syncTick, tools/sync-tmdb.hl; tracker#18): after sync.hl `syncShow` (seasons, episodes, poster,// ids, TVmaze) the record's own fields (withDetailsFields) and its cast + crew (#28) from the SAME details answer (`res.details`)// — no extra request. Answers { renamed (the title changed: the search re-indexes it), cast, crew, people, ms, error }static applySynced = (&showId, &d) => {out = { renamed = false cast = 0 crew = 0 people = 0 ms = 0 error = '' }t0 = now()show = showsTable.fetch(showId)if (show == null || d == null) { return out }movie = show.type == 'movie'before = show.titlelet x = withDetailsFields(show, d, movie)out.renamed = x.title != beforeif (x.adult != true && hasCredits(d, movie)) {w = withCredits(x, d, movie)x = w.showout.cast = w.castout.crew = w.crewout.people = w.people}if (showsTable.update(x.id, x) == null) { out.error = x.title + ': record not stored: ' + showsTable.lastError() }out.ms = now() - t0return out}// 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// request at a time, every page waited): step 1 = the details request (fields + cast); then one step per PART of its seasons// (≤ `partEpisodes` episodes, ≤ 20 seasons = one TMDB request — sync.hl syncShowPart); the last step = ids, TVmaze, poster,// `detailsAt`. A movie is one step. Between two steps every other request is served.// show id → { d (TMDB's answer of step 1), parts ([[season numbers]]), pos, sum } while a series is under way — in memory: a// restart begins again with the details request; what is stored by then is matched by tmdbId, nothing is duplicatedstatic jobs = {}// (TRACKER_DETAILS_PART_EPISODES: the gate sets 1 → one season per step)static partEpisodes = env('TRACKER_DETAILS_PART_EPISODES') != null ? toNumber(env('TRACKER_DETAILS_PART_EPISODES')) : 300static 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 = '' } }static addSum = (sum, &r, &ms) => {o = sumo.steps = o.steps + 1o.requests = o.requests + r.requestso.tvmazeRequests = o.tvmazeRequests + r.tvmazeRequestso.newSeasons = o.newSeasons + r.newSeasons + r.tvmazeSeasonso.newEpisodes = o.newEpisodes + r.newEpisodes + r.tvmazeEpisodeso.posters = o.posters + r.posterso.ids = o.ids + r.idso.ms = o.ms + msif (r.error != '') { o.error = r.error }return o}// TMDB's season list → parts of ≤ partEpisodes episodes (a bigger season is a part of its own), ≤ 20 seasons each; which of// them are really synced (season 0, empty ones) syncShowPart decidesstatic seasonParts = (&d) => {parts = []let cur = []let n = 0for (x of listOr(d.seasons)) {if (x != null && x.season_number != null) {let c = x.episode_count != null ? x.episode_count : 0if (cur.length > 0 && (n + c > partEpisodes || cur.length == 20)) {parts.push(cur)cur = []n = 0}cur.push(x.season_number)n = n + c}}if (cur.length > 0) { parts.push(cur) }return parts}// a step's answer: { skipped (nothing to do), done, requests / tvmazeRequests (THIS step's — what the pause counts), adult,// error ('' = none), retry (a passing failure: no answer, 429, 5xx), sum (the whole title's counts so far: steps, requests,// cast, newSeasons, newEpisodes, posters, ids, ms) }static stepOut = () => { return { skipped = false done = false requests = 0 tvmazeRequests = 0 adult = null error = '' retry = false sum = emptySum() } }// the last step: the title is completestatic markComplete = (&showId, &sum) => {done = showsTable.fetch(showId)done.detailsAt = now()if (sum.error != '') { done.detailsError = sum.error }ok = showsTable.update(done.id, done) != nullrefreshCatalogShow(done.id)searchTitleChanged(done.id)return ok}// step 1: the details request — the record's fields, the cast; a movie also its ids + poster (done); a series' parts are plannedstatic firstStep = (show) => {out = stepOut()t0 = now()movie = show.type == 'movie'out.requests = 1d = tmdbGet((movie ? '/movie/' : '/tv/') + show.tmdbId + '?append_to_response=external_ids,' + (movie ? 'credits' : 'aggregate_credits'))if (d.failed != null) {out.error = show.title + ': ' + d.failedif (d.status == 404) {show.detailsCheck = 'TMDB 404'showsTable.update(show.id, show)out.done = true} else { out.retry = true }return out}out.adult = adultOf(d)let x = withDetailsFields(show, d, movie)// mission 025: a movie has no season/episode counts (hasTvLeftovers)if (movie) { x.seasonsCount = null x.episodesCount = null }if (out.adult == true) {// never public: the flag, no poster, no seasons, no cast (mission 056)x.detailsCheck = 'adult'showsTable.update(x.id, x)refreshCatalogShow(x.id)searchTitleChanged(x.id)out.done = truereturn out}// tracker#28: the whole cast + the crew, from this same answerw = withCredits(x, d, movie)x = w.showif (showsTable.update(x.id, x) == null) {out.error = show.title + ': not updated: ' + showsTable.lastError()out.retry = truereturn out}sum = emptySum()sum.cast = w.castsum.crew = w.crewsum.people = w.peopleif (movie) {// one request was enough: ids + poster now (syncMovie with this answer)r = syncShowWith(x.id, d)out.requests = r.requestsout.sum = addSum(sum, r, now() - t0)if (r.error != '') { out.error = r.error }markComplete(x.id, out.sum)out.done = truereturn out}sum.steps = 1sum.requests = 1sum.ms = now() - t0// the next steps need the details, not the credits (a long series' aggregate_credits are ~1 MB, copied on every step)dd = ddd.aggregate_credits = nulljobs[x.id] = { d = dd parts = seasonParts(d) pos = 0 sum = sum }out.sum = sumreturn out}// ONE step of completing this title (by our id) — called until `done` (the page's face, the repair job)static completeStep = (&showId) => {show = showsTable.fetch(showId)if (!needsDetails(show)) {jobs[showId] = nulllet out = stepOut()out.skipped = trueout.done = truereturn out}job = jobs[showId]if (job == null) { return firstStep(show) }t0 = now()let out = stepOut()last = job.pos >= job.parts.lengthr = syncShowPart(showId, job.d, { seasons = last ? [] : job.parts[job.pos] finish = last })out.requests = r.requestsout.tvmazeRequests = r.tvmazeRequestsif (r.error != '') { out.error = r.error }j = jobj.sum = addSum(j.sum, r, now() - t0)j.pos = j.pos + 1out.sum = j.sumif (!last) {jobs[showId] = jreturn out}jobs[showId] = nullmarkComplete(showId, j.sum)out.done = truereturn out}// the line every completed title logsstatic completeLine = (&title, &sum) => {return '"' + title + '" steps=' + sum.steps + ' requests=' + sum.requests + ' tvmazeRequests=' + sum.tvmazeRequests + ' cast=' + sum.cast + ' newSeasons=' + sum.newSeasons+ ' newEpisodes=' + sum.newEpisodes + ' posters=' + sum.posters + ' ids=' + sum.ids + ' ms=' + sum.ms + ' crew=' + sum.crew + ' people=' + sum.people + (sum.error != '' ? ' error: ' + sum.error : '')}// flush shows/seasons/episodes + people (new cast members)static persistDetails = () => {persistSync()personsTable.persist()return true}
Branches
- mainmain branch
Latest commits
- 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
- dc1d4be4tracker mission 027: Hybriel master 06617221 vendored (plugin allocator fixes 3a781359 + 413f60e4); real copy RSS through first-start jobs + 400 loads flat ~2.55 GB (190aa11d 2.3 -> 5.6 GB), page times <= 1.1x; gates 342/0, 32/0, 52/0mre
- 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