package title import ( "errors" "fmt" "slices" "strconv" "strings" "time" "unicode" ) type ContentTitle struct { Title string ContentType struct { Name string Version int Modifiers []string } ProjectorAspectRatio struct { Name string Ratio int } Language ContentTitleLanguage Territory string Rating string Audio struct { Name string Modifiers []string } Resolution string Studio string Date string Facility string Standard struct { Name string Modifiers []string } PackageType string PackageVersion int } func (t ContentTitle) Time() time.Time { if t.Date == "" { return time.Time{} } result, err := time.ParseInLocation("20060102", t.Date, time.UTC) if err != nil { return time.Time{} } return result } func (t ContentTitle) NormalizedTitle() string { parts := strings.Split(t.Title, "-") if len(parts) == 1 { return parts[0] } for i := range parts { if len(parts[i]) > 0 { parts[i] = strings.ToUpper(parts[i][:1]) + strings.ToLower(parts[i][1:]) } } return strings.Join(parts, "") } type ContentTitleLanguage struct { Audio string Subtitle string SubtitleKind string } func (l ContentTitleLanguage) HasSubtitle() bool { return (l.Subtitle != "" && l.Subtitle != "XX") || l.SubtitleKind != "" } func (l ContentTitleLanguage) IsBurnedIn() bool { return l.Subtitle != "" && strings.ToLower(l.Subtitle) == l.Subtitle && strings.ToUpper(l.SubtitleKind) != "CCAP" } var KnownContentTypes = []string{ "FTR", "SHR", "EPS", "TLR", "TSR", "PRO", "CPL", "STR", "RTG", "POL", "PSA", "ADV", "XSN", "TST", } var KnownAudioTypes = []string{ "10", "20", "30", "40", "50", "51", "71", "MOS", } var KnownResolutions = []string{ // standard naming "2K", "4K", // 2K full frame "2048x1080", // 2K flat "1998x1080", // 2K scope "2048x858", // 2K HD, non-standard "1920x1080", // 4K full frame "4096x2160", // 4K flat "3996x2160", // 4K scope "4096x1716", // 4K UHD, non-standard "3840x2160", } var KnownStandards = []string{ "IOP", "SMPTE", } var KnownPackageTypes = []string{ "OV", "VF", } var KnownSubtitleTypes = []string{ "OCAP", "CCAP", //alternate "OSUB", } var KnownProjectorAspectRatios = []string{ // full frame "C", // flat "F", // scope "S", } func ParseContentTitle(contentTitleText string) (t ContentTitle, err error) { entries := strings.Split(strings.ReplaceAll(contentTitleText, "__", "_"), "_") if len(entries) <= 1 { return t, errors.New("empty") } current := func() string { return entries[0] } advance := func() { entries = entries[1:] } t.Title = current() advance() var state int for i := range entries { entry := entries[i] parts := strings.Split(entry, "-") switch state { case 0: //content type if slices.Contains(KnownContentTypes, parts[0]) { t.ContentType.Name = parts[0] parts = parts[1:] if len(parts) > 0 { versionNumber, err := strconv.ParseUint(parts[0], 10, 0) if err == nil { t.ContentType.Version = int(versionNumber) } parts = parts[1:] } if len(parts) > 0 { t.ContentType.Modifiers = parts } state++ break } //keep filling name t.Title += "_" + strings.Join(parts, "-") case 1: //projector aspect ratio if slices.Contains(KnownProjectorAspectRatios, parts[0]) { t.ProjectorAspectRatio.Name = parts[0] parts = parts[1:] if len(parts) > 0 { ar, err := strconv.ParseUint(parts[0], 10, 0) if err == nil { t.ProjectorAspectRatio.Ratio = int(ar) } parts = parts[1:] } if len(parts) > 0 { return t, fmt.Errorf("unsupported extra modifiers %s for %s", strings.Join(parts, "-"), entry) } state++ break } //sometimes not included state++ fallthrough case 2: //language isNumber := true for _, r := range parts[0] { if !unicode.IsNumber(r) { isNumber = false break } } if !isNumber { if len(parts[0]) < 2 || len(parts[0]) > 4 { return t, fmt.Errorf("invalid language %s for %s", parts[0], entry) } t.Language.Audio = parts[0] parts = parts[1:] if len(parts) > 0 && parts[0] == "MUS" { //skip MUS??? t.Language.SubtitleKind = parts[0] parts = parts[1:] } if len(parts) > 0 { if len(parts[0]) < 2 || len(parts[0]) > 4 { return t, fmt.Errorf("invalid language %s for %s", parts[0], entry) } t.Language.Subtitle = parts[0] parts = parts[1:] } if len(parts) > 0 { if !slices.Contains(KnownSubtitleTypes, parts[0]) { return t, fmt.Errorf("invalid subtitle kind %s for %s", parts[0], entry) } t.Language.SubtitleKind = parts[0] parts = parts[1:] } if len(parts) > 0 { return t, fmt.Errorf("unsupported extra modifiers %s for %s", strings.Join(parts, "-"), entry) } state++ break } //sometimes lang is not there state++ fallthrough case 3: //territory and rating if !slices.Contains(KnownAudioTypes, parts[0]) { if slices.Contains(KnownSubtitleTypes, parts[0]) { //sometimes they leak through t.Language.SubtitleKind = parts[0] parts = parts[1:] if len(parts) > 0 { return t, fmt.Errorf("unsupported extra modifiers %s for %s", strings.Join(parts, "-"), entry) } //repeat selection break } if parts[0] != "NULL" && (len(parts[0]) < 2 || len(parts[0]) > 3) { return t, fmt.Errorf("invalid territory %s for %s", parts[0], entry) } t.Territory = parts[0] parts = parts[1:] if len(parts) > 0 { /*if parts[0] != "NULL" && (len(parts[0]) < 1 || len(parts[0]) > 3) { return t, fmt.Errorf("invalid rating %s for %s", parts[0], entry) }*/ t.Rating = parts[0] parts = parts[1:] } if len(parts) > 0 { return t, fmt.Errorf("unsupported extra modifiers %s for %s", strings.Join(parts, "-"), entry) } state++ break } state++ fallthrough case 4: //audio type if !slices.Contains(KnownAudioTypes, parts[0]) { return t, fmt.Errorf("unknown audio type %s for %s", parts[0], entry) } t.Audio.Name = parts[0] parts = parts[1:] if len(parts) > 0 { t.Audio.Modifiers = parts } state++ case 5: //resolution if slices.Contains(KnownResolutions, parts[0]) { t.Resolution = parts[0] parts = parts[1:] if len(parts) > 0 { return t, fmt.Errorf("unsupported extra modifiers %s for %s", strings.Join(parts, "-"), entry) } state++ break } //sometimes skipped state++ fallthrough case 6: //studio code isNumber := true for _, r := range parts[0] { if !unicode.IsNumber(r) { isNumber = false break } } if !isNumber { //if len(parts[0]) >= 2 && len(parts[0]) <= 4 { //a studio code t.Studio = parts[0] parts = parts[1:] if len(parts) > 0 { return t, fmt.Errorf("unsupported extra modifiers %s for %s", strings.Join(parts, "-"), entry) } state++ break //} } state++ fallthrough case 7: //date for _, r := range parts[0] { if !unicode.IsNumber(r) { return t, fmt.Errorf("unknown date %s for %s", parts[0], entry) } } t.Date = parts[0] parts = parts[1:] if len(parts) > 0 { return t, fmt.Errorf("unsupported extra modifiers %s for %s", strings.Join(parts, "-"), entry) } state++ case 8: //facility code if !slices.Contains(KnownStandards, parts[0]) && !slices.Contains(KnownPackageTypes, parts[0]) { //a facility code t.Facility = parts[0] parts = parts[1:] if len(parts) > 0 { return t, fmt.Errorf("unsupported extra modifiers %s for %s", strings.Join(parts, "-"), entry) } state++ break } state++ fallthrough case 9: //standard if slices.Contains(KnownStandards, parts[0]) { // a standard t.Standard.Name = parts[0] parts = parts[1:] if len(parts) > 0 { t.Standard.Modifiers = parts } state++ break } state++ fallthrough case 10: //package type if !slices.Contains(KnownPackageTypes, parts[0]) { return t, fmt.Errorf("unknown package type %s for %s", parts[0], entry) } t.PackageType = parts[0] parts = parts[1:] if len(parts) > 0 { pv, err := strconv.ParseUint(parts[0], 10, 0) if err == nil { t.PackageVersion = int(pv) } parts = parts[1:] } if len(parts) > 0 { return t, fmt.Errorf("unsupported extra modifiers %s for %s", strings.Join(parts, "-"), entry) } state++ default: return t, fmt.Errorf("unmatched state %d", state) } } if state != 11 { return t, fmt.Errorf("not enough fields reached %d", state) } return t, nil }