id3 0.4.0

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