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.
- data/CHANGES +14 -0
- data/LICENSE.html +8 -1
- data/README.md +39 -0
- data/docs/ID3_comparison.html +10 -2
- data/docs/ID3_comparison2.html +29 -21
- data/docs/ID3v2_frames_overview.txt +172 -35
- data/{lib → docs}/hexdump.rb +0 -0
- data/docs/index.html +29 -9
- data/lib/helpers/hash_extensions.rb +20 -0
- data/lib/helpers/hexdump.rb +136 -0
- data/lib/helpers/invert_hash.rb +128 -0
- data/lib/helpers/recursive_helper.rb +39 -0
- data/lib/helpers/restricted_ordered_hash.rb +88 -0
- data/lib/helpers/ruby_1.8_1.9_compatibility.rb +62 -0
- data/lib/id3.rb +23 -1252
- data/lib/id3/audiofile.rb +261 -0
- data/lib/id3/constants.rb +292 -0
- data/lib/id3/frame.rb +178 -0
- data/lib/id3/frame_array.rb +19 -0
- data/lib/id3/generic_tag.rb +73 -0
- data/lib/id3/id3.rb +159 -0
- data/lib/id3/io_extensions.rb +44 -0
- data/lib/id3/module_methods.rb +127 -0
- data/lib/id3/string_extensions.rb +40 -0
- data/lib/id3/tag1.rb +131 -0
- data/lib/id3/tag2.rb +261 -0
- metadata +87 -58
- data/README +0 -18
- data/docs/ID3v2_frames_comparison.txt +0 -197
- data/lib/invert_hash.rb +0 -105
@@ -0,0 +1,261 @@
|
|
1
|
+
# ==============================================================================
|
2
|
+
# Class AudioFile may call this ID3File
|
3
|
+
#
|
4
|
+
# reads and parses audio files for tags
|
5
|
+
# writes audio files and attaches dumped tags to it..
|
6
|
+
# revert feature would be nice to have..
|
7
|
+
#
|
8
|
+
# If we query and AudioFile object, we query what's currently associated with it
|
9
|
+
# e.g. we're not querying the file itself, but the Tag object which is perhaps modified.
|
10
|
+
# To query the file itself, use the ID3 module functions
|
11
|
+
#
|
12
|
+
# By default the audio portion of the file is not(!) read - to reduce memory footprint - the audioportion could be very long!
|
13
|
+
#
|
14
|
+
# BUG: (1) : when a id3v2 frame is deleted from a tag, e.g. 'PICTURE', then the raw tag is not updated
|
15
|
+
# BUG: (2) : when a AudioFile is written to file, the raw tag is not updated to reflect the new raw tag value
|
16
|
+
# BUG: (3) : FIXED. when a AufioFile is written, the order of frames is not the same as in the original file.. fixed using OrderedHash
|
17
|
+
# BUG: (5) : when a FrameType is set for a ID3v2 tag, e.g. 'TITLE', the underlying attributes are not automatically pre-filled..
|
18
|
+
|
19
|
+
class AudioFile
|
20
|
+
attr_reader :audioStartX , :audioEndX # begin and end indices of audio data in file
|
21
|
+
|
22
|
+
attr_reader :pwd, :filename # PWD and relative path/name how file was first referenced
|
23
|
+
attr_reader :dirname, :basename # absolute dirname and basename of the file (computed)
|
24
|
+
|
25
|
+
attr_accessor :tagID3v1, :tagID3v2 # should make aliases id3v1_tag , id3v2_tag
|
26
|
+
|
27
|
+
# ----------------------------------------------------------------------------
|
28
|
+
# initialize
|
29
|
+
#
|
30
|
+
# AudioFile.new does NOT keep the file open, but scans it and parses the info
|
31
|
+
# e.g.: ID3::AudioFile.new('mp3/a.mp3')
|
32
|
+
|
33
|
+
# this should take two parameters, either Filename or String, and an options hash, e.g. {:read_audio => false}
|
34
|
+
|
35
|
+
def initialize( filename )
|
36
|
+
@filename = filename # similar to path method from class File, which is a mis-nomer!
|
37
|
+
@pwd = ENV["PWD"]
|
38
|
+
@dirname = File.dirname( filename )
|
39
|
+
@basename = File.basename( filename )
|
40
|
+
|
41
|
+
@tagID3v1 = nil
|
42
|
+
@tagID3v2 = nil
|
43
|
+
|
44
|
+
@audio = nil # this doesn't get initialized with the actual audio during new(), so we don't waste memory
|
45
|
+
|
46
|
+
audioStartX = 0
|
47
|
+
audioEndX = File.size(filename) - 1 # points to the last index
|
48
|
+
|
49
|
+
if ID3.hasID3v1tag?(@filename)
|
50
|
+
@tagID3v1 = ID3::Tag1.new
|
51
|
+
@tagID3v1.read(@filename)
|
52
|
+
|
53
|
+
audioEndX -= ID3::ID3v1tagSize
|
54
|
+
end
|
55
|
+
if ID3.hasID3v2tag?(@filename)
|
56
|
+
@tagID3v2 = ID3::Tag2.new
|
57
|
+
@tagID3v2.read(@filename)
|
58
|
+
|
59
|
+
audioStartX = @tagID3v2.raw.size
|
60
|
+
end
|
61
|
+
|
62
|
+
# audioStartX audioEndX indices into the file need to be set
|
63
|
+
@audioStartX = audioStartX # first byte of audio data
|
64
|
+
@audioEndX = audioEndX # last byte of audio data
|
65
|
+
|
66
|
+
# user may compute the MD5sum of the audio content later..
|
67
|
+
# but we're only doing this if the user requests it..
|
68
|
+
# because MD5sum computation takes a little bit time.
|
69
|
+
|
70
|
+
@audioMD5sum = nil
|
71
|
+
@audioSHA1sum = nil
|
72
|
+
end
|
73
|
+
|
74
|
+
# ----------------------------------------------------------------------------
|
75
|
+
# version aka versions
|
76
|
+
# queries the tag objects and returns the version numbers of those tags
|
77
|
+
# NOTE: this does not reflect what's currently in the file, but what's
|
78
|
+
# currently in the AudioFile object
|
79
|
+
|
80
|
+
def id3_versions # returns Array of ID3 tag versions found
|
81
|
+
a = Array.new
|
82
|
+
a.push(@tagID3v1.version) if @tagID3v1
|
83
|
+
a.push(@tagID3v2.version) if @tagID3v2
|
84
|
+
return a
|
85
|
+
end
|
86
|
+
alias versions id3_versions
|
87
|
+
alias version id3_versions
|
88
|
+
# ----------------------------------------------------------------------------
|
89
|
+
def has_id3v1tag?
|
90
|
+
return @tagID3v1
|
91
|
+
end
|
92
|
+
# ----------------------------------------------------------------------------
|
93
|
+
def has_id3v2tag?
|
94
|
+
return @tagID3v2
|
95
|
+
end
|
96
|
+
# ----------------------------------------------------------------------------
|
97
|
+
def audioLength
|
98
|
+
@audioEndX - @audioStartX + 1
|
99
|
+
end
|
100
|
+
# ----------------------------------------------------------------------------
|
101
|
+
# write
|
102
|
+
# write the AudioFile to file, including any ID3-tags
|
103
|
+
# We keep backups if we write to a specific filename
|
104
|
+
|
105
|
+
def write(*filename)
|
106
|
+
backups = false
|
107
|
+
|
108
|
+
if filename.size == 0 # this is an Array!!
|
109
|
+
filename = @filename
|
110
|
+
backups = true # keep backups if we write to a specific filename
|
111
|
+
else
|
112
|
+
filename = filename[0]
|
113
|
+
backups = false
|
114
|
+
end
|
115
|
+
|
116
|
+
tf = Tempfile.new( @basename )
|
117
|
+
tmpname = tf.path
|
118
|
+
|
119
|
+
# write ID3v2 tag:
|
120
|
+
|
121
|
+
if @tagID3v2
|
122
|
+
tf.write( @tagID3v2.dump )
|
123
|
+
end
|
124
|
+
|
125
|
+
# write Audio Data:
|
126
|
+
|
127
|
+
tf.write( audio ) # reads audio from file if nil
|
128
|
+
|
129
|
+
# write ID3v1 tag:
|
130
|
+
|
131
|
+
if @tagID3v1
|
132
|
+
tf.write( @tagID3v1.dump )
|
133
|
+
end
|
134
|
+
|
135
|
+
tf.close
|
136
|
+
|
137
|
+
# now some logic about moving the tempfile and replacing the original
|
138
|
+
|
139
|
+
bakname = filename + '.bak'
|
140
|
+
move(filename, bakname) if backups && FileTest.exists?(filename) && ! FileTest.exists?(bakname)
|
141
|
+
|
142
|
+
move(tmpname, filename)
|
143
|
+
tf.close(true)
|
144
|
+
|
145
|
+
# write md5sum sha1sum files:
|
146
|
+
writeMD5sum if @audioMD5sum
|
147
|
+
writeSHA1sum if @audioSHA1sum
|
148
|
+
end
|
149
|
+
|
150
|
+
# ----------------------------------------------------------------------------
|
151
|
+
# readAudion
|
152
|
+
# read audio into @audio buffer either from String or from File
|
153
|
+
def audio
|
154
|
+
@audio ||= readAudio # read the audio portion of the file only once, the first time this is called.
|
155
|
+
end
|
156
|
+
|
157
|
+
def readAudio
|
158
|
+
File.open( File.join(@dirname, @basename) ) do |f|
|
159
|
+
f.seek(@audioStartX)
|
160
|
+
f.read(@audioEndX - @audioStartX + 1)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
# ----------------------------------------------------------------------------
|
164
|
+
# writeAudio
|
165
|
+
# only for debugging, does not write any ID3-tags, but just the audio portion
|
166
|
+
|
167
|
+
def writeAudioOnly
|
168
|
+
tf = Tempfile.new( @basename )
|
169
|
+
|
170
|
+
File.open( @filename ) { |f|
|
171
|
+
f.seek(@audioStartX)
|
172
|
+
tf.write( audio ) # reads the audio from file if nil
|
173
|
+
}
|
174
|
+
tf.close
|
175
|
+
path = tf.path
|
176
|
+
|
177
|
+
tf.open
|
178
|
+
tf.close(true)
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
# ----------------------------------------------------------------------------
|
183
|
+
# NOTE on md5sum's:
|
184
|
+
# If you don't know what an md5sum is, you can think of it as a unique
|
185
|
+
# fingerprint of a file or some data. I added the md5sum computation to
|
186
|
+
# help users keep track of their converted songs - even if the ID3-tag of
|
187
|
+
# a file changes(!), the md5sum of the audio data does not change..
|
188
|
+
# The md5sum can help you ensure that the audio-portion of the file
|
189
|
+
# was not changed after modifying, adding or deleting ID3-tags.
|
190
|
+
# It can also help you identifying duplicates.
|
191
|
+
|
192
|
+
# ----------------------------------------------------------------------------
|
193
|
+
# audioMD5sum
|
194
|
+
# if the user tries to access @audioMD5sum, it will be computed for him,
|
195
|
+
# unless it was previously computed. We try to calculate that only once
|
196
|
+
# and on demand, because it's a bit expensive to compute..
|
197
|
+
|
198
|
+
def audioMD5sum
|
199
|
+
@audioMD5sum ||= MD5.hexdigest( audio )
|
200
|
+
end
|
201
|
+
|
202
|
+
def audioSHA1sum
|
203
|
+
@audioSHA1sum ||= SHA1.hexdigest( audio )
|
204
|
+
end
|
205
|
+
# ----------------------------------------------------------------------------
|
206
|
+
# writeMD5sum
|
207
|
+
# write the filename and MD5sum of the audio portion into an ascii file
|
208
|
+
# in the same location as the audio file, but with suffix .md5
|
209
|
+
#
|
210
|
+
# computes the @audioMD5sum, if it wasn't previously computed..
|
211
|
+
|
212
|
+
def writeMD5sum
|
213
|
+
base = @basename.sub( /(.)\.[^.]+$/ , '\1')
|
214
|
+
base += '.md5'
|
215
|
+
File.open( File.join(@dirname,base) ,"w") { |f|
|
216
|
+
f.printf("%s %s\n", File.join(@dirname,@basename), audioMD5sum ) # computes it if nil
|
217
|
+
}
|
218
|
+
@audioMD5sum
|
219
|
+
end
|
220
|
+
|
221
|
+
def writeSHA1sum
|
222
|
+
base = @basename.sub( /(.)\.[^.]+$/ , '\1')
|
223
|
+
base += '.sha1'
|
224
|
+
File.open( File.join(@dirname,base) ,"w") { |f|
|
225
|
+
f.printf("%s %s\n", File.join(@dirname,@basename), audioSHA1sum ) # computes it if nil
|
226
|
+
}
|
227
|
+
@audioSHA1sum
|
228
|
+
end
|
229
|
+
# ----------------------------------------------------------------------------
|
230
|
+
# verifyMD5sum
|
231
|
+
# compare the audioMD5sum against a previously stored md5sum file
|
232
|
+
# and returns boolean value of comparison
|
233
|
+
#
|
234
|
+
# If no md5sum file existed, we create one and return true.
|
235
|
+
#
|
236
|
+
# computes the @audioMD5sum, if it wasn't previously computed..
|
237
|
+
|
238
|
+
def verifyMD5sum
|
239
|
+
|
240
|
+
oldMD5sum = ''
|
241
|
+
|
242
|
+
self.audioMD5sum if ! @audioMD5sum # compute MD5sum if it's not computed yet
|
243
|
+
|
244
|
+
base = @basename.sub( /(.)\.[^.]+$/ , '\1') # remove suffix from audio-file
|
245
|
+
base += '.md5' # add new suffix .md5
|
246
|
+
md5name = File.join(@dirname,base)
|
247
|
+
|
248
|
+
# if a MD5-file doesn't exist, we should create one and return TRUE ...
|
249
|
+
if File.exists?(md5name)
|
250
|
+
File.open( md5name ,"r") { |f|
|
251
|
+
oldname,oldMD5sum = f.readline.split # read old MD5-sum
|
252
|
+
}
|
253
|
+
else
|
254
|
+
oldMD5sum = self.writeMD5sum # create MD5-file and return true..
|
255
|
+
end
|
256
|
+
@audioMD5sum == oldMD5sum
|
257
|
+
|
258
|
+
end
|
259
|
+
# ----------------------------------------------------------------------------
|
260
|
+
|
261
|
+
end # of class AudioFile
|
@@ -0,0 +1,292 @@
|
|
1
|
+
module ID3
|
2
|
+
|
3
|
+
# ----------------------------------------------------------------------------
|
4
|
+
# CONSTANTS
|
5
|
+
# ----------------------------------------------------------------------------
|
6
|
+
Version = '1.0.0_pre4'
|
7
|
+
|
8
|
+
ID3v1tagSize = 128 # ID3v1 and ID3v1.1 have fixed size tags
|
9
|
+
ID3v1versionbyte = 125
|
10
|
+
|
11
|
+
ID3v2headerSize = 10
|
12
|
+
ID3v2major = 3
|
13
|
+
ID3v2minor = 4
|
14
|
+
ID3v2flags = 5
|
15
|
+
ID3v2tagSize = 6
|
16
|
+
|
17
|
+
|
18
|
+
VERSIONS =
|
19
|
+
SUPPORTED_VERSIONS = ["1.0", "1.1", "2.2.0", "2.3.0", "2.4.0"]
|
20
|
+
|
21
|
+
SUPPORTED_SYMBOLS = {
|
22
|
+
"1.0" => {
|
23
|
+
"ARTIST"=>33..62 , "ALBUM"=>63..92 ,"TITLE"=>3..32,
|
24
|
+
"YEAR"=>93..96 , "COMMENT"=>97..126,"GENREID"=>127,
|
25
|
+
# "VERSION"=>"1.0"
|
26
|
+
} ,
|
27
|
+
"1.1" => {
|
28
|
+
"ARTIST"=>33..62 , "ALBUM"=>63..92 ,"TITLE"=>3..32,
|
29
|
+
"YEAR"=>93..96 , "COMMENT"=>97..124,
|
30
|
+
"TRACKNUM"=>126, "GENREID"=>127,
|
31
|
+
# "VERSION"=>"1.1"
|
32
|
+
} ,
|
33
|
+
|
34
|
+
"2.2.0" => {"CONTENTGROUP"=>"TT1", "TITLE"=>"TT2", "SUBTITLE"=>"TT3",
|
35
|
+
"ARTIST"=>"TP1", "BAND"=>"TP2", "CONDUCTOR"=>"TP3", "MIXARTIST"=>"TP4",
|
36
|
+
"COMPOSER"=>"TCM", "LYRICIST"=>"TXT", "LANGUAGE"=>"TLA", "CONTENTTYPE"=>"TCO",
|
37
|
+
"ALBUM"=>"TAL", "TRACKNUM"=>"TRK", "PARTINSET"=>"TPA", "ISRC"=>"TRC",
|
38
|
+
"DATE"=>"TDA", "YEAR"=>"TYE", "TIME"=>"TIM", "RECORDINGDATES"=>"TRD",
|
39
|
+
"ORIGYEAR"=>"TOR", "BPM"=>"TBP", "MEDIATYPE"=>"TMT", "FILETYPE"=>"TFT",
|
40
|
+
"COPYRIGHT"=>"TCR", "PUBLISHER"=>"TPB", "ENCODEDBY"=>"TEN",
|
41
|
+
"ENCODERSETTINGS"=>"TSS", "SONGLEN"=>"TLE", "SIZE"=>"TSI",
|
42
|
+
"PLAYLISTDELAY"=>"TDY", "INITIALKEY"=>"TKE", "ORIGALBUM"=>"TOT",
|
43
|
+
"ORIGFILENAME"=>"TOF", "ORIGARTIST"=>"TOA", "ORIGLYRICIST"=>"TOL",
|
44
|
+
"USERTEXT"=>"TXX",
|
45
|
+
"WWWAUDIOFILE"=>"WAF", "WWWARTIST"=>"WAR", "WWWAUDIOSOURCE"=>"WAS",
|
46
|
+
"WWWCOMMERCIALINFO"=>"WCM", "WWWCOPYRIGHT"=>"WCP", "WWWPUBLISHER"=>"WPB",
|
47
|
+
"WWWUSER"=>"WXX", "UNIQUEFILEID"=>"UFI",
|
48
|
+
"INVOLVEDPEOPLE"=>"IPL", "UNSYNCEDLYRICS"=>"ULT", "COMMENT"=>"COM",
|
49
|
+
"CDID"=>"MCI", "EVENTTIMING"=>"ETC", "MPEGLOOKUP"=>"MLL",
|
50
|
+
"SYNCEDTEMPO"=>"STC", "SYNCEDLYRICS"=>"SLT", "VOLUMEADJ"=>"RVA",
|
51
|
+
"EQUALIZATION"=>"EQU", "REVERB"=>"REV", "PICTURE"=>"PIC",
|
52
|
+
"GENERALOBJECT"=>"GEO", "PLAYCOUNTER"=>"CNT", "POPULARIMETER"=>"POP",
|
53
|
+
"BUFFERSIZE"=>"BUF", "CRYPTEDMETA"=>"CRM", "AUDIOCRYPTO"=>"CRA",
|
54
|
+
"LINKED"=>"LNK"
|
55
|
+
} ,
|
56
|
+
|
57
|
+
"2.3.0" => {"CONTENTGROUP"=>"TIT1", "TITLE"=>"TIT2", "SUBTITLE"=>"TIT3",
|
58
|
+
"ARTIST"=>"TPE1", "BAND"=>"TPE2", "CONDUCTOR"=>"TPE3", "MIXARTIST"=>"TPE4",
|
59
|
+
"COMPOSER"=>"TCOM", "LYRICIST"=>"TEXT", "LANGUAGE"=>"TLAN", "CONTENTTYPE"=>"TCON",
|
60
|
+
"ALBUM"=>"TALB", "TRACKNUM"=>"TRCK", "PARTINSET"=>"TPOS", "ISRC"=>"TSRC",
|
61
|
+
"DATE"=>"TDAT", "YEAR"=>"TYER", "TIME"=>"TIME", "RECORDINGDATES"=>"TRDA",
|
62
|
+
"ORIGYEAR"=>"TORY", "SIZE"=>"TSIZ",
|
63
|
+
"BPM"=>"TBPM", "MEDIATYPE"=>"TMED", "FILETYPE"=>"TFLT", "COPYRIGHT"=>"TCOP",
|
64
|
+
"PUBLISHER"=>"TPUB", "ENCODEDBY"=>"TENC", "ENCODERSETTINGS"=>"TSSE",
|
65
|
+
"SONGLEN"=>"TLEN", "PLAYLISTDELAY"=>"TDLY", "INITIALKEY"=>"TKEY",
|
66
|
+
"ORIGALBUM"=>"TOAL", "ORIGFILENAME"=>"TOFN", "ORIGARTIST"=>"TOPE",
|
67
|
+
"ORIGLYRICIST"=>"TOLY", "FILEOWNER"=>"TOWN", "NETRADIOSTATION"=>"TRSN",
|
68
|
+
"NETRADIOOWNER"=>"TRSO", "USERTEXT"=>"TXXX",
|
69
|
+
"WWWAUDIOFILE"=>"WOAF", "WWWARTIST"=>"WOAR", "WWWAUDIOSOURCE"=>"WOAS",
|
70
|
+
"WWWCOMMERCIALINFO"=>"WCOM", "WWWCOPYRIGHT"=>"WCOP", "WWWPUBLISHER"=>"WPUB",
|
71
|
+
"WWWRADIOPAGE"=>"WORS", "WWWPAYMENT"=>"WPAY", "WWWUSER"=>"WXXX", "UNIQUEFILEID"=>"UFID",
|
72
|
+
"INVOLVEDPEOPLE"=>"IPLS",
|
73
|
+
"UNSYNCEDLYRICS"=>"USLT", "COMMENT"=>"COMM", "TERMSOFUSE"=>"USER",
|
74
|
+
"CDID"=>"MCDI", "EVENTTIMING"=>"ETCO", "MPEGLOOKUP"=>"MLLT",
|
75
|
+
"SYNCEDTEMPO"=>"SYTC", "SYNCEDLYRICS"=>"SYLT",
|
76
|
+
"VOLUMEADJ"=>"RVAD", "EQUALIZATION"=>"EQUA",
|
77
|
+
"REVERB"=>"RVRB", "PICTURE"=>"APIC", "GENERALOBJECT"=>"GEOB",
|
78
|
+
"PLAYCOUNTER"=>"PCNT", "POPULARIMETER"=>"POPM", "BUFFERSIZE"=>"RBUF",
|
79
|
+
"AUDIOCRYPTO"=>"AENC", "LINKEDINFO"=>"LINK", "POSITIONSYNC"=>"POSS",
|
80
|
+
"COMMERCIAL"=>"COMR", "CRYPTOREG"=>"ENCR", "GROUPINGREG"=>"GRID",
|
81
|
+
"PRIVATE"=>"PRIV"
|
82
|
+
} ,
|
83
|
+
|
84
|
+
"2.4.0" => {"CONTENTGROUP"=>"TIT1", "TITLE"=>"TIT2", "SUBTITLE"=>"TIT3",
|
85
|
+
"ARTIST"=>"TPE1", "BAND"=>"TPE2", "CONDUCTOR"=>"TPE3", "MIXARTIST"=>"TPE4",
|
86
|
+
"COMPOSER"=>"TCOM", "LYRICIST"=>"TEXT", "LANGUAGE"=>"TLAN", "CONTENTTYPE"=>"TCON",
|
87
|
+
"ALBUM"=>"TALB", "TRACKNUM"=>"TRCK", "PARTINSET"=>"TPOS", "ISRC"=>"TSRC",
|
88
|
+
"RECORDINGTIME"=>"TDRC", "ORIGRELEASETIME"=>"TDOR",
|
89
|
+
"BPM"=>"TBPM", "MEDIATYPE"=>"TMED", "FILETYPE"=>"TFLT", "COPYRIGHT"=>"TCOP",
|
90
|
+
"PUBLISHER"=>"TPUB", "ENCODEDBY"=>"TENC", "ENCODERSETTINGS"=>"TSSE",
|
91
|
+
"SONGLEN"=>"TLEN", "PLAYLISTDELAY"=>"TDLY", "INITIALKEY"=>"TKEY",
|
92
|
+
"ORIGALBUM"=>"TOAL", "ORIGFILENAME"=>"TOFN", "ORIGARTIST"=>"TOPE",
|
93
|
+
"ORIGLYRICIST"=>"TOLY", "FILEOWNER"=>"TOWN", "NETRADIOSTATION"=>"TRSN",
|
94
|
+
"NETRADIOOWNER"=>"TRSO", "USERTEXT"=>"TXXX",
|
95
|
+
"SETSUBTITLE"=>"TSST", "MOOD"=>"TMOO", "PRODUCEDNOTICE"=>"TPRO",
|
96
|
+
"ENCODINGTIME"=>"TDEN", "RELEASETIME"=>"TDRL", "TAGGINGTIME"=>"TDTG",
|
97
|
+
"ALBUMSORTORDER"=>"TSOA", "PERFORMERSORTORDER"=>"TSOP", "TITLESORTORDER"=>"TSOT",
|
98
|
+
"WWWAUDIOFILE"=>"WOAF", "WWWARTIST"=>"WOAR", "WWWAUDIOSOURCE"=>"WOAS",
|
99
|
+
"WWWCOMMERCIALINFO"=>"WCOM", "WWWCOPYRIGHT"=>"WCOP", "WWWPUBLISHER"=>"WPUB",
|
100
|
+
"WWWRADIOPAGE"=>"WORS", "WWWPAYMENT"=>"WPAY", "WWWUSER"=>"WXXX", "UNIQUEFILEID"=>"UFID",
|
101
|
+
"MUSICIANCREDITLIST"=>"TMCL", "INVOLVEDPEOPLE2"=>"TIPL",
|
102
|
+
"UNSYNCEDLYRICS"=>"USLT", "COMMENT"=>"COMM", "TERMSOFUSE"=>"USER",
|
103
|
+
"CDID"=>"MCDI", "EVENTTIMING"=>"ETCO", "MPEGLOOKUP"=>"MLLT",
|
104
|
+
"SYNCEDTEMPO"=>"SYTC", "SYNCEDLYRICS"=>"SYLT",
|
105
|
+
"VOLUMEADJ2"=>"RVA2", "EQUALIZATION2"=>"EQU2",
|
106
|
+
"REVERB"=>"RVRB", "PICTURE"=>"APIC", "GENERALOBJECT"=>"GEOB",
|
107
|
+
"PLAYCOUNTER"=>"PCNT", "POPULARIMETER"=>"POPM", "BUFFERSIZE"=>"RBUF",
|
108
|
+
"AUDIOCRYPTO"=>"AENC", "LINKEDINFO"=>"LINK", "POSITIONSYNC"=>"POSS",
|
109
|
+
"COMMERCIAL"=>"COMR", "CRYPTOREG"=>"ENCR", "GROUPINGREG"=>"GRID",
|
110
|
+
"PRIVATE"=>"PRIV",
|
111
|
+
"OWNERSHIP"=>"OWNE", "SIGNATURE"=>"SIGN", "SEEKFRAME"=>"SEEK",
|
112
|
+
"AUDIOSEEKPOINT"=>"ASPI"
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
116
|
+
|
117
|
+
# ----------------------------------------------------------------------------
|
118
|
+
# Flags in the ID3-Tag Header:
|
119
|
+
|
120
|
+
TAG_HEADER_FLAG_MASK = { # the mask is inverse, for error detection
|
121
|
+
# those flags are supposed to be zero!
|
122
|
+
"2.2.0" => 0x3F, # 0xC0 ,
|
123
|
+
"2.3.0" => 0x1F, # 0xE0 ,
|
124
|
+
"2.4.0" => 0x0F # 0xF0
|
125
|
+
}
|
126
|
+
|
127
|
+
TAG_HEADER_FLAGS = {
|
128
|
+
"2.2.0" => {
|
129
|
+
"Unsynchronisation" => 0x80 ,
|
130
|
+
"Compression" => 0x40 ,
|
131
|
+
} ,
|
132
|
+
"2.3.0" => {
|
133
|
+
"Unsynchronisation" => 0x80 ,
|
134
|
+
"ExtendedHeader" => 0x40 ,
|
135
|
+
"Experimental" => 0x20 ,
|
136
|
+
} ,
|
137
|
+
"2.4.0" => {
|
138
|
+
"Unsynchronisation" => 0x80 ,
|
139
|
+
"ExtendedHeader" => 0x40 ,
|
140
|
+
"Experimental" => 0x20 ,
|
141
|
+
"Footer" => 0x10 ,
|
142
|
+
}
|
143
|
+
}
|
144
|
+
|
145
|
+
# ----------------------------------------------------------------------------
|
146
|
+
# Flags in the ID3-Frame Header:
|
147
|
+
|
148
|
+
FRAME_HEADER_FLAG_MASK = { # the mask is inverse, for error detection
|
149
|
+
# those flags are supposed to be zero!
|
150
|
+
"2.3.0" => 0x1F1F, # 0xD0D0 ,
|
151
|
+
"2.4.0" => 0x8FB0 # 0x704F ,
|
152
|
+
}
|
153
|
+
|
154
|
+
FRAME_HEADER_FLAGS = {
|
155
|
+
"2.3.0" => {
|
156
|
+
"TagAlterPreservation" => 0x8000 ,
|
157
|
+
"FileAlterPreservation" => 0x4000 ,
|
158
|
+
"ReadOnly" => 0x2000 ,
|
159
|
+
|
160
|
+
"Compression" => 0x0080 ,
|
161
|
+
"Encryption" => 0x0040 ,
|
162
|
+
"GroupIdentity" => 0x0020 ,
|
163
|
+
} ,
|
164
|
+
"2.4.0" => {
|
165
|
+
"TagAlterPreservation" => 0x4000 ,
|
166
|
+
"FileAlterPreservation" => 0x2000 ,
|
167
|
+
"ReadOnly" => 0x1000 ,
|
168
|
+
|
169
|
+
"GroupIdentity" => 0x0040 ,
|
170
|
+
"Compression" => 0x0008 ,
|
171
|
+
"Encryption" => 0x0004 ,
|
172
|
+
"Unsynchronisation" => 0x0002 ,
|
173
|
+
"DataLengthIndicator" => 0x0001 ,
|
174
|
+
}
|
175
|
+
}
|
176
|
+
|
177
|
+
# the FrameTypes are not visible to the user - they are just a mechanism
|
178
|
+
# to define only one parser for multiple FraneNames..
|
179
|
+
#
|
180
|
+
|
181
|
+
FRAMETYPE2FRAMENAME = {
|
182
|
+
"TEXT" => %w(TENTGROUP TITLE SUBTITLE ARTIST BAND CONDUCTOR MIXARTIST COMPOSER LYRICIST LANGUAGE CONTENTTYPE ALBUM TRACKNUM PARTINSET ISRC DATE YEAR TIME RECORDINGDATES ORIGYEAR BPM MEDIATYPE FILETYPE COPYRIGHT PUBLISHER ENCODEDBY ENCODERSETTINGS SONGLEN SIZE PLAYLISTDELAY INITIALKEY ORIGALBUM ORIGFILENAME ORIGARTIST ORIGLYRICIST FILEOWNER NETRADIOSTATION NETRADIOOWNER SETSUBTITLE MOOD PRODUCEDNOTICE ALBUMSORTORDER PERFORMERSORTORDER TITLESORTORDER INVOLVEDPEOPLE),
|
183
|
+
"USERTEXT" => "USERTEXT",
|
184
|
+
|
185
|
+
"WEB" => %w(WWWAUDIOFILE WWWARTIST WWWAUDIOSOURCE WWWCOMMERCIALINFO WWWCOPYRIGHT WWWPUBLISHER WWWRADIOPAGE WWWPAYMENT) ,
|
186
|
+
"WWWUSER" => "WWWUSER",
|
187
|
+
"LTEXT" => "TERMSOFUSE" ,
|
188
|
+
"PICTURE" => "PICTURE" ,
|
189
|
+
"UNSYNCEDLYRICS" => "UNSYNCEDLYRICS" ,
|
190
|
+
"COMMENT" => "COMMENT" ,
|
191
|
+
|
192
|
+
"PLAYCOUNTER" => "PLAYCOUNTER" ,
|
193
|
+
"POPULARIMETER" => "POPULARIMETER",
|
194
|
+
|
195
|
+
"BINARY" => %w( CDID ) , # Cee Dee I Dee
|
196
|
+
|
197
|
+
# For the following Frames there are no parser stings defined .. the user has access to the raw data
|
198
|
+
# The following frames are good examples for completely useless junk which was put into the ID3-definitions.. what were they smoking?
|
199
|
+
#
|
200
|
+
"UNPARSED" => %w(UNIQUEFILEID OWNERSHIP SYNCEDTEMPO MPEGLOOKUP REVERB SYNCEDLYRICS CONTENTGROUP GENERALOBJECT VOLUMEADJ AUDIOCRYPTO CRYPTEDMETA BUFFERSIZE EVENTTIMING EQUALIZATION LINKED PRIVATE LINKEDINFO POSITIONSYNC GROUPINGREG CRYPTOREG COMMERCIAL SEEKFRAME AUDIOSEEKPOINT SIGNATURE EQUALIZATION2 VOLUMEADJ2 MUSICIANCREDITLIST INVOLVEDPEOPLE2 RECORDINGTIME ORIGRELEASETIME ENCODINGTIME RELEASETIME TAGGINGTIME)
|
201
|
+
}
|
202
|
+
|
203
|
+
VARS = 0
|
204
|
+
PACKING = 1
|
205
|
+
|
206
|
+
# ----------------------------------------------------------------------------
|
207
|
+
# String Encodings: See id3v2.4.0-structure document, at section 4.
|
208
|
+
# see also: http://en.wikipedia.org/wiki/ID3#ID3v2_Chapters
|
209
|
+
#
|
210
|
+
# Frames that allow different types of text encoding contains a text
|
211
|
+
# encoding description byte. Possible encodings:
|
212
|
+
#
|
213
|
+
# $00 ISO-8859-1 [ISO-8859-1]. Terminated with $00. (ASCII)
|
214
|
+
# $01 [UCS-2] in ID3v2.2,ID3v2.3 / UTF-16 [UTF-16] encoded Unicode [UNICODE] with BOM All in ID3v2.4
|
215
|
+
# strings in the same frame SHALL have the same byteorder.
|
216
|
+
# Terminated with $00 00.
|
217
|
+
# $02 UTF-16BE [UTF-16] encoded Unicode [UNICODE] without BOM. (ID3v2.4 only)
|
218
|
+
# Terminated with $00 00.
|
219
|
+
# $03 UTF-8 [UTF-8] encoded Unicode [UNICODE]. Terminated with $00. (ID3v2.4 only)
|
220
|
+
|
221
|
+
TEXT_ENCODINGS = ["ISO-8859-1", "UTF-16", "UTF-16BE", "UTF-8"]
|
222
|
+
|
223
|
+
# to get the BYTE-code for the encoding type: TEXT_ENCODINGS.index( string.encoding.to_s ).chr
|
224
|
+
# to read the string : .force_encoding( Encoding::whatever )
|
225
|
+
# in Ruby 1.9 : Encoding::UTF_8 , Encoding::UTF_16BE, Encoding::ISO_8859_1
|
226
|
+
# BOM: see: http://www.websina.com/bugzero/kb/unicode-bom.html
|
227
|
+
# ----------------------------------------------------------------------------
|
228
|
+
|
229
|
+
# not sure if it's Z* or A*
|
230
|
+
# A* does not append a \0 when writing!
|
231
|
+
|
232
|
+
# STILL NEED TO GET MORE TEST-CASES! e.g. Japanese ID3-Tags! or other encodings..
|
233
|
+
# seems like i have no version 2.4.x ID3-tags!! If you have some, send them my way!
|
234
|
+
|
235
|
+
# NOTE: please note that all the first array entries need to be hashes, in order for Ruby 1.9 to handle this correctly!
|
236
|
+
|
237
|
+
FRAME_PARSER = {
|
238
|
+
"TEXT" => [ %w(encoding text) , 'CZ*' ] ,
|
239
|
+
"USERTEXT" => [ %w(encoding description value) , 'CZ*Z*' ] ,
|
240
|
+
|
241
|
+
"PICTURE" => [ %w(encoding mime_type pict_type description picture) , 'CZ*CZ*a*' ] ,
|
242
|
+
|
243
|
+
"WEB" => [ %w(url) , 'Z*' ] ,
|
244
|
+
"WWWUSER" => [ %w(encoding description url) , 'CZ*Z*' ] ,
|
245
|
+
|
246
|
+
"LTEXT" => [ %w(encoding language text) , 'CZ*Z*' ] ,
|
247
|
+
"UNSYNCEDLYRICS" => [ %w(encoding language content text) , 'Ca3Z*Z*' ] ,
|
248
|
+
"COMMENT" => [ %w(encoding language short long) , 'Ca3Z*Z*' ] ,
|
249
|
+
|
250
|
+
"PLAYCOUNTER" => [%w(counter), 'C*'] ,
|
251
|
+
"POPULARIMETER" => [%w(email rating counter), 'Z*CC*'] ,
|
252
|
+
|
253
|
+
"BINARY" => [ %w(binary) , 'a*' ] ,
|
254
|
+
"UNPARSED" => [ %w(raw) , 'a*' ] # how would we do value checking for this?
|
255
|
+
}
|
256
|
+
|
257
|
+
# ----------------------------------------------------------------------------
|
258
|
+
|
259
|
+
Symbol2framename = ID3::SUPPORTED_SYMBOLS
|
260
|
+
Framename2symbol = Hash.new
|
261
|
+
Framename2symbol["1.0"] = ID3::SUPPORTED_SYMBOLS["1.0"].invert
|
262
|
+
Framename2symbol["1.1"] = ID3::SUPPORTED_SYMBOLS["1.1"].invert
|
263
|
+
Framename2symbol["2.2.0"] = ID3::SUPPORTED_SYMBOLS["2.2.0"].invert
|
264
|
+
Framename2symbol["2.3.0"] = ID3::SUPPORTED_SYMBOLS["2.3.0"].invert
|
265
|
+
Framename2symbol["2.4.0"] = ID3::SUPPORTED_SYMBOLS["2.4.0"].invert
|
266
|
+
|
267
|
+
FrameType2FrameName = ID3::FRAMETYPE2FRAMENAME
|
268
|
+
|
269
|
+
FrameName2FrameType = FrameType2FrameName.invert
|
270
|
+
|
271
|
+
# ----------------------------------------------------------------------------
|
272
|
+
# the following piece of code is just for debugging, to sanity-check that all
|
273
|
+
# the FrameSymbols map back to a FrameType -- otherwise the library code will
|
274
|
+
# break if we encounter a Frame which can't be mapped to a FrameType..
|
275
|
+
# ----------------------------------------------------------------------------
|
276
|
+
#
|
277
|
+
# ensure we have a FrameType defined for each FrameName, otherwise
|
278
|
+
# code might break later..
|
279
|
+
#
|
280
|
+
|
281
|
+
# print "\nMISSING SYMBOLS:\n"
|
282
|
+
|
283
|
+
(ID3::Framename2symbol["2.2.0"].values +
|
284
|
+
ID3::Framename2symbol["2.3.0"].values +
|
285
|
+
ID3::Framename2symbol["2.4.0"].values).uniq.each { |symbol|
|
286
|
+
# print "#{symbol} " if ! ID3::FrameName2FrameType[symbol]
|
287
|
+
print "SYMBOL: #{symbol} not defined!\n" if ! ID3::FrameName2FrameType[symbol]
|
288
|
+
}
|
289
|
+
# print "\n\n"
|
290
|
+
|
291
|
+
end
|
292
|
+
|