captive 1.0.0
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +94 -0
- data/bin/console +7 -0
- data/lib/captive.rb +9 -0
- data/lib/captive/base.rb +91 -0
- data/lib/captive/cue.rb +92 -0
- data/lib/captive/error.rb +19 -0
- data/lib/captive/formats/srt.rb +75 -0
- data/lib/captive/formats/vtt.rb +101 -0
- data/lib/captive/util.rb +55 -0
- data/lib/captive/version.rb +5 -0
- metadata +124 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bf5006d6ac1443e871a8d081974beaa6eabbf72546a25e2fa7aadefd6b9c50f7
|
4
|
+
data.tar.gz: 0f4eadcf1a9cb07883cdf46f79ad371095339b8e2182ae842fbe0ffb1c833a47
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a3e7f5db587122c189bf7253e02ed79b0e4df13039aa3689d1b0a8c9062b16308bae85d5480b2b1574fb105688274c5dc0abaf8864416a14e8886ac02b4c36f2
|
7
|
+
data.tar.gz: 6622b6df7e11103bea6a511b2cf53e918023955e6e6fbb1fa3cbb7d6f81373f4896cb6504754eeaf0bff717f5608f64360dc050ae1f0e750c0aa6577da4a9c85
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 navin
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# Captive - Ruby Subtitle Editor and Converter
|
2
|
+
|
3
|
+
Captive can read subtitles from various formats. Subtitles can be modified and exported to another format as well as JSON. Captive currently supports `SRT` and `WebVTT` formats.
|
4
|
+
|
5
|
+
## Supported Features
|
6
|
+
|
7
|
+
- Read subtitles
|
8
|
+
- Convert to another format (SRT, WebVTT)
|
9
|
+
- Save to file
|
10
|
+
- Serialize as JSON
|
11
|
+
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
**Parse subtitles**
|
16
|
+
|
17
|
+
Subtitles can be read from a file using `from_file` and providing a filename
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
s = Captive::SRT.from_file(filename: 'test.srt')
|
21
|
+
|
22
|
+
s = Captive::VTT.from_file(filename: 'test.vtt')
|
23
|
+
```
|
24
|
+
|
25
|
+
Alternately, subtitles can be parsed from a blob of text if you don't want to load from a file
|
26
|
+
```ruby
|
27
|
+
s = Captive::SRT.from_blob(filename: 'test.srt')
|
28
|
+
|
29
|
+
s = Captive::VTT.from_blob(filename: 'test.vtt')
|
30
|
+
```
|
31
|
+
|
32
|
+
Or to get instantiate an empty captive object simply
|
33
|
+
```ruby
|
34
|
+
s = Captive::SRT.new
|
35
|
+
|
36
|
+
s = Captive::VTT.new
|
37
|
+
```
|
38
|
+
|
39
|
+
**Save to File**
|
40
|
+
|
41
|
+
Subtitles can be saved to a file using `save_as`
|
42
|
+
```ruby
|
43
|
+
s = Captive::VTT.from_file(filename: 'test.vtt')
|
44
|
+
s.save_as(filaname: 'output.vtt')
|
45
|
+
```
|
46
|
+
|
47
|
+
**Serializing as JSON**
|
48
|
+
|
49
|
+
Need to store your subtitle data in a format agnostic way? `as_json` is your friend
|
50
|
+
```ruby
|
51
|
+
s = Captive::VTT.from_file(filename: 'test.vtt')
|
52
|
+
s.as_json
|
53
|
+
```
|
54
|
+
|
55
|
+
**Switch Formats**
|
56
|
+
|
57
|
+
Subtitles parsed in one format can be converted to another format. Currently, **SRT** and **WebVTT** are supported.
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
s = Captive::SRT.new('test.srt')
|
61
|
+
vtt = s.as_vtt # Will return a Captive::VTT instance
|
62
|
+
vtt.save_as(filename: 'conversion.vtt')
|
63
|
+
```
|
64
|
+
|
65
|
+
|
66
|
+
## Installation
|
67
|
+
|
68
|
+
Simply add `captive` to your Gemfile
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
gem 'captive'
|
72
|
+
```
|
73
|
+
|
74
|
+
Or install it yourself with:
|
75
|
+
|
76
|
+
$ gem install captive
|
77
|
+
|
78
|
+
## Dependencies
|
79
|
+
|
80
|
+
Captive is lightweight and has no external dependencies.
|
81
|
+
|
82
|
+
## Development
|
83
|
+
|
84
|
+
After cloning the repo, run `bundle install` to get the development dependencies. Use `bin/console` to spin up an IRB instance with captive loaded.
|
85
|
+
|
86
|
+
## Contributing
|
87
|
+
|
88
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/mserran2/captive.
|
89
|
+
|
90
|
+
|
91
|
+
## License
|
92
|
+
|
93
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
94
|
+
|
data/bin/console
ADDED
data/lib/captive.rb
ADDED
data/lib/captive/base.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Captive
|
4
|
+
module Base
|
5
|
+
module ClassMethods
|
6
|
+
def from_file(filename:)
|
7
|
+
file = File.new(filename, 'r:bom|utf-8')
|
8
|
+
blob = file.read
|
9
|
+
from_blob(blob: blob)
|
10
|
+
ensure
|
11
|
+
file&.close
|
12
|
+
end
|
13
|
+
|
14
|
+
def from_blob(blob:)
|
15
|
+
new(cue_list: parse(blob: blob))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.included(base)
|
20
|
+
base.extend(ClassMethods)
|
21
|
+
base.include(Util)
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_accessor(:cue_list)
|
25
|
+
|
26
|
+
def initialize(cue_list: nil)
|
27
|
+
@cue_list = cue_list || []
|
28
|
+
end
|
29
|
+
|
30
|
+
def save_as(filename:)
|
31
|
+
File.open(filename, 'w') do |file|
|
32
|
+
file.write(to_s)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def as_json(**args)
|
37
|
+
results = {
|
38
|
+
'version' => VERSION,
|
39
|
+
'cues' => @cue_list.map(&:as_json),
|
40
|
+
}
|
41
|
+
if results.respond_to?(:as_json)
|
42
|
+
results.as_json(**args)
|
43
|
+
else
|
44
|
+
results
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def method_missing(method_name, *_args)
|
49
|
+
super unless (match = /^as_([a-z]+)$/.match(method_name))
|
50
|
+
|
51
|
+
if valid_format?(match.captures.first.upcase)
|
52
|
+
define_format_conversion(method_name, match.captures.first.upcase)
|
53
|
+
send(method_name)
|
54
|
+
elsif valid_format?(match.captures.first.capitalize)
|
55
|
+
define_format_conversion(method_name, match.captures.first.capitalize)
|
56
|
+
send(method_name)
|
57
|
+
else
|
58
|
+
super
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def respond_to_missing?(method_name, _)
|
63
|
+
super unless (match = /^as_([a-z]+)$/.match(method_name))
|
64
|
+
|
65
|
+
return true if valid_format?(match.captures.first.upcase) || valid_format?(match.captures.first.capitalize)
|
66
|
+
|
67
|
+
super
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def base_klass
|
73
|
+
base = self.class.to_s.split('::')
|
74
|
+
Kernel.const_get(base.first)
|
75
|
+
end
|
76
|
+
|
77
|
+
def valid_format?(format)
|
78
|
+
base_klass.const_defined?(format) && base_klass.const_get(format).include?(Base)
|
79
|
+
end
|
80
|
+
|
81
|
+
def define_format_conversion(method_name, format)
|
82
|
+
self.class.define_method(method_name) do
|
83
|
+
if self.class.to_s.split('::').last == format
|
84
|
+
self
|
85
|
+
else
|
86
|
+
base_klass.const_get(format).new(cue_list: cue_list)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/captive/cue.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Captive
|
4
|
+
class Cue
|
5
|
+
include Util
|
6
|
+
|
7
|
+
# Text Properties supported
|
8
|
+
ALIGNMENT = 'alignment'
|
9
|
+
COLOR = 'color'
|
10
|
+
POSITION = 'position'
|
11
|
+
|
12
|
+
# List of Text Properties
|
13
|
+
TEXT_PROPERTIES = [ALIGNMENT, COLOR, POSITION].freeze
|
14
|
+
|
15
|
+
attr_accessor :number, :text, :properties
|
16
|
+
attr_reader :start_time, :end_time
|
17
|
+
|
18
|
+
# Creates a new Cue class denoting a subtitle.
|
19
|
+
def initialize(text: nil, start_time: nil, end_time: nil, cue_number: nil, properties: {})
|
20
|
+
self.text = text
|
21
|
+
self.start_time = start_time
|
22
|
+
self.end_time = end_time
|
23
|
+
self.number = cue_number
|
24
|
+
self.properties = properties || {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def start_time=(time)
|
28
|
+
set_time(:start_time, time)
|
29
|
+
end
|
30
|
+
|
31
|
+
def end_time=(time)
|
32
|
+
set_time(:end_time, time)
|
33
|
+
end
|
34
|
+
|
35
|
+
def set_times(start_time:, end_time:)
|
36
|
+
self.start_time = start_time
|
37
|
+
self.end_time = end_time
|
38
|
+
end
|
39
|
+
|
40
|
+
# Getter and Setter methods for Text Properties
|
41
|
+
TEXT_PROPERTIES.each do |setting|
|
42
|
+
define_method :"#{setting}" do
|
43
|
+
return properties[setting] if properties[setting].present?
|
44
|
+
|
45
|
+
return nil
|
46
|
+
end
|
47
|
+
|
48
|
+
define_method :"#{setting}=" do |value|
|
49
|
+
properties[setting] = value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def duration
|
54
|
+
end_time - start_time
|
55
|
+
end
|
56
|
+
|
57
|
+
# Adds text. If text is already present, new-line is added before text.
|
58
|
+
def add_text(text)
|
59
|
+
if self.text.nil?
|
60
|
+
self.text = text
|
61
|
+
else
|
62
|
+
self.text += "\n" + text
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def <=>(other)
|
67
|
+
start_time <=> other.start_time
|
68
|
+
end
|
69
|
+
|
70
|
+
def as_json(**args)
|
71
|
+
if respond_to?(:instance_values) && instance_values.respond_to?(:as_json)
|
72
|
+
instance_values.as_json(**args)
|
73
|
+
else
|
74
|
+
instance_variables.each_with_object({}) { |key, hash| hash[key[1..-1]] = instance_variable_get(key) }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def set_time(field, time)
|
81
|
+
return if time.nil?
|
82
|
+
|
83
|
+
if time.is_a?(Integer)
|
84
|
+
instance_variable_set("@#{field}", time)
|
85
|
+
elsif TIMECODE_REGEX.match(time)
|
86
|
+
instance_variable_set("@#{field}", timecode_to_milliseconds(time))
|
87
|
+
else
|
88
|
+
raise InvalidInput, "Input for #{field} should be an integer denoting milliseconds or a valid timecode."
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Captive
|
4
|
+
# Base error class.
|
5
|
+
class CaptiveError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
# Error denoting a malformed string during subtitle parsing.
|
9
|
+
class MalformedString < CaptiveError
|
10
|
+
end
|
11
|
+
|
12
|
+
# Error denoting a malformed subtitle input format.
|
13
|
+
class InvalidSubtitle < CaptiveError
|
14
|
+
end
|
15
|
+
|
16
|
+
# Error denoting incorrect input to a method.
|
17
|
+
class InvalidInput < CaptiveError
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Captive
|
4
|
+
class SRT
|
5
|
+
include Base
|
6
|
+
|
7
|
+
def self.parse(blob:)
|
8
|
+
cue_list = []
|
9
|
+
lines = blob.split("\n")
|
10
|
+
count = 0
|
11
|
+
state = :new_cue
|
12
|
+
cue = nil
|
13
|
+
|
14
|
+
lines.each_with_index do |line, _index|
|
15
|
+
line.strip!
|
16
|
+
case state
|
17
|
+
when :new_cue
|
18
|
+
next if line.empty? ## just another blank line, remain in new_cue state
|
19
|
+
|
20
|
+
raise InvalidSubtitle, "Invalid Cue Number at line #{count}" if /^\d+$/.match(line).nil?
|
21
|
+
|
22
|
+
cue = Cue.new(cue_number: line.to_i)
|
23
|
+
state = :time
|
24
|
+
when :time
|
25
|
+
raise InvalidSubtitle, "Invalid Time Format at line #{count}" unless timecode?(line)
|
26
|
+
|
27
|
+
start_time, end_time = line.split('-->').map(&:strip)
|
28
|
+
cue.set_times(
|
29
|
+
start_time: format_time(start_time),
|
30
|
+
end_time: format_time(end_time)
|
31
|
+
)
|
32
|
+
state = :text
|
33
|
+
when :text
|
34
|
+
if line.empty?
|
35
|
+
## end of previous cue
|
36
|
+
cue_list << cue
|
37
|
+
cue = nil
|
38
|
+
state = :new_cue
|
39
|
+
else
|
40
|
+
cue.add_text(line)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Check to make sure we add the last cue if for some reason the file lacks an empty line at the end
|
46
|
+
cue_list << cue unless cue.nil?
|
47
|
+
|
48
|
+
# Return the cue_list
|
49
|
+
cue_list
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_s
|
53
|
+
string = String.new
|
54
|
+
@cue_list.each do |cue|
|
55
|
+
string << cue.number.to_s
|
56
|
+
string << "\n"
|
57
|
+
string << milliseconds_to_timecode(cue.start_time).gsub!('.', ',')
|
58
|
+
string << ' --> '
|
59
|
+
string << milliseconds_to_timecode(cue.end_time).gsub!('.', ',')
|
60
|
+
string << "\n"
|
61
|
+
string << cue.text
|
62
|
+
string << "\n\n"
|
63
|
+
end
|
64
|
+
string
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.format_time(text)
|
68
|
+
text.strip.gsub(/,/, '.')
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.timecode?(text)
|
72
|
+
!!text.match(/^\d{2,}:\d{2}:\d{2},\d{3}.*\d{2,}:\d{2}:\d{2},\d{3}$/)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Captive
|
4
|
+
class VTT
|
5
|
+
include Base
|
6
|
+
|
7
|
+
# Standard VTT Header
|
8
|
+
VTT_HEADER = 'WEBVTT'
|
9
|
+
|
10
|
+
# VTT METADATA Regex
|
11
|
+
VTT_METADATA = /^NOTE|^STYLE/.freeze
|
12
|
+
|
13
|
+
# Parse VTT blob and return array of cues
|
14
|
+
def self.parse(blob:)
|
15
|
+
cue_list = []
|
16
|
+
lines = blob.split("\n")
|
17
|
+
cue_count = 1
|
18
|
+
state = :new_cue
|
19
|
+
cue = nil
|
20
|
+
raise InvalidSubtitle, 'Invalid VTT Signature' unless validate_header(lines.shift)
|
21
|
+
|
22
|
+
lines.each_with_index do |line, index|
|
23
|
+
line.strip!
|
24
|
+
|
25
|
+
case state
|
26
|
+
when :new_cue
|
27
|
+
next if line.empty?
|
28
|
+
|
29
|
+
if metadata?(line)
|
30
|
+
state = :metadata
|
31
|
+
next
|
32
|
+
end
|
33
|
+
|
34
|
+
# If its not metadata, and its not an empty line, it should be a timestamp
|
35
|
+
raise InvalidSubtitle, "Invalid Time Format at line #{index}" unless time?(line)
|
36
|
+
|
37
|
+
elements = line.split
|
38
|
+
start_time = elements[0]
|
39
|
+
end_time = elements[2]
|
40
|
+
cue = Cue.new(cue_number: cue_count, start_time: start_time, end_time: end_time)
|
41
|
+
cue_count += 1
|
42
|
+
state = :text
|
43
|
+
when :text
|
44
|
+
if line.empty?
|
45
|
+
## end of previous cue
|
46
|
+
cue_list << cue
|
47
|
+
cue = nil
|
48
|
+
state = :new_cue
|
49
|
+
else
|
50
|
+
cue.add_text(line)
|
51
|
+
end
|
52
|
+
when :metadata
|
53
|
+
next unless line.empty?
|
54
|
+
|
55
|
+
# Line is empty which means metadata block is over
|
56
|
+
state = :new_cue
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Check to make sure we add the last cue if for some reason the file lacks an empty line at the end
|
61
|
+
cue_list << cue unless cue.nil?
|
62
|
+
|
63
|
+
# Return the cue_list
|
64
|
+
cue_list
|
65
|
+
end
|
66
|
+
|
67
|
+
# Dump contents to String
|
68
|
+
def to_s
|
69
|
+
string = VTT_HEADER.dup
|
70
|
+
string << "\n\n"
|
71
|
+
@cue_list.each do |cue|
|
72
|
+
string << milliseconds_to_timecode(cue.start_time)
|
73
|
+
string << ' --> '
|
74
|
+
string << milliseconds_to_timecode(cue.end_time)
|
75
|
+
string << "\n"
|
76
|
+
string << cue.text
|
77
|
+
string << "\n\n"
|
78
|
+
end
|
79
|
+
string
|
80
|
+
end
|
81
|
+
|
82
|
+
# VTT Header tag matcher
|
83
|
+
def self.validate_header(line)
|
84
|
+
!!line.strip.match(/^#{VTT_HEADER}/)
|
85
|
+
end
|
86
|
+
|
87
|
+
# VTT Metadata tag matcher
|
88
|
+
def self.metadata?(text)
|
89
|
+
!!text.match(VTT_METADATA)
|
90
|
+
end
|
91
|
+
|
92
|
+
# VTT Timecode matcher
|
93
|
+
def self.time?(text)
|
94
|
+
!!text.match(/^(\d{2}:)?\d{2}:\d{2}.\d{3}.*(\d{2}:)?\d{2}:\d{2}.\d{3}/)
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.integer?(val)
|
98
|
+
val.to_i.to_s == val
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/captive/util.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Captive
|
4
|
+
module Util
|
5
|
+
TIMECODE_REGEX = /^(\d{2,}:)?[0-5]\d:[0-5]\d(\.\d{1,3})?$/.freeze
|
6
|
+
|
7
|
+
# Converts timecode in HH:MM:SS.MSEC (or) MM:SS.MSEC to milliseconds.
|
8
|
+
def timecode_to_milliseconds(timecode)
|
9
|
+
raise InvalidInput, 'Input should be a valid Timecode' unless TIMECODE_REGEX.match(timecode)
|
10
|
+
|
11
|
+
timecode_split = timecode.split('.')
|
12
|
+
time_split = timecode_split[0].split(':')
|
13
|
+
|
14
|
+
# To handle MM:SS.MSEC format
|
15
|
+
time_split.unshift('00') if time_split.length == 2
|
16
|
+
|
17
|
+
# Get HH:MM:SS in seconds
|
18
|
+
seconds = time_split[-1].to_i
|
19
|
+
seconds += minutes_to_seconds(time_split[-2].to_i)
|
20
|
+
seconds += hours_to_seconds(time_split[-3].to_i)
|
21
|
+
|
22
|
+
milliseconds = seconds_to_milliseconds(seconds)
|
23
|
+
|
24
|
+
# Millisecond component exists. Pad it to make sure its a full 3 digits
|
25
|
+
milliseconds += timecode_split[1].ljust(3, '0').to_i if timecode_split[1]
|
26
|
+
|
27
|
+
milliseconds
|
28
|
+
end
|
29
|
+
|
30
|
+
def minutes_to_seconds(minutes)
|
31
|
+
minutes * 60
|
32
|
+
end
|
33
|
+
|
34
|
+
def hours_to_seconds(hours)
|
35
|
+
minutes_to_seconds(hours * 60)
|
36
|
+
end
|
37
|
+
|
38
|
+
def seconds_to_milliseconds(seconds)
|
39
|
+
seconds * 1000
|
40
|
+
end
|
41
|
+
|
42
|
+
# Converts milliseconds to timecode format and returns HH+:MM:SS.MSEC where hours are two or more characters.
|
43
|
+
def milliseconds_to_timecode(milliseconds)
|
44
|
+
ms_in_a_second = 1000
|
45
|
+
ms_in_a_minute = ms_in_a_second * 60
|
46
|
+
ms_in_an_hour = ms_in_a_minute * 60
|
47
|
+
|
48
|
+
hours, remaider = milliseconds.divmod(ms_in_an_hour)
|
49
|
+
minutes, remaider = remaider.divmod(ms_in_a_minute)
|
50
|
+
seconds, milliseconds = remaider.divmod(ms_in_a_second)
|
51
|
+
|
52
|
+
format('%<h>02d:%<m>02d:%<s>02d.%<ms>03d', { h: hours, m: minutes, s: seconds, ms: milliseconds })
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
metadata
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: captive
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- mserran2
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-07-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '12.3'
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 12.3.3
|
37
|
+
type: :development
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '12.3'
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 12.3.3
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rspec
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.9'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '3.9'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rubocop
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0.87'
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 0.87.1
|
71
|
+
type: :development
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - "~>"
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0.87'
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: 0.87.1
|
81
|
+
description: Captive allows for parsing and converting of subtitles across different
|
82
|
+
formats in addition to JSON
|
83
|
+
email:
|
84
|
+
- mark@markserrano.me
|
85
|
+
executables: []
|
86
|
+
extensions: []
|
87
|
+
extra_rdoc_files: []
|
88
|
+
files:
|
89
|
+
- LICENSE.txt
|
90
|
+
- README.md
|
91
|
+
- bin/console
|
92
|
+
- lib/captive.rb
|
93
|
+
- lib/captive/base.rb
|
94
|
+
- lib/captive/cue.rb
|
95
|
+
- lib/captive/error.rb
|
96
|
+
- lib/captive/formats/srt.rb
|
97
|
+
- lib/captive/formats/vtt.rb
|
98
|
+
- lib/captive/util.rb
|
99
|
+
- lib/captive/version.rb
|
100
|
+
homepage: https://github.com/mserran2/captive
|
101
|
+
licenses:
|
102
|
+
- MIT
|
103
|
+
metadata: {}
|
104
|
+
post_install_message:
|
105
|
+
rdoc_options: []
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
requirements: []
|
119
|
+
rubyforge_project:
|
120
|
+
rubygems_version: 2.7.6
|
121
|
+
signing_key:
|
122
|
+
specification_version: 4
|
123
|
+
summary: Ruby Subtitle Manager, Editor and Converter
|
124
|
+
test_files: []
|