srt 0.0.5 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 28e7323497b349d7a2089aa395ad2c1ba1c3985729111dc56706c3280521bb5f
4
+ data.tar.gz: 82fe89057aee1e6a6aea48a16d33740686a18d0444f5559820603ff1b9a2a062
5
+ SHA512:
6
+ metadata.gz: 699aaea2dda022acb10cb2e5d1db24d36ad8adec53528b1e186c6b75a549a049cbfd33f806d2cd4e276ae7ecabde412c9e9181c50335dc2f613ac79cefda5f31
7
+ data.tar.gz: c8abffc1bf997c624ee0cb0e06b0a49b9dd88b584a6fdb3c9edbd3170acb3a5e8a449b4d35eb3edfd2bc825c79389170da2d44589be602b02dced7a39223e4ef
data/.gitignore CHANGED
@@ -2,6 +2,8 @@
2
2
  *.rbc
3
3
  .bundle
4
4
  .config
5
+ .ruby-gemset
6
+ .ruby-version
5
7
  .rvmrc
6
8
  .yardoc
7
9
  Gemfile.lock
data/.travis.yml CHANGED
@@ -1,2 +1,8 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - 2.3.0
1
7
  script:
2
- - bundle exec rake spec
8
+ - bundle exec rake spec
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
- # SRT [![Build Status](https://travis-ci.org/cpetersen/srt.png?branch=master)](https://travis-ci.org/cpetersen/srt)
1
+ # SRT
2
+ [![Build Status](https://travis-ci.org/cpetersen/srt.png?branch=master)](https://travis-ci.org/cpetersen/srt)
3
+ [![Code Climate](https://codeclimate.com/github/cpetersen/srt.png)](https://codeclimate.com/github/cpetersen/srt)
4
+ [![Coverage Status](https://coveralls.io/repos/cpetersen/srt/badge.png?branch=master)](https://coveralls.io/r/cpetersen/srt?branch=master)
5
+ [![Gem Version](https://badge.fury.io/rb/srt.png)](http://badge.fury.io/rb/srt)
2
6
 
3
- SRT stands for SubRip text file format, which is a file for storing subtitles; This is a Ruby library for manipulating SRT files.
7
+ SRT stands for SubRip text file format, which is a file for storing subtitles; This is a Ruby library for manipulating SRT files.
4
8
  Current functionality includes **parsing**, **appending**, **splitting** and **timeshifting** (constant, progressive and framerate-based).
5
9
 
6
10
  ## Installation
@@ -12,7 +16,7 @@ Add this line to your application's Gemfile:
12
16
  And then execute:
13
17
 
14
18
  $ bundle
15
-
19
+
16
20
  Or install it yourself as:
17
21
 
18
22
  $ gem install srt
@@ -31,7 +35,7 @@ You can parse an SRT file with the following code:
31
35
  Each line exposes the following methods/members:
32
36
  * `sequence` The incrementing subtitle ID (starts at 1)
33
37
  * `text` An **Array** holding one or multiple lines of text.
34
- * `start_time` The subtitle start timecode in seconds as a float
38
+ * `start_time` The subtitle start timecode in seconds as a float
35
39
  * `end_time` The subtitle end timecode in seconds as a float
36
40
  * `time_str` Returns a timecode string of the form `"00:53:35,558 --> 00:53:36,556"`
37
41
  * `display_coordinates` Optional display coordinates of the form `"X1:100 X2:600 Y1:100 Y2:400"`
@@ -60,37 +64,56 @@ The method `split` splits your subtitles at one (or more) points and returns an
60
64
  By default, the timecodes of the split parts are relatively shifted towards their beginnings (to line up with correspondingly split multi-part video);
61
65
  By additionally passing `:timeshift => false` you can prevent that behaviour and retain the original timecodes for each split part.
62
66
 
67
+ Pass the option `:renumber => false` to prevent the line sequence number from being reset for a segment.
68
+
69
+ ```ruby
70
+ parts = file.split( :at => "01:09:24,000", :renumber => false ) # Split the file in two at 01:09:24 but do not reset the sequence number on the second part
71
+ ```
72
+
63
73
  Example options for a multi-split: `{ :at => ["00:19:24,500", "01:32:09,120", ...] }`
64
74
 
75
+
76
+ Optionally, for multi-splitting, you can pass a ":every" option to split the subtitles at a fixed interval.
77
+
78
+ ```ruby
79
+ parts = file.split( :every => "00:01:00,000" ) # Split the file every 1 minute
80
+ ```
81
+ Note that the options :at and :every are mutually exclusive, and :at takes precedence.
82
+
65
83
  #### Timeshifting
66
84
 
67
85
  The method `timeshift` takes a hash and supports three different modes of timecode processing:
68
86
 
69
- **Constant timeshift**
87
+ **Constant timeshift**
70
88
 
71
89
  ```ruby
72
- file.timeshift( :all => "-2.5s" ) # Shift all subtitles so they show up 2.5 seconds earlier
90
+ file.timeshift( :all => "-2.5s" ) # Shift all subtitles so they show up 2.5 seconds earlier
73
91
  ```
74
92
 
75
- Simply pass a hash of the form `:all => "[+|-][amount][h|m|s|mil]"`
76
- Other example options, e.g.: `:all => "+700mil"`, `:all => "1.34m"`, `:all => "0.15h"`
93
+ Simply pass a hash of the form `:all => "[+|-][amount][h|m|s|ms]"`
94
+ Other example options, e.g.: `:all => "+1.34m"`, `:all => "0.15h"`, `:all => "90ms"`
77
95
 
78
96
  **Progressive timeshift**
79
97
 
80
98
  ```ruby
81
- file.timeshift({ 1 => "00:02:12,000", 843 => "01:38:06,000" }) # Correct drifting-out-of-sync
99
+ file.timeshift({ "#1" => "00:02:12,000", "#843" => "01:38:06,000" }) # Correct drifting-out-of-sync
82
100
  ```
83
101
 
84
102
  This example call would shift the **first subtitle** to `00:02:12`, the **last subtitle** (assuming here that `#843` is the last one in your file) to `01:38:06`, and all the ones before, after, and in between those two reference points seamlessly to their own resulting earlier or later begin times.
85
103
 
86
- To make this work pass two `original timecode/id => target timecode` pairs where each takes any of these 4 forms:
104
+ To make this work pass two `origin timecode => target timecode` pairs, where the *origin timecodes* can be supplied as:
87
105
 
88
- * `[id] => "[hh]:[mm]:[ss],[mil]"`
89
- * `[id] => "[+/-][amount][h|m|s|mil]"`
90
- * `"[hh]:[mm]:[ss],[mil]" => "[hh]:[mm]:[ss],[mil]"`
91
- * `"[hh]:[mm]:[ss],[mil]" => "[+/-][amount][h|m|s|mil]"`
106
+ * `float` providing the raw timecode in *seconds*, e.g.: `195.65`
107
+ * `"[hh]:[mm]:[ss],[ms]"` string, which is a timecode in SRT notation, e.g.: `"00:02:12,000"`
108
+ * `"#[id]"` string, which references the timecode of the subtitle with the supplied id, e.g.: `"#317"`
92
109
 
93
- Another full example: `{ "00:00:51,400" => "+13s", "01:12:44,320" => "+2.436m" }`
110
+ ... and the *target timecodes* can be supplied as:
111
+
112
+ * `float` providing the raw timecode in *seconds*, e.g.: `3211.3`
113
+ * `"[hh]:[mm]:[ss],[ms]"` string, which is a timecode in SRT notation, e.g.: `"01:01:03,300"`
114
+ * `"[+/-][amount][h|m|s|ms]"` string, describing the amount by which to shift the origin timecode, e.g.: `"+1.5s"`
115
+
116
+ So for example: `{ "00:00:51,400" => "+13s", "01:12:44,320" => "+2.436m" }`
94
117
 
95
118
  This method can be used to fix subtitles that are *at different times differently out of sync*,
96
119
  and comes in handy especially if you have no idea what framerate your video or the video for which your subtitles
@@ -113,3 +136,4 @@ This is usually only useful if you have some background information about the de
113
136
  3. Commit your changes (`git commit -am 'Added some feature'`)
114
137
  4. Push to the branch (`git push origin my-new-feature`)
115
138
  5. Create new Pull Request
139
+
data/lib/srt/file.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  module SRT
2
2
  class File
3
- def self.parse(input)
3
+ def self.parse(input, options = {})
4
+ @debug = options.fetch(:debug, false)
4
5
  if input.is_a?(String)
5
6
  parse_string(input)
6
7
  elsif input.is_a?(::File)
@@ -15,58 +16,58 @@ module SRT
15
16
  end
16
17
 
17
18
  def self.parse_string(srt_data)
18
- result = SRT::File.new
19
- line = SRT::Line.new
20
-
19
+ result = new
20
+ line = Line.new
21
+
21
22
  split_srt_data(srt_data).each_with_index do |str, index|
22
23
  begin
23
24
  if str.strip.empty?
24
25
  result.lines << line unless line.empty?
25
- line = SRT::Line.new
26
+ line = Line.new
26
27
  elsif !line.error
27
28
  if line.sequence.nil?
28
29
  line.sequence = str.to_i
29
30
  elsif line.start_time.nil?
30
31
  if mres = str.match(/(?<start_timecode>[^[[:space:]]]+) -+> (?<end_timecode>[^[[:space:]]]+) ?(?<display_coordinates>X1:\d+ X2:\d+ Y1:\d+ Y2:\d+)?/)
31
32
 
32
- if (line.start_time = SRT::File.parse_timecode(mres["start_timecode"])) == nil
33
- line.error = "#{line}, Invalid formatting of start timecode, [#{mres["start_timecode"]}]"
34
- puts line.error
33
+ if (line.start_time = Parser.timecode(mres["start_timecode"])) == nil
34
+ line.error = "#{index}, Invalid formatting of start timecode, [#{mres["start_timecode"]}]"
35
+ $stderr.puts line.error if @debug
35
36
  end
36
37
 
37
- if (line.end_time = SRT::File.parse_timecode(mres["end_timecode"])) == nil
38
- line.error = "#{line}, Invalid formatting of end timecode, [#{mres["end_timecode"]}]"
39
- puts line.error
38
+ if (line.end_time = Parser.timecode(mres["end_timecode"])) == nil
39
+ line.error = "#{index}, Invalid formatting of end timecode, [#{mres["end_timecode"]}]"
40
+ $stderr.puts line.error if @debug
40
41
  end
41
42
 
42
43
  if mres["display_coordinates"]
43
44
  line.display_coordinates = mres["display_coordinates"]
44
45
  end
45
46
  else
46
- line.error = "#{line}, Invalid Time Line formatting, [#{str}]"
47
- puts line.error
47
+ line.error = "#{index}, Invalid Time Line formatting, [#{str}]"
48
+ $stderr.puts line.error if @debug
48
49
  end
49
50
  else
50
51
  line.text << str.strip
51
52
  end
52
-
53
53
  end
54
54
  rescue
55
55
  line.error = "#{index}, General Error, [#{str}]"
56
- puts line.error
56
+ $stderr.puts line.error if @debug
57
57
  end
58
58
  end
59
59
  result
60
60
  end
61
61
 
62
- # Ruby often gets the wrong encoding for a file and will throw
63
- # errors on `split` for invalid byte sequences. This chain of
62
+ # Ruby often gets the wrong encoding for a file and will throw
63
+ # errors on `split` for invalid byte sequences. This chain of
64
64
  # fallback encodings lets us get something that works.
65
65
  def self.split_srt_data(srt_data)
66
66
  begin
67
67
  srt_data.split(/\n/) + ["\n"]
68
68
  rescue
69
69
  begin
70
+ srt_data = srt_data.unpack("C*").pack("U*")
70
71
  srt_data.force_encoding('utf-8').split(/\n/) + ["\n"]
71
72
  rescue
72
73
  srt_data.force_encoding('iso-8859-1').split(/\n/) + ["\n"]
@@ -75,8 +76,8 @@ module SRT
75
76
  end
76
77
 
77
78
  def append(options)
78
- if options.length == 1 && options.values[0].class == SRT::File
79
- reshift = SRT::File.parse_timecode(options.keys[0]) || (lines.last.end_time + SRT::File.parse_timespan(options.keys[0]))
79
+ if options.length == 1 && options.values[0].class == self.class
80
+ reshift = Parser.timecode(options.keys[0]) || (lines.last.end_time + Parser.timespan(options.keys[0]))
80
81
  renumber = lines.last.sequence
81
82
 
82
83
  options.values[0].lines.each do |line|
@@ -91,10 +92,20 @@ module SRT
91
92
  end
92
93
 
93
94
  def split(options)
94
- options = { :timeshift => true }.merge(options)
95
- if options[:at]
96
- split_points = [options[:at]].flatten.map{ |timecode| SRT::File.parse_timecode(timecode) }.sort
97
- split_offsprings = [SRT::File.new]
95
+ options = { :timeshift => true, :renumber => true }.merge(options)
96
+
97
+ split_points = []
98
+
99
+ if (options[:at])
100
+ split_points = [options[:at]].flatten.map{ |timecode| Parser.timecode(timecode) }.sort
101
+ elsif (options[:every])
102
+ interval = Parser.timecode(options[:every])
103
+ max = lines.last.end_time
104
+ (interval..max).step(interval){ |t| split_points << t }
105
+ end
106
+
107
+ if (split_points.count > 0)
108
+ split_offsprings = [File.new]
98
109
 
99
110
  reshift = 0
100
111
  renumber = 0
@@ -102,7 +113,7 @@ module SRT
102
113
  lines.each do |line|
103
114
  if split_points.empty? || line.end_time <= split_points.first
104
115
  cloned_line = line.clone
105
- cloned_line.sequence -= renumber
116
+ cloned_line.sequence -= renumber if options[:renumber]
106
117
  if options[:timeshift]
107
118
  cloned_line.start_time -= reshift
108
119
  cloned_line.end_time -= reshift
@@ -110,7 +121,7 @@ module SRT
110
121
  split_offsprings.last.lines << cloned_line
111
122
  elsif line.start_time < split_points.first
112
123
  cloned_line = line.clone
113
- cloned_line.sequence -= renumber
124
+ cloned_line.sequence -= renumber if options[:renumber]
114
125
  if options[:timeshift]
115
126
  cloned_line.start_time -= reshift
116
127
  cloned_line.end_time = split_points.first - reshift
@@ -121,9 +132,9 @@ module SRT
121
132
  reshift = split_points.first
122
133
  split_points.delete_at(0)
123
134
 
124
- split_offsprings << SRT::File.new
135
+ split_offsprings << File.new
125
136
  cloned_line = line.clone
126
- cloned_line.sequence -= renumber
137
+ cloned_line.sequence -= renumber if options[:renumber]
127
138
  if options[:timeshift]
128
139
  cloned_line.start_time = 0
129
140
  cloned_line.end_time -= reshift
@@ -134,9 +145,9 @@ module SRT
134
145
  reshift = split_points.first
135
146
  split_points.delete_at(0)
136
147
 
137
- split_offsprings << SRT::File.new
148
+ split_offsprings << File.new
138
149
  cloned_line = line.clone
139
- cloned_line.sequence -= renumber
150
+ cloned_line.sequence -= renumber if options[:renumber]
140
151
  if options[:timeshift]
141
152
  cloned_line.start_time -= reshift
142
153
  cloned_line.end_time -= reshift
@@ -151,26 +162,37 @@ module SRT
151
162
 
152
163
  def timeshift(options)
153
164
  if options.length == 1
154
- if options[:all] && (seconds = SRT::File.parse_timespan(options[:all]))
165
+ if options[:all] && (seconds = Parser.timespan(options[:all]))
155
166
  lines.each do |line|
156
167
  line.start_time += seconds
157
168
  line.end_time += seconds
158
169
  end
159
- elsif (original_framerate = SRT::File.parse_framerate(options.keys[0])) && (target_framerate = SRT::File.parse_framerate(options.values[0]))
160
- ratio = target_framerate / original_framerate
170
+ elsif (original_framerate = Parser.framerate(options.keys[0])) && (target_framerate = Parser.framerate(options.values[0]))
171
+ time_ratio = original_framerate / target_framerate
161
172
  lines.each do |line|
162
- line.start_time *= ratio
163
- line.end_time *= ratio
173
+ line.start_time *= time_ratio
174
+ line.end_time *= time_ratio
164
175
  end
165
176
  end
166
177
  elsif options.length == 2
167
- original_timecode_a = (options.keys[0].is_a?(String) ? SRT::File.parse_timecode(options.keys[0]) : lines[options.keys[0] - 1].start_time)
168
- original_timecode_b = (options.keys[1].is_a?(String) ? SRT::File.parse_timecode(options.keys[1]) : lines[options.keys[1] - 1].start_time)
169
- target_timecode_a = SRT::File.parse_timecode(options.values[0]) || (original_timecode_a + SRT::File.parse_timespan(options.values[0]))
170
- target_timecode_b = SRT::File.parse_timecode(options.values[1]) || (original_timecode_b + SRT::File.parse_timespan(options.values[1]))
178
+ origins, targets = options.keys, options.values
179
+
180
+ [0,1].each do |i|
181
+ if origins[i].is_a?(String) && Parser.id(origins[i])
182
+ origins[i] = lines[Parser.id(origins[i]) - 1].start_time
183
+ elsif origins[i].is_a?(String) && Parser.timecode(origins[i])
184
+ origins[i] = Parser.timecode(origins[i])
185
+ end
186
+
187
+ if targets[i].is_a?(String) && Parser.timecode(targets[i])
188
+ targets[i] = Parser.timecode(targets[i])
189
+ elsif targets[i].is_a?(String) && Parser.timespan(targets[i])
190
+ targets[i] = origins[i] + Parser.timespan(targets[i])
191
+ end
192
+ end
171
193
 
172
- time_rescale_factor = (target_timecode_b - target_timecode_a) / (original_timecode_b - original_timecode_a)
173
- time_rebase_shift = target_timecode_a - original_timecode_a * time_rescale_factor
194
+ time_rescale_factor = (targets[1] - targets[0]) / (origins[1] - origins[0])
195
+ time_rebase_shift = targets[0] - origins[0] * time_rescale_factor
174
196
 
175
197
  lines.each do |line|
176
198
  line.start_time = line.start_time * time_rescale_factor + time_rebase_shift
@@ -187,8 +209,17 @@ module SRT
187
209
  end
188
210
  end
189
211
 
190
- def to_s
191
- lines.map { |l| [l.sequence, (l.display_coordinates ? l.time_str + l.display_coordinates : l.time_str), l.text, ""] }.flatten.join("\n")
212
+ def to_s(time_str_function=:time_str)
213
+ lines.map { |l| l.to_s(time_str_function) }.join("\n")
214
+ end
215
+
216
+ def to_webvtt
217
+ header = <<eos
218
+ WEBVTT
219
+ X-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000
220
+
221
+ eos
222
+ header + to_s(:webvtt_time_str)
192
223
  end
193
224
 
194
225
  attr_writer :lines
@@ -200,29 +231,5 @@ module SRT
200
231
  def errors
201
232
  lines.collect { |l| l.error if l.error }.compact
202
233
  end
203
-
204
- protected
205
-
206
- def self.parse_framerate(framerate_string)
207
- mres = framerate_string.match(/(?<fps>\d+((\.)?\d+))(fps)/)
208
- mres ? mres["fps"].to_f : nil
209
- end
210
-
211
- def self.parse_timecode(timecode_string)
212
- mres = timecode_string.match(/(?<h>\d+):(?<m>\d+):(?<s>\d+),(?<mil>\d+)/)
213
- mres ? "#{mres["h"].to_i * 3600 + mres["m"].to_i * 60 + mres["s"].to_i}.#{mres["mil"]}".to_f : nil
214
- end
215
-
216
- def self.parse_timespan(timespan_string)
217
- factors = {
218
- "mil" => 0.001,
219
- "s" => 1,
220
- "m" => 60,
221
- "h" => 3600
222
- }
223
-
224
- mres = timespan_string.match(/(?<amount>(\+|-)?\d+((\.)?\d+))(?<unit>mil|s|m|h)/)
225
- mres ? mres["amount"].to_f * factors[mres["unit"]] : nil
226
- end
227
234
  end
228
235
  end
data/lib/srt/line.rb CHANGED
@@ -32,8 +32,18 @@ module SRT
32
32
  sequence.nil? && start_time.nil? && end_time.nil? && text.empty?
33
33
  end
34
34
 
35
- def time_str
36
- [@start_time, @end_time].map { |t| sprintf("%02d:%02d:%02d,%s", t / 3600, (t % 3600) / 60, t % 60, sprintf("%.3f", t)[-3, 3]) }.join(" --> ")
35
+ def time_str(subframe_separator=",")
36
+ [@start_time, @end_time].map { |t| f=sprintf("%.3f", t); ip=f[0,f.size-4].to_i;fp=f[-3,3]; "%02d:%02d:%02d#{subframe_separator}%s" % [ip / 3600, (ip % 3600) / 60, ip % 60,fp] }.join(" --> ")
37
+ end
38
+
39
+ def webvtt_time_str
40
+ time_str(".")
41
+ end
42
+
43
+ def to_s(time_str_function=:time_str)
44
+ content = text.empty? ? [''] : text
45
+ coordinates = display_coordinates ? display_coordinates : ""
46
+ [sequence, send(time_str_function) + coordinates, content, ""].flatten.join("\n")
37
47
  end
38
48
  end
39
49
  end
data/lib/srt/parser.rb ADDED
@@ -0,0 +1,31 @@
1
+ module SRT
2
+ class Parser
3
+ class << self
4
+ def framerate(framerate_string)
5
+ mres = framerate_string.match(/(?<fps>\d+((\.)?\d+))(fps)/)
6
+ mres ? mres["fps"].to_f : nil
7
+ end
8
+
9
+ def id(id_string)
10
+ mres = id_string.match(/#(?<id>\d+)/)
11
+ mres ? mres["id"].to_i : nil
12
+ end
13
+
14
+ def timecode(timecode_string)
15
+ mres = timecode_string.match(/(?<h>\d+):(?<m>\d+):(?<s>\d+)[,.]?(?<ms>\d+)?/)
16
+ mres ? "#{mres["h"].to_i * 3600 + mres["m"].to_i * 60 + mres["s"].to_i}.#{mres["ms"]}".to_f : nil
17
+ end
18
+
19
+ def timespan(timespan_string)
20
+ factors = {
21
+ "ms" => 0.001,
22
+ "s" => 1,
23
+ "m" => 60,
24
+ "h" => 3600
25
+ }
26
+ mres = timespan_string.match(/(?<amount>(\+|-)?\d+((\.)?\d+)?)(?<unit>ms|s|m|h)/)
27
+ mres ? mres["amount"].to_f * factors[mres["unit"]] : nil
28
+ end
29
+ end
30
+ end
31
+ end
data/lib/srt/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module SRT
2
- VERSION = "0.0.5"
2
+ VERSION = "0.1.5"
3
3
  end
data/lib/srt.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  require "srt/file"
2
2
  require "srt/line"
3
- require "srt/version"
3
+ require "srt/parser"
4
+ require "srt/version"