gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commitfbb903ccfbb903cctracker#33/#37 (mission 032): a title without a TMDB poster gets its backdrop (w780, posterFromBackdrop, shown 2:3 centre-cropped); movies store runtime, the page shows release date + runtime; art backfill (public titles without poster file / movies without runtime) and person photo backfill (tmdbProfile / photoCheck) as the last start jobs (TRACKER_ART, TRACKER_PHOTOS; off in every gate start); gates 379/0, 32/0, 53/0, 229/0, 26/0, check-theme 0; live copy: art 4977 titles in 42 min (115 posters, 15 backdrops, 4609 runtimes), The Remaining shows its backdrop + 7 minmrefbb903cc/lib/backfill.hl

11.6 KB

  1. // lib/backfill.hl — THE BACKFILLS of a title's flags (adult, kind; tracker#35 TVmaze's status), one title per step (run by jobs.hl `backfillTick` / `kindTick`, on the
  2. // app's clock like the daily sync). Mission 028: out of tmdbsync.hl.
  3. //
  4. // ADULT FLAG (mission 054): every details answer carries TMDB's `adult` — the sync, the import (search.hl) and the
  5. // BACKFILL below store it as show.adult (true/false; null = not known yet, hidden from the public lists, shows.hl
  6. // isPublicTitle). THE BACKFILL (`backfillTitle`, run one title per step by jobs.hl `backfillTick`, like the daily sync):
  7. // GET <TMDB_BASE_URL>/movie|tv/<tmdbId>?append_to_response=external_ids ONE request: adult, poster_path, imdb/tvdb id
  8. // GET <TMDB_IMAGE_URL>/w342<poster_path> the poster when its file is missing (not for
  9. // an adult title — never shown, mission 056)
  10. // for every title whose adult flag is unknown (no TVmaze, no seasons — the daily sync does those for followed shows). A
  11. // title TMDB answers 404 for (or one without tmdbId) gets `adultCheck` = why, stays unknown (hidden) and is not asked again.
  12. //
  13. // KIND (tracker.worldapi.org#21, mission 068): a TV details answer carries TMDB's `type` (Scripted, Miniseries, Documentary,
  14. // Reality, Talk Show, News, Video) — the sync, the adult backfill and the import (search.hl) store it as show.tmdbType and the
  15. // title's kind ('series' | 'show', shows.hl kindOfTitle) as show.kind. THE KIND BACKFILL (`kindTitle`, run one title per step by
  16. // jobs.hl `kindTick`, after the adult backfill, on the same clock):
  17. // a title with a show genre (Reality / Talk / News) kind = 'show' from the stored data, NO request
  18. // any other TV title without `kind` GET <TMDB_BASE_URL>/tv/<tmdbId> ONE request → tmdbType + kind
  19. // A 404 (or no tmdbId) stores the genre's answer ('series') with `kindCheck` = why; it is not asked again. Resumes after a
  20. // restart (the queue = every TV title still without `kind`).
  21. //
  22. // TVMAZE STATUS (tracker#35, mission 030): the sync stores TVmaze's status (Running / Ended / To Be Determined / In Development)
  23. // as show.tvmazeStatus whenever it merges TVmaze's episodes (sync.hl, followed series and every completion). THE STATUS BACKFILL
  24. // (`tvmazeStatusTitle`, one series per step by jobs.hl `statusTick`, after the details repair, part of that job):
  25. // every series with a TVmaze id and no tvmazeStatus GET <TVMAZE_BASE_URL>/shows/<tvmzId> ONE TVmaze request, 550 ms pause
  26. // A 404 (or no status in the answer) stores `tvmazeStatusCheck` = why; it is not asked again. Resumes after a restart.
  27. //
  28. // ART + RUNTIME (tracker#33, mission 032, The Remaining: TMDB has no poster, only a backdrop): THE ART BACKFILL (`artTitle`, one
  29. // title per step by jobs.hl `artTick`, one of the last start jobs):
  30. // every PUBLIC title without a poster file, every public movie without `runtime` GET <TMDB_BASE_URL>/movie|tv/<tmdbId>
  31. // ONE request → the poster (else the backdrop, tmdb.hl syncPoster) + a movie's runtime; `artAt` = asked (never again), a 404
  32. // → `artCheck`. A passing failure (no answer, 429, 5xx) is asked again later. Resumes after a restart.
  33. import { now } from 'hl:time'
  34. import { showsTable, kindOfTitle, hasShowGenre, isPublicTitle, posterUrlOf } from './shows.hl'
  35. import { textOr, hasId } from './util.hl'
  36. import { tmdbGet, externalIdsFor, syncPoster, adultOf, tvmazeShow } from './tmdb.hl'
  37. // ---- the adult backfill (mission 054) ------------------------------------------------------------
  38. // a title still to ask: its adult flag unknown, it has a tmdbId and TMDB never answered "no such title" for it
  39. static needsAdultFlag = (&show) => { return show.adult != true && show.adult != false && show.adultCheck == null && show.tmdbId != null && show.mergedInto == null }
  40. // the ids of every title the backfill still has to ask, in table order (read once when the backfill starts — so after a
  41. // restart it goes on with what is left)
  42. static adultBackfillQueue = () => {
  43. out = []
  44. for (s of showsTable.find(null, null)) { if (needsAdultFlag(s)) { out.push(s.id) } }
  45. return out
  46. }
  47. // ONE title: one TMDB details request (adult, poster_path, external ids) + its poster when the file is missing. Answers
  48. // { requests (TMDB API requests: what the pause counts), posters, adult (true / false / null), ids, errors, error, retry
  49. // (a passing failure: no answer, 429, 5xx — ask again later) }. A 404 marks the title `adultCheck` (never asked again).
  50. static backfillTitle = (&showId) => {
  51. out = { requests = 0 posters = 0 adult = null ids = 0 errors = 0 error = '' retry = false }
  52. show = showsTable.fetch(showId)
  53. if (show == null || !needsAdultFlag(show)) { return out }
  54. movie = show.type == 'movie'
  55. out.requests = 1
  56. details = tmdbGet((movie ? '/movie/' : '/tv/') + show.tmdbId + '?append_to_response=external_ids')
  57. if (details.failed != null) {
  58. out.errors = 1
  59. out.error = show.title + ': ' + details.failed
  60. if (details.status == 404) {
  61. show.adultCheck = 'TMDB 404'
  62. showsTable.update(show.id, show)
  63. } else { out.retry = true }
  64. return out
  65. }
  66. ids = externalIdsFor(show, details.external_ids, !movie, false)
  67. out.ids = ids.filled
  68. out.adult = adultOf(details)
  69. // an ADULT title's poster is never shown publicly → not downloaded (mission 056: ~170 MB of the live data); its flag and
  70. // ids are stored as for any other title
  71. let poster = { saved = 0 failed = null image = show.image tmdbPoster = show.tmdbPoster }
  72. if (out.adult != true) { poster = syncPoster(show, details.poster_path, details.backdrop_path) }
  73. out.posters = poster.saved
  74. if (poster.failed != null) { out.errors = out.errors + 1 out.error = show.title + ': ' + poster.failed }
  75. if (out.adult == null) { show.adultCheck = 'TMDB gave no adult flag' } else { show.adult = out.adult }
  76. if (!movie && textOr(details.type) != null) { show.tmdbType = details.type show.kind = kindOfTitle(details.type, show.genres) }
  77. show.imdbId = ids.imdbId
  78. show.tvdbId = ids.tvdbId
  79. show.image = poster.image
  80. show.tmdbPoster = poster.tmdbPoster
  81. show.posterFromBackdrop = poster.fromBackdrop
  82. if (showsTable.update(show.id, show) == null) { out.errors = out.errors + 1 out.error = show.title + ': not updated: ' + showsTable.lastError() }
  83. return out
  84. }
  85. // ---- the kind backfill (tracker.worldapi.org#21, mission 068) ----------------------------------------------------
  86. // a TV title whose kind is not stored yet
  87. static needsKind = (&show) => { return show.type == 'series' && show.kind != 'series' && show.kind != 'show' && show.mergedInto == null }
  88. // the ids of every TV title still without a kind, in table order (read once when the backfill starts — it resumes)
  89. static kindBackfillQueue = () => {
  90. out = []
  91. for (s of showsTable.find(null, null)) { if (needsKind(s)) { out.push(s.id) } }
  92. return out
  93. }
  94. // ONE title → { requests (TMDB API requests: what the pause counts), kind ('series' | 'show' | null), fromGenre, errors, error,
  95. // retry (a passing failure: no answer, 429, 5xx) }
  96. static kindTitle = (&showId) => {
  97. out = { requests = 0 kind = null fromGenre = false errors = 0 error = '' retry = false }
  98. show = showsTable.fetch(showId)
  99. if (show == null || !needsKind(show)) { return out }
  100. if (hasShowGenre(show.genres) || show.tmdbId == null) {
  101. out.fromGenre = true
  102. show.kind = kindOfTitle(null, show.genres)
  103. if (!hasShowGenre(show.genres)) { show.kindCheck = 'no tmdbId' }
  104. } else {
  105. out.requests = 1
  106. details = tmdbGet('/tv/' + show.tmdbId)
  107. if (details.failed != null) {
  108. out.errors = 1
  109. out.error = show.title + ': ' + details.failed
  110. if (details.status != 404) { out.retry = true return out }
  111. show.kindCheck = 'TMDB 404'
  112. show.kind = kindOfTitle(null, show.genres)
  113. } else {
  114. if (textOr(details.type) != null) { show.tmdbType = details.type } else { show.kindCheck = 'TMDB gave no type' }
  115. show.kind = kindOfTitle(textOr(details.type), show.genres)
  116. }
  117. }
  118. out.kind = show.kind
  119. if (showsTable.update(show.id, show) == null) { out.errors = out.errors + 1 out.error = show.title + ': not updated: ' + showsTable.lastError() }
  120. return out
  121. }
  122. // ---- the TVmaze status backfill (tracker#35, mission 030) -------------------------------------------------------------
  123. // a live, non-adult series with a TVmaze id whose TVmaze status is not stored and was not given up on
  124. static needsTvmazeStatus = (&show) => {
  125. return show.type == 'series' && hasId(show.tvmzId) && show.tvmazeStatus == null && show.tvmazeStatusCheck == null && show.adult != true && show.mergedInto == null
  126. }
  127. // the ids of every such series, in table order (read once when the backfill starts — it resumes)
  128. static tvmazeStatusQueue = () => {
  129. out = []
  130. for (s of showsTable.find(null, null)) { if (needsTvmazeStatus(s)) { out.push(s.id) } }
  131. return out
  132. }
  133. // ONE series → { tvmazeRequests (what the pause counts), status (or null), errors, error, retry (a passing failure) }
  134. static tvmazeStatusTitle = (&showId) => {
  135. out = { requests = 0 tvmazeRequests = 0 status = null errors = 0 error = '' retry = false }
  136. show = showsTable.fetch(showId)
  137. if (show == null || !needsTvmazeStatus(show)) { return out }
  138. out.tvmazeRequests = 1
  139. got = tvmazeShow(show.tvmzId)
  140. if (got.failed != null) {
  141. out.errors = 1
  142. out.error = show.title + ': ' + got.failed
  143. if (got.notFound != true) { out.retry = true return out }
  144. show.tvmazeStatusCheck = 'TVmaze 404'
  145. } else if (got.status == null) { show.tvmazeStatusCheck = 'TVmaze gave no status' } else {
  146. show.tvmazeStatus = got.status
  147. out.status = got.status
  148. }
  149. if (showsTable.update(show.id, show) == null) { out.errors = out.errors + 1 out.error = show.title + ': not updated: ' + showsTable.lastError() }
  150. return out
  151. }
  152. // ---- the art backfill (tracker#33, mission 032) ---------------------------------------------------------------------------
  153. // a public title we never asked for its art: no poster file, or a movie without runtime
  154. static needsArt = (&show) => {
  155. if (show.artAt != null || show.artCheck != null || show.tmdbId == null || !isPublicTitle(show)) { return false }
  156. if (show.type == 'movie' && show.runtime == null) { return true }
  157. return (show.type == 'movie' || show.type == 'series') && posterUrlOf(show) == '/posters/none'
  158. }
  159. static artQueue = () => {
  160. out = []
  161. for (s of showsTable.iterate(null, null)) { if (needsArt(s)) { out.push(s.id) } }
  162. return out
  163. }
  164. // ONE title → { requests (TMDB requests: what the pause counts), posters, backdrops, runtime (stored or null), errors, error, retry }
  165. static artTitle = (&showId) => {
  166. out = { requests = 0 posters = 0 backdrops = 0 runtime = null errors = 0 error = '' retry = false }
  167. show = showsTable.fetch(showId)
  168. if (show == null || !needsArt(show)) { return out }
  169. movie = show.type == 'movie'
  170. out.requests = 1
  171. details = tmdbGet((movie ? '/movie/' : '/tv/') + show.tmdbId)
  172. if (details.failed != null) {
  173. out.errors = 1
  174. out.error = show.title + ': ' + details.failed
  175. if (details.status != 404) { out.retry = true return out }
  176. show.artCheck = 'TMDB 404'
  177. showsTable.update(show.id, show)
  178. return out
  179. }
  180. poster = syncPoster(show, details.poster_path, details.backdrop_path)
  181. out.requests = out.requests + poster.requests
  182. if (poster.saved > 0) { if (poster.fromBackdrop) { out.backdrops = 1 } else { out.posters = 1 } }
  183. if (poster.failed != null) { out.errors = out.errors + 1 out.error = show.title + ': ' + poster.failed }
  184. show.image = poster.image
  185. show.tmdbPoster = poster.tmdbPoster
  186. show.posterFromBackdrop = poster.fromBackdrop
  187. if (movie && details.runtime != null && hlTypeName(details.runtime) == 'Number' && details.runtime > 0) {
  188. show.runtime = details.runtime
  189. out.runtime = details.runtime
  190. }
  191. show.artAt = now()
  192. if (showsTable.update(show.id, show) == null) { out.errors = out.errors + 1 out.error = show.title + ': not updated: ' + showsTable.lastError() }
  193. return out
  194. }

Branches

Latest commits

  • fbb903cctracker#33/#37 (mission 032): a title without a TMDB poster gets its backdrop (w780, posterFromBackdrop, shown 2:3 centre-cropped); movies store runtime, the page shows release date + runtime; art backfill (public titles without poster file / movies without runtime) and person photo backfill (tmdbProfile / photoCheck) as the last start jobs (TRACKER_ART, TRACKER_PHOTOS; off in every gate start); gates 379/0, 32/0, 53/0, 229/0, 26/0, check-theme 0; live copy: art 4977 titles in 42 min (115 posters, 15 backdrops, 4609 runtimes), The Remaining shows its backdrop + 7 minmre
  • 4ecc67b2tracker: STATUS/LOG for the t38 + t40 merge (gates 374/0, 32/0, 53/0, 229/0, 26/0, check-theme 0; live copy checks)mre
  • e5945d2fMerge t40 (tracker#40 curated franchises, Franchises menu, superseded collections) into main: deploy.sh lists all six gates (browser, kinds, franchises, pager, franchiseseed, check-theme); search.hl keeps #37's personPhotoOf + #40's importWithCredits; pager gate runs with TRACKER_FRANCHISE_SEED=0; gates 374/0, 32/0, 53/0, 229/0, 26/0, check-theme 0mre
  • b638e99dMerge t38 (tracker#38 pagination, #35 Returning/Airing label) into main: LOG/STATUS keep both sides; pager gate follows #37's /people (everyone, last updated first: seed updatedAt); gates pager 229/0, browser 374/0mre
  • f8ffa4dbtracker: report 032 + The Remaining + #39mre
  • 7565a863tracker: LOG timemre
  • b10f00c8tracker#39: double episodes — migrated episodes whose TMDB id TMDB replaced are adopted by their number in the sync (old id -> migratedTmdbId); merge.hl step 3 merges each season's doubles at start (keeper: most watches > synced > first; watches moved/parked; tombstones into mergedEpisodes, nothing deleted); tools/count-duplicate-episodes.hl; gate fixture + paths-m039; live copy 850 -> 0 in 64 s; gates 373/0, 32/0, 52/0mre
  • 8f1d4542tracker#40: superseded collections — a TMDB collection timeline whose titles are all in one curated timeline is hidden (supersededBy; kept: own page + editor finder), set by the collection seed when it makes one and by the curated build (lifted when the cover is gone); partly covered ones join that franchise; no second widget (First Contact: only Star Trek — Prime); gate franchiseseed 26/0, browser 365/0, kinds 32/0, franchises 53/0, check-theme 0; real copy 24 supersededmre
  • 9448d643tracker#35 follow-up (mission 033): TVmaze 'Running' is labelled 'Returning', or 'Airing' while a non-special episode of the two newest seasons is released within today +-7 days (data unchanged); gate fixtures Running/Airing/Aired + a special; browser 366/0, kinds 32/0, franchises 52/0, pager 229/0, check-theme 0mre
  • 7d4b293dtracker#40: "Franchises" in the main menu (desktop header after People, phone sidebar) → /franchises, marked on franchise and timeline pages; gates 365/0, 32/0, 53/0, 24/0, check-theme 0mre
  • 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
  • 93dfb0batracker#40 (mission 034): the curated franchises — data/franchises.json (17 franchises, 31 timelines, 285 TMDB titles, movies + series, in-universe/release order, 12 TMDB collections attached); lib/franchiseseed.hl + jobs.hl franchiseSeedTick (last start job, imports missing titles via details.hl importWithCredits = the search's Add, one per step paced, then one build; franchiseseed.db: editor changes win, the creator's same-name franchise adopted / timeline left alone, 404 remembered, resumable, idempotent); timeline heads 'N titles · in-universe order' (orderKind) and wrap on a phone; series pages show the widget; new gate tests/franchiseseed.mjs (5th in deploy.sh), the others run with TRACKER_FRANCHISE_SEED=0; gates 365/0, 32/0, 52/0, 24/0, check-theme 0; real copy 196 imported, 0 failed, 7 min, restart unchanged=31mre
  • 03ec792ftracker#38 (mission 033): pagination goes exactly to the clicked page — tilelist read the clicked button's text after pagination.hl's own handler had rebuilt the buttons (real clicks only); now li.current, else the button's own text; new gate tests/pager.mjs (5 lists x 11 pages, 390/1280, real + script clicks, Back/Forward) 229/0; browser 365/0, kinds 32/0, franchises 52/0, check-theme 0mre
  • 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