diff -Nru mutagen-1.42.0/azure-pipelines.yml mutagen-1.43.0+1/azure-pipelines.yml --- mutagen-1.42.0/azure-pipelines.yml 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/azure-pipelines.yml 2019-11-17 19:59:34.000000000 +0000 @@ -7,9 +7,6 @@ Python27-x64: python.version: '2.7' python.arch: 'x64' - Python34-x64: - python.version: '3.4' - python.arch: 'x64' Python35-x64: python.version: '3.5' python.arch: 'x64' diff -Nru mutagen-1.42.0/debian/changelog mutagen-1.43.0+1/debian/changelog --- mutagen-1.42.0/debian/changelog 2019-10-24 20:33:56.000000000 +0000 +++ mutagen-1.43.0+1/debian/changelog 2019-11-24 17:16:35.000000000 +0000 @@ -1,5 +1,5 @@ -mutagen (1.42.0-0~ppa0~xenial) xenial; urgency=medium +mutagen (1.43.0+1-0~ppa1~xenial) xenial; urgency=medium * unofficial/untested trunk build - -- Christoph Reiter Thu, 24 Oct 2019 22:33:56 +0200 + -- Christoph Reiter Sun, 24 Nov 2019 18:16:35 +0100 diff -Nru mutagen-1.42.0/debian/control mutagen-1.43.0+1/debian/control --- mutagen-1.42.0/debian/control 2019-10-24 20:33:56.000000000 +0000 +++ mutagen-1.43.0+1/debian/control 2019-11-24 17:16:35.000000000 +0000 @@ -13,6 +13,8 @@ oggz-tools, python-all (>= 2.6.6-3~), python3-all, + python-setuptools, + python3-setuptools, vorbis-tools, dh-python Standards-Version: 3.9.6 @@ -24,7 +26,8 @@ Package: python-mutagen Architecture: all -Depends: ${misc:Depends}, ${python:Depends} +Depends: ${misc:Depends}, ${python:Depends}, + python-pkg-resources Description: audio metadata editing library Mutagen is a Python module to handle audio metadata. It supports FLAC, M4A, MP3, Ogg FLAC, Ogg Speex, Ogg Theora, Ogg Vorbis, True Audio, and @@ -37,7 +40,8 @@ Package: python3-mutagen Architecture: all -Depends: ${misc:Depends}, ${python3:Depends} +Depends: ${misc:Depends}, ${python3:Depends}, + python3-pkg-resources Description: audio metadata editing library (Python 3) Mutagen is a Python module to handle audio metadata. It supports FLAC, M4A, MP3, Ogg FLAC, Ogg Speex, Ogg Theora, Ogg Vorbis, True Audio, and diff -Nru mutagen-1.42.0/docs/api/ac3.rst mutagen-1.43.0+1/docs/api/ac3.rst --- mutagen-1.42.0/docs/api/ac3.rst 1970-01-01 00:00:00.000000000 +0000 +++ mutagen-1.43.0+1/docs/api/ac3.rst 2019-11-17 19:59:34.000000000 +0000 @@ -0,0 +1,14 @@ +AC3 +=== + +.. automodule:: mutagen.ac3 + +.. autoexception:: mutagen.ac3.AC3Error + +.. autoclass:: mutagen.ac3.AC3(filename) + :show-inheritance: + :members: + +.. autoclass:: mutagen.ac3.AC3Info() + :show-inheritance: + :members: diff -Nru mutagen-1.42.0/docs/api/index.rst mutagen-1.43.0+1/docs/api/index.rst --- mutagen-1.42.0/docs/api/index.rst 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/docs/api/index.rst 2019-11-17 19:59:34.000000000 +0000 @@ -5,6 +5,7 @@ base aac + ac3 aiff ape asf @@ -23,6 +24,7 @@ oggvorbis optimfrog smf + tak trueaudio vcomment wavpack diff -Nru mutagen-1.42.0/docs/api/tak.rst mutagen-1.43.0+1/docs/api/tak.rst --- mutagen-1.42.0/docs/api/tak.rst 1970-01-01 00:00:00.000000000 +0000 +++ mutagen-1.43.0+1/docs/api/tak.rst 2019-11-17 19:59:34.000000000 +0000 @@ -0,0 +1,11 @@ +TAK +=== + +.. automodule:: mutagen.tak + +.. autoclass:: mutagen.tak.TAK + :show-inheritance: + :members: + +.. autoclass:: mutagen.tak.TAKInfo + :members: diff -Nru mutagen-1.42.0/docs/user/gettingstarted.rst mutagen-1.43.0+1/docs/user/gettingstarted.rst --- mutagen-1.42.0/docs/user/gettingstarted.rst 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/docs/user/gettingstarted.rst 2019-11-17 19:59:34.000000000 +0000 @@ -32,7 +32,7 @@ audio.save() -The following example gets the length and bitrate of an MP3 file +The following example gets the length and bitrate of an MP3 file. :: @@ -43,7 +43,7 @@ print(audio.info.bitrate) -The following deletes an ID3 tag from an MP3 file +The following deletes an ID3 tag from an MP3 file. :: @@ -78,13 +78,13 @@ Mutagen has full Unicode support for all formats. When you assign text strings, we strongly recommend using Python unicode objects rather than str objects. If you use str objects, Mutagen will assume they are -in UTF-8 (This does not apply to filenames) +in UTF-8. (This does not apply to filenames.) Multiple Values ^^^^^^^^^^^^^^^ Most tag formats support multiple values for each key, so when you -access then (e.g. ``audio["title"]``) you will get a list of strings +access them (e.g. ``audio["title"]``) you will get a list of strings rather than a single one (``[u"An example"]`` rather than ``u"An example"``). Similarly, you can assign a list of strings rather than a single one. diff -Nru mutagen-1.42.0/mutagen/ac3.py mutagen-1.43.0+1/mutagen/ac3.py --- mutagen-1.42.0/mutagen/ac3.py 1970-01-01 00:00:00.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/ac3.py 2019-11-17 19:59:34.000000000 +0000 @@ -0,0 +1,330 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2019 Philipp Wolfer +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + + +"""Pure AC3 file information. +""" + +__all__ = ["AC3", "Open"] + +from mutagen import StreamInfo +from mutagen._compat import endswith +from mutagen._file import FileType +from mutagen._util import ( + BitReader, + BitReaderError, + MutagenError, + convert_error, + enum, + loadfile, +) + + +@enum +class ChannelMode(object): + DUALMONO = 0 + MONO = 1 + STEREO = 2 + C3F = 3 + C2F1R = 4 + C3F1R = 5 + C2F2R = 6 + C3F2R = 7 + + +AC3_CHANNELS = { + ChannelMode.DUALMONO: 2, + ChannelMode.MONO: 1, + ChannelMode.STEREO: 2, + ChannelMode.C3F: 3, + ChannelMode.C2F1R: 3, + ChannelMode.C3F1R: 4, + ChannelMode.C2F2R: 4, + ChannelMode.C3F2R: 5 +} + +AC3_HEADER_SIZE = 7 + +AC3_SAMPLE_RATES = [48000, 44100, 32000] + +AC3_BITRATES = [ + 32, 40, 48, 56, 64, 80, 96, 112, 128, + 160, 192, 224, 256, 320, 384, 448, 512, 576, 640 +] + + +@enum +class EAC3FrameType(object): + INDEPENDENT = 0 + DEPENDENT = 1 + AC3_CONVERT = 2 + RESERVED = 3 + + +EAC3_BLOCKS = [1, 2, 3, 6] + + +class AC3Error(MutagenError): + pass + + +class AC3Info(StreamInfo): + + """AC3 stream information. + The length of the stream is just a guess and might not be correct. + + Attributes: + channels (`int`): number of audio channels + length (`float`): file length in seconds, as a float + sample_rate (`int`): audio sampling rate in Hz + bitrate (`int`): audio bitrate, in bits per second + codec (`str`): ac-3 or ec-3 (Enhanced AC-3) + """ + + channels = 0 + length = 0 + sample_rate = 0 + bitrate = 0 + codec = 'ac-3' + + @convert_error(IOError, AC3Error) + def __init__(self, fileobj): + """Raises AC3Error""" + header = bytearray(fileobj.read(6)) + + if len(header) < 6: + raise AC3Error("not enough data") + + if not header.startswith(b"\x0b\x77"): + raise AC3Error("not a AC3 file") + + bitstream_id = header[5] >> 3 + if bitstream_id > 16: + raise AC3Error("invalid bitstream_id %i" % bitstream_id) + + fileobj.seek(2) + self._read_header(fileobj, bitstream_id) + + def _read_header(self, fileobj, bitstream_id): + bitreader = BitReader(fileobj) + try: + # This is partially based on code from + # https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/ac3_parser.c + if bitstream_id <= 10: # Normal AC-3 + self._read_header_normal(bitreader, bitstream_id) + else: # Enhanced AC-3 + self._read_header_enhanced(bitreader) + except BitReaderError as e: + raise AC3Error(e) + + self.length = self._guess_length(fileobj) + + def _read_header_normal(self, bitreader, bitstream_id): + r = bitreader + r.skip(16) # 16 bit CRC + sr_code = r.bits(2) + if sr_code == 3: + raise AC3Error("invalid sample rate code %i" % sr_code) + + frame_size_code = r.bits(6) + if frame_size_code > 37: + raise AC3Error("invalid frame size code %i" % frame_size_code) + + r.skip(5) # bitstream ID, already read + r.skip(3) # bitstream mode, not needed + channel_mode = ChannelMode(r.bits(3)) + r.skip(2) # dolby surround mode or surround mix level + lfe_on = r.bits(1) + + sr_shift = max(bitstream_id, 8) - 8 + try: + self.sample_rate = AC3_SAMPLE_RATES[sr_code] >> sr_shift + self.bitrate = (AC3_BITRATES[frame_size_code >> 1] * 1000 + ) >> sr_shift + except KeyError as e: + raise AC3Error(e) + self.channels = self._get_channels(channel_mode, lfe_on) + self._skip_unused_header_bits_normal(r, channel_mode) + + def _read_header_enhanced(self, bitreader): + r = bitreader + self.codec = "ec-3" + frame_type = r.bits(2) + if frame_type == EAC3FrameType.RESERVED: + raise AC3Error("invalid frame type %i" % frame_type) + + r.skip(3) # substream ID, not needed + + frame_size = (r.bits(11) + 1) << 1 + if frame_size < AC3_HEADER_SIZE: + raise AC3Error("invalid frame size %i" % frame_size) + + sr_code = r.bits(2) + try: + if sr_code == 3: + sr_code2 = r.bits(2) + if sr_code2 == 3: + raise AC3Error("invalid sample rate code %i" % sr_code2) + + numblocks_code = 3 + self.sample_rate = AC3_SAMPLE_RATES[sr_code2] // 2 + else: + numblocks_code = r.bits(2) + self.sample_rate = AC3_SAMPLE_RATES[sr_code] + + channel_mode = ChannelMode(r.bits(3)) + lfe_on = r.bits(1) + self.bitrate = 8 * frame_size * self.sample_rate // ( + EAC3_BLOCKS[numblocks_code] * 256) + except KeyError as e: + raise AC3Error(e) + r.skip(5) # bitstream ID, already read + self.channels = self._get_channels(channel_mode, lfe_on) + self._skip_unused_header_bits_enhanced( + r, frame_type, channel_mode, sr_code, numblocks_code) + + @staticmethod + def _skip_unused_header_bits_normal(bitreader, channel_mode): + r = bitreader + r.skip(5) # Dialogue Normalization + if r.bits(1): # Compression Gain Word Exists + r.skip(8) # Compression Gain Word + if r.bits(1): # Language Code Exists + r.skip(8) # Language Code + if r.bits(1): # Audio Production Information Exists + # Mixing Level, 5 Bits + # Room Type, 2 Bits + r.skip(7) + if channel_mode == ChannelMode.DUALMONO: + r.skip(5) # Dialogue Normalization, ch2 + if r.bits(1): # Compression Gain Word Exists, ch2 + r.skip(8) # Compression Gain Word, ch2 + if r.bits(1): # Language Code Exists, ch2 + r.skip(8) # Language Code, ch2 + if r.bits(1): # Audio Production Information Exists, ch2 + # Mixing Level, ch2, 5 Bits + # Room Type, ch2, 2 Bits + r.skip(7) + # Copyright Bit, 1 Bit + # Original Bit Stream, 1 Bit + r.skip(2) + timecod1e = r.bits(1) # Time Code First Halve Exists + timecod2e = r.bits(1) # Time Code Second Halve Exists + if timecod1e: + r.skip(14) # Time Code First Half + if timecod2e: + r.skip(14) # Time Code Second Half + if r.bits(1): # Additional Bit Stream Information Exists + addbsil = r.bit(6) # Additional Bit Stream Information Length + r.skip((addbsil + 1) * 8) + + @staticmethod + def _skip_unused_header_bits_enhanced(bitreader, frame_type, channel_mode, + sr_code, numblocks_code): + r = bitreader + r.skip(5) # Dialogue Normalization + if r.bits(1): # Compression Gain Word Exists + r.skip(8) # Compression Gain Word + if channel_mode == ChannelMode.DUALMONO: + r.skip(5) # Dialogue Normalization, ch2 + if r.bits(1): # Compression Gain Word Exists, ch2 + r.skip(8) # Compression Gain Word, ch2 + if frame_type == EAC3FrameType.DEPENDENT: + if r.bits(1): # chanmap exists + r.skip(16) # chanmap + if r.bits(1): # mixmdate, 1 Bit + # FIXME: Handle channel dependent fields + return + if r.bits(1): # Informational Metadata Exists + # bsmod, 3 Bits + # Copyright Bit, 1 Bit + # Original Bit Stream, 1 Bit + r.skip(5) + if channel_mode == ChannelMode.STEREO: + # dsurmod. 2 Bits + # dheadphonmod, 2 Bits + r.skip(4) + elif channel_mode >= ChannelMode.C2F2R: + r.skip(2) # dsurexmod + if r.bits(1): # Audio Production Information Exists + # Mixing Level, 5 Bits + # Room Type, 2 Bits + # adconvtyp, 1 Bit + r.skip(8) + if channel_mode == ChannelMode.DUALMONO: + if r.bits(1): # Audio Production Information Exists, ch2 + # Mixing Level, ch2, 5 Bits + # Room Type, ch2, 2 Bits + # adconvtyp, ch2, 1 Bit + r.skip(8) + if sr_code < 3: # if not half sample rate + r.skip(1) # sourcefscod + if frame_type == EAC3FrameType.INDEPENDENT and numblocks_code == 3: + r.skip(1) # convsync + if frame_type == EAC3FrameType.AC3_CONVERT: + if numblocks_code != 3: + if r.bits(1): # blkid + r.skip(6) # frmsizecod + if r.bits(1): # Additional Bit Stream Information Exists + addbsil = r.bit(6) # Additional Bit Stream Information Length + r.skip((addbsil + 1) * 8) + + @staticmethod + def _get_channels(channel_mode, lfe_on): + try: + return AC3_CHANNELS[channel_mode] + lfe_on + except KeyError as e: + raise AC3Error(e) + + def _guess_length(self, fileobj): + # use bitrate + data size to guess length + if self.bitrate == 0: + return + start = fileobj.tell() + fileobj.seek(0, 2) + length = fileobj.tell() - start + return 8.0 * length / self.bitrate + + def pprint(self): + return u"%s, %d Hz, %.2f seconds, %d channel(s), %d bps" % ( + self.codec, self.sample_rate, self.length, self.channels, + self.bitrate) + + +class AC3(FileType): + """AC3(filething) + + Arguments: + filething (filething) + + Load AC3 or EAC3 files. + + Tagging is not supported. + Use the ID3/APEv2 classes directly instead. + + Attributes: + info (`AC3Info`) + """ + + _mimes = ["audio/ac3"] + + @loadfile() + def load(self, filething): + self.info = AC3Info(filething.fileobj) + + def add_tags(self): + raise AC3Error("doesn't support tags") + + @staticmethod + def score(filename, fileobj, header): + return header.startswith(b"\x0b\x77") * 2 \ + + (endswith(filename, ".ac3") or endswith(filename, ".eac3")) + + +Open = AC3 +error = AC3Error diff -Nru mutagen-1.42.0/mutagen/aiff.py mutagen-1.43.0+1/mutagen/aiff.py --- mutagen-1.42.0/mutagen/aiff.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/aiff.py 2019-11-17 19:59:34.000000000 +0000 @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright (C) 2014 Evan Purkhiser # 2014 Ben Ockmore +# 2019 Philipp Wolfer # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,8 +19,14 @@ from mutagen.id3 import ID3 from mutagen.id3._util import ID3NoHeaderError, error as ID3Error -from mutagen._util import resize_bytes, delete_bytes, MutagenError, loadfile, \ - convert_error +from mutagen._util import ( + MutagenError, + convert_error, + delete_bytes, + insert_bytes, + loadfile, + resize_bytes, +) __all__ = ["AIFF", "Open", "delete"] @@ -73,33 +80,44 @@ # Chunk headers are 8 bytes long (4 for ID and 4 for the size) HEADER_SIZE = 8 - def __init__(self, fileobj, parent_chunk=None): - self.__fileobj = fileobj - self.parent_chunk = parent_chunk - self.offset = fileobj.tell() - - header = fileobj.read(self.HEADER_SIZE) - if len(header) < self.HEADER_SIZE: - raise InvalidChunk() - - self.id, self.data_size = struct.unpack('>4si', header) + @classmethod + def parse(cls, fileobj, parent_chunk=None): + header = fileobj.read(cls.HEADER_SIZE) + if len(header) < cls.HEADER_SIZE: + raise InvalidChunk('Header size < %i' % cls.HEADER_SIZE) + id, data_size = struct.unpack('>4sI', header) try: - self.id = self.id.decode('ascii') - except UnicodeDecodeError: - raise InvalidChunk() - - if not is_valid_chunk_id(self.id): - raise InvalidChunk() - - self.size = self.HEADER_SIZE + self.data_size + id = id.decode('ascii').rstrip() + except UnicodeDecodeError as e: + raise InvalidChunk(e) + + if not is_valid_chunk_id(id): + raise InvalidChunk('Invalid chunk ID %s' % id) + + return cls.get_class(id)(fileobj, id, data_size, parent_chunk) + + @classmethod + def get_class(cls, id): + if id == 'FORM': + return FormIFFChunk + else: + return cls + + def __init__(self, fileobj, id, data_size, parent_chunk): + self._fileobj = fileobj + self.id = id + self.data_size = data_size + self.parent_chunk = parent_chunk self.data_offset = fileobj.tell() + self.offset = self.data_offset - self.HEADER_SIZE + self._calculate_size() def read(self): """Read the chunks data""" - self.__fileobj.seek(self.data_offset) - return self.__fileobj.read(self.data_size) + self._fileobj.seek(self.data_offset) + return self._fileobj.read(self.data_size) def write(self, data): """Write the chunk data""" @@ -107,109 +125,200 @@ if len(data) > self.data_size: raise ValueError - self.__fileobj.seek(self.data_offset) - self.__fileobj.write(data) + self._fileobj.seek(self.data_offset) + self._fileobj.write(data) + # Write the padding bytes + padding = self.padding() + if padding: + self._fileobj.seek(self.data_offset + self.data_size) + self._fileobj.write(b'\x00' * padding) def delete(self): """Removes the chunk from the file""" - delete_bytes(self.__fileobj, self.size, self.offset) + delete_bytes(self._fileobj, self.size, self.offset) if self.parent_chunk is not None: - self.parent_chunk._update_size( - self.parent_chunk.data_size - self.size) + self.parent_chunk._remove_subchunk(self) + self._fileobj.flush() - def _update_size(self, data_size): + def _update_size(self, size_diff, changed_subchunk=None): """Update the size of the chunk""" - self.__fileobj.seek(self.offset + 4) - self.__fileobj.write(pack('>I', data_size)) + old_size = self.size + self.data_size += size_diff + self._fileobj.seek(self.offset + 4) + self._fileobj.write(pack('>I', self.data_size)) + self._calculate_size() if self.parent_chunk is not None: - size_diff = self.data_size - data_size - self.parent_chunk._update_size( - self.parent_chunk.data_size - size_diff) - self.data_size = data_size - self.size = data_size + self.HEADER_SIZE + self.parent_chunk._update_size(self.size - old_size, self) + if changed_subchunk: + self._update_sibling_offsets( + changed_subchunk, old_size - self.size) + + def _calculate_size(self): + self.size = self.HEADER_SIZE + self.data_size + self.padding() + assert self.size % 2 == 0 def resize(self, new_data_size): """Resize the file and update the chunk sizes""" - resize_bytes( - self.__fileobj, self.data_size, new_data_size, self.data_offset) - self._update_size(new_data_size) + padding = new_data_size % 2 + resize_bytes(self._fileobj, self.data_size + self.padding(), + new_data_size + padding, self.data_offset) + size_diff = new_data_size - self.data_size + self._update_size(size_diff) + self._fileobj.flush() + + def padding(self): + """Returns the number of padding bytes (0 or 1). + IFF chunks are required to be a even number in total length. If + data_size is odd a padding byte will be added at the end. + """ + return self.data_size % 2 + + +class FormIFFChunk(IFFChunk): + """A IFF chunk containing other chunks. + This is either a 'LIST' or 'RIFF' + """ + + MIN_DATA_SIZE = 4 + + def __init__(self, fileobj, id, data_size, parent_chunk): + if id != u'FORM': + raise InvalidChunk('Expected FORM chunk, got %s' % id) + + IFFChunk.__init__(self, fileobj, id, data_size, parent_chunk) + + # Lists always store an addtional identifier as 4 bytes + if data_size < self.MIN_DATA_SIZE: + raise InvalidChunk('FORM data size < %i' % self.MIN_DATA_SIZE) + + # Read the FORM id (usually AIFF) + try: + self.name = fileobj.read(4).decode('ascii') + except UnicodeDecodeError as e: + raise error(e) + + # Load all IFF subchunks + self.__subchunks = [] + + def subchunks(self): + """Returns a list of all subchunks. + The list is lazily loaded on first access. + """ + if not self.__subchunks: + next_offset = self.data_offset + 4 + while next_offset < self.offset + self.size: + self._fileobj.seek(next_offset) + try: + chunk = IFFChunk.parse(self._fileobj, self) + except InvalidChunk: + break + self.__subchunks.append(chunk) + + # Calculate the location of the next chunk + next_offset = chunk.offset + chunk.size + return self.__subchunks + + def insert_chunk(self, id_, data=None): + """Insert a new chunk at the end of the FORM chunk""" + + assert isinstance(id_, text_type) + + if not is_valid_chunk_id(id_): + raise KeyError("Invalid IFF key.") + + next_offset = self.offset + self.size + size = self.HEADER_SIZE + data_size = 0 + if data: + data_size = len(data) + padding = data_size % 2 + size += data_size + padding + insert_bytes(self._fileobj, size, next_offset) + self._fileobj.seek(next_offset) + self._fileobj.write( + pack('>4si', id_.ljust(4).encode('ascii'), data_size)) + self._fileobj.seek(next_offset) + chunk = IFFChunk.parse(self._fileobj, self) + self._update_size(chunk.size) + if data: + chunk.write(data) + self.subchunks().append(chunk) + self._fileobj.flush() + return chunk + + def _remove_subchunk(self, chunk): + assert chunk in self.__subchunks + self._update_size(-chunk.size, chunk) + self.__subchunks.remove(chunk) + + def _update_sibling_offsets(self, changed_subchunk, size_diff): + """Update the offsets of subchunks after `changed_subchunk`. + """ + index = self.__subchunks.index(changed_subchunk) + sibling_chunks = self.__subchunks[index + 1:len(self.__subchunks)] + for sibling in sibling_chunks: + sibling.offset -= size_diff + sibling.data_offset -= size_diff class IFFFile(object): """Representation of a IFF file""" def __init__(self, fileobj): - self.__fileobj = fileobj - self.__chunks = {} - # AIFF Files always start with the FORM chunk which contains a 4 byte # ID before the start of other chunks fileobj.seek(0) - self.__chunks[u'FORM'] = IFFChunk(fileobj) - - # Skip past the 4 byte FORM id - fileobj.seek(IFFChunk.HEADER_SIZE + 4) - - # Where the next chunk can be located. We need to keep track of this - # since the size indicated in the FORM header may not match up with the - # offset determined from the size of the last chunk in the file - self.__next_offset = fileobj.tell() - - # Load all of the chunks - while True: - try: - chunk = IFFChunk(fileobj, self[u'FORM']) - except InvalidChunk: - break - self.__chunks[chunk.id.strip()] = chunk + self.root = IFFChunk.parse(fileobj) - # Calculate the location of the next chunk, - # considering the pad byte - self.__next_offset = chunk.offset + chunk.size - self.__next_offset += self.__next_offset % 2 - fileobj.seek(self.__next_offset) + if self.root.id != u'FORM': + raise InvalidChunk("Root chunk must be a RIFF chunk, got %s" + % self.root.id) def __contains__(self, id_): """Check if the IFF file contains a specific chunk""" assert_valid_chunk_id(id_) - - return id_ in self.__chunks + try: + self[id_] + return True + except KeyError: + return False def __getitem__(self, id_): """Get a chunk from the IFF file""" assert_valid_chunk_id(id_) - - try: - return self.__chunks[id_] - except KeyError: - raise KeyError( - "%r has no %r chunk" % (self.__fileobj, id_)) + if id_ == 'FORM': # For backwards compatibility + return self.root + found_chunk = None + for chunk in self.root.subchunks(): + if chunk.id == id_: + found_chunk = chunk + break + else: + raise KeyError("No %r chunk found" % id_) + return found_chunk def __delitem__(self, id_): """Remove a chunk from the IFF file""" assert_valid_chunk_id(id_) + self.delete_chunk(id_) - self.__chunks.pop(id_).delete() - - def insert_chunk(self, id_): - """Insert a new chunk at the end of the IFF file""" + def delete_chunk(self, id_): + """Remove a chunk from the RIFF file""" assert_valid_chunk_id(id_) + self[id_].delete() - self.__fileobj.seek(self.__next_offset) - self.__fileobj.write(pack('>4si', id_.ljust(4).encode('ascii'), 0)) - self.__fileobj.seek(self.__next_offset) - chunk = IFFChunk(self.__fileobj, self[u'FORM']) - self[u'FORM']._update_size(self[u'FORM'].data_size + chunk.size) + def insert_chunk(self, id_, data=None): + """Insert a new chunk at the end of the IFF file""" - self.__chunks[id_] = chunk - self.__next_offset = chunk.offset + chunk.size + assert_valid_chunk_id(id_) + return self.root.insert_chunk(id_, data) class AIFFInfo(StreamInfo): @@ -224,7 +333,7 @@ bitrate (`int`): audio bitrate, in bits per second channels (`int`): The number of audio channels sample_rate (`int`): audio sample rate, in Hz - sample_size (`int`): The audio sample size + bits_per_sample (`int`): The audio sample size """ length = 0 @@ -250,7 +359,8 @@ channels, frame_count, sample_size, sample_rate = info self.sample_rate = int(read_float(sample_rate)) - self.sample_size = sample_size + self.bits_per_sample = sample_size + self.sample_size = sample_size # For backward compatibility self.channels = channels self.bitrate = channels * sample_size * self.sample_rate self.length = frame_count / float(self.sample_rate) @@ -290,12 +400,7 @@ except ID3Error as e: reraise(error, e, sys.exc_info()[2]) - new_size = len(data) - new_size += new_size % 2 # pad byte - assert new_size % 2 == 0 - chunk.resize(new_size) - data += (new_size - len(data)) * b'\x00' - assert new_size == len(data) + chunk.resize(len(data)) chunk.write(data) @loadfile(writable=True) diff -Nru mutagen-1.42.0/mutagen/apev2.py mutagen-1.43.0+1/mutagen/apev2.py --- mutagen-1.42.0/mutagen/apev2.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/apev2.py 2019-11-17 19:59:34.000000000 +0000 @@ -32,7 +32,12 @@ import sys import struct -from collections import MutableSequence +try: + # Python 3 + from collections.abc import MutableSequence +except ImportError: + # Python 2.7 + from collections import MutableSequence from ._compat import (cBytesIO, PY3, text_type, PY2, reraise, swap_to_string, xrange) diff -Nru mutagen-1.42.0/mutagen/_compat.py mutagen-1.43.0+1/mutagen/_compat.py --- mutagen-1.42.0/mutagen/_compat.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/_compat.py 2019-11-17 19:59:34.000000000 +0000 @@ -16,7 +16,7 @@ from StringIO import StringIO BytesIO = StringIO from cStringIO import StringIO as cBytesIO - from itertools import izip + from itertools import izip, izip_longest long_ = long integer_types = (int, long) @@ -55,12 +55,14 @@ StringIO = StringIO from io import BytesIO cBytesIO = BytesIO + from itertools import zip_longest long_ = int integer_types = (int,) string_types = (str,) text_type = str + izip_longest = zip_longest izip = zip xrange = range cmp = lambda a, b: (a > b) - (a < b) diff -Nru mutagen-1.42.0/mutagen/_file.py mutagen-1.43.0+1/mutagen/_file.py --- mutagen-1.42.0/mutagen/_file.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/_file.py 2019-11-17 19:59:34.000000000 +0000 @@ -264,12 +264,14 @@ from mutagen.optimfrog import OptimFROG from mutagen.aiff import AIFF from mutagen.aac import AAC + from mutagen.ac3 import AC3 from mutagen.smf import SMF + from mutagen.tak import TAK from mutagen.dsf import DSF options = [MP3, TrueAudio, OggTheora, OggSpeex, OggVorbis, OggFLAC, FLAC, AIFF, APEv2File, MP4, ID3FileType, WavPack, - Musepack, MonkeysAudio, OptimFROG, ASF, OggOpus, AAC, - SMF, DSF] + Musepack, MonkeysAudio, OptimFROG, ASF, OggOpus, AAC, AC3, + SMF, TAK, DSF] if not options: return None diff -Nru mutagen-1.42.0/mutagen/flac.py mutagen-1.43.0+1/mutagen/flac.py --- mutagen-1.42.0/mutagen/flac.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/flac.py 2019-11-17 19:59:34.000000000 +0000 @@ -732,7 +732,9 @@ if self.tags is None: self.tags = block else: - raise FLACVorbisError("> 1 Vorbis comment block found") + # https://github.com/quodlibet/mutagen/issues/377 + # Something writes multiple and metaflac doesn't care + pass elif block.code == CueSheet.code: if self.cuesheet is None: self.cuesheet = block @@ -756,6 +758,7 @@ add_vorbiscomment = add_tags + @convert_error(IOError, error) @loadfile(writable=True) def delete(self, filething=None): """Remove Vorbis comments from a file. @@ -764,11 +767,12 @@ """ if self.tags is not None: - self.metadata_blocks.remove(self.tags) - try: - self.save(filething, padding=lambda x: 0) - finally: - self.metadata_blocks.append(self.tags) + temp_blocks = [ + b for b in self.metadata_blocks if b.code != VCFLACDict.code] + self._save(filething, temp_blocks, False, padding=lambda x: 0) + self.metadata_blocks[:] = [ + b for b in self.metadata_blocks + if b.code != VCFLACDict.code or b is self.tags] self.tags.clear() vc = property(lambda s: s.tags, doc="Alias for tags; don't use this.") @@ -840,6 +844,9 @@ If no filename is given, the one most recently loaded is used. """ + self._save(filething, self.metadata_blocks, deleteid3, padding) + + def _save(self, filething, metadata_blocks, deleteid3, padding): f = StrictFileObject(filething.fileobj) header = self.__check_header(f, filething.name) audio_offset = self.__find_audio_offset(f) @@ -854,7 +861,7 @@ content_size = get_size(f) - audio_offset assert content_size >= 0 data = MetadataBlock._writeblocks( - self.metadata_blocks, available, content_size, padding) + metadata_blocks, available, content_size, padding) data_size = len(data) resize_bytes(filething.fileobj, available, data_size, header) diff -Nru mutagen-1.42.0/mutagen/id3/_id3v1.py mutagen-1.43.0+1/mutagen/id3/_id3v1.py --- mutagen-1.42.0/mutagen/id3/_id3v1.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/id3/_id3v1.py 2019-11-17 19:59:34.000000000 +0000 @@ -147,8 +147,8 @@ elif frame_class["TDRC"]: frames["TDRC"] = frame_class["TDRC"](encoding=0, text=year) if comment and frame_class["COMM"]: - frames["COMM"] = frame_class["COMM"]( - encoding=0, lang="eng", desc="ID3v1 Comment", text=comment) + frames["COMM"] = frame_class["COMM"]( + encoding=0, lang="eng", desc="ID3v1 Comment", text=comment) # Don't read a track number if it looks like the comment was # padded with spaces instead of nulls (thanks, WinAmp). diff -Nru mutagen-1.42.0/mutagen/id3/_tags.py mutagen-1.43.0+1/mutagen/id3/_tags.py --- mutagen-1.42.0/mutagen/id3/_tags.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/id3/_tags.py 2019-11-17 19:59:34.000000000 +0000 @@ -7,11 +7,12 @@ # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. +import re import struct from mutagen._tags import Tags from mutagen._util import DictProxy, convert_error, read_full -from mutagen._compat import PY3, text_type, itervalues +from mutagen._compat import PY3, itervalues, izip_longest from ._util import BitPaddedInt, unsynch, ID3JunkFrameError, \ ID3EncryptionUnsupportedError, is_valid_frame_id, error, \ @@ -369,24 +370,23 @@ self.__update_common() # TDAT, TYER, and TIME have been turned into TDRC. - try: - date = text_type(self.get("TYER", "")) - if date.strip(u"\x00"): - self.pop("TYER") - dat = text_type(self.get("TDAT", "")) - if dat.strip("\x00"): - self.pop("TDAT") - date = "%s-%s-%s" % (date, dat[2:], dat[:2]) - time = text_type(self.get("TIME", "")) - if time.strip("\x00"): - self.pop("TIME") - date += "T%s:%s:00" % (time[:2], time[2:]) - if "TDRC" not in self: - self.add(TDRC(encoding=0, text=date)) - except UnicodeDecodeError: - # Old ID3 tags have *lots* of Unicode problems, so if TYER - # is bad, just chuck the frames. - pass + timestamps = [] + old_frames = [self.pop(n, []) for n in ["TYER", "TDAT", "TIME"]] + for y, d, t in izip_longest(*old_frames, fillvalue=u""): + ym = re.match(r"([0-9]+)\Z", y) + dm = re.match(r"([0-9]{2})([0-9]{2})\Z", d) + tm = re.match(r"([0-9]{2})([0-9]{2})\Z", t) + timestamp = "" + if ym: + timestamp += u"%s" % ym.groups() + if dm: + timestamp += u"-%s-%s" % dm.groups()[::-1] + if tm: + timestamp += u"T%s:%s:00" % tm.groups() + if timestamp: + timestamps.append(timestamp) + if timestamps and "TDRC" not in self: + self.add(TDRC(encoding=0, text=timestamps)) # TORY can be the first part of a TDOR. if "TORY" in self: diff -Nru mutagen-1.42.0/mutagen/__init__.py mutagen-1.43.0+1/mutagen/__init__.py --- mutagen-1.42.0/mutagen/__init__.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/__init__.py 2019-11-17 19:59:34.000000000 +0000 @@ -23,7 +23,7 @@ from mutagen._file import FileType, StreamInfo, File from mutagen._tags import Tags, Metadata, PaddingInfo -version = (1, 42, 0) +version = (1, 43, 0) """Version tuple.""" version_string = ".".join(map(str, version)) diff -Nru mutagen-1.42.0/mutagen/mp3/__init__.py mutagen-1.43.0+1/mutagen/mp3/__init__.py --- mutagen-1.42.0/mutagen/mp3/__init__.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/mp3/__init__.py 2019-11-17 19:59:34.000000000 +0000 @@ -306,7 +306,7 @@ bitrate (`int`): audio bitrate, in bits per second. In case :attr:`bitrate_mode` is :attr:`BitrateMode.UNKNOWN` the bitrate is guessed based on the first frame. - sample_rate (`int`) audio sample rate, in Hz + sample_rate (`int`): audio sample rate, in Hz encoder_info (`mutagen.text`): a string containing encoder name and possibly version. In case a lame tag is present this will start with ``"LAME "``, if unknown it is empty, otherwise the @@ -357,7 +357,7 @@ # find a sync in the first 1024K, give up after some invalid syncs max_read = 1024 * 1024 - max_syncs = 1000 + max_syncs = 1500 enough_frames = 4 min_frames = 2 diff -Nru mutagen-1.42.0/mutagen/mp4/__init__.py mutagen-1.43.0+1/mutagen/mp4/__init__.py --- mutagen-1.42.0/mutagen/mp4/__init__.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/mp4/__init__.py 2019-11-17 19:59:34.000000000 +0000 @@ -311,6 +311,7 @@ * '\\xa9mvi' -- Movement Index * 'shwm' -- work/movement * 'stik' -- Media Kind + * 'hdvd' -- HD Video * 'rtng' -- Content Rating * 'tves' -- TV Episode * 'tvsn' -- TV Season @@ -852,6 +853,7 @@ b"pcst": (__parse_bool, __render_bool), b"shwm": (__parse_integer, __render_integer, 1), b"stik": (__parse_integer, __render_integer, 1), + b"hdvd": (__parse_integer, __render_integer, 1), b"rtng": (__parse_integer, __render_integer, 1), b"covr": (__parse_cover, __render_cover), b"purl": (__parse_text, __render_text), diff -Nru mutagen-1.42.0/mutagen/ogg.py mutagen-1.43.0+1/mutagen/ogg.py --- mutagen-1.42.0/mutagen/ogg.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/ogg.py 2019-11-17 19:59:34.000000000 +0000 @@ -216,7 +216,7 @@ so also the CRC. If an error occurs (e.g. non-Ogg data is found), fileobj will - be left pointing to the place in the stream the error occured, + be left pointing to the place in the stream the error occurred, but the invalid data will be left intact (since this function does not change the total file size). """ diff -Nru mutagen-1.42.0/mutagen/oggvorbis.py mutagen-1.43.0+1/mutagen/oggvorbis.py --- mutagen-1.42.0/mutagen/oggvorbis.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/oggvorbis.py 2019-11-17 19:59:34.000000000 +0000 @@ -43,7 +43,7 @@ length (`float`): File length in seconds, as a float channels (`int`): Number of channels bitrate (`int`): Nominal ('average') bitrate in bits per second - sample_Rate (`int`): Sample rate in Hz + sample_rate (`int`): Sample rate in Hz """ diff -Nru mutagen-1.42.0/mutagen/optimfrog.py mutagen-1.43.0+1/mutagen/optimfrog.py --- mutagen-1.42.0/mutagen/optimfrog.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/optimfrog.py 2019-11-17 19:59:34.000000000 +0000 @@ -28,6 +28,18 @@ from mutagen.apev2 import APEv2File, error, delete +SAMPLE_TYPE_BITS = { + 0: 8, + 1: 8, + 2: 16, + 3: 16, + 4: 24, + 5: 24, + 6: 32, + 7: 32, +} + + class OptimFROGHeaderError(error): pass @@ -41,6 +53,8 @@ channels (`int`): number of audio channels length (`float`): file length in seconds, as a float sample_rate (`int`): audio sampling rate in Hz + bits_per_sample (`int`): the audio sample size + encoder_info (`mutagen.text`): encoder version, e.g. "5.100" """ @convert_error(IOError, OptimFROGHeaderError) @@ -48,18 +62,27 @@ """Raises OptimFROGHeaderError""" header = fileobj.read(76) - if (len(header) != 76 or not header.startswith(b"OFR ") or - struct.unpack("= 15: + encoder_id = struct.unpack("> 4) + 4500) + self.encoder_info = "%s.%s" % (version[0], version[1:]) + else: + self.encoder_info = "" def pprint(self): return u"OptimFROG, %.2f seconds, %d Hz" % (self.length, diff -Nru mutagen-1.42.0/mutagen/_senf/_argv.py mutagen-1.43.0+1/mutagen/_senf/_argv.py --- mutagen-1.42.0/mutagen/_senf/_argv.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/_senf/_argv.py 2019-11-17 19:59:34.000000000 +0000 @@ -22,7 +22,10 @@ import sys import ctypes -import collections +try: + from collections import abc +except ImportError: + import collections as abc from functools import total_ordering from ._compat import PY2, string_types @@ -57,7 +60,7 @@ @total_ordering -class Argv(collections.MutableSequence): +class Argv(abc.MutableSequence): """List[`fsnative`]: Like `sys.argv` but contains unicode keys and values under Windows + Python 2. diff -Nru mutagen-1.42.0/mutagen/_senf/_environ.py mutagen-1.43.0+1/mutagen/_senf/_environ.py --- mutagen-1.42.0/mutagen/_senf/_environ.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/_senf/_environ.py 2019-11-17 19:59:34.000000000 +0000 @@ -22,7 +22,10 @@ import os import ctypes -import collections +try: + from collections import abc +except ImportError: + import collections as abc from ._compat import text_type, PY2 from ._fsnative import path2fsn, is_win, _fsn2legacy, fsnative @@ -130,7 +133,7 @@ return key -class Environ(collections.MutableMapping): +class Environ(abc.MutableMapping): """Dict[`fsnative`, `fsnative`]: Like `os.environ` but contains unicode keys and values under Windows + Python 2. diff -Nru mutagen-1.42.0/mutagen/_senf/__init__.py mutagen-1.43.0+1/mutagen/_senf/__init__.py --- mutagen-1.42.0/mutagen/_senf/__init__.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/_senf/__init__.py 2019-11-17 19:59:34.000000000 +0000 @@ -42,7 +42,7 @@ supports_ansi_escape_codes, fsn2norm -version = (1, 3, 4) +version = (1, 3, 5) """Tuple[`int`, `int`, `int`]: The version tuple (major, minor, micro)""" diff -Nru mutagen-1.42.0/mutagen/tak.py mutagen-1.43.0+1/mutagen/tak.py --- mutagen-1.42.0/mutagen/tak.py 1970-01-01 00:00:00.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/tak.py 2019-11-17 19:59:34.000000000 +0000 @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2008 Lukáš Lalinský +# Copyright (C) 2019 Philipp Wolfer +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +"""Tom's lossless Audio Kompressor (TAK) streams with APEv2 tags. + +TAK is a lossless audio compressor developed by Thomas Becker. + +For more information, see: + +* http://www.thbeck.de/Tak/Tak.html +* http://wiki.hydrogenaudio.org/index.php?title=TAK +""" + +__all__ = ["TAK", "Open", "delete"] + +import struct + +from ._compat import endswith +from mutagen import StreamInfo +from mutagen.apev2 import ( + APEv2File, + delete, + error, +) +from mutagen._util import ( + BitReader, + BitReaderError, + convert_error, + enum, +) + + +@enum +class TAKMetadata(object): + END = 0 + STREAM_INFO = 1 + SEEK_TABLE = 2 # Removed in TAK 1.1.1 + SIMPLE_WAVE_DATA = 3 + ENCODER_INFO = 4 + UNUSED_SPACE = 5 # New in TAK 1.0.3 + MD5 = 6 # New in TAK 1.1.1 + LAST_FRAME_INFO = 7 # New in TAK 1.1.1 + + +CRC_SIZE = 3 + +ENCODER_INFO_CODEC_BITS = 6 +ENCODER_INFO_PROFILE_BITS = 4 +ENCODER_INFO_TOTAL_BITS = ENCODER_INFO_CODEC_BITS + ENCODER_INFO_PROFILE_BITS + +SIZE_INFO_FRAME_DURATION_BITS = 4 +SIZE_INFO_SAMPLE_NUM_BITS = 35 +SIZE_INFO_TOTAL_BITS = (SIZE_INFO_FRAME_DURATION_BITS + + SIZE_INFO_SAMPLE_NUM_BITS) + +AUDIO_FORMAT_DATA_TYPE_BITS = 3 +AUDIO_FORMAT_SAMPLE_RATE_BITS = 18 +AUDIO_FORMAT_SAMPLE_BITS_BITS = 5 +AUDIO_FORMAT_CHANNEL_NUM_BITS = 4 +AUDIO_FORMAT_HAS_EXTENSION_BITS = 1 +AUDIO_FORMAT_BITS_MIN = 31 +AUDIO_FORMAT_BITS_MAX = 31 + 102 + +SAMPLE_RATE_MIN = 6000 +SAMPLE_BITS_MIN = 8 +CHANNEL_NUM_MIN = 1 + +STREAM_INFO_BITS_MIN = (ENCODER_INFO_TOTAL_BITS + + SIZE_INFO_TOTAL_BITS + + AUDIO_FORMAT_BITS_MIN) +STREAM_INFO_BITS_MAX = (ENCODER_INFO_TOTAL_BITS + + SIZE_INFO_TOTAL_BITS + + AUDIO_FORMAT_BITS_MAX) +STREAM_INFO_SIZE_MIN = (STREAM_INFO_BITS_MIN + 7) / 8 +STREAM_INFO_SIZE_MAX = (STREAM_INFO_BITS_MAX + 7) / 8 + + +class _LSBBitReader(BitReader): + """BitReader implementation which reads bits starting at LSB in each byte. + """ + + def _lsb(self, count): + value = self._buffer & 0xff >> (8 - count) + self._buffer = self._buffer >> count + self._bits -= count + return value + + def bits(self, count): + """Reads `count` bits and returns an uint, LSB read first. + + May raise BitReaderError if not enough data could be read or + IOError by the underlying file object. + """ + if count < 0: + raise ValueError + + value = 0 + if count <= self._bits: + value = self._lsb(count) + else: + # First read all available bits + shift = 0 + remaining = count + if self._bits > 0: + remaining -= self._bits + shift = self._bits + value = self._lsb(self._bits) + assert self._bits == 0 + + # Now add additional bytes + n_bytes = (remaining - self._bits + 7) // 8 + data = self._fileobj.read(n_bytes) + if len(data) != n_bytes: + raise BitReaderError("not enough data") + for b in bytearray(data): + if remaining > 8: # Use full byte + remaining -= 8 + value = (b << shift) | value + shift += 8 + else: + self._buffer = b + self._bits = 8 + b = self._lsb(remaining) + value = (b << shift) | value + + assert 0 <= self._bits < 8 + return value + + +class TAKHeaderError(error): + pass + + +class TAKInfo(StreamInfo): + + """TAK stream information. + + Attributes: + channels (`int`): number of audio channels + length (`float`): file length in seconds, as a float + sample_rate (`int`): audio sampling rate in Hz + bits_per_sample (`int`): audio sample size + encoder_info (`mutagen.text`): encoder version + """ + + channels = 0 + length = 0 + sample_rate = 0 + bitrate = 0 + encoder_info = "" + + @convert_error(IOError, TAKHeaderError) + @convert_error(BitReaderError, TAKHeaderError) + def __init__(self, fileobj): + stream_id = fileobj.read(4) + if len(stream_id) != 4 or not stream_id == b"tBaK": + raise TAKHeaderError("not a TAK file") + + bitreader = _LSBBitReader(fileobj) + while True: + type = TAKMetadata(bitreader.bits(7)) + bitreader.skip(1) # Unused + size = struct.unpack(" 0: + self.length = self.number_of_samples / float(self.sample_rate) + + def _parse_stream_info(self, bitreader, size): + if size < STREAM_INFO_SIZE_MIN or size > STREAM_INFO_SIZE_MAX: + raise TAKHeaderError("stream info has invalid length") + + # Encoder Info + bitreader.skip(ENCODER_INFO_CODEC_BITS) + bitreader.skip(ENCODER_INFO_PROFILE_BITS) + + # Size Info + bitreader.skip(SIZE_INFO_FRAME_DURATION_BITS) + self.number_of_samples = bitreader.bits(SIZE_INFO_SAMPLE_NUM_BITS) + + # Audio Format + bitreader.skip(AUDIO_FORMAT_DATA_TYPE_BITS) + self.sample_rate = (bitreader.bits(AUDIO_FORMAT_SAMPLE_RATE_BITS) + + SAMPLE_RATE_MIN) + self.bits_per_sample = (bitreader.bits(AUDIO_FORMAT_SAMPLE_BITS_BITS) + + SAMPLE_BITS_MIN) + self.channels = (bitreader.bits(AUDIO_FORMAT_CHANNEL_NUM_BITS) + + CHANNEL_NUM_MIN) + bitreader.skip(AUDIO_FORMAT_HAS_EXTENSION_BITS) + + def _parse_encoder_info(self, bitreader, size): + patch = bitreader.bits(8) + minor = bitreader.bits(8) + major = bitreader.bits(8) + self.encoder_info = "TAK %d.%d.%d" % (major, minor, patch) + + def pprint(self): + return u"%s, %d Hz, %d bits, %.2f seconds, %d channel(s)" % ( + self.encoder_info or "TAK", self.sample_rate, self.bits_per_sample, + self.length, self.channels) + + +class TAK(APEv2File): + """TAK(filething) + + Arguments: + filething (filething) + + Attributes: + info (`TAKInfo`) + """ + + _Info = TAKInfo + _mimes = ["audio/x-tak"] + + @staticmethod + def score(filename, fileobj, header): + return header.startswith(b"tBaK") + endswith(filename.lower(), ".tak") + + +Open = TAK diff -Nru mutagen-1.42.0/mutagen/wavpack.py mutagen-1.43.0+1/mutagen/wavpack.py --- mutagen-1.42.0/mutagen/wavpack.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/mutagen/wavpack.py 2019-11-17 19:59:34.000000000 +0000 @@ -76,9 +76,9 @@ Attributes: channels (int): number of audio channels (1 or 2) - length (float: file length in seconds, as a float + length (float): file length in seconds, as a float sample_rate (int): audio sampling rate in Hz - version (int) WavPack stream version + version (int): WavPack stream version """ def __init__(self, fileobj): @@ -114,6 +114,15 @@ class WavPack(APEv2File): + """WavPack(filething) + + Arguments: + filething (filething) + + Attributes: + info (`WavPackInfo`) + """ + _Info = WavPackInfo _mimes = ["audio/x-wavpack"] diff -Nru mutagen-1.42.0/NEWS mutagen-1.43.0+1/NEWS --- mutagen-1.42.0/NEWS 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/NEWS 2019-11-17 19:59:34.000000000 +0000 @@ -1,3 +1,32 @@ +.. _release-1.43.0: + +1.43.0 - 2019-11-17 +------------------- + +* **Note: 1.43.x might be the last version supporting Python 2** +* Python 3.4 is no longer supported +* Building requires 'setuptools' now, CLI tools depend on 'pkg_resources' +* CLI tools are setuptools entry points now + +.. + +* Fix collections ABCs deprecation warning :pr:`371` (:user:`Ken Sato `) +* Minor typo fixes :pr:`375` (:user:`Nicholas Chammas `) +* MP3: increase max initial wrong syncs from 1000 to 1500 :pr:`376` (:user:`Hamid Alaei Varnosfaderani `) +* FLAC: support files with multiple VORBIS_COMMENT blocks like libflac :pr:`378` +* ID3: Improved TYER/TDAT/TIME upgrade to TDRC :pr:`385` +* MP4: Add support for iTunes HD Video tag (hdvd) :pr:`386` (:user:`Jay Sandhu `) +* Add AC3 file type :pr:`400` (:user:`Philipp Wolfer `) +* AIFF: renamed sample_size to bits_per_sample (sample_size still works) :pr:`403` (:user:`Philipp Wolfer `) +* API doc fixes :pr:`404` :pr:`407` (:user:`Philipp Wolfer `) +* Add support for Tom's lossless Audio Kompressor (TAK) :pr:`405` (:user:`Philipp Wolfer `) +* OptimFROG: support encoder version >= 5.100 :pr:`406` (:user:`Philipp Wolfer `) +* AIFF: Fix handling of padding bytes, safe chunk manipulation :pr:`409` (:user:`Philipp Wolfer `) +* Fix typos :pr:`412` (:user:`Tim Gates `) + + +.. _release-1.42.0: + 1.42.0 - 2018-12-26 ------------------- @@ -12,12 +41,16 @@ * Fix pylint warnings when using the various ``save()`` methods :pr:`364` +.. _release-1.41.1: + 1.41.1 - 2018-08-11 ------------------- * MP4: fix rtng, stik, shwm getting saved as 16bit ints instead of 8bit :bug:`349` +.. _release-1.41.0: + 1.41.0 - 2018-07-15 ------------------- @@ -28,6 +61,8 @@ * MonkeysAudio: set bits_per_sample for older files :bug:`347` +.. _release-1.40.0: + 1.40.0 - 2018-01-25 ------------------- diff -Nru mutagen-1.42.0/README.rst mutagen-1.43.0+1/README.rst --- mutagen-1.42.0/README.rst 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/README.rst 2019-11-17 19:59:34.000000000 +0000 @@ -12,7 +12,7 @@ MP3s. ID3 and APEv2 tags can be edited regardless of audio format. It can also manipulate Ogg streams on an individual packet/page level. -Mutagen works with Python 2.7, 3.4+ (CPython and PyPy) on Linux, Windows and +Mutagen works with Python 2.7, 3.5+ (CPython and PyPy) on Linux, Windows and macOS, and has no dependencies outside the Python standard library. Mutagen is licensed under the GPL version 2 or later. diff -Nru mutagen-1.42.0/setup.cfg mutagen-1.43.0+1/setup.cfg --- mutagen-1.42.0/setup.cfg 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/setup.cfg 2019-11-17 19:59:34.000000000 +0000 @@ -9,3 +9,10 @@ ignore=E128,W601,E402,E731,W503,E741,E305,E121,E124,W504 builtins=cmp,unicode,long,xrange,basestring exclude= + +[tool:pytest] +markers= + quality + +[bdist_wheel] +universal = 1 diff -Nru mutagen-1.42.0/setup.py mutagen-1.43.0+1/setup.py --- mutagen-1.42.0/setup.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/setup.py 2019-11-17 19:59:34.000000000 +0000 @@ -14,7 +14,7 @@ import subprocess import tarfile -from distutils.core import setup, Command, Distribution +from setuptools import setup, Command, Distribution from distutils import dir_util @@ -135,7 +135,8 @@ def run(self): docs = "docs" target = os.path.join(self.build_dir, "sphinx") - self.spawn(["sphinx-build", "-b", "html", "-n", docs, target]) + self.spawn([ + sys.executable, "-m", "sphinx", "-b", "html", "-n", docs, target]) class test_cmd(Command): @@ -237,11 +238,6 @@ else: version_string = ".".join(map(str, version)) - if os.name == "posix": - data_files = [('share/man/man1', glob.glob("man/*.1"))] - else: - data_files = [] - cmd_classes = { "clean": clean, "test": test_cmd, @@ -256,18 +252,18 @@ version=version_string, url="https://github.com/quodlibet/mutagen", description="read and write audio tags for many formats", - author="Michael Urman", - author_email="quod-libet-development@groups.google.com", + author="Christoph Reiter", + author_email="reiter.christoph@gmail.com", license="GPL-2.0-or-later", classifiers=[ 'Operating System :: OS Independent', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ('License :: OSI Approved :: ' @@ -283,14 +279,20 @@ "mutagen._senf", "mutagen._tools", ], - data_files=data_files, - scripts=[os.path.join("tools", name) for name in [ - "mid3cp", - "mid3iconv", - "mid3v2", - "moggsplit", - "mutagen-inspect", - "mutagen-pony", - ]], + data_files=[ + ('share/man/man1', glob.glob("man/*.1")), + ], + python_requires=( + '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4'), + entry_points={ + 'console_scripts': [ + 'mid3cp=mutagen._tools.mid3cp:entry_point', + 'mid3iconv=mutagen._tools.mid3iconv:entry_point', + 'mid3v2=mutagen._tools.mid3v2:entry_point', + 'moggsplit=mutagen._tools.moggsplit:entry_point', + 'mutagen-inspect=mutagen._tools.mutagen_inspect:entry_point', + 'mutagen-pony=mutagen._tools.mutagen_pony:entry_point', + ], + }, long_description=long_description, ) Binary files /tmp/tmpeahnBR/9r_dpm0hPA/mutagen-1.42.0/tests/data/has-tags.tak and /tmp/tmpeahnBR/IVJYi8fZ70/mutagen-1.43.0+1/tests/data/has-tags.tak differ Binary files /tmp/tmpeahnBR/9r_dpm0hPA/mutagen-1.42.0/tests/data/silence-2s-44100-16.ofr and /tmp/tmpeahnBR/IVJYi8fZ70/mutagen-1.43.0+1/tests/data/silence-2s-44100-16.ofr differ Binary files /tmp/tmpeahnBR/9r_dpm0hPA/mutagen-1.42.0/tests/data/silence-2s-44100-16.ofs and /tmp/tmpeahnBR/IVJYi8fZ70/mutagen-1.43.0+1/tests/data/silence-2s-44100-16.ofs differ Binary files /tmp/tmpeahnBR/9r_dpm0hPA/mutagen-1.42.0/tests/data/silence-44-s.ac3 and /tmp/tmpeahnBR/IVJYi8fZ70/mutagen-1.43.0+1/tests/data/silence-44-s.ac3 differ Binary files /tmp/tmpeahnBR/9r_dpm0hPA/mutagen-1.42.0/tests/data/silence-44-s.eac3 and /tmp/tmpeahnBR/IVJYi8fZ70/mutagen-1.43.0+1/tests/data/silence-44-s.eac3 differ Binary files /tmp/tmpeahnBR/9r_dpm0hPA/mutagen-1.42.0/tests/data/silence-44-s.tak and /tmp/tmpeahnBR/IVJYi8fZ70/mutagen-1.43.0+1/tests/data/silence-44-s.tak differ diff -Nru mutagen-1.42.0/tests/test_ac3.py mutagen-1.43.0+1/tests/test_ac3.py --- mutagen-1.42.0/tests/test_ac3.py 1970-01-01 00:00:00.000000000 +0000 +++ mutagen-1.43.0+1/tests/test_ac3.py 2019-11-17 19:59:34.000000000 +0000 @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +import os + +from mutagen.ac3 import AC3, AC3Error + +from tests import TestCase, DATA_DIR + + +class TAC3(TestCase): + + def setUp(self): + self.ac3 = AC3(os.path.join(DATA_DIR, "silence-44-s.ac3")) + self.eac3 = AC3(os.path.join(DATA_DIR, "silence-44-s.eac3")) + + def test_channels(self): + self.failUnlessEqual(self.ac3.info.channels, 2) + self.failUnlessEqual(self.eac3.info.channels, 2) + + def test_bitrate(self): + self.failUnlessEqual(self.ac3.info.bitrate, 192000) + self.failUnlessAlmostEqual(self.eac3.info.bitrate, 192000, delta=500) + + def test_sample_rate(self): + self.failUnlessEqual(self.ac3.info.sample_rate, 44100) + self.failUnlessEqual(self.eac3.info.sample_rate, 44100) + + def test_length(self): + self.failUnlessAlmostEqual(self.ac3.info.length, 3.70, delta=0.009) + self.failUnlessAlmostEqual(self.eac3.info.length, 3.70, delta=0.009) + + def test_type(self): + self.failUnlessEqual(self.ac3.info.codec, "ac-3") + self.failUnlessEqual(self.eac3.info.codec, "ec-3") + + def test_not_my_file(self): + self.failUnlessRaises( + AC3Error, AC3, + os.path.join(DATA_DIR, "empty.ogg")) + + self.failUnlessRaises( + AC3Error, AC3, + os.path.join(DATA_DIR, "silence-44-s.mp3")) + + def test_pprint(self): + self.assertTrue("ac-3" in self.ac3.pprint()) + self.assertTrue("ec-3" in self.eac3.pprint()) diff -Nru mutagen-1.42.0/tests/test_aiff.py mutagen-1.43.0+1/tests/test_aiff.py --- mutagen-1.42.0/tests/test_aiff.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/tests/test_aiff.py 2019-11-17 19:59:34.000000000 +0000 @@ -60,12 +60,18 @@ self.failUnlessEqual(self.aiff_4.info.sample_rate, 8000) self.failUnlessEqual(self.aiff_5.info.sample_rate, 8000) + def test_bits_per_sample(self): + self.failUnlessEqual(self.aiff_1.info.bits_per_sample, 16) + self.failUnlessEqual(self.aiff_2.info.bits_per_sample, 16) + self.failUnlessEqual(self.aiff_3.info.bits_per_sample, 16) + self.failUnlessEqual(self.aiff_4.info.bits_per_sample, 16) + self.failUnlessEqual(self.aiff_5.info.bits_per_sample, 16) + def test_sample_size(self): - self.failUnlessEqual(self.aiff_1.info.sample_size, 16) - self.failUnlessEqual(self.aiff_2.info.sample_size, 16) - self.failUnlessEqual(self.aiff_3.info.sample_size, 16) - self.failUnlessEqual(self.aiff_4.info.sample_size, 16) - self.failUnlessEqual(self.aiff_5.info.sample_size, 16) + for test in [self.aiff_1, self.aiff_2, self.aiff_3, self.aiff_4, + self.aiff_5]: + info = test.info + self.failUnlessEqual(info.sample_size, info.bits_per_sample) def test_notaiff(self): self.failUnlessRaises( @@ -227,8 +233,8 @@ self.iff_1_tmp[u'FORM'].resize(17000) self.failUnlessEqual( IFFFile(self.file_1_tmp)[u'FORM'].data_size, 17000) - self.iff_2_tmp[u'FORM'].resize(0) - self.failUnlessEqual(IFFFile(self.file_2_tmp)[u'FORM'].data_size, 0) + self.iff_2_tmp[u'FORM'].resize(4) + self.failUnlessEqual(IFFFile(self.file_2_tmp)[u'FORM'].data_size, 4) def test_child_chunk_resize(self): self.iff_1_tmp[u'ID3'].resize(128) @@ -261,3 +267,61 @@ self.failUnlessEqual(new_iff[u'FORM'].data_size, 16054) self.failUnlessEqual(new_iff[u'ID3'].size, 8) self.failUnlessEqual(new_iff[u'ID3'].data_size, 0) + + def test_insert_padded_chunks(self): + padded = self.iff_2_tmp.insert_chunk(u'TST1') + unpadded = self.iff_2_tmp.insert_chunk(u'TST2') + # The second chunk needs no padding + unpadded.resize(4) + self.failUnlessEqual(4, unpadded.data_size) + self.failUnlessEqual(0, unpadded.padding()) + self.failUnlessEqual(12, unpadded.size) + # Resize the first chunk so it needs padding + padded.resize(3) + self.failUnlessEqual(3, padded.data_size) + self.failUnlessEqual(1, padded.padding()) + self.failUnlessEqual(12, padded.size) + self.failUnlessEqual(padded.offset + padded.size, unpadded.offset) + # Verify the padding byte gets written correctly + self.file_2_tmp.seek(padded.data_offset) + self.file_2_tmp.write(b'ABCD') + padded.write(b'ABC') + self.file_2_tmp.seek(padded.data_offset) + self.failUnlessEqual(b'ABC\x00', self.file_2_tmp.read(4)) + # Verify the second chunk got not overwritten + self.file_2_tmp.seek(unpadded.offset) + self.failUnlessEqual(b'TST2', self.file_2_tmp.read(4)) + + def test_delete_padded_chunks(self): + iff_file = self.iff_2_tmp + iff_file.insert_chunk(u'TST') + # Resize to odd length, should insert 1 padding byte + iff_file[u'TST'].resize(3) + # Insert another chunk after the first one + iff_file.insert_chunk(u'TST2') + iff_file[u'TST2'].resize(2) + self.failUnlessEqual(iff_file[u'FORM'].size, 16076) + self.failUnlessEqual(iff_file[u'FORM'].data_size, 16068) + self.failUnlessEqual(iff_file[u'TST'].size, 12) + self.failUnlessEqual(iff_file[u'TST'].data_size, 3) + self.failUnlessEqual(iff_file[u'TST'].data_offset, 16062) + self.failUnlessEqual(iff_file[u'TST2'].size, 10) + self.failUnlessEqual(iff_file[u'TST2'].data_size, 2) + self.failUnlessEqual(iff_file[u'TST2'].data_offset, 16074) + # Delete the odd chunk + iff_file.delete_chunk(u'TST') + self.failUnlessEqual(iff_file[u'FORM'].size, 16064) + self.failUnlessEqual(iff_file[u'FORM'].data_size, 16056) + self.failUnlessEqual(iff_file[u'TST2'].size, 10) + self.failUnlessEqual(iff_file[u'TST2'].data_size, 2) + self.failUnlessEqual(iff_file[u'TST2'].data_offset, 16062) + # Reloading the file should give the same results + new_iff_file = IFFFile(self.file_2_tmp) + self.failUnlessEqual(new_iff_file[u'FORM'].size, + iff_file[u'FORM'].size) + self.failUnlessEqual(new_iff_file[u'TST2'].size, + iff_file[u'TST2'].size) + self.failUnlessEqual(new_iff_file[u'TST2'].data_size, + iff_file[u'TST2'].data_size) + self.failUnlessEqual(new_iff_file[u'TST2'].data_offset, + iff_file[u'TST2'].data_offset) diff -Nru mutagen-1.42.0/tests/test_flac.py mutagen-1.43.0+1/tests/test_flac.py --- mutagen-1.42.0/tests/test_flac.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/tests/test_flac.py 2019-11-17 19:59:34.000000000 +0000 @@ -3,6 +3,8 @@ import os import subprocess +import pytest + from mutagen import MutagenError from mutagen.id3 import ID3, TIT2, ID3NoHeaderError from mutagen.flac import to_int_be, Padding, VCFLACDict, MetadataBlock, error @@ -355,6 +357,19 @@ flac = FLAC(self.NEW) self.assertTrue(flac.tags is None) + def test_delete_change_reload(self): + self.flac.delete() + self.flac.tags["FOO"] = ["BAR"] + self.flac.save() + assert FLAC(self.flac.filename)["FOO"] == ["BAR"] + + # same with delete failing due to IO etc. + with pytest.raises(MutagenError): + self.flac.delete(os.devnull) + self.flac.tags["FOO"] = ["QUUX"] + self.flac.save() + assert FLAC(self.flac.filename)["FOO"] == ["QUUX"] + def test_module_delete(self): delete(self.NEW) flac = FLAC(self.NEW) @@ -619,6 +634,56 @@ self.failIf(b"Tunng" in data) +class TFLACBadDuplicateVorbisComment(TestCase): + + def setUp(self): + self.filename = get_temp_copy( + os.path.join(DATA_DIR, "silence-44-s.flac")) + + # add a second vorbis comment block to the file right after the first + some_tags = VCFLACDict() + some_tags["DUPLICATE"] = ["SECOND"] + f = FLAC(self.filename) + f.tags["DUPLICATE"] = ["FIRST"] + assert f.tags is f.metadata_blocks[2] + f.metadata_blocks.insert(3, some_tags) + f.save() + + def tearDown(self): + os.unlink(self.filename) + + def test_load_multiple(self): + # on load always use the first one, like metaflac + f = FLAC(self.filename) + assert f["DUPLICATE"] == ["FIRST"] + assert f.metadata_blocks[2] is f.tags + assert f.metadata_blocks[3]["DUPLICATE"] == ["SECOND"] + + # save in the same order + f.save() + f = FLAC(self.filename) + assert f["DUPLICATE"] == ["FIRST"] + + def test_delete_multiple(self): + # on delete we delete both + f = FLAC(self.filename) + f.delete() + assert len(f.tags) == 0 + f = FLAC(self.filename) + assert f.tags is None + + def test_delete_multiple_fail(self): + f = FLAC(self.filename) + with pytest.raises(MutagenError): + f.delete(os.devnull) + f.save() + + # if delete failed we shouldn't see a difference + f = FLAC(self.filename) + assert f.metadata_blocks[2] is f.tags + assert f.metadata_blocks[3].code == f.tags.code + + class TFLACBadBlockSizeOverflow(TestCase): def setUp(self): diff -Nru mutagen-1.42.0/tests/test_id3.py mutagen-1.43.0+1/tests/test_id3.py --- mutagen-1.42.0/tests/test_id3.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/tests/test_id3.py 2019-11-17 19:59:34.000000000 +0000 @@ -326,6 +326,16 @@ id3.update_to_v24() self.failUnlessEqual(id3["TDRC"], "2006-03-06 11:27:00") + def test_multiple_tyer_tdat_time(self): + id3 = ID3() + id3.version = (2, 3) + id3.add(TYER(text=['2000', '2001', '2002', '19xx', 'foo'])) + id3.add(TDAT(text=['0102', '0304', '1111bar'])) + id3.add(TIME(text=['1220', '1111quux', '1111'])) + id3.update_to_v24() + assert [str(t) for t in id3['TDRC']] == \ + ['2000-02-01 12:20:00', '2001-04-03', '2002'] + def test_tory(self): id3 = ID3() id3.version = (2, 3) diff -Nru mutagen-1.42.0/tests/test___init__.py mutagen-1.43.0+1/tests/test___init__.py --- mutagen-1.42.0/tests/test___init__.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/tests/test___init__.py 2019-11-17 19:59:34.000000000 +0000 @@ -31,7 +31,9 @@ from mutagen.asf import ASF from mutagen.aiff import AIFF from mutagen.aac import AAC +from mutagen.ac3 import AC3 from mutagen.smf import SMF +from mutagen.tak import TAK from mutagen.dsf import DSF from os import devnull @@ -493,6 +495,10 @@ os.path.join(DATA_DIR, "empty.aac"), os.path.join(DATA_DIR, "adif.aac"), ], + AC3: [ + os.path.join(DATA_DIR, "silence-44-s.ac3"), + os.path.join(DATA_DIR, "silence-44-s.eac3"), + ], ASF: [ os.path.join(DATA_DIR, "silence-1.wma"), os.path.join(DATA_DIR, "silence-2.wma"), @@ -519,6 +525,10 @@ SMF: [ os.path.join(DATA_DIR, "sample.mid"), ], + TAK: [ + os.path.join(DATA_DIR, "silence-44-s.tak"), + os.path.join(DATA_DIR, "has-tags.tak"), + ], DSF: [ os.path.join(DATA_DIR, '2822400-1ch-0s-silence.dsf'), os.path.join(DATA_DIR, '5644800-2ch-s01-silence.dsf'), diff -Nru mutagen-1.42.0/tests/test_mp4.py mutagen-1.43.0+1/tests/test_mp4.py --- mutagen-1.42.0/tests/test_mp4.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/tests/test_mp4.py 2019-11-17 19:59:34.000000000 +0000 @@ -645,8 +645,8 @@ def test_various_int(self): keys = [ - "stik", "rtng", "plID", "cnID", "geID", "atID", "sfID", - "cmID", "akID", "tvsn", "tves", + "stik", "hdvd", "rtng", "plID", "cnID", "geID", "atID", + "sfID", "cmID", "akID", "tvsn", "tves", ] for key in keys: diff -Nru mutagen-1.42.0/tests/test_optimfrog.py mutagen-1.43.0+1/tests/test_optimfrog.py --- mutagen-1.42.0/tests/test_optimfrog.py 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/tests/test_optimfrog.py 2019-11-17 19:59:34.000000000 +0000 @@ -11,18 +11,40 @@ def setUp(self): self.ofr = OptimFROG(os.path.join(DATA_DIR, "empty.ofr")) self.ofs = OptimFROG(os.path.join(DATA_DIR, "empty.ofs")) + self.ofr_5100 = OptimFROG( + os.path.join(DATA_DIR, "silence-2s-44100-16.ofr")) + self.ofs_5100 = OptimFROG( + os.path.join(DATA_DIR, "silence-2s-44100-16.ofs")) def test_channels(self): self.failUnlessEqual(self.ofr.info.channels, 2) self.failUnlessEqual(self.ofs.info.channels, 2) + self.failUnlessEqual(self.ofr_5100.info.channels, 2) + self.failUnlessEqual(self.ofs_5100.info.channels, 2) def test_sample_rate(self): self.failUnlessEqual(self.ofr.info.sample_rate, 44100) self.failUnlessEqual(self.ofs.info.sample_rate, 44100) + self.failUnlessEqual(self.ofr_5100.info.sample_rate, 44100) + self.failUnlessEqual(self.ofs_5100.info.sample_rate, 44100) + + def test_bits_per_sample(self): + self.failUnlessEqual(self.ofr.info.bits_per_sample, 16) + self.failUnlessEqual(self.ofs.info.bits_per_sample, 16) + self.failUnlessEqual(self.ofr_5100.info.bits_per_sample, 16) + self.failUnlessEqual(self.ofs_5100.info.bits_per_sample, 16) def test_length(self): self.failUnlessAlmostEqual(self.ofr.info.length, 3.68, 2) self.failUnlessAlmostEqual(self.ofs.info.length, 3.68, 2) + self.failUnlessAlmostEqual(self.ofr_5100.info.length, 2.0, 2) + self.failUnlessAlmostEqual(self.ofs_5100.info.length, 2.0, 2) + + def test_encoder_info(self): + self.failUnlessEqual(self.ofr.info.encoder_info, "4.520") + self.failUnlessEqual(self.ofs.info.encoder_info, "4.520") + self.failUnlessEqual(self.ofr_5100.info.encoder_info, "5.100") + self.failUnlessEqual(self.ofs_5100.info.encoder_info, "5.100") def test_not_my_file(self): self.failUnlessRaises( diff -Nru mutagen-1.42.0/tests/test_tak.py mutagen-1.43.0+1/tests/test_tak.py --- mutagen-1.42.0/tests/test_tak.py 1970-01-01 00:00:00.000000000 +0000 +++ mutagen-1.43.0+1/tests/test_tak.py 2019-11-17 19:59:34.000000000 +0000 @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +import os + +from mutagen.tak import TAK, TAKHeaderError +from tests import TestCase, DATA_DIR + + +class TTAK(TestCase): + + def setUp(self): + self.tak_no_tags = TAK(os.path.join(DATA_DIR, "silence-44-s.tak")) + self.tak_tags = TAK(os.path.join(DATA_DIR, "has-tags.tak")) + + def test_channels(self): + self.failUnlessEqual(self.tak_no_tags.info.channels, 2) + self.failUnlessEqual(self.tak_tags.info.channels, 2) + + def test_length(self): + self.failUnlessAlmostEqual(self.tak_no_tags.info.length, 3.68, + delta=0.009) + self.failUnlessAlmostEqual(self.tak_tags.info.length, 0.08, + delta=0.009) + + def test_sample_rate(self): + self.failUnlessEqual(self.tak_no_tags.info.sample_rate, 44100) + self.failUnlessEqual(self.tak_tags.info.sample_rate, 44100) + + def test_bits_per_sample(self): + self.failUnlessEqual(self.tak_no_tags.info.bits_per_sample, 16) + self.failUnlessAlmostEqual(self.tak_tags.info.bits_per_sample, 16) + + def test_encoder_info(self): + self.failUnlessEqual(self.tak_no_tags.info.encoder_info, "TAK 2.3.0") + self.failUnlessEqual(self.tak_tags.info.encoder_info, "TAK 2.3.0") + + def test_not_my_file(self): + self.failUnlessRaises( + TAKHeaderError, TAK, + os.path.join(DATA_DIR, "empty.ogg")) + self.failUnlessRaises( + TAKHeaderError, TAK, + os.path.join(DATA_DIR, "click.mpc")) + + def test_mime(self): + self.failUnless("audio/x-tak" in self.tak_no_tags.mime) + + def test_pprint(self): + self.failUnless(self.tak_no_tags.pprint()) + self.failUnless(self.tak_tags.pprint()) diff -Nru mutagen-1.42.0/tools/mid3cp mutagen-1.43.0+1/tools/mid3cp --- mutagen-1.42.0/tools/mid3cp 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/tools/mid3cp 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import sys - -from mutagen._tools.mid3cp import entry_point - - -if __name__ == "__main__": - sys.exit(entry_point()) diff -Nru mutagen-1.42.0/tools/mid3iconv mutagen-1.43.0+1/tools/mid3iconv --- mutagen-1.42.0/tools/mid3iconv 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/tools/mid3iconv 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import sys - -from mutagen._tools.mid3iconv import entry_point - - -if __name__ == "__main__": - sys.exit(entry_point()) diff -Nru mutagen-1.42.0/tools/mid3v2 mutagen-1.43.0+1/tools/mid3v2 --- mutagen-1.42.0/tools/mid3v2 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/tools/mid3v2 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import sys - -from mutagen._tools.mid3v2 import entry_point - - -if __name__ == "__main__": - sys.exit(entry_point()) diff -Nru mutagen-1.42.0/tools/moggsplit mutagen-1.43.0+1/tools/moggsplit --- mutagen-1.42.0/tools/moggsplit 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/tools/moggsplit 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import sys - -from mutagen._tools.moggsplit import entry_point - - -if __name__ == "__main__": - sys.exit(entry_point()) diff -Nru mutagen-1.42.0/tools/mutagen-inspect mutagen-1.43.0+1/tools/mutagen-inspect --- mutagen-1.42.0/tools/mutagen-inspect 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/tools/mutagen-inspect 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import sys - -from mutagen._tools.mutagen_inspect import entry_point - - -if __name__ == "__main__": - sys.exit(entry_point()) diff -Nru mutagen-1.42.0/tools/mutagen-pony mutagen-1.43.0+1/tools/mutagen-pony --- mutagen-1.42.0/tools/mutagen-pony 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/tools/mutagen-pony 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import sys - -from mutagen._tools.mutagen_pony import entry_point - - -if __name__ == "__main__": - sys.exit(entry_point()) diff -Nru mutagen-1.42.0/.travis.yml mutagen-1.43.0+1/.travis.yml --- mutagen-1.42.0/.travis.yml 2018-12-26 13:59:06.000000000 +0000 +++ mutagen-1.43.0+1/.travis.yml 2019-11-17 19:59:34.000000000 +0000 @@ -8,11 +8,6 @@ - os: linux dist: trusty language: python - python: "3.4" - env: TYPE="linux" PYVER="3.4" PYARGS="-R -bb" - - os: linux - dist: trusty - language: python python: "3.5" env: TYPE="linux" PYVER="3.5" PYARGS="-R -bb" - os: linux @@ -27,6 +22,12 @@ python: "3.7" env: TYPE="linux" PYVER="3.7" PYARGS="-R -bb" - os: linux + dist: xenial + sudo: required + language: python + python: "3.8" + env: TYPE="linux" PYVER="3.8" PYARGS="-R -bb" + - os: linux dist: trusty language: python python: "pypy" @@ -37,11 +38,9 @@ python: "pypy3" env: TYPE="linux" PYVER="pypy3" PYARGS="-R -bb" - os: osx - osx_image: xcode6.4 language: generic env: TYPE="osx" PYVER="3" PYARGS="-R -bb" - os: osx - osx_image: xcode6.4 language: generic env: TYPE="osx" PYVER="2" PYARGS="-R" @@ -54,7 +53,7 @@ - if [ "$TYPE" == "osx" ] && [ "$PYVER" == "2" ]; then python2 -m pip install virtualenv; fi - if [ "$TYPE" == "osx" ] && [ "$PYVER" == "2" ]; then virtualenv venv -p python2; fi - if [ "$TYPE" == "osx" ]; then source venv/bin/activate; fi - - if [ "$TYPE" == "linux" ] || [ "$TYPE" == "osx" ]; then python -m pip install --upgrade pycodestyle pyflakes pytest hypothesis coverage codecov; fi + - if [ "$TYPE" == "linux" ] || [ "$TYPE" == "osx" ]; then python -m pip install --upgrade pycodestyle pyflakes pytest hypothesis coverage codecov attrs; fi - if [ "$TYPE" == "linux" ] || [ "$PYVER" == "3.6" ]; then python -m pip install --upgrade sphinx sphinx_rtd_theme; fi script: