Travis Shirk avatar Travis Shirk committed 021084b Merge

Merge

Comments (0)

Files changed (11)

etc/config.ini

-
-[DEFAULT]
-plugin = classic
-options = -Q --pdb -l debug
+
+[default]
+
+# Default plugin to use.
+plugin = 
+
+# General options to always use. These can be plugin specific but SHOULD NOT be
+# A -C/--config and -P/--plugin options are ignored.
+options = -Q --pdb -l debug
+
+# Extra directories to load plugins. Separated by ':'
+plugin_path = ~/.eyeD3:${HOME}/.eyeD3/plugins
+
+
+
 
 - clean working copy / use sandbox
 - Set version in ``version`` file.
+- Update doc/changelog.rst with date, features, etc.
 - paver release
 - hg commit -m 'prep for release'
 

src/eyed3/id3/frames.py

         self.text = decodeUnicode(self.data[1:], self.encoding)
         log.debug("TextFrame text: %s" % self.text)
 
-    # TODO: writing, XSO* can only be carried over in v2.3, 
-    # in 2.4 they should be converted to TSO*
-    # TODO: writing, XDOR only v2.3, convert to TDRC for v2.4
-
     def render(self):
         self._initEncoding()
         self.data = b"%s%s" % \
     log.debug("createFrame '%s' with class '%s'" % (fid, FrameClass))
     if tag_header.version[:2] == (2, 4) and tag_header.unsync:
         frame_header.unsync = True
+
     frame = FrameClass(fid)
     frame.parse(data, frame_header)
     return frame
         return orig_id
     return TAGS2_2_TO_TAGS_2_3_AND_4[orig_id]
 
-# FIXME: these mappings do not handle 2.3 *and* 2.4 support..
-#        TOR->TORY(2.3)->???(2.4)
-#        needs a test case where v2.2 containing TOR is converted to 2.4 
-#        which does not use TORY
-#
 # mapping of 2.2 frames to 2.3/2.4
 TAGS2_2_TO_TAGS_2_3_AND_4 = {
     "TT1" : "TIT1", # CONTENTGROUP content group description

src/eyed3/id3/tag.py

         std_frames = []
         converted_frames = []
         for f in self.frame_set.getAllFrames():
-            _, fversion, _ = frames.ID3_FRAMES[f.id]
-            if fversion in (version, ID3_V2):
-                std_frames.append(f)
-            else:
-                converted_frames.append(f)
+            try:
+                _, fversion, _ = frames.ID3_FRAMES[f.id]
+                if fversion in (version, ID3_V2):
+                    std_frames.append(f)
+                else:
+                    converted_frames.append(f)
+            except KeyError:
+                # Not a standard frame (ID3_FRAMES)
+                try:
+                    _, fversion, _ = frames.NONSTANDARD_ID3_FRAMES[f.id]
+                    # but it is one we can handle.
+                    if fversion in (version, ID3_V2):
+                        std_frames.append(f)
+                    else:
+                        converted_frames.append(f)
+                except KeyError:
+                    # Don't know anything about this pass it on for the error
+                    # check there.
+                    converted_frames.append(f)
 
         if converted_frames:
             # actually, they're not converted yet
         file_exists = os.path.exists(self.file_info.name)
 
         if encoding:
+            # Any invalid encoding is going to get coersed to a valid value
+            # when the frame is rendered.
             for f in self.frame_set.getAllFrames():
                 f.encoding = frames.stringToEncoding(encoding)
 
 
                 flist.remove(date_frames[fid])
 
+        # Convert sort order frames 2.3 (XSO*) <-> 2.4 (TSO*)
+        prefix = "X" if version == ID3_V2_4 else "T"
+        fids = ["%s%s" % (prefix, suffix) for suffix in ["SOA", "SOP", "SOT"]]
+        soframes = [f for f in flist if f.id in fids]
+
+        for frame in soframes:
+            frame.id = ("X" if prefix == "T" else "T") + frame.id[1:]
+            flist.remove(frame)
+            converted_frames.append(frame)
+
+        # TODO: writing, XDOR only v2.3, convert to TDRC for v2.4
+
         if len(flist) != 0:
             unconverted = ", ".join([f.id for f in flist])
             raise TagException("Unable to covert the following frames to "
 
         return retval
 
-        # FIXME: work in progress.. want to make smarter properties for
-        # encodings, defaults, and checking
-        def setTextEncoding(self, enc):
-            if enc not in (LATIN1_ENCODING, UTF_16_ENCODING,
-                           UTF_16BE_ENCODING, UTF_8_ENCODING):
-                raise ValueError("Invalid encoding")
-            elif self.getVersion() & ID3_V1 and enc != LATIN1_ENCODING:
-                raise TagException("ID3 v1.x supports ISO-8859 encoding only")
-            elif self.getVersion() <= ID3_V2_3 and enc == UTF_8_ENCODING:
-                # This is unfortunate.
-                raise TagException("UTF-8 is not supported by ID3 v2.3")
-
-            self.encoding = enc
-            for f in self.frame_set:
-                f.encoding = enc
 
 ##
 # This class is for storing information about a parsed file. It containts info 
         return None
 
     def remove(self, *args, **kwargs):
+        '''Returns the removed item or ``None`` if not found.'''
         fid_frames = self._fs[self._fid] or []
         for frame in fid_frames:
             if self._match_func(frame, *args, **kwargs):
                                                    fs, match_func)
 
     def set(self, data, owner_id):
+        data = str(data)
+        if len(data) > 64:
+            raise TagException("UFID data must be 64 bytes or less")
+
         flist = self._fs[frames.UNIQUE_FILE_ID_FID] or []
         for f in flist:
             if f.owner_id == owner_id:

src/eyed3/info.py.in

 %s
 """ % (VERSION, RELEASE, AUTHOR, URL)
 
-USER_DIR = os.path.expandvars(os.path.join("${HOME}", ".eyeD3"))
-PLUGIN_DIRS = [ os.path.join(USER_DIR, "plugins") ]
+USER_CONFIG = os.path.expandvars(os.path.join("${HOME}", ".eyeD3rc"))
 
 LICENCE = """
 		    GNU GENERAL PUBLIC LICENSE

src/eyed3/main.py

 
 
 DEFAULT_PLUGIN = "classic"
-DEFAULT_CONFIG = os.path.join(eyed3.info.USER_DIR, "config.ini")
+DEFAULT_CONFIG = eyed3.info.USER_CONFIG
 
 
 def main(args, config):
     config = _loadConfig(args.config)
 
     if args.plugin:
-        # Plugins on the command line take precedence over config.
+        # Plugin on the command line takes precedence over config.
         plugin_name = args.plugin
-    elif config:
+    elif config and config.has_option("default", "plugin"):
         # Get default plugin from config or use DEFAULT_CONFIG
-        try:
-            plugin_name = config.get("DEFAULT", "plugin")
-        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) as ex:
+        plugin_name = config.get("default", "plugin")
+        if not plugin_name:
             plugin_name = DEFAULT_PLUGIN
     else:
         plugin_name = DEFAULT_PLUGIN
     assert(plugin_name)
 
-    PluginClass = eyed3.plugins.load(plugin_name)
+    plugin_path = []
+    if config and config.has_option("default", "plugin_path"):
+        val = config.get("default", "plugin_path")
+        plugin_path = [os.path.expanduser(os.path.expandvars(d)) for d
+                            in val.split(':')]
+
+
+    PluginClass = eyed3.plugins.load(plugin_name, paths=plugin_path)
     if PluginClass is None:
         eyed3.utils.cli.printError("Plugin not found: %s" % plugin_name)
         parser.exit(1)
     plugin = PluginClass(parser)
 
-    # Reparse the command line with options from the config.
-    if config:
-        try:
-            config_opts = config.get("DEFAULT", "options").split()
-            cmd_line_args.extend(config_opts)
-        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) as ex:
-            pass
+    if config and config.has_option("default", "options"):
+        cmd_line_args.extend(config.get("default", "options").split())
+
+    # Reparse the command line including options from the config.
     args = parser.parse_args(args=cmd_line_args)
 
     if args.list_plugins:

src/eyed3/mp3/__init__.py

             try:
                 self._info = Mp3AudioInfo(file_obj, mp3_offset, self._tag)
             except Mp3Exception as ex:
-                # FIXME: core.parseError() or is this even needed?
+                # Only logging a warning here since we can still operate on 
+                # the tag.
                 log.warning(ex)
                 self._info = None
 

src/eyed3/plugins/__init__.py

 
 log = logging.getLogger(__name__)
 
-def load(name=None, reload=False):
+def load(name=None, reload=False, paths=None):
     '''Returns the eyed3.plugins.Plugin *class* identified by ``name``.
     If ``name`` is ``None`` then the full list of plugins is returned.
     Once a plugin is loaded its class object is cached, and future calls to
     this function will returned the cached version. Use ``reload=True`` to
     refresh the cache.'''
-    from eyed3.info import PLUGIN_DIRS
     global _PLUGINS
 
     if len(_PLUGINS.keys()) and reload == False:
                     and f[0] not in ('_', '.')
                     and f.endswith(".py"))
 
-    for d in [os.path.dirname(__file__)] + PLUGIN_DIRS:
+    for d in [os.path.dirname(__file__)] + (paths if paths else []):
         log.debug("Searching '%s' for plugins", d)
         if not os.path.isdir(d):
             continue

src/eyed3/plugins/classic.py

             desc = vals[0].strip() or u""
             text = vals[1] if len(vals) > 1 else u""
             return (desc, text)
+        KeyValueArg = DescTextArg
         def DescUrlArg(arg):
             desc, url = DescTextArg(arg)
             return (desc, url.encode("latin1"))
             else:
                 raise ValueError("path required")
             return (path, mt, desc, filename)
+        def UniqFileIdArg(arg):
+            owner_id, id = KeyValueArg(arg)
+            if not owner_id:
+                raise ValueError("owner_id required")
+            id = str(id) # don't want to pass unicocode
+            if len(id) > 64:
+                raise ValueError("id must be <= 64 bytes")
+            return (owner_id, id)
 
         # Tag versions
         gid3.add_argument("-1", "--v1", action="store_const", const=id3.ID3_V1,
                           help=ARGS_HELP["--play-count"])
         gid3.add_argument("--bpm", type=BpmArg, dest="bpm", metavar="N",
                           default=None, help=ARGS_HELP["--bpm"])
-        gid3.add_argument("--unique-file-id", action="append", type=str,
-                          dest="unique_file_ids", metavar="OWNER_ID:ID",
-                          default=[], help=ARGS_HELP["--unique-file-id"])
+        gid3.add_argument("--unique-file-id", action="append",
+                          type=UniqFileIdArg, dest="unique_file_ids",
+                          metavar="OWNER_ID:ID", default=[],
+                          help=ARGS_HELP["--unique-file-id"])
 
         # Comments
         gid3.add_argument("--add-comment", action="append", dest="comments",
                           choices=_encodings, metavar='|'.join(_encodings),
                           help=ARGS_HELP["--encoding"])
 
-        # Misc options in the main group
-        g.add_argument("--force-update", action="store_true", default=False,
-                       dest="force_update", help=ARGS_HELP["--force-update"])
-        g.add_argument("-F", dest="field_delim", default=FIELD_DELIM,
-                       metavar="CHAR", help=ARGS_HELP["-F"])
-        g.add_argument("-v", "--verbose", action="store_true", dest="verbose",
-                       help=ARGS_HELP["--verbose"])
+        # Misc options 
+        gid4 = arg_parser.add_argument_group("Misc options")
+        gid4.add_argument("--backup", action="store_true", default=False,
+                          dest="backup", help=ARGS_HELP["--backup"])
+        gid4.add_argument("--force-update", action="store_true", default=False,
+                          dest="force_update", help=ARGS_HELP["--force-update"])
+        gid4.add_argument("-F", dest="field_delim", default=FIELD_DELIM,
+                          metavar="CHAR", help=ARGS_HELP["-F"])
+        gid4.add_argument("-v", "--verbose", action="store_true",
+                          dest="verbose", help=ARGS_HELP["--verbose"])
 
 
     def start(self, args, config):
                            self.audio_file.tag.version)
                 printWarning("Writing ID3 version %s" %
                              id3.versionToString(version))
-                # FIXME: backup option for cli
+
                 self.audio_file.tag.save(version=version,
                                          encoding=self.args.text_encoding,
-                                         backup=False)
+                                         backup=self.args.backup)
 
             if self.args.rename_pattern:
                 # Handle file renaming.
 
         # --unique-file-id
         for arg in self.args.unique_file_ids:
-            # FIXME: force an owner_id
-            owner_id, id = arg.split(':', 1)
+            owner_id, id = arg
             if not id:
-                tag.unique_file_ids.remove(owner_id)
+                if tag.unique_file_ids.remove(owner_id):
+                    printWarning("Removed unique file ID '%s'" % owner_id)
+                    retval = True
+                else:
+                    printWarning("Unique file ID '%s' not found" % owner_id)
             else:
                 tag.unique_file_ids.set(id, owner_id)
+                printWarning("Setting unique file ID '%s' to %s" %
+                              (owner_id, id))
+                retval = True
 
         return retval
 
         "--remove-v2": "Remove ID3 v2.x tag.",
         "--remove-all": "Remove ID3 v1.x and v2.x tags.",
 
+        "--backup": "Make a backup of any file modified. The backup is made in "
+                    "same directory with a '.orig' extension added.",
         "--force-update": "Rewrite the tag despite there being no edit "
                           "options.",
         "-F": "Specify the delimiter used for multi-part argument values. "
               "The default is '%s'." % FIELD_DELIM,
         "--verbose": "Show all available tag data",
         "--unique-file-id": "Add a unique file ID frame. If the ID arg is "
-                            "empty corresponding OWNER_ID frames is removed. "
-                             "An OWNER_ID is required.",
+                            "empty the frame is removed. An OWNER_ID is "
+                            "required. The ID may be no more than 64 bytes.",
         "--encoding": "Set the encoding that is used for all text frames. "
                       "This option is only applied if the tag is updated "
                       "as the result of an edit option (e.g. --artist, "

src/test/id3/test_tag.py

 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
 ################################################################################
+import os
 import unittest
 from nose.tools import *
 import eyed3
 from eyed3.core import Date
-from eyed3.id3 import Tag, ID3_DEFAULT_VERSION
+from eyed3.id3 import Tag, ID3_DEFAULT_VERSION, ID3_V2_3, ID3_V2_4
 from eyed3.id3 import frames
 
 def testTagImport():
     tag.user_url_frames.set("Foobazz", u"Desc2")
     assert_equal(len(tag.user_url_frames), 1)
 
+def testSortOrderConversions():
+    test_file = "/tmp/soconvert.id3"
+
+    tag = Tag()
+    # 2.3 frames to 2.4
+    for fid in ["XSOA", "XSOP", "XSOT"]:
+        frame = frames.TextFrame(fid)
+        frame.text = unicode(fid)
+        tag.frame_set[fid] = frame
+    try:
+        tag.save(test_file)  # v2.4 is the default
+        tag = eyed3.load(test_file).tag
+        assert_equal(tag.version, ID3_V2_4)
+        assert_equal(len(tag.frame_set), 3)
+        del tag.frame_set["TSOA"]
+        del tag.frame_set["TSOP"]
+        del tag.frame_set["TSOT"]
+        assert_equal(len(tag.frame_set), 0)
+    finally:
+        os.remove(test_file)
+
+    tag = Tag()
+    # 2.4 frames to 2.3
+    for fid in ["TSOA", "TSOP", "TSOT"]:
+        frame = frames.TextFrame(fid)
+        frame.text = unicode(fid)
+        tag.frame_set[fid] = frame
+    try:
+        tag.save(test_file, version=eyed3.id3.ID3_V2_3)
+        tag = eyed3.load(test_file).tag
+        assert_equal(tag.version, ID3_V2_3)
+        assert_equal(len(tag.frame_set), 3)
+        del tag.frame_set["XSOA"]
+        del tag.frame_set["XSOP"]
+        del tag.frame_set["XSOT"]
+        assert_equal(len(tag.frame_set), 0)
+    finally:
+        os.remove(test_file)
+
+
 # TODO
 class ParseTests(unittest.TestCase):
     def setUp(self):
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.