id3 0.5.0 → 1.0.0.pre4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,178 @@
1
+ module ID3
2
+ # ==============================================================================
3
+ # Class Frame ID3 Version 2.x.y Frame
4
+ #
5
+ # parses ID3v2 frames from a binary array
6
+ # dumps ID3v2 frames into a binary array
7
+ # allows to modify frame's contents if the frame was decoded..
8
+ #
9
+ # NOTE: right now the class Frame is derived from Hash, which is wrong..
10
+ # It should really be derived from something like RestrictedOrderedHash
11
+ # ... a new class, which preserves the order of keys, and which does
12
+ # strict checking that all keys are present and reference correct values!
13
+ # e.g. frames["COMMENT"]
14
+ # ==> {"encoding"=>Byte, "language"=>Chars3, "text1"=>String, "text2"=>String}
15
+ #
16
+ # e.g. user should be able to create a new frame , like:
17
+ # tag2.frames["COMMENT"] = "right side"
18
+ #
19
+ # and the following checks should be done:
20
+ #
21
+ # 1) if "COMMENT" is a correct key for tag2
22
+ # 2) if the "right side" contains the correct keys
23
+ # 3) if the "right side" contains the correct value for each key
24
+ #
25
+ # In the simplest case, the "right side" might be just a string,
26
+ # but for most FrameTypes, it's a complex datastructure.. and we need
27
+ # to check it for correctness before doing the assignment..
28
+ #
29
+ # NOTE2: the class Tag2 should have hash-like accessor functions to let the user
30
+ # easily access frames and their contents..
31
+ #
32
+ # e.g. tag2[framename] would really access tag2.frames[framename]
33
+ #
34
+ # and if that works, we can make tag2.frames private and hidden!
35
+ #
36
+ # This means, that when we generate the parse and dump routines dynamically,
37
+ # we may want to create the corresponding accessor methods for Tag2 class
38
+ # as well...? or are generic ones enough?
39
+ #
40
+ #
41
+ # NOTE3:
42
+ #
43
+ # The old way to pack / unpack frames to encode / decode them, is working,
44
+ # but has the disadvantage that it's a little bit too close to the metal.
45
+ # e.g. encoding and textcontent are both accessible, but ideally only
46
+ # the textvalue should be accessible and settable, and the encoding should
47
+ # automatically be set correctly / accordingly...
48
+ #
49
+ # NOTE4:
50
+ # for frames like TXXX , WXXX , which can occur multiple times in a ID3v2 frame,
51
+ # we should manage those tags as an Array...
52
+ #
53
+
54
+ class Frame < RestrictedOrderedHash
55
+
56
+ attr_reader :name, :version
57
+ attr_reader :headerStartX, :dataStartX, :dataEndX, :rawdata, :rawheader # debugging only
58
+
59
+ # ----------------------------------------------------------------------
60
+ # return the complete raw frame
61
+
62
+ def raw
63
+ return @rawheader + @rawdata
64
+ end
65
+ # ----------------------------------------------------------------------
66
+
67
+ def initialize(name, version = '2.3.0', flags = 0, tag, headerStartX, dataStartX, dataEndX )
68
+ super
69
+
70
+ @name = name
71
+ @headerStartX = headerStartX if headerStartX
72
+ @dataStartX = dataStartX if dataStartX
73
+ @dataEndX = dataEndX if dataEndX
74
+
75
+ if tag
76
+ @rawdata = tag.raw[dataStartX..dataEndX]
77
+ @rawheader = tag.raw[headerStartX..dataStartX-1]
78
+ # parse the darn flags, if there are any..
79
+ @version = tag.version # caching..
80
+ else
81
+ @rawdata = ''
82
+ @rawheader= ''
83
+ @version = version
84
+ end
85
+
86
+ case @version
87
+ when /2\.2\.[0-9]/
88
+ # no flags, no extra attributes necessary
89
+
90
+ when /2\.[34]\.0/
91
+
92
+ # dynamically create attributes and reader functions for flags in ID3-frames:
93
+ # (not defined in earlier ID3 versions)
94
+ instance_eval <<-EOB
95
+ class << self
96
+ attr_reader :rawflags, :flags
97
+ end
98
+ EOB
99
+
100
+ @rawflags = flags.to_i # preserve the raw flags (for debugging only)
101
+
102
+ if (flags.to_i & FRAME_HEADER_FLAG_MASK[@version] != 0)
103
+ # in this case we need to skip parsing the frame... and skip to the next one...
104
+ wrong = flags.to_i & FRAME_HEADER_FLAG_MASK[@version]
105
+ error = printf "ID3 version %s frame header flags 0x%X contain invalid flags 0x%X !\n", @version, flags, wrong
106
+ raise ArgumentError, error
107
+ end
108
+
109
+ @flags = Hash.new
110
+
111
+ FRAME_HEADER_FLAGS[@version].each do |key,val|
112
+ # only define the flags which are set..
113
+ @flags[key] = true if (flags.to_i & val == 1)
114
+ end
115
+
116
+ else
117
+ raise ArgumentError, "ID3 version #{@version} not recognized when parsing frame header flags\n"
118
+ end # parsing flags
119
+
120
+ # generate methods for parsing data (low-level read support) and for dumping data out (low-level write-support)
121
+ #
122
+ # based on the particular ID3-version and the ID3-frame name, we basically obtain a string saying how to pack/unpack the data for that frame
123
+ # then we use that packing-string to define a parser and dump method for this particular frame
124
+
125
+ instance_eval <<-EOB
126
+ class << self
127
+
128
+ def parse
129
+ # here we GENERATE the code to parse, dump and verify methods
130
+
131
+ vars,packing = ID3::FRAME_PARSER[ ID3::FrameName2FrameType[ ID3::Framename2symbol[self.version][self.name]] ]
132
+
133
+ values = self.rawdata.unpack(packing)
134
+
135
+ vars.each do |key|
136
+ self[key] = values.shift
137
+ end
138
+ self.lock # lock the OrderedHash
139
+ end
140
+
141
+
142
+ def dump
143
+ vars,packing = ID3::FRAME_PARSER[ ID3::FrameName2FrameType[ ID3::Framename2symbol[self.version][self.name]] ]
144
+
145
+ data = self.values.pack(packing) # we depend on an OrderedHash, so the values are in the correct order!!!
146
+ header = self.name.dup # we want the value! not the reference!!
147
+ len = data.length
148
+ if self.version =~ /^2\.2\./
149
+ byte2,rest = len.divmod(256**2)
150
+ byte1,byte0 = rest.divmod(256)
151
+
152
+ header << byte2 << byte1 << byte0
153
+
154
+ elsif self.version =~ /^2\.[34]\./ # 10-byte header
155
+ byte3,rest = len.divmod(256**3)
156
+ byte2,rest = rest.divmod(256**2)
157
+ byte1,byte0 = rest.divmod(256)
158
+
159
+ flags1,flags0 = self.rawflags.divmod(256)
160
+
161
+ header << byte3 << byte2 << byte1 << byte0 << flags1 << flags0
162
+ end
163
+ header << data
164
+ end
165
+
166
+ end
167
+ EOB
168
+
169
+ self.parse # now we're using the just defined parsing routine
170
+
171
+ return self
172
+ end
173
+ # ----------------------------------------------------------------------
174
+
175
+ end # of class Frame
176
+ # ==============================================================================
177
+
178
+ end
@@ -0,0 +1,19 @@
1
+ module ID3
2
+ # ==============================================================================
3
+ # Class FrameArray
4
+ #
5
+ # basically nothing more than an Array, but it knows how to dump it's contents as ID3v2 frames
6
+ #
7
+ # this solves in part the problem of having multiple ID3v2 frames in one tag, e.g. TXXX , WXXX, APIC
8
+
9
+ class FrameArray < Array
10
+ def dump
11
+ result = ''
12
+ self.each do |element|
13
+ result << element.dump
14
+ end
15
+ return result
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,73 @@
1
+ module ID3
2
+
3
+ # ==============================================================================
4
+ # Class GenericTag
5
+ #
6
+ # Helper class for Tag1 and Tag2
7
+ #
8
+ # Checks that user uses a valid key, and adds methods for size computation
9
+ #
10
+ # as per ID3-definition, the frames are in no fixed order! that's why we can derive
11
+ # this class from Hash. But in the future we may want to write certain frames first
12
+ # into the ID3-tag and therefore may want to derive it from RestrictedOrderedHash
13
+
14
+ # BUG (4) : When an ID3frame is assigned a value, e.g. a String, then the Hash just stores the value right now.
15
+ # Whereas when you read the ID3v2 tag, the object for the frame is ID3::Frame
16
+
17
+ class GenericTag < RestrictedOrderedHash
18
+ attr_accessor :version
19
+ attr_reader :raw
20
+
21
+ # these definitions are to prevent users from inventing their own field names..
22
+ # but on the other hand, they should be able to create a new valid field, if
23
+ # it's not yet in the current tag, but it's valid for that ID3-version...
24
+
25
+ alias old_set []=
26
+ private :old_set
27
+
28
+ # ----------------------------------------------------------------------
29
+ def []=(key,val)
30
+ if @version == ""
31
+ raise ArgumentError, "undefined version of ID3-tag! - set version before accessing components!\n"
32
+ else
33
+ if ID3::SUPPORTED_SYMBOLS[@version].keys.include?(key)
34
+ old_set(key,val)
35
+ else
36
+ # exception
37
+ raise ArgumentError, "Incorrect ID3-field \"#{key}\" for ID3 version #{@version}\n" +
38
+ " valid ID3-fields are: " + SUPPORTED_SYMBOLS[@version].keys.join(",") +"\n"
39
+ end
40
+ end
41
+ end
42
+ # ----------------------------------------------------------------------
43
+ # convert the 4 bytes found in the id3v2 header and return the size
44
+ private
45
+ def unmungeSize(bytes)
46
+ size = 0
47
+ j = 0; i = 3
48
+ while i >= 0
49
+ size += 128**i * (bytes.getbyte(j) & 0x7f)
50
+ j += 1
51
+ i -= 1
52
+ end
53
+ return size
54
+ end
55
+ # ----------------------------------------------------------------------
56
+ # convert the size into 4 bytes to be written into an id3v2 header
57
+ private
58
+ def mungeSize(size)
59
+ bytes = Array.new(4,0)
60
+ j = 0; i = 3
61
+ while i >= 0
62
+ bytes[j],size = size.divmod(128**i)
63
+ j += 1
64
+ i -= 1
65
+ end
66
+
67
+ return bytes
68
+ end
69
+ # ----------------------------------------------------------------------------
70
+
71
+ end # of class GenericTag
72
+
73
+ end # of module ID3
@@ -0,0 +1,159 @@
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 .. 2011 by Tilo Sloboda <firstname.lastname@google_email>
6
+ #
7
+ # created: 12 Oct 2002
8
+ # updated: Time-stamp: <Fri, 21 Oct 2011, 11:54:26 PDT tilo>
9
+ #
10
+ # Docs: http://www.id3.org/id3v2-00.txt
11
+ # http://www.id3.org/id3v2.3.0.txt
12
+ # http://www.id3.org/id3v2.4.0-changes.txt
13
+ # http://www.id3.org/id3v2.4.0-structure.txt
14
+ # http://www.id3.org/id3v2.4.0-frames.txt
15
+ #
16
+ # different versions of ID3 tags, support different fields.
17
+ # See: http://www.unixgods.org/~tilo/Ruby/ID3/docs/ID3v2_frames_comparison.txt
18
+ # See: http://www.unixgods.org/~tilo/Ruby/ID3/docs/ID3_comparison2.html
19
+ #
20
+ # PLEASE HELP:
21
+ #
22
+ # >>> Please contact me and email me the extracted ID3v2 tags, if you:
23
+ # >>> - if you have tags with exotic character encodings (exotic for me, not for you, obviously ;-) )
24
+ # >>> - if you find need support for any ID3v2 tags which are not yet supported by this library
25
+ # >>> (e.g. they are currently just parsed 'raw' and you need them fully parsed)
26
+ # >>> - if something terribly breaks
27
+ # >>>
28
+ # >>> You can find a small helper program in the examples folder, which extracts a ID3v2 tag from a file,
29
+ # >>> and saves it separately, so you can email it to me without emailing the whole audio file.
30
+ # >>>
31
+ # >>> THANK YOU FOR YOUR HELP!
32
+ #
33
+ # Non-ASCII encoded Strings:
34
+ #
35
+ # This library's main purpose is to unify access across different ID3-tag versions. So you don't have to worry
36
+ # about the changing names of the frames in different ID3-versions, and e.g. just access "AUTHOR" symbolically
37
+ # no matter if it's ID3 v1.0 v2.1.0 or v2.4.0. Think of this as a low-level library in that sense.
38
+ #
39
+ # Non-ASCII encodings are currently not really dealt with. For Strings which can be encoded differntly,
40
+ # you will see attributes like 'encoding' and 'text', where 'encoding' is a number representing the encoding,
41
+ # and the other attribute, e.g. 'text' or 'description', is the raw uninterpreted String.
42
+ #
43
+ # If your code requires to assign values to a ID3v2-frame which are foreign encoded Strings, you will need to make
44
+ # a small wrapper class on top of ID3::Frame which detects the encoding and properly saves it as a number.
45
+ # I'd love to add this -- but I don't have enough examples of ID3-tags in foreign languages. See: PLEASE HELP
46
+ #
47
+ # Limitations:
48
+ #
49
+ # - this library currently does not support the ID3v2.4 feature of having ID3v2 tags at the end of the file
50
+ # IMHO this doesn't make much sense in the age of streaming, and I haven't found examples for ths in any MP3-files.
51
+ # I think this is just one of the many unused "features" in the ID3v2 specifications ;-)
52
+ #
53
+ # - ID3v2 Chapters are not supported (see: Wikipedia)
54
+ #
55
+ # - ID3v1 extended tags are currently not supported (see: Wikipedia)
56
+ #
57
+ # License:
58
+ # Freely available under the terms of the OpenSource "Artistic License"
59
+ # in combination with the Addendum A (below)
60
+ #
61
+ # In case you did not get a copy of the license along with the software,
62
+ # it is also available at: http://www.unixgods.org/~tilo/artistic-license.html
63
+ #
64
+ # Addendum A:
65
+ # THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU!
66
+ # SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
67
+ # REPAIR OR CORRECTION.
68
+ #
69
+ # IN NO EVENT WILL THE COPYRIGHT HOLDERS BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL,
70
+ # SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY
71
+ # TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
72
+ # INACCURATE OR USELESS OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
73
+ # TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF THE COPYRIGHT HOLDERS OR OTHER PARTY HAS BEEN
74
+ # ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
75
+ #
76
+ #
77
+ # Author's Rant:
78
+ # The author of this ID3-library for Ruby is not responsible in any way for
79
+ # the awkward definition of the ID3-standards..
80
+ #
81
+ # You're lucky though that you can use this little library, rather than having
82
+ # to parse ID3v2 tags yourself! Trust me! At the first glance it doesn't seem
83
+ # to be so complicated, but the ID3v2 definitions are so convoluted and
84
+ # unnecessarily complicated, with so many useless frame-types, it's a pain to
85
+ # read the documents describing the ID3 V2.x standards.. with tiny bits of information
86
+ # strewn all accross the documents here and there.. and even worse to implement them.
87
+ #
88
+ # I don't know what these people were thinking... can we make it any more
89
+ # complicated than that?? ID3 version 2.4.0 tops everything! If this flag
90
+ # is set and it's a full moon, and an even weekday number, then do this..
91
+ # Outch!!! I assume that's why I don't find any 2.4.0 tags in any of my
92
+ # MP3-files... seems like noone is writing 2.4.0 tags... iTunes writes 2.3.0
93
+ #
94
+ ################################################################################
95
+ # How does it work?
96
+ #
97
+ # Main concepts used:
98
+ #
99
+ # - Unification of ID3 Frames according to this nomenclature, using "pretty" names for frames:
100
+ # http://www.unixgods.org/~tilo/Ruby/ID3/docs/ID3_comparison2.html
101
+ #
102
+ # - String pack/unpack to parse and dump contents of ID3v2 frames;
103
+ # For each ID3v2 frame type, there is a specific list of attributes for that frame
104
+ # and a pack/unpack recipe associated with that frame type (see: FRAME_PARSER Hash)
105
+ #
106
+ # - if there is a ID3v2 frame that's not parsed yet, it's easy to add:
107
+ # - create a new entry for that frame's symbolic (pretty) name in FRAMETYPE2FRAMENAME Hash
108
+ # - make sure to delete that name from the "UNPARSED" category
109
+ # - add an entry to FRAME_PARSER Hash
110
+ # - note how these pre-defined Hashes are used in ID3::Frame class during parse() and dump()
111
+ #
112
+ # - Metaprogramming: when ID3v2 frames are instanciated when they are read,
113
+ # we define parse and dump methods individually, using above pack/unpack recipes
114
+ # (check the two lines which use ID3::FRAME_PARSER to better understand the internal mechanics)
115
+ #
116
+ # - After the ID3v2 frames are parsed, they are Hashes; the keys are the attributes defined in FRAME_PARSER,
117
+ # the values are the extracted data from the ID3v2 tag.
118
+ #
119
+ ################################################################################
120
+ #--
121
+ # TO DO:
122
+ #
123
+ # - haven't touched the code in a very long time..
124
+ # - I should write a general write-up and explanation on how to use the classes
125
+ # - I should write a general write-up to explain the metaprogramming ;)
126
+ #
127
+ # - they really changed all the IO calls in Ruby 1.9 -- how painful!!
128
+ # I need to make some wrappers, to handle this nicely in both Ruby 1.9 and 1.8
129
+ #
130
+ # - should probably use IO#sysopen , IO#sysseek , IO#sysread , IO#syswrite for low-level i/o
131
+ # - files should be opened with the 'b' option - to tell Ruby 1.9 to open them in binary mode
132
+ #
133
+ # - Note: the external representation for non-printable characters in strings is now hex, not octal
134
+ # - should use sha1 instead of md5
135
+ # - some functionality , like has_id3...tag? and is_mp3_file? should extend class File instead of being an ID3 module method
136
+ # - class AudioFile could extend class IO or File - hmm, not sure
137
+ # - class RestrictedOrderedHash vs OrderedHash vs Hash ...?? can we just do this with the ordered hash in 1.9?
138
+ # should probably at least inherit RestrictedOrderedHash < ActiveSupport::OrderedHash
139
+ #
140
+ # - tripple-check if the semantics of pack/unpack has changed between 1.8 and 1.9
141
+ # - hexdump definitely barfs in Ruby 1.9 -- needs fixing
142
+ #
143
+ # - check out ruby-uuid on how he manipulates raw bytes.. looks like it is Ruby 1.9 compatible.. .ord .char .bytes
144
+ #
145
+ # - this needs some serious refactoring..
146
+ #++
147
+
148
+
149
+ # ==============================================================================
150
+
151
+
152
+
153
+ # ==============================================================================
154
+
155
+ module ID3
156
+
157
+ # ... moved everything into separate files
158
+
159
+ end # of module ID3
@@ -0,0 +1,44 @@
1
+ #
2
+ # EXTENSIONS to Class IO (included in File)
3
+ #
4
+
5
+ # if you have a (partial) MP3-file stored in a File or IO object, you can check if it contains ID3 tags
6
+ # NOTE: file needs to be opened in binary mode! 'rb:binary'
7
+
8
+ class IO
9
+ def id3_versions
10
+ [ hasID3v1tag? ,hasID3v2tag? ].compact # returns an Array of version numbers
11
+ end
12
+
13
+ def hasID3tag?
14
+ hasID3v2tag? || hasID3v1tag? ? true : false # returns true or false
15
+ end
16
+
17
+ def hasID3v1tag?
18
+ seek(-ID3::ID3v1tagSize, IO::SEEK_END)
19
+ if (read(3) == 'TAG')
20
+ seek(-ID3::ID3v1tagSize + ID3::ID3v1versionbyte, IO::SEEK_END)
21
+ return get_byte == 0 ? "1.0" : "1.1"
22
+ else
23
+ return nil
24
+ end
25
+ end
26
+
27
+ def ID3v2_tag_size
28
+ rewind
29
+ return 0 if (read(3) != 'ID3')
30
+ read_bytes(3) # skip version and flags
31
+ return ID3::ID3v2headerSize + ID3.unmungeSize( read_bytes(4) )
32
+ end
33
+
34
+ def hasID3v2tag?
35
+ rewind
36
+ if (read(3) == "ID3")
37
+ major = get_byte
38
+ minor = get_byte
39
+ return version = "2." + major.to_s + '.' + minor.to_s
40
+ else
41
+ return nil
42
+ end
43
+ end
44
+ end