Commit b4193b2
2016-04-17 01:17:47
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
+ }
+
+ }
+}