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
data/lib/id3.rb
CHANGED
@@ -1,1267 +1,38 @@
|
|
1
|
-
################################################################################
|
2
|
-
# id3.rb Ruby Module for handling the following ID3-tag versions:
|
3
|
-
# ID3v1.0 , ID3v1.1, ID3v2.2.0, ID3v2.3.0, ID3v2.4.0
|
4
|
-
#
|
5
|
-
# Copyright (C) 2002 .. 2008 by Tilo Sloboda <tilo@unixgods.org>
|
6
|
-
#
|
7
|
-
# created: 12 Oct 2002
|
8
|
-
# updated: Time-stamp: <Mon 18-Aug-2008 06:16:19 Tilo Sloboda>
|
9
|
-
#
|
10
|
-
# Docs: http://www.id3.org/id3v2-00.txt
|
11
|
-
# http://www.id3.org/id3v2.3.0.txt
|
12
|
-
# http://www.id3.org/id3v2.4.0-changes.txt
|
13
|
-
# http://www.id3.org/id3v2.4.0-structure.txt
|
14
|
-
# http://www.id3.org/id3v2.4.0-frames.txt
|
15
|
-
#
|
16
|
-
# different versions of ID3 tags, support different fields.
|
17
|
-
# See: http://www.unixgods.org/~tilo/Ruby/ID3/docs/ID3v2_frames_comparison.txt
|
18
|
-
# See: http://www.unixgods.org/~tilo/Ruby/ID3/docs/ID3_comparison.html
|
19
|
-
#
|
20
|
-
# License:
|
21
|
-
# Freely available under the terms of the OpenSource "Artistic License"
|
22
|
-
# in combination with the Addendum A (below)
|
23
|
-
#
|
24
|
-
# In case you did not get a copy of the license along with the software,
|
25
|
-
# it is also available at: http://www.unixgods.org/~tilo/artistic-license.html
|
26
|
-
#
|
27
|
-
# Addendum A:
|
28
|
-
# THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU!
|
29
|
-
# SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
30
|
-
# REPAIR OR CORRECTION.
|
31
|
-
#
|
32
|
-
# IN NO EVENT WILL THE COPYRIGHT HOLDERS BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL,
|
33
|
-
# SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY
|
34
|
-
# TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
|
35
|
-
# INACCURATE OR USELESS OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
36
|
-
# TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF THE COPYRIGHT HOLDERS OR OTHER PARTY HAS BEEN
|
37
|
-
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
38
|
-
#
|
39
|
-
#
|
40
|
-
# Author's Rant:
|
41
|
-
# The author of this ID3-library for Ruby is not responsible in any way for
|
42
|
-
# the definition of the ID3-standards..
|
43
|
-
#
|
44
|
-
# You're lucky though that you can use this little library, rather than having
|
45
|
-
# to parse ID3v2 tags yourself! Trust me! At the first glance it doesn't seem
|
46
|
-
# to be so complicated, but the ID3v2 definitions are so convoluted and
|
47
|
-
# unnecessarily complicated, with so many useless frame-types, it's a pain to
|
48
|
-
# read the documents describing the ID3 V2.x standards.. and even worse
|
49
|
-
# to implement them..
|
50
|
-
#
|
51
|
-
# I don't know what these people were thinking... can we make it any more
|
52
|
-
# complicated than that?? ID3 version 2.4.0 tops everything! If this flag
|
53
|
-
# is set and it's a full moon, and an even weekday number, then do this..
|
54
|
-
# Outch!!! I assume that's why I don't find any 2.4.0 tags in any of my
|
55
|
-
# MP3-files... seems like noone is writing 2.4.0 tags... iTunes writes 2.3.0
|
56
|
-
#
|
57
|
-
# If you have some files with valid 2.4.0 tags, please send them my way!
|
58
|
-
# Thank you!
|
59
|
-
#
|
60
|
-
#-------------------------------------------------------------------------------
|
61
|
-
# Module ID3
|
62
|
-
#
|
63
|
-
# Module Functions:
|
64
|
-
# hasID3v1tag?(filename)
|
65
|
-
# hasID3v2tag?(filename)
|
66
|
-
# removeID3v1tag(filename)
|
67
|
-
#
|
68
|
-
# Classes:
|
69
|
-
# AudioFile
|
70
|
-
# Tag1
|
71
|
-
# Tag2
|
72
|
-
# Frame
|
73
|
-
#
|
74
|
-
################################################################################
|
75
1
|
|
76
|
-
# ==============================================================================
|
77
|
-
# Loading other stuff..
|
78
|
-
# ==============================================================================
|
79
|
-
|
80
|
-
require 'md5'
|
81
2
|
require 'tempfile'
|
82
|
-
require '
|
83
|
-
|
84
|
-
# my extensions:
|
85
|
-
|
86
|
-
require 'hexdump' # load hexdump method to extend class String
|
87
|
-
|
88
|
-
require 'invert_hash' # new invert method for old Hash
|
89
|
-
|
90
|
-
|
91
|
-
class Hash # overwrite Hash.invert method
|
92
|
-
alias old_invert invert
|
93
|
-
|
94
|
-
def invert
|
95
|
-
self.inverse
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
# ==============================================================================
|
100
|
-
|
101
|
-
module ID3
|
102
|
-
|
103
|
-
# ----------------------------------------------------------------------------
|
104
|
-
# CONSTANTS
|
105
|
-
# ----------------------------------------------------------------------------
|
106
|
-
@@RCSid = '$Id: id3.rb,v 1.2 2004/11/29 05:18:44 tilo Exp tilo $'
|
107
|
-
|
108
|
-
ID3v1tagSize = 128 # ID3v1 and ID3v1.1 have fixed size tags
|
109
|
-
ID3v1versionbyte = 125
|
110
|
-
ID3v2headerSize = 10
|
111
|
-
|
112
|
-
|
113
|
-
SUPPORTED_SYMBOLS = {
|
114
|
-
"1.0" => {"ARTIST"=>33..62 , "ALBUM"=>63..92 ,"TITLE"=>3..32,
|
115
|
-
"YEAR"=>93..96 , "COMMENT"=>97..126,"GENREID"=>127,
|
116
|
-
# "VERSION"=>"1.0"
|
117
|
-
} ,
|
118
|
-
"1.1" => {"ARTIST"=>33..62 , "ALBUM"=>63..92 ,"TITLE"=>3..32,
|
119
|
-
"YEAR"=>93..96 , "COMMENT"=>97..124,
|
120
|
-
"TRACKNUM"=>126, "GENREID"=>127,
|
121
|
-
# "VERSION"=>"1.1"
|
122
|
-
} ,
|
123
|
-
|
124
|
-
"2.2.0" => {"CONTENTGROUP"=>"TT1", "TITLE"=>"TT2", "SUBTITLE"=>"TT3",
|
125
|
-
"ARTIST"=>"TP1", "BAND"=>"TP2", "CONDUCTOR"=>"TP3", "MIXARTIST"=>"TP4",
|
126
|
-
"COMPOSER"=>"TCM", "LYRICIST"=>"TXT", "LANGUAGE"=>"TLA", "CONTENTTYPE"=>"TCO",
|
127
|
-
"ALBUM"=>"TAL", "TRACKNUM"=>"TRK", "PARTINSET"=>"TPA", "ISRC"=>"TRC",
|
128
|
-
"DATE"=>"TDA", "YEAR"=>"TYE", "TIME"=>"TIM", "RECORDINGDATES"=>"TRD",
|
129
|
-
"ORIGYEAR"=>"TOR", "BPM"=>"TBP", "MEDIATYPE"=>"TMT", "FILETYPE"=>"TFT",
|
130
|
-
"COPYRIGHT"=>"TCR", "PUBLISHER"=>"TPB", "ENCODEDBY"=>"TEN",
|
131
|
-
"ENCODERSETTINGS"=>"TSS", "SONGLEN"=>"TLE", "SIZE"=>"TSI",
|
132
|
-
"PLAYLISTDELAY"=>"TDY", "INITIALKEY"=>"TKE", "ORIGALBUM"=>"TOT",
|
133
|
-
"ORIGFILENAME"=>"TOF", "ORIGARTIST"=>"TOA", "ORIGLYRICIST"=>"TOL",
|
134
|
-
"USERTEXT"=>"TXX",
|
135
|
-
"WWWAUDIOFILE"=>"WAF", "WWWARTIST"=>"WAR", "WWWAUDIOSOURCE"=>"WAS",
|
136
|
-
"WWWCOMMERCIALINFO"=>"WCM", "WWWCOPYRIGHT"=>"WCP", "WWWPUBLISHER"=>"WPB",
|
137
|
-
"WWWUSER"=>"WXX", "UNIQUEFILEID"=>"UFI",
|
138
|
-
"INVOLVEDPEOPLE"=>"IPL", "UNSYNCEDLYRICS"=>"ULT", "COMMENT"=>"COM",
|
139
|
-
"CDID"=>"MCI", "EVENTTIMING"=>"ETC", "MPEGLOOKUP"=>"MLL",
|
140
|
-
"SYNCEDTEMPO"=>"STC", "SYNCEDLYRICS"=>"SLT", "VOLUMEADJ"=>"RVA",
|
141
|
-
"EQUALIZATION"=>"EQU", "REVERB"=>"REV", "PICTURE"=>"PIC",
|
142
|
-
"GENERALOBJECT"=>"GEO", "PLAYCOUNTER"=>"CNT", "POPULARIMETER"=>"POP",
|
143
|
-
"BUFFERSIZE"=>"BUF", "CRYPTEDMETA"=>"CRM", "AUDIOCRYPTO"=>"CRA",
|
144
|
-
"LINKED"=>"LNK"
|
145
|
-
} ,
|
146
|
-
|
147
|
-
"2.3.0" => {"CONTENTGROUP"=>"TIT1", "TITLE"=>"TIT2", "SUBTITLE"=>"TIT3",
|
148
|
-
"ARTIST"=>"TPE1", "BAND"=>"TPE2", "CONDUCTOR"=>"TPE3", "MIXARTIST"=>"TPE4",
|
149
|
-
"COMPOSER"=>"TCOM", "LYRICIST"=>"TEXT", "LANGUAGE"=>"TLAN", "CONTENTTYPE"=>"TCON",
|
150
|
-
"ALBUM"=>"TALB", "TRACKNUM"=>"TRCK", "PARTINSET"=>"TPOS", "ISRC"=>"TSRC",
|
151
|
-
"DATE"=>"TDAT", "YEAR"=>"TYER", "TIME"=>"TIME", "RECORDINGDATES"=>"TRDA",
|
152
|
-
"ORIGYEAR"=>"TORY", "SIZE"=>"TSIZ",
|
153
|
-
"BPM"=>"TBPM", "MEDIATYPE"=>"TMED", "FILETYPE"=>"TFLT", "COPYRIGHT"=>"TCOP",
|
154
|
-
"PUBLISHER"=>"TPUB", "ENCODEDBY"=>"TENC", "ENCODERSETTINGS"=>"TSSE",
|
155
|
-
"SONGLEN"=>"TLEN", "PLAYLISTDELAY"=>"TDLY", "INITIALKEY"=>"TKEY",
|
156
|
-
"ORIGALBUM"=>"TOAL", "ORIGFILENAME"=>"TOFN", "ORIGARTIST"=>"TOPE",
|
157
|
-
"ORIGLYRICIST"=>"TOLY", "FILEOWNER"=>"TOWN", "NETRADIOSTATION"=>"TRSN",
|
158
|
-
"NETRADIOOWNER"=>"TRSO", "USERTEXT"=>"TXXX",
|
159
|
-
"WWWAUDIOFILE"=>"WOAF", "WWWARTIST"=>"WOAR", "WWWAUDIOSOURCE"=>"WOAS",
|
160
|
-
"WWWCOMMERCIALINFO"=>"WCOM", "WWWCOPYRIGHT"=>"WCOP", "WWWPUBLISHER"=>"WPUB",
|
161
|
-
"WWWRADIOPAGE"=>"WORS", "WWWPAYMENT"=>"WPAY", "WWWUSER"=>"WXXX", "UNIQUEFILEID"=>"UFID",
|
162
|
-
"INVOLVEDPEOPLE"=>"IPLS",
|
163
|
-
"UNSYNCEDLYRICS"=>"USLT", "COMMENT"=>"COMM", "TERMSOFUSE"=>"USER",
|
164
|
-
"CDID"=>"MCDI", "EVENTTIMING"=>"ETCO", "MPEGLOOKUP"=>"MLLT",
|
165
|
-
"SYNCEDTEMPO"=>"SYTC", "SYNCEDLYRICS"=>"SYLT",
|
166
|
-
"VOLUMEADJ"=>"RVAD", "EQUALIZATION"=>"EQUA",
|
167
|
-
"REVERB"=>"RVRB", "PICTURE"=>"APIC", "GENERALOBJECT"=>"GEOB",
|
168
|
-
"PLAYCOUNTER"=>"PCNT", "POPULARIMETER"=>"POPM", "BUFFERSIZE"=>"RBUF",
|
169
|
-
"AUDIOCRYPTO"=>"AENC", "LINKEDINFO"=>"LINK", "POSITIONSYNC"=>"POSS",
|
170
|
-
"COMMERCIAL"=>"COMR", "CRYPTOREG"=>"ENCR", "GROUPINGREG"=>"GRID",
|
171
|
-
"PRIVATE"=>"PRIV"
|
172
|
-
} ,
|
173
|
-
|
174
|
-
"2.4.0" => {"CONTENTGROUP"=>"TIT1", "TITLE"=>"TIT2", "SUBTITLE"=>"TIT3",
|
175
|
-
"ARTIST"=>"TPE1", "BAND"=>"TPE2", "CONDUCTOR"=>"TPE3", "MIXARTIST"=>"TPE4",
|
176
|
-
"COMPOSER"=>"TCOM", "LYRICIST"=>"TEXT", "LANGUAGE"=>"TLAN", "CONTENTTYPE"=>"TCON",
|
177
|
-
"ALBUM"=>"TALB", "TRACKNUM"=>"TRCK", "PARTINSET"=>"TPOS", "ISRC"=>"TSRC",
|
178
|
-
"RECORDINGTIME"=>"TDRC", "ORIGRELEASETIME"=>"TDOR",
|
179
|
-
"BPM"=>"TBPM", "MEDIATYPE"=>"TMED", "FILETYPE"=>"TFLT", "COPYRIGHT"=>"TCOP",
|
180
|
-
"PUBLISHER"=>"TPUB", "ENCODEDBY"=>"TENC", "ENCODERSETTINGS"=>"TSSE",
|
181
|
-
"SONGLEN"=>"TLEN", "PLAYLISTDELAY"=>"TDLY", "INITIALKEY"=>"TKEY",
|
182
|
-
"ORIGALBUM"=>"TOAL", "ORIGFILENAME"=>"TOFN", "ORIGARTIST"=>"TOPE",
|
183
|
-
"ORIGLYRICIST"=>"TOLY", "FILEOWNER"=>"TOWN", "NETRADIOSTATION"=>"TRSN",
|
184
|
-
"NETRADIOOWNER"=>"TRSO", "USERTEXT"=>"TXXX",
|
185
|
-
"SETSUBTITLE"=>"TSST", "MOOD"=>"TMOO", "PRODUCEDNOTICE"=>"TPRO",
|
186
|
-
"ENCODINGTIME"=>"TDEN", "RELEASETIME"=>"TDRL", "TAGGINGTIME"=>"TDTG",
|
187
|
-
"ALBUMSORTORDER"=>"TSOA", "PERFORMERSORTORDER"=>"TSOP", "TITLESORTORDER"=>"TSOT",
|
188
|
-
"WWWAUDIOFILE"=>"WOAF", "WWWARTIST"=>"WOAR", "WWWAUDIOSOURCE"=>"WOAS",
|
189
|
-
"WWWCOMMERCIALINFO"=>"WCOM", "WWWCOPYRIGHT"=>"WCOP", "WWWPUBLISHER"=>"WPUB",
|
190
|
-
"WWWRADIOPAGE"=>"WORS", "WWWPAYMENT"=>"WPAY", "WWWUSER"=>"WXXX", "UNIQUEFILEID"=>"UFID",
|
191
|
-
"MUSICIANCREDITLIST"=>"TMCL", "INVOLVEDPEOPLE2"=>"TIPL",
|
192
|
-
"UNSYNCEDLYRICS"=>"USLT", "COMMENT"=>"COMM", "TERMSOFUSE"=>"USER",
|
193
|
-
"CDID"=>"MCDI", "EVENTTIMING"=>"ETCO", "MPEGLOOKUP"=>"MLLT",
|
194
|
-
"SYNCEDTEMPO"=>"SYTC", "SYNCEDLYRICS"=>"SYLT",
|
195
|
-
"VOLUMEADJ2"=>"RVA2", "EQUALIZATION2"=>"EQU2",
|
196
|
-
"REVERB"=>"RVRB", "PICTURE"=>"APIC", "GENERALOBJECT"=>"GEOB",
|
197
|
-
"PLAYCOUNTER"=>"PCNT", "POPULARIMETER"=>"POPM", "BUFFERSIZE"=>"RBUF",
|
198
|
-
"AUDIOCRYPTO"=>"AENC", "LINKEDINFO"=>"LINK", "POSITIONSYNC"=>"POSS",
|
199
|
-
"COMMERCIAL"=>"COMR", "CRYPTOREG"=>"ENCR", "GROUPINGREG"=>"GRID",
|
200
|
-
"PRIVATE"=>"PRIV",
|
201
|
-
"OWNERSHIP"=>"OWNE", "SIGNATURE"=>"SIGN", "SEEKFRAME"=>"SEEK",
|
202
|
-
"AUDIOSEEKPOINT"=>"ASPI"
|
203
|
-
}
|
204
|
-
}
|
205
|
-
|
206
|
-
# ----------------------------------------------------------------------------
|
207
|
-
# Flags in the ID3-Tag Header:
|
208
|
-
|
209
|
-
TAG_HEADER_FLAG_MASK = { # the mask is inverse, for error detection
|
210
|
-
# those flags are supposed to be zero!
|
211
|
-
"2.2.0" => 0x3F, # 0xC0 ,
|
212
|
-
"2.3.0" => 0x1F, # 0xE0 ,
|
213
|
-
"2.4.0" => 0x0F # 0xF0
|
214
|
-
}
|
215
|
-
|
216
|
-
TAG_HEADER_FLAGS = {
|
217
|
-
"2.2.0" => { "Unsynchronisation" => 0x80 ,
|
218
|
-
"Compression" => 0x40 ,
|
219
|
-
} ,
|
220
|
-
"2.3.0" => { "Unsynchronisation" => 0x80 ,
|
221
|
-
"ExtendedHeader" => 0x40 ,
|
222
|
-
"Experimental" => 0x20 ,
|
223
|
-
} ,
|
224
|
-
"2.4.0" => { "Unsynchronisation" => 0x80 ,
|
225
|
-
"ExtendedHeader" => 0x40 ,
|
226
|
-
"Experimental" => 0x20 ,
|
227
|
-
"Footer" => 0x10 ,
|
228
|
-
}
|
229
|
-
}
|
230
|
-
|
231
|
-
# ----------------------------------------------------------------------------
|
232
|
-
# Flags in the ID3-Frame Header:
|
233
|
-
|
234
|
-
FRAME_HEADER_FLAG_MASK = { # the mask is inverse, for error detection
|
235
|
-
# those flags are supposed to be zero!
|
236
|
-
"2.3.0" => 0x1F1F, # 0xD0D0 ,
|
237
|
-
"2.4.0" => 0x8FB0 # 0x704F ,
|
238
|
-
}
|
239
|
-
|
240
|
-
FRAME_HEADER_FLAGS = {
|
241
|
-
"2.3.0" => { "TagAlterPreservation" => 0x8000 ,
|
242
|
-
"FileAlterPreservation" => 0x4000 ,
|
243
|
-
"ReadOnly" => 0x2000 ,
|
244
|
-
|
245
|
-
"Compression" => 0x0080 ,
|
246
|
-
"Encryption" => 0x0040 ,
|
247
|
-
"GroupIdentity" => 0x0020 ,
|
248
|
-
} ,
|
249
|
-
"2.4.0" => { "TagAlterPreservation" => 0x4000 ,
|
250
|
-
"FileAlterPreservation" => 0x2000 ,
|
251
|
-
"ReadOnly" => 0x1000 ,
|
252
|
-
|
253
|
-
"GroupIdentity" => 0x0040 ,
|
254
|
-
"Compression" => 0x0008 ,
|
255
|
-
"Encryption" => 0x0004 ,
|
256
|
-
"Unsynchronisation" => 0x0002 ,
|
257
|
-
"DataLengthIndicator" => 0x0001 ,
|
258
|
-
}
|
259
|
-
}
|
260
|
-
|
261
|
-
# the FrameTypes are not visible to the user - they are just a mechanism
|
262
|
-
# to define only one parser for multiple FraneNames..
|
263
|
-
#
|
264
|
-
|
265
|
-
FRAMETYPE2FRAMENAME = {
|
266
|
-
"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),
|
267
|
-
"USERTEXT" => "USERTEXT",
|
268
|
-
|
269
|
-
"WEB" => %w(WWWAUDIOFILE WWWARTIST WWWAUDIOSOURCE WWWCOMMERCIALINFO WWWCOPYRIGHT WWWPUBLISHER WWWRADIOPAGE WWWPAYMENT) ,
|
270
|
-
"WWWUSER" => "WWWUSER",
|
271
|
-
"LTEXT" => "TERMSOFUSE" ,
|
272
|
-
"PICTURE" => "PICTURE" ,
|
273
|
-
"UNSYNCEDLYRICS" => "UNSYNCEDLYRICS" ,
|
274
|
-
"COMMENT" => "COMMENT" ,
|
275
|
-
"BINARY" => %w(PLAYCOUNTER CDID) ,
|
276
|
-
|
277
|
-
# For the following Frames there are no parser stings defined .. the user has access to the raw data
|
278
|
-
# The following frames are good examples for completely useless junk which was put into the ID3-definitions.. what were they smoking?
|
279
|
-
#
|
280
|
-
"UNPARSED" => %w(UNIQUEFILEID OWNERSHIP SYNCEDTEMPO MPEGLOOKUP REVERB SYNCEDLYRICS CONTENTGROUP POPULARIMETER 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)
|
281
|
-
}
|
282
|
-
|
283
|
-
VARS = 0
|
284
|
-
PACKING = 1
|
285
|
-
|
286
|
-
# not sure if it's Z* or A*
|
287
|
-
# A* does not append a \0 when writing!
|
288
|
-
|
289
|
-
# STILL NEED TO GET MORE TEST-CASES! e.g. Japanese ID3-Tags! or other encodings..
|
290
|
-
# seems like i have no version 2.4.x ID3-tags!! If you have some, send them my way!
|
291
|
-
|
292
|
-
FRAME_PARSER = {
|
293
|
-
"TEXT" => [ %w(encoding text) , 'CZ*' ] ,
|
294
|
-
"USERTEXT" => [ %w(encoding description value) , 'CZ*Z*' ] ,
|
295
|
-
|
296
|
-
"PICTURE" => [ %w(encoding mimeType pictType description picture) , 'CZ*CZ*a*' ] ,
|
297
|
-
|
298
|
-
"WEB" => [ "url" , 'Z*' ] ,
|
299
|
-
"WWWUSER" => [ %w(encoding description url) , 'CZ*Z*' ] ,
|
300
|
-
|
301
|
-
"LTEXT" => [ %w(encoding language text) , 'CZ*Z*' ] ,
|
302
|
-
"UNSYNCEDLYRICS" => [ %w(encoding language content text) , 'Ca3Z*Z*' ] ,
|
303
|
-
"COMMENT" => [ %w(encoding language short long) , 'Ca3Z*Z*' ] ,
|
304
|
-
"BINARY" => [ "binary" , 'a*' ] ,
|
305
|
-
"UNPARSED" => [ "raw" , 'a*' ] # how would we do value checking for this?
|
306
|
-
}
|
307
|
-
|
308
|
-
# ----------------------------------------------------------------------------
|
309
|
-
# MODULE VARIABLES
|
310
|
-
# ----------------------------------------------------------------------------
|
311
|
-
Symbol2framename = ID3::SUPPORTED_SYMBOLS
|
312
|
-
Framename2symbol = Hash.new
|
313
|
-
Framename2symbol["1.0"] = ID3::SUPPORTED_SYMBOLS["1.0"].invert
|
314
|
-
Framename2symbol["1.1"] = ID3::SUPPORTED_SYMBOLS["1.1"].invert
|
315
|
-
Framename2symbol["2.2.0"] = ID3::SUPPORTED_SYMBOLS["2.2.0"].invert
|
316
|
-
Framename2symbol["2.3.0"] = ID3::SUPPORTED_SYMBOLS["2.3.0"].invert
|
317
|
-
Framename2symbol["2.4.0"] = ID3::SUPPORTED_SYMBOLS["2.4.0"].invert
|
318
|
-
|
319
|
-
FrameType2FrameName = ID3::FRAMETYPE2FRAMENAME
|
320
|
-
|
321
|
-
FrameName2FrameType = FrameType2FrameName.invert
|
322
|
-
|
323
|
-
# ----------------------------------------------------------------------------
|
324
|
-
# the following piece of code is just for debugging, to sanity-check that all
|
325
|
-
# the FrameSymbols map back to a FrameType -- otherwise the library code will
|
326
|
-
# break if we encounter a Frame which can't be mapped to a FrameType..
|
327
|
-
# ----------------------------------------------------------------------------
|
328
|
-
#
|
329
|
-
# ensure we have a FrameType defined for each FrameName, otherwise
|
330
|
-
# code might break later..
|
331
|
-
#
|
332
|
-
|
333
|
-
# print "\nMISSING SYMBOLS:\n"
|
334
|
-
|
335
|
-
(ID3::Framename2symbol["2.2.0"].values +
|
336
|
-
ID3::Framename2symbol["2.3.0"].values +
|
337
|
-
ID3::Framename2symbol["2.4.0"].values).uniq.each { |symbol|
|
338
|
-
# print "#{symbol} " if ! ID3::FrameName2FrameType[symbol]
|
339
|
-
print "SYMBOL: #{symbol} not defined!\n" if ! ID3::FrameName2FrameType[symbol]
|
340
|
-
}
|
341
|
-
# print "\n\n"
|
342
|
-
|
343
|
-
# ----------------------------------------------------------------------------
|
344
|
-
# MODULE FUNCTIONS:
|
345
|
-
# ----------------------------------------------------------------------------
|
346
|
-
# The ID3 module functions are to query or modify files directly.
|
347
|
-
# They check directly if a file has a ID3-tag, but they don't parse the tags!
|
348
|
-
|
349
|
-
|
350
|
-
# ----------------------------------------------------------------------------
|
351
|
-
# hasID3v1tag?
|
352
|
-
# returns string with version 1.0 or 1.1 if tag was found
|
353
|
-
# returns false otherwise
|
354
|
-
|
355
|
-
def ID3.hasID3v1tag?(filename)
|
356
|
-
hasID3v1tag = false
|
357
|
-
|
358
|
-
# be careful with empty or corrupt files..
|
359
|
-
return false if File.size(filename) < ID3v1tagSize
|
360
|
-
|
361
|
-
f = File.open(filename, 'r')
|
362
|
-
f.seek(-ID3v1tagSize, IO::SEEK_END)
|
363
|
-
if (f.read(3) == "TAG")
|
364
|
-
f.seek(-ID3v1tagSize + ID3v1versionbyte, IO::SEEK_END)
|
365
|
-
c = f.getc; # this is character 125 of the tag
|
366
|
-
if (c == 0)
|
367
|
-
hasID3v1tag = "1.1"
|
368
|
-
else
|
369
|
-
hasID3v1tag = "1.0"
|
370
|
-
end
|
371
|
-
end
|
372
|
-
f.close
|
373
|
-
return hasID3v1tag
|
374
|
-
end
|
375
|
-
|
376
|
-
# ----------------------------------------------------------------------------
|
377
|
-
# hasID3v2tag?
|
378
|
-
# returns string with version 2.2.0, 2.3.0 or 2.4.0 if tag found
|
379
|
-
# returns false otherwise
|
380
|
-
|
381
|
-
def ID3.hasID3v2tag?(filename)
|
382
|
-
hasID3v2tag = false
|
383
|
-
|
384
|
-
f = File.open(filename, 'r')
|
385
|
-
if (f.read(3) == "ID3")
|
386
|
-
major = f.getc
|
387
|
-
minor = f.getc
|
388
|
-
version = "2." + major.to_s + '.' + minor.to_s
|
389
|
-
hasID3v2tag = version
|
390
|
-
end
|
391
|
-
f.close
|
392
|
-
return hasID3v2tag
|
393
|
-
end
|
394
|
-
|
395
|
-
# ----------------------------------------------------------------------------
|
396
|
-
# hasID3tag?
|
397
|
-
# returns string with all versions found, space separated
|
398
|
-
# returns false otherwise
|
399
|
-
|
400
|
-
def ID3.hasID3tag?(filename)
|
401
|
-
v1 = ID3.hasID3v1tag?(filename)
|
402
|
-
v2 = ID3.hasID3v2tag?(filename)
|
403
|
-
|
404
|
-
return false if !v1 && !v2
|
405
|
-
return v1 if !v2
|
406
|
-
return v2 if !v1
|
407
|
-
return "#{v1} #{v2}"
|
408
|
-
end
|
409
|
-
|
410
|
-
# ----------------------------------------------------------------------------
|
411
|
-
# removeID3v1tag
|
412
|
-
# returns nil if no v1 tag was found, or it couldn't be removed
|
413
|
-
# returns true if v1 tag found and it was removed..
|
414
|
-
#
|
415
|
-
# in the future:
|
416
|
-
# returns ID3.Tag1 object if a v1 tag was found and removed
|
417
|
-
|
418
|
-
def ID3.removeID3v1tag(filename)
|
419
|
-
stat = File.stat(filename)
|
420
|
-
if stat.file? && stat.writable? && ID3.hasID3v1tag?(filename)
|
421
|
-
|
422
|
-
# CAREFUL: this does not check if there really is a valid tag,
|
423
|
-
# that's why we need to check above!!
|
424
|
-
|
425
|
-
newsize = stat.size - ID3v1tagSize
|
426
|
-
File.open(filename, "r+") { |f| f.truncate(newsize) }
|
427
|
-
|
428
|
-
return true
|
429
|
-
else
|
430
|
-
return nil
|
431
|
-
end
|
432
|
-
end
|
433
|
-
# ----------------------------------------------------------------------------
|
434
|
-
|
435
|
-
|
436
|
-
# ==============================================================================
|
437
|
-
# Class AudioFile may call this ID3File
|
438
|
-
#
|
439
|
-
# reads and parses audio files for tags
|
440
|
-
# writes audio files and attaches dumped tags to it..
|
441
|
-
# revert feature would be nice to have..
|
442
|
-
#
|
443
|
-
# If we query and AudioFile object, we query what's currently associated with it
|
444
|
-
# e.g. we're not querying the file itself, but the Tag object which is perhaps modified.
|
445
|
-
# To query the file itself, use the ID3 module functions
|
446
|
-
|
447
|
-
class AudioFile
|
448
|
-
|
449
|
-
attr_reader :audioStartX , :audioEndX # begin and end indices of audio data in file
|
450
|
-
attr_reader :audioMD5sum # MD5sum of the audio portion of the file
|
451
|
-
|
452
|
-
attr_reader :pwd, :filename # PWD and relative path/name how file was first referenced
|
453
|
-
attr_reader :dirname, :basename # absolute dirname and basename of the file (computed)
|
454
|
-
|
455
|
-
attr_accessor :tagID3v1, :tagID3v2
|
456
|
-
|
457
|
-
# ----------------------------------------------------------------------------
|
458
|
-
# initialize
|
459
|
-
#
|
460
|
-
# AudioFile.new does NOT keep the file open, but scans it and parses the info
|
461
|
-
|
462
|
-
# e.g.: ID3::AudioFile.new('mp3/a.mp3')
|
463
|
-
|
464
|
-
def initialize(filename)
|
465
|
-
@filename = filename # similar to path method from class File, which is a mis-nomer!
|
466
|
-
@pwd = ENV["PWD"]
|
467
|
-
@dirname = File.dirname( "#{@pwd}/#{@filename}" ) # just sugar
|
468
|
-
@basename = File.basename( "#{@pwd}/#{@filename}" ) # just sugar
|
469
|
-
|
470
|
-
@tagID3v1 = nil
|
471
|
-
@tagID3v2 = nil
|
472
|
-
|
473
|
-
audioStartX = 0
|
474
|
-
audioEndX = File.size(filename) - 1 # points to the last index
|
475
|
-
|
476
|
-
if ID3.hasID3v1tag?(@filename)
|
477
|
-
@tagID3v1 = Tag1.new
|
478
|
-
@tagID3v1.read(@filename)
|
479
|
-
|
480
|
-
audioEndX -= ID3::ID3v1tagSize
|
481
|
-
end
|
482
|
-
if ID3.hasID3v2tag?(@filename)
|
483
|
-
@tagID3v2 = Tag2.new
|
484
|
-
@tagID3v2.read(@filename)
|
485
|
-
|
486
|
-
audioStartX = @tagID3v2.raw.size
|
487
|
-
end
|
488
|
-
|
489
|
-
# audioStartX audioEndX indices into the file need to be set
|
490
|
-
@audioStartX = audioStartX # first byte of audio data
|
491
|
-
@audioEndX = audioEndX # last byte of audio data
|
492
|
-
|
493
|
-
# user may compute the MD5sum of the audio content later..
|
494
|
-
# but we're only doing this if the user requests it..
|
495
|
-
# because MD5sum computation takes a little bit time.
|
496
|
-
|
497
|
-
@audioMD5sum = nil
|
498
|
-
end
|
499
|
-
|
500
|
-
# ----------------------------------------------------------------------------
|
501
|
-
def audioLength
|
502
|
-
@audioEndX - @audioStartX + 1
|
503
|
-
end
|
504
|
-
# ----------------------------------------------------------------------------
|
505
|
-
# write
|
506
|
-
# write the AudioFile to file, including any ID3-tags
|
507
|
-
# We keep backups if we write to a specific filename
|
508
|
-
|
509
|
-
def write(*filename)
|
510
|
-
backups = false
|
511
|
-
|
512
|
-
if filename.size == 0 # this is an Array!!
|
513
|
-
filename = @filename
|
514
|
-
backups = true # keep backups if we write to a specific filename
|
515
|
-
else
|
516
|
-
filename = filename[0]
|
517
|
-
backups = false
|
518
|
-
end
|
519
|
-
|
520
|
-
tf = Tempfile.new( @basename )
|
521
|
-
tmpname = tf.path
|
522
|
-
|
523
|
-
# write ID3v2 tag:
|
524
|
-
|
525
|
-
if @tagID3v2
|
526
|
-
tf.write( @tagID3v2.dump )
|
527
|
-
end
|
528
|
-
|
529
|
-
# write Audio Data:
|
530
|
-
|
531
|
-
File.open( @filename ) { |f|
|
532
|
-
f.seek(@audioStartX)
|
533
|
-
tf.write( f.read(@audioEndX - @audioStartX +1) )
|
534
|
-
}
|
535
|
-
|
536
|
-
# write ID3v1 tag:
|
537
|
-
|
538
|
-
if @tagID3v1
|
539
|
-
tf.write( @tagID3v1.dump )
|
540
|
-
end
|
541
|
-
|
542
|
-
tf.close
|
543
|
-
|
544
|
-
# now some logic about moving the tempfile and replacing the original
|
3
|
+
require 'active_support' # we'll borrow OrdreedHash from here.. no need to reinvent the wheel
|
545
4
|
|
546
|
-
bakname = filename + '.bak'
|
547
|
-
File.move(filename, bakname) if backups && FileTest.exists?(filename) && ! FileTest.exists?(bakname)
|
548
5
|
|
549
|
-
|
550
|
-
tf.close(true)
|
551
|
-
|
552
|
-
# write md5sum file:
|
553
|
-
|
554
|
-
writeMD5sum if @audioMD5sum
|
6
|
+
require 'helpers/ruby_1.8_1.9_compatibility' # define helper methods, so that we have the same interface for Ruby 1.8 and 1.9
|
555
7
|
|
556
|
-
|
557
|
-
|
558
|
-
# ----------------------------------------------------------------------------
|
559
|
-
# writeAudio
|
560
|
-
# only for debugging, does not write any ID3-tags, but just the audio portion
|
561
|
-
|
562
|
-
def writeAudio
|
563
|
-
tf = Tempfile.new( @basename )
|
564
|
-
|
565
|
-
File.open( @filename ) { |f|
|
566
|
-
f.seek(@audioStartX)
|
567
|
-
tf.write( f.read(@audioEndX - @audioStartX + 1) )
|
568
|
-
}
|
569
|
-
tf.close
|
570
|
-
path = tf.path
|
571
|
-
|
572
|
-
tf.open
|
573
|
-
tf.close(true)
|
574
|
-
end
|
575
|
-
|
576
|
-
|
577
|
-
# ----------------------------------------------------------------------------
|
578
|
-
# NOTE on md5sum's:
|
579
|
-
# If you don't know what an md5sum is, you can think of it as a unique
|
580
|
-
# fingerprint of a file or some data. I added the md5sum computation to
|
581
|
-
# help users keep track of their converted songs - even if the ID3-tag of
|
582
|
-
# a file changes, the md5sum of the audio data does not change..
|
583
|
-
# The md5sum can help you ensure that the audio-portion of the file
|
584
|
-
# was not changed after modifying, adding or deleting ID3-tags.
|
585
|
-
|
586
|
-
# ----------------------------------------------------------------------------
|
587
|
-
# audioMD5sum
|
588
|
-
# if the user tries to access @audioMD5sum, it will be computed for him,
|
589
|
-
# unless it was previously computed. We try to calculate that only once
|
590
|
-
# and on demand, because it's a bit expensive to compute..
|
591
|
-
|
592
|
-
def audioMD5sum
|
593
|
-
if ! @audioMD5sum
|
594
|
-
|
595
|
-
File.open( File.join(@dirname,@basename) ) { |f|
|
596
|
-
f.seek(@audioStartX)
|
597
|
-
@audioMD5sum = MD5.new( f.read(@audioEndX - @audioStartX + 1) )
|
598
|
-
}
|
8
|
+
require 'helpers/hash_extensions' # loads Hash#inverse and overloads Hash#invert
|
9
|
+
require 'helpers/restricted_ordered_hash' # derived from OrderedHash; used throughout ID3 library
|
599
10
|
|
600
|
-
|
601
|
-
|
602
|
-
end
|
603
|
-
# ----------------------------------------------------------------------------
|
604
|
-
# writeMD5sum
|
605
|
-
# write the filename and MD5sum of the audio portion into an ascii file
|
606
|
-
# in the same location as the audio file, but with suffix .md5
|
607
|
-
#
|
608
|
-
# computes the @audioMD5sum, if it wasn't previously computed..
|
11
|
+
# load hexdump method to extend class String
|
12
|
+
require "helpers/hexdump" # only needed for debugging -> autoload
|
609
13
|
|
610
|
-
|
611
|
-
|
612
|
-
self.audioMD5sum if ! @audioMD5sum # compute MD5sum if it's not computed yet
|
613
|
-
|
614
|
-
base = @basename.sub( /(.)\.[^.]+$/ , '\1')
|
615
|
-
base += '.md5'
|
616
|
-
File.open( File.join(@dirname,base) ,"w") { |f|
|
617
|
-
f.printf("%s %s\n", File.join(@dirname,@basename), @audioMD5sum)
|
618
|
-
}
|
619
|
-
@audioMD5sum
|
620
|
-
end
|
621
|
-
# ----------------------------------------------------------------------------
|
622
|
-
# verifyMD5sum
|
623
|
-
# compare the audioMD5sum against a previously stored md5sum file
|
624
|
-
# and returns boolean value of comparison
|
625
|
-
#
|
626
|
-
# If no md5sum file existed, we create one and return true.
|
627
|
-
#
|
628
|
-
# computes the @audioMD5sum, if it wasn't previously computed..
|
14
|
+
require 'id3/string_extensions' # adds ID3 methods to String
|
15
|
+
require 'id3/io_extensions' # adds ID3 methods to IO and File
|
629
16
|
|
630
|
-
|
17
|
+
require 'id3/constants' # Constants used throughout the code
|
18
|
+
require 'id3/module_methods' # add ID3 methods to ID3 which operate on filenames
|
631
19
|
|
632
|
-
|
633
|
-
|
634
|
-
self.audioMD5sum if ! @audioMD5sum # compute MD5sum if it's not computed yet
|
20
|
+
require 'id3/audiofile' # Higher-Level access to Audio Files with ID3 tags
|
635
21
|
|
636
|
-
base = @basename.sub( /(.)\.[^.]+$/ , '\1') # remove suffix from audio-file
|
637
|
-
base += '.md5' # add new suffix .md5
|
638
|
-
md5name = File.join(@dirname,base)
|
639
|
-
|
640
|
-
# if a MD5-file doesn't exist, we should create one and return TRUE ...
|
641
|
-
if File.exists?(md5name)
|
642
|
-
File.open( md5name ,"r") { |f|
|
643
|
-
oldname,oldMD5sum = f.readline.split # read old MD5-sum
|
644
|
-
}
|
645
|
-
else
|
646
|
-
oldMD5sum = self.writeMD5sum # create MD5-file and return true..
|
647
|
-
end
|
648
|
-
@audioMD5sum == oldMD5sum
|
649
|
-
|
650
|
-
end
|
651
|
-
# ----------------------------------------------------------------------------
|
652
|
-
# version aka versions
|
653
|
-
# queries the tag objects and returns the version numbers of those tags
|
654
|
-
# NOTE: this does not reflect what's currently in the file, but what's
|
655
|
-
# currently in the AudioFile object
|
656
|
-
|
657
|
-
def version
|
658
|
-
a = Array.new
|
659
|
-
a.push(@tagID3v1.version) if @tagID3v1
|
660
|
-
a.push(@tagID3v2.version) if @tagID3v2
|
661
|
-
return nil if a == []
|
662
|
-
a.join(' ')
|
663
|
-
end
|
664
|
-
alias versions version
|
665
|
-
# ----------------------------------------------------------------------------
|
666
22
|
|
667
|
-
|
668
|
-
|
669
|
-
|
23
|
+
require 'id3/generic_tag'
|
24
|
+
require 'id3/tag1' # ID3v1 tag class
|
25
|
+
require 'id3/tag2' # ID3v2 tag class
|
26
|
+
require 'id3/frame' # ID3v2 frame class
|
27
|
+
require 'id3/frame_array' # Array extension for arrays of ID3::Frame s
|
670
28
|
|
671
|
-
|
672
|
-
# ==============================================================================
|
673
|
-
# Class RestrictedOrderedHash
|
674
|
-
# this is a helper Class for ID3::Frame
|
675
|
-
#
|
676
|
-
|
677
|
-
class RestrictedOrderedHash < Hash
|
678
29
|
|
679
|
-
|
680
|
-
|
681
|
-
def lock
|
682
|
-
@locked = true
|
683
|
-
end
|
684
|
-
|
685
|
-
def initialize
|
686
|
-
@locked = false
|
687
|
-
@count = 0
|
688
|
-
@order = []
|
689
|
-
super
|
690
|
-
end
|
691
|
-
|
692
|
-
alias old_store []=
|
693
|
-
|
694
|
-
def []= (key,val)
|
695
|
-
if self[key]
|
696
|
-
self.old_store(key,val)
|
697
|
-
else
|
698
|
-
if @locked
|
699
|
-
# we're not allowed to add new keys!
|
700
|
-
raise ArgumentError, "You can not add new keys! The ID3-frame #{@name} has fixed entries!\n" +
|
701
|
-
" valid key are: " + self.keys.join(",") +"\n"
|
702
|
-
|
703
|
-
else
|
704
|
-
@count += 1
|
705
|
-
@order += [key]
|
706
|
-
self.old_store(key,val)
|
707
|
-
end
|
708
|
-
end
|
709
|
-
end
|
710
|
-
|
711
|
-
def values
|
712
|
-
array = []
|
713
|
-
@order.each { |key|
|
714
|
-
array.push self[key]
|
715
|
-
}
|
716
|
-
array
|
717
|
-
end
|
718
|
-
|
719
|
-
# returns the human-readable ordered hash in correct order .. ;-)
|
720
|
-
|
721
|
-
def inspect
|
722
|
-
first = true
|
723
|
-
str = "{"
|
724
|
-
self.order.each{ |key|
|
725
|
-
str += ", " if !first
|
726
|
-
str += key.inspect
|
727
|
-
str += "=>"
|
728
|
-
str += (self[key]).inspect
|
729
|
-
first = false
|
730
|
-
}
|
731
|
-
str +="}"
|
732
|
-
end
|
733
|
-
|
734
|
-
# users can not delete entries from a locked hash..
|
735
|
-
|
736
|
-
alias old_delete delete
|
737
|
-
|
738
|
-
def delete (key)
|
739
|
-
if !@locked
|
740
|
-
old_delete(key)
|
741
|
-
@order.delete(key)
|
742
|
-
end
|
743
|
-
end
|
744
|
-
|
745
|
-
end
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
# ==============================================================================
|
750
|
-
# Class GenericTag
|
751
|
-
#
|
752
|
-
# Helper class for Tag1 and Tag2
|
753
|
-
#
|
754
|
-
# Checks that user uses a valid key, and adds methods for size computation
|
755
|
-
#
|
756
|
-
# as per ID3-definition, the frames are in no fixed order! that's why we can derive
|
757
|
-
# this class from Hash. But in the future we may want to write certain frames first
|
758
|
-
# into the ID3-tag and therefore may want to derive it from RestrictedOrderedHash
|
759
|
-
|
760
|
-
class GenericTag < Hash
|
761
|
-
attr_reader :version, :raw
|
762
|
-
|
763
|
-
# these definitions are to prevent users from inventing their own field names..
|
764
|
-
# but on the other hand, they should be able to create a new valid field, if
|
765
|
-
# it's not yet in the current tag, but it's valid for that ID3-version...
|
766
|
-
|
767
|
-
alias old_set []=
|
768
|
-
private :old_set
|
30
|
+
# module ID3
|
769
31
|
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
if ID3::SUPPORTED_SYMBOLS[@version].keys.include?(key)
|
776
|
-
old_set(key,val)
|
777
|
-
else
|
778
|
-
# exception
|
779
|
-
raise ArgumentError, "Incorrect ID3-field \"#{key}\" for ID3 version #{@version}\n" +
|
780
|
-
" valid fields are: " + SUPPORTED_SYMBOLS[@version].keys.join(",") +"\n"
|
781
|
-
end
|
782
|
-
end
|
783
|
-
end
|
784
|
-
# ----------------------------------------------------------------------
|
785
|
-
# convert the 4 bytes found in the id3v2 header and return the size
|
786
|
-
private
|
787
|
-
def unmungeSize(bytes)
|
788
|
-
size = 0
|
789
|
-
j = 0; i = 3
|
790
|
-
while i >= 0
|
791
|
-
size += 128**i * (bytes[j] & 0x7f)
|
792
|
-
j += 1
|
793
|
-
i -= 1
|
794
|
-
end
|
795
|
-
return size
|
796
|
-
end
|
797
|
-
# ----------------------------------------------------------------------
|
798
|
-
# convert the size into 4 bytes to be written into an id3v2 header
|
799
|
-
private
|
800
|
-
def mungeSize(size)
|
801
|
-
bytes = Array.new(4,0)
|
802
|
-
j = 0; i = 3
|
803
|
-
while i >= 0
|
804
|
-
bytes[j],size = size.divmod(128**i)
|
805
|
-
j += 1
|
806
|
-
i -= 1
|
807
|
-
end
|
808
|
-
|
809
|
-
return bytes
|
810
|
-
end
|
811
|
-
# ----------------------------------------------------------------------------
|
812
|
-
|
813
|
-
end # of class GenericTag
|
814
|
-
|
815
|
-
# ==============================================================================
|
816
|
-
# Class Tag1 ID3 Version 1.x Tag
|
817
|
-
#
|
818
|
-
# parses ID3v1 tags from a binary array
|
819
|
-
# dumps ID3v1 tags into a binary array
|
820
|
-
# allows to modify tag's contents
|
821
|
-
|
822
|
-
class Tag1 < GenericTag
|
823
|
-
|
824
|
-
# ----------------------------------------------------------------------
|
825
|
-
# read reads a version 1.x ID3tag
|
826
|
-
#
|
827
|
-
|
828
|
-
def read(filename)
|
829
|
-
f = File.open(filename, 'r')
|
830
|
-
f.seek(-ID3::ID3v1tagSize, IO::SEEK_END)
|
831
|
-
hastag = (f.read(3) == 'TAG')
|
832
|
-
if hastag
|
833
|
-
f.seek(-ID3::ID3v1tagSize, IO::SEEK_END)
|
834
|
-
@raw = f.read(ID3::ID3v1tagSize)
|
835
|
-
|
836
|
-
# self.parse!(raw) # we should use "parse!" instead of duplicating code!
|
837
|
-
|
838
|
-
if (raw[ID3v1versionbyte] == 0)
|
839
|
-
@version = "1.1"
|
840
|
-
else
|
841
|
-
@version = "1.0"
|
842
|
-
end
|
843
|
-
else
|
844
|
-
@raw = @version = nil
|
845
|
-
end
|
846
|
-
f.close
|
847
|
-
#
|
848
|
-
# now parse all the fields
|
849
|
-
|
850
|
-
ID3::SUPPORTED_SYMBOLS[@version].each{ |key,val|
|
851
|
-
if val.class == Range
|
852
|
-
self[key] = @raw[val].squeeze(" \000").chomp(" ").chomp("\000")
|
853
|
-
elsif val.class == Fixnum
|
854
|
-
self[key] = @raw[val].to_s
|
855
|
-
else
|
856
|
-
# this can't happen the way we defined the hash..
|
857
|
-
# printf "unknown key/val : #{key} / #{val} ; val-type: %s\n", val.type
|
858
|
-
end
|
859
|
-
}
|
860
|
-
hastag
|
861
|
-
end
|
862
|
-
# ----------------------------------------------------------------------
|
863
|
-
# write writes a version 1.x ID3tag
|
864
|
-
#
|
865
|
-
# not implemented yet..
|
866
|
-
#
|
867
|
-
# need to loacte old tag, and remove it, then append new tag..
|
868
|
-
#
|
869
|
-
# always upgrade version 1.0 to 1.1 when writing
|
870
|
-
|
871
|
-
# not yet implemented, because AudioFile.write does the job better
|
872
|
-
|
873
|
-
# ----------------------------------------------------------------------
|
874
|
-
# this routine modifies self, e.g. the Tag1 object
|
875
|
-
#
|
876
|
-
# tag.parse!(raw) returns boolean value, showing if parsing was successful
|
877
|
-
|
878
|
-
def parse!(raw)
|
879
|
-
|
880
|
-
return false if raw.size != ID3::ID3v1tagSize
|
881
|
-
|
882
|
-
if (raw[ID3v1versionbyte] == 0)
|
883
|
-
@version = "1.1"
|
884
|
-
else
|
885
|
-
@version = "1.0"
|
886
|
-
end
|
887
|
-
|
888
|
-
self.clear # remove all entries from Hash, we don't want left-overs..
|
889
|
-
|
890
|
-
ID3::SUPPORTED_SYMBOLS[@version].each{ |key,val|
|
891
|
-
if val.class == Range
|
892
|
-
self[key] = raw[val].squeeze(" \000").chomp(" ").chomp("\000")
|
893
|
-
elsif val.class == Fixnum
|
894
|
-
self[key] = raw[val].to_s
|
895
|
-
else
|
896
|
-
# this can't happen the way we defined the hash..
|
897
|
-
# printf "unknown key/val : #{key} / #{val} ; val-type: %s\n", val.class
|
898
|
-
end
|
899
|
-
}
|
900
|
-
@raw = raw
|
901
|
-
return true
|
902
|
-
end
|
903
|
-
# ----------------------------------------------------------------------
|
904
|
-
# dump version 1.1 ID3 Tag into a binary array
|
905
|
-
#
|
906
|
-
# although we provide this method, it's stongly discouraged to use it,
|
907
|
-
# because ID3 version 1.x tags are inferior to version 2.x tags, as entries
|
908
|
-
# are often truncated and hence ID3 v1 tags are often useless..
|
909
|
-
|
910
|
-
def dump
|
911
|
-
zeroes = "\0" * 32
|
912
|
-
raw = "\0" * ID3::ID3v1tagSize
|
913
|
-
raw[0..2] = 'TAG'
|
914
|
-
|
915
|
-
self.each{ |key,value|
|
916
|
-
|
917
|
-
range = ID3::Symbol2framename['1.1'][key]
|
918
|
-
|
919
|
-
if range.class == Range
|
920
|
-
length = range.last - range.first + 1
|
921
|
-
paddedstring = value + zeroes
|
922
|
-
raw[range] = paddedstring[0..length-1]
|
923
|
-
elsif range.class == Fixnum
|
924
|
-
raw[range] = value.to_i
|
925
|
-
else
|
926
|
-
# this can't happen the way we defined the hash..
|
927
|
-
next
|
928
|
-
end
|
929
|
-
}
|
930
|
-
|
931
|
-
return raw
|
932
|
-
end
|
933
|
-
# ----------------------------------------------------------------------
|
934
|
-
end # of class Tag1
|
935
|
-
|
936
|
-
# ==============================================================================
|
937
|
-
# Class Tag2 ID3 Version 2.x.y Tag
|
938
|
-
#
|
939
|
-
# parses ID3v2 tags from a binary array
|
940
|
-
# dumps ID3v2 tags into a binary array
|
941
|
-
# allows to modify tag's contents
|
942
|
-
#
|
943
|
-
# as per definition, the frames are in no fixed order
|
944
|
-
|
945
|
-
class Tag2 < GenericTag
|
946
|
-
|
947
|
-
attr_reader :rawflags, :flags
|
948
|
-
|
949
|
-
def initalize
|
950
|
-
@rawflags = 0
|
951
|
-
@flags = {}
|
952
|
-
super
|
953
|
-
end
|
954
|
-
|
955
|
-
def read(filename)
|
956
|
-
f = File.open(filename, 'r')
|
957
|
-
hastag = (f.read(3) == "ID3")
|
958
|
-
if hastag
|
959
|
-
major = f.getc
|
960
|
-
minor = f.getc
|
961
|
-
@version = "2." + major.to_s + '.' + minor.to_s
|
962
|
-
@rawflags = f.getc
|
963
|
-
size = ID3::ID3v2headerSize + unmungeSize(f.read(4))
|
964
|
-
f.seek(0)
|
965
|
-
@raw = f.read(size)
|
966
|
-
|
967
|
-
# parse the raw flags:
|
968
|
-
if (@rawflags & TAG_HEADER_FLAG_MASK[@version] != 0)
|
969
|
-
# in this case we need to skip parsing the frame... and skip to the next one...
|
970
|
-
wrong = @rawflags & TAG_HEADER_FLAG_MASK[@version]
|
971
|
-
error = printf "ID3 version %s header flags 0x%X contain invalid flags 0x%X !\n", @version, @rawflags, wrong
|
972
|
-
raise ArgumentError, error
|
973
|
-
end
|
974
|
-
|
975
|
-
@flags = Hash.new
|
976
|
-
|
977
|
-
TAG_HEADER_FLAGS[@version].each{ |key,val|
|
978
|
-
# only define the flags which are set..
|
979
|
-
@flags[key] = true if (@rawflags & val == 1)
|
980
|
-
}
|
981
|
-
|
982
|
-
|
983
|
-
else
|
984
|
-
@raw = nil
|
985
|
-
@version = nil
|
986
|
-
return false
|
987
|
-
end
|
988
|
-
f.close
|
989
|
-
#
|
990
|
-
# now parse all the frames
|
991
|
-
#
|
992
|
-
i = ID3::ID3v2headerSize; # we start parsing right after the ID3v2 header
|
993
|
-
|
994
|
-
while (i < @raw.size) && (@raw[i] != 0)
|
995
|
-
len,frame = parse_frame_header(i) # this will create the correct frame
|
996
|
-
if len != 0
|
997
|
-
i += len
|
998
|
-
else
|
999
|
-
break
|
1000
|
-
end
|
1001
|
-
end
|
1002
|
-
|
1003
|
-
hastag
|
1004
|
-
end
|
1005
|
-
|
1006
|
-
# ----------------------------------------------------------------------
|
1007
|
-
# write
|
1008
|
-
#
|
1009
|
-
# writes and replaces existing ID3-v2-tag if one is present
|
1010
|
-
# Careful, this does NOT merge or append, it overwrites!
|
1011
|
-
|
1012
|
-
# not yet implemented, because AudioFile.write does the job better
|
1013
|
-
|
1014
|
-
# def write(filename)
|
1015
|
-
# check how long the old ID3-v2 tag is
|
1016
|
-
|
1017
|
-
# dump ID3-v2-tag
|
1018
|
-
|
1019
|
-
# append old audio to new tag
|
1020
|
-
|
1021
|
-
# end
|
1022
|
-
|
1023
|
-
# ----------------------------------------------------------------------------
|
1024
|
-
# writeID3v2
|
1025
|
-
# just writes the ID3v2 tag by itself into a file, no audio data is written
|
1026
|
-
#
|
1027
|
-
# for backing up ID3v2 tags and debugging only..
|
1028
|
-
#
|
1029
|
-
|
1030
|
-
# def writeID3v2
|
1031
|
-
|
1032
|
-
# end
|
1033
|
-
|
1034
|
-
# ----------------------------------------------------------------------
|
1035
|
-
# parse_frame_header
|
1036
|
-
#
|
1037
|
-
# each frame consists of a header of fixed length;
|
1038
|
-
# depending on the ID3version, either 6 or 10 bytes.
|
1039
|
-
# and of a data portion which is of variable length,
|
1040
|
-
# and which contents might not be parsable by us
|
1041
|
-
#
|
1042
|
-
# INPUT: index to where in the @raw data the frame starts
|
1043
|
-
# RETURNS: if successful parse:
|
1044
|
-
# total size in bytes, ID3frame struct
|
1045
|
-
# else:
|
1046
|
-
# 0, nil
|
1047
|
-
#
|
1048
|
-
#
|
1049
|
-
# Struct of type ID3frame which contains:
|
1050
|
-
# the name, size (in bytes), headerX,
|
1051
|
-
# dataStartX, dataEndX, flags
|
1052
|
-
# the data indices point into the @raw data, so we can cut out
|
1053
|
-
# and parse the data at a later point in time.
|
1054
|
-
#
|
1055
|
-
# total frame size = dataEndX - headerX
|
1056
|
-
# total header size= dataStartX - headerX
|
1057
|
-
# total data size = dataEndX - dataStartX
|
1058
|
-
#
|
1059
|
-
private
|
1060
|
-
def parse_frame_header(x)
|
1061
|
-
framename = ""; flags = nil
|
1062
|
-
size = 0
|
1063
|
-
|
1064
|
-
if @version =~ /^2\.2\./
|
1065
|
-
frameHeaderSize = 6 # 2.2.x Header Size is 6 bytes
|
1066
|
-
header = @raw[x..x+frameHeaderSize-1]
|
1067
|
-
|
1068
|
-
framename = header[0..2]
|
1069
|
-
size = (header[3]*256**2)+(header[4]*256)+header[5]
|
1070
|
-
flags = nil
|
1071
|
-
# printf "frame: %s , size: %d\n", framename , size
|
1072
|
-
|
1073
|
-
elsif @version =~ /^2\.[34]\./
|
1074
|
-
# for version 2.3.0 and 2.4.0 the header is 10 bytes long
|
1075
|
-
frameHeaderSize = 10
|
1076
|
-
header = @raw[x..x+frameHeaderSize-1]
|
1077
|
-
|
1078
|
-
framename = header[0..3]
|
1079
|
-
size = (header[4]*256**3)+(header[5]*256**2)+(header[6]*256)+header[7]
|
1080
|
-
flags= header[8..9]
|
1081
|
-
# printf "frame: %s , size: %d, flags: %s\n", framename , size, flags
|
1082
|
-
|
1083
|
-
else
|
1084
|
-
# we can't parse higher versions
|
1085
|
-
return 0, false
|
1086
|
-
end
|
1087
|
-
|
1088
|
-
# if this is a valid frame of known type, we return it's total length and a struct
|
1089
|
-
#
|
1090
|
-
if ID3::SUPPORTED_SYMBOLS[@version].has_value?(framename)
|
1091
|
-
frame = ID3::Frame.new(self, framename, x, x+frameHeaderSize , x+frameHeaderSize + size - 1 , flags)
|
1092
|
-
self[ Framename2symbol[@version][frame.name] ] = frame
|
1093
|
-
return size+frameHeaderSize , frame
|
1094
|
-
else
|
1095
|
-
return 0, nil
|
1096
|
-
end
|
1097
|
-
end
|
1098
|
-
# ----------------------------------------------------------------------
|
1099
|
-
# dump a ID3-v2 tag into a binary array
|
1100
|
-
#
|
1101
|
-
# NOTE:
|
1102
|
-
# when "dumping" an ID3-v2 tag, I would like to have more control about
|
1103
|
-
# which frames get dumped first.. e.g. the most important frames (with the
|
1104
|
-
# most important information) should be dumped first..
|
1105
|
-
#
|
1106
|
-
|
1107
|
-
public
|
1108
|
-
def dump
|
1109
|
-
data = ""
|
1110
|
-
|
1111
|
-
# dump all the frames
|
1112
|
-
self.each { |framename,framedata|
|
1113
|
-
data << framedata.dump
|
1114
|
-
}
|
1115
|
-
# add some padding perhaps 32 bytes (should be defined by the user!)
|
1116
|
-
# NOTE: I noticed that iTunes adds excessive amounts of padding
|
1117
|
-
data << "\0" * 32
|
1118
|
-
|
1119
|
-
# calculate the complete length of the data-section
|
1120
|
-
size = mungeSize(data.size)
|
1121
|
-
|
1122
|
-
major,minor = @version.sub(/^2\.([0-9])\.([0-9])/, '\1 \2').split
|
1123
|
-
|
1124
|
-
# prepend a valid ID3-v2.x header to the data block
|
1125
|
-
header = "ID3" << major.to_i << minor.to_i << @rawflags << size[0] << size[1] << size[2] << size[3]
|
1126
|
-
|
1127
|
-
header + data
|
1128
|
-
end
|
1129
|
-
# ----------------------------------------------------------------------
|
1130
|
-
|
1131
|
-
end # of class Tag2
|
1132
|
-
|
1133
|
-
# ==============================================================================
|
1134
|
-
# Class Frame ID3 Version 2.x.y Frame
|
1135
|
-
#
|
1136
|
-
# parses ID3v2 frames from a binary array
|
1137
|
-
# dumps ID3v2 frames into a binary array
|
1138
|
-
# allows to modify frame's contents if the frame was decoded..
|
1139
|
-
#
|
1140
|
-
|
1141
|
-
class Frame < RestrictedOrderedHash
|
1142
|
-
|
1143
|
-
attr_reader :name, :version
|
1144
|
-
attr_reader :headerStartX, :dataStartX, :dataEndX, :rawdata, :rawheader # debugging only
|
1145
|
-
|
1146
|
-
# ----------------------------------------------------------------------
|
1147
|
-
# return the complete raw frame
|
1148
|
-
|
1149
|
-
def raw
|
1150
|
-
return @rawheader + @rawdata
|
1151
|
-
end
|
1152
|
-
# ----------------------------------------------------------------------
|
1153
|
-
alias old_init initialize
|
1154
|
-
|
1155
|
-
def initialize(tag, name, headerStartX, dataStartX, dataEndX, flags)
|
1156
|
-
@name = name
|
1157
|
-
@headerStartX = headerStartX
|
1158
|
-
@dataStartX = dataStartX
|
1159
|
-
@dataEndX = dataEndX
|
1160
|
-
|
1161
|
-
@rawdata = tag.raw[dataStartX..dataEndX]
|
1162
|
-
@rawheader = tag.raw[headerStartX..dataStartX-1]
|
1163
|
-
|
1164
|
-
# initialize the super class..
|
1165
|
-
old_init
|
1166
|
-
|
1167
|
-
# parse the darn flags, if there are any..
|
1168
|
-
|
1169
|
-
@version = tag.version # caching..
|
1170
|
-
case @version
|
1171
|
-
when /2\.2\.[0-9]/
|
1172
|
-
# no flags, no extra attributes necessary
|
1173
|
-
|
1174
|
-
when /2\.[34]\.0/
|
1175
|
-
|
1176
|
-
# dynamically create attributes and reader functions:
|
1177
|
-
instance_eval <<-EOB
|
1178
|
-
class << self
|
1179
|
-
attr_reader :rawflags, :flags
|
1180
|
-
end
|
1181
|
-
EOB
|
1182
|
-
|
1183
|
-
@rawflags = flags.to_i # preserve the raw flags (for debugging only)
|
1184
|
-
|
1185
|
-
if (flags.to_i & FRAME_HEADER_FLAG_MASK[@version] != 0)
|
1186
|
-
# in this case we need to skip parsing the frame... and skip to the next one...
|
1187
|
-
wrong = flags.to_i & FRAME_HEADER_FLAG_MASK[@version]
|
1188
|
-
error = printf "ID3 version %s frame header flags 0x%X contain invalid flags 0x%X !\n", @version, flags, wrong
|
1189
|
-
raise ArgumentError, error
|
1190
|
-
end
|
1191
|
-
|
1192
|
-
@flags = Hash.new
|
1193
|
-
|
1194
|
-
FRAME_HEADER_FLAGS[@version].each{ |key,val|
|
1195
|
-
# only define the flags which are set..
|
1196
|
-
@flags[key] = true if (flags.to_i & val == 1)
|
1197
|
-
}
|
1198
|
-
|
1199
|
-
else
|
1200
|
-
raise ArgumentError, "ID3 version #{@version} not recognized when parsing frame header flags\n"
|
1201
|
-
end # parsing flags
|
1202
|
-
|
1203
|
-
# generate method for parsing data
|
1204
|
-
|
1205
|
-
instance_eval <<-EOB
|
1206
|
-
class << self
|
1207
|
-
|
1208
|
-
def parse
|
1209
|
-
# here we GENERATE the code to parse, dump and verify methods
|
1210
|
-
|
1211
|
-
vars,packing = ID3::FRAME_PARSER[ ID3::FrameName2FrameType[ ID3::Framename2symbol[self.version][self.name]] ]
|
1212
|
-
|
1213
|
-
# debugging print-out:
|
1214
|
-
|
1215
|
-
if vars.class == Array
|
1216
|
-
vars2 = vars.join(",")
|
1217
|
-
else
|
1218
|
-
vars2 = vars
|
1219
|
-
end
|
1220
|
-
|
1221
|
-
values = self.rawdata.unpack(packing)
|
1222
|
-
vars.each { |key|
|
1223
|
-
self[key] = values.shift
|
1224
|
-
}
|
1225
|
-
self.lock # lock the OrderedHash
|
1226
|
-
end
|
1227
|
-
|
1228
|
-
def dump
|
1229
|
-
vars,packing = ID3::FRAME_PARSER[ ID3::FrameName2FrameType[ ID3::Framename2symbol[self.version][self.name]] ]
|
1230
|
-
|
1231
|
-
data = self.values.pack(packing) # we depend on an OrderedHash, so the values are in the correct order!!!
|
1232
|
-
header = self.name.dup # we want the value! not the reference!!
|
1233
|
-
len = data.length
|
1234
|
-
if self.version =~ /^2\.2\./
|
1235
|
-
byte2,rest = len.divmod(256**2)
|
1236
|
-
byte1,byte0 = rest.divmod(256)
|
1237
|
-
|
1238
|
-
header << byte2 << byte1 << byte0
|
1239
|
-
|
1240
|
-
elsif self.version =~ /^2\.[34]\./ # 10-byte header
|
1241
|
-
byte3,rest = len.divmod(256**3)
|
1242
|
-
byte2,rest = rest.divmod(256**2)
|
1243
|
-
byte1,byte0 = rest.divmod(256)
|
1244
|
-
|
1245
|
-
flags1,flags0 = self.rawflags.divmod(256)
|
1246
|
-
|
1247
|
-
header << byte3 << byte2 << byte1 << byte0 << flags1 << flags0
|
1248
|
-
end
|
1249
|
-
header << data
|
1250
|
-
end
|
1251
|
-
end
|
1252
|
-
EOB
|
1253
|
-
self.parse # now we're using the just defined parsing routine
|
1254
|
-
|
1255
|
-
self
|
1256
|
-
end
|
1257
|
-
# ----------------------------------------------------------------------
|
1258
|
-
|
1259
|
-
|
1260
|
-
|
1261
|
-
end # of class Frame
|
1262
|
-
|
1263
|
-
# ==============================================================================
|
1264
|
-
|
1265
|
-
|
32
|
+
# autoload :AudioFile , 'id3/audio_file.rb'
|
33
|
+
# autoload :GenericTag, 'id3/tag_generic.rb'
|
34
|
+
# autoload :Tag1 , 'id3/tag1.rb'
|
35
|
+
# autoload :Tag2 , 'id3/tag2.rb'
|
36
|
+
# autoload :Frame , 'id3/frame.rb'
|
1266
37
|
|
1267
|
-
|
38
|
+
# end
|