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
2 changes: 2 additions & 0 deletions internal/metadata/media_details.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,8 @@ func RebuildReleaseName(meta *api.PreparedMetadata, logger api.Logger) {
return
}

applyReleaseNameValueOverrides(meta)

nameRequest := releaseNameRequestFromMeta(*meta, logger)
nameRequest = applyReleaseNameOverrides(nameRequest, meta.ReleaseNameOverrides, logger)
nameResult := BuildReleaseName(nameRequest, logger)
Expand Down
111 changes: 111 additions & 0 deletions internal/metadata/naming_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -782,3 +782,114 @@ func (l *captureLogger) contains(value string) bool {
}
return false
}

func TestRebuildReleaseNameFoldsTagOverrideIntoMeta(t *testing.T) {
tag := "GRP"
noTag := true

meta := api.PreparedMetadata{
Type: "REMUX",
Source: "BluRay",
VideoCodec: "AVC",
SeasonStr: "S01",
Tag: "",
Release: api.ReleaseInfo{Title: "The Show", Year: 2021, Resolution: "1080p"},
ExternalIDs: api.ExternalIDs{Category: "TV"},
}
meta.ReleaseNameOverrides = api.ReleaseNameOverrides{Tag: &tag}

RebuildReleaseName(&meta, nil)
if meta.Tag != "-GRP" {
t.Fatalf("expected tag override folded into meta.Tag, got %q", meta.Tag)
}
if meta.Release.Group != "GRP" {
t.Fatalf("expected tag override folded into meta.Release.Group (resolveGroup prefers it), got %q", meta.Release.Group)
}
if !strings.HasSuffix(meta.ReleaseName, "-GRP") {
t.Fatalf("expected release name to carry the tag, got %q", meta.ReleaseName)
}

meta.ReleaseNameOverrides.NoTag = &noTag
RebuildReleaseName(&meta, nil)
if meta.Tag != "" {
t.Fatalf("expected NoTag to clear meta.Tag, got %q", meta.Tag)
}
}

func TestRebuildReleaseNameFoldsValueOverridesIntoMeta(t *testing.T) {
category := "TV"
releaseType := "REMUX"
source := "BluRay"
resolution := "1080p"
service := "NF"
edition := "Extended"
region := "EUR"
season := "S02"
episode := "E07"
episodeTitle := "The Finale"
year := 2019
dualAudio := true

meta := api.PreparedMetadata{
Type: "ENCODE",
Source: "WEB",
Service: "AMZN",
Edition: "Theatrical",
Region: "USA",
SeasonStr: "S01",
SeasonInt: 1,
EpisodeStr: "E01",
EpisodeInt: 1,
Audio: "FLAC 2.0",
VideoCodec: "AVC",
Release: api.ReleaseInfo{Title: "The Show", Year: 2021, Resolution: "2160p"},
ExternalIDs: api.ExternalIDs{Category: "MOVIE"},
}
meta.ReleaseNameOverrides = api.ReleaseNameOverrides{
Category: &category,
Type: &releaseType,
Source: &source,
Resolution: &resolution,
Service: &service,
Edition: &edition,
Region: &region,
Season: &season,
Episode: &episode,
EpisodeTitle: &episodeTitle,
ManualYear: &year,
DualAudio: &dualAudio,
}

RebuildReleaseName(&meta, nil)

checks := []struct {
field string
got string
want string
}{
{"ExternalIDs.Category", meta.ExternalIDs.Category, "TV"},
{"Type", meta.Type, "REMUX"},
{"Source", meta.Source, "BluRay"},
{"Release.Resolution", meta.Release.Resolution, "1080p"},
{"Service", meta.Service, "NF"},
{"Edition", meta.Edition, "Extended"},
{"Region", meta.Region, "EUR"},
{"SeasonStr", meta.SeasonStr, "S02"},
{"EpisodeStr", meta.EpisodeStr, "E07"},
{"EpisodeTitle", meta.EpisodeTitle, "The Finale"},
}
for _, check := range checks {
if check.got != check.want {
t.Errorf("%s: got %q, want %q", check.field, check.got, check.want)
}
}
if meta.SeasonInt != 2 || meta.EpisodeInt != 7 {
t.Errorf("season/episode numbers: got S%d E%d, want S2 E7", meta.SeasonInt, meta.EpisodeInt)
}
if meta.Release.Year != 2019 {
t.Errorf("Release.Year: got %d, want 2019", meta.Release.Year)
}
if !strings.Contains(meta.Audio, "Dual-Audio") {
t.Errorf("Audio: expected Dual-Audio marker, got %q", meta.Audio)
}
}
100 changes: 100 additions & 0 deletions internal/metadata/release_overrides.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
package metadata

import (
"strconv"
"strings"

"github.com/autobrr/upbrr/internal/metadata/seasonep"
"github.com/autobrr/upbrr/pkg/api"
)

Expand Down Expand Up @@ -105,6 +107,104 @@ func hasReleaseNameOverrides(overrides api.ReleaseNameOverrides) bool {
overrides.Region != nil
}

// applyReleaseNameValueOverrides folds the naming value overrides into the
// metadata itself. They otherwise reach only the release-name string, so every
// consumer that reads the component field keeps the parsed value the user just
// corrected: the UNIT3D payload ids (category_id, type_id, resolution_id,
// season_number, episode_number), the tracker rules, the dupe checks, and the
// trackers that rebuild the name from components instead of ReleaseName (UTP,
// RHD, TVC, ASC).
//
// Only value overrides belong here. The name-suppression toggles (NoSeason,
// NoYear, NoAKA) stay naming-only — dropping season_number from the payload
// because the user hid the season in the name would be a different change.
func applyReleaseNameValueOverrides(meta *api.PreparedMetadata) {
if meta == nil {
return
}
overrides := meta.ReleaseNameOverrides

if overrides.Category != nil {
if category := normalizeNamingCategory(*overrides.Category); category != "" {
meta.ExternalIDs.Category = category
}
}
if overrides.Type != nil {
meta.Type = strings.TrimSpace(*overrides.Type)
}
if overrides.Source != nil {
meta.Source = strings.TrimSpace(*overrides.Source)
}
if overrides.Resolution != nil {
meta.Release.Resolution = strings.TrimSpace(*overrides.Resolution)
}
if overrides.Service != nil {
meta.Service = strings.TrimSpace(*overrides.Service)
}
if overrides.Region != nil {
meta.Region = strings.TrimSpace(*overrides.Region)
}
if overrides.EpisodeTitle != nil {
meta.EpisodeTitle = strings.TrimSpace(*overrides.EpisodeTitle)
}
if overrides.ManualYear != nil && *overrides.ManualYear > 0 {
meta.Release.Year = *overrides.ManualYear
}

if overrides.Edition != nil {
meta.Edition = strings.TrimSpace(*overrides.Edition)
}
if overrides.NoEdition != nil && *overrides.NoEdition {
meta.Edition = ""
}

if overrides.Season != nil {
meta.SeasonInt = parseSeasonEpisodeNumber(*overrides.Season)
meta.SeasonStr = seasonep.FormatSeason(meta.SeasonInt)
}
if overrides.Episode != nil {
meta.EpisodeInt = parseSeasonEpisodeNumber(*overrides.Episode)
meta.EpisodeStr = seasonep.FormatEpisode(meta.EpisodeInt)
}

meta.Audio = applyAudioOverrides(meta.Audio, overrides)

// Release.Group moves with the tag: resolveGroup (trackers/rules.go,
// unit3d/additional) prefers it over meta.Tag, so leaving it behind keeps the
// parsed group in the banned-group and internal-group checks.
if overrides.Tag != nil {
tag := strings.TrimSpace(*overrides.Tag)
if tag != "" && !strings.HasPrefix(tag, "-") {
tag = "-" + tag
}
meta.Tag = tag
meta.Release.Group = strings.TrimPrefix(tag, "-")
}
if overrides.NoTag != nil && *overrides.NoTag {
meta.Tag = ""
meta.Release.Group = ""
}
}

// parseSeasonEpisodeNumber reads the number out of a manual season/episode entry,
// accepting both the "S01"/"E05" tokens the GUI shows and a bare number.
func parseSeasonEpisodeNumber(value string) int {
digits := strings.Map(func(r rune) rune {
if r >= '0' && r <= '9' {
return r
}
return -1
}, strings.TrimSpace(value))
if digits == "" {
return 0
}
number, err := strconv.Atoi(digits)
if err != nil {
return 0
}
return number
}

func applyReleaseNameOverrides(req api.ReleaseNameRequest, overrides api.ReleaseNameOverrides, logger api.Logger) api.ReleaseNameRequest {
if logger == nil {
logger = api.NopLogger{}
Expand Down
Loading