def delete_metadata(self): """deletes the track's MetaData this removes or unsets tags as necessary in order to remove all data raises IOError if unable to write the file""" import os from audiotools import (transfer_data, LimitedFileReader, TemporaryFile) from audiotools.id3 import skip_id3v2_comment if (not os.access(self.filename, os.W_OK)): raise IOError(self.filename) # overwrite original with no tags attached old_tta = open(self.filename, "rb") skip_id3v2_comment(old_tta) old_tta = LimitedFileReader(old_tta, self.data_size()) new_tta = TemporaryFile(self.filename) transfer_data(old_tta.read, new_tta.write) old_tta.close() new_tta.close()
def delete_metadata(self): """deletes the track's MetaData this removes or unsets tags as necessary in order to remove all data raises IOError if unable to write the file""" import os from audiotools import (TemporaryFile, LimitedFileReader, transfer_data) #this works a lot like update_metadata #but without any new metadata to set if (not os.access(self.filename, os.W_OK)): raise IOError(self.filename) new_mp3 = TemporaryFile(self.filename) #get the original MP3 data old_mp3 = open(self.filename, "rb") MP3Audio.__find_last_mp3_frame__(old_mp3) data_end = old_mp3.tell() old_mp3.seek(0, 0) MP3Audio.__find_mp3_start__(old_mp3) data_start = old_mp3.tell() old_mp3 = LimitedFileReader(old_mp3, data_end - data_start) #write data to file transfer_data(old_mp3.read, new_mp3.write) #commit change to disk old_mp3.close() new_mp3.close()
def update_metadata(self, metadata): """takes this track's current MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object raises IOError if unable to write the file """ from audiotools import transfer_data, TemporaryFile from audiotools.id3 import ID3v22Comment from audiotools.bitstream import BitstreamRecorder from audiotools.text import ERR_FOREIGN_METADATA import os if metadata is None: return elif not isinstance(metadata, ID3v22Comment): raise ValueError(ERR_FOREIGN_METADATA) elif not os.access(self.filename, os.W_OK): raise IOError(self.filename) # turn our ID3v2.2 tag into a raw binary chunk id3_chunk = BitstreamRecorder(0) metadata.build(id3_chunk) # generate a temporary AIFF file in which our new ID3v2.2 chunk # replaces the existing ID3v2.2 chunk new_aiff = TemporaryFile(self.filename) self.__class__.aiff_from_chunks( new_aiff, [(chunk if chunk.id != b"ID3 " else AIFF_Chunk( b"ID3 ", id3_chunk.bytes(), id3_chunk.data())) for chunk in self.chunks()]) new_aiff.close()
def set_metadata(self, metadata): """takes a MetaData object and sets this track's metadata this metadata includes track name, album name, and so on raises IOError if unable to write the file""" from audiotools.id3 import ID3v22Comment if metadata is None: return self.delete_metadata() elif self.get_metadata() is not None: # current file has metadata, so replace it with new metadata self.update_metadata(ID3v22Comment.converted(metadata)) else: # current file has no metadata, so append new ID3 block import os from audiotools.bitstream import BitstreamRecorder from audiotools import transfer_data, TemporaryFile if not os.access(self.filename, os.W_OK): raise IOError(self.filename) # turn our ID3v2.2 tag into a raw binary chunk id3_chunk = BitstreamRecorder(0) ID3v22Comment.converted(metadata).build(id3_chunk) # generate a temporary AIFF file in which our new ID3v2.2 chunk # is appended to the file's set of chunks new_aiff = TemporaryFile(self.filename) self.__class__.aiff_from_chunks( new_aiff, [c for c in self.chunks()] + [AIFF_Chunk(b"ID3 ", id3_chunk.bytes(), id3_chunk.data())]) new_aiff.close()
def update_metadata(self, metadata): """takes this track's current MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object raises IOError if unable to write the file """ if (metadata is None): return elif (not isinstance(metadata, ApeTag)): from audiotools.text import ERR_FOREIGN_METADATA raise ValueError(ERR_FOREIGN_METADATA) from audiotools.bitstream import BitstreamReader, BitstreamWriter from audiotools import transfer_data f = open(self.filename, "r+b") f.seek(-32, 2) (preamble, version, tag_size, item_count, read_only, item_encoding, is_header, no_footer, has_header) = BitstreamReader(f, 1).parse(ApeTag.HEADER_FORMAT) if ((preamble == 'APETAGEX') and (version == 2000)): if (has_header): old_tag_size = 32 + tag_size else: old_tag_size = tag_size if (metadata.total_size() >= old_tag_size): # metadata has grown # so append it to existing file f.seek(-old_tag_size, 2) metadata.build(BitstreamWriter(f, 1)) else: # metadata has shrunk # so rewrite file with smaller metadata from audiotools import TemporaryFile from os.path import getsize # copy everything but the last "old_tag_size" bytes # from existing file to rewritten file new_apev2 = TemporaryFile(self.filename) old_apev2 = open(self.filename, "rb") limited_transfer_data(old_apev2.read, new_apev2.write, getsize(self.filename) - old_tag_size) # append new tag to rewritten file metadata.build(BitstreamWriter(new_apev2, 1)) old_apev2.close() new_apev2.close() else: # no existing metadata, so simply append a fresh tag f = open(self.filename, "ab") metadata.build(BitstreamWriter(f, 1)) f.close()
def delete_metadata(self): """deletes the track's MetaData raises IOError if unable to write the file""" if ((self.get_replay_gain() is not None) or (self.get_cuesheet() is not None)): # non-textual metadata is present and needs preserving self.set_metadata(MetaData()) else: # no non-textual metadata, so wipe out the entire block from os import access, R_OK, W_OK from audiotools.bitstream import BitstreamReader from audiotools import transfer_data if not access(self.filename, R_OK | W_OK): raise IOError(self.filename) with open(self.filename, "rb") as f: f.seek(-32, 2) (preamble, version, tag_size, item_count, read_only, item_encoding, is_header, no_footer, has_header) = BitstreamReader(f, True).parse( ApeTag.HEADER_FORMAT) if (preamble == b'APETAGEX') and (version == 2000): from audiotools import TemporaryFile from os.path import getsize # there's existing metadata to delete # so rewrite file without trailing metadata tag if has_header: old_tag_size = 32 + tag_size else: old_tag_size = tag_size # copy everything but the last "old_tag_size" bytes # from existing file to rewritten file new_apev2 = TemporaryFile(self.filename) old_apev2 = open(self.filename, "rb") limited_transfer_data( old_apev2.read, new_apev2.write, getsize(self.filename) - old_tag_size) old_apev2.close() new_apev2.close()
def update_metadata(self, metadata): """takes this track's current MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object raises IOError if unable to write the file """ import os from audiotools import (TemporaryFile, LimitedFileReader, transfer_data) from audiotools.id3 import (ID3v2Comment, ID3CommentPair) from audiotools.id3v1 import ID3v1Comment from audiotools.bitstream import BitstreamWriter if (metadata is None): return elif (not (isinstance(metadata, ID3v2Comment) or isinstance(metadata, ID3CommentPair) or isinstance(metadata, ID3v1Comment))): from .text import ERR_FOREIGN_METADATA raise ValueError(ERR_FOREIGN_METADATA) elif (not os.access(self.filename, os.W_OK)): raise IOError(self.filename) new_mp3 = TemporaryFile(self.filename) #get the original MP3 data old_mp3 = open(self.filename, "rb") MP3Audio.__find_last_mp3_frame__(old_mp3) data_end = old_mp3.tell() old_mp3.seek(0, 0) MP3Audio.__find_mp3_start__(old_mp3) data_start = old_mp3.tell() old_mp3 = LimitedFileReader(old_mp3, data_end - data_start) #write id3v2 + data + id3v1 to file if (isinstance(metadata, ID3CommentPair)): metadata.id3v2.build(BitstreamWriter(new_mp3, 0)) transfer_data(old_mp3.read, new_mp3.write) metadata.id3v1.build(new_mp3) elif (isinstance(metadata, ID3v2Comment)): metadata.build(BitstreamWriter(new_mp3, 0)) transfer_data(old_mp3.read, new_mp3.write) elif (isinstance(metadata, ID3v1Comment)): transfer_data(old_mp3.read, new_mp3.write) metadata.build(new_mp3) #commit change to disk old_mp3.close() new_mp3.close()
def update_metadata(self, metadata): """takes this track's current MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object raises IOError if unable to write the file """ import os from audiotools import (TemporaryFile, LimitedFileReader, transfer_data) from audiotools.id3 import (ID3v2Comment, ID3CommentPair) from audiotools.id3v1 import ID3v1Comment from audiotools.bitstream import BitstreamWriter if metadata is None: return elif (not (isinstance(metadata, ID3v2Comment) or isinstance(metadata, ID3CommentPair) or isinstance(metadata, ID3v1Comment))): from audiotools.text import ERR_FOREIGN_METADATA raise ValueError(ERR_FOREIGN_METADATA) elif not os.access(self.filename, os.W_OK): raise IOError(self.filename) new_mp3 = TemporaryFile(self.filename) # get the original MP3 data old_mp3 = open(self.filename, "rb") MP3Audio.__find_last_mp3_frame__(old_mp3) data_end = old_mp3.tell() old_mp3.seek(0, 0) MP3Audio.__find_mp3_start__(old_mp3) data_start = old_mp3.tell() old_mp3 = LimitedFileReader(old_mp3, data_end - data_start) # write id3v2 + data + id3v1 to file if isinstance(metadata, ID3CommentPair): metadata.id3v2.build(BitstreamWriter(new_mp3, False)) transfer_data(old_mp3.read, new_mp3.write) metadata.id3v1.build(new_mp3) elif isinstance(metadata, ID3v2Comment): metadata.build(BitstreamWriter(new_mp3, False)) transfer_data(old_mp3.read, new_mp3.write) elif isinstance(metadata, ID3v1Comment): transfer_data(old_mp3.read, new_mp3.write) metadata.build(new_mp3) # commit change to disk old_mp3.close() new_mp3.close()
def delete_metadata(self): """deletes the track's MetaData this removes or unsets tags as necessary in order to remove all data raises IOError if unable to write the file""" import os from audiotools import transfer_data, TemporaryFile if not os.access(self.filename, os.W_OK): raise IOError(self.filename) new_aiff = TemporaryFile(self.filename) self.__class__.aiff_from_chunks( new_aiff, [c for c in self.chunks() if c.id != b"ID3 "]) new_aiff.close()
def update_metadata(self, metadata): """takes this track's current MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object raises IOError if unable to write the file """ from audiotools import transfer_data, TemporaryFile from audiotools.id3 import ID3v22Comment from audiotools.bitstream import BitstreamRecorder from audiotools.text import ERR_FOREIGN_METADATA import os if metadata is None: return elif not isinstance(metadata, ID3v22Comment): raise ValueError(ERR_FOREIGN_METADATA) elif not os.access(self.filename, os.W_OK): raise IOError(self.filename) # turn our ID3v2.2 tag into a raw binary chunk id3_chunk = BitstreamRecorder(0) metadata.build(id3_chunk) # generate a temporary AIFF file in which our new ID3v2.2 chunk # replaces the existing ID3v2.2 chunk new_aiff = TemporaryFile(self.filename) self.__class__.aiff_from_chunks( new_aiff, [(chunk if chunk.id != b"ID3 " else AIFF_Chunk(b"ID3 ", id3_chunk.bytes(), id3_chunk.data())) for chunk in self.chunks()]) new_aiff.close()
def set_replay_gain(self, replaygain): """given a ReplayGain object, sets the track's gain to those values may raise IOError if unable to modify the file""" from math import log10 from audiotools import TemporaryFile gain_title = int(round((64.82 - replaygain.track_gain) * 256)) if replaygain.track_peak > 0.0: peak_title = int(log10(replaygain.track_peak * 2**15) * 20 * 256) else: peak_title = 0 gain_album = int(round((64.82 - replaygain.album_gain) * 256)) if replaygain.album_peak > 0.0: peak_album = int(log10(replaygain.album_peak * 2**15) * 20 * 256) else: peak_album = 0 #FIXME - check for missing "RG" block and add one if not present metadata = self.get_metadata() writer = BitstreamWriter(TemporaryFile(self.filename), False) writer.write_bytes(b"MPCK") for key, size, block in self.blocks(): if key != b"RG": writer.write_bytes(key) size.build(writer) writer.write_bytes(block) else: writer.write_bytes(b"RG") MPC_Size(2 + 1 + 1 + 2 * 4, 1).build(writer) writer.write(8, 1) writer.write(16, gain_title) writer.write(16, peak_title) writer.write(16, gain_album) writer.write(16, peak_album) if metadata is not None: writer.set_endianness(True) metadata.build(writer) writer.close()
def delete_replay_gain(self): """removes ReplayGain values from file, if any may raise IOError if unable to modify the file""" from audiotools import TemporaryFile writer = BitstreamWriter(TemporaryFile(self.filename), False) writer.write_bytes(b"MPCK") for key, size, block in self.blocks(): if key != b"RG": writer.write_bytes(key) size.build(writer) writer.write_bytes(block) else: writer.write_bytes(b"RG") MPC_Size(2 + 1 + 1 + 2 * 4, 1).build(writer) writer.write(8, 1) writer.write(16, 0) writer.write(16, 0) writer.write(16, 0) writer.write(16, 0) writer.close()
def update_metadata(self, metadata): """takes this track's current MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object raises IOError if unable to write the file """ from audiotools.bitstream import (parse, BitstreamWriter, BitstreamReader) from audiotools import transfer_data if metadata is None: return elif not isinstance(metadata, ApeTag): from audiotools.text import ERR_FOREIGN_METADATA raise ValueError(ERR_FOREIGN_METADATA) elif len(metadata.keys()) == 0: # wipe out entire block of metadata from os import access, R_OK, W_OK if not access(self.filename, R_OK | W_OK): raise IOError(self.filename) with open(self.filename, "rb") as f: f.seek(-32, 2) (preamble, version, tag_size, item_count, read_only, item_encoding, is_header, no_footer, has_header) = BitstreamReader(f, True).parse( ApeTag.HEADER_FORMAT) if (preamble == b'APETAGEX') and (version == 2000): from audiotools import TemporaryFile, transfer_data from os.path import getsize # there's existing metadata to delete # so rewrite file without trailing metadata tag if has_header: old_tag_size = 32 + tag_size else: old_tag_size = tag_size # copy everything but the last "old_tag_size" bytes # from existing file to rewritten file new_apev2 = TemporaryFile(self.filename) old_apev2 = open(self.filename, "rb") limited_transfer_data( old_apev2.read, new_apev2.write, getsize(self.filename) - old_tag_size) old_apev2.close() new_apev2.close() else: # re-set metadata block at end of file f = open(self.filename, "r+b") f.seek(-32, 2) tag_footer = f.read(32) if len(tag_footer) < 32: # no existing ApeTag can fit, so append fresh tag f.close() with BitstreamWriter(open(self.filename, "ab"), True) as writer: metadata.build(writer) return (preamble, version, tag_size, item_count, read_only, item_encoding, is_header, no_footer, has_header) = parse(ApeTag.HEADER_FORMAT, True, tag_footer) if (preamble == b'APETAGEX') and (version == 2000): if has_header: old_tag_size = 32 + tag_size else: old_tag_size = tag_size if metadata.total_size() >= old_tag_size: # metadata has grown # so append it to existing file f.seek(-old_tag_size, 2) writer = BitstreamWriter(f, True) metadata.build(writer) writer.close() else: f.close() # metadata has shrunk # so rewrite file with smaller metadata from audiotools import TemporaryFile from os.path import getsize # copy everything but the last "old_tag_size" bytes # from existing file to rewritten file new_apev2 = TemporaryFile(self.filename) with open(self.filename, "rb") as old_apev2: limited_transfer_data( old_apev2.read, new_apev2.write, getsize(self.filename) - old_tag_size) # append new tag to rewritten file with BitstreamWriter(new_apev2, True) as writer: metadata.build(writer) # closing writer closes new_apev2 also else: # no existing metadata, so simply append a fresh tag f.close() with BitstreamWriter(open(self.filename, "ab"), True) as writer: metadata.build(writer)
def update_metadata(self, metadata): """takes this track's current MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object raises IOError if unable to write the file """ import os from audiotools.ape import ApeTag from audiotools.id3 import ID3v2Comment from audiotools.id3 import ID3CommentPair from audiotools.id3v1 import ID3v1Comment if (metadata is None): return elif (not os.access(self.filename, os.W_OK)): raise IOError(self.filename) # ensure metadata is APEv2, ID3v2, ID3v1, or ID3CommentPair if (((not isinstance(metadata, ApeTag)) and (not isinstance(metadata, ID3v2Comment)) and (not isinstance(metadata, ID3CommentPair)) and (not isinstance(metadata, ID3v1Comment)))): from audiotools.text import ERR_FOREIGN_METADATA raise ValueError(ERR_FOREIGN_METADATA) current_metadata = self.get_metadata() if (isinstance(metadata, ApeTag) and (current_metadata is None)): # if new metadata is APEv2 and no current metadata, # simply append APEv2 tag from audiotools.bitstream import BitstreamWriter with BitstreamWriter(open(self.filename, "ab"), True) as writer: metadata.build(writer) elif (isinstance(metadata, ApeTag) and isinstance(current_metadata, ApeTag) and (metadata.total_size() > current_metadata.total_size())): # if new metadata is APEv2, current metadata is APEv2 # and new metadata is larger, # overwrite old tag with new tag from audiotools.bitstream import BitstreamWriter with open(self.filename, "r+b") as f: f.seek(-current_metadata.total_size(), 2) metadata.build(BitstreamWriter(f, True)) else: from audiotools.bitstream import BitstreamWriter from audiotools import (transfer_data, LimitedFileReader, TemporaryFile) from audiotools.id3 import skip_id3v2_comment # otherwise, rebuild TTA with APEv2/ID3 tags in place old_tta = open(self.filename, "rb") skip_id3v2_comment(old_tta) old_tta = LimitedFileReader(old_tta, self.data_size()) new_tta = TemporaryFile(self.filename) if (isinstance(metadata, ApeTag)): transfer_data(old_tta.read, new_tta.write) metadata.build(BitstreamWriter(new_tta, True)) elif (isinstance(metadata, ID3CommentPair)): metadata.id3v2.build(BitstreamWriter(new_tta, False)) transfer_data(old_tta.read, new_tta.write) metadata.id3v1.build(new_tta) elif (isinstance(metadata, ID3v2Comment)): metadata.build(BitstreamWriter(new_tta, False)) transfer_data(old_tta.read, new_tta.write) else: # ID3v1Comment transfer_data(old_tta.read, new_tta.write) metadata.build(new_tta) old_tta.close() new_tta.close()
def update_metadata(self, metadata, old_metadata=None): """takes this track's updated MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object old_metadata is the unmodifed metadata returned by get_metadata() raises IOError if unable to write the file """ from audiotools.bitstream import BitstreamWriter from audiotools.bitstream import BitstreamReader import os.path if metadata is None: return if not isinstance(metadata, M4A_META_Atom): from audiotools.text import ERR_FOREIGN_METADATA raise ValueError(ERR_FOREIGN_METADATA) if old_metadata is None: # get_metadata() result may still be None, and that's okay old_metadata = self.get_metadata() # M4A streams often have *two* "free" atoms we can attempt to resize # first, attempt to resize the one inside the "meta" atom if ((old_metadata is not None) and metadata.has_child(b"free") and ((metadata.size() - metadata[b"free"].size()) <= old_metadata.size())): metadata.replace_child( M4A_FREE_Atom(old_metadata.size() - (metadata.size() - metadata[b"free"].size()))) f = open(self.filename, 'r+b') (meta_size, meta_offset) = get_m4a_atom_offset(BitstreamReader(f, False), b"moov", b"udta", b"meta") f.seek(meta_offset + 8, 0) with BitstreamWriter(f, False) as writer: metadata.build(writer) # writer will close "f" when finished else: from audiotools import TemporaryFile # if there's insufficient room, # attempt to resize the outermost "free" also # this is only possible if the file is laid out correctly, # with "free" coming after "moov" but before "mdat" # FIXME # if neither fix is possible, the whole file must be rewritten # which also requires adjusting the "stco" atom offsets with open(self.filename, "rb") as f: m4a_tree = M4A_Tree_Atom.parse( None, os.path.getsize(self.filename), BitstreamReader(f, False), { b"moov": M4A_Tree_Atom, b"trak": M4A_Tree_Atom, b"mdia": M4A_Tree_Atom, b"minf": M4A_Tree_Atom, b"stbl": M4A_Tree_Atom, b"stco": M4A_STCO_Atom, b"udta": M4A_Tree_Atom }) # find initial mdat offset initial_mdat_offset = m4a_tree.child_offset(b"mdat") # adjust moov -> udta -> meta atom # (generating sub-atoms as necessary) if not m4a_tree.has_child(b"moov"): return else: moov = m4a_tree[b"moov"] if not moov.has_child(b"udta"): moov.add_child(M4A_Tree_Atom(b"udta", [])) udta = moov[b"udta"] if not udta.has_child(b"meta"): udta.add_child(metadata) else: udta.replace_child(metadata) # find new mdat offset new_mdat_offset = m4a_tree.child_offset(b"mdat") # adjust moov -> trak -> mdia -> minf -> stbl -> stco offsets # based on the difference between the new mdat position and the old try: delta_offset = new_mdat_offset - initial_mdat_offset stco = m4a_tree[b"moov"][b"trak"][b"mdia"][b"minf"][b"stbl"][ b"stco"] stco.offsets = [ offset + delta_offset for offset in stco.offsets ] except KeyError: # if there is no stco atom, don't worry about it pass # then write entire tree back to disk with BitstreamWriter(TemporaryFile(self.filename), False) as writer: m4a_tree.build(writer)
def update_metadata(self, metadata): """takes this track's current MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object raises IOError if unable to write the file """ import os from audiotools import TemporaryFile from audiotools.ogg import (PageReader, PacketReader, PageWriter, packet_to_pages, packets_to_pages) from audiotools.vorbiscomment import VorbisComment from audiotools.bitstream import BitstreamRecorder if (metadata is None): return elif (not isinstance(metadata, VorbisComment)): from .text import ERR_FOREIGN_METADATA raise ValueError(ERR_FOREIGN_METADATA) elif (not os.access(self.filename, os.W_OK)): raise IOError(self.filename) original_ogg = PacketReader(PageReader(file(self.filename, "rb"))) new_ogg = PageWriter(TemporaryFile(self.filename)) sequence_number = 0 #transfer current file's identification packet in its own page identification_packet = original_ogg.read_packet() for (i, page) in enumerate(packet_to_pages( identification_packet, self.__serial_number__, starting_sequence_number=sequence_number)): page.stream_beginning = (i == 0) new_ogg.write(page) sequence_number += 1 #discard the current file's comment packet comment_packet = original_ogg.read_packet() #generate new comment packet comment_writer = BitstreamRecorder(True) comment_writer.build("8u 6b", (3, "vorbis")) vendor_string = metadata.vendor_string.encode('utf-8') comment_writer.build("32u %db" % (len(vendor_string)), (len(vendor_string), vendor_string)) comment_writer.write(32, len(metadata.comment_strings)) for comment_string in metadata.comment_strings: comment_string = comment_string.encode('utf-8') comment_writer.build("32u %db" % (len(comment_string)), (len(comment_string), comment_string)) comment_writer.build("1u a", (1,)) # framing bit #transfer codebooks packet from original file to new file codebooks_packet = original_ogg.read_packet() for page in packets_to_pages( [comment_writer.data(), codebooks_packet], self.__serial_number__, starting_sequence_number=sequence_number): new_ogg.write(page) sequence_number += 1 #transfer remaining pages after re-sequencing page = original_ogg.read_page() page.sequence_number = sequence_number sequence_number += 1 new_ogg.write(page) while (not page.stream_end): page = original_ogg.read_page() page.sequence_number = sequence_number page.bitstream_serial_number = self.__serial_number__ sequence_number += 1 new_ogg.write(page) original_ogg.close() new_ogg.close()
def update_metadata(self, metadata): """takes this track's current MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object raises IOError if unable to write the file """ if (metadata is None): return elif (not isinstance(metadata, ApeTag)): from audiotools.text import ERR_FOREIGN_METADATA raise ValueError(ERR_FOREIGN_METADATA) from audiotools.bitstream import BitstreamReader, BitstreamWriter from audiotools import transfer_data f = open(self.filename, "r+b") f.seek(-32, 2) (preamble, version, tag_size, item_count, read_only, item_encoding, is_header, no_footer, has_header) = BitstreamReader(f, 1).parse(ApeTag.HEADER_FORMAT) if ((preamble == 'APETAGEX') and (version == 2000)): if (has_header): old_tag_size = 32 + tag_size else: old_tag_size = tag_size if (metadata.total_size() >= old_tag_size): # metadata has grown # so append it to existing file f.seek(-old_tag_size, 2) metadata.build(BitstreamWriter(f, 1)) else: # metadata has shrunk # so rewrite file with smaller metadata from audiotools import TemporaryFile from os.path import getsize # copy everything but the last "old_tag_size" bytes # from existing file to rewritten file new_apev2 = TemporaryFile(self.filename) old_apev2 = open(self.filename, "rb") limited_transfer_data( old_apev2.read, new_apev2.write, getsize(self.filename) - old_tag_size) # append new tag to rewritten file metadata.build(BitstreamWriter(new_apev2, 1)) old_apev2.close() new_apev2.close() else: # no existing metadata, so simply append a fresh tag f = open(self.filename, "ab") metadata.build(BitstreamWriter(f, 1)) f.close()
def update_metadata(self, metadata): """takes this track's current MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object raises IOError if unable to write the file """ import os from audiotools import TemporaryFile from audiotools.ogg import (PageReader, PacketReader, PageWriter, packet_to_pages) from audiotools.bitstream import BitstreamRecorder if (metadata is None): return elif (not isinstance(metadata, VorbisComment)): from .text import ERR_FOREIGN_METADATA raise ValueError(ERR_FOREIGN_METADATA) elif (not os.access(self.filename, os.W_OK)): raise IOError(self.filename) original_ogg = PacketReader(PageReader(file(self.filename, "rb"))) new_ogg = PageWriter(TemporaryFile(self.filename)) #transfer current file's identification page/packet #(the ID packet is always fixed size, and fits in one page) identification_page = original_ogg.read_page() new_ogg.write(identification_page) sequence_number = 1 #discard the current file's comment packet original_ogg.read_packet() #write the new comment packet in its own page(s) comment_writer = BitstreamRecorder(True) comment_writer.write_bytes("OpusTags") vendor_string = metadata.vendor_string.encode('utf-8') comment_writer.build("32u %db" % (len(vendor_string)), (len(vendor_string), vendor_string)) comment_writer.write(32, len(metadata.comment_strings)) for comment_string in metadata.comment_strings: comment_string = comment_string.encode('utf-8') comment_writer.build("32u %db" % (len(comment_string)), (len(comment_string), comment_string)) for page in packet_to_pages( comment_writer.data(), identification_page.bitstream_serial_number, starting_sequence_number=sequence_number): new_ogg.write(page) sequence_number += 1 #transfer remaining pages after re-sequencing page = original_ogg.read_page() page.sequence_number = sequence_number sequence_number += 1 new_ogg.write(page) while (not page.stream_end): page = original_ogg.read_page() page.sequence_number = sequence_number sequence_number += 1 new_ogg.write(page) original_ogg.close() new_ogg.close()