1# -*- coding: utf-8 -*- 2# Copyright 2006 Joe Wreschnig 3# 4# This program is free software; you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation; either version 2 of the License, or 7# (at your option) any later version. 8 9"""Read and write Ogg Vorbis comments. 10 11This module handles Vorbis files wrapped in an Ogg bitstream. The 12first Vorbis stream found is used. 13 14Read more about Ogg Vorbis at http://vorbis.com/. This module is based 15on the specification at http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html. 16""" 17 18__all__ = ["OggVorbis", "Open", "delete"] 19 20import struct 21 22from mutagen import StreamInfo 23from mutagen._vorbis import VCommentDict 24from mutagen._util import get_size, loadfile, convert_error 25from mutagen._tags import PaddingInfo 26from mutagen.ogg import OggPage, OggFileType, error as OggError 27 28 29class error(OggError): 30 pass 31 32 33class OggVorbisHeaderError(error): 34 pass 35 36 37class OggVorbisInfo(StreamInfo): 38 """OggVorbisInfo() 39 40 Ogg Vorbis stream information. 41 42 Attributes: 43 length (`float`): File length in seconds, as a float 44 channels (`int`): Number of channels 45 bitrate (`int`): Nominal ('average') bitrate in bits per second 46 sample_Rate (`int`): Sample rate in Hz 47 48 """ 49 50 length = 0.0 51 channels = 0 52 bitrate = 0 53 sample_rate = 0 54 55 def __init__(self, fileobj): 56 """Raises ogg.error, IOError""" 57 58 page = OggPage(fileobj) 59 while not page.packets[0].startswith(b"\x01vorbis"): 60 page = OggPage(fileobj) 61 if not page.first: 62 raise OggVorbisHeaderError( 63 "page has ID header, but doesn't start a stream") 64 (self.channels, self.sample_rate, max_bitrate, nominal_bitrate, 65 min_bitrate) = struct.unpack("<B4i", page.packets[0][11:28]) 66 self.serial = page.serial 67 68 max_bitrate = max(0, max_bitrate) 69 min_bitrate = max(0, min_bitrate) 70 nominal_bitrate = max(0, nominal_bitrate) 71 72 if nominal_bitrate == 0: 73 self.bitrate = (max_bitrate + min_bitrate) // 2 74 elif max_bitrate and max_bitrate < nominal_bitrate: 75 # If the max bitrate is less than the nominal, we know 76 # the nominal is wrong. 77 self.bitrate = max_bitrate 78 elif min_bitrate > nominal_bitrate: 79 self.bitrate = min_bitrate 80 else: 81 self.bitrate = nominal_bitrate 82 83 def _post_tags(self, fileobj): 84 """Raises ogg.error""" 85 86 page = OggPage.find_last(fileobj, self.serial, finishing=True) 87 if page is None: 88 raise OggVorbisHeaderError 89 self.length = page.position / float(self.sample_rate) 90 91 def pprint(self): 92 return u"Ogg Vorbis, %.2f seconds, %d bps" % ( 93 self.length, self.bitrate) 94 95 96class OggVCommentDict(VCommentDict): 97 """Vorbis comments embedded in an Ogg bitstream.""" 98 99 def __init__(self, fileobj, info): 100 pages = [] 101 complete = False 102 while not complete: 103 page = OggPage(fileobj) 104 if page.serial == info.serial: 105 pages.append(page) 106 complete = page.complete or (len(page.packets) > 1) 107 data = OggPage.to_packets(pages)[0][7:] # Strip off "\x03vorbis". 108 super(OggVCommentDict, self).__init__(data) 109 self._padding = len(data) - self._size 110 111 def _inject(self, fileobj, padding_func): 112 """Write tag data into the Vorbis comment packet/page.""" 113 114 # Find the old pages in the file; we'll need to remove them, 115 # plus grab any stray setup packet data out of them. 116 fileobj.seek(0) 117 page = OggPage(fileobj) 118 while not page.packets[0].startswith(b"\x03vorbis"): 119 page = OggPage(fileobj) 120 121 old_pages = [page] 122 while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1): 123 page = OggPage(fileobj) 124 if page.serial == old_pages[0].serial: 125 old_pages.append(page) 126 127 packets = OggPage.to_packets(old_pages, strict=False) 128 129 content_size = get_size(fileobj) - len(packets[0]) # approx 130 vcomment_data = b"\x03vorbis" + self.write() 131 padding_left = len(packets[0]) - len(vcomment_data) 132 133 info = PaddingInfo(padding_left, content_size) 134 new_padding = info._get_padding(padding_func) 135 136 # Set the new comment packet. 137 packets[0] = vcomment_data + b"\x00" * new_padding 138 139 new_pages = OggPage._from_packets_try_preserve(packets, old_pages) 140 OggPage.replace(fileobj, old_pages, new_pages) 141 142 143class OggVorbis(OggFileType): 144 """OggVorbis(filething) 145 146 Arguments: 147 filething (filething) 148 149 An Ogg Vorbis file. 150 151 Attributes: 152 info (`OggVorbisInfo`) 153 tags (`mutagen._vorbis.VCommentDict`) 154 """ 155 156 _Info = OggVorbisInfo 157 _Tags = OggVCommentDict 158 _Error = OggVorbisHeaderError 159 _mimes = ["audio/vorbis", "audio/x-vorbis"] 160 161 info = None 162 tags = None 163 164 @staticmethod 165 def score(filename, fileobj, header): 166 return (header.startswith(b"OggS") * (b"\x01vorbis" in header)) 167 168 169Open = OggVorbis 170 171 172@convert_error(IOError, error) 173@loadfile(method=False, writable=True) 174def delete(filething): 175 """ delete(filething) 176 177 Arguments: 178 filething (filething) 179 Raises: 180 mutagen.MutagenError 181 182 Remove tags from a file. 183 """ 184 185 t = OggVorbis(filething) 186 filething.fileobj.seek(0) 187 t.delete(filething) 188