subtitle-library 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+