Commit b4193b2

bryfry <bryon.fryer@gmail.com>
2016-04-17 01:17:47
init
Changed files (2)
build.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+GOOS=windows GOARCH=386 go build -o /Volumes/production/video/post/fcpxml2vtt.exe fcpxml2vtt.go
+GOOS=linux GOARCH=amd64 go build -o /Volumes/production/video/post/fcpxml2vtt.linux fcpxml2vtt.go
+GOOS=darwin GOARCH=amd64 go build -o /Volumes/production/video/post/fcpxml2vtt.darwin fcpxml2vtt.go
fcpxml2vtt.go
@@ -0,0 +1,203 @@
+package main
+
+import (
+	"encoding/xml"
+	"fmt"
+	"math"
+	"os"
+	"path"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/beevik/etree"
+	"github.com/dchest/uniuri"
+)
+
+// Sometimes Chapters are inside a transition :/
+type Transition struct {
+	Start_s string  `xml:"offset,attr"`
+	Chapter Chapter `xml:"chapter-marker"`
+}
+
+// Sometimes Chapters are inside a clip :/
+type Clip struct {
+	Start_s string  `xml:"offset,attr"`
+	Chapter Chapter `xml:"chapter-marker"`
+}
+
+// "_s" values are string fcpxml representations of time, they need extra attention to be
+// parsed, converted, and stored as their non-"_s" float values
+type Chapter struct {
+	Value      string `xml:"value,attr"`
+	Start_s    string `xml:"start,attr"`
+	Duration_s string `xml:"posterOffset,attr"`
+	Start      float64
+	Duration   float64
+}
+
+func fcpxTimeConv(s string) (f float64, err error) {
+	s = strings.TrimSuffix(s, "s")
+	fraction := strings.Split(s, "/")
+	if len(fraction) == 2 {
+
+		numer, err := strconv.ParseFloat(fraction[0], 64)
+		if err != nil {
+			return f, err
+		}
+		denom, err := strconv.ParseFloat(fraction[1], 64)
+		if err != nil {
+			return f, err
+		}
+
+		f = numer / denom
+		return f, err
+
+	} else if len(fraction) == 1 {
+		return strconv.ParseFloat(fraction[0], 64)
+
+	} else {
+		fmt.Println("not a fraction!")
+		return f, fmt.Errorf("unrecognized fcp time structure")
+	}
+
+}
+
+func fcpxParseChapters(filepath string) (chapters []Chapter, err error) {
+	doc := etree.NewDocument()
+	if err := doc.ReadFromFile(filepath); err != nil {
+		return chapters, err
+	}
+
+	// find the pesky chapter-markers... parents
+	for _, e := range doc.FindElements("//chapter-marker/..") {
+
+		// encode to unmarshall.. yes really
+		doc := etree.NewDocument()
+		doc.SetRoot(e.Copy())
+		b, _ := doc.WriteToBytes()
+
+		if e.Tag == "transition" {
+
+			var tr Transition
+			err = xml.Unmarshal(b, &tr)
+			if err != nil {
+				fmt.Println("Unmarshaling transition failed", tr, err)
+				return chapters, err
+			}
+			if tr.Chapter != (Chapter{}) {
+
+				tr.Chapter.Start, err = fcpxTimeConv(tr.Start_s)
+				if err != nil {
+					fmt.Println("Transition start timeconf failed: ", err)
+					return chapters, err
+				}
+
+				tr.Chapter.Duration, err = fcpxTimeConv(tr.Chapter.Duration_s)
+				if err != nil {
+					fmt.Println("Chapter duration timeconf failed: ", err)
+					return chapters, err
+				}
+				chapters = append(chapters, tr.Chapter)
+			}
+
+		} else if e.Tag == "clip" {
+
+			var cl Clip
+			xml.Unmarshal(b, &cl)
+			if err != nil {
+				fmt.Println("Unmarshaling clip failed", cl, err)
+				return chapters, err
+			}
+
+			if cl.Chapter != (Chapter{}) {
+				cl.Chapter.Start, err = fcpxTimeConv(cl.Start_s)
+				if err != nil {
+					fmt.Println("Clip start timeconf failed: ", err)
+					continue
+				}
+				cl.Chapter.Duration, err = fcpxTimeConv(cl.Chapter.Duration_s)
+				if err != nil {
+					fmt.Println("Chapter duration timeconf failed: ", err)
+					continue
+				}
+				chapters = append(chapters, cl.Chapter)
+			}
+
+		} else {
+
+			fmt.Println("Unexpected chapter-marker parent type!")
+			return chapters, err
+
+		}
+	}
+	return chapters, nil
+}
+
+func vttTimeFmt(seconds float64) string {
+	t := time.Duration(seconds*1000) * time.Millisecond
+	h := math.Floor(t.Hours())
+	m := math.Floor((t - time.Duration(h)*time.Hour).Minutes())
+	s := (t - time.Duration(m)*time.Minute).Seconds()
+	return fmt.Sprintf("%02.f:%02.f:%06.3f", h, m, s)
+}
+
+func vttCreateFile(chapters []Chapter, filename string) error {
+	os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0700)
+	f, err := os.Create(filename)
+	defer f.Close()
+	if err != nil {
+		return err
+	}
+
+	_, err = f.WriteString("WEBVTT\n\n")
+	if err != nil {
+		return err
+	}
+
+	for _, c := range chapters {
+		id := uniuri.NewLen(8)
+		vttString := fmt.Sprintf("%s --> %s\n%s,%s\n\n", vttTimeFmt(c.Start), vttTimeFmt(c.Start+c.Duration), id, c.Value)
+		_, err = f.WriteString(vttString)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func main() {
+	files, _ := filepath.Glob("*.fcpxml")
+	if len(files) == 0 {
+		fmt.Println("No .fcpxml files to turn into vtts, exiting...")
+	}
+	for _, filename := range files {
+
+		chaptername := strings.TrimSuffix(filename, ".fcpxml")
+		vttDest := path.Join("..", "prod", chaptername, chaptername+".vtt")
+		//vttDest := path.Join(chaptername + ".vtt")
+
+		fmt.Println("Parsing: " + filename)
+		chapters, _ := fcpxParseChapters(filename)
+		fmt.Println("\tChapters decoded:", len(chapters))
+
+		// ensure chapter folder exists, ignoring error if it exists already
+		_ = os.Mkdir("../prod/"+chaptername, 0700)
+
+		fmt.Println("\tCreating vtt: " + vttDest)
+		err := vttCreateFile(chapters, vttDest)
+		if err != nil {
+			fmt.Println(err)
+			return
+		}
+
+		fmt.Println("\tCleaning up: ", filename)
+		err = os.Rename(filename, "vtted/"+filename)
+		if err != nil {
+			fmt.Println(err)
+			return
+		}
+
+	}
+}