gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit54070a4e54070a4etracker#8: /my/unwatched + /my/schedule (301 from old), S01E01, title (year), 1 episode, watched-set lookup (unwatched 15s -> 1s)mre54070a4e/shows.hl

5.2 KB

  1. // shows.hl — READ-ONLY data access for a show's page (tracker.worldapi.org#4): shows, seasons,
  2. // episodes, persons, genres (storage/mpackdb/, migrated by tools/migrate.hl, tracker.worldapi.org#2).
  3. // Kept apart from watches.hl (the per-user watch state) and from components/show.hl (the page itself)
  4. // so /unwatched and /schedule (tickets #5/#6, "reuse, don't copy") can import the same functions.
  5. //
  6. // No declared index beyond the migration's own ['!oldId'] (opening a table with indexes it wasn't
  7. // created with is untested territory, hybriel#21 "opening a table rewrites its files") — a slug
  8. // lookup is a linear scan of ~9500 shows, cheap for one page load.
  9. import { MPackDB } from 'hl:mpackdb'
  10. import { env } from 'hl:proc'
  11. import { exists } from 'hl:fs'
  12. static envOr = (name, fallback) => {
  13. let v = env(name)
  14. return v != null && v.trim() != '' ? v.trim() : fallback
  15. }
  16. static storageDir = envOr('TRACKER_STORAGE', './storage/mpackdb')
  17. static genresTable = new MPackDB(file = storageDir + '/genres.db', primaryKey = '@id', indexes = ['!oldId'])
  18. static personsTable = new MPackDB(file = storageDir + '/persons.db', primaryKey = '@id', indexes = ['!oldId'])
  19. static showsTable = new MPackDB(file = storageDir + '/shows.db', primaryKey = '@id', indexes = ['!oldId'])
  20. static seasonsTable = new MPackDB(file = storageDir + '/seasons.db', primaryKey = '@id', indexes = ['!oldId'])
  21. static episodesTable = new MPackDB(file = storageDir + '/episodes.db', primaryKey = '@id', indexes = ['!oldId'])
  22. // the show with this urlSegment (slug), or null
  23. static showBySlug = (slug) => {
  24. if (slug == null || hlTypeName(slug) != 'String' || slug == '') { return null }
  25. for (s of showsTable.find(null, null)) { if (s.urlSegment == slug) { return s } }
  26. return null
  27. }
  28. static genreById = (id) => { return id == null ? null : genresTable.fetch(id) }
  29. static personById = (id) => { return id == null ? null : personsTable.fetch(id) }
  30. static seasonById = (id) => { return id == null ? null : seasonsTable.fetch(id) }
  31. static showById = (id) => { return id == null ? null : showsTable.fetch(id) }
  32. static episodeById = (id) => { return id == null ? null : episodesTable.fetch(id) }
  33. // insertion sort, hand-written (no list sort(), hybriel#1) — a show has a handful of seasons at most
  34. static insertDesc = (sorted, s) => {
  35. let out = []
  36. let inserted = false
  37. for (x of sorted) {
  38. if (!inserted && s.seasonNumber >= x.seasonNumber) { out.push(s) inserted = true }
  39. out.push(x)
  40. }
  41. if (!inserted) { out.push(s) }
  42. return out
  43. }
  44. static insertAsc = (sorted, e) => {
  45. let out = []
  46. let inserted = false
  47. for (x of sorted) {
  48. if (!inserted && e.episodeNumber <= x.episodeNumber) { out.push(e) inserted = true }
  49. out.push(x)
  50. }
  51. if (!inserted) { out.push(e) }
  52. return out
  53. }
  54. // a show's seasons, latest (highest seasonNumber) first — creator: "all seasons in reverse order,
  55. // latest on top"
  56. static seasonsOfShow = (show) => {
  57. let sorted = []
  58. for (id of show.seasons) {
  59. let s = seasonsTable.fetch(id)
  60. if (s != null) { sorted = insertDesc(sorted, s) }
  61. }
  62. return sorted
  63. }
  64. // a season's episodes, in episode-number order
  65. static episodesOfSeason = (season) => {
  66. let sorted = []
  67. for (id of season.episodes) {
  68. let e = episodesTable.fetch(id)
  69. if (e != null) { sorted = insertAsc(sorted, e) }
  70. }
  71. return sorted
  72. }
  73. // ---- labels shared by every list (tracker.worldapi.org#8) -------------------------------------
  74. static pad2 = (n) => {
  75. let s = '' + n
  76. while (s.length < 2) { s = '0' + s }
  77. return s
  78. }
  79. // an episode as `S01E01` (creator: "write it like that: S01E01") — /my/shows, /my/unwatched, /my/schedule
  80. static episodeCode = (seasonNumber, episodeNumber) => { return 'S' + pad2(seasonNumber) + 'E' + pad2(episodeNumber) }
  81. // the show's year: `year`, else the first 4 characters of `release` ('YYYY-MM-DD'; 197 migrated shows have
  82. // no year, a few of those an empty release) — null when neither gives one
  83. static showYear = (show) => {
  84. if (show.year != null) { return '' + show.year }
  85. if (show.release != null && hlTypeName(show.release) == 'String' && show.release.length >= 4) { return show.release.slice(0, 4) }
  86. return null
  87. }
  88. // a show's title as every list shows it: `Doctor Who (2005)` (creator: so same-named shows can be told apart)
  89. static titleWithYear = (show) => {
  90. let y = showYear(show)
  91. return y != null ? show.title + ' (' + y + ')' : show.title
  92. }
  93. // the poster file name (oldId + the migrated extension) — null when either is missing; served at
  94. // /posters/<name> (project.hl), which falls back to a placeholder when the file itself isn't there
  95. // yet (the real poster files are not part of the mongo export, see the report's `open`)
  96. static posterName = (show) => {
  97. if (show.oldId == null || show.image == null || show.image == '') { return null }
  98. return show.oldId + '.' + show.image
  99. }
  100. // the poster's URL for a LIST (/my/shows, tracker.worldapi.org#8): `/posters/<name>` only when the file really
  101. // is in storage/mpackdb/posters/ (project.hl posterRoute reads the same folder), else the ONE placeholder URL
  102. // `/posters/none` — the real data has no poster files yet, and 128 different URLs all answering the same
  103. // placeholder were 128 requests per page load (the browser can't cache them as one)
  104. static posterUrlOf = (show) => {
  105. let name = posterName(show)
  106. return name != null && exists(storageDir + '/posters/' + name) ? '/posters/' + name : '/posters/none'
  107. }

Branches

Latest commits

  • 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