captive 1.1.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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 507732c8b1ccb140a3ad934f6c8fd4e8f05e49703ce9e48b5e07c4cb815ad6b0
4
+ data.tar.gz: f1c314eea9df3e4482e44db07a3ec664da05f7032712a883c7826a23096c8dbf
5
+ SHA512:
6
+ metadata.gz: 1845a4d5314701d636400b753ff609638466151ba24b27edf77a6e670380cbb258388529c788e93d5e3e440c9ab84bd3d9a373d4d7d461343968ba7739834b00
7
+ data.tar.gz: 36f0ba341f0878eb7a2d5cb11fc36feea8182b55754762d308f7073be715ad5af208e89a22562cecad34fc66a183fd4e747e8a6879ea1b7cecf46ae3aa960739
@@ -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.
@@ -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
+
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'captive'
6
+ require 'irb'
7
+ IRB.start
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'captive/version'
4
+ require 'captive/error'
5
+ require 'captive/util'
6
+ require 'captive/base'
7
+ require 'captive/cue'
8
+ require 'captive/formats/vtt'
9
+ require 'captive/formats/srt'
@@ -0,0 +1,100 @@
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
+
18
+ def from_json(json:, mapping: {})
19
+ json = JSON.parse(json) if json.is_a?(String)
20
+ cues = json['cues'] || json[:cues]
21
+ raise InvalidJsonInput unless cues.is_a?(Array)
22
+
23
+ cues.map! { |cue_json| Cue.from_json(json: cue_json, mapping: mapping) }
24
+ new(cue_list: cues)
25
+ end
26
+ end
27
+
28
+ def self.included(base)
29
+ base.extend(ClassMethods)
30
+ base.include(Util)
31
+ end
32
+
33
+ attr_accessor(:cues)
34
+
35
+ def initialize(cue_list: nil)
36
+ @cues = cue_list || []
37
+ end
38
+
39
+ def save_as(filename:)
40
+ File.open(filename, 'w') do |file|
41
+ file.write(to_s)
42
+ end
43
+ end
44
+
45
+ def as_json(**args)
46
+ results = {
47
+ 'version' => VERSION,
48
+ 'cues' => cues.map(&:as_json),
49
+ }
50
+ if results.respond_to?(:as_json)
51
+ results.as_json(**args)
52
+ else
53
+ results
54
+ end
55
+ end
56
+
57
+ def method_missing(method_name, *_args)
58
+ super unless (match = /^as_([a-z]+)$/.match(method_name))
59
+
60
+ if valid_format?(match.captures.first.upcase)
61
+ define_format_conversion(method_name, match.captures.first.upcase)
62
+ send(method_name)
63
+ elsif valid_format?(match.captures.first.capitalize)
64
+ define_format_conversion(method_name, match.captures.first.capitalize)
65
+ send(method_name)
66
+ else
67
+ super
68
+ end
69
+ end
70
+
71
+ def respond_to_missing?(method_name, _)
72
+ super unless (match = /^as_([a-z]+)$/.match(method_name))
73
+
74
+ return true if valid_format?(match.captures.first.upcase) || valid_format?(match.captures.first.capitalize)
75
+
76
+ super
77
+ end
78
+
79
+ private
80
+
81
+ def base_klass
82
+ base = self.class.to_s.split('::')
83
+ Kernel.const_get(base.first)
84
+ end
85
+
86
+ def valid_format?(format)
87
+ base_klass.const_defined?(format) && base_klass.const_get(format).include?(Base)
88
+ end
89
+
90
+ def define_format_conversion(method_name, format)
91
+ self.class.define_method(method_name) do
92
+ if self.class.to_s.split('::').last == format
93
+ self
94
+ else
95
+ base_klass.const_get(format).new(cue_list: cues)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,113 @@
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 :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, properties: {})
20
+ self.text = text
21
+ self.start_time = start_time
22
+ self.end_time = end_time
23
+ self.properties = properties || {}
24
+ end
25
+
26
+ def self.from_json(json:, mapping: {})
27
+ schema = {}
28
+ %i[text! start_time! end_time! properties].each do |field|
29
+ field_name = field.to_s.delete('!')
30
+ schema[field] = mapping[field_name] || mapping[field_name.to_sym] || field_name.to_sym
31
+ end
32
+ data = {}
33
+ schema.each do |mask, mapper|
34
+ key = mask[-1] == '!' ? mask.to_s[0...-1].to_sym : mask
35
+ if key.to_s != mask.to_s && !(json.key?(mapper.to_s) || json.key?(mapper.to_sym))
36
+ raise InvalidJsonInput, "Cue missing field: #{mapper}"
37
+ end
38
+
39
+ data[key] = json[mapper.to_s] || json[mapper.to_sym]
40
+ end
41
+ new(**data)
42
+ end
43
+
44
+ def start_time=(time)
45
+ set_time(:start_time, time)
46
+ end
47
+
48
+ def end_time=(time)
49
+ set_time(:end_time, time)
50
+ end
51
+
52
+ def set_times(start_time:, end_time:)
53
+ self.start_time = start_time
54
+ self.end_time = end_time
55
+ end
56
+
57
+ # Getter and Setter methods for Text Properties
58
+ TEXT_PROPERTIES.each do |setting|
59
+ define_method :"#{setting}" do
60
+ return properties[setting] if properties[setting].present?
61
+
62
+ return nil
63
+ end
64
+
65
+ define_method :"#{setting}=" do |value|
66
+ properties[setting] = value
67
+ end
68
+ end
69
+
70
+ def duration
71
+ end_time - start_time
72
+ end
73
+
74
+ # Adds text. If text is already present, new-line is added before text.
75
+ def add_text(text)
76
+ if self.text.nil?
77
+ self.text = text
78
+ else
79
+ self.text += "\n" + text
80
+ end
81
+ end
82
+
83
+ def <=>(other)
84
+ start_time <=> other.start_time
85
+ end
86
+
87
+ def as_json(**args)
88
+ options = args.delete(:options) || {}
89
+ format = options['format'] || {}
90
+ obj = {
91
+ 'start_time' => format[:time] == :timecode ? milliseconds_to_timecode(start_time) : start_time,
92
+ 'end_time' => format[:time] == :timecode ? milliseconds_to_timecode(end_time) : end_time,
93
+ 'text' => text,
94
+ 'properties' => properties,
95
+ }
96
+ obj.respond_to?(:as_json) ? obj.as_json(**args) : obj
97
+ end
98
+
99
+ private
100
+
101
+ def set_time(field, time)
102
+ return if time.nil?
103
+
104
+ if time.is_a?(Integer)
105
+ instance_variable_set("@#{field}", time)
106
+ elsif TIMECODE_REGEX.match(time)
107
+ instance_variable_set("@#{field}", timecode_to_milliseconds(time))
108
+ else
109
+ raise InvalidInput, "Input for #{field} should be an integer denoting milliseconds or a valid timecode."
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,22 @@
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
+
20
+ class InvalidJsonInput < CaptiveError
21
+ end
22
+ end
@@ -0,0 +1,74 @@
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
+ state = :time
23
+ when :time
24
+ raise InvalidSubtitle, "Invalid Time Format at line #{count}" unless timecode?(line)
25
+
26
+ start_time, end_time = line.split('-->').map(&:strip)
27
+ cue = Cue.new(
28
+ start_time: format_time(start_time),
29
+ end_time: format_time(end_time)
30
+ )
31
+ state = :text
32
+ when :text
33
+ if line.empty?
34
+ ## end of previous cue
35
+ cue_list << cue
36
+ cue = nil
37
+ state = :new_cue
38
+ else
39
+ cue.add_text(line)
40
+ end
41
+ end
42
+ end
43
+
44
+ # Check to make sure we add the last cue if for some reason the file lacks an empty line at the end
45
+ cue_list << cue unless cue.nil?
46
+
47
+ # Return the cue_list
48
+ cue_list
49
+ end
50
+
51
+ def to_s
52
+ string = String.new
53
+ cues.each_with_index do |cue, index|
54
+ string << (index + 1).to_s
55
+ string << "\n"
56
+ string << milliseconds_to_timecode(cue.start_time).gsub!('.', ',')
57
+ string << ' --> '
58
+ string << milliseconds_to_timecode(cue.end_time).gsub!('.', ',')
59
+ string << "\n"
60
+ string << cue.text
61
+ string << "\n\n"
62
+ end
63
+ string
64
+ end
65
+
66
+ def self.format_time(text)
67
+ text.strip.gsub(/,/, '.')
68
+ end
69
+
70
+ def self.timecode?(text)
71
+ !!text.match(/^\d{2,}:\d{2}:\d{2},\d{3}.*\d{2,}:\d{2}:\d{2},\d{3}$/)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,105 @@
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
+ state = :new_cue
18
+ cue = nil
19
+ raise InvalidSubtitle, 'Invalid VTT Signature' unless validate_header(lines.shift)
20
+
21
+ lines.each_with_index do |line, index|
22
+ line.strip!
23
+
24
+ case state
25
+ when :new_cue
26
+ next if line.empty?
27
+
28
+ if metadata?(line)
29
+ state = :metadata
30
+ next
31
+ end
32
+
33
+ # If its not metadata, and its not an empty line, it should be a timestamp or an identifier
34
+ unless time?(line)
35
+ # If this line is an identifier the next line should be a timecode
36
+ next if time?(lines[index + 1])
37
+
38
+ raise InvalidSubtitle, "Invalid Time Format at line #{index + 1}" unless time?(line)
39
+ end
40
+
41
+ elements = line.split
42
+ start_time = elements[0]
43
+ end_time = elements[2]
44
+ cue = Cue.new(start_time: start_time, end_time: end_time)
45
+ state = :text
46
+ when :text
47
+ if line.empty?
48
+ ## end of previous cue
49
+ cue_list << cue
50
+ cue = nil
51
+ state = :new_cue
52
+ else
53
+ cue.add_text(line)
54
+ end
55
+ when :metadata
56
+ next unless line.empty?
57
+
58
+ # Line is empty which means metadata block is over
59
+ state = :new_cue
60
+ end
61
+ end
62
+
63
+ # Check to make sure we add the last cue if for some reason the file lacks an empty line at the end
64
+ cue_list << cue unless cue.nil?
65
+
66
+ # Return the cue_list
67
+ cue_list
68
+ end
69
+
70
+ # Dump contents to String
71
+ def to_s
72
+ string = VTT_HEADER.dup
73
+ string << "\n\n"
74
+ cues.each do |cue|
75
+ string << milliseconds_to_timecode(cue.start_time)
76
+ string << ' --> '
77
+ string << milliseconds_to_timecode(cue.end_time)
78
+ string << "\n"
79
+ string << cue.text
80
+ string << "\n\n"
81
+ end
82
+ string
83
+ end
84
+
85
+ # VTT Header tag matcher
86
+ def self.validate_header(line)
87
+ # Make sure BOM does not interfere with header detection
88
+ !!line.force_encoding('UTF-8').delete("\xEF\xBB\xBF").strip.match(/^#{VTT_HEADER}/)
89
+ end
90
+
91
+ # VTT Metadata tag matcher
92
+ def self.metadata?(text)
93
+ !!text.match(VTT_METADATA)
94
+ end
95
+
96
+ # VTT Timecode matcher
97
+ def self.time?(text)
98
+ !!text.match(/^(\d{2}:)?\d{2}:\d{2}.\d{3}.*(\d{2}:)?\d{2}:\d{2}.\d{3}/)
99
+ end
100
+
101
+ def self.integer?(val)
102
+ val.to_i.to_s == val
103
+ end
104
+ end
105
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captive
4
+ VERSION = '1.1.1'
5
+ end
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: captive
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.1
5
+ platform: ruby
6
+ authors:
7
+ - mserran2
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-07-22 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: 1.9.2
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: []