captions 1.3.0 → 1.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: aabb7f8d8e05f0bdd97f11c7d3d6ad2272daa4a8
4
- data.tar.gz: 2487803e670a82b5280cbe710d8b1f53aeb7ad7d
2
+ SHA256:
3
+ metadata.gz: f28c0fae37de5bc2c02bc8e47e088a595ae63c5a6579f2f7592ddd52f5f7a3b6
4
+ data.tar.gz: 0d9be70a499a0664aa1d1102bcc6a66aa91d963b38a8c9bdd1fbac2f5f44bf7e
5
5
  SHA512:
6
- metadata.gz: 247cbaedb31a2c5afc6d987b9521a62ef11e7c40460d1a5831093e00f1308ef682e86ee68832695110f85452dd3365fbdf3ea1d76219147b18838b0dd8812d22
7
- data.tar.gz: ad8c0e97e9fb83e97facabfd17058427368672aa5ac2aa79f9f3b5908b5cf2c9e1e23c9a84c5e2ac8e3369e4e527144c6ab557e82126f15eb5cca828657cbe59
6
+ metadata.gz: 85a5e3b4adbd7fd0ae4bda2a42e53d1a44ec72143911017759f5e4220aeee9c9b8bbfd5d54bd64751935440796b552f6ab111f680bd8eba8f4520a9e7a1a5609
7
+ data.tar.gz: ea86bfa471ef771f0b714f162e807adb9c0364e708512f3feca04c601053ad0b3cd2a3c25b85be5ef1d35baba82ce75571a5f10b0490a943535343ea4d295d9c
data/.travis.yml CHANGED
@@ -2,7 +2,7 @@ language: ruby
2
2
  cache: bundler
3
3
 
4
4
  rvm:
5
- - 2.0.0-p247
5
+ - ruby-2.3.8
6
6
 
7
7
  script: 'bundle exec rspec'
8
8
 
data/bin/console CHANGED
@@ -1,14 +1,14 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "captions"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "captions"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/captions.gemspec CHANGED
@@ -1,27 +1,27 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'captions/version'
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "captions"
8
- spec.version = Captions::VERSION
9
- spec.authors = ["navin"]
10
- spec.email = ["navin@amagi.com"]
11
-
12
- spec.summary = %q{Subtitle Editor and Converter written in Ruby}
13
- spec.description = %q{Subtitle Editor and Converter written in Ruby. Captions can read/modify/export subtitles from one format to another }
14
- spec.homepage = "https://github.com/navinre/captions"
15
- spec.license = "MIT"
16
-
17
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
- f.match(%r{^(test|spec|features)/})
19
- end
20
- spec.bindir = "exe"
21
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
- spec.require_paths = ["lib"]
23
-
24
- spec.add_development_dependency "rspec"
25
- spec.add_development_dependency "bundler", "~> 1.13"
26
- spec.add_development_dependency "rake", "~> 10.0"
27
- end
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'captions/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "captions"
8
+ spec.version = Captions::VERSION
9
+ spec.authors = ["navin"]
10
+ spec.email = ["navin@amagi.com"]
11
+
12
+ spec.summary = %q{Subtitle Editor and Converter written in Ruby}
13
+ spec.description = %q{Subtitle Editor and Converter written in Ruby. Captions can read/modify/export subtitles from one format to another }
14
+ spec.homepage = "https://github.com/navinre/captions"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_development_dependency "rspec"
25
+ spec.add_development_dependency "bundler"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ end
data/lib/captions/cue.rb CHANGED
@@ -1,93 +1,93 @@
1
- module Captions
2
- class Cue
3
- include Util
4
-
5
- # Text Properties supported
6
- ALIGNMENT = "alignment"
7
- COLOR = "color"
8
- POSITION = "position"
9
-
10
- # List of Text Properties
11
- TEXT_PROPERTIES = [ALIGNMENT, COLOR, POSITION]
12
-
13
- attr_accessor :number, :start_time, :end_time, :duration, :text, :properties
14
-
15
- # Creates a new Cue class
16
- # Each cue denotes a subtitle.
17
- def initialize
18
- self.text = nil
19
- self.start_time = nil
20
- self.end_time = nil
21
- self.duration = nil
22
- self.number = nil
23
- self.properties = {}
24
- end
25
-
26
- # Sets the time for the cue. Both start-time and
27
- # end-time can be passed together. This just assigns
28
- # the value passed.
29
- def set_time(start_time, end_time, duration = nil)
30
- self.start_time = start_time
31
- self.end_time = end_time
32
- self.duration = duration
33
- end
34
-
35
- # Getter and Setter methods for Text Properties
36
- # These are pre-defined properties. This is just to assign
37
- # or access the properties of that text.
38
- TEXT_PROPERTIES.each do |setting|
39
- define_method :"#{setting}" do
40
- if self.properties[setting].present?
41
- return self.properties[setting]
42
- end
43
- return nil
44
- end
45
-
46
- define_method :"#{setting}=" do |value|
47
- self.properties[setting] = value
48
- end
49
- end
50
-
51
- # Serializes the values set for the cue.
52
- # Converts start-time, end-time and duration to milliseconds
53
- # If duration is not found, it will be calculated based on
54
- # start-time and end-time.
55
- def serialize(fps)
56
- raise InvalidSubtitle, "Subtitle should have start time" if self.start_time.nil?
57
- raise InvalidSubtitle, "Subtitle shold have end time" if self.end_time.nil?
58
-
59
- begin
60
- ms_per_frame = (1000.0 / fps)
61
- self.start_time = convert_to_msec(self.start_time, ms_per_frame)
62
- self.end_time = convert_to_msec(self.end_time, ms_per_frame)
63
- if duration.nil?
64
- self.duration = self.end_time - self.start_time
65
- else
66
- self.duration = convert_to_msec(self.duration, ms_per_frame)
67
- end
68
- rescue
69
- raise InvalidSubtitle, "Cannot calculate start-time or end-time"
70
- end
71
- end
72
-
73
- # Changes start-time, end-time and duration based on new frame-rate
74
- def change_frame_rate(old_rate, new_rate)
75
- self.start_time = convert_frame_rate(self.start_time, old_rate, new_rate)
76
- self.end_time = convert_frame_rate(self.end_time, old_rate, new_rate)
77
- self.duration = convert_frame_rate(self.duration, old_rate, new_rate)
78
- end
79
-
80
- # Adds text. If text is already found, new-line is appended.
81
- def add_text(text)
82
- if self.text.nil?
83
- self.text = text
84
- else
85
- self.text += "\n" + text
86
- end
87
- end
88
-
89
- def <=>(other_cue)
90
- self.start_time <=> other_cue.start_time
91
- end
92
- end
93
- end
1
+ module Captions
2
+ class Cue
3
+ include Util
4
+
5
+ # Text Properties supported
6
+ ALIGNMENT = "alignment"
7
+ COLOR = "color"
8
+ POSITION = "position"
9
+
10
+ # List of Text Properties
11
+ TEXT_PROPERTIES = [ALIGNMENT, COLOR, POSITION]
12
+
13
+ attr_accessor :number, :start_time, :end_time, :duration, :text, :properties
14
+
15
+ # Creates a new Cue class
16
+ # Each cue denotes a subtitle.
17
+ def initialize(cue_number = nil)
18
+ self.text = nil
19
+ self.start_time = nil
20
+ self.end_time = nil
21
+ self.duration = nil
22
+ self.number = cue_number
23
+ self.properties = {}
24
+ end
25
+
26
+ # Sets the time for the cue. Both start-time and
27
+ # end-time can be passed together. This just assigns
28
+ # the value passed.
29
+ def set_time(start_time, end_time, duration = nil)
30
+ self.start_time = start_time
31
+ self.end_time = end_time
32
+ self.duration = duration
33
+ end
34
+
35
+ # Getter and Setter methods for Text Properties
36
+ # These are pre-defined properties. This is just to assign
37
+ # or access the properties of that text.
38
+ TEXT_PROPERTIES.each do |setting|
39
+ define_method :"#{setting}" do
40
+ if self.properties[setting].present?
41
+ return self.properties[setting]
42
+ end
43
+ return nil
44
+ end
45
+
46
+ define_method :"#{setting}=" do |value|
47
+ self.properties[setting] = value
48
+ end
49
+ end
50
+
51
+ # Serializes the values set for the cue.
52
+ # Converts start-time, end-time and duration to milliseconds
53
+ # If duration is not found, it will be calculated based on
54
+ # start-time and end-time.
55
+ def serialize(fps)
56
+ raise InvalidSubtitle, "Subtitle should have start time" if self.start_time.nil?
57
+ raise InvalidSubtitle, "Subtitle shold have end time" if self.end_time.nil?
58
+
59
+ begin
60
+ ms_per_frame = (1000.0 / fps)
61
+ self.start_time = convert_to_msec(self.start_time, ms_per_frame)
62
+ self.end_time = convert_to_msec(self.end_time, ms_per_frame)
63
+ if duration.nil?
64
+ self.duration = self.end_time - self.start_time
65
+ else
66
+ self.duration = convert_to_msec(self.duration, ms_per_frame)
67
+ end
68
+ rescue
69
+ raise InvalidSubtitle, "Cannot calculate start-time or end-time"
70
+ end
71
+ end
72
+
73
+ # Changes start-time, end-time and duration based on new frame-rate
74
+ def change_frame_rate(old_rate, new_rate)
75
+ self.start_time = convert_frame_rate(self.start_time, old_rate, new_rate)
76
+ self.end_time = convert_frame_rate(self.end_time, old_rate, new_rate)
77
+ self.duration = convert_frame_rate(self.duration, old_rate, new_rate)
78
+ end
79
+
80
+ # Adds text. If text is already found, new-line is appended.
81
+ def add_text(text)
82
+ if self.text.nil?
83
+ self.text = text
84
+ else
85
+ self.text += "\n" + text
86
+ end
87
+ end
88
+
89
+ def <=>(other_cue)
90
+ self.start_time <=> other_cue.start_time
91
+ end
92
+ end
93
+ end
@@ -3,34 +3,43 @@ module Captions
3
3
 
4
4
  def parse
5
5
  base_parser do
6
- count = 1
7
- cue = Cue.new
8
- number = nil
9
- lines = []
10
- while(line = @file.gets) do
11
- line = line.strip
12
- if line.empty?
13
- @cue_list.append(cue) if cue and cue.start_time
14
- cue = Cue.new
15
- # This is done to handle white spaces at the beginning
16
- # and end of the file
17
- if(number = @file.gets)
18
- # Reads the number immediately followed by white-space
19
- cue.number = number.to_i
20
- else
6
+ count = 0
7
+ state = :new_cue
8
+ cue = nil
9
+ loop do
10
+ count += 1
11
+ line = @file.gets
12
+ break if line.nil? ## End of file
13
+ line.chomp!
14
+ case state
15
+ when :new_cue
16
+ line.strip!
17
+ next if line.empty? ## just another blank line, remain in new_cue state
18
+ begin
19
+ cue = Cue.new(Integer(line))
20
+ rescue ArgumentError
21
+ raise InvalidSubtitle, "Invalid Cue Number at line #{count}"
22
+ end
23
+ state = :time
24
+ when :time
25
+ line.strip!
26
+ raise InvalidSubtitle, "Invalid Time Format at line #{count}" unless is_time?(line)
27
+ start_time, end_time = get_time(line)
28
+ cue.set_time(start_time, end_time)
29
+ state = :text
30
+ when :text
31
+ if line.empty?
32
+ ## end of previous cue
33
+ @cue_list.append(cue) if cue && cue.start_time
21
34
  cue = nil
35
+ state = :new_cue
36
+ else
37
+ line.strip!
38
+ cue.add_text(line)
22
39
  end
23
- elsif is_time?(line)
24
- s , e = get_time(line)
25
- cue.set_time(s, e)
26
- elsif is_text?(line)
27
- cue.add_text(line)
28
- elsif !line.empty?
29
- raise MalformedString, "Invalid format at line #{count}"
30
40
  end
31
- count += 1
32
41
  end
33
- @cue_list.append(cue) if cue
42
+ @cue_list.append(cue) if cue && cue.start_time
34
43
  end
35
44
  end
36
45
 
@@ -1,121 +1,122 @@
1
- module Captions
2
- class VTT < Base
3
-
4
- # Header used for all VTT files
5
- VTT_HEADER = "WEBVTT"
6
-
7
- # VTT file comments/style section
8
- VTT_METADATA = /^NOTE|^STYLE/
9
-
10
- # Auto Keyword used in Alignment
11
- AUTO_KEYWORD = "auto"
12
-
13
- # Alignment Data
14
- ALIGNMENT_VALUES = {
15
- "middle" => "middle",
16
- "left" => "left",
17
- "right" => "right",
18
- "start" => "start",
19
- "end" => "end",
20
- }
21
-
22
- # Parse VTT file and update CueList
23
- def parse
24
- base_parser do
25
- count = 1
26
- cue_count = 0
27
- meta_data_section = false
28
- cue = nil
29
- raise InvalidSubtitle, "Invalid VTT Signature" unless validate_header(@file.gets)
30
- while(line = @file.gets) do
31
- line = line.strip
32
- if line.empty?
33
- meta_data_section = false
34
- elsif is_meta_data?(line)
35
- meta_data_section = true
36
- elsif is_time?(line)
37
- @cue_list.append(cue) if cue
38
- cue_count += 1
39
- cue = Cue.new
40
- cue.number = cue_count
41
- line = line.split
42
- cue.set_time(line[0], line[2])
43
- set_properties(cue, line[3..-1])
44
- elsif !meta_data_section and is_text?(line)
45
- cue.add_text(line)
46
- end
47
- end
48
- @cue_list.append(cue) if cue
49
- end
50
- end
51
-
52
- # Export CueList to VTT file
53
- def dump(file)
54
- base_dump(file) do |file|
55
- file.write(VTT_HEADER)
56
- @cue_list.each do |cue|
57
- file.write("\n\n")
58
- file.write(msec_to_timecode(cue.start_time))
59
- file.write(" --> ")
60
- file.write(msec_to_timecode(cue.end_time))
61
- file.write("\n")
62
- file.write(cue.text)
63
- end
64
- end
65
- end
66
-
67
- # Check whether its a VTT_HEADER or not
68
- def validate_header(line)
69
- !!line.strip.match(/^#{VTT_HEADER}/)
70
- end
71
-
72
- # Check whether its a meta-data or not
73
- def is_meta_data?(text)
74
- !!text.match(VTT_METADATA)
75
- end
76
-
77
- # Timecode format used in VTT file
78
- def is_time?(text)
79
- !!text.match(/^(\d{2}:)?\d{2}:\d{2}.\d{3}.*(\d{2}:)?\d{2}:\d{2}.\d{3}/)
80
- end
81
-
82
- # Check whether if its subtilte text or not
83
- def is_text?(text)
84
- !text.empty? and text.is_a?(String) and text != VTT_HEADER
85
- end
86
-
87
- def set_properties(cue, properties)
88
- properties.each do |prop|
89
- prop, value = prop.split(":")
90
- value.gsub!("%","")
91
- case prop
92
- when "align"
93
- cue.alignment = get_alignment(value)
94
- when "line"
95
- value = value.split(",")[0]
96
- cue.position = get_line(value)
97
- end
98
- end
99
- end
100
-
101
- def get_alignment(value)
102
- raise InvalidSubtitle, "Invalid VTT Alignment Property" unless ALIGNMENT_VALUES[value]
103
- return ALIGNMENT_VALUES[value]
104
- end
105
-
106
- def get_line(value)
107
- raise InvalidSubtitle, "VTT Line property should be a valid number" if !is_integer?(value) and value != AUTO_KEYWORD
108
- return value.to_i
109
- end
110
-
111
- def get_position(value)
112
- raise InvalidSubtitle, "VTT Position should be a valid number" if !is_integer?(value)
113
- raise InvalidSubtitle, "VTT Position should be a number between 0 to 100" if (value.to_i < 0) or (value.to_i > 100)
114
- return value.to_i
115
- end
116
-
117
- def is_integer?(val)
118
- val.to_i.to_s == val
119
- end
120
- end
121
- end
1
+ module Captions
2
+ class VTT < Base
3
+
4
+ # Header used for all VTT files
5
+ VTT_HEADER = "WEBVTT"
6
+
7
+ # VTT file comments/style section
8
+ VTT_METADATA = /^NOTE|^STYLE/
9
+
10
+ # Auto Keyword used in Alignment
11
+ AUTO_KEYWORD = "auto"
12
+
13
+ # Alignment Data
14
+ ALIGNMENT_VALUES = {
15
+ "middle" => "middle",
16
+ "center" => "middle",
17
+ "left" => "left",
18
+ "right" => "right",
19
+ "start" => "start",
20
+ "end" => "end",
21
+ }
22
+
23
+ # Parse VTT file and update CueList
24
+ def parse
25
+ base_parser do
26
+ count = 1
27
+ cue_count = 0
28
+ meta_data_section = false
29
+ cue = nil
30
+ raise InvalidSubtitle, "Invalid VTT Signature" unless validate_header(@file.gets)
31
+ while(line = @file.gets) do
32
+ line = line.strip
33
+ if line.empty?
34
+ meta_data_section = false
35
+ elsif is_meta_data?(line)
36
+ meta_data_section = true
37
+ elsif is_time?(line)
38
+ @cue_list.append(cue) if cue
39
+ cue_count += 1
40
+ cue = Cue.new
41
+ cue.number = cue_count
42
+ line = line.split
43
+ cue.set_time(line[0], line[2])
44
+ set_properties(cue, line[3..-1])
45
+ elsif !meta_data_section and is_text?(line)
46
+ cue.add_text(line)
47
+ end
48
+ end
49
+ @cue_list.append(cue) if cue
50
+ end
51
+ end
52
+
53
+ # Export CueList to VTT file
54
+ def dump(file)
55
+ base_dump(file) do |file|
56
+ file.write(VTT_HEADER)
57
+ @cue_list.each do |cue|
58
+ file.write("\n\n")
59
+ file.write(msec_to_timecode(cue.start_time))
60
+ file.write(" --> ")
61
+ file.write(msec_to_timecode(cue.end_time))
62
+ file.write("\n")
63
+ file.write(cue.text)
64
+ end
65
+ end
66
+ end
67
+
68
+ # Check whether its a VTT_HEADER or not
69
+ def validate_header(line)
70
+ !!line.strip.match(/^#{VTT_HEADER}/)
71
+ end
72
+
73
+ # Check whether its a meta-data or not
74
+ def is_meta_data?(text)
75
+ !!text.match(VTT_METADATA)
76
+ end
77
+
78
+ # Timecode format used in VTT file
79
+ def is_time?(text)
80
+ !!text.match(/^(\d{2}:)?\d{2}:\d{2}.\d{3}.*(\d{2}:)?\d{2}:\d{2}.\d{3}/)
81
+ end
82
+
83
+ # Check whether if its subtilte text or not
84
+ def is_text?(text)
85
+ !text.empty? and text.is_a?(String) and text != VTT_HEADER
86
+ end
87
+
88
+ def set_properties(cue, properties)
89
+ properties.each do |prop|
90
+ prop, value = prop.split(":")
91
+ value.gsub!("%","")
92
+ case prop
93
+ when "align"
94
+ cue.alignment = get_alignment(value)
95
+ when "line"
96
+ value = value.split(",")[0]
97
+ cue.position = get_line(value)
98
+ end
99
+ end
100
+ end
101
+
102
+ def get_alignment(value)
103
+ raise InvalidSubtitle, "Invalid VTT Alignment Property" unless ALIGNMENT_VALUES[value]
104
+ return ALIGNMENT_VALUES[value]
105
+ end
106
+
107
+ def get_line(value)
108
+ raise InvalidSubtitle, "VTT Line property should be a valid number" if !is_integer?(value) and value != AUTO_KEYWORD
109
+ return value.to_i
110
+ end
111
+
112
+ def get_position(value)
113
+ raise InvalidSubtitle, "VTT Position should be a valid number" if !is_integer?(value)
114
+ raise InvalidSubtitle, "VTT Position should be a number between 0 to 100" if (value.to_i < 0) or (value.to_i > 100)
115
+ return value.to_i
116
+ end
117
+
118
+ def is_integer?(val)
119
+ val.to_i.to_s == val
120
+ end
121
+ end
122
+ end
@@ -1,3 +1,3 @@
1
1
  module Captions
2
- VERSION = "1.3.0"
2
+ VERSION = "1.3.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: captions
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - navin
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-10-11 00:00:00.000000000 Z
11
+ date: 2023-01-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -28,16 +28,16 @@ dependencies:
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '1.13'
33
+ version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '1.13'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -84,7 +84,7 @@ homepage: https://github.com/navinre/captions
84
84
  licenses:
85
85
  - MIT
86
86
  metadata: {}
87
- post_install_message:
87
+ post_install_message:
88
88
  rdoc_options: []
89
89
  require_paths:
90
90
  - lib
@@ -99,9 +99,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
99
99
  - !ruby/object:Gem::Version
100
100
  version: '0'
101
101
  requirements: []
102
- rubyforge_project:
103
- rubygems_version: 2.4.8
104
- signing_key:
102
+ rubygems_version: 3.1.2
103
+ signing_key:
105
104
  specification_version: 4
106
105
  summary: Subtitle Editor and Converter written in Ruby
107
106
  test_files: []