1. Adrian Sampson
  2. beets

Commits

adrian.sampson  committed 10fd3d7

changed beets.tag to beets.mediafile

  • Participants
  • Parent commits 50627c5
  • Branches default

Comments (0)

Files changed (8)

File beets/library.py

View file
 import sqlite3, os, sys, operator, re, shutil
-from beets.tag import MediaFile, FileTypeError
+from beets.mediafile import MediaFile, FileTypeError
 from string import Template
 
 # Fields in the "items" table; all the metadata available for items in the

File beets/mediafile.py

View file
+"""Handles low-level interfacing for files' tags. Wraps Mutagen to
+automatically detect file types and provide a unified interface for a useful
+subset of music files' tags.
+
+Usage:
+>>> f = MediaFile('something.mp3')
+>>> f.title
+u'Lucy in the Sky with Diamonds'
+>>> f.artist = 'The Beatles'
+
+A field will always return a reasonable value of the correct type, even if no
+tag is present. If no value is available, the value will be false (e.g., zero
+or the empty string)."""
+
+from mutagen import mp4, mp3, id3
+import os.path
+
+__all__ = ['FileTypeError', 'MediaFile']
+
+# Currently allowed values for type:
+# mp3, mp4
+class FileTypeError(IOError):
+    pass
+
+
+
+#### utility functions ####
+
+def fromslashed(slashed, sep=u'/'):
+    """Extract a pair of items from a slashed string. If only one
+    value is present, it is assumed to be the left-hand value."""
+    
+    if slashed is None:
+        return (None, None)
+    
+    items = slashed.split(sep)
+    
+    if len(items) == 1:
+        out = (items[0], None)
+    else:
+        out = (items[0], items[1])
+    
+    # represent "nothing stored" more gracefully
+    if out[0] == '': out[0] = None
+    if out[1] == '': out[1] = None
+    
+    return out
+
+def toslashed(pair_or_val, sep=u'/'):
+    """Store a pair of items or a single item in a slashed string. If
+    only one value is provided (in a list/tuple or as a single value),
+    no slash is used."""
+    if type(pair_or_val) is list or type(pair_or_val) is tuple:
+        if len(pair_or_val) == 0:
+            out = [u'']
+        elif len(pair_or_val) == 1:
+            out = [unicode(pair_or_val[0])]
+        else:
+            out = [unicode(pair_or_val[0]), unicode(pair_or_val[1])]
+    else: # "scalar"
+        out = [unicode(pair_or_val)]
+    return sep.join(out)
+
+def unpair(pair, right=False, noneval=None):
+    """Return the left or right value in a pair (as selected by the "right"
+    parameter. If the value on that side is not available, return noneval.)"""
+    if right: idx = 1
+    else: idx = 0
+    
+    try:
+        out = pair[idx]
+    except:
+        out = None
+    finally:
+        if out is None:
+            return noneval
+        else:
+            return out
+
+def normalize_pair(pair, noneval=None):
+    """Make sure the pair is a tuple that has exactly two entries. If we need
+    to fill anything in, we'll use noneval."""
+    return (unpair(pair, False, noneval),
+            unpair(pair, True, noneval))
+
+
+
+
+
+class MediaField(object):
+    """A descriptor providing access to a particular (abstract) metadata
+    field. The various messy parameters control the translation to concrete
+    metadata manipulations in the language of mutagen."""
+    
+    # possible types used to store the relevant data
+    TYPE_RAW =     0      # stored as a single object (not in a list)
+    TYPE_LIST =    1 << 0 # stored in the first element of a list
+    TYPE_UNICODE = 1 << 1 # stored as a unicode object
+    TYPE_INTEGER = 1 << 2 # as an int
+    TYPE_BOOLEAN = 1 << 3 # as a bool
+    # RAW and LIST are mutually exclusive, as are UNICODE, INTEGER and
+    # BOOLEAN. Must pick either RAW or LIST, but none of the other types
+    # are necessary.
+    
+    # non-type aspects of data storage
+    STYLE_PLAIN =   0      # no filtering
+    STYLE_UNICODE = 1 << 0 # value is a string, stored as a string
+    STYLE_INTEGER = 1 << 1 # value is an integer, maybe stored as a string
+    STYLE_BOOLEAN = 1 << 2 # value is a boolean, probably stored as a string
+    STYLE_SLASHED = 1 << 3 # int stored in a string on one side of a / char
+    STYLE_2PLE =    1 << 4 # stored as one value in an integer 2-tuple
+    # The above styles are all mutually exclusive.
+    STYLE_LEFT =    1 << 5 # for SLASHED or 2PLE, value is in first entry
+    STYLE_RIGHT =   1 << 6 # likewise, in second entry
+    # These are mutually exclusive and relevant only with SLASHED and 2PLE.
+    
+    def __init__(self, id3key, mp4key,
+            # in ID3 tags, use only the frame with this "desc" field
+            id3desc=None,
+            # compositions of the TYPE_ flag above
+            id3type=TYPE_UNICODE|TYPE_LIST, mp4type=TYPE_UNICODE|TYPE_LIST,
+            # compositions of STYLE_ flags
+            id3style=STYLE_UNICODE, mp4style=STYLE_UNICODE
+            ):
+        
+        self.keys = { 'mp3': id3key,
+                      'mp4': mp4key }
+        self.types = { 'mp3': id3type,
+                       'mp4': mp4type }
+        self.styles = { 'mp3': id3style,
+                        'mp4': mp4style }
+        self.id3desc = id3desc
+    
+    def _fetchdata(self, obj):
+        """Get the value associated with this descriptor's key (and id3desc if
+        present) from the mutagen tag dict. Unwraps from a list if
+        necessary."""
+        (mykey, mytype, mystyle) = self._params(obj)
+        
+        try:
+            # fetch the value, which may be a scalar or a list
+            if obj.type == 'mp3':
+                if self.id3desc is not None: # also match on 'desc' field
+                    frames = obj.mgfile.tags.getall(mykey)
+                    entry = None
+                    for frame in frames:
+                        if frame.desc == self.id3desc:
+                            entry = frame.text
+                            break
+                    if entry is None: # no desc match
+                        return None
+                else:
+                    entry = obj.mgfile[mykey].text
+            else:
+                entry = obj.mgfile[mykey]
+            
+            # possibly index the list
+            if mytype & self.TYPE_LIST:
+                return entry[0]
+            else:
+                return entry
+        except KeyError: # the tag isn't present
+            return None
+    
+    def _storedata(self, obj, val):
+        """Store val for this descriptor's key in the tag dictionary. Store it
+        as a single-item list if necessary. Uses id3desc if present."""
+        (mykey, mytype, mystyle) = self._params(obj)
+        
+        # wrap as a list if necessary
+        if mytype & self.TYPE_LIST: out = [val]
+        else:                       out = val
+        
+        if obj.type == 'mp3':
+            if self.id3desc is not None: # match on id3desc
+                frames = obj.mgfile.tags.getall(mykey)
+                
+                # try modifying in place
+                found = False
+                for frame in frames:
+                    if frame.desc == self.id3desc:
+                        frame.text = out
+                        found = True
+                        break
+                
+                # need to make a new frame?
+                if not found:
+                    frame = id3.Frames[mykey](encoding=3, desc=self.id3desc,
+                                              text=val)
+                    obj.mgfile.tags.add(frame)
+            
+            else: # no match on desc; just replace based on key
+                frame = id3.Frames[mykey](encoding=3, text=val)
+                obj.mgfile.tags.setall(mykey, [frame])
+        else:
+            obj.mgfile[mykey] = out
+    
+    def _params(self, obj):
+        return (self.keys[obj.type],
+                self.types[obj.type],
+                self.styles[obj.type])
+    
+    def __get__(self, obj, owner):
+        """Retrieve the value of this metadata field."""
+        out = None
+        (mykey, mytype, mystyle) = self._params(obj)
+        
+        out = self._fetchdata(obj)
+        
+        # deal with slashed and tuple storage
+        if mystyle & self.STYLE_SLASHED or mystyle & self.STYLE_2PLE:
+            if mystyle & self.STYLE_SLASHED:
+                out = fromslashed(out)
+            out = unpair(out, mystyle & self.STYLE_RIGHT, noneval=0)
+        
+        # return the appropriate type
+        if mystyle & self.STYLE_INTEGER or mystyle & self.STYLE_SLASHED \
+                    or mystyle & self.STYLE_2PLE:
+            if out is None:
+                return 0
+            else:
+                try:
+                    return int(out)
+                except: # in case out is not convertible directly to an int
+                    return int(unicode(out))
+        elif mystyle & self.STYLE_BOOLEAN:
+            if out is None:
+                return False
+            else:
+                return bool(int(out)) # should work for strings, bools, ints
+        elif mystyle & self.STYLE_UNICODE:
+            if out is None:
+                return u''
+            else:
+                return unicode(out)
+        else:
+            return out
+    
+    def __set__(self, obj, val):
+        """Set the value of this metadata field."""
+        (mykey, mytype, mystyle) = self._params(obj)
+        
+        # apply style filters
+        if mystyle & self.STYLE_SLASHED or mystyle & self.STYLE_2PLE:
+            # fetch the existing value so we can preserve half of it
+            pair = self._fetchdata(obj)
+            if mystyle & self.STYLE_SLASHED:
+                pair = fromslashed(pair)
+            pair = normalize_pair(pair, noneval=0)
+            
+            # set the appropriate side of the pair
+            if mystyle & self.STYLE_LEFT:
+                pair = (val, pair[1])
+            else:
+                pair = (pair[0], val)
+            
+            if mystyle & self.STYLE_SLASHED:
+                out = toslashed(pair)
+            else:
+                out = pair
+        else: # plain, integer, or boolean
+            out = val
+        
+        # deal with Nones according to abstract type if present
+        if out is None:
+            if mystyle & self.STYLE_INTEGER:
+                out = 0
+            elif mystyle & self.STYLE_BOOLEAN:
+                out = False
+            elif mystyle & self.STYLE_UNICODE:
+                out = u''
+            # We trust that SLASHED and 2PLE are handled above.
+        
+        # convert to correct storage type
+        if mytype & self.TYPE_UNICODE:
+            if out is None:
+                out = u''
+            else:
+                if mystyle & self.STYLE_BOOLEAN:
+                    # store bools as 1,0 instead of True,False
+                    out = unicode(int(out))
+                else:
+                    out = unicode(out)
+        elif mytype & self.TYPE_INTEGER:
+            if out is None:
+                out = 0
+            else:
+                out = int(out)
+        elif mytype & self.TYPE_BOOLEAN:
+            out = bool(out)
+        
+        # store the data
+        self._storedata(obj, out)
+
+
+
+
+class MediaFile(object):
+    """Represents a multimedia file on disk and provides access to its
+    metadata."""
+    
+    def __init__(self, path):
+        root, ext = os.path.splitext(path)
+        if ext == '.mp3':
+            self.type = 'mp3'
+            self.mgfile = mp3.Open(path)
+            # mgfile = mutagen file = that which a MediaFile wraps
+        elif ext == '.m4a' or ext == '.mp4' or ext == '.m4b' or ext == '.m4p':
+            self.type = 'mp4'
+            self.mgfile = mp4.Open(path)
+        else:
+            raise FileTypeError('unsupported file extension: ' + ext)
+        
+        # add a set of tags if it's missing
+        if not self.mgfile.tags:
+            self.mgfile.add_tags()
+    
+    def save_tags(self):
+        self.mgfile.save()
+    
+    
+    #### field definitions ####
+    
+    title = MediaField('TIT2', "\xa9nam")
+    artist = MediaField('TPE1', "\xa9ART")
+    album = MediaField('TALB', "\xa9alb")
+    genre = MediaField('TCON', "\xa9gen")
+    composer = MediaField('TCOM', "\xa9wrt")
+    grouping = MediaField('TIT1', "\xa9grp")
+    year = MediaField('TDRC', "\xa9day",
+                id3style=MediaField.STYLE_INTEGER,
+                mp4style=MediaField.STYLE_INTEGER)
+    track = MediaField('TRCK', 'trkn',
+                id3style=MediaField.STYLE_SLASHED | MediaField.STYLE_LEFT,
+                mp4type=MediaField.TYPE_LIST,
+                mp4style=MediaField.STYLE_2PLE | MediaField.STYLE_LEFT)
+    maxtrack = MediaField('TRCK', 'trkn',
+                id3style=MediaField.STYLE_SLASHED | MediaField.STYLE_RIGHT,
+                mp4type=MediaField.TYPE_LIST,
+                mp4style=MediaField.STYLE_2PLE | MediaField.STYLE_RIGHT)
+    disc = MediaField('TPOS', 'disk',
+                id3style=MediaField.STYLE_SLASHED | MediaField.STYLE_LEFT,
+                mp4type=MediaField.TYPE_LIST,
+                mp4style=MediaField.STYLE_2PLE | MediaField.STYLE_LEFT)
+    maxdisc = MediaField('TPOS', 'disk',
+                id3style=MediaField.STYLE_SLASHED | MediaField.STYLE_RIGHT,
+                mp4type=MediaField.TYPE_LIST,
+                mp4style=MediaField.STYLE_2PLE | MediaField.STYLE_RIGHT)
+    lyrics = MediaField(u"USLT", "\xa9lyr", id3desc=u'',
+                id3type=MediaField.TYPE_UNICODE)
+    comments = MediaField(u"COMM", "\xa9cmt", id3desc=u'')
+    bpm = MediaField('TBPM', 'tmpo',
+                id3style=MediaField.STYLE_INTEGER,
+                mp4type=MediaField.TYPE_LIST | MediaField.TYPE_INTEGER,
+                mp4style=MediaField.STYLE_INTEGER)
+    comp = MediaField('TCMP', 'cpil',
+                id3style=MediaField.STYLE_BOOLEAN,
+                mp4type=MediaField.TYPE_BOOLEAN,
+                mp4style=MediaField.STYLE_BOOLEAN)

File beets/tag.py

-"""Handles low-level interfacing for files' tags. Wraps Mutagen to
-automatically detect file types and provide a unified interface for a useful
-subset of music files' tags.
-
-Usage:
->>> f = MediaFile('something.mp3')
->>> f.title
-u'Lucy in the Sky with Diamonds'
->>> f.artist = 'The Beatles'
-
-A field will always return a reasonable value of the correct type, even if no
-tag is present. If no value is available, the value will be false (e.g., zero
-or the empty string)."""
-
-from mutagen import mp4, mp3, id3
-import os.path
-
-__all__ = ['FileTypeError', 'MediaFile']
-
-# Currently allowed values for type:
-# mp3, mp4
-class FileTypeError(IOError):
-    pass
-
-
-
-#### utility functions ####
-
-def fromslashed(slashed, sep=u'/'):
-    """Extract a pair of items from a slashed string. If only one
-    value is present, it is assumed to be the left-hand value."""
-    
-    if slashed is None:
-        return (None, None)
-    
-    items = slashed.split(sep)
-    
-    if len(items) == 1:
-        out = (items[0], None)
-    else:
-        out = (items[0], items[1])
-    
-    # represent "nothing stored" more gracefully
-    if out[0] == '': out[0] = None
-    if out[1] == '': out[1] = None
-    
-    return out
-
-def toslashed(pair_or_val, sep=u'/'):
-    """Store a pair of items or a single item in a slashed string. If
-    only one value is provided (in a list/tuple or as a single value),
-    no slash is used."""
-    if type(pair_or_val) is list or type(pair_or_val) is tuple:
-        if len(pair_or_val) == 0:
-            out = [u'']
-        elif len(pair_or_val) == 1:
-            out = [unicode(pair_or_val[0])]
-        else:
-            out = [unicode(pair_or_val[0]), unicode(pair_or_val[1])]
-    else: # "scalar"
-        out = [unicode(pair_or_val)]
-    return sep.join(out)
-
-def unpair(pair, right=False, noneval=None):
-    """Return the left or right value in a pair (as selected by the "right"
-    parameter. If the value on that side is not available, return noneval.)"""
-    if right: idx = 1
-    else: idx = 0
-    
-    try:
-        out = pair[idx]
-    except:
-        out = None
-    finally:
-        if out is None:
-            return noneval
-        else:
-            return out
-
-def normalize_pair(pair, noneval=None):
-    """Make sure the pair is a tuple that has exactly two entries. If we need
-    to fill anything in, we'll use noneval."""
-    return (unpair(pair, False, noneval),
-            unpair(pair, True, noneval))
-
-
-
-
-
-class MediaField(object):
-    """A descriptor providing access to a particular (abstract) metadata
-    field. The various messy parameters control the translation to concrete
-    metadata manipulations in the language of mutagen."""
-    
-    # possible types used to store the relevant data
-    TYPE_RAW =     0      # stored as a single object (not in a list)
-    TYPE_LIST =    1 << 0 # stored in the first element of a list
-    TYPE_UNICODE = 1 << 1 # stored as a unicode object
-    TYPE_INTEGER = 1 << 2 # as an int
-    TYPE_BOOLEAN = 1 << 3 # as a bool
-    # RAW and LIST are mutually exclusive, as are UNICODE, INTEGER and
-    # BOOLEAN. Must pick either RAW or LIST, but none of the other types
-    # are necessary.
-    
-    # non-type aspects of data storage
-    STYLE_PLAIN =   0      # no filtering
-    STYLE_UNICODE = 1 << 0 # value is a string, stored as a string
-    STYLE_INTEGER = 1 << 1 # value is an integer, maybe stored as a string
-    STYLE_BOOLEAN = 1 << 2 # value is a boolean, probably stored as a string
-    STYLE_SLASHED = 1 << 3 # int stored in a string on one side of a / char
-    STYLE_2PLE =    1 << 4 # stored as one value in an integer 2-tuple
-    # The above styles are all mutually exclusive.
-    STYLE_LEFT =    1 << 5 # for SLASHED or 2PLE, value is in first entry
-    STYLE_RIGHT =   1 << 6 # likewise, in second entry
-    # These are mutually exclusive and relevant only with SLASHED and 2PLE.
-    
-    def __init__(self, id3key, mp4key,
-            # in ID3 tags, use only the frame with this "desc" field
-            id3desc=None,
-            # compositions of the TYPE_ flag above
-            id3type=TYPE_UNICODE|TYPE_LIST, mp4type=TYPE_UNICODE|TYPE_LIST,
-            # compositions of STYLE_ flags
-            id3style=STYLE_UNICODE, mp4style=STYLE_UNICODE
-            ):
-        
-        self.keys = { 'mp3': id3key,
-                      'mp4': mp4key }
-        self.types = { 'mp3': id3type,
-                       'mp4': mp4type }
-        self.styles = { 'mp3': id3style,
-                        'mp4': mp4style }
-        self.id3desc = id3desc
-    
-    def _fetchdata(self, obj):
-        """Get the value associated with this descriptor's key (and id3desc if
-        present) from the mutagen tag dict. Unwraps from a list if
-        necessary."""
-        (mykey, mytype, mystyle) = self._params(obj)
-        
-        try:
-            # fetch the value, which may be a scalar or a list
-            if obj.type == 'mp3':
-                if self.id3desc is not None: # also match on 'desc' field
-                    frames = obj.mgfile.tags.getall(mykey)
-                    entry = None
-                    for frame in frames:
-                        if frame.desc == self.id3desc:
-                            entry = frame.text
-                            break
-                    if entry is None: # no desc match
-                        return None
-                else:
-                    entry = obj.mgfile[mykey].text
-            else:
-                entry = obj.mgfile[mykey]
-            
-            # possibly index the list
-            if mytype & self.TYPE_LIST:
-                return entry[0]
-            else:
-                return entry
-        except KeyError: # the tag isn't present
-            return None
-    
-    def _storedata(self, obj, val):
-        """Store val for this descriptor's key in the tag dictionary. Store it
-        as a single-item list if necessary. Uses id3desc if present."""
-        (mykey, mytype, mystyle) = self._params(obj)
-        
-        # wrap as a list if necessary
-        if mytype & self.TYPE_LIST: out = [val]
-        else:                       out = val
-        
-        if obj.type == 'mp3':
-            if self.id3desc is not None: # match on id3desc
-                frames = obj.mgfile.tags.getall(mykey)
-                
-                # try modifying in place
-                found = False
-                for frame in frames:
-                    if frame.desc == self.id3desc:
-                        frame.text = out
-                        found = True
-                        break
-                
-                # need to make a new frame?
-                if not found:
-                    frame = id3.Frames[mykey](encoding=3, desc=self.id3desc,
-                                              text=val)
-                    obj.mgfile.tags.add(frame)
-            
-            else: # no match on desc; just replace based on key
-                frame = id3.Frames[mykey](encoding=3, text=val)
-                obj.mgfile.tags.setall(mykey, [frame])
-        else:
-            obj.mgfile[mykey] = out
-    
-    def _params(self, obj):
-        return (self.keys[obj.type],
-                self.types[obj.type],
-                self.styles[obj.type])
-    
-    def __get__(self, obj, owner):
-        """Retrieve the value of this metadata field."""
-        out = None
-        (mykey, mytype, mystyle) = self._params(obj)
-        
-        out = self._fetchdata(obj)
-        
-        # deal with slashed and tuple storage
-        if mystyle & self.STYLE_SLASHED or mystyle & self.STYLE_2PLE:
-            if mystyle & self.STYLE_SLASHED:
-                out = fromslashed(out)
-            out = unpair(out, mystyle & self.STYLE_RIGHT, noneval=0)
-        
-        # return the appropriate type
-        if mystyle & self.STYLE_INTEGER or mystyle & self.STYLE_SLASHED \
-                    or mystyle & self.STYLE_2PLE:
-            if out is None:
-                return 0
-            else:
-                try:
-                    return int(out)
-                except: # in case out is not convertible directly to an int
-                    return int(unicode(out))
-        elif mystyle & self.STYLE_BOOLEAN:
-            if out is None:
-                return False
-            else:
-                return bool(int(out)) # should work for strings, bools, ints
-        elif mystyle & self.STYLE_UNICODE:
-            if out is None:
-                return u''
-            else:
-                return unicode(out)
-        else:
-            return out
-    
-    def __set__(self, obj, val):
-        """Set the value of this metadata field."""
-        (mykey, mytype, mystyle) = self._params(obj)
-        
-        # apply style filters
-        if mystyle & self.STYLE_SLASHED or mystyle & self.STYLE_2PLE:
-            # fetch the existing value so we can preserve half of it
-            pair = self._fetchdata(obj)
-            if mystyle & self.STYLE_SLASHED:
-                pair = fromslashed(pair)
-            pair = normalize_pair(pair, noneval=0)
-            
-            # set the appropriate side of the pair
-            if mystyle & self.STYLE_LEFT:
-                pair = (val, pair[1])
-            else:
-                pair = (pair[0], val)
-            
-            if mystyle & self.STYLE_SLASHED:
-                out = toslashed(pair)
-            else:
-                out = pair
-        else: # plain, integer, or boolean
-            out = val
-        
-        # deal with Nones according to abstract type if present
-        if out is None:
-            if mystyle & self.STYLE_INTEGER:
-                out = 0
-            elif mystyle & self.STYLE_BOOLEAN:
-                out = False
-            elif mystyle & self.STYLE_UNICODE:
-                out = u''
-            # We trust that SLASHED and 2PLE are handled above.
-        
-        # convert to correct storage type
-        if mytype & self.TYPE_UNICODE:
-            if out is None:
-                out = u''
-            else:
-                if mystyle & self.STYLE_BOOLEAN:
-                    # store bools as 1,0 instead of True,False
-                    out = unicode(int(out))
-                else:
-                    out = unicode(out)
-        elif mytype & self.TYPE_INTEGER:
-            if out is None:
-                out = 0
-            else:
-                out = int(out)
-        elif mytype & self.TYPE_BOOLEAN:
-            out = bool(out)
-        
-        # store the data
-        self._storedata(obj, out)
-
-
-
-
-class MediaFile(object):
-    """Represents a multimedia file on disk and provides access to its
-    metadata."""
-    
-    def __init__(self, path):
-        root, ext = os.path.splitext(path)
-        if ext == '.mp3':
-            self.type = 'mp3'
-            self.mgfile = mp3.Open(path)
-            # mgfile = mutagen file = that which a MediaFile wraps
-        elif ext == '.m4a' or ext == '.mp4' or ext == '.m4b' or ext == '.m4p':
-            self.type = 'mp4'
-            self.mgfile = mp4.Open(path)
-        else:
-            raise FileTypeError('unsupported file extension: ' + ext)
-        
-        # add a set of tags if it's missing
-        if not self.mgfile.tags:
-            self.mgfile.add_tags()
-    
-    def save_tags(self):
-        self.mgfile.save()
-    
-    
-    #### field definitions ####
-    
-    title = MediaField('TIT2', "\xa9nam")
-    artist = MediaField('TPE1', "\xa9ART")
-    album = MediaField('TALB', "\xa9alb")
-    genre = MediaField('TCON', "\xa9gen")
-    composer = MediaField('TCOM', "\xa9wrt")
-    grouping = MediaField('TIT1', "\xa9grp")
-    year = MediaField('TDRC', "\xa9day",
-                id3style=MediaField.STYLE_INTEGER,
-                mp4style=MediaField.STYLE_INTEGER)
-    track = MediaField('TRCK', 'trkn',
-                id3style=MediaField.STYLE_SLASHED | MediaField.STYLE_LEFT,
-                mp4type=MediaField.TYPE_LIST,
-                mp4style=MediaField.STYLE_2PLE | MediaField.STYLE_LEFT)
-    maxtrack = MediaField('TRCK', 'trkn',
-                id3style=MediaField.STYLE_SLASHED | MediaField.STYLE_RIGHT,
-                mp4type=MediaField.TYPE_LIST,
-                mp4style=MediaField.STYLE_2PLE | MediaField.STYLE_RIGHT)
-    disc = MediaField('TPOS', 'disk',
-                id3style=MediaField.STYLE_SLASHED | MediaField.STYLE_LEFT,
-                mp4type=MediaField.TYPE_LIST,
-                mp4style=MediaField.STYLE_2PLE | MediaField.STYLE_LEFT)
-    maxdisc = MediaField('TPOS', 'disk',
-                id3style=MediaField.STYLE_SLASHED | MediaField.STYLE_RIGHT,
-                mp4type=MediaField.TYPE_LIST,
-                mp4style=MediaField.STYLE_2PLE | MediaField.STYLE_RIGHT)
-    lyrics = MediaField(u"USLT", "\xa9lyr", id3desc=u'',
-                id3type=MediaField.TYPE_UNICODE)
-    comments = MediaField(u"COMM", "\xa9cmt", id3desc=u'')
-    bpm = MediaField('TBPM', 'tmpo',
-                id3style=MediaField.STYLE_INTEGER,
-                mp4type=MediaField.TYPE_LIST | MediaField.TYPE_INTEGER,
-                mp4style=MediaField.STYLE_INTEGER)
-    comp = MediaField('TCMP', 'cpil',
-                id3style=MediaField.STYLE_BOOLEAN,
-                mp4type=MediaField.TYPE_BOOLEAN,
-                mp4style=MediaField.STYLE_BOOLEAN)

File test/alltests.py

View file
 #!/usr/bin/env python
 import unittest
 
-test_modules = ['tag', 'library']
+test_modules = ['test_mediafile', 'test_library']
 
 def suite():
     s = unittest.TestSuite()

File test/library.py

-#!/usr/bin/env python
-import unittest, sys, os
-sys.path.append('..')
-import beets.library
-
-parse_query = beets.library.CollectionQuery._parse_query
-
-class QueryParseTest(unittest.TestCase):
-    def test_one_basic_term(self):
-        q = 'test'
-        r = [(None, 'test')]
-        self.assertEqual(parse_query(q), r)
-    
-    def test_three_basic_terms(self):
-        q = 'test one two'
-        r = [(None, 'test'), (None, 'one'), (None, 'two')]
-        self.assertEqual(parse_query(q), r)
-    
-    def test_one_keyed_term(self):
-        q = 'test:val'
-        r = [('test', 'val')]
-        self.assertEqual(parse_query(q), r)
-    
-    def test_one_keyed_one_basic(self):
-        q = 'test:val one'
-        r = [('test', 'val'), (None, 'one')]
-        self.assertEqual(parse_query(q), r)
-    
-    def test_colon_at_end(self):
-        q = 'test:'
-        r = [(None, 'test:')]
-        self.assertEqual(parse_query(q), r)
-    
-    def test_colon_at_start(self):
-        q = ':test'
-        r = [(None, ':test')]
-        self.assertEqual(parse_query(q), r)
-    
-    def test_escaped_colon(self):
-        q = r'test\:val'
-        r = [((None), 'test:val')]
-        self.assertEqual(parse_query(q), r)
-
-class GetTest(unittest.TestCase):
-    def setUp(self):
-        self.lib = beets.library.Library('rsrc' + os.sep + 'get.blb')
-    
-    def assert_matched(self, result_iterator, title):
-        self.assertEqual(result_iterator.next().title, title)
-    def assert_done(self, result_iterator):
-        self.assertRaises(StopIteration, result_iterator.next)
-    def assert_matched_all(self, result_iterator):
-        self.assert_matched(result_iterator, 'Littlest Things')
-        self.assert_matched(result_iterator, 'Lovers Who Uncover')
-        self.assert_matched(result_iterator, 'Boracay')
-        self.assert_matched(result_iterator, 'Take Pills')
-        self.assert_done(result_iterator)
-    
-    def test_get_empty(self):
-        q = ''
-        results = self.lib.get(q)
-        self.assert_matched_all(results)
-    
-    def test_get_none(self):
-        q = None
-        results = self.lib.get(q)
-        self.assert_matched_all(results)
-    
-    def test_get_one_keyed_term(self):
-        q = 'artist:Lil'
-        results = self.lib.get(q)
-        self.assert_matched(results, 'Littlest Things')
-        self.assert_done(results)
-    
-    def test_get_one_unkeyed_term(self):
-        q = 'Terry'
-        results = self.lib.get(q)
-        self.assert_matched(results, 'Boracay')
-        self.assert_done(results)
-    
-    def test_get_no_matches(self):
-        q = 'popebear'
-        results = self.lib.get(q)
-        self.assert_done(results)
-    
-    def test_invalid_key(self):
-        q = 'pope:bear'
-        results = self.lib.get(q)
-        self.assert_matched_all(results)
-    
-    def test_term_case_insensitive(self):
-        q = 'UNCoVER'
-        results = self.lib.get(q)
-        self.assert_matched(results, 'Lovers Who Uncover')
-        self.assert_done(results)
-    
-    def test_term_case_insensitive_with_key(self):
-        q = 'album:stiLL'
-        results = self.lib.get(q)
-        self.assert_matched(results, 'Littlest Things')
-        self.assert_done(results)
-    
-    def test_key_case_insensitive(self):
-        q = 'ArTiST:Allen'
-        results = self.lib.get(q)
-        self.assert_matched(results, 'Littlest Things')
-        self.assert_done(results)
-    
-    def test_unkeyed_term_matches_multiple_columns(self):
-        q = 'little'
-        results = self.lib.get(q)
-        self.assert_matched(results, 'Littlest Things')
-        self.assert_matched(results, 'Lovers Who Uncover')
-        self.assert_matched(results, 'Boracay')
-        self.assert_done(results)
-    
-    def test_keyed_term_matches_only_one_column(self):
-        q = 'artist:little'
-        results = self.lib.get(q)
-        self.assert_matched(results, 'Lovers Who Uncover')
-        self.assert_matched(results, 'Boracay')
-        self.assert_done(results)
-    
-    def test_mulitple_terms_narrow_search(self):
-        q = 'little ones'
-        results = self.lib.get(q)
-        self.assert_matched(results, 'Lovers Who Uncover')
-        self.assert_matched(results, 'Boracay')
-        self.assert_done(results)
-        
-def suite():
-    return unittest.TestLoader().loadTestsFromName(__name__)
-
-if __name__ == '__main__':
-    unittest.main(defaultTest='suite')

File test/tag.py

-#!/usr/bin/env python
-import unittest, sys, os, shutil
-sys.path.append('..')
-import beets.tag
-
-def MakeReadingTest(path, correct_dict, field):
-    class ReadingTest(unittest.TestCase):
-        def setUp(self):
-            self.f = beets.tag.MediaFile(path)
-        def runTest(self):
-            got = getattr(self.f, field)
-            correct = correct_dict[field]
-            self.assertEqual(got, correct,
-                field + ' incorrect (expected ' + repr(correct) + ', got ' + \
-                repr(got) + ') when testing ' + os.path.basename(path))
-    return ReadingTest
-
-def MakeWritingTest(path, correct_dict, field, testsuffix='_test'):
-    
-    class WritingTest(unittest.TestCase):
-        def setUp(self):
-            # make a copy of the file we'll work on
-            root, ext = os.path.splitext(path)
-            self.tpath = root + testsuffix + ext
-            shutil.copy(path, self.tpath)
-            
-            # generate the new value we'll try storing
-            if type(correct_dict[field]) is unicode:
-                self.value = u'TestValue: ' + field
-            elif type(correct_dict[field]) is int:
-                self.value = correct_dict[field] + 42
-            elif type(correct_dict[field]) is bool:
-                self.value = not correct_dict[field]
-            else:
-                raise ValueError('unknown field type ' + \
-                        str(type(correct_dict[field])))
-        
-        def runTest(self):    
-            # write new tag
-            a = beets.tag.MediaFile(self.tpath)
-            setattr(a, field, self.value)
-            a.save_tags()
-            
-            # verify ALL tags are correct with modification
-            b = beets.tag.MediaFile(self.tpath)
-            for readfield in correct_dict.keys():
-                got = getattr(b, readfield)
-                if readfield is field:
-                    self.assertEqual(got, self.value,
-                        field + ' modified incorrectly (changed to ' + \
-                        repr(self.value) + ' but read ' + repr(got) + \
-                        ') when testing ' + os.path.basename(path))
-                else:
-                    correct = getattr(a, readfield)
-                    self.assertEqual(got, correct,
-                        readfield + ' changed when it should not have (expected'
-                        ' ' + repr(correct) + ', got ' + repr(got) + ') when '
-                        'modifying ' + field + ' in ' + os.path.basename(path))
-                
-        def tearDown(self):
-            os.remove(self.tpath)
-    
-    return WritingTest
-
-correct_dicts = {
-
-    'full': {
-        'title':    u'full',
-        'artist':   u'the artist',
-        'album':    u'the album',
-        'genre':    u'the genre',
-        'composer': u'the composer',
-        'grouping': u'the grouping',
-        'year':     2001,
-        'track':    2,
-        'maxtrack': 3,
-        'disc':     4,
-        'maxdisc':  5,
-        'lyrics':   u'the lyrics',
-        'comments': u'the comments',
-        'bpm':      6,
-        'comp':     True
-    },
-
-    'partial': {
-        'title':    u'partial',
-        'artist':   u'the artist',
-        'album':    u'the album',
-        'genre':    u'',
-        'composer': u'',
-        'grouping': u'',
-        'year':     0,
-        'track':    2,
-        'maxtrack': 0,
-        'disc':     4,
-        'maxdisc':  0,
-        'lyrics':   u'',
-        'comments': u'',
-        'bpm':      0,
-        'comp':     False
-    },
-
-    'min': {
-        'title':    u'min',
-        'artist':   u'',
-        'album':    u'',
-        'genre':    u'',
-        'composer': u'',
-        'grouping': u'',
-        'year':     0,
-        'track':    0,
-        'maxtrack': 0,
-        'disc':     0,
-        'maxdisc':  0,
-        'lyrics':   u'',
-        'comments': u'',
-        'bpm':      0,
-        'comp':     False
-    },
-    
-    # empty.mp3 has had its ID3 tag deleted with mp3info -d
-    'empty': {
-        'title':    u'',
-        'artist':   u'',
-        'album':    u'',
-        'genre':    u'',
-        'composer': u'',
-        'grouping': u'',
-        'year':     0,
-        'track':    0,
-        'maxtrack': 0,
-        'disc':     0,
-        'maxdisc':  0,
-        'lyrics':   u'',
-        'comments': u'',
-        'bpm':      0,
-        'comp':     False
-    }
-
-}
-
-def suite_for_file(path, correct_dict):
-    s = unittest.TestSuite()
-    for field in correct_dict.keys():
-        s.addTest(MakeReadingTest(path, correct_dict, field)())
-        s.addTest(MakeWritingTest(path, correct_dict, field)())
-    return s
-
-def suite():
-    s = unittest.TestSuite()
-    
-    # General tests.
-    for kind in ('m4a', 'mp3'):
-        for tagset in ('full', 'partial', 'min'):
-            path = 'rsrc' + os.sep + tagset + '.' + kind
-            correct_dict = correct_dicts[tagset]
-            s.addTest(suite_for_file(path, correct_dict))
-    
-    # Special test for missing ID3 tag.
-    s.addTest(suite_for_file('rsrc' + os.sep + 'empty.mp3',
-                             correct_dicts['empty']))
-    
-    return s
-
-if __name__ == '__main__':
-    unittest.main(defaultTest='suite')

File test/test_library.py

View file
+#!/usr/bin/env python
+import unittest, sys, os
+sys.path.append('..')
+import beets.library
+
+parse_query = beets.library.CollectionQuery._parse_query
+
+class QueryParseTest(unittest.TestCase):
+    def test_one_basic_term(self):
+        q = 'test'
+        r = [(None, 'test')]
+        self.assertEqual(parse_query(q), r)
+    
+    def test_three_basic_terms(self):
+        q = 'test one two'
+        r = [(None, 'test'), (None, 'one'), (None, 'two')]
+        self.assertEqual(parse_query(q), r)
+    
+    def test_one_keyed_term(self):
+        q = 'test:val'
+        r = [('test', 'val')]
+        self.assertEqual(parse_query(q), r)
+    
+    def test_one_keyed_one_basic(self):
+        q = 'test:val one'
+        r = [('test', 'val'), (None, 'one')]
+        self.assertEqual(parse_query(q), r)
+    
+    def test_colon_at_end(self):
+        q = 'test:'
+        r = [(None, 'test:')]
+        self.assertEqual(parse_query(q), r)
+    
+    def test_colon_at_start(self):
+        q = ':test'
+        r = [(None, ':test')]
+        self.assertEqual(parse_query(q), r)
+    
+    def test_escaped_colon(self):
+        q = r'test\:val'
+        r = [((None), 'test:val')]
+        self.assertEqual(parse_query(q), r)
+
+class GetTest(unittest.TestCase):
+    def setUp(self):
+        self.lib = beets.library.Library('rsrc' + os.sep + 'get.blb')
+    
+    def assert_matched(self, result_iterator, title):
+        self.assertEqual(result_iterator.next().title, title)
+    def assert_done(self, result_iterator):
+        self.assertRaises(StopIteration, result_iterator.next)
+    def assert_matched_all(self, result_iterator):
+        self.assert_matched(result_iterator, 'Littlest Things')
+        self.assert_matched(result_iterator, 'Lovers Who Uncover')
+        self.assert_matched(result_iterator, 'Boracay')
+        self.assert_matched(result_iterator, 'Take Pills')
+        self.assert_done(result_iterator)
+    
+    def test_get_empty(self):
+        q = ''
+        results = self.lib.get(q)
+        self.assert_matched_all(results)
+    
+    def test_get_none(self):
+        q = None
+        results = self.lib.get(q)
+        self.assert_matched_all(results)
+    
+    def test_get_one_keyed_term(self):
+        q = 'artist:Lil'
+        results = self.lib.get(q)
+        self.assert_matched(results, 'Littlest Things')
+        self.assert_done(results)
+    
+    def test_get_one_unkeyed_term(self):
+        q = 'Terry'
+        results = self.lib.get(q)
+        self.assert_matched(results, 'Boracay')
+        self.assert_done(results)
+    
+    def test_get_no_matches(self):
+        q = 'popebear'
+        results = self.lib.get(q)
+        self.assert_done(results)
+    
+    def test_invalid_key(self):
+        q = 'pope:bear'
+        results = self.lib.get(q)
+        self.assert_matched_all(results)
+    
+    def test_term_case_insensitive(self):
+        q = 'UNCoVER'
+        results = self.lib.get(q)
+        self.assert_matched(results, 'Lovers Who Uncover')
+        self.assert_done(results)
+    
+    def test_term_case_insensitive_with_key(self):
+        q = 'album:stiLL'
+        results = self.lib.get(q)
+        self.assert_matched(results, 'Littlest Things')
+        self.assert_done(results)
+    
+    def test_key_case_insensitive(self):
+        q = 'ArTiST:Allen'
+        results = self.lib.get(q)
+        self.assert_matched(results, 'Littlest Things')
+        self.assert_done(results)
+    
+    def test_unkeyed_term_matches_multiple_columns(self):
+        q = 'little'
+        results = self.lib.get(q)
+        self.assert_matched(results, 'Littlest Things')
+        self.assert_matched(results, 'Lovers Who Uncover')
+        self.assert_matched(results, 'Boracay')
+        self.assert_done(results)
+    
+    def test_keyed_term_matches_only_one_column(self):
+        q = 'artist:little'
+        results = self.lib.get(q)
+        self.assert_matched(results, 'Lovers Who Uncover')
+        self.assert_matched(results, 'Boracay')
+        self.assert_done(results)
+    
+    def test_mulitple_terms_narrow_search(self):
+        q = 'little ones'
+        results = self.lib.get(q)
+        self.assert_matched(results, 'Lovers Who Uncover')
+        self.assert_matched(results, 'Boracay')
+        self.assert_done(results)
+        
+def suite():
+    return unittest.TestLoader().loadTestsFromName(__name__)
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')

File test/test_mediafile.py

View file
+#!/usr/bin/env python
+import unittest, sys, os, shutil
+sys.path.append('..')
+import beets.mediafile
+
+def MakeReadingTest(path, correct_dict, field):
+    class ReadingTest(unittest.TestCase):
+        def setUp(self):
+            self.f = beets.mediafile.MediaFile(path)
+        def runTest(self):
+            got = getattr(self.f, field)
+            correct = correct_dict[field]
+            self.assertEqual(got, correct,
+                field + ' incorrect (expected ' + repr(correct) + ', got ' + \
+                repr(got) + ') when testing ' + os.path.basename(path))
+    return ReadingTest
+
+def MakeWritingTest(path, correct_dict, field, testsuffix='_test'):
+    
+    class WritingTest(unittest.TestCase):
+        def setUp(self):
+            # make a copy of the file we'll work on
+            root, ext = os.path.splitext(path)
+            self.tpath = root + testsuffix + ext
+            shutil.copy(path, self.tpath)
+            
+            # generate the new value we'll try storing
+            if type(correct_dict[field]) is unicode:
+                self.value = u'TestValue: ' + field
+            elif type(correct_dict[field]) is int:
+                self.value = correct_dict[field] + 42
+            elif type(correct_dict[field]) is bool:
+                self.value = not correct_dict[field]
+            else:
+                raise ValueError('unknown field type ' + \
+                        str(type(correct_dict[field])))
+        
+        def runTest(self):    
+            # write new tag
+            a = beets.mediafile.MediaFile(self.tpath)
+            setattr(a, field, self.value)
+            a.save_tags()
+            
+            # verify ALL tags are correct with modification
+            b = beets.mediafile.MediaFile(self.tpath)
+            for readfield in correct_dict.keys():
+                got = getattr(b, readfield)
+                if readfield is field:
+                    self.assertEqual(got, self.value,
+                        field + ' modified incorrectly (changed to ' + \
+                        repr(self.value) + ' but read ' + repr(got) + \
+                        ') when testing ' + os.path.basename(path))
+                else:
+                    correct = getattr(a, readfield)
+                    self.assertEqual(got, correct,
+                        readfield + ' changed when it should not have (expected'
+                        ' ' + repr(correct) + ', got ' + repr(got) + ') when '
+                        'modifying ' + field + ' in ' + os.path.basename(path))
+                
+        def tearDown(self):
+            os.remove(self.tpath)
+    
+    return WritingTest
+
+correct_dicts = {
+
+    'full': {
+        'title':    u'full',
+        'artist':   u'the artist',
+        'album':    u'the album',
+        'genre':    u'the genre',
+        'composer': u'the composer',
+        'grouping': u'the grouping',
+        'year':     2001,
+        'track':    2,
+        'maxtrack': 3,
+        'disc':     4,
+        'maxdisc':  5,
+        'lyrics':   u'the lyrics',
+        'comments': u'the comments',
+        'bpm':      6,
+        'comp':     True
+    },
+
+    'partial': {
+        'title':    u'partial',
+        'artist':   u'the artist',
+        'album':    u'the album',
+        'genre':    u'',
+        'composer': u'',
+        'grouping': u'',
+        'year':     0,
+        'track':    2,
+        'maxtrack': 0,
+        'disc':     4,
+        'maxdisc':  0,
+        'lyrics':   u'',
+        'comments': u'',
+        'bpm':      0,
+        'comp':     False
+    },
+
+    'min': {
+        'title':    u'min',
+        'artist':   u'',
+        'album':    u'',
+        'genre':    u'',
+        'composer': u'',
+        'grouping': u'',
+        'year':     0,
+        'track':    0,
+        'maxtrack': 0,
+        'disc':     0,
+        'maxdisc':  0,
+        'lyrics':   u'',
+        'comments': u'',
+        'bpm':      0,
+        'comp':     False
+    },
+    
+    # empty.mp3 has had its ID3 tag deleted with mp3info -d
+    'empty': {
+        'title':    u'',
+        'artist':   u'',
+        'album':    u'',
+        'genre':    u'',
+        'composer': u'',
+        'grouping': u'',
+        'year':     0,
+        'track':    0,
+        'maxtrack': 0,
+        'disc':     0,
+        'maxdisc':  0,
+        'lyrics':   u'',
+        'comments': u'',
+        'bpm':      0,
+        'comp':     False
+    }
+
+}
+
+def suite_for_file(path, correct_dict):
+    s = unittest.TestSuite()
+    for field in correct_dict.keys():
+        s.addTest(MakeReadingTest(path, correct_dict, field)())
+        s.addTest(MakeWritingTest(path, correct_dict, field)())
+    return s
+
+def suite():
+    s = unittest.TestSuite()
+    
+    # General tests.
+    for kind in ('m4a', 'mp3'):
+        for tagset in ('full', 'partial', 'min'):
+            path = 'rsrc' + os.sep + tagset + '.' + kind
+            correct_dict = correct_dicts[tagset]
+            s.addTest(suite_for_file(path, correct_dict))
+    
+    # Special test for missing ID3 tag.
+    s.addTest(suite_for_file('rsrc' + os.sep + 'empty.mp3',
+                             correct_dicts['empty']))
+    
+    return s
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')