dded full date access to MediaFile (yyyy-mm-dd)

--HG--
extra : convert_revision : svn%3A41726ec3-264d-0410-9c23-a9f1637257cc/trunk%4085
This commit is contained in:
adrian.sampson 2008-08-04 05:46:44 +00:00
parent 605343fb57
commit 1294d573d6
4 changed files with 155 additions and 96 deletions

View file

@ -25,66 +25,6 @@ class FileTypeError(IOError):
#### 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))
#### flags used to define fields' behavior ####
@ -93,7 +33,11 @@ class Enumeration(object):
def __init__(self, *values):
for value, equiv in zip(values, range(1, len(values)+1)):
setattr(self, value, equiv)
packing = Enumeration('SLASHED', 'TUPLE')
# determine style of packing if any
packing = Enumeration('SLASHED', # pair delimited by /
'TUPLE', # a python tuple of 2 items
'DATE' # YYYY-MM-DD
)
class StorageStyle(object):
"""Parameterizes the storage behavior of a single field for a certain tag
@ -124,6 +68,86 @@ class StorageStyle(object):
#### dealing with packings ####
class Packed(object):
"""Makes a packed list of values subscriptable. To access the packed output
after making changes, use packed_thing.items."""
def __init__(self, items, packstyle, none_val=0, out_type=int):
"""Create a Packed object for subscripting the packed values in items.
The items are packed using packstyle, which is a value from the
packing enum. none_val is returned from a request when no suitable
value is found in the items. Vales are converted to out_type before
they are returned."""
self.items = items
self.packstyle = packstyle
self.none_val = none_val
self.out_type = out_type
def __getitem__(self, index):
if not isinstance(index, int):
raise TypeError('index must be an integer')
if self.items is None:
return self.none_val
# transform from a string packing into a list we can index into
if self.packstyle == packing.SLASHED:
seq = unicode(self.items).split('/')
elif self.packstyle == packing.DATE:
seq = unicode(self.items).split('-')
elif self.packstyle == packing.TUPLE:
seq = self.items # tuple: items is already indexable
try:
out = seq[index]
except:
out = None
if out is None or out == self.none_val or out == '':
return self.out_type(self.none_val)
else:
return self.out_type(out)
def __setitem__(self, index, value):
if self.packstyle in (packing.SLASHED, packing.TUPLE):
# SLASHED and TUPLE are always two-item packings
length = 2
else:
# DATE can have up to three fields
length = 3
# make a list of the items we'll pack
new_items = []
for i in range(length):
if i == index:
next_item = value
else:
next_item = self[i]
new_items.append(next_item)
if self.packstyle == packing.DATE:
# Truncate the items wherever we reach an invalid (none) entry.
# This prevents dates like 2008-00-05.
for i, item in enumerate(new_items):
if item == self.none_val or item is None:
del(new_items[i:]) # truncate
break
if self.packstyle == packing.SLASHED:
self.items = '/'.join(map(unicode, new_items))
elif self.packstyle == packing.DATE:
field_lengths = [4, 2, 2] # YYYY-MM-DD
elems = []
for i, item in enumerate(new_items):
elems.append( ('%0' + str(field_lengths[i]) + 'i') % item )
self.items = '-'.join(elems)
elif self.packstyle == packing.TUPLE:
self.items = new_items
class MediaField(object):
"""A descriptor providing access to a particular (abstract) metadata
@ -217,11 +241,7 @@ class MediaField(object):
style = self._style(obj)
out = self._fetchdata(obj)
# deal with slashed and tuple storage
if style.packing:
if style.packing == packing.SLASHED:
out = fromslashed(out)
out = unpair(out, style.pack_pos, noneval=0)
if style.packing: out = Packed(out, style.packing)[style.pack_pos]
# return the appropriate type
if self.out_type == int:
@ -250,24 +270,11 @@ class MediaField(object):
style = self._style(obj)
if style.packing:
# fetch the existing value so we can preserve half of it
pair = self._fetchdata(obj)
if style.packing == packing.SLASHED:
pair = fromslashed(pair)
pair = normalize_pair(pair, noneval=0)
p = Packed(self._fetchdata(obj), style.packing)
p[style.pack_pos] = val
out = p.items
# set the appropriate side of the pair
if style.pack_pos == 0:
pair = (val, pair[1])
else:
pair = (pair[0], val)
if style.packing == packing.SLASHED:
out = toslashed(pair)
else:
out = pair
else: # unicode, integer, or boolean
else: # unicode, integer, or boolean scalar
out = val
# deal with Nones according to abstract type if present
@ -367,9 +374,37 @@ class MediaFile(object):
flac = StorageStyle('grouping')
)
year = MediaField(out_type=int,
mp3 = StorageStyle('TDRC'),
mp4 = StorageStyle("\xa9day"),
flac = StorageStyle('date')
mp3 = StorageStyle('TDRC',
packing = packing.DATE,
pack_pos = 0),
mp4 = StorageStyle("\xa9day",
packing = packing.DATE,
pack_pos = 0),
flac = StorageStyle('date',
packing = packing.DATE,
pack_pos = 0)
)
month = MediaField(out_type=int,
mp3 = StorageStyle('TDRC',
packing = packing.DATE,
pack_pos = 1),
mp4 = StorageStyle("\xa9day",
packing = packing.DATE,
pack_pos = 1),
flac = StorageStyle('date',
packing = packing.DATE,
pack_pos = 1)
)
day = MediaField(out_type=int,
mp3 = StorageStyle('TDRC',
packing = packing.DATE,
pack_pos = 2),
mp4 = StorageStyle("\xa9day",
packing = packing.DATE,
pack_pos = 2),
flac = StorageStyle('date',
packing = packing.DATE,
pack_pos = 2)
)
track = MediaField(out_type = int,
mp3 = StorageStyle('TRCK',

Binary file not shown.

View file

@ -19,13 +19,15 @@ def item(lib=None): return beets.library.Item({
'composer': u'the composer',
'grouping': u'the grouping',
'year': 1,
'track': 2,
'tracktotal': 3,
'disc': 4,
'disctotal': 5,
'month': 2,
'day': 3,
'track': 4,
'tracktotal': 5,
'disc': 6,
'disctotal': 7,
'lyrics': u'the lyrics',
'comments': u'the comments',
'bpm': 6,
'bpm': 8,
'comp': True,
'path': 'somepath',
}, lib)

View file

@ -78,6 +78,8 @@ correct_dicts = {
'composer': u'the composer',
'grouping': u'the grouping',
'year': 2001,
'month': 0,
'day': 0,
'track': 2,
'tracktotal': 3,
'disc': 4,
@ -96,6 +98,8 @@ correct_dicts = {
'composer': u'',
'grouping': u'',
'year': 0,
'month': 0,
'day': 0,
'track': 2,
'tracktotal': 0,
'disc': 4,
@ -114,6 +118,8 @@ correct_dicts = {
'composer': u'',
'grouping': u'',
'year': 0,
'month': 0,
'day': 0,
'track': 0,
'tracktotal': 0,
'disc': 0,
@ -133,6 +139,8 @@ correct_dicts = {
'composer': u'',
'grouping': u'',
'year': 0,
'month': 0,
'day': 0,
'track': 0,
'tracktotal': 0,
'disc': 0,
@ -141,14 +149,24 @@ correct_dicts = {
'comments': u'',
'bpm': 0,
'comp': False
}
},
# full release date
'date': {
'year': 1987,
'month': 3,
'day': 31
},
}
def suite_for_file(path, correct_dict):
s = unittest.TestSuite()
for field in correct_dict.keys():
for field in correct_dict:
s.addTest(MakeReadingTest(path, correct_dict, field)())
if not ( field == 'month' and correct_dict['year'] == 0
or field == 'day' and correct_dict['month'] == 0):
# ensure that we don't test fields that can't be modified
s.addTest(MakeWritingTest(path, correct_dict, field)())
return s
@ -158,14 +176,18 @@ def suite():
# General tests.
for kind in ('m4a', 'mp3', 'flac'):
for tagset in ('full', 'partial', 'min'):
path = 'rsrc' + os.sep + tagset + '.' + kind
path = os.path.join('rsrc', 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',
s.addTest(suite_for_file(os.path.join('rsrc', 'empty.mp3'),
correct_dicts['empty']))
# Special test for advanced release date.
s.addTest(suite_for_file(os.path.join('rsrc', 'date.mp3'),
correct_dicts['date']))
return s
if __name__ == '__main__':