From 0d0d7bab1ec4ad0f3eab9bae36c8ba6957c12151 Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:12:05 -0400 Subject: [PATCH] fix(naming): let trailing bare IMDb ids anchor group identity (#113) * fix(naming): let trailing bare IMDb ids anchor group identity hasStructuredIDAnchor only consulted ParseStructuredFolderIDs, so folders tagged with an unbracketed trailing id ("Eggs Run (2021) tt8049994") still went ambiguous on title conflicts. ParseFolderIDs now returns only explicit evidence (structured tags plus unambiguous tt-prefixed ids), so use it for the anchor check. Covers the last 3 ambiguous-with-IDs groups on dev. Co-Authored-By: Claude Fable 5 * fix(naming): persist file-stem trailing IMDb ids into group identity InferGroupIdentity anchored on file-level trailing bare IMDb ids via hasStructuredIDAnchor but extracted file-stem IDs with ParseStructuredFolderIDs, so the id that justified resolving a title conflict never reached GroupIdentity/ScannedMediaGroup and downstream matching saw a resolved group without it. Use ParseFolderIDs for the file-stem extraction to mirror the anchor check. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- internal/naming/group_identity.go | 15 +++++---- internal/naming/group_identity_test.go | 42 ++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/internal/naming/group_identity.go b/internal/naming/group_identity.go index f9b990f0..97320024 100644 --- a/internal/naming/group_identity.go +++ b/internal/naming/group_identity.go @@ -60,13 +60,15 @@ func InferGroupIdentity(filePath string, libraryType string, assignment RootAssi populateMovieGroupIdentity(cleanFilePath, assignment, idAnchored, &group) } + // ID persistence must mirror hasStructuredIDAnchor: any evidence strong + // enough to anchor identity must also reach downstream matching. if ids := ParseFolderIDs(filepath.Base(group.ObservedRootPath)); ids != nil { group.TmdbID = ids.TmdbID group.ImdbID = ids.ImdbID group.TvdbID = ids.TvdbID } if group.TmdbID == "" || group.ImdbID == "" || group.TvdbID == "" { - if ids := ParseStructuredFolderIDs(strings.TrimSuffix(filepath.Base(cleanFilePath), filepath.Ext(cleanFilePath))); ids != nil { + if ids := ParseFolderIDs(strings.TrimSuffix(filepath.Base(cleanFilePath), filepath.Ext(cleanFilePath))); ids != nil { if group.TmdbID == "" { group.TmdbID = ids.TmdbID } @@ -249,15 +251,16 @@ func populateSeriesGroupIdentity(filePath string, libraryType string, assignment } // hasStructuredIDAnchor reports whether the observed root folder or the file -// name carries explicit structured provider IDs (e.g. {tmdb-694938}, -// [tvdbid-81189]). Bare trailing numerics are deliberately excluded — only an -// explicit tag is strong enough to anchor identity over a title conflict. +// name carries explicit provider IDs — structured tags (e.g. {tmdb-694938}, +// [tvdbid-81189]) or an unambiguous tt-prefixed IMDb id ("Eggs Run (2021) +// tt8049994"). ParseFolderIDs returns only such explicit evidence, which is +// strong enough to anchor identity over a title conflict. func hasStructuredIDAnchor(filePath, observedRootPath string) bool { - if ParseStructuredFolderIDs(filepath.Base(observedRootPath)) != nil { + if ParseFolderIDs(filepath.Base(observedRootPath)) != nil { return true } baseNoExt := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)) - return ParseStructuredFolderIDs(baseNoExt) != nil + return ParseFolderIDs(baseNoExt) != nil } func deriveObservedRootPath(filePath, candidateRoot string) string { diff --git a/internal/naming/group_identity_test.go b/internal/naming/group_identity_test.go index 087231f9..f4c0a741 100644 --- a/internal/naming/group_identity_test.go +++ b/internal/naming/group_identity_test.go @@ -30,6 +30,48 @@ func TestInferGroupIdentity_StructuredIDsResolveMovieTitleConflict(t *testing.T) } } +// An unbracketed trailing IMDb id anchors identity the same way a structured +// tag does — the tt prefix is unambiguous. Mirrors the dev-library case +// "Eggs Run (2021) tt8049994" whose file carries a different release title. +func TestInferGroupIdentity_TrailingImdbIDResolvesMovieTitleConflict(t *testing.T) { + group := InferGroupIdentity( + "/movies-int/es/Eggs Run (2021) tt8049994/Huevitos Congelados (2022) [WEBDL-1080p]-GRP.mkv", + "movies", + RootAssignment{ + RootPath: "/movies-int/es/Eggs Run (2021) tt8049994", + InferredType: "movie", + }, + ) + + if got, want := group.State, "resolved"; got != want { + t.Fatalf("State = %q, want %q", got, want) + } + if got, want := group.ImdbID, "tt8049994"; got != want { + t.Fatalf("ImdbID = %q, want %q", got, want) + } +} + +// A trailing IMDb id on the file stem (folder carries none) must both anchor +// the identity and be persisted into the group, so downstream matching sees +// the provider ID that justified the resolution. +func TestInferGroupIdentity_FileLevelTrailingImdbIDAnchorsAndPersists(t *testing.T) { + group := InferGroupIdentity( + "/movies/On Fire (2024)/Soul on Fire tt28078628.mkv", + "movies", + RootAssignment{ + RootPath: "/movies/On Fire (2024)", + InferredType: "movie", + }, + ) + + if got, want := group.State, "resolved"; got != want { + t.Fatalf("State = %q, want %q", got, want) + } + if got, want := group.ImdbID, "tt28078628"; got != want { + t.Fatalf("ImdbID = %q, want %q", got, want) + } +} + // Without provider IDs the same folder/file title conflict must still be // flagged ambiguous — there is nothing to anchor the identity. func TestInferGroupIdentity_TitleConflictWithoutIDsStaysAmbiguous(t *testing.T) {