# -*- coding: utf-8 -*- # Copyright (C) 2005-2006 Joe Wreschnig # Copyright (C) 2006-2007 Lukas Lalinsky # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. """Read and write ASF (Window Media Audio) files.""" __all__ = ["ASF", "Open"] import sys import struct from mutagen import FileType, Metadata, StreamInfo from mutagen._util import (insert_bytes, delete_bytes, DictMixin, total_ordering, MutagenError) from ._compat import swap_to_string, text_type, PY2, string_types, reraise, \ xrange, long_, PY3 class error(IOError, MutagenError): pass class ASFError(error): pass class ASFHeaderError(error): pass class ASFInfo(StreamInfo): """ASF stream information.""" def __init__(self): self.length = 0.0 self.sample_rate = 0 self.bitrate = 0 self.channels = 0 def pprint(self): s = "Windows Media Audio %d bps, %s Hz, %d channels, %.2f seconds" % ( self.bitrate, self.sample_rate, self.channels, self.length) return s class ASFTags(list, DictMixin, Metadata): """Dictionary containing ASF attributes.""" def pprint(self): return "\n".join("%s=%s" % (k, v) for k, v in self) def __getitem__(self, key): """A list of values for the key. This is a copy, so comment['title'].append('a title') will not work. """ # PY3 only if isinstance(key, slice): return list.__getitem__(self, key) values = [value for (k, value) in self if k == key] if not values: raise KeyError(key) else: return values def __delitem__(self, key): """Delete all values associated with the key.""" # PY3 only if isinstance(key, slice): return list.__delitem__(self, key) to_delete = [x for x in self if x[0] == key] if not to_delete: raise KeyError(key) else: for k in to_delete: self.remove(k) def __contains__(self, key): """Return true if the key has any values.""" for k, value in self: if k == key: return True else: return False def __setitem__(self, key, values): """Set a key's value or values. Setting a value overwrites all old ones. The value may be a list of Unicode or UTF-8 strings, or a single Unicode or UTF-8 string. """ # PY3 only if isinstance(key, slice): return list.__setitem__(self, key, values) if not isinstance(values, list): values = [values] to_append = [] for value in values: if not isinstance(value, ASFBaseAttribute): if isinstance(value, string_types): value = ASFUnicodeAttribute(value) elif PY3 and isinstance(value, bytes): value = ASFByteArrayAttribute(value) elif isinstance(value, bool): value = ASFBoolAttribute(value) elif isinstance(value, int): value = ASFDWordAttribute(value) elif isinstance(value, long_): value = ASFQWordAttribute(value) else: raise TypeError("Invalid type %r" % type(value)) to_append.append((key, value)) try: del(self[key]) except KeyError: pass self.extend(to_append) def keys(self): """Return all keys in the comment.""" return self and set(next(iter(zip(*self)))) def as_dict(self): """Return a copy of the comment data in a real dict.""" d = {} for key, value in self: d.setdefault(key, []).append(value) return d class ASFBaseAttribute(object): """Generic attribute.""" TYPE = None def __init__(self, value=None, data=None, language=None, stream=None, **kwargs): self.language = language self.stream = stream if data: self.value = self.parse(data, **kwargs) else: if value is None: # we used to support not passing any args and instead assign # them later, keep that working.. self.value = None else: self.value = self._validate(value) def _validate(self, value): """Raises TypeError or ValueError in case the user supplied value isn't valid. """ return value def data_size(self): raise NotImplementedError def __repr__(self): name = "%s(%r" % (type(self).__name__, self.value) if self.language: name += ", language=%d" % self.language if self.stream: name += ", stream=%d" % self.stream name += ")" return name def render(self, name): name = name.encode("utf-16-le") + b"\x00\x00" data = self._render() return (struct.pack(" 0: texts.append(data[pos:end].decode("utf-16-le").strip(u"\x00")) else: texts.append(None) pos = end for key, value in zip(self.NAMES, texts): if value is not None: value = ASFUnicodeAttribute(value=value) asf._tags.setdefault(self.GUID, []).append((key, value)) def render(self, asf): def render_text(name): value = asf.to_content_description.get(name) if value is not None: return text_type(value).encode("utf-16-le") + b"\x00\x00" else: return b"" texts = [render_text(x) for x in self.NAMES] data = struct.pack(" 0xFFFF or value.TYPE == GUID) can_cont_desc = value.TYPE == UNICODE if library_only or value.language is not None: self.to_metadata_library.append((name, value)) elif value.stream is not None: if name not in self.to_metadata: self.to_metadata[name] = value else: self.to_metadata_library.append((name, value)) elif name in ContentDescriptionObject.NAMES: if name not in self.to_content_description and can_cont_desc: self.to_content_description[name] = value else: self.to_metadata_library.append((name, value)) else: if name not in self.to_extended_content_description: self.to_extended_content_description[name] = value else: self.to_metadata_library.append((name, value)) # Add missing objects if not self.content_description_obj: self.content_description_obj = \ ContentDescriptionObject() self.objects.append(self.content_description_obj) if not self.extended_content_description_obj: self.extended_content_description_obj = \ ExtendedContentDescriptionObject() self.objects.append(self.extended_content_description_obj) if not self.header_extension_obj: self.header_extension_obj = \ HeaderExtensionObject() self.objects.append(self.header_extension_obj) if not self.metadata_obj: self.metadata_obj = \ MetadataObject() self.header_extension_obj.objects.append(self.metadata_obj) if not self.metadata_library_obj: self.metadata_library_obj = \ MetadataLibraryObject() self.header_extension_obj.objects.append(self.metadata_library_obj) # Render the header data = b"".join([obj.render(self) for obj in self.objects]) data = (HeaderObject.GUID + struct.pack(" self.size: insert_bytes(fileobj, size - self.size, self.size) if size < self.size: delete_bytes(fileobj, self.size - size, 0) fileobj.seek(0) fileobj.write(data) self.size = size self.num_objects = len(self.objects) def __read_file(self, fileobj): header = fileobj.read(30) if len(header) != 30 or header[:16] != HeaderObject.GUID: raise ASFHeaderError("Not an ASF file.") self.extended_content_description_obj = None self.content_description_obj = None self.header_extension_obj = None self.metadata_obj = None self.metadata_library_obj = None self.size, self.num_objects = struct.unpack("