subtitle-library 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'subtitle-library'
4
+
5
+ HELP_MENU = <<-eos
6
+ -f : The local file, to do an operation on.
7
+ -op : The operation to be performed.
8
+ 'recognise' - recognise the subtitle format
9
+ 'verify' - verify subtitle syntax
10
+ 'carriage' - set the max line length
11
+ 'shift' - shift the subtitles by seconds/frames
12
+ 'stretch' - stretch the subtitles
13
+ 'save' - save the subtitles in new file (perhaps with a new format)
14
+ -cr : The max line length to be used
15
+ -ss : The amount of seconds to be used when shifting/stretching
16
+ -fs : The amount of frames to be used when shifting/stretching
17
+ -fps : Frames per second (will be used when saving/changing subtitles)
18
+ -nf : The path to the new file to be saved
19
+ -t : The type of the new file
20
+ 'sr' - SubRip subtitle type
21
+ 'md' - MicroDVD type
22
+ 'sv' - SubViewer type
23
+ eos
24
+
25
+ index = 0
26
+ fps = -1
27
+ while index < ARGV.length
28
+ case ARGV[index]
29
+ when '-f'
30
+ index += 1
31
+ subs_path = ARGV[index]
32
+ when '-op'
33
+ index += 1
34
+ operation = ARGV[index]
35
+ when '-cr'
36
+ index += 1
37
+ carriage = ARGV[index].to_i
38
+ when '-ss'
39
+ index += 1
40
+ seconds = ARGV[index].to_f
41
+ when '-fs'
42
+ index += 1
43
+ frames = ARGV[index].to_f
44
+ when '-fps'
45
+ index += 1
46
+ fps = ARGV[index].to_f
47
+ when '-nf'
48
+ index += 1
49
+ new_path = ARGV[index]
50
+ when '-t'
51
+ index += 1
52
+ type = ARGV[index]
53
+ end
54
+ index += 1
55
+ end
56
+
57
+ if operation and subs_path
58
+ reader = SubsReader.new subs_path
59
+ if reader.type == 'uk'
60
+ puts 'Unknown file format.'
61
+ else
62
+ case operation
63
+ when 'recognise'
64
+ case reader.type
65
+ when 'sr'
66
+ puts 'SubRip format.'
67
+ when 'md'
68
+ puts 'MicroDVD format.'
69
+ when 'sv'
70
+ puts 'SubViewer format.'
71
+ end
72
+ when 'verify'
73
+ puts reader.check_syntax
74
+ when 'carriage'
75
+ if not carriage or carriage <= 0
76
+ puts "Select a positive number for a max line length (-cr option).\n" + HELP_MENU
77
+ else
78
+ SubsChanger.new(subs_path).set_max_line carriage
79
+ end
80
+ when 'shift'
81
+ if not seconds and not frames
82
+ puts "Select an amount of seconds (-ss) or frames (-fs) to shift by.\n" + HELP_MENU
83
+ elsif seconds
84
+ SubsChanger.new(subs_path).shift 'ss', seconds, fps
85
+ else
86
+ SubsChanger.new(subs_path).shift 'fs', frames, fps
87
+ end
88
+ when 'stretch'
89
+ if not seconds and not frames
90
+ puts "Select an amount of seconds (-ss) or frames (-fs) to stretch by.\n" + HELP_MENU
91
+ elsif seconds
92
+ SubsChanger.new(subs_path).stretch 'ss', seconds, fps
93
+ else
94
+ SubsChanger.new(subs_path).stretch 'fs', frames, fps
95
+ end
96
+ when 'save'
97
+ if not new_path or not type
98
+ puts "Select the new name of the file (-nf) and the type it should be (-t).\n" + HELP_MENU
99
+ elsif not ['sr', 'md', 'sv'].member? type
100
+ puts "Select the type of the new file (-t option) from one of the listed below.\n" + HELP_MENU
101
+ else
102
+ reader.load_cues
103
+ SubsWriter.new(reader).save_as new_path, type, fps
104
+ end
105
+ else
106
+ puts "Select one of the operations listed below.\n" + HELP_MENU
107
+ end
108
+ end
109
+ elsif not operation
110
+ puts "Select an operation to be performed (-op option).\n" + HELP_MENU
111
+ else
112
+ puts "Select a local subtitle file (-f option).\n" + HELP_MENU
113
+ end
@@ -0,0 +1,3 @@
1
+ require 'subtitle-library/reader'
2
+ require 'subtitle-library/changer'
3
+ require 'subtitle-library/writer'
@@ -0,0 +1,117 @@
1
+ class SubsChanger
2
+ require 'subtitle-library/reader'
3
+ require 'subtitle-library/writer'
4
+
5
+ def initialize(subs_path)
6
+ @subs_path = subs_path
7
+ @reader = SubsReader.new subs_path
8
+ @reader.load_cues
9
+ end
10
+
11
+ def shift(disp_type, pos, fps = -1)
12
+ fps = @reader.fps if fps == -1
13
+ if @reader.type == 'md'
14
+ disposition_microdvd pos, fps, false, disp_type == 'ss'
15
+ else
16
+ disposition_timing pos, fps, false, disp_type == 'ss'
17
+ end
18
+ end
19
+
20
+ def stretch(disp_type, pos, fps = -1)
21
+ fps = @reader.fps if fps == -1
22
+ if @reader.type == 'md'
23
+ disposition_microdvd pos, fps, true, disp_type == 'ss'
24
+ else
25
+ disposition_timing pos, fps, true, disp_type == 'ss'
26
+ end
27
+ end
28
+
29
+ def disposition_microdvd(pos, fps, stretch, disp_seconds)
30
+ bottom_time = Time.mktime 1, 1, 1
31
+ last_end_time = bottom_time
32
+ invalid_timing = false
33
+ if stretch
34
+ step = disp_seconds ? pos * fps : pos
35
+ disposition = 0
36
+ else
37
+ disposition = disp_seconds ? (pos * fps).ceil : pos.ceil
38
+ end
39
+ @reader.cues.each do |cue|
40
+ cue.start += disposition
41
+ cue.ending += disposition
42
+ new_start = bottom_time + cue.start / fps
43
+ new_end = bottom_time + cue.ending / fps
44
+ if new_start.year + new_start.month + new_start.day +
45
+ new_end.year + new_end.month + new_end.day != 6 or
46
+ new_start < bottom_time or new_end < bottom_time or
47
+ new_start < last_end_time
48
+ invalid_timing = true
49
+ puts 'Invalid timing'
50
+ break
51
+ end
52
+ last_end_time = new_end
53
+ disposition = (disposition + step).ceil if stretch
54
+ end
55
+ SubsWriter.new(@reader).save_as(@subs_path, @reader.type) unless invalid_timing
56
+ end
57
+
58
+ def disposition_timing(pos, fps, stretch, disp_seconds)
59
+ bottom_time = Time.mktime 1, 1, 1
60
+ last_end_time = bottom_time
61
+ invalid_timing = false
62
+ if stretch
63
+ step = disp_seconds ? pos : pos / fps
64
+ disposition = 0
65
+ else
66
+ disposition = disp_seconds ? pos : pos / fps
67
+ end
68
+ @reader.cues.each do |cue|
69
+ cue.start += disposition
70
+ cue.ending += disposition
71
+ if cue.start.year + cue.start.month + cue.start.day +
72
+ cue.ending.year + cue.ending.month + cue.ending.day != 6 or
73
+ cue.start < bottom_time or cue.ending < bottom_time or
74
+ cue.start < last_end_time
75
+ invalid_timing = true
76
+ puts 'Invalid timing'
77
+ break
78
+ end
79
+ last_end_time = cue.ending
80
+ disposition += step if stretch
81
+ end
82
+ SubsWriter.new(@reader).save_as(@subs_path, @reader.type) unless invalid_timing
83
+ end
84
+
85
+ def set_max_line(max_line)
86
+ line_break = @reader.type == 'sr' ? "\n" : (@reader.type == 'md' ? '|' : '[br]')
87
+ @reader.cues.each do |cue|
88
+ text = cue.text
89
+ lines = text.split /\n+/
90
+ carriage_needed = false
91
+ lines.each do |line|
92
+ if line.length > max_line
93
+ carriage_needed = true
94
+ break
95
+ end
96
+ end
97
+ if carriage_needed
98
+ new_text = ''
99
+ words = lines.collect { |line| line.split /\s+/ }.flatten
100
+ current_line = ''
101
+ for i in (0 .. words.length - 1)
102
+ current_line += (current_line == '' ? '' : ' ') + words[i]
103
+ if i == words.length - 1
104
+ new_text += current_line
105
+ break
106
+ end
107
+ if current_line.length > max_line
108
+ new_text += current_line + line_break
109
+ current_line = ''
110
+ end
111
+ end
112
+ cue.text = new_text
113
+ end
114
+ end
115
+ SubsWriter.new(@reader).save_as @subs_path, @reader.type
116
+ end
117
+ end
@@ -0,0 +1,16 @@
1
+ class Cue
2
+ attr_accessor :start, :ending, :text
3
+
4
+ def initialize(start, ending, text)
5
+ @start = start
6
+ @ending = ending
7
+ @text = text
8
+ end
9
+
10
+ def ==(other_cue)
11
+ @start == other_cue.start and
12
+ @ending == other_cue.ending and
13
+ @text == other_cue.text
14
+ end
15
+ end
16
+
@@ -0,0 +1,357 @@
1
+ class SubsReader
2
+ require 'subtitle-library/regex-patterns'
3
+ require 'subtitle-library/cue'
4
+ include Patterns
5
+
6
+ attr_reader :type, :fps
7
+ attr_accessor :cues
8
+
9
+ def initialize(subs_path)
10
+ @subs_path = subs_path
11
+ @type = recognize
12
+ case @type
13
+ when 'sr'
14
+ @inner_reader = SubRipReader.new subs_path
15
+ when 'md'
16
+ @inner_reader = MicroDVDReader.new subs_path
17
+ when 'sv'
18
+ @inner_reader = SubviewerReader.new subs_path
19
+ end
20
+ @fps = 23.976
21
+ end
22
+
23
+ def recognize
24
+ File.open(@subs_path, 'r') do |subs|
25
+ while line = subs.gets
26
+ return 'sr' if SUB_RIP_LINE =~ line
27
+ return 'md' if MICRO_DVD_LINE =~ line
28
+ return 'sv' if SUBVIEWER_LINE =~ line
29
+ end
30
+ end
31
+ 'uk'
32
+ end
33
+
34
+ def load_cues
35
+ if @inner_reader
36
+ @cues, @fps = @inner_reader.read_subs false
37
+ else
38
+ 'Unknown file format'
39
+ end
40
+ end
41
+
42
+ def check_syntax
43
+ if @inner_reader
44
+ @inner_reader.read_subs true
45
+ else
46
+ 'Unknown file format'
47
+ end
48
+ end
49
+
50
+ end
51
+
52
+ class SubRipReader
53
+ include Patterns
54
+
55
+ def initialize(subs_path)
56
+ @subs_path = subs_path
57
+ @fps = 23.976
58
+ end
59
+
60
+ def read_subs(check_syntax)
61
+ cues = []
62
+ actual_lines = 0
63
+ error_log = ""
64
+ is_eof = false
65
+ last_end_time = Time.new 1, 1, 1
66
+ File.open(@subs_path, 'r') do |subs|
67
+ while true
68
+ is_eof, actual_lines, strip_line = read_until_timing(subs, actual_lines)
69
+ break if is_eof
70
+ if SUB_RIP_LINE =~ strip_line
71
+ start_time, end_time = parse_timing strip_line
72
+ valid_timing, error_log = check_timing start_time, end_time, last_end_time, error_log, check_syntax, actual_lines
73
+ unless valid_timing
74
+ is_eof, actual_lines, strip_line = read_until_index subs, actual_lines, strip_line, false
75
+ break if is_eof
76
+ next
77
+ end
78
+ last_end_time = end_time
79
+ line = subs.gets
80
+ break unless line
81
+ actual_lines += 1
82
+ text = line.strip
83
+ strip_line = line.strip
84
+ while strip_line != ''
85
+ line = subs.gets
86
+ unless line
87
+ is_eof = true
88
+ break
89
+ end
90
+ actual_lines += 1
91
+ strip_line = line.strip
92
+ text += "\n" + strip_line
93
+ end
94
+ if is_eof
95
+ cues << Cue.new(start_time, end_time, text.rstrip) unless check_syntax
96
+ break
97
+ end
98
+ line = subs.gets
99
+ unless line
100
+ cues << Cue.new(start_time, end_time, text.rstrip) unless check_syntax
101
+ break
102
+ end
103
+ actual_lines += 1
104
+ is_eof, actual_lines, strip_line, text = read_until_index subs, actual_lines, line, true, text
105
+ cues << Cue.new(start_time, end_time, text.strip) unless check_syntax
106
+ break if is_eof
107
+ elsif check_syntax
108
+ error_log += "Syntax error at line #{actual_lines}.\n"
109
+ line = subs.gets
110
+ break unless line
111
+ actual_lines += 1
112
+ is_eof, actual_lines, strip_line = read_until_index subs, actual_lines, line, false
113
+ break if is_eof
114
+ else
115
+ line = subs.gets
116
+ break unless line
117
+ actual_lines += 1
118
+ is_eof, actual_lines, strip_line = read_until_index subs, actual_lines, line, false
119
+ break if is_eof
120
+ end
121
+ end
122
+ end
123
+ if check_syntax
124
+ error_log == '' ? 'No errors were found.' : error_log.rstrip
125
+ else
126
+ [cues, @fps]
127
+ end
128
+ end
129
+
130
+ def check_timing(start_time, end_time, last_end_time, error_log, check_syntax, actual_lines)
131
+ if start_time.year + start_time.month + start_time.day +
132
+ end_time.year + end_time.month + end_time.day != 6 or
133
+ start_time >= end_time or start_time < last_end_time
134
+ if check_syntax
135
+ error_log += "Invalid timing at line #{actual_lines}.\n"
136
+ else
137
+ puts "Invalid timing at line #{actual_lines}.\n"
138
+ end
139
+ return [false, error_log]
140
+ end
141
+ [true, error_log]
142
+ end
143
+
144
+ def read_until_timing(subs, actual_lines)
145
+ line = subs.gets
146
+ return true unless line
147
+ actual_lines += 1
148
+ strip_line = line.strip
149
+ while strip_line == '' or /\A\d+$/ =~ strip_line
150
+ line = subs.gets
151
+ unless line
152
+ is_eof = true
153
+ break
154
+ end
155
+ actual_lines += 1
156
+ strip_line = line.strip
157
+ end
158
+ [is_eof, actual_lines, strip_line]
159
+ end
160
+
161
+ def parse_timing(line)
162
+ match = SUB_RIP_TIMING.match line
163
+ time_args = [1, 1, 1] + match.to_s.split(/,|:/).collect(&:to_i)
164
+ time_args[6] *= 1000
165
+ start_time = Time.mktime *time_args
166
+ match = SUB_RIP_TIMING.match match.post_match
167
+ time_args = [1, 1, 1] + match.to_s.split(/,|:/).collect(&:to_i)
168
+ time_args[6] *= 1000
169
+ [start_time, Time.mktime(*time_args)]
170
+ end
171
+
172
+ def read_until_index(subs, actual_lines, line, append, text = nil)
173
+ strip_line = line.strip
174
+ while not /\A\d+$/ =~ strip_line
175
+ text += "\n" + strip_line if append
176
+ line = subs.gets
177
+ unless line
178
+ is_eof = true
179
+ break
180
+ end
181
+ actual_lines += 1
182
+ strip_line = line.strip
183
+ end
184
+ [is_eof, actual_lines, strip_line] + (append ? [text] : [])
185
+ end
186
+
187
+ end
188
+
189
+ class MicroDVDReader
190
+ include Patterns
191
+
192
+ def initialize(subs_path)
193
+ @subs_path = subs_path
194
+ @fps = 23.976
195
+ end
196
+
197
+ def read_subs(check_syntax)
198
+ cues = []
199
+ error_log = ''
200
+ last_end_frame = 0
201
+ File.open(@subs_path, 'r') do |subs|
202
+ line, actual_lines = find_out_fps subs
203
+ while line
204
+ strip_line = line.strip
205
+ if strip_line != ''
206
+ if MICRO_DVD_LINE =~ strip_line
207
+ last_end_frame, error_log = add_new_line strip_line, cues, last_end_frame, error_log, check_syntax, actual_lines
208
+ elsif check_syntax
209
+ error_log += "Syntax error at line #{actual_lines}.\n"
210
+ end
211
+ end
212
+ line = subs.gets
213
+ actual_lines += 1
214
+ end
215
+ end
216
+ if check_syntax
217
+ error_log == '' ? 'No errors were found.' : error_log.rstrip
218
+ else
219
+ [cues, @fps]
220
+ end
221
+ end
222
+
223
+ def find_out_fps(subs)
224
+ line = subs.gets
225
+ actual_lines = 1
226
+ while line
227
+ strip_line = line.strip
228
+ if strip_line != ''
229
+ if MICRO_DVD_LINE =~ strip_line
230
+ first_line = MICRO_DVD_LINE.match(strip_line).post_match.strip
231
+ if /\A\d*\.?\d*$/ =~ first_line
232
+ @fps = first_line.to_f
233
+ line = subs.gets
234
+ actual_lines += 1
235
+ end
236
+ end
237
+ break
238
+ end
239
+ line = subs.gets
240
+ actual_lines += 1
241
+ end
242
+ [line, actual_lines]
243
+ end
244
+
245
+ def add_new_line(line, cues, last_end_frame, error_log, check_syntax, actual_lines)
246
+ match = /\d+/.match line
247
+ start_frame = match.to_s.to_i
248
+ match = /\d+/.match match.post_match
249
+ end_frame = match.to_s.to_i
250
+ if start_frame <= end_frame and start_frame >= last_end_frame
251
+ unless check_syntax
252
+ text = MICRO_DVD_LINE.match(line).post_match
253
+ cues << Cue.new(start_frame, end_frame, text.gsub('|', "\n"))
254
+ end
255
+ last_end_frame = end_frame
256
+ elsif check_syntax
257
+ error_log += "Syntax error at line #{actual_lines}.\n"
258
+ end
259
+ [last_end_frame, error_log]
260
+ end
261
+
262
+ end
263
+
264
+ class SubviewerReader
265
+ include Patterns
266
+
267
+ def initialize(subs_path)
268
+ @subs_path = subs_path
269
+ @fps = 23.976
270
+ end
271
+
272
+ def read_subs(check_syntax)
273
+ cues = []
274
+ error_log = ''
275
+ last_end_time = Time.mktime 1, 1, 1
276
+ File.open(@subs_path, 'r') do |subs|
277
+ actual_lines, error_log, line = read_metadata subs, check_syntax
278
+ while line
279
+ strip_line = line.strip
280
+ if strip_line != ''
281
+ if SUBVIEWER_LINE =~ strip_line
282
+ start_time, end_time = parse_timing strip_line
283
+ valid_timing, error_log = check_timing start_time, end_time, last_end_time, error_log, check_syntax, actual_lines
284
+ unless valid_timing
285
+ break unless subs.gets
286
+ line = subs.gets
287
+ actual_lines += 2
288
+ next
289
+ end
290
+ line = subs.gets
291
+ break unless line
292
+ actual_lines += 1
293
+ cues << Cue.new(start_time, end_time, line.strip.gsub('[br]', "\n")) unless check_syntax
294
+ last_end_time = end_time
295
+ else
296
+ error_log += "Syntax error at line #{actual_lines}.\n" if check_syntax
297
+ break unless subs.gets
298
+ line = subs.gets
299
+ actual_lines += 2
300
+ next
301
+ end
302
+ end
303
+ line = subs.gets
304
+ actual_lines += 1
305
+ end
306
+ end
307
+ if check_syntax
308
+ error_log == '' ? 'No errors were found.' : error_log.rstrip
309
+ else
310
+ [cues, @fps]
311
+ end
312
+ end
313
+
314
+ def read_metadata(subs, check_syntax)
315
+ actual_lines = 0
316
+ error_log = ''
317
+ metadata = ''
318
+ while line = subs.gets
319
+ actual_lines += 1
320
+ strip_line = line.strip
321
+ if strip_line != ''
322
+ if /\A\d/ =~ strip_line
323
+ error_log += "Syntax error in metadata.\n" if check_syntax and not SUBVIEWER_METADATA =~ metadata
324
+ break
325
+ end
326
+ metadata += strip_line
327
+ end
328
+ end
329
+ [actual_lines, error_log, line]
330
+ end
331
+
332
+ def parse_timing(line)
333
+ start_end = line.split ','
334
+ time_args = [1,1,1] + start_end[0].split(/\.|:/).collect(&:to_i)
335
+ time_args[6] *= 1000
336
+ start_time = Time.mktime *time_args
337
+ time_args = [1,1,1] + start_end[1].split(/\.|:/).collect(&:to_i)
338
+ time_args[6] *= 1000
339
+ [start_time, Time.mktime(*time_args)]
340
+ end
341
+
342
+ def check_timing(start_time, end_time, last_end_time, error_log, check_syntax, actual_lines)
343
+ if start_time.year + start_time.month + start_time.day +
344
+ end_time.year + end_time.month + end_time.day != 6 or
345
+ start_time >= end_time or start_time < last_end_time
346
+ if check_syntax
347
+ error_log += "Invalid timing at line #{actual_lines}.\n"
348
+ else
349
+ puts "Invalid timing at line #{actual_lines}.\n"
350
+ end
351
+ return [false, error_log]
352
+ end
353
+ [true, error_log]
354
+ end
355
+
356
+ end
357
+