From 890ff5631177433792f29e57ca0a01b5f4bcb84c Mon Sep 17 00:00:00 2001 From: thebigmunch Date: Fri, 21 Feb 2020 15:57:56 -0500 Subject: [PATCH] Add support for ID3v2 involved people list frames [#22] --- CHANGELOG.md | 6 +++ src/audio_metadata/formats/id3v2.py | 4 ++ src/audio_metadata/formats/id3v2_frames.py | 54 ++++++++++++++++++++-- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2db855..945b55e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ This project adheres to [Semantic Versioning](https://semver.org). [Commits](https://github.com/thebigmunch/audio-metadata/compare/0.7.0...master) +### Added + +* Support for ID3v2 involved people list frames. + * ``ID3v2InvolvedPerson``. + * ``ID3v2InvolvedPeopleListFrame``. + ### Changed * Make all attrs classes require keyword arguments. diff --git a/src/audio_metadata/formats/id3v2.py b/src/audio_metadata/formats/id3v2.py index 19bbb9b..85e1b50 100644 --- a/src/audio_metadata/formats/id3v2.py +++ b/src/audio_metadata/formats/id3v2.py @@ -70,6 +70,7 @@ class ID3v2Frames(Tags): 'originalartist': 'TOA', 'originalauthor': 'TOL', 'originaldate': 'TOR', + 'people': 'IPL', 'pictures': 'PIC', 'playcount': 'CNT', 'remixer': 'TP4', @@ -113,6 +114,7 @@ class ID3v2Frames(Tags): 'originalartist': 'TOPE', 'originalauthor': 'TOLY', 'originaldate': 'TORY', + 'people': 'IPLS', 'pictures': 'APIC', 'playcount': 'PCNT', 'remixer': 'TPE4', @@ -158,6 +160,7 @@ class ID3v2Frames(Tags): 'originalartist': 'TOPE', 'originalauthor': 'TOLY', 'originaldate': 'TDOR', + 'people': 'TIPL', 'pictures': 'APIC', 'playcount': 'PCNT', 'remixer': 'TPE4', @@ -257,6 +260,7 @@ def load(cls, data, id3_version): elif isinstance( frame, ( + ID3v2InvolvedPeopleListFrame, ID3v2NumericTextFrame, ID3v2TextFrame, ID3v2TimestampFrame, diff --git a/src/audio_metadata/formats/id3v2_frames.py b/src/audio_metadata/formats/id3v2_frames.py index 99d8c3d..1af0a07 100644 --- a/src/audio_metadata/formats/id3v2_frames.py +++ b/src/audio_metadata/formats/id3v2_frames.py @@ -6,6 +6,8 @@ 'ID3v2Frame', 'ID3v2GEOBFrame', 'ID3v2GenreFrame', + 'ID3v2InvolvedPeopleListFrame', + 'ID3v2InvolvedPerson', 'ID3v2NumberFrame', 'ID3v2NumericTextFrame', 'ID3v2Picture', @@ -26,6 +28,7 @@ import struct from urllib.parse import unquote +import more_itertools from attr import ( attrib, attrs, @@ -52,6 +55,15 @@ _genre_re = re.compile(r"((?:\((?P\d+|RX|CR)\))*)(?P.+)?") +@attrs( + repr=False, + kw_only=True, +) +class ID3v2InvolvedPerson(AttrMapping): + involvement = attrib() + name = attrib() + + class ID3v2Picture(Picture): @datareader @classmethod @@ -117,6 +129,14 @@ class ID3v2GEOBFrame(ID3v2BaseFrame): value = attrib() +@attrs( + repr=False, + kw_only=True, +) +class ID3v2InvolvedPeopleListFrame(ID3v2BaseFrame): + value = attrib() + + @attrs( repr=False, kw_only=True, @@ -311,26 +331,29 @@ class ID3v2Frame(ID3v2BaseFrame): value = attrib() # TODO:ID3v2.2 - # TODO: BUF, CNT, CRA, CRM, ETC, EQU, IPL, LNK, MCI, MLL, POP, REV, + # TODO: BUF, CNT, CRA, CRM, ETC, EQU, LNK, MCI, MLL, POP, REV, # TODO: RVA, STC, UFI # TODO: ID3v2.3 - # TODO: AENC, COMR, ENCR, EQUA, ETCO, GRID, IPLS, LINK, MCDI, MLLT, OWNE + # TODO: AENC, COMR, ENCR, EQUA, ETCO, GRID, LINK, MCDI, MLLT, OWNE # TODO: PCNT, POPM, POSS, RBUF, RVAD, RVRB, SYTC, UFID, USER # TODO: ID3v2.4 - # TODO: ASPI, EQU2, RVA2, SEEK, SIGN, TDEN, TDOR, TDRC, TDRL, TDTG, TIPL - # TODO: TMCL, TPRO, + # TODO: ASPI, EQU2, RVA2, SEEK, SIGN, TDEN, TDOR, TDRC, TDRL, TDTG + # TODO: TMCL, TPRO _FRAME_TYPES = { # Complex Text Frames 'COM': ID3v2CommentFrame, 'GEO': ID3v2GEOBFrame, + 'IPL': ID3v2InvolvedPeopleListFrame, 'TXX': ID3v2UserTextFrame, 'COMM': ID3v2CommentFrame, 'GEOB': ID3v2GEOBFrame, + 'IPLS': ID3v2InvolvedPeopleListFrame, 'PRIV': ID3v2PrivateFrame, + 'TIPL': ID3v2InvolvedPeopleListFrame, 'TXXX': ID3v2UserTextFrame, # Genre Frame @@ -492,6 +515,29 @@ def load(cls, data, struct_pattern, size_len, per_byte): kwargs['language'] = decode_bytestring(frame_data[1:4]) kwargs['description'] = decode_bytestring(values[0], encoding) kwargs['value'] = decode_bytestring(values[1], encoding) + elif frame_type is ID3v2InvolvedPeopleListFrame: + encoding = determine_encoding(frame_data[0:1]) + + values = [] + tail = frame_data[1:] + + while tail: + head, tail = split_encoded(tail, encoding) + values.append(head) + + people = [ + ID3v2InvolvedPerson( + involvement=decode_bytestring(involvement, encoding), + name=decode_bytestring(name, encoding), + ) + for involvement, name in more_itertools.chunked(values, 2) + ] + + # Ignore empty people list. + if len(values) < 1: + return None + + kwargs['value'] = people elif frame_type is ID3v2GenreFrame: encoding = determine_encoding(frame_data[0:1])