wahwah 1.5.0 → 1.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02bf7083fb530d058140db0b5f5e18cf2710ba7a6ba7fed2d2978c2109764ab2
4
- data.tar.gz: dc9a32191879dfda4ea08477579ec4c631be6f0829d25ad066623232cd926a15
3
+ metadata.gz: 7ed4753543c1ad49e2c3c274198e3bc9a4a2416ff4e8937d6d418587f777756f
4
+ data.tar.gz: 16079b5131d51a3ba203a859acd7c17627a34d705105234a4a15a00bd68c9eb3
5
5
  SHA512:
6
- metadata.gz: 59b2cd82d8d52b273bb2abfa4085a083716fb9cb701102c0103e6ccf2a08b3909253c8bda2ffe2d423bed43a056a8aab1bd7d3c172468c6f72fa7dafc4879b56
7
- data.tar.gz: 578a61f74b3eea858ba31817d8d16e663b3e7680806886e2e44fc7b4c93b09b601a5559c07326f63cc0e43a6be2c1d362e6247297679e65578e301f0f7c591ee
6
+ metadata.gz: bf8adc7cef79b90caa947f5997a489b7b678defac1efc64fc51e2e95c9a50453a762dcf03a8295e4036cd5c9b6bd55fbd565381ba7d1fa9fcc74008bc4013eb0
7
+ data.tar.gz: a09f45c83ce58685d6da2df6ebffc20883acb2ce70cf95168c6896659b98fd682d696f3c52a066b092ac92694a9c3cb92e790c08edf3d0485e4a125116fac9ed
data/lib/wahwah/helper.rb CHANGED
@@ -25,13 +25,52 @@ module WahWah
25
25
  string.split(Regexp.new(('\x00' * terminator_size).b), 2)
26
26
  end
27
27
 
28
- def self.file_format(file_path)
29
- File.extname(file_path).downcase.delete(".")
28
+ def self.file_format(io)
29
+ if io.respond_to?(:path) && io.path && !io.path.empty?
30
+ file_format = file_format_from_extension io.path
31
+ return file_format unless file_format.nil? || file_format.empty?
32
+ end
33
+
34
+ file_format_from_signature io
30
35
  end
31
36
 
32
37
  def self.byte_string_to_guid(byte_string)
33
38
  guid = byte_string.unpack("NnnA*").pack("VvvA*").unpack1("H*")
34
39
  [guid[0..7], guid[8..11], guid[12..15], guid[16..19], guid[20..]].join("-").upcase
35
40
  end
41
+
42
+ def self.file_format_from_extension(file_path)
43
+ File.extname(file_path).downcase[1..]
44
+ end
45
+
46
+ def self.file_format_from_signature(io)
47
+ io.rewind
48
+ signature = io.read(16)
49
+
50
+ # Source: https://en.wikipedia.org/wiki/List_of_file_signatures
51
+
52
+ # M4A is checked for first, since MP4 files start with a chunk size -
53
+ # and that chunk size may incidentally match another signature.
54
+ # No other formats would reasonably have "ftyp" as the next for bytes.
55
+ return "m4a" if signature[4...12] == "ftypM4A ".b
56
+ # Handled separately simply because it requires two checks.
57
+ return "wav" if signature.start_with?("RIFF".b) && signature[8...12] == "WAVE".b
58
+ magic_numbers = {
59
+ "fLaC".b => "flac",
60
+ "\xFF\xFB".b => "mp3",
61
+ "\xFF\xF3".b => "mp3",
62
+ "\xFF\xF2".b => "mp3",
63
+ "ID3".b => "mp3",
64
+ "OggS".b => "ogg",
65
+ "\x30\x26\xB2\x75\x8E\x66\xCF\x11\xA6\xD9\x00\xAA\x00\x62\xCE\x6C".b => "wma"
66
+ }
67
+ magic_numbers.each do |expected_signature, file_format|
68
+ return file_format if signature.start_with? expected_signature
69
+ end
70
+
71
+ nil
72
+ end
73
+
74
+ private_class_method :file_format_from_extension, :file_format_from_signature
36
75
  end
37
76
  end
@@ -17,7 +17,7 @@ module WahWah
17
17
  attr_reader :major_version, :size
18
18
 
19
19
  def initialize(file_io)
20
- header_content = file_io.read(HEADER_SIZE)
20
+ header_content = file_io.read(HEADER_SIZE) || ""
21
21
  @id, @major_version, @flags, size_bits = header_content.unpack(HEADER_FORMAT) if header_content.size >= HEADER_SIZE
22
22
 
23
23
  return unless valid?
@@ -50,11 +50,20 @@ module WahWah
50
50
  end
51
51
 
52
52
  def parse_id3_tag
53
- id3_v1_tag = ID3::V1.new(@file_io.dup)
54
- id3_v2_tag = ID3::V2.new(@file_io.dup)
55
-
56
- return id3_v2_tag if id3_v2_tag.valid?
57
- id3_v1_tag if id3_v1_tag.valid?
53
+ @file_io.rewind
54
+ signature = @file_io.read(6)
55
+
56
+ if signature.start_with?("ID3".b)
57
+ id3_v2_tag = ID3::V2.new(@file_io)
58
+ id3_v2_tag if id3_v2_tag.valid?
59
+ elsif ["\xFF\xFB".b, "\xFF\xF3".b, "\xFF\xF2".b].include? signature[...2]
60
+ # It's important to only initialize an ID3v1 object as the last option,
61
+ # as it requires reading to the end of the file. If the file is a
62
+ # streaming download (using, for example, Down), that requires
63
+ # downloading the whole file, which is not the case with ID3v2.
64
+ id3_v1_tag = ID3::V1.new(@file_io.dup)
65
+ id3_v1_tag if id3_v1_tag.valid?
66
+ end
58
67
  end
59
68
 
60
69
  def parse_duration
@@ -17,6 +17,20 @@ module WahWah
17
17
  :sample_rate,
18
18
  :lyrics
19
19
 
20
+ # Oggs require reading to the end of the file in order to calculate their
21
+ # duration (as it's not stored in any header or metadata). Thus, if the
22
+ # IO like object supplied to WahWah is a streaming download, getting the
23
+ # duration would download the entire audio file, which may not be
24
+ # desirable. Making it lazy in this manner allows the user to avoid that.
25
+ def duration
26
+ @duration ||= parse_duration
27
+ end
28
+
29
+ # Requires duration to calculate, so lazy as well.
30
+ def bitrate
31
+ @bitrate ||= parse_bitrate
32
+ end
33
+
20
34
  private
21
35
 
22
36
  def packets
@@ -41,8 +55,6 @@ module WahWah
41
55
  Ogg::FlacTag.new(identification_packet, comment_packet)
42
56
  end
43
57
 
44
- @duration = parse_duration
45
- @bitrate = parse_bitrate
46
58
  @bit_depth = parse_bit_depth
47
59
  end
48
60
 
@@ -37,7 +37,7 @@ module WahWah
37
37
  end
38
38
 
39
39
  def valid?
40
- !@id.empty? && !@size.nil? && @size > 0
40
+ @id && !@id.empty? && !@size.nil? && @size > 0
41
41
  end
42
42
 
43
43
  private
@@ -95,6 +95,12 @@ module WahWah
95
95
  def parse_fmt_chunk(chunk)
96
96
  _, @channel, @sample_rate, _, _, @bit_depth = chunk.data.unpack("vvVVvv")
97
97
  @bitrate = @sample_rate * @channel * @bit_depth / 1000
98
+
99
+ # There is a rare situation where the data chunk is not included in the top RIFF chunk.
100
+ # In this case, the duration can not be got from the data chunk, some player even cannot play the file.
101
+ # So try to estimate the duration from the file size and bitrate.
102
+ # This may be inaccurate, but at least can get approximate duration.
103
+ @duration = (@file_size * 8) / (@bitrate * 1000).to_f if @duration.nil?
98
104
  end
99
105
 
100
106
  def parse_data_chunk(chunk)
data/lib/wahwah/tag.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module WahWah
4
4
  class Tag
5
5
  INTEGER_ATTRIBUTES = %i[disc disc_total track track_total]
6
- INSPECT_ATTRIBUTES = %i[title artist album albumartist composer track track_total genre year disc disc_total duration bitrate sample_rate bit_depth]
6
+ INSPECT_ATTRIBUTES = %i[title artist album albumartist composer track track_total genre year disc disc_total]
7
7
 
8
8
  attr_reader(
9
9
  :title,
@@ -26,14 +26,9 @@ module WahWah
26
26
  :file_size
27
27
  )
28
28
 
29
- def initialize(file)
30
- if file.is_a?(IO) || file.is_a?(StringIO)
31
- @file_size = file.size
32
- @file_io = file
33
- else
34
- @file_size = File.size(file)
35
- @file_io = File.open(file)
36
- end
29
+ def initialize(io)
30
+ @file_size = io.size
31
+ @file_io = io
37
32
 
38
33
  @comments = []
39
34
  @images_data = []
@@ -44,8 +39,6 @@ module WahWah
44
39
  value = instance_variable_get("@#{attr_name}")&.to_i
45
40
  instance_variable_set("@#{attr_name}", value)
46
41
  end
47
- ensure
48
- @file_io.close
49
42
  end
50
43
 
51
44
  def inspect
@@ -58,7 +51,7 @@ module WahWah
58
51
  def images
59
52
  return @images_data if @images_data.empty?
60
53
 
61
- @images_data.map do |data|
54
+ @images ||= @images_data.map do |data|
62
55
  parse_image_data(data)
63
56
  end
64
57
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WahWah
4
- VERSION = "1.5.0"
4
+ VERSION = "1.6.0"
5
5
  end
data/lib/wahwah.rb CHANGED
@@ -60,23 +60,36 @@ module WahWah
60
60
  Mp4Tag: ["m4a"]
61
61
  }.freeze
62
62
 
63
- def self.open(file_path)
64
- file_path = file_path.to_path if file_path.respond_to? :to_path
65
- file_path = file_path.to_str
63
+ def self.open(path_or_io)
64
+ with_io path_or_io do |io|
65
+ file_format = Helper.file_format io
66
66
 
67
- file_format = Helper.file_format(file_path)
67
+ raise WahWahArgumentError, "No supported format found" unless support_formats.include? file_format
68
68
 
69
- raise WahWahArgumentError, "File is not exists" unless File.exist? file_path
70
- raise WahWahArgumentError, "File is unreadable" unless File.readable? file_path
71
- raise WahWahArgumentError, "File is empty" unless File.size(file_path) > 0
72
- raise WahWahArgumentError, "No supported format found" unless support_formats.include? file_format
73
-
74
- FORMATE_MAPPING.each do |tag, formats|
75
- break const_get(tag).new(file_path) if formats.include?(file_format)
69
+ FORMATE_MAPPING.each do |tag, formats|
70
+ break const_get(tag).new(io) if formats.include?(file_format)
71
+ end
76
72
  end
77
73
  end
78
74
 
79
75
  def self.support_formats
80
76
  FORMATE_MAPPING.values.flatten
81
77
  end
78
+
79
+ def self.with_io(path_or_io, &block)
80
+ path_or_io = Pathname.new path_or_io if path_or_io.respond_to? :to_str
81
+
82
+ if path_or_io.is_a? Pathname
83
+ raise WahWahArgumentError, "File does not exist" unless File.exist? path_or_io
84
+ raise WahWahArgumentError, "File is unreadable" unless File.readable? path_or_io
85
+ raise WahWahArgumentError, "File is empty" unless File.size(path_or_io) > 0
86
+
87
+ path_or_io.open(&block)
88
+ else
89
+
90
+ block.call path_or_io
91
+ end
92
+ end
93
+
94
+ private_class_method :with_io
82
95
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wahwah
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - aidewoode
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-10 00:00:00.000000000 Z
11
+ date: 2024-06-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -175,7 +175,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
175
175
  - !ruby/object:Gem::Version
176
176
  version: '0'
177
177
  requirements: []
178
- rubygems_version: 3.3.26
178
+ rubygems_version: 3.3.27
179
179
  signing_key:
180
180
  specification_version: 4
181
181
  summary: Audio metadata reader ruby gem