Files
OrbitTv/docs/metadata-and-matching.md
pauljdandClaude Opus 5.5 80db5495e6
pauljd/davidson-ci: Davidson CI / quality (push) Failing after 1m46s
pauljd/davidson-ci: Davidson CI / ai-review (push) Failing after 1m26s
Give channel tuning more time and one retry (0.23.0)
Vortex failed to tune when the provider took over 15 seconds to answer.
Allows 45 seconds, retries once, and records the health check results.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-01 22:12:42 +00:00

11 KiB

Orbit TV metadata and matching

Approved direction, 1 November 2026. This is a design: none of it is built yet except the coverage check described in the roadmap.

Metadata and tagging strategy

Orbit keeps only a thin record per title today, so the plan is to widen it and keep Orbit's own tags in a separate layer. The Emby data itself is never edited.

What the code keeps now

Record Fields held Used for
SampleItem ID, title, type, minutes, format, duration Everything scheduled
AfterDarkMovie Item, year, rating, up to 8 genres Film review for 102
SeriesCandidate ID, title, year, rating, variant label Series review for 103
EpisodeCandidate ID, title, season, episode, duration, media label Episode order
ScheduledMedia Item, locked source ID, locked duration The timeline

What to add

Need Field Status
Better identification Original title, stable external IDs, collections Not requested today
Classification Genres on all types, tags, studios, overview Genres on films only
Era and order Year, original air date, season, episode Partly held
Suitability Content rating Held on films and series, not used for any filtering
Curation Cast, director Not requested today

Emby's documentation has to confirm each field's request name, and the coverage check in the roadmap shows how often the provider fills them.

Orbit's own tag layer

Each tag records its value, where it came from and how sure Orbit is. Sources rank in this order: user-confirmed, curated title rule, provider metadata, then inferred. Only user-confirmed tags count as settled, and an unknown value stays unknown.

Example Format and genre tags Primary preset Possible crossover
The X-Files Scripted, science fiction, supernatural mystery, cult, 1990s Paradox Orbit Sci-Fi
CSI Scripted, crime, procedural, forensic Evidence None
Blue Planet Documentary, wildlife, ocean, natural history Terra Odyssey
The 'Burbs Film, horror comedy, cult, 1980s Orbit VHS After Dark
Dragon Ball Z Animated series, action anime, fantasy Vortex None

Documentary versus scripted, and traditional versus CGI, need explicit values including "unknown". Ring X adds promotion, show type, event date and PPV status.

Three limits in today's code

  • Saved data cap: saved setup is capped at 512 KiB in one encrypted file, too small for a real library. The catalogue needs its own encrypted local store, chosen after the audit gives real record sizes.
  • Hardcoded channels: saved state has fixed slots for one, toonami and afterDark, and only accepts 101 to 103. Adding channel 26 needs a generic channel list with a tested migration.
  • Listed isn't playable: a catalogue entry only proves a title is listed. Keep separate states for listed, source checked and failed, and recheck when tuning, as Orbit does now.

Library scale

The checks on Paul's account reported 98,441 films, 34,103 series and 1,053,303 episodes on 1 November 2026. At 200 titles a page, films and series take about 663 requests. Episodes would take over 5,000, which is why they aren't imported up front. The design has to hold at that size:

  • Storage: an indexed on-device database, not the saved setup file. Android's built-in SQLite needs no new dependency.
  • Import: paged in the background, resumable, throttled, and stopped by a rate-limit reply. Films and series first.
  • Episodes: fetched per series only when a series is approved or needed, not imported up front.
  • Refresh: changes since the last import, never a full re-read.
  • Matching: filter with provider queries or the local index, so no screen or schedule step scans the whole library in memory.
  • Review screens: show a short ranked list, with paging for the rest.
  • Playback checks: only for titles going onto a channel, never for the whole library.

Measured on 1 November 2026: the full import finished at 132,545 titles (98,441 films, 34,104 series) in a 33.9 MB database, about 261 bytes per title. The second half, 67,145 titles, took 1,588 seconds with a slowest page of 57 seconds. Page size never dropped below 200, so the longer timeout was enough; a 15-second limit would have failed on those pages.

Duplicates and smart selection

Paul's point, 1 November 2026: the library holds several copies of many titles (remux, 4K, 1080p and so on), and a channel that matches 15,000 films shouldn't schedule all of them. How many duplicates exist isn't known yet.

  • One title, many versions: group versions by external ID first, then by normalised title and year. Uncertain matches stay separate and go to review, so different films aren't merged by mistake. Remakes share a title but not a year or ID.
  • Choosing a version: pick the best copy this box can play directly, checked only when it is going on a channel. Keep the others as fallbacks if a source fails. A viewer preference for quality or file size can come later.
  • Pool and schedule: matching builds a channel's pool, which can be any size. The schedule draws a short rolling window from it, such as the next few days, and rotates through the pool over time.
  • Selection: weigh fit, era and variety, avoid recent repeats, and spread series and franchises out. Rules scale to pool size, since three films can't follow the same rules as 15,000.
  • Staying on the clock: selection must be repeatable from a saved seed and the pool, so restarting the app rejoins the same programme. A window already published doesn't change when the library does.
  • Across channels: a title can sit in several pools, but the same film shouldn't air on two channels at once unless that's intended.

Health check results

Measured on the box, 1 November 2026: 132,545 titles, 127,091 different by external ID, 125,643 by title and year. Likely extra copies by ID are 5,454, about 4%, and 1,258 titles have no ID. Copies that Emby already merges as versions of one item wouldn't appear here, so remux and 4K copies may be hiding inside single entries.

Without new indexes, a one-off genre-and-decade filter took 255 to 500 ms and a full pass 42 seconds. That's fine for occasional review and matching runs, so link tables aren't urgent. They'd matter if filters run on every screen.

Genres are usable as they stand: Drama 50,541, Comedy 35,196, Documentary 21,821, Thriller 19,783, Action 17,424, Romance 17,257, Crime 15,401, Horror 12,777. Tags are mostly provider housekeeping (foreign, foreignautopath, new, favail, archive), so don't treat tags as programme descriptions without filtering. Curated lists and genres have to carry matching.

Broadcast, not playlist

Paul's direction, 1 November 2026: at this scale Orbit must stay smart and feel like a TV broadcast, not a running playlist.

  • Indexing: filter quickly by channel tag, genre, decade and series. Today's database only indexes type and title, and genres and tags sit in one text field, so a separate indexed table for them is needed.
  • Series grouping: a series is what gets recommended or placed on a channel. Episodes stay attached to it and are fetched when it's needed, so a million episodes never compete as separate choices.
  • Programming: schedules are built from series and films in sensible order and slots, with next-episode progress for each series, rather than shuffled single items.

What the metadata supports

Early samples show genres and external IDs on nearly every film and series (3,998 and 3,997 of the first 4,000 imported), so stable-ID identification and genre matching look reliable. Studios and cast are usable but not complete, and episodes carry little of their own, so they should inherit from their series. Coverage across all 132,545 titles: year 132,111, external IDs 131,287, date 129,182, genres 129,119, rating 86,863, tags 58,224. So about a third of titles have no rating, which matters for profiles and mature channels: an unrated title can't be treated as safe.

Recommendation engine design

The engine should behave like a TV programmer who proposes and waits: it suggests from rules it can explain, and nothing reaches a channel until the viewer accepts it. It runs on the box, with no AI or cloud service.

Pipeline

Each stage builds on what Orbit has, and the existing clock schedule stays as the last step.

Stage What happens What it reuses or needs
A. Discovery Paged read of what this account can see EmbyClient, with new paging and rate-limit handling
B. Normalise Map provider data to one internal record, keeping provider IDs separate Existing validation and text limits
C. Identify Match by stable ID first, then title and year. Handle remakes, alternate titles and duplicates. labelSeriesVariants already flags duplicate entries
D. Tag Add Orbit tags with source and confidence New, from the tag layer above
E. Match Rank titles against each preset New
F. Review Show a short list with ticks and reasons Existing review screens as the pattern
G. Schedule Feed confirmed titles to the clock engine SampleChannel, with new limits
H. Repeats Rotation and cooldowns scaled to library size New

How matching decides

Titles are ranked in tiers, because exact weights shouldn't be picked before the audit shows real data.

  1. User decisions: explicit picks and exclusions always win.
  2. Hard filters: rating, playability and low-confidence identity can remove a title, whatever else matches.
  3. Curated title match: the preset's own list and verified aliases.
  4. Strong signals: format or subgenre, such as documentary or police procedural.
  5. Medium signals: genre, era and stated preference.
  6. Context signals: cast, studio or franchise, where they make sense for that preset.
  7. Weak signals: keyword similarity can add a candidate but never justify one alone.

Every suggestion stores the reasons behind it, so "why recommended" reads directly from them. A three-film channel can't follow the repeat rules of a large one, and Orbit shouldn't create empty schedules to satisfy a rule.

What the viewer sees

A suggestion is short and gives its reason: "Paradox: 8 matching series found. These match your interest in cult science fiction and supernatural mysteries."

  • Controls: add, remove, replace, ignore and view why.
  • Missing titles: shown unticked and marked unavailable, never implied as playable.
  • Dismissals: remembered, so a dismissed idea doesn't keep returning without a new reason.
  • Discovery: familiar favourites, balanced discovery or surprise me. This is a possible later preference.
  • Remote use: a few items per screen, since the viewer is using a D-pad from the sofa.

Privacy and control

Orbit learns only from what the viewer chooses or enables: favourite genres and decades, accepted and rejected suggestions, explicit exclusions, and optional local watch history. Personalisation can be turned off, its history cleared, and it never adds channels or titles by itself after setup. There are no engagement goals.

OrbitOffice uses the same matching for events such as an Arnie Triple Bill or a Wrestling PPV Night. Events are optional, never scheduled automatically, name missing films honestly and can be switched off entirely.