format_parser 0.29.0 → 1.1.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 +4 -4
- data/.github/workflows/main.yml +2 -6
- data/CHANGELOG.md +10 -0
- data/Gemfile +4 -0
- data/format_parser.gemspec +1 -1
- data/lib/format_parser/version.rb +1 -1
- data/lib/parsers/moov_parser/decoder.rb +19 -0
- data/lib/parsers/moov_parser.rb +36 -4
- data/lib/remote_io.rb +19 -5
- data/lib/video.rb +2 -0
- data/spec/integration/active_storage/rails_app.rb +6 -3
- data/spec/parsers/moov_parser_spec.rb +2 -0
- data/spec/remote_fetching_spec.rb +23 -2
- metadata +12 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e49394510e191185e3be1f3560ca182a103b9446225a1163294f2db6a912ae76
|
4
|
+
data.tar.gz: 4f52d761857020a0957711528ad4d10ee885c74745897fa14a5e69292abf0cc2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 829bd488d021cb0db5ce2b718aa661bdf3d4f86a55e55808c8279367e955d35b2bdccdd2bdf805ff736cef6d49f8c0f425c1ec65c0d7f07ada2db684581d4c1f
|
7
|
+
data.tar.gz: ed76f04c157a865de03df0d614d0d353b53e1e9c9b0a164ac6b2a6612a358643ae210b00fd92b91fec6cefd8c8c71e6ff559e63df3343b13ce161175425dbd3a
|
data/.github/workflows/main.yml
CHANGED
@@ -16,9 +16,6 @@ jobs:
|
|
16
16
|
- 2.7
|
17
17
|
- 2.6
|
18
18
|
- 2.5
|
19
|
-
- 2.4
|
20
|
-
- 2.3
|
21
|
-
- 2.2
|
22
19
|
- jruby
|
23
20
|
steps:
|
24
21
|
- name: Checkout
|
@@ -65,12 +62,11 @@ jobs:
|
|
65
62
|
- 2.7
|
66
63
|
- 2.6
|
67
64
|
- 2.5
|
68
|
-
- 2.4
|
69
|
-
- 2.3
|
70
|
-
- 2.2
|
71
65
|
- jruby
|
72
66
|
experimental: [false]
|
73
67
|
include:
|
68
|
+
- ruby: 3.1
|
69
|
+
experimental: true
|
74
70
|
- ruby: 3.0
|
75
71
|
experimental: true
|
76
72
|
steps:
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
## 1.1.0
|
2
|
+
* Add support for `frame_rate` in moov_parser
|
3
|
+
|
4
|
+
## 1.0.0
|
5
|
+
* Dropping support for Ruby 2.2.X, 2.3.X and 2.4.X
|
6
|
+
* MP3: Fix negative length reads in edge cases by bumping `id3tag` version to `v0.14.2`
|
7
|
+
|
8
|
+
## 0.29.1
|
9
|
+
* Fix handling of 200 responses with `parse_http` as well as handling of very small responses which do not need range access
|
10
|
+
|
1
11
|
## 0.29.0
|
2
12
|
* Add option `headers:` to `FormatParser.parse_http`
|
3
13
|
|
data/Gemfile
CHANGED
data/format_parser.gemspec
CHANGED
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
|
|
32
32
|
|
33
33
|
spec.add_dependency 'ks', '~> 0.0'
|
34
34
|
spec.add_dependency 'exifr', '~> 1', '>= 1.3.8'
|
35
|
-
spec.add_dependency 'id3tag', '~> 0.14'
|
35
|
+
spec.add_dependency 'id3tag', '~> 0.14', '>= 0.14.2'
|
36
36
|
spec.add_dependency 'faraday', '~> 0.13'
|
37
37
|
spec.add_dependency 'faraday_middleware', '~> 0.14'
|
38
38
|
spec.add_dependency 'measurometer', '~> 1'
|
@@ -72,6 +72,7 @@ class FormatParser::MOOVParser::Decoder
|
|
72
72
|
|
73
73
|
trak_atoms.find do |trak_atom|
|
74
74
|
hdlr_atom = find_first_atom_by_path([trak_atom], 'trak', 'mdia', 'hdlr')
|
75
|
+
next if hdlr_atom.nil?
|
75
76
|
hdlr_atom.atom_fields[:component_type] == 'mhlr' && hdlr_atom.atom_fields[:component_subtype] == 'vide'
|
76
77
|
end
|
77
78
|
end
|
@@ -111,6 +112,24 @@ class FormatParser::MOOVParser::Decoder
|
|
111
112
|
}
|
112
113
|
end
|
113
114
|
|
115
|
+
def parse_stts_atom(io, _)
|
116
|
+
version = read_byte_value(io)
|
117
|
+
is_v1 = version == 1
|
118
|
+
stts = {
|
119
|
+
version: version,
|
120
|
+
flags: read_bytes(io, 3),
|
121
|
+
number_of_entries: is_v1 ? read_64bit_uint(io) : read_32bit_uint(io),
|
122
|
+
entries: []
|
123
|
+
}
|
124
|
+
stts[:number_of_entries].times {
|
125
|
+
stts[:entries] << {
|
126
|
+
sample_count: read_32bit_uint(io),
|
127
|
+
sample_duration: read_32bit_uint(io)
|
128
|
+
}
|
129
|
+
}
|
130
|
+
stts
|
131
|
+
end
|
132
|
+
|
114
133
|
def parse_mdhd_atom(io, _)
|
115
134
|
version = read_byte_value(io)
|
116
135
|
is_v1 = version == 1
|
data/lib/parsers/moov_parser.rb
CHANGED
@@ -48,9 +48,9 @@ class FormatParser::MOOVParser
|
|
48
48
|
width, height = parse_dimensions(decoder, atom_tree)
|
49
49
|
|
50
50
|
# Try to find the "topmost" duration (respecting edits)
|
51
|
-
if
|
52
|
-
timescale =
|
53
|
-
duration =
|
51
|
+
if mvhd = decoder.find_first_atom_by_path(atom_tree, 'moov', 'mvhd')
|
52
|
+
timescale = mvhd.field_value(:tscale)
|
53
|
+
duration = mvhd.field_value(:duration)
|
54
54
|
media_duration_s = duration / timescale.to_f
|
55
55
|
end
|
56
56
|
|
@@ -64,13 +64,15 @@ class FormatParser::MOOVParser
|
|
64
64
|
intrinsics: atom_tree,
|
65
65
|
)
|
66
66
|
else
|
67
|
+
frame_rate = parse_sample_atom(decoder, atom_tree)&.truncate(2)
|
67
68
|
FormatParser::Video.new(
|
68
69
|
format: format_from_moov_type(file_type),
|
69
70
|
width_px: width,
|
70
71
|
height_px: height,
|
72
|
+
frame_rate: frame_rate,
|
71
73
|
media_duration_seconds: media_duration_s,
|
72
74
|
content_type: MP4_MIXED_MIME_TYPE,
|
73
|
-
intrinsics: atom_tree
|
75
|
+
intrinsics: atom_tree
|
74
76
|
)
|
75
77
|
end
|
76
78
|
end
|
@@ -115,5 +117,35 @@ class FormatParser::MOOVParser
|
|
115
117
|
maybe_atom_size >= minimum_ftyp_atom_size && maybe_ftyp_atom_signature == 'ftyp'
|
116
118
|
end
|
117
119
|
|
120
|
+
# Sample information is found in the 'time-to-sample' stts atom.
|
121
|
+
# The media atom mdhd is needed too in order to get the movie timescale
|
122
|
+
def parse_sample_atom(decoder, atom_tree)
|
123
|
+
video_trak_atom = decoder.find_video_trak_atom(atom_tree)
|
124
|
+
|
125
|
+
stts = if video_trak_atom
|
126
|
+
decoder.find_first_atom_by_path([video_trak_atom], 'trak', 'mdia', 'minf', 'stbl', 'stts')
|
127
|
+
else
|
128
|
+
decoder.find_first_atom_by_path(atom_tree, 'moov', 'trak', 'mdia', 'minf', 'stbl', 'stts')
|
129
|
+
end
|
130
|
+
|
131
|
+
mdhd = if video_trak_atom
|
132
|
+
decoder.find_first_atom_by_path([video_trak_atom], 'trak', 'mdia', 'mdhd')
|
133
|
+
else
|
134
|
+
decoder.find_first_atom_by_path(atom_tree, 'moov', 'trak', 'mdia', 'mdhd')
|
135
|
+
end
|
136
|
+
|
137
|
+
if stts && mdhd
|
138
|
+
timescale = mdhd.atom_fields[:tscale]
|
139
|
+
sample_duration = stts.field_value(:entries).first[:sample_duration]
|
140
|
+
if timescale.nil? || timescale == 0 || sample_duration.nil? || sample_duration == 0
|
141
|
+
nil
|
142
|
+
else
|
143
|
+
timescale.to_f / sample_duration
|
144
|
+
end
|
145
|
+
else
|
146
|
+
nil
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
118
150
|
FormatParser.register_parser new, natures: :video, formats: FTYP_MAP.values, priority: 1
|
119
151
|
end
|
data/lib/remote_io.rb
CHANGED
@@ -89,18 +89,32 @@ class FormatParser::RemoteIO
|
|
89
89
|
response = conn.get(@uri, nil, range: 'bytes=%d-%d' % [range.begin, range.end])
|
90
90
|
|
91
91
|
case response.status
|
92
|
-
when 200
|
92
|
+
when 200
|
93
|
+
# S3 returns 200 when you request a Range that is fully satisfied by the entire object,
|
94
|
+
# we take that into account here. Also, for very tiny responses (and also for empty responses)
|
95
|
+
# the responses are going to be 200 which does not mean we cannot proceed
|
96
|
+
# To have a good check for both of these conditions we need to know whether the ranges overlap fully
|
97
|
+
response_size = response.body.bytesize
|
98
|
+
requested_range_size = range.end - range.begin + 1
|
99
|
+
if response_size > requested_range_size
|
100
|
+
error_message = [
|
101
|
+
"We requested #{requested_range_size} bytes, but the server sent us more",
|
102
|
+
"(#{response_size} bytes) - it likely has no `Range:` support.",
|
103
|
+
"The error occurred when talking to #{@uri})"
|
104
|
+
]
|
105
|
+
raise InvalidRequest.new(response.status, error_message.join("\n"))
|
106
|
+
end
|
107
|
+
[response_size, response.body]
|
108
|
+
when 206
|
93
109
|
# Figure out of the server supports content ranges, if it doesn't we have no
|
94
110
|
# business working with that server
|
95
111
|
range_header = response.headers['Content-Range']
|
96
|
-
raise InvalidRequest.new(response.status, "
|
112
|
+
raise InvalidRequest.new(response.status, "The server replied with 206 status but no Content-Range at #{@uri}") unless range_header
|
97
113
|
|
98
114
|
# "Content-Range: bytes 0-0/307404381" is how the response header is structured
|
99
115
|
size = range_header[/\/(\d+)$/, 1].to_i
|
100
116
|
|
101
|
-
#
|
102
|
-
# we take that into account here. For other servers, 206 is the expected response code.
|
103
|
-
# Also, if we request a _larger_ range than what can be satisfied by the server,
|
117
|
+
# If we request a _larger_ range than what can be satisfied by the server,
|
104
118
|
# the response is going to only contain what _can_ be sent and the status is also going
|
105
119
|
# to be 206
|
106
120
|
return [size, response.body]
|
data/lib/video.rb
CHANGED
@@ -53,19 +53,22 @@ end
|
|
53
53
|
require 'minitest/autorun'
|
54
54
|
require 'open-uri'
|
55
55
|
|
56
|
+
fixtures_dir = File.join(File.dirname(__FILE__), '../../fixtures')
|
57
|
+
|
56
58
|
describe User do
|
57
59
|
describe "profile_picture's metadatas" do
|
58
60
|
it 'parse metadatas with format_parser' do
|
61
|
+
fixture_path = fixtures_dir + '/PNG/cat.png'
|
59
62
|
user = User.create
|
60
63
|
user.profile_picture.attach(
|
61
64
|
filename: 'cat.png',
|
62
|
-
io:
|
65
|
+
io: File.open(fixture_path, 'rb')
|
63
66
|
)
|
64
67
|
|
65
68
|
user.profile_picture.analyze
|
66
69
|
|
67
|
-
_(user.profile_picture.metadata[:width_px]).must_equal
|
68
|
-
_(user.profile_picture.metadata[:height_px]).must_equal
|
70
|
+
_(user.profile_picture.metadata[:width_px]).must_equal 600
|
71
|
+
_(user.profile_picture.metadata[:height_px]).must_equal 600
|
69
72
|
_(user.profile_picture.metadata[:color_mode]).must_equal 'rgba'
|
70
73
|
end
|
71
74
|
end
|
@@ -106,6 +106,7 @@ describe FormatParser::MOOVParser do
|
|
106
106
|
expect(result.format).to eq(:mov)
|
107
107
|
expect(result.width_px).to eq(160)
|
108
108
|
expect(result.height_px).to eq(90)
|
109
|
+
expect(result.frame_rate).to eq(14.98)
|
109
110
|
end
|
110
111
|
|
111
112
|
it 'provides filename hints' do
|
@@ -122,6 +123,7 @@ describe FormatParser::MOOVParser do
|
|
122
123
|
expect(result.format).to eq(:mov)
|
123
124
|
expect(result.width_px).to eq(640)
|
124
125
|
expect(result.height_px).to eq(360)
|
126
|
+
expect(result.frame_rate).to eq(30)
|
125
127
|
end
|
126
128
|
|
127
129
|
it 'does not raise error when a meta atom has size 0' do
|
@@ -19,18 +19,27 @@ describe 'Fetching data from HTTP remotes' do
|
|
19
19
|
res.status = 302
|
20
20
|
res.header['Location'] = req.path.sub('/redirect', '')
|
21
21
|
end
|
22
|
+
@server.mount_proc '/empty' do |_req, res|
|
23
|
+
res.status = 200
|
24
|
+
res.body = ''
|
25
|
+
end
|
26
|
+
@server.mount_proc '/tiny' do |_req, res|
|
27
|
+
res.status = 200
|
28
|
+
res.body = File.read(fixtures_dir + '/test.gif')
|
29
|
+
end
|
30
|
+
|
22
31
|
trap('INT') { @server.stop }
|
23
32
|
@server_thread = Thread.new { @server.start }
|
24
33
|
end
|
25
34
|
|
26
|
-
it '
|
35
|
+
it 'works with .parse_http called without any options' do
|
27
36
|
result = FormatParser.parse_http('http://localhost:9399/PNG/anim.png')
|
28
37
|
|
29
38
|
expect(result.format).to eq(:png)
|
30
39
|
expect(result.height_px).to eq(180)
|
31
40
|
end
|
32
41
|
|
33
|
-
it '
|
42
|
+
it 'works with .parse_http called with additional options' do
|
34
43
|
fake_result = double(nature: :audio, format: :aiff)
|
35
44
|
expect_any_instance_of(FormatParser::AIFFParser).to receive(:call).and_return(fake_result)
|
36
45
|
results = FormatParser.parse_http('http://localhost:9399/PNG/anim.png', results: :all)
|
@@ -39,6 +48,18 @@ describe 'Fetching data from HTTP remotes' do
|
|
39
48
|
expect(results).to include(fake_result)
|
40
49
|
end
|
41
50
|
|
51
|
+
it 'is able to cope with a 0-size resource which does not provide Content-Range' do
|
52
|
+
file_information = FormatParser.parse_http('http://localhost:9399/empty')
|
53
|
+
|
54
|
+
expect(file_information).to be_nil
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'is able to cope with a tiny resource which fits into the first requested range completely' do
|
58
|
+
file_information = FormatParser.parse_http('http://localhost:9399/tiny')
|
59
|
+
expect(file_information).not_to be_nil
|
60
|
+
expect(file_information.nature).to eq(:image)
|
61
|
+
end
|
62
|
+
|
42
63
|
it 'parses the animated PNG over HTTP' do
|
43
64
|
file_information = FormatParser.parse_http('http://localhost:9399/PNG/anim.png')
|
44
65
|
expect(file_information).not_to be_nil
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: format_parser
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Noah Berman
|
8
8
|
- Julik Tarkhanov
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2022-04-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: ks
|
@@ -52,6 +52,9 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0.14'
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 0.14.2
|
55
58
|
type: :runtime
|
56
59
|
prerelease: false
|
57
60
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -59,6 +62,9 @@ dependencies:
|
|
59
62
|
- - "~>"
|
60
63
|
- !ruby/object:Gem::Version
|
61
64
|
version: '0.14'
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 0.14.2
|
62
68
|
- !ruby/object:Gem::Dependency
|
63
69
|
name: faraday
|
64
70
|
requirement: !ruby/object:Gem::Requirement
|
@@ -294,7 +300,7 @@ licenses:
|
|
294
300
|
- MIT (Hippocratic)
|
295
301
|
metadata:
|
296
302
|
allowed_push_host: https://rubygems.org
|
297
|
-
post_install_message:
|
303
|
+
post_install_message:
|
298
304
|
rdoc_options: []
|
299
305
|
require_paths:
|
300
306
|
- lib
|
@@ -309,8 +315,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
309
315
|
- !ruby/object:Gem::Version
|
310
316
|
version: '0'
|
311
317
|
requirements: []
|
312
|
-
rubygems_version: 3.
|
313
|
-
signing_key:
|
318
|
+
rubygems_version: 3.3.4
|
319
|
+
signing_key:
|
314
320
|
specification_version: 4
|
315
321
|
summary: A library for efficient parsing of file metadata
|
316
322
|
test_files: []
|