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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/lib/wahwah.rb +74 -3
  4. data/lib/wahwah/asf/object.rb +39 -0
  5. data/lib/wahwah/asf_tag.rb +220 -0
  6. data/lib/wahwah/errors.rb +6 -0
  7. data/lib/wahwah/flac/block.rb +57 -0
  8. data/lib/wahwah/flac/streaminfo_block.rb +51 -0
  9. data/lib/wahwah/flac_tag.rb +84 -0
  10. data/lib/wahwah/helper.rb +37 -0
  11. data/lib/wahwah/id3/comment_frame_body.rb +21 -0
  12. data/lib/wahwah/id3/frame.rb +180 -0
  13. data/lib/wahwah/id3/frame_body.rb +36 -0
  14. data/lib/wahwah/id3/genre_frame_body.rb +15 -0
  15. data/lib/wahwah/id3/image_frame_body.rb +60 -0
  16. data/lib/wahwah/id3/text_frame_body.rb +16 -0
  17. data/lib/wahwah/id3/v1.rb +96 -0
  18. data/lib/wahwah/id3/v2.rb +60 -0
  19. data/lib/wahwah/id3/v2_header.rb +53 -0
  20. data/lib/wahwah/mp3/mpeg_frame_header.rb +141 -0
  21. data/lib/wahwah/mp3/vbri_header.rb +47 -0
  22. data/lib/wahwah/mp3/xing_header.rb +45 -0
  23. data/lib/wahwah/mp3_tag.rb +110 -0
  24. data/lib/wahwah/mp4/atom.rb +105 -0
  25. data/lib/wahwah/mp4_tag.rb +126 -0
  26. data/lib/wahwah/ogg/flac_tag.rb +37 -0
  27. data/lib/wahwah/ogg/opus_tag.rb +33 -0
  28. data/lib/wahwah/ogg/packets.rb +41 -0
  29. data/lib/wahwah/ogg/page.rb +121 -0
  30. data/lib/wahwah/ogg/pages.rb +24 -0
  31. data/lib/wahwah/ogg/vorbis_comment.rb +51 -0
  32. data/lib/wahwah/ogg/vorbis_tag.rb +35 -0
  33. data/lib/wahwah/ogg_tag.rb +66 -0
  34. data/lib/wahwah/riff/chunk.rb +54 -0
  35. data/lib/wahwah/riff_tag.rb +140 -0
  36. data/lib/wahwah/tag.rb +59 -0
  37. data/lib/wahwah/tag_delegate.rb +16 -0
  38. data/lib/wahwah/version.rb +4 -2
  39. metadata +94 -23
  40. data/.gitignore +0 -8
  41. data/.travis.yml +0 -5
  42. data/Gemfile +0 -6
  43. data/Gemfile.lock +0 -22
  44. data/README.md +0 -35
  45. data/Rakefile +0 -10
  46. data/bin/console +0 -14
  47. data/bin/setup +0 -8
  48. 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