Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions internal/trackers/impl/unit3d/additional/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var trackerRuleFactories = map[string]func() RuleSet{
"DP": rulesDP,
"HHD": rulesHHD,
"LST": rulesLST,
"LT": rulesLT,
"LUME": rulesLUME,
"MNS": rulesMNS,
"OE": rulesOE,
Expand Down Expand Up @@ -279,3 +280,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,
},
}
}
76 changes: 76 additions & 0 deletions internal/trackers/impl/unit3d/lt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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,
}
}

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
}

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"
}

hasAsianCountry := false
for _, c := range originCountries {
if ltAsianCountries[strings.ToUpper(c)] {
hasAsianCountry = true
break
}
}

if strings.Contains(genres, "drama") && hasAsianCountry {
return "20"
}
}

return categoryID
}
198 changes: 198 additions & 0 deletions internal/trackers/impl/unit3d/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
package unit3d

import (
"encoding/json"
"fmt"
"os"
"regexp"
"strconv"
"strings"
Expand All @@ -23,6 +26,23 @@
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 {
Expand All @@ -49,6 +69,8 @@
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":
Expand Down Expand Up @@ -474,3 +496,179 @@
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)
if loc := markerPattern.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
}
}

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

isDisc := false

Check failure on line 554 in internal/trackers/impl/unit3d/names.go

View workflow job for this annotation

GitHub Actions / golangci-lint

QF1007: could merge conditional assignment into variable declaration (staticcheck)
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

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 "-<tag>" 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")
}
}
}

ltName = multipleDotsPattern.ReplaceAllString(ltName, ".")
ltName = multipleSpacesPattern.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 ""
}
1 change: 1 addition & 0 deletions internal/trackers/impl/unit3d/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var unit3DSiteProfiles = map[string]unit3DSiteProfile{
"ITT": siteITTProfile(),
"LDU": siteLDUProfile(),
"LST": siteLSTProfile(),
"LT": siteLTProfile(),
"OE": siteOEProfile(),
"OTW": siteOTWProfile(),
"PT": sitePTProfile(),
Expand Down
Loading
Loading