From f33c0bceff4ed693db5b32726bd64aac11b21937 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Mon, 3 Mar 2014 00:02:42 +0100 Subject: [PATCH] Remove date packing and invert date dependency Before, each year, month, and day field used packing to store its values in the same tag but at different positions. We then instantiated a `CompositeDateField` to combine the different values. This lead to code duplication in the storage styles for these fields. It also inverted the data dependency. It's more natural to think of year, month, and day as part of a date then as of a date as composed of these. Now, only `DateField` class stores data in the files tag. This makes sense: One tag, one field that accesses it. To obtain access to the year, month, and day parts, the DateField is equipped with factories that create `DateItemField` instances associated to a `DateField`. These descriptor allow us to get and set parts of a date field. --- beets/mediafile.py | 206 ++++++++++++++++++++--------------------- test/rsrc/year.ogg | Bin 0 -> 8556 bytes test/test_mediafile.py | 26 ++++++ 3 files changed, 125 insertions(+), 107 deletions(-) create mode 100644 test/rsrc/year.ogg diff --git a/beets/mediafile.py b/beets/mediafile.py index de63a1f58..02d23c596 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -257,8 +257,7 @@ def _sc_encode(gain, peak): # Flags for encoding field behavior. # Determine style of packing, if any. -packing = enum('DATE', # YYYY-MM-DD - 'SC', # Sound Check gain/peak encoding +packing = enum('SC', # Sound Check gain/peak encoding name='packing') @@ -323,20 +322,12 @@ class StorageStyle(object): def unpack(self, data): """Splits raw data from a tag into a list of values.""" - if self.packing == packing.DATE: - packing_length = 3 - else: - packing_length = 2 + packing_length = 2 if data is None: return [None] * packing_length - if self.packing == packing.DATE: - # Remove time information from dates. Usually delimited by - # a "T" or a space. - data = re.sub(r'[Tt ].*$', '', unicode(data)) - items = unicode(data).split('-') - elif self.packing == packing.SC: + if self.packing == packing.SC: items = _sc_decode(data) return list(items) + [None] * (packing_length - len(items)) @@ -369,20 +360,7 @@ class StorageStyle(object): items[i] = self._none_value() items[self.pack_pos] = value - - if self.packing == packing.DATE: - # Truncate the items wherever we reach an invalid (none) - # entry. This prevents dates like 2008-00-05. - for i, item in enumerate(items): - if item == self._none_value() or item is None: - del(items[i:]) # truncate - break - field_lengths = [4, 2, 2] # YYYY-MM-DD - elems = [] - for i, item in enumerate(items): - elems.append('{0:0{1}}'.format(int(item), field_lengths[i])) - data = '-'.join(elems) - elif self.packing == packing.SC: + if self.packing == packing.SC: data = _sc_encode(*items) return data @@ -851,7 +829,8 @@ class MediaField(object): if mediafile.type in style.formats: yield style - def __get__(self, mediafile, owner): + def __get__(self, mediafile, owner=None): + out = None for style in self.styles(mediafile): out = style.get(mediafile.mgfile) if out: @@ -889,43 +868,89 @@ class ListMediaField(MediaField): return MediaField(*self._styles, **options) -class CompositeDateField(MediaField): - """A MediaFile field for conveniently accessing the year, month, and - day fields as a datetime.date object. Allows both getting and - setting of the component fields. - """ - def __init__(self, year_field, month_field, day_field): - """Create a new date field from the indicated MediaFields for - the component values. - """ - self.year_field = year_field - self.month_field = month_field - self.day_field = day_field +class DateField(MediaField): + """Descriptor that handles serializing and deserializing dates - def __get__(self, mediafile, owner): - """Return a datetime.date object whose components indicating the - smallest valid date whose components are at least as large as - the three component fields (that is, if year == 1999, month == 0, - and day == 0, then date == datetime.date(1999, 1, 1)). If the - components indicate an invalid date (e.g., if month == 47), - datetime.date.min is returned. + The getter parses value from tags into a ``datetime.date`` instance + and setter serializes such an instance into a string. + + For granular access to year, month, and day, use the ``*_field`` + methods to create corresponding ``DateItemFields``. + """ + + def __init__(self, *date_styles, **kwargs): + """``date_styles`` is a list of ``StorageStyle``s to store and + retrieve the whole date from. The ``year`` option is an + additional list of fallback styles for the year. The year is + always set on this style, but is only retrieved if the main + storage styles do not return a value. """ + super(DateField, self).__init__(*date_styles) + year_style = kwargs.get('year', None) + if year_style: + self._year_field = MediaField(*year_style) + + def __get__(self, mediafile, owner=None): + year, month, day = self._get_date_tuple(mediafile) try: return datetime.date( - max(self.year_field.__get__(mediafile, owner), datetime.MINYEAR), - max(self.month_field.__get__(mediafile, owner), 1), - max(self.day_field.__get__(mediafile, owner), 1) + year or datetime.MINYEAR, + month or 1, + day or 1 ) except ValueError: # Out of range values. return datetime.date.min def __set__(self, mediafile, date): - """Set the year, month, and day fields to match the components of - the provided datetime.date object. - """ - self.year_field.__set__(mediafile, date.year) - self.month_field.__set__(mediafile, date.month) - self.day_field.__set__(mediafile, date.day) + self._set_date_tuple(mediafile, date.year, date.month, date.day) + + def _get_date_tuple(self, mediafile): + datestring = MediaField.__get__(self, mediafile, None) + datestring = re.sub(r'[Tt ].*$', '', unicode(datestring)) + items = unicode(datestring).split('-') + items = items + [None]*(3 - len(items)) + if not items[0] and hasattr(self, '_year_field'): + # Fallback to addition year field + items[0] = self._year_field.__get__(mediafile) + return [int(item or 0) for item in items] + + def _set_date_tuple(self, mediafile, year, month=None, day=None): + date = [year or 0] + if month: + date.append(month) + if month and day: + date.append(day) + date = map(unicode, date) + super(DateField, self).__set__(mediafile, '-'.join(date)) + + if hasattr(self, '_year_field'): + self._year_field.__set__(mediafile, year) + + def year_field(self): + return DateItemField(self, 0) + + def month_field(self): + return DateItemField(self, 1) + + def day_field(self): + return DateItemField(self, 2) + + +class DateItemField(MediaField): + """Descriptor that gets and sets parts of a ``DateField``. + """ + + def __init__(self, date_field, item_pos): + self.date_field = date_field + self.item_pos = item_pos + + def __get__(self, mediafile, _): + return self.date_field._get_date_tuple(mediafile)[self.item_pos] + + def __set__(self, mediafile, value): + items = self.date_field._get_date_tuple(mediafile) + items[self.item_pos] = value + self.date_field._set_date_tuple(mediafile, *items) class ImageField(MediaField): @@ -1278,60 +1303,27 @@ class MediaFile(object): ) # Release date. - year = MediaField( - MP3StorageStyle('TDRC', packing=packing.DATE, pack_pos=0), - MP4StorageStyle("\xa9day", packing=packing.DATE, pack_pos=0), - StorageStyle('DATE', packing=packing.DATE, pack_pos=0), - StorageStyle('YEAR'), - ASFStorageStyle('WM/Year', packing=packing.DATE, pack_pos=0), - out_type=int, - ) - month = MediaField( - MP3StorageStyle('TDRC', packing=packing.DATE, pack_pos=1), - MP4StorageStyle("\xa9day", packing=packing.DATE, pack_pos=1), - StorageStyle('DATE', packing=packing.DATE, pack_pos=1), - ASFStorageStyle('WM/Year', packing=packing.DATE, pack_pos=1), - out_type=int, - ) - day = MediaField( - MP3StorageStyle('TDRC', packing=packing.DATE, pack_pos=2), - MP4StorageStyle("\xa9day", packing=packing.DATE, pack_pos=2), - StorageStyle('DATE', packing=packing.DATE, pack_pos=2), - ASFStorageStyle('WM/Year', packing=packing.DATE, pack_pos=2), - out_type=int, - ) - date = CompositeDateField(year, month, day) + date = DateField( + MP3StorageStyle('TDRC'), + MP4StorageStyle("\xa9day"), + StorageStyle('DATE'), + ASFStorageStyle('WM/Year'), + year=(StorageStyle('YEAR'),)) + + year = date.year_field() + month = date.month_field() + day = date.day_field() # *Original* release date. - original_year = MediaField( - MP3StorageStyle('TDOR', packing=packing.DATE, pack_pos=0), - MP4StorageStyle('----:com.apple.iTunes:ORIGINAL YEAR', - packing=packing.DATE, pack_pos=0), - StorageStyle('ORIGINALDATE', packing=packing.DATE, pack_pos=0), - ASFStorageStyle('WM/OriginalReleaseYear', packing=packing.DATE, - pack_pos=0), - out_type=int, - ) - original_month = MediaField( - MP3StorageStyle('TDOR', packing=packing.DATE, pack_pos=1), - MP4StorageStyle('----:com.apple.iTunes:ORIGINAL YEAR', - packing=packing.DATE, pack_pos=1), - StorageStyle('ORIGINALDATE', packing=packing.DATE, pack_pos=1), - ASFStorageStyle('WM/OriginalReleaseYear', packing=packing.DATE, - pack_pos=1), - out_type=int, - ) - original_day = MediaField( - MP3StorageStyle('TDOR', packing=packing.DATE, pack_pos=2), - MP4StorageStyle('----:com.apple.iTunes:ORIGINAL YEAR', - packing=packing.DATE, pack_pos=2), - StorageStyle('ORIGINALDATE', packing=packing.DATE, pack_pos=2), - ASFStorageStyle('WM/OriginalReleaseYear', packing=packing.DATE, - pack_pos=2), - out_type=int, - ) - original_date = CompositeDateField(original_year, original_month, - original_day) + original_date = DateField( + MP3StorageStyle('TDOR'), + MP4StorageStyle('----:com.apple.iTunes:ORIGINAL YEAR'), + StorageStyle('ORIGINALDATE'), + ASFStorageStyle('WM/OriginalReleaseYear')) + + original_year = original_date.year_field() + original_month = original_date.month_field() + original_day = original_date.day_field() # Nonstandard metadata. artist_credit = MediaField( diff --git a/test/rsrc/year.ogg b/test/rsrc/year.ogg new file mode 100644 index 0000000000000000000000000000000000000000..955e8a560c5aab7a3adffb3ae33a9ac5cb5d2315 GIT binary patch literal 8556 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