Files
silo-server/internal/naming/filename_test.go
T

554 lines
18 KiB
Go

package naming
import "testing"
func TestDetectSeriesRoot(t *testing.T) {
tests := []struct {
name string
path string
libraryType string
wantRoot string
wantOK bool
}{
{
name: "standard season directory",
path: "/tv/Breaking Bad/Season 01/Breaking.Bad.S01E01.mkv",
wantRoot: "/tv/Breaking Bad",
wantOK: true,
},
{
name: "season directory case insensitive",
path: "/tv/Show Name/season 03/Show.Name.S03E01.mkv",
wantRoot: "/tv/Show Name",
wantOK: true,
},
{
name: "one pace arc folder collapses to series root",
path: "/television/anime/One Pace/Season 01 - Arc 01 - Romance Dawn/One.Piece.E01.1080p.mkv",
wantRoot: "/television/anime/One Pace",
wantOK: true,
},
{
name: "specials directory",
path: "/tv/Show Name/Specials/Show.Name.S00E01.mkv",
wantRoot: "/tv/Show Name",
wantOK: true,
},
{
name: "episodic file without season dir",
path: "/tv/Show Name/Show.Name.S01E01.mkv",
wantRoot: "/tv/Show Name",
wantOK: true,
},
{
name: "movie file no series root",
path: "/movies/Arrival (2016)/Arrival.2016.1080p.mkv",
wantRoot: "",
wantOK: false,
},
{
name: "year folder is not a season dir",
path: "/movies/2024/Some.Movie.mkv",
wantRoot: "",
wantOK: false,
},
{
name: "1080p in path is not a season dir",
path: "/movies/Movie Name/1080p/Movie.Name.mkv",
wantRoot: "",
wantOK: false,
},
{
name: "movie library ignores episode shaped movie folder",
path: "/movies/s01e03 (2020) {imdb-tt12261772} {tmdb-588077}/s01e03 (2020).mkv",
libraryType: "movies",
wantRoot: "",
wantOK: false,
},
{
name: "mixed library movie folder beats bare episode token",
path: "/mixed/s01e03 (2020) {imdb-tt12261772} {tmdb-588077}/s01e03 (2020).mkv",
libraryType: "mixed",
wantRoot: "",
wantOK: false,
},
{
name: "series library remains authoritative",
path: "/tv/Standalone Folder/random-file.mkv",
libraryType: "series",
wantRoot: "/tv/Standalone Folder",
wantOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
root, ok := DetectSeriesRoot(tt.path, tt.libraryType)
if ok != tt.wantOK {
t.Fatalf("DetectSeriesRoot(%q, %q) ok = %v, want %v", tt.path, tt.libraryType, ok, tt.wantOK)
}
if !tt.wantOK {
return
}
if root.RootPath != tt.wantRoot {
t.Errorf("DetectSeriesRoot(%q, %q) root = %q, want %q", tt.path, tt.libraryType, root.RootPath, tt.wantRoot)
}
})
}
}
func TestParseFilename(t *testing.T) {
tests := []struct {
name string
path string
libraryType string
wantTitle string
wantYear int
wantType string
wantSeason int
wantEp int
wantAirDate string
}{
{
name: "standard series episode",
path: "/tv/Breaking Bad (2008)/Season 01/Breaking.Bad.S01E01.mkv",
wantTitle: "Breaking Bad",
wantYear: 2008,
wantType: "series",
wantSeason: 1,
wantEp: 1,
},
{
name: "extras maps to season zero",
path: "/tv/Show Name/Extras/Show.Name.S00E01.mkv",
wantTitle: "Show Name",
wantYear: 0,
wantType: "series",
wantSeason: 0,
wantEp: 1,
},
{
name: "anime release with decimal episode suffix and release junk",
path: "/television/anime/A Sister's All You Need (2017) {tvdb-332994}/Season 01/A Sister's All You Need (2017) - S01E01.001 - I Only Need a Little Brother Who Can Cook a Beautiful Naked Girl and Friends I Can Relate to [Bluray-1080p][10bit][x265][AC3 5.1][EN+JA]-GHOST][.mkv",
wantTitle: "A Sister's All You Need",
wantYear: 2017,
wantType: "series",
wantSeason: 1,
wantEp: 1,
},
{
name: "movie library episode token folder is movie",
path: "/movies/s01e03 (2020) {imdb-tt12261772} {tmdb-588077}/s01e03 (2020).mkv",
libraryType: "movies",
wantTitle: "s01e03",
wantYear: 2020,
wantType: "movie",
wantSeason: 0,
wantEp: 0,
},
{
name: "mixed library obvious movie folder beats episode token",
path: "/mixed/s01e03 (2020) {imdb-tt12261772} {tmdb-588077}/s01e03 (2020).mkv",
libraryType: "mixed",
wantTitle: "s01e03",
wantYear: 2020,
wantType: "movie",
wantSeason: 0,
wantEp: 0,
},
{
name: "mixed library flat tv folder stays series",
path: "/mixed/Show Name/Show Name S01E03.mkv",
libraryType: "mixed",
wantTitle: "Show Name",
wantYear: 0,
wantType: "series",
wantSeason: 1,
wantEp: 3,
},
{
name: "mixed library season dir stays series",
path: "/mixed/Show Name/Season 01/Show Name S01E03.mkv",
libraryType: "mixed",
wantTitle: "Show Name",
wantYear: 0,
wantType: "series",
wantSeason: 1,
wantEp: 3,
},
{
name: "movies library tv looking path stays movie",
path: "/movies/Show Name/Season 01/Show Name S01E03.mkv",
libraryType: "movies",
wantTitle: "Show Name S01E03",
wantYear: 0,
wantType: "movie",
wantSeason: 0,
wantEp: 0,
},
{
name: "series daily date episode",
path: "/tv/Jeopardy! (1984)/Season 2026/Jeopardy! (1984) - 2026-04-24 - Jamie Ding Zach Pollock Nicco Martinez.mkv",
libraryType: "series",
wantTitle: "Jeopardy!",
wantYear: 1984,
wantType: "series",
wantSeason: 2026,
wantEp: 0,
wantAirDate: "2026-04-24",
},
{
name: "series daily date episode with dotted separator",
path: "/tv/The Daily Show/The.Daily.Show.2024.02.15.Guest.Name.mkv",
libraryType: "series",
wantTitle: "The Daily Show",
wantYear: 0,
wantType: "series",
wantSeason: 0,
wantEp: 0,
wantAirDate: "2024-02-15",
},
{
name: "movie library date does not become tv air date",
path: "/movies/News Archive (2026)/News Archive 2026-04-24.mkv",
libraryType: "movies",
wantTitle: "News Archive",
wantYear: 2026,
wantType: "movie",
wantSeason: 0,
wantEp: 0,
wantAirDate: "",
},
{
name: "movie in folder with year",
path: "/movies/Arrival (2016)/Arrival.2016.1080p.mkv",
wantTitle: "Arrival",
wantYear: 2016,
wantType: "movie",
wantSeason: 0,
wantEp: 0,
},
{
name: "movie with imdbid tag no year",
path: "/movies/Big Buck Bunny [imdbid-tt1254207]/Big Buck Bunny.mp4",
wantTitle: "Big Buck Bunny",
wantYear: 0,
wantType: "movie",
wantSeason: 0,
wantEp: 0,
},
{
name: "1080p in filename is not a year",
path: "/movies/Movie Name (2020)/Movie.Name.1080p.BluRay.mkv",
wantTitle: "Movie Name",
wantYear: 2020,
wantType: "movie",
wantSeason: 0,
wantEp: 0,
},
{
name: "numeric folder not treated as season without evidence",
path: "/movies/2024/Some.Movie.mkv",
wantTitle: "Some.Movie",
wantYear: 0,
wantType: "movie",
wantSeason: 0,
wantEp: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := ParseFilename(tt.path, tt.libraryType)
if h.Type != tt.wantType {
t.Errorf("Type = %q, want %q", h.Type, tt.wantType)
}
if h.Title != tt.wantTitle {
t.Errorf("Title = %q, want %q", h.Title, tt.wantTitle)
}
if h.Year != tt.wantYear {
t.Errorf("Year = %d, want %d", h.Year, tt.wantYear)
}
if h.SeasonNum != tt.wantSeason {
t.Errorf("SeasonNum = %d, want %d", h.SeasonNum, tt.wantSeason)
}
if h.EpisodeNum != tt.wantEp {
t.Errorf("EpisodeNum = %d, want %d", h.EpisodeNum, tt.wantEp)
}
if h.AirDate != tt.wantAirDate {
t.Errorf("AirDate = %q, want %q", h.AirDate, tt.wantAirDate)
}
})
}
}
func TestResolvePathContext(t *testing.T) {
tests := []struct {
name string
path string
libraryType string
wantType string
wantRoot string
wantTitle string
wantYear int
wantSeason int
wantEpisode int
wantAirDate string
wantAirDatePattern bool
wantEpisodePattern bool
wantSeasonStructure bool
wantMovieFolderEvidence bool
}{
{
name: "movies library episode token folder is movie",
path: "/movies/s01e03 (2020) {imdb-tt12261772} {tmdb-588077}/s01e03 (2020).mkv",
libraryType: "movies",
wantType: "movie",
wantRoot: "/movies/s01e03 (2020) {imdb-tt12261772} {tmdb-588077}",
wantTitle: "s01e03",
wantYear: 2020,
wantEpisodePattern: true,
wantSeasonStructure: false,
wantMovieFolderEvidence: true,
},
{
name: "mixed library obvious movie folder beats episode token",
path: "/mixed/s01e03 (2020) {imdb-tt12261772} {tmdb-588077}/s01e03 (2020).mkv",
libraryType: "mixed",
wantType: "movie",
wantRoot: "/mixed/s01e03 (2020) {imdb-tt12261772} {tmdb-588077}",
wantTitle: "s01e03",
wantYear: 2020,
wantEpisodePattern: true,
wantSeasonStructure: false,
wantMovieFolderEvidence: true,
},
{
name: "mixed flat tv folder stays series",
path: "/mixed/Show Name/Show Name S01E03.mkv",
libraryType: "mixed",
wantType: "series",
wantRoot: "/mixed/Show Name",
wantTitle: "Show Name",
wantSeason: 1,
wantEpisode: 3,
wantEpisodePattern: true,
wantSeasonStructure: false,
wantMovieFolderEvidence: false,
},
{
name: "mixed season dir stays series",
path: "/mixed/Show Name/Season 01/Show Name S01E03.mkv",
libraryType: "mixed",
wantType: "series",
wantRoot: "/mixed/Show Name",
wantTitle: "Show Name",
wantSeason: 1,
wantEpisode: 3,
wantEpisodePattern: true,
wantSeasonStructure: true,
wantMovieFolderEvidence: false,
},
{
name: "anime release with decimal episode suffix keeps show root",
path: "/television/anime/A Sister's All You Need (2017) {tvdb-332994}/Season 01/A Sister's All You Need (2017) - S01E01.001 - I Only Need a Little Brother Who Can Cook a Beautiful Naked Girl and Friends I Can Relate to [Bluray-1080p][10bit][x265][AC3 5.1][EN+JA]-GHOST][.mkv",
libraryType: "series",
wantType: "series",
wantRoot: "/television/anime/A Sister's All You Need (2017) {tvdb-332994}",
wantTitle: "A Sister's All You Need",
wantYear: 2017,
wantSeason: 1,
wantEpisode: 1,
wantEpisodePattern: true,
wantSeasonStructure: true,
wantMovieFolderEvidence: false,
},
{
name: "series daily date has air date but no episode number",
path: "/tv/Jeopardy! (1984)/Season 2026/Jeopardy! (1984) - 2026_04_24 - Jamie Ding.mkv",
libraryType: "series",
wantType: "series",
wantRoot: "/tv/Jeopardy! (1984)",
wantTitle: "Jeopardy!",
wantYear: 1984,
wantSeason: 2026,
wantEpisode: 0,
wantAirDate: "2026-04-24",
wantAirDatePattern: true,
wantEpisodePattern: false,
wantSeasonStructure: true,
wantMovieFolderEvidence: false,
},
{
name: "numeric year folder is not season structure",
path: "/movies/2024/Some.Movie.mkv",
wantType: "movie",
wantRoot: "/movies/2024/Some.Movie",
wantTitle: "Some.Movie",
wantEpisodePattern: false,
wantSeasonStructure: false,
wantMovieFolderEvidence: false,
},
{
name: "id tagged movie folder beats divergent release filename",
path: "/movies/The Expendables 4 {imdb-tt3291150} {tmdb-299054}/Expend4bles (2023) [Remux-1080p 8-bit AVC TrueHD Atmos 7.1]-CiNEPHiLES.mkv",
libraryType: "movies",
wantType: "movie",
wantRoot: "/movies/The Expendables 4 {imdb-tt3291150} {tmdb-299054}",
wantTitle: "The Expendables 4",
wantYear: 0,
wantEpisodePattern: false,
wantSeasonStructure: false,
wantMovieFolderEvidence: true,
},
{
name: "mixed library id tagged movie folder beats divergent release filename",
path: "/mixed/The Expendables 4 {imdb-tt3291150} {tmdb-299054}/Expend4bles (2023) [Remux-1080p 8-bit AVC TrueHD Atmos 7.1]-CiNEPHiLES.mkv",
libraryType: "mixed",
wantType: "movie",
wantRoot: "/mixed/The Expendables 4 {imdb-tt3291150} {tmdb-299054}",
wantTitle: "The Expendables 4",
wantYear: 0,
wantEpisodePattern: false,
wantSeasonStructure: false,
wantMovieFolderEvidence: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := ResolvePathContext(tt.path, tt.libraryType)
if ctx == nil {
t.Fatal("expected non-nil PathContext")
}
if ctx.Type != tt.wantType {
t.Errorf("Type = %q, want %q", ctx.Type, tt.wantType)
}
if ctx.RootPath != tt.wantRoot {
t.Errorf("RootPath = %q, want %q", ctx.RootPath, tt.wantRoot)
}
if ctx.Title != tt.wantTitle {
t.Errorf("Title = %q, want %q", ctx.Title, tt.wantTitle)
}
if ctx.Year != tt.wantYear {
t.Errorf("Year = %d, want %d", ctx.Year, tt.wantYear)
}
if ctx.SeasonNum != tt.wantSeason {
t.Errorf("SeasonNum = %d, want %d", ctx.SeasonNum, tt.wantSeason)
}
if ctx.EpisodeNum != tt.wantEpisode {
t.Errorf("EpisodeNum = %d, want %d", ctx.EpisodeNum, tt.wantEpisode)
}
if ctx.AirDate != tt.wantAirDate {
t.Errorf("AirDate = %q, want %q", ctx.AirDate, tt.wantAirDate)
}
if ctx.HasAirDatePattern != tt.wantAirDatePattern {
t.Errorf("HasAirDatePattern = %v, want %v", ctx.HasAirDatePattern, tt.wantAirDatePattern)
}
if ctx.HasEpisodePattern != tt.wantEpisodePattern {
t.Errorf("HasEpisodePattern = %v, want %v", ctx.HasEpisodePattern, tt.wantEpisodePattern)
}
if ctx.HasSeasonStructure != tt.wantSeasonStructure {
t.Errorf("HasSeasonStructure = %v, want %v", ctx.HasSeasonStructure, tt.wantSeasonStructure)
}
if ctx.HasMovieFolderEvidence != tt.wantMovieFolderEvidence {
t.Errorf("HasMovieFolderEvidence = %v, want %v", ctx.HasMovieFolderEvidence, tt.wantMovieFolderEvidence)
}
})
}
}
func TestDetectCanonicalRoot(t *testing.T) {
tests := []struct {
name string
path string
libraryType string
wantRoot string
wantType string
wantOK bool
}{
{
name: "standard series root",
path: "/tv/Breaking Bad/Season 01/Breaking.Bad.S01E01.mkv",
wantRoot: "/tv/Breaking Bad",
wantType: "series",
wantOK: true,
},
{
name: "movie in folder",
path: "/movies/Arrival (2016)/Arrival.2016.1080p.mkv",
wantRoot: "/movies/Arrival (2016)",
wantType: "movie",
wantOK: true,
},
{
name: "loose movie file uses file stem as root",
path: "/movies/Arrival (2016).mkv",
wantRoot: "/movies/Arrival (2016)",
wantType: "movie",
wantOK: true,
},
{
name: "mixed library obvious movie folder uses movie root",
path: "/mixed/s01e03 (2020) {imdb-tt12261772} {tmdb-588077}/s01e03 (2020).mkv",
libraryType: "mixed",
wantRoot: "/mixed/s01e03 (2020) {imdb-tt12261772} {tmdb-588077}",
wantType: "movie",
wantOK: true,
},
{
name: "movies library tv looking path still uses movie root",
path: "/movies/Show Name/Season 01/Show Name S01E03.mkv",
libraryType: "movies",
wantRoot: "/movies/Show Name/Season 01/Show Name S01E03",
wantType: "movie",
wantOK: true,
},
{
name: "mixed flat tv folder uses series root",
path: "/mixed/Show Name/Show Name S01E03.mkv",
libraryType: "mixed",
wantRoot: "/mixed/Show Name",
wantType: "series",
wantOK: true,
},
{
name: "movie root uses parent when comparable normalization differs",
path: "/movies/WALL-E {imdb-tt0910970}/Wall-E.mkv",
wantRoot: "/movies/WALL-E {imdb-tt0910970}",
wantType: "movie",
wantOK: true,
},
{
name: "movie root uses id tagged parent for divergent release filename",
path: "/movies/The Expendables 4 {imdb-tt3291150} {tmdb-299054}/Expend4bles (2023) [Remux-1080p 8-bit AVC TrueHD Atmos 7.1]-CiNEPHiLES.mkv",
wantRoot: "/movies/The Expendables 4 {imdb-tt3291150} {tmdb-299054}",
wantType: "movie",
wantOK: true,
},
{
name: "absolute root-level file returns false",
path: "/single-file.mkv",
wantOK: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cr, ok := DetectCanonicalRoot(tt.path, tt.libraryType)
if ok != tt.wantOK {
t.Fatalf("DetectCanonicalRoot(%q, %q) ok = %v, want %v", tt.path, tt.libraryType, ok, tt.wantOK)
}
if !tt.wantOK {
return
}
if cr.RootPath != tt.wantRoot {
t.Errorf("RootPath = %q, want %q", cr.RootPath, tt.wantRoot)
}
if cr.Type != tt.wantType {
t.Errorf("Type = %q, want %q", cr.Type, tt.wantType)
}
})
}
}