473 lines
8.6 KiB
Go
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
|
|
}
|