Merge pull request #21 from geekmug/patch-1
[pyTivo/wmcbrine.git] / mutagen / _vorbis.py
blob4f8467a5be3f5fdac4c839183111dec3bc091390
1 # Vorbis comment support for Mutagen
2 # Copyright 2005-2006 Joe Wreschnig
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of version 2 of the GNU General Public License as
6 # published by the Free Software Foundation.
8 """Read and write Vorbis comment data.
10 Vorbis comments are freeform key/value pairs; keys are
11 case-insensitive ASCII and values are Unicode strings. A key may have
12 multiple values.
14 The specification is at http://www.xiph.org/vorbis/doc/v-comment.html.
15 """
17 import sys
19 from cStringIO import StringIO
21 import mutagen
22 from mutagen._util import DictMixin, cdata
24 try: set
25 except NameError:
26 from sets import Set as set
28 def is_valid_key(key):
29 """Return true if a string is a valid Vorbis comment key.
31 Valid Vorbis comment keys are printable ASCII between 0x20 (space)
32 and 0x7D ('}'), excluding '='.
33 """
34 for c in key:
35 if c < " " or c > "}" or c == "=": return False
36 else: return bool(key)
37 istag = is_valid_key
39 class error(IOError): pass
40 class VorbisUnsetFrameError(error): pass
41 class VorbisEncodingError(error): pass
43 class VComment(mutagen.Metadata, list):
44 """A Vorbis comment parser, accessor, and renderer.
46 All comment ordering is preserved. A VComment is a list of
47 key/value pairs, and so any Python list method can be used on it.
49 Vorbis comments are always wrapped in something like an Ogg Vorbis
50 bitstream or a FLAC metadata block, so this loads string data or a
51 file-like object, not a filename.
53 Attributes:
54 vendor -- the stream 'vendor' (i.e. writer); default 'Mutagen'
55 """
57 vendor = u"Mutagen " + mutagen.version_string
59 def __init__(self, data=None, *args, **kwargs):
60 # Collect the args to pass to load, this lets child classes
61 # override just load and get equivalent magic for the
62 # constructor.
63 if data is not None:
64 if isinstance(data, str):
65 data = StringIO(data)
66 elif not hasattr(data, 'read'):
67 raise TypeError("VComment requires string data or a file-like")
68 self.load(data, *args, **kwargs)
70 def load(self, fileobj, errors='replace', framing=True):
71 """Parse a Vorbis comment from a file-like object.
73 Keyword arguments:
74 errors:
75 'strict', 'replace', or 'ignore'. This affects Unicode decoding
76 and how other malformed content is interpreted.
77 framing -- if true, fail if a framing bit is not present
79 Framing bits are required by the Vorbis comment specification,
80 but are not used in FLAC Vorbis comment blocks.
82 """
83 try:
84 vendor_length = cdata.uint_le(fileobj.read(4))
85 self.vendor = fileobj.read(vendor_length).decode('utf-8', errors)
86 count = cdata.uint_le(fileobj.read(4))
87 for i in range(count):
88 length = cdata.uint_le(fileobj.read(4))
89 try: string = fileobj.read(length).decode('utf-8', errors)
90 except (OverflowError, MemoryError):
91 raise error("cannot read %d bytes, too large" % length)
92 try: tag, value = string.split('=', 1)
93 except ValueError, err:
94 if errors == "ignore":
95 continue
96 elif errors == "replace":
97 tag, value = u"unknown%d" % i, string
98 else:
99 raise VorbisEncodingError, str(err), sys.exc_info()[2]
100 try: tag = tag.encode('ascii', errors)
101 except UnicodeEncodeError:
102 raise VorbisEncodingError, "invalid tag name %r" % tag
103 else:
104 if is_valid_key(tag): self.append((tag, value))
105 if framing and not ord(fileobj.read(1)) & 0x01:
106 raise VorbisUnsetFrameError("framing bit was unset")
107 except (cdata.error, TypeError):
108 raise error("file is not a valid Vorbis comment")
110 def validate(self):
111 """Validate keys and values.
113 Check to make sure every key used is a valid Vorbis key, and
114 that every value used is a valid Unicode or UTF-8 string. If
115 any invalid keys or values are found, a ValueError is raised.
118 if not isinstance(self.vendor, unicode):
119 try: self.vendor.decode('utf-8')
120 except UnicodeDecodeError: raise ValueError
122 for key, value in self:
123 try:
124 if not is_valid_key(key): raise ValueError
125 except: raise ValueError("%r is not a valid key" % key)
126 if not isinstance(value, unicode):
127 try: value.encode("utf-8")
128 except: raise ValueError("%r is not a valid value" % value)
129 else: return True
131 def clear(self):
132 """Clear all keys from the comment."""
133 del(self[:])
135 def write(self, framing=True):
136 """Return a string representation of the data.
138 Validation is always performed, so calling this function on
139 invalid data may raise a ValueError.
141 Keyword arguments:
142 framing -- if true, append a framing bit (see load)
145 self.validate()
147 f = StringIO()
148 f.write(cdata.to_uint_le(len(self.vendor.encode('utf-8'))))
149 f.write(self.vendor.encode('utf-8'))
150 f.write(cdata.to_uint_le(len(self)))
151 for tag, value in self:
152 comment = "%s=%s" % (tag, value.encode('utf-8'))
153 f.write(cdata.to_uint_le(len(comment)))
154 f.write(comment)
155 if framing: f.write("\x01")
156 return f.getvalue()
158 def pprint(self):
159 return "\n".join(["%s=%s" % (k.lower(), v) for k, v in self])
161 class VCommentDict(VComment, DictMixin):
162 """A VComment that looks like a dictionary.
164 This object differs from a dictionary in two ways. First,
165 len(comment) will still return the number of values, not the
166 number of keys. Secondly, iterating through the object will
167 iterate over (key, value) pairs, not keys. Since a key may have
168 multiple values, the same value may appear multiple times while
169 iterating.
171 Since Vorbis comment keys are case-insensitive, all keys are
172 normalized to lowercase ASCII.
175 def __getitem__(self, key):
176 """A list of values for the key.
178 This is a copy, so comment['title'].append('a title') will not
179 work.
182 key = key.lower().encode('ascii')
183 values = [value for (k, value) in self if k.lower() == key]
184 if not values: raise KeyError, key
185 else: return values
187 def __delitem__(self, key):
188 """Delete all values associated with the key."""
189 key = key.lower().encode('ascii')
190 to_delete = filter(lambda x: x[0].lower() == key, self)
191 if not to_delete:raise KeyError, key
192 else: map(self.remove, to_delete)
194 def __contains__(self, key):
195 """Return true if the key has any values."""
196 key = key.lower().encode('ascii')
197 for k, value in self:
198 if k.lower() == key: return True
199 else: return False
201 def __setitem__(self, key, values):
202 """Set a key's value or values.
204 Setting a value overwrites all old ones. The value may be a
205 list of Unicode or UTF-8 strings, or a single Unicode or UTF-8
206 string.
209 key = key.lower().encode('ascii')
210 if not isinstance(values, list):
211 values = [values]
212 try: del(self[key])
213 except KeyError: pass
214 for value in values:
215 self.append((key, value))
217 def keys(self):
218 """Return all keys in the comment."""
219 return self and list(set([k.lower() for k, v in self]))
221 def as_dict(self):
222 """Return a copy of the comment data in a real dict."""
223 return dict([(key, self[key]) for key in self.keys()])