wahwah 0.1.0.pre.test

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