package nfo import ( "context" "os" "path/filepath" "slices" "testing" "github.com/Silo-Server/silo-server/internal/metadata" ) func TestFindNFO_FilePathFallsBackToDirectoryLevelSidecar(t *testing.T) { t.Parallel() dir := t.TempDir() filePath := filepath.Join(dir, "Blade Runner (1982).mkv") if err := os.WriteFile(filePath, []byte("video"), 0o644); err != nil { t.Fatalf("WriteFile(file) error = %v", err) } if err := os.WriteFile(filepath.Join(dir, "movie.nfo"), []byte("Dir"), 0o644); err != nil { t.Fatalf("WriteFile(movie.nfo) error = %v", err) } want := filepath.Join(dir, "movie.nfo") if got, _ := findNFO([]string{filePath}, ""); got != want { t.Fatalf("findNFO(file path) = %q, want %q", got, want) } } func TestFindNFO_DirectoryPathUsesDirectoryLevelSidecar(t *testing.T) { t.Parallel() dir := t.TempDir() want := filepath.Join(dir, "movie.nfo") if err := os.WriteFile(want, []byte("Dir"), 0o644); err != nil { t.Fatalf("WriteFile(movie.nfo) error = %v", err) } if got, _ := findNFO([]string{dir}, ""); got != want { t.Fatalf("findNFO(directory path) = %q, want %q", got, want) } } func TestFindNFO_FilePathUsesBasenameMatchedNFO(t *testing.T) { t.Parallel() dir := t.TempDir() filePath := filepath.Join(dir, "Blade Runner (1982).mkv") want := filepath.Join(dir, "Blade Runner (1982).nfo") if err := os.WriteFile(filePath, []byte("video"), 0o644); err != nil { t.Fatalf("WriteFile(file) error = %v", err) } if err := os.WriteFile(want, []byte("File"), 0o644); err != nil { t.Fatalf("WriteFile(file.nfo) error = %v", err) } if got, _ := findNFO([]string{filePath}, ""); got != want { t.Fatalf("findNFO(file path) = %q, want %q", got, want) } } func TestFindNFO_ExtensionlessFilePathUsesDirectoryLevelSidecar(t *testing.T) { t.Parallel() dir := t.TempDir() filePath := filepath.Join(dir, "Blade Runner (1982)") want := filepath.Join(dir, "movie.nfo") if err := os.WriteFile(filePath, []byte("video"), 0o644); err != nil { t.Fatalf("WriteFile(file) error = %v", err) } if err := os.WriteFile(want, []byte("Dir"), 0o644); err != nil { t.Fatalf("WriteFile(movie.nfo) error = %v", err) } if got, _ := findNFO([]string{filePath}, ""); got != want { t.Fatalf("findNFO(extensionless file path) = %q, want %q", got, want) } } func TestFindNFO_ExtensionlessFilePathUsesBasenameMatchedNFO(t *testing.T) { t.Parallel() dir := t.TempDir() filePath := filepath.Join(dir, "Blade Runner (1982)") want := filepath.Join(dir, "Blade Runner (1982).nfo") if err := os.WriteFile(filePath, []byte("video"), 0o644); err != nil { t.Fatalf("WriteFile(file) error = %v", err) } if err := os.WriteFile(want, []byte("File"), 0o644); err != nil { t.Fatalf("WriteFile(file.nfo) error = %v", err) } if got, _ := findNFO([]string{filePath}, ""); got != want { t.Fatalf("findNFO(extensionless file path) = %q, want %q", got, want) } } func TestNFOCandidatesForMissingDottedDirectoryPathIncludeDirectoryLevelSidecars(t *testing.T) { t.Parallel() path := filepath.Join("/media/shows", "Mr. Robot") got := nfoCandidatesForPath(path) wantMovie := filepath.Join(path, "movie.nfo") wantTV := filepath.Join(path, "tvshow.nfo") if !slices.Contains(got, wantMovie) { t.Fatalf("nfoCandidatesForPath(%q) missing %q in %#v", path, wantMovie, got) } if !slices.Contains(got, wantTV) { t.Fatalf("nfoCandidatesForPath(%q) missing %q in %#v", path, wantTV, got) } } // A tvshow.nfo sitting next to a movie file must not inject series data into // a movie item at top priority: GetMetadata carries the same ContentType // guard Search has. func TestGetMetadata_TVShowNFOBesideMovieFileDoesNotInject(t *testing.T) { t.Parallel() dir := t.TempDir() filePath := filepath.Join(dir, "Home Movie (2021).mkv") if err := os.WriteFile(filePath, []byte("video"), 0o644); err != nil { t.Fatalf("WriteFile(file) error = %v", err) } nfoXML := `Some Series123` if err := os.WriteFile(filepath.Join(dir, "tvshow.nfo"), []byte(nfoXML), 0o644); err != nil { t.Fatalf("WriteFile(tvshow.nfo) error = %v", err) } p := NewProvider() result, err := p.GetMetadata(context.Background(), metadata.MetadataRequest{ ContentType: "movie", FilePath: filePath, }) if err != nil { t.Fatalf("GetMetadata() error = %v", err) } if result.HasMetadata { t.Fatalf("GetMetadata() = %#v, want no metadata (series NFO must not inject into a movie)", result) } } // A stray movie.nfo in a series root must not shadow tvshow.nfo: findNFO // falls through to the next candidate on type mismatch. func TestFindNFO_StrayMovieNFOInSeriesRootFallsThroughToTVShow(t *testing.T) { t.Parallel() dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "movie.nfo"), []byte(`Stray`), 0o644); err != nil { t.Fatalf("WriteFile(movie.nfo) error = %v", err) } want := filepath.Join(dir, "tvshow.nfo") if err := os.WriteFile(want, []byte(`The Show`), 0o644); err != nil { t.Fatalf("WriteFile(tvshow.nfo) error = %v", err) } got, parsed := findNFO([]string{dir}, "series") if got != want { t.Fatalf("findNFO(series root) = %q, want %q", got, want) } if parsed == nil || parsed.Title != "The Show" { t.Fatalf("parsed = %#v, want tvshow content", parsed) } } // A candidate that exists but fails to parse falls through to the next // candidate instead of masking a valid sidecar. func TestFindNFO_ParseFailureFallsThroughToNextCandidate(t *testing.T) { t.Parallel() dir := t.TempDir() filePath := filepath.Join(dir, "Blade Runner (1982).mkv") if err := os.WriteFile(filePath, []byte("video"), 0o644); err != nil { t.Fatalf("WriteFile(file) error = %v", err) } if err := os.WriteFile(filepath.Join(dir, "movie.nfo"), []byte("not xml at all"), 0o644); err != nil { t.Fatalf("WriteFile(movie.nfo) error = %v", err) } want := filepath.Join(dir, "Blade Runner (1982).nfo") if err := os.WriteFile(want, []byte(`Blade Runner`), 0o644); err != nil { t.Fatalf("WriteFile(basename nfo) error = %v", err) } got, parsed := findNFO([]string{filePath}, "movie") if got != want { t.Fatalf("findNFO(parse-failure fallthrough) = %q, want %q", got, want) } if parsed == nil || parsed.Title != "Blade Runner" { t.Fatalf("parsed = %#v, want valid movie content", parsed) } } // GetMetadata surfaces the full Phase-B field set from a curated movie NFO. func TestGetMetadata_FullMovieFieldSet(t *testing.T) { t.Parallel() dir := t.TempDir() filePath := filepath.Join(dir, "Blade Runner (1982).mkv") if err := os.WriteFile(filePath, []byte("video"), 0o644); err != nil { t.Fatalf("WriteFile(file) error = %v", err) } if err := os.WriteFile(filepath.Join(dir, "movie.nfo"), []byte(fullMovieNFO), 0o644); err != nil { t.Fatalf("WriteFile(movie.nfo) error = %v", err) } p := NewProvider() result, err := p.GetMetadata(context.Background(), metadata.MetadataRequest{ ContentType: "movie", FilePath: filePath, }) if err != nil { t.Fatalf("GetMetadata() error = %v", err) } if !result.HasMetadata { t.Fatal("HasMetadata = false, want true") } if result.OriginalTitle != "Blade Runner: The Original" { t.Errorf("OriginalTitle = %q", result.OriginalTitle) } if result.Tagline != "Man has made his match" { t.Errorf("Tagline = %q", result.Tagline) } if result.Runtime != 117 { t.Errorf("Runtime = %d", result.Runtime) } if result.ReleaseDate != "1982-06-25" { t.Errorf("ReleaseDate = %q", result.ReleaseDate) } if result.ContentRating != "R" { t.Errorf("ContentRating = %q", result.ContentRating) } if len(result.Genres) != 2 || len(result.Studios) != 2 || len(result.Countries) != 2 || len(result.Keywords) != 2 { t.Errorf("collections = genres %d studios %d countries %d keywords %d, want 2 each", len(result.Genres), len(result.Studios), len(result.Countries), len(result.Keywords)) } if result.Ratings.IMDB != 8.1 || result.Ratings.TMDB != 7.9 || result.Ratings.RTCritic != 89 || result.Ratings.RTAudience != 91 { t.Errorf("Ratings = %#v", result.Ratings) } if len(result.People) != 5 { t.Errorf("People len = %d, want 5", len(result.People)) } if result.FirstAirDate != "" { t.Errorf("FirstAirDate = %q, want empty for a movie", result.FirstAirDate) } } // A minimal NFO must not emit placeholder collections: empty slices stay nil // so MergeFillEmpty's early-return lets remote providers fill them. func TestGetMetadata_MinimalNFOEmitsNoPlaceholders(t *testing.T) { t.Parallel() dir := t.TempDir() filePath := filepath.Join(dir, "Home Movie (2021).mkv") if err := os.WriteFile(filePath, []byte("video"), 0o644); err != nil { t.Fatalf("WriteFile(file) error = %v", err) } if err := os.WriteFile(filepath.Join(dir, "movie.nfo"), []byte(`Home Movie`), 0o644); err != nil { t.Fatalf("WriteFile(movie.nfo) error = %v", err) } p := NewProvider() result, err := p.GetMetadata(context.Background(), metadata.MetadataRequest{ ContentType: "movie", FilePath: filePath, }) if err != nil { t.Fatalf("GetMetadata() error = %v", err) } if result.Genres != nil || result.Studios != nil || result.Countries != nil || result.Keywords != nil || result.People != nil { t.Errorf("placeholder collections emitted: %#v", result) } if result.Runtime != 0 || result.ReleaseDate != "" || result.ContentRating != "" { t.Errorf("placeholder scalars emitted: %#v", result) } } // The series GetMetadata path maps aired/premiered onto FirstAirDate. func TestGetMetadata_SeriesFirstAirDate(t *testing.T) { t.Parallel() dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "tvshow.nfo"), []byte(fullTVShowNFO), 0o644); err != nil { t.Fatalf("WriteFile(tvshow.nfo) error = %v", err) } p := NewProvider() result, err := p.GetMetadata(context.Background(), metadata.MetadataRequest{ ContentType: "series", PrimarySidecarSearchPaths: []string{dir}, }) if err != nil { t.Fatalf("GetMetadata() error = %v", err) } if !result.HasMetadata { t.Fatal("HasMetadata = false, want true") } if result.FirstAirDate != "2015-06-24" { t.Errorf("FirstAirDate = %q, want 2015-06-24", result.FirstAirDate) } if result.ReleaseDate != "" { t.Errorf("ReleaseDate = %q, want empty for a series", result.ReleaseDate) } }