id3 0.5.0 → 1.0.0.pre4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,127 @@
1
+ # ----------------------------------------------------------------------------
2
+ # Module ID3 - MODULE METHODS
3
+ # ----------------------------------------------------------------------------
4
+ module ID3
5
+
6
+ # The ID3 module methods are to query or modify files directly by filename.
7
+ # They check directly if a file has a ID3-tag, but they don't parse the tags!
8
+
9
+ # ----------------------------------------------------------------------------
10
+ # id3_versions
11
+
12
+ def ID3.id3_versions
13
+ [ hasID3v1tag?(filename) ,hasID3v2tag?(filename) ].compact # returns Array of ID3 tag versions found
14
+ end
15
+
16
+ # ----------------------------------------------------------------------------
17
+ # hasID3v1tag?
18
+ # returns string with version 1.0 or 1.1 if tag was found
19
+ # returns false otherwise
20
+
21
+ def ID3.hasID3v1tag?(filename)
22
+ hasID3v1tag = false
23
+
24
+ # be careful with empty or corrupt files..
25
+ return false if File.size(filename) < ID3v1tagSize
26
+
27
+ f = File.open(filename, 'rb:binary')
28
+ f.seek(-ID3v1tagSize, IO::SEEK_END)
29
+ if (f.read(3) == "TAG")
30
+ f.seek(-ID3v1tagSize + ID3v1versionbyte, IO::SEEK_END)
31
+ c = f.get_byte # this is character 125 of the tag
32
+ if (c == 0)
33
+ hasID3v1tag = "1.0"
34
+ else
35
+ hasID3v1tag = "1.1"
36
+ end
37
+ end
38
+ f.close
39
+ return hasID3v1tag
40
+ end
41
+
42
+ # ----------------------------------------------------------------------------
43
+ # hasID3v2tag?
44
+ # returns string with version 2.2.0, 2.3.0 or 2.4.0 if tag found
45
+ # returns false otherwise
46
+
47
+ def ID3.hasID3v2tag?(filename)
48
+ hasID3v2tag = false
49
+
50
+ f = File.open(filename, 'rb:binary')
51
+ if (f.read(3) == "ID3")
52
+ major = f.get_byte
53
+ minor = f.get_byte
54
+ version = "2." + major.to_s + '.' + minor.to_s
55
+ hasID3v2tag = version
56
+ end
57
+ f.close
58
+ return hasID3v2tag
59
+ end
60
+
61
+ # ----------------------------------------------------------------------------
62
+ # hasID3tag?
63
+ # returns string with all versions found, space separated
64
+ # returns false otherwise
65
+
66
+ def ID3.hasID3tag?(filename)
67
+ v1 = ID3.hasID3v1tag?(filename)
68
+ v2 = ID3.hasID3v2tag?(filename)
69
+
70
+ return false if !v1 && !v2
71
+ return v1 if !v2
72
+ return v2 if !v1
73
+ return "#{v1} #{v2}"
74
+ end
75
+
76
+ # ----------------------------------------------------------------------------
77
+ # removeID3v1tag
78
+ # returns nil if no v1 tag was found, or it couldn't be removed
79
+ # returns true if v1 tag found and it was removed..
80
+ #
81
+ # in the future:
82
+ # returns ID3.Tag1 object if a v1 tag was found and removed
83
+
84
+ def ID3.removeID3v1tag(filename)
85
+ stat = File.stat(filename)
86
+ if stat.file? && stat.writable? && ID3.hasID3v1tag?(filename)
87
+
88
+ # CAREFUL: this does not check if there really is a valid tag,
89
+ # that's why we need to check above!!
90
+
91
+ newsize = stat.size - ID3v1tagSize
92
+ File.open(filename, "r+") { |f| f.truncate(newsize) }
93
+
94
+ return true
95
+ else
96
+ return nil
97
+ end
98
+ end
99
+
100
+ # ----------------------------------------------------------------------
101
+ # convert the 4 bytes found in the id3v2 header and return the size
102
+ def ID3.unmungeSize(bytes)
103
+ size = 0
104
+ j = 0; i = 3
105
+ while i >= 0
106
+ size += 128**i * (bytes.getbyte(j) & 0x7f)
107
+ j += 1
108
+ i -= 1
109
+ end
110
+ return size
111
+ end
112
+ # ----------------------------------------------------------------------
113
+ # convert the size into 4 bytes to be written into an id3v2 header
114
+ def ID3.mungeSize(size)
115
+ bytes = Array.new(4,0)
116
+ j = 0; i = 3
117
+ while i >= 0
118
+ bytes[j],size = size.divmod(128**i)
119
+ j += 1
120
+ i -= 1
121
+ end
122
+ return bytes
123
+ end
124
+
125
+ end
126
+ # ----------------------------------------------------------------------------
127
+
@@ -0,0 +1,40 @@
1
+ #
2
+ # EXTENSIONS to Class String
3
+ #
4
+ # if you have a (partial) MP3-file stored in a String.. you can check if it contains ID3 tags
5
+
6
+ class String
7
+ # str = File.open(filename, 'rb:binary').read; 1
8
+ # str.hasID3v2tag?
9
+ # str.hasID3v1tag?
10
+
11
+ def id3_versions
12
+ [ hasID3v1tag? ,hasID3v2tag? ].compact # returns an Array of version numbers
13
+ end
14
+
15
+ def hasID3tag?
16
+ hasID3v2tag? || hasID3v1tag? ? true : false # returns true or false
17
+ end
18
+
19
+ def hasID3v2tag? # returns either nil or the version number -- this can be used in a boolean comparison
20
+ return nil if self !~ /^ID3/
21
+ major = self.getbyte(ID3::ID3v2major)
22
+ minor = self.getbyte(ID3::ID3v2minor)
23
+ version = "2." + major.to_s + '.' + minor.to_s
24
+ end
25
+
26
+ # we also need a method to return the size of the ID3v2 tag ,
27
+ # e.g. needed when we need to determine the buffersize to read the tag from a file or from a remote location
28
+ def ID3v2_tag_size
29
+ return 0 if self !~ /^ID3/
30
+ return ID3::ID3v2headerSize + ID3.unmungeSize( self[ID3::ID3v2tagSize..ID3::ID3v2tagSize+4] )
31
+ end
32
+
33
+ def hasID3v1tag? # returns either nil or the version number -- this can be used in a boolean comparison
34
+ return nil if size < ID3::ID3v1tagSize # if the String is too small to contain a tag
35
+ size = self.bytesize
36
+ tag = self[size-128,size] # get the last 128 bytes
37
+ return nil if tag !~/^TAG/
38
+ return tag[ID3::ID3v1versionbyte] == ZEROBYTE ? "1.0" : "1.1" # return version number otherwise
39
+ end
40
+ end
@@ -0,0 +1,131 @@
1
+ module ID3
2
+
3
+ # ==============================================================================
4
+ # Class Tag1 ID3 Version 1.x Tag
5
+ #
6
+ # parses ID3v1 tags from a binary array
7
+ # dumps ID3v1 tags into a binary array
8
+ # allows to modify tag's contents
9
+
10
+ class Tag1 < GenericTag
11
+
12
+ def initialize
13
+ super
14
+ @version = '1.1'
15
+ end
16
+
17
+ # ----------------------------------------------------------------------
18
+ # read reads a version 1.x ID3tag
19
+ #
20
+
21
+ def read(filename)
22
+ f = File.open(filename, 'r')
23
+ f.seek(-ID3::ID3v1tagSize, IO::SEEK_END)
24
+ hastag = (f.read(3) == 'TAG')
25
+ if hastag
26
+ f.seek(-ID3::ID3v1tagSize, IO::SEEK_END)
27
+ @raw = f.read(ID3::ID3v1tagSize)
28
+
29
+ # self.parse!(raw) # we should use "parse!" instead of duplicating code!
30
+
31
+ if (raw.getbyte(ID3v1versionbyte) == 0)
32
+ @version = "1.0"
33
+ else
34
+ @version = "1.1"
35
+ end
36
+ else
37
+ @raw = @version = nil
38
+ end
39
+ f.close
40
+ #
41
+ # now parse all the fields
42
+
43
+ ID3::SUPPORTED_SYMBOLS[@version].each{ |key,val|
44
+ if val.class == Range
45
+ # self[key] = @raw[val].squeeze(" \000").chomp(" ").chomp("\000")
46
+ self[key] = @raw[val].strip
47
+ elsif val.class == Fixnum
48
+ self[key] = @raw.getbyte(val).to_s
49
+ else
50
+ # this can't happen the way we defined the hash..
51
+ # printf "unknown key/val : #{key} / #{val} ; val-type: %s\n", val.type
52
+ end
53
+ }
54
+ hastag
55
+ end
56
+ # ----------------------------------------------------------------------
57
+ # write writes a version 1.x ID3tag
58
+ #
59
+ # not implemented yet..
60
+ #
61
+ # need to loacte old tag, and remove it, then append new tag..
62
+ #
63
+ # always upgrade version 1.0 to 1.1 when writing
64
+
65
+ # not yet implemented, because AudioFile.write does the job better
66
+
67
+ # ----------------------------------------------------------------------
68
+ # this routine modifies self, e.g. the Tag1 object
69
+ #
70
+ # tag.parse!(raw) returns boolean value, showing if parsing was successful
71
+
72
+ def parse!(raw)
73
+
74
+ return false if raw.size != ID3::ID3v1tagSize
75
+
76
+ if (raw[ID3v1versionbyte] == 0)
77
+ @version = "1.0"
78
+ else
79
+ @version = "1.1"
80
+ end
81
+
82
+ self.clear # remove all entries from Hash, we don't want left-overs..
83
+
84
+ ID3::SUPPORTED_SYMBOLS[@version].each{ |key,val|
85
+ if val.class == Range
86
+ # self[key] = raw[val].squeeze(" \000").chomp(" ").chomp("\000")
87
+ self[key] = raw[val].strip
88
+ elsif val.class == Fixnum
89
+ self[key] = raw[val].to_s
90
+ else
91
+ # this can't happen the way we defined the hash..
92
+ # printf "unknown key/val : #{key} / #{val} ; val-type: %s\n", val.class
93
+ end
94
+ }
95
+ @raw = raw
96
+ return true
97
+ end
98
+ # ----------------------------------------------------------------------
99
+ # dump version 1.1 ID3 Tag into a binary array
100
+ #
101
+ # although we provide this method, it's stongly discouraged to use it,
102
+ # because ID3 version 1.x tags are inferior to version 2.x tags, as entries
103
+ # are often truncated and hence ID3 v1 tags are often useless..
104
+
105
+ def dump
106
+ zeroes = ZEROBYTE * 32
107
+ raw = ZEROBYTE * ID3::ID3v1tagSize
108
+ raw[0..2] = 'TAG'
109
+
110
+ self.each{ |key,value|
111
+
112
+ range = ID3::Symbol2framename['1.1'][key]
113
+
114
+ if range.class == Range
115
+ length = range.last - range.first + 1
116
+ paddedstring = value + zeroes
117
+ raw[range] = paddedstring[0..length-1]
118
+ elsif range.class == Fixnum
119
+ raw[range] = value.to_i.chr # supposedly assigning a binary integer value to the location in the string
120
+ else
121
+ # this can't happen the way we defined the hash..
122
+ next
123
+ end
124
+ }
125
+
126
+ return raw
127
+ end
128
+ # ----------------------------------------------------------------------
129
+ end # of class Tag1
130
+
131
+ end
@@ -0,0 +1,261 @@
1
+ module ID3
2
+
3
+ # ==============================================================================
4
+ # Class Tag2 ID3 Version 2.x.y Tag
5
+ #
6
+ # parses ID3v2 tags from a binary array
7
+ # dumps ID3v2 tags into a binary array
8
+ # allows to modify tag's contents
9
+ #
10
+ # as per definition, the frames are in no fixed order
11
+
12
+ class Tag2 < GenericTag
13
+ attr_reader :rawflags, :flags
14
+
15
+ def initialize
16
+ super
17
+ @rawflags = 0
18
+ @flags = {}
19
+ @version = '2.3.0' # default version
20
+ end
21
+
22
+ # this is obviously half-baked.. does not really work! We need SubClasses of ID3::Frame with specific handling
23
+ #
24
+ def []=(framename,val)
25
+ # if this is a valid frame of known type, we return it's total length and a struct
26
+ #
27
+ if ID3::SUPPORTED_SYMBOLS[@version].has_value?(framename)
28
+ frame = ID3::Frame.new( framename, @version)
29
+ self[ framename ] = frame
30
+ frame['text'] = val if frame.has_key?('text')
31
+ return frame
32
+ else
33
+ return nil
34
+ end
35
+ end
36
+
37
+ def read_from_buffer(string)
38
+ has_tag = string =~ /^ID3/
39
+ if has_tag
40
+ major = string.getbyte(ID3::ID3v2major)
41
+ minor = string.getbyte(ID3::ID3v2minor)
42
+ @version = "2." + major.to_s + '.' + minor.to_s
43
+ @rawflags = string.getbyte(ID3::ID3v2flags)
44
+ size = ID3::ID3v2headerSize + ID3.unmungeSize( string[ID3::ID3v2tagSize..ID3::ID3v2tagSize+4] )
45
+ return false if string.size < size
46
+ @raw = string[0...size]
47
+ # parse the raw flags:
48
+ if (@rawflags & ID3::TAG_HEADER_FLAG_MASK[@version] != 0)
49
+ # in this case we need to skip parsing the frame... and skip to the next one...
50
+ wrong = @rawflags & ID3::TAG_HEADER_FLAG_MASK[@version]
51
+ error = printf "ID3 version %s header flags 0x%X contain invalid flags 0x%X !\n", @version, @rawflags, wrong
52
+ raise ArgumentError, error
53
+ end
54
+
55
+ @flags = Hash.new
56
+
57
+ ID3::TAG_HEADER_FLAGS[@version].each{ |key,val|
58
+ # only define the flags which are set..
59
+ @flags[key] = true if (@rawflags & val == 1)
60
+ }
61
+ else
62
+ @raw = nil
63
+ @version = nil
64
+ return false
65
+ end
66
+ #
67
+ # now parse all the frames
68
+ #
69
+ i = ID3::ID3v2headerSize; # we start parsing right after the ID3v2 header
70
+
71
+ while (i < @raw.size) && (@raw.getbyte(i) != 0)
72
+ len,frame = parse_frame_header(i) # this will create the correct frame
73
+ if len != 0
74
+ i += len
75
+ else
76
+ break
77
+ end
78
+ end
79
+
80
+ has_tag
81
+ end
82
+
83
+ def read_from_file(filename)
84
+ f = File.open(filename, 'rb:BINARY')
85
+ has_tag = (f.read(3) == "ID3")
86
+ if has_tag
87
+ major = f.get_byte
88
+ minor = f.get_byte
89
+ @version = "2." + major.to_s + '.' + minor.to_s
90
+ @rawflags = f.get_byte
91
+ size = ID3::ID3v2headerSize + unmungeSize(f.read(4)) # was read_bytes, which was a BUG!!
92
+ f.seek(0)
93
+ @raw = f.read(size)
94
+
95
+ # parse the raw flags:
96
+ if (@rawflags & ID3::TAG_HEADER_FLAG_MASK[@version] != 0)
97
+ # in this case we need to skip parsing the frame... and skip to the next one...
98
+ wrong = @rawflags & ID3::TAG_HEADER_FLAG_MASK[@version]
99
+ error = printf "ID3 version %s header flags 0x%X contain invalid flags 0x%X !\n", @version, @rawflags, wrong
100
+ raise ArgumentError, error
101
+ end
102
+
103
+ @flags = Hash.new
104
+
105
+ ID3::TAG_HEADER_FLAGS[@version].each{ |key,val|
106
+ # only define the flags which are set..
107
+ @flags[key] = true if (@rawflags & val == 1)
108
+ }
109
+ else
110
+ @raw = nil
111
+ @version = nil
112
+ return false
113
+ end
114
+ f.close
115
+ #
116
+ # now parse all the frames
117
+ #
118
+ i = ID3::ID3v2headerSize; # we start parsing right after the ID3v2 header
119
+
120
+ while (i < @raw.size) && (@raw.getbyte(i) != 0)
121
+ len,frame = parse_frame_header(i) # this will create the correct frame
122
+ if len != 0
123
+ i += len
124
+ else
125
+ break
126
+ end
127
+ end
128
+ has_tag
129
+ end
130
+ alias read read_from_file
131
+
132
+ # ----------------------------------------------------------------------
133
+ # write
134
+ #
135
+ # writes and replaces existing ID3-v2-tag if one is present
136
+ # Careful, this does NOT merge or append, it overwrites!
137
+
138
+ # not yet implemented, because AudioFile.write does the job better
139
+
140
+ # def write(filename)
141
+ # check how long the old ID3-v2 tag is
142
+
143
+ # dump ID3-v2-tag
144
+
145
+ # append old audio to new tag
146
+
147
+ # end
148
+
149
+ # ----------------------------------------------------------------------------
150
+ # writeID3v2
151
+ # just writes the ID3v2 tag by itself into a file, no audio data is written
152
+ #
153
+ # for backing up ID3v2 tags and debugging only..
154
+ #
155
+
156
+ # def writeID3v2
157
+
158
+ # end
159
+
160
+ # ----------------------------------------------------------------------
161
+ # parse_frame_header
162
+ #
163
+ # each frame consists of a header of fixed length;
164
+ # depending on the ID3version, either 6 or 10 bytes.
165
+ # and of a data portion which is of variable length,
166
+ # and which contents might not be parsable by us
167
+ #
168
+ # INPUT: index to where in the @raw data the frame starts
169
+ # RETURNS: if successful parse:
170
+ # total size in bytes, ID3frame struct
171
+ # else:
172
+ # 0, nil
173
+ #
174
+ #
175
+ # Struct of type ID3frame which contains:
176
+ # the name, size (in bytes), headerX,
177
+ # dataStartX, dataEndX, flags
178
+ # the data indices point into the @raw data, so we can cut out
179
+ # and parse the data at a later point in time.
180
+ #
181
+ # total frame size = dataEndX - headerX
182
+ # total header size= dataStartX - headerX
183
+ # total data size = dataEndX - dataStartX
184
+ #
185
+ private
186
+ def parse_frame_header(x)
187
+ framename = ""; flags = nil
188
+ size = 0
189
+
190
+ if @version =~ /^2\.2\./
191
+ frameHeaderSize = 6 # 2.2.x Header Size is 6 bytes
192
+ header = @raw[x..x+frameHeaderSize-1]
193
+
194
+ framename = header[0..2]
195
+ size = (header.getbyte(3)*256**2)+(header.getbyte(4)*256)+header.getbyte(5)
196
+ flags = nil
197
+ # printf "frame: %s , size: %d\n", framename , size
198
+
199
+ elsif @version =~ /^2\.[34]\./
200
+ # for version 2.3.0 and 2.4.0 the header is 10 bytes long
201
+ frameHeaderSize = 10
202
+ header = @raw[x..x+frameHeaderSize-1]
203
+
204
+ # puts @raw.inspect
205
+
206
+ framename = header[0..3]
207
+ size = (header.getbyte(4)*256**3)+(header.getbyte(5)*256**2)+(header.getbyte(6)*256)+header.getbyte(7)
208
+ flags= header[8..9]
209
+ # printf "frame: %s , size: %d, flags: %s\n", framename , size, flags
210
+
211
+ else
212
+ # we can't parse higher versions
213
+ return 0, false
214
+ end
215
+
216
+ # if this is a valid frame of known type, we return it's total length and a struct
217
+ #
218
+ if ID3::SUPPORTED_SYMBOLS[@version].has_value?(framename)
219
+ frame = ID3::Frame.new( framename, @version , flags, self, x, x+frameHeaderSize , x+frameHeaderSize + size - 1 )
220
+ self[ ID3::Framename2symbol[@version][frame.name] ] = frame
221
+ return size+frameHeaderSize , frame
222
+ else
223
+ return 0, nil
224
+ end
225
+ end
226
+ # ----------------------------------------------------------------------
227
+ # dump a ID3-v2 tag into a binary array
228
+ #
229
+ # NOTE:
230
+ # when "dumping" an ID3-v2 tag, I would like to have more control about
231
+ # which frames get dumped first.. e.g. the most important frames (with the
232
+ # most important information) should be dumped first..
233
+ #
234
+
235
+ public
236
+ def dump
237
+ data = ""
238
+
239
+ # dump all the frames
240
+ self.each { |framename,framedata|
241
+ data << framedata.dump
242
+ }
243
+ # add some padding perhaps 32 bytes (should be defined by the user!)
244
+ # NOTE: I noticed that iTunes adds excessive amounts of padding
245
+ data << ZEROBYTE * 32
246
+
247
+ # calculate the complete length of the data-section
248
+ size = mungeSize(data.size)
249
+
250
+ major,minor = @version.sub(/^2\.([0-9])\.([0-9])/, '\1 \2').split
251
+
252
+ # prepend a valid ID3-v2.x header to the data block
253
+ header = "ID3" << major.to_i << minor.to_i << @rawflags << size[0] << size[1] << size[2] << size[3]
254
+
255
+ header + data
256
+ end
257
+ # ----------------------------------------------------------------------
258
+
259
+ end # of class Tag2
260
+
261
+ end