master
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}