tracker
All repositories: gitoria
9.8 KB
// deltasync.hl — WHICH TITLES THE DAILY SYNC ASKS FOR (tracker.worldapi.org#18). Creator: "it checks every show in a request to// the api? cause they have an endpoint that gives a delta on bulk". Before: 2 requests per FOLLOWED show every day (256 for 126// shows), the other titles never. Now the daily run (project.hl syncTick, tools/sync-tmdb.hl --delta) first asks what changed// since the last run and syncs ONLY those titles of ours — followed or not://// GET <TMDB_BASE_URL>/tv/changes?start_date=<last run day>&end_date=<today>&page=<n> changed TV ids, 100 per page// GET <TMDB_BASE_URL>/movie/changes?start_date=…&end_date=…&page=<n> changed movie ids, 100 per page// GET <TVMAZE_BASE_URL>/updates/shows?since=day|week|month { "<tvmaze id>": <updated at> }//// → intersected with our titles (search.hl's index: TMDB id by kind, TVmaze id — no table scan) → the sync's queue, FOLLOWED// titles first. One change-list request per clock step (project.hl), 260 ms per TMDB / 550 ms per TVmaze request between them.// A FOLLOWED title gets the full step (all seasons, TVmaze — as before #18); one NOBODY follows the light step (tmdbsync.hl// syncShowDelta: details, ids, poster, record, cast + only the seasons that can have changed, no TVmaze) — so TVmaze's list// only brings followed series.// Left out: titles without a TMDB id, incomplete ones (details.hl isIncomplete — the details repair completes them) and ADULT// ones nobody follows (never public, their posters are not kept — mission 056).//// THE LAST RUN: `<storage>/sync-state.txt` = the day ('YYYY-MM-DD', UTC) the last finished run STARTED (its change window began// at 00:00 of that day, so two runs overlap by a few hours, never leave a gap). FULL WALK (every followed show, as before #18)// when there is no such day (the first run), when it is more than `maxDeltaDays` (14: TMDB's change lists reach 14 days back)// ago, or when a change list cannot be read (then the day is NOT advanced: the next run asks for the missed days too).import { fetch } from 'hl:fetch'import { now } from 'hl:time'import { readFile, writeFile, exists } from 'hl:fs'import { showsTable, storageDir } from './shows.hl'import { tmdbGet, tvmazeBase, textOr, numIdOr, dayNumber } from './tmdbsync.hl'import { isIncomplete } from './details.hl'import { allFollowedShowIds } from './follows.hl'import { titleIdByTmdb, titleIdByTvmaze } from './search.hl'static maxDeltaDays = 14// TMDB answers at most 500 pages of a liststatic maxPages = 500static stateFile = storageDir + '/sync-state.txt'// the day of the last finished run, or nullstatic lastSyncDay = () => {if (!exists(stateFile)) { return null }let t = readFile(stateFile)if (t == null || hlTypeName(t) != 'String') { return null }let d = t.trim()return dayNumber(d) != null ? d.slice(0, 10) : null}static markSynced = (day) => {writeFile(stateFile, day, 420)return true}// ---- the plan of one run (one run at a time: the app's clock or the tool) --------------------------------------------------// flat statics written key by key (like people.hl's maps); `run.no` tells this run's entries from older onesstatic run = { no = 0 mode = '' why = '' since = '' until = '' tvmazeSince = 'day' phase = 'done' page = 1 pages = 1requests = 0 tvmazeRequests = 0 tvChanged = 0 movieChanged = 0 tvmazeChanged = 0 count = 0 followed = 0 failed = '' }static followedRun = {}static pickedRun = {}static pickedAt = {}static followedList = { ids = [] }// a run starts: { mode ('delta' | 'full'), why, since, days }. 'delta' → call planStep() until it answers done, then planIds().// `sinceDay` (the tool's --since) stands in for the stored last day; null = the stored onestatic planStart = (today, sinceDay) => {run.no = run.no + 1run.since = ''run.until = todayrun.phase = 'done'run.page = 1run.pages = 1run.requests = 0run.tvmazeRequests = 0run.tvChanged = 0run.movieChanged = 0run.tvmazeChanged = 0run.count = 0run.followed = 0run.failed = ''let last = sinceDay != null ? sinceDay : lastSyncDay()let days = last != null && dayNumber(today) != null ? dayNumber(today) - dayNumber(last) : nullif (last == null) {run.mode = 'full'run.why = 'no last run'} else if (days == null || days > maxDeltaDays || days < 0) {run.mode = 'full'run.why = 'last run ' + last + ', ' + days + ' days ago > ' + maxDeltaDays} else {run.mode = 'delta'run.why = 'since ' + last + ' (' + days + (days == 1 ? ' day' : ' days') + ')'run.since = lastrun.tvmazeSince = days <= 1 ? 'day' : (days <= 7 ? 'week' : 'month')run.phase = 'tv'let ids = allFollowedShowIds()for (id of ids) { followedRun[id] = run.no }followedList.ids = ids}return { mode = run.mode why = run.why since = run.since days = days }}// is this title followed by anyone (as of this run's start)? — such a title gets the FULL step (all seasons + TVmaze, as// before #18), one nobody follows the LIGHT one (tmdbsync.hl syncShowDelta: changed seasons, no TVmaze)static isFollowedInRun = (showId) => { return followedRun[showId] == run.no }// one of ours that changed → into the queue once, when the daily sync may touch it. `viaTvmaze`: found by TVmaze's list — only// a followed series counts there (TVmaze is merged for the followed ones only)static pick = (showId, viaTvmaze) => {if (showId == null || pickedRun[showId] == run.no) { return false }let s = showsTable.fetch(showId)let followed = followedRun[showId] == run.noif (s == null || s.tmdbId == null || isIncomplete(s) || (s.adult == true && !followed) || (viaTvmaze && !followed)) { return false }pickedRun[showId] = run.nopickedAt['' + run.count] = showIdrun.count = run.count + 1if (followed) { run.followed = run.followed + 1 }return true}// ONE change-list request (TMDB tv page, TMDB movie page or TVmaze's list): { done, requests, tvmazeRequests, failed ('' = none) }static planStep = () => {let out = { done = false requests = 0 tvmazeRequests = 0 failed = '' }if (run.phase == 'tv' || run.phase == 'movie') {let kind = run.phaseout.requests = 1run.requests = run.requests + 1let j = tmdbGet('/' + kind + '/changes?start_date=' + run.since + '&end_date=' + run.until + '&page=' + run.page)if (j.failed != null || j.results == null) {run.failed = j.failed != null ? j.failed : 'no results in /' + kind + '/changes'run.phase = 'done'out.failed = run.failedout.done = truereturn out}let type = kind == 'tv' ? 'series' : 'movie'for (c of j.results) {if (c != null && numIdOr(c.id) != null) {if (kind == 'tv') { run.tvChanged = run.tvChanged + 1 } else { run.movieChanged = run.movieChanged + 1 }let sid = titleIdByTmdb(type, c.id)if (sid != null) { pick(sid, false) }}}run.pages = j.total_pages != null && hlTypeName(j.total_pages) == 'Number' ? j.total_pages : 1if (run.pages > maxPages) { run.pages = maxPages }if (run.page < run.pages) { run.page = run.page + 1 } else {run.phase = kind == 'tv' ? 'movie' : 'tvmaze'run.page = 1run.pages = 1}return out}if (run.phase == 'tvmaze') {out.tvmazeRequests = 1run.tvmazeRequests = run.tvmazeRequests + 1run.phase = 'done'let f = fetch(tvmazeBase + '/updates/shows?since=' + run.tvmazeSince, { headers = { 'accept' = 'application/json' 'user-agent' = 'tracker.worldapi.org (tmdb sync)' } timeoutMs = 30000 })let j = f != null && f.status == 200 ? f.json() : nullif (j == null) {run.failed = 'TVmaze: ' + (f == null || f.status == null || f.status == 0 ? 'no answer' : 'HTTP ' + f.status) + ' for /updates/shows?since=' + run.tvmazeSinceout.failed = run.failedout.done = truereturn out}for (k of j.keys()) {run.tvmazeChanged = run.tvmazeChanged + 1let sid = titleIdByTvmaze(k)if (sid != null) { pick(sid, true) }}}out.done = truereturn out}// the queue: the changed titles of ours, the followed ones first (in follow-table order), then the rest (in change-list order)static planIds = () => {let out = []for (id of followedList.ids) { if (pickedRun[id] == run.no) { out.push(id) } }let i = 0while (i < run.count) {let id = pickedAt['' + i]if (followedRun[id] != run.no) { out.push(id) }i = i + 1}return out}// the log line of a finished planstatic planLine = () => {return 'changes ' + run.why + ': tmdb tv=' + run.tvChanged + ' movie=' + run.movieChanged + ' tvmaze(' + run.tvmazeSince + ')=' + run.tvmazeChanged+ ' → ' + run.count + ' of our titles (' + run.followed + ' followed), changeRequests=' + run.requests + ' tvmazeChangeRequests=' + run.tvmazeRequests+ (run.failed != '' ? ' FAILED: ' + run.failed : '')}// ---- the copied summaries (tracker#18, one time) ---------------------------------------------------------------------------// The migration copied TMDB's text into `summary` too (9,648 of 10,011 titles on the live copy, 2026-10-01): such a `summary` is// no text of the creator's — it is cleared, so the CURRENT tmdbSummary shows (shows.hl summaryOf). A `summary` that differs is// the creator's own and stays. ONCE: `<storage>/summaries-cleared.txt` records that it ran (and the counts). → { ran, cleared, kept }static clearedFile = storageDir + '/summaries-cleared.txt'static clearCopiedSummaries = () => {let out = { ran = false cleared = 0 kept = 0 errors = 0 ms = 0 }if (exists(clearedFile)) { return out }out.ran = truelet t0 = now()for (s of showsTable.find(null, null)) {if (textOr(s.summary) != null) {if (s.summary == s.tmdbSummary) {let x = sx.summary = nullif (showsTable.update(x.id, x) == null) { out.errors = out.errors + 1 } else { out.cleared = out.cleared + 1 }} else { out.kept = out.kept + 1 }}}showsTable.persist()out.ms = now() - t0writeFile(clearedFile, 'cleared=' + out.cleared + ' kept=' + out.kept + ' errors=' + out.errors, 420)return out}
Branches
- mainmain branch
Latest commits
- f83571c3tracker#18 (mission 067): daily sync by change lists — TMDB /tv|movie/changes (since the stored day, paged) + TVmaze /updates/shows → only our changed titles (followed: full step, unfollowed: light step — changed seasons, no TVmaze), full walk on first run / gap > 14 days / failed list; show record refreshed (title, tmdbSummary, tagline, status, genres …; renamed titles re-indexed); summary = the creator's own text (page: summary > tmdbSummary > tvmazeSummary), one-time clear of copied summaries (9,647 on the live copy); gate 261, tests/realdata-018*.mjs, README + STATUSmre
- 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