From 05c76632efc4585c866a80419c14faf961d53366 Mon Sep 17 00:00:00 2001 From: HichamLL04 Date: Thu, 9 Jul 2026 21:47:48 +0200 Subject: [PATCH 1/2] feat(trackers): add Lat-Team (LT) tracker support --- .../trackers/impl/unit3d/additional/rules.go | 11 + internal/trackers/impl/unit3d/lt.go | 79 +++++++ internal/trackers/impl/unit3d/names.go | 195 ++++++++++++++++++ internal/trackers/impl/unit3d/profiles.go | 1 + internal/trackers/impl/unit3d/upload_test.go | 192 +++++++++++++++++ 5 files changed, 478 insertions(+) create mode 100644 internal/trackers/impl/unit3d/lt.go diff --git a/internal/trackers/impl/unit3d/additional/rules.go b/internal/trackers/impl/unit3d/additional/rules.go index 4ef5b575..eda185ad 100644 --- a/internal/trackers/impl/unit3d/additional/rules.go +++ b/internal/trackers/impl/unit3d/additional/rules.go @@ -37,6 +37,7 @@ var trackerRuleFactories = map[string]func() RuleSet{ "DP": rulesDP, "HHD": rulesHHD, "LST": rulesLST, + "LT": rulesLT, "LUME": rulesLUME, "OE": rulesOE, "OTW": rulesOTW, @@ -269,3 +270,13 @@ func rulesZNTH() RuleSet { AdultMessage: "Porn/xxx is not allowed at ZNTH.", } } + +func rulesLT() RuleSet { + return RuleSet{ + Language: &LanguageRule{ + Languages: languagesSpanish, + RequireAudio: true, + RequireSubs: true, + }, + } +} diff --git a/internal/trackers/impl/unit3d/lt.go b/internal/trackers/impl/unit3d/lt.go new file mode 100644 index 00000000..429f4e28 --- /dev/null +++ b/internal/trackers/impl/unit3d/lt.go @@ -0,0 +1,79 @@ +// Copyright (c) 2025-2026, Audionut and the autobrr contributors. +// SPDX-License-Identifier: GPL-2.0-or-later + +package unit3d + +import ( + "strings" + + "github.com/autobrr/upbrr/pkg/api" +) + +func siteLTProfile() unit3DSiteProfile { + return unit3DSiteProfile{ + resolveCategoryID: resolveUnit3DLTCategoryID, + } +} + +func resolveUnit3DLTCategoryID(meta api.PreparedMetadata) string { + category := resolveUnit3DCategory(meta) + + categoryID := "1" // Default MOVIE + if category == "TV" { + categoryID = "2" // Default TV + } else if category != "MOVIE" { + categoryID = "1" + } + + if category == "TV" { + if meta.Anime { + return "5" + } + + keywords := "" + overview := "" + genres := "" + var originCountries []string + + if meta.ExternalMetadata.TMDB != nil { + keywords = strings.ToLower(meta.ExternalMetadata.TMDB.Keywords) + overview = strings.ToLower(meta.ExternalMetadata.TMDB.Overview) + genres = strings.ToLower(meta.ExternalMetadata.TMDB.Genres) + originCountries = meta.ExternalMetadata.TMDB.OriginCountry + } + + soapKeywords := []string{"telenovela", "novela", "soap", "culebrón", "culebron"} + hasSoap := false + for _, kw := range soapKeywords { + if strings.Contains(keywords, kw) || strings.Contains(overview, kw) { + hasSoap = true + break + } + } + if hasSoap { + return "8" + } + + // Turkish & Asian + asianCountries := map[string]bool{ + "AE": true, "AF": true, "AM": true, "AZ": true, "BD": true, "BH": true, "BN": true, "BT": true, "CN": true, "CY": true, "GE": true, "HK": true, "ID": true, "IL": true, "IN": true, + "IQ": true, "IR": true, "JO": true, "JP": true, "KG": true, "KH": true, "KP": true, "KR": true, "KW": true, "KZ": true, "LA": true, "LB": true, "LK": true, "MM": true, "MN": true, + "MO": true, "MV": true, "MY": true, "NP": true, "OM": true, "PH": true, "PK": true, "PS": true, "QA": true, "SA": true, "SG": true, "SY": true, "TH": true, "TJ": true, "TL": true, + "TM": true, "TR": true, "TW": true, "UZ": true, "VN": true, "YE": true, + } + + hasAsianCountry := false + for _, c := range originCountries { + if asianCountries[strings.ToUpper(c)] { + hasAsianCountry = true + break + } + } + + if strings.Contains(genres, "drama") && hasAsianCountry { + return "20" + } + } + + return categoryID +} diff --git a/internal/trackers/impl/unit3d/names.go b/internal/trackers/impl/unit3d/names.go index 0bf1fe23..5f181308 100644 --- a/internal/trackers/impl/unit3d/names.go +++ b/internal/trackers/impl/unit3d/names.go @@ -4,6 +4,9 @@ package unit3d import ( + "encoding/json" + "fmt" + "os" "regexp" "strconv" "strings" @@ -49,6 +52,8 @@ func buildUnit3DName(tracker string, meta api.PreparedMetadata, cfg config.Track return BuildCBRName(meta, cfg.TagForCustomRelease) case "LDU": return buildLDUName(name, meta) + case "LT": + return buildLTName(name, meta) case "RF": return addNoGroupSuffix(name, meta, "NoGroup") case "SAM": @@ -474,3 +479,193 @@ func normalizeZNTHAlphaNum(value string) string { func isZNTHAlphaNum(r rune) bool { return unicode.IsLetter(r) || unicode.IsDigit(r) } + +func buildLTName(name string, meta api.PreparedMetadata) string { + aka := "" + title := "" + origTitle := "" + origLang := "" + if meta.ExternalMetadata.TMDB != nil { + aka = meta.ExternalMetadata.TMDB.RetrievedAKA + title = meta.ExternalMetadata.TMDB.Title + origTitle = meta.ExternalMetadata.TMDB.OriginalTitle + origLang = meta.ExternalMetadata.TMDB.OriginalLanguage + } + if title == "" && meta.ExternalMetadata.IMDB != nil { + title = meta.ExternalMetadata.IMDB.Title + } + if origTitle == "" { + origTitle = title + } + ltName := name + ltName = strings.ReplaceAll(ltName, "Dual-Audio", "") + ltName = strings.ReplaceAll(ltName, "Dubbed", "") + + // Find the end of the title block (start of year, resolution, source, etc.) + titleEndIdx := len(ltName) + markerRegex := regexp.MustCompile(`(?i)\.(19\d\d|20\d\d|3d|480p|576p|720p|1080p|1080i|2160p|4k|mkv|avi|mp4|remux|bluray|blu-ray|web-dl|webdl|web|hdtv|dvdrip|dvd|bd25|bd50|uhd)\b`) + if loc := markerRegex.FindStringIndex(ltName); loc != nil { + titleEndIdx = loc[0] + } + + titleBlock := ltName[:titleEndIdx] + restOfName := ltName[titleEndIdx:] + + // Determine the correct target title to use + targetTitle := title + if origLang == "es" { + // Use Spanish title for Spanish original series/movies + if aka != "" { + akaClean := strings.TrimSpace(strings.ReplaceAll(aka, "AKA", "")) + if akaClean != "" { + targetTitle = akaClean + } + } else if origTitle != "" { + targetTitle = origTitle + } + } + targetTitleDotted := strings.ReplaceAll(targetTitle, " ", ".") + + // If the title block contains AKA, or we need to replace a mismatched title: + akaRegex := regexp.MustCompile(`(?i)[. _-]aka[. _-]`) + if akaRegex.MatchString(titleBlock) || !strings.EqualFold(strings.ReplaceAll(titleBlock, ".", " "), targetTitle) { + titleBlock = targetTitleDotted + } + + ltName = titleBlock + restOfName + + isDisc := false + if normalizeUnit3DTypeCandidate(meta.Type) == "DISC" || normalizeUnit3DTypeCandidate(meta.Release.Type) == "DISC" || meta.DiscType != "" { + isDisc = true + } + + if !isDisc { + type rawMediaInfoDoc struct { + Media struct { + Track []map[string]any `json:"track"` + } `json:"media"` + } + + var tracks []map[string]any + if meta.MediaInfoJSONPath != "" { + if payload, err := os.ReadFile(meta.MediaInfoJSONPath); err == nil { + var doc rawMediaInfoDoc + if err := json.Unmarshal(payload, &doc); err == nil { + tracks = doc.Media.Track + } + } + } + + hasSpanishAudio := false + hasLatino := false + hasCastilian := false + + audioLatinoCheck := map[string]bool{ + "es-419": true, "es-mx": true, "es-ar": true, "es-cl": true, "es-ve": true, + "es-bo": true, "es-co": true, "es-cr": true, "es-do": true, "es-ec": true, + "es-sv": true, "es-gt": true, "es-hn": true, "es-ni": true, "es-pa": true, + "es-py": true, "es-pe": true, "es-pr": true, "es-uy": true, + } + audioCastilianCheck := map[string]bool{ + "es": true, "es-es": true, + } + latinoKeywords := []string{"latino", "latin america"} + castilianKeywords := []string{"castellano"} + + hasTracks := false + if len(tracks) > 0 { + for _, track := range tracks { + trackType := "" + if val, ok := track["@type"]; ok { + trackType = strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", val))) + } + if trackType != "audio" { + continue + } + hasTracks = true + lang := strings.ToLower(namesTrackString(track, "Language", "Language_String", "Language_String2", "Language_String3")) + titleText := strings.ToLower(namesTrackString(track, "Title", "Title_String", "Title_String2", "Title_String3")) + + if strings.Contains(titleText, "commentary") { + continue + } + + isLatinoTitle := false + for _, kw := range latinoKeywords { + if strings.Contains(titleText, kw) { + isLatinoTitle = true + break + } + } + isCastilianTitle := false + for _, kw := range castilianKeywords { + if strings.Contains(titleText, kw) { + isCastilianTitle = true + break + } + } + + if audioLatinoCheck[lang] || (lang == "es" && isLatinoTitle) { + hasLatino = true + hasSpanishAudio = true + } else if (lang == "es" && isCastilianTitle) || audioCastilianCheck[lang] { + hasCastilian = true + hasSpanishAudio = true + } + } + } + + if !hasTracks { + // Fallback to PreparedMetadata.AudioLanguages if MediaInfo JSON wasn't parsed + for _, lang := range meta.AudioLanguages { + if strings.EqualFold(lang, "Spanish") || strings.EqualFold(lang, "es") { + hasSpanishAudio = true + hasLatino = true + break + } + } + } + + tag := strings.TrimSpace(meta.Tag) + // insertTagBracket inserts the label just before "-" so the result + // is "… [LABEL]-TAG" rather than "…- [LABEL]TAG". + insertTagBracket := func(s, label string) string { + if tag != "" { + sep := "-" + tag + if idx := strings.LastIndex(s, sep); idx != -1 { + return s[:idx] + " [" + label + "]" + s[idx:] + } + } + return s + " [" + label + "]" + } + if hasSpanishAudio { + if !hasLatino && hasCastilian { + if !strings.Contains(ltName, "[CAST]") { + ltName = insertTagBracket(ltName, "CAST") + } + } + } else { + if !strings.Contains(ltName, "[SUBS]") { + ltName = insertTagBracket(ltName, "SUBS") + } + } + } + + multipleDots := regexp.MustCompile(`\.{2,}`) + ltName = multipleDots.ReplaceAllString(ltName, ".") + multipleSpaces := regexp.MustCompile(`\s{2,}`) + ltName = multipleSpaces.ReplaceAllString(ltName, " ") + return strings.Trim(ltName, ". ") +} + +func namesTrackString(track map[string]any, keys ...string) string { + for _, key := range keys { + if value, ok := track[key]; ok { + trimmed := strings.TrimSpace(fmt.Sprintf("%v", value)) + if trimmed != "" { + return trimmed + } + } + } + return "" +} diff --git a/internal/trackers/impl/unit3d/profiles.go b/internal/trackers/impl/unit3d/profiles.go index 40a9c713..c5f60655 100644 --- a/internal/trackers/impl/unit3d/profiles.go +++ b/internal/trackers/impl/unit3d/profiles.go @@ -28,6 +28,7 @@ var unit3DSiteProfiles = map[string]unit3DSiteProfile{ "ITT": siteITTProfile(), "LDU": siteLDUProfile(), "LST": siteLSTProfile(), + "LT": siteLTProfile(), "OE": siteOEProfile(), "OTW": siteOTWProfile(), "PT": sitePTProfile(), diff --git a/internal/trackers/impl/unit3d/upload_test.go b/internal/trackers/impl/unit3d/upload_test.go index 6f924ce9..f4f58c07 100644 --- a/internal/trackers/impl/unit3d/upload_test.go +++ b/internal/trackers/impl/unit3d/upload_test.go @@ -1887,3 +1887,195 @@ func TestResolveUnit3DTypeIDForTrackerTIKOverrideDiscType(t *testing.T) { t.Fatalf("expected TIK BD66 override type_id=4, got %q", got) } } + +func TestBuildUnit3DNameLT(t *testing.T) { + t.Run("cleans Dual-Audio and Dubbed and AKA", func(t *testing.T) { + meta := api.PreparedMetadata{ + ReleaseName: "Movie.2024.Dual-Audio.Dubbed.1080p.Bluray-GRP", + Tag: "GRP", + ExternalMetadata: api.ExternalMetadata{ + TMDB: &api.TMDBMetadata{ + RetrievedAKA: "AKA Alternative Title", + }, + }, + } + got := buildUnit3DName("LT", meta, config.TrackerConfig{}) + if strings.Contains(got, "Dual-Audio") || strings.Contains(got, "Dubbed") { + t.Fatalf("expected Dual-Audio and Dubbed to be removed, got %q", got) + } + }) + + t.Run("replaces English title with Spanish AKA when origLang is es", func(t *testing.T) { + meta := api.PreparedMetadata{ + ReleaseName: "Original.Title.2024.1080p.Bluray-GRP", + Tag: "GRP", + ExternalMetadata: api.ExternalMetadata{ + TMDB: &api.TMDBMetadata{ + Title: "Original Title", + OriginalLanguage: "es", + RetrievedAKA: "AKA Titulo en Español", + }, + }, + } + got := buildUnit3DName("LT", meta, config.TrackerConfig{}) + if !strings.Contains(got, "Titulo.en.Español") { + t.Fatalf("expected English title replaced by Spanish AKA, got %q", got) + } + }) + + t.Run("replaces Spanish title with English title when origLang is not es (foreign)", func(t *testing.T) { + meta := api.PreparedMetadata{ + ReleaseName: "Spanish.Title.2024.1080p.Bluray-GRP", + Tag: "GRP", + ExternalMetadata: api.ExternalMetadata{ + TMDB: &api.TMDBMetadata{ + Title: "English Title", + OriginalLanguage: "en", + RetrievedAKA: "Spanish Title", + }, + }, + } + got := buildUnit3DName("LT", meta, config.TrackerConfig{}) + if !strings.Contains(got, "English.Title") || strings.Contains(got, "Spanish.Title") { + t.Fatalf("expected Spanish title replaced by English title, got %q", got) + } + }) + + t.Run("removes AKA tag from Title.AKA.SpanishTitle pattern for foreign movies (keeping English)", func(t *testing.T) { + meta := api.PreparedMetadata{ + ReleaseName: "English.Title.AKA.Spanish.Title.2024.1080p.Bluray-GRP", + Tag: "GRP", + ExternalMetadata: api.ExternalMetadata{ + TMDB: &api.TMDBMetadata{ + Title: "English Title", + OriginalTitle: "English Title", + OriginalLanguage: "en", + RetrievedAKA: "AKA Spanish Title", + }, + }, + } + got := buildUnit3DName("LT", meta, config.TrackerConfig{}) + if !strings.Contains(got, "English.Title") || strings.Contains(got, "Spanish.Title") || strings.Contains(got, "AKA") { + t.Fatalf("expected English title kept and AKA/Spanish title removed, got %q", got) + } + }) + + t.Run("removes AKA tag from EnglishTitle.AKA.SpanishTitle pattern for Spanish original movies (keeping Spanish)", func(t *testing.T) { + meta := api.PreparedMetadata{ + ReleaseName: "English.Title.AKA.Spanish.Title.2024.1080p.Bluray-GRP", + Tag: "GRP", + ExternalMetadata: api.ExternalMetadata{ + TMDB: &api.TMDBMetadata{ + Title: "English Title", + OriginalTitle: "Spanish Title", + OriginalLanguage: "es", + RetrievedAKA: "AKA Spanish Title", + }, + }, + } + got := buildUnit3DName("LT", meta, config.TrackerConfig{}) + if !strings.Contains(got, "Spanish.Title") || strings.Contains(got, "English.Title") || strings.Contains(got, "AKA") { + t.Fatalf("expected Spanish title kept and AKA/English title removed, got %q", got) + } + }) + + t.Run("adds CAST tag when Castilian-only audio", func(t *testing.T) { + // Mock track list by using a temp file for MediaInfoJSONPath + tempFile, err := os.CreateTemp("", "mediainfo-*.json") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(tempFile.Name()) + + mediaInfoJSON := `{ + "media": { + "track": [ + {"@type": "General"}, + {"@type": "Video"}, + {"@type": "Audio", "Language": "es", "Title": "Castellano"} + ] + } + }` + if _, err := tempFile.Write([]byte(mediaInfoJSON)); err != nil { + t.Fatalf("failed to write temp file: %v", err) + } + tempFile.Close() + + meta := api.PreparedMetadata{ + ReleaseName: "Movie.2024.1080p.Bluray-GRP", + Tag: "GRP", + MediaInfoJSONPath: tempFile.Name(), + } + got := buildUnit3DName("LT", meta, config.TrackerConfig{}) + if !strings.Contains(got, " [CAST]-GRP") { + t.Fatalf("expected [CAST] tag added before GRP, got %q", got) + } + }) + + t.Run("adds SUBS tag when no Spanish audio", func(t *testing.T) { + meta := api.PreparedMetadata{ + ReleaseName: "Movie.2024.1080p.Bluray-GRP", + Tag: "GRP", + AudioLanguages: []string{"English"}, + } + got := buildUnit3DName("LT", meta, config.TrackerConfig{}) + if !strings.Contains(got, " [SUBS]-GRP") { + t.Fatalf("expected [SUBS] tag added before GRP, got %q", got) + } + }) +} + +func TestResolveUnit3DLTCategoryID(t *testing.T) { + t.Run("default movie and TV categories", func(t *testing.T) { + metaMovie := api.PreparedMetadata{Type: "MOVIE"} + if got := resolveUnit3DCategoryIDForTracker("LT", metaMovie); got != "1" { + t.Fatalf("expected MOVIE category_id=1, got %q", got) + } + + metaTV := api.PreparedMetadata{ + Release: api.ReleaseInfo{Category: "TV"}, + } + if got := resolveUnit3DCategoryIDForTracker("LT", metaTV); got != "2" { + t.Fatalf("expected TV category_id=2, got %q", got) + } + }) + + t.Run("Anime category", func(t *testing.T) { + meta := api.PreparedMetadata{ + Release: api.ReleaseInfo{Category: "TV"}, + Anime: true, + } + if got := resolveUnit3DCategoryIDForTracker("LT", meta); got != "5" { + t.Fatalf("expected Anime category_id=5, got %q", got) + } + }) + + t.Run("Telenovela category", func(t *testing.T) { + meta := api.PreparedMetadata{ + Release: api.ReleaseInfo{Category: "TV"}, + ExternalMetadata: api.ExternalMetadata{ + TMDB: &api.TMDBMetadata{ + Keywords: "some keywords, novela, drama", + }, + }, + } + if got := resolveUnit3DCategoryIDForTracker("LT", meta); got != "8" { + t.Fatalf("expected Telenovela category_id=8, got %q", got) + } + }) + + t.Run("Turkish & Asian drama category", func(t *testing.T) { + meta := api.PreparedMetadata{ + Release: api.ReleaseInfo{Category: "TV"}, + ExternalMetadata: api.ExternalMetadata{ + TMDB: &api.TMDBMetadata{ + Genres: "action, drama, comedy", + OriginCountry: []string{"TR"}, + }, + }, + } + if got := resolveUnit3DCategoryIDForTracker("LT", meta); got != "20" { + t.Fatalf("expected Turkish drama category_id=20, got %q", got) + } + }) +} From 081c78fbb69e1fd0e7dfb2713d680d00a4cf8343 Mon Sep 17 00:00:00 2001 From: HichamLL04 Date: Sun, 12 Jul 2026 00:27:36 +0200 Subject: [PATCH 2/2] refactor(trackers): address review feedback for Lat-Team (LT) support --- internal/trackers/impl/unit3d/lt.go | 19 +++---- internal/trackers/impl/unit3d/names.go | 49 ++++++++-------- internal/trackers/impl/unit3d/upload_test.go | 59 +++++++++++++------- 3 files changed, 74 insertions(+), 53 deletions(-) diff --git a/internal/trackers/impl/unit3d/lt.go b/internal/trackers/impl/unit3d/lt.go index 429f4e28..2714dd67 100644 --- a/internal/trackers/impl/unit3d/lt.go +++ b/internal/trackers/impl/unit3d/lt.go @@ -15,14 +15,19 @@ func siteLTProfile() unit3DSiteProfile { } } +var ltAsianCountries = map[string]bool{ + "AE": true, "AF": true, "AM": true, "AZ": true, "BD": true, "BH": true, "BN": true, "BT": true, "CN": true, "CY": true, "GE": true, "HK": true, "ID": true, "IL": true, "IN": true, + "IQ": true, "IR": true, "JO": true, "JP": true, "KG": true, "KH": true, "KP": true, "KR": true, "KW": true, "KZ": true, "LA": true, "LB": true, "LK": true, "MM": true, "MN": true, + "MO": true, "MV": true, "MY": true, "NP": true, "OM": true, "PH": true, "PK": true, "PS": true, "QA": true, "SA": true, "SG": true, "SY": true, "TH": true, "TJ": true, "TL": true, + "TM": true, "TR": true, "TW": true, "UZ": true, "VN": true, "YE": true, +} + func resolveUnit3DLTCategoryID(meta api.PreparedMetadata) string { category := resolveUnit3DCategory(meta) categoryID := "1" // Default MOVIE if category == "TV" { categoryID = "2" // Default TV - } else if category != "MOVIE" { - categoryID = "1" } if category == "TV" { @@ -54,17 +59,9 @@ func resolveUnit3DLTCategoryID(meta api.PreparedMetadata) string { return "8" } - // Turkish & Asian - asianCountries := map[string]bool{ - "AE": true, "AF": true, "AM": true, "AZ": true, "BD": true, "BH": true, "BN": true, "BT": true, "CN": true, "CY": true, "GE": true, "HK": true, "ID": true, "IL": true, "IN": true, - "IQ": true, "IR": true, "JO": true, "JP": true, "KG": true, "KH": true, "KP": true, "KR": true, "KW": true, "KZ": true, "LA": true, "LB": true, "LK": true, "MM": true, "MN": true, - "MO": true, "MV": true, "MY": true, "NP": true, "OM": true, "PH": true, "PK": true, "PS": true, "QA": true, "SA": true, "SG": true, "SY": true, "TH": true, "TJ": true, "TL": true, - "TM": true, "TR": true, "TW": true, "UZ": true, "VN": true, "YE": true, - } - hasAsianCountry := false for _, c := range originCountries { - if asianCountries[strings.ToUpper(c)] { + if ltAsianCountries[strings.ToUpper(c)] { hasAsianCountry = true break } diff --git a/internal/trackers/impl/unit3d/names.go b/internal/trackers/impl/unit3d/names.go index 5f181308..69f85244 100644 --- a/internal/trackers/impl/unit3d/names.go +++ b/internal/trackers/impl/unit3d/names.go @@ -26,6 +26,23 @@ var noGroupTagPattern = regexp.MustCompile(`(?i)-(nogrp|nogroup|unknown|-unk-)`) var ( languageTagLookupOnce sync.Once languageTagLookup map[string]language.Tag + + markerPattern = regexp.MustCompile(`(?i)\.(19\d\d|20\d\d|3d|480p|576p|720p|1080p|1080i|2160p|4k|mkv|avi|mp4|remux|bluray|blu-ray|web-dl|webdl|web|hdtv|dvdrip|dvd|bd25|bd50|uhd)\b`) + akaPattern = regexp.MustCompile(`(?i)[. _-]aka[. _-]`) + multipleDotsPattern = regexp.MustCompile(`\.{2,}`) + multipleSpacesPattern = regexp.MustCompile(`\s{2,}`) + + audioLatinoCheck = map[string]bool{ + "es-419": true, "es-mx": true, "es-ar": true, "es-cl": true, "es-ve": true, + "es-bo": true, "es-co": true, "es-cr": true, "es-do": true, "es-ec": true, + "es-sv": true, "es-gt": true, "es-hn": true, "es-ni": true, "es-pa": true, + "es-py": true, "es-pe": true, "es-pr": true, "es-uy": true, + } + audioCastilianCheck = map[string]bool{ + "es": true, "es-es": true, + } + latinoKeywords = []string{"latino", "latin america"} + castilianKeywords = []string{"castellano"} ) func buildUnit3DName(tracker string, meta api.PreparedMetadata, cfg config.TrackerConfig) string { @@ -503,8 +520,7 @@ func buildLTName(name string, meta api.PreparedMetadata) string { // Find the end of the title block (start of year, resolution, source, etc.) titleEndIdx := len(ltName) - markerRegex := regexp.MustCompile(`(?i)\.(19\d\d|20\d\d|3d|480p|576p|720p|1080p|1080i|2160p|4k|mkv|avi|mp4|remux|bluray|blu-ray|web-dl|webdl|web|hdtv|dvdrip|dvd|bd25|bd50|uhd)\b`) - if loc := markerRegex.FindStringIndex(ltName); loc != nil { + if loc := markerPattern.FindStringIndex(ltName); loc != nil { titleEndIdx = loc[0] } @@ -524,12 +540,13 @@ func buildLTName(name string, meta api.PreparedMetadata) string { targetTitle = origTitle } } - targetTitleDotted := strings.ReplaceAll(targetTitle, " ", ".") - // If the title block contains AKA, or we need to replace a mismatched title: - akaRegex := regexp.MustCompile(`(?i)[. _-]aka[. _-]`) - if akaRegex.MatchString(titleBlock) || !strings.EqualFold(strings.ReplaceAll(titleBlock, ".", " "), targetTitle) { - titleBlock = targetTitleDotted + if targetTitle != "" { + targetTitleDotted := strings.ReplaceAll(targetTitle, " ", ".") + // If the title block contains AKA, or we need to replace a mismatched title: + if akaPattern.MatchString(titleBlock) || !strings.EqualFold(strings.ReplaceAll(titleBlock, ".", " "), targetTitle) { + titleBlock = targetTitleDotted + } } ltName = titleBlock + restOfName @@ -560,18 +577,6 @@ func buildLTName(name string, meta api.PreparedMetadata) string { hasLatino := false hasCastilian := false - audioLatinoCheck := map[string]bool{ - "es-419": true, "es-mx": true, "es-ar": true, "es-cl": true, "es-ve": true, - "es-bo": true, "es-co": true, "es-cr": true, "es-do": true, "es-ec": true, - "es-sv": true, "es-gt": true, "es-hn": true, "es-ni": true, "es-pa": true, - "es-py": true, "es-pe": true, "es-pr": true, "es-uy": true, - } - audioCastilianCheck := map[string]bool{ - "es": true, "es-es": true, - } - latinoKeywords := []string{"latino", "latin america"} - castilianKeywords := []string{"castellano"} - hasTracks := false if len(tracks) > 0 { for _, track := range tracks { @@ -651,10 +656,8 @@ func buildLTName(name string, meta api.PreparedMetadata) string { } } - multipleDots := regexp.MustCompile(`\.{2,}`) - ltName = multipleDots.ReplaceAllString(ltName, ".") - multipleSpaces := regexp.MustCompile(`\s{2,}`) - ltName = multipleSpaces.ReplaceAllString(ltName, " ") + ltName = multipleDotsPattern.ReplaceAllString(ltName, ".") + ltName = multipleSpacesPattern.ReplaceAllString(ltName, " ") return strings.Trim(ltName, ". ") } diff --git a/internal/trackers/impl/unit3d/upload_test.go b/internal/trackers/impl/unit3d/upload_test.go index f4f58c07..48e4220b 100644 --- a/internal/trackers/impl/unit3d/upload_test.go +++ b/internal/trackers/impl/unit3d/upload_test.go @@ -1900,8 +1900,8 @@ func TestBuildUnit3DNameLT(t *testing.T) { }, } got := buildUnit3DName("LT", meta, config.TrackerConfig{}) - if strings.Contains(got, "Dual-Audio") || strings.Contains(got, "Dubbed") { - t.Fatalf("expected Dual-Audio and Dubbed to be removed, got %q", got) + if strings.Contains(got, "Dual-Audio") || strings.Contains(got, "Dubbed") || strings.Contains(got, "AKA") { + t.Fatalf("expected Dual-Audio, Dubbed, and AKA to be removed, got %q", got) } }) @@ -1980,13 +1980,8 @@ func TestBuildUnit3DNameLT(t *testing.T) { }) t.Run("adds CAST tag when Castilian-only audio", func(t *testing.T) { - // Mock track list by using a temp file for MediaInfoJSONPath - tempFile, err := os.CreateTemp("", "mediainfo-*.json") - if err != nil { - t.Fatalf("failed to create temp file: %v", err) - } - defer os.Remove(tempFile.Name()) - + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "mediainfo.json") mediaInfoJSON := `{ "media": { "track": [ @@ -1996,31 +1991,57 @@ func TestBuildUnit3DNameLT(t *testing.T) { ] } }` - if _, err := tempFile.Write([]byte(mediaInfoJSON)); err != nil { + if err := os.WriteFile(filePath, []byte(mediaInfoJSON), 0644); err != nil { t.Fatalf("failed to write temp file: %v", err) } - tempFile.Close() meta := api.PreparedMetadata{ ReleaseName: "Movie.2024.1080p.Bluray-GRP", Tag: "GRP", - MediaInfoJSONPath: tempFile.Name(), + MediaInfoJSONPath: filePath, + ExternalMetadata: api.ExternalMetadata{ + TMDB: &api.TMDBMetadata{ + Title: "Movie", + }, + }, } got := buildUnit3DName("LT", meta, config.TrackerConfig{}) - if !strings.Contains(got, " [CAST]-GRP") { - t.Fatalf("expected [CAST] tag added before GRP, got %q", got) + expected := "Movie.2024.1080p.Bluray [CAST]-GRP" + if got != expected { + t.Fatalf("expected name to be %q, got %q", expected, got) } }) t.Run("adds SUBS tag when no Spanish audio", func(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "mediainfo.json") + mediaInfoJSON := `{ + "media": { + "track": [ + {"@type": "General"}, + {"@type": "Video"}, + {"@type": "Audio", "Language": "en"} + ] + } + }` + if err := os.WriteFile(filePath, []byte(mediaInfoJSON), 0644); err != nil { + t.Fatalf("failed to write temp file: %v", err) + } + meta := api.PreparedMetadata{ - ReleaseName: "Movie.2024.1080p.Bluray-GRP", - Tag: "GRP", - AudioLanguages: []string{"English"}, + ReleaseName: "Movie.2024.1080p.Bluray-GRP", + Tag: "GRP", + MediaInfoJSONPath: filePath, + ExternalMetadata: api.ExternalMetadata{ + TMDB: &api.TMDBMetadata{ + Title: "Movie", + }, + }, } got := buildUnit3DName("LT", meta, config.TrackerConfig{}) - if !strings.Contains(got, " [SUBS]-GRP") { - t.Fatalf("expected [SUBS] tag added before GRP, got %q", got) + expected := "Movie.2024.1080p.Bluray [SUBS]-GRP" + if got != expected { + t.Fatalf("expected name to be %q, got %q", expected, got) } }) }