From 9228f87928699e1f1424e07fd17df187127da7df Mon Sep 17 00:00:00 2001 From: Martin Lindhe Date: Mon, 19 Jun 2017 00:19:29 +0200 Subject: [PATCH] refactor and export api --- LICENSE | 2 +- README.md | 25 +++++- caption.go | 11 --- caption_test.go | 2 +- cleaner.go | 44 +++++----- cleaner_test.go | 10 +-- filter.go | 7 +- caps.go => filter_caps.go | 14 +-- caps_test.go => filter_caps_test.go | 10 +-- html.go => filter_html.go | 8 +- html_test.go => filter_html_test.go | 10 +-- thesubdb.go => finder_thesubdb.go | 2 +- thesubdb_test.go => finder_thesubdb_test.go | 2 + parser.go | 8 +- srt.go | 49 ++++++----- srt_test.go | 96 +++++++++++++-------- ssa.go | 16 ++-- ssa_test.go | 10 ++- vtt.go | 30 +++++++ vtt_test.go | 53 ++++++++++++ 20 files changed, 265 insertions(+), 144 deletions(-) rename caps.go => filter_caps.go (75%) rename caps_test.go => filter_caps_test.go (71%) rename html.go => filter_html.go (68%) rename html_test.go => filter_html_test.go (73%) rename thesubdb.go => finder_thesubdb.go (96%) rename thesubdb_test.go => finder_thesubdb_test.go (98%) create mode 100644 vtt.go create mode 100644 vtt_test.go diff --git a/LICENSE b/LICENSE index a2c53b8..aabb9a6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Martin Lindhe +Copyright (c) 2015-2017 Martin Lindhe Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 2ff9562..5b10424 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,31 @@ WARNING: The API is unstable, work in progress! go get -u github.com/martinlindhe/subtitles ``` +# Example - convert srt to vtt -# Example +```go +in := "1\n" + + "00:00:04,630 --> 00:00:06,018\n" + + "Go ninja!\n" + + "\n" + + "1\n" + + "00:01:09,630 --> 00:01:11,005\n" + + "No ninja!\n" + +res, _ := NewFromSRT(in) + +// Output: WEBVTT +// +// 00:00:04.630 --> 00:00:06.018 +// Go ninja! +// +// 00:01:09.630 --> 00:01:11.005 +// No ninja! +fmt.Println(res.AsVTT()) +``` + +# Example - download subtitle from thesubdb.com -Fetch subtitle from thesubdb.com: ```go f, _ := os.Open(fileName) defer f.Close() diff --git a/caption.go b/caption.go index 383629a..e5322ff 100644 --- a/caption.go +++ b/caption.go @@ -1,7 +1,6 @@ package subtitles import ( - "fmt" "time" ) @@ -12,13 +11,3 @@ type Caption struct { End time.Time Text []string } - -// AsSrt renders the caption as srt -func (cap Caption) AsSrt() string { - res := fmt.Sprintf("%d", cap.Seq) + eol + - SrtTime(cap.Start) + " --> " + SrtTime(cap.End) + eol - for _, line := range cap.Text { - res += line + eol - } - return res + eol -} diff --git a/caption_test.go b/caption_test.go index 7104173..6323cba 100644 --- a/caption_test.go +++ b/caption_test.go @@ -18,5 +18,5 @@ func TestRenderTime(t *testing.T) { assert.Equal(t, "1\n"+ "18:40:22,110 --> 18:41:20,123\n"+ "Go ninja!\n\n", - cap.AsSrt()) + cap.AsSRT()) } diff --git a/cleaner.go b/cleaner.go index fe62fa5..4718a7f 100644 --- a/cleaner.go +++ b/cleaner.go @@ -1,7 +1,6 @@ package subtitles import ( - "fmt" "strings" "time" @@ -10,44 +9,45 @@ import ( // CleanupSub parses .srt or .ssa, performs cleanup and renders to a .srt, returning a string. caller is responsible for passing UTF8 string func CleanupSub(utf8 string, filterName string, keepAds bool, sync int) (string, error) { - var captions []Caption + var subtitle Subtitle + var err error if looksLikeSrt(utf8) { - captions = parseSrt(utf8) + subtitle, err = NewFromSRT(utf8) } else { // falls back on .ssa decoding, for now - captions = parseSsa(utf8) + subtitle, err = NewFromSSA(utf8) + } + if err != nil { + return "", err } if !keepAds { - captions = removeAds(captions) + subtitle.removeAds() } if sync != 0 { - captions = resyncSubs(captions, sync) + subtitle.resyncSubs(sync) } - captions = filterSubs(captions, filterName) - out := renderSrt(captions) + subtitle.filterSubs(filterName) + out := subtitle.AsSRT() return out, nil } -func resyncSubs(subs []Caption, sync int) []Caption { - - // var res []caption - fmt.Printf("resyncing with %d\n", sync) - - for i := range subs { - subs[i].Start = subs[i].Start.Add(time.Duration(sync) * time.Millisecond) - subs[i].End = subs[i].End.Add(time.Duration(sync) * time.Millisecond) +func (subtitle *Subtitle) resyncSubs(sync int) { + // log.Printf("resyncing with %d\n", sync) + for i := range subtitle.Captions { + subtitle.Captions[i].Start = subtitle.Captions[i].Start. + Add(time.Duration(sync) * time.Millisecond) + subtitle.Captions[i].End = subtitle.Captions[i].End. + Add(time.Duration(sync) * time.Millisecond) } - - return subs } // RemoveAds removes advertisement from the subtitles -func removeAds(subs []Caption) (res []Caption) { +func (subtitle *Subtitle) removeAds() *Subtitle { ads := []string{ // english: "captions paid for by", @@ -109,7 +109,8 @@ func removeAds(subs []Caption) (res []Caption) { } seq := 1 - for orgSeq, sub := range subs { + res := []Caption{} + for orgSeq, sub := range subtitle.Captions { isAd := false @@ -130,5 +131,6 @@ func removeAds(subs []Caption) (res []Caption) { seq++ } } - return + subtitle.Captions = res + return subtitle } diff --git a/cleaner_test.go b/cleaner_test.go index 506e599..7564743 100644 --- a/cleaner_test.go +++ b/cleaner_test.go @@ -8,7 +8,7 @@ import ( func TestRemoveAds(t *testing.T) { - in := []Caption{{ + in := Subtitle{[]Caption{{ 1, MakeTime(0, 0, 4, 630), MakeTime(0, 0, 6, 18), @@ -23,9 +23,9 @@ func TestRemoveAds(t *testing.T) { MakeTime(0, 1, 9, 630), MakeTime(0, 1, 11, 005), []string{"No ninja!"}, - }} + }}} - expected := []Caption{{ + expected := Subtitle{[]Caption{{ 1, MakeTime(0, 0, 4, 630), MakeTime(0, 0, 6, 18), @@ -35,7 +35,7 @@ func TestRemoveAds(t *testing.T) { MakeTime(0, 1, 9, 630), MakeTime(0, 1, 11, 005), []string{"No ninja!"}, - }} + }}} - assert.Equal(t, expected, removeAds(in)) + assert.Equal(t, &expected, in.removeAds()) } diff --git a/filter.go b/filter.go index 073407a..f0a82af 100644 --- a/filter.go +++ b/filter.go @@ -5,15 +5,14 @@ import ( ) // filterSubs pass the captions through a filter function -func filterSubs(captions []Caption, filter string) []Caption { +func (subtitle *Subtitle) filterSubs(filter string) { if filter == "caps" { - return filterCapitalization(captions) + subtitle.filterCapitalization() } if filter == "html" { - return filterHTML(captions) + subtitle.filterHTML() } if filter != "none" { fmt.Printf("Unrecognized filter name: %s\n", filter) } - return captions } diff --git a/caps.go b/filter_caps.go similarity index 75% rename from caps.go rename to filter_caps.go index 086f10a..5dbe36c 100644 --- a/caps.go +++ b/filter_caps.go @@ -7,27 +7,22 @@ import ( ) // filterCapitalization converts "ALL CAPS" text into "Initial letter capped" -func filterCapitalization(captions []Caption) []Caption { - - for _, cap := range captions { +func (subtitle *Subtitle) filterCapitalization() *Subtitle { + for _, cap := range subtitle.Captions { for i, line := range cap.Text { clean := ucFirst(line) - if clean != cap.Text[i] { log.Printf("[caps] %s -> %s\n", cap.Text[i], clean) - cap.Text[i] = clean + cap.Text[i] = clean // XXX updated?! } } } - - return captions + return subtitle } func ucFirst(s string) string { - res := "" - for i, c := range s { if i == 0 { res += strings.ToUpper(string(c)) @@ -35,6 +30,5 @@ func ucFirst(s string) string { res += strings.ToLower(string(c)) } } - return res } diff --git a/caps_test.go b/filter_caps_test.go similarity index 71% rename from caps_test.go rename to filter_caps_test.go index a069781..fe69eaa 100644 --- a/caps_test.go +++ b/filter_caps_test.go @@ -8,19 +8,19 @@ import ( func TestFilterCapitalization(t *testing.T) { - in := []Caption{{ + in := Subtitle{Captions: []Caption{{ Seq: 1, Start: MakeTime(0, 0, 4, 630), End: MakeTime(0, 0, 6, 18), Text: []string{"GO NINJA!", "NINJA GO!"}, - }} + }}} - expected := []Caption{{ + expected := Subtitle{[]Caption{{ 1, MakeTime(0, 0, 4, 630), MakeTime(0, 0, 6, 18), []string{"Go ninja!", "Ninja go!"}, - }} + }}} - assert.Equal(t, expected, filterCapitalization(in)) + assert.Equal(t, &expected, in.filterCapitalization()) } diff --git a/html.go b/filter_html.go similarity index 68% rename from html.go rename to filter_html.go index 49e98c1..809d7b3 100644 --- a/html.go +++ b/filter_html.go @@ -7,15 +7,15 @@ import ( ) // filterHTML removes all html tags from captions -func filterHTML(captions []Caption) []Caption { - for _, cap := range captions { +func (subtitle *Subtitle) filterHTML() *Subtitle { + for _, cap := range subtitle.Captions { for i, line := range cap.Text { clean := sanitize.HTML(line) if clean != cap.Text[i] { log.Printf("[html] %s -> %s\n", cap.Text[i], clean) - cap.Text[i] = clean + cap.Text[i] = clean // XXX works?! } } } - return captions + return subtitle } diff --git a/html_test.go b/filter_html_test.go similarity index 73% rename from html_test.go rename to filter_html_test.go index 0b87cec..903f6e4 100644 --- a/html_test.go +++ b/filter_html_test.go @@ -8,19 +8,19 @@ import ( func TestFilterHTML(t *testing.T) { - in := []Caption{{ + in := Subtitle{[]Caption{{ 1, MakeTime(0, 0, 4, 630), MakeTime(0, 0, 6, 18), []string{"GO NINJA!", "NINJA GO!"}, - }} + }}} - expected := []Caption{{ + expected := Subtitle{[]Caption{{ 1, MakeTime(0, 0, 4, 630), MakeTime(0, 0, 6, 18), []string{"GO NINJA!", "NINJA GO!"}, - }} + }}} - assert.Equal(t, expected, filterHTML(in)) + assert.Equal(t, &expected, in.filterHTML()) } diff --git a/thesubdb.go b/finder_thesubdb.go similarity index 96% rename from thesubdb.go rename to finder_thesubdb.go index c252d73..5408b66 100644 --- a/thesubdb.go +++ b/finder_thesubdb.go @@ -40,7 +40,7 @@ func (f SubFinder) TheSubDb(args ...string) ([]byte, error) { } req.Header.Set("User-Agent", - "SubDB/1.0 (GoSubber/1.0; https://github.com/martinlindhe/subber)") + "SubDB/1.0 (GoSubber/1.0; https://github.com/martinlindhe/subtitles)") resp, err := client.Do(req) if err != nil { diff --git a/thesubdb_test.go b/finder_thesubdb_test.go similarity index 98% rename from thesubdb_test.go rename to finder_thesubdb_test.go index aa02e41..54d6a55 100644 --- a/thesubdb_test.go +++ b/finder_thesubdb_test.go @@ -1,3 +1,5 @@ +// +build network + package subtitles import ( diff --git a/parser.go b/parser.go index 76d63e0..8955f67 100644 --- a/parser.go +++ b/parser.go @@ -1,15 +1,15 @@ package subtitles // parse tries to parse a subtitle from the data stream -func parse(b []byte) []Caption { +func parse(b []byte) (Subtitle, error) { s := convertToUTF8(b) if s[0] == '[' { // looks like ssa - return parseSsa(s) + return NewFromSSA(s) } - // XXXX - return parseSrt(s) + // XXX + return NewFromSRT(s) } diff --git a/srt.go b/srt.go index 6cefdd4..6a5a3d4 100644 --- a/srt.go +++ b/srt.go @@ -2,7 +2,6 @@ package subtitles import ( "fmt" - "io/ioutil" "regexp" "strconv" "strings" @@ -19,8 +18,8 @@ func looksLikeSrt(s string) bool { return false } -// ParseSrt parses a .srt text into []Caption, assumes s is a clean utf8 string -func parseSrt(s string) (res []Caption) { +// NewFromSRT parses a .srt text into Subtitle, assumes s is a clean utf8 string +func NewFromSRT(s string) (res Subtitle, err error) { r1 := regexp.MustCompile("([0-9:.,]*) --> ([0-9:.,]*)") lines := strings.Split(s, "\n") outSeq := 1 @@ -33,8 +32,8 @@ func parseSrt(s string) (res []Caption) { _, err := strconv.Atoi(seq) if err != nil { - fmt.Printf("[srt] Parse error 1 at line %d: %v\n", i, err) - continue + err = fmt.Errorf("srt: atoi error at line %d: %v", i, err) + break } var o Caption @@ -47,20 +46,20 @@ func parseSrt(s string) (res []Caption) { matches := r1.FindStringSubmatch(lines[i]) if len(matches) < 3 { - fmt.Printf("[srt] Parser error 2 at line %d (idx out of range)\n", i) - continue + err = fmt.Errorf("srt: parse error at line %d (idx out of range)", i) + break } o.Start, err = ParseTime(matches[1]) if err != nil { - fmt.Printf("[srt] Parse error 3 at line %d: %v\n", i, err) - continue + err = fmt.Errorf("srt: start error at line %d: %v", i, err) + break } o.End, err = ParseTime(matches[2]) if err != nil { - fmt.Printf("[srt] Parse error 4 at line %d: %v\n", i, err) - continue + err = fmt.Errorf("srt: end error at line %d: %v", i, err) + break } i++ @@ -87,7 +86,7 @@ func parseSrt(s string) (res []Caption) { } if len(o.Text) > 0 { - res = append(res, o) + res.Captions = append(res.Captions, o) outSeq++ } } @@ -129,22 +128,26 @@ func ParseTime(in string) (time.Time, error) { return MakeTime(h, m, s, ms), nil } -// writeSrt prints a srt render to outFileName -func writeSrt(subs []Caption, outFileName string) error { - text := renderSrt(subs) - return ioutil.WriteFile(outFileName, []byte(text), 0644) +// AsSRT renders the sub in .srt format +func (subtitle *Subtitle) AsSRT() (res string) { + for _, sub := range subtitle.Captions { + res += sub.AsSRT() + } + return } -// renderSrt produces a text representation of the subtitles -func renderSrt(subs []Caption) (res string) { - for _, sub := range subs { - res += sub.AsSrt() +// AsSRT renders the caption as srt +func (cap Caption) AsSRT() string { + res := fmt.Sprintf("%d", cap.Seq) + eol + + TimeSRT(cap.Start) + " --> " + TimeSRT(cap.End) + eol + for _, line := range cap.Text { + res += line + eol } - return + return res + eol } -// SrtTime renders a timestamp for use in .srt -func SrtTime(t time.Time) string { +// TimeSRT renders a timestamp for use in .srt +func TimeSRT(t time.Time) string { res := t.Format("15:04:05.000") return strings.Replace(res, ".", ",", 1) } diff --git a/srt_test.go b/srt_test.go index 8e55fdc..a27ca3e 100644 --- a/srt_test.go +++ b/srt_test.go @@ -25,7 +25,7 @@ func TestParseTime(t *testing.T) { assert.Equal(t, MakeTime(0, 14, 52, 12), t7) } -func TestParseSrt(t *testing.T) { +func TestNewFromSRT(t *testing.T) { in := "1\n" + "00:00:04,630 --> 00:00:06,018\n" + @@ -40,7 +40,7 @@ func TestParseSrt(t *testing.T) { "00:01:09,630 --> 00:01:11,005\n" + "No ninja!\n" - expected := []Caption{{ + expected := Subtitle{[]Caption{{ 1, MakeTime(0, 0, 4, 630), MakeTime(0, 0, 6, 18), @@ -55,12 +55,14 @@ func TestParseSrt(t *testing.T) { MakeTime(0, 1, 9, 630), MakeTime(0, 1, 11, 005), []string{"No ninja!"}, - }} + }}} - assert.Equal(t, expected, parseSrt(in)) + res, err := NewFromSRT(in) + assert.Equal(t, nil, err) + assert.Equal(t, expected, res) } -func TestParseSrtWithMacLinebreaks(t *testing.T) { +func TestNewFromSRTWithMacLinebreaks(t *testing.T) { in := "1\r" + "00:00:04,630 --> 00:00:06,018\r" + @@ -70,7 +72,7 @@ func TestParseSrtWithMacLinebreaks(t *testing.T) { "00:01:09,630 --> 00:01:11,005\r" + "No ninja!\r" - expected := []Caption{{ + expected := Subtitle{[]Caption{{ 1, MakeTime(0, 0, 4, 630), MakeTime(0, 0, 6, 18), @@ -80,14 +82,16 @@ func TestParseSrtWithMacLinebreaks(t *testing.T) { MakeTime(0, 1, 9, 630), MakeTime(0, 1, 11, 005), []string{"No ninja!"}, - }} + }}} utf8 := convertToUTF8([]byte(in)) - assert.Equal(t, expected, parseSrt(utf8)) + res, err := NewFromSRT(utf8) + assert.Equal(t, nil, err) + assert.Equal(t, expected, res) } -func TestParseSrtSkipEmpty(t *testing.T) { +func TestNewFromSRTSkipEmpty(t *testing.T) { in := "1\n" + "00:00:04,630 --> 00:00:06,018\n" + @@ -101,7 +105,7 @@ func TestParseSrtSkipEmpty(t *testing.T) { "00:01:09,630 --> 00:01:11,005\n" + "No ninja!\n" - expected := []Caption{{ + expected := Subtitle{[]Caption{{ 1, MakeTime(0, 0, 4, 630), MakeTime(0, 0, 6, 18), @@ -111,26 +115,30 @@ func TestParseSrtSkipEmpty(t *testing.T) { MakeTime(0, 1, 9, 630), MakeTime(0, 1, 11, 005), []string{"No ninja!"}, - }} + }}} - assert.Equal(t, expected, parseSrt(in)) + res, err := NewFromSRT(in) + assert.Equal(t, nil, err) + assert.Equal(t, expected, res) } -func TestParseSrtCrlf(t *testing.T) { +func TestNewFromSRTCrlf(t *testing.T) { in := "1\n" + "00:00:04,630 --> 00:00:06,018\r\n" + "Go ninja!\r\n" + "\r\n" - expected := []Caption{{ + expected := Subtitle{[]Caption{{ 1, MakeTime(0, 0, 4, 630), MakeTime(0, 0, 6, 18), []string{"Go ninja!"}, - }} + }}} - assert.Equal(t, expected, parseSrt(in)) + res, err := NewFromSRT(in) + assert.Equal(t, nil, err) + assert.Equal(t, expected, res) } func TestParseExtraLineBreak(t *testing.T) { @@ -143,14 +151,16 @@ func TestParseExtraLineBreak(t *testing.T) { "Go ninja!\r\n" + "\r\n" - expected := []Caption{{ + expected := Subtitle{[]Caption{{ 1, MakeTime(0, 0, 4, 630), MakeTime(0, 0, 6, 18), []string{"Go ninja!"}, - }} + }}} - assert.Equal(t, expected, parseSrt(in)) + res, err := NewFromSRT(in) + assert.Equal(t, nil, err) + assert.Equal(t, expected, res) } func TestParseWierdTimestamp(t *testing.T) { @@ -159,17 +169,19 @@ func TestParseWierdTimestamp(t *testing.T) { "00:14:52.00 --> 00:14:57,500\r\n" + "Go ninja!\r\n" - expected := []Caption{{ + expected := Subtitle{[]Caption{{ 1, MakeTime(0, 14, 52, 0), MakeTime(0, 14, 57, 500), []string{"Go ninja!"}, - }} + }}} - assert.Equal(t, expected, parseSrt(in)) + res, err := NewFromSRT(in) + assert.Equal(t, nil, err) + assert.Equal(t, expected, res) } -func TestRenderSrt(t *testing.T) { +func TestAsSRT(t *testing.T) { expected := "1\n" + "00:00:04,630 --> 00:00:06,018\n" + @@ -179,7 +191,7 @@ func TestRenderSrt(t *testing.T) { "00:01:09,630 --> 00:01:11,005\n" + "No ninja!\n\n" - in := []Caption{{ + in := Subtitle{[]Caption{{ 1, MakeTime(0, 0, 4, 630), MakeTime(0, 0, 6, 18), @@ -189,9 +201,9 @@ func TestRenderSrt(t *testing.T) { MakeTime(0, 1, 9, 630), MakeTime(0, 1, 11, 005), []string{"No ninja!"}, - }} + }}} - assert.Equal(t, expected, renderSrt(in)) + assert.Equal(t, expected, in.AsSRT()) } func TestParseLatin1Srt(t *testing.T) { @@ -199,16 +211,18 @@ func TestParseLatin1Srt(t *testing.T) { "00:14:52.00 --> 00:14:57,500\r\n" + "Hall\xe5 ninja!\r\n" - expected := []Caption{{ + expected := Subtitle{[]Caption{{ 1, MakeTime(0, 14, 52, 0), MakeTime(0, 14, 57, 500), []string{"HallĂ„ ninja!"}, - }} + }}} utf8 := convertToUTF8([]byte(in)) - assert.Equal(t, expected, parseSrt(utf8)) + res, err := NewFromSRT(utf8) + assert.Equal(t, nil, err) + assert.Equal(t, expected, res) } func TestParseUTF16BESrt(t *testing.T) { @@ -228,16 +242,18 @@ func TestParseUTF16BESrt(t *testing.T) { 0, '\r', 0, '\n', } - expected := []Caption{{ + expected := Subtitle{[]Caption{{ 1, MakeTime(0, 0, 0, 0), MakeTime(0, 0, 0, 1), []string{"Test"}, - }} + }}} utf8 := convertToUTF8(in) - assert.Equal(t, expected, parseSrt(utf8)) + res, err := NewFromSRT(utf8) + assert.Equal(t, nil, err) + assert.Equal(t, expected, res) } func TestParseUTF16LESrt(t *testing.T) { @@ -257,16 +273,18 @@ func TestParseUTF16LESrt(t *testing.T) { '\r', 0, '\n', 0, } - expected := []Caption{{ + expected := Subtitle{[]Caption{{ 1, MakeTime(0, 0, 0, 0), MakeTime(0, 0, 0, 1), []string{"Test"}, - }} + }}} utf8 := convertToUTF8(in) - assert.Equal(t, expected, parseSrt(utf8)) + res, err := NewFromSRT(utf8) + assert.Equal(t, nil, err) + assert.Equal(t, expected, res) } func TestParseUTF8BomSrt(t *testing.T) { @@ -286,14 +304,16 @@ func TestParseUTF8BomSrt(t *testing.T) { '\r', '\n', } - expected := []Caption{{ + expected := Subtitle{[]Caption{{ 1, MakeTime(0, 0, 0, 0), MakeTime(0, 0, 0, 1), []string{"Test"}, - }} + }}} utf8 := convertToUTF8(in) - assert.Equal(t, expected, parseSrt(utf8)) + res, err := NewFromSRT(utf8) + assert.Equal(t, nil, err) + assert.Equal(t, expected, res) } diff --git a/ssa.go b/ssa.go index e9269b6..e9688f0 100644 --- a/ssa.go +++ b/ssa.go @@ -8,11 +8,17 @@ import ( "time" ) -func parseSsa(s string) (res []Caption) { - chunk, err := extractSsaChunk("[Events]", s) +// Subtitle holds a parsed subtitle +type Subtitle struct { + Captions []Caption +} + +// NewFromSSA parses a .ssa text into []Caption, assumes s is a clean utf8 string +func NewFromSSA(s string) (res Subtitle, err error) { + var chunk string + chunk, err = extractSsaChunk("[Events]", s) if err != nil { - fmt.Println("[ssa]", err) - return nil + return } lines := strings.Split(chunk, "\n") @@ -56,7 +62,7 @@ func parseSsa(s string) (res []Caption) { o.Text = strings.Split(text, "\\n") if len(o.Text) > 0 { - res = append(res, o) + res.Captions = append(res.Captions, o) outSeq++ } } diff --git a/ssa_test.go b/ssa_test.go index f7791ee..7b01086 100644 --- a/ssa_test.go +++ b/ssa_test.go @@ -6,14 +6,14 @@ import ( "github.com/stretchr/testify/assert" ) -func TestParseSsa(t *testing.T) { +func TestNewFromSsa(t *testing.T) { in := "[Events]\n" + "Format: Layer, Start, End, Style, Actor, MarginL, MarginR, MarginV, Effect, Text\n" + "Dialogue: 0,0:01:06.37,0:01:08.04,Default,,0000,0000,0000,,Honey, I'm home!\n" + "Dialogue: 0,0:01:09.05,0:01:10.69,Default,,0000,0000,0000,,Hi.\\n- Hi, love.\n" - expected := []Caption{{ + expected := Subtitle{[]Caption{{ 1, MakeTime(0, 1, 6, 370), MakeTime(0, 1, 8, 40), @@ -23,9 +23,11 @@ func TestParseSsa(t *testing.T) { MakeTime(0, 1, 9, 50), MakeTime(0, 1, 10, 690), []string{"Hi.", "- Hi, love."}, - }} + }}} - assert.Equal(t, expected, parseSsa(in)) + res, err := NewFromSSA(in) + assert.Equal(t, nil, err) + assert.Equal(t, expected, res) } func TestParseSsaFormat(t *testing.T) { diff --git a/vtt.go b/vtt.go new file mode 100644 index 0000000..daf6773 --- /dev/null +++ b/vtt.go @@ -0,0 +1,30 @@ +package subtitles + +import ( + "time" +) + +// AsVTT renders the sub in WebVTT format +// https://en.wikipedia.org/wiki/WebVTT +func (subtitle *Subtitle) AsVTT() (res string) { + res = "WEBVTT\n\n" // XXX + for _, sub := range subtitle.Captions { + res += sub.AsVTT() + } + return +} + +// AsVTT renders the caption as WebVTT +func (cap Caption) AsVTT() string { + res := TimeVTT(cap.Start) + " --> " + TimeVTT(cap.End) + eol + for _, line := range cap.Text { + res += line + eol + } + return res + eol +} + +// TimeVTT renders a timestamp for use in WebVTT +func TimeVTT(t time.Time) string { + // XXX hours are optional, size-optimize! + return t.Format("15:04:05.000") +} diff --git a/vtt_test.go b/vtt_test.go new file mode 100644 index 0000000..0d6ef3d --- /dev/null +++ b/vtt_test.go @@ -0,0 +1,53 @@ +package subtitles + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAsVTT(t *testing.T) { + expected := "WEBVTT\n" + + "\n" + + "00:00:04.630 --> 00:00:06.018\n" + + "Go ninja!\n" + + "\n" + + "00:01:09.630 --> 00:01:11.005\n" + + "No ninja!\n\n" + + in := Subtitle{[]Caption{{ + 1, + MakeTime(0, 0, 4, 630), + MakeTime(0, 0, 6, 18), + []string{"Go ninja!"}, + }, { + 2, + MakeTime(0, 1, 9, 630), + MakeTime(0, 1, 11, 005), + []string{"No ninja!"}, + }}} + + assert.Equal(t, expected, in.AsVTT()) +} + +func ExampleNewFromSRT() { + in := "1\n" + + "00:00:04,630 --> 00:00:06,018\n" + + "Go ninja!\n" + + "\n" + + "1\n" + + "00:01:09,630 --> 00:01:11,005\n" + + "No ninja!\n" + + res, _ := NewFromSRT(in) + + // Output: WEBVTT + // + // 00:00:04.630 --> 00:00:06.018 + // Go ninja! + // + // 00:01:09.630 --> 00:01:11.005 + // No ninja! + fmt.Println(res.AsVTT()) +}