id3 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.html +146 -0
- data/README +23 -0
- data/docs/Class_AudioFile.html +110 -0
- data/docs/Class_Frame.html +125 -0
- data/docs/Class_Tag1.html +121 -0
- data/docs/Class_Tag2.html +167 -0
- data/docs/ID3-Standards/id3v2-00.html +1669 -0
- data/docs/ID3-Standards/id3v2-00.txt +1660 -0
- data/docs/ID3-Standards/id3v2.3.0.html +2033 -0
- data/docs/ID3-Standards/id3v2.3.0.txt +2025 -0
- data/docs/ID3-Standards/id3v2.4.0-changes.html +159 -0
- data/docs/ID3-Standards/id3v2.4.0-changes.txt +149 -0
- data/docs/ID3-Standards/id3v2.4.0-frames.html +1743 -0
- data/docs/ID3-Standards/id3v2.4.0-frames.txt +1732 -0
- data/docs/ID3-Standards/id3v2.4.0-structure.html +742 -0
- data/docs/ID3-Standards/id3v2.4.0-structure.txt +732 -0
- data/docs/ID3_comparison.html +2121 -0
- data/docs/ID3_comparison2.html +2043 -0
- data/docs/ID3v2_frames_comparison.html +2121 -0
- data/docs/ID3v2_frames_comparison.txt +197 -0
- data/docs/ID3v2_frames_overview.txt +60 -0
- data/docs/Module_ID3.html +146 -0
- data/docs/id3.html +245 -0
- data/docs/index.html +252 -0
- data/index.html +8 -0
- data/lib/hexdump.rb +113 -0
- data/lib/id3.rb +1177 -0
- data/lib/invert_hash.rb +105 -0
- metadata +75 -0
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 <tools-NOSPAM@unixgods.org><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. 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
|
+
<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:
|
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. 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:
|
100
|
+
"garbage in --> crash!" of the application which tries to make
|
101
|
+
sense of such frames later.. 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. <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! 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.
|
139
|
+
Currently there is no support for this. 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"
|
171
|
+
anytime. 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 overloaded with useless frame-types and
|
181
|
+
side-effects... The worst is ID3 v2.4.0! 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.. 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:
|
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>: who came up with all those special frame types? Cheez! KISS! (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
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
|