Airwave

Filters

The filter builder — conditions, AND/OR nesting, how fields resolve for movies vs TV, and previewing the resolved pool.

The Content & filter section decides the pool. First pick the content types with the Movies and TV Shows checkboxes (at least one is required); then build a predicate in the filter builder (apps/web/src/features/channels/filter-builder.tsx).

Conditions

A condition is field · operator · value. The available fields, their kinds, and which operators each kind allows come from the server catalog (packages/api/src/services/plex/filter-fields.ts, exposed via channels.filterFields). Field kinds and their operators (OPS_FOR_KIND):

KindOperatorsNotes
tagis / is notValue picked from a dropdown of real library values (genre, studio, network, actor, collection, content rating, resolution, …)
textcontains / does not contain / is exactly / is not exactly / begins with / ends withTitle, episode title. contains (the default) is a substring match; is exactly is an exact whole-title match
intis / ≥ / ≤Year, decade, ratings, duration (minutes), play count, …
date≥ / ≤Release / air date, last watched — a date picker
recencyis"Added within N days"
boolistrue/false — unwatched, in progress, HDR, Dolby Vision, …

Matching titles: substring vs exact

Text fields (Title, Episode title) support both loose and precise matching:

  • contains is a substring match (the default) — Title contains "Star" matches Star Wars, A Star is Born, and everything else with "Star" in the title. Best for franchises and broad themes.
  • begins with / ends with anchor the substring to the start or end of the title.
  • is exactly is a full-title match — it matches only the title you type, nothing more.

One thing to watch with is exactly: your media server often stores the year as part of the title (a show may be catalogued as Bluey (2018), not Bluey). Exact match compares the whole stored title, so is exactly "Bluey" returns nothing there, while is exactly "Bluey (2018)" matches. When in doubt, use contains or begins with, and check the Preview to confirm the pool is right.

For a tag field the value box becomes a dropdown populated live from your libraries — the channels.filterValues endpoint unions the distinct tag titles across the enabled libraries of the selected content types. Tag values are matched by title, then resolved to the per-library key at query time (each library keys its tags differently), so you pick "Comedy" and Airwave finds the right key in each library.

AND / OR and nesting

The builder is a recursive predicate tree. Each group combines its children with all (AND) or any (OR); a group can hold conditions and nested sub-groups, so you can express Genre is Comedy AND (Year ≥ 1990 OR Studio is HBO). The resolver (packages/api/src/services/plex/resolve.ts) evaluates this with set algebra: it runs each leaf as a simple Plex query and combines results in code — intersect for AND, union for OR. That's why arbitrary nesting works even though the media server only understands simple operators. An all-conditions AND group is optimized into a single query (the fast path).

UI nesting cap. The resolver handles any depth, but the builder only offers the Add group button at the root (filter-builder.tsx), so through the UI you get one level of sub-groups. That's enough for the vast majority of channels; deeper trees are only reachable via import or the API.

Movies vs TV — where a field applies

Movies resolve at the movie level; TV resolves at the episode level. Airwave uses the media server's dotted advanced-filter syntax so a single query mixes both levels: show.genre, episode.resolution, and so on (filter-fields.ts tvScope, applied in buildParam). The practical consequences:

  • Genre lives on the show, not the episode. genre resolves as show.genre; there is no such thing as an episode genre. Filtering TV by genre filters by the parent series' genre.
  • Some fields are episode-level: resolution, audio/subtitle language, release/air date, added-within, HDR/DoVi, unwatched/in-progress. On TV these describe the individual episode.
  • A field can be scoped to one library type (appliesTo). "Network" is show-only; "Duration (min)" is movie-only; "Episode title" / "Episode year" are TV-only. A field that doesn't apply to a given library type is simply skipped for it.

Other definition kinds

The data model reserves other ways to define a pool — PLEX_COLLECTION, PLEX_PLAYLIST, and MANUAL_ITEMS (explicit include/exclude of specific items) — see ChannelDefinitionKind in packages/db/prisma/schema/channel.prisma. The current admin builder and resolver implement the PREDICATE (filter) path only; collection/playlist/manual definitions are scaffolding for later and aren't yet selectable in the UI. (Note: the strategy "Filtered set" scope described under Strategies is a grouping concept, not a Plex collection.)

Previewing the pool

On the channel page, the Preview card shows what the filter currently resolves to — shows with their episode counts coalesced up, movies passed through (channels.previewresolveChannel). Use Refresh preview after editing the filter to re-resolve against the server.

On this page