mirror of
https://github.com/beetbox/beets.git
synced 2025-12-07 17:16:07 +01:00
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:
parent
605343fb57
commit
1294d573d6
4 changed files with 155 additions and 96 deletions
|
|
@ -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 ####
|
#### flags used to define fields' behavior ####
|
||||||
|
|
@ -93,7 +33,11 @@ class Enumeration(object):
|
||||||
def __init__(self, *values):
|
def __init__(self, *values):
|
||||||
for value, equiv in zip(values, range(1, len(values)+1)):
|
for value, equiv in zip(values, range(1, len(values)+1)):
|
||||||
setattr(self, value, equiv)
|
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):
|
class StorageStyle(object):
|
||||||
"""Parameterizes the storage behavior of a single field for a certain tag
|
"""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):
|
class MediaField(object):
|
||||||
"""A descriptor providing access to a particular (abstract) metadata
|
"""A descriptor providing access to a particular (abstract) metadata
|
||||||
|
|
@ -217,11 +241,7 @@ class MediaField(object):
|
||||||
style = self._style(obj)
|
style = self._style(obj)
|
||||||
out = self._fetchdata(obj)
|
out = self._fetchdata(obj)
|
||||||
|
|
||||||
# deal with slashed and tuple storage
|
if style.packing: out = Packed(out, style.packing)[style.pack_pos]
|
||||||
if style.packing:
|
|
||||||
if style.packing == packing.SLASHED:
|
|
||||||
out = fromslashed(out)
|
|
||||||
out = unpair(out, style.pack_pos, noneval=0)
|
|
||||||
|
|
||||||
# return the appropriate type
|
# return the appropriate type
|
||||||
if self.out_type == int:
|
if self.out_type == int:
|
||||||
|
|
@ -250,24 +270,11 @@ class MediaField(object):
|
||||||
style = self._style(obj)
|
style = self._style(obj)
|
||||||
|
|
||||||
if style.packing:
|
if style.packing:
|
||||||
# fetch the existing value so we can preserve half of it
|
p = Packed(self._fetchdata(obj), style.packing)
|
||||||
pair = self._fetchdata(obj)
|
p[style.pack_pos] = val
|
||||||
if style.packing == packing.SLASHED:
|
out = p.items
|
||||||
pair = fromslashed(pair)
|
|
||||||
pair = normalize_pair(pair, noneval=0)
|
|
||||||
|
|
||||||
# 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
|
out = val
|
||||||
|
|
||||||
# deal with Nones according to abstract type if present
|
# deal with Nones according to abstract type if present
|
||||||
|
|
@ -367,9 +374,37 @@ class MediaFile(object):
|
||||||
flac = StorageStyle('grouping')
|
flac = StorageStyle('grouping')
|
||||||
)
|
)
|
||||||
year = MediaField(out_type=int,
|
year = MediaField(out_type=int,
|
||||||
mp3 = StorageStyle('TDRC'),
|
mp3 = StorageStyle('TDRC',
|
||||||
mp4 = StorageStyle("\xa9day"),
|
packing = packing.DATE,
|
||||||
flac = StorageStyle('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,
|
track = MediaField(out_type = int,
|
||||||
mp3 = StorageStyle('TRCK',
|
mp3 = StorageStyle('TRCK',
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -19,13 +19,15 @@ def item(lib=None): return beets.library.Item({
|
||||||
'composer': u'the composer',
|
'composer': u'the composer',
|
||||||
'grouping': u'the grouping',
|
'grouping': u'the grouping',
|
||||||
'year': 1,
|
'year': 1,
|
||||||
'track': 2,
|
'month': 2,
|
||||||
'tracktotal': 3,
|
'day': 3,
|
||||||
'disc': 4,
|
'track': 4,
|
||||||
'disctotal': 5,
|
'tracktotal': 5,
|
||||||
|
'disc': 6,
|
||||||
|
'disctotal': 7,
|
||||||
'lyrics': u'the lyrics',
|
'lyrics': u'the lyrics',
|
||||||
'comments': u'the comments',
|
'comments': u'the comments',
|
||||||
'bpm': 6,
|
'bpm': 8,
|
||||||
'comp': True,
|
'comp': True,
|
||||||
'path': 'somepath',
|
'path': 'somepath',
|
||||||
}, lib)
|
}, lib)
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,8 @@ correct_dicts = {
|
||||||
'composer': u'the composer',
|
'composer': u'the composer',
|
||||||
'grouping': u'the grouping',
|
'grouping': u'the grouping',
|
||||||
'year': 2001,
|
'year': 2001,
|
||||||
|
'month': 0,
|
||||||
|
'day': 0,
|
||||||
'track': 2,
|
'track': 2,
|
||||||
'tracktotal': 3,
|
'tracktotal': 3,
|
||||||
'disc': 4,
|
'disc': 4,
|
||||||
|
|
@ -96,6 +98,8 @@ correct_dicts = {
|
||||||
'composer': u'',
|
'composer': u'',
|
||||||
'grouping': u'',
|
'grouping': u'',
|
||||||
'year': 0,
|
'year': 0,
|
||||||
|
'month': 0,
|
||||||
|
'day': 0,
|
||||||
'track': 2,
|
'track': 2,
|
||||||
'tracktotal': 0,
|
'tracktotal': 0,
|
||||||
'disc': 4,
|
'disc': 4,
|
||||||
|
|
@ -114,6 +118,8 @@ correct_dicts = {
|
||||||
'composer': u'',
|
'composer': u'',
|
||||||
'grouping': u'',
|
'grouping': u'',
|
||||||
'year': 0,
|
'year': 0,
|
||||||
|
'month': 0,
|
||||||
|
'day': 0,
|
||||||
'track': 0,
|
'track': 0,
|
||||||
'tracktotal': 0,
|
'tracktotal': 0,
|
||||||
'disc': 0,
|
'disc': 0,
|
||||||
|
|
@ -133,6 +139,8 @@ correct_dicts = {
|
||||||
'composer': u'',
|
'composer': u'',
|
||||||
'grouping': u'',
|
'grouping': u'',
|
||||||
'year': 0,
|
'year': 0,
|
||||||
|
'month': 0,
|
||||||
|
'day': 0,
|
||||||
'track': 0,
|
'track': 0,
|
||||||
'tracktotal': 0,
|
'tracktotal': 0,
|
||||||
'disc': 0,
|
'disc': 0,
|
||||||
|
|
@ -141,15 +149,25 @@ correct_dicts = {
|
||||||
'comments': u'',
|
'comments': u'',
|
||||||
'bpm': 0,
|
'bpm': 0,
|
||||||
'comp': False
|
'comp': False
|
||||||
}
|
},
|
||||||
|
|
||||||
|
# full release date
|
||||||
|
'date': {
|
||||||
|
'year': 1987,
|
||||||
|
'month': 3,
|
||||||
|
'day': 31
|
||||||
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def suite_for_file(path, correct_dict):
|
def suite_for_file(path, correct_dict):
|
||||||
s = unittest.TestSuite()
|
s = unittest.TestSuite()
|
||||||
for field in correct_dict.keys():
|
for field in correct_dict:
|
||||||
s.addTest(MakeReadingTest(path, correct_dict, field)())
|
s.addTest(MakeReadingTest(path, correct_dict, field)())
|
||||||
s.addTest(MakeWritingTest(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
|
return s
|
||||||
|
|
||||||
def suite():
|
def suite():
|
||||||
|
|
@ -158,14 +176,18 @@ def suite():
|
||||||
# General tests.
|
# General tests.
|
||||||
for kind in ('m4a', 'mp3', 'flac'):
|
for kind in ('m4a', 'mp3', 'flac'):
|
||||||
for tagset in ('full', 'partial', 'min'):
|
for tagset in ('full', 'partial', 'min'):
|
||||||
path = 'rsrc' + os.sep + tagset + '.' + kind
|
path = os.path.join('rsrc', tagset + '.' + kind)
|
||||||
correct_dict = correct_dicts[tagset]
|
correct_dict = correct_dicts[tagset]
|
||||||
s.addTest(suite_for_file(path, correct_dict))
|
s.addTest(suite_for_file(path, correct_dict))
|
||||||
|
|
||||||
# Special test for missing ID3 tag.
|
# 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']))
|
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
|
return s
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue