Update mutagen to 1.45.1

This commit is contained in:
rembo10
2022-02-07 09:30:17 +05:30
parent e2db680b9e
commit f03b82e5f6
71 changed files with 3744 additions and 1953 deletions

2
lib/mutagen/__init__.py Executable file → Normal file
View File

@@ -23,7 +23,7 @@ from mutagen._util import MutagenError
from mutagen._file import FileType, StreamInfo, File
from mutagen._tags import Tags, Metadata, PaddingInfo
version = (1, 38, -1)
version = (1, 45, 1)
"""Version tuple."""
version_string = ".".join(map(str, version))

View File

@@ -1,86 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2013 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
PY2 = sys.version_info[0] == 2
PY3 = not PY2
if PY2:
from io import StringIO
BytesIO = StringIO
from io import StringIO as cBytesIO
long_ = int
integer_types = (int, int)
string_types = (str, str)
text_type = str
xrange = xrange
cmp = cmp
chr_ = chr
def endswith(text, end):
return text.endswith(end)
iteritems = lambda d: iter(d.items())
itervalues = lambda d: iter(d.values())
iterkeys = lambda d: iter(d.keys())
iterbytes = lambda b: iter(b)
exec("def reraise(tp, value, tb):\n raise tp, value, tb")
def swap_to_string(cls):
if "__str__" in cls.__dict__:
cls.__unicode__ = cls.__str__
if "__bytes__" in cls.__dict__:
cls.__str__ = cls.__bytes__
return cls
elif PY3:
from io import StringIO
StringIO = StringIO
from io import BytesIO
cBytesIO = BytesIO
long_ = int
integer_types = (int,)
string_types = (str,)
text_type = str
izip = zip
xrange = range
cmp = lambda a, b: (a > b) - (a < b)
chr_ = lambda x: bytes([x])
def endswith(text, end):
# usefull for paths which can be both, str and bytes
if isinstance(text, str):
if not isinstance(end, str):
end = end.decode("ascii")
else:
if not isinstance(end, bytes):
end = end.encode("ascii")
return text.endswith(end)
iteritems = lambda d: iter(list(d.items()))
itervalues = lambda d: iter(list(d.values()))
iterkeys = lambda d: iter(list(d.keys()))
iterbytes = lambda b: (bytes([v]) for v in b)
def reraise(tp, value, tb):
raise tp(value).with_traceback(tb)
def swap_to_string(cls):
return cls

384
lib/mutagen/_constants.py Executable file → Normal file
View File

@@ -8,197 +8,197 @@
"""Constants used by Mutagen."""
GENRES = [
"Blues",
"Classic Rock",
"Country",
"Dance",
"Disco",
"Funk",
"Grunge",
"Hip-Hop",
"Jazz",
"Metal",
"New Age",
"Oldies",
"Other",
"Pop",
"R&B",
"Rap",
"Reggae",
"Rock",
"Techno",
"Industrial",
"Alternative",
"Ska",
"Death Metal",
"Pranks",
"Soundtrack",
"Euro-Techno",
"Ambient",
"Trip-Hop",
"Vocal",
"Jazz+Funk",
"Fusion",
"Trance",
"Classical",
"Instrumental",
"Acid",
"House",
"Game",
"Sound Clip",
"Gospel",
"Noise",
"Alt. Rock",
"Bass",
"Soul",
"Punk",
"Space",
"Meditative",
"Instrumental Pop",
"Instrumental Rock",
"Ethnic",
"Gothic",
"Darkwave",
"Techno-Industrial",
"Electronic",
"Pop-Folk",
"Eurodance",
"Dream",
"Southern Rock",
"Comedy",
"Cult",
"Gangsta Rap",
"Top 40",
"Christian Rap",
"Pop/Funk",
"Jungle",
"Native American",
"Cabaret",
"New Wave",
"Psychedelic",
"Rave",
"Showtunes",
"Trailer",
"Lo-Fi",
"Tribal",
"Acid Punk",
"Acid Jazz",
"Polka",
"Retro",
"Musical",
"Rock & Roll",
"Hard Rock",
"Folk",
"Folk-Rock",
"National Folk",
"Swing",
"Fast-Fusion",
"Bebop",
"Latin",
"Revival",
"Celtic",
"Bluegrass",
"Avantgarde",
"Gothic Rock",
"Progressive Rock",
"Psychedelic Rock",
"Symphonic Rock",
"Slow Rock",
"Big Band",
"Chorus",
"Easy Listening",
"Acoustic",
"Humour",
"Speech",
"Chanson",
"Opera",
"Chamber Music",
"Sonata",
"Symphony",
"Booty Bass",
"Primus",
"Porn Groove",
"Satire",
"Slow Jam",
"Club",
"Tango",
"Samba",
"Folklore",
"Ballad",
"Power Ballad",
"Rhythmic Soul",
"Freestyle",
"Duet",
"Punk Rock",
"Drum Solo",
"A Cappella",
"Euro-House",
"Dance Hall",
"Goa",
"Drum & Bass",
"Club-House",
"Hardcore",
"Terror",
"Indie",
"BritPop",
"Afro-Punk",
"Polsk Punk",
"Beat",
"Christian Gangsta Rap",
"Heavy Metal",
"Black Metal",
"Crossover",
"Contemporary Christian",
"Christian Rock",
"Merengue",
"Salsa",
"Thrash Metal",
"Anime",
"JPop",
"Synthpop",
"Abstract",
"Art Rock",
"Baroque",
"Bhangra",
"Big Beat",
"Breakbeat",
"Chillout",
"Downtempo",
"Dub",
"EBM",
"Eclectic",
"Electro",
"Electroclash",
"Emo",
"Experimental",
"Garage",
"Global",
"IDM",
"Illbient",
"Industro-Goth",
"Jam Band",
"Krautrock",
"Leftfield",
"Lounge",
"Math Rock",
"New Romantic",
"Nu-Breakz",
"Post-Punk",
"Post-Rock",
"Psytrance",
"Shoegaze",
"Space Rock",
"Trop Rock",
"World Music",
"Neoclassical",
"Audiobook",
"Audio Theatre",
"Neue Deutsche Welle",
"Podcast",
"Indie Rock",
"G-Funk",
"Dubstep",
"Garage Rock",
"Psybient",
u"Blues",
u"Classic Rock",
u"Country",
u"Dance",
u"Disco",
u"Funk",
u"Grunge",
u"Hip-Hop",
u"Jazz",
u"Metal",
u"New Age",
u"Oldies",
u"Other",
u"Pop",
u"R&B",
u"Rap",
u"Reggae",
u"Rock",
u"Techno",
u"Industrial",
u"Alternative",
u"Ska",
u"Death Metal",
u"Pranks",
u"Soundtrack",
u"Euro-Techno",
u"Ambient",
u"Trip-Hop",
u"Vocal",
u"Jazz+Funk",
u"Fusion",
u"Trance",
u"Classical",
u"Instrumental",
u"Acid",
u"House",
u"Game",
u"Sound Clip",
u"Gospel",
u"Noise",
u"Alt. Rock",
u"Bass",
u"Soul",
u"Punk",
u"Space",
u"Meditative",
u"Instrumental Pop",
u"Instrumental Rock",
u"Ethnic",
u"Gothic",
u"Darkwave",
u"Techno-Industrial",
u"Electronic",
u"Pop-Folk",
u"Eurodance",
u"Dream",
u"Southern Rock",
u"Comedy",
u"Cult",
u"Gangsta Rap",
u"Top 40",
u"Christian Rap",
u"Pop/Funk",
u"Jungle",
u"Native American",
u"Cabaret",
u"New Wave",
u"Psychedelic",
u"Rave",
u"Showtunes",
u"Trailer",
u"Lo-Fi",
u"Tribal",
u"Acid Punk",
u"Acid Jazz",
u"Polka",
u"Retro",
u"Musical",
u"Rock & Roll",
u"Hard Rock",
u"Folk",
u"Folk-Rock",
u"National Folk",
u"Swing",
u"Fast-Fusion",
u"Bebop",
u"Latin",
u"Revival",
u"Celtic",
u"Bluegrass",
u"Avantgarde",
u"Gothic Rock",
u"Progressive Rock",
u"Psychedelic Rock",
u"Symphonic Rock",
u"Slow Rock",
u"Big Band",
u"Chorus",
u"Easy Listening",
u"Acoustic",
u"Humour",
u"Speech",
u"Chanson",
u"Opera",
u"Chamber Music",
u"Sonata",
u"Symphony",
u"Booty Bass",
u"Primus",
u"Porn Groove",
u"Satire",
u"Slow Jam",
u"Club",
u"Tango",
u"Samba",
u"Folklore",
u"Ballad",
u"Power Ballad",
u"Rhythmic Soul",
u"Freestyle",
u"Duet",
u"Punk Rock",
u"Drum Solo",
u"A Cappella",
u"Euro-House",
u"Dance Hall",
u"Goa",
u"Drum & Bass",
u"Club-House",
u"Hardcore",
u"Terror",
u"Indie",
u"BritPop",
u"Afro-Punk",
u"Polsk Punk",
u"Beat",
u"Christian Gangsta Rap",
u"Heavy Metal",
u"Black Metal",
u"Crossover",
u"Contemporary Christian",
u"Christian Rock",
u"Merengue",
u"Salsa",
u"Thrash Metal",
u"Anime",
u"JPop",
u"Synthpop",
u"Abstract",
u"Art Rock",
u"Baroque",
u"Bhangra",
u"Big Beat",
u"Breakbeat",
u"Chillout",
u"Downtempo",
u"Dub",
u"EBM",
u"Eclectic",
u"Electro",
u"Electroclash",
u"Emo",
u"Experimental",
u"Garage",
u"Global",
u"IDM",
u"Illbient",
u"Industro-Goth",
u"Jam Band",
u"Krautrock",
u"Leftfield",
u"Lounge",
u"Math Rock",
u"New Romantic",
u"Nu-Breakz",
u"Post-Punk",
u"Post-Rock",
u"Psytrance",
u"Shoegaze",
u"Space Rock",
u"Trop Rock",
u"World Music",
u"Neoclassical",
u"Audiobook",
u"Audio Theatre",
u"Neue Deutsche Welle",
u"Podcast",
u"Indie Rock",
u"G-Funk",
u"Dubstep",
u"Garage Rock",
u"Psybient",
]
"""The ID3v1 genre list."""

28
lib/mutagen/_file.py Executable file → Normal file
View File

@@ -9,7 +9,6 @@
import warnings
from mutagen._util import DictMixin, loadfile
from mutagen._compat import izip
class FileType(DictMixin):
@@ -94,10 +93,10 @@ class FileType(DictMixin):
if self.tags is None:
return []
else:
return list(self.tags.keys())
return self.tags.keys()
@loadfile(writable=True)
def delete(self, filething):
def delete(self, filething=None):
"""delete(filething=None)
Remove tags from a file.
@@ -113,14 +112,14 @@ class FileType(DictMixin):
Does nothing if the file has no tags.
Raises:
MutagenError: if deleting wasn't possible
mutagen.MutagenError: if deleting wasn't possible
"""
if self.tags is not None:
return self.tags.delete(filething)
@loadfile(writable=True)
def save(self, filething, **kwargs):
def save(self, filething=None, **kwargs):
"""save(filething=None, **kwargs)
Save metadata tags.
@@ -150,14 +149,15 @@ class FileType(DictMixin):
"""Adds new tags to the file.
Raises:
MutagenError: if tags already exist or adding is not possible.
mutagen.MutagenError:
if tags already exist or adding is not possible.
"""
raise NotImplementedError
@property
def mime(self):
"""A list of mime types (`text`)"""
"""A list of mime types (:class:`mutagen.text`)"""
mimes = []
for Kind in type(self).__mro__:
@@ -171,7 +171,7 @@ class FileType(DictMixin):
"""Returns a score for how likely the file can be parsed by this type.
Args:
filename (path): a file path
filename (fspath): a file path
fileobj (fileobj): a file object open in rb mode. Position is
undefined
header (bytes): data of undefined length, starts with the start of
@@ -220,13 +220,13 @@ def File(filething, options=None, easy=False):
filething (filething)
options: Sequence of :class:`FileType` implementations,
defaults to all included ones.
easy (bool): If the easy wrappers should be returnd if available.
easy (bool): If the easy wrappers should be returned if available.
For example :class:`EasyMP3 <mp3.EasyMP3>` instead of
:class:`MP3 <mp3.MP3>`.
Returns:
FileType: A FileType instance for the detected type or `None` in case
the type couln't be determined.
the type couldn't be determined.
Raises:
MutagenError: in case the detected type fails to load the file.
@@ -263,12 +263,16 @@ def File(filething, options=None, easy=False):
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
from mutagen.dsdiff import DSDIFF
from mutagen.wave import WAVE
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, DSDIFF, WAVE]
if not options:
return None

387
lib/mutagen/_iff.py Normal file
View File

@@ -0,0 +1,387 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014 Evan Purkhiser
# 2014 Ben Ockmore
# 2017 Borewit
# 2019-2020 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.
"""Base classes for various IFF based formats (e.g. AIFF or RIFF)."""
import sys
from mutagen.id3 import ID3
from mutagen.id3._util import ID3NoHeaderError, error as ID3Error
from mutagen._util import (
MutagenError,
convert_error,
delete_bytes,
insert_bytes,
loadfile,
reraise,
resize_bytes,
)
class error(MutagenError):
pass
class InvalidChunk(error):
pass
class EmptyChunk(InvalidChunk):
pass
def is_valid_chunk_id(id):
""" is_valid_chunk_id(FOURCC)
Arguments:
id (FOURCC)
Returns:
true if valid; otherwise false
Check if argument id is valid FOURCC type.
"""
assert isinstance(id, str), \
'id is of type %s, must be str: %r' % (type(id), id)
return ((0 < len(id) <= 4) and (min(id) >= ' ') and
(max(id) <= '~'))
# Assert FOURCC formatted valid
def assert_valid_chunk_id(id):
if not is_valid_chunk_id(id):
raise ValueError("IFF chunk ID must be four ASCII characters.")
class IffChunk(object):
"""Generic representation of a single IFF chunk.
IFF chunks always consist of an ID followed by the chunk size. The exact
format varies between different IFF based formats, e.g. AIFF uses
big-endian while RIFF uses little-endian.
"""
# Chunk headers are usually 8 bytes long (4 for ID and 4 for the size)
HEADER_SIZE = 8
@classmethod
def parse_header(cls, header):
"""Read ID and data_size from the given header.
Must be implemented in subclasses."""
raise error("Not implemented")
def write_new_header(self, id_, size):
"""Write the chunk header with id_ and size to the file.
Must be implemented in subclasses. The data must be written
to the current position in self._fileobj."""
raise error("Not implemented")
def write_size(self):
"""Write self.data_size to the file.
Must be implemented in subclasses. The data must be written
to the current position in self._fileobj."""
raise error("Not implemented")
@classmethod
def get_class(cls, id):
"""Returns the class for a new chunk for a given ID.
Can be overridden in subclasses to implement specific chunk types."""
return cls
@classmethod
def parse(cls, fileobj, parent_chunk=None):
header = fileobj.read(cls.HEADER_SIZE)
if len(header) < cls.HEADER_SIZE:
raise EmptyChunk('Header size < %i' % cls.HEADER_SIZE)
id, data_size = cls.parse_header(header)
try:
id = id.decode('ascii').rstrip()
except UnicodeDecodeError as e:
raise InvalidChunk(e)
if not is_valid_chunk_id(id):
raise InvalidChunk('Invalid chunk ID %r' % id)
return cls.get_class(id)(fileobj, id, data_size, parent_chunk)
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 __repr__(self):
return ("<%s id=%s, offset=%i, size=%i, data_offset=%i, data_size=%i>"
% (type(self).__name__, self.id, self.offset, self.size,
self.data_offset, self.data_size))
def read(self):
"""Read the chunks data"""
self._fileobj.seek(self.data_offset)
return self._fileobj.read(self.data_size)
def write(self, data):
"""Write the chunk data"""
if len(data) > self.data_size:
raise ValueError
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)
if self.parent_chunk is not None:
self.parent_chunk._remove_subchunk(self)
self._fileobj.flush()
def _update_size(self, size_diff, changed_subchunk=None):
"""Update the size of the chunk"""
old_size = self.size
self.data_size += size_diff
self._fileobj.seek(self.offset + 4)
self.write_size()
self._calculate_size()
if self.parent_chunk is not None:
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"""
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 IffContainerChunkMixin():
"""A IFF chunk containing other chunks.
A container chunk can have an additional name as the first 4 bytes of the
chunk data followed by an arbitrary number of subchunks. The root chunk of
the file is always a container chunk (e.g. the AIFF chunk or the FORM chunk
for RIFF) but there can be other types of container chunks (e.g. the LIST
chunks used in RIFF).
"""
def parse_next_subchunk(self):
""""""
raise error("Not implemented")
def init_container(self, name_size=4):
# Lists can store an additional name identifier before the subchunks
self.__name_size = name_size
if self.data_size < name_size:
raise InvalidChunk(
'Container chunk data size < %i' % name_size)
# Read the container name
if name_size > 0:
try:
self.name = self._fileobj.read(name_size).decode('ascii')
except UnicodeDecodeError as e:
raise error(e)
else:
self.name = None
# 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 + self.__name_size
while next_offset < self.offset + self.size:
self._fileobj.seek(next_offset)
try:
chunk = self.parse_next_subchunk()
except EmptyChunk:
break
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 container chunk"""
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.write_new_header(id_.ljust(4).encode('ascii'), data_size)
self._fileobj.seek(next_offset)
chunk = self.parse_next_subchunk()
self._update_size(chunk.size)
if data:
chunk.write(data)
self.subchunks().append(chunk)
self._fileobj.flush()
return chunk
def __contains__(self, id_):
"""Check if this chunk contains a specific subchunk."""
assert_valid_chunk_id(id_)
try:
self[id_]
return True
except KeyError:
return False
def __getitem__(self, id_):
"""Get a subchunk by ID."""
assert_valid_chunk_id(id_)
found_chunk = None
for chunk in self.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[id_].delete()
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:
"""Representation of a IFF file"""
def __init__(self, chunk_cls, fileobj):
fileobj.seek(0)
self.root = chunk_cls.parse(fileobj)
def __contains__(self, id_):
"""Check if the IFF file contains a specific chunk"""
return id_ in self.root
def __getitem__(self, id_):
"""Get a chunk from the IFF file"""
return self.root[id_]
def __delitem__(self, id_):
"""Remove a chunk from the IFF file"""
self.delete_chunk(id_)
def delete_chunk(self, id_):
"""Remove a chunk from the IFF file"""
del self.root[id_]
def insert_chunk(self, id_, data=None):
"""Insert a new chunk at the end of the IFF file"""
return self.root.insert_chunk(id_, data)
class IffID3(ID3):
"""A generic IFF file with ID3v2 tags"""
def _load_file(self, fileobj):
raise error("Not implemented")
def _pre_load_header(self, fileobj):
try:
fileobj.seek(self._load_file(fileobj)['ID3'].data_offset)
except (InvalidChunk, KeyError):
raise ID3NoHeaderError("No ID3 chunk")
@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething=None, v2_version=4, v23_sep='/', padding=None):
"""Save ID3v2 data to the IFF file"""
fileobj = filething.fileobj
iff_file = self._load_file(fileobj)
if 'ID3' not in iff_file:
iff_file.insert_chunk('ID3')
chunk = iff_file['ID3']
try:
data = self._prepare_data(
fileobj, chunk.data_offset, chunk.data_size, v2_version,
v23_sep, padding)
except ID3Error as e:
reraise(error, e, sys.exc_info()[2])
chunk.resize(len(data))
chunk.write(data)
@convert_error(IOError, error)
@loadfile(writable=True)
def delete(self, filething=None):
"""Completely removes the ID3 chunk from the IFF file"""
try:
iff_file = self._load_file(filething.fileobj)
del iff_file['ID3']
except KeyError:
pass
self.clear()

70
lib/mutagen/_riff.py Normal file
View File

@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2017 Borewit
# Copyright (C) 2019-2020 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.
"""Resource Interchange File Format (RIFF)."""
import struct
from struct import pack
from mutagen._iff import (
IffChunk,
IffContainerChunkMixin,
IffFile,
InvalidChunk,
)
class RiffChunk(IffChunk):
"""Generic RIFF chunk"""
@classmethod
def parse_header(cls, header):
return struct.unpack('<4sI', header)
@classmethod
def get_class(cls, id):
if id in (u'LIST', u'RIFF'):
return RiffListChunk
else:
return cls
def write_new_header(self, id_, size):
self._fileobj.write(pack('<4sI', id_, size))
def write_size(self):
self._fileobj.write(pack('<I', self.data_size))
class RiffListChunk(RiffChunk, IffContainerChunkMixin):
"""A RIFF chunk containing other chunks.
This is either a 'LIST' or 'RIFF'
"""
def parse_next_subchunk(self):
return RiffChunk.parse(self._fileobj, self)
def __init__(self, fileobj, id, data_size, parent_chunk):
if id not in (u'RIFF', u'LIST'):
raise InvalidChunk('Expected RIFF or LIST chunk, got %s' % id)
RiffChunk.__init__(self, fileobj, id, data_size, parent_chunk)
self.init_container()
class RiffFile(IffFile):
"""Representation of a RIFF file"""
def __init__(self, fileobj):
super().__init__(RiffChunk, fileobj)
if self.root.id != u'RIFF':
raise InvalidChunk("Root chunk must be a RIFF chunk, got %s"
% self.root.id)
self.file_type = self.root.name

0
lib/mutagen/_senf/README.rst Executable file → Normal file
View File

27
lib/mutagen/_senf/__init__.py Executable file → Normal file
View File

@@ -9,18 +9,20 @@
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
import os
if os.name != "nt":
# make imports work
_winapi = object()
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from ._fsnative import fsnative, path2fsn, fsn2text, fsn2bytes, \
bytes2fsn, uri2fsn, fsn2uri, text2fsn
from ._print import print_, input_
bytes2fsn, uri2fsn, fsn2uri, text2fsn, fsn2norm
from ._print import print_, input_, supports_ansi_escape_codes
from ._stdlib import sep, pathsep, curdir, pardir, altsep, extsep, devnull, \
defpath, getcwd, expanduser, expandvars
from ._argv import argv
@@ -30,10 +32,11 @@ from ._temp import mkstemp, gettempdir, gettempprefix, mkdtemp
fsnative, print_, getcwd, getenv, unsetenv, putenv, environ, expandvars, \
path2fsn, fsn2text, fsn2bytes, bytes2fsn, uri2fsn, fsn2uri, mkstemp, \
gettempdir, gettempprefix, mkdtemp, input_, expanduser, text2fsn
gettempdir, gettempprefix, mkdtemp, input_, expanduser, text2fsn, \
supports_ansi_escape_codes, fsn2norm
version = (1, 2, 2)
version = (1, 4, 2)
"""Tuple[`int`, `int`, `int`]: The version tuple (major, minor, micro)"""

View File

@@ -0,0 +1,104 @@
import sys
import os
from typing import Text, Union, Any, Optional, Tuple, List, Dict
if sys.version_info[0] == 2:
_pathlike = Union[Text, bytes]
else:
_pathlike = Union[Text, bytes, 'os.PathLike[Any]']
_uri = Union[Text, str]
if sys.version_info[0] == 2:
if sys.platform == "win32":
_base = Text
else:
_base = bytes
else:
_base = Text
class fsnative(_base):
def __init__(self, object: Text=u"") -> None:
...
_fsnative = Union[fsnative, _base]
if sys.platform == "win32":
_bytes_default_encoding = str
else:
_bytes_default_encoding = Optional[str]
def path2fsn(path: _pathlike) -> _fsnative:
...
def fsn2text(path: _fsnative, strict: bool=False) -> Text:
...
def text2fsn(text: Text) -> _fsnative:
...
def fsn2bytes(path: _fsnative, encoding: _bytes_default_encoding="utf-8") -> bytes:
...
def bytes2fsn(data: bytes, encoding: _bytes_default_encoding="utf-8") -> _fsnative:
...
def uri2fsn(uri: _uri) -> _fsnative:
...
def fsn2uri(path: _fsnative) -> Text:
...
def fsn2norm(path: _fsnative) -> _fsnative:
...
sep: _fsnative
pathsep: _fsnative
curdir: _fsnative
pardir: _fsnative
altsep: _fsnative
extsep: _fsnative
devnull: _fsnative
defpath: _fsnative
def getcwd() -> _fsnative:
...
def getenv(key: _pathlike, value: Optional[_fsnative]=None) -> Optional[_fsnative]:
...
def putenv(key: _pathlike, value: _pathlike):
...
def unsetenv(key: _pathlike) -> None:
...
def supports_ansi_escape_codes(fd: int) -> bool:
...
def expandvars(path: _pathlike) -> _fsnative:
...
def expanduser(path: _pathlike) -> _fsnative:
...
environ: Dict[_fsnative,_fsnative]
argv: List[_fsnative]
def gettempdir() -> _fsnative:
pass
def mkstemp(suffix: Optional[_pathlike]=None, prefix: Optional[_pathlike]=None, dir: Optional[_pathlike]=None, text: bool=False) -> Tuple[int, _fsnative]:
...
def mkdtemp(suffix: Optional[_pathlike]=None, prefix: Optional[_pathlike]=None, dir: Optional[_pathlike]=None) -> _fsnative:
...
version_string: str
version: Tuple[int, int, int]
print_ = print
def input_(prompt: Any=None) -> _fsnative:
...

19
lib/mutagen/_senf/_argv.py Executable file → Normal file
View File

@@ -9,12 +9,23 @@
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import sys
import ctypes
import collections
try:
from collections import abc
except ImportError:
import collections as abc # type: ignore
from functools import total_ordering
from ._compat import PY2, string_types
@@ -49,7 +60,7 @@ def _get_win_argv():
@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.

31
lib/mutagen/_senf/_compat.py Executable file → Normal file
View File

@@ -9,8 +9,16 @@
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import sys
@@ -20,26 +28,23 @@ PY3 = not PY2
if PY2:
from urllib.parse import urlparse, urlunparse
from urlparse import urlparse, urlunparse
urlparse, urlunparse
from urllib.request import pathname2url, url2pathname
from urllib.parse import quote, unquote
pathname2url, url2pathname, quote, unquote
from urllib import quote, unquote
quote, unquote
from io import StringIO
from StringIO import StringIO
BytesIO = StringIO
from io import StringIO as TextIO
TextIO
string_types = (str, str)
text_type = str
string_types = (str, unicode)
text_type = unicode
iteritems = lambda d: iter(d.items())
iteritems = lambda d: d.iteritems()
elif PY3:
from urllib.parse import urlparse, quote, unquote, urlunparse
urlparse, quote, unquote, urlunparse
from urllib.request import pathname2url, url2pathname
pathname2url, url2pathname
from io import StringIO
StringIO = StringIO
@@ -50,4 +55,4 @@ elif PY3:
string_types = (str,)
text_type = str
iteritems = lambda d: iter(list(d.items()))
iteritems = lambda d: iter(d.items())

27
lib/mutagen/_senf/_environ.py Executable file → Normal file
View File

@@ -9,12 +9,23 @@
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import os
import ctypes
import collections
try:
from collections import abc
except ImportError:
import collections as abc # type: ignore
from ._compat import text_type, PY2
from ._fsnative import path2fsn, is_win, _fsn2legacy, fsnative
@@ -86,23 +97,23 @@ def read_windows_environ():
res = ctypes.cast(res, ctypes.POINTER(ctypes.c_wchar))
done = []
current = ""
current = u""
i = 0
while 1:
c = res[i]
i += 1
if c == "\x00":
if c == u"\x00":
if not current:
break
done.append(current)
current = ""
current = u""
continue
current += c
dict_ = {}
for entry in done:
try:
key, value = entry.split("=", 1)
key, value = entry.split(u"=", 1)
except ValueError:
continue
key = _norm_key(key)
@@ -122,7 +133,7 @@ def _norm_key(key):
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.

205
lib/mutagen/_senf/_fsnative.py Executable file → Normal file
View File

@@ -9,8 +9,16 @@
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import os
import sys
@@ -18,8 +26,7 @@ import ctypes
import codecs
from . import _winapi as winapi
from ._compat import text_type, PY3, PY2, url2pathname, urlparse, quote, \
unquote, urlunparse
from ._compat import text_type, PY3, PY2, urlparse, quote, unquote, urlunparse
is_win = os.name == "nt"
@@ -49,45 +56,9 @@ def _swap_bytes(data):
return bytes(data)
def _codec_fails_on_encode_surrogates(codec, _cache={}):
"""Returns if a codec fails correctly when passing in surrogates with
a surrogatepass/surrogateescape error handler. Some codecs were broken
in Python <3.4
"""
try:
return _cache[codec]
except KeyError:
try:
"\uD800\uDC01".encode(codec)
except UnicodeEncodeError:
_cache[codec] = True
else:
_cache[codec] = False
return _cache[codec]
def _codec_can_decode_with_surrogatepass(codec, _cache={}):
"""Returns if a codec supports the surrogatepass error handler when
decoding. Some codecs were broken in Python <3.4
"""
try:
return _cache[codec]
except KeyError:
try:
"\ud83d".encode(
codec, _surrogatepass).decode(codec, _surrogatepass)
except UnicodeDecodeError:
_cache[codec] = False
else:
_cache[codec] = True
return _cache[codec]
def _bytes2winpath(data, codec):
def _decode_surrogatepass(data, codec):
"""Like data.decode(codec, 'surrogatepass') but makes utf-16-le/be work
on Python < 3.4 + Windows
on Python 2.
https://bugs.python.org/issue27971
@@ -97,7 +68,7 @@ def _bytes2winpath(data, codec):
try:
return data.decode(codec, _surrogatepass)
except UnicodeDecodeError:
if not _codec_can_decode_with_surrogatepass(codec):
if PY2:
if _normalize_codec(codec) == "utf-16-be":
data = _swap_bytes(data)
codec = "utf-16-le"
@@ -113,30 +84,45 @@ def _bytes2winpath(data, codec):
raise
def _winpath2bytes_py3(text, codec):
"""Fallback implementation for text including surrogates"""
def _merge_surrogates(text):
"""Returns a copy of the text with all surrogate pairs merged"""
# merge surrogate codepoints
if _normalize_codec(codec).startswith("utf-16"):
# fast path, utf-16 merges anyway
return text.encode(codec, _surrogatepass)
return _bytes2winpath(
return _decode_surrogatepass(
text.encode("utf-16-le", _surrogatepass),
"utf-16-le").encode(codec, _surrogatepass)
"utf-16-le")
if PY2:
def _winpath2bytes(text, codec):
return text.encode(codec)
else:
def _winpath2bytes(text, codec):
if _codec_fails_on_encode_surrogates(codec):
try:
return text.encode(codec)
except UnicodeEncodeError:
return _winpath2bytes_py3(text, codec)
else:
return _winpath2bytes_py3(text, codec)
def fsn2norm(path):
"""
Args:
path (fsnative): The path to normalize
Returns:
`fsnative`
Normalizes an fsnative path.
The same underlying path can have multiple representations as fsnative
(due to surrogate pairs and variable length encodings). When concatenating
fsnative the result might be different than concatenating the serialized
form and then deserializing it.
This returns the normalized form i.e. the form which os.listdir() would
return. This is useful when you alter fsnative but require that the same
underlying path always maps to the same fsnative value.
All functions like :func:`bytes2fsn`, :func:`fsnative`, :func:`text2fsn`
and :func:`path2fsn` always return a normalized path, independent of their
input.
"""
native = _fsn2native(path)
if is_win:
return _merge_surrogates(native)
elif PY3:
return bytes2fsn(native, None)
else:
return path
def _fsn2legacy(path):
@@ -173,14 +159,15 @@ def _fsnative(text):
path = text.encode("utf-8", _surrogatepass)
if b"\x00" in path:
path = path.replace(b"\x00", fsn2bytes(_fsnative("\uFFFD"), None))
path = path.replace(b"\x00", fsn2bytes(_fsnative(u"\uFFFD"), None))
if PY3:
return path.decode(_encoding, "surrogateescape")
return path
else:
if "\x00" in text:
text = text.replace("\x00", "\uFFFD")
if u"\x00" in text:
text = text.replace(u"\x00", u"\uFFFD")
text = fsn2norm(text)
return text
@@ -235,7 +222,7 @@ def _create_fsnative(type_):
the `str` only contains ASCII and no NULL.
"""
def __new__(cls, text=""):
def __new__(cls, text=u""):
return _fsnative(text)
new_type = meta("fsnative", (object,), dict(impl.__dict__))
@@ -259,10 +246,10 @@ def _typecheck_fsnative(path):
return False
if PY3 or is_win:
if "\x00" in path:
if u"\x00" in path:
return False
if is_unix and not _is_unicode_encoding:
if is_unix:
try:
path.encode(_encoding, "surrogateescape")
except UnicodeEncodeError:
@@ -297,7 +284,6 @@ def _fsn2native(path):
try:
path = path.encode(_encoding, "surrogateescape")
except UnicodeEncodeError:
assert not _is_unicode_encoding
# This look more like ValueError, but raising only one error
# makes things simpler... also one could say str + surrogates
# is its own type
@@ -309,7 +295,7 @@ def _fsn2native(path):
if b"\x00" in path:
raise TypeError("fsnative can't contain nulls")
else:
if "\x00" in path:
if u"\x00" in path:
raise TypeError("fsnative can't contain nulls")
return path
@@ -331,7 +317,6 @@ def _get_encoding():
_encoding = _get_encoding()
_is_unicode_encoding = _encoding.startswith("utf")
def path2fsn(path):
@@ -369,9 +354,11 @@ def path2fsn(path):
data = path.encode(_encoding, "surrogateescape")
if b"\x00" in data:
raise ValueError("embedded null")
path = fsn2norm(path)
else:
if "\x00" in path:
if u"\x00" in path:
raise ValueError("embedded null")
path = fsn2norm(path)
if not isinstance(path, fsnative_type):
raise TypeError("path needs to be %s", fsnative_type.__name__)
@@ -430,22 +417,21 @@ def text2fsn(text):
return fsnative(text)
def fsn2bytes(path, encoding):
def fsn2bytes(path, encoding="utf-8"):
"""
Args:
path (fsnative): The path to convert
encoding (`str` or `None`): `None` if you don't care about Windows
encoding (`str`): encoding used for Windows
Returns:
`bytes`
Raises:
TypeError: If no `fsnative` path is passed
ValueError: If encoding fails or no encoding is given
ValueError: If encoding fails or the encoding is invalid
Converts a `fsnative` path to `bytes`.
The passed *encoding* is only used on platforms where paths are not
associated with an encoding (Windows for example). If you don't care about
Windows you can pass `None`.
associated with an encoding (Windows for example).
For Windows paths, lone surrogates will be encoded like normal code points
and surrogate pairs will be merged before encoding. In case of ``utf-8``
@@ -459,30 +445,45 @@ def fsn2bytes(path, encoding):
if encoding is None:
raise ValueError("invalid encoding %r" % encoding)
try:
return _winpath2bytes(path, encoding)
except LookupError:
raise ValueError("invalid encoding %r" % encoding)
if PY2:
try:
return path.encode(encoding)
except LookupError:
raise ValueError("invalid encoding %r" % encoding)
else:
try:
return path.encode(encoding)
except LookupError:
raise ValueError("invalid encoding %r" % encoding)
except UnicodeEncodeError:
# Fallback implementation for text including surrogates
# merge surrogate codepoints
if _normalize_codec(encoding).startswith("utf-16"):
# fast path, utf-16 merges anyway
return path.encode(encoding, _surrogatepass)
return _merge_surrogates(path).encode(encoding, _surrogatepass)
else:
return path
def bytes2fsn(data, encoding):
def bytes2fsn(data, encoding="utf-8"):
"""
Args:
data (bytes): The data to convert
encoding (`str` or `None`): `None` if you don't care about Windows
encoding (`str`): encoding used for Windows
Returns:
`fsnative`
Raises:
TypeError: If no `bytes` path is passed
ValueError: If decoding fails or no encoding is given
ValueError: If decoding fails or the encoding is invalid
Turns `bytes` to a `fsnative` path.
The passed *encoding* is only used on platforms where paths are not
associated with an encoding (Windows for example). If you don't care about
Windows you can pass `None`.
associated with an encoding (Windows for example).
For Windows paths ``WTF-8`` is accepted if ``utf-8`` is used and
``WTF-16`` accepted if ``utf-16-le`` is used.
"""
if not isinstance(data, bytes):
@@ -492,10 +493,10 @@ def bytes2fsn(data, encoding):
if encoding is None:
raise ValueError("invalid encoding %r" % encoding)
try:
path = _bytes2winpath(data, encoding)
path = _decode_surrogatepass(data, encoding)
except LookupError:
raise ValueError("invalid encoding %r" % encoding)
if "\x00" in path:
if u"\x00" in path:
raise ValueError("contains nulls")
return path
else:
@@ -543,20 +544,32 @@ def uri2fsn(uri):
uri = urlunparse(parsed)[7:]
if is_win:
path = url2pathname(uri)
try:
drive, rest = uri.split(":", 1)
except ValueError:
path = ""
rest = uri.replace("/", "\\")
else:
path = drive[-1] + ":"
rest = rest.replace("/", "\\")
if PY2:
path += unquote(rest)
else:
path += unquote(rest, encoding="utf-8", errors="surrogatepass")
if netloc:
path = "\\\\" + path
if PY2:
path = path.decode("utf-8")
if "\x00" in path:
if u"\x00" in path:
raise ValueError("embedded null")
return path
else:
path = url2pathname(uri)
if PY2:
path = unquote(uri)
else:
path = unquote(uri, encoding=_encoding, errors="surrogateescape")
if "\x00" in path:
raise ValueError("embedded null")
if PY3:
path = fsnative(path)
return path
@@ -594,6 +607,8 @@ def fsn2uri(path):
except WindowsError as e:
raise ValueError(e)
uri = buf[:length.value]
# https://bitbucket.org/pypy/pypy/issues/3133
uri = _merge_surrogates(uri)
# For some reason UrlCreateFromPathW escapes some chars outside of
# ASCII and some not. Unquote and re-quote with utf-8.
@@ -607,4 +622,4 @@ def fsn2uri(path):
return _quote_path(uri.encode("utf-8", _surrogatepass))
else:
return "file://" + _quote_path(path)
return u"file://" + _quote_path(path)

85
lib/mutagen/_senf/_print.py Executable file → Normal file
View File

@@ -9,14 +9,23 @@
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import sys
import os
import ctypes
import re
from ._fsnative import _encoding, is_win, is_unix, _surrogatepass
from ._fsnative import _encoding, is_win, is_unix, _surrogatepass, bytes2fsn
from ._compat import text_type, PY2, PY3
from ._winansi import AnsiState, ansi_split
from . import _winapi as winapi
@@ -155,7 +164,7 @@ def _print_windows(objects, sep, end, file, flush):
if not isinstance(end, text_type):
raise TypeError
if end == "\n":
if end == u"\n":
end = os.linesep
text = sep.join(parts) + end
@@ -225,7 +234,7 @@ def _readline_windows():
buf = ctypes.create_string_buffer(buf_size * ctypes.sizeof(winapi.WCHAR))
read = winapi.DWORD()
text = ""
text = u""
while True:
if winapi.ReadConsoleW(
h, buf, buf_size, ctypes.byref(read), None) == 0:
@@ -234,7 +243,7 @@ def _readline_windows():
raise ctypes.WinError()
data = buf[:read.value * ctypes.sizeof(winapi.WCHAR)]
text += data.decode("utf-16-le", _surrogatepass)
if text.endswith("\r\n"):
if text.endswith(u"\r\n"):
return text[:-2]
@@ -253,7 +262,7 @@ def _decode_codepage(codepage, data):
assert isinstance(data, bytes)
if not data:
return ""
return u""
# get the required buffer length first
length = winapi.MultiByteToWideChar(codepage, 0, data, len(data), None, 0)
@@ -351,3 +360,65 @@ def input_(prompt=None):
print_(prompt, end="")
return _readline()
def _get_file_name_for_handle(handle):
"""(Windows only) Returns a file name for a file handle.
Args:
handle (winapi.HANDLE)
Returns:
`text` or `None` if no file name could be retrieved.
"""
assert is_win
assert handle != winapi.INVALID_HANDLE_VALUE
size = winapi.FILE_NAME_INFO.FileName.offset + \
winapi.MAX_PATH * ctypes.sizeof(winapi.WCHAR)
buf = ctypes.create_string_buffer(size)
if winapi.GetFileInformationByHandleEx is None:
# Windows XP
return None
status = winapi.GetFileInformationByHandleEx(
handle, winapi.FileNameInfo, buf, size)
if status == 0:
return None
name_info = ctypes.cast(
buf, ctypes.POINTER(winapi.FILE_NAME_INFO)).contents
offset = winapi.FILE_NAME_INFO.FileName.offset
data = buf[offset:offset + name_info.FileNameLength]
return bytes2fsn(data, "utf-16-le")
def supports_ansi_escape_codes(fd):
"""Returns whether the output device is capable of interpreting ANSI escape
codes when :func:`print_` is used.
Args:
fd (int): file descriptor (e.g. ``sys.stdout.fileno()``)
Returns:
`bool`
"""
if os.isatty(fd):
return True
if not is_win:
return False
# Check for cygwin/msys terminal
handle = winapi._get_osfhandle(fd)
if handle == winapi.INVALID_HANDLE_VALUE:
return False
if winapi.GetFileType(handle) != winapi.FILE_TYPE_PIPE:
return False
file_name = _get_file_name_for_handle(handle)
match = re.match(
"^\\\\(cygwin|msys)-[a-z0-9]+-pty[0-9]+-(from|to)-master$", file_name)
return match is not None

14
lib/mutagen/_senf/_stdlib.py Executable file → Normal file
View File

@@ -9,8 +9,16 @@
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import re
import os
@@ -38,7 +46,7 @@ def getcwd():
"""
if is_win and PY2:
return os.getcwd()
return os.getcwdu()
return os.getcwd()

12
lib/mutagen/_senf/_temp.py Executable file → Normal file
View File

@@ -9,8 +9,16 @@
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import tempfile

14
lib/mutagen/_senf/_winansi.py Executable file → Normal file
View File

@@ -9,8 +9,16 @@
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import ctypes
import re
@@ -25,7 +33,7 @@ def ansi_parse(code):
return code[-1:], tuple([int(v or "0") for v in code[2:-1].split(";")])
def ansi_split(text, _re=re.compile("(\x1b\[(\d*;?)*\S)")):
def ansi_split(text, _re=re.compile(u"(\x1b\\[(\\d*;?)*\\S)")):
"""Yields (is_ansi, text)"""
for part in _re.split(text):

298
lib/mutagen/_senf/_winapi.py Executable file → Normal file
View File

@@ -9,175 +9,213 @@
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import sys
import ctypes
from ctypes import WinDLL, wintypes
if sys.platform == 'win32':
from ctypes import WinDLL, CDLL, wintypes
shell32 = WinDLL("shell32")
kernel32 = WinDLL("kernel32")
shlwapi = WinDLL("shlwapi")
shell32 = WinDLL("shell32")
kernel32 = WinDLL("kernel32")
shlwapi = WinDLL("shlwapi")
msvcrt = CDLL("msvcrt")
GetCommandLineW = kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = wintypes.LPCWSTR
GetCommandLineW = kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = wintypes.LPCWSTR
CommandLineToArgvW = shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [
wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int)]
CommandLineToArgvW.restype = ctypes.POINTER(wintypes.LPWSTR)
CommandLineToArgvW = shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [
wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int)]
CommandLineToArgvW.restype = ctypes.POINTER(wintypes.LPWSTR)
LocalFree = kernel32.LocalFree
LocalFree.argtypes = [wintypes.HLOCAL]
LocalFree.restype = wintypes.HLOCAL
LocalFree = kernel32.LocalFree
LocalFree.argtypes = [wintypes.HLOCAL]
LocalFree.restype = wintypes.HLOCAL
# https://msdn.microsoft.com/en-us/library/windows/desktop/aa383751.aspx
LPCTSTR = ctypes.c_wchar_p
LPWSTR = wintypes.LPWSTR
LPCWSTR = ctypes.c_wchar_p
LPTSTR = LPWSTR
PCWSTR = ctypes.c_wchar_p
PCTSTR = PCWSTR
PWSTR = ctypes.c_wchar_p
PTSTR = PWSTR
LPVOID = wintypes.LPVOID
WCHAR = wintypes.WCHAR
LPSTR = ctypes.c_char_p
# https://msdn.microsoft.com/en-us/library/windows/desktop/aa383751.aspx
LPCTSTR = ctypes.c_wchar_p
LPWSTR = wintypes.LPWSTR
LPCWSTR = ctypes.c_wchar_p
LPTSTR = LPWSTR
PCWSTR = ctypes.c_wchar_p
PCTSTR = PCWSTR
PWSTR = ctypes.c_wchar_p
PTSTR = PWSTR
LPVOID = wintypes.LPVOID
WCHAR = wintypes.WCHAR
LPSTR = ctypes.c_char_p
BOOL = wintypes.BOOL
LPBOOL = ctypes.POINTER(BOOL)
UINT = wintypes.UINT
WORD = wintypes.WORD
DWORD = wintypes.DWORD
SHORT = wintypes.SHORT
HANDLE = wintypes.HANDLE
ULONG = wintypes.ULONG
LPCSTR = wintypes.LPCSTR
BOOL = wintypes.BOOL
LPBOOL = ctypes.POINTER(BOOL)
UINT = wintypes.UINT
WORD = wintypes.WORD
DWORD = wintypes.DWORD
SHORT = wintypes.SHORT
HANDLE = wintypes.HANDLE
ULONG = wintypes.ULONG
LPCSTR = wintypes.LPCSTR
STD_INPUT_HANDLE = DWORD(-10)
STD_OUTPUT_HANDLE = DWORD(-11)
STD_ERROR_HANDLE = DWORD(-12)
STD_INPUT_HANDLE = DWORD(-10)
STD_OUTPUT_HANDLE = DWORD(-11)
STD_ERROR_HANDLE = DWORD(-12)
INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value
INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value
INTERNET_MAX_SCHEME_LENGTH = 32
INTERNET_MAX_PATH_LENGTH = 2048
INTERNET_MAX_URL_LENGTH = (
INTERNET_MAX_SCHEME_LENGTH + len("://") + INTERNET_MAX_PATH_LENGTH)
INTERNET_MAX_SCHEME_LENGTH = 32
INTERNET_MAX_PATH_LENGTH = 2048
INTERNET_MAX_URL_LENGTH = (
INTERNET_MAX_SCHEME_LENGTH + len("://") + INTERNET_MAX_PATH_LENGTH)
FOREGROUND_BLUE = 0x0001
FOREGROUND_GREEN = 0x0002
FOREGROUND_RED = 0x0004
FOREGROUND_INTENSITY = 0x0008
FOREGROUND_BLUE = 0x0001
FOREGROUND_GREEN = 0x0002
FOREGROUND_RED = 0x0004
FOREGROUND_INTENSITY = 0x0008
BACKGROUND_BLUE = 0x0010
BACKGROUND_GREEN = 0x0020
BACKGROUND_RED = 0x0040
BACKGROUND_INTENSITY = 0x0080
BACKGROUND_BLUE = 0x0010
BACKGROUND_GREEN = 0x0020
BACKGROUND_RED = 0x0040
BACKGROUND_INTENSITY = 0x0080
COMMON_LVB_REVERSE_VIDEO = 0x4000
COMMON_LVB_UNDERSCORE = 0x8000
COMMON_LVB_REVERSE_VIDEO = 0x4000
COMMON_LVB_UNDERSCORE = 0x8000
UrlCreateFromPathW = shlwapi.UrlCreateFromPathW
UrlCreateFromPathW.argtypes = [
PCTSTR, PTSTR, ctypes.POINTER(DWORD), DWORD]
UrlCreateFromPathW.restype = ctypes.HRESULT
UrlCreateFromPathW = shlwapi.UrlCreateFromPathW
UrlCreateFromPathW.argtypes = [
PCTSTR, PTSTR, ctypes.POINTER(DWORD), DWORD]
UrlCreateFromPathW.restype = ctypes.HRESULT
SetEnvironmentVariableW = kernel32.SetEnvironmentVariableW
SetEnvironmentVariableW.argtypes = [LPCTSTR, LPCTSTR]
SetEnvironmentVariableW.restype = wintypes.BOOL
SetEnvironmentVariableW = kernel32.SetEnvironmentVariableW
SetEnvironmentVariableW.argtypes = [LPCTSTR, LPCTSTR]
SetEnvironmentVariableW.restype = wintypes.BOOL
GetEnvironmentVariableW = kernel32.GetEnvironmentVariableW
GetEnvironmentVariableW.argtypes = [LPCTSTR, LPTSTR, DWORD]
GetEnvironmentVariableW.restype = DWORD
GetEnvironmentVariableW = kernel32.GetEnvironmentVariableW
GetEnvironmentVariableW.argtypes = [LPCTSTR, LPTSTR, DWORD]
GetEnvironmentVariableW.restype = DWORD
GetEnvironmentStringsW = kernel32.GetEnvironmentStringsW
GetEnvironmentStringsW.argtypes = []
GetEnvironmentStringsW.restype = ctypes.c_void_p
GetEnvironmentStringsW = kernel32.GetEnvironmentStringsW
GetEnvironmentStringsW.argtypes = []
GetEnvironmentStringsW.restype = ctypes.c_void_p
FreeEnvironmentStringsW = kernel32.FreeEnvironmentStringsW
FreeEnvironmentStringsW.argtypes = [ctypes.c_void_p]
FreeEnvironmentStringsW.restype = ctypes.c_bool
FreeEnvironmentStringsW = kernel32.FreeEnvironmentStringsW
FreeEnvironmentStringsW.argtypes = [ctypes.c_void_p]
FreeEnvironmentStringsW.restype = ctypes.c_bool
GetStdHandle = kernel32.GetStdHandle
GetStdHandle.argtypes = [DWORD]
GetStdHandle.restype = HANDLE
GetStdHandle = kernel32.GetStdHandle
GetStdHandle.argtypes = [DWORD]
GetStdHandle.restype = HANDLE
class COORD(ctypes.Structure):
class COORD(ctypes.Structure):
_fields_ = [
("X", SHORT),
("Y", SHORT),
]
_fields_ = [
("X", SHORT),
("Y", SHORT),
]
class SMALL_RECT(ctypes.Structure):
_fields_ = [
("Left", SHORT),
("Top", SHORT),
("Right", SHORT),
("Bottom", SHORT),
]
class SMALL_RECT(ctypes.Structure):
class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
_fields_ = [
("Left", SHORT),
("Top", SHORT),
("Right", SHORT),
("Bottom", SHORT),
]
_fields_ = [
("dwSize", COORD),
("dwCursorPosition", COORD),
("wAttributes", WORD),
("srWindow", SMALL_RECT),
("dwMaximumWindowSize", COORD),
]
GetConsoleScreenBufferInfo = kernel32.GetConsoleScreenBufferInfo
GetConsoleScreenBufferInfo.argtypes = [
HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)]
GetConsoleScreenBufferInfo.restype = BOOL
class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
GetConsoleOutputCP = kernel32.GetConsoleOutputCP
GetConsoleOutputCP.argtypes = []
GetConsoleOutputCP.restype = UINT
_fields_ = [
("dwSize", COORD),
("dwCursorPosition", COORD),
("wAttributes", WORD),
("srWindow", SMALL_RECT),
("dwMaximumWindowSize", COORD),
]
SetConsoleOutputCP = kernel32.SetConsoleOutputCP
SetConsoleOutputCP.argtypes = [UINT]
SetConsoleOutputCP.restype = BOOL
GetConsoleCP = kernel32.GetConsoleCP
GetConsoleCP.argtypes = []
GetConsoleCP.restype = UINT
GetConsoleScreenBufferInfo = kernel32.GetConsoleScreenBufferInfo
GetConsoleScreenBufferInfo.argtypes = [
HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)]
GetConsoleScreenBufferInfo.restype = BOOL
SetConsoleCP = kernel32.SetConsoleCP
SetConsoleCP.argtypes = [UINT]
SetConsoleCP.restype = BOOL
GetConsoleOutputCP = kernel32.GetConsoleOutputCP
GetConsoleOutputCP.argtypes = []
GetConsoleOutputCP.restype = UINT
SetConsoleTextAttribute = kernel32.SetConsoleTextAttribute
SetConsoleTextAttribute.argtypes = [HANDLE, WORD]
SetConsoleTextAttribute.restype = BOOL
SetConsoleOutputCP = kernel32.SetConsoleOutputCP
SetConsoleOutputCP.argtypes = [UINT]
SetConsoleOutputCP.restype = BOOL
SetConsoleCursorPosition = kernel32.SetConsoleCursorPosition
SetConsoleCursorPosition.argtypes = [HANDLE, COORD]
SetConsoleCursorPosition.restype = BOOL
GetConsoleCP = kernel32.GetConsoleCP
GetConsoleCP.argtypes = []
GetConsoleCP.restype = UINT
ReadConsoleW = kernel32.ReadConsoleW
ReadConsoleW.argtypes = [
HANDLE, LPVOID, DWORD, ctypes.POINTER(DWORD), LPVOID]
ReadConsoleW.restype = BOOL
SetConsoleCP = kernel32.SetConsoleCP
SetConsoleCP.argtypes = [UINT]
SetConsoleCP.restype = BOOL
MultiByteToWideChar = kernel32.MultiByteToWideChar
MultiByteToWideChar.argtypes = [
UINT, DWORD, LPCSTR, ctypes.c_int, LPWSTR, ctypes.c_int]
MultiByteToWideChar.restype = ctypes.c_int
SetConsoleTextAttribute = kernel32.SetConsoleTextAttribute
SetConsoleTextAttribute.argtypes = [HANDLE, WORD]
SetConsoleTextAttribute.restype = BOOL
WideCharToMultiByte = kernel32.WideCharToMultiByte
WideCharToMultiByte.argtypes = [
UINT, DWORD, LPCWSTR, ctypes.c_int, LPSTR, ctypes.c_int,
LPCSTR, LPBOOL]
WideCharToMultiByte.restype = ctypes.c_int
SetConsoleCursorPosition = kernel32.SetConsoleCursorPosition
SetConsoleCursorPosition.argtypes = [HANDLE, COORD]
SetConsoleCursorPosition.restype = BOOL
MoveFileW = kernel32.MoveFileW
MoveFileW.argtypes = [LPCTSTR, LPCTSTR]
MoveFileW.restype = BOOL
ReadConsoleW = kernel32.ReadConsoleW
ReadConsoleW.argtypes = [HANDLE, LPVOID, DWORD, ctypes.POINTER(DWORD), LPVOID]
ReadConsoleW.restype = BOOL
GetFileInformationByHandleEx = None
if hasattr(kernel32, "GetFileInformationByHandleEx"):
GetFileInformationByHandleEx = kernel32.GetFileInformationByHandleEx
GetFileInformationByHandleEx.argtypes = [
HANDLE, ctypes.c_int, ctypes.c_void_p, DWORD]
GetFileInformationByHandleEx.restype = BOOL
else:
# Windows XP
pass
MultiByteToWideChar = kernel32.MultiByteToWideChar
MultiByteToWideChar.argtypes = [
UINT, DWORD, LPCSTR, ctypes.c_int, LPWSTR, ctypes.c_int]
MultiByteToWideChar.restype = ctypes.c_int
MAX_PATH = 260
FileNameInfo = 2
WideCharToMultiByte = kernel32.WideCharToMultiByte
WideCharToMultiByte.argtypes = [
UINT, DWORD, LPCWSTR, ctypes.c_int, LPSTR, ctypes.c_int, LPCSTR, LPBOOL]
WideCharToMultiByte.restpye = ctypes.c_int
class FILE_NAME_INFO(ctypes.Structure):
_fields_ = [
("FileNameLength", DWORD),
("FileName", WCHAR),
]
MoveFileW = kernel32.MoveFileW
MoveFileW.argtypes = [LPCTSTR, LPCTSTR]
MoveFileW.restype = BOOL
_get_osfhandle = msvcrt._get_osfhandle
_get_osfhandle.argtypes = [ctypes.c_int]
_get_osfhandle.restype = HANDLE
GetFileType = kernel32.GetFileType
GetFileType.argtypes = [HANDLE]
GetFileType.restype = DWORD
FILE_TYPE_PIPE = 0x0003

View File

4
lib/mutagen/_tags.py Executable file → Normal file
View File

@@ -115,7 +115,7 @@ class Metadata(Tags):
raise NotImplementedError
@loadfile(writable=False)
def save(self, filething, **kwargs):
def save(self, filething=None, **kwargs):
"""save(filething=None, **kwargs)
Save changes to a file.
@@ -129,7 +129,7 @@ class Metadata(Tags):
raise NotImplementedError
@loadfile(writable=False)
def delete(self, filething):
def delete(self, filething=None):
"""delete(filething=None)
Remove tags from a file.

0
lib/mutagen/_tools/__init__.py Executable file → Normal file
View File

4
lib/mutagen/_tools/_util.py Executable file → Normal file
View File

@@ -12,7 +12,7 @@ import contextlib
import optparse
from mutagen._senf import print_
from mutagen._compat import text_type, iterbytes
from mutagen._util import iterbytes
def split_escape(string, sep, maxsplit=None, escape_char="\\"):
@@ -25,7 +25,7 @@ def split_escape(string, sep, maxsplit=None, escape_char="\\"):
assert len(escape_char) == 1
if isinstance(string, bytes):
if isinstance(escape_char, text_type):
if isinstance(escape_char, str):
escape_char = escape_char.encode("ascii")
iter_ = iterbytes
else:

15
lib/mutagen/_tools/mid3cp.py Executable file → Normal file
View File

@@ -16,7 +16,6 @@ import os.path
import mutagen
import mutagen.id3
from mutagen._senf import print_, argv
from mutagen._compat import text_type
from ._util import SignalHandler, OptionParser
@@ -52,14 +51,14 @@ def copy(src, dst, merge, write_v1=True, excluded_tags=None, verbose=False):
try:
id3 = mutagen.id3.ID3(src, translate=False)
except mutagen.id3.ID3NoHeaderError:
print_("No ID3 header found in ", src, file=sys.stderr)
print_(u"No ID3 header found in ", src, file=sys.stderr)
return 1
except Exception as err:
print_(str(err), file=sys.stderr)
return 1
if verbose:
print_("File", src, "contains:", file=sys.stderr)
print_(u"File", src, u"contains:", file=sys.stderr)
print_(id3.pprint(), file=sys.stderr)
for tag in excluded_tags:
@@ -75,7 +74,7 @@ def copy(src, dst, merge, write_v1=True, excluded_tags=None, verbose=False):
print_(str(err), file=sys.stderr)
return 1
else:
for frame in list(id3.values()):
for frame in id3.values():
target.add(frame)
id3 = target
@@ -91,12 +90,12 @@ def copy(src, dst, merge, write_v1=True, excluded_tags=None, verbose=False):
try:
id3.save(dst, v1=(2 if write_v1 else 0), v2_version=v2_version)
except Exception as err:
print_("Error saving", dst, ":\n%s" % text_type(err),
print_(u"Error saving", dst, u":\n%s" % str(err),
file=sys.stderr)
return 1
else:
if verbose:
print_("Successfully saved", dst, file=sys.stderr)
print_(u"Successfully saved", dst, file=sys.stderr)
return 0
@@ -120,12 +119,12 @@ def main(argv):
(src, dst) = args
if not os.path.isfile(src):
print_("File not found:", src, file=sys.stderr)
print_(u"File not found:", src, file=sys.stderr)
parser.print_help(file=sys.stderr)
return 1
if not os.path.isfile(dst):
printerr("File not found:", dst, file=sys.stderr)
printerr(u"File not found:", dst, file=sys.stderr)
parser.print_help(file=sys.stderr)
return 1

17
lib/mutagen/_tools/mid3iconv.py Executable file → Normal file
View File

@@ -16,7 +16,6 @@ import locale
import mutagen
import mutagen.id3
from mutagen._senf import argv, print_, fsnative
from mutagen._compat import text_type
from ._util import SignalHandler, OptionParser
@@ -75,7 +74,7 @@ def update(options, filenames):
for filename in filenames:
with _sig.block():
if verbose != "quiet":
print_("Updating", filename)
print_(u"Updating", filename)
if has_id3v1(filename) and not noupdate and force_v1:
mutagen.id3.delete(filename, False, True)
@@ -84,13 +83,13 @@ def update(options, filenames):
id3 = mutagen.id3.ID3(filename)
except mutagen.id3.ID3NoHeaderError:
if verbose != "quiet":
print_("No ID3 header found; skipping...")
print_(u"No ID3 header found; skipping...")
continue
except Exception as err:
print_(text_type(err), file=sys.stderr)
print_(str(err), file=sys.stderr)
continue
for tag in [t for t in id3 if t.startswith(("T", "COMM"))]:
for tag in filter(lambda t: t.startswith(("T", "COMM")), id3):
frame = id3[tag]
if isinstance(frame, mutagen.id3.TimeStampTextFrame):
# non-unicode fields
@@ -105,7 +104,7 @@ def update(options, filenames):
continue
else:
frame.text = text
if not text or min(list(map(isascii, text))):
if not text or min(map(isascii, text)):
frame.encoding = 3
else:
frame.encoding = 1
@@ -122,7 +121,7 @@ def update(options, filenames):
def has_id3v1(filename):
try:
with open(filename, 'rb+') as f:
with open(filename, 'rb') as f:
f.seek(-128, 2)
return f.read(3) == b"TAG"
except IOError:
@@ -154,9 +153,9 @@ def main(argv):
for i, arg in enumerate(argv):
if arg == "-v1":
argv[i] = fsnative("--force-v1")
argv[i] = fsnative(u"--force-v1")
elif arg == "-removev1":
argv[i] = fsnative("--remove-v1")
argv[i] = fsnative(u"--remove-v1")
(options, args) = parser.parse_args(argv[1:])

119
lib/mutagen/_tools/mid3v2.py Executable file → Normal file
View File

@@ -11,6 +11,7 @@
import sys
import codecs
import mimetypes
import warnings
from optparse import SUPPRESS_HELP
@@ -19,7 +20,6 @@ import mutagen.id3
from mutagen.id3 import Encoding, PictureType
from mutagen._senf import fsnative, print_, argv, fsn2text, fsn2bytes, \
bytes2fsn
from mutagen._compat import PY2, text_type
from ._util import split_escape, SignalHandler, OptionParser
@@ -55,23 +55,23 @@ Any editing operation will cause the ID3 tag to be upgraded to ID3v2.4.
def list_frames(option, opt, value, parser):
items = list(mutagen.id3.Frames.items())
items = mutagen.id3.Frames.items()
for name, frame in sorted(items):
print_(" --%s %s" % (name, frame.__doc__.split("\n")[0]))
print_(u" --%s %s" % (name, frame.__doc__.split("\n")[0]))
raise SystemExit
def list_frames_2_2(option, opt, value, parser):
items = list(mutagen.id3.Frames_2_2.items())
items = mutagen.id3.Frames_2_2.items()
items.sort()
for name, frame in items:
print_(" --%s %s" % (name, frame.__doc__.split("\n")[0]))
print_(u" --%s %s" % (name, frame.__doc__.split("\n")[0]))
raise SystemExit
def list_genres(option, opt, value, parser):
for i, genre in enumerate(mutagen.id3.TCON.GENRES):
print_("%3d: %s" % (i, genre))
print_(u"%3d: %s" % (i, genre))
raise SystemExit
@@ -79,7 +79,7 @@ def delete_tags(filenames, v1, v2):
for filename in filenames:
with _sig.block():
if verbose:
print_("deleting ID3 tag info in", filename, file=sys.stderr)
print_(u"deleting ID3 tag info in", filename, file=sys.stderr)
mutagen.id3.delete(filename, v1, v2)
@@ -88,22 +88,22 @@ def delete_frames(deletes, filenames):
try:
deletes = frame_from_fsnative(deletes)
except ValueError as err:
print_(text_type(err), file=sys.stderr)
print_(str(err), file=sys.stderr)
frames = deletes.split(",")
for filename in filenames:
with _sig.block():
if verbose:
print_("deleting %s from" % deletes, filename,
print_(u"deleting %s from" % deletes, filename,
file=sys.stderr)
try:
id3 = mutagen.id3.ID3(filename)
except mutagen.id3.ID3NoHeaderError:
if verbose:
print_("No ID3 header found; skipping.", file=sys.stderr)
print_(u"No ID3 header found; skipping.", file=sys.stderr)
except Exception as err:
print_(text_type(err), file=sys.stderr)
print_(str(err), file=sys.stderr)
raise SystemExit(1)
else:
for frame in frames:
@@ -119,26 +119,24 @@ def frame_from_fsnative(arg):
assert isinstance(arg, fsnative)
text = fsn2text(arg, strict=True)
if PY2:
return text.encode("ascii")
else:
return text.encode("ascii").decode("ascii")
return text.encode("ascii").decode("ascii")
def value_from_fsnative(arg, escape):
"""Takes an item from argv and returns a text_type value without
"""Takes an item from argv and returns a str value without
surrogate escapes or raises ValueError.
"""
assert isinstance(arg, fsnative)
if escape:
bytes_ = fsn2bytes(arg, "utf-8")
if PY2:
bytes_ = bytes_.decode("string_escape")
else:
bytes_ = fsn2bytes(arg)
# With py3.7 this has started to warn for invalid escapes, but we
# don't control the input so ignore it.
with warnings.catch_warnings():
warnings.simplefilter("ignore")
bytes_ = codecs.escape_decode(bytes_)[0]
arg = bytes2fsn(bytes_, "utf-8")
arg = bytes2fsn(bytes_)
text = fsn2text(arg, strict=True)
return text
@@ -167,7 +165,7 @@ def write_files(edits, filenames, escape):
try:
frame = frame_from_fsnative(frame)
except ValueError as err:
print_(text_type(err), file=sys.stderr)
print_(str(err), file=sys.stderr)
assert isinstance(frame, str)
@@ -177,9 +175,9 @@ def write_files(edits, filenames, escape):
try:
value = value_from_fsnative(value, escape)
except ValueError as err:
error("%s: %s" % (frame, text_type(err)))
error(u"%s: %s" % (frame, str(err)))
assert isinstance(value, text_type)
assert isinstance(value, str)
encoded_edits.append((frame, value))
edits = encoded_edits
@@ -205,18 +203,18 @@ def write_files(edits, filenames, escape):
for filename in filenames:
with _sig.block():
if verbose:
print_("Writing", filename, file=sys.stderr)
print_(u"Writing", filename, file=sys.stderr)
try:
id3 = mutagen.id3.ID3(filename)
except mutagen.id3.ID3NoHeaderError:
if verbose:
print_("No ID3 header found; creating a new tag",
print_(u"No ID3 header found; creating a new tag",
file=sys.stderr)
id3 = mutagen.id3.ID3()
except Exception as err:
print_(str(err), file=sys.stderr)
continue
for (frame, vlist) in list(edits.items()):
for (frame, vlist) in edits.items():
if frame == "POPM":
for value in vlist:
values = string_split(value, ":")
@@ -240,13 +238,13 @@ def write_files(edits, filenames, escape):
if len(values) >= 2:
desc = values[1]
else:
desc = "cover"
desc = u"cover"
if len(values) >= 3:
try:
picture_type = int(values[2])
except ValueError:
error("Invalid picture type: %r" % values[1])
error(u"Invalid picture type: %r" % values[1])
else:
picture_type = PictureType.COVER_FRONT
@@ -264,7 +262,7 @@ def write_files(edits, filenames, escape):
with open(fn, "rb") as h:
data = h.read()
except IOError as e:
error(text_type(e))
error(str(e))
frame = mutagen.id3.APIC(encoding=encoding, mime=mime,
desc=desc, type=picture_type, data=data)
@@ -283,11 +281,24 @@ def write_files(edits, filenames, escape):
frame = mutagen.id3.COMM(
encoding=3, text=value, lang=lang, desc=desc)
id3.add(frame)
elif frame == "USLT":
for value in vlist:
values = string_split(value, ":")
if len(values) == 1:
value, desc, lang = values[0], "", "eng"
elif len(values) == 2:
desc, value, lang = values[0], values[1], "eng"
else:
value = ":".join(values[1:-1])
desc, lang = values[0], values[-1]
frame = mutagen.id3.USLT(
encoding=3, text=value, lang=lang, desc=desc)
id3.add(frame)
elif frame == "UFID":
for value in vlist:
values = string_split(value, ":")
if len(values) != 2:
error("Invalid value: %r" % values)
error(u"Invalid value: %r" % values)
owner = values[0]
data = values[1].encode("utf-8")
frame = mutagen.id3.UFID(owner=owner, data=data)
@@ -302,9 +313,20 @@ def write_files(edits, filenames, escape):
frame = mutagen.id3.TXXX(
encoding=3, text=value, desc=desc)
id3.add(frame)
elif frame == "WXXX":
for value in vlist:
values = string_split(value, ":", 1)
if len(values) == 1:
desc, value = "", values[0]
else:
desc, value = values[0], values[1]
frame = mutagen.id3.WXXX(
encoding=3, url=value, desc=desc)
id3.add(frame)
elif issubclass(mutagen.id3.Frames[frame],
mutagen.id3.UrlFrame):
frame = mutagen.id3.Frames[frame](encoding=3, url=vlist)
frame = mutagen.id3.Frames[frame](
encoding=3, url=vlist[-1])
id3.add(frame)
else:
frame = mutagen.id3.Frames[frame](encoding=3, text=vlist)
@@ -318,9 +340,9 @@ def list_tags(filenames):
try:
id3 = mutagen.id3.ID3(filename, translate=False)
except mutagen.id3.ID3NoHeaderError:
print_("No ID3 header found; skipping.")
print_(u"No ID3 header found; skipping.")
except Exception as err:
print_(text_type(err), file=sys.stderr)
print_(str(err), file=sys.stderr)
raise SystemExit(1)
else:
print_(id3.pprint())
@@ -332,13 +354,13 @@ def list_tags_raw(filenames):
try:
id3 = mutagen.id3.ID3(filename, translate=False)
except mutagen.id3.ID3NoHeaderError:
print_("No ID3 header found; skipping.")
print_(u"No ID3 header found; skipping.")
except Exception as err:
print_(text_type(err), file=sys.stderr)
print_(str(err), file=sys.stderr)
raise SystemExit(1)
else:
for frame in list(id3.values()):
print_(text_type(repr(frame)))
for frame in id3.values():
print_(str(repr(frame)))
def main(argv):
@@ -387,50 +409,51 @@ def main(argv):
parser.add_option(
"-a", "--artist", metavar='"ARTIST"', action="callback",
help="Set the artist information", type="string",
callback=lambda *args: args[3].edits.append((fsnative("--TPE1"),
callback=lambda *args: args[3].edits.append((fsnative(u"--TPE1"),
args[2])))
parser.add_option(
"-A", "--album", metavar='"ALBUM"', action="callback",
help="Set the album title information", type="string",
callback=lambda *args: args[3].edits.append((fsnative("--TALB"),
callback=lambda *args: args[3].edits.append((fsnative(u"--TALB"),
args[2])))
parser.add_option(
"-t", "--song", metavar='"SONG"', action="callback",
help="Set the song title information", type="string",
callback=lambda *args: args[3].edits.append((fsnative("--TIT2"),
callback=lambda *args: args[3].edits.append((fsnative(u"--TIT2"),
args[2])))
parser.add_option(
"-c", "--comment", metavar='"DESCRIPTION":"COMMENT":"LANGUAGE"',
action="callback", help="Set the comment information", type="string",
callback=lambda *args: args[3].edits.append((fsnative("--COMM"),
callback=lambda *args: args[3].edits.append((fsnative(u"--COMM"),
args[2])))
parser.add_option(
"-p", "--picture",
metavar='"FILENAME":"DESCRIPTION":"IMAGE-TYPE":"MIME-TYPE"',
action="callback", help="Set the picture", type="string",
callback=lambda *args: args[3].edits.append((fsnative("--APIC"),
callback=lambda *args: args[3].edits.append((fsnative(u"--APIC"),
args[2])))
parser.add_option(
"-g", "--genre", metavar='"GENRE"', action="callback",
help="Set the genre or genre number", type="string",
callback=lambda *args: args[3].edits.append((fsnative("--TCON"),
callback=lambda *args: args[3].edits.append((fsnative(u"--TCON"),
args[2])))
parser.add_option(
"-y", "--year", "--date", metavar='YYYY[-MM-DD]', action="callback",
help="Set the year/date", type="string",
callback=lambda *args: args[3].edits.append((fsnative("--TDRC"),
callback=lambda *args: args[3].edits.append((fsnative(u"--TDRC"),
args[2])))
parser.add_option(
"-T", "--track", metavar='"num/num"', action="callback",
help="Set the track number/(optional) total tracks", type="string",
callback=lambda *args: args[3].edits.append((fsnative("--TRCK"),
callback=lambda *args: args[3].edits.append((fsnative(u"--TRCK"),
args[2])))
for key, frame in list(mutagen.id3.Frames.items()):
for key, frame in mutagen.id3.Frames.items():
if (issubclass(frame, mutagen.id3.TextFrame)
or issubclass(frame, mutagen.id3.UrlFrame)
or issubclass(frame, mutagen.id3.POPM)
or frame in (mutagen.id3.APIC, mutagen.id3.UFID)):
or frame in (mutagen.id3.APIC, mutagen.id3.UFID,
mutagen.id3.USLT)):
parser.add_option(
"--" + key, action="callback", help=SUPPRESS_HELP,
type='string', metavar="value", # optparse blows up with this

42
lib/mutagen/_tools/moggsplit.py Executable file → Normal file
View File

@@ -46,28 +46,28 @@ def main(argv):
with _sig.block():
fileobjs = {}
format["base"] = os.path.splitext(os.path.basename(filename))[0]
fileobj = open(filename, "rb")
if options.m3u:
m3u = open(format["base"] + ".m3u", "w")
fileobjs["m3u"] = m3u
else:
m3u = None
while True:
try:
page = OggPage(fileobj)
except EOFError:
break
with open(filename, "rb") as fileobj:
if options.m3u:
m3u = open(format["base"] + ".m3u", "w")
fileobjs["m3u"] = m3u
else:
format["stream"] = page.serial
if page.serial not in fileobjs:
new_filename = options.pattern % format
new_fileobj = open(new_filename, "wb")
fileobjs[page.serial] = new_fileobj
if m3u:
m3u.write(new_filename + "\r\n")
fileobjs[page.serial].write(page.write())
for f in list(fileobjs.values()):
f.close()
m3u = None
while True:
try:
page = OggPage(fileobj)
except EOFError:
break
else:
format["stream"] = page.serial
if page.serial not in fileobjs:
new_filename = options.pattern % format
new_fileobj = open(new_filename, "wb")
fileobjs[page.serial] = new_fileobj
if m3u:
m3u.write(new_filename + "\r\n")
fileobjs[page.serial].write(page.write())
for f in fileobjs.values():
f.close()
def entry_point():

11
lib/mutagen/_tools/mutagen_inspect.py Executable file → Normal file
View File

@@ -9,7 +9,6 @@
"""Full tag list for any given file."""
from mutagen._senf import print_, argv
from mutagen._compat import text_type
from ._util import SignalHandler, OptionParser
@@ -30,14 +29,14 @@ def main(argv):
raise SystemExit(parser.print_help() or 1)
for filename in args:
print_("--", filename)
print_(u"--", filename)
try:
print_("-", File(filename).pprint())
print_(u"-", File(filename).pprint())
except AttributeError:
print_("- Unknown file type")
print_(u"- Unknown file type")
except Exception as err:
print_(text_type(err))
print_("")
print_(str(err))
print_(u"")
def entry_point():

4
lib/mutagen/_tools/mutagen_pony.py Executable file → Normal file
View File

@@ -83,7 +83,7 @@ def check_dir(path):
from mutagen.mp3 import MP3
rep = Report(path)
print_("Scanning", path)
print_(u"Scanning", path)
for path, dirs, files in os.walk(path):
files.sort()
for fn in files:
@@ -105,7 +105,7 @@ def check_dir(path):
def main(argv):
if len(argv) == 1:
print_("Usage:", argv[0], "directory ...")
print_(u"Usage:", argv[0], u"directory ...")
else:
for path in argv[1:]:
check_dir(path)

238
lib/mutagen/_util.py Executable file → Normal file
View File

@@ -16,21 +16,48 @@ import sys
import struct
import codecs
import errno
try:
import mmap
except ImportError:
# Google App Engine has no mmap:
# https://github.com/quodlibet/mutagen/issues/286
mmap = None
import decimal
from io import BytesIO
from collections import namedtuple
from contextlib import contextmanager
from functools import wraps
from fnmatch import fnmatchcase
from ._compat import chr_, PY2, iteritems, iterbytes, integer_types, xrange, \
izip, text_type, reraise
_DEFAULT_BUFFER_SIZE = 2 ** 18
def endswith(text, end):
# usefull for paths which can be both, str and bytes
if isinstance(text, str):
if not isinstance(end, str):
end = end.decode("ascii")
else:
if not isinstance(end, bytes):
end = end.encode("ascii")
return text.endswith(end)
def reraise(tp, value, tb):
raise tp(value).with_traceback(tb)
def bchr(x):
return bytes([x])
def iterbytes(b):
return (bytes([v]) for v in b)
def intround(value):
"""Given a float returns a rounded int. Should give the same result on
both Py2/3
"""
return int(decimal.Decimal.from_float(
value).to_integral_value(decimal.ROUND_HALF_EVEN))
def is_fileobj(fileobj):
@@ -39,8 +66,8 @@ def is_fileobj(fileobj):
file object
"""
# open() only handles str/bytes, so we can be strict
return not isinstance(fileobj, (text_type, bytes))
return not (isinstance(fileobj, (str, bytes)) or
hasattr(fileobj, "__fspath__"))
def verify_fileobj(fileobj, writable=False):
@@ -93,9 +120,9 @@ def fileobj_name(fileobj):
path type, but might be empty or non-existent.
"""
value = getattr(fileobj, "name", "")
if not isinstance(value, (text_type, bytes)):
value = text_type(value)
value = getattr(fileobj, "name", u"")
if not isinstance(value, (str, bytes)):
value = str(value)
return value
@@ -199,6 +226,10 @@ def _openfile(instance, filething, filename, fileobj, writable, create):
if filething is not None:
if is_fileobj(filething):
fileobj = filething
elif hasattr(filething, "__fspath__"):
filename = filething.__fspath__()
if not isinstance(filename, (bytes, str)):
raise TypeError("expected __fspath__() to return a filename")
else:
filename = filething
@@ -214,10 +245,24 @@ def _openfile(instance, filething, filename, fileobj, writable, create):
yield FileThing(fileobj, filename, filename or fileobj_name(fileobj))
elif filename is not None:
verify_filename(filename)
inmemory_fileobj = False
try:
fileobj = open(filename, "rb+" if writable else "rb")
except IOError as e:
if create and e.errno == errno.ENOENT:
if writable and e.errno == errno.EOPNOTSUPP:
# Some file systems (gvfs over fuse) don't support opening
# files read/write. To make things still work read the whole
# file into an in-memory file like object and write it back
# later.
# https://github.com/quodlibet/mutagen/issues/300
try:
with open(filename, "rb") as fileobj:
fileobj = BytesIO(fileobj.read())
except IOError as e2:
raise MutagenError(e2)
inmemory_fileobj = True
elif create and e.errno == errno.ENOENT:
assert writable
try:
fileobj = open(filename, "wb+")
@@ -228,6 +273,15 @@ def _openfile(instance, filething, filename, fileobj, writable, create):
with fileobj as fileobj:
yield FileThing(fileobj, filename, filename)
if inmemory_fileobj:
assert writable
data = fileobj.getvalue()
try:
with open(filename, "wb") as fileobj:
fileobj.write(data)
except IOError as e:
raise MutagenError(e)
else:
raise TypeError("Missing filename or fileobj argument")
@@ -264,9 +318,6 @@ def hashable(cls):
Needs a working __eq__ and __hash__ and will add a __ne__.
"""
# py2
assert "__hash__" in cls.__dict__
# py3
assert cls.__dict__["__hash__"] is not None
assert "__eq__" in cls.__dict__
@@ -302,8 +353,8 @@ def enum(cls):
new_type.__module__ = cls.__module__
map_ = {}
for key, value in iteritems(d):
if key.upper() == key and isinstance(value, integer_types):
for key, value in d.items():
if key.upper() == key and isinstance(value, int):
value_instance = new_type(value)
setattr(new_type, key, value_instance)
map_[value] = key
@@ -351,8 +402,8 @@ def flags(cls):
new_type.__module__ = cls.__module__
map_ = {}
for key, value in iteritems(d):
if key.upper() == key and isinstance(value, integer_types):
for key, value in d.items():
if key.upper() == key and isinstance(value, int):
value_instance = new_type(value)
setattr(new_type, key, value_instance)
map_[value] = key
@@ -360,12 +411,12 @@ def flags(cls):
def str_(self):
value = int(self)
matches = []
for k, v in list(map_.items()):
for k, v in map_.items():
if value & k:
matches.append("%s.%s" % (type(self).__name__, v))
value &= ~k
if value != 0 or not matches:
matches.append(text_type(value))
matches.append(str(value))
return " | ".join(matches)
@@ -395,7 +446,7 @@ class DictMixin(object):
"""
def __iter__(self):
return iter(list(self.keys()))
return iter(self.keys())
def __has_key(self, key):
try:
@@ -405,25 +456,13 @@ class DictMixin(object):
else:
return True
if PY2:
has_key = __has_key
__contains__ = __has_key
if PY2:
iterkeys = lambda self: iter(list(self.keys()))
def values(self):
return [self[k] for k in list(self.keys())]
if PY2:
itervalues = lambda self: iter(list(self.values()))
return [self[k] for k in self.keys()]
def items(self):
return list(zip(list(self.keys()), list(self.values())))
if PY2:
iteritems = lambda s: iter(list(s.items()))
return list(zip(self.keys(), self.values()))
def clear(self):
for key in list(self.keys()):
@@ -443,7 +482,7 @@ class DictMixin(object):
return value
def popitem(self):
for key in list(self.keys()):
for key in self.keys():
break
else:
raise KeyError("dictionary is empty")
@@ -455,7 +494,7 @@ class DictMixin(object):
other = {}
try:
for key, value in list(other.items()):
for key, value in other.items():
self.__setitem__(key, value)
except AttributeError:
for key, value in other:
@@ -475,18 +514,18 @@ class DictMixin(object):
return default
def __repr__(self):
return repr(dict(list(self.items())))
return repr(dict(self.items()))
def __eq__(self, other):
return dict(list(self.items())) == other
return dict(self.items()) == other
def __lt__(self, other):
return dict(list(self.items())) < other
return dict(self.items()) < other
__hash__ = object.__hash__
def __len__(self):
return len(list(self.keys()))
return len(self.keys())
class DictProxy(DictMixin):
@@ -504,7 +543,7 @@ class DictProxy(DictMixin):
del(self.__dict[key])
def keys(self):
return list(self.__dict.keys())
return self.__dict.keys()
def _fill_cdata(cls):
@@ -553,7 +592,7 @@ def _fill_cdata(cls):
funcs["to_%s%s%s" % (prefix, name, esuffix)] = pack
funcs["to_%sint%s%s" % (prefix, bits, esuffix)] = pack
for key, func in iteritems(funcs):
for key, func in funcs.items():
setattr(cls, key, staticmethod(func))
@@ -564,11 +603,10 @@ class cdata(object):
uint32_le(data)/to_uint32_le(num)/uint32_le_from(data, offset=0)
"""
from struct import error
error = error
error = struct.error
bitswap = b''.join(
chr_(sum(((val >> i) & 1) << (7 - i) for i in range(8)))
bchr(sum(((val >> i) & 1) << (7 - i) for i in range(8)))
for val in range(256))
test_bit = staticmethod(lambda value, n: bool((value >> n) & 1))
@@ -598,7 +636,7 @@ def get_size(fileobj):
def read_full(fileobj, size):
"""Like fileobj.read but raises IOError if no all requested data is
"""Like fileobj.read but raises IOError if not all requested data is
returned.
If you want to distinguish IOError and the EOS case, better handle
@@ -645,65 +683,7 @@ def seek_end(fileobj, offset):
fileobj.seek(-offset, 2)
def mmap_move(fileobj, dest, src, count):
"""Mmaps the file object if possible and moves 'count' data
from 'src' to 'dest'. All data has to be inside the file size
(enlarging the file through this function isn't possible)
Will adjust the file offset.
Args:
fileobj (fileobj)
dest (int): The destination offset
src (int): The source offset
count (int) The amount of data to move
Raises:
mmap.error: In case move failed
IOError: In case an operation on the fileobj fails
ValueError: In case invalid parameters were given
"""
assert mmap is not None, "no mmap support"
if dest < 0 or src < 0 or count < 0:
raise ValueError("Invalid parameters")
try:
fileno = fileobj.fileno()
except (AttributeError, IOError):
raise mmap.error(
"File object does not expose/support a file descriptor")
fileobj.seek(0, 2)
filesize = fileobj.tell()
length = max(dest, src) + count
if length > filesize:
raise ValueError("Not in file size boundary")
offset = ((min(dest, src) // mmap.ALLOCATIONGRANULARITY) *
mmap.ALLOCATIONGRANULARITY)
assert dest >= offset
assert src >= offset
assert offset % mmap.ALLOCATIONGRANULARITY == 0
# Windows doesn't handle empty mappings, add a fast path here instead
if count == 0:
return
# fast path
if src == dest:
return
fileobj.flush()
file_map = mmap.mmap(fileno, length - offset, offset=offset)
try:
file_map.move(dest - offset, src - offset, count)
finally:
file_map.close()
def resize_file(fobj, diff, BUFFER_SIZE=2 ** 16):
def resize_file(fobj, diff, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE):
"""Resize a file by `diff`.
New space will be filled with zeros.
@@ -740,7 +720,7 @@ def resize_file(fobj, diff, BUFFER_SIZE=2 ** 16):
raise
def fallback_move(fobj, dest, src, count, BUFFER_SIZE=2 ** 16):
def move_bytes(fobj, dest, src, count, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE):
"""Moves data around using read()/write().
Args:
@@ -783,12 +763,11 @@ def fallback_move(fobj, dest, src, count, BUFFER_SIZE=2 ** 16):
fobj.flush()
def insert_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16):
def insert_bytes(fobj, size, offset, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE):
"""Insert size bytes of empty space starting at offset.
fobj must be an open file object, open rb+ or
equivalent. Mutagen tries to use mmap to resize the file, but
falls back to a significantly slower method if mmap fails.
equivalent.
Args:
fobj (fileobj)
@@ -809,22 +788,14 @@ def insert_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16):
raise ValueError
resize_file(fobj, size, BUFFER_SIZE)
if mmap is not None:
try:
mmap_move(fobj, offset + size, offset, movesize)
except mmap.error:
fallback_move(fobj, offset + size, offset, movesize, BUFFER_SIZE)
else:
fallback_move(fobj, offset + size, offset, movesize, BUFFER_SIZE)
move_bytes(fobj, offset + size, offset, movesize, BUFFER_SIZE)
def delete_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16):
def delete_bytes(fobj, size, offset, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE):
"""Delete size bytes of empty space starting at offset.
fobj must be an open file object, open rb+ or
equivalent. Mutagen tries to use mmap to resize the file, but
falls back to a significantly slower method if mmap fails.
equivalent.
Args:
fobj (fileobj)
@@ -844,14 +815,7 @@ def delete_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16):
if movesize < 0:
raise ValueError
if mmap is not None:
try:
mmap_move(fobj, offset, offset + size, movesize)
except mmap.error:
fallback_move(fobj, offset, offset + size, movesize, BUFFER_SIZE)
else:
fallback_move(fobj, offset, offset + size, movesize, BUFFER_SIZE)
move_bytes(fobj, offset, offset + size, movesize, BUFFER_SIZE)
resize_file(fobj, -size, BUFFER_SIZE)
@@ -895,7 +859,7 @@ def dict_match(d, key, default=None):
if key in d and "[" not in key:
return d[key]
else:
for pattern, value in iteritems(d):
for pattern, value in d.items():
if fnmatchcase(key, pattern):
return value
return default
@@ -976,15 +940,15 @@ def decode_terminated(data, encoding, strict=True):
r = []
for i, b in enumerate(iterbytes(data)):
c = decoder.decode(b)
if c == "\x00":
return "".join(r), data[i + 1:]
if c == u"\x00":
return u"".join(r), data[i + 1:]
r.append(c)
else:
# make sure the decoder is finished
r.append(decoder.decode(b"", True))
if strict:
raise ValueError("not null terminated")
return "".join(r), b""
return u"".join(r), b""
class BitReaderError(Exception):

51
lib/mutagen/_vorbis.py Executable file → Normal file
View File

@@ -17,10 +17,10 @@ The specification is at http://www.xiph.org/vorbis/doc/v-comment.html.
"""
import sys
from io import BytesIO
import mutagen
from ._compat import reraise, BytesIO, text_type, xrange, PY3, PY2
from mutagen._util import DictMixin, cdata, MutagenError
from mutagen._util import DictMixin, cdata, MutagenError, reraise
def is_valid_key(key):
@@ -32,7 +32,7 @@ def is_valid_key(key):
Takes str/unicode in Python 2, unicode in Python 3
"""
if PY3 and isinstance(key, bytes):
if isinstance(key, bytes):
raise TypeError("needs to be str not bytes")
for c in key:
@@ -71,7 +71,7 @@ class VComment(mutagen.Tags, list):
vendor (text): the stream 'vendor' (i.e. writer); default 'Mutagen'
"""
vendor = "Mutagen " + mutagen.version_string
vendor = u"Mutagen " + mutagen.version_string
def __init__(self, data=None, *args, **kwargs):
self._size = 0
@@ -116,7 +116,7 @@ class VComment(mutagen.Tags, list):
if errors == "ignore":
continue
elif errors == "replace":
tag, value = "unknown%d" % i, string
tag, value = u"unknown%d" % i, string
else:
reraise(VorbisEncodingError, err, sys.exc_info()[2])
try:
@@ -124,9 +124,7 @@ class VComment(mutagen.Tags, list):
except UnicodeEncodeError:
raise VorbisEncodingError("invalid tag name %r" % tag)
else:
# string keys in py3k
if PY3:
tag = tag.decode("ascii")
tag = tag.decode("ascii")
if is_valid_key(tag):
self.append((tag, value))
@@ -145,30 +143,19 @@ class VComment(mutagen.Tags, list):
In Python 3 all keys and values have to be a string.
"""
if not isinstance(self.vendor, text_type):
if PY3:
raise ValueError("vendor needs to be str")
try:
self.vendor.decode('utf-8')
except UnicodeDecodeError:
raise ValueError
if not isinstance(self.vendor, str):
raise ValueError("vendor needs to be str")
for key, value in self:
try:
if not is_valid_key(key):
raise ValueError
raise ValueError("%r is not a valid key" % key)
except TypeError:
raise ValueError("%r is not a valid key" % key)
if not isinstance(value, text_type):
if PY3:
raise ValueError("%r needs to be str" % key)
try:
value.decode("utf-8")
except:
raise ValueError("%r is not a valid value" % value)
if not isinstance(value, str):
err = "%r needs to be str for key %r" % (value, key)
raise ValueError(err)
return True
@@ -213,12 +200,12 @@ class VComment(mutagen.Tags, list):
def pprint(self):
def _decode(value):
if not isinstance(value, text_type):
if not isinstance(value, str):
return value.decode('utf-8', 'replace')
return value
tags = ["%s=%s" % (_decode(k), _decode(v)) for k, v in self]
return "\n".join(tags)
tags = [u"%s=%s" % (_decode(k), _decode(v)) for k, v in self]
return u"\n".join(tags)
class VCommentDict(VComment, DictMixin):
@@ -242,7 +229,6 @@ class VCommentDict(VComment, DictMixin):
work.
"""
# PY3 only
if isinstance(key, slice):
return VComment.__getitem__(self, key)
@@ -260,7 +246,6 @@ class VCommentDict(VComment, DictMixin):
def __delitem__(self, key):
"""Delete all values associated with the key."""
# PY3 only
if isinstance(key, slice):
return VComment.__delitem__(self, key)
@@ -296,7 +281,6 @@ class VCommentDict(VComment, DictMixin):
string.
"""
# PY3 only
if isinstance(key, slice):
return VComment.__setitem__(self, key, values)
@@ -310,9 +294,6 @@ class VCommentDict(VComment, DictMixin):
except KeyError:
pass
if PY2:
key = key.encode('ascii')
for value in values:
self.append((key, value))
@@ -324,4 +305,4 @@ class VCommentDict(VComment, DictMixin):
def as_dict(self):
"""Return a copy of the comment data in a real dict."""
return dict([(key, self[key]) for key in list(self.keys())])
return dict([(key, self[key]) for key in self.keys()])

10
lib/mutagen/aac.py Executable file → Normal file
View File

@@ -15,9 +15,8 @@
from mutagen import StreamInfo
from mutagen._file import FileType
from mutagen._util import BitReader, BitReaderError, MutagenError, loadfile, \
convert_error
convert_error, endswith
from mutagen.id3._util import BitPaddedInt
from mutagen._compat import endswith, xrange
_FREQS = [
@@ -375,10 +374,13 @@ class AACInfo(StreamInfo):
fileobj.seek(0, 2)
stream_size = fileobj.tell() - (offset + s.offset)
# approx
self.length = float(s.samples * stream_size) / (s.size * s.frequency)
self.length = 0.0
if s.frequency != 0:
self.length = \
float(s.samples * stream_size) / (s.size * s.frequency)
def pprint(self):
return "AAC (%s), %d Hz, %.2f seconds, %d channel(s), %d bps" % (
return u"AAC (%s), %d Hz, %.2f seconds, %d channel(s), %d bps" % (
self._type, self.sample_rate, self.length, self.channels,
self.bitrate)

330
lib/mutagen/ac3.py Normal file
View File

@@ -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._file import FileType
from mutagen._util import (
BitReader,
BitReaderError,
MutagenError,
convert_error,
enum,
loadfile,
endswith,
)
@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

286
lib/mutagen/aiff.py Executable file → Normal file
View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014 Evan Purkhiser
# 2014 Ben Ockmore
# 2019-2020 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
@@ -9,26 +10,30 @@
"""AIFF audio stream information and tags."""
import sys
import struct
from struct import pack
from ._compat import endswith, text_type, reraise
from mutagen import StreamInfo, FileType
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._iff import (
IffChunk,
IffContainerChunkMixin,
IffFile,
IffID3,
InvalidChunk,
error as IffError,
)
from mutagen._util import (
convert_error,
loadfile,
endswith,
)
__all__ = ["AIFF", "Open", "delete"]
class error(MutagenError):
pass
class InvalidChunk(error):
class error(IffError):
pass
@@ -36,14 +41,10 @@ class InvalidChunk(error):
_HUGE_VAL = 1.79769313486231e+308
def is_valid_chunk_id(id):
assert isinstance(id, text_type)
def read_float(data):
"""Raises OverflowError"""
return ((len(id) <= 4) and (min(id) >= ' ') and
(max(id) <= '~'))
def read_float(data): # 10 bytes
assert len(data) == 10
expon, himant, lomant = struct.unpack('>hLL', data)
sign = 1
if expon < 0:
@@ -52,168 +53,70 @@ def read_float(data): # 10 bytes
if expon == himant == lomant == 0:
f = 0.0
elif expon == 0x7FFF:
f = _HUGE_VAL
raise OverflowError("inf and nan not supported")
else:
expon = expon - 16383
# this can raise OverflowError too
f = (himant * 0x100000000 + lomant) * pow(2.0, expon - 63)
return sign * f
class IFFChunk(object):
class AIFFChunk(IffChunk):
"""Representation of a single IFF chunk"""
# Chunk headers are 8 bytes long (4 for ID and 4 for the size)
HEADER_SIZE = 8
@classmethod
def parse_header(cls, header):
return struct.unpack('>4sI', header)
def __init__(self, fileobj, parent_chunk=None):
self.__fileobj = fileobj
self.parent_chunk = parent_chunk
self.offset = fileobj.tell()
@classmethod
def get_class(cls, id):
if id == 'FORM':
return AIFFFormChunk
else:
return cls
header = fileobj.read(self.HEADER_SIZE)
if len(header) < self.HEADER_SIZE:
raise InvalidChunk()
def write_new_header(self, id_, size):
self._fileobj.write(pack('>4sI', id_, size))
self.id, self.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
self.data_offset = fileobj.tell()
def read(self):
"""Read the chunks data"""
self.__fileobj.seek(self.data_offset)
return self.__fileobj.read(self.data_size)
def write(self, data):
"""Write the chunk data"""
if len(data) > self.data_size:
raise ValueError
self.__fileobj.seek(self.data_offset)
self.__fileobj.write(data)
def delete(self):
"""Removes the chunk from the file"""
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)
def _update_size(self, data_size):
"""Update the size of the chunk"""
self.__fileobj.seek(self.offset + 4)
self.__fileobj.write(pack('>I', data_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
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)
def write_size(self):
self._fileobj.write(pack('>I', self.data_size))
class IFFFile(object):
"""Representation of a IFF file"""
class AIFFFormChunk(AIFFChunk, IffContainerChunkMixin):
"""The AIFF root chunk."""
def parse_next_subchunk(self):
return AIFFChunk.parse(self._fileobj, self)
def __init__(self, fileobj, id, data_size, parent_chunk):
if id != u'FORM':
raise InvalidChunk('Expected FORM chunk, got %s' % id)
AIFFChunk.__init__(self, fileobj, id, data_size, parent_chunk)
self.init_container()
class AIFFFile(IffFile):
"""Representation of a AIFF 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['FORM'] = IFFChunk(fileobj)
super().__init__(AIFFChunk, 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['FORM'])
except InvalidChunk:
break
self.__chunks[chunk.id.strip()] = chunk
# 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 FORM chunk, got %s"
% self.root.id)
def __contains__(self, id_):
"""Check if the IFF file contains a specific chunk"""
assert isinstance(id_, text_type)
if not is_valid_chunk_id(id_):
raise KeyError("AIFF key must be four ASCII characters.")
return id_ in self.__chunks
if id_ == 'FORM': # For backwards compatibility
return True
return super().__contains__(id_)
def __getitem__(self, id_):
"""Get a chunk from the IFF file"""
assert isinstance(id_, text_type)
if not is_valid_chunk_id(id_):
raise KeyError("AIFF key must be four ASCII characters.")
try:
return self.__chunks[id_]
except KeyError:
raise KeyError(
"%r has no %r chunk" % (self.__fileobj, id_))
def __delitem__(self, id_):
"""Remove a chunk from the IFF file"""
assert isinstance(id_, text_type)
if not is_valid_chunk_id(id_):
raise KeyError("AIFF key must be four ASCII characters.")
self.__chunks.pop(id_).delete()
def insert_chunk(self, id_):
"""Insert a new chunk at the end of the IFF file"""
assert isinstance(id_, text_type)
if not is_valid_chunk_id(id_):
raise KeyError("AIFF key must be four ASCII characters.")
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['FORM'])
self['FORM']._update_size(self['FORM'].data_size + chunk.size)
self.__chunks[id_] = chunk
self.__next_offset = chunk.offset + chunk.size
if id_ == 'FORM': # For backwards compatibility
return self.root
return super().__getitem__(id_)
class AIFFInfo(StreamInfo):
@@ -228,7 +131,7 @@ class AIFFInfo(StreamInfo):
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
@@ -240,9 +143,9 @@ class AIFFInfo(StreamInfo):
def __init__(self, fileobj):
"""Raises error"""
iff = IFFFile(fileobj)
iff = AIFFFile(fileobj)
try:
common_chunk = iff['COMM']
common_chunk = iff[u'COMM']
except KeyError as e:
raise error(str(e))
@@ -253,61 +156,30 @@ class AIFFInfo(StreamInfo):
info = struct.unpack('>hLh10s', data[:18])
channels, frame_count, sample_size, sample_rate = info
self.sample_rate = int(read_float(sample_rate))
self.sample_size = sample_size
try:
self.sample_rate = int(read_float(sample_rate))
except OverflowError:
raise error("Invalid sample rate")
if self.sample_rate < 0:
raise error("Invalid sample rate")
if self.sample_rate != 0:
self.length = frame_count / float(self.sample_rate)
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)
def pprint(self):
return "%d channel AIFF @ %d bps, %s Hz, %.2f seconds" % (
return u"%d channel AIFF @ %d bps, %s Hz, %.2f seconds" % (
self.channels, self.bitrate, self.sample_rate, self.length)
class _IFFID3(ID3):
class _IFFID3(IffID3):
"""A AIFF file with ID3v2 tags"""
def _pre_load_header(self, fileobj):
try:
fileobj.seek(IFFFile(fileobj)['ID3'].data_offset)
except (InvalidChunk, KeyError):
raise ID3NoHeaderError("No ID3 chunk")
@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething, v2_version=4, v23_sep='/', padding=None):
"""Save ID3v2 data to the AIFF file"""
fileobj = filething.fileobj
iff_file = IFFFile(fileobj)
if 'ID3' not in iff_file:
iff_file.insert_chunk('ID3')
chunk = iff_file['ID3']
try:
data = self._prepare_data(
fileobj, chunk.data_offset, chunk.data_size, v2_version,
v23_sep, padding)
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.write(data)
@loadfile(writable=True)
def delete(self, filething):
"""Completely removes the ID3 chunk from the AIFF file"""
delete(filething)
self.clear()
def _load_file(self, fileobj):
return AIFFFile(fileobj)
@convert_error(IOError, error)
@@ -316,7 +188,7 @@ def delete(filething):
"""Completely removes the ID3 chunk from the AIFF file"""
try:
del IFFFile(filething.fileobj)['ID3']
del AIFFFile(filething.fileobj)[u'ID3']
except KeyError:
pass

116
lib/mutagen/apev2.py Executable file → Normal file
View File

@@ -32,29 +32,22 @@ __all__ = ["APEv2", "APEv2File", "Open", "delete"]
import sys
import struct
from io import BytesIO
from collections.abc import MutableSequence
from ._compat import (cBytesIO, PY3, text_type, PY2, reraise, swap_to_string,
xrange)
from mutagen import Metadata, FileType, StreamInfo
from mutagen._util import DictMixin, cdata, delete_bytes, total_ordering, \
MutagenError, loadfile, convert_error, seek_end, get_size
MutagenError, loadfile, convert_error, seek_end, get_size, reraise
def is_valid_apev2_key(key):
if not isinstance(key, text_type):
if PY3:
raise TypeError("APEv2 key must be str")
try:
key = key.decode('ascii')
except UnicodeDecodeError:
return False
if not isinstance(key, str):
raise TypeError("APEv2 key must be str")
# PY26 - Change to set literal syntax (since set is faster than list here)
return ((2 <= len(key) <= 255) and (min(key) >= ' ') and
(max(key) <= '~') and
(key not in ["OggS", "TAG", "ID3", "MP+"]))
return ((2 <= len(key) <= 255) and (min(key) >= u' ') and
(max(key) <= u'~') and
(key not in [u"OggS", u"TAG", u"ID3", u"MP+"]))
# There are three different kinds of APE tag values.
# "0: Item contains text information coded in UTF-8
@@ -263,7 +256,7 @@ class _CIDictProxy(DictMixin):
del(self.__dict[lower])
def keys(self):
return [self.__casemap.get(key, key) for key in list(self.__dict.keys())]
return [self.__casemap.get(key, key) for key in self.__dict.keys()]
class APEv2(_CIDictProxy, Metadata):
@@ -280,7 +273,7 @@ class APEv2(_CIDictProxy, Metadata):
"""Return tag key=value pairs in a human-readable format."""
items = sorted(self.items())
return "\n".join("%s=%s" % (k, v.pprint()) for k, v in items)
return u"\n".join(u"%s=%s" % (k, v.pprint()) for k, v in items)
@convert_error(IOError, error)
@loadfile()
@@ -301,7 +294,7 @@ class APEv2(_CIDictProxy, Metadata):
def __parse_tag(self, tag, count):
"""Raises IOError and APEBadItemError"""
fileobj = cBytesIO(tag)
fileobj = BytesIO(tag)
for i in range(count):
tag_data = fileobj.read(8)
@@ -330,11 +323,10 @@ class APEv2(_CIDictProxy, Metadata):
if key[-1:] == b"\x00":
key = key[:-1]
if PY3:
try:
key = key.decode("ascii")
except UnicodeError as err:
reraise(APEBadItemError, err, sys.exc_info()[2])
try:
key = key.decode("ascii")
except UnicodeError as err:
reraise(APEBadItemError, err, sys.exc_info()[2])
value = fileobj.read(size)
if len(value) != size:
raise APEBadItemError
@@ -346,16 +338,12 @@ class APEv2(_CIDictProxy, Metadata):
def __getitem__(self, key):
if not is_valid_apev2_key(key):
raise KeyError("%r is not a valid APEv2 key" % key)
if PY2:
key = key.encode('ascii')
return super(APEv2, self).__getitem__(key)
def __delitem__(self, key):
if not is_valid_apev2_key(key):
raise KeyError("%r is not a valid APEv2 key" % key)
if PY2:
key = key.encode('ascii')
super(APEv2, self).__delitem__(key)
@@ -383,43 +371,28 @@ class APEv2(_CIDictProxy, Metadata):
if not is_valid_apev2_key(key):
raise KeyError("%r is not a valid APEv2 key" % key)
if PY2:
key = key.encode('ascii')
if not isinstance(value, _APEValue):
# let's guess at the content if we're not already a value...
if isinstance(value, text_type):
if isinstance(value, str):
# unicode? we've got to be text.
value = APEValue(value, TEXT)
elif isinstance(value, list):
items = []
for v in value:
if not isinstance(v, text_type):
if PY3:
raise TypeError("item in list not str")
v = v.decode("utf-8")
if not isinstance(v, str):
raise TypeError("item in list not str")
items.append(v)
# list? text.
value = APEValue("\0".join(items), TEXT)
value = APEValue(u"\0".join(items), TEXT)
else:
if PY3:
value = APEValue(value, BINARY)
else:
try:
value.decode("utf-8")
except UnicodeError:
# invalid UTF8 text, probably binary
value = APEValue(value, BINARY)
else:
# valid UTF8, probably text
value = APEValue(value, TEXT)
value = APEValue(value, BINARY)
super(APEv2, self).__setitem__(key, value)
@convert_error(IOError, error)
@loadfile(writable=True, create=True)
def save(self, filething):
def save(self, filething=None):
"""Save changes to a file.
If no filename is given, the one most recently loaded is used.
@@ -441,7 +414,7 @@ class APEv2(_CIDictProxy, Metadata):
fileobj.seek(0, 2)
tags = []
for key, value in list(self.items()):
for key, value in self.items():
# Packed format for an item:
# 4B: Value length
# 4B: Value type
@@ -459,7 +432,7 @@ class APEv2(_CIDictProxy, Metadata):
# "APE tags items should be sorted ascending by size... This is
# not a MUST, but STRONGLY recommended. Actually the items should
# be sorted by importance/byte, but this is not feasible."
tags.sort(key=len)
tags.sort(key=lambda tag: (len(tag), tag))
num_tags = len(tags)
tags = b"".join(tags)
@@ -481,7 +454,7 @@ class APEv2(_CIDictProxy, Metadata):
@convert_error(IOError, error)
@loadfile(writable=True)
def delete(self, filething):
def delete(self, filething=None):
"""Remove tags from a file."""
fileobj = filething.fileobj
@@ -578,7 +551,6 @@ class _APEValue(object):
return "%s(%r, %d)" % (type(self).__name__, self.value, self.kind)
@swap_to_string
@total_ordering
class _APEUtf8Value(_APEValue):
@@ -589,11 +561,8 @@ class _APEUtf8Value(_APEValue):
reraise(APEBadItemError, e, sys.exc_info()[2])
def _validate(self, value):
if not isinstance(value, text_type):
if PY3:
raise TypeError("value not str")
else:
value = value.decode("utf-8")
if not isinstance(value, str):
raise TypeError("value not str")
return value
def _write(self):
@@ -627,46 +596,39 @@ class APETextValue(_APEUtf8Value, MutableSequence):
def __iter__(self):
"""Iterate over the strings of the value (not the characters)"""
return iter(self.value.split("\0"))
return iter(self.value.split(u"\0"))
def __getitem__(self, index):
return self.value.split("\0")[index]
return self.value.split(u"\0")[index]
def __len__(self):
return self.value.count("\0") + 1
return self.value.count(u"\0") + 1
def __setitem__(self, index, value):
if not isinstance(value, text_type):
if PY3:
raise TypeError("value not str")
else:
value = value.decode("utf-8")
if not isinstance(value, str):
raise TypeError("value not str")
values = list(self)
values[index] = value
self.value = "\0".join(values)
self.value = u"\0".join(values)
def insert(self, index, value):
if not isinstance(value, text_type):
if PY3:
raise TypeError("value not str")
else:
value = value.decode("utf-8")
if not isinstance(value, str):
raise TypeError("value not str")
values = list(self)
values.insert(index, value)
self.value = "\0".join(values)
self.value = u"\0".join(values)
def __delitem__(self, index):
values = list(self)
del values[index]
self.value = "\0".join(values)
self.value = u"\0".join(values)
def pprint(self):
return " / ".join(self)
return u" / ".join(self)
@swap_to_string
@total_ordering
class APEBinaryValue(_APEValue):
"""An APEv2 binary value."""
@@ -697,7 +659,7 @@ class APEBinaryValue(_APEValue):
return self.value < other
def pprint(self):
return "[%d bytes]" % len(self)
return u"[%d bytes]" % len(self)
class APEExtValue(_APEUtf8Value):
@@ -709,7 +671,7 @@ class APEExtValue(_APEUtf8Value):
kind = EXTERNAL
def pprint(self):
return "[External] %s" % self.value
return u"[External] %s" % self.value
class APEv2File(FileType):
@@ -731,7 +693,7 @@ class APEv2File(FileType):
@staticmethod
def pprint():
return "Unknown format with APEv2 tag."
return u"Unknown format with APEv2 tag."
@loadfile()
def load(self, filething):

34
lib/mutagen/asf/__init__.py Executable file → Normal file
View File

@@ -13,7 +13,6 @@ __all__ = ["ASF", "Open"]
from mutagen import FileType, Tags, StreamInfo
from mutagen._util import resize_bytes, DictMixin, loadfile, convert_error
from mutagen._compat import string_types, long_, PY3, izip
from ._util import error, ASFError, ASFHeaderError
from ._objects import HeaderObject, MetadataLibraryObject, MetadataObject, \
@@ -24,7 +23,7 @@ from ._attrs import ASFGUIDAttribute, ASFWordAttribute, ASFQWordAttribute, \
ASFUnicodeAttribute, ASFBaseAttribute, ASFValue
# pyflakes
# flake8
error, ASFError, ASFHeaderError, ASFValue
@@ -51,26 +50,26 @@ class ASFInfo(StreamInfo):
sample_rate = 0
bitrate = 0
channels = 0
codec_type = ""
codec_name = ""
codec_description = ""
codec_type = u""
codec_name = u""
codec_description = u""
def __init__(self):
self.length = 0.0
self.sample_rate = 0
self.bitrate = 0
self.channels = 0
self.codec_type = ""
self.codec_name = ""
self.codec_description = ""
self.codec_type = u""
self.codec_name = u""
self.codec_description = u""
def pprint(self):
"""Returns:
text: a stream information text summary
"""
s = "ASF (%s) %d bps, %s Hz, %d channels, %.2f seconds" % (
self.codec_type or self.codec_name or "???", self.bitrate,
s = u"ASF (%s) %d bps, %s Hz, %d channels, %.2f seconds" % (
self.codec_type or self.codec_name or u"???", self.bitrate,
self.sample_rate, self.channels, self.length)
return s
@@ -89,7 +88,6 @@ class ASFTags(list, DictMixin, Tags):
"""
# PY3 only
if isinstance(key, slice):
return list.__getitem__(self, key)
@@ -102,7 +100,6 @@ class ASFTags(list, DictMixin, Tags):
def __delitem__(self, key):
"""Delete all values associated with the key."""
# PY3 only
if isinstance(key, slice):
return list.__delitem__(self, key)
@@ -129,7 +126,6 @@ class ASFTags(list, DictMixin, Tags):
string.
"""
# PY3 only
if isinstance(key, slice):
return list.__setitem__(self, key, values)
@@ -139,16 +135,14 @@ class ASFTags(list, DictMixin, Tags):
to_append = []
for value in values:
if not isinstance(value, ASFBaseAttribute):
if isinstance(value, string_types):
if isinstance(value, str):
value = ASFUnicodeAttribute(value)
elif PY3 and isinstance(value, bytes):
elif isinstance(value, bytes):
value = ASFByteArrayAttribute(value)
elif isinstance(value, bool):
value = ASFBoolAttribute(value)
elif isinstance(value, int):
value = ASFDWordAttribute(value)
elif isinstance(value, long_):
value = ASFQWordAttribute(value)
else:
raise TypeError("Invalid type %r" % type(value))
to_append.append((key, value))
@@ -252,14 +246,14 @@ class ASF(FileType):
@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething, padding=None):
def save(self, filething=None, padding=None):
"""save(filething=None, padding=None)
Save tag changes back to the loaded file.
Args:
filething (filething)
padding (PaddingFunction)
padding (:obj:`mutagen.PaddingFunction`)
Raises:
mutagen.MutagenError
"""
@@ -319,7 +313,7 @@ class ASF(FileType):
raise ASFError
@loadfile(writable=True)
def delete(self, filething):
def delete(self, filething=None):
"""delete(filething=None)
Args:

39
lib/mutagen/asf/_attrs.py Executable file → Normal file
View File

@@ -10,8 +10,7 @@
import sys
import struct
from mutagen._compat import swap_to_string, text_type, PY2, reraise
from mutagen._util import total_ordering
from mutagen._util import total_ordering, reraise
from ._util import ASFError
@@ -36,7 +35,7 @@ class ASFBaseAttribute(object):
stream=None, **kwargs):
self.language = language
self.stream = stream
if data:
if data is not None:
self.value = self.parse(data, **kwargs)
else:
if value is None:
@@ -103,7 +102,6 @@ class ASFBaseAttribute(object):
@ASFBaseAttribute._register
@swap_to_string
@total_ordering
class ASFUnicodeAttribute(ASFBaseAttribute):
"""Unicode string attribute.
@@ -122,11 +120,8 @@ class ASFUnicodeAttribute(ASFBaseAttribute):
reraise(ASFError, e, sys.exc_info()[2])
def _validate(self, value):
if not isinstance(value, text_type):
if PY2:
return value.decode("utf-8")
else:
raise TypeError("%r not str" % value)
if not isinstance(value, str):
raise TypeError("%r not str" % value)
return value
def _render(self):
@@ -142,16 +137,15 @@ class ASFUnicodeAttribute(ASFBaseAttribute):
return self.value
def __eq__(self, other):
return text_type(self) == other
return str(self) == other
def __lt__(self, other):
return text_type(self) < other
return str(self) < other
__hash__ = ASFBaseAttribute.__hash__
@ASFBaseAttribute._register
@swap_to_string
@total_ordering
class ASFByteArrayAttribute(ASFBaseAttribute):
"""Byte array attribute.
@@ -194,7 +188,6 @@ class ASFByteArrayAttribute(ASFBaseAttribute):
@ASFBaseAttribute._register
@swap_to_string
@total_ordering
class ASFBoolAttribute(ASFBaseAttribute):
"""Bool attribute.
@@ -228,10 +221,10 @@ class ASFBoolAttribute(ASFBaseAttribute):
return bool(self.value)
def __bytes__(self):
return text_type(self.value).encode('utf-8')
return str(self.value).encode('utf-8')
def __str__(self):
return text_type(self.value)
return str(self.value)
def __eq__(self, other):
return bool(self.value) == other
@@ -243,7 +236,6 @@ class ASFBoolAttribute(ASFBaseAttribute):
@ASFBaseAttribute._register
@swap_to_string
@total_ordering
class ASFDWordAttribute(ASFBaseAttribute):
"""DWORD attribute.
@@ -274,10 +266,10 @@ class ASFDWordAttribute(ASFBaseAttribute):
return self.value
def __bytes__(self):
return text_type(self.value).encode('utf-8')
return str(self.value).encode('utf-8')
def __str__(self):
return text_type(self.value)
return str(self.value)
def __eq__(self, other):
return int(self.value) == other
@@ -289,7 +281,6 @@ class ASFDWordAttribute(ASFBaseAttribute):
@ASFBaseAttribute._register
@swap_to_string
@total_ordering
class ASFQWordAttribute(ASFBaseAttribute):
"""QWORD attribute.
@@ -320,10 +311,10 @@ class ASFQWordAttribute(ASFBaseAttribute):
return self.value
def __bytes__(self):
return text_type(self.value).encode('utf-8')
return str(self.value).encode('utf-8')
def __str__(self):
return text_type(self.value)
return str(self.value)
def __eq__(self, other):
return int(self.value) == other
@@ -335,7 +326,6 @@ class ASFQWordAttribute(ASFBaseAttribute):
@ASFBaseAttribute._register
@swap_to_string
@total_ordering
class ASFWordAttribute(ASFBaseAttribute):
"""WORD attribute.
@@ -366,10 +356,10 @@ class ASFWordAttribute(ASFBaseAttribute):
return self.value
def __bytes__(self):
return text_type(self.value).encode('utf-8')
return str(self.value).encode('utf-8')
def __str__(self):
return text_type(self.value)
return str(self.value)
def __eq__(self, other):
return int(self.value) == other
@@ -381,7 +371,6 @@ class ASFWordAttribute(ASFBaseAttribute):
@ASFBaseAttribute._register
@swap_to_string
@total_ordering
class ASFGUIDAttribute(ASFBaseAttribute):
"""GUID attribute."""

41
lib/mutagen/asf/_objects.py Executable file → Normal file
View File

@@ -10,7 +10,6 @@
import struct
from mutagen._util import cdata, get_size
from mutagen._compat import text_type, xrange, izip
from mutagen._tags import PaddingInfo
from ._util import guid2bytes, bytes2guid, CODECS, ASFError, ASFHeaderError
@@ -108,13 +107,16 @@ class HeaderObject(BaseObject):
try:
data = fileobj.read(payload_size)
except OverflowError:
except (OverflowError, MemoryError):
# read doesn't take 64bit values
raise ASFHeaderError("invalid header size")
if len(data) != payload_size:
raise ASFHeaderError("truncated")
obj.parse(asf, data)
try:
obj.parse(asf, data)
except struct.error:
raise ASFHeaderError("truncated")
header.objects.append(obj)
return header
@@ -151,7 +153,8 @@ class HeaderObject(BaseObject):
# ask the user for padding adjustments
file_size = get_size(fileobj)
content_size = file_size - available
assert content_size >= 0
if content_size < 0:
raise ASFHeaderError("truncated content")
info = PaddingInfo(available - needed_size, content_size)
# add padding
@@ -180,11 +183,11 @@ class ContentDescriptionObject(BaseObject):
GUID = guid2bytes("75B22633-668E-11CF-A6D9-00AA0062CE6C")
NAMES = [
"Title",
"Author",
"Copyright",
"Description",
"Rating",
u"Title",
u"Author",
u"Copyright",
u"Description",
u"Rating",
]
def parse(self, asf, data):
@@ -195,7 +198,7 @@ class ContentDescriptionObject(BaseObject):
for length in lengths:
end = pos + length
if length > 0:
texts.append(data[pos:end].decode("utf-16-le").strip("\x00"))
texts.append(data[pos:end].decode("utf-16-le").strip(u"\x00"))
else:
texts.append(None)
pos = end
@@ -209,12 +212,12 @@ class ContentDescriptionObject(BaseObject):
def render_text(name):
value = asf.to_content_description.get(name)
if value is not None:
return text_type(value).encode("utf-16-le") + b"\x00\x00"
return str(value).encode("utf-16-le") + b"\x00\x00"
else:
return b""
texts = [render_text(x) for x in self.NAMES]
data = struct.pack("<HHHHH", *list(map(len, texts))) + b"".join(texts)
data = struct.pack("<HHHHH", *map(len, texts)) + b"".join(texts)
return self.GUID + struct.pack("<Q", 24 + len(data)) + data
@@ -242,7 +245,7 @@ class ExtendedContentDescriptionObject(BaseObject):
asf._tags.setdefault(self.GUID, []).append((name, attr))
def render(self, asf):
attrs = list(asf.to_extended_content_description.items())
attrs = asf.to_extended_content_description.items()
data = b"".join(attr.render(name) for (name, attr) in attrs)
data = struct.pack("<QH", 26 + len(data), len(attrs)) + data
return self.GUID + data
@@ -256,6 +259,8 @@ class FilePropertiesObject(BaseObject):
def parse(self, asf, data):
super(FilePropertiesObject, self).parse(asf, data)
if len(data) < 64:
raise ASFError("invalid field property entry")
length, _, preroll = struct.unpack("<QQQ", data[40:64])
# there are files where preroll is larger than length, limit to >= 0
asf.info.length = max((length / 10000000.0) - (preroll / 1000.0), 0.0)
@@ -292,7 +297,7 @@ class CodecListObject(BaseObject):
try:
name = data[offset:next_offset].decode("utf-16-le").strip("\x00")
except UnicodeDecodeError:
name = ""
name = u""
offset = next_offset
units, offset = cdata.uint16_le_from(data, offset)
@@ -300,12 +305,12 @@ class CodecListObject(BaseObject):
try:
desc = data[offset:next_offset].decode("utf-16-le").strip("\x00")
except UnicodeDecodeError:
desc = ""
desc = u""
offset = next_offset
bytes_, offset = cdata.uint16_le_from(data, offset)
next_offset = offset + bytes_
codec = ""
codec = u""
if bytes_ == 2:
codec_id = cdata.uint16_le_from(data, offset)[0]
if codec_id in CODECS:
@@ -377,6 +382,8 @@ class HeaderExtensionObject(BaseObject):
while datapos < datasize:
guid, size = struct.unpack(
"<16sQ", data[22 + datapos:22 + datapos + 24])
if size < 1:
raise ASFHeaderError("invalid size in header extension")
obj = BaseObject._get_object(guid)
obj.parse(asf, data[22 + datapos + 24:22 + datapos + size])
self.objects.append(obj)
@@ -423,7 +430,7 @@ class MetadataObject(BaseObject):
asf._tags.setdefault(self.GUID, []).append((name, attr))
def render(self, asf):
attrs = list(asf.to_metadata.items())
attrs = asf.to_metadata.items()
data = b"".join([attr.render_m(name) for (name, attr) in attrs])
return (self.GUID + struct.pack("<QH", 26 + len(data), len(attrs)) +
data)

522
lib/mutagen/asf/_util.py Executable file → Normal file
View File

@@ -52,265 +52,265 @@ def bytes2guid(s):
# Names from http://windows.microsoft.com/en-za/windows7/c00d10d1-[0-9A-F]{1,4}
CODECS = {
0x0000: "Unknown Wave Format",
0x0001: "Microsoft PCM Format",
0x0002: "Microsoft ADPCM Format",
0x0003: "IEEE Float",
0x0004: "Compaq Computer VSELP",
0x0005: "IBM CVSD",
0x0006: "Microsoft CCITT A-Law",
0x0007: "Microsoft CCITT u-Law",
0x0008: "Microsoft DTS",
0x0009: "Microsoft DRM",
0x000A: "Windows Media Audio 9 Voice",
0x000B: "Windows Media Audio 10 Voice",
0x000C: "OGG Vorbis",
0x000D: "FLAC",
0x000E: "MOT AMR",
0x000F: "Nice Systems IMBE",
0x0010: "OKI ADPCM",
0x0011: "Intel IMA ADPCM",
0x0012: "Videologic MediaSpace ADPCM",
0x0013: "Sierra Semiconductor ADPCM",
0x0014: "Antex Electronics G.723 ADPCM",
0x0015: "DSP Solutions DIGISTD",
0x0016: "DSP Solutions DIGIFIX",
0x0017: "Dialogic OKI ADPCM",
0x0018: "MediaVision ADPCM",
0x0019: "Hewlett-Packard CU codec",
0x001A: "Hewlett-Packard Dynamic Voice",
0x0020: "Yamaha ADPCM",
0x0021: "Speech Compression SONARC",
0x0022: "DSP Group True Speech",
0x0023: "Echo Speech EchoSC1",
0x0024: "Ahead Inc. Audiofile AF36",
0x0025: "Audio Processing Technology APTX",
0x0026: "Ahead Inc. AudioFile AF10",
0x0027: "Aculab Prosody 1612",
0x0028: "Merging Technologies S.A. LRC",
0x0030: "Dolby Labs AC2",
0x0031: "Microsoft GSM 6.10",
0x0032: "Microsoft MSNAudio",
0x0033: "Antex Electronics ADPCME",
0x0034: "Control Resources VQLPC",
0x0035: "DSP Solutions Digireal",
0x0036: "DSP Solutions DigiADPCM",
0x0037: "Control Resources CR10",
0x0038: "Natural MicroSystems VBXADPCM",
0x0039: "Crystal Semiconductor IMA ADPCM",
0x003A: "Echo Speech EchoSC3",
0x003B: "Rockwell ADPCM",
0x003C: "Rockwell DigiTalk",
0x003D: "Xebec Multimedia Solutions",
0x0040: "Antex Electronics G.721 ADPCM",
0x0041: "Antex Electronics G.728 CELP",
0x0042: "Intel G.723",
0x0043: "Intel G.723.1",
0x0044: "Intel G.729 Audio",
0x0045: "Sharp G.726 Audio",
0x0050: "Microsoft MPEG-1",
0x0052: "InSoft RT24",
0x0053: "InSoft PAC",
0x0055: "MP3 - MPEG Layer III",
0x0059: "Lucent G.723",
0x0060: "Cirrus Logic",
0x0061: "ESS Technology ESPCM",
0x0062: "Voxware File-Mode",
0x0063: "Canopus Atrac",
0x0064: "APICOM G.726 ADPCM",
0x0065: "APICOM G.722 ADPCM",
0x0066: "Microsoft DSAT",
0x0067: "Microsoft DSAT Display",
0x0069: "Voxware Byte Aligned",
0x0070: "Voxware AC8",
0x0071: "Voxware AC10",
0x0072: "Voxware AC16",
0x0073: "Voxware AC20",
0x0074: "Voxware RT24 MetaVoice",
0x0075: "Voxware RT29 MetaSound",
0x0076: "Voxware RT29HW",
0x0077: "Voxware VR12",
0x0078: "Voxware VR18",
0x0079: "Voxware TQ40",
0x007A: "Voxware SC3",
0x007B: "Voxware SC3",
0x0080: "Softsound",
0x0081: "Voxware TQ60",
0x0082: "Microsoft MSRT24",
0x0083: "AT&T Labs G.729A",
0x0084: "Motion Pixels MVI MV12",
0x0085: "DataFusion Systems G.726",
0x0086: "DataFusion Systems GSM610",
0x0088: "Iterated Systems ISIAudio",
0x0089: "Onlive",
0x008A: "Multitude FT SX20",
0x008B: "Infocom ITS ACM G.721",
0x008C: "Convedia G.729",
0x008D: "Congruency Audio",
0x0091: "Siemens Business Communications SBC24",
0x0092: "Sonic Foundry Dolby AC3 SPDIF",
0x0093: "MediaSonic G.723",
0x0094: "Aculab Prosody 8KBPS",
0x0097: "ZyXEL ADPCM",
0x0098: "Philips LPCBB",
0x0099: "Studer Professional Audio AG Packed",
0x00A0: "Malden Electronics PHONYTALK",
0x00A1: "Racal Recorder GSM",
0x00A2: "Racal Recorder G720.a",
0x00A3: "Racal Recorder G723.1",
0x00A4: "Racal Recorder Tetra ACELP",
0x00B0: "NEC AAC",
0x00FF: "CoreAAC Audio",
0x0100: "Rhetorex ADPCM",
0x0101: "BeCubed Software IRAT",
0x0111: "Vivo G.723",
0x0112: "Vivo Siren",
0x0120: "Philips CELP",
0x0121: "Philips Grundig",
0x0123: "Digital G.723",
0x0125: "Sanyo ADPCM",
0x0130: "Sipro Lab Telecom ACELP.net",
0x0131: "Sipro Lab Telecom ACELP.4800",
0x0132: "Sipro Lab Telecom ACELP.8V3",
0x0133: "Sipro Lab Telecom ACELP.G.729",
0x0134: "Sipro Lab Telecom ACELP.G.729A",
0x0135: "Sipro Lab Telecom ACELP.KELVIN",
0x0136: "VoiceAge AMR",
0x0140: "Dictaphone G.726 ADPCM",
0x0141: "Dictaphone CELP68",
0x0142: "Dictaphone CELP54",
0x0150: "Qualcomm PUREVOICE",
0x0151: "Qualcomm HALFRATE",
0x0155: "Ring Zero Systems TUBGSM",
0x0160: "Windows Media Audio Standard",
0x0161: "Windows Media Audio 9 Standard",
0x0162: "Windows Media Audio 9 Professional",
0x0163: "Windows Media Audio 9 Lossless",
0x0164: "Windows Media Audio Pro over SPDIF",
0x0170: "Unisys NAP ADPCM",
0x0171: "Unisys NAP ULAW",
0x0172: "Unisys NAP ALAW",
0x0173: "Unisys NAP 16K",
0x0174: "Sycom ACM SYC008",
0x0175: "Sycom ACM SYC701 G725",
0x0176: "Sycom ACM SYC701 CELP54",
0x0177: "Sycom ACM SYC701 CELP68",
0x0178: "Knowledge Adventure ADPCM",
0x0180: "Fraunhofer IIS MPEG-2 AAC",
0x0190: "Digital Theater Systems DTS",
0x0200: "Creative Labs ADPCM",
0x0202: "Creative Labs FastSpeech8",
0x0203: "Creative Labs FastSpeech10",
0x0210: "UHER informatic GmbH ADPCM",
0x0215: "Ulead DV Audio",
0x0216: "Ulead DV Audio",
0x0220: "Quarterdeck",
0x0230: "I-link Worldwide ILINK VC",
0x0240: "Aureal Semiconductor RAW SPORT",
0x0249: "Generic Passthru",
0x0250: "Interactive Products HSX",
0x0251: "Interactive Products RPELP",
0x0260: "Consistent Software CS2",
0x0270: "Sony SCX",
0x0271: "Sony SCY",
0x0272: "Sony ATRAC3",
0x0273: "Sony SPC",
0x0280: "Telum Audio",
0x0281: "Telum IA Audio",
0x0285: "Norcom Voice Systems ADPCM",
0x0300: "Fujitsu TOWNS SND",
0x0350: "Micronas SC4 Speech",
0x0351: "Micronas CELP833",
0x0400: "Brooktree BTV Digital",
0x0401: "Intel Music Coder",
0x0402: "Intel Audio",
0x0450: "QDesign Music",
0x0500: "On2 AVC0 Audio",
0x0501: "On2 AVC1 Audio",
0x0680: "AT&T Labs VME VMPCM",
0x0681: "AT&T Labs TPC",
0x08AE: "ClearJump Lightwave Lossless",
0x1000: "Olivetti GSM",
0x1001: "Olivetti ADPCM",
0x1002: "Olivetti CELP",
0x1003: "Olivetti SBC",
0x1004: "Olivetti OPR",
0x1100: "Lernout & Hauspie",
0x1101: "Lernout & Hauspie CELP",
0x1102: "Lernout & Hauspie SBC8",
0x1103: "Lernout & Hauspie SBC12",
0x1104: "Lernout & Hauspie SBC16",
0x1400: "Norris Communication",
0x1401: "ISIAudio",
0x1500: "AT&T Labs Soundspace Music Compression",
0x1600: "Microsoft MPEG ADTS AAC",
0x1601: "Microsoft MPEG RAW AAC",
0x1608: "Nokia MPEG ADTS AAC",
0x1609: "Nokia MPEG RAW AAC",
0x181C: "VoxWare MetaVoice RT24",
0x1971: "Sonic Foundry Lossless",
0x1979: "Innings Telecom ADPCM",
0x1FC4: "NTCSoft ALF2CD ACM",
0x2000: "Dolby AC3",
0x2001: "DTS",
0x4143: "Divio AAC",
0x4201: "Nokia Adaptive Multi-Rate",
0x4243: "Divio G.726",
0x4261: "ITU-T H.261",
0x4263: "ITU-T H.263",
0x4264: "ITU-T H.264",
0x674F: "Ogg Vorbis Mode 1",
0x6750: "Ogg Vorbis Mode 2",
0x6751: "Ogg Vorbis Mode 3",
0x676F: "Ogg Vorbis Mode 1+",
0x6770: "Ogg Vorbis Mode 2+",
0x6771: "Ogg Vorbis Mode 3+",
0x7000: "3COM NBX Audio",
0x706D: "FAAD AAC Audio",
0x77A1: "True Audio Lossless Audio",
0x7A21: "GSM-AMR CBR 3GPP Audio",
0x7A22: "GSM-AMR VBR 3GPP Audio",
0xA100: "Comverse Infosys G723.1",
0xA101: "Comverse Infosys AVQSBC",
0xA102: "Comverse Infosys SBC",
0xA103: "Symbol Technologies G729a",
0xA104: "VoiceAge AMR WB",
0xA105: "Ingenient Technologies G.726",
0xA106: "ISO/MPEG-4 Advanced Audio Coding (AAC)",
0xA107: "Encore Software Ltd's G.726",
0xA108: "ZOLL Medical Corporation ASAO",
0xA109: "Speex Voice",
0xA10A: "Vianix MASC Speech Compression",
0xA10B: "Windows Media 9 Spectrum Analyzer Output",
0xA10C: "Media Foundation Spectrum Analyzer Output",
0xA10D: "GSM 6.10 (Full-Rate) Speech",
0xA10E: "GSM 6.20 (Half-Rate) Speech",
0xA10F: "GSM 6.60 (Enchanced Full-Rate) Speech",
0xA110: "GSM 6.90 (Adaptive Multi-Rate) Speech",
0xA111: "GSM Adaptive Multi-Rate WideBand Speech",
0xA112: "Polycom G.722",
0xA113: "Polycom G.728",
0xA114: "Polycom G.729a",
0xA115: "Polycom Siren",
0xA116: "Global IP Sound ILBC",
0xA117: "Radio Time Time Shifted Radio",
0xA118: "Nice Systems ACA",
0xA119: "Nice Systems ADPCM",
0xA11A: "Vocord Group ITU-T G.721",
0xA11B: "Vocord Group ITU-T G.726",
0xA11C: "Vocord Group ITU-T G.722.1",
0xA11D: "Vocord Group ITU-T G.728",
0xA11E: "Vocord Group ITU-T G.729",
0xA11F: "Vocord Group ITU-T G.729a",
0xA120: "Vocord Group ITU-T G.723.1",
0xA121: "Vocord Group LBC",
0xA122: "Nice G.728",
0xA123: "France Telecom G.729 ACM Audio",
0xA124: "CODIAN Audio",
0xCC12: "Intel YUV12 Codec",
0xCFCC: "Digital Processing Systems Perception Motion JPEG",
0xD261: "DEC H.261",
0xD263: "DEC H.263",
0xFFFE: "Extensible Wave Format",
0xFFFF: "Unregistered",
0x0000: u"Unknown Wave Format",
0x0001: u"Microsoft PCM Format",
0x0002: u"Microsoft ADPCM Format",
0x0003: u"IEEE Float",
0x0004: u"Compaq Computer VSELP",
0x0005: u"IBM CVSD",
0x0006: u"Microsoft CCITT A-Law",
0x0007: u"Microsoft CCITT u-Law",
0x0008: u"Microsoft DTS",
0x0009: u"Microsoft DRM",
0x000A: u"Windows Media Audio 9 Voice",
0x000B: u"Windows Media Audio 10 Voice",
0x000C: u"OGG Vorbis",
0x000D: u"FLAC",
0x000E: u"MOT AMR",
0x000F: u"Nice Systems IMBE",
0x0010: u"OKI ADPCM",
0x0011: u"Intel IMA ADPCM",
0x0012: u"Videologic MediaSpace ADPCM",
0x0013: u"Sierra Semiconductor ADPCM",
0x0014: u"Antex Electronics G.723 ADPCM",
0x0015: u"DSP Solutions DIGISTD",
0x0016: u"DSP Solutions DIGIFIX",
0x0017: u"Dialogic OKI ADPCM",
0x0018: u"MediaVision ADPCM",
0x0019: u"Hewlett-Packard CU codec",
0x001A: u"Hewlett-Packard Dynamic Voice",
0x0020: u"Yamaha ADPCM",
0x0021: u"Speech Compression SONARC",
0x0022: u"DSP Group True Speech",
0x0023: u"Echo Speech EchoSC1",
0x0024: u"Ahead Inc. Audiofile AF36",
0x0025: u"Audio Processing Technology APTX",
0x0026: u"Ahead Inc. AudioFile AF10",
0x0027: u"Aculab Prosody 1612",
0x0028: u"Merging Technologies S.A. LRC",
0x0030: u"Dolby Labs AC2",
0x0031: u"Microsoft GSM 6.10",
0x0032: u"Microsoft MSNAudio",
0x0033: u"Antex Electronics ADPCME",
0x0034: u"Control Resources VQLPC",
0x0035: u"DSP Solutions Digireal",
0x0036: u"DSP Solutions DigiADPCM",
0x0037: u"Control Resources CR10",
0x0038: u"Natural MicroSystems VBXADPCM",
0x0039: u"Crystal Semiconductor IMA ADPCM",
0x003A: u"Echo Speech EchoSC3",
0x003B: u"Rockwell ADPCM",
0x003C: u"Rockwell DigiTalk",
0x003D: u"Xebec Multimedia Solutions",
0x0040: u"Antex Electronics G.721 ADPCM",
0x0041: u"Antex Electronics G.728 CELP",
0x0042: u"Intel G.723",
0x0043: u"Intel G.723.1",
0x0044: u"Intel G.729 Audio",
0x0045: u"Sharp G.726 Audio",
0x0050: u"Microsoft MPEG-1",
0x0052: u"InSoft RT24",
0x0053: u"InSoft PAC",
0x0055: u"MP3 - MPEG Layer III",
0x0059: u"Lucent G.723",
0x0060: u"Cirrus Logic",
0x0061: u"ESS Technology ESPCM",
0x0062: u"Voxware File-Mode",
0x0063: u"Canopus Atrac",
0x0064: u"APICOM G.726 ADPCM",
0x0065: u"APICOM G.722 ADPCM",
0x0066: u"Microsoft DSAT",
0x0067: u"Microsoft DSAT Display",
0x0069: u"Voxware Byte Aligned",
0x0070: u"Voxware AC8",
0x0071: u"Voxware AC10",
0x0072: u"Voxware AC16",
0x0073: u"Voxware AC20",
0x0074: u"Voxware RT24 MetaVoice",
0x0075: u"Voxware RT29 MetaSound",
0x0076: u"Voxware RT29HW",
0x0077: u"Voxware VR12",
0x0078: u"Voxware VR18",
0x0079: u"Voxware TQ40",
0x007A: u"Voxware SC3",
0x007B: u"Voxware SC3",
0x0080: u"Softsound",
0x0081: u"Voxware TQ60",
0x0082: u"Microsoft MSRT24",
0x0083: u"AT&T Labs G.729A",
0x0084: u"Motion Pixels MVI MV12",
0x0085: u"DataFusion Systems G.726",
0x0086: u"DataFusion Systems GSM610",
0x0088: u"Iterated Systems ISIAudio",
0x0089: u"Onlive",
0x008A: u"Multitude FT SX20",
0x008B: u"Infocom ITS ACM G.721",
0x008C: u"Convedia G.729",
0x008D: u"Congruency Audio",
0x0091: u"Siemens Business Communications SBC24",
0x0092: u"Sonic Foundry Dolby AC3 SPDIF",
0x0093: u"MediaSonic G.723",
0x0094: u"Aculab Prosody 8KBPS",
0x0097: u"ZyXEL ADPCM",
0x0098: u"Philips LPCBB",
0x0099: u"Studer Professional Audio AG Packed",
0x00A0: u"Malden Electronics PHONYTALK",
0x00A1: u"Racal Recorder GSM",
0x00A2: u"Racal Recorder G720.a",
0x00A3: u"Racal Recorder G723.1",
0x00A4: u"Racal Recorder Tetra ACELP",
0x00B0: u"NEC AAC",
0x00FF: u"CoreAAC Audio",
0x0100: u"Rhetorex ADPCM",
0x0101: u"BeCubed Software IRAT",
0x0111: u"Vivo G.723",
0x0112: u"Vivo Siren",
0x0120: u"Philips CELP",
0x0121: u"Philips Grundig",
0x0123: u"Digital G.723",
0x0125: u"Sanyo ADPCM",
0x0130: u"Sipro Lab Telecom ACELP.net",
0x0131: u"Sipro Lab Telecom ACELP.4800",
0x0132: u"Sipro Lab Telecom ACELP.8V3",
0x0133: u"Sipro Lab Telecom ACELP.G.729",
0x0134: u"Sipro Lab Telecom ACELP.G.729A",
0x0135: u"Sipro Lab Telecom ACELP.KELVIN",
0x0136: u"VoiceAge AMR",
0x0140: u"Dictaphone G.726 ADPCM",
0x0141: u"Dictaphone CELP68",
0x0142: u"Dictaphone CELP54",
0x0150: u"Qualcomm PUREVOICE",
0x0151: u"Qualcomm HALFRATE",
0x0155: u"Ring Zero Systems TUBGSM",
0x0160: u"Windows Media Audio Standard",
0x0161: u"Windows Media Audio 9 Standard",
0x0162: u"Windows Media Audio 9 Professional",
0x0163: u"Windows Media Audio 9 Lossless",
0x0164: u"Windows Media Audio Pro over SPDIF",
0x0170: u"Unisys NAP ADPCM",
0x0171: u"Unisys NAP ULAW",
0x0172: u"Unisys NAP ALAW",
0x0173: u"Unisys NAP 16K",
0x0174: u"Sycom ACM SYC008",
0x0175: u"Sycom ACM SYC701 G725",
0x0176: u"Sycom ACM SYC701 CELP54",
0x0177: u"Sycom ACM SYC701 CELP68",
0x0178: u"Knowledge Adventure ADPCM",
0x0180: u"Fraunhofer IIS MPEG-2 AAC",
0x0190: u"Digital Theater Systems DTS",
0x0200: u"Creative Labs ADPCM",
0x0202: u"Creative Labs FastSpeech8",
0x0203: u"Creative Labs FastSpeech10",
0x0210: u"UHER informatic GmbH ADPCM",
0x0215: u"Ulead DV Audio",
0x0216: u"Ulead DV Audio",
0x0220: u"Quarterdeck",
0x0230: u"I-link Worldwide ILINK VC",
0x0240: u"Aureal Semiconductor RAW SPORT",
0x0249: u"Generic Passthru",
0x0250: u"Interactive Products HSX",
0x0251: u"Interactive Products RPELP",
0x0260: u"Consistent Software CS2",
0x0270: u"Sony SCX",
0x0271: u"Sony SCY",
0x0272: u"Sony ATRAC3",
0x0273: u"Sony SPC",
0x0280: u"Telum Audio",
0x0281: u"Telum IA Audio",
0x0285: u"Norcom Voice Systems ADPCM",
0x0300: u"Fujitsu TOWNS SND",
0x0350: u"Micronas SC4 Speech",
0x0351: u"Micronas CELP833",
0x0400: u"Brooktree BTV Digital",
0x0401: u"Intel Music Coder",
0x0402: u"Intel Audio",
0x0450: u"QDesign Music",
0x0500: u"On2 AVC0 Audio",
0x0501: u"On2 AVC1 Audio",
0x0680: u"AT&T Labs VME VMPCM",
0x0681: u"AT&T Labs TPC",
0x08AE: u"ClearJump Lightwave Lossless",
0x1000: u"Olivetti GSM",
0x1001: u"Olivetti ADPCM",
0x1002: u"Olivetti CELP",
0x1003: u"Olivetti SBC",
0x1004: u"Olivetti OPR",
0x1100: u"Lernout & Hauspie",
0x1101: u"Lernout & Hauspie CELP",
0x1102: u"Lernout & Hauspie SBC8",
0x1103: u"Lernout & Hauspie SBC12",
0x1104: u"Lernout & Hauspie SBC16",
0x1400: u"Norris Communication",
0x1401: u"ISIAudio",
0x1500: u"AT&T Labs Soundspace Music Compression",
0x1600: u"Microsoft MPEG ADTS AAC",
0x1601: u"Microsoft MPEG RAW AAC",
0x1608: u"Nokia MPEG ADTS AAC",
0x1609: u"Nokia MPEG RAW AAC",
0x181C: u"VoxWare MetaVoice RT24",
0x1971: u"Sonic Foundry Lossless",
0x1979: u"Innings Telecom ADPCM",
0x1FC4: u"NTCSoft ALF2CD ACM",
0x2000: u"Dolby AC3",
0x2001: u"DTS",
0x4143: u"Divio AAC",
0x4201: u"Nokia Adaptive Multi-Rate",
0x4243: u"Divio G.726",
0x4261: u"ITU-T H.261",
0x4263: u"ITU-T H.263",
0x4264: u"ITU-T H.264",
0x674F: u"Ogg Vorbis Mode 1",
0x6750: u"Ogg Vorbis Mode 2",
0x6751: u"Ogg Vorbis Mode 3",
0x676F: u"Ogg Vorbis Mode 1+",
0x6770: u"Ogg Vorbis Mode 2+",
0x6771: u"Ogg Vorbis Mode 3+",
0x7000: u"3COM NBX Audio",
0x706D: u"FAAD AAC Audio",
0x77A1: u"True Audio Lossless Audio",
0x7A21: u"GSM-AMR CBR 3GPP Audio",
0x7A22: u"GSM-AMR VBR 3GPP Audio",
0xA100: u"Comverse Infosys G723.1",
0xA101: u"Comverse Infosys AVQSBC",
0xA102: u"Comverse Infosys SBC",
0xA103: u"Symbol Technologies G729a",
0xA104: u"VoiceAge AMR WB",
0xA105: u"Ingenient Technologies G.726",
0xA106: u"ISO/MPEG-4 Advanced Audio Coding (AAC)",
0xA107: u"Encore Software Ltd's G.726",
0xA108: u"ZOLL Medical Corporation ASAO",
0xA109: u"Speex Voice",
0xA10A: u"Vianix MASC Speech Compression",
0xA10B: u"Windows Media 9 Spectrum Analyzer Output",
0xA10C: u"Media Foundation Spectrum Analyzer Output",
0xA10D: u"GSM 6.10 (Full-Rate) Speech",
0xA10E: u"GSM 6.20 (Half-Rate) Speech",
0xA10F: u"GSM 6.60 (Enchanced Full-Rate) Speech",
0xA110: u"GSM 6.90 (Adaptive Multi-Rate) Speech",
0xA111: u"GSM Adaptive Multi-Rate WideBand Speech",
0xA112: u"Polycom G.722",
0xA113: u"Polycom G.728",
0xA114: u"Polycom G.729a",
0xA115: u"Polycom Siren",
0xA116: u"Global IP Sound ILBC",
0xA117: u"Radio Time Time Shifted Radio",
0xA118: u"Nice Systems ACA",
0xA119: u"Nice Systems ADPCM",
0xA11A: u"Vocord Group ITU-T G.721",
0xA11B: u"Vocord Group ITU-T G.726",
0xA11C: u"Vocord Group ITU-T G.722.1",
0xA11D: u"Vocord Group ITU-T G.728",
0xA11E: u"Vocord Group ITU-T G.729",
0xA11F: u"Vocord Group ITU-T G.729a",
0xA120: u"Vocord Group ITU-T G.723.1",
0xA121: u"Vocord Group LBC",
0xA122: u"Nice G.728",
0xA123: u"France Telecom G.729 ACM Audio",
0xA124: u"CODIAN Audio",
0xCC12: u"Intel YUV12 Codec",
0xCFCC: u"Digital Processing Systems Perception Motion JPEG",
0xD261: u"DEC H.261",
0xD263: u"DEC H.263",
0xFFFE: u"Extensible Wave Format",
0xFFFF: u"Unregistered",
}

267
lib/mutagen/dsdiff.py Normal file
View File

@@ -0,0 +1,267 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2020 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.
"""DSDIFF audio stream information and tags."""
import struct
from mutagen import StreamInfo
from mutagen._file import FileType
from mutagen._iff import (
IffChunk,
IffContainerChunkMixin,
IffID3,
IffFile,
InvalidChunk,
error as IffError,
)
from mutagen.id3._util import ID3NoHeaderError, error as ID3Error
from mutagen._util import (
convert_error,
loadfile,
endswith,
)
__all__ = ["DSDIFF", "Open", "delete"]
class error(IffError):
pass
# See
# https://dsd-guide.com/sites/default/files/white-papers/DSDIFF_1.5_Spec.pdf
class DSDIFFChunk(IffChunk):
"""Representation of a single DSDIFF chunk"""
HEADER_SIZE = 12
@classmethod
def parse_header(cls, header):
return struct.unpack('>4sQ', header)
@classmethod
def get_class(cls, id):
if id in DSDIFFListChunk.LIST_CHUNK_IDS:
return DSDIFFListChunk
elif id == 'DST':
return DSTChunk
else:
return cls
def write_new_header(self, id_, size):
self._fileobj.write(struct.pack('>4sQ', id_, size))
def write_size(self):
self._fileobj.write(struct.pack('>Q', self.data_size))
class DSDIFFListChunk(DSDIFFChunk, IffContainerChunkMixin):
"""A DSDIFF chunk containing other chunks.
"""
LIST_CHUNK_IDS = ['FRM8', 'PROP']
def parse_next_subchunk(self):
return DSDIFFChunk.parse(self._fileobj, self)
def __init__(self, fileobj, id, data_size, parent_chunk):
if id not in self.LIST_CHUNK_IDS:
raise InvalidChunk('Not a list chunk: %s' % id)
DSDIFFChunk.__init__(self, fileobj, id, data_size, parent_chunk)
self.init_container()
class DSTChunk(DSDIFFChunk, IffContainerChunkMixin):
"""A DSDIFF chunk containing other chunks.
"""
def parse_next_subchunk(self):
return DSDIFFChunk.parse(self._fileobj, self)
def __init__(self, fileobj, id, data_size, parent_chunk):
if id != 'DST':
raise InvalidChunk('Not a DST chunk: %s' % id)
DSDIFFChunk.__init__(self, fileobj, id, data_size, parent_chunk)
self.init_container(name_size=0)
class DSDIFFFile(IffFile):
"""Representation of a DSDIFF file"""
def __init__(self, fileobj):
super().__init__(DSDIFFChunk, fileobj)
if self.root.id != u'FRM8':
raise InvalidChunk("Root chunk must be a FRM8 chunk, got %r"
% self.root)
class DSDIFFInfo(StreamInfo):
"""DSDIFF 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 (for DSD this is always 1)
bitrate (`int`): audio bitrate, in bits per second
compression (`str`): DSD (uncompressed) or DST
"""
channels = 0
length = 0
sample_rate = 0
bits_per_sample = 1
bitrate = 0
compression = None
@convert_error(IOError, error)
def __init__(self, fileobj):
"""Raises error"""
iff = DSDIFFFile(fileobj)
try:
prop_chunk = iff['PROP']
except KeyError as e:
raise error(str(e))
if prop_chunk.name == 'SND ':
for chunk in prop_chunk.subchunks():
if chunk.id == 'FS' and chunk.data_size == 4:
data = chunk.read()
if len(data) < 4:
raise InvalidChunk("Not enough data in FS chunk")
self.sample_rate, = struct.unpack('>L', data[:4])
elif chunk.id == 'CHNL' and chunk.data_size >= 2:
data = chunk.read()
if len(data) < 2:
raise InvalidChunk("Not enough data in CHNL chunk")
self.channels, = struct.unpack('>H', data[:2])
elif chunk.id == 'CMPR' and chunk.data_size >= 4:
data = chunk.read()
if len(data) < 4:
raise InvalidChunk("Not enough data in CMPR chunk")
compression_id, = struct.unpack('>4s', data[:4])
self.compression = compression_id.decode('ascii').rstrip()
if self.sample_rate < 0:
raise error("Invalid sample rate")
if self.compression == 'DSD': # not compressed
try:
dsd_chunk = iff['DSD']
except KeyError as e:
raise error(str(e))
# DSD data has one bit per sample. Eight samples of a channel
# are clustered together for a channel byte. For multiple channels
# the channel bytes are interleaved (in the order specified in the
# CHNL chunk). See DSDIFF spec chapter 3.3.
sample_count = dsd_chunk.data_size * 8 / (self.channels or 1)
if self.sample_rate != 0:
self.length = sample_count / float(self.sample_rate)
self.bitrate = (self.channels * self.bits_per_sample
* self.sample_rate)
elif self.compression == 'DST':
try:
dst_frame = iff['DST']
dst_frame_info = dst_frame['FRTE']
except KeyError as e:
raise error(str(e))
if dst_frame_info.data_size >= 6:
data = dst_frame_info.read()
if len(data) < 6:
raise InvalidChunk("Not enough data in FRTE chunk")
frame_count, frame_rate = struct.unpack('>LH', data[:6])
if frame_rate:
self.length = frame_count / frame_rate
if frame_count:
dst_data_size = dst_frame.data_size - dst_frame_info.size
avg_frame_size = dst_data_size / frame_count
self.bitrate = avg_frame_size * 8 * frame_rate
def pprint(self):
return u"%d channel DSDIFF (%s) @ %d bps, %s Hz, %.2f seconds" % (
self.channels, self.compression, self.bitrate, self.sample_rate,
self.length)
class _DSDIFFID3(IffID3):
"""A DSDIFF file with ID3v2 tags"""
def _load_file(self, fileobj):
return DSDIFFFile(fileobj)
@convert_error(IOError, error)
@loadfile(method=False, writable=True)
def delete(filething):
"""Completely removes the ID3 chunk from the DSDIFF file"""
try:
del DSDIFFFile(filething.fileobj)[u'ID3']
except KeyError:
pass
class DSDIFF(FileType):
"""DSDIFF(filething)
An DSDIFF audio file.
For tagging ID3v2 data is added to a chunk with the ID "ID3 ".
Arguments:
filething (filething)
Attributes:
tags (`mutagen.id3.ID3`)
info (`DSDIFFInfo`)
"""
_mimes = ["audio/x-dff"]
@convert_error(IOError, error)
@loadfile()
def load(self, filething, **kwargs):
fileobj = filething.fileobj
try:
self.tags = _DSDIFFID3(fileobj, **kwargs)
except ID3NoHeaderError:
self.tags = None
except ID3Error as e:
raise error(e)
else:
self.tags.filename = self.filename
fileobj.seek(0, 0)
self.info = DSDIFFInfo(fileobj)
def add_tags(self):
"""Add empty ID3 tags to the file."""
if self.tags is None:
self.tags = _DSDIFFID3()
else:
raise error("an ID3 tag already exists")
@staticmethod
def score(filename, fileobj, header):
return header.startswith(b"FRM8") * 2 + endswith(filename, ".dff")
Open = DSDIFF

24
lib/mutagen/dsf.py Executable file → Normal file
View File

@@ -11,11 +11,11 @@
import sys
import struct
from ._compat import cBytesIO, reraise, endswith
from io import BytesIO
from mutagen import FileType, StreamInfo
from mutagen._util import cdata, MutagenError, loadfile, convert_error
from mutagen._util import cdata, MutagenError, loadfile, \
convert_error, reraise, endswith
from mutagen.id3 import ID3
from mutagen.id3._util import ID3NoHeaderError, error as ID3Error
@@ -80,7 +80,7 @@ class DSDChunk(DSFChunk):
self.offset_metdata_chunk = cdata.ulonglong_le(data[20:28])
def write(self):
f = cBytesIO()
f = BytesIO()
f.write(self.chunk_header)
f.write(struct.pack("<Q", DSDChunk.CHUNK_SIZE))
f.write(struct.pack("<Q", self.total_size))
@@ -90,8 +90,8 @@ class DSDChunk(DSFChunk):
self.fileobj.write(f.getvalue())
def pprint(self):
return ("DSD Chunk (Total file size = %d, "
"Pointer to Metadata chunk = %d)" % (
return (u"DSD Chunk (Total file size = %d, "
u"Pointer to Metadata chunk = %d)" % (
self.total_size, self.offset_metdata_chunk))
@@ -148,8 +148,8 @@ class FormatChunk(DSFChunk):
self.sample_count = cdata.ulonglong_le(data[36:44])
def pprint(self):
return "fmt Chunk (Channel Type = %d, Channel Num = %d, " \
"Sampling Frequency = %d, %.2f seconds)" % \
return u"fmt Chunk (Channel Type = %d, Channel Num = %d, " \
u"Sampling Frequency = %d, %.2f seconds)" % \
(self.channel_type, self.channel_num, self.sampling_frequency,
self.length)
@@ -181,7 +181,7 @@ class DataChunk(DSFChunk):
raise error("DSF data header size mismatch")
def pprint(self):
return "data Chunk (Chunk Offset = %d, Chunk Size = %d)" % (
return u"data Chunk (Chunk Offset = %d, Chunk Size = %d)" % (
self.chunk_offset, self.chunk_size)
@@ -199,7 +199,7 @@ class _DSFID3(ID3):
@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething, v2_version=4, v23_sep='/', padding=None):
def save(self, filething=None, v2_version=4, v23_sep='/', padding=None):
"""Save ID3v2 data to the DSF file"""
fileobj = filething.fileobj
@@ -269,7 +269,7 @@ class DSFInfo(StreamInfo):
return self.sample_rate * self.bits_per_sample * self.channels
def pprint(self):
return "%d channel DSF @ %d bits, %s Hz, %.2f seconds" % (
return u"%d channel DSF @ %d bits, %s Hz, %.2f seconds" % (
self.channels, self.bits_per_sample, self.sample_rate, self.length)
@@ -328,7 +328,7 @@ class DSF(FileType):
self.info = DSFInfo(dsf_file.fmt_chunk)
@loadfile(writable=True)
def delete(self, filething):
def delete(self, filething=None):
self.tags = None
delete(filething)

73
lib/mutagen/easyid3.py Executable file → Normal file
View File

@@ -14,7 +14,6 @@ more like Vorbis or APEv2 tags.
import mutagen.id3
from ._compat import iteritems, text_type, PY2
from mutagen import Metadata
from mutagen._util import DictMixin, dict_match, loadfile
from mutagen.id3 import ID3, error, delete, ID3FileType
@@ -153,7 +152,7 @@ class EasyID3(DictMixin, Metadata):
enc = 0
# Store 8859-1 if we can, per MusicBrainz spec.
for v in value:
if v and max(v) > '\x7f':
if v and max(v) > u'\x7f':
enc = 3
break
@@ -173,7 +172,8 @@ class EasyID3(DictMixin, Metadata):
lambda s, v: setattr(s.__id3, 'load', v))
@loadfile(writable=True, create=True)
def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None):
def save(self, filething=None, v1=1, v2_version=4, v23_sep='/',
padding=None):
"""save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None)
Save changes to a file.
@@ -203,8 +203,9 @@ class EasyID3(DictMixin, Metadata):
filename = property(lambda s: s.__id3.filename,
lambda s, fn: setattr(s.__id3, 'filename', fn))
size = property(lambda s: s.__id3.size,
lambda s, fn: setattr(s.__id3, 'size', s))
@property
def size(self):
return self.__id3.size
def __getitem__(self, key):
func = dict_match(self.Get, key.lower(), self.GetFallback)
@@ -214,12 +215,8 @@ class EasyID3(DictMixin, Metadata):
raise EasyID3KeyError("%r is not a valid key" % key)
def __setitem__(self, key, value):
if PY2:
if isinstance(value, str):
value = [value]
else:
if isinstance(value, text_type):
value = [value]
if isinstance(value, str):
value = [value]
func = dict_match(self.Set, key.lower(), self.SetFallback)
if func is not None:
return func(self.__id3, key, value)
@@ -235,7 +232,7 @@ class EasyID3(DictMixin, Metadata):
def keys(self):
keys = []
for key in list(self.Get.keys()):
for key in self.Get.keys():
if key in self.List:
keys.extend(self.List[key](self.__id3, key))
elif key in self:
@@ -398,7 +395,7 @@ def gain_get(id3, key):
except KeyError:
raise EasyID3KeyError(key)
else:
return ["%+f dB" % frame.gain]
return [u"%+f dB" % frame.gain]
def gain_set(id3, key, value):
@@ -432,7 +429,7 @@ def peak_get(id3, key):
except KeyError:
raise EasyID3KeyError(key)
else:
return ["%f" % frame.peak]
return [u"%f" % frame.peak]
def peak_set(id3, key, value):
@@ -469,7 +466,7 @@ def peakgain_list(id3, key):
keys.append("replaygain_%s_peak" % frame.desc)
return keys
for frameid, key in iteritems({
for frameid, key in {
"TALB": "album",
"TBPM": "bpm",
"TCMP": "compilation", # iTunes extension
@@ -498,7 +495,7 @@ for frameid, key in iteritems({
"TSRC": "isrc",
"TSST": "discsubtitle",
"TLAN": "language",
}):
}.items():
EasyID3.RegisterTextKey(key, frameid)
EasyID3.RegisterKey("genre", genre_get, genre_set, genre_delete)
@@ -519,28 +516,28 @@ EasyID3.RegisterKey("replaygain_*_peak", peak_get, peak_set, peak_delete)
# http://musicbrainz.org/docs/specs/metadata_tags.html
# http://bugs.musicbrainz.org/ticket/1383
# http://musicbrainz.org/doc/MusicBrainzTag
for desc, key in iteritems({
"MusicBrainz Artist Id": "musicbrainz_artistid",
"MusicBrainz Album Id": "musicbrainz_albumid",
"MusicBrainz Album Artist Id": "musicbrainz_albumartistid",
"MusicBrainz TRM Id": "musicbrainz_trmid",
"MusicIP PUID": "musicip_puid",
"MusicMagic Fingerprint": "musicip_fingerprint",
"MusicBrainz Album Status": "musicbrainz_albumstatus",
"MusicBrainz Album Type": "musicbrainz_albumtype",
"MusicBrainz Album Release Country": "releasecountry",
"MusicBrainz Disc Id": "musicbrainz_discid",
"ASIN": "asin",
"ALBUMARTISTSORT": "albumartistsort",
"PERFORMER": "performer",
"BARCODE": "barcode",
"CATALOGNUMBER": "catalognumber",
"MusicBrainz Release Track Id": "musicbrainz_releasetrackid",
"MusicBrainz Release Group Id": "musicbrainz_releasegroupid",
"MusicBrainz Work Id": "musicbrainz_workid",
"Acoustid Fingerprint": "acoustid_fingerprint",
"Acoustid Id": "acoustid_id",
}):
for desc, key in {
u"MusicBrainz Artist Id": "musicbrainz_artistid",
u"MusicBrainz Album Id": "musicbrainz_albumid",
u"MusicBrainz Album Artist Id": "musicbrainz_albumartistid",
u"MusicBrainz TRM Id": "musicbrainz_trmid",
u"MusicIP PUID": "musicip_puid",
u"MusicMagic Fingerprint": "musicip_fingerprint",
u"MusicBrainz Album Status": "musicbrainz_albumstatus",
u"MusicBrainz Album Type": "musicbrainz_albumtype",
u"MusicBrainz Album Release Country": "releasecountry",
u"MusicBrainz Disc Id": "musicbrainz_discid",
u"ASIN": "asin",
u"ALBUMARTISTSORT": "albumartistsort",
u"PERFORMER": "performer",
u"BARCODE": "barcode",
u"CATALOGNUMBER": "catalognumber",
u"MusicBrainz Release Track Id": "musicbrainz_releasetrackid",
u"MusicBrainz Release Group Id": "musicbrainz_releasegroupid",
u"MusicBrainz Work Id": "musicbrainz_workid",
u"Acoustid Fingerprint": "acoustid_fingerprint",
u"Acoustid Id": "acoustid_id",
}.items():
EasyID3.RegisterTXXXKey(key, desc)

44
lib/mutagen/easymp4.py Executable file → Normal file
View File

@@ -9,7 +9,6 @@
from mutagen import Tags
from mutagen._util import DictMixin, dict_match
from mutagen.mp4 import MP4, MP4Tags, error, delete
from ._compat import PY2, text_type, PY3
__all__ = ["EasyMP4Tags", "EasyMP4", "delete", "error"]
@@ -42,11 +41,14 @@ class EasyMP4Tags(DictMixin, Tags):
self.load = self.__mp4.load
self.save = self.__mp4.save
self.delete = self.__mp4.delete
self._padding = self.__mp4._padding
filename = property(lambda s: s.__mp4.filename,
lambda s, fn: setattr(s.__mp4, 'filename', fn))
@property
def _padding(self):
return self.__mp4._padding
@classmethod
def RegisterKey(cls, key,
getter=None, setter=None, deleter=None, lister=None):
@@ -103,7 +105,7 @@ class EasyMP4Tags(DictMixin, Tags):
"""
def getter(tags, key):
return list(map(text_type, tags[atomid]))
return list(map(str, tags[atomid]))
def setter(tags, key, value):
clamp = lambda x: int(min(max(min_value, x), max_value))
@@ -121,9 +123,9 @@ class EasyMP4Tags(DictMixin, Tags):
ret = []
for (track, total) in tags[atomid]:
if total:
ret.append("%d/%d" % (track, total))
ret.append(u"%d/%d" % (track, total))
else:
ret.append(text_type(track))
ret.append(str(track))
return ret
def setter(tags, key, value):
@@ -164,10 +166,8 @@ class EasyMP4Tags(DictMixin, Tags):
def setter(tags, key, value):
encoded = []
for v in value:
if not isinstance(v, text_type):
if PY3:
raise TypeError("%r not str" % v)
v = v.decode("utf-8")
if not isinstance(v, str):
raise TypeError("%r not str" % v)
encoded.append(v.encode("utf-8"))
tags[atomid] = encoded
@@ -187,12 +187,8 @@ class EasyMP4Tags(DictMixin, Tags):
def __setitem__(self, key, value):
key = key.lower()
if PY2:
if isinstance(value, str):
value = [value]
else:
if isinstance(value, text_type):
value = [value]
if isinstance(value, str):
value = [value]
func = dict_match(self.Set, key)
if func is not None:
@@ -210,7 +206,7 @@ class EasyMP4Tags(DictMixin, Tags):
def keys(self):
keys = []
for key in list(self.Get.keys()):
for key in self.Get.keys():
if key in self.List:
keys.extend(self.List[key](self.__mp4, key))
elif key in self:
@@ -226,7 +222,7 @@ class EasyMP4Tags(DictMixin, Tags):
strings.append("%s=%s" % (key, value))
return "\n".join(strings)
for atomid, key in list({
for atomid, key in {
'\xa9nam': 'title',
'\xa9alb': 'album',
'\xa9ART': 'artist',
@@ -242,10 +238,10 @@ for atomid, key in list({
'soar': 'artistsort',
'sonm': 'titlesort',
'soco': 'composersort',
}.items()):
}.items():
EasyMP4Tags.RegisterTextKey(key, atomid)
for name, key in list({
for name, key in {
'MusicBrainz Artist Id': 'musicbrainz_artistid',
'MusicBrainz Track Id': 'musicbrainz_trackid',
'MusicBrainz Album Id': 'musicbrainz_albumid',
@@ -254,18 +250,18 @@ for name, key in list({
'MusicBrainz Album Status': 'musicbrainz_albumstatus',
'MusicBrainz Album Type': 'musicbrainz_albumtype',
'MusicBrainz Release Country': 'releasecountry',
}.items()):
}.items():
EasyMP4Tags.RegisterFreeformKey(key, name)
for name, key in list({
for name, key in {
"tmpo": "bpm",
}.items()):
}.items():
EasyMP4Tags.RegisterIntKey(key, name)
for name, key in list({
for name, key in {
"trkn": "tracknumber",
"disk": "discnumber",
}.items()):
}.items():
EasyMP4Tags.RegisterIntPairKey(key, name)

73
lib/mutagen/flac.py Executable file → Normal file
View File

@@ -23,12 +23,12 @@ http://flac.sourceforge.net/format.html
__all__ = ["FLAC", "Open", "delete"]
import struct
from io import BytesIO
from ._vorbis import VCommentDict
import mutagen
from ._compat import cBytesIO, endswith, chr_, xrange
from mutagen._util import resize_bytes, MutagenError, get_size, loadfile, \
convert_error
convert_error, bchr, endswith
from mutagen._tags import PaddingInfo
from mutagen.id3._util import BitPaddedInt
from functools import reduce
@@ -101,7 +101,7 @@ class MetadataBlock(object):
if data is not None:
if not isinstance(data, StrictFileObject):
if isinstance(data, bytes):
data = cBytesIO(data)
data = BytesIO(data)
elif not hasattr(data, 'read'):
raise TypeError(
"StreamInfo requires string data or a file-like")
@@ -201,7 +201,7 @@ class StreamInfo(MetadataBlock, mutagen.StreamInfo):
self.channels == other.channels and
self.bits_per_sample == other.bits_per_sample and
self.total_samples == other.total_samples)
except:
except Exception:
return False
__hash__ = MetadataBlock.__hash__
@@ -232,7 +232,7 @@ class StreamInfo(MetadataBlock, mutagen.StreamInfo):
self.md5_signature = to_int_be(data.read(16))
def write(self):
f = cBytesIO()
f = BytesIO()
f.write(struct.pack(">I", self.min_blocksize)[-2:])
f.write(struct.pack(">I", self.max_blocksize)[-2:])
f.write(struct.pack(">I", self.min_framesize)[-3:])
@@ -244,11 +244,11 @@ class StreamInfo(MetadataBlock, mutagen.StreamInfo):
byte = (self.sample_rate & 0xF) << 4
byte += ((self.channels - 1) & 7) << 1
byte += ((self.bits_per_sample - 1) >> 4) & 1
f.write(chr_(byte))
f.write(bchr(byte))
# 4 bits of bps, 4 of sample count
byte = ((self.bits_per_sample - 1) & 0xF) << 4
byte += (self.total_samples >> 32) & 0xF
f.write(chr_(byte))
f.write(bchr(byte))
# last 32 of sample count
f.write(struct.pack(">I", self.total_samples & 0xFFFFFFFF))
# MD5 signature
@@ -259,7 +259,7 @@ class StreamInfo(MetadataBlock, mutagen.StreamInfo):
return f.getvalue()
def pprint(self):
return "FLAC, %.2f seconds, %d Hz" % (self.length, self.sample_rate)
return u"FLAC, %.2f seconds, %d Hz" % (self.length, self.sample_rate)
class SeekPoint(tuple):
@@ -284,6 +284,9 @@ class SeekPoint(tuple):
return super(cls, SeekPoint).__new__(
cls, (first_sample, byte_offset, num_samples))
def __getnewargs__(self):
return self.first_sample, self.byte_offset, self.num_samples
first_sample = property(lambda self: self[0])
byte_offset = property(lambda self: self[1])
num_samples = property(lambda self: self[2])
@@ -322,7 +325,7 @@ class SeekTable(MetadataBlock):
sp = data.tryread(self.__SEEKPOINT_SIZE)
def write(self):
f = cBytesIO()
f = BytesIO()
for seekpoint in self.seekpoints:
packed = struct.pack(
self.__SEEKPOINT_FORMAT,
@@ -391,10 +394,10 @@ class CueSheetTrack(object):
Attributes:
track_number (`int`): track number
start_offset (`int`): track offset in samples from start of FLAC stream
isrc (`text`): ISRC code, exactly 12 characters
isrc (`mutagen.text`): ISRC code, exactly 12 characters
type (`int`): 0 for audio, 1 for digital data
pre_emphasis (`bool`): true if the track is recorded with pre-emphasis
indexes (List[`mutagen.flac.CueSheetTrackIndex`]):
indexes (list[CueSheetTrackIndex]):
list of CueSheetTrackIndex objects
"""
@@ -437,14 +440,14 @@ class CueSheet(MetadataBlock):
in the cue sheet.
Attributes:
media_catalog_number (`text`): media catalog number in ASCII,
media_catalog_number (`mutagen.text`): media catalog number in ASCII,
up to 128 characters
lead_in_samples (`int`): number of lead-in samples
compact_disc (`bool`): true if the cuesheet corresponds to a
compact disc
tracks (List[`mutagen.flac.CueSheetTrack`]):
tracks (list[CueSheetTrack]):
list of CueSheetTrack objects
lead_out (`mutagen.flac.CueSheetTrack` or `None`):
lead_out (`CueSheetTrack` or `None`):
lead-out as CueSheetTrack or None if lead-out was not found
"""
@@ -502,7 +505,7 @@ class CueSheet(MetadataBlock):
self.tracks.append(val)
def write(self):
f = cBytesIO()
f = BytesIO()
flags = 0
if self.compact_disc:
flags |= 0x80
@@ -574,8 +577,8 @@ class Picture(MetadataBlock):
def __init__(self, data=None):
self.type = 0
self.mime = ''
self.desc = ''
self.mime = u''
self.desc = u''
self.width = 0
self.height = 0
self.depth = 0
@@ -608,7 +611,7 @@ class Picture(MetadataBlock):
self.data = data.read(length)
def write(self):
f = cBytesIO()
f = BytesIO()
mime = self.mime.encode('UTF-8')
f.write(struct.pack('>2I', self.type, len(mime)))
f.write(mime)
@@ -678,7 +681,7 @@ class FLAC(mutagen.FileType):
Attributes:
cuesheet (`CueSheet`): if any or `None`
seektable (`SeekTable`): if any or `None`
pictures (List[`Picture`]): list of embedded pictures
pictures (list[Picture]): list of embedded pictures
info (`StreamInfo`)
tags (`mutagen._vorbis.VCommentDict`)
"""
@@ -732,7 +735,9 @@ class FLAC(mutagen.FileType):
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,19 +761,21 @@ class FLAC(mutagen.FileType):
add_vorbiscomment = add_tags
@convert_error(IOError, error)
@loadfile(writable=True)
def delete(self, filething):
def delete(self, filething=None):
"""Remove Vorbis comments from a file.
If no filename is given, the one most recently loaded is used.
"""
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.")
@@ -823,26 +830,24 @@ class FLAC(mutagen.FileType):
@property
def pictures(self):
"""
Returns:
List[`Picture`]: List of embedded pictures
"""
return [b for b in self.metadata_blocks if b.code == Picture.code]
@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething, deleteid3=False, padding=None):
def save(self, filething=None, deleteid3=False, padding=None):
"""Save metadata blocks to a file.
Args:
filething (filething)
deleteid3 (bool): delete id3 tags while at it
padding (PaddingFunction)
padding (:obj:`mutagen.PaddingFunction`)
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)
@@ -857,7 +862,7 @@ class FLAC(mutagen.FileType):
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)

36
lib/mutagen/id3/__init__.py Executable file → Normal file
View File

@@ -38,22 +38,30 @@ from ._frames import Frames, Frames_2_2, Frame, TextFrame, UrlFrame, \
from ._util import ID3NoHeaderError, error, ID3UnsupportedVersionError
from ._id3v1 import ParseID3v1, MakeID3v1
from ._tags import ID3Tags
from ._frames import (AENC, APIC, ASPI, BUF, CHAP, CNT, COM, COMM, COMR, CRA,
CRM, CTOC, ENCR, EQU2, ETC, ETCO, GEO, GEOB, GP1, GRID, GRP1, IPL, IPLS,
LINK, LNK, MCDI, MCI, MLL, MLLT, MVI, MVIN, MVN, MVNM, OWNE, PCNT, PCST,
PIC, POP, POPM, POSS, PRIV, RBUF, REV, RVA, RVA2, RVAD, RVRB, SEEK, SIGN,
SLT, STC, SYLT, SYTC, TAL, TALB, TBP, TBPM, TCAT, TCM, TCMP, TCO, TCOM,
TCON, TCOP, TCP, TCR, TDA, TDAT, TDEN, TDES, TDLY, TDOR, TDRC, TDRL, TDTG,
TDY, TEN, TENC, TEXT, TFLT, TFT, TGID, TIM, TIME, TIPL, TIT1, TIT2, TIT3,
TKE, TKEY, TKWD, TLA, TLAN, TLE, TLEN, TMCL, TMED, TMOO, TMT, TOA, TOAL,
TOF, TOFN, TOL, TOLY, TOPE, TOR, TORY, TOT, TOWN, TP1, TP2, TP3, TP4, TPA,
TPB, TPE1, TPE2, TPE3, TPE4, TPOS, TPRO, TPUB, TRC, TRCK, TRD, TRDA, TRK,
TRSN, TRSO, TS2, TSA, TSC, TSI, TSIZ, TSO2, TSOA, TSOC, TSOP, TSOT, TSP,
TSRC, TSS, TSSE, TSST, TST, TT1, TT2, TT3, TXT, TXX, TXXX, TYE, TYER, UFI,
UFID, ULT, USER, USLT, WAF, WAR, WAS, WCM, WCOM, WCOP, WCP, WFED, WOAF,
WOAR, WOAS, WORS, WPAY, WPB, WPUB, WXX, WXXX)
# deprecated
from ._util import ID3EncryptionUnsupportedError, ID3JunkFrameError, \
ID3BadUnsynchData, ID3BadCompressedData, ID3TagError, ID3Warning, \
BitPaddedInt as _BitPaddedIntForPicard
for f in Frames:
globals()[f] = Frames[f]
for f in Frames_2_2:
globals()[f] = Frames_2_2[f]
# support open(filename) as interface
Open = ID3
# pyflakes
# flake8
ID3, ID3FileType, delete, ID3v1SaveOptions, Encoding, PictureType, CTOCFlags,
ID3TimeStamp, Frames, Frames_2_2, Frame, TextFrame, UrlFrame, UrlFrameU,
TimeStampTextFrame, BinaryFrame, NumericPartTextFrame, NumericTextFrame,
@@ -62,6 +70,20 @@ ParseID3v1, MakeID3v1, ID3Tags, ID3EncryptionUnsupportedError,
ID3JunkFrameError, ID3BadUnsynchData, ID3BadCompressedData, ID3TagError,
ID3Warning
AENC, APIC, ASPI, BUF, CHAP, CNT, COM, COMM, COMR, CRA, CRM, CTOC, ENCR, EQU2,
ETC, ETCO, GEO, GEOB, GP1, GRID, GRP1, IPL, IPLS, LINK, LNK, MCDI, MCI, MLL,
MLLT, MVI, MVIN, MVN, MVNM, OWNE, PCNT, PCST, PIC, POP, POPM, POSS, PRIV,
RBUF, REV, RVA, RVA2, RVAD, RVRB, SEEK, SIGN, SLT, STC, SYLT, SYTC, TAL, TALB,
TBP, TBPM, TCAT, TCM, TCMP, TCO, TCOM, TCON, TCOP, TCP, TCR, TDA, TDAT, TDEN,
TDES, TDLY, TDOR, TDRC, TDRL, TDTG, TDY, TEN, TENC, TEXT, TFLT, TFT, TGID,
TIM, TIME, TIPL, TIT1, TIT2, TIT3, TKE, TKEY, TKWD, TLA, TLAN, TLE, TLEN,
TMCL, TMED, TMOO, TMT, TOA, TOAL, TOF, TOFN, TOL, TOLY, TOPE, TOR, TORY, TOT,
TOWN, TP1, TP2, TP3, TP4, TPA, TPB, TPE1, TPE2, TPE3, TPE4, TPOS, TPRO, TPUB,
TRC, TRCK, TRD, TRDA, TRK, TRSN, TRSO, TS2, TSA, TSC, TSI, TSIZ, TSO2, TSOA,
TSOC, TSOP, TSOT, TSP, TSRC, TSS, TSSE, TSST, TST, TT1, TT2, TT3, TXT, TXX,
TXXX, TYE, TYER, UFI, UFID, ULT, USER, USLT, WAF, WAR, WAS, WCM, WCOM, WCOP,
WCP, WFED, WOAF, WOAR, WOAS, WORS, WPAY, WPB, WPUB, WXX, WXXX
# Workaround for http://tickets.musicbrainz.org/browse/PICARD-833
class _DummySpecForPicard(object):

47
lib/mutagen/id3/_file.py Executable file → Normal file
View File

@@ -53,8 +53,8 @@ class ID3(ID3Tags, mutagen.Metadata):
filething (filething): or `None`
Attributes:
version (Tuple[int]): ID3 tag version as a tuple
unknown_frames (List[bytes]): raw frame data of any unknown frames
version (tuple[int]): ID3 tag version as a tuple
unknown_frames (list[bytes]): raw frame data of any unknown frames
found
size (int): the total size of the ID3 tag, including the header
"""
@@ -78,8 +78,6 @@ class ID3(ID3Tags, mutagen.Metadata):
@property
def version(self):
"""`tuple`: ID3 tag version as a tuple (of the loaded file)"""
if self._header is not None:
return self._header.version
return self._version
@@ -112,10 +110,9 @@ class ID3(ID3Tags, mutagen.Metadata):
@convert_error(IOError, error)
@loadfile()
def load(self, filething, known_frames=None, translate=True, v2_version=4):
"""load(filething, known_frames=None, translate=True, v2_version=4)
Load tags from a filename.
def load(self, filething, known_frames=None, translate=True, v2_version=4,
load_v1=True):
"""Load tags from a filename.
Args:
filename (filething): filename or file object to load tag data from
@@ -126,6 +123,11 @@ class ID3(ID3Tags, mutagen.Metadata):
call update_to_v23() / update_to_v24() manually.
v2_version (int): if update_to_v23 or update_to_v24 get called
(3 or 4)
load_v1 (bool): Load tags from ID3v1 header if present. If both
ID3v1 and ID3v2 headers are present, combine the tags from
the two, with ID3v2 having precedence.
.. versionadded:: 1.42
Example of loading a custom frame::
@@ -149,13 +151,17 @@ class ID3(ID3Tags, mutagen.Metadata):
try:
self._header = ID3Header(fileobj)
except (ID3NoHeaderError, ID3UnsupportedVersionError):
frames, offset = find_id3v1(fileobj)
if not load_v1:
raise
frames, offset = find_id3v1(fileobj, v2_version, known_frames)
if frames is None:
raise
self.version = ID3Header._V11
for v in list(frames.values()):
self.add(v)
for v in frames.values():
if len(self.getall(v.HashKey)) == 0:
self.add(v)
else:
# XXX: attach to the header object so we have it in spec parsing..
if known_frames is not None:
@@ -165,6 +171,14 @@ class ID3(ID3Tags, mutagen.Metadata):
remaining_data = self._read(self._header, data)
self._padding = len(remaining_data)
if load_v1:
v1v2_ver = 4 if self.version[1] == 4 else 3
frames, offset = find_id3v1(fileobj, v1v2_ver, known_frames)
if frames:
for v in frames.values():
if len(self.getall(v.HashKey)) == 0:
self.add(v)
if translate:
if v2_version == 3:
self.update_to_v23()
@@ -204,13 +218,14 @@ class ID3(ID3Tags, mutagen.Metadata):
@convert_error(IOError, error)
@loadfile(writable=True, create=True)
def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None):
def save(self, filething=None, v1=1, v2_version=4, v23_sep='/',
padding=None):
"""save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None)
Save changes to a file.
Args:
filename (fspath):
filething (filething):
Filename to save the tag to. If no filename is given,
the one most recently loaded is used.
v1 (ID3v1SaveOptions):
@@ -223,7 +238,7 @@ class ID3(ID3Tags, mutagen.Metadata):
the separator used to join multiple text values
if v2_version == 3. Defaults to '/' but if it's None
will be the ID3v2v2.4 null separator.
padding (PaddingFunction)
padding (:obj:`mutagen.PaddingFunction`)
Raises:
mutagen.MutagenError
@@ -268,7 +283,7 @@ class ID3(ID3Tags, mutagen.Metadata):
f.truncate()
@loadfile(writable=True)
def delete(self, filething, delete_v1=True, delete_v2=True):
def delete(self, filething=None, delete_v1=True, delete_v2=True):
"""delete(filething=None, delete_v1=True, delete_v2=True)
Remove tags from a file.
@@ -352,7 +367,7 @@ class ID3FileType(mutagen.FileType):
@staticmethod
def pprint():
return "Unknown format with ID3 tag"
return u"Unknown format with ID3 tag"
@staticmethod
def score(filename, fileobj, header_data):

115
lib/mutagen/id3/_frames.py Executable file → Normal file
View File

@@ -18,8 +18,6 @@ from ._specs import BinaryDataSpec, StringSpec, Latin1TextSpec, \
KeyEventSpec, TimeStampSpec, EncodedNumericPartTextSpec, \
EncodedNumericTextSpec, SpecError, PictureTypeSpec, ID3FramesSpec, \
Latin1TextListSpec, CTOCFlagsSpec, FrameIDSpec, RVASpec
from .._compat import text_type, string_types, swap_to_string, iteritems, \
izip, itervalues
def _bytes2key(b):
@@ -265,7 +263,7 @@ class Frame(object):
if tflags & Frame.FLAG24_COMPRESS:
try:
data = zlib.decompress(data)
except zlib.error as err:
except zlib.error:
# the initial mutagen that went out with QL 0.12 did not
# write the 4 bytes of uncompressed size. Compensate.
data = datalen_bytes + data
@@ -277,6 +275,8 @@ class Frame(object):
elif header.version >= header._V23:
if tflags & Frame.FLAG23_COMPRESS:
if len(data) < 4:
raise ID3JunkFrameError('frame too small: %r' % data)
usize, = unpack('>L', data[:4])
data = data[4:]
if tflags & Frame.FLAG23_ENCRYPT:
@@ -329,11 +329,11 @@ class CHAP(Frame):
__hash__ = Frame.__hash__
def _pprint(self):
frame_pprint = ""
for frame in itervalues(self.sub_frames):
frame_pprint = u""
for frame in self.sub_frames.values():
for line in frame.pprint().splitlines():
frame_pprint += "\n" + " " * 4 + line
return "%s time=%d..%d offset=%d..%d%s" % (
return u"%s time=%d..%d offset=%d..%d%s" % (
self.element_id, self.start_time, self.end_time,
self.start_offset, self.end_offset, frame_pprint)
@@ -368,16 +368,15 @@ class CTOC(Frame):
self.child_element_ids == other.child_element_ids
def _pprint(self):
frame_pprint = ""
frame_pprint = u""
if getattr(self, "sub_frames", None):
frame_pprint += "\n" + "\n".join(
[" " * 4 + f.pprint() for f in list(self.sub_frames.values())])
return "%s flags=%d child_element_ids=%s%s" % (
[" " * 4 + f.pprint() for f in self.sub_frames.values()])
return u"%s flags=%d child_element_ids=%s%s" % (
self.element_id, int(self.flags),
",".join(self.child_element_ids), frame_pprint)
u",".join(self.child_element_ids), frame_pprint)
@swap_to_string
class TextFrame(Frame):
"""Text strings.
@@ -395,20 +394,20 @@ class TextFrame(Frame):
_framespec = [
EncodingSpec('encoding', default=Encoding.UTF16),
MultiSpec('text', EncodedTextSpec('text'), sep='\u0000', default=[]),
MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000', default=[]),
]
def __bytes__(self):
return text_type(self).encode('utf-8')
return str(self).encode('utf-8')
def __str__(self):
return '\u0000'.join(self.text)
return u'\u0000'.join(self.text)
def __eq__(self, other):
if isinstance(other, bytes):
return bytes(self) == other
elif isinstance(other, text_type):
return text_type(self) == other
elif isinstance(other, str):
return str(self) == other
return self.text == other
__hash__ = Frame.__hash__
@@ -434,6 +433,7 @@ class TextFrame(Frame):
for val in other[:]:
if val not in self:
self.append(val)
self.encoding = max(self.encoding, other.encoding)
return self
def _pprint(self):
@@ -451,7 +451,7 @@ class NumericTextFrame(TextFrame):
_framespec = [
EncodingSpec('encoding', default=Encoding.UTF16),
MultiSpec('text', EncodedNumericTextSpec('text'), sep='\u0000',
MultiSpec('text', EncodedNumericTextSpec('text'), sep=u'\u0000',
default=[]),
]
@@ -472,7 +472,7 @@ class NumericPartTextFrame(TextFrame):
_framespec = [
EncodingSpec('encoding', default=Encoding.UTF16),
MultiSpec('text', EncodedNumericPartTextSpec('text'), sep='\u0000',
MultiSpec('text', EncodedNumericPartTextSpec('text'), sep=u'\u0000',
default=[]),
]
@@ -480,7 +480,6 @@ class NumericPartTextFrame(TextFrame):
return int(self.text[0].split("/")[0])
@swap_to_string
class TimeStampTextFrame(TextFrame):
"""A list of time stamps.
@@ -490,20 +489,19 @@ class TimeStampTextFrame(TextFrame):
_framespec = [
EncodingSpec('encoding', default=Encoding.UTF16),
MultiSpec('text', TimeStampSpec('stamp'), sep=',', default=[]),
MultiSpec('text', TimeStampSpec('stamp'), sep=u',', default=[]),
]
def __bytes__(self):
return text_type(self).encode('utf-8')
return str(self).encode('utf-8')
def __str__(self):
return ','.join([stamp.text for stamp in self.text])
return u','.join([stamp.text for stamp in self.text])
def _pprint(self):
return " / ".join([stamp.text for stamp in self.text])
return u" / ".join([stamp.text for stamp in self.text])
@swap_to_string
class UrlFrame(Frame):
"""A frame containing a URL string.
@@ -574,11 +572,11 @@ class TCON(TextFrame):
try:
genres.append(self.GENRES[int(value)])
except IndexError:
genres.append("Unknown")
genres.append(u"Unknown")
elif value == "CR":
genres.append("Cover")
genres.append(u"Cover")
elif value == "RX":
genres.append("Remix")
genres.append(u"Remix")
elif value:
newgenres = []
genreid, dummy, genrename = genre_re.match(value).groups()
@@ -586,14 +584,14 @@ class TCON(TextFrame):
if genreid:
for gid in genreid[1:-1].split(")("):
if gid.isdigit() and int(gid) < len(self.GENRES):
gid = text_type(self.GENRES[int(gid)])
gid = str(self.GENRES[int(gid)])
newgenres.append(gid)
elif gid == "CR":
newgenres.append("Cover")
newgenres.append(u"Cover")
elif gid == "RX":
newgenres.append("Remix")
newgenres.append(u"Remix")
else:
newgenres.append("Unknown")
newgenres.append(u"Unknown")
if genrename:
# "Unescaping" the first parenthesis
@@ -607,7 +605,7 @@ class TCON(TextFrame):
return genres
def __set_genres(self, genres):
if isinstance(genres, string_types):
if isinstance(genres, str):
genres = [genres]
self.text = [self.__decode(g) for g in genres]
@@ -669,6 +667,14 @@ class MVI(MVIN):
"iTunes Movement Number/Count"
class GRP1(TextFrame):
"iTunes Grouping"
class GP1(GRP1):
"iTunes Grouping"
class TDOR(TimeStampTextFrame):
"Original Release Time"
@@ -860,7 +866,7 @@ class TXXX(TextFrame):
_framespec = [
EncodingSpec('encoding'),
EncodedTextSpec('desc'),
MultiSpec('text', EncodedTextSpec('text'), sep='\u0000', default=[]),
MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000', default=[]),
]
@property
@@ -1035,7 +1041,6 @@ class SYTC(Frame):
__hash__ = Frame.__hash__
@swap_to_string
class USLT(Frame):
"""Unsynchronised lyrics/text transcription.
@@ -1045,7 +1050,7 @@ class USLT(Frame):
_framespec = [
EncodingSpec('encoding', default=Encoding.UTF16),
StringSpec('lang', length=3, default="XXX"),
StringSpec('lang', length=3, default=u"XXX"),
EncodedTextSpec('desc'),
EncodedTextSpec('text'),
]
@@ -1065,14 +1070,16 @@ class USLT(Frame):
__hash__ = Frame.__hash__
def _pprint(self):
return "%s=%s=%s" % (self.desc, self.lang, self.text)
@swap_to_string
class SYLT(Frame):
"""Synchronised lyrics/text."""
_framespec = [
EncodingSpec('encoding'),
StringSpec('lang', length=3, default="XXX"),
StringSpec('lang', length=3, default=u"XXX"),
ByteSpec('format', default=1),
ByteSpec('type', default=0),
EncodedTextSpec('desc'),
@@ -1083,16 +1090,21 @@ class SYLT(Frame):
def HashKey(self):
return '%s:%s:%s' % (self.FrameID, self.desc, self.lang)
def _pprint(self):
return str(self)
def __eq__(self, other):
return str(self) == other
__hash__ = Frame.__hash__
def __str__(self):
return "".join(text for (text, time) in self.text)
unit = 'fr' if self.format == 1 else 'ms'
return u"\n".join("[{0}{1}]: {2}".format(time, unit, text)
for (text, time) in self.text)
def __bytes__(self):
return text_type(self).encode("utf-8")
return str(self).encode("utf-8")
class COMM(TextFrame):
@@ -1106,7 +1118,7 @@ class COMM(TextFrame):
EncodingSpec('encoding'),
StringSpec('lang', length=3, default="XXX"),
EncodedTextSpec('desc'),
MultiSpec('text', EncodedTextSpec('text'), sep='\u0000', default=[]),
MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000', default=[]),
]
@property
@@ -1263,11 +1275,11 @@ class APIC(Frame):
return '%s:%s' % (self.FrameID, self.desc)
def _merge_frame(self, other):
other.desc += " "
other.desc += u" "
return other
def _pprint(self):
type_desc = text_type(self.type)
type_desc = str(self.type)
if hasattr(self.type, "_pprint"):
type_desc = self.type._pprint()
@@ -1297,7 +1309,7 @@ class PCNT(Frame):
return self.count
def _pprint(self):
return text_type(self.count)
return str(self.count)
class PCST(Frame):
@@ -1316,7 +1328,7 @@ class PCST(Frame):
return self.value
def _pprint(self):
return text_type(self.value)
return str(self.value)
class POPM(Frame):
@@ -1420,7 +1432,6 @@ class RBUF(Frame):
return self.size
@swap_to_string
class AENC(Frame):
"""Audio encryption.
@@ -1537,7 +1548,6 @@ class UFID(Frame):
return "%s=%r" % (self.owner, self.data)
@swap_to_string
class USER(Frame):
"""Terms of use.
@@ -1550,7 +1560,7 @@ class USER(Frame):
_framespec = [
EncodingSpec('encoding'),
StringSpec('lang', length=3, default="XXX"),
StringSpec('lang', length=3, default=u"XXX"),
EncodedTextSpec('text'),
]
@@ -1573,14 +1583,13 @@ class USER(Frame):
return "%r=%s" % (self.lang, self.text)
@swap_to_string
class OWNE(Frame):
"""Ownership frame."""
_framespec = [
EncodingSpec('encoding'),
Latin1TextSpec('price'),
StringSpec('date', length=8, default="19700101"),
StringSpec('date', length=8, default=u"19700101"),
EncodedTextSpec('seller'),
]
@@ -1602,7 +1611,7 @@ class COMR(Frame):
_framespec = [
EncodingSpec('encoding'),
Latin1TextSpec('price'),
StringSpec('valid_until', length=8, default="19700101"),
StringSpec('valid_until', length=8, default=u"19700101"),
Latin1TextSpec('contact'),
ByteSpec('format', default=0),
EncodedTextSpec('seller'),
@@ -1624,7 +1633,6 @@ class COMR(Frame):
__hash__ = Frame.__hash__
@swap_to_string
class ENCR(Frame):
"""Encryption method registration.
@@ -1651,7 +1659,6 @@ class ENCR(Frame):
__hash__ = Frame.__hash__
@swap_to_string
class GRID(Frame):
"""Group identification registration."""
@@ -1680,7 +1687,6 @@ class GRID(Frame):
__hash__ = Frame.__hash__
@swap_to_string
class PRIV(Frame):
"""Private frame."""
@@ -1706,7 +1712,6 @@ class PRIV(Frame):
__hash__ = Frame.__hash__
@swap_to_string
class SIGN(Frame):
"""Signature frame."""
@@ -2118,7 +2123,7 @@ Frames_2_2 = {}
k, v = None, None
for k, v in iteritems(globals()):
for k, v in globals().items():
if isinstance(v, type) and issubclass(v, Frame):
v.__module__ = "mutagen.id3"

92
lib/mutagen/id3/_id3v1.py Executable file → Normal file
View File

@@ -11,22 +11,32 @@
import errno
from struct import error as StructError, unpack
from mutagen._util import chr_, text_type
from mutagen._util import bchr
from ._frames import TCON, TRCK, COMM, TDRC, TALB, TPE1, TIT2
from ._frames import TCON, TRCK, COMM, TDRC, TYER, TALB, TPE1, TIT2
def find_id3v1(fileobj):
def find_id3v1(fileobj, v2_version=4, known_frames=None):
"""Returns a tuple of (id3tag, offset_to_end) or (None, 0)
offset mainly because we used to write too short tags in some cases and
we need the offset to delete them.
v2_version: Decides whether ID3v2.3 or ID3v2.4 tags
should be returned. Must be 3 or 4.
known_frames (Dict[`mutagen.text`, `Frame`]): dict mapping frame
IDs to Frame objects
"""
if v2_version not in (3, 4):
raise ValueError("Only 3 and 4 possible for v2_version")
# id3v1 is always at the end (after apev2)
extra_read = b"APETAGEX".index(b"TAG")
old_pos = fileobj.tell()
try:
fileobj.seek(-128 - extra_read, 2)
except IOError as e:
@@ -38,6 +48,7 @@ def find_id3v1(fileobj):
raise
data = fileobj.read(128 + extra_read)
fileobj.seek(old_pos, 0)
try:
idx = data.index(b"TAG")
except ValueError:
@@ -53,7 +64,7 @@ def find_id3v1(fileobj):
if idx == ape_idx + extra_read:
return (None, 0)
tag = ParseID3v1(data[idx:])
tag = ParseID3v1(data[idx:], v2_version, known_frames)
if tag is None:
return (None, 0)
@@ -62,12 +73,21 @@ def find_id3v1(fileobj):
# ID3v1.1 support.
def ParseID3v1(data):
"""Parse an ID3v1 tag, returning a list of ID3v2.4 frames.
def ParseID3v1(data, v2_version=4, known_frames=None):
"""Parse an ID3v1 tag, returning a list of ID3v2 frames
Returns a {frame_name: frame} dict or None.
v2_version: Decides whether ID3v2.3 or ID3v2.4 tags
should be returned. Must be 3 or 4.
known_frames (Dict[`mutagen.text`, `Frame`]): dict mapping frame
IDs to Frame objects
"""
if v2_version not in (3, 4):
raise ValueError("Only 3 and 4 possible for v2_version")
try:
data = data[data.index(b"TAG"):]
except ValueError:
@@ -94,26 +114,48 @@ def ParseID3v1(data):
def fix(data):
return data.split(b"\x00")[0].strip().decode('latin1')
title, artist, album, year, comment = list(map(
fix, [title, artist, album, year, comment]))
title, artist, album, year, comment = map(
fix, [title, artist, album, year, comment])
frame_class = {
"TIT2": TIT2,
"TPE1": TPE1,
"TALB": TALB,
"TYER": TYER,
"TDRC": TDRC,
"COMM": COMM,
"TRCK": TRCK,
"TCON": TCON,
}
for key in frame_class:
if known_frames is not None:
if key in known_frames:
frame_class[key] = known_frames[key]
else:
frame_class[key] = None
frames = {}
if title:
frames["TIT2"] = TIT2(encoding=0, text=title)
if artist:
frames["TPE1"] = TPE1(encoding=0, text=[artist])
if album:
frames["TALB"] = TALB(encoding=0, text=album)
if title and frame_class["TIT2"]:
frames["TIT2"] = frame_class["TIT2"](encoding=0, text=title)
if artist and frame_class["TPE1"]:
frames["TPE1"] = frame_class["TPE1"](encoding=0, text=[artist])
if album and frame_class["TALB"]:
frames["TALB"] = frame_class["TALB"](encoding=0, text=album)
if year:
frames["TDRC"] = TDRC(encoding=0, text=year)
if comment:
frames["COMM"] = COMM(
if v2_version == 3 and frame_class["TYER"]:
frames["TYER"] = frame_class["TYER"](encoding=0, text=year)
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)
# Don't read a track number if it looks like the comment was
# padded with spaces instead of nulls (thanks, WinAmp).
if track and ((track != 32) or (data[-3] == b'\x00'[0])):
if (track and frame_class["TRCK"] and
((track != 32) or (data[-3] == b'\x00'[0]))):
frames["TRCK"] = TRCK(encoding=0, text=str(track))
if genre != 255:
if genre != 255 and frame_class["TCON"]:
frames["TCON"] = TCON(encoding=0, text=str(genre))
return frames
@@ -123,8 +165,8 @@ def MakeID3v1(id3):
v1 = {}
for v2id, name in list({"TIT2": "title", "TPE1": "artist",
"TALB": "album"}.items()):
for v2id, name in {"TIT2": "title", "TPE1": "artist",
"TALB": "album"}.items():
if v2id in id3:
text = id3[v2id].text[0].encode('latin1', 'replace')[:30]
else:
@@ -139,7 +181,7 @@ def MakeID3v1(id3):
if "TRCK" in id3:
try:
v1["track"] = chr_(+id3["TRCK"])
v1["track"] = bchr(+id3["TRCK"])
except ValueError:
v1["track"] = b"\x00"
else:
@@ -152,14 +194,14 @@ def MakeID3v1(id3):
pass
else:
if genre in TCON.GENRES:
v1["genre"] = chr_(TCON.GENRES.index(genre))
v1["genre"] = bchr(TCON.GENRES.index(genre))
if "genre" not in v1:
v1["genre"] = b"\xff"
if "TDRC" in id3:
year = text_type(id3["TDRC"]).encode('ascii')
year = str(id3["TDRC"]).encode('ascii')
elif "TYER" in id3:
year = text_type(id3["TYER"]).encode('ascii')
year = str(id3["TYER"]).encode('ascii')
else:
year = b""
v1["year"] = (year + b"\x00\x00\x00\x00")[:4]

67
lib/mutagen/id3/_specs.py Executable file → Normal file
View File

@@ -10,10 +10,8 @@ import struct
import codecs
from struct import unpack, pack
from .._compat import text_type, chr_, PY3, swap_to_string, string_types, \
xrange
from .._util import total_ordering, decode_terminated, enum, izip, flags, \
cdata, encode_endian
from .._util import total_ordering, decode_terminated, enum, flags, \
cdata, encode_endian, intround, bchr
from ._util import BitPaddedInt, is_valid_frame_id
@@ -87,7 +85,7 @@ class PictureType(object):
"""Publisher/Studio logotype"""
def _pprint(self):
return text_type(self).split(".", 1)[-1].lower().replace("_", " ")
return str(self).split(".", 1)[-1].lower().replace("_", " ")
@flags
@@ -165,11 +163,11 @@ class ByteSpec(Spec):
return bytearray(data)[0], data[1:]
def write(self, config, frame, value):
return chr_(value)
return bchr(value)
def validate(self, frame, value):
if value is not None:
chr_(value)
bchr(value)
return value
@@ -278,7 +276,7 @@ class StringSpec(Spec):
def __init__(self, name, length, default=None):
if default is None:
default = " " * length
default = u" " * length
super(StringSpec, self).__init__(name, default)
self.len = length
@@ -289,26 +287,22 @@ class StringSpec(Spec):
except UnicodeDecodeError:
raise SpecError("not ascii")
else:
if PY3:
chunk = ascii
chunk = ascii
return chunk, data[s.len:]
def write(self, config, frame, value):
if PY3:
value = value.encode("ascii")
value = value.encode("ascii")
return (bytes(value) + b'\x00' * self.len)[:self.len]
def validate(self, frame, value):
if value is None:
raise TypeError
if PY3:
if not isinstance(value, str):
raise TypeError("%s has to be str" % self.name)
value.encode("ascii")
else:
if not isinstance(value, bytes):
value = value.encode("ascii")
if not isinstance(value, str):
raise TypeError("%s has to be str" % self.name)
value.encode("ascii")
if len(value) == self.len:
return value
@@ -402,7 +396,7 @@ class RVASpec(Spec):
class FrameIDSpec(StringSpec):
def __init__(self, name, length):
super(FrameIDSpec, self).__init__(name, length, "X" * length)
super(FrameIDSpec, self).__init__(name, length, u"X" * length)
def validate(self, frame, value):
value = super(FrameIDSpec, self).validate(frame, value)
@@ -424,7 +418,7 @@ class BinaryDataSpec(Spec):
def write(self, config, frame, value):
if isinstance(value, bytes):
return value
value = text_type(value).encode("ascii")
value = str(value).encode("ascii")
return value
def validate(self, frame, value):
@@ -432,10 +426,10 @@ class BinaryDataSpec(Spec):
raise TypeError
if isinstance(value, bytes):
return value
elif PY3:
else:
raise TypeError("%s has to be bytes" % self.name)
value = text_type(value).encode("ascii")
value = str(value).encode("ascii")
return value
@@ -464,7 +458,7 @@ class EncodedTextSpec(Spec):
Encoding.UTF8: ('utf8', b'\x00'),
}
def __init__(self, name, default=""):
def __init__(self, name, default=u""):
super(EncodedTextSpec, self).__init__(name, default)
def read(self, header, frame, data):
@@ -493,7 +487,7 @@ class EncodedTextSpec(Spec):
raise SpecError(e)
def validate(self, frame, value):
return text_type(value)
return str(value)
class MultiSpec(Spec):
@@ -527,7 +521,7 @@ class MultiSpec(Spec):
return b''.join(data)
def validate(self, frame, value):
if self.sep and isinstance(value, string_types):
if self.sep and isinstance(value, str):
value = value.split(self.sep)
if isinstance(value, list):
if len(self.specs) == 1:
@@ -568,7 +562,7 @@ class EncodedNumericPartTextSpec(EncodedTextSpec):
class Latin1TextSpec(Spec):
def __init__(self, name, default=""):
def __init__(self, name, default=u""):
super(Latin1TextSpec, self).__init__(name, default)
def read(self, header, frame, data):
@@ -582,7 +576,7 @@ class Latin1TextSpec(Spec):
return value.encode('latin1') + b'\x00'
def validate(self, frame, value):
return text_type(value)
return str(value)
class ID3FramesSpec(Spec):
@@ -602,7 +596,7 @@ class ID3FramesSpec(Spec):
from ._tags import ID3Tags
v = ID3Tags()
for frame in list(value.values()):
for frame in value.values():
v.add(frame._get_v23_frame(**kwargs))
return v
@@ -647,7 +641,6 @@ class Latin1TextListSpec(Spec):
return [self._lspec.validate(frame, v) for v in value]
@swap_to_string
@total_ordering
class ID3TimeStamp(object):
"""A time stamp in ID3v2 format.
@@ -665,10 +658,8 @@ class ID3TimeStamp(object):
def __init__(self, text):
if isinstance(text, ID3TimeStamp):
text = text.text
elif not isinstance(text, text_type):
if PY3:
raise TypeError("not a str")
text = text.decode("utf-8")
elif not isinstance(text, str):
raise TypeError("not a str")
self.text = text
@@ -683,9 +674,9 @@ class ID3TimeStamp(object):
if part is None:
break
pieces.append(self.__formats[i] % part + self.__seps[i])
return ''.join(pieces)[:-1]
return u''.join(pieces)[:-1]
def set_text(self, text, splitre=re.compile('[-T:/.]|\s+')):
def set_text(self, text, splitre=re.compile('[-T:/.]|\\s+')):
year, month, day, hour, minute, second = \
splitre.split(text + ':::::')[:6]
for a in 'year month day hour minute second'.split():
@@ -745,7 +736,7 @@ class VolumeAdjustmentSpec(Spec):
return value / 512.0, data[2:]
def write(self, config, frame, value):
number = int(round(value * 512))
number = intround(value * 512)
# pack only fails in 2.7, do it manually in 2.6
if not -32768 <= number <= 32767:
raise SpecError("not in range")
@@ -778,7 +769,7 @@ class VolumePeakSpec(Spec):
return (float(peak) / (2 ** 31 - 1)), data[1 + vol_bytes:]
def write(self, config, frame, value):
number = int(round(value * 32768))
number = intround(value * 32768)
# pack only fails in 2.7, do it manually in 2.6
if not 0 <= number <= 65535:
raise SpecError("not in range")

92
lib/mutagen/id3/_tags.py Executable file → Normal file
View File

@@ -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 itertools import zip_longest
from mutagen._tags import Tags
from mutagen._util import DictProxy, convert_error, read_full
from mutagen._compat import PY3, text_type, itervalues
from ._util import BitPaddedInt, unsynch, ID3JunkFrameError, \
ID3EncryptionUnsupportedError, is_valid_frame_id, error, \
@@ -82,10 +83,7 @@ class ID3Header(object):
if self.f_extended:
extsize_data = read_full(fileobj, 4)
if PY3:
frame_id = extsize_data.decode("ascii", "replace")
else:
frame_id = extsize_data
frame_id = extsize_data.decode("ascii", "replace")
if frame_id in Frames:
# Some tagger sets the extended header flag but
@@ -131,11 +129,10 @@ def determine_bpi(data, frames, EMPTY=b"\x00" * 10):
name, size, flags = struct.unpack('>4sLH', part)
size = BitPaddedInt(size)
o += 10 + size
if PY3:
try:
name = name.decode("ascii")
except UnicodeDecodeError:
continue
try:
name = name.decode("ascii")
except UnicodeDecodeError:
continue
if name in frames:
asbpi += 1
else:
@@ -151,11 +148,10 @@ def determine_bpi(data, frames, EMPTY=b"\x00" * 10):
break
name, size, flags = struct.unpack('>4sLH', part)
o += 10 + size
if PY3:
try:
name = name.decode("ascii")
except UnicodeDecodeError:
continue
try:
name = name.decode("ascii")
except UnicodeDecodeError:
continue
if name in frames:
asint += 1
else:
@@ -191,7 +187,7 @@ class ID3Tags(DictProxy, Tags):
order = ["TIT2", "TPE1", "TRCK", "TALB", "TPOS", "TDRC", "TCON"]
framedata = [
(f, save_frame(f, config=config)) for f in itervalues(self)]
(f, save_frame(f, config=config)) for f in self.values()]
def get_prio(frame):
try:
@@ -236,14 +232,14 @@ class ID3Tags(DictProxy, Tags):
return [self[key]]
else:
key = key + ":"
return [v for s, v in list(self.items()) if s.startswith(key)]
return [v for s, v in self.items() if s.startswith(key)]
def setall(self, key, values):
"""Delete frames of the given type and add frames in 'values'.
Args:
key (text): key for frames to delete
values (List[`Frame`]): frames to add
values (list[Frame]): frames to add
"""
self.delall(key)
@@ -280,7 +276,7 @@ class ID3Tags(DictProxy, Tags):
``POPM=user@example.org=3 128/255``
"""
frames = sorted(Frame.pprint(s) for s in list(self.values()))
frames = sorted(Frame.pprint(s) for s in self.values())
return "\n".join(frames)
def _add(self, frame, strict):
@@ -369,24 +365,23 @@ class ID3Tags(DictProxy, Tags):
self.__update_common()
# TDAT, TYER, and TIME have been turned into TDRC.
try:
date = text_type(self.get("TYER", ""))
if date.strip("\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 zip_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:
@@ -482,7 +477,7 @@ class ID3Tags(DictProxy, Tags):
def _copy(self):
"""Creates a shallow copy of all tags"""
items = list(self.items())
items = self.items()
subs = {}
for f in (self.getall("CHAP") + self.getall("CTOC")):
subs[f.HashKey] = f.sub_frames._copy()
@@ -533,8 +528,7 @@ def save_frame(frame, name=None, config=None):
frame_name = name
else:
frame_name = type(frame).__name__
if PY3:
frame_name = frame_name.encode("ascii")
frame_name = frame_name.encode("ascii")
header = struct.pack('>4s4sH', frame_name, datasize, flags)
return header + framedata
@@ -575,11 +569,10 @@ def read_frames(id3, data, frames):
if size == 0:
continue # drop empty frames
if PY3:
try:
name = name.decode('ascii')
except UnicodeDecodeError:
continue
try:
name = name.decode('ascii')
except UnicodeDecodeError:
continue
try:
# someone writes 2.3 frames with 2.2 names
@@ -614,11 +607,10 @@ def read_frames(id3, data, frames):
if size == 0:
continue # drop empty frames
if PY3:
try:
name = name.decode('ascii')
except UnicodeDecodeError:
continue
try:
name = name.decode('ascii')
except UnicodeDecodeError:
continue
try:
tag = frames[name]

16
lib/mutagen/id3/_util.py Executable file → Normal file
View File

@@ -8,7 +8,6 @@
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
from mutagen._compat import long_, integer_types, PY3
from mutagen._util import MutagenError
@@ -110,7 +109,7 @@ class _BitPaddedMixin(object):
mask = (((1 << (8 - bits)) - 1) << bits)
if isinstance(value, integer_types):
if isinstance(value, int):
while value:
if value & mask:
return False
@@ -133,7 +132,7 @@ class BitPaddedInt(int, _BitPaddedMixin):
numeric_value = 0
shift = 0
if isinstance(value, integer_types):
if isinstance(value, int):
if value < 0:
raise ValueError
while value:
@@ -149,21 +148,12 @@ class BitPaddedInt(int, _BitPaddedMixin):
else:
raise TypeError
if isinstance(numeric_value, int):
self = int.__new__(BitPaddedInt, numeric_value)
else:
self = long_.__new__(BitPaddedLong, numeric_value)
self = int.__new__(BitPaddedInt, numeric_value)
self.bits = bits
self.bigendian = bigendian
return self
if PY3:
BitPaddedLong = BitPaddedInt
else:
class BitPaddedLong(long_, _BitPaddedMixin):
pass
class ID3BadUnsynchData(error, ValueError):
"""Deprecated"""

4
lib/mutagen/m4a.py Executable file → Normal file
View File

@@ -66,7 +66,7 @@ class M4ATags(DictProxy, Tags):
raise error("deprecated")
def pprint(self):
return ""
return u""
class M4AInfo(StreamInfo):
@@ -77,7 +77,7 @@ class M4AInfo(StreamInfo):
raise error("deprecated")
def pprint(self):
return ""
return u""
class M4A(FileType):

8
lib/mutagen/monkeysaudio.py Executable file → Normal file
View File

@@ -18,10 +18,9 @@ __all__ = ["MonkeysAudio", "Open", "delete"]
import struct
from ._compat import endswith
from mutagen import StreamInfo
from mutagen.apev2 import APEv2File, error, delete
from mutagen._util import cdata, convert_error
from mutagen._util import cdata, convert_error, endswith
class MonkeysAudioHeaderError(error):
@@ -66,6 +65,9 @@ class MonkeysAudioInfo(StreamInfo):
blocks_per_frame = 73728
else:
blocks_per_frame = 9216
self.bits_per_sample = 0
if header[48:].startswith(b"WAVEfmt"):
self.bits_per_sample = struct.unpack("<H", header[74:76])[0]
self.version /= 1000.0
self.length = 0.0
if (self.sample_rate != 0) and (total_frames > 0):
@@ -74,7 +76,7 @@ class MonkeysAudioInfo(StreamInfo):
self.length = float(total_blocks) / self.sample_rate
def pprint(self):
return "Monkey's Audio %.2f, %.2f seconds, %d Hz" % (
return u"Monkey's Audio %.2f, %.2f seconds, %d Hz" % (
self.version, self.length, self.sample_rate)

40
lib/mutagen/mp3/__init__.py Executable file → Normal file
View File

@@ -12,8 +12,7 @@ import struct
from mutagen import StreamInfo
from mutagen._util import MutagenError, enum, BitReader, BitReaderError, \
convert_error
from mutagen._compat import endswith, xrange
convert_error, intround, endswith
from mutagen.id3 import ID3FileType, delete
from mutagen.id3._util import BitPaddedInt
@@ -165,11 +164,13 @@ class MPEGFrame(object):
# Try to find/parse the Xing header, which trumps the above length
# and bitrate calculation.
if self.layer == 3:
self._parse_vbr_header(fileobj, self.frame_offset, frame_size)
self._parse_vbr_header(fileobj, self.frame_offset, frame_size,
frame_length)
fileobj.seek(self.frame_offset + frame_length, 0)
def _parse_vbr_header(self, fileobj, frame_offset, frame_size):
def _parse_vbr_header(self, fileobj, frame_offset, frame_size,
frame_length):
"""Does not raise"""
# Xing
@@ -186,6 +187,12 @@ class MPEGFrame(object):
self.encoder_settings = xing.get_encoder_settings()
if xing.frames != -1:
samples = frame_size * xing.frames
if xing.bytes != -1 and samples > 0:
# the first frame is only included in xing.bytes but
# not in xing.frames, skip it.
audio_bytes = max(0, xing.bytes - frame_length)
self.bitrate = intround((
audio_bytes * 8 * self.sample_rate) / float(samples))
if lame is not None:
samples -= lame.encoder_delay_start
samples -= lame.encoder_padding_end
@@ -194,10 +201,8 @@ class MPEGFrame(object):
# files with low bitrate
samples = 0
self.length = float(samples) / self.sample_rate
if xing.bytes != -1 and self.length:
self.bitrate = int((xing.bytes * 8) / self.length)
if xing.lame_version_desc:
self.encoder_info = "LAME %s" % xing.lame_version_desc
self.encoder_info = u"LAME %s" % xing.lame_version_desc
if lame is not None:
self.track_gain = lame.track_gain_adjustment
self.track_peak = lame.track_peak
@@ -213,7 +218,7 @@ class MPEGFrame(object):
pass
else:
self.bitrate_mode = BitrateMode.VBR
self.encoder_info = "FhG"
self.encoder_info = u"FhG"
self.sketchy = False
self.length = float(frame_size * vbri.frames) / self.sample_rate
if self.length:
@@ -297,8 +302,10 @@ class MPEGInfo(StreamInfo):
Attributes:
length (`float`): audio length, in seconds
channels (`int`): number of audio channels
bitrate (`int`): audio bitrate, in bits per second
sample_rate (`int`) audio sample rate, in Hz
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
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
@@ -318,13 +325,12 @@ class MPEGInfo(StreamInfo):
layer (`int`): 1, 2, or 3
mode (`int`): One of STEREO, JOINTSTEREO, DUALCHANNEL, or MONO (0-3)
protected (`bool`): whether or not the file is "protected"
padding (`bool`) whether or not audio frames are padded
sketchy (`bool`): if true, the file may not be valid MPEG audio
"""
sketchy = False
encoder_info = ""
encoder_settings = ""
encoder_info = u""
encoder_settings = u""
bitrate_mode = BitrateMode.UNKNOWN
track_gain = track_peak = album_gain = album_peak = None
@@ -350,7 +356,7 @@ class MPEGInfo(StreamInfo):
# 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
@@ -411,16 +417,16 @@ class MPEGInfo(StreamInfo):
def pprint(self):
info = str(self.bitrate_mode).split(".", 1)[-1]
if self.bitrate_mode == BitrateMode.UNKNOWN:
info = "CBR?"
info = u"CBR?"
if self.encoder_info:
info += ", %s" % self.encoder_info
if self.encoder_settings:
info += ", %s" % self.encoder_settings
s = "MPEG %s layer %d, %d bps (%s), %s Hz, %d chn, %.2f seconds" % (
s = u"MPEG %s layer %d, %d bps (%s), %s Hz, %d chn, %.2f seconds" % (
self.version, self.layer, self.bitrate, info,
self.sample_rate, self.channels, self.length)
if self.sketchy:
s += " (sketchy)"
s += u" (sketchy)"
return s

94
lib/mutagen/mp3/_util.py Executable file → Normal file
View File

@@ -11,10 +11,11 @@ http://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header
http://wiki.hydrogenaud.io/index.php?title=MP3
"""
from __future__ import division
from functools import partial
from io import BytesIO
from mutagen._util import cdata, BitReader
from mutagen._compat import xrange, iterbytes, cBytesIO
from mutagen._util import cdata, BitReader, iterbytes
class LAMEError(Exception):
@@ -37,7 +38,9 @@ class LAMEHeader(object):
"""VBR quality: 0..9"""
track_peak = None
"""Peak signal amplitude as float. None if unknown."""
"""Peak signal amplitude as float. 1.0 is maximal signal amplitude
in decoded format. None if unknown.
"""
track_gain_origin = 0
"""see the docs"""
@@ -106,7 +109,7 @@ class LAMEHeader(object):
raise LAMEError("Not enough data")
# extended lame header
r = BitReader(cBytesIO(payload))
r = BitReader(BytesIO(payload))
revision = r.bits(4)
if revision != 0:
raise LAMEError("unsupported header revision %d" % revision)
@@ -123,8 +126,7 @@ class LAMEHeader(object):
self.track_peak = None
else:
# see PutLameVBR() in LAME's VbrTag.c
self.track_peak = (
cdata.uint32_be(track_peak_data) - 0.5) / 2 ** 23
self.track_peak = cdata.uint32_be(track_peak_data) / 2 ** 23
track_gain_type = r.bits(3)
self.track_gain_origin = r.bits(3)
sign = r.bits(1)
@@ -194,64 +196,64 @@ class LAMEHeader(object):
if self.vbr_method == 2:
if version in ((3, 90), (3, 91), (3, 92)) and self.encoding_flags:
if self.bitrate < 255:
return "--alt-preset %d" % self.bitrate
return u"--alt-preset %d" % self.bitrate
else:
return "--alt-preset %d+" % self.bitrate
return u"--alt-preset %d+" % self.bitrate
if self.preset_used != 0:
return "--preset %d" % self.preset_used
return u"--preset %d" % self.preset_used
elif self.bitrate < 255:
return "--abr %d" % self.bitrate
return u"--abr %d" % self.bitrate
else:
return "--abr %d+" % self.bitrate
return u"--abr %d+" % self.bitrate
elif self.vbr_method == 1:
if self.preset_used == 0:
if self.bitrate < 255:
return "-b %d" % self.bitrate
return u"-b %d" % self.bitrate
else:
return "-b 255+"
return u"-b 255+"
elif self.preset_used == 1003:
return "--preset insane"
return "-b %d" % self.preset_used
return u"--preset insane"
return u"-b %d" % self.preset_used
elif version in ((3, 90), (3, 91), (3, 92)):
preset_key = (self.vbr_quality, self.quality, self.vbr_method,
self.lowpass_filter, self.ath_type)
if preset_key == (1, 2, 4, 19500, 3):
return "--preset r3mix"
return u"--preset r3mix"
if preset_key == (2, 2, 3, 19000, 4):
return "--alt-preset standard"
return u"--alt-preset standard"
if preset_key == (2, 2, 3, 19500, 2):
return "--alt-preset extreme"
return u"--alt-preset extreme"
if self.vbr_method == 3:
return "-V %s" % self.vbr_quality
return u"-V %s" % self.vbr_quality
elif self.vbr_method in (4, 5):
return "-V %s --vbr-new" % self.vbr_quality
return u"-V %s --vbr-new" % self.vbr_quality
elif version in ((3, 93), (3, 94), (3, 95), (3, 96), (3, 97)):
if self.preset_used == 1001:
return "--preset standard"
return u"--preset standard"
elif self.preset_used == 1002:
return "--preset extreme"
return u"--preset extreme"
elif self.preset_used == 1004:
return "--preset fast standard"
return u"--preset fast standard"
elif self.preset_used == 1005:
return "--preset fast extreme"
return u"--preset fast extreme"
elif self.preset_used == 1006:
return "--preset medium"
return u"--preset medium"
elif self.preset_used == 1007:
return "--preset fast medium"
return u"--preset fast medium"
if self.vbr_method == 3:
return "-V %s" % self.vbr_quality
return u"-V %s" % self.vbr_quality
elif self.vbr_method in (4, 5):
return "-V %s --vbr-new" % self.vbr_quality
return u"-V %s --vbr-new" % self.vbr_quality
elif version == (3, 98):
if self.vbr_method == 3:
return "-V %s --vbr-old" % self.vbr_quality
return u"-V %s --vbr-old" % self.vbr_quality
elif self.vbr_method in (4, 5):
return "-V %s" % self.vbr_quality
return u"-V %s" % self.vbr_quality
elif version >= (3, 99):
if self.vbr_method == 3:
return "-V %s --vbr-old" % self.vbr_quality
return u"-V %s --vbr-old" % self.vbr_quality
elif self.vbr_method in (4, 5):
p = self.vbr_quality
adjust_key = (p, self.bitrate, self.lowpass_filter)
@@ -261,9 +263,9 @@ class LAMEHeader(object):
(5, 8, 0): 8,
(6, 8, 0): 9,
}.get(adjust_key, p)
return "-V %s" % p
return u"-V %s" % p
return ""
return u""
@classmethod
def parse_version(cls, fileobj):
@@ -303,36 +305,36 @@ class LAMEHeader(object):
if (major, minor) < (3, 90) or (
(major, minor) == (3, 90) and data[-11:-10] == b"("):
flag = data.strip(b"\x00").rstrip().decode("ascii")
return (major, minor), "%d.%d%s" % (major, minor, flag), False
return (major, minor), u"%d.%d%s" % (major, minor, flag), False
if len(data) < 11:
raise LAMEError("Invalid version: too long")
flag = data[:-11].rstrip(b"\x00")
flag_string = ""
patch = ""
flag_string = u""
patch = u""
if flag == b"a":
flag_string = " (alpha)"
flag_string = u" (alpha)"
elif flag == b"b":
flag_string = " (beta)"
flag_string = u" (beta)"
elif flag == b"r":
patch = ".1+"
patch = u".1+"
elif flag == b" ":
if (major, minor) > (3, 96):
patch = ".0"
patch = u".0"
else:
patch = ".0+"
patch = u".0+"
elif flag == b"" or flag == b".":
patch = ".0+"
patch = u".0+"
else:
flag_string = " (?)"
flag_string = u" (?)"
# extended header, seek back to 9 bytes for the caller
fileobj.seek(-11, 1)
return (major, minor), \
"%d.%d%s%s" % (major, minor, patch, flag_string), True
u"%d.%d%s%s" % (major, minor, patch, flag_string), True
class XingHeaderError(Exception):
@@ -369,7 +371,7 @@ class XingHeader(object):
lame_version = (0, 0)
"""The LAME version as two element tuple (major, minor)"""
lame_version_desc = ""
lame_version_desc = u""
"""The version of the LAME encoder e.g. '3.99.0'. Empty if unknown"""
is_info = False
@@ -425,7 +427,7 @@ class XingHeader(object):
"""Returns the guessed encoder settings"""
if self.lame_header is None:
return ""
return u""
return self.lame_header.guess_settings(*self.lame_version)
@classmethod

233
lib/mutagen/mp4/__init__.py Executable file → Normal file
View File

@@ -25,13 +25,15 @@ were all consulted.
import struct
import sys
from io import BytesIO
from collections.abc import Sequence
from datetime import timedelta
from mutagen import FileType, Tags, StreamInfo, PaddingInfo
from mutagen._constants import GENRES
from mutagen._util import cdata, insert_bytes, DictProxy, MutagenError, \
hashable, enum, get_size, resize_bytes, loadfile, convert_error
from mutagen._compat import (reraise, PY2, string_types, text_type, chr_,
iteritems, PY3, cBytesIO, izip, xrange)
hashable, enum, get_size, resize_bytes, loadfile, convert_error, bchr, \
reraise
from ._atom import Atoms, Atom, AtomError
from ._util import parse_full_atom
from ._as_entry import AudioSampleEntry, ASEntryError
@@ -205,14 +207,10 @@ class MP4FreeForm(bytes):
def _name2key(name):
if PY2:
return name
return name.decode("latin-1")
def _key2name(key):
if PY2:
return key
return key.encode("latin-1")
@@ -290,6 +288,8 @@ class MP4Tags(DictProxy, Tags):
* 'soco' -- composer sort order
* 'sosn' -- show sort order
* 'tvsh' -- show name
* '\\xa9wrk' -- work
* '\\xa9mvn' -- movement
Boolean values:
@@ -309,6 +309,7 @@ class MP4Tags(DictProxy, Tags):
* '\\xa9mvi' -- Movement Index
* 'shwm' -- work/movement
* 'stik' -- Media Kind
* 'hdvd' -- HD Video
* 'rtng' -- Content Rating
* 'tves' -- TV Episode
* 'tvsn' -- TV Season
@@ -390,17 +391,17 @@ class MP4Tags(DictProxy, Tags):
@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething, padding=None):
def save(self, filething=None, padding=None):
values = []
items = sorted(list(self.items()), key=lambda kv: _item_sort_key(*kv))
items = sorted(self.items(), key=lambda kv: _item_sort_key(*kv))
for key, value in items:
try:
values.append(self._render(key, value))
except (TypeError, ValueError) as s:
reraise(MP4MetadataValueError, s, sys.exc_info()[2])
for key, failed in iteritems(self._failed_atoms):
for key, failed in self._failed_atoms.items():
# don't write atoms back if we have added a new one with
# the same name, this excludes freeform which can have
# multiple atoms with the same key (most parsers seem to be able
@@ -560,6 +561,9 @@ class MP4Tags(DictProxy, Tags):
if len(head) != 12:
raise MP4MetadataError("truncated atom % r" % atom.name)
length, name = struct.unpack(">I4s", head[:8])
if length < 1:
raise MP4MetadataError(
"atom %r has a length of zero" % atom.name)
version = ord(head[8:9])
flags = struct.unpack(">I", b"\x00" + head[9:12])[0]
if name != b"data":
@@ -599,7 +603,9 @@ class MP4Tags(DictProxy, Tags):
if atom_name != b"data":
raise MP4MetadataError(
"unexpected atom %r inside %r" % (atom_name, atom.name))
if length < 1:
raise MP4MetadataError(
"atom %r has a length of zero" % atom.name)
version = ord(data[pos + 8:pos + 8 + 1])
flags = struct.unpack(">I", b"\x00" + data[pos + 9:pos + 12])[0]
value.append(MP4FreeForm(data[pos + 16:pos + length],
@@ -714,7 +720,8 @@ class MP4Tags(DictProxy, Tags):
# by itunes for compatibility.
if cdata.int8_min <= v <= cdata.int8_max and min_bytes <= 1:
data = cdata.to_int8(v)
if cdata.int16_min <= v <= cdata.int16_max and min_bytes <= 2:
elif cdata.int16_min <= v <= cdata.int16_max and \
min_bytes <= 2:
data = cdata.to_int16_be(v)
elif cdata.int32_min <= v <= cdata.int32_max and \
min_bytes <= 4:
@@ -743,7 +750,7 @@ class MP4Tags(DictProxy, Tags):
def __render_bool(self, key, value):
return self.__render_data(
key, 0, AtomDataType.INTEGER, [chr_(bool(value))])
key, 0, AtomDataType.INTEGER, [bchr(bool(value))])
def __parse_cover(self, atom, data):
values = []
@@ -757,6 +764,9 @@ class MP4Tags(DictProxy, Tags):
continue
raise MP4MetadataError(
"unexpected atom %r inside 'covr'" % name)
if length < 1:
raise MP4MetadataError(
"atom %r has a length of zero" % atom.name)
if imageformat not in (MP4Cover.FORMAT_JPEG, MP4Cover.FORMAT_PNG):
# Sometimes AtomDataType.IMPLICIT or simply wrong.
# In all cases it was jpeg, so default to it
@@ -804,18 +814,14 @@ class MP4Tags(DictProxy, Tags):
self.__add(key, values)
def __render_text(self, key, value, flags=AtomDataType.UTF8):
if isinstance(value, string_types):
if isinstance(value, str):
value = [value]
encoded = []
for v in value:
if not isinstance(v, text_type):
if PY3:
raise TypeError("%r not str" % v)
try:
v = v.decode("utf-8")
except (AttributeError, UnicodeDecodeError) as e:
raise TypeError(e)
if not isinstance(v, str):
raise TypeError("%r not str" % v)
encoded.append(v.encode("utf-8"))
return self.__render_data(key, 0, flags, encoded)
@@ -849,6 +855,7 @@ class MP4Tags(DictProxy, Tags):
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),
@@ -866,24 +873,141 @@ class MP4Tags(DictProxy, Tags):
def pprint(self):
def to_line(key, value):
assert isinstance(key, text_type)
if isinstance(value, text_type):
return "%s=%s" % (key, value)
return "%s=%r" % (key, value)
assert isinstance(key, str)
if isinstance(value, str):
return u"%s=%s" % (key, value)
return u"%s=%r" % (key, value)
values = []
for key, value in sorted(iteritems(self)):
if not isinstance(key, text_type):
for key, value in sorted(self.items()):
if not isinstance(key, str):
key = key.decode("latin-1")
if key == "covr":
values.append("%s=%s" % (key, ", ".join(
["[%d bytes of data]" % len(data) for data in value])))
values.append(u"%s=%s" % (key, u", ".join(
[u"[%d bytes of data]" % len(data) for data in value])))
elif isinstance(value, list):
for v in value:
values.append(to_line(key, v))
else:
values.append(to_line(key, value))
return "\n".join(values)
return u"\n".join(values)
class Chapter(object):
"""Chapter()
Chapter information container
"""
def __init__(self, start, title):
self.start = start
self.title = title
class MP4Chapters(Sequence):
"""MP4Chapters()
MPEG-4 Chapter information.
Supports the 'moov.udta.chpl' box.
A sequence of Chapter objects with the following members:
start (`float`): position from the start of the file in seconds
title (`str`): title of the chapter
"""
def __init__(self, *args, **kwargs):
self._timescale = None
self._duration = None
self._chapters = []
super(MP4Chapters, self).__init__()
if args or kwargs:
self.load(*args, **kwargs)
def __len__(self):
return self._chapters.__len__()
def __getitem__(self, key):
return self._chapters.__getitem__(key)
def load(self, atoms, fileobj):
try:
mvhd = atoms.path(b"moov", b"mvhd")[-1]
except KeyError as key:
return MP4MetadataError(key)
self._parse_mvhd(mvhd, fileobj)
if not self._timescale:
raise MP4MetadataError("Unable to get timescale")
try:
chpl = atoms.path(b"moov", b"udta", b"chpl")[-1]
except KeyError as key:
return MP4MetadataError(key)
self._parse_chpl(chpl, fileobj)
@classmethod
def _can_load(cls, atoms):
return b"moov.udta.chpl" in atoms and b"moov.mvhd" in atoms
def _parse_mvhd(self, atom, fileobj):
assert atom.name == b"mvhd"
ok, data = atom.read(fileobj)
if not ok:
raise MP4StreamInfoError("Invalid mvhd")
version = data[0]
pos = 4
if version == 0:
pos += 8 # created, modified
self._timescale = struct.unpack(">l", data[pos:pos + 4])[0]
pos += 4
self._duration = struct.unpack(">l", data[pos:pos + 4])[0]
pos += 4
elif version == 1:
pos += 16 # created, modified
self._timescale = struct.unpack(">l", data[pos:pos + 4])[0]
pos += 4
self._duration = struct.unpack(">q", data[pos:pos + 8])[0]
pos += 8
def _parse_chpl(self, atom, fileobj):
assert atom.name == b"chpl"
ok, data = atom.read(fileobj)
if not ok:
raise MP4StreamInfoError("Invalid atom")
chapters = data[8]
pos = 9
for i in range(chapters):
start = struct.unpack(">Q", data[pos:pos + 8])[0] / 10000
pos += 8
title_len = data[pos]
pos += 1
try:
title = data[pos:pos + title_len].decode()
except UnicodeDecodeError as e:
raise MP4MetadataError("chapter %d title: %s" % (i, e))
pos += title_len
self._chapters.append(Chapter(start / self._timescale, title))
def pprint(self):
chapters = ["%s %s" % (timedelta(seconds=chapter.start), chapter.title)
for chapter in self._chapters]
return "chapters=%s" % '\n '.join(chapters)
class MP4Info(StreamInfo):
@@ -915,8 +1039,8 @@ class MP4Info(StreamInfo):
channels = 0
sample_rate = 0
bits_per_sample = 0
codec = ""
codec_description = ""
codec = u""
codec_description = u""
def __init__(self, *args, **kwargs):
if args or kwargs:
@@ -1001,7 +1125,7 @@ class MP4Info(StreamInfo):
return
# look at the first entry if there is one
entry_fileobj = cBytesIO(data[offset:])
entry_fileobj = BytesIO(data[offset:])
try:
entry_atom = Atom(entry_fileobj)
except AtomError as e:
@@ -1041,6 +1165,7 @@ class MP4(FileType):
"""
MP4Tags = MP4Tags
MP4Chapters = MP4Chapters
_mimes = ["audio/mp4", "audio/x-m4a", "audio/mpeg4", "audio/aac"]
@@ -1065,7 +1190,6 @@ class MP4(FileType):
if not MP4Tags._can_load(atoms):
self.tags = None
self._padding = 0
else:
try:
self.tags = self.MP4Tags(atoms, fileobj)
@@ -1073,14 +1197,51 @@ class MP4(FileType):
raise
except Exception as err:
reraise(MP4MetadataError, err, sys.exc_info()[2])
else:
self._padding = self.tags._padding
if not MP4Chapters._can_load(atoms):
self.chapters = None
else:
try:
self.chapters = self.MP4Chapters(atoms, fileobj)
except error:
raise
except Exception as err:
reraise(MP4MetadataError, err, sys.exc_info()[2])
@property
def _padding(self):
if self.tags is None:
return 0
else:
return self.tags._padding
def save(self, *args, **kwargs):
"""save(filething=None, padding=None)"""
super(MP4, self).save(*args, **kwargs)
def pprint(self):
"""
Returns:
text: stream information, comment key=value pairs and chapters.
"""
stream = "%s (%s)" % (self.info.pprint(), self.mime[0])
try:
tags = self.tags.pprint()
except AttributeError:
pass
else:
stream += ((tags and "\n" + tags) or "")
try:
chapters = self.chapters.pprint()
except AttributeError:
pass
else:
stream += "\n" + chapters
return stream
def add_tags(self):
if self.tags is None:
self.tags = self.MP4Tags()

28
lib/mutagen/mp4/_as_entry.py Executable file → Normal file
View File

@@ -6,10 +6,10 @@
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
from mutagen._compat import cBytesIO, xrange
from io import BytesIO
from mutagen.aac import ProgramConfigElement
from mutagen._util import BitReader, BitReaderError, cdata
from mutagen._compat import text_type
from ._util import parse_full_atom
from ._atom import Atom, AtomError
@@ -47,7 +47,7 @@ class AudioSampleEntry(object):
if not ok:
raise ASEntryError("too short %r atom" % atom.name)
fileobj = cBytesIO(data)
fileobj = BytesIO(data)
r = BitReader(fileobj)
try:
@@ -93,7 +93,7 @@ class AudioSampleEntry(object):
ok, data = atom.read(fileobj)
if not ok:
raise ASEntryError("truncated %s atom" % atom.name)
fileobj = cBytesIO(data)
fileobj = BytesIO(data)
r = BitReader(fileobj)
# sample_rate in AudioSampleEntry covers values in
@@ -134,7 +134,7 @@ class AudioSampleEntry(object):
if version != 0:
raise ASEntryError("Unsupported version %d" % version)
fileobj = cBytesIO(data)
fileobj = BytesIO(data)
r = BitReader(fileobj)
try:
@@ -168,7 +168,7 @@ class AudioSampleEntry(object):
if version != 0:
raise ASEntryError("Unsupported version %d" % version)
fileobj = cBytesIO(data)
fileobj = BytesIO(data)
r = BitReader(fileobj)
try:
@@ -239,9 +239,13 @@ class BaseDescriptor(object):
pos = fileobj.tell()
instance = cls(fileobj, length)
left = length - (fileobj.tell() - pos)
if left < 0:
raise DescriptorError("descriptor parsing read too much data")
fileobj.seek(left, 1)
if left > 0:
fileobj.seek(left, 1)
else:
# XXX: In case the instance length is shorted than the content
# assume the size is wrong and just continue parsing
# https://github.com/quodlibet/mutagen/issues/444
pass
return instance
@@ -318,10 +322,10 @@ class DecoderConfigDescriptor(BaseDescriptor):
def codec_param(self):
"""string"""
param = ".%X" % self.objectTypeIndication
param = u".%X" % self.objectTypeIndication
info = self.decSpecificInfo
if info is not None:
param += ".%d" % info.audioObjectType
param += u".%d" % info.audioObjectType
return param
@property
@@ -371,7 +375,7 @@ class DecoderSpecificInfo(BaseDescriptor):
name += "+SBR"
if self.psPresentFlag == 1:
name += "+PS"
return text_type(name)
return str(name)
@property
def sample_rate(self):

9
lib/mutagen/mp4/_atom.py Executable file → Normal file
View File

@@ -8,7 +8,6 @@
import struct
from mutagen._compat import PY2
from mutagen._util import convert_error
# This is not an exhaustive list of container atoms, but just the
@@ -180,12 +179,8 @@ class Atoms(object):
specifying the complete path ('moov.udta').
"""
if PY2:
if isinstance(names, str):
names = names.split(b".")
else:
if isinstance(names, bytes):
names = names.split(b".")
if isinstance(names, bytes):
names = names.split(b".")
for child in self.atoms:
if child.name == names[0]:

0
lib/mutagen/mp4/_util.py Executable file → Normal file
View File

23
lib/mutagen/musepack.py Executable file → Normal file
View File

@@ -19,11 +19,10 @@ __all__ = ["Musepack", "Open", "delete"]
import struct
from ._compat import endswith, xrange
from mutagen import StreamInfo
from mutagen.apev2 import APEv2File, error, delete
from mutagen.id3._util import BitPaddedInt
from mutagen._util import cdata, convert_error
from mutagen._util import cdata, convert_error, intround, endswith
class MusepackHeaderError(error):
@@ -118,7 +117,7 @@ class MusepackInfo(StreamInfo):
if not self.bitrate and self.length != 0:
fileobj.seek(0, 2)
self.bitrate = int(round(fileobj.tell() * 8 / self.length))
self.bitrate = intround(fileobj.tell() * 8 / self.length)
def __parse_sv8(self, fileobj):
# SV8 http://trac.musepack.net/trac/wiki/SV8Specification
@@ -143,9 +142,13 @@ class MusepackInfo(StreamInfo):
# packets can be at maximum data_size big and are padded with zeros
if frame_type == b"SH":
if frame_type not in mandatory_packets:
raise MusepackHeaderError("Duplicate SH packet")
mandatory_packets.remove(frame_type)
self.__parse_stream_header(fileobj, data_size)
elif frame_type == b"RG":
if frame_type not in mandatory_packets:
raise MusepackHeaderError("Duplicate RG packet")
mandatory_packets.remove(frame_type)
self.__parse_replaygain_packet(fileobj, data_size)
else:
@@ -184,9 +187,13 @@ class MusepackInfo(StreamInfo):
remaining_size -= l1 + l2
data = fileobj.read(remaining_size)
if len(data) != remaining_size:
if len(data) != remaining_size or len(data) < 2:
raise MusepackHeaderError("SH packet ended unexpectedly.")
self.sample_rate = RATES[bytearray(data)[0] >> 5]
rate_index = (bytearray(data)[0] >> 5)
try:
self.sample_rate = RATES[rate_index]
except IndexError:
raise MusepackHeaderError("Invalid sample rate")
self.channels = (bytearray(data)[1] >> 4) + 1
def __parse_replaygain_packet(self, fileobj, data_size):
@@ -253,12 +260,12 @@ class MusepackInfo(StreamInfo):
def pprint(self):
rg_data = []
if hasattr(self, "title_gain"):
rg_data.append("%+0.2f (title)" % self.title_gain)
rg_data.append(u"%+0.2f (title)" % self.title_gain)
if hasattr(self, "album_gain"):
rg_data.append("%+0.2f (album)" % self.album_gain)
rg_data.append(u"%+0.2f (album)" % self.album_gain)
rg_data = (rg_data and ", Gain: " + ", ".join(rg_data)) or ""
return "Musepack SV%d, %.2f seconds, %d Hz, %d bps%s" % (
return u"Musepack SV%d, %.2f seconds, %d Hz, %d bps%s" % (
self.version, self.length, self.sample_rate, self.bitrate, rg_data)

60
lib/mutagen/ogg.py Executable file → Normal file
View File

@@ -19,10 +19,11 @@ http://www.xiph.org/ogg/doc/rfc3533.txt.
import struct
import sys
import zlib
from io import BytesIO
from mutagen import FileType
from mutagen._util import cdata, resize_bytes, MutagenError, loadfile, seek_end
from ._compat import cBytesIO, reraise, chr_, izip, xrange
from mutagen._util import cdata, resize_bytes, MutagenError, loadfile, \
seek_end, bchr, reraise
class error(MutagenError):
@@ -37,7 +38,7 @@ class OggPage(object):
A page is a header of 26 bytes, followed by the length of the
data, followed by the data.
The constructor is givin a file-like object pointing to the start
The constructor is given a file-like object pointing to the start
of an Ogg page. After the constructor is finished it is pointing
to the start of the next page.
@@ -50,7 +51,7 @@ class OggPage(object):
offset (`int` or `None`): offset this page was read from (default None)
complete (`bool`): if the last packet on this page is complete
(default True)
packets (List[`bytes`]): list of raw packet data (default [])
packets (list[bytes]): list of raw packet data (default [])
Note that if 'complete' is false, the next page's 'continued'
property must be true (so set both when constructing pages).
@@ -145,11 +146,11 @@ class OggPage(object):
lacing_data = []
for datum in self.packets:
quot, rem = divmod(len(datum), 255)
lacing_data.append(b"\xff" * quot + chr_(rem))
lacing_data.append(b"\xff" * quot + bchr(rem))
lacing_data = b"".join(lacing_data)
if not self.complete and lacing_data.endswith(b"\x00"):
lacing_data = lacing_data[:-1]
data.append(chr_(len(lacing_data)))
data.append(bchr(len(lacing_data)))
data.append(lacing_data)
data.extend(self.packets)
data = b"".join(data)
@@ -216,7 +217,7 @@ class OggPage(object):
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).
"""
@@ -267,11 +268,12 @@ class OggPage(object):
else:
sequence += 1
if page.continued:
packets[-1].append(page.packets[0])
else:
packets.append([page.packets[0]])
packets.extend([p] for p in page.packets[1:])
if page.packets:
if page.continued:
packets[-1].append(page.packets[0])
else:
packets.append([page.packets[0]])
packets.extend([p] for p in page.packets[1:])
return [b"".join(p) for p in packets]
@@ -388,7 +390,7 @@ class OggPage(object):
# Number the new pages starting from the first old page.
first = old_pages[0].sequence
for page, seq in zip(new_pages,
range(first, first + len(new_pages))):
range(first, first + len(new_pages))):
page.sequence = seq
page.serial = old_pages[0].serial
@@ -434,7 +436,7 @@ class OggPage(object):
cls.renumber(fileobj, serial, sequence)
@staticmethod
def find_last(fileobj, serial):
def find_last(fileobj, serial, finishing=False):
"""Find the last page of the stream 'serial'.
If the file is not multiplexed this function is fast. If it is,
@@ -443,6 +445,10 @@ class OggPage(object):
This finds the last page in the actual file object, or the last
page in the stream (with eos set), whichever comes first.
If finishing is True it returns the last page which contains a packet
finishing on it. If there exist pages but none with finishing packets
returns None.
Returns None in case no page with the serial exists.
Raises error in case this isn't a valid ogg stream.
Raises IOError.
@@ -456,14 +462,18 @@ class OggPage(object):
index = data.rindex(b"OggS")
except ValueError:
raise error("unable to find final Ogg header")
bytesobj = cBytesIO(data[index:])
bytesobj = BytesIO(data[index:])
def is_valid(page):
return not finishing or page.position != -1
best_page = None
try:
page = OggPage(bytesobj)
except error:
pass
else:
if page.serial == serial:
if page.serial == serial and is_valid(page):
if page.last:
return page
else:
@@ -475,12 +485,14 @@ class OggPage(object):
fileobj.seek(0)
try:
page = OggPage(fileobj)
while not page.last:
while True:
if page.serial == serial:
if is_valid(page):
best_page = page
if page.last:
break
page = OggPage(fileobj)
while page.serial != serial:
page = OggPage(fileobj)
best_page = page
return page
return best_page
except error:
return best_page
except EOFError:
@@ -525,7 +537,7 @@ class OggFileType(FileType):
raise self._Error("no appropriate stream found")
@loadfile(writable=True)
def delete(self, filething):
def delete(self, filething=None):
"""delete(filething=None)
Remove tags from a file.
@@ -557,7 +569,7 @@ class OggFileType(FileType):
raise self._Error
@loadfile(writable=True)
def save(self, filething, padding=None):
def save(self, filething=None, padding=None):
"""save(filething=None, padding=None)
Save a tag to a file.
@@ -566,7 +578,7 @@ class OggFileType(FileType):
Args:
filething (filething)
padding (PaddingFunction)
padding (:obj:`mutagen.PaddingFunction`)
Raises:
mutagen.MutagenError
"""

13
lib/mutagen/oggflac.py Executable file → Normal file
View File

@@ -18,8 +18,7 @@ http://flac.sourceforge.net/ogg_mapping.html.
__all__ = ["OggFLAC", "Open", "delete"]
import struct
from ._compat import cBytesIO
from io import BytesIO
from mutagen import StreamInfo
from mutagen.flac import StreamInfo as FLACStreamInfo, error as FLACError
@@ -65,7 +64,7 @@ class OggFLACStreamInfo(StreamInfo):
self.serial = page.serial
# Skip over the block header.
stringobj = cBytesIO(page.packets[0][17:])
stringobj = BytesIO(page.packets[0][17:])
try:
flac_info = FLACStreamInfo(stringobj)
@@ -79,11 +78,13 @@ class OggFLACStreamInfo(StreamInfo):
def _post_tags(self, fileobj):
if self.length:
return
page = OggPage.find_last(fileobj, self.serial)
page = OggPage.find_last(fileobj, self.serial, finishing=True)
if page is None:
raise OggFLACHeaderError
self.length = page.position / float(self.sample_rate)
def pprint(self):
return "Ogg FLAC, %.2f seconds, %d Hz" % (
return u"Ogg FLAC, %.2f seconds, %d Hz" % (
self.length, self.sample_rate)
@@ -99,7 +100,7 @@ class OggFLACVComment(VCommentDict):
if page.serial == info.serial:
pages.append(page)
complete = page.complete or (len(page.packets) > 1)
comment = cBytesIO(OggPage.to_packets(pages)[0][4:])
comment = BytesIO(OggPage.to_packets(pages)[0][4:])
super(OggFLACVComment, self).__init__(comment, framing=False)
def _inject(self, fileobj, padding_func):

6
lib/mutagen/oggopus.py Executable file → Normal file
View File

@@ -17,9 +17,9 @@ Based on http://tools.ietf.org/html/draft-terriberry-oggopus-01
__all__ = ["OggOpus", "Open", "delete"]
import struct
from io import BytesIO
from mutagen import StreamInfo
from mutagen._compat import BytesIO
from mutagen._util import get_size, loadfile, convert_error
from mutagen._tags import PaddingInfo
from mutagen._vorbis import VCommentDict
@@ -69,13 +69,13 @@ class OggOpusInfo(StreamInfo):
raise OggOpusHeaderError("version %r unsupported" % major)
def _post_tags(self, fileobj):
page = OggPage.find_last(fileobj, self.serial)
page = OggPage.find_last(fileobj, self.serial, finishing=True)
if page is None:
raise OggOpusHeaderError
self.length = (page.position - self.__pre_skip) / float(48000)
def pprint(self):
return "Ogg Opus, %.2f seconds" % (self.length)
return u"Ogg Opus, %.2f seconds" % (self.length)
class OggOpusVComment(VCommentDict):

4
lib/mutagen/oggspeex.py Executable file → Normal file
View File

@@ -64,13 +64,13 @@ class OggSpeexInfo(StreamInfo):
self.serial = page.serial
def _post_tags(self, fileobj):
page = OggPage.find_last(fileobj, self.serial)
page = OggPage.find_last(fileobj, self.serial, finishing=True)
if page is None:
raise OggSpeexHeaderError
self.length = page.position / float(self.sample_rate)
def pprint(self):
return "Ogg Speex, %.2f seconds" % self.length
return u"Ogg Speex, %.2f seconds" % self.length
class OggSpeexVComment(VCommentDict):

20
lib/mutagen/oggtheora.py Executable file → Normal file
View File

@@ -50,33 +50,39 @@ class OggTheoraInfo(StreamInfo):
def __init__(self, fileobj):
page = OggPage(fileobj)
while not page.packets[0].startswith(b"\x80theora"):
while not page.packets or \
not page.packets[0].startswith(b"\x80theora"):
page = OggPage(fileobj)
if not page.first:
raise OggTheoraHeaderError(
"page has ID header, but doesn't start a stream")
data = page.packets[0]
if len(data) < 42:
raise OggTheoraHeaderError("Truncated header")
vmaj, vmin = struct.unpack("2B", data[7:9])
if (vmaj, vmin) != (3, 2):
raise OggTheoraHeaderError(
"found Theora version %d.%d != 3.2" % (vmaj, vmin))
fps_num, fps_den = struct.unpack(">2I", data[22:30])
if not fps_den or not fps_num:
raise OggTheoraHeaderError("FRN or FRD is equal to zero")
self.fps = fps_num / float(fps_den)
self.bitrate = cdata.uint_be(b"\x00" + data[37:40])
self.granule_shift = (cdata.ushort_be(data[40:42]) >> 5) & 0x1F
self.serial = page.serial
def _post_tags(self, fileobj):
page = OggPage.find_last(fileobj, self.serial)
page = OggPage.find_last(fileobj, self.serial, finishing=True)
if page is None:
raise OggTheoraHeaderError
position = page.position
mask = (1 << self.granule_shift) - 1
frames = (position >> self.granule_shift) + (position & mask)
assert self.fps
self.length = frames / float(self.fps)
def pprint(self):
return "Ogg Theora, %.2f seconds, %d bps" % (self.length,
return u"Ogg Theora, %.2f seconds, %d bps" % (self.length,
self.bitrate)
@@ -91,7 +97,10 @@ class OggTheoraCommentDict(VCommentDict):
if page.serial == info.serial:
pages.append(page)
complete = page.complete or (len(page.packets) > 1)
data = OggPage.to_packets(pages)[0][7:]
packets = OggPage.to_packets(pages)
if not packets:
raise error("Missing metadata packet")
data = packets[0][7:]
super(OggTheoraCommentDict, self).__init__(data, framing=False)
self._padding = len(data) - self._size
@@ -100,7 +109,8 @@ class OggTheoraCommentDict(VCommentDict):
fileobj.seek(0)
page = OggPage(fileobj)
while not page.packets[0].startswith(b"\x81theora"):
while not page.packets or \
not page.packets[0].startswith(b"\x81theora"):
page = OggPage(fileobj)
old_pages = [page]

15
lib/mutagen/oggvorbis.py Executable file → Normal file
View File

@@ -43,7 +43,7 @@ class OggVorbisInfo(StreamInfo):
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
"""
@@ -56,13 +56,20 @@ class OggVorbisInfo(StreamInfo):
"""Raises ogg.error, IOError"""
page = OggPage(fileobj)
if not page.packets:
raise OggVorbisHeaderError("page has not packets")
while not page.packets[0].startswith(b"\x01vorbis"):
page = OggPage(fileobj)
if not page.first:
raise OggVorbisHeaderError(
"page has ID header, but doesn't start a stream")
if len(page.packets[0]) < 28:
raise OggVorbisHeaderError(
"page contains a packet too short to be valid")
(self.channels, self.sample_rate, max_bitrate, nominal_bitrate,
min_bitrate) = struct.unpack("<B4i", page.packets[0][11:28])
min_bitrate) = struct.unpack("<BI3i", page.packets[0][11:28])
if self.sample_rate == 0:
raise OggVorbisHeaderError("sample rate can't be zero")
self.serial = page.serial
max_bitrate = max(0, max_bitrate)
@@ -83,13 +90,13 @@ class OggVorbisInfo(StreamInfo):
def _post_tags(self, fileobj):
"""Raises ogg.error"""
page = OggPage.find_last(fileobj, self.serial)
page = OggPage.find_last(fileobj, self.serial, finishing=True)
if page is None:
raise OggVorbisHeaderError
self.length = page.position / float(self.sample_rate)
def pprint(self):
return "Ogg Vorbis, %.2f seconds, %d bps" % (
return u"Ogg Vorbis, %.2f seconds, %d bps" % (
self.length, self.bitrate)

32
lib/mutagen/optimfrog.py Executable file → Normal file
View File

@@ -22,12 +22,23 @@ __all__ = ["OptimFROG", "Open", "delete"]
import struct
from ._compat import endswith
from ._util import convert_error
from ._util import convert_error, endswith
from mutagen import StreamInfo
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 +52,8 @@ class OptimFROGInfo(StreamInfo):
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,21 +61,30 @@ class OptimFROGInfo(StreamInfo):
"""Raises OptimFROGHeaderError"""
header = fileobj.read(76)
if (len(header) != 76 or not header.startswith(b"OFR ") or
struct.unpack("<I", header[4:8])[0] not in [12, 15]):
if len(header) != 76 or not header.startswith(b"OFR "):
raise OptimFROGHeaderError("not an OptimFROG file")
data_size = struct.unpack("<I", header[4:8])[0]
if data_size != 12 and data_size < 15:
raise OptimFROGHeaderError("not an OptimFROG file")
(total_samples, total_samples_high, sample_type, self.channels,
self.sample_rate) = struct.unpack("<IHBBI", header[8:20])
total_samples += total_samples_high << 32
self.channels += 1
self.bits_per_sample = SAMPLE_TYPE_BITS.get(sample_type)
if self.sample_rate:
self.length = float(total_samples) / (self.channels *
self.sample_rate)
else:
self.length = 0.0
if data_size >= 15:
encoder_id = struct.unpack("<H", header[20:22])[0]
version = str((encoder_id >> 4) + 4500)
self.encoder_info = "%s.%s" % (version[0], version[1:])
else:
self.encoder_info = ""
def pprint(self):
return "OptimFROG, %.2f seconds, %d Hz" % (self.length,
return u"OptimFROG, %.2f seconds, %d Hz" % (self.length,
self.sample_rate)

9
lib/mutagen/smf.py Executable file → Normal file
View File

@@ -12,8 +12,7 @@ import struct
from mutagen import StreamInfo, MutagenError
from mutagen._file import FileType
from mutagen._util import loadfile
from mutagen._compat import xrange, endswith
from mutagen._util import loadfile, endswith
class SMFError(MutagenError):
@@ -36,7 +35,7 @@ def _var_int(data, offset=0):
def _read_track(chunk):
"""Retuns a list of midi events and tempo change events"""
TEMPO, MIDI = list(range(2))
TEMPO, MIDI = range(2)
# Deviations: The running status should be reset on non midi events, but
# some files contain meta events inbetween.
@@ -91,7 +90,7 @@ def _read_track(chunk):
def _read_midi_length(fileobj):
"""Returns the duration in seconds. Can raise all kind of errors..."""
TEMPO, MIDI = list(range(2))
TEMPO, MIDI = range(2)
def read_chunk(fileobj):
info = fileobj.read(8)
@@ -178,7 +177,7 @@ class SMFInfo(StreamInfo):
self.length = _read_midi_length(fileobj)
def pprint(self):
return "SMF, %.2f seconds" % self.length
return u"SMF, %.2f seconds" % self.length
class SMF(FileType):

238
lib/mutagen/tak.py Normal file
View File

@@ -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 mutagen import StreamInfo
from mutagen.apev2 import (
APEv2File,
delete,
error,
)
from mutagen._util import (
BitReader,
BitReaderError,
convert_error,
enum,
endswith,
)
@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("<I", bitreader.bytes(3) + b'\0')[0]
data_size = size - CRC_SIZE
pos = fileobj.tell()
if type == TAKMetadata.END:
break
elif type == TAKMetadata.STREAM_INFO:
self._parse_stream_info(bitreader, size)
elif type == TAKMetadata.ENCODER_INFO:
self._parse_encoder_info(bitreader, data_size)
assert bitreader.is_aligned()
fileobj.seek(pos + size)
if self.sample_rate > 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

7
lib/mutagen/trueaudio.py Executable file → Normal file
View File

@@ -10,17 +10,16 @@
True Audio is a lossless format designed for real-time encoding and
decoding. This module is based on the documentation at
http://www.true-audio.com/TTA_Lossless_Audio_Codec\_-_Format_Description
http://www.true-audio.com/TTA_Lossless_Audio_Codec\\_-_Format_Description
True Audio files use ID3 tags.
"""
__all__ = ["TrueAudio", "Open", "delete", "EasyTrueAudio"]
from ._compat import endswith
from mutagen import StreamInfo
from mutagen.id3 import ID3FileType, delete
from mutagen._util import cdata, MutagenError, convert_error
from mutagen._util import cdata, MutagenError, convert_error, endswith
class error(MutagenError):
@@ -54,7 +53,7 @@ class TrueAudioInfo(StreamInfo):
self.length = float(samples) / self.sample_rate
def pprint(self):
return "True Audio, %.2f seconds, %d Hz." % (
return u"True Audio, %.2f seconds, %d Hz." % (
self.length, self.sample_rate)

210
lib/mutagen/wave.py Normal file
View File

@@ -0,0 +1,210 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2017 Borewit
# Copyright (C) 2019-2020 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.
"""Microsoft WAVE/RIFF audio file/stream information and tags."""
import sys
import struct
from mutagen import StreamInfo, FileType
from mutagen.id3 import ID3
from mutagen._riff import RiffFile, InvalidChunk
from mutagen._iff import error as IffError
from mutagen.id3._util import ID3NoHeaderError, error as ID3Error
from mutagen._util import (
convert_error,
endswith,
loadfile,
reraise,
)
__all__ = ["WAVE", "Open", "delete"]
class error(IffError):
"""WAVE stream parsing errors."""
class _WaveFile(RiffFile):
"""Representation of a RIFF/WAVE file"""
def __init__(self, fileobj):
RiffFile.__init__(self, fileobj)
if self.file_type != u'WAVE':
raise error("Expected RIFF/WAVE.")
# Normalize ID3v2-tag-chunk to lowercase
if u'ID3' in self:
self[u'ID3'].id = u'id3'
class WaveStreamInfo(StreamInfo):
"""WaveStreamInfo()
Microsoft WAVE file information.
Information is parsed from the 'fmt' & 'data'chunk of the RIFF/WAVE file
Attributes:
length (`float`): audio length, in seconds
bitrate (`int`): audio bitrate, in bits per second
channels (`int`): The number of audio channels
sample_rate (`int`): audio sample rate, in Hz
bits_per_sample (`int`): The audio sample size
"""
length = 0.0
bitrate = 0
channels = 0
sample_rate = 0
bits_per_sample = 0
SIZE = 16
@convert_error(IOError, error)
def __init__(self, fileobj):
"""Raises error"""
wave_file = _WaveFile(fileobj)
try:
format_chunk = wave_file[u'fmt']
except KeyError as e:
raise error(str(e))
data = format_chunk.read()
if len(data) < 16:
raise InvalidChunk()
# RIFF: http://soundfile.sapp.org/doc/WaveFormat/
# Python struct.unpack:
# https://docs.python.org/2/library/struct.html#byte-order-size-and-alignment
info = struct.unpack('<hhLLhh', data[:self.SIZE])
self.audio_format, self.channels, self.sample_rate, byte_rate, \
block_align, self.bits_per_sample = info
self.bitrate = self.channels * block_align * self.sample_rate
# Calculate duration
self._number_of_samples = 0
if block_align > 0:
try:
data_chunk = wave_file[u'data']
self._number_of_samples = data_chunk.data_size / block_align
except KeyError:
pass
if self.sample_rate > 0:
self.length = self._number_of_samples / self.sample_rate
def pprint(self):
return u"%d channel RIFF @ %d bps, %s Hz, %.2f seconds" % (
self.channels, self.bitrate, self.sample_rate, self.length)
class _WaveID3(ID3):
"""A Wave file with ID3v2 tags"""
def _pre_load_header(self, fileobj):
try:
fileobj.seek(_WaveFile(fileobj)[u'id3'].data_offset)
except (InvalidChunk, KeyError):
raise ID3NoHeaderError("No ID3 chunk")
@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None):
"""Save ID3v2 data to the Wave/RIFF file"""
fileobj = filething.fileobj
wave_file = _WaveFile(fileobj)
if u'id3' not in wave_file:
wave_file.insert_chunk(u'id3')
chunk = wave_file[u'id3']
try:
data = self._prepare_data(
fileobj, chunk.data_offset, chunk.data_size, v2_version,
v23_sep, padding)
except ID3Error as e:
reraise(error, e, sys.exc_info()[2])
chunk.resize(len(data))
chunk.write(data)
def delete(self, filething):
"""Completely removes the ID3 chunk from the RIFF/WAVE file"""
delete(filething)
self.clear()
@convert_error(IOError, error)
@loadfile(method=False, writable=True)
def delete(filething):
"""Completely removes the ID3 chunk from the RIFF/WAVE file"""
try:
_WaveFile(filething.fileobj).delete_chunk(u'id3')
except KeyError:
pass
class WAVE(FileType):
"""WAVE(filething)
A Waveform Audio File Format
(WAVE, or more commonly known as WAV due to its filename extension)
Arguments:
filething (filething)
Attributes:
tags (`mutagen.id3.ID3`)
info (`WaveStreamInfo`)
"""
_mimes = ["audio/wav", "audio/wave"]
@staticmethod
def score(filename, fileobj, header):
filename = filename.lower()
return (header.startswith(b"RIFF") + (header[8:12] == b'WAVE')
+ endswith(filename, b".wav") + endswith(filename, b".wave"))
def add_tags(self):
"""Add an empty ID3 tag to the file."""
if self.tags is None:
self.tags = _WaveID3()
else:
raise error("an ID3 tag already exists")
@convert_error(IOError, error)
@loadfile()
def load(self, filething, **kwargs):
"""Load stream and tag information from a file."""
fileobj = filething.fileobj
self.info = WaveStreamInfo(fileobj)
fileobj.seek(0, 0)
try:
self.tags = _WaveID3(fileobj, **kwargs)
except ID3NoHeaderError:
self.tags = None
except ID3Error as e:
raise error(e)
else:
self.tags.filename = self.filename
Open = WAVE

22
lib/mutagen/wavpack.py Executable file → Normal file
View File

@@ -76,9 +76,10 @@ class WavPackInfo(StreamInfo):
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
bits_per_sample (int): audio sample size
version (int): WavPack stream version
"""
def __init__(self, fileobj):
@@ -90,6 +91,12 @@ class WavPackInfo(StreamInfo):
self.version = header.version
self.channels = bool(header.flags & 4) or 2
self.sample_rate = RATES[(header.flags >> 23) & 0xF]
self.bits_per_sample = ((header.flags & 3) + 1) * 8
# most common multiplier (DSD64)
if (header.flags >> 31) & 1:
self.sample_rate *= 4
self.bits_per_sample = 1
if header.total_samples == -1 or header.block_index != 0:
# TODO: we could make this faster by using the tag size
@@ -109,11 +116,20 @@ class WavPackInfo(StreamInfo):
self.length = float(samples) / self.sample_rate
def pprint(self):
return "WavPack, %.2f seconds, %d Hz" % (self.length,
return u"WavPack, %.2f seconds, %d Hz" % (self.length,
self.sample_rate)
class WavPack(APEv2File):
"""WavPack(filething)
Arguments:
filething (filething)
Attributes:
info (`WavPackInfo`)
"""
_Info = WavPackInfo
_mimes = ["audio/x-wavpack"]