Skip to content

Commit

Permalink
Support multiple fields with the same name
Browse files Browse the repository at this point in the history
  • Loading branch information
mathiascode committed Feb 26, 2024
1 parent 839cc2b commit 480f94c
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 30 deletions.
4 changes: 4 additions & 0 deletions README.md
Expand Up @@ -73,6 +73,10 @@ List of possible attributes you can get with TinyTag:
tag.track_total # total number of tracks as string
tag.year # year or date as string

If multiple fields with the same name are provided, the values are separated with a null character:

tag.artist == 'artist 1\x00artist 2\x00artist 3'

For non-common fields and fields specific to certain file formats, use `extra`:

tag.extra # a dict of additional data
Expand Down
Binary file added tinytag/tests/samples/flac_multiple_fields.flac
Binary file not shown.
23 changes: 15 additions & 8 deletions tinytag/tests/test_all.py
Expand Up @@ -130,7 +130,8 @@
('samples/id3v1_does_not_overwrite_id3v2.mp3',
{'filesize': 1130, 'album': 'Somewhere Far Beyond', 'albumartist': 'Blind Guardian',
'artist': 'Blind Guardian', 'extra': {'love rating': 'L'},
'genre': 'Power Metal', 'title': 'Time What Is Time', 'track': 1, 'year': '1992'}),
'genre': 'Power Metal\x00Other', 'title': 'Time What Is Time', 'track': 1,
'year': '1992'}),
('samples/nicotinetestdata.mp3',
{'extra': {}, 'filesize': 80919, 'channels': 2,
'duration': 5.067755102040817, 'samplerate': 44100, 'bitrate': 127.6701030927835}),
Expand Down Expand Up @@ -291,7 +292,8 @@
('samples/id3_header_with_a_zero_byte.wav',
{'extra': {}, 'channels': 1, 'duration': 1.0, 'filesize': 44280, 'bitrate': 352.8,
'samplerate': 22050, 'bitdepth': 16, 'artist': 'Purpley',
'title': 'Test000', 'track': 17, 'album': 'prototypes'}),
'title': 'Test000\x00Stacked\x00Test000\x00Stacked', 'track': 17,
'album': 'prototypes'}),
('samples/adpcm.wav',
{'extra': {}, 'channels': 1, 'duration': 12.167256235827665, 'filesize': 268686,
'bitrate': 176.4, 'samplerate': 44100, 'bitdepth': 4,
Expand Down Expand Up @@ -362,9 +364,9 @@
{'extra': {}, 'filesize': 4692, 'bitrate': 10.186943678613627, 'channels': 2,
'duration': 3.68, 'samplerate': 44100, 'bitdepth': 16}),
('samples/with_id3_header.flac',
{'extra': {'id': '8591671910'}, 'filesize': 64837, 'album': ' ',
'artist': '群星',
'title': 'A 梦 哆啦 机器猫 短信铃声', 'track': 1, 'bitrate': 1143.72468, 'channels': 1,
{'extra': {'id': '8591671910'}, 'filesize': 64837, 'album': ' \x00album',
'artist': '群星\x00artist',
'title': 'A 梦 哆啦 机器猫 短信铃声\x00title', 'track': 1, 'bitrate': 1143.72468, 'channels': 1,
'duration': 0.45351473922902497, 'genre': 'genre', 'samplerate': 44100, 'bitdepth': 16,
'year': '2018', 'comment': 'comment'}),
('samples/with_padded_id3_header.flac',
Expand All @@ -373,11 +375,11 @@
'duration': 0.45351473922902497, 'genre': 'genre', 'samplerate': 44100, 'bitdepth': 16,
'title': 'title', 'track': 1, 'year': '2018', 'comment': 'comment'}),
('samples/with_padded_id3_header2.flac',
{'extra': {}, 'filesize': 19522, 'album': 'Unbekannter Titel',
'artist': 'Unbekannter Künstler', 'bitrate': 344.36807999999996,
{'extra': {}, 'filesize': 19522, 'album': 'Unbekannter Titel\x00album',
'artist': 'Unbekannter Künstler\x00artist', 'bitrate': 344.36807999999996,
'channels': 1, 'disc': 1, 'disc_total': 1,
'duration': 0.45351473922902497, 'genre': 'genre', 'samplerate': 44100, 'bitdepth': 16,
'title': 'Track01', 'track': 1, 'track_total': 5, 'year': '2018',
'title': 'Track01\x00title', 'track': 1, 'track_total': 5, 'year': '2018',
'comment': 'comment'}),
('samples/flac_with_image.flac',
{'extra': {}, 'filesize': 80000, 'album': 'smilin´ in circles',
Expand All @@ -388,6 +390,11 @@
('samples/flac_invalid_track_number.flac',
{'extra': {}, 'filesize': 235, 'bitrate': 18.8, 'channels': 1,
'duration': 0.1, 'samplerate': 44100, 'bitdepth': 16}),
('samples/flac_multiple_fields.flac',
{'extra': {}, 'filesize': 235, 'album': 'album 1\x00album 2',
'artist': 'artist 1\x00artist 2\x00artist 3',
'bitrate': 18.8, 'channels': 1, 'duration': 0.1, 'genre': 'genre 1\x00genre 2',
'samplerate': 44100, 'bitdepth': 16}),

# WMA
('samples/test2.wma',
Expand Down
48 changes: 26 additions & 22 deletions tinytag/tinytag.py
Expand Up @@ -229,46 +229,50 @@ def load(self, tags, duration, image=False):
self._filehandler.seek(0)
self._determine_duration(self._filehandler)

def _set_field(self, fieldname, value, overwrite=True):
def _set_field(self, fieldname, value):
"""convenience function to set fields of the tinytag by name"""
write_dest = self # write into the TinyTag by default
get_func = getattr
set_func = setattr
is_extra = fieldname.startswith('extra.') # but if it's marked as extra field
is_str = isinstance(value, str)
if is_extra:
fieldname = fieldname[6:]
write_dest = self.extra # write into the extra field instead
get_func = operator.getitem
set_func = operator.setitem
if isinstance(value, str) and not value:
if is_str and not value:
# don't set empty value
return
if get_func(write_dest, fieldname): # do not overwrite existing data
old_value = get_func(write_dest, fieldname)
if old_value and fieldname == "comment":
# Don't combine comment fields for now
return
if is_str and old_value and old_value != value:
# Combine same field with a null character
value = old_value + '\x00' + value
if DEBUG:
stderr(f'Setting field "{fieldname}" to "{value}"')
if overwrite or not get_func(write_dest, fieldname):
set_func(write_dest, fieldname, value)
set_func(write_dest, fieldname, value)

def _determine_duration(self, fh):
raise NotImplementedError()

def _parse_tag(self, fh):
raise NotImplementedError()

def update(self, other, all_fields=False):
def update(self, other):
# update the values of this tag with the values from another tag
if all_fields:
self.__dict__.update(other.__dict__)
return
for key in ['track', 'track_total', 'title', 'artist',
'album', 'albumartist', 'year', 'duration',
'genre', 'disc', 'disc_total', 'comment', 'composer',
'extra', '_image_data']:
if not getattr(self, key) and getattr(other, key):
setattr(self, key, getattr(other, key))
if other.extra:
self.extra.update(other.extra)
'bitdepth', 'bitrate', 'channels', 'samplerate',
'_image_data']:
new_value = getattr(other, key)
if new_value:
self._set_field(key, new_value)
for key, value in other.extra.items():
self._set_field("extra." + key, value)

@staticmethod
def _unpad(s):
Expand Down Expand Up @@ -779,18 +783,18 @@ def _parse_id3v1(self, fh):
def asciidecode(x):
return self._unpad(x.decode(self._default_encoding or 'latin1'))
fields = fh.read(30 + 30 + 30 + 4 + 30 + 1)
self._set_field('title', asciidecode(fields[:30]), overwrite=False)
self._set_field('artist', asciidecode(fields[30:60]), overwrite=False)
self._set_field('album', asciidecode(fields[60:90]), overwrite=False)
self._set_field('year', asciidecode(fields[90:94]), overwrite=False)
self._set_field('title', asciidecode(fields[:30]))
self._set_field('artist', asciidecode(fields[30:60]))
self._set_field('album', asciidecode(fields[60:90]))
self._set_field('year', asciidecode(fields[90:94]))
comment = fields[94:124]
if b'\x00\x00' < comment[-2:] < b'\x01\x00':
self._set_field('track', ord(comment[-1:]), overwrite=False)
self._set_field('track', ord(comment[-1:]))
comment = comment[:-2]
self._set_field('comment', asciidecode(comment), overwrite=False)
self._set_field('comment', asciidecode(comment))
genre_id = ord(fields[124:125])
if genre_id < len(self.ID3V1_GENRES):
self._set_field('genre', self.ID3V1_GENRES[genre_id], overwrite=False)
self._set_field('genre', self.ID3V1_GENRES[genre_id])

@staticmethod
def index_utf16(s, search):
Expand Down Expand Up @@ -975,7 +979,7 @@ def _parse_tag(self, fh):
flactag = _Flac(io.BufferedReader(walker), self.filesize)
flactag.load(tags=self._parse_tags, duration=self._parse_duration,
image=self._load_image)
self.update(flactag, all_fields=True)
self.update(flactag)
check_flac_second_packet = True
elif check_flac_second_packet:
# second packet contains FLAC metadata block
Expand Down

0 comments on commit 480f94c

Please sign in to comment.