wahwah 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|