Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New tags: Copyright, ISRC, Artists, AlbumArtists, URL #42

Merged
merged 5 commits into from
May 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ The metadata schema is generally based on MusicBrainz' schema with similar namin

* basic fields like ``title``, ``album``, ``artist`` and ``albumartist``,
* sorting variants like ``albumartist_sort`` and ``composer_sort``,
* identifiers like ``asin`` or ``mb_releasegroupid``,
* plural/list variants like ``artists`` and ``albumartists``,
* identifiers like ``asin``, ``isrc`` or ``mb_releasegroupid``,
* dates like the release ``year``, ``month`` and ``day`` with convenience wrapper ``date``,
* detailed metadata like ``language`` or ``media``,
* ``lyrics``,
* ``lyrics``, ``copyright``, ``url``
* calculated metadata like ``bpm`` (beats per minute) and ``r128_track_gain`` (ReplayGain),
* embedded images (e.g. album art),
* file metadata like ``bitrate`` and ``length``.
Expand Down
86 changes: 78 additions & 8 deletions mediafile.py
Original file line number Diff line number Diff line change
Expand Up @@ -839,31 +839,38 @@ def store(self, mutagen_file, value):
class MP3DescStorageStyle(MP3StorageStyle):
"""Store data in a TXXX (or similar) ID3 frame. The frame is
selected based its ``desc`` field.
JuniorJPDJ marked this conversation as resolved.
Show resolved Hide resolved
``attr`` allows to specify name of data accessor property in the frame.
Most of frames use `text`.
``multispec`` specifies if frame data is ``mutagen.id3.MultiSpec``
which means that the data is being packed in the list.
"""
def __init__(self, desc=u'', key='TXXX', **kwargs):
def __init__(self, desc=u'', key='TXXX', attr='text', multispec=True,
**kwargs):
assert isinstance(desc, six.text_type)
self.description = desc
self.attr = attr
self.multispec = multispec
super(MP3DescStorageStyle, self).__init__(key=key, **kwargs)

def store(self, mutagen_file, value):
frames = mutagen_file.tags.getall(self.key)
if self.key != 'USLT':
if self.multispec:
value = [value]

# Try modifying in place.
found = False
for frame in frames:
if frame.desc.lower() == self.description.lower():
frame.text = value
setattr(frame, self.attr, value)
frame.encoding = mutagen.id3.Encoding.UTF8
found = True

# Try creating a new frame.
if not found:
frame = mutagen.id3.Frames[self.key](
desc=self.description,
text=value,
encoding=mutagen.id3.Encoding.UTF8,
**{self.attr: value}
)
if self.id3_lang:
frame.lang = self.id3_lang
Expand All @@ -872,10 +879,10 @@ def store(self, mutagen_file, value):
def fetch(self, mutagen_file):
for frame in mutagen_file.tags.getall(self.key):
if frame.desc.lower() == self.description.lower():
if self.key == 'USLT':
return frame.text
if not self.multispec:
return getattr(frame, self.attr)
try:
return frame.text[0]
return getattr(frame, self.attr)[0]
except IndexError:
return None

Expand All @@ -889,6 +896,34 @@ def delete(self, mutagen_file):
del mutagen_file[frame.HashKey]


class MP3ListDescStorageStyle(MP3DescStorageStyle, ListStorageStyle):
def __init__(self, desc=u'', key='TXXX', split_v23=False, **kwargs):
self.split_v23 = split_v23
super(MP3ListDescStorageStyle, self).__init__(
desc=desc, key=key, **kwargs
)

def fetch(self, mutagen_file):
for frame in mutagen_file.tags.getall(self.key):
if frame.desc.lower() == self.description.lower():
if mutagen_file.tags.version == (2, 3, 0) and self.split_v23:
return sum([el.split('/') for el in frame.text], start=[])
else:
return frame.text
return []

def store(self, mutagen_file, values):
self.delete(mutagen_file)
frame = mutagen.id3.Frames[self.key](
desc=self.description,
text=values,
encoding=mutagen.id3.Encoding.UTF8,
)
if self.id3_lang:
frame.lang = self.id3_lang
mutagen_file.tags.add(frame)


class MP3SlashPackStorageStyle(MP3StorageStyle):
"""Store value as part of pair that is serialized as a slash-
separated string.
Expand Down Expand Up @@ -1643,6 +1678,12 @@ def update(self, dict):
StorageStyle('ARTIST'),
ASFStorageStyle('Author'),
)
artists = ListMediaField(
MP3ListDescStorageStyle(desc=u'ARTISTS'),
MP4ListStorageStyle('----:com.apple.iTunes:ARTISTS'),
ListStorageStyle('ARTISTS'),
ASFStorageStyle('WM/ARTISTS'),
)
album = MediaField(
MP3StorageStyle('TALB'),
MP4StorageStyle('\xa9alb'),
Expand Down Expand Up @@ -1722,8 +1763,15 @@ def update(self, dict):
ASFStorageStyle('TotalDiscs'),
out_type=int,
)

url = MediaField(
MP3DescStorageStyle(key='WXXX', attr='url', multispec=False),
MP4StorageStyle('\xa9url'),
StorageStyle('URL'),
ASFStorageStyle('WM/URL'),
)
lyrics = MediaField(
MP3DescStorageStyle(key='USLT'),
MP3DescStorageStyle(key='USLT', multispec=False),
MP4StorageStyle('\xa9lyr'),
StorageStyle('LYRICS'),
ASFStorageStyle('WM/Lyrics'),
Expand All @@ -1736,6 +1784,12 @@ def update(self, dict):
ASFStorageStyle('WM/Comments'),
ASFStorageStyle('Description')
)
copyright = MediaField(
MP3StorageStyle('TCOP'),
MP4StorageStyle('cprt'),
StorageStyle('COPYRIGHT'),
ASFStorageStyle('Copyright'),
)
bpm = MediaField(
MP3StorageStyle('TBPM'),
MP4StorageStyle('tmpo', as_type=int),
Expand All @@ -1757,6 +1811,12 @@ def update(self, dict):
StorageStyle('ALBUMARTIST'),
ASFStorageStyle('WM/AlbumArtist'),
)
albumartists = ListMediaField(
MP3ListDescStorageStyle(desc=u'ALBUMARTISTS'),
MP4ListStorageStyle('----:com.apple.iTunes:ALBUMARTISTS'),
ListStorageStyle('ALBUMARTISTS'),
ASFStorageStyle('WM/AlbumArtists'),
)
albumtype = MediaField(
MP3DescStorageStyle(u'MusicBrainz Album Type'),
MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Type'),
Expand Down Expand Up @@ -1801,8 +1861,18 @@ def update(self, dict):
MP3DescStorageStyle(u'BARCODE'),
MP4StorageStyle('----:com.apple.iTunes:BARCODE'),
StorageStyle('BARCODE'),
StorageStyle('UPC', read_only=True),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effect I wanted to achieve was reading data from this 3 tags and saving only BARCODE tag.
Is it a way to do this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting! Can you explain a little more about why you want that behavior? (I think this is the way to achieve it, but some testing would be useful, probably.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UPC, UPN and EAN are some specific tags for barcodes which are not supported on most of formats and by most of players.
It would be cool to fill main tag with data from those.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it... but I guess what I was asking about was a little more detail on which software does support those fields. It seems somewhat harmless to write fields that are unpopular, if that has the effect of increasing compatibility. (That's the usual policy we use in this library.)

Copy link
Contributor Author

@JuniorJPDJ JuniorJPDJ May 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem is that you can't easly guess if it's UPC, UPN or EAN, all of them are barcodes but those are different standards for those.
UPC and UPN are the same, but EAN is other standard, all of them are BARCODEs tho and IMO should fill main BARCODE tag.

I'm not sure which players does support them but those were specified in this index as supported in some standards: https://wiki.hydrogenaud.io/index.php?title=Tag_Mapping

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StorageStyle('EAN/UPN', read_only=True),
StorageStyle('EAN', read_only=True),
StorageStyle('UPN', read_only=True),
ASFStorageStyle('WM/Barcode'),
)
isrc = MediaField(
MP3StorageStyle(u'TSRC'),
MP4StorageStyle('----:com.apple.iTunes:ISRC'),
StorageStyle('ISRC'),
ASFStorageStyle('WM/ISRC'),
)
disctitle = MediaField(
MP3StorageStyle('TSST'),
MP4StorageStyle('----:com.apple.iTunes:DISCSUBTITLE'),
Expand Down
22 changes: 19 additions & 3 deletions test/test_mediafile.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin,
'disctotal',
'lyrics',
'comments',
'copyright',
'bpm',
'comp',
'mb_trackid',
Expand All @@ -390,6 +391,7 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin,
'asin',
'catalognum',
'barcode',
'isrc',
'disctitle',
'script',
'language',
Expand Down Expand Up @@ -449,7 +451,11 @@ def test_read_full(self):
def test_read_empty(self):
mediafile = self._mediafile_fixture('empty')
for field in self.tag_fields:
self.assertIsNone(getattr(mediafile, field))
value = getattr(mediafile, field)
if isinstance(value, list):
assert not value
else:
self.assertIsNone(value)

def test_write_empty(self):
mediafile = self._mediafile_fixture('empty')
Expand Down Expand Up @@ -620,7 +626,11 @@ def test_delete_tag(self):
mediafile = MediaFile(mediafile.path)

for key in keys:
self.assertIsNone(getattr(mediafile, key))
value = getattr(mediafile, key)
if isinstance(value, list):
assert not value
else:
self.assertIsNone(value)

def test_delete_packed_total(self):
mediafile = self._mediafile_fixture('full')
Expand Down Expand Up @@ -705,9 +715,14 @@ def _generate_tags(self, base=None):
for key in ['disc', 'disctotal', 'track', 'tracktotal', 'bpm']:
tags[key] = 1

for key in ['artists', 'albumartists']:
tags[key] = ['multival', 'test']

tags['art'] = self.jpg_data
tags['comp'] = True

tags['url'] = "https://example.com/"

date = datetime.date(2001, 4, 3)
tags['date'] = date
tags['year'] = date.year
Expand Down Expand Up @@ -976,7 +991,8 @@ def test_properties_from_readable_fields(self):

def test_known_fields(self):
fields = list(ReadWriteTestBase.tag_fields)
fields.extend(('encoder', 'images', 'genres', 'albumtype'))
fields.extend(('encoder', 'images', 'genres', 'albumtype', 'artists',
'albumartists', 'url'))
assertCountEqual(self, MediaFile.fields(), fields)

def test_fields_in_readable_fields(self):
Expand Down