Closed MCheping8108 closed 1 year ago
好吧
对不起,这个程序目前依赖于一个有问题的模块。有关于stagger的代码问题……我通过修改源码的方式修复了它,所以,你需要修改tags.py
和frames.py
,下面是修改好后能使用的案例。我以后会弃用这个模块,或向模块作者报告问题。
Sorry, this program currently depends on a problematic module. There is a problem with the stagger code... I fixed it by modifying the source code, so you need to modify tags.py
and frames.py
, the following is a case that can be used after modification. I will deprecate this module in the future, or report problems to the module author.
这个报错的原因是在stagger的源码中存在有过时的内容。stagger使用的collections
库中的大部分东西被移动到了collections.abc
中,因此会出现AttributeError。
The reason for this error is that there is outdated content in the stagger source code. Most of the stuff in the collections
library used by the stagger was moved into collections.abc
, hence the AttributeError.
请把tags.py
和frames.py
中所有 collections.
替换为👉 collections.abc.
,并手动导入collections.abc
来解决它。
Please replace all collections.
in tags.py
and frames.py
with 👉 collections.abc.
, and manually import collections.abc
to solve it.
这是暂时的,我现在将把重心放在修复这个BUG/寻找新的模块上,尽情谅解
This is temporary, I will now focus on fixing this BUG/finding new modules, please understand
也可以替换我的文件。You can also replace my file.
下载replace_files.zip
Downloadreplace_files.zip
或者
Or
#
# tags.py
# From the stagger project: http://code.google.com/p/stagger/
#
# Copyright (c) 2009-2011 Karoly Lorentey <karoly@lorentey.hu>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# - Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# - Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import abc
import struct
import re
import collections
import collections.abc
import io
import imghdr
import zlib
from abc import abstractmethod, abstractproperty
from warnings import warn
from contextlib import contextmanager
from stagger.errors import *
from stagger.conversion import *
import stagger.frames as Frames
import stagger.fileutil as fileutil
_FRAME23_FORMAT_COMPRESSED = 0x0080
_FRAME23_FORMAT_ENCRYPTED = 0x0040
_FRAME23_FORMAT_GROUP = 0x0020
_FRAME23_FORMAT_UNKNOWN_MASK = 0x001F
_FRAME23_STATUS_DISCARD_ON_TAG_ALTER = 0x8000
_FRAME23_STATUS_DISCARD_ON_FILE_ALTER = 0x4000
_FRAME23_STATUS_READ_ONLY = 0x2000
_FRAME23_STATUS_UNKNOWN_MASK = 0x1F00
_TAG24_UNSYNCHRONISED = 0x80
_TAG24_EXTENDED_HEADER = 0x40
_TAG24_EXPERIMENTAL = 0x20
_TAG24_FOOTER = 0x10
_TAG24_UNKNOWN_MASK = 0x0F
_FRAME24_FORMAT_GROUP = 0x0040
_FRAME24_FORMAT_COMPRESSED = 0x0008
_FRAME24_FORMAT_ENCRYPTED = 0x0004
_FRAME24_FORMAT_UNSYNCHRONISED = 0x0002
_FRAME24_FORMAT_DATA_LENGTH_INDICATOR = 0x0001
_FRAME24_FORMAT_UNKNOWN_MASK = 0x00B0
_FRAME24_STATUS_DISCARD_ON_TAG_ALTER = 0x4000
_FRAME24_STATUS_DISCARD_ON_FILE_ALTER = 0x2000
_FRAME24_STATUS_READ_ONLY = 0x1000
_FRAME24_STATUS_UNKNOWN_MASK = 0x8F00
def read_tag(filename):
with fileutil.opened(filename, "rb") as file:
(cls, offset, length) = detect_tag(file)
return cls.read(file, offset)
def decode_tag(data):
return read_tag(io.BytesIO(data))
def delete_tag(filename):
with fileutil.opened(filename, "rb+") as file:
try:
(cls, offset, length) = detect_tag(file)
fileutil.replace_chunk(file, offset, length, bytes())
except NoTagError:
pass
def detect_tag(filename):
"""Return type and position of ID3v2 tag in filename.
Returns (tag_class, offset, length), where tag_class
is either Tag22, Tag23, or Tag24, and (offset, length)
is the position of the tag in the file.
"""
with fileutil.opened(filename, "rb") as file:
file.seek(0)
header = file.read(10)
file.seek(0)
if len(header) < 10:
raise NoTagError("File too short")
if header[0:3] != b"ID3":
raise NoTagError("ID3v2 tag not found")
if header[3] not in _tag_versions or header[4] != 0:
raise TagError("Unknown ID3 version: 2.{0}.{1}"
.format(*header[3:5]))
cls = _tag_versions[header[3]]
offset = 0
length = Syncsafe.decode(header[6:10]) + 10
if header[3] == 4 and header[5] & _TAG24_FOOTER:
length += 10
return (cls, offset, length)
def frameclass(cls):
"""Register cls as a class representing an ID3 frame.
Sets cls.frameid and cls._version if not present, and registers the
new frame in Tag's known_frames dictionary.
To be used as a decorator on the class definition:
@frameclass
class UFID(Frame):
_framespec = (NullTerminatedStringSpec("owner"), BinaryDataSpec("data"))
"""
assert issubclass(cls, Frames.Frame)
# Register v2.2 versions of v2.3/v2.4 frames if encoded by inheritance.
if len(cls.__name__) == 3:
base = cls.__bases__[0]
if issubclass(base, Frames.Frame) and base._in_version(3, 4):
assert not hasattr(base, "_v2_frame")
base._v2_frame = cls
# Override frameid from base with v2.2 name
if base.frameid == cls.frameid:
cls.frameid = cls.__name__
# Add frameid.
if not hasattr(cls, "frameid"):
cls.frameid = cls.__name__
assert Tag._is_frame_id(cls.frameid)
# Supply _version attribute if missing.
if len(cls.frameid) == 3:
cls._version = 2
if len(cls.frameid) == 4 and not cls._version:
cls._version = (3, 4)
# Register cls as a known frame.
assert cls.frameid not in Tag.known_frames
Tag.known_frames[cls.frameid] = cls
return cls
class FrameOrder:
"""Order frames based on their position in a predefined list of patterns,
and their original position in the source tag.
A pattern may be a frame class, or a regular expression that is to be
matched against the frame id.
>>> order = FrameOrder(TIT1, "T.*", TXXX)
>>> order.key(TIT1())
(0, 1)
>>> order.key(TPE1())
(1, 1)
>>> order.key(TXXX())
(2, 1)
>>> order.key(APIC())
(3, 1)
>>> order.key(APIC(frameno=3))
(3, 0, 3)
"""
def __init__(self, *patterns):
self.re_keys = []
self.frame_keys = dict()
i = -1
for (i, pattern) in zip(range(len(patterns)), patterns):
if isinstance(pattern, str):
self.re_keys.append((pattern, i))
else:
assert issubclass(pattern, Frames.Frame)
self.frame_keys[pattern] = i
self.unknown_key = i + 1
def key(self, frame):
"Return the sort key for the given frame."
def keytuple(primary):
if frame.frameno is None:
return (primary, 1)
return (primary, 0, frame.frameno)
# Look up frame by exact match
if type(frame) in self.frame_keys:
return keytuple(self.frame_keys[type(frame)])
# Look up parent frame for v2.2 frames
if frame._in_version(2) and type(frame).__bases__[0] in self.frame_keys:
return keytuple(self.frame_keys[type(frame).__bases__[0]])
# Try each pattern
for (pattern, key) in self.re_keys:
if re.match(pattern, frame.frameid):
return keytuple(key)
return keytuple(self.unknown_key)
def __repr__(self):
order = []
order.extend((repr(pair[0]), pair[1]) for pair in self.re_keys)
order.extend((cls.__name__, self.frame_keys[cls])
for cls in self.frame_keys)
order.sort(key=lambda pair: pair[1])
return "<FrameOrder: {0}>".format(", ".join(pair[0] for pair in order))
class Tag(collections.abc.MutableMapping, metaclass=abc.ABCMeta):
known_frames = { } # Maps known frameids to Frame class objects
frame_order = None # Initialized by stagger.id3
def __init__(self):
self.flags = set()
self._frames = dict()
self._filename = None
# Primary accessor (no magic)
def frames(self, key=None, orig_order=False):
"""Returns a list of frames in this tag.
If KEY is None, returns all frames in the tag; otherwise returns all frames
whose frameid matches KEY.
If ORIG_ORDER is True, then the frames are returned in their original order.
Otherwise the frames are sorted in canonical order according to the frame_order
field of this tag.
"""
if key is not None:
# If there are multiple frames, then they are already in original order.
key = self._normalize_key(key)
if len(self._frames[key]) == 0:
raise KeyError("Key not found: " + repr(key))
return self._frames[key]
frames = []
for frameid in self._frames.keys():
for frame in self._frames[frameid]:
frames.append(frame)
if orig_order:
key = (lambda frame:
(0, frame.frameno)
if frame.frameno is not None
else (1,))
else:
key = self.frame_order.key
frames.sort(key=key)
return frames
# MutableMapping API
def __iter__(self):
for frameid in self._frames:
yield frameid
def __len__(self):
return sum(len(self._frames[l]) for l in self._frames)
def __eq__(self, other):
return (self.version == other.version
and self.flags == other.flags
and self._frames == other._frames)
def _normalize_key(self, key, unknown_ok=True):
"""Return the normalized version of KEY.
KEY may be a frameid (a string), or a Frame class object.
If KEY corresponds to a registered frameid, then that frameid is returned.
Otherwise, either KeyError is raised, or KEY is returned verbatim,
depending on the value of UNKNOWN_OK.
"""
if Frames.is_frame_class(key):
key = key.frameid
if isinstance(key, str):
if not self._is_frame_id(key):
raise KeyError("{0}: Invalid frame id".format(key))
if key not in self.known_frames:
if unknown_ok:
warn("{0}: Unknown frame id".format(key), UnknownFrameWarning)
else:
raise KeyError("{0}: Unknown frame id".format(key))
return key
# Mapping accessor (with extra magic, for convenience)
def __getitem__(self, key):
key = self._normalize_key(key)
fs = self.frames(key)
allow_duplicates = (key not in self.known_frames
or self.known_frames[key]._allow_duplicates)
if allow_duplicates:
return fs
if len(fs) > 1:
# Merge duplicates into one ephemeral frame, and return that.
# This may break users' expectations when they try to make changes
# to the attributes of the returned frame; however, I think
# sometimes returning a list, sometimes a single frame for the same
# frame id would be even worse.
fs = fs[0]._merge(fs)
assert len(fs) == 1
return fs[0]
def __setitem__(self, key, value):
key = self._normalize_key(key, unknown_ok=False)
if isinstance(value, self.known_frames[key]):
self._frames[key] = [value]
return
if self.known_frames[key]._allow_duplicates:
if not isinstance(value, collections.abc.Iterable) or isinstance(value, str):
raise ValueError("{0} requires a list of frame values".format(key))
self._frames[key] = [val if isinstance(val, self.known_frames[key])
else self.known_frames[key](val)
for val in value]
else: # not _allow_duplicates
self._frames[key] = [self.known_frames[key](value)]
def __delitem__(self, key):
del self._frames[self._normalize_key(key)]
def values(self):
for frameid in self._frames.keys():
for frame in self._frames[frameid]:
yield frame
# Friendly names API
_friendly_names = [ "title", "artist",
"date",
"album-artist", "album",
"track", "track-total",
"disc", "disc-total",
"grouping", "composer",
"genre",
"comment",
#"compilation",
"picture",
"sort-title", "sort-artist",
"sort-album-artist", "sort-album",
"sort-composer",
]
title = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
artist = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
date = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
album_artist = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
album = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
track = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
track_total = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
disc = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
disc_total = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
composer = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
genre = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
comment = abstractproperty(fget=lambda self: Non, fset=lambda self, value: None)
grouping = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
picture = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
sort_title = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
sort_artist = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
sort_album_artist = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
sort_album = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
sort_composer = abstractproperty(fget=lambda self: None, fset=lambda self, value: None)
def __friendly_text_collect(self, frameid):
"""Collect text values from all instances of FRAMEID into a single list.
Returns an empty list if there are no instances of FRAMEID with a text attribute.
"""
try:
return self[frameid].text
except (KeyError, AttributeError):
return []
@classmethod
def _friendly_text_frame(cls, frameid):
def getter(self):
return " / ".join(self.__friendly_text_collect(frameid))
def setter(self, value):
if isinstance(value, str):
if len(value):
# For non-empty strings, split value
self[frameid] = value.split(" / ")
elif frameid in self:
# For empty strings, delete frame
del self[frameid]
else:
self[frameid] = value
return (getter, setter)
@classmethod
def _friendly_track(cls, frameid, totalattr):
def getter(self):
ts = self.__friendly_text_collect(frameid)
try:
return int(ts[0].partition("/")[0])
except (ValueError, IndexError):
return 0
def setter(self, value):
value = int(value)
total = getattr(self, totalattr)
if total > 0:
self[frameid] = "{0}/{1}".format(value, total)
elif value:
self[frameid] = str(value)
elif frameid in self:
del self[frameid]
return (getter, setter)
@classmethod
def _friendly_track_total(cls, frameid, trackattr):
def getter(self):
ts = self.__friendly_text_collect(frameid)
try:
return int(ts[0].partition("/")[2])
except (ValueError, IndexError):
return 0
def setter(self, value):
value = int(value)
track = getattr(self, trackattr)
if value:
self[frameid] = "{0}/{1}".format(track, value)
elif track:
self[frameid] = str(track)
elif frameid in self:
del self[frameid]
return (getter, setter)
__date_pattern = re.compile(r"""(?x)\s*
((?P<year>[0-9]{4}) # YYYY
(-(?P<month>[01][0-9]) # -MM
(-(?P<day>[0-3][0-9]) # -DD
)?)?)?
[ T]?
((?P<hour>[0-2][0-9]) # HH
(:(?P<min>[0-6][0-9]) # :MM
(:(?P<sec>[0-6][0-9]) # :SS
)?)?)?\s*
""")
@classmethod
def _validate_friendly_date(cls, string):
m = cls.__date_pattern.match(string)
if m is None or m.end() != len(string):
raise ValueError("date must be in 'YYYY-MM-DD HH:MM:SS' format")
@classmethod
def _get_friendly_date(cls, string):
m = cls.__date_pattern.match(string)
if m is None:
return (None, None, None, None, None, None)
res = []
for field in ("year", "month", "day", "hour", "min", "sec"):
v = m.group(field)
res.append(int(v) if v is not None else None)
return res
def _get_date(self, yearframe, dateframe, timeframe):
year = month = day = hour = minute = second = None
# Parse year.
try:
year = int(self.__friendly_text_collect(yearframe)[0])
except (IndexError, ValueError):
pass
# Parse month and date.
try:
date = self.__friendly_text_collect(dateframe)[0]
m = re.match(r"\s*(?P<month>[01][0-9])\s*-?\s*(?P<day>[0-3][0-9])?\s*$",
date)
if m is not None:
month = int(m.group("month"))
day = int(m.group("day"))
except IndexError:
pass
# Parse time.
try:
time = self.__friendly_text_collect(timeframe)[0]
m = re.match(r"\s*(?P<hour>[0-2][0-9])\s*:?\s*"
"(?P<minute>[0-5][0-9])\s*:?\s*"
"(?P<second>[0-5][0-9])?\s*$", time)
if m is not None:
hour = int(m.group("hour"))
minute = int(m.group("minute"))
s = m.group("second")
second = int(s) if s is not None else None
except IndexError:
pass
return (year, month, day, hour, minute, second)
def _friendly_date_string(self, *fields):
seps = ("", "-", "-", " ", ":", ":")
formats = ("04", "02", "02", "02", "02", "02")
res = []
for i in range(len(fields)):
if fields[i] is None:
break
res.append(seps[i])
res.append("{0:{1}}".format(fields[i], formats[i]))
return "".join(res)
@classmethod
def _friendly_picture(cls, frameid):
def getter(self):
if frameid not in self:
return ""
else:
return ", ".join("{0}:{1}:<{2} bytes of {3} data>"
.format(f._spec("type").to_str(f.type),
f.desc,
len(f.data),
imghdr.what(None, f.data[:32]))
for f in self[frameid])
def setter(self, value):
if len(value) > 0:
self[frameid] = [self.known_frames[frameid](value=value)]
elif frameid in self:
del self[frameid]
return (getter, setter)
@classmethod
def _friendly_comment(cls, frameid):
def comment_frame_index(self):
if frameid not in self:
return None
# Return comment with lang="eng", desc="", if present.
# Otherwise return the first comment with no description,
# regardless of language.
icmt = None
for i in range(len(self[frameid])):
f = self[frameid][i]
if f.desc == "":
if f.lang == "eng":
return i
if icmt is None:
icmt = i
return icmt
def getter(self):
i = comment_frame_index(self)
if i is None:
return ""
else:
return self[frameid][i].text
def setter(self, value):
assert isinstance(value, str)
i = comment_frame_index(self)
if i is not None:
del self._frames[frameid][i]
if len(value) > 0:
frame = self.known_frames[frameid](lang="eng", desc="", text=value)
if frameid not in self._frames:
self._frames[frameid] = []
self._frames[frameid].append(frame)
return (getter, setter)
# Misc
def __repr__(self):
return "<{0}: ID3v2.{1} tag{2} with {3} frames>".format(
type(self).__name__,
self.version,
("({0})".format(", ".join(self.flags))
if len(self.flags) > 0 else ""),
len(self._frames))
# Reading tags
@classmethod
def read(cls, filename, offset=0):
"""Read an ID3v2 tag from a file."""
i = 0
with fileutil.opened(filename, "rb") as file:
file.seek(offset)
tag = cls()
tag._read_header(file)
for (frameid, bflags, data) in tag._read_frames(file):
if len(data) == 0:
warn("{0}: Ignoring empty frame".format(frameid),
EmptyFrameWarning)
else:
frame = tag._decode_frame(frameid, bflags, data, i)
if frame is not None:
l = tag._frames.setdefault(frame.frameid, [])
l.append(frame)
if file.tell() > tag.offset + tag.size:
break
i += 1
try:
tag._filename = file.name
except AttributeError:
pass
return tag
@classmethod
def decode(cls, data):
return cls.read(io.BytesIO(data))
def _decode_frame(self, frameid, bflags, data, frameno=None):
try:
(flags, data) = self._interpret_frame_flags(frameid, bflags, data)
if flags is None:
flags = set()
if frameid in self.known_frames:
return self.known_frames[frameid]._decode(frameid, data,
flags,
frameno=frameno)
else:
# Unknown frame
flags.add("unknown")
warn("{0}: Unknown frame".format(frameid), UnknownFrameWarning)
if frameid.startswith('T'): # Unknown text frame
return Frames.TextFrame._decode(frameid, data, flags,
frameno=frameno)
elif frameid.startswith('W'): # Unknown URL frame
return Frames.URLFrame._decode(frameid, data, flags,
frameno=frameno)
else:
return Frames.UnknownFrame._decode(frameid, data, flags,
frameno=frameno)
except (FrameError, ValueError, EOFError) as e:
warn("{0}: Invalid frame".format(frameid), ErrorFrameWarning)
return Frames.ErrorFrame(frameid, data, exception=e, frameno=frameno)
@abstractmethod
def _read_header(self, file): pass
@abstractmethod
def _read_frames(self, file): pass
@abstractmethod
def _interpret_frame_flags(self, frameid, bflags, data): pass
# Writing tags
def write(self, filename=None):
if not filename:
filename = self._filename
if not filename:
raise TypeError("invalid file: {0}".format(filename))
with fileutil.opened(filename, "rb+") as file:
try:
(offset, length) = detect_tag(file)[1:3]
except NoTagError:
(offset, length) = (0, 0)
if offset > 0:
delete_tag(file)
(offset, length) = (0, 0)
tag_data = self.encode(size_hint=length)
fileutil.replace_chunk(file, offset, length, tag_data)
@abstractmethod
def encode(self, size_hint=None):
pass
padding_default = 128
padding_max = 1024
def _get_size_with_padding(self, size_desired, size_actual):
size = size_actual
if (size_desired is not None and size < size_desired
and (self.padding_max is None or
size_desired - size_actual <= self.padding_max)):
size = size_desired
elif self.padding_default:
size += min(self.padding_default, self.padding_max)
return size
@staticmethod
def _is_frame_id(data):
if isinstance(data, str):
try:
data = data.encode("ASCII")
except UnicodeEncodeError:
return false
# Allow a single space at end of four-character ids
# Some programs (e.g. iTunes 8.2) generate such frames when converting
# from 2.2 to 2.3/2.4 tags.
pattern = re.compile(b"^[A-Z][A-Z0-9]{2}[A-Z0-9 ]?$")
return pattern.match(data)
def _prepare_frames_hook(self):
pass
def _prepare_frames(self):
# Generate dictionary of frames
d = self._frames
# Merge duplicate frames
for frameid in self._frames.keys():
fs = self._frames[frameid]
if len(fs) > 1:
d[frameid] = fs[0]._merge(fs)
self._prepare_frames_hook()
# Convert frames
newframes = []
for frameid in self._frames.keys():
for frame in self._frames[frameid]:
try:
newframes.append(frame._to_version(self.version))
except IncompatibleFrameError:
warn("{0}: Ignoring incompatible frame".format(frameid),
FrameWarning)
except ValueError as e:
warn("{0}: Ignoring invalid frame ({1})".format(frameid, e),
FrameWarning)
# Sort frames
newframes.sort(key=self.frame_order.key)
return newframes
class Tag22(Tag):
version = 2
encodings = ("latin-1", "utf-16")
def __init__(self):
super().__init__()
title = property(*Tag._friendly_text_frame("TT2"))
artist = property(*Tag._friendly_text_frame("TP1"))
@property
def date(self):
fields = self._get_date("TYE", "TDA", "TIM")
return self._friendly_date_string(*fields)
@date.setter
def date(self, value):
self._validate_friendly_date(value)
(year, month, day, hour, minute, second) = self._get_friendly_date(value)
for f in "TYE", "TDA", "TIM":
if f in self:
del self[f]
if year is not None:
self["TYE"] = "{0:04}".format(year)
if month is not None and day is not None:
self["TDA"] = "{0:02}{1:02}".format(month, day)
if hour is not None and minute is not None:
self["TIM"] = "{0:02}{1:02}".format(hour, minute)
album_artist = property(*Tag._friendly_text_frame("TP2"))
album = property(*Tag._friendly_text_frame("TAL"))
track = property(*Tag._friendly_track("TRK", "track_total"))
track_total = property(*Tag._friendly_track_total("TRK", "track"))
disc = property(*Tag._friendly_track("TPA", "disc_total"))
disc_total = property(*Tag._friendly_track_total("TPA", "disc"))
composer = property(*Tag._friendly_text_frame("TCM"))
genre = property(*Tag._friendly_text_frame("TCO"))
comment = property(*Tag._friendly_comment("COM"))
grouping = property(*Tag._friendly_text_frame("TT1"))
# TODO: compilation
picture = property(*Tag._friendly_picture("PIC"))
sort_title = property(*Tag._friendly_text_frame("TST"))
sort_artist = property(*Tag._friendly_text_frame("TSP"))
sort_album_artist = property(*Tag._friendly_text_frame("TS2"))
sort_album = property(*Tag._friendly_text_frame("TSA"))
sort_composer = property(*Tag._friendly_text_frame("TSC"))
def _read_header(self, file):
self.offset = file.tell()
header = fileutil.xread(file, 10)
if header[0:5] != b"ID3\x02\00":
raise NoTagError("ID3v2.2 header not found")
if header[5] & 0x80:
self.flags.add("unsynchronised")
if header[5] & 0x40: # Compression bit is ill-defined in standard
raise TagError("ID3v2.2 tag compression is not supported")
if header[5] & 0x3F:
warn("Unknown ID3v2.2 flags", TagWarning)
self.size = Syncsafe.decode(header[6:10]) + 10
def _read_frames(self, file):
if "unsynchronised" in self.flags:
ufile = UnsyncReader(file)
else:
ufile = file
while file.tell() < self.offset + self.size:
header = fileutil.xread(ufile, 6)
if not self._is_frame_id(header[0:3]):
break
frameid = header[0:3].decode("ASCII")
size = Int8.decode(header[3:6])
data = fileutil.xread(ufile, size)
yield (frameid, None, data)
def _interpret_frame_flags(self, frameid, bflags, data):
# No frame flags in v2.2
return (None, data)
def __encode_one_frame(self, frame):
framedata = frame._encode(encodings=self.encodings)
data = bytearray()
# Frame id
if len(frame.frameid) != 3 or not self._is_frame_id(frame.frameid):
raise "Invalid ID3v2.2 frame id {0}".format(repr(frame.frameid))
data.extend(frame.frameid.encode("ASCII"))
# Size
data.extend(Int8.encode(len(framedata), width=3))
assert(len(data) == 6)
data.extend(framedata)
return data
def _prepare_frames_hook(self):
for frameid in self._frames.keys():
for frame in self._frames[frameid]:
if isinstance(frame, Frames.TextFrame):
# ID3v2.2 doesn't support multiple text values
if len(frame.text) > 1:
warn("{0}: merged multiple text strings into one value"
.format(frame.frameid), FrameWarning)
frame.text = [" / ".join(frame.text)]
def encode(self, size_hint=None):
if len(self) == 0: # No frames -> no tag
return b""
frames = self._prepare_frames()
framedata = bytearray().join(self.__encode_one_frame(frame)
for frame in frames)
if "unsynchronised" in self.flags:
framedata = Unsync.encode(framedata)
size = self._get_size_with_padding(size_hint, len(framedata) + 10)
data = bytearray()
data.extend(b"ID3\x02\x00")
data.append(0x80 if "unsynchronised" in self.flags else 0x00)
data.extend(Syncsafe.encode(size - 10, width=4))
assert len(data) == 10
data.extend(framedata)
if size > len(data):
data.extend(b"\x00" * (size - len(data)))
assert len(data) == size
return data
class Tag23(Tag):
version = 3
encodings = ("latin-1", "utf-16")
def __init__(self):
super().__init__()
title = property(*Tag._friendly_text_frame("TIT2"))
artist = property(*Tag._friendly_text_frame("TPE1"))
@property
def date(self):
fields = self._get_date("TYER", "TDAT", "TIME")
return self._friendly_date_string(*fields)
@date.setter
def date(self, value):
self._validate_friendly_date(value)
(year, month, day, hour, minute, second) = self._get_friendly_date(value)
for f in "TYER", "TDAT", "TIME":
if f in self:
del self[f]
if year is not None:
self["TYER"] = "{0:04}".format(year)
if month is not None and day is not None:
self["TDAT"] = "{0:02}{1:02}".format(month, day)
if hour is not None and minute is not None:
self["TIME"] = "{0:02}{1:02}".format(hour, minute)
album_artist = property(*Tag._friendly_text_frame("TPE2"))
album = property(*Tag._friendly_text_frame("TALB"))
track = property(*Tag._friendly_track("TRCK", "track_total"))
track_total = property(*Tag._friendly_track_total("TRCK", "track"))
disc = property(*Tag._friendly_track("TPOS", "disc_total"))
disc_total = property(*Tag._friendly_track_total("TPOS", "disc"))
composer = property(*Tag._friendly_text_frame("TCOM"))
genre = property(*Tag._friendly_text_frame("TCON"))
comment = property(*Tag._friendly_comment("COMM"))
grouping = property(*Tag._friendly_text_frame("TIT1"))
# TODO: compilation
picture = property(*Tag._friendly_picture("APIC"))
sort_title = property(*Tag._friendly_text_frame("TSOT"))
sort_artist = property(*Tag._friendly_text_frame("TSOP"))
sort_album_artist = property(*Tag._friendly_text_frame("TSO2"))
sort_album = property(*Tag._friendly_text_frame("TSOA"))
sort_composer = property(*Tag._friendly_text_frame("TSOC"))
def _read_header(self, file):
self.offset = file.tell()
header = fileutil.xread(file, 10)
if header[0:5] != b"ID3\x03\x00":
raise NoTagError("ID3v2.3 header not found")
if header[5] & 0x80:
self.flags.add("unsynchronised")
if header[5] & 0x40:
self.flags.add("extended_header")
if header[5] & 0x20:
self.flags.add("experimental")
if header[5] & 0x1F:
warn("Unknown ID3v2.3 flags", TagWarning)
self.size = Syncsafe.decode(header[6:10]) + 10
if "extended_header" in self.flags:
self.__read_extended_header(file)
def __read_extended_header(self, file):
(size, ext_flags, self.padding_size) = \
struct.unpack("!IHI", fileutil.xread(file, 10))
if size != 6 and size != 10:
warn("Unexpected size of ID3v2.3 extended header: {0}".format(size),
TagWarning)
if ext_flags & 32768:
if size < 10:
warn("Extended header is too short for a CRC field: {0} bytes instead of 10".format(size),
TagWarning)
else:
self.flags.add("ext:crc_present")
(self.crc32,) = struct.unpack("!I", fileutil.xread(file, 4))
size -= 6
if size > 6:
fileutil.xread(file, size - 6)
def _read_frames(self, file):
if "unsynchronised" in self.flags:
ufile = UnsyncReader(file)
else:
ufile = file
while file.tell() < self.offset + self.size:
header = fileutil.xread(ufile, 10)
if not self._is_frame_id(header[0:4]):
break
frameid = header[0:4].decode("ASCII")
size = Int8.decode(header[4:8])
bflags = Int8.decode(header[8:10])
data = fileutil.xread(ufile, size)
yield (frameid, bflags, data)
def _interpret_frame_flags(self, frameid, bflags, data):
flags = set()
# Frame encoding flags
if bflags & _FRAME23_FORMAT_UNKNOWN_MASK:
raise FrameError("{0}: Invalid ID3v2.3 frame encoding flags: 0x{0:X}".format(frameid, bflags))
if bflags & _FRAME23_FORMAT_COMPRESSED:
flags.add("compressed")
expanded_size = Int8.decode(data[0:4])
data = zlib.decompress(data[4:])
if bflags & _FRAME23_FORMAT_ENCRYPTED:
raise FrameError("{0}: Can't read ID3v2.3 encrypted frames".format(frameid))
if bflags & _FRAME23_FORMAT_GROUP:
flags.add("group")
flags.add("group={0}".format(data[0])) # Hack
data = data[1:]
# Frame status messages
if bflags & _FRAME23_STATUS_DISCARD_ON_TAG_ALTER:
flags.add("discard_on_tag_alter")
if bflags & _FRAME23_STATUS_DISCARD_ON_FILE_ALTER:
flags.add("discard_on_file_alter")
if bflags & _FRAME23_STATUS_READ_ONLY:
flags.add("read_only")
if bflags & _FRAME23_STATUS_UNKNOWN_MASK:
warn("{0}: Unexpected ID3v2.3 frame status flags: 0x{1:X}".format(frameid, bflags),
TagWarning)
return flags, data
def __encode_one_frame(self, frame):
framedata = frame._encode(encodings=self.encodings)
origlen = len(framedata)
flagval = 0
frameinfo = bytearray()
if "compressed" in frame.flags:
framedata = zlib.compress(framedata)
flagval |= _FRAME23_FORMAT_COMPRESSED
frameinfo.extend(Int8.encode(origlen, width=4))
if "group" in frame.flags:
grp = 0
for flag in frame.flags:
if flag.startswith("group="):
grp = int(flag[6:])
frameinfo.append(grp)
flagval |= _FRAME23_FORMAT_GROUP
if "discard_on_tag_alter" in frame.flags:
flagval |= _FRAME23_STATUS_DISCARD_ON_TAG_ALTER
if "discard_on_file_alter" in frame.flags:
flagval |= _FRAME23_STATUS_DISCARD_ON_FILE_ALTER
if "read_only" in frame.flags:
flagval |= _FRAME23_STATUS_READ_ONLY
data = bytearray()
# Frame id
if len(frame.frameid) != 4 or not self._is_frame_id(frame.frameid.encode("ASCII")):
raise ValueError("Invalid ID3v2.3 frame id {0}".format(repr(frame.frameid)))
data.extend(frame.frameid.encode("ASCII"))
# Size
data.extend(Int8.encode(len(frameinfo) + len(framedata), width=4))
# Flags
data.extend(Int8.encode(flagval, width=2))
assert len(data) == 10
# Format info
data.extend(frameinfo)
# Frame data
data.extend(framedata)
return data
def encode(self, size_hint=None):
if len(self) == 0: # No frames -> no tag
return b""
frames = self._prepare_frames()
framedata = bytearray().join(self.__encode_one_frame(frame)
for frame in frames)
if "unsynchronised" in self.flags:
framedata = Unsync.encode(framedata)
size = self._get_size_with_padding(size_hint, len(framedata) + 10)
data = bytearray()
data.extend(b"ID3\x03\x00")
flagval = 0x00
if "unsynchronised" in self.flags:
flagval |= 0x80
data.append(flagval)
data.extend(Syncsafe.encode(size - 10, width=4))
assert len(data) == 10
data.extend(framedata)
if size > len(data):
data.extend(b"\x00" * (size - len(data)))
assert len(data) == size
return data
class Tag24(Tag):
ITUNES_WORKAROUND = False
version = 4
encodings = ("latin-1", "utf-8")
def __init__(self):
super().__init__()
title = property(*Tag._friendly_text_frame("TIT2"))
artist = property(*Tag._friendly_text_frame("TPE1"))
@property
def date(self):
try:
frame = self["TDRC"]
except KeyError:
return ""
else:
return frame.text[0]
@date.setter
def date(self, value):
self._validate_friendly_date(value)
fields = self._get_friendly_date(value)
val = self._friendly_date_string(*fields)
if val:
self["TDRC"] = val
elif "TDRC" in self:
del self["TDRC"]
album = property(*Tag._friendly_text_frame("TALB"))
album_artist = property(*Tag._friendly_text_frame("TPE2"))
track = property(*Tag._friendly_track("TRCK", "track_total"))
track_total = property(*Tag._friendly_track_total("TRCK", "track"))
disc = property(*Tag._friendly_track("TPOS", "disc_total"))
disc_total = property(*Tag._friendly_track_total("TPOS", "disc"))
composer = property(*Tag._friendly_text_frame("TCOM"))
genre = property(*Tag._friendly_text_frame("TCON"))
comment = property(*Tag._friendly_comment("COMM"))
grouping = property(*Tag._friendly_text_frame("TIT1"))
# TODO: compilation
picture = property(*Tag._friendly_picture("APIC"))
sort_title = property(*Tag._friendly_text_frame("TSOT"))
sort_artist = property(*Tag._friendly_text_frame("TSOP"))
sort_album_artist = property(*Tag._friendly_text_frame("TSO2"))
sort_album = property(*Tag._friendly_text_frame("TSOA"))
sort_composer = property(*Tag._friendly_text_frame("TSOC"))
def _read_header(self, file):
self.offset = file.tell()
header = fileutil.xread(file, 10)
if header[0:5] != b"ID3\x04\x00":
raise NoTagError("ID3v2 header not found")
if header[5] & _TAG24_UNSYNCHRONISED:
self.flags.add("unsynchronised")
if header[5] & _TAG24_EXTENDED_HEADER:
self.flags.add("extended_header")
if header[5] & _TAG24_EXPERIMENTAL:
self.flags.add("experimental")
if header[5] & _TAG24_FOOTER:
self.flags.add("footer")
if header[5] & _TAG24_UNKNOWN_MASK:
warn("Unknown ID3v2.4 flags", TagWarning)
self.size = (Syncsafe.decode(header[6:10]) + 10
+ (10 if "footer" in self.flags else 0))
if "extended_header" in self.flags:
self.__read_extended_header(file)
def __read_extended_header_flag_data(self, data):
# 1-byte length + data
length = data[0]
if length & 128:
raise TagError("Invalid size of extended header field")
return (data[1:1+length], data[1+length:])
def __read_extended_header(self, file):
size = Syncsafe.decode(fileutil.xread(file, 4))
if size < 6:
warn("Unexpected size of ID3v2.4 extended header: {0}".format(size),
TagWarning)
data = fileutil.xread(file, size - 4)
numflags = data[0]
if numflags != 1:
warn("Unexpected number of ID3v2.4 extended flag bytes: {0}"
.format(numflags),
TagWarning)
flags = data[1]
data = data[1+numflags:]
if flags & 0x40:
self.flags.add("ext:update")
(dummy, data) = self.__read_extended_header_flag_data(data)
if flags & 0x20:
self.flags.add("ext:crc_present")
(self.crc32, data) = self.__read_extended_header_flag_data(data)
self.crc32 = Syncsafe.decode(self.crc32)
if flags & 0x10:
self.flags.add("ext:restrictions")
(self.restrictions, data) = self.__read_extended_header_flag_data(data)
def _read_frames(self, file, syncsafe_workaround = None):
# Older versions of iTunes stored frame sizes as straight 8bit integers,
# not syncsafe values as the spec requires.
# (The bug is known to be fixed in iTunes 8.2.)
#
# To work around such an erroneous encoding, we re-read the entire tag
# in non-syncsafe mode when we detect a frame with a bad size.
# This heuristic does not detect all badly encoded tags;
# it fails when the 8-bit frame size happens to be in syncsafe format.
#
# We could improve detection by parsing the tag both ways and see which
# interpretation produces more frames. However, the extra effort doesn't
# seem worthwhile to do by default.
#
# If you have many files with iTunes-encoded tags, you can force stagger
# to read them in non-syncsafe mode setting the ITUNES_WORKAROUND
# class attribute to True and let stagger reencode your tags. (Stagger
# will never produce a 2.4 tag with non-syncsafe frame lengths.)
if syncsafe_workaround is None:
syncsafe_workaround = self.ITUNES_WORKAROUND
origfpos = file.tell()
frames = []
while file.tell() < self.offset + self.size:
header = fileutil.xread(file, 10)
if not self._is_frame_id(header[0:4]):
break
frameid = header[0:4].decode("ASCII")
if syncsafe_workaround:
size = Int8.decode(header[4:8])
else:
try:
size = Syncsafe.decode(header[4:8])
except ValueError:
if syncsafe_workaround:
raise
warn("Invalid syncsafe frame size; switching to 8-bit mode")
file.seek(origfpos)
return self._read_frames(file, True)
bflags = Int8.decode(header[8:10])
data = fileutil.xread(file, size)
frames.append((frameid, bflags, data))
return frames
def _interpret_frame_flags(self, frameid, bflags, data):
flags = set()
# Frame format flags
if bflags & _FRAME24_FORMAT_UNKNOWN_MASK:
raise FrameError("{0}: Unknown frame encoding flags: 0x{1:X}".format(frameid, bflags))
if bflags & _FRAME24_FORMAT_GROUP:
flags.add("group")
flags.add("group={0}".format(data[0])) # hack
data = data[1:]
if bflags & _FRAME24_FORMAT_COMPRESSED:
flags.add("compressed")
if bflags & _FRAME24_FORMAT_ENCRYPTED:
raise FrameError("{0}: Can't read encrypted frames".format(frameid))
if bflags & _FRAME24_FORMAT_UNSYNCHRONISED:
flags.add("unsynchronised")
expanded_size = len(data)
if bflags & _FRAME24_FORMAT_DATA_LENGTH_INDICATOR:
flags.add("data_length_indicator")
expanded_size = Syncsafe.decode(data[0:4])
data = data[4:]
if "unsynchronised" in self.flags:
data = Unsync.decode(data)
if "compressed" in self.flags:
data = zlib.decompress(data)
# Frame status flags
if bflags & _FRAME24_STATUS_DISCARD_ON_TAG_ALTER:
flags.add("discard_on_tag_alter")
if bflags & _FRAME24_STATUS_DISCARD_ON_FILE_ALTER:
flags.add("discard_on_file_alter")
if bflags & _FRAME24_STATUS_READ_ONLY:
flags.add("read_only")
if bflags & _FRAME24_STATUS_UNKNOWN_MASK:
warn("{0}: Unexpected status flags: 0x{1:X}"
.format(frameid, bflags), FrameWarning)
return flags, data
def __encode_one_frame(self, frame):
framedata = frame._encode(encodings=self.encodings)
origlen = len(framedata)
flagval = 0
frameinfo = bytearray()
if "group" in frame.flags:
grp = 0
for flag in frame.flags:
if flag.startswith("group="):
grp = int(flag[6:])
frameinfo.append(grp)
flagval |= _FRAME24_FORMAT_GROUP
if "compressed" in frame.flags:
frame.flags.add("data_length_indicator")
framedata = zlib.compress(framedata)
flagval |= _FRAME24_FORMAT_COMPRESSED
if "unsynchronised" in frame.flags:
frame.flags.add("data_length_indicator")
framedata = Unsync.encode(framedata)
flagval |= _FRAME24_FORMAT_UNSYNCHRONISED
if "data_length_indicator" in frame.flags:
frameinfo.extend(Syncsafe.encode(origlen, width=4))
flagval |= _FRAME24_FORMAT_DATA_LENGTH_INDICATOR
if "discard_on_tag_alter" in frame.flags:
flagval |= _FRAME24_STATUS_DISCARD_ON_TAG_ALTER
if "discard_on_file_alter" in frame.flags:
flagval |= _FRAME24_STATUS_DISCARD_ON_FILE_ALTER
if "read_only" in frame.flags:
flagval |= _FRAME24_STATUS_READ_ONLY
data = bytearray()
# Frame id
if len(frame.frameid) != 4 or not self._is_frame_id(frame.frameid):
raise "{0}: Invalid frame id".format(repr(frame.frameid))
data.extend(frame.frameid.encode("ASCII"))
# Size
data.extend(Syncsafe.encode(len(frameinfo) + len(framedata), width=4))
# Flags
data.extend(Int8.encode(flagval, width=2))
assert len(data) == 10
# Format info
data.extend(frameinfo)
# Frame data
data.extend(framedata)
return data
def encode(self, size_hint=None):
if len(self) == 0: # No frames -> no tag
return b""
frames = self._prepare_frames()
if "unsynchronised" in self.flags:
for frame in frames:
frame.flags.add("unsynchronised")
framedata = bytearray().join(self.__encode_one_frame(frame)
for frame in frames)
size = self._get_size_with_padding(size_hint, len(framedata) + 10)
data = bytearray()
data.extend(b"ID3\x04\x00")
flagval = 0x00
if "unsynchronised" in self.flags:
flagval |= 0x80
data.append(flagval)
data.extend(Syncsafe.encode(size - 10, width=4))
assert len(data) == 10
data.extend(framedata)
if size > len(data):
data.extend(b"\x00" * (size - len(data)))
assert len(data) == size
return data
_tag_versions = {
2: Tag22,
3: Tag23,
4: Tag24,
}
#
# frames.py
# From the stagger project: http://code.google.com/p/stagger/
#
# Copyright (c) 2009-2011 Karoly Lorentey <karoly@lorentey.hu>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# - Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# - Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""Class definitions for ID3v2 frames."""
import abc
import collections
import collections.abc
import imghdr
from abc import abstractmethod
from warnings import warn
from stagger.errors import *
from stagger.specs import *
class Frame(metaclass=abc.ABCMeta):
_framespec = tuple()
_version = tuple()
_allow_duplicates = False
def __init__(self, frameid=None, flags=None, frameno=None, **kwargs):
self.frameid = frameid if frameid else type(self).__name__
self.flags = flags if flags else set()
self.frameno = frameno
assert len(self._framespec) > 0
for spec in self._framespec:
val = kwargs.get(spec.name, None)
setattr(self, spec.name, val)
def __setattr__(self, name, value):
# Automatic validation on assignment
for spec in self._framespec:
if name == spec.name:
value = spec.validate(self, value)
break
super().__setattr__(name, value)
def __eq__(self, other):
return (isinstance(other, type(self))
and self.frameid == other.frameid
and self.flags == other.flags
and self._framespec == other._framespec
and all(getattr(self, spec.name, None) ==
getattr(other, spec.name, None)
for spec in self._framespec))
@classmethod
def _decode(cls, frameid, data, flags=None, frameno=None):
frame = cls(frameid=frameid, flags=flags, frameno=frameno)
if getattr(frame, "_untested", False):
warn("{0}: Untested frame; please verify results".format(frameid),
UntestedFrameWarning)
for spec in frame._framespec:
try:
val, data = spec.read(frame, data)
setattr(frame, spec.name, val)
except EOFError:
if not spec._optional:
raise
return frame
@classmethod
def _from_frame(cls, frame):
"Copy constructor"
assert frame._framespec == cls._framespec
new = cls(flags=frame.flags, frameno=frame.frameno)
for spec in cls._framespec:
setattr(new, spec.name, getattr(frame, spec.name, None))
return new
@classmethod
def _merge(cls, frames):
if cls._allow_duplicates:
return frames
else:
if len(frames) > 1:
# TODO: Research what iTunes does in this case
# Mutagen displays the first frame only.
warn("{0}: Duplicate frame; only the first instance is kept"
.format(frames[0].frameid),
DuplicateFrameWarning)
return frames[0]
@classmethod
def _in_version(self, *versions):
"Returns true if this frame is in any of the specified versions of ID3."
for version in versions:
if (self._version == version
or (isinstance(self._version, collections.abc.Container)
and version in self._version)):
return True
return False
def _to_version(self, version):
if self._in_version(version):
return self
if version == 2 and hasattr(self, "_v2_frame"):
return self._v2_frame._from_frame(self)
if self._in_version(2):
base = type(self).__bases__[0]
if issubclass(base, Frame) and base._in_version(version):
return base._from_frame(self)
raise IncompatibleFrameError("Frame {0} cannot be converted "
"to ID3v2.{1} format".format(self.frameid, version))
def _encode(self, encodings=("latin-1", "utf-16")):
# if getattr(self, "_bozo", False):
# warn("{0}: Frame type is not widely implemented, "
# "its use is discouraged".format(self.frameid),
# BozoFrameWarning)
def encode_fields():
data = bytearray()
for spec in self._framespec:
if spec._optional and getattr(self, spec.name) is None:
break
data.extend(spec.write(self, getattr(self, spec.name)))
return data
def try_preferred_encodings():
orig_encoding = self.encoding
try:
for encoding in encodings:
try:
self.encoding = encoding
return encode_fields()
except UnicodeEncodeError:
pass
finally:
self.encoding = orig_encoding
raise ValueError("Could not encode strings")
if not isinstance(self._framespec[0], EncodingSpec):
return encode_fields()
elif self.encoding is None:
return try_preferred_encodings()
else:
try:
# Try specified encoding before others
return encode_fields()
except UnicodeEncodeError:
return try_preferred_encodings()
def __repr__(self):
stype = type(self).__name__
args = []
if stype != self.frameid:
args.append("frameid={0!r}".format(self.frameid))
if self.flags:
args.append("flags={0!r}".format(self.flags))
for spec in self._framespec:
if isinstance(spec, BinaryDataSpec):
data = getattr(self, spec.name)
if isinstance(data, (bytes, bytearray)):
args.append("{0}=<{1} bytes of binary data {2!r}{3}>".format(
spec.name, len(data),
data[:20], "..." if len(data) > 20 else ""))
else:
args.append(repr(data))
else:
args.append("{0}={1!r}".format(spec.name, getattr(self, spec.name)))
return "{0}({1})".format(stype, ", ".join(args))
def _spec(self, name):
"Return the named spec."
for s in self._framespec:
if s.name == name:
return s
raise ValueError("Unknown spec: " + name)
def _str_fields(self):
fields = []
# Determine how many fields to show
cutoff = max(i for i in range(len(self._framespec))
if i == 0 # don't call max with an the empty sequence
or not self._framespec[i]._optional
or getattr(self, self._framespec[i].name, None) is not None)
for spec in self._framespec[:cutoff + 1]:
fields.append("{0}={1}"
.format(spec.name,
repr(spec.to_str(getattr(self, spec.name, None)))))
return ", ".join(fields)
def __str__(self):
flag = " "
if "unknown" in self.flags: flag = "?"
if isinstance(self, ErrorFrame): flag = "!"
return "{0}{1}({2})".format(flag, self.frameid, self._str_fields())
class UnknownFrame(Frame):
_framespec = (BinaryDataSpec("data"),)
class ErrorFrame(Frame):
_framespec = (BinaryDataSpec("data"),)
def __init__(self, frameid, data, exception, frameno=None, **kwargs):
super().__init__(frameid=frameid, flags={}, frameno=frameno, **kwargs)
self.data = data
self.exception = exception
def _str_fields(self):
strs = ["ERROR"]
if self.exception:
strs.append(str(self.exception))
strs.append(repr(self.data))
return ", ".join(strs)
class TextFrame(Frame):
_framespec = (EncodingSpec("encoding"),
SequenceSpec("text", EncodedStringSpec("text")))
def __init__(self, *values, frameid=None, flags=None, frameno=None, **kwargs):
def extract_strs(values):
if values is None:
return
if isinstance(values, str):
yield values
elif isinstance(values, collections.abc.Iterable):
for val in values:
for v in extract_strs(val):
yield v
else:
raise ValueError("Invalid text frame value")
super().__init__(frameid=frameid, flags=flags, frameno=frameno, **kwargs)
self.text.extend(list(extract_strs(values)))
@classmethod
def _decode(cls, frameid, data, flags=None, frameno=None):
frame = super()._decode(frameid, data, flags=flags, frameno=frameno)
count = 0
while len(frame.text) > 0 and frame.text[-1] == "":
frame.text = frame.text[0:-1]
count += 1
if count > 0:
warn("{0}: Stripped {1} empty strings from end of frame"
.format(frameid, count), FrameWarning)
if len(frame.text) == 0 or sum(len(t) for t in frame.text) == 0:
warn("{0}: Ignoring empty text frame".format(frameid),
EmptyFrameWarning)
return None
return frame
def _str_fields(self):
return ", ".join(repr(t) for t in self.text)
# return "{0} {1}".format((EncodedStringSpec._encodings[self.encoding][0]
# if self.encoding is not None else "<undef>"),
# ", ".join(repr(t) for t in self.text))
@classmethod
def _merge(cls, frames):
if len(frames) == 1:
return frames
res = cls(text=[])
enc = None
for f in frames:
if enc is None:
enc = f.encoding
elif enc != f.encoding:
enc = False
try:
res.text.extend(f.text)
except KeyError: # f may be an ErrorFrame here.
pass
if enc is not False:
res.encoding = enc
return [res]
class URLFrame(Frame):
_framespec = (URLStringSpec("url"), )
def _str_fields(self):
return repr(self.url)
class CreditsFrame(Frame):
_framespec = (EncodingSpec("encoding"),
MultiSpec("people",
EncodedStringSpec("involvement"),
EncodedStringSpec("person")))
class PictureFrame(Frame):
def __init__(self, value=None, frameid=None, flags=None, frameno=None, **kwargs):
super().__init__(frameid=frameid, flags=flags, frameno=frameno, **kwargs)
if value is not None:
with open(value, "rb") as file:
self.data = file.read()
self.type = 0
self.desc = ""
format = imghdr.what(None, self.data[:32])
if not format:
format = value.rpartition(".")[2]
self._set_format(format)
@abstractmethod
def _set_format(self, format):
pass
def is_frame_class(cls):
return (isinstance(cls, type)
and issubclass(cls, Frame)
and 3 <= len(cls.__name__) <= 4
and cls.__name__ == cls.__name__.upper())
刚才我去看了一下,这个项目已经没人维护了,也的确有人提出过pull request,但没人接受
I took a look just now, this project is no longer maintained, and indeed someone made a pull request, but no one accepted it
所以我并不打算为stagger修复bug了,我将开始寻找新的解决方案。
So instead of fixing bugs for stagger, I'm going to start looking for new solutions.
BUG修复了,重新克隆仓库再试试。
BUG fixed, re-clone the repository and try again.
额,解决了吗?解决了记得回我
或者关闭话题也行
解决了,这几天在摸鱼打游戏,把这事情给忘了(笑哭)
但是显示只有几张壁纸没啥其他东西
解决了,这几天在摸鱼打游戏,把这事情给忘了(笑哭)
但是显示只有几张壁纸没啥其他东西
试试拖入mp3文件,带信息的那种,不带信息的也没关系
怎么运行这个项目?