Doushabao233 / MusicPlayer

A music player based on Pygame-ce. (work-in-progress) (temporarily stopped)
4 stars 0 forks source link

How run this project? #1

Closed MCheping8108 closed 1 year ago

MCheping8108 commented 1 year ago

怎么运行这个项目? image

Doushabao233 commented 1 year ago

好吧

对不起,这个程序目前依赖于一个有问题的模块。有关于stagger的代码问题……我通过修改源码的方式修复了它,所以,你需要修改tags.pyframes.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.pyframes.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

#
# 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

#
# 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())
Doushabao233 commented 1 year ago

刚才我去看了一下,这个项目已经没人维护了,也的确有人提出过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.

Doushabao233 commented 1 year ago

BUG修复了,重新克隆仓库再试试。
BUG fixed, re-clone the repository and try again.

Doushabao233 commented 1 year ago

额,解决了吗?解决了记得回我

或者关闭话题也行

MCheping8108 commented 1 year ago

解决了,这几天在摸鱼打游戏,把这事情给忘了(笑哭)

但是显示只有几张壁纸没啥其他东西

Doushabao233 commented 1 year ago

解决了,这几天在摸鱼打游戏,把这事情给忘了(笑哭)

但是显示只有几张壁纸没啥其他东西

试试拖入mp3文件,带信息的那种,不带信息的也没关系