1# -*- coding: utf-8 -*- 2# Copyright (C) 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 FLAC comments. 10 11This module handles FLAC files wrapped in an Ogg bitstream. The first 12FLAC stream found is used. For 'naked' FLACs, see mutagen.flac. 13 14This module is based off the specification at 15http://flac.sourceforge.net/ogg_mapping.html. 16""" 17 18__all__ = ["OggFLAC", "Open", "delete"] 19 20import struct 21 22from ._compat import cBytesIO 23 24from mutagen import StreamInfo 25from mutagen.flac import StreamInfo as FLACStreamInfo, error as FLACError 26from mutagen._vorbis import VCommentDict 27from mutagen._util import loadfile, convert_error 28from mutagen.ogg import OggPage, OggFileType, error as OggError 29 30 31class error(OggError): 32 pass 33 34 35class OggFLACHeaderError(error): 36 pass 37 38 39class OggFLACStreamInfo(StreamInfo): 40 """OggFLACStreamInfo() 41 42 Ogg FLAC stream info. 43 44 Attributes: 45 length (`float`): File length in seconds, as a float 46 channels (`float`): Number of channels 47 sample_rate (`int`): Sample rate in Hz" 48 """ 49 50 length = 0 51 channels = 0 52 sample_rate = 0 53 54 def __init__(self, fileobj): 55 page = OggPage(fileobj) 56 while not page.packets[0].startswith(b"\x7FFLAC"): 57 page = OggPage(fileobj) 58 major, minor, self.packets, flac = struct.unpack( 59 ">BBH4s", page.packets[0][5:13]) 60 if flac != b"fLaC": 61 raise OggFLACHeaderError("invalid FLAC marker (%r)" % flac) 62 elif (major, minor) != (1, 0): 63 raise OggFLACHeaderError( 64 "unknown mapping version: %d.%d" % (major, minor)) 65 self.serial = page.serial 66 67 # Skip over the block header. 68 stringobj = cBytesIO(page.packets[0][17:]) 69 70 try: 71 flac_info = FLACStreamInfo(stringobj) 72 except FLACError as e: 73 raise OggFLACHeaderError(e) 74 75 for attr in ["min_blocksize", "max_blocksize", "sample_rate", 76 "channels", "bits_per_sample", "total_samples", "length"]: 77 setattr(self, attr, getattr(flac_info, attr)) 78 79 def _post_tags(self, fileobj): 80 if self.length: 81 return 82 page = OggPage.find_last(fileobj, self.serial, finishing=True) 83 if page is None: 84 raise OggFLACHeaderError 85 self.length = page.position / float(self.sample_rate) 86 87 def pprint(self): 88 return u"Ogg FLAC, %.2f seconds, %d Hz" % ( 89 self.length, self.sample_rate) 90 91 92class OggFLACVComment(VCommentDict): 93 94 def __init__(self, fileobj, info): 95 # data should be pointing at the start of an Ogg page, after 96 # the first FLAC page. 97 pages = [] 98 complete = False 99 while not complete: 100 page = OggPage(fileobj) 101 if page.serial == info.serial: 102 pages.append(page) 103 complete = page.complete or (len(page.packets) > 1) 104 comment = cBytesIO(OggPage.to_packets(pages)[0][4:]) 105 super(OggFLACVComment, self).__init__(comment, framing=False) 106 107 def _inject(self, fileobj, padding_func): 108 """Write tag data into the FLAC Vorbis comment packet/page.""" 109 110 # Ogg FLAC has no convenient data marker like Vorbis, but the 111 # second packet - and second page - must be the comment data. 112 fileobj.seek(0) 113 page = OggPage(fileobj) 114 while not page.packets[0].startswith(b"\x7FFLAC"): 115 page = OggPage(fileobj) 116 117 first_page = page 118 while not (page.sequence == 1 and page.serial == first_page.serial): 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 == first_page.serial: 125 old_pages.append(page) 126 127 packets = OggPage.to_packets(old_pages, strict=False) 128 129 # Set the new comment block. 130 data = self.write(framing=False) 131 data = packets[0][:1] + struct.pack(">I", len(data))[-3:] + data 132 packets[0] = data 133 134 new_pages = OggPage.from_packets(packets, old_pages[0].sequence) 135 OggPage.replace(fileobj, old_pages, new_pages) 136 137 138class OggFLAC(OggFileType): 139 """OggFLAC(filething) 140 141 An Ogg FLAC file. 142 143 Arguments: 144 filething (filething) 145 146 Attributes: 147 info (`OggFLACStreamInfo`) 148 tags (`mutagen._vorbis.VCommentDict`) 149 """ 150 151 _Info = OggFLACStreamInfo 152 _Tags = OggFLACVComment 153 _Error = OggFLACHeaderError 154 _mimes = ["audio/x-oggflac"] 155 156 info = None 157 tags = None 158 159 @staticmethod 160 def score(filename, fileobj, header): 161 return (header.startswith(b"OggS") * ( 162 (b"FLAC" in header) + (b"fLaC" in header))) 163 164 165Open = OggFLAC 166 167 168@convert_error(IOError, error) 169@loadfile(method=False, writable=True) 170def delete(filething): 171 """ delete(filething) 172 173 Arguments: 174 filething (filething) 175 Raises: 176 mutagen.MutagenError 177 178 Remove tags from a file. 179 """ 180 181 t = OggFLAC(filething) 182 filething.fileobj.seek(0) 183 t.delete(filething) 184