master
Raw Download raw file
  1package main
  2
  3import (
  4	"encoding/xml"
  5	"fmt"
  6	"math"
  7	"os"
  8	"path"
  9	"path/filepath"
 10	"strconv"
 11	"strings"
 12	"time"
 13
 14	"github.com/beevik/etree"
 15	"github.com/dchest/uniuri"
 16)
 17
 18type Parent struct {
 19	Start_s string  `xml:"offset,attr"`
 20	Chapter Chapter `xml:"chapter-marker"`
 21}
 22
 23// "_s" values are string fcpxml representations of time, they need extra attention to be
 24// parsed, converted, and stored as their non-"_s" float values
 25type Chapter struct {
 26	Value      string `xml:"value,attr"`
 27	Start_s    string `xml:"start,attr"`
 28	Duration_s string `xml:"posterOffset,attr"`
 29	Start      float64
 30	Duration   float64
 31}
 32
 33func fcpxTimeConv(s string) (f float64, err error) {
 34	s = strings.TrimSuffix(s, "s")
 35	fraction := strings.Split(s, "/")
 36	if len(fraction) == 2 {
 37
 38		numer, err := strconv.ParseFloat(fraction[0], 64)
 39		if err != nil {
 40			return f, err
 41		}
 42		denom, err := strconv.ParseFloat(fraction[1], 64)
 43		if err != nil {
 44			return f, err
 45		}
 46
 47		f = numer / denom
 48		return f, err
 49
 50	} else if len(fraction) == 1 {
 51		return strconv.ParseFloat(fraction[0], 64)
 52
 53	} else {
 54		fmt.Println("not a fraction!")
 55		return f, fmt.Errorf("unrecognized fcp time structure")
 56	}
 57
 58}
 59
 60func fcpxParseChapters(filepath string) (chapters []Chapter, err error) {
 61	doc := etree.NewDocument()
 62	if err := doc.ReadFromFile(filepath); err != nil {
 63		return chapters, err
 64	}
 65
 66	// find the pesky chapter-markers... parents
 67	for _, e := range doc.FindElements("//chapter-marker/..") {
 68
 69		// encode to unmarshall.. yes really
 70		doc := etree.NewDocument()
 71		doc.SetRoot(e.Copy())
 72		b, _ := doc.WriteToBytes()
 73
 74		if e.Tag == "transition" || e.Tag == "clip" {
 75
 76			var pr Parent
 77			err = xml.Unmarshal(b, &pr)
 78			if err != nil {
 79				fmt.Println("Unmarshaling transition failed", pr, err)
 80				return chapters, err
 81			}
 82			if pr.Chapter != (Chapter{}) {
 83
 84				pr.Chapter.Start, err = fcpxTimeConv(pr.Start_s)
 85				if err != nil {
 86					fmt.Println("Transition start timeconf failed: ", pr.Start_s, err)
 87					return chapters, err
 88				}
 89
 90				pr.Chapter.Duration, err = fcpxTimeConv(pr.Chapter.Duration_s)
 91				if err != nil {
 92					fmt.Println("Chapter duration timeconf failed: ", pr.Chapter.Duration_s, err)
 93					return chapters, err
 94				}
 95				chapters = append(chapters, pr.Chapter)
 96			}
 97
 98		} else {
 99
100			fmt.Println("Unexpected chapter-marker parent type! Call bryfry!")
101			return chapters, err
102
103		}
104	}
105	return chapters, nil
106}
107
108func vttTimeFmt(seconds float64) string {
109	t := time.Duration(seconds*1000) * time.Millisecond
110	h := math.Floor(t.Hours())
111	m := math.Floor((t - time.Duration(h)*time.Hour).Minutes())
112	s := (t - time.Duration(m)*time.Minute - time.Duration(h)*time.Hour).Seconds()
113	return fmt.Sprintf("%02.f:%02.f:%06.3f", h, m, s)
114}
115
116func vttCreateFile(chapters []Chapter, filename string) error {
117	os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0755)
118	f, err := os.Create(filename)
119	defer f.Close()
120	if err != nil {
121		return err
122	}
123
124	_, err = f.WriteString("WEBVTT\n\n")
125	if err != nil {
126		return err
127	}
128
129	for _, c := range chapters {
130		id := uniuri.NewLen(8)
131		vttString := fmt.Sprintf("%s --> %s\n%s,%s\n\n", vttTimeFmt(c.Start), vttTimeFmt(c.Start+c.Duration), id, c.Value)
132		_, err = f.WriteString(vttString)
133		if err != nil {
134			return err
135		}
136	}
137	return nil
138}
139
140func main() {
141	files, _ := filepath.Glob("*.fcpxml")
142	if len(files) == 0 {
143		fmt.Println("No .fcpxml files to turn into vtts, exiting...")
144	}
145	for _, filename := range files {
146
147		chaptername := strings.TrimSuffix(filename, ".fcpxml")
148		vttDest := path.Join("..", "prod", chaptername, chaptername+"_chapters.vtt")
149		//vttDest := path.Join(chaptername + ".vtt")
150
151		fmt.Println("Parsing: " + filename)
152		chapters, _ := fcpxParseChapters(filename)
153		fmt.Println("\tChapters decoded:", len(chapters))
154
155		// ensure chapter folder exists, ignoring error if it exists already
156		_ = os.Mkdir("../prod/"+chaptername, 0755)
157
158		fmt.Println("\tCreating vtt: " + vttDest)
159		err := vttCreateFile(chapters, vttDest)
160		if err != nil {
161			fmt.Println(err)
162			return
163		}
164
165		fmt.Println("\tCleaning up: ", filename)
166		err = os.Rename(filename, "vtted/"+filename)
167		if err != nil {
168			fmt.Println(err)
169			return
170		}
171
172	}
173}