454 lines
10 KiB
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
|
|
}
|