wahwah 0.1.0 → 1.2.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/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
|