srt 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ Mzk5Mjk2ZGU5NjI2NTZjYWVmN2JkN2E1MTY4ZjJkMmEzZmRmNzA5ZA==
5
+ data.tar.gz: !binary |-
6
+ MTIxY2EyYTczM2Q2NTg1NjZlZGNlNDQ4ZGQ4MDRmZDBmOWE3OTA1Yg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ MTQyM2M1Y2I4ZDY5MGJkNTk3MWVkNGJkNzk5ZWI5MTJkYjZkMjBiYzhmMDY0
10
+ YThlZDQyOWQ5YzRmYjNjODkwYjFmMGM4ZjQ1ZTgwN2QwMTJiOWM3Y2I0Y2M1
11
+ Njc4NTg5ZjI5ZTNiZTAxZjYzYzQzZjg2YmFjODY5NzUyOTVjOTE=
12
+ data.tar.gz: !binary |-
13
+ NzIyNzExYzBiMjYzZWMzOGVkZDMyZjhiN2EyNWNmNGZhNThhNzkwMDhmNGRk
14
+ NGE3ZTMxMjA5YmNlYjJmYmNmZmZmYTgwNDIzOWJkZjA2YzE2YzAyZThlOGY5
15
+ ZTNhYTBlN2ZiYmFlYmY2ZmM4NzI0ODc5NTg4MmFmNDY5YjE1NTE=
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/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,52 +16,51 @@ 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
@@ -75,8 +75,8 @@ module SRT
75
75
  end
76
76
 
77
77
  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]))
78
+ if options.length == 1 && options.values[0].class == self.class
79
+ reshift = Parser.timecode(options.keys[0]) || (lines.last.end_time + Parser.timespan(options.keys[0]))
80
80
  renumber = lines.last.sequence
81
81
 
82
82
  options.values[0].lines.each do |line|
@@ -91,10 +91,20 @@ module SRT
91
91
  end
92
92
 
93
93
  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]
94
+ options = { :timeshift => true, :renumber => true }.merge(options)
95
+
96
+ split_points = []
97
+
98
+ if (options[:at])
99
+ split_points = [options[:at]].flatten.map{ |timecode| Parser.timecode(timecode) }.sort
100
+ elsif (options[:every])
101
+ interval = Parser.timecode(options[:every])
102
+ max = lines.last.end_time
103
+ (interval..max).step(interval){ |t| split_points << t }
104
+ end
105
+
106
+ if (split_points.count > 0)
107
+ split_offsprings = [File.new]
98
108
 
99
109
  reshift = 0
100
110
  renumber = 0
@@ -102,7 +112,7 @@ module SRT
102
112
  lines.each do |line|
103
113
  if split_points.empty? || line.end_time <= split_points.first
104
114
  cloned_line = line.clone
105
- cloned_line.sequence -= renumber
115
+ cloned_line.sequence -= renumber if options[:renumber]
106
116
  if options[:timeshift]
107
117
  cloned_line.start_time -= reshift
108
118
  cloned_line.end_time -= reshift
@@ -110,7 +120,7 @@ module SRT
110
120
  split_offsprings.last.lines << cloned_line
111
121
  elsif line.start_time < split_points.first
112
122
  cloned_line = line.clone
113
- cloned_line.sequence -= renumber
123
+ cloned_line.sequence -= renumber if options[:renumber]
114
124
  if options[:timeshift]
115
125
  cloned_line.start_time -= reshift
116
126
  cloned_line.end_time = split_points.first - reshift
@@ -121,9 +131,9 @@ module SRT
121
131
  reshift = split_points.first
122
132
  split_points.delete_at(0)
123
133
 
124
- split_offsprings << SRT::File.new
134
+ split_offsprings << File.new
125
135
  cloned_line = line.clone
126
- cloned_line.sequence -= renumber
136
+ cloned_line.sequence -= renumber if options[:renumber]
127
137
  if options[:timeshift]
128
138
  cloned_line.start_time = 0
129
139
  cloned_line.end_time -= reshift
@@ -134,9 +144,9 @@ module SRT
134
144
  reshift = split_points.first
135
145
  split_points.delete_at(0)
136
146
 
137
- split_offsprings << SRT::File.new
147
+ split_offsprings << File.new
138
148
  cloned_line = line.clone
139
- cloned_line.sequence -= renumber
149
+ cloned_line.sequence -= renumber if options[:renumber]
140
150
  if options[:timeshift]
141
151
  cloned_line.start_time -= reshift
142
152
  cloned_line.end_time -= reshift
@@ -151,12 +161,12 @@ module SRT
151
161
 
152
162
  def timeshift(options)
153
163
  if options.length == 1
154
- if options[:all] && (seconds = SRT::File.parse_timespan(options[:all]))
164
+ if options[:all] && (seconds = Parser.timespan(options[:all]))
155
165
  lines.each do |line|
156
166
  line.start_time += seconds
157
167
  line.end_time += seconds
158
168
  end
159
- elsif (original_framerate = SRT::File.parse_framerate(options.keys[0])) && (target_framerate = SRT::File.parse_framerate(options.values[0]))
169
+ elsif (original_framerate = Parser.framerate(options.keys[0])) && (target_framerate = Parser.framerate(options.values[0]))
160
170
  ratio = target_framerate / original_framerate
161
171
  lines.each do |line|
162
172
  line.start_time *= ratio
@@ -164,13 +174,24 @@ module SRT
164
174
  end
165
175
  end
166
176
  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]))
177
+ origins, targets = options.keys, options.values
178
+
179
+ [0,1].each do |i|
180
+ if origins[i].is_a?(String) && Parser.id(origins[i])
181
+ origins[i] = lines[Parser.id(origins[i]) - 1].start_time
182
+ elsif origins[i].is_a?(String) && Parser.timecode(origins[i])
183
+ origins[i] = Parser.timecode(origins[i])
184
+ end
185
+
186
+ if targets[i].is_a?(String) && Parser.timecode(targets[i])
187
+ targets[i] = Parser.timecode(targets[i])
188
+ elsif targets[i].is_a?(String) && Parser.timespan(targets[i])
189
+ targets[i] = origins[i] + Parser.timespan(targets[i])
190
+ end
191
+ end
171
192
 
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
193
+ time_rescale_factor = (targets[1] - targets[0]) / (origins[1] - origins[0])
194
+ time_rebase_shift = targets[0] - origins[0] * time_rescale_factor
174
195
 
175
196
  lines.each do |line|
176
197
  line.start_time = line.start_time * time_rescale_factor + time_rebase_shift
@@ -187,8 +208,17 @@ module SRT
187
208
  end
188
209
  end
189
210
 
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")
211
+ def to_s(time_str_function=:time_str)
212
+ lines.map { |l| [l.sequence, (l.display_coordinates ? l.send(time_str_function) + l.display_coordinates : l.send(time_str_function)), l.text, ""] }.flatten.join("\n")
213
+ end
214
+
215
+ def to_webvtt
216
+ header = <<eos
217
+ WEBVTT
218
+ X-TIMESTAMP-MAP=MPEGTS:0,LOCAL:00:00:00.000
219
+
220
+ eos
221
+ header + to_s(:webvtt_time_str)
192
222
  end
193
223
 
194
224
  attr_writer :lines
@@ -200,29 +230,5 @@ module SRT
200
230
  def errors
201
231
  lines.collect { |l| l.error if l.error }.compact
202
232
  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
233
  end
228
234
  end
data/lib/srt/line.rb CHANGED
@@ -32,8 +32,12 @@ 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| sprintf("%02d:%02d:%02d#{subframe_separator}%s", t / 3600, (t % 3600) / 60, t % 60, sprintf("%.3f", t)[-3, 3]) }.join(" --> ")
37
+ end
38
+
39
+ def webvtt_time_str
40
+ time_str(".")
37
41
  end
38
42
  end
39
43
  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.0"
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"