It seems not. :(
[pyTivo/wmcbrine/lucasnz.git] / mutagen / flac.py
blobc372e12696185e7163770b2e3fcd13fb3371fa39
1 # FLAC comment support for Mutagen
2 # Copyright 2005 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 FLAC Vorbis comments and stream information.
10 Read more about FLAC at http://flac.sourceforge.net.
12 FLAC supports arbitrary metadata blocks. The two most interesting ones
13 are the FLAC stream information block, and the Vorbis comment block;
14 these are also the only ones Mutagen can currently read.
16 This module does not handle Ogg FLAC files.
18 Based off documentation available at
19 http://flac.sourceforge.net/format.html
20 """
22 __all__ = ["FLAC", "Open", "delete"]
24 import struct
25 from cStringIO import StringIO
26 from _vorbis import VCommentDict
27 from mutagen import FileType
28 from mutagen._util import insert_bytes
29 from mutagen.id3 import BitPaddedInt
30 import sys
31 if sys.version_info >= (2, 6):
32 from functools import reduce
34 class error(IOError): pass
35 class FLACNoHeaderError(error): pass
36 class FLACVorbisError(ValueError, error): pass
38 def to_int_be(string):
39 """Convert an arbitrarily-long string to a long using big-endian
40 byte order."""
41 return reduce(lambda a, b: (a << 8) + ord(b), string, 0L)
43 class MetadataBlock(object):
44 """A generic block of FLAC metadata.
46 This class is extended by specific used as an ancestor for more specific
47 blocks, and also as a container for data blobs of unknown blocks.
49 Attributes:
50 data -- raw binary data for this block
51 """
53 def __init__(self, data):
54 """Parse the given data string or file-like as a metadata block.
55 The metadata header should not be included."""
56 if data is not None:
57 if isinstance(data, str): data = StringIO(data)
58 elif not hasattr(data, 'read'):
59 raise TypeError(
60 "StreamInfo requires string data or a file-like")
61 self.load(data)
63 def load(self, data): self.data = data.read()
64 def write(self): return self.data
66 def writeblocks(blocks):
67 """Render metadata block as a byte string."""
68 data = []
69 codes = [[block.code, block.write()] for block in blocks]
70 codes[-1][0] |= 128
71 for code, datum in codes:
72 byte = chr(code)
73 if len(datum) > 2**24:
74 raise error("block is too long to write")
75 length = struct.pack(">I", len(datum))[-3:]
76 data.append(byte + length + datum)
77 return "".join(data)
78 writeblocks = staticmethod(writeblocks)
80 def group_padding(blocks):
81 """Consolidate FLAC padding metadata blocks.
83 The overall size of the rendered blocks does not change, so
84 this adds several bytes of padding for each merged block."""
85 paddings = filter(lambda x: isinstance(x, Padding), blocks)
86 map(blocks.remove, paddings)
87 padding = Padding()
88 # total padding size is the sum of padding sizes plus 4 bytes
89 # per removed header.
90 size = sum([padding.length for padding in paddings])
91 padding.length = size + 4 * (len(paddings) - 1)
92 blocks.append(padding)
93 group_padding = staticmethod(group_padding)
95 class StreamInfo(MetadataBlock):
96 """FLAC stream information.
98 This contains information about the audio data in the FLAC file.
99 Unlike most stream information objects in Mutagen, changes to this
100 one will rewritten to the file when it is saved. Unless you are
101 actually changing the audio stream itself, don't change any
102 attributes of this block.
104 Attributes:
105 min_blocksize -- minimum audio block size
106 max_blocksize -- maximum audio block size
107 sample_rate -- audio sample rate in Hz
108 channels -- audio channels (1 for mono, 2 for stereo)
109 bits_per_sample -- bits per sample
110 total_samples -- total samples in file
111 length -- audio length in seconds
114 code = 0
116 def __eq__(self, other):
117 try: return (self.min_blocksize == other.min_blocksize and
118 self.max_blocksize == other.max_blocksize and
119 self.sample_rate == other.sample_rate and
120 self.channels == other.channels and
121 self.bits_per_sample == other.bits_per_sample and
122 self.total_samples == other.total_samples)
123 except: return False
124 __hash__ = MetadataBlock.__hash__
126 def load(self, data):
127 self.min_blocksize = int(to_int_be(data.read(2)))
128 self.max_blocksize = int(to_int_be(data.read(2)))
129 self.min_framesize = int(to_int_be(data.read(3)))
130 self.max_framesize = int(to_int_be(data.read(3)))
131 # first 16 bits of sample rate
132 sample_first = to_int_be(data.read(2))
133 # last 4 bits of sample rate, 3 of channels, first 1 of bits/sample
134 sample_channels_bps = to_int_be(data.read(1))
135 # last 4 of bits/sample, 36 of total samples
136 bps_total = to_int_be(data.read(5))
138 sample_tail = sample_channels_bps >> 4
139 self.sample_rate = int((sample_first << 4) + sample_tail)
140 self.channels = int(((sample_channels_bps >> 1) & 7) + 1)
141 bps_tail = bps_total >> 36
142 bps_head = (sample_channels_bps & 1) << 4
143 self.bits_per_sample = int(bps_head + bps_tail + 1)
144 self.total_samples = bps_total & 0xFFFFFFFFFL
145 self.length = self.total_samples / float(self.sample_rate)
147 self.md5_signature = to_int_be(data.read(16))
149 def write(self):
150 f = StringIO()
151 f.write(struct.pack(">I", self.min_blocksize)[-2:])
152 f.write(struct.pack(">I", self.max_blocksize)[-2:])
153 f.write(struct.pack(">I", self.min_framesize)[-3:])
154 f.write(struct.pack(">I", self.max_framesize)[-3:])
156 # first 16 bits of sample rate
157 f.write(struct.pack(">I", self.sample_rate >> 4)[-2:])
158 # 4 bits sample, 3 channel, 1 bps
159 byte = (self.sample_rate & 0xF) << 4
160 byte += ((self.channels - 1) & 7) << 1
161 byte += ((self.bits_per_sample - 1) >> 4) & 1
162 f.write(chr(byte))
163 # 4 bits of bps, 4 of sample count
164 byte = ((self.bits_per_sample - 1) & 0xF) << 4
165 byte += (self.total_samples >> 32) & 0xF
166 f.write(chr(byte))
167 # last 32 of sample count
168 f.write(struct.pack(">I", self.total_samples & 0xFFFFFFFFL))
169 # MD5 signature
170 sig = self.md5_signature
171 f.write(struct.pack(
172 ">4I", (sig >> 96) & 0xFFFFFFFFL, (sig >> 64) & 0xFFFFFFFFL,
173 (sig >> 32) & 0xFFFFFFFFL, sig & 0xFFFFFFFFL))
174 return f.getvalue()
176 def pprint(self):
177 return "FLAC, %.2f seconds, %d Hz" % (self.length, self.sample_rate)
179 class SeekPoint(tuple):
180 """A single seek point in a FLAC file.
182 Placeholder seek points have first_sample of 0xFFFFFFFFFFFFFFFFL,
183 and byte_offset and num_samples undefined. Seek points must be
184 sorted in ascending order by first_sample number. Seek points must
185 be unique by first_sample number, except for placeholder
186 points. Placeholder points must occur last in the table and there
187 may be any number of them.
189 Attributes:
190 first_sample -- sample number of first sample in the target frame
191 byte_offset -- offset from first frame to target frame
192 num_samples -- number of samples in target frame
195 def __new__(cls, first_sample, byte_offset, num_samples):
196 return super(cls, SeekPoint).__new__(cls, (first_sample,
197 byte_offset, num_samples))
198 first_sample = property(lambda self: self[0])
199 byte_offset = property(lambda self: self[1])
200 num_samples = property(lambda self: self[2])
202 class SeekTable(MetadataBlock):
203 """Read and write FLAC seek tables.
205 Attributes:
206 seekpoints -- list of SeekPoint objects
209 __SEEKPOINT_FORMAT = '>QQH'
210 __SEEKPOINT_SIZE = struct.calcsize(__SEEKPOINT_FORMAT)
212 code = 3
214 def __init__(self, data):
215 self.seekpoints = []
216 super(SeekTable, self).__init__(data)
218 def __eq__(self, other):
219 try: return (self.seekpoints == other.seekpoints)
220 except (AttributeError, TypeError): return False
221 __hash__ = MetadataBlock.__hash__
223 def load(self, data):
224 self.seekpoints = []
225 sp = data.read(self.__SEEKPOINT_SIZE)
226 while len(sp) == self.__SEEKPOINT_SIZE:
227 self.seekpoints.append(SeekPoint(
228 *struct.unpack(self.__SEEKPOINT_FORMAT, sp)))
229 sp = data.read(self.__SEEKPOINT_SIZE)
231 def write(self):
232 f = StringIO()
233 for seekpoint in self.seekpoints:
234 packed = struct.pack(self.__SEEKPOINT_FORMAT,
235 seekpoint.first_sample, seekpoint.byte_offset,
236 seekpoint.num_samples)
237 f.write(packed)
238 return f.getvalue()
240 def __repr__(self):
241 return "<%s seekpoints=%r>" % (type(self).__name__, self.seekpoints)
243 class VCFLACDict(VCommentDict):
244 """Read and write FLAC Vorbis comments.
246 FLACs don't use the framing bit at the end of the comment block.
247 So this extends VCommentDict to not use the framing bit.
250 code = 4
252 def load(self, data, errors='replace', framing=False):
253 super(VCFLACDict, self).load(data, errors=errors, framing=framing)
255 def write(self, framing=False):
256 return super(VCFLACDict, self).write(framing=framing)
258 class CueSheetTrackIndex(tuple):
259 """Index for a track in a cuesheet.
261 For CD-DA, an index_number of 0 corresponds to the track
262 pre-gap. The first index in a track must have a number of 0 or 1,
263 and subsequently, index_numbers must increase by 1. Index_numbers
264 must be unique within a track. And index_offset must be evenly
265 divisible by 588 samples.
267 Attributes:
268 index_number -- index point number
269 index_offset -- offset in samples from track start
272 def __new__(cls, index_number, index_offset):
273 return super(cls, CueSheetTrackIndex).__new__(cls,
274 (index_number, index_offset))
275 index_number = property(lambda self: self[0])
276 index_offset = property(lambda self: self[1])
278 class CueSheetTrack(object):
279 """A track in a cuesheet.
281 For CD-DA, track_numbers must be 1-99, or 170 for the
282 lead-out. Track_numbers must be unique within a cue sheet. There
283 must be atleast one index in every track except the lead-out track
284 which must have none.
286 Attributes:
287 track_number -- track number
288 start_offset -- track offset in samples from start of FLAC stream
289 isrc -- ISRC code
290 type -- 0 for audio, 1 for digital data
291 pre_emphasis -- true if the track is recorded with pre-emphasis
292 indexes -- list of CueSheetTrackIndex objects
295 def __init__(self, track_number, start_offset, isrc='', type_=0,
296 pre_emphasis=False):
297 self.track_number = track_number
298 self.start_offset = start_offset
299 self.isrc = isrc
300 self.type = type_
301 self.pre_emphasis = pre_emphasis
302 self.indexes = []
304 def __eq__(self, other):
305 try: return (self.track_number == other.track_number and
306 self.start_offset == other.start_offset and
307 self.isrc == other.isrc and
308 self.type == other.type and
309 self.pre_emphasis == other.pre_emphasis and
310 self.indexes == other.indexes)
311 except (AttributeError, TypeError): return False
312 __hash__ = object.__hash__
314 def __repr__(self):
315 return ("<%s number=%r, offset=%d, isrc=%r, type=%r, "
316 "pre_emphasis=%r, indexes=%r)>") % (
317 type(self).__name__, self.track_number, self.start_offset,
318 self.isrc, self.type, self.pre_emphasis, self.indexes)
320 class CueSheet(MetadataBlock):
321 """Read and write FLAC embedded cue sheets.
323 Number of tracks should be from 1 to 100. There should always be
324 exactly one lead-out track and that track must be the last track
325 in the cue sheet.
327 Attributes:
328 media_catalog_number -- media catalog number in ASCII
329 lead_in_samples -- number of lead-in samples
330 compact_disc -- true if the cuesheet corresponds to a compact disc
331 tracks -- list of CueSheetTrack objects
332 lead_out -- lead-out as CueSheetTrack or None if lead-out was not found
335 __CUESHEET_FORMAT = '>128sQB258xB'
336 __CUESHEET_SIZE = struct.calcsize(__CUESHEET_FORMAT)
337 __CUESHEET_TRACK_FORMAT = '>QB12sB13xB'
338 __CUESHEET_TRACK_SIZE = struct.calcsize(__CUESHEET_TRACK_FORMAT)
339 __CUESHEET_TRACKINDEX_FORMAT = '>QB3x'
340 __CUESHEET_TRACKINDEX_SIZE = struct.calcsize(__CUESHEET_TRACKINDEX_FORMAT)
342 code = 5
344 media_catalog_number = ''
345 lead_in_samples = 88200
346 compact_disc = True
348 def __init__(self, data):
349 self.tracks = []
350 super(CueSheet, self).__init__(data)
352 def __eq__(self, other):
353 try:
354 return (self.media_catalog_number == other.media_catalog_number and
355 self.lead_in_samples == other.lead_in_samples and
356 self.compact_disc == other.compact_disc and
357 self.tracks == other.tracks)
358 except (AttributeError, TypeError): return False
359 __hash__ = MetadataBlock.__hash__
361 def load(self, data):
362 header = data.read(self.__CUESHEET_SIZE)
363 media_catalog_number, lead_in_samples, flags, num_tracks = \
364 struct.unpack(self.__CUESHEET_FORMAT, header)
365 self.media_catalog_number = media_catalog_number.rstrip('\0')
366 self.lead_in_samples = lead_in_samples
367 self.compact_disc = bool(flags & 0x80)
368 self.tracks = []
369 for i in range(num_tracks):
370 track = data.read(self.__CUESHEET_TRACK_SIZE)
371 start_offset, track_number, isrc_padded, flags, num_indexes = \
372 struct.unpack(self.__CUESHEET_TRACK_FORMAT, track)
373 isrc = isrc_padded.rstrip('\0')
374 type_ = (flags & 0x80) >> 7
375 pre_emphasis = bool(flags & 0x40)
376 val = CueSheetTrack(
377 track_number, start_offset, isrc, type_, pre_emphasis)
378 for j in range(num_indexes):
379 index = data.read(self.__CUESHEET_TRACKINDEX_SIZE)
380 index_offset, index_number = struct.unpack(
381 self.__CUESHEET_TRACKINDEX_FORMAT, index)
382 val.indexes.append(
383 CueSheetTrackIndex(index_number, index_offset))
384 self.tracks.append(val)
386 def write(self):
387 f = StringIO()
388 flags = 0
389 if self.compact_disc: flags |= 0x80
390 packed = struct.pack(
391 self.__CUESHEET_FORMAT, self.media_catalog_number,
392 self.lead_in_samples, flags, len(self.tracks))
393 f.write(packed)
394 for track in self.tracks:
395 track_flags = 0
396 track_flags |= (track.type & 1) << 7
397 if track.pre_emphasis: track_flags |= 0x40
398 track_packed = struct.pack(
399 self.__CUESHEET_TRACK_FORMAT, track.start_offset,
400 track.track_number, track.isrc, track_flags,
401 len(track.indexes))
402 f.write(track_packed)
403 for index in track.indexes:
404 index_packed = struct.pack(
405 self.__CUESHEET_TRACKINDEX_FORMAT,
406 index.index_offset, index.index_number)
407 f.write(index_packed)
408 return f.getvalue()
410 def __repr__(self):
411 return ("<%s media_catalog_number=%r, lead_in=%r, compact_disc=%r, "
412 "tracks=%r>") % (
413 type(self).__name__, self.media_catalog_number,
414 self.lead_in_samples, self.compact_disc, self.tracks)
416 class Picture(MetadataBlock):
417 """Read and write FLAC embed pictures.
419 Attributes:
420 type -- picture type (same as types for ID3 APIC frames)
421 mime -- MIME type of the picture
422 desc -- picture's description
423 width -- width in pixels
424 height -- height in pixels
425 depth -- color depth in bits-per-pixel
426 colors -- number of colors for indexed palettes (like GIF),
427 0 for non-indexed
428 data -- picture data
431 code = 6
433 def __init__(self, data=None):
434 self.type = 0
435 self.mime = u''
436 self.desc = u''
437 self.width = 0
438 self.height = 0
439 self.depth = 0
440 self.colors = 0
441 self.data = ''
442 super(Picture, self).__init__(data)
444 def __eq__(self, other):
445 try: return (self.type == other.type and
446 self.mime == other.mime and
447 self.desc == other.desc and
448 self.width == other.width and
449 self.height == other.height and
450 self.depth == other.depth and
451 self.colors == other.colors and
452 self.data == other.data)
453 except (AttributeError, TypeError): return False
454 __hash__ = MetadataBlock.__hash__
456 def load(self, data):
457 self.type, length = struct.unpack('>2I', data.read(8))
458 self.mime = data.read(length).decode('UTF-8', 'replace')
459 length, = struct.unpack('>I', data.read(4))
460 self.desc = data.read(length).decode('UTF-8', 'replace')
461 (self.width, self.height, self.depth,
462 self.colors, length) = struct.unpack('>5I', data.read(20))
463 self.data = data.read(length)
465 def write(self):
466 f = StringIO()
467 mime = self.mime.encode('UTF-8')
468 f.write(struct.pack('>2I', self.type, len(mime)))
469 f.write(mime)
470 desc = self.desc.encode('UTF-8')
471 f.write(struct.pack('>I', len(desc)))
472 f.write(desc)
473 f.write(struct.pack('>5I', self.width, self.height, self.depth,
474 self.colors, len(self.data)))
475 f.write(self.data)
476 return f.getvalue()
478 def __repr__(self):
479 return "<%s '%s' (%d bytes)>" % (type(self).__name__, self.mime,
480 len(self.data))
482 class Padding(MetadataBlock):
483 """Empty padding space for metadata blocks.
485 To avoid rewriting the entire FLAC file when editing comments,
486 metadata is often padded. Padding should occur at the end, and no
487 more than one padding block should be in any FLAC file. Mutagen
488 handles this with MetadataBlock.group_padding.
491 code = 1
493 def __init__(self, data=""): super(Padding, self).__init__(data)
494 def load(self, data): self.length = len(data.read())
495 def write(self):
496 try: return "\x00" * self.length
497 # On some 64 bit platforms this won't generate a MemoryError
498 # or OverflowError since you might have enough RAM, but it
499 # still generates a ValueError. On other 64 bit platforms,
500 # this will still succeed for extremely large values.
501 # Those should never happen in the real world, and if they
502 # do, writeblocks will catch it.
503 except (OverflowError, ValueError, MemoryError):
504 raise error("cannot write %d bytes" % self.length)
505 def __eq__(self, other):
506 return isinstance(other, Padding) and self.length == other.length
507 __hash__ = MetadataBlock.__hash__
508 def __repr__(self):
509 return "<%s (%d bytes)>" % (type(self).__name__, self.length)
511 class FLAC(FileType):
512 """A FLAC audio file.
514 Attributes:
515 info -- stream information (length, bitrate, sample rate)
516 tags -- metadata tags, if any
517 cuesheet -- CueSheet object, if any
518 seektable -- SeekTable object, if any
519 pictures -- list of embedded pictures
522 _mimes = ["audio/x-flac", "application/x-flac"]
524 METADATA_BLOCKS = [StreamInfo, Padding, None, SeekTable, VCFLACDict,
525 CueSheet, Picture]
526 """Known metadata block types, indexed by ID."""
528 def score(filename, fileobj, header):
529 return (header.startswith("fLaC") +
530 filename.lower().endswith(".flac") * 3)
531 score = staticmethod(score)
533 def __read_metadata_block(self, fileobj):
534 byte = ord(fileobj.read(1))
535 size = to_int_be(fileobj.read(3))
536 try:
537 if (byte & 0x7F) == VCFLACDict.code:
538 # Some jackass is writing broken Metadata block length
539 # for Vorbis comment blocks, and the FLAC reference
540 # implementaton can parse them (mostly by accident),
541 # so we have to too. Instead of parsing the size
542 # given, parse an actual Vorbis comment, leaving
543 # fileobj in the right position.
544 # http://code.google.com/p/mutagen/issues/detail?id=52
545 block = VCFLACDict(fileobj)
546 else:
547 data = fileobj.read(size)
548 if len(data) != size:
549 raise error("file said %d bytes, read %d bytes" %(
550 size, len(data)))
551 block = self.METADATA_BLOCKS[byte & 0x7F](data)
552 except (IndexError, TypeError):
553 block = MetadataBlock(data)
554 block.code = byte & 0x7F
556 if block.code == VCFLACDict.code:
557 if self.tags is None:
558 self.tags = block
559 else:
560 raise FLACVorbisError("> 1 Vorbis comment block found")
561 elif block.code == CueSheet.code:
562 if self.cuesheet is None:
563 self.cuesheet = block
564 else:
565 raise error("> 1 CueSheet block found")
566 elif block.code == SeekTable.code:
567 if self.seektable is None:
568 self.seektable = block
569 else:
570 raise error("> 1 SeekTable block found")
571 self.metadata_blocks.append(block)
572 return not (byte & 0x80);
574 def add_tags(self):
575 """Add a Vorbis comment block to the file."""
576 if self.tags is None:
577 self.tags = VCFLACDict()
578 self.metadata_blocks.append(self.tags)
579 else: raise FLACVorbisError("a Vorbis comment already exists")
580 add_vorbiscomment = add_tags
582 def delete(self, filename=None):
583 """Remove Vorbis comments from a file.
585 If no filename is given, the one most recently loaded is used.
587 if filename is None: filename = self.filename
588 for s in list(self.metadata_blocks):
589 if isinstance(s, VCFLACDict):
590 self.metadata_blocks.remove(s)
591 self.tags = None
592 self.save()
593 break
595 vc = property(lambda s: s.tags, doc="Alias for tags; don't use this.")
597 def load(self, filename):
598 """Load file information from a filename."""
600 self.metadata_blocks = []
601 self.tags = None
602 self.cuesheet = None
603 self.seektable = None
604 self.filename = filename
605 fileobj = open(filename, "rb")
606 try:
607 self.__check_header(fileobj)
608 while self.__read_metadata_block(fileobj):
609 pass
610 finally:
611 fileobj.close()
613 try:
614 self.metadata_blocks[0].length
615 except (AttributeError, IndexError):
616 raise FLACNoHeaderError("Stream info block not found")
618 info = property(lambda s: s.metadata_blocks[0])
620 def add_picture(self, picture):
621 """Add a new picture to the file."""
622 self.metadata_blocks.append(picture)
624 def clear_pictures(self):
625 """Delete all pictures from the file."""
626 self.metadata_blocks = filter(lambda b: b.code != Picture.code,
627 self.metadata_blocks)
629 def __get_pictures(self):
630 return filter(lambda b: b.code == Picture.code, self.metadata_blocks)
631 pictures = property(__get_pictures, doc="List of embedded pictures")
633 def save(self, filename=None, deleteid3=False):
634 """Save metadata blocks to a file.
636 If no filename is given, the one most recently loaded is used.
639 if filename is None: filename = self.filename
640 f = open(filename, 'rb+')
642 # Ensure we've got padding at the end, and only at the end.
643 # If adding makes it too large, we'll scale it down later.
644 self.metadata_blocks.append(Padding('\x00' * 1020))
645 MetadataBlock.group_padding(self.metadata_blocks)
647 header = self.__check_header(f)
648 available = self.__find_audio_offset(f) - header # "fLaC" and maybe ID3
649 data = MetadataBlock.writeblocks(self.metadata_blocks)
651 # Delete ID3v2
652 if deleteid3 and header > 4:
653 available += header - 4
654 header = 4
656 if len(data) > available:
657 # If we have too much data, see if we can reduce padding.
658 padding = self.metadata_blocks[-1]
659 newlength = padding.length - (len(data) - available)
660 if newlength > 0:
661 padding.length = newlength
662 data = MetadataBlock.writeblocks(self.metadata_blocks)
663 assert len(data) == available
665 elif len(data) < available:
666 # If we have too little data, increase padding.
667 self.metadata_blocks[-1].length += (available - len(data))
668 data = MetadataBlock.writeblocks(self.metadata_blocks)
669 assert len(data) == available
671 if len(data) != available:
672 # We couldn't reduce the padding enough.
673 diff = (len(data) - available)
674 insert_bytes(f, diff, header)
676 f.seek(header - 4)
677 f.write("fLaC" + data)
679 # Delete ID3v1
680 if deleteid3:
681 try: f.seek(-128, 2)
682 except IOError: pass
683 else:
684 if f.read(3) == "TAG":
685 f.seek(-128, 2)
686 f.truncate()
688 def __find_audio_offset(self, fileobj):
689 byte = 0x00
690 while not (byte & 0x80):
691 byte = ord(fileobj.read(1))
692 size = to_int_be(fileobj.read(3))
693 if (byte & 0x7F) == VCFLACDict.code:
694 # See comments in read_metadata_block; the size can't
695 # be trusted for Vorbis comment blocks.
696 VCFLACDict(fileobj)
697 else:
698 fileobj.read(size)
699 return fileobj.tell()
701 def __check_header(self, fileobj):
702 size = 4
703 header = fileobj.read(4)
704 if header != "fLaC":
705 size = None
706 if header[:3] == "ID3":
707 size = 14 + BitPaddedInt(fileobj.read(6)[2:])
708 fileobj.seek(size - 4)
709 if fileobj.read(4) != "fLaC": size = None
710 if size is None:
711 raise FLACNoHeaderError(
712 "%r is not a valid FLAC file" % fileobj.name)
713 return size
715 Open = FLAC
717 def delete(filename):
718 """Remove tags from a file."""
719 FLAC(filename).delete()