wahwah 0.1.0 → 1.0.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/LICENSE +21 -0
- data/lib/wahwah.rb +74 -3
- data/lib/wahwah/asf/object.rb +39 -0
- data/lib/wahwah/asf_tag.rb +220 -0
- data/lib/wahwah/errors.rb +6 -0
- data/lib/wahwah/flac/block.rb +57 -0
- data/lib/wahwah/flac/streaminfo_block.rb +51 -0
- data/lib/wahwah/flac_tag.rb +84 -0
- data/lib/wahwah/helper.rb +37 -0
- data/lib/wahwah/id3/comment_frame_body.rb +21 -0
- data/lib/wahwah/id3/frame.rb +180 -0
- data/lib/wahwah/id3/frame_body.rb +36 -0
- data/lib/wahwah/id3/genre_frame_body.rb +15 -0
- data/lib/wahwah/id3/image_frame_body.rb +60 -0
- data/lib/wahwah/id3/text_frame_body.rb +16 -0
- data/lib/wahwah/id3/v1.rb +96 -0
- data/lib/wahwah/id3/v2.rb +60 -0
- data/lib/wahwah/id3/v2_header.rb +53 -0
- data/lib/wahwah/mp3/mpeg_frame_header.rb +141 -0
- data/lib/wahwah/mp3/vbri_header.rb +47 -0
- data/lib/wahwah/mp3/xing_header.rb +45 -0
- data/lib/wahwah/mp3_tag.rb +110 -0
- data/lib/wahwah/mp4/atom.rb +105 -0
- data/lib/wahwah/mp4_tag.rb +126 -0
- data/lib/wahwah/ogg/flac_tag.rb +37 -0
- data/lib/wahwah/ogg/opus_tag.rb +33 -0
- data/lib/wahwah/ogg/packets.rb +41 -0
- data/lib/wahwah/ogg/page.rb +121 -0
- data/lib/wahwah/ogg/pages.rb +24 -0
- data/lib/wahwah/ogg/vorbis_comment.rb +51 -0
- data/lib/wahwah/ogg/vorbis_tag.rb +35 -0
- data/lib/wahwah/ogg_tag.rb +66 -0
- data/lib/wahwah/riff/chunk.rb +54 -0
- data/lib/wahwah/riff_tag.rb +140 -0
- data/lib/wahwah/tag.rb +59 -0
- data/lib/wahwah/tag_delegate.rb +16 -0
- data/lib/wahwah/version.rb +4 -2
- metadata +94 -23
- data/.gitignore +0 -8
- data/.travis.yml +0 -5
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -22
- data/README.md +0 -35
- data/Rakefile +0 -10
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/wahwah.gemspec +0 -27
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
class FlacTag < Tag
|
5
|
+
include Ogg::VorbisComment
|
6
|
+
include Flac::StreaminfoBlock
|
7
|
+
|
8
|
+
TAG_ID = 'fLaC'
|
9
|
+
|
10
|
+
private
|
11
|
+
# FLAC structure:
|
12
|
+
#
|
13
|
+
# The four byte string "fLaC"
|
14
|
+
# The STREAMINFO metadata block
|
15
|
+
# Zero or more other metadata blocks
|
16
|
+
# One or more audio frames
|
17
|
+
def parse
|
18
|
+
# Flac file maybe contain ID3 header on the start, so skip it if exists
|
19
|
+
id3_header = ID3::V2Header.new(@file_io)
|
20
|
+
id3_header.valid? ? @file_io.seek(id3_header.size) : @file_io.rewind
|
21
|
+
|
22
|
+
return if @file_io.read(4) != TAG_ID
|
23
|
+
|
24
|
+
loop do
|
25
|
+
block = Flac::Block.new(@file_io)
|
26
|
+
parse_block(block)
|
27
|
+
|
28
|
+
break if block.is_last? || @file_io.eof?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse_block(block)
|
33
|
+
return unless block.valid?
|
34
|
+
|
35
|
+
case block.type
|
36
|
+
when 'STREAMINFO'
|
37
|
+
parse_streaminfo_block(block.data)
|
38
|
+
when 'VORBIS_COMMENT'
|
39
|
+
parse_vorbis_comment(block.data)
|
40
|
+
when 'PICTURE'
|
41
|
+
parse_picture_block(block.data)
|
42
|
+
else
|
43
|
+
@file_io.seek(block.size, IO::SEEK_CUR)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# PICTURE block data structure:
|
48
|
+
#
|
49
|
+
# Length(bit) Meaning
|
50
|
+
#
|
51
|
+
# 32 The picture type according to the ID3v2 APIC frame:
|
52
|
+
#
|
53
|
+
# 32 The length of the MIME type string in bytes.
|
54
|
+
#
|
55
|
+
# n*8 The MIME type string.
|
56
|
+
#
|
57
|
+
# 32 The length of the description string in bytes.
|
58
|
+
#
|
59
|
+
# n*8 The description of the picture, in UTF-8.
|
60
|
+
#
|
61
|
+
# 32 The width of the picture in pixels.
|
62
|
+
#
|
63
|
+
# 32 The height of the picture in pixels.
|
64
|
+
#
|
65
|
+
# 32 The color depth of the picture in bits-per-pixel.
|
66
|
+
#
|
67
|
+
# 32 For indexed-color pictures (e.g. GIF), the number of colors used, or 0 for non-indexed pictures.
|
68
|
+
#
|
69
|
+
# 32 The length of the picture data in bytes.
|
70
|
+
#
|
71
|
+
# n*8 The binary picture data.
|
72
|
+
def parse_picture_block(block_data)
|
73
|
+
block_content = StringIO.new(block_data)
|
74
|
+
|
75
|
+
type_index, mime_type_length = block_content.read(8).unpack('NN')
|
76
|
+
mime_type = Helper.encode_to_utf8(block_content.read(mime_type_length))
|
77
|
+
description_length = block_content.read(4).unpack('N').first
|
78
|
+
data_length = block_content.read(description_length + 20).unpack("#{'x' * (description_length + 16)}N").first
|
79
|
+
data = block_content.read(data_length)
|
80
|
+
|
81
|
+
@images.push({ data: data, mime_type: mime_type, type: ID3::ImageFrameBody::TYPES[type_index] })
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Helper
|
5
|
+
def self.encode_to_utf8(string, source_encoding: '')
|
6
|
+
encoded_string = source_encoding.empty? ?
|
7
|
+
string.force_encoding('utf-8') :
|
8
|
+
string.encode('utf-8', source_encoding, invalid: :replace, undef: :replace, replace: '')
|
9
|
+
|
10
|
+
encoded_string.valid_encoding? ? encoded_string.strip : ''
|
11
|
+
end
|
12
|
+
|
13
|
+
# ID3 size is encoded with four bytes where may the most significant
|
14
|
+
# bit (bit 7) is set to zero in every byte,
|
15
|
+
# making a total of 28 bits. The zeroed bits are ignored
|
16
|
+
def self.id3_size_caculate(bits_string, has_zero_bit: true)
|
17
|
+
if has_zero_bit
|
18
|
+
bits_string.scan(/.{8}/).map { |byte_string| byte_string[1..-1] }.join.to_i(2)
|
19
|
+
else
|
20
|
+
bits_string.to_i(2)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.split_with_terminator(string, terminator_size)
|
25
|
+
string.split(Regexp.new(('\x00' * terminator_size).b), 2)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.file_format(file_path)
|
29
|
+
File.extname(file_path).downcase.delete('.')
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.byte_string_to_guid(byte_string)
|
33
|
+
guid = byte_string.unpack('NnnA*').pack('VvvA*').unpack('H*').first
|
34
|
+
[guid[0..7], guid[8..11], guid[12..15], guid[16..19], guid[20..-1]].join('-').upcase
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module ID3
|
5
|
+
class CommentFrameBody < FrameBody
|
6
|
+
# Comment frame body structure:
|
7
|
+
#
|
8
|
+
# Text encoding $xx
|
9
|
+
# Language $xx xx xx
|
10
|
+
# Short content description <textstring> $00 (00)
|
11
|
+
# The actual text <textstring>
|
12
|
+
def parse
|
13
|
+
encoding_id, _language, reset_content = @content.unpack('CA3a*')
|
14
|
+
encoding = ENCODING_MAPPING[encoding_id]
|
15
|
+
_description, comment_text = Helper.split_with_terminator(reset_content, ENCODING_TERMINATOR_SIZE[encoding])
|
16
|
+
|
17
|
+
@value = Helper.encode_to_utf8(comment_text, source_encoding: encoding)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zlib'
|
4
|
+
|
5
|
+
module WahWah
|
6
|
+
module ID3
|
7
|
+
class Frame
|
8
|
+
ID_MAPPING = {
|
9
|
+
# ID3v2.2 frame id
|
10
|
+
COM: :comment,
|
11
|
+
TRK: :track,
|
12
|
+
TYE: :year,
|
13
|
+
TAL: :album,
|
14
|
+
TP1: :artist,
|
15
|
+
TT2: :title,
|
16
|
+
TCO: :genre,
|
17
|
+
TPA: :disc,
|
18
|
+
TP2: :albumartist,
|
19
|
+
TCM: :composer,
|
20
|
+
PIC: :image,
|
21
|
+
|
22
|
+
# ID3v2.3 and ID3v2.4 frame id
|
23
|
+
COMM: :comment,
|
24
|
+
TRCK: :track,
|
25
|
+
TYER: :year,
|
26
|
+
TALB: :album,
|
27
|
+
TPE1: :artist,
|
28
|
+
TIT2: :title,
|
29
|
+
TCON: :genre,
|
30
|
+
TPOS: :disc,
|
31
|
+
TPE2: :albumartist,
|
32
|
+
TCOM: :composer,
|
33
|
+
APIC: :image,
|
34
|
+
|
35
|
+
# ID3v2.4 use TDRC replace TYER
|
36
|
+
TDRC: :year
|
37
|
+
}
|
38
|
+
|
39
|
+
# ID3v2.3 frame flags field is defined as follows.
|
40
|
+
#
|
41
|
+
# %abc00000 %ijk00000
|
42
|
+
#
|
43
|
+
# a - Tag alter preservation
|
44
|
+
# b - File alter preservation
|
45
|
+
# c - Read only
|
46
|
+
# i - Compression
|
47
|
+
# j - Encryption
|
48
|
+
# k - Grouping identity
|
49
|
+
V3_HEADER_FLAGS_INDICATIONS = Array.new(16).tap do |array|
|
50
|
+
array[0] = :tag_alter_preservation
|
51
|
+
array[1] = :file_alter_preservation
|
52
|
+
array[2] = :read_only
|
53
|
+
array[8] = :compression
|
54
|
+
array[9] = :encryption
|
55
|
+
array[10] = :grouping_identity
|
56
|
+
end
|
57
|
+
|
58
|
+
# ID3v2.4 frame flags field is defined as follows.
|
59
|
+
#
|
60
|
+
# %0abc0000 %0h00kmnp
|
61
|
+
#
|
62
|
+
# a - Tag alter preservation
|
63
|
+
# b - File alter preservation
|
64
|
+
# c - Read only
|
65
|
+
# h - Grouping identity
|
66
|
+
# k - Compression
|
67
|
+
# m - Encryption
|
68
|
+
# n - Unsynchronisation
|
69
|
+
# p - Data length indicator
|
70
|
+
V4_HEADER_FLAGS_INDICATIONS = Array.new(16).tap do |array|
|
71
|
+
array[1] = :tag_alter_preservation
|
72
|
+
array[2] = :file_alter_preservation
|
73
|
+
array[3] = :read_only
|
74
|
+
array[9] = :grouping_identity
|
75
|
+
array[12] = :compression
|
76
|
+
array[13] = :encryption
|
77
|
+
array[14] = :unsynchronisation
|
78
|
+
array[15] = :data_length_indicator
|
79
|
+
end
|
80
|
+
|
81
|
+
attr_reader :name, :value
|
82
|
+
|
83
|
+
def initialize(file_io, version)
|
84
|
+
@file_io = file_io
|
85
|
+
@version = version
|
86
|
+
|
87
|
+
parse_frame_header
|
88
|
+
|
89
|
+
# In ID3v2.3 when frame is compressed using zlib
|
90
|
+
# with 4 bytes for 'decompressed size' appended to the frame header.
|
91
|
+
#
|
92
|
+
# In ID3v2.4 A 'Data Length Indicator' byte MUST be included in the frame
|
93
|
+
# when frame is compressed, and 'Data Length Indicator'represented as a 32 bit
|
94
|
+
# synchsafe integer
|
95
|
+
#
|
96
|
+
# So skip those 4 byte.
|
97
|
+
if compressed? || data_length_indicator?
|
98
|
+
@file_io.seek(4, IO::SEEK_CUR)
|
99
|
+
@size = @size - 4
|
100
|
+
end
|
101
|
+
|
102
|
+
parse_body
|
103
|
+
end
|
104
|
+
|
105
|
+
def valid?
|
106
|
+
@size > 0 && !@name.nil?
|
107
|
+
end
|
108
|
+
|
109
|
+
def compressed?
|
110
|
+
@flags.include? :compression
|
111
|
+
end
|
112
|
+
|
113
|
+
def data_length_indicator?
|
114
|
+
@flags.include? :data_length_indicator
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
private
|
119
|
+
# ID3v2.2 frame header structure:
|
120
|
+
#
|
121
|
+
# Frame ID $xx xx xx(tree characters)
|
122
|
+
# Size 3 * %xxxxxxxx
|
123
|
+
#
|
124
|
+
# ID3v2.3 frame header structure:
|
125
|
+
#
|
126
|
+
# Frame ID $xx xx xx xx (four characters)
|
127
|
+
# Size 4 * %xxxxxxxx
|
128
|
+
# Flags $xx xx
|
129
|
+
#
|
130
|
+
# ID3v2.4 frame header structure:
|
131
|
+
#
|
132
|
+
# Frame ID $xx xx xx xx (four characters)
|
133
|
+
# Size 4 * %0xxxxxxx
|
134
|
+
# Flags $xx xx
|
135
|
+
def parse_frame_header
|
136
|
+
header_size = @version == 2 ? 6 : 10
|
137
|
+
header_formate = @version == 2 ? 'A3B24' : 'A4B32B16'
|
138
|
+
id, size_bits, flags_bits = @file_io.read(header_size).unpack(header_formate)
|
139
|
+
|
140
|
+
@name = ID_MAPPING[id.to_sym]
|
141
|
+
@size = Helper.id3_size_caculate(size_bits, has_zero_bit: @version == 4)
|
142
|
+
@flags = parse_flags(flags_bits)
|
143
|
+
end
|
144
|
+
|
145
|
+
def parse_flags(flags_bits)
|
146
|
+
return [] if flags_bits.nil?
|
147
|
+
|
148
|
+
frame_flags_indications = @version == 4 ?
|
149
|
+
V4_HEADER_FLAGS_INDICATIONS :
|
150
|
+
V3_HEADER_FLAGS_INDICATIONS
|
151
|
+
|
152
|
+
flags_bits.split('').map.with_index do |flag_bit, index|
|
153
|
+
frame_flags_indications[index] if flag_bit == '1'
|
154
|
+
end.compact
|
155
|
+
end
|
156
|
+
|
157
|
+
def parse_body
|
158
|
+
return unless @size > 0
|
159
|
+
(@file_io.seek(@size, IO::SEEK_CUR); return) if @name.nil?
|
160
|
+
|
161
|
+
content = compressed? ? Zlib.inflate(@file_io.read(@size)) : @file_io.read(@size)
|
162
|
+
frame_body = frame_body_class.new(content, @version)
|
163
|
+
@value = frame_body.value
|
164
|
+
end
|
165
|
+
|
166
|
+
def frame_body_class
|
167
|
+
case @name
|
168
|
+
when :comment
|
169
|
+
CommentFrameBody
|
170
|
+
when :genre
|
171
|
+
GenreFrameBody
|
172
|
+
when :image
|
173
|
+
ImageFrameBody
|
174
|
+
else
|
175
|
+
TextFrameBody
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module ID3
|
5
|
+
class FrameBody
|
6
|
+
# Textual frames are marked with an encoding byte.
|
7
|
+
#
|
8
|
+
# $00 ISO-8859-1 [ISO-8859-1]. Terminated with $00.
|
9
|
+
# $01 UTF-16 [UTF-16] encoded Unicode [UNICODE] with BOM.
|
10
|
+
# $02 UTF-16BE [UTF-16] encoded Unicode [UNICODE] without BOM.
|
11
|
+
# $03 UTF-8 [UTF-8] encoded Unicode [UNICODE].
|
12
|
+
ENCODING_MAPPING = %w(ISO-8859-1 UTF-16 UTF-16BE UTF-8)
|
13
|
+
|
14
|
+
ENCODING_TERMINATOR_SIZE = {
|
15
|
+
'ISO-8859-1' => 1,
|
16
|
+
'UTF-16' => 2,
|
17
|
+
'UTF-16BE' => 2,
|
18
|
+
'UTF-8' => 1
|
19
|
+
}
|
20
|
+
|
21
|
+
|
22
|
+
attr_reader :value
|
23
|
+
|
24
|
+
def initialize(content, version)
|
25
|
+
@content = content
|
26
|
+
@version = version
|
27
|
+
|
28
|
+
parse
|
29
|
+
end
|
30
|
+
|
31
|
+
def parse
|
32
|
+
raise WahWahNotImplementedError, 'The parse method is not implemented'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module ID3
|
5
|
+
class GenreFrameBody < TextFrameBody
|
6
|
+
def parse
|
7
|
+
super
|
8
|
+
|
9
|
+
# If value is numeric value, or contain numeric value in parens
|
10
|
+
# can use as index for ID3v1 genre list
|
11
|
+
@value = ID3::V1::GENRES[$1.to_i] if @value =~ /^\((\d+)\)$/ || @value =~ /^(\d+)$/
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module ID3
|
5
|
+
class ImageFrameBody < FrameBody
|
6
|
+
TYPES = %i(
|
7
|
+
other
|
8
|
+
file_icon
|
9
|
+
other_file_icon
|
10
|
+
cover_front
|
11
|
+
cover_back
|
12
|
+
leaflet
|
13
|
+
media
|
14
|
+
lead_artist
|
15
|
+
artist
|
16
|
+
conductor
|
17
|
+
band
|
18
|
+
composer
|
19
|
+
lyricist
|
20
|
+
recording_location
|
21
|
+
during_recording
|
22
|
+
during_performance
|
23
|
+
movie_screen_capture
|
24
|
+
bright_coloured_fish
|
25
|
+
illustration
|
26
|
+
band_logotype
|
27
|
+
publisher_logotype
|
28
|
+
)
|
29
|
+
|
30
|
+
def mime_type
|
31
|
+
mime_type = @mime_type.downcase.yield_self { |type| type == 'jpg' ? 'jpeg' : type }
|
32
|
+
@version > 2 ? mime_type : "image/#{mime_type}"
|
33
|
+
end
|
34
|
+
|
35
|
+
# ID3v2.2 image frame structure:
|
36
|
+
#
|
37
|
+
# Text encoding $xx
|
38
|
+
# Image format $xx xx xx
|
39
|
+
# Picture type $xx
|
40
|
+
# Description <text string according to encoding> $00 (00)
|
41
|
+
# Picture data <binary data>
|
42
|
+
#
|
43
|
+
# ID3v2.3 and ID3v2.4 image frame structure:
|
44
|
+
#
|
45
|
+
# Text encoding $xx
|
46
|
+
# MIME type <text string> $00
|
47
|
+
# Picture type $xx
|
48
|
+
# Description <text string according to encoding> $00 (00)
|
49
|
+
# Picture data <binary data>
|
50
|
+
def parse
|
51
|
+
frame_format = @version > 2 ? 'CZ*Ca*' : 'Ca3Ca*'
|
52
|
+
encoding_id, @mime_type, type_index, reset_content = @content.unpack(frame_format)
|
53
|
+
encoding = ENCODING_MAPPING[encoding_id]
|
54
|
+
_description, data = Helper.split_with_terminator(reset_content, ENCODING_TERMINATOR_SIZE[encoding])
|
55
|
+
|
56
|
+
@value = { data: data, mime_type: mime_type, type: TYPES[type_index] }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module ID3
|
5
|
+
class TextFrameBody < FrameBody
|
6
|
+
# Text frame boby structure:
|
7
|
+
#
|
8
|
+
# Text encoding $xx
|
9
|
+
# Information <text string according to encoding>
|
10
|
+
def parse
|
11
|
+
encoding_id, text = @content.unpack('Ca*')
|
12
|
+
@value = Helper.encode_to_utf8(text, source_encoding: ENCODING_MAPPING[encoding_id])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|