go-dcp/dcp/dcp.go

454 lines
10 KiB
Go

package dcp
import (
"encoding/xml"
"errors"
"fmt"
"github.com/google/uuid"
"io"
"net/http"
"os"
"path"
"slices"
"strconv"
"strings"
"sync"
)
type DCP struct {
Path string
AssetMapFile string
AssetMap AssetMap
pklLock sync.Mutex
pkl []PackingList
}
func (dcp *DCP) IsIOP() bool {
return dcp.AssetMap.IsIOP()
}
func (dcp *DCP) IsSMPTE() bool {
return dcp.AssetMap.IsSMPTE()
}
func (dcp *DCP) GetPackingLists() (pkls []PackingList, err error) {
dcp.pklLock.Lock()
defer dcp.pklLock.Unlock()
if dcp.pkl != nil {
return dcp.pkl, nil
}
pkls = make([]PackingList, 0)
for _, l := range dcp.AssetMap.GetPackingLists() {
pkl, err := dcp.GetPackingList(l.Id)
if err != nil {
return nil, err
}
pkls = append(pkls, pkl)
}
dcp.pkl = pkls
return pkls, nil
}
func (dcp *DCP) GetPackingList(id string) (pkl PackingList, err error) {
assetPath, ok := dcp.GetAssetPath(id)
if !ok {
return PackingList{}, fmt.Errorf("could not find asset path for PKL %s", id)
}
xmlData, err := dcp.readFile(assetPath)
if err != nil {
return PackingList{}, err
}
err = xml.Unmarshal(xmlData, &pkl)
if err != nil {
return PackingList{}, err
}
if !(pkl.IsSMPTE() && dcp.IsSMPTE()) && !(pkl.IsIOP() && dcp.IsIOP()) {
return pkl, fmt.Errorf("unexpected namespace %s", pkl.XMLName.Space)
}
return pkl, nil
}
func (dcp *DCP) GetCompositionPlaylist(id string) (cpl CompositionPlaylist, err error) {
assetPath, ok := dcp.GetAssetPath(id)
if !ok {
return CompositionPlaylist{}, fmt.Errorf("could not find asset path for CPL %s", id)
}
xmlData, err := dcp.readFile(assetPath)
if err != nil {
return CompositionPlaylist{}, err
}
err = xml.Unmarshal(xmlData, &cpl)
if err != nil {
return CompositionPlaylist{}, err
}
if !(cpl.IsSMPTE() && dcp.IsSMPTE()) && !(cpl.IsIOP() && dcp.IsIOP()) {
return cpl, fmt.Errorf("unexpected namespace %s", cpl.XMLName.Space)
}
return cpl, nil
}
func (dcp *DCP) GetAbsoluteAssetPath(id string) (assetPath string, ok bool) {
for _, a := range dcp.AssetMap.Assets {
if a.Id == id && len(a.Chunks) > 0 {
return path.Join(dcp.Path, a.Chunks[0].Path), true
}
}
return "", false
}
func (dcp *DCP) GetAssetPaths() map[string]string {
result := make(map[string]string)
for _, a := range dcp.AssetMap.Assets {
if len(a.Chunks) > 0 {
result[a.Id] = a.Chunks[0].Path
}
}
return result
}
func (dcp *DCP) GetAssetPath(id string) (assetPath string, ok bool) {
for _, a := range dcp.AssetMap.Assets {
if a.Id == id && len(a.Chunks) > 0 {
return a.Chunks[0].Path, true
}
}
return "", false
}
func (dcp *DCP) SplitPackingList(id string, copyFile func(assetName string, size int64, sourcePath string) error, openWriter func(assetName string, size int64) (io.WriteCloser, error)) (err error) {
if dcp.isHTTP() {
return errors.New("url path not supported")
}
pkl, err := dcp.GetPackingList(id)
if err != nil {
return err
}
pkls, err := dcp.GetPackingLists()
uniquePKL := err == nil && len(pkls) == 1
pklBool := "true"
var assets []AssetMapEntry
pklPath, ok := dcp.GetAssetPath(id)
if !ok {
return errors.New("unknown PKL path")
}
pklSize, err := dcp.statFile(pklPath)
if err != nil {
}
for _, a := range pkl.Assets {
assetPath, ok := dcp.GetAssetPath(a.Id)
if !ok {
return errors.New("unknown asset path")
}
if _, err = dcp.statFile(assetPath); err != nil {
return err
}
}
assets = append(assets, AssetMapEntry{
Id: pkl.Id,
PackingList: &pklBool,
Chunks: []AssetMapChunk{
{
Path: pklPath,
Length: &pklSize,
},
},
})
if err = copyFile(pklPath, pklSize, path.Join(dcp.Path, pklPath)); err != nil {
return err
}
for _, a := range pkl.Assets {
assetPath, ok := dcp.GetAssetPath(a.Id)
if !ok {
return errors.New("unknown asset path")
}
absoluteAssetPath := path.Join(dcp.Path, assetPath)
assets = append(assets, AssetMapEntry{
Id: a.Id,
Chunks: []AssetMapChunk{
{
Path: assetPath,
Length: &a.Size,
},
},
})
if err = copyFile(assetPath, pklSize, absoluteAssetPath); err != nil {
return err
}
}
createAM, createVI := true, true
if uniquePKL {
//try to directly link assetmap / volindex
if size, err := dcp.statFile(dcp.AssetMapFile); err == nil {
if err = copyFile(dcp.AssetMapFile, size, path.Join(dcp.Path, dcp.AssetMapFile)); err != nil {
return err
}
createAM = false
}
if dir, err := dcp.readDir(); err == nil {
for _, e := range dir {
if (e.Name() == "VOLINDEX" || e.Name() == "VOLINDEX.xml") && !e.IsDir() {
if finfo, err := e.Info(); err == nil {
if err = copyFile(e.Name(), finfo.Size(), path.Join(dcp.Path, e.Name())); err != nil {
return err
}
createVI = false
break
}
}
}
}
}
if createAM {
slices.SortFunc(assets, func(a, b AssetMapEntry) int {
return strings.Compare(a.Chunks[0].Path, b.Chunks[0].Path)
})
assetMapUUID, err := uuid.NewRandom()
if err != nil {
return err
}
if dcp.IsSMPTE() {
xmlData, err := xml.MarshalIndent(assetMapSMPTE{
AssetMap: AssetMap{
Id: "urn:uuid:" + assetMapUUID.String(),
AnnotationText: pkl.AnnotationText,
VolumeCount: 1,
IssueDate: pkl.IssueDate,
Issuer: pkl.Issuer,
Creator: pkl.Creator,
Assets: assets,
},
}, "", " ")
if err != nil {
return err
}
xmlData = append([]byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"), xmlData...)
writer, err := openWriter("ASSETMAP.xml", int64(len(xmlData)))
if err != nil {
return err
}
defer writer.Close()
_, err = writer.Write(xmlData)
if err != nil {
return err
}
} else {
xmlData, err := xml.MarshalIndent(assetMapIOP{
AssetMap: AssetMap{
Id: "urn:uuid:" + assetMapUUID.String(),
AnnotationText: pkl.AnnotationText,
VolumeCount: 1,
IssueDate: pkl.IssueDate,
Issuer: pkl.Issuer,
Creator: pkl.Creator,
Assets: assets,
},
}, "", " ")
if err != nil {
return err
}
xmlData = append([]byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"), xmlData...)
writer, err := openWriter("ASSETMAP", int64(len(xmlData)))
if err != nil {
return err
}
defer writer.Close()
_, err = writer.Write(xmlData)
if err != nil {
return err
}
}
}
if createVI {
if dcp.IsSMPTE() {
xmlData, err := xml.MarshalIndent(volumeIndexSMPTE{
VolumeIndex: VolumeIndex{
Index: 1,
},
}, "", " ")
if err != nil {
return err
}
xmlData = append([]byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"), xmlData...)
writer, err := openWriter("VOLINDEX.xml", int64(len(xmlData)))
if err != nil {
return err
}
defer writer.Close()
_, err = writer.Write(xmlData)
if err != nil {
return err
}
} else {
xmlData, err := xml.MarshalIndent(volumeIndexIOP{
VolumeIndex: VolumeIndex{
Index: 1,
},
}, "", " ")
if err != nil {
return err
}
xmlData = append([]byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"), xmlData...)
writer, err := openWriter("VOLINDEX", int64(len(xmlData)))
if err != nil {
return err
}
defer writer.Close()
_, err = writer.Write(xmlData)
if err != nil {
return err
}
}
}
return nil
}
func (dcp *DCP) isHTTP() bool {
return strings.HasPrefix(dcp.Path, "http://") || strings.HasPrefix(dcp.Path, "https://")
}
func (dcp *DCP) readDir() (dir []os.DirEntry, err error) {
if dcp.isHTTP() {
return nil, errors.New("url path not supported")
}
return os.ReadDir(dcp.Path)
}
func (dcp *DCP) statFile(relPath string) (size int64, err error) {
if dcp.isHTTP() {
response, err := http.DefaultClient.Head(strings.Join(append(strings.Split(dcp.Path, "/"), relPath), "/"))
if err != nil {
return 0, err
}
defer response.Body.Close()
defer io.ReadAll(response.Body)
if response.StatusCode == http.StatusOK {
size, err = strconv.ParseInt(response.Header.Get("Content-Length"), 10, 0)
if err != nil {
return 0, err
}
return size, nil
}
return 0, fmt.Errorf("status code %d: %s", response.StatusCode, response.Status)
}
stat, err := os.Stat(path.Join(dcp.Path, relPath))
if err != nil {
return 0, err
}
return stat.Size(), nil
}
func (dcp *DCP) filePath(relPath string) string {
if dcp.isHTTP() {
return strings.Join(append(strings.Split(dcp.Path, "/"), relPath), "/")
}
return path.Join(dcp.Path, relPath)
}
func (dcp *DCP) readFile(relPath string) (data []byte, err error) {
if dcp.isHTTP() {
response, err := http.DefaultClient.Get(strings.Join(append(strings.Split(dcp.Path, "/"), relPath), "/"))
if err != nil {
return nil, err
}
defer response.Body.Close()
defer io.ReadAll(response.Body)
if response.StatusCode == http.StatusOK {
return io.ReadAll(response.Body)
}
return nil, fmt.Errorf("status code %d: %s", response.StatusCode, response.Status)
}
return os.ReadFile(path.Join(dcp.Path, relPath))
}
// Open Opens a DCP. Provide path to its directory or its ASSETMAP file, which can also be an URL
func Open(dcpPath string) (*DCP, error) {
assetMapBase := path.Base(dcpPath)
dirPath := path.Dir(dcpPath)
if !strings.HasPrefix(dcpPath, "http://") && !strings.HasPrefix(dcpPath, "https://") {
stat, err := os.Stat(dcpPath)
if err != nil {
return nil, err
}
// Found a directory. Try to find ASSETMAP
if stat.IsDir() {
dir, err := os.ReadDir(dcpPath)
if err != nil {
return nil, err
}
if i := slices.IndexFunc(dir, func(entry os.DirEntry) bool {
return strings.HasPrefix(strings.ToUpper(entry.Name()), "ASSETMAP")
}); i == -1 {
return nil, errors.New("could not find ASSETMAP")
} else {
assetMapBase = path.Base(path.Join(dcpPath, dir[i].Name()))
dirPath = path.Dir(path.Join(dcpPath, dir[i].Name()))
}
}
} else {
pathParts := strings.Split(dcpPath, "/")
assetMapBase = pathParts[len(pathParts)-1]
dirPath = strings.Join(pathParts[0:len(pathParts)-1], "/")
}
dcp := &DCP{
Path: dirPath,
AssetMapFile: assetMapBase,
}
xmlData, err := dcp.readFile(assetMapBase)
if err != nil {
return nil, err
}
var assetMap AssetMap
err = xml.Unmarshal(xmlData, &assetMap)
if err != nil {
return nil, err
}
if assetMap.XMLName.Local != "AssetMap" {
return nil, fmt.Errorf("unexpected name %s", assetMap.XMLName.Local)
}
if !assetMap.IsSMPTE() && !assetMap.IsIOP() {
return nil, fmt.Errorf("unexpected namespace %s", assetMap.XMLName.Space)
}
dcp.AssetMap = assetMap
return dcp, nil
}