format_parser 0.24.1 → 0.25.3

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
2
  SHA256:
3
- metadata.gz: c76879d955fbe7598ee7ccdfc663876a29621e1fe4e54721edbba19d8e5f9c81
4
- data.tar.gz: 7cd4161abb24e1a195fec86dc6c9ced63cb642832edf4d4d9c33129208fdf8b4
3
+ metadata.gz: 2d0b2c07289221019c42f9546eee65b4ccd5c49aadc3c16f7e4192f356821bcb
4
+ data.tar.gz: 7163ca3bfac79fe5539979e5723902db8fe530ee721d09ba3b18cf635280ece6
5
5
  SHA512:
6
- metadata.gz: 6125db42f078e6e7d4fb0a9111ce29d6750a6b5fedcbfd9a7b28f66fca8fbf59e513f2b91cccc7a2409744dd1a49b7e60361bc7af0c8414bde42e9fd535941e9
7
- data.tar.gz: f285a67739a9722a77aa9871e0d9b553a463dd22090c3728c8f97898740b303d91c214f2565d679895325e2d45399be311550e4263554d5e056d2ce528108374
6
+ metadata.gz: dfb72a909878a032d6f832aa0a52bd0521df31faacff2f5d070e9b4f555d420ad11abecfc30e2074f7897250b530543a4892b60d37037f67b87dd8c9b10c2b8b
7
+ data.tar.gz: 24b69b8ad67b5d4461f63055b379af270a936172660d6350cc98ee2caa2c47ff3b7d94bf353de673ba331ba273b1e86090f83ef282933f261978250562aa22cd
@@ -1,3 +1,21 @@
1
+ ## 0.25.3
2
+ * MP3: Fix parser to not skip the first bytes if it's not an ID3 header
3
+
4
+ ## 0.25.2
5
+ * Hotfix Moov parser
6
+
7
+ ## 0.25.1
8
+ * MOV: Fix error "negative length"
9
+ * MOV: Fix reading dimensions in multi-track files
10
+ * MP3: Fix parse of the Xing header to not raise errors
11
+
12
+ ## 0.25.0
13
+ * MP3: add suport to id3 v2.4.x
14
+ * JPEG: Update gem exifr to 1.3.8 to fix a bug
15
+
16
+ ## 0.24.2
17
+ * Update gem id3tag to 0.14.0 to fix MP3 issues
18
+
1
19
  ## 0.24.1
2
20
  * Fix MP3 frames reading to jump correctly to the next bytes
3
21
 
@@ -83,32 +83,59 @@ of software. Ideally, this file is going to be something you have produced yours
83
83
  and you are permitted to share under the MIT license provisions.
84
84
 
85
85
  When writing a parser, please try to ensure it returns a usable result as soon as possible,
86
- or no result as soon as possible (once you know the file is not fit for your specific parser).
86
+ or `nil` as soon as possible (once you know the file is not fit for your specific parser).
87
87
  Bear in mind that we enforce read budgets per-parser, so you will not be allowed to perform
88
88
  too many reads, or perform reads which are too large.
89
89
 
90
- In order to create new parsers, it is recommended to make a well-named class with an instance method `call`.
90
+ In order to create new parsers, make a well-named class with an instance method `call`,
91
+ and to register a single instance of that class as the parser - so that only one object needs to be stored
92
+ in memory when parsing multiple inputs. In that case your object must be **thread-safe and stateless** - this
93
+ is really important since FormatParser is thread-safe and multiple parsing procedures may be in progress
94
+ concurrently against the same parser object. You can also create a Proc if your parser is fairly trivial.
91
95
 
92
- `call` accepts the IO-ish object as an argument, parses data that it reads from it,
93
- and then returns the metadata for the file (if it could recover any) or `nil` if it couldn't. All files pass
94
- through all parsers by default, so if you are dealing with a file that is not "your" format - return `nil` from
95
- your method or `break` your Proc as early as possible. A blank `return` works fine too.
96
+ If it will be difficult to have your parser thread-safe you can register your class itself as
97
+ the parser and define the `self.call` method to parse using a fresh instance every time, allowing
98
+ object-level state:
99
+
100
+ ```ruby
101
+ class MyParser
102
+ def self.call(io)
103
+ new.call(io)
104
+ end
105
+
106
+ def call(io)
107
+ @state = ...
108
+ end
109
+ ```
110
+
111
+ `call` accepts a single argument - an IO-ish object which is guaranteed to respond to the same methods as the
112
+ ones defined in `IOConstraint` - that is, it is a strict subset of a standard Ruby IO object. *All reads from
113
+ this IO object are guaranteed to be returned in binary encoding.* The IO will be at offset of 0 when your parsing
114
+ proc receives it and there will be no concurrent calls to that object until your proc returns.
96
115
 
97
- The IO will at the minimum support the subset of the IO API defined in `IOConstraint`
116
+ Your parsing procedure may read from this IO object, and should return either a `Result`-like object with
117
+ the file metadata (if it could recover any) or `nil` if it couldn't. All files pass
118
+ through all parsers by default, so if you are dealing with a file that is not "your" format - return `nil` from
119
+ your method or `break` your Proc as early as possible. A blank `return` works fine too as it actually returns `nil`.
98
120
 
99
- Your parser has to be registered using `FormatParser.register_parser` with the information on the formats
100
- and file natures it provides.
121
+ Your parser then needs to be registered using `FormatParser.register_parser` with the information on the formats
122
+ and file natures it provides. This allows FormatParser to skip your parser if, say, the user only want to parse for
123
+ `:image` nature files but your parser parses `:audio`.
101
124
 
102
- Down below you can find the most basic parser implementation:
125
+ Down below you can find the most basic parser implementation which parses an imaginary `IMGA` file format:
103
126
 
104
127
  ```ruby
105
128
  MyParser = ->(io) {
106
- # ... do some parsing with `io`
129
+ # ... Read the magic bytes from the start of IO - the IO is
130
+ # guaranteed to be fed to you at offset 0, start-of-file.
107
131
  magic_bytes = io.read(4)
132
+
108
133
  # breaking the block returns `nil` to the caller signaling "no match"
109
134
  break if magic_bytes != 'IMGA'
110
135
 
136
+ # Our file format stores the width and height as 2 32-bit unsigned integers
111
137
  parsed_witdh, parsed_height = io.read(8).unpack('VV')
138
+
112
139
  # ...and return the FileInformation::Image object with the metadata.
113
140
  FormatParser::Image.new(
114
141
  format: :imga,
@@ -135,8 +162,8 @@ class MyParser
135
162
  # ... do some parsing with `io`
136
163
  # The instance will be discarded after parsing, so using instance variables
137
164
  # is permitted - they are not shared between calls to `call`
138
- @magic_bytes = io.read(4)
139
- break if @magic_bytes != 'IMGA'
165
+ magic_bytes = io.read(4)
166
+ break if magic_bytes != 'IMGA'
140
167
  parsed_witdh, parsed_height = io.read(8).unpack('VV')
141
168
  FormatParser::Image.new(
142
169
  format: :imga,
@@ -145,23 +172,33 @@ class MyParser
145
172
  )
146
173
  end
147
174
 
148
- FormatParser.register_parser self, natures: :image, formats: :bmp
175
+ # Note that we register an instance of the class, not the class. It is the
176
+ # instance that responds to `call()` and we can do this because our object
177
+ # is stateless.
178
+ FormatParser.register_parser new, natures: :image, formats: :bmp
149
179
  end
150
180
  ```
151
181
 
152
- ### Calling convention for preparing parsers
182
+ If your parser supports file types which have a known filename extension, you can add a method to it called `likely_match?`,
183
+ add this method on the object you register itself. For example, for the ZIP parser we use:
184
+
185
+ ```ruby
186
+ def likely_match?(filename)
187
+ filename =~ /\.(zip|docx|keynote|numbers|pptx|xlsx)$/i
188
+ end
189
+ ```
153
190
 
154
- A parser that gets registered using `register_parser` must be either:
191
+ If your parser matches the filename it is going to be applied *earlier*, saving time. Since most FormatParser users are
192
+ likely to only want the first result of the parsing, the sooner your parser gets applied - the sooner you can return the result,
193
+ avoiding unnecessary reads.
155
194
 
156
- 1) An object that can be `call()`-ed itself, with an argument that conforms to `IOConstraint`
157
- 2) An object that responds to `new` and returns something that can be `call()`-ed with with an argument that conforms to `IOConstraint`.
195
+ ### Calling convention for preparing parsers
158
196
 
159
- The second opton is recommended for most cases.
197
+ A parser that gets registered using `register_parser` must be an object that can be `call()`-ed, with an argument that conforms to `IOConstraint`
160
198
 
161
199
  FormatParser is made to be used in threaded environments, and if you use instance variables
162
- you need your parser to be isolated from it's siblings in other threads - therefore you can pass
163
- a Class on registration to have your parser instantiated for each `call()`, anew.
164
-
200
+ you need your parser to be isolated from it's siblings in other threads - create a copy for one-off use inside
201
+ your `call` method.
165
202
 
166
203
  ## Pull requests
167
204
 
@@ -31,8 +31,8 @@ Gem::Specification.new do |spec|
31
31
  spec.require_paths = ['lib']
32
32
 
33
33
  spec.add_dependency 'ks', '~> 0.0'
34
- spec.add_dependency 'exifr', '~> 1', '>= 1.3.7'
35
- spec.add_dependency 'id3tag', '~> 0.13'
34
+ spec.add_dependency 'exifr', '~> 1', '>= 1.3.8'
35
+ spec.add_dependency 'id3tag', '~> 0.14'
36
36
  spec.add_dependency 'faraday', '~> 0.13'
37
37
  spec.add_dependency 'measurometer', '~> 1'
38
38
 
@@ -36,7 +36,7 @@ module FormatParser
36
36
  # Register a parser object to be used to perform file format detection. Each parser FormatParser
37
37
  # provides out of the box registers itself using this method.
38
38
  #
39
- # @param callable_or_responding_to_new[#call, #new] an object that either responds to #new or to #call
39
+ # @param callable_parser[#call] an object that responds to #call for parsing an IO
40
40
  # @param formats[Array<Symbol>] file formats that the parser provides
41
41
  # @param natures[Array<Symbol>] file natures that the parser provides
42
42
  # @param priority[Integer] whether the parser has to be applied first or later. Parsers that offer the safest
@@ -45,39 +45,39 @@ module FormatParser
45
45
  # with a lower priority value will be applied first, and if a single result is requested, will also return
46
46
  # first.
47
47
  # @return void
48
- def self.register_parser(callable_or_responding_to_new, formats:, natures:, priority: LEAST_PRIORITY)
48
+ def self.register_parser(callable_parser, formats:, natures:, priority: LEAST_PRIORITY)
49
49
  parser_provided_formats = Array(formats)
50
50
  parser_provided_natures = Array(natures)
51
51
  PARSER_MUX.synchronize do
52
52
  @parsers ||= Set.new
53
- @parsers << callable_or_responding_to_new
53
+ @parsers << callable_parser
54
54
  @parsers_per_nature ||= {}
55
55
  parser_provided_natures.each do |provided_nature|
56
56
  @parsers_per_nature[provided_nature] ||= Set.new
57
- @parsers_per_nature[provided_nature] << callable_or_responding_to_new
57
+ @parsers_per_nature[provided_nature] << callable_parser
58
58
  end
59
59
  @parsers_per_format ||= {}
60
60
  parser_provided_formats.each do |provided_format|
61
61
  @parsers_per_format[provided_format] ||= Set.new
62
- @parsers_per_format[provided_format] << callable_or_responding_to_new
62
+ @parsers_per_format[provided_format] << callable_parser
63
63
  end
64
64
  @parser_priorities ||= {}
65
- @parser_priorities[callable_or_responding_to_new] = priority
65
+ @parser_priorities[callable_parser] = priority
66
66
  end
67
67
  end
68
68
 
69
69
  # Deregister a parser object (makes FormatParser forget this parser existed). Is mostly used in
70
70
  # tests, but can also be used to forcibly disable some formats completely.
71
71
  #
72
- # @param callable_or_responding_to_new[#call, #new] an object that either responds to #new or to #call
72
+ # @param callable_parser[#==] an object that is identity-equal to any other registered parser
73
73
  # @return void
74
- def self.deregister_parser(callable_or_responding_to_new)
74
+ def self.deregister_parser(callable_parser)
75
75
  # Used only in tests
76
76
  PARSER_MUX.synchronize do
77
- (@parsers || []).delete(callable_or_responding_to_new)
78
- (@parsers_per_nature || {}).values.map { |e| e.delete(callable_or_responding_to_new) }
79
- (@parsers_per_format || {}).values.map { |e| e.delete(callable_or_responding_to_new) }
80
- (@parser_priorities || {}).delete(callable_or_responding_to_new)
77
+ (@parsers || []).delete(callable_parser)
78
+ (@parsers_per_nature || {}).values.map { |e| e.delete(callable_parser) }
79
+ (@parsers_per_format || {}).values.map { |e| e.delete(callable_parser) }
80
+ (@parser_priorities || {}).delete(callable_parser)
81
81
  end
82
82
  end
83
83
 
@@ -1,3 +1,3 @@
1
1
  module FormatParser
2
- VERSION = '0.24.1'
2
+ VERSION = '0.25.3'
3
3
  end
@@ -38,14 +38,8 @@ class FormatParser::MOOVParser
38
38
  ftyp_atom = decoder.find_first_atom_by_path(atom_tree, 'ftyp')
39
39
  file_type = ftyp_atom.field_value(:major_brand)
40
40
 
41
- width = nil
42
- height = nil
43
-
44
41
  # Try to find the width and height in the tkhd
45
- if tkhd = decoder.find_first_atom_by_path(atom_tree, 'moov', 'trak', 'tkhd')
46
- width = tkhd.field_value(:track_width).first
47
- height = tkhd.field_value(:track_height).first
48
- end
42
+ width, height = parse_dimensions(decoder, atom_tree)
49
43
 
50
44
  # Try to find the "topmost" duration (respecting edits)
51
45
  if mdhd = decoder.find_first_atom_by_path(atom_tree, 'moov', 'mvhd')
@@ -78,6 +72,31 @@ class FormatParser::MOOVParser
78
72
  FTYP_MAP.fetch(file_type.downcase, :mov)
79
73
  end
80
74
 
75
+ # The dimensions are located in tkhd atom, but in some files it is necessary
76
+ # to get it below the video track, because it can have other tracks such as
77
+ # audio which does not have the dimensions.
78
+ # More details in https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-DontLinkElementID_147
79
+ #
80
+ # Returns [width, height] if the dimension is found
81
+ # Returns [nil, nil] if the dimension is not found
82
+ def parse_dimensions(decoder, atom_tree)
83
+ video_trak_atom = decoder.find_video_trak_atom(atom_tree)
84
+
85
+ tkhd = begin
86
+ if video_trak_atom
87
+ decoder.find_first_atom_by_path([video_trak_atom], 'trak', 'tkhd')
88
+ else
89
+ decoder.find_first_atom_by_path(atom_tree, 'moov', 'trak', 'tkhd')
90
+ end
91
+ end
92
+
93
+ if tkhd
94
+ [tkhd.field_value(:track_width).first, tkhd.field_value(:track_height).first]
95
+ else
96
+ [nil, nil]
97
+ end
98
+ end
99
+
81
100
  # An MPEG4/MOV/M4A will start with the "ftyp" atom. The atom must have a length
82
101
  # of at least 8 (to accomodate the atom size and the atom type itself) plus the major
83
102
  # and minor version fields. If we cannot find it we can be certain this is not our file.
@@ -1,6 +1,7 @@
1
1
  # Handles decoding of MOV/MPEG4 atoms/boxes in a stream. Will recursively
2
2
  # read atoms and parse their data fields if applicable. Also contains
3
3
  # a few utility functions for finding atoms in a list etc.
4
+ # To know more about Atoms: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html
4
5
  class FormatParser::MOOVParser::Decoder
5
6
  include FormatParser::IOUtils
6
7
 
@@ -47,6 +48,34 @@ class FormatParser::MOOVParser::Decoder
47
48
  find_first_atom_by_path(requisite.children || [], *atom_types)
48
49
  end
49
50
 
51
+ def find_atoms_by_path(atoms, atom_types)
52
+ type_to_find = atom_types.shift
53
+ requisites = atoms.select { |e| e.atom_type == type_to_find }
54
+
55
+ # Return if we found our match
56
+ return requisites if atom_types.empty?
57
+
58
+ # Return nil if we didn't find the match at this nesting level
59
+ return unless requisites
60
+
61
+ # ...otherwise drill further down
62
+ find_atoms_by_path(requisites.flat_map(&:children).compact || [], atom_types)
63
+ end
64
+
65
+ # A file can have multiple tracks. To identify the type it is necessary to check
66
+ # the fields `omponent_subtype` in hdlr atom under the trak atom
67
+ # More details in https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-DontLinkElementID_147
68
+ def find_video_trak_atom(atoms)
69
+ trak_atoms = find_atoms_by_path(atoms, ['moov', 'trak'])
70
+
71
+ return if trak_atoms.empty?
72
+
73
+ trak_atoms.find do |trak_atom|
74
+ hdlr_atom = find_first_atom_by_path([trak_atom], 'trak', 'mdia', 'hdlr')
75
+ hdlr_atom.atom_fields[:component_type] == 'mhlr' && hdlr_atom.atom_fields[:component_subtype] == 'vide'
76
+ end
77
+ end
78
+
50
79
  def parse_ftyp_atom(io, atom_size)
51
80
  # Subtract 8 for the atom_size+atom_type,
52
81
  # and 8 once more for the major_brand and minor_version. The remaining
@@ -194,6 +223,8 @@ class FormatParser::MOOVParser::Decoder
194
223
  end
195
224
 
196
225
  def parse_meta_atom(io, atom_size)
226
+ return if atom_size == 0 # this atom can be empty
227
+
197
228
  parse_hdlr_atom(io, atom_size)
198
229
  end
199
230
 
@@ -73,6 +73,8 @@ class FormatParser::MP3Parser
73
73
  id3v1 = ID3Extraction.attempt_id3_v1_extraction(io)
74
74
  tags = [id3v1, ID3Extraction.attempt_id3_v2_extraction(io)].compact
75
75
 
76
+ io.seek(0) if tags.empty?
77
+
76
78
  # Compute how many bytes are occupied by the actual MPEG frames
77
79
  ignore_bytes_at_tail = id3v1 ? 128 : 0
78
80
  ignore_bytes_at_head = io.pos
@@ -249,16 +251,16 @@ class FormatParser::MP3Parser
249
251
  io.seek(xing_offset + 4) # Include the length of "Xing" itself
250
252
 
251
253
  # https://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header#XINGHeader
252
- header_flags, _ = io.read(4).unpack('s>s>')
254
+ header_flags, _ = io.read(4).unpack('i>')
253
255
  frames = byte_count = toc = vbr_scale = nil
254
256
 
255
- frames = io.read(4).unpack('N1').first if header_flags & 1 # FRAMES FLAG
257
+ frames = io.read(4).unpack('N1').first if header_flags & 1 != 0 # FRAMES FLAG
256
258
 
257
- byte_count = io.read(4).unpack('N1').first if header_flags & 2 # BYTES FLAG
259
+ byte_count = io.read(4).unpack('N1').first if header_flags & 2 != 0 # BYTES FLAG
258
260
 
259
- toc = io.read(100).unpack('C100') if header_flags & 4 # TOC FLAG
261
+ toc = io.read(100).unpack('C100') if header_flags & 4 != 0 # TOC FLAG
260
262
 
261
- vbr_scale = io.read(4).unpack('N1').first if header_flags & 8 # VBR SCALE FLAG
263
+ vbr_scale = io.read(4).unpack('N1').first if header_flags & 8 != 0 # VBR SCALE FLAG
262
264
 
263
265
  VBRHeader.new(frames: frames, byte_count: byte_count, toc_entries: toc, vbr_scale: vbr_scale)
264
266
  end
@@ -1,6 +1,8 @@
1
1
  module FormatParser::MP3Parser::ID3Extraction
2
2
  ID3V1_TAG_SIZE_BYTES = 128
3
- ID3V2_TAG_VERSIONS = ["\x43\x00".b, "\x03\x00".b, "\x02\x00".b]
3
+ # it supports 2.4.x, 2.3.x, 2.2.x which are supported by the gem id3tag
4
+ # see https://id3.org/Developer%20Information for more details of each version
5
+ ID3V2_MINOR_TAG_VERSIONS = [2, 3, 4]
4
6
  MAX_SIZE_FOR_ID3V2 = 1 * 1024 * 1024
5
7
 
6
8
  extend FormatParser::IOUtils
@@ -22,7 +24,7 @@ module FormatParser::MP3Parser::ID3Extraction
22
24
  io.seek(0) # Only support header ID3v2
23
25
  header = parse_id3_v2_header(io)
24
26
  return unless header[:tag] == 'ID3' && header[:size] > 0
25
- return unless ID3V2_TAG_VERSIONS.include?(header[:version])
27
+ return unless ID3V2_MINOR_TAG_VERSIONS.include?(header[:version].unpack('C').first)
26
28
 
27
29
  id3_tag_size = io.pos + header[:size]
28
30
 
@@ -108,4 +108,24 @@ describe FormatParser::MOOVParser do
108
108
  it 'provides filename hints' do
109
109
  expect(subject).to be_likely_match('file.m4v')
110
110
  end
111
+
112
+ it 'reads correctly the video dimensions' do
113
+ mov_path = fixtures_dir + '/MOOV/MOV/Test_Dimensions.mov'
114
+
115
+ result = subject.call(File.open(mov_path, 'rb'))
116
+
117
+ expect(result).not_to be_nil
118
+ expect(result.nature).to eq(:video)
119
+ expect(result.format).to eq(:mov)
120
+ expect(result.width_px).to eq(640)
121
+ expect(result.height_px).to eq(360)
122
+ end
123
+
124
+ it 'does not raise error when a meta atom has size 0' do
125
+ mov_path = fixtures_dir + '/MOOV/MOV/Test_Meta_Atom_With_Size_Zero.mov'
126
+
127
+ result = subject.call(File.open(mov_path, 'rb'))
128
+ expect(result).not_to be_nil
129
+ expect(result.format).to eq(:mov)
130
+ end
111
131
  end
@@ -15,6 +15,20 @@ describe FormatParser::MP3Parser do
15
15
  expect(parsed.media_duration_seconds).to be_within(0.1).of(0.836)
16
16
  end
17
17
 
18
+ it 'reads the Xing header without raising errors' do
19
+ fpath = fixtures_dir + '/MP3/test_xing_header.mp3'
20
+ parsed = subject.call(File.open(fpath, 'rb'))
21
+
22
+ expect(parsed).not_to be_nil
23
+
24
+ expect(parsed.nature).to eq(:audio)
25
+ expect(parsed.format).to eq(:mp3)
26
+ expect(parsed.num_audio_channels).to eq(2)
27
+ expect(parsed.audio_sample_rate_hz).to eq(48000)
28
+ expect(parsed.intrinsics).not_to be_nil
29
+ expect(parsed.media_duration_seconds).to be_within(0.1).of(0.0342)
30
+ end
31
+
18
32
  it 'does not misdetect a PNG' do
19
33
  fpath = fixtures_dir + '/PNG/anim.png'
20
34
  parsed = subject.call(File.open(fpath, 'rb'))
@@ -73,7 +87,7 @@ describe FormatParser::MP3Parser do
73
87
 
74
88
  large_syncsfe_size = [ID3Tag::SynchsafeInteger.encode(more_bytes_than_permitted)].pack('N')
75
89
  prepped = StringIO.new(
76
- 'ID3' + "\x43\x00".b + "\x00".b + large_syncsfe_size + gunk
90
+ 'ID3' + "\x03\x00".b + "\x00".b + large_syncsfe_size + gunk
77
91
  )
78
92
 
79
93
  expect(ID3Tag).not_to receive(:read)
@@ -144,6 +158,26 @@ describe FormatParser::MP3Parser do
144
158
  }.to raise_error(FormatParser::IOUtils::InvalidRead)
145
159
  end
146
160
 
161
+ it 'supports id3 v2.4.x' do
162
+ fpath = fixtures_dir + '/MP3/id3v24.mp3'
163
+
164
+ parsed = subject.call(File.open(fpath, 'rb'))
165
+
166
+ expect(parsed.artist). to eq('wetransfer')
167
+ end
168
+
169
+ it 'does not skip the first bytes if it is not a id3 tag header' do
170
+ fpath = fixtures_dir + '/MP3/no_id3_tags.mp3'
171
+
172
+ parsed = subject.call(File.open(fpath, 'rb'))
173
+
174
+ expect(parsed).not_to be_nil
175
+
176
+ expect(parsed.nature).to eq(:audio)
177
+ expect(parsed.format).to eq(:mp3)
178
+ expect(parsed.audio_sample_rate_hz).to eq(44100)
179
+ end
180
+
147
181
  describe '#as_json' do
148
182
  it 'converts all hash keys to string when stringify_keys: true' do
149
183
  fpath = fixtures_dir + '/MP3/Cassy.mp3'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: format_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.24.1
4
+ version: 0.25.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Noah Berman
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2020-09-16 00:00:00.000000000 Z
12
+ date: 2020-10-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: ks
@@ -34,7 +34,7 @@ dependencies:
34
34
  version: '1'
35
35
  - - ">="
36
36
  - !ruby/object:Gem::Version
37
- version: 1.3.7
37
+ version: 1.3.8
38
38
  type: :runtime
39
39
  prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
@@ -44,21 +44,21 @@ dependencies:
44
44
  version: '1'
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 1.3.7
47
+ version: 1.3.8
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: id3tag
50
50
  requirement: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0.13'
54
+ version: '0.14'
55
55
  type: :runtime
56
56
  prerelease: false
57
57
  version_requirements: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0.13'
61
+ version: '0.14'
62
62
  - !ruby/object:Gem::Dependency
63
63
  name: faraday
64
64
  requirement: !ruby/object:Gem::Requirement
@@ -292,7 +292,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
292
292
  - !ruby/object:Gem::Version
293
293
  version: '0'
294
294
  requirements: []
295
- rubygems_version: 3.0.3
295
+ rubygems_version: 3.1.2
296
296
  signing_key:
297
297
  specification_version: 4
298
298
  summary: A library for efficient parsing of file metadata