id3 0.5.0 → 1.0.0.pre4

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.
@@ -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