From 0758335ede1ae31ab87c48ea201815c8fc34cca3 Mon Sep 17 00:00:00 2001 From: thebigmunch Date: Thu, 9 Apr 2020 10:06:11 -0400 Subject: [PATCH] Refactor handling of ID3v2 number frames [#22] --- CHANGELOG.md | 13 ++++ src/audio_metadata/formats/id3v2.py | 6 ++ src/audio_metadata/formats/id3v2frames.py | 86 ++++++++++++++++++----- 3 files changed, 88 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 208fdce..e9c341e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,19 @@ This project adheres to [Semantic Versioning](https://semver.org). [Commits](https://github.com/thebigmunch/audio-metadata/compare/0.10.0...master) +### Added + +* ``ID3v2Disc``. +* ``ID3v2DiscFrame``. +* ``ID3v2Track``. +* ``ID3v2TrackFrame``. + +### Changed + +* ``ID3v2NumberFrame`` value is not the appropriate class + (``ID3v2Disc`` or ``ID3v2Track``) rather than having + the number/total abstraction on the frame class itself. + ## [0.10.0](https://github.com/thebigmunch/audio-metadata/releases/tag/0.10.0) (2020-04-08) diff --git a/src/audio_metadata/formats/id3v2.py b/src/audio_metadata/formats/id3v2.py index eb2bff0..173be88 100644 --- a/src/audio_metadata/formats/id3v2.py +++ b/src/audio_metadata/formats/id3v2.py @@ -23,6 +23,7 @@ from .id3v2frames import ( ID3v2Frame, ID3v2GenreFrame, + ID3v2NumberFrame, ID3v2NumericTextFrame, ID3v2PeopleListFrame, ID3v2TextFrame, @@ -111,6 +112,11 @@ def parse(cls, data, id3_version, unsync=False): ), ): frames[frame.name] = frame.value + elif isinstance(frame, ID3v2NumberFrame): + if frame.value.total is not None: + frames[frame.name] = [f"{frame.value.number}/{frame.value.total}"] + else: + frames[frame.name] = [frame.value.number] else: frames[frame.name].append(frame.value) diff --git a/src/audio_metadata/formats/id3v2frames.py b/src/audio_metadata/formats/id3v2frames.py index aabbc6b..f2c9795 100644 --- a/src/audio_metadata/formats/id3v2frames.py +++ b/src/audio_metadata/formats/id3v2frames.py @@ -6,6 +6,8 @@ 'ID3v2Comment', 'ID3v2CommentFrame', 'ID3v2DateFrame', + 'ID3v2Disc', + 'ID3v2DiscFrame', 'ID3v2Frame', 'ID3v2FrameFlags', 'ID3v2FrameTypes', @@ -37,6 +39,8 @@ 'ID3v2TermsOfUse', 'ID3v2TextFrame', 'ID3v2TimestampFrame', + 'ID3v2Track', + 'ID3v2TrackFrame', 'ID3v2USERFrame', 'ID3v2UniqueFileIdentifier', 'ID3v2UniqueFileIdentifierFrame', @@ -111,6 +115,15 @@ class ID3v2Comment(AttrMapping): text = attrib() +@attrs( + repr=False, + kw_only=True, +) +class ID3v2Disc(AttrMapping): + number = attrib() + total = attrib(default=None) + + @attrs( repr=False, kw_only=True, @@ -255,6 +268,15 @@ class ID3v2TermsOfUse(AttrMapping): text = attrib() +@attrs( + repr=False, + kw_only=True, +) +class ID3v2Track(AttrMapping): + number = attrib() + total = attrib(default=None) + + @attrs( repr=False, kw_only=True, @@ -593,13 +615,18 @@ class ID3v2LyricsFrame(ID3v2Frame): class ID3v2NumberFrame(ID3v2Frame): value = attrib() - @value.validator - def validate_value(self, attribute, value): + def _validate_value(value): if not all(char in [*string.digits, '/'] for char in value): raise TagError( "Number frame values must consist only of digits and '/'.", ) + +@attrs( + repr=False, + kw_only=True, +) +class ID3v2DiscFrame(ID3v2NumberFrame): @datareader @staticmethod def _parse_frame_data(data): @@ -607,23 +634,48 @@ def _parse_frame_data(data): encoding = determine_encoding(frame_data) + value = decode_bytestring(frame_data[1:], encoding) + ID3v2NumberFrame._validate_value(value) + + values = value.split('/') + number = values[0] + total = values[1] if len(values) == 2 else None + return ( - decode_bytestring(frame_data[1:], encoding), + ID3v2Disc( + number=number, + total=total, + ), encoding, ) - @property - def number(self): - return self.value.split('/')[0] - @property - def total(self): - try: - tot = self.value.split('/')[1] - except IndexError: - tot = None +@attrs( + repr=False, + kw_only=True, +) +class ID3v2TrackFrame(ID3v2NumberFrame): + @datareader + @staticmethod + def _parse_frame_data(data): + frame_data = data.read() + + encoding = determine_encoding(frame_data) + + value = decode_bytestring(frame_data[1:], encoding) + ID3v2NumberFrame._validate_value(value) + + values = value.split('/') + number = values[0] + total = values[1] if len(values) == 2 else None - return tot + return ( + ID3v2Track( + number=number, + total=total, + ), + encoding, + ) @attrs( @@ -1278,11 +1330,11 @@ def _parse_frame_data(data): 'USLT': ID3v2UnsynchronizedLyricsFrame, # Number Frames - 'TPA': ID3v2NumberFrame, - 'TRK': ID3v2NumberFrame, + 'TPA': ID3v2DiscFrame, + 'TRK': ID3v2TrackFrame, - 'TPOS': ID3v2NumberFrame, - 'TRCK': ID3v2NumberFrame, + 'TPOS': ID3v2DiscFrame, + 'TRCK': ID3v2TrackFrame, # Numeric Text Frames 'TBP': ID3v2NumericTextFrame,