Commits

Travis Shirk committed 0b09ab0 Merge

merge

Comments (0)

Files changed (12)

dev-requirements.txt

 paver==1.1.1
-nose
+nose2
 coverage
 Sphinx
 sphinxcontrib-bitbucket

docs/changelog.rst

 ChangeLog
 #########
 
+.. _release-0.7.2:
+# Up to date as of r222
+
+**0.7.2** - TBD (TBD)
+  New Features:
+    * Documentation and examples are now included in source distribution.
+    * [classic plugin] Removed ``-p`` for setting publisher since using it
+      when ``-P`` is intended is destructive.
+    * [statistics plgin] Added rules for "lint-like" checking of a collection.
+      The rules are not yet configurable.
+    * Error is now the default log level.
+
+  Bug fixes:
+    * Convert '/' to '-' in TagTemplate names (i.e. --rename)
+    * Drop TSIZ frames when converting to ID3 v2.4
+    * ID3 tag padding size now set correctly.
+    * Fixes for Unicode paths.
+    * License clarification in pkg-info.
+    * The ``-b`` setup.py argument is now properly supported.
+
 .. _release-0.7.1:
 .. _ID3 chapters and table-of-contents: http://www.id3.org/id3v2-chapters-1.0
 
 PROJECT = u"eyeD3"
 VERSION = "0.7.2"
 
-LICENSE     = open("COPYING", "r").read().strip('\n')
+LICENSE = open("COPYING", "r").read().strip('\n')
 DESCRIPTION = "Python audio data toolkit (ID3 and MP3)"
 LONG_DESCRIPTION = """
 eyeD3 is a Python module and command line program for processing ID3 tags.
 play time, etc.) is also provided. The formats supported are ID3
 v1.0/v1.1 and v2.3/v2.4.
 """
-URL          = "http://eyeD3.nicfit.net"
-AUTHOR       = "Travis Shirk"
+URL = "http://eyeD3.nicfit.net"
+AUTHOR = "Travis Shirk"
 AUTHOR_EMAIL = "travis@pobox.com"
 SRC_DIST_TGZ = "%s-%s.tgz" % (PROJECT, VERSION)
 SRC_DIST_ZIP = "%s.zip" % os.path.splitext(SRC_DIST_TGZ)[0]
-DOC_DIST     = "%s_docs-%s.tgz" % (PROJECT, VERSION)
-MD5_DIST     = "%s.md5" % os.path.splitext(SRC_DIST_TGZ)[0]
-DOC_BUILD_D  = "docs/_build"
+DOC_DIST = "%s_docs-%s.tgz" % (PROJECT, VERSION)
+MD5_DIST = "%s.md5" % os.path.splitext(SRC_DIST_TGZ)[0]
+DOC_BUILD_D = "docs/_build"
 
 PACKAGE_DATA = paver.setuputils.find_package_data("src/eyed3",
                                                   package="eyed3",
         url=URL,
         download_url="%s/releases/%s" % (URL, SRC_DIST_TGZ),
         license="GPL",
-        package_dir={"": "src" },
+        package_dir={"": "src"},
         packages=setuptools.find_packages("src",
                                           exclude=["test", "test.*"]),
         zip_safe=False,
     ),
 
     cog=Bunch(
-       beginspec='{{{cog',
-       endspec='}}}',
-       endoutput='{{{end}}}',
-       includedir=path(__file__).abspath().dirname(),
+        beginspec='{{{cog',
+        endspec='}}}',
+        endoutput='{{{end}}}',
+        includedir=path(__file__).abspath().dirname(),
     ),
 
     test=Bunch(
-       pdb=False,
-       coverage=False,
+        pdb=False,
+        coverage=False,
     ),
 
     release=Bunch(
     ),
 )
 
+
 @task
 @no_help
 def eyed3_info():
         target_file.write(src_data)
         target_file.close()
 
+
 @task
 @needs("eyed3_info",
        "setuptools.command.build")
     '''Build the code'''
     pass
 
+
 @task
 @needs("test_clean")
 def clean():
     except ImportError:
         pass
 
+
 @task
 def docs_clean(options):
     '''Clean docs'''
     except ImportError:
         pass
 
+
 @task
 @needs("distclean", "docs_clean")
 def maintainer_clean():
     path("paver-minilib.zip").remove()
     path("setup.py").remove()
 
+
 @task
 @needs("clean")
 def distclean():
         f.remove()
     path("src/eyed3/info.py").remove()
 
+
 @task
 @needs("cog")
 def docs(options):
     print("Docs: file://%s/%s/%s/html/index.html" %
           (os.getcwd(), options.docroot, options.builddir))
 
+
 @task
 @needs("distclean",
        "eyed3_info",
 
     pass
 
+
 @task
 def checklist():
     '''Show release procedure'''
 - ebuild
 """ % globals())
 
+
 @task
 @cmdopts([("test", "",
            u"Run in a mode where commits, pushes, etc. are performed"),
     resp = raw_input()
     return True if resp in ["y", "yes"] else False
 
+
 def cog_pluginHelp(name):
     from string import Template
     import argparse
     return template.substitute(substs)
 __builtins__["cog_pluginHelp"] = cog_pluginHelp
 
+
 # XXX: modified from paver.doctools._runcog to add includers
 def _runcog(options, uncog=False):
     """Common function for the cog and runcog tasks."""
         dry("cog %s" % f, c.processOneFile, f)
 
 from paver.doctools import Includer, _cogsh
+
+
 class CliExample(Includer):
     def __call__(self, fn, section=None, lang="bash"):
         # Resetting self.cog to get a string back from Includer.__call__
         raw = Includer.__call__(self, fn, section=section)
         self.cog = cog
 
-        commands = []
-
         self.cog.gen.out(u"\n.. code-block:: %s\n\n" % lang)
         for line in raw.splitlines(True):
             if line.strip() == "":
                 if output:
                     self.cog.gen.out("\n")
 
+
 @task
 def cog(options):
     '''Run cog on all docs'''

src/eyed3/__init__.py

 #
 ################################################################################
 '''Top-level module.'''
-import sys, locale, exceptions
+import sys
+import locale
+import exceptions
 
 
 _DEFAULT_ENCODING = "latin1"
     '''
     import types
     from .info import VERSION_TUPLE as CURRENT_VERSION
+
     def t2s(_t):
         return ".".join([str(v) for v in _t])
 

src/eyed3/id3/frames.py

 import re
 from cStringIO import StringIO
 from collections import namedtuple
+import logging
 
 from .. import core
 from ..utils import requireUnicode
 from ..utils.binfuncs import *
+from .. import Exception as BaseException
 from . import ID3_V2, ID3_V2_3, ID3_V2_4
 from . import (LATIN1_ENCODING, UTF_8_ENCODING, UTF_16BE_ENCODING,
                UTF_16_ENCODING, DEFAULT_LANG)
 from .headers import FrameHeader
 
-import logging
+
 log = logging.getLogger(__name__)
 
-from .. import Exception as BaseException
+
 class FrameException(BaseException):
     pass
 
         # Format flags in the frame header may add extra data to the
         # beginning of this data.
         if header.minor_version <= 3:
-            # 2.3:  compression(4), encryption(1), group(1) 
+            # 2.3:  compression(4), encryption(1), group(1)
             if header.compressed:
                 self.decompressed_size = bin2dec(bytes2bin(data[:4]))
                 data = data[4:]
                 data = data[4:]
                 log.debug("Data Length: %d" % self.data_len)
                 if header.compressed:
-                   self.decompressed_size = self.data_len
-                   log.debug("Decompressed Size: %d" % self.decompressed_size)
+                    self.decompressed_size = self.data_len
+                    log.debug("Decompressed Size: %d" % self.decompressed_size)
 
         if header.minor_version == 4 and header.unsync:
             data = deunsyncData(data)
     # Process a 3 byte language code (ISO 639-2).
     # This code must match the [A-Z][A-Z][A-Z]
     # (although case is ignored) and be ascii to be considered valid. When
-    # deemed invalid warnings are logged and the value is changed to 
+    # deemed invalid warnings are logged and the value is changed to
     # \c DEFAULT_LANG.
     #
     # \param lang The code.
         try:
             # Test ascii encoding, it MUST be
             lang = lang.encode("ascii")
-        except (UnicodeEncodeError, UnicodeDecodeError) as ex:
+        except (UnicodeEncodeError, UnicodeDecodeError):
             log.warning("Fixing invalid lyrics language code: %s" % lang)
             lang = DEFAULT_LANG
 
     def description(self, txt):
         self._description = txt
 
-    # Data string format: encoding (one byte) + description + "\x00" + text
     def parse(self, data, frame_header):
+        '''Data string format:
+        encoding (one byte) + description + "\x00" + text '''
         # Calling Frame, not TextFrame implementation here since TextFrame
         # does not know about description
         Frame.parse(self, data, frame_header)
         self.date = self.text
         self.encoding = LATIN1_ENCODING
 
+    def parse(self, data, frame_header):
+        super(DateFrame, self).parse(data, frame_header)
+        try:
+            if self.text:
+                _ = core.Date.parse(self.text.encode("latin1"))
+        except ValueError:
+            # Date is invalid, log it and reset.
+            core.parseError(FrameException(u"Invalid date: " + self.text))
+            self.text = u''
+
     @property
     def date(self):
         return core.Date.parse(self.text.encode("latin1")) if self.text \
         self.data = self.url
         return super(UrlFrame, self).render()
 
-##
-# Data string format:
-# encoding (one byte) + description + "\x00" + url (ascii)
+
 class UserUrlFrame(UrlFrame):
+    '''
+    Data string format:
+    encoding (one byte) + description + "\x00" + url (ascii)
+    '''
     @requireUnicode("description")
     def __init__(self, id=USERURL_FID, description=u"", url=""):
         UrlFrame.__init__(self, id, url=url)
                                            "type"))
         if (self.mime_type != self.URL_MIME_TYPE and
                 self.mime_type.find("/") == -1):
-           self.mime_type = "image/" + self.mime_type
+            self.mime_type = "image/" + self.mime_type
 
         pt = ord(input.read(1))
         log.debug("Initial APIC picture type: %d" % pt)
         log.debug("description len: %d" % len(desc))
         log.debug("image len: %d" % len(img))
         self.description = decodeUnicode(desc, encoding)
-        log.debug("APIC description: %s" % self.description);
+        log.debug("APIC description: %s" % self.description)
 
         if self.mime_type.find(self.URL_MIME_TYPE) != -1:
             self.image_data = None
         self.data = data
         return super(ImageFrame, self).render()
 
-
     @staticmethod
     def picTypeToString(t):
         if t == ImageFrame.OTHER:
         elif s == "PUBLISHER_LOGO":
             return ImageFrame.PUBLISHER_LOGO
         else:
-          raise ValueError("Invalid APIC picture type: %s" % s)
+            raise ValueError("Invalid APIC picture type: %s" % s)
 
 
 class ObjectFrame(Frame):
             self.mime_type = input.read(3)
         log.debug("GEOB mime type: %s" % self.mime_type)
         if not self.mime_type:
-           core.parseError(FrameException("GEOB frame does not contain a mime "
-                                          "type"))
+            core.parseError(FrameException("GEOB frame does not contain a "
+                                           "mime type"))
         if self.mime_type.find("/") == -1:
-           core.parseError(FrameException("GEOB frame does not contain a valid "
-                                          "mime type"))
+            core.parseError(FrameException("GEOB frame does not contain a "
+                                           "valid mime type"))
 
         self.filename = u""
         self.description = u""
     @property
     def rating(self):
         return self._rating
+
     @rating.setter
     def rating(self, rating):
         if rating < 0 or rating > 255:
     @property
     def email(self):
         return self._email
+
     @email.setter
     def email(self, email):
         self._email = email.encode("ascii")
     @property
     def count(self):
         return self._count
+
     @count.setter
     def count(self, count):
         if count < 0:
         self.data = data
         return super(PopularityFrame, self).render()
 
+
 class UniqueFileIDFrame(Frame):
     def __init__(self, id=UNIQUE_FILE_ID_FID, owner_id=None, uniq_id=None):
         super(UniqueFileIDFrame, self).__init__(id)
         self.data = self.owner_id + "\x00" + self.uniq_id
         return super(UniqueFileIDFrame, self).render()
 
+
 class DescriptionLangTextFrame(Frame):
 
     @requireUnicode(2, 4)
         super(CommentFrame, self).__init__(id, description, lang, text)
         assert(self.id == COMMENT_FID)
 
+
 class LyricsFrame(DescriptionLangTextFrame):
     def __init__(self, id=LYRICS_FID, description=u"", lang=DEFAULT_LANG,
                  text=u""):
         super(LyricsFrame, self).__init__(id, description, lang, text)
         assert(self.id == LYRICS_FID)
 
+
 class TermsOfUseFrame(Frame):
     @requireUnicode("text")
     def __init__(self, id="USER", text=u"", lang=DEFAULT_LANG):
                      self.text.encode(id3EncodingToString(self.encoding)))
         return super(TermsOfUseFrame, self).render()
 
+
 class TocFrame(Frame):
     '''Table of content frame. There may be more than one, but only one may
     have the top-level flag set.
 
 StartEndTuple = namedtuple("StartEndTuple", ["start", "end"])
 
+
 class ChapterFrame(Frame):
     '''Frame type for chapter/section of the audio file.
     <ID3v2.3 or ID3v2.4 frame header, ID: "CHAP">           (10 bytes)
         '''Read frames starting from the current read position of the file
         object. Returns the amount of padding which occurs after the tag, but
         before the audio content.  A return valule of 0 does not mean error.'''
-        from .headers import FrameHeader
-
         self.clear()
 
         padding_size = 0
             log.debug("De-unsynch'd %d bytes at once (<= 2.3 tag) to %d bytes" %
                       (og_size, size_left))
 
-        # Adding bytes to simulate the tag header(s) in the buffer.  This keeps 
+        # Adding bytes to simulate the tag header(s) in the buffer.  This keeps
         # f.tell() values matching the file offsets for logging.
         prepadding = '\x00' * 10  # Tag header
         prepadding += '\x00' * extended_header.size
             dict.__setitem__(self, fid, [frame])
 
     def getAllFrames(self):
+        '''Return all the frames in the set as a list. The list is sorted
+        in an arbitrary but consistent order.'''
         frames = []
         for flist in list(self.values()):
             frames += flist
+        frames.sort()
         return frames
 
     @requireUnicode(2)
         assert(fid[0] == "T" and fid in list(ID3_FRAMES.keys()))
 
         if fid in self:
-            curr = self[fid][0].text = text
+            self[fid][0].text = text
         else:
             if fid in DATE_FIDS:
                 self[fid] = DateFrame(fid, date=text)
             else:
                 self[fid] = TextFrame(fid, text=text)
 
+
 def deunsyncData(data):
     output = []
     safe = True
         if encoding == LATIN1_ENCODING or encoding == UTF_8_ENCODING:
             (d, t) = data.split("\x00", 1)
         elif encoding == UTF_16_ENCODING or encoding == UTF_16BE_ENCODING:
-            # Two null bytes split, but since each utf16 char is also two 
+            # Two null bytes split, but since each utf16 char is also two
             # bytes we need to ensure we found a proper boundary.
             (d, t) = data.split("\x00\x00", 1)
             if (len(d) % 2) != 0:
     else:
         raise ValueError("Encoding unknown: %s" % encoding)
 
+
 def stringToEncoding(s):
     s = s.replace('-', '_')
     if s in ("latin_1", "latin1"):
     "TCP" : "TCP ", # iTunes "extension" for compilation marking
     "CM1" : "CM1 ", # Seems to be some script kiddie tagging the tag.
                     # For example, [rH] join #rH on efnet [rH]
-    "PCS" : "PCST", # iTunes extension for podcast marking. 
+    "PCS" : "PCST", # iTunes extension for podcast marking.
 }
 
 import apple
 NONSTANDARD_ID3_FRAMES = {
-        "NCON": ("Undefined MusicMatch extension", ID3_V2, Frame),
-        "TCMP": ("iTunes complilation flag extension", ID3_V2, TextFrame),
-        "XSOA": ("Album sort-order string extension for v2.3",
-                 ID3_V2_3, TextFrame),
-        "XSOP": ("Performer sort-order string extension for v2.3",
-                 ID3_V2_3, TextFrame),
-        "XSOT": ("Title sort-order string extension for v2.3",
-                 ID3_V2_3, TextFrame),
-        "XDOR": ("MusicBrainz release date (full) extension for v2.3",
-                 ID3_V2_3, TextFrame),
+    "NCON": ("Undefined MusicMatch extension", ID3_V2, Frame),
+    "TCMP": ("iTunes complilation flag extension", ID3_V2, TextFrame),
+    "XSOA": ("Album sort-order string extension for v2.3",
+             ID3_V2_3, TextFrame),
+    "XSOP": ("Performer sort-order string extension for v2.3",
+             ID3_V2_3, TextFrame),
+    "XSOT": ("Title sort-order string extension for v2.3",
+             ID3_V2_3, TextFrame),
+    "XDOR": ("MusicBrainz release date (full) extension for v2.3",
+             ID3_V2_3, TextFrame),
 
-        "PCST": ("iTunes extension; marks the file as a podcast",
-                 ID3_V2, apple.PCST),
-        "TKWD": ("iTunes extension; podcast keywords?",
-                 ID3_V2, apple.TKWD),
-        "TDES": ("iTunes extension; podcast description?",
-                 ID3_V2, apple.TDES),
-        "TGID": ("iTunes extension; podcast ?????",
-                 ID3_V2, apple.TGID),
-        "WFED": ("iTunes extension; podcast feed URL?",
-                 ID3_V2, apple.WFED),
+    "PCST": ("iTunes extension; marks the file as a podcast",
+             ID3_V2, apple.PCST),
+    "TKWD": ("iTunes extension; podcast keywords?",
+             ID3_V2, apple.TKWD),
+    "TDES": ("iTunes extension; podcast description?",
+             ID3_V2, apple.TDES),
+    "TGID": ("iTunes extension; podcast ?????",
+             ID3_V2, apple.TGID),
+    "WFED": ("iTunes extension; podcast feed URL?",
+             ID3_V2, apple.WFED),
 }
 

src/eyed3/id3/tag.py

 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
 ################################################################################
-import types, sys, string, re, os, shutil, types, tempfile
+import os
+import types
+import string
+import shutil
+import tempfile
 
 from ..utils import requireUnicode, chunkCopy
 from .. import core
+from .. import Exception as BaseException
 from . import (ID3_ANY_VERSION, ID3_V1, ID3_V1_0, ID3_V1_1,
                ID3_V2, ID3_V2_2, ID3_V2_3, ID3_V2_4, versionToString)
 from . import DEFAULT_LANG
 from . import frames
 from .headers import TagHeader, ExtendedTagHeader
 
-
 import logging
 log = logging.getLogger(__name__)
 
-from .. import Exception as BaseException
+
 class TagException(BaseException):
-   pass
+    pass
+
 
 ID3_V1_COMMENT_DESC = u"ID3v1.x Comment"
 DEFAULT_PADDING = 1024
 
+
 class Tag(core.Tag):
     def __init__(self):
         core.Tag.__init__(self)
         ## Optional extended header in v2 tags.
         self.extended_header = ExtendedTagHeader()
         ## Contains the tag's frames. ID3v1 fields are read and converted
-        #  the the corresponding v2 frame.  
+        #  the the corresponding v2 frame.
         self.frame_set = frames.FrameSet()
         self._comments = CommentsAccessor(self.frame_set)
         self._images = ImagesAccessor(self.frame_set)
         version = version or ID3_ANY_VERSION
 
         close_file = False
-        if type(fileobj) is types.FileType:
+        if isinstance(fileobj, types.FileType):
             filename = fileobj.name
         elif type(fileobj) in types.StringTypes:
             filename = fileobj
         if artist:
             self.artist = unicode(artist, v1_enc)
 
-
         album = tag_data[63:93].strip(STRIP_CHARS)
         log.debug("Album: %s" % album)
         if album:
     def version(self, v):
         self.header.version = v
 
-    ## Test ID3 major version for v1.x
     def isV1(self):
+        '''Test ID3 major version for v1.x'''
         return self.header.major_version == 1
-    ## Test ID3 major version for v2.x
+
     def isV2(self):
+        '''Test ID3 major version for v2.x'''
         return self.header.major_version == 2
 
     @requireUnicode(2)
     @requireUnicode(1)
     def _setArtist(self, val):
         self.setTextFrame(frames.ARTIST_FID, val)
+
     def _getArtist(self):
         return self.getTextFrame(frames.ARTIST_FID)
 
     @requireUnicode(1)
     def _setAlbum(self, val):
         self.setTextFrame(frames.ALBUM_FID, val)
+
     def _getAlbum(self):
         return self.getTextFrame(frames.ALBUM_FID)
 
     @requireUnicode(1)
     def _setTitle(self, val):
         self.setTextFrame(frames.TITLE_FID, val)
+
     def _getTitle(self):
         return self.getTextFrame(frames.TITLE_FID)
 
 
         n = (tn, tt)
 
-        if n[0] == None and n[1] == None:
+        if n[0] is None and n[1] is None:
             if self.frame_set[fid]:
                 del self.frame_set[fid]
             return
 
         total_str = ""
-        if n[1] != None:
+        if n[1] is not None:
             if n[1] >= 0 and n[1] <= 9:
                 total_str = "0" + str(n[1])
             else:
             bpm_str = self.frame_set[frames.BPM_FID][0].text or u"0"
             try:
                 # Round floats since the spec says this is an integer
-                bpm = int(float(bpm_str) + 0.5)
+                bpm = int(round(float(bpm_str)))
             except ValueError as ex:
                 log.warning(ex)
         return bpm
 
     def _setBpm(self, bpm):
-       assert(bpm >= 0)
-       self.setTextFrame(frames.BPM_FID, unicode(str(bpm)))
+        assert(bpm >= 0)
+        self.setTextFrame(frames.BPM_FID, unicode(str(bpm)))
 
     bpm = property(_getBpm, _setBpm)
 
     def play_count(self):
         if frames.PLAYCOUNT_FID in self.frame_set:
             pc = self.frame_set[frames.PLAYCOUNT_FID][0]
-            assert(type(pc.count) in (int,))
             return pc.count
         else:
             return None
             pc.count = count
         else:
             self.frame_set[frames.PLAYCOUNT_FID] = \
-                    frames.PlayCountFrame(count=count)
+                frames.PlayCountFrame(count=count)
 
     def _getPublisher(self):
         if frames.PUBLISHER_FID in self.frame_set:
             cdid.toc = str(toc)
         else:
             self.frame_set[frames.CDID_FID] = \
-                    frames.MusicCDIdFrame(toc=toc)
+                frames.MusicCDIdFrame(toc=toc)
 
     @property
     def images(self):
 
     def _getEncodingDate(self):
         return self._getDate("TDEN")
+
     def _setEncodingDate(self, date):
         self._setDate("TDEN", date)
     encoding_date = property(_getEncodingDate, _setEncodingDate)
                 self.recording_date)
 
     def _getReleaseDate(self):
-        return self._getDate("TDRL") if self.version == ID3_V2_4\
+        return self._getDate("TDRL") if self.version == ID3_V2_4 \
                                      else self._getV23OrignalReleaseDate()
+
     def _setReleaseDate(self, date):
         self._setDate("TDRL" if self.version == ID3_V2_4 else "TORY", date)
 
 
     def _getOrigReleaseDate(self):
         return self._getDate("TDOR") or self._getV23OrignalReleaseDate()
+
     def _setOrigReleaseDate(self, date):
         self._setDate("TDOR", date)
 
 
     def _getRecordingDate(self):
         return self._getDate("TDRC") or self._getV23RecordingDate()
+
     def _setRecordingDate(self, date):
         if self.version == ID3_V2_4:
             self._setDate("TDRC", date)
 
     def _getTaggingDate(self):
         return self._getDate("TDTG")
+
     def _setTaggingDate(self, date):
         self._setDate("TDTG", date)
     tagging_date = property(_getTaggingDate, _setTaggingDate)
 
     def _setDate(self, fid, date):
         assert(fid in frames.DATE_FIDS or
-                fid in frames.DEPRECATED_DATE_FIDS)
+               fid in frames.DEPRECATED_DATE_FIDS)
 
         if date is None:
             try:
     @property
     def disc_num(self):
         return self._splitNum(frames.DISCNUM_FID)
+
     @disc_num.setter
     def disc_num(self, val):
         self._setNum(frames.DISCNUM_FID, val)
             return Genre.parse(f[0].text)
         else:
             return None
+
     def _setGenre(self, g):
         '''
         Set the genre. Four types are accepted for the ``g`` argument.
     @property
     def commercial_url(self):
         return self._getUrlFrame(frames.URL_COMMERCIAL_FID)
+
     @commercial_url.setter
     def commercial_url(self, url):
         self._setUrlFrame(frames.URL_COMMERCIAL_FID, url)
     @property
     def copyright_url(self):
         return self._getUrlFrame(frames.URL_COPYRIGHT_FID)
+
     @copyright_url.setter
     def copyright_url(self, url):
         self._setUrlFrame(frames.URL_COPYRIGHT_FID, url)
     @property
     def audio_file_url(self):
         return self._getUrlFrame(frames.URL_AUDIOFILE_FID)
+
     @audio_file_url.setter
     def audio_file_url(self, url):
         self._setUrlFrame(frames.URL_AUDIOFILE_FID, url)
     @property
     def audio_source_url(self):
         return self._getUrlFrame(frames.URL_AUDIOSRC_FID)
+
     @audio_source_url.setter
     def audio_source_url(self, url):
         self._setUrlFrame(frames.URL_AUDIOSRC_FID, url)
     @property
     def artist_url(self):
         return self._getUrlFrame(frames.URL_ARTIST_FID)
+
     @artist_url.setter
     def artist_url(self, url):
         self._setUrlFrame(frames.URL_ARTIST_FID, url)
     @property
     def internet_radio_url(self):
         return self._getUrlFrame(frames.URL_INET_RADIO_FID)
+
     @internet_radio_url.setter
     def internet_radio_url(self, url):
         self._setUrlFrame(frames.URL_INET_RADIO_FID, url)
     @property
     def payment_url(self):
         return self._getUrlFrame(frames.URL_PAYMENT_FID)
+
     @payment_url.setter
     def payment_url(self, url):
         self._setUrlFrame(frames.URL_PAYMENT_FID, url)
     @property
     def publisher_url(self):
         return self._getUrlFrame(frames.URL_PUBLISHER_FID)
+
     @publisher_url.setter
     def publisher_url(self, url):
         self._setUrlFrame(frames.URL_PUBLISHER_FID, url)
             shutil.copyfile(self.file_info.name, backup_name)
 
         if version[0] == 1:
-            self.__saveV1Tag(version)
+            self._saveV1Tag(version)
         elif version[0] == 2:
-            self.__saveV2Tag(version, encoding)
+            self._saveV2Tag(version, encoding)
         else:
             assert(not "Version bug: %s" % str(version))
 
-    def __saveV1Tag(self, version):
+    def _saveV1Tag(self, version):
         assert(version[0] == 1)
 
         def pack(s, n):
 
         cmt = ""
         for c in self.comments:
-           if c.description == ID3_V1_COMMENT_DESC:
-              cmt = c.text
-              # We prefer this one over ""
-              break
-           elif c.description == "":
-              cmt = c.text
-              # Keep searching in case we find the description eyeD3 uses.
+            if c.description == ID3_V1_COMMENT_DESC:
+                cmt = c.text
+                # We prefer this one over ""
+                break
+            elif c.description == "":
+                cmt = c.text
+                # Keep searching in case we find the description eyeD3 uses.
         cmt = pack(cmt.encode("latin_1"), 30)
 
         if version != ID3_V1_0:
-           track = self.track_num[0]
-           if track != None:
-              cmt = cmt[0:28] + "\x00" + chr(int(track) & 0xff)
+            track = self.track_num[0]
+            if track is not None:
+                cmt = cmt[0:28] + "\x00" + chr(int(track) & 0xff)
         tag += cmt
 
         if not self.genre or self.genre.id is None:
-           genre = 0
+            genre = 0
         else:
-           genre = self.genre.id
+            genre = self.genre.id
         tag += chr(genre & 0xff)
 
         assert(len(tag) == 128)
                 else:
                     tag_file.seek(0, 2)
             except IOError:
-               # File is smaller than 128 bytes.
-               tag_file.seek(0, 2)
+                # File is smaller than 128 bytes.
+                tag_file.seek(0, 2)
 
             tag_file.write(tag)
             tag_file.flush()
                    {"tag_header": header_data,
                     "ext_header": ext_header_data,
                     "frames": frame_data,
-                   }
+                    }
         assert(len(tag_data) == (total_size - padding_size))
         return (rewrite_required, tag_data, "\x00" * padding_size)
 
-    def __saveV2Tag(self, version, encoding):
+    def _saveV2Tag(self, version, encoding):
         assert(version[0] == 2 and version[1] != 2)
         log.debug("Rendering tag version: %s" % versionToString(version))
 
         flist = list(convert_list)
 
         # Date frame conversions.
-        date_frames = {f.id: f for f in flist if f.id in DEPRECATED_DATE_FIDS}\
-                      if version == ID3_V2_4 else \
-                      {f.id: f for f in flist if f.id in DATE_FIDS}
+        date_frames = {}
+        for f in flist:
+            if version == ID3_V2_4:
+                if f.id in DEPRECATED_DATE_FIDS:
+                    date_frames[f.id] = f
+            else:
+                if f.id in DATE_FIDS:
+                    date_frames[f.id] = f
+
         if date_frames:
             if version == ID3_V2_4:
                 if "TORY" in date_frames or "XDOR" in date_frames:
 
 
 ##
-# This class is for storing information about a parsed file. It containts info 
+# This class is for storing information about a parsed file. It containts info
 # such as the filename, original tag size, and amount of padding all of which
 # can make rewriting faster.
 class FileInfo:
                 # Work around the local encoding not matching that of a mounted
                 # filesystem
                 log.warning(u"Mismatched file system encoding for file '%s'" %
-                            file_name)
+                            repr(file_name))
                 self.name = file_name
 
         self.tag_size = 0  # This includes the padding byte count.
         self.tag_padding_size = 0
 
+
 class AccessorBase(object):
     def __init__(self, fid, fs, match_func=None):
         self._fid = fid
     def get(self, description, lang=DEFAULT_LANG):
         return super(DltAccessor, self).get(description, lang=lang)
 
+
 class CommentsAccessor(DltAccessor):
     def __init__(self, fs):
         super(CommentsAccessor, self).__init__(frames.CommentFrame,
                                                frames.COMMENT_FID, fs)
 
+
 class LyricsAccessor(DltAccessor):
     def __init__(self, fs):
         super(LyricsAccessor, self).__init__(frames.LyricsFrame,
                                              frames.LYRICS_FID, fs)
 
+
 class ImagesAccessor(AccessorBase):
     def __init__(self, fs):
         def match_func(frame, description):
     def get(self, description):
         return super(ImagesAccessor, self).get(description)
 
+
 class ObjectsAccessor(AccessorBase):
     def __init__(self, fs):
 
     def get(self, description):
         return super(ObjectsAccessor, self).get(description)
 
+
 class PrivatesAccessor(AccessorBase):
     def __init__(self, fs):
 
     def get(self, owner_id):
         return super(PrivatesAccessor, self).get(owner_id)
 
+
 class UserTextsAccessor(AccessorBase):
     def __init__(self, fs):
         def match_func(frame, description):
     def get(self, description):
         return super(UserTextsAccessor, self).get(description)
 
+
 class UniqueFileIdAccessor(AccessorBase):
     def __init__(self, fs):
         def match_func(frame, owner_id):
     def get(self, description):
         return super(UserUrlsAccessor, self).get(description)
 
+
 class PopularitiesAccessor(AccessorBase):
     def __init__(self, fs):
         def match_func(frame, email):
             return frame.element_id == element_id
         super(ChaptersAccessor, self).__init__(frames.CHAPTER_FID, fs,
                                                match_func)
+
     def set(self, element_id, times, offsets=(None, None), sub_frames=None):
         flist = self._fs[frames.CHAPTER_FID] or []
         for chap in flist:
         raise IndexError("toc '%s' not found" % elem_id)
 
 
-
-import string
 class TagTemplate(string.Template):
     idpattern = r'[_a-z][_a-z0-9:]*'
 

src/eyed3/main.py

 #
 ################################################################################
 from __future__ import print_function
-import sys, exceptions, os.path
-import ConfigParser
-import traceback
+import sys
+import exceptions
+import os.path
 import textwrap
-import eyed3, eyed3.utils, eyed3.utils.cli, eyed3.plugins, eyed3.info
+import eyed3
+import eyed3.utils
+import eyed3.utils.cli
+import eyed3.plugins
+import eyed3.info
 
 
 DEFAULT_PLUGIN = "classic"
 
 
 def _listPlugins(config):
-    from eyed3.utils.cli import GREEN, GREY, boldText, colorText
+    from eyed3.utils.cli import GREEN, GREY, boldText
 
     print("")
+
     def header(name):
         is_default = name == DEFAULT_PLUGIN
         return (boldText("* ", c=GREEN if is_default else None) +
 
     if args.config:
         config_file = os.path.abspath(config_file)
-    elif args.no_config == False:
+    elif args.no_config is False:
         config_file = DEFAULT_CONFIG
 
     if not config_file:
 
     return config
 
+
 def _getPluginPath(config):
     plugin_path = [eyed3.info.USER_PLUGINS_DIR]
 
     '''This is the main function for profiling
     http://code.google.com/appengine/kb/commontasks.html#profiling
     '''
-    import cProfile, pstats, StringIO
+    import cProfile
+    import pstats
+    import StringIO
 
     eyed3.log.debug("driver profileMain")
     prof = cProfile.Profile()
                        help="Do not load the default user config '%s'. "
                             "The -c/--config options are still honored if "
                             "present." % DEFAULT_CONFIG)
+        p.add_argument("--no-color", action="store_true", dest="no_color",
+                       help="Do not load the default user config '%s'. "
+                            "Suppress color codes in console output.")
 
         # Debugging options
         group = p.debug_arg_group
         args, _, config = parseCommandLine()
 
         for fp in [sys.stdout, sys.stderr]:
-            eyed3.utils.cli.enableColorOutput(fp, os.isatty(fp.fileno()))
+            color = not args.no_color and os.isatty(fp.fileno())
+            eyed3.utils.cli.enableColorOutput(fp, color)
 
-        mainFunc = main if args.debug_profile == False else profileMain
+        mainFunc = main if args.debug_profile is False else profileMain
         retval = mainFunc(args, config)
     except KeyboardInterrupt:
         retval = 0

src/eyed3/plugins/__init__.py

 ################################################################################
 from __future__ import print_function
 import os, sys, logging, exceptions, types
-from collections import OrderedDict
+
+try:
+    from collections import OrderedDict
+except ImportError:
+    from ordereddict import OrderedDict
 from eyed3 import core, utils
 from eyed3.utils.cli import printMsg, printError
 

src/eyed3/plugins/classic.py

                 raise ValueError("too few parts")
 
             path, type_str = args[:2]
-            desc = args[2] if len(args) > 2 else u""
+            desc = unicode(args[2], LOCAL_ENCODING) if len(args) > 2 else u""
             mt = None
             try:
                 type_id = id3.frames.ImageFrame.stringToPicType(type_str)

src/eyed3/utils/__init__.py

 #
 ################################################################################
 from __future__ import print_function
-import os, re
+import os
+import re
 
-import mimetypes, StringIO
+ID3_MIME_TYPE = "application/x-id3"
+ID3_MIME_TYPE_EXTENSIONS = ("id3", "tag")
+
+import StringIO
+import mimetypes
 _mime_types = mimetypes.MimeTypes()
-_mime_types.readfp(StringIO.StringIO("application/x-id3 id3 tag"))
+_mime_types.readfp(StringIO.StringIO("%s %s" %
+                                     (ID3_MIME_TYPE,
+                                      " ".join(ID3_MIME_TYPE_EXTENSIONS))))
 del mimetypes
 del StringIO
 
     import magic as magic_mod
     # Need to handle different versions of magic, as the new
     # APIs are totally different
-    if hasattr(magic_mod, open) and hasattr(magic_mod, load):
+    if hasattr(magic_mod, "open") and hasattr(magic_mod, "load"):
         # old magic
-        _magic = magic_mod.open(magic.MAGIC_SYMLINK | magic.MAGIC_MIME)
+        _magic = magic_mod.open(magic_mod.MAGIC_SYMLINK | magic_mod.MAGIC_MIME)
         _magic.load()
+
         def magic_func(path):
             return _magic.file(path)
     else:
         # new magic
         _magic = magic_mod.Magic(mime=True)
+
         def magic_func(path):
-            return _magic.from_file(path)
+            # If a unicode path is passed a conversion to ascii is attempted
+            # by from_file. Give the path encoded per LOCAL_FS_ENCODING
+            return _magic.from_file(path.encode(LOCAL_FS_ENCODING))
 except:
     magic_func = None
 
     mime = None
 
     if magic_func:
-        mime = magic_func(filename)
-        if mime:
-            mime = mime.split(";")[0]
+        if (os.path.splitext(filename)[1] in
+                (".%s" % ext for ext in ID3_MIME_TYPE_EXTENSIONS)):
+            # Need to check custom types manually if not using _mime_types
+            mime = ID3_MIME_TYPE
+        else:
+            mime = magic_func(filename)
+            if mime:
+                mime = mime.split(";")[0]
 
     if not mime:
         mime, enc = _mime_types.guess_type(filename, strict=False)
         '''Called when there are no more files to handle.'''
         pass
 
+
 def requireUnicode(*args):
     '''Function decorator to enforce unicode argument types.
     ``None`` is a valid argument value, in all cases, regardless of not being
     return "%.2f %s" % (sz, unit)
 
 
-##
-# \brief Format a timedelta object into a string
-# \param td The timedelta to represent.
 def formatTimeDelta(td):
+    '''Format a timedelta object ``td`` into a string. '''
     days = td.days
     hours = td.seconds / 3600
     mins = (td.seconds % 3600) / 60
         else:
             done = True
         del data
-

src/test/__init__.py

 #
 ################################################################################
 from StringIO import StringIO
-import os, logging, sys
+import os
+import sys
+import logging
 import eyed3
 
 DATA_D = os.path.join(os.path.abspath(os.path.curdir), "src", "test", "data")
             if not s.isatty():
                 s.seek(self._seek_offset)
         sys.stdout, sys.stderr = self._orig_stdout, self._orig_stderr
-

src/test/id3/test_tag.py

         assert_equal(name, unicode(name))
         assert_equal(fi.tag_size, 0)
 
-    # FIXME Passing invalid unicode 
+    # FIXME Passing invalid unicode
 
 def testTagMainProps():
     tag = Tag()
     tag.user_url_frames.set("Foobazz", u"Desc2")
     assert_equal(len(tag.user_url_frames), 1)
 
+
 def testSortOrderConversions():
     test_file = "/tmp/soconvert.id3"