go-dcp/title/naming.go
2024-07-17 00:39:30 +02:00

473 lines
8.6 KiB
Go

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
}