tracker
All repositories: gitoria
5.2 KB
// shows.hl — READ-ONLY data access for a show's page (tracker.worldapi.org#4): shows, seasons,// episodes, persons, genres (storage/mpackdb/, migrated by tools/migrate.hl, tracker.worldapi.org#2).// Kept apart from watches.hl (the per-user watch state) and from components/show.hl (the page itself)// so /unwatched and /schedule (tickets #5/#6, "reuse, don't copy") can import the same functions.//// No declared index beyond the migration's own ['!oldId'] (opening a table with indexes it wasn't// created with is untested territory, hybriel#21 "opening a table rewrites its files") — a slug// lookup is a linear scan of ~9500 shows, cheap for one page load.import { MPackDB } from 'hl:mpackdb'import { env } from 'hl:proc'import { exists } from 'hl:fs'static envOr = (name, fallback) => {let v = env(name)return v != null && v.trim() != '' ? v.trim() : fallback}static storageDir = envOr('TRACKER_STORAGE', './storage/mpackdb')static genresTable = new MPackDB(file = storageDir + '/genres.db', primaryKey = '@id', indexes = ['!oldId'])static personsTable = new MPackDB(file = storageDir + '/persons.db', primaryKey = '@id', indexes = ['!oldId'])static showsTable = new MPackDB(file = storageDir + '/shows.db', primaryKey = '@id', indexes = ['!oldId'])static seasonsTable = new MPackDB(file = storageDir + '/seasons.db', primaryKey = '@id', indexes = ['!oldId'])static episodesTable = new MPackDB(file = storageDir + '/episodes.db', primaryKey = '@id', indexes = ['!oldId'])// the show with this urlSegment (slug), or nullstatic showBySlug = (slug) => {if (slug == null || hlTypeName(slug) != 'String' || slug == '') { return null }for (s of showsTable.find(null, null)) { if (s.urlSegment == slug) { return s } }return null}static genreById = (id) => { return id == null ? null : genresTable.fetch(id) }static personById = (id) => { return id == null ? null : personsTable.fetch(id) }static seasonById = (id) => { return id == null ? null : seasonsTable.fetch(id) }static showById = (id) => { return id == null ? null : showsTable.fetch(id) }static episodeById = (id) => { return id == null ? null : episodesTable.fetch(id) }// insertion sort, hand-written (no list sort(), hybriel#1) — a show has a handful of seasons at moststatic insertDesc = (sorted, s) => {let out = []let inserted = falsefor (x of sorted) {if (!inserted && s.seasonNumber >= x.seasonNumber) { out.push(s) inserted = true }out.push(x)}if (!inserted) { out.push(s) }return out}static insertAsc = (sorted, e) => {let out = []let inserted = falsefor (x of sorted) {if (!inserted && e.episodeNumber <= x.episodeNumber) { out.push(e) inserted = true }out.push(x)}if (!inserted) { out.push(e) }return out}// a show's seasons, latest (highest seasonNumber) first — creator: "all seasons in reverse order,// latest on top"static seasonsOfShow = (show) => {let sorted = []for (id of show.seasons) {let s = seasonsTable.fetch(id)if (s != null) { sorted = insertDesc(sorted, s) }}return sorted}// a season's episodes, in episode-number orderstatic episodesOfSeason = (season) => {let sorted = []for (id of season.episodes) {let e = episodesTable.fetch(id)if (e != null) { sorted = insertAsc(sorted, e) }}return sorted}// ---- labels shared by every list (tracker.worldapi.org#8) -------------------------------------static pad2 = (n) => {let s = '' + nwhile (s.length < 2) { s = '0' + s }return s}// an episode as `S01E01` (creator: "write it like that: S01E01") — /my/shows, /my/unwatched, /my/schedulestatic episodeCode = (seasonNumber, episodeNumber) => { return 'S' + pad2(seasonNumber) + 'E' + pad2(episodeNumber) }// the show's year: `year`, else the first 4 characters of `release` ('YYYY-MM-DD'; 197 migrated shows have// no year, a few of those an empty release) — null when neither gives onestatic showYear = (show) => {if (show.year != null) { return '' + show.year }if (show.release != null && hlTypeName(show.release) == 'String' && show.release.length >= 4) { return show.release.slice(0, 4) }return null}// a show's title as every list shows it: `Doctor Who (2005)` (creator: so same-named shows can be told apart)static titleWithYear = (show) => {let y = showYear(show)return y != null ? show.title + ' (' + y + ')' : show.title}// the poster file name (oldId + the migrated extension) — null when either is missing; served at// /posters/<name> (project.hl), which falls back to a placeholder when the file itself isn't there// yet (the real poster files are not part of the mongo export, see the report's `open`)static posterName = (show) => {if (show.oldId == null || show.image == null || show.image == '') { return null }return show.oldId + '.' + show.image}// the poster's URL for a LIST (/my/shows, tracker.worldapi.org#8): `/posters/<name>` only when the file really// is in storage/mpackdb/posters/ (project.hl posterRoute reads the same folder), else the ONE placeholder URL// `/posters/none` — the real data has no poster files yet, and 128 different URLs all answering the same// placeholder were 128 requests per page load (the browser can't cache them as one)static posterUrlOf = (show) => {let name = posterName(show)return name != null && exists(storageDir + '/posters/' + name) ? '/posters/' + name : '/posters/none'}
Branches
- mainmain branch
Latest commits
- 31b758aatracker#10: installable app (manifest, service worker, offline shell), own icon + faviconmre
- 2fa9d997tracker#9: TMDB sync (followed shows: seasons, episodes, posters), tools/sync-tmdb.hl + daily run 04:00 UTC, fake TMDB in gatemre
- 49e1f61edeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- 54070a4etracker#8: /my/unwatched + /my/schedule (301 from old), S01E01, title (year), 1 episode, watched-set lookup (unwatched 15s -> 1s)mre
- 3251488atracker#7: /my/shows (followed shows, newest follow first, poster, title, last watched SxxEyy); gate can take screenshots (TRACKER_GATE_SHOTS)mre
- 44b7d9f9tracker#6: /schedule — upcoming episodes of followed shows, soonest firstmre
- 91c9fc8ctracker#5: /unwatched — unwatched released episodes of followed shows, newest firstmre
- a97c0295tracker#4: show page /shows/:slug (header, seasons, episodes, watch checks) + tools/relink-episode-seasons.hlmre
- 05f407c5tracker: no border on any button except inverted ones (Log out, ident status and identities too); header brand weight 100mre
- 17375427tracker#2: tools/migrate.hl + tools/verify.hl — old MongoDB data into mpackdb with new idsmre
- 31aac936tracker#3: no border on the header and on filled buttons; inverted buttons keep theirsmre
- 2ad9d29ctracker#1: login exactly like calendar (identity selector in the header, empty homepage)mre
- 3691e176tracker#1: empty tracker with the ident login (state of 2026-09-27)mre