commit 940391cfe43ca8e71791de6ca68152eee3b48f2b
Author: oblique <psyberbits@gmail.com>
Date: Sat, 13 Apr 2013 08:31:07 +0300
Initial commit
Diffstat:
4 files changed, 299 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+/subsync
diff --git a/Makefile b/Makefile
@@ -0,0 +1,8 @@
+all: subsync
+
+subsync: subsync.go
+ go get .
+ go build subsync.go
+
+clean:
+ rm -f subsync
diff --git a/README.md b/README.md
@@ -0,0 +1,16 @@
+subsync is a simple CLI tool written in Go, that synchronizes SubRip
+(srt) subtitles automatically. You only have to know when the first
+and the last subtitle must be shown.
+
+## examples
+
+ subsync -f 00:01:33,492 -l 01:39:23,561 -i file.srt
+
+In the above command the input is the `file.srt`. We set the first
+subtitle to be shown at `00:01:33,492` and the last to be shown at
+`01:39:23,561`. The synced file will be saved at `file.srt`.
+
+ subsync -f 00:01:33,492 -l 01:39:23,561 -i file.srt -o newfile.srt
+
+The above command is the same as the previous, but the synced file
+will be saved at `newfile.srt`.
+\ No newline at end of file
diff --git a/subsync.go b/subsync.go
@@ -0,0 +1,272 @@
+package main
+
+import (
+ "fmt"
+ "errors"
+ "strings"
+ "strconv"
+ "math"
+ "os"
+ "path"
+ "io"
+ "bufio"
+ "container/list"
+ "github.com/jessevdk/go-flags"
+)
+
+type subtitle struct {
+ text string
+ start uint
+ end uint
+}
+
+func die(err error) {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ os.Exit(1)
+}
+
+func roundFloat64(f float64) float64 {
+ val := f - float64(int64(f))
+ if val >= 0.5 {
+ return math.Ceil(f)
+ } else if val > 0 {
+ return math.Floor(f)
+ } else if val <= -0.5 {
+ return math.Floor(f)
+ } else if val < 0 {
+ return math.Ceil(f)
+ }
+ return f
+}
+
+/* converts hh:mm:ss,mss to milliseconds */
+func time_to_msecs(tm string) (uint, error) {
+ var msecs uint
+ var h, m, s, ms uint
+
+ tm = strings.Replace(tm, ".", ",", 1)
+ num, err := fmt.Sscanf(tm, "%d:%d:%d,%d", &h, &m, &s, &ms)
+
+ if num != 4 || err != nil {
+ return 0, errors.New("Parsing error: Can not covert `" + tm + "' to milliseconds.")
+ }
+
+ msecs = h * 60 * 60 * 1000
+ msecs += m * 60 * 1000
+ msecs += s * 1000
+ msecs += ms
+
+ return msecs, nil
+}
+
+/* converts milliseconds to hh:mm:ss,mss */
+func msecs_to_time(msecs uint) string {
+ var h, m, s, ms uint
+
+ h = msecs / (60 * 60 * 1000)
+ msecs = msecs % (60 * 60 * 1000)
+ m = msecs / (60 * 1000)
+ msecs = msecs % (60 * 1000)
+ s = msecs / 1000
+ msecs = msecs % 1000
+ ms = msecs
+
+ tm := fmt.Sprintf("%02d:%02d:%02d,%03d", h, m, s, ms)
+
+ return tm
+}
+
+/* read SubRip (srt) file */
+func read_srt(filename string) (*list.List, error) {
+ var state int = 0
+ var subs *list.List
+ var sub *subtitle
+
+ f, err := os.Open(filename)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ r := bufio.NewReader(f)
+ subs = list.New()
+ sub = new(subtitle)
+
+ for {
+ var (
+ isprefix bool = true
+ err error = nil
+ ln, line []byte
+ )
+
+ for isprefix && err == nil {
+ line, isprefix, err = r.ReadLine()
+ if err != nil && err != io.EOF {
+ return nil, err
+ }
+ ln = append(ln, line...)
+ }
+
+ /* parse subtitle id */
+ if state == 0 {
+ /* avoid false-positive parsing error */
+ if err == io.EOF && len(ln) == 0 {
+ break;
+ }
+ id := strings.Split(string(ln), " ")
+ if len(id) != 1 {
+ return nil, errors.New("Parsing error: Wrong file format")
+ }
+ _, err = strconv.ParseUint(id[0], 10, 0)
+ if err != nil {
+ return nil, err
+ return nil, errors.New("Parsing error: Wrong file format")
+ }
+ state = 1
+ /* parse start, end times */
+ } else if state == 1 {
+ tm := strings.Split(string(ln), " ")
+ if len(tm) != 3 || tm[1] != "-->" {
+ return nil, errors.New("Parsing error: Wrong file format")
+ }
+ sub.start, err = time_to_msecs(tm[0])
+ if err != nil {
+ return nil, err
+ }
+ sub.end, err = time_to_msecs(tm[2])
+ if err != nil {
+ return nil, err
+ }
+ state = 2
+ /* parse the actual subtitle text */
+ } else if state == 2 {
+ if len(ln) == 0 {
+ subs.PushBack(sub)
+ sub = new(subtitle)
+ state = 0
+ } else {
+ sub.text += string(ln) + "\r\n"
+ }
+ }
+
+ if err == io.EOF {
+ break;
+ }
+ }
+
+ return subs, nil
+}
+
+/* write SubRip (srt) file */
+func write_srt(filename string, subs *list.List) error {
+ var id int = 0
+
+ f, err := os.Create(filename)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ w := bufio.NewWriter(f)
+ defer w.Flush()
+
+ for e := subs.Front(); e != nil; e = e.Next() {
+ id++
+ sub := e.Value.(*subtitle)
+ fmt.Fprintf(w, "%d\r\n", id)
+ fmt.Fprintf(w, "%s --> %s\r\n", msecs_to_time(sub.start), msecs_to_time(sub.end))
+ fmt.Fprintf(w, "%s\r\n", sub.text)
+ }
+
+ return nil
+}
+
+/* synchronize subtitles by knowing the time of the first and the last subtitle.
+ * to archive this we must use the linear equation: y = mx + b */
+func sync_subs(subs *list.List, synced_first_ms uint, synced_last_ms uint) {
+ var slope, yint float64
+
+ desynced_first_ms := subs.Front().Value.(*subtitle).start
+ desynced_last_ms := subs.Back().Value.(*subtitle).start
+
+ slope = float64(synced_last_ms - synced_first_ms) / float64(desynced_last_ms - desynced_first_ms)
+ yint = float64(synced_last_ms) - slope * float64(desynced_last_ms)
+
+ for e := subs.Front(); e != nil; e = e.Next() {
+ sub := e.Value.(*subtitle)
+ sub.start = uint(roundFloat64(slope * float64(sub.start) + yint))
+ sub.end = uint(roundFloat64(slope * float64(sub.end) + yint))
+ }
+}
+
+func main() {
+ var first_ms, last_ms uint
+
+ var opts struct {
+ FirstTm string `short:"f" long:"first-sub" description:"Time of first subtitle"`
+ LastTm string `short:"l" long:"last-sub" description:"Time of last subtitle"`
+ InputFl string `short:"i" long:"input" description:"Input file" required:"true"`
+ OutputFl string `short:"o" long:"output" description:"Output file"`
+ }
+
+ _, err := flags.Parse(&opts)
+ if err != nil {
+ if err.(*flags.Error).Type == flags.ErrHelp {
+ fmt.Printf("example: %s -f 00:01:33,492 -l 01:39:23,561 -i sub.srt\n",
+ path.Base(os.Args[0]))
+ os.Exit(0)
+ }
+ os.Exit(1)
+ }
+
+ if opts.FirstTm != "" {
+ first_ms, err = time_to_msecs(opts.FirstTm)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Please check the value of -f option.\n")
+ die(err)
+ }
+ }
+
+ if opts.LastTm != "" {
+ last_ms, err = time_to_msecs(opts.LastTm)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Please check the value of -l option.\n")
+ die(err)
+ }
+ }
+
+ /* if output file is not set, use the input file */
+ if opts.OutputFl == "" {
+ opts.OutputFl = opts.InputFl
+ }
+
+ subs, err := read_srt(opts.InputFl)
+ if err != nil {
+ die(err)
+ }
+
+ /* if time of the first synced subtitle is not set,
+ * use the time of the first desynced subtitle */
+ if opts.FirstTm == "" {
+ first_ms = subs.Front().Value.(*subtitle).start
+ }
+
+ /* if time of the last synced subtitle is not set,
+ * use the time of the last desynced subtitle */
+ if opts.LastTm == "" {
+ last_ms = subs.Back().Value.(*subtitle).start
+ }
+
+ if first_ms > last_ms {
+ fmt.Fprintf(os.Stderr, "First subtitle can not be after last subtitle.\n")
+ fmt.Fprintf(os.Stderr, "Please check the values of -f and/or -l options.\n")
+ os.Exit(1)
+ }
+
+ sync_subs(subs, first_ms, last_ms)
+
+ err = write_srt(opts.OutputFl, subs)
+ if err != nil {
+ die(err)
+ }
+}
+\ No newline at end of file