Vellum/internal/library/registry_test.go
mithral 1fcc7344cb Separate video libraries by kind: movies, TV, adult, hentai
One "video" kind meant every video library behaved the same, so a film shelf
tried to parse episode numbers and a TV library could not be told apart from
it. The kinds are now a declaration rather than a special case.

Five video kinds, all owning the same containers and differing only in how
their files become items:

  movies  flat                    tv      series/season/episode
  adult   flat                    hentai  series/season/episode
  video   series/season/episode   (kept: existing libraries already use it)

The scanner dispatches on library.GroupModeFor(lib.Kind) instead of comparing
the kind to "video", so a new episodic kind is a registry entry and nothing in
the scanner changes.

The decision worth recording, because it could have gone the other way:
media_items.kind stays STRUCTURAL. Every video library stores 'video' on its
rows, not its own kind. media_items.kind answers "is this a playable video",
which is what the playback, catalog and Jellyfin paths actually ask, and
migration 0013 already committed to that rule when it decided an episode would
stay kind='video'.

Storing the library kind instead would have meant teaching every query that
says kind='video' about five new values - catalog's episode and next-episode
reads, Jellyfin's episodes-only filter, the item-type mapping, the web
player's routing - and every kind added later would have had to do it again.

So the two jobs are split: the ROW kind says what a thing is, the LIBRARY kind
says how it is classified, and the library kind is what routes metadata. The
enricher now reads the library to choose providers, because a movies library
and an adult library hold identical rows and must not query the same
catalogue.

The adult kinds have no metadata provider. There is no mainstream catalogue
for that content - the ecosystem uses StashDB or local NFO files - so they
scan and browse and stay unenriched until someone registers one. That is
stated in the registry rather than left as a surprise.

Verified end-to-end: the same tree under a movies library stays flat, under a
tv library builds series/season/episodes, and every row in both is stored as
kind='video'. The UI needs no change - it reads /api/v1/media-types, so the
new kinds appeared in the picker by themselves.

Red-green: dispatching on the literal kind fails the tv and hentai cases;
storing the library kind on rows fails the structural-kind assertion.
2026-10-12 11:20:51 -04:00

59 lines
1.9 KiB
Go

package library
import "testing"
// The video kinds share their extensions and differ only in how they group and
// which kind they store. Those two facts are what let a movies library and a TV
// library behave differently without the scanner knowing either name.
func TestVideoKindsGroupAndStoreCorrectly(t *testing.T) {
tests := []struct {
kind string
wantGroup GroupMode
wantItem string
}{
{"movies", GroupByFile, "video"},
{"tv", GroupByEpisode, "video"},
{"video", GroupByEpisode, "video"},
{"adult", GroupByFile, "video"},
{"hentai", GroupByEpisode, "video"},
// Every other media type stores its own kind, which is what they all
// did before the video kinds were split.
{"music", GroupByDirectory, "music"},
{"ebooks", GroupByFile, "ebooks"},
{"audiobooks", GroupByDirectory, "audiobooks"},
}
for _, tt := range tests {
if got := GroupModeFor(tt.kind); got != tt.wantGroup {
t.Errorf("GroupModeFor(%q) = %q, want %q", tt.kind, got, tt.wantGroup)
}
if got := ItemKindFor(tt.kind); got != tt.wantItem {
t.Errorf("ItemKindFor(%q) = %q, want %q", tt.kind, got, tt.wantItem)
}
}
}
// Every video kind owns the same containers. They are separated by how their
// files become items, never by what they can hold.
func TestVideoKindsShareTheirExtensions(t *testing.T) {
want := ExtensionsFor("video")
if len(want) == 0 {
t.Fatal("the video kind owns no extensions")
}
for _, kind := range []string{"movies", "tv", "adult", "hentai"} {
got := ExtensionsFor(kind)
for ext := range want {
if !got[ext] {
t.Errorf("%s does not own %s, but every video kind should", kind, ext)
}
}
}
}
// A kind nobody registered is still described by what is already on the row, so
// a library created before a kind existed keeps working.
func TestItemKindForUnregisteredKind(t *testing.T) {
if got := ItemKindFor("nonsense"); got != "nonsense" {
t.Errorf("ItemKindFor(nonsense) = %q, want nonsense", got)
}
}