Skip to content

Universal Parameterized Collection System

In this page

Understand how structural shelves and rule-driven Collections resolve owned media. The shared catalog machinery supports distinct product experiences; profile bookmarks, reactions, and playback progress remain independent state.

Where this lives in the code

  • src/MediaEngine.Storage/CollectionRepository.cs
  • src/MediaEngine.Domain/Contracts/ICollectionRepository.cs
  • src/MediaEngine.Storage/ProfileStateRepository.cs
  • src/MediaEngine.Api/Endpoints

Overview

Tuvima Library uses collection records for structural grouping and for custom containers with explicit or rule-driven membership. Type, ownership, audience, and resolution determine how a container behaves. A shared internal record does not make an album, TV show, personal bookmark, and curated Collection interchangeable product concepts.

A collection is defined by membership or rules, its type, ownership/audience, and supported placements. My List bookmarks, Favorites reactions, and Continue progress use independent profile state. They must never be represented by hidden Collections or Playlists.

Product role note: lane-level shelves and top-level Collections are intentionally different surfaces. A generated book series, movie series, TV show, music album, or audiobook series is a shelf and belongs in its lane: books/comics in Read, movies/TV in Watch, and music/audiobooks in Listen. The Collections page shows broader rollups only when a shared series/franchise/universe relationship connects at least two shelves. A single shelf, even with multiple items, stays out of the Collections surface; ebook/audiobook variants of the same work also do not trigger a rollup.

One Universe record: every Series shelf names the Universe it belongs to (collections.universe_qid, the narrative_roots row), even when it is the only Series there. ParentCollectionResolver decides it with the one UniverseIdentityRule (a fictional universe, then a franchise, then a broader Series, then a work it is based on, then the shelf’s own Series; the lowest QID wins within a type), and the Work-level narrative root uses the same rule, so the Library and the graph agree. A Universe shelf is created once at least two Series shelves name the same record (the book and audiobook Series of one book Series, both carrying the Universe’s own QID, do not count as two on their own), and it presents that record (universe_qid equals its wikidata_qid). The decision is recomputed on every call, so a shelf that moves to another Universe leaves the old one cleanly: a Universe shelf left without children is removed only if nobody edited it.

Shelf Identity, Ordering, And Counts

Lane shelves are immediate containers. A comic shelf is a Comic Vine volume/run, a music shelf is the accepted album/release, TV uses show/season placement, movies use true film collections when available, and books/audiobooks use an ordered series container when supported by provider or Wikidata evidence.

Broader franchises, universes, publisher lists, production lists, and Wikimedia list articles may support diagnostics or top-level discovery rollups, but they must not be used as the lane shelf itself. This is what prevents two same-name comic shelves with different totals, movie franchise totals replacing trilogy counts, or list articles appearing as real series.

Counts are stored as facts about the accepted container. sequence_total records the expected count, and sequence_total_scope states what the count means: main sequence, extras included, standalone, collected edition, or broader franchise. The UI should show Owned X of Y only for an authoritative finite/known Y from the same immediate container shown to the user. Partial manifest row counts degrade to X owned. Comics never present the provider’s current run count as a completion target; their UI shows issue identity and the number of owned issues while retaining the count internally for matching and diagnostics.

Wikidata manifest rows also carry membership_scope. Incoming P179 and direct P527 works are MainSequence; incoming P361 works are Supplementary; children reached by expanding a P527 collection are CollectedContent; explicitly expanded P8345 franchise members are BroaderContext; ambiguous direct members without ordinal or chain evidence beside a positioned run are Unpositioned. All factual rows remain available, but lane totals use only the main sequence.

Series identity, scope and ordering

These rules are the same for every reader (the shelf, the item page, the Series page and the native apps), so they cannot disagree.

Identity. A Series is its Wikidata Series QID plus its media type (collections.media_type). A book Series and an audiobook Series with the same QID are two Series; the Expanse novels and the Expanse TV show are two Series. ICollectionRepository.FindSeriesShelfAsync(qid, mediaType) only ever returns a ContentGroup of that media type, never a Universe or a Series of another media type. Only when there is no QID does a Series fall back to a provider key (tmdb:collection:{id}, tvdb:tv:{id}, comicvine:volume:{id}) or the local local:{type}:{label} key.

Placement. One pure rule, SeriesPlacementRule (Domain), says which Series a work belongs to and with what scope. CollectionAssignmentService and SeriesManifestRepository.LinkOwnedWorksAsync both call it, so running either any number of times, in any order relative to manifest hydration, leaves a work on the same shelf. Precedence:

  1. A hydrated Wikidata Series manifest that lists the work (any scope) decides the Series, with the scope the manifest gives it.
  2. Otherwise a parent-collection manifest that lists the work places it in the Series whose manifest lists that parent, as CollectedContent. A collection is a parent (for example Memory’s Legion) when another Series manifest of the same media type lists its QID as a collection item; this is read from the data every time, never from a flag. The parent collection never becomes a shelf for those works, and manifest linking moves them to the listing Series without waiting for a later assignment run.
  3. A Series of another media type is never a candidate. If two Series of the work’s own media type both list it in the main sequence, the work is ambiguous: it is left on no Series and one Needs Review item (AmbiguousSeriesMembership, category Metadata Conflicts) names the competing Series; assignment and manifest linking raise the same item. Nothing is guessed. A tie at any other scope is not ambiguous: the Series the work’s own series_qid claim names wins, else the lowest QID.
  4. With no Wikidata manifest, a retail provider’s own manifest, then the retail identity (provider key, then label plus media type), as MainSequence.

A person’s choice is a lock. When a person picks a Series in the editor’s Matching section’s Series override (or “Remove from its Series”), the Engine saves it as works.series_placement_locked = 1 with works.collection_id as their choice. SeriesPlacementInput.Lock comes before every rule above: the rule returns the locked shelf (scope from that shelf’s manifest when it lists the work, else main sequence) or “no Series”, and is never ambiguous. CollectionAssignmentService (and so backfill), LinkOwnedWorksAsync (which only refreshes the work’s position on its chosen Series) and the repository’s assign and clear calls all leave a locked work where the person put it. “Go back to the Library’s choice” clears the lock, takes the work off its shelf and runs placement for it again. The lock is a placement lock, not a claim lock (PriorityCascadeEngine’s lockable fields are unchanged).

One claimed-Series reader. ClaimedSeriesQidReader (Domain) names the Series a work’s own claims point to, for the tie-break above: series_qid from the lineage root’s canonicals, then from the work’s files (read in file-id order); then a QID-shaped series value; then, for comics, wikidata_qid. Assignment and manifest linking both feed it the same values (the root, then every file of the work), so the same work gets the same shelf from either path. If the chosen Series shelf is deleted, a database trigger clears the lock with it.

One “which Series?” question. SeriesAmbiguityReviewer raises the AmbiguousSeriesMembership item on the work’s earliest-stored Normal original file, skips it when a pending one already exists on the work or any of its files, and resolves it whenever the work is later placed without a tie (any path, including a person’s choice). Manifest linking reports every work it decided (including works reached through a parent collection) and the items are closed in one batch. Saving or releasing a choice also runs the Universe (parent-collection) step for the Series the work joined and the one it left.

A work moves only when the rule’s shelf differs from its current one, and the log gives the real reason. A work with no manifest row (retail only) is MainSequence by definition.

Ordering. SeriesOrdering (Domain) sorts a Series: the manifest’s parsed ordinal for main-sequence members when present, then the work’s own decimal position (works.ordinal_sort), then publication date, then title. A manifest ordinal counts only when its ordinal scope is this Series. Decimals are never rounded; the number shown is the source’s own text (the manifest’s raw ordinal, else the retail series_position), so 1.5 shows “1.5”. A manifest row number is never used as a position.

Numbered run and “Also in this world”. Only main-sequence members are numbered and counted in “x of y”. Unpositioned main-sequence members stay in the Series after the numbered run, unnumbered. Supplementary, collected-content and broader-context members are listed apart as Also in this world for the same Series, on the Series page (AlsoInThisWorld), on the item page and in the native items list (each item carries membership_scope and position_label). They are never numbered with the run. The existing missing-items preference applies to both lists.

Domain note: Collection, Work, and Edition expose aggregate children and metadata property bags as read-only views. Code that changes membership or metadata history should use explicit domain methods such as AddWork, AddRelationship, AddEdition, AddMetadataClaim, and AddCanonicalValue. Metadata claims remain append-only.


Collection Rules - The Universal Predicate Model

Every collection stores its membership criteria as a JSON array of predicates. Each predicate is a simple object:

{ "field": "genre", "op": "eq", "value": "Science Fiction" }

A collection’s full rule set might look like:

[
{ "field": "media_type", "op": "eq", "value": "Movie" },
{ "field": "genre", "op": "eq", "value": "Horror" },
{ "field": "vibe", "op": "in", "value": "atmospheric,tense,unsettling" }
]

Supported Operators

OperatorMeaningExample
eqEqualsgenre eq "Horror"
neqNot equalsmedia_type neq "TV"
containsText contains substringtitle contains "Dune"
gtGreater thanuser_rating gt 3
ltLess thanduration lt 480
betweenRange (inclusive)year between "1980,1989"
inAny of (comma-separated)vibe in "atmospheric,cerebral"
likePattern matchauthor like "Herbert%"

Match Mode

Each collection has a match_mode that controls how predicates combine:

  • ALL (default) - item must satisfy every predicate (AND logic)
  • ANY - item must satisfy at least one predicate (OR logic)

Field Vocabulary

Rules reference fields from six categories:

Identity fields

FieldDescriptionExample use
titleWork titletitle contains "Dune"
authorCreator / authorauthor eq "Frank Herbert"
media_typeMedia categorymedia_type eq "Book"
genreGenre taggenre eq "Science Fiction"
seriesSeries nameseries eq "Dune Novels"
universeUniverse nameuniverse eq "Dune"
formatFile format labelformat eq "EPUB"
publisherPublisher namepublisher contains "Penguin"
languageContent languagelanguage eq "en"
narratorAudiobook narratornarrator eq "Steven Pacey"

Wikidata fields

FieldDescriptionExample use
wikidata_qidWikidata Q-identifierwikidata_qid eq "Q190228"
directorDirector (from Wikidata P57)director eq "Denis Villeneuve"
composerComposer (from Wikidata P86)composer eq "Hans Zimmer"

Engagement fields

FieldDescriptionExample use
user_ratingUser star rating (1-5)user_rating gt 3
provider_ratingRetail provider rating (0-5)provider_rating gt 4
play_countTimes played/readplay_count gt 5
completion_statusNot Started / In Progress / Completedcompletion_status eq "Not Started"
Profile stateUse canonical saved-item/reaction filters where supportedDo not infer Favorites from a named System collection

Temporal fields

FieldDescriptionExample use
date_addedWhen added to librarydate_added gt "2026-03-01"
date_publishedPublication/release yeardate_published between "1980,1989"
last_playedLast consumption datelast_played lt "2025-10-01"
date_completedWhen marked completedate_completed gt "2026-01-01"

File fields

FieldDescriptionExample use
file_sizeSize in bytesfile_size lt 524288000
durationLength in minutesduration between "120,360"
qualityQuality label (4K, 1080p, 320kbps)quality eq "4K HDR"
bitrateAudio bitrate in kbpsbitrate gt 256

AI-derived fields

FieldDescriptionExample use
vibeAI-generated vibe tagvibe in "atmospheric,cerebral"
tldrAI summary texttldr contains "space exploration"
taste_matchAI taste match percentagetaste_match gt 80

Collection Types - Presentation Hints

The collection type is a presentation hint - it tells the Dashboard how to render the collection, who owns it, and what the user can do with it. The underlying rule mechanism is the same for all types.

TypeCreated byResolutionScoped toEditablePrimary surface
ContentGroupEngine (during ingestion)MaterializedLibraryRead-onlyLane shelf/detail route
SmartEngine (auto-generated from templates)Query-resolvedLibraryDisable/enable, feature, adjust thresholdNo
SystemInternal/legacy typeImplementation-dependentInternalNot a My List or Favorites backing storeNo
MixEngine + AI (from taste profile)MaterializedUser (per profile)Enable/disableNo
PlaylistUserMaterializedUser (per profile)Full CRUDNo
CustomAuthorized profile or administratorManual or dynamicExplicit owner and audienceSwitch membership mode; edit complete rulesFor Me or published Collections

Hybrid Resolution

Collections resolve their items in one of two ways:

  • Query-resolved - the Engine evaluates the collection’s predicates against the data store at display time. Items are always fresh. Used by Smart collections and Custom collections.
  • Materialized - items are explicitly linked to the collection in the data store. Membership changes when files are ingested (ContentGroup), when an authorized user changes explicit membership, or when the AI refreshes recommendations (Mix).

Query-resolved collections store their predicates in the rule_json column. Materialized collections may also have rules (for display purposes or re-evaluation) but their membership is tracked via the collection_works junction table.


ContentGroup Collections - Finalized During Readiness

ContentGroup collections represent natural groupings that emerge from the media itself: TV shows, music albums, book series, comic volumes, audiobook series, and movie series. They are materialized during the ingestion/readiness path so Read, Watch, and Listen can show lane shelves before every broader Wikidata relationship has been hydrated. Display rules are stricter than storage rules: TV shows may show with one owned episode, while non-TV/non-music series cards require at least two distinct owned works by default and use collection/root artwork before item-level artwork. Book, comic, and movie series cards expose a bounded structural preview of owned works. The Dashboard renders every non-TV series and collection container through the same larger fixed-size landscape card with an artwork-only rest state. Two to four representatives form a slightly angled, overlapping cluster, and hover/focus preserves that exact composition while revealing a lightly shaded compact top overlay for type, title, and one status line. On Watch, the Series shelf is movie-only and explains that the Engine dynamically aligned those movies; TV shows occupy a separate TV Shows shelf. TV shows use the rich media tile: show cover at rest, followed by the show-level cinematic backdrop and identity on hover; owned episodes remain on the show detail surface. Container cards never contain inline actions, rotate artwork, or navigate between children; the whole card opens the container detail surface where browsing and actions belong.

When a file is ingested:

  1. Processors and retail providers extract grouping metadata such as show name, album name, series name, Comic Vine series/volume facts, or TMDB belongs_to_collection.
  2. CollectionFinalizationService runs after quick hydration, or after retained-retail organization when Wikidata cannot resolve a QID.
  3. CollectionAssignmentService asks SeriesPlacementRule which Series the item belongs to (see Series identity, scope and ordering above): hydrated Wikidata manifests first, then the retail identity. The retail identity prefers a Wikidata series_qid when known; otherwise provider-backed keys such as tmdb:collection:{id}, tmdb:tv:{id}, tvdb:tv:{id}, album IDs, or a normalized local series/show/album label are used.
  4. The service checks for an existing ContentGroup by QID and media type (FindSeriesShelfAsync) or provider key (rule_hash) and creates one only when no matching shelf exists.
  5. The Work is linked to the shelf collection inside the per-shelf lock, and only when that is not already its shelf. If a later enrichment pass discovers the QID, the provider-backed shelf is upgraded instead of duplicated.
  6. Parent rollup resolution runs only after shelf assignment and only creates a broader parent when trusted relationship rows connect at least two shelves.

CollectionBackfillService repairs older works that have visible owned media assets but missed shelf assignment. It runs a bounded pass during library reconciliation and is also exposed through POST /collections/reconcile for manual repair.

ContentGroup collections have additional fields:

  • group_by_field - the metadata field that defines the group (e.g. series, album, show)
  • sort_field / sort_direction - default ordering for items (e.g. episode number ascending, track number ascending)

These ContentGroup records power lane shelves in Read, Watch, and Listen, but the user-facing surface follows the media concept rather than the storage type. When Watch groups TV by show, clicking a show opens /details/tvshow/{id}, the unified detail page with seasons and episodes. The Collections page uses broader rollups only when relationships such as series, franchise, or fictional universe connect multiple shelves.

ContentGroup by Media Type

Media typeGroup identitySort defaultExample
TVShow nameSeason -> Episode ascending“Breaking Bad”
MusicAlbum name (+ artist)Track number ascending“OK Computer - Radiohead”
BooksSeries name (+ author)Sequence index ascending“Dune Novels - Frank Herbert”
AudiobooksSeries name (+ author)Sequence index ascending“First Law - Joe Abercrombie”
ComicsVolume nameIssue number ascending“Saga - Brian K. Vaughan”
MoviesFilm series nameRelease year ascending“The Matrix film series”

Smart Collections - Auto-Generated from Library Data

Smart collections are auto-generated from library metadata using templates. One collection is created per qualifying instance (one per genre, one per decade, etc.). Minimum item thresholds prevent clutter from one-off entries.

Template Categories

TemplateRule patternDefault thresholdMedia typesIcon
By Genre: [name]genre eq "{name}"3+ itemsAnyTag
By Vibe: [tag]vibe eq "{tag}"5+ itemsAnyMood/wave
By Author: [name]author eq "{name}"3+ worksBooks, Audiobooks, ComicsPerson
By Director: [name]director eq "{name}"3+ worksMovies, TVPerson
By Narrator: [name]narrator eq "{name}"3+ worksAudiobooksPerson
By Decade: [period]date_published between "{start},{end}"5+ itemsAnyCalendar
Recently Addeddate_added gt "{30_days_ago}"Always shownAnyClock
Highest Ratedprovider_rating gt 4Always shownAnyStar
Unrateduser_rating eq "unrated"Always shownAnyCircle-dashed

Smart collections refresh automatically - their rules are re-evaluated as the library changes.

Entity-backed rules store a Wikidata QID plus a display label. The editor groups factual dimensions under Media, People & Organizations, Story & World, Recognition, Production, Relationships, and My Library. Missing structured metadata is not treated as a negative assertion: neq means “known and different,” while is unknown only matches a field that a completed structured-discovery lookup could not populate. Relationship rules currently cover adaptations and local ownership of their source/adaptation work.

Configuration

  • Generation threshold - adjustable minimum item count per template (default 3 or 5 depending on template)
  • Featured toggle - pin a collection to appear prominently on the relevant media lane page
  • Enabled toggle - disable to hide from browsing without deleting

These are managed on the Collections page (/collections).


Profile lists and state

The former Reading List, Watchlist, Currently Watching, Listening Queue, and name-based System Collection behavior are retired. /for-me?view=my-list reads saved references; /for-me?view=favorites reads reactions. ProfileStateRepository persists those independent states in profile_saved_items and profile_reactions.

Continue comes from playback/reading progress, without requiring list membership. Profile-owned Collections, Playlists, and Galleries appear automatically only in their respective Your shelves. Accessible library Collections and shared Playlists enter My List only when explicitly saved.

Personalised Mixes

The following mix table preserves design examples rather than a list of shipped automatic collections. It must not be used to synthesize hidden Collections for current For Me shelves. Continue is a current progress projection; the other proposed mixes require individual implementation and acceptance evidence.

MixLogicRefresh
ContinueIn-progress items across all listsReal-time
Heavy RotationMost consumed in last 30 daysDaily
Discovery QueueIn your library, matches your taste, untouchedDaily
New For YouRecently added items matching taste profileOn new ingestion
Because You Liked [X]Items similar to a specific highly-rated item (vibe + genre overlap)On rating change
Taste Mix: [cluster]Per taste cluster - your atmospheric sci-fi cluster, your cozy mystery clusterWeekly
On RepeatCurrent obsessions (high consumption recently)Daily
RediscoverThings you loved 6+ months ago but haven’t touched sinceWeekly

Personalised mixes use: genres (from Wikidata/retail), vibe tags (AI-generated), consumption history, user ratings, and the Taste Profile model. See docs/architecture/ai-integration.md for the Genre vs Vibe discovery model.

Configuration

  • Enabled toggle - per mix type, disable to hide for all users
  • No other configuration - the AI handles everything

Playlists & Curated Collections

These are separate products. Playlists are Listen queue/edit surfaces. Custom Collections have an explicit owner and audience and can span media types; profile-owned Collections are private unless supported sharing policy says otherwise, while administrator-managed library Collections may be published to everyone or selected profiles.

Playlists (Materialized)

Traditional playlists where the user hand-picks items. Items are explicitly linked.

  • Create - from the Listen playlists surface
  • Rename / Delete - from within the playlist detail page
  • Add items - from library browsing via the “Add to Playlist…” picker
  • Remove items - from within the playlist detail page
  • Reorder - drag to reorder within the playlist
  • Progress tracking - optional toggle per playlist
  • Artwork - auto-composed from items’ cover art. User can upload a custom override.

Curated Collections (Custom, Query-Resolved or Hand-Picked)

Authorized users create custom Collections with manual membership or dynamic rules. Profile-owned Collections appear in For Me > Your Collections. Only administrators can publish or manage library-wide Collections. Dynamic mode requires at least one complete rule before saving; the editor can switch membership mode in either direction.

  • Create - the authorized New Collection action is available throughout Collections, including Discovery
  • Edit rules - from the collection detail page (opens collection builder)
  • Rename / Delete - from the collection detail page
  • Live update - items auto-add/remove as they match or stop matching rules (toggleable via live_updating flag, default on)
  • Artwork - auto-composed from matched items’ cover art. User can upload a custom override.

Rule-driven curated collections do not support manual add/remove or manual reordering because their rules control membership and sort.


Collection Builder

The collection builder is an Apple Music Smart Playlist-inspired filter interface for creating Custom collections. It lives on the Collections page (/collections) and follows a row-based filter pattern.

Builder Layout

  1. Collection name - text field at top
  2. Match mode - toggle: Match ALL rules / Match ANY rule
  3. Filter rows - each row is: Field dropdown -> Operator dropdown -> Value input. Plus/minus buttons to add/remove rows. A “Make group” button creates nested ALL/ANY logic for complex queries.
  4. Live preview - section below the rules showing how many items currently match and a scrollable list of the first 20 matches. Updates as rules are added or changed. Powered by POST /collections/preview.
  5. Limit and sort - optional item limit (10, 25, 50, 100, 200, or no limit) and sort controls (field + direction)
  6. Live updating toggle - when on, items auto-add/remove as they match or stop matching rules
  7. Save - creates the Custom collection

Field Autocomplete

The value input adapts based on the selected field:

  • Genre, Vibe - multi-select picker populated from GET /collections/field-values/genre
  • Author, Director, Narrator - person picker with search
  • Media Type - media type picker
  • Format, Quality - format/quality picker
  • Series, Universe - text picker with search
  • Dates - date picker or duration selector (last N days/weeks/months)
  • Numbers - numeric input with range support

Example Custom Collections

Collection nameRulesLimit / Sort
“Unread Sci-Fi”Genre is “Science Fiction” + Completion Status is “Not Started”Sort by Provider Rating, descending
“Short Listens”Media Type is “Audiobook” + Duration < 8 hours + User Rating > 3Sort by Duration, ascending
“Atmospheric Horror”Genre is “Horror” + Vibe includes any of “atmospheric, haunting, unsettling”Limit 50, sort by Taste Match
“Denis Villeneuve Marathon”Director is “Denis Villeneuve”Sort by Date Published, ascending
“4K Movie Night”Media Type is “Movie” + Quality is “4K HDR” + Duration between 90-180 minSort by Random, limit 10
“New This Month”Date Added is in the last 30 days + Taste Match > 70%Sort by Taste Match, descending

Save, react, and queue actions

Media and group cards remain one semantic detail link without inline playback, My List, reaction, or remove buttons. Actions live on details and purpose-built playback/queue surfaces.

My List saves a movie, TV show, book, comic, audiobook, album, accessible Collection, or library/shared Playlist as one reference. Songs use Favorite and Add to Playlist. Favorites are separate reactions; saving or favoriting never creates a hidden collection.

Progress Tracking

Progress is profile state for the experienced asset or work. Not Started, In Progress, and Completed are independent of My List and playlist membership. Continue projects in-progress media across lanes and retains the actual episode/track playback identity where applicable.

Partial long-form items may show artwork-edge progress. Albums, songs, and View assets do not show completion bars. Ownership counts on authoritative finite structural containers are separate from playback completion.

Collection Artwork

Collections can use preferred managed artwork or a bounded structural preview. Derived previews are presentation data rather than new canonical artwork assets.

Auto-composed artwork

The shared MediaArtworkGroupPreview renders up to four representative owned images with their natural aspect ratios in a fixed landscape composition. It does not generate or persist a 2x2 collage, rotate children, or add child navigation. Ordered series previews preserve sequence order; curated Collections may use decorative representative stacks.

Auto-generated banner

Standard Collection details use a neutral cinematic wash and the same representative artwork cluster at hero scale. They do not derive a backdrop from a member or generate a composite banner to stand in for canonical artwork.

User override

The shared artwork editor manages supported semantic roles for the selected container. A preferred user override belongs to its entity-artwork link; it does not replace the source image’s identity or turn the derived preview into a stored collage. Do not give an ordinary Collection a person’s Headshot role solely because it contains that person’s works.


Collection Placements - Location as Display Constraint

The collection_placements table maps collections to UI locations. A single collection can appear in multiple locations with different display constraints.

Placement Model

Each placement record contains:

FieldDescription
collection_idWhich collection
locationWhere it appears (e.g. home, media_lane_books, read_lane_books, my_library, collections_page)
display_limitMaximum items to show at this location (e.g. 10 for a home swimlane, null for full detail)
sort_overrideOptional sort that overrides the collection’s default for this location
positionDisplay order within the location

Location Vocabulary

LocationWhat appears thereCollection types shown
homePersonalised dashboardMix, Smart (Recently Added), System (shortcuts)
media_lane_{type}Category browsing by laneSmart (filtered to media type)
my_libraryProfile listsSystem, Playlist
collections_pageCollections section (/collections)Automatic broader rollups and Curated
read_lane_books, watch_lane_video, listen_lane_audiomedia lane browse surfacesContentGroup (filtered to media type)

This model replaces hardcoded rules about where each collection type appears. Adding a new location or changing which collections appear where is a data change, not a code change.


The Collections Section (/collections)

A component-driven section for browsing organization across the whole library. It shares the persistent rail, compact route navigation, filters, tiles, and page-state components used by Read, Watch, and Listen. Collection management stays in this top-level product surface rather than a separate media-fixing workspace.

Page Layout

  • Overview - preview rows for Automatic, Curated, and Shelves, plus role shortcuts into People
  • Automatic - trusted fictional-universe, franchise, based-on/adaptation, and series rollups with search, lane, relationship, sort, and tile-size filters. Broad cross-media identity is evaluated before lane-local parent or series structure, so adaptations such as Batman films and comics can share one rollup when their trusted QID relationship matches.
  • Curated - administrator-authored, library-published collections with search, lane, membership, publication, sort, and tile-size filters
  • Shelves - one cross-lane index of book series, comic volumes, movie series, TV shows, albums, and audiobook series
  • People - a paged list with managed headshots, canonical roles, owned Read/Watch/Listen counts, and search, role, lane, and sort filters

Only administrators see New Collection. Playlists are intentionally absent because their sole browse and management home is Listen.

Collection Detail

Click into any collection to see:

  • Collection artwork (auto-composed or custom)
  • Name, description, type indicator
  • Rules displayed as readable chips (for Smart and Custom types)
  • Items as a grid or list
  • Add/remove/reorder controls (only for System lists and Playlists)
  • Progress indicators (only for lists/playlists with progress tracking)
  • Configuration controls (thresholds for Smart, rules for Custom, placements for all)

The Collections page is accessible from the primary Dashboard navigation alongside Read, Watch, Listen, and Search. The navigation entry uses the collections/grid icon.


Engine Actions

Rule Evaluation

CollectionRuleEvaluator (Storage layer) translates collection predicates into SQL queries against the data store. It:

  1. Parses the JSON predicate array
  2. Maps each field to its data store column (joining across works, editions, canonical values, metadata claims as needed)
  3. Applies the match mode (AND/OR)
  4. Applies sort and limit constraints
  5. Returns matching Work IDs

For performance, Smart collections cache a rule_hash - the hash of the serialized rule JSON. When rules haven’t changed, cached results can be served. The live_updating flag controls whether materialized collections re-evaluate on library changes.

Available Actions

ActionMethodPurpose
Collections surface catalogGET /collections/catalogServer-classified catalog for /collections: system/user/managed collections plus broader multi-shelf rollups
Collection summaryGET /collections/{id}/summaryOne catalog summary for a detail page without loading the full catalog
Collection itemsGET /collections/{id}/itemsDetail-page items, including generated rollup aggregation
Repair shelf assignmentsPOST /collections/reconcileDry-run or run collection backfill for already-ingested media
Resolve collection itemsGET /collections/resolve/{id}Evaluate rules, return matching items
Preview rulesPOST /collections/previewEvaluate rules without saving - powers the live preview in the collection builder
Collections at locationGET /collections/by-location/{location}All collections placed at a UI location, with display limits applied
Field valuesGET /collections/field-values/{field}Distinct values for a field - powers autocomplete in the collection builder
Create collectionPOST /collectionsCreate a profile playlist or an administrator-only, library-published Curated (Custom) collection
Update collectionPUT /collections/{id}Update collection properties or rules
Delete collectionDELETE /collections/{id}Delete an editable Playlist or Curated (Custom) collection
Get placementsGET /collections/{id}/placementsWhere a collection appears
Set placementsPUT /collections/{id}/placementsUpdate collection placements

Platform Inspiration

The universal collection model draws from patterns proven by major media platforms:

PlatformPatternHow it maps to collections
Netflix“Because you watched X”, category rows, “My List”Mix (Because You Liked), Smart (genre rows), Independent My List saved references
SpotifyDaily Mixes, Discover Weekly, user playlists, album pagesMix (Taste Mix), Mix (Discovery Queue), Playlist, ContentGroup (albums)
Apple MusicSmart Playlists with field+operator+value rulesCustom collections via collection builder - direct inspiration for the filter UI
PlexAuto-generated collections by genre/decade/director, user collectionsSmart collections (auto-generated templates), Custom collections

Data Store Changes

New columns on the collections table:

ColumnTypePurpose
resolutionTEXTquery or materialized
rule_jsonTEXTJSON array of predicates
rule_hashTEXTSHA-256 of rule_json for cache invalidation
group_by_fieldTEXTField that defines ContentGroup identity
match_modeTEXTall or any
sort_fieldTEXTDefault sort field
sort_directionTEXTasc or desc
live_updatingINTEGER1 = re-evaluate on changes, 0 = static

New table: collection_placements

ColumnTypePurpose
idINTEGER PKAuto-increment
collection_idTEXT FKReferences collections
locationTEXTUI location key
display_limitINTEGERMax items at this location (null = unlimited)
sort_overrideTEXTOptional sort override
positionINTEGERDisplay order

Backfill migration converts existing collections to the new schema, generating rule_json from existing collection properties.


Implementation Stages

StageScope
Stage 1 - FoundationCollection entity changes, CollectionRuleEvaluator, predicate model, actions (resolve/preview/field-values)
Stage 2 - ContentGroupIngestion-time creation in MediaEntityChainFactory, browse container views driven by ContentGroup collections
Stage 3 - Smart Collection GenerationTemplate engine, auto-generation from library data, threshold configuration
Stage 4 - Collections Page & Collection Builder/collections page, collection builder UI, Custom collection CRUD, placement management
Stage 5 - MigrationCollections page removal, navigation updates, existing collection backfill

Wikidata Series Manifests

When a ContentGroup collection has a canonical Wikidata series QID, hydration can attach a factual manifest from Tuvima.Wikidata. The manifest is stored separately from owned Works: owned entries link to local works, while missing entries remain lightweight series_manifest_items rows with names, QIDs, ordering, parent collection labels, and provenance.

This lets the UI show views such as “owned 2 of 18” for a series without creating fake media assets. For example, an Expanse collection can show owned novels plus missing novels, novellas, and short fiction when Wikidata models them. Manifest warnings indicate Wikidata modeling gaps or local ambiguity; user display overrides may affect presentation, but QID-based manifest facts remain canonical.

Collections and People presentation rules

Collections owns one padded content scroller beside its fixed rail; the document must not add a second scrollbar. People catalog counts use distinct top-level works, so tracks count once as their album and episodes count once as their show.

Person linked identities distinguish authorship aliases from music-group membership. A collective pen name uses alias relationships. A musical group uses every canonical Wikidata has parts identity as a person_group_members relationship, queues incomplete member records for full person hydration, and displays those people as linked member cards.