wahwah 0.1.0 → 1.2.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/asf/object.rb +34 -0
- data/lib/wahwah/asf_tag.rb +221 -0
- data/lib/wahwah/errors.rb +7 -0
- data/lib/wahwah/flac/block.rb +51 -0
- data/lib/wahwah/flac/streaminfo_block.rb +51 -0
- data/lib/wahwah/flac_tag.rb +86 -0
- data/lib/wahwah/helper.rb +37 -0
- data/lib/wahwah/id3/comment_frame_body.rb +21 -0
- data/lib/wahwah/id3/frame.rb +176 -0
- data/lib/wahwah/id3/frame_body.rb +35 -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 +97 -0
- data/lib/wahwah/id3/v2.rb +67 -0
- data/lib/wahwah/id3/v2_header.rb +53 -0
- data/lib/wahwah/lazy_read.rb +40 -0
- data/lib/wahwah/mp3/mpeg_frame_header.rb +143 -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 +111 -0
- data/lib/wahwah/mp4/atom.rb +101 -0
- data/lib/wahwah/mp4_tag.rb +137 -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 +50 -0
- data/lib/wahwah/riff_tag.rb +142 -0
- data/lib/wahwah/tag.rb +71 -0
- data/lib/wahwah/tag_delegate.rb +16 -0
- data/lib/wahwah/version.rb +4 -2
- data/lib/wahwah.rb +78 -2
- metadata +109 -24
- 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,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module ID3
|
5
|
+
class Frame
|
6
|
+
prepend LazyRead
|
7
|
+
|
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
|
82
|
+
|
83
|
+
def initialize(version)
|
84
|
+
@version = version
|
85
|
+
|
86
|
+
parse_frame_header
|
87
|
+
|
88
|
+
# In ID3v2.3 when frame is compressed using zlib
|
89
|
+
# with 4 bytes for 'decompressed size' appended to the frame header.
|
90
|
+
#
|
91
|
+
# In ID3v2.4 A 'Data Length Indicator' byte MUST be included in the frame
|
92
|
+
# when frame is compressed, and 'Data Length Indicator'represented as a 32 bit
|
93
|
+
# synchsafe integer
|
94
|
+
#
|
95
|
+
# So skip those 4 byte.
|
96
|
+
if compressed? || data_length_indicator?
|
97
|
+
@file_io.seek(4, IO::SEEK_CUR)
|
98
|
+
@size -= 4
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def valid?
|
103
|
+
@size > 0 && !@name.nil?
|
104
|
+
end
|
105
|
+
|
106
|
+
def compressed?
|
107
|
+
@flags.include? :compression
|
108
|
+
end
|
109
|
+
|
110
|
+
def data_length_indicator?
|
111
|
+
@flags.include? :data_length_indicator
|
112
|
+
end
|
113
|
+
|
114
|
+
def value
|
115
|
+
return unless @size > 0
|
116
|
+
|
117
|
+
content = compressed? ? Zlib.inflate(data) : data
|
118
|
+
frame_body = frame_body_class.new(content, @version)
|
119
|
+
frame_body.value
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
# ID3v2.2 frame header structure:
|
125
|
+
#
|
126
|
+
# Frame ID $xx xx xx(tree characters)
|
127
|
+
# Size 3 * %xxxxxxxx
|
128
|
+
#
|
129
|
+
# ID3v2.3 frame header structure:
|
130
|
+
#
|
131
|
+
# Frame ID $xx xx xx xx (four characters)
|
132
|
+
# Size 4 * %xxxxxxxx
|
133
|
+
# Flags $xx xx
|
134
|
+
#
|
135
|
+
# ID3v2.4 frame header structure:
|
136
|
+
#
|
137
|
+
# Frame ID $xx xx xx xx (four characters)
|
138
|
+
# Size 4 * %0xxxxxxx
|
139
|
+
# Flags $xx xx
|
140
|
+
def parse_frame_header
|
141
|
+
header_size = @version == 2 ? 6 : 10
|
142
|
+
header_formate = @version == 2 ? "A3B24" : "A4B32B16"
|
143
|
+
id, size_bits, flags_bits = @file_io.read(header_size).unpack(header_formate)
|
144
|
+
|
145
|
+
@name = ID_MAPPING[id.to_sym]
|
146
|
+
@size = Helper.id3_size_caculate(size_bits, has_zero_bit: @version == 4)
|
147
|
+
@flags = parse_flags(flags_bits)
|
148
|
+
end
|
149
|
+
|
150
|
+
def parse_flags(flags_bits)
|
151
|
+
return [] if flags_bits.nil?
|
152
|
+
|
153
|
+
frame_flags_indications = @version == 4 ?
|
154
|
+
V4_HEADER_FLAGS_INDICATIONS :
|
155
|
+
V3_HEADER_FLAGS_INDICATIONS
|
156
|
+
|
157
|
+
flags_bits.chars.map.with_index do |flag_bit, index|
|
158
|
+
frame_flags_indications[index] if flag_bit == "1"
|
159
|
+
end.compact
|
160
|
+
end
|
161
|
+
|
162
|
+
def frame_body_class
|
163
|
+
case @name
|
164
|
+
when :comment
|
165
|
+
CommentFrameBody
|
166
|
+
when :genre
|
167
|
+
GenreFrameBody
|
168
|
+
when :image
|
169
|
+
ImageFrameBody
|
170
|
+
else
|
171
|
+
TextFrameBody
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,35 @@
|
|
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
|
+
attr_reader :value
|
22
|
+
|
23
|
+
def initialize(content, version)
|
24
|
+
@content = content
|
25
|
+
@version = version
|
26
|
+
|
27
|
+
parse
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse
|
31
|
+
raise WahWahNotImplementedError, "The parse method is not implemented"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
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
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module ID3
|
5
|
+
class V1 < Tag
|
6
|
+
TAG_SIZE = 128
|
7
|
+
TAG_ID = "TAG"
|
8
|
+
DEFAULT_ENCODING = "iso-8859-1"
|
9
|
+
GENRES = [
|
10
|
+
# Standard Genres
|
11
|
+
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge",
|
12
|
+
"Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop",
|
13
|
+
"R&B", "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative",
|
14
|
+
"Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop",
|
15
|
+
"Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental", "Acid",
|
16
|
+
"House", "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass",
|
17
|
+
"Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic",
|
18
|
+
"Gothic", "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream",
|
19
|
+
"Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk",
|
20
|
+
"Jungle", "Native American", "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes",
|
21
|
+
"Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro",
|
22
|
+
"Musical", "Rock & Roll", "Hard Rock",
|
23
|
+
|
24
|
+
# Winamp Extended Genres
|
25
|
+
"Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin",
|
26
|
+
"Revival", "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock",
|
27
|
+
"Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour",
|
28
|
+
"Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass",
|
29
|
+
"Primus", "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
|
30
|
+
"Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock",
|
31
|
+
"Drum Solo", "A capella", "Euro-House", "Dance Hall", "Goa", "Drum & Bass", "Club-House",
|
32
|
+
"Hardcore Techno", "Terror", "Indie", "BritPop", "Negerpunk", "Polsk Punk", "Beat",
|
33
|
+
"Christian Gangsta Rap", "Heavy Metal", "Black Metal", "Contemporary Christian", "Christian Rock",
|
34
|
+
|
35
|
+
# Added on WinAmp 1.91
|
36
|
+
"Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", "Synthpop",
|
37
|
+
|
38
|
+
# Added on WinAmp 5.6
|
39
|
+
"Abstract", "Art Rock", "Baroque", "Bhangra", "Big Beat", "Breakbeat", "Chillout",
|
40
|
+
"Downtempo", "Dub", "EBM", "Eclectic", "Electro", "Electroclash", "Emo",
|
41
|
+
"Experimental", "Garage", "Illbient", "Industro-Goth", "Jam Band", "Krautrock", "Leftfield",
|
42
|
+
"Lounge", "Math Rock", "New Romantic", "Nu-Breakz", "Post-Punk", "Post-Rock", "Psytrance",
|
43
|
+
"Shoegaze", "Space Rock", "Trop Rock", "World Music", "Neoclassical", "Audiobook", "Audio Theatre",
|
44
|
+
"Neue Deutsche Welle", "Podcast", "Indie Rock", "G-Funk", "Dubstep", "Garage Rock", "Psybient"
|
45
|
+
]
|
46
|
+
|
47
|
+
def size
|
48
|
+
TAG_SIZE
|
49
|
+
end
|
50
|
+
|
51
|
+
def version
|
52
|
+
"v1"
|
53
|
+
end
|
54
|
+
|
55
|
+
def valid?
|
56
|
+
@id == TAG_ID
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# For ID3v1 info, see here https://en.wikipedia.org/wiki/ID3#ID3v1
|
62
|
+
#
|
63
|
+
# header 3 "TAG"
|
64
|
+
# title 30 30 characters of the title
|
65
|
+
# artist 30 30 characters of the artist name
|
66
|
+
# album 30 30 characters of the album name
|
67
|
+
# year 4 A four-digit year
|
68
|
+
# comment 28 or 30 The comment.
|
69
|
+
# zero-byte 1 If a track number is stored, this byte contains a binary 0.
|
70
|
+
# track 1 The number of the track on the album, or 0. Invalid, if previous byte is not a binary 0.
|
71
|
+
# genre 1 Index in a list of genres, or 255
|
72
|
+
def parse
|
73
|
+
return unless @file_io.size >= TAG_SIZE
|
74
|
+
|
75
|
+
@file_io.seek(-TAG_SIZE, IO::SEEK_END)
|
76
|
+
@id = Helper.encode_to_utf8(@file_io.read(3), source_encoding: DEFAULT_ENCODING)
|
77
|
+
|
78
|
+
return unless valid?
|
79
|
+
|
80
|
+
@title = Helper.encode_to_utf8(@file_io.read(30), source_encoding: DEFAULT_ENCODING)
|
81
|
+
@artist = Helper.encode_to_utf8(@file_io.read(30), source_encoding: DEFAULT_ENCODING)
|
82
|
+
@album = Helper.encode_to_utf8(@file_io.read(30), source_encoding: DEFAULT_ENCODING)
|
83
|
+
@year = Helper.encode_to_utf8(@file_io.read(4), source_encoding: DEFAULT_ENCODING)
|
84
|
+
|
85
|
+
comment = @file_io.read(30)
|
86
|
+
|
87
|
+
if comment.getbyte(-2) == 0
|
88
|
+
@track = comment.getbyte(-1)
|
89
|
+
comment = comment.byteslice(0..-3)
|
90
|
+
end
|
91
|
+
|
92
|
+
@comments.push(Helper.encode_to_utf8(comment, source_encoding: DEFAULT_ENCODING))
|
93
|
+
@genre = GENRES[@file_io.getbyte] || ""
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module ID3
|
5
|
+
class V2 < Tag
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegators :@header, :major_version, :size, :has_extended_header?, :valid?
|
9
|
+
|
10
|
+
def version
|
11
|
+
"v2.#{major_version}"
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def parse
|
17
|
+
@file_io.rewind
|
18
|
+
@header = V2Header.new(@file_io)
|
19
|
+
|
20
|
+
return unless valid?
|
21
|
+
|
22
|
+
until end_of_tag?
|
23
|
+
frame = ID3::Frame.new(@file_io, major_version)
|
24
|
+
|
25
|
+
unless frame.valid?
|
26
|
+
frame.skip
|
27
|
+
next
|
28
|
+
end
|
29
|
+
|
30
|
+
update_attribute(frame)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def update_attribute(frame)
|
35
|
+
name = frame.name
|
36
|
+
|
37
|
+
case name
|
38
|
+
when :comment
|
39
|
+
# Because there may be more than one comment frame in each tag,
|
40
|
+
# so push it into a array.
|
41
|
+
@comments.push(frame.value)
|
42
|
+
when :image
|
43
|
+
# Because there may be more than one image frame in each tag,
|
44
|
+
# so push it into a array.
|
45
|
+
@images_data.push(frame)
|
46
|
+
frame.skip
|
47
|
+
when :track, :disc
|
48
|
+
# Track and disc value may be extended with a "/" character
|
49
|
+
# and a numeric string containing the total numer.
|
50
|
+
count, total_count = frame.value.split("/", 2)
|
51
|
+
instance_variable_set("@#{name}", count)
|
52
|
+
instance_variable_set("@#{name}_total", total_count) unless total_count.nil?
|
53
|
+
else
|
54
|
+
instance_variable_set("@#{name}", frame.value)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def end_of_tag?
|
59
|
+
size <= @file_io.pos || file_size <= @file_io.pos
|
60
|
+
end
|
61
|
+
|
62
|
+
def parse_image_data(image_frame)
|
63
|
+
image_frame.value
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module ID3
|
5
|
+
# The ID3v2 tag header, which should be the first information in the file,
|
6
|
+
# is 10 bytes as follows:
|
7
|
+
|
8
|
+
# ID3v2/file identifier "ID3"
|
9
|
+
# ID3v2 version $03 00
|
10
|
+
# ID3v2 flags %abc00000
|
11
|
+
# ID3v2 size 4 * %0xxxxxxx
|
12
|
+
class V2Header
|
13
|
+
TAG_ID = "ID3"
|
14
|
+
HEADER_SIZE = 10
|
15
|
+
HEADER_FORMAT = "A3CxB8B*"
|
16
|
+
|
17
|
+
attr_reader :major_version, :size
|
18
|
+
|
19
|
+
def initialize(file_io)
|
20
|
+
header_content = file_io.read(HEADER_SIZE)
|
21
|
+
@id, @major_version, @flags, size_bits = header_content.unpack(HEADER_FORMAT) if header_content.size >= HEADER_SIZE
|
22
|
+
|
23
|
+
return unless valid?
|
24
|
+
|
25
|
+
# Tag size is the size excluding the header size,
|
26
|
+
# so add header size back to get total size.
|
27
|
+
@size = Helper.id3_size_caculate(size_bits) + HEADER_SIZE
|
28
|
+
|
29
|
+
if has_extended_header?
|
30
|
+
# Extended header structure:
|
31
|
+
#
|
32
|
+
# Extended header size $xx xx xx xx
|
33
|
+
# Extended Flags $xx xx
|
34
|
+
# Size of padding $xx xx xx xx
|
35
|
+
|
36
|
+
# Skip extended_header
|
37
|
+
extended_header_size = Helper.id3_size_caculate(file_io.read(4).unpack1("B32"))
|
38
|
+
file_io.seek(extended_header_size - 4, IO::SEEK_CUR)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def valid?
|
43
|
+
@id == TAG_ID
|
44
|
+
end
|
45
|
+
|
46
|
+
# The second bit in flags byte indicates whether or not the header
|
47
|
+
# is followed by an extended header.
|
48
|
+
def has_extended_header?
|
49
|
+
@flags[1] == "1"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module LazyRead
|
5
|
+
def self.prepended(base)
|
6
|
+
base.class_eval do
|
7
|
+
attr_reader :size
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(file_io, *arg)
|
12
|
+
@file_io = file_io
|
13
|
+
super(*arg)
|
14
|
+
@position = @file_io.pos
|
15
|
+
@data = get_data if @file_io.is_a?(StringIO)
|
16
|
+
end
|
17
|
+
|
18
|
+
def data
|
19
|
+
if @file_io.closed? && @file_io.is_a?(File)
|
20
|
+
@file_io = File.open(@file_io.path)
|
21
|
+
@data = get_data
|
22
|
+
@file_io.close
|
23
|
+
end
|
24
|
+
|
25
|
+
@data ||= get_data
|
26
|
+
end
|
27
|
+
|
28
|
+
def skip
|
29
|
+
@file_io.seek(@position)
|
30
|
+
@file_io.seek(size, IO::SEEK_CUR)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def get_data
|
36
|
+
@file_io.seek(@position)
|
37
|
+
@file_io.read(size)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|