X-Git-Url: https://git.mdrn.pl/wolnelektury.git/blobdiff_plain/e8390b9f10338ff5ced700f5dbd87ba30a3566bd..1477a9281c6c552b0cb00cf967c1cf1bc8739a54:/lib/mutagen/flac.py diff --git a/lib/mutagen/flac.py b/lib/mutagen/flac.py deleted file mode 100644 index 1669a029b..000000000 --- a/lib/mutagen/flac.py +++ /dev/null @@ -1,687 +0,0 @@ -# FLAC comment support for Mutagen -# Copyright 2005 Joe Wreschnig -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of version 2 of the GNU General Public License as -# published by the Free Software Foundation. - -"""Read and write FLAC Vorbis comments and stream information. - -Read more about FLAC at http://flac.sourceforge.net. - -FLAC supports arbitrary metadata blocks. The two most interesting ones -are the FLAC stream information block, and the Vorbis comment block; -these are also the only ones Mutagen can currently read. - -This module does not handle Ogg FLAC files. - -Based off documentation available at -http://flac.sourceforge.net/format.html -""" - -__all__ = ["FLAC", "Open", "delete"] - -import struct -from cStringIO import StringIO -from _vorbis import VCommentDict -from mutagen import FileType -from mutagen._util import insert_bytes -from mutagen.id3 import BitPaddedInt - -class error(IOError): pass -class FLACNoHeaderError(error): pass -class FLACVorbisError(ValueError, error): pass - -def to_int_be(string): - """Convert an arbitrarily-long string to a long using big-endian - byte order.""" - return reduce(lambda a, b: (a << 8) + ord(b), string, 0L) - -class MetadataBlock(object): - """A generic block of FLAC metadata. - - This class is extended by specific used as an ancestor for more specific - blocks, and also as a container for data blobs of unknown blocks. - - Attributes: - data -- raw binary data for this block - """ - - def __init__(self, data): - """Parse the given data string or file-like as a metadata block. - The metadata header should not be included.""" - if data is not None: - if isinstance(data, str): data = StringIO(data) - elif not hasattr(data, 'read'): - raise TypeError( - "StreamInfo requires string data or a file-like") - self.load(data) - - def load(self, data): self.data = data.read() - def write(self): return self.data - - def writeblocks(blocks): - """Render metadata block as a byte string.""" - data = [] - codes = [[block.code, block.write()] for block in blocks] - codes[-1][0] |= 128 - for code, datum in codes: - byte = chr(code) - if len(datum) > 2**24: - raise error("block is too long to write") - length = struct.pack(">I", len(datum))[-3:] - data.append(byte + length + datum) - return "".join(data) - writeblocks = staticmethod(writeblocks) - - def group_padding(blocks): - """Consolidate FLAC padding metadata blocks. - - The overall size of the rendered blocks does not change, so - this adds several bytes of padding for each merged block.""" - paddings = filter(lambda x: isinstance(x, Padding), blocks) - map(blocks.remove, paddings) - padding = Padding() - # total padding size is the sum of padding sizes plus 4 bytes - # per removed header. - size = sum([padding.length for padding in paddings]) - padding.length = size + 4 * (len(paddings) - 1) - blocks.append(padding) - group_padding = staticmethod(group_padding) - -class StreamInfo(MetadataBlock): - """FLAC stream information. - - This contains information about the audio data in the FLAC file. - Unlike most stream information objects in Mutagen, changes to this - one will rewritten to the file when it is saved. Unless you are - actually changing the audio stream itself, don't change any - attributes of this block. - - Attributes: - min_blocksize -- minimum audio block size - max_blocksize -- maximum audio block size - sample_rate -- audio sample rate in Hz - channels -- audio channels (1 for mono, 2 for stereo) - bits_per_sample -- bits per sample - total_samples -- total samples in file - length -- audio length in seconds - """ - - code = 0 - - def __eq__(self, other): - try: return (self.min_blocksize == other.min_blocksize and - self.max_blocksize == other.max_blocksize and - self.sample_rate == other.sample_rate and - self.channels == other.channels and - self.bits_per_sample == other.bits_per_sample and - self.total_samples == other.total_samples) - except: return False - - def load(self, data): - self.min_blocksize = int(to_int_be(data.read(2))) - self.max_blocksize = int(to_int_be(data.read(2))) - self.min_framesize = int(to_int_be(data.read(3))) - self.max_framesize = int(to_int_be(data.read(3))) - # first 16 bits of sample rate - sample_first = to_int_be(data.read(2)) - # last 4 bits of sample rate, 3 of channels, first 1 of bits/sample - sample_channels_bps = to_int_be(data.read(1)) - # last 4 of bits/sample, 36 of total samples - bps_total = to_int_be(data.read(5)) - - sample_tail = sample_channels_bps >> 4 - self.sample_rate = int((sample_first << 4) + sample_tail) - self.channels = int(((sample_channels_bps >> 1) & 7) + 1) - bps_tail = bps_total >> 36 - bps_head = (sample_channels_bps & 1) << 4 - self.bits_per_sample = int(bps_head + bps_tail + 1) - self.total_samples = bps_total & 0xFFFFFFFFFL - self.length = self.total_samples / float(self.sample_rate) - - self.md5_signature = to_int_be(data.read(16)) - - def write(self): - f = StringIO() - f.write(struct.pack(">I", self.min_blocksize)[-2:]) - f.write(struct.pack(">I", self.max_blocksize)[-2:]) - f.write(struct.pack(">I", self.min_framesize)[-3:]) - f.write(struct.pack(">I", self.max_framesize)[-3:]) - - # first 16 bits of sample rate - f.write(struct.pack(">I", self.sample_rate >> 4)[-2:]) - # 4 bits sample, 3 channel, 1 bps - byte = (self.sample_rate & 0xF) << 4 - byte += ((self.channels - 1) & 3) << 1 - byte += ((self.bits_per_sample - 1) >> 4) & 1 - f.write(chr(byte)) - # 4 bits of bps, 4 of sample count - byte = ((self.bits_per_sample - 1) & 0xF) << 4 - byte += (self.total_samples >> 32) & 0xF - f.write(chr(byte)) - # last 32 of sample count - f.write(struct.pack(">I", self.total_samples & 0xFFFFFFFFL)) - # MD5 signature - sig = self.md5_signature - f.write(struct.pack( - ">4I", (sig >> 96) & 0xFFFFFFFFL, (sig >> 64) & 0xFFFFFFFFL, - (sig >> 32) & 0xFFFFFFFFL, sig & 0xFFFFFFFFL)) - return f.getvalue() - - def pprint(self): - return "FLAC, %.2f seconds, %d Hz" % (self.length, self.sample_rate) - -class SeekPoint(tuple): - """A single seek point in a FLAC file. - - Placeholder seek points have first_sample of 0xFFFFFFFFFFFFFFFFL, - and byte_offset and num_samples undefined. Seek points must be - sorted in ascending order by first_sample number. Seek points must - be unique by first_sample number, except for placeholder - points. Placeholder points must occur last in the table and there - may be any number of them. - - Attributes: - first_sample -- sample number of first sample in the target frame - byte_offset -- offset from first frame to target frame - num_samples -- number of samples in target frame - """ - - def __new__(cls, first_sample, byte_offset, num_samples): - return super(cls, SeekPoint).__new__(cls, (first_sample, - byte_offset, num_samples)) - first_sample = property(lambda self: self[0]) - byte_offset = property(lambda self: self[1]) - num_samples = property(lambda self: self[2]) - -class SeekTable(MetadataBlock): - """Read and write FLAC seek tables. - - Attributes: - seekpoints -- list of SeekPoint objects - """ - - __SEEKPOINT_FORMAT = '>QQH' - __SEEKPOINT_SIZE = struct.calcsize(__SEEKPOINT_FORMAT) - - code = 3 - - def __init__(self, data): - self.seekpoints = [] - super(SeekTable, self).__init__(data) - - def __eq__(self, other): - try: return (self.seekpoints == other.seekpoints) - except (AttributeError, TypeError): return False - - def load(self, data): - self.seekpoints = [] - sp = data.read(self.__SEEKPOINT_SIZE) - while len(sp) == self.__SEEKPOINT_SIZE: - self.seekpoints.append(SeekPoint( - *struct.unpack(self.__SEEKPOINT_FORMAT, sp))) - sp = data.read(self.__SEEKPOINT_SIZE) - - def write(self): - f = StringIO() - for seekpoint in self.seekpoints: - packed = struct.pack(self.__SEEKPOINT_FORMAT, - seekpoint.first_sample, seekpoint.byte_offset, - seekpoint.num_samples) - f.write(packed) - return f.getvalue() - - def __repr__(self): - return "<%s seekpoints=%r>" % (type(self).__name__, self.seekpoints) - -class VCFLACDict(VCommentDict): - """Read and write FLAC Vorbis comments. - - FLACs don't use the framing bit at the end of the comment block. - So this extends VCommentDict to not use the framing bit. - """ - - code = 4 - - def load(self, data, errors='replace', framing=False): - super(VCFLACDict, self).load(data, errors=errors, framing=framing) - - def write(self, framing=False): - return super(VCFLACDict, self).write(framing=framing) - -class CueSheetTrackIndex(tuple): - """Index for a track in a cuesheet. - - For CD-DA, an index_number of 0 corresponds to the track - pre-gap. The first index in a track must have a number of 0 or 1, - and subsequently, index_numbers must increase by 1. Index_numbers - must be unique within a track. And index_offset must be evenly - divisible by 588 samples. - - Attributes: - index_number -- index point number - index_offset -- offset in samples from track start - """ - - def __new__(cls, index_number, index_offset): - return super(cls, CueSheetTrackIndex).__new__(cls, - (index_number, index_offset)) - index_number = property(lambda self: self[0]) - index_offset = property(lambda self: self[1]) - -class CueSheetTrack(object): - """A track in a cuesheet. - - For CD-DA, track_numbers must be 1-99, or 170 for the - lead-out. Track_numbers must be unique within a cue sheet. There - must be atleast one index in every track except the lead-out track - which must have none. - - Attributes: - track_number -- track number - start_offset -- track offset in samples from start of FLAC stream - isrc -- ISRC code - type -- 0 for audio, 1 for digital data - pre_emphasis -- true if the track is recorded with pre-emphasis - indexes -- list of CueSheetTrackIndex objects - """ - - def __init__(self, track_number, start_offset, isrc='', type_=0, - pre_emphasis=False): - self.track_number = track_number - self.start_offset = start_offset - self.isrc = isrc - self.type = type_ - self.pre_emphasis = pre_emphasis - self.indexes = [] - - def __eq__(self, other): - try: return (self.track_number == other.track_number and - self.start_offset == other.start_offset and - self.isrc == other.isrc and - self.type == other.type and - self.pre_emphasis == other.pre_emphasis and - self.indexes == other.indexes) - except (AttributeError, TypeError): return False - - def __repr__(self): - return ("<%s number=%r, offset=%d, isrc=%r, type=%r, " - "pre_emphasis=%r, indexes=%r)>") % ( - type(self).__name__, self.track_number, self.start_offset, - self.isrc, self.type, self.pre_emphasis, self.indexes) - -class CueSheet(MetadataBlock): - """Read and write FLAC embedded cue sheets. - - Number of tracks should be from 1 to 100. There should always be - exactly one lead-out track and that track must be the last track - in the cue sheet. - - Attributes: - media_catalog_number -- media catalog number in ASCII - lead_in_samples -- number of lead-in samples - compact_disc -- true if the cuesheet corresponds to a compact disc - tracks -- list of CueSheetTrack objects - lead_out -- lead-out as CueSheetTrack or None if lead-out was not found - """ - - __CUESHEET_FORMAT = '>128sQB258xB' - __CUESHEET_SIZE = struct.calcsize(__CUESHEET_FORMAT) - __CUESHEET_TRACK_FORMAT = '>QB12sB13xB' - __CUESHEET_TRACK_SIZE = struct.calcsize(__CUESHEET_TRACK_FORMAT) - __CUESHEET_TRACKINDEX_FORMAT = '>QB3x' - __CUESHEET_TRACKINDEX_SIZE = struct.calcsize(__CUESHEET_TRACKINDEX_FORMAT) - - code = 5 - - media_catalog_number = '' - lead_in_samples = 88200 - compact_disc = True - - def __init__(self, data): - self.tracks = [] - super(CueSheet, self).__init__(data) - - def __eq__(self, other): - try: - return (self.media_catalog_number == other.media_catalog_number and - self.lead_in_samples == other.lead_in_samples and - self.compact_disc == other.compact_disc and - self.tracks == other.tracks) - except (AttributeError, TypeError): return False - - def load(self, data): - header = data.read(self.__CUESHEET_SIZE) - media_catalog_number, lead_in_samples, flags, num_tracks = \ - struct.unpack(self.__CUESHEET_FORMAT, header) - self.media_catalog_number = media_catalog_number.rstrip('\0') - self.lead_in_samples = lead_in_samples - self.compact_disc = bool(flags & 0x80) - self.tracks = [] - for i in range(num_tracks): - track = data.read(self.__CUESHEET_TRACK_SIZE) - start_offset, track_number, isrc_padded, flags, num_indexes = \ - struct.unpack(self.__CUESHEET_TRACK_FORMAT, track) - isrc = isrc_padded.rstrip('\0') - type_ = (flags & 0x80) >> 7 - pre_emphasis = bool(flags & 0x40) - val = CueSheetTrack( - track_number, start_offset, isrc, type_, pre_emphasis) - for j in range(num_indexes): - index = data.read(self.__CUESHEET_TRACKINDEX_SIZE) - index_offset, index_number = struct.unpack( - self.__CUESHEET_TRACKINDEX_FORMAT, index) - val.indexes.append( - CueSheetTrackIndex(index_number, index_offset)) - self.tracks.append(val) - - def write(self): - f = StringIO() - flags = 0 - if self.compact_disc: flags |= 0x80 - packed = struct.pack( - self.__CUESHEET_FORMAT, self.media_catalog_number, - self.lead_in_samples, flags, len(self.tracks)) - f.write(packed) - for track in self.tracks: - track_flags = 0 - track_flags |= (track.type & 1) << 7 - if track.pre_emphasis: track_flags |= 0x40 - track_packed = struct.pack( - self.__CUESHEET_TRACK_FORMAT, track.start_offset, - track.track_number, track.isrc, track_flags, - len(track.indexes)) - f.write(track_packed) - for index in track.indexes: - index_packed = struct.pack( - self.__CUESHEET_TRACKINDEX_FORMAT, - index.index_offset, index.index_number) - f.write(index_packed) - return f.getvalue() - - def __repr__(self): - return ("<%s media_catalog_number=%r, lead_in=%r, compact_disc=%r, " - "tracks=%r>") % ( - type(self).__name__, self.media_catalog_number, - self.lead_in_samples, self.compact_disc, self.tracks) - -class Picture(MetadataBlock): - """Read and write FLAC embed pictures. - - Attributes: - type -- picture type (same as types for ID3 APIC frames) - mime -- MIME type of the picture - desc -- picture's description - width -- width in pixels - height -- height in pixels - depth -- color depth in bits-per-pixel - colors -- number of colors for indexed palettes (like GIF), - 0 for non-indexed - data -- picture data - """ - - code = 6 - - def __init__(self, data=None): - self.type = 0 - self.mime = u'' - self.desc = u'' - self.width = 0 - self.height = 0 - self.depth = 0 - self.colors = 0 - self.data = '' - super(Picture, self).__init__(data) - - def __eq__(self, other): - try: return (self.type == other.type and - self.mime == other.mime and - self.desc == other.desc and - self.width == other.width and - self.height == other.height and - self.depth == other.depth and - self.colors == other.colors and - self.data == other.data) - except (AttributeError, TypeError): return False - - def load(self, data): - self.type, length = struct.unpack('>2I', data.read(8)) - self.mime = data.read(length).decode('UTF-8', 'replace') - length, = struct.unpack('>I', data.read(4)) - self.desc = data.read(length).decode('UTF-8', 'replace') - (self.width, self.height, self.depth, - self.colors, length) = struct.unpack('>5I', data.read(20)) - self.data = data.read(length) - - def write(self): - f = StringIO() - mime = self.mime.encode('UTF-8') - f.write(struct.pack('>2I', self.type, len(mime))) - f.write(mime) - desc = self.desc.encode('UTF-8') - f.write(struct.pack('>I', len(desc))) - f.write(desc) - f.write(struct.pack('>5I', self.width, self.height, self.depth, - self.colors, len(self.data))) - f.write(self.data) - return f.getvalue() - - def __repr__(self): - return "<%s '%s' (%d bytes)>" % (type(self).__name__, self.mime, - len(self.data)) - -class Padding(MetadataBlock): - """Empty padding space for metadata blocks. - - To avoid rewriting the entire FLAC file when editing comments, - metadata is often padded. Padding should occur at the end, and no - more than one padding block should be in any FLAC file. Mutagen - handles this with MetadataBlock.group_padding. - """ - - code = 1 - - def __init__(self, data=""): super(Padding, self).__init__(data) - def load(self, data): self.length = len(data.read()) - def write(self): - try: return "\x00" * self.length - # On some 64 bit platforms this won't generate a MemoryError - # or OverflowError since you might have enough RAM, but it - # still generates a ValueError. On other 64 bit platforms, - # this will still succeed for extremely large values. - # Those should never happen in the real world, and if they - # do, writeblocks will catch it. - except (OverflowError, ValueError, MemoryError): - raise error("cannot write %d bytes" % self.length) - def __eq__(self, other): - return isinstance(other, Padding) and self.length == other.length - def __repr__(self): - return "<%s (%d bytes)>" % (type(self).__name__, self.length) - -class FLAC(FileType): - """A FLAC audio file. - - Attributes: - info -- stream information (length, bitrate, sample rate) - tags -- metadata tags, if any - cuesheet -- CueSheet object, if any - seektable -- SeekTable object, if any - pictures -- list of embedded pictures - """ - - _mimes = ["audio/x-flac", "application/x-flac"] - - METADATA_BLOCKS = [StreamInfo, Padding, None, SeekTable, VCFLACDict, - CueSheet, Picture] - """Known metadata block types, indexed by ID.""" - - def score(filename, fileobj, header): - return header.startswith("fLaC") - score = staticmethod(score) - - def __read_metadata_block(self, file): - byte = ord(file.read(1)) - size = to_int_be(file.read(3)) - try: - data = file.read(size) - if len(data) != size: - raise error( - "file said %d bytes, read %d bytes" % (size, len(data))) - block = self.METADATA_BLOCKS[byte & 0x7F](data) - except (IndexError, TypeError): - block = MetadataBlock(data) - block.code = byte & 0x7F - self.metadata_blocks.append(block) - else: - self.metadata_blocks.append(block) - if block.code == VCFLACDict.code: - if self.tags is None: self.tags = block - else: raise FLACVorbisError("> 1 Vorbis comment block found") - elif block.code == CueSheet.code: - if self.cuesheet is None: self.cuesheet = block - else: raise error("> 1 CueSheet block found") - elif block.code == SeekTable.code: - if self.seektable is None: self.seektable = block - else: raise error("> 1 SeekTable block found") - return (byte >> 7) ^ 1 - - def add_tags(self): - """Add a Vorbis comment block to the file.""" - if self.tags is None: - self.tags = VCFLACDict() - self.metadata_blocks.append(self.tags) - else: raise FLACVorbisError("a Vorbis comment already exists") - add_vorbiscomment = add_tags - - def delete(self, filename=None): - """Remove Vorbis comments from a file. - - If no filename is given, the one most recently loaded is used. - """ - if filename is None: filename = self.filename - for s in list(self.metadata_blocks): - if isinstance(s, VCFLACDict): - self.metadata_blocks.remove(s) - self.tags = None - self.save() - break - - vc = property(lambda s: s.tags, doc="Alias for tags; don't use this.") - - def load(self, filename): - """Load file information from a filename.""" - - self.metadata_blocks = [] - self.tags = None - self.cuesheet = None - self.seektable = None - self.filename = filename - fileobj = file(filename, "rb") - try: - self.__check_header(fileobj) - while self.__read_metadata_block(fileobj): pass - finally: - fileobj.close() - - try: self.metadata_blocks[0].length - except (AttributeError, IndexError): - raise FLACNoHeaderError("Stream info block not found") - - info = property(lambda s: s.metadata_blocks[0]) - - def add_picture(self, picture): - """Add a new picture to the file.""" - self.metadata_blocks.append(picture) - - def clear_pictures(self): - """Delete all pictures from the file.""" - self.metadata_blocks = filter(lambda b: b.code != Picture.code, - self.metadata_blocks) - - def __get_pictures(self): - return filter(lambda b: b.code == Picture.code, self.metadata_blocks) - pictures = property(__get_pictures, doc="List of embedded pictures") - - def save(self, filename=None, deleteid3=False): - """Save metadata blocks to a file. - - If no filename is given, the one most recently loaded is used. - """ - - if filename is None: filename = self.filename - f = open(filename, 'rb+') - - # Ensure we've got padding at the end, and only at the end. - # If adding makes it too large, we'll scale it down later. - self.metadata_blocks.append(Padding('\x00' * 1020)) - MetadataBlock.group_padding(self.metadata_blocks) - - header = self.__check_header(f) - available = self.__find_audio_offset(f) - header # "fLaC" and maybe ID3 - data = MetadataBlock.writeblocks(self.metadata_blocks) - - # Delete ID3v2 - if deleteid3 and header > 4: - available += header - 4 - header = 4 - - if len(data) > available: - # If we have too much data, see if we can reduce padding. - padding = self.metadata_blocks[-1] - newlength = padding.length - (len(data) - available) - if newlength > 0: - padding.length = newlength - data = MetadataBlock.writeblocks(self.metadata_blocks) - assert len(data) == available - - elif len(data) < available: - # If we have too little data, increase padding. - self.metadata_blocks[-1].length += (available - len(data)) - data = MetadataBlock.writeblocks(self.metadata_blocks) - assert len(data) == available - - if len(data) != available: - # We couldn't reduce the padding enough. - diff = (len(data) - available) - insert_bytes(f, diff, header) - - f.seek(header - 4) - f.write("fLaC" + data) - - # Delete ID3v1 - if deleteid3: - try: f.seek(-128, 2) - except IOError: pass - else: - if f.read(3) == "TAG": - f.seek(-128, 2) - f.truncate() - - def __find_audio_offset(self, fileobj): - byte = 0x00 - while not (byte >> 7) & 1: - byte = ord(fileobj.read(1)) - size = to_int_be(fileobj.read(3)) - fileobj.read(size) - return fileobj.tell() - - def __check_header(self, fileobj): - size = 4 - header = fileobj.read(4) - if header != "fLaC": - size = None - if header[:3] == "ID3": - size = 14 + BitPaddedInt(fileobj.read(6)[2:]) - fileobj.seek(size - 4) - if fileobj.read(4) != "fLaC": size = None - if size is None: - raise FLACNoHeaderError( - "%r is not a valid FLAC file" % fileobj.name) - return size - -Open = FLAC - -def delete(filename): - """Remove tags from a file.""" - FLAC(filename).delete()