Skip to content
Open
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
89 changes: 83 additions & 6 deletions orpheus/tagging.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from dataclasses import asdict

from PIL import Image
from mutagen.aiff import AIFF as _AIFFBase
from mutagen.easyid3 import EasyID3
from mutagen.easymp4 import EasyMP4
from mutagen.flac import FLAC, Picture
Expand All @@ -20,6 +21,43 @@
MP4Tags._padding = 0


class EasyAIFF(_AIFFBase):
"""AIFF with EasyID3-style dictionary access.

Wraps the _IFFID3 tag storage with an EasyID3 key-mapping layer so that
the same easy key names used for EasyMP3 (title, album, artist, …) work
transparently while saving via the native AIFF/IFF chunk mechanism.
"""

def _init_easy(self):
if self.tags is not None:
easy = EasyID3.__new__(EasyID3)
easy._EasyID3__id3 = self.tags
self._easy = easy
else:
self._easy = None

def __init__(self, filename=None, **kwargs):
super().__init__(filename, **kwargs)
self._init_easy()

def add_tags(self):
super().add_tags()
self._init_easy()

def __setitem__(self, key, value):
self._easy[key] = value

def __getitem__(self, key):
return self._easy[key]

def __delitem__(self, key):
del self._easy[key]

def __contains__(self, key):
return self._easy is not None and key in self._easy


def tag_file(file_path: str, image_path: str, track_info: TrackInfo, credits_list: list, embedded_lyrics: str, container: ContainerEnum):
if container == ContainerEnum.flac:
tagger = FLAC(file_path)
Expand All @@ -42,6 +80,21 @@ def tag_file(file_path: str, image_path: str, track_info: TrackInfo, credits_lis
tagger.tags.RegisterTXXXKey('upc', 'BARCODE')

tagger.tags.pop('encoded', None)
elif container == ContainerEnum.aiff:
tagger = EasyAIFF(file_path)

if tagger.tags is None:
tagger.add_tags()

# Register same custom keys as MP3 (class-level, affects all EasyID3 instances)
EasyID3.RegisterTextKey('encoded', 'TSSE')
EasyID3.RegisterTXXXKey('compatible_brands', 'compatible_brands')
EasyID3.RegisterTXXXKey('major_brand', 'major_brand')
EasyID3.RegisterTXXXKey('minor_version', 'minor_version')
EasyID3.RegisterTXXXKey('Rating', 'Rating')
EasyID3.RegisterTXXXKey('upc', 'BARCODE')

tagger._easy.pop('encoded', None)
elif container == ContainerEnum.m4a:
tagger = EasyMP4(file_path)

Expand Down Expand Up @@ -71,7 +124,7 @@ def tag_file(file_path: str, image_path: str, track_info: TrackInfo, credits_lis

tagger['artist'] = track_info.artists

if container == ContainerEnum.m4a or container == ContainerEnum.mp3:
if container in {ContainerEnum.m4a, ContainerEnum.mp3, ContainerEnum.aiff}:
if track_info.tags.track_number and track_info.tags.total_tracks:
tagger['tracknumber'] = str(track_info.tags.track_number) + '/' + str(track_info.tags.total_tracks)
elif track_info.tags.track_number:
Expand All @@ -87,12 +140,13 @@ def tag_file(file_path: str, image_path: str, track_info: TrackInfo, credits_lis
if track_info.tags.total_discs: tagger['totaldiscs'] = str(track_info.tags.total_discs)

if track_info.tags.release_date:
if container == ContainerEnum.mp3:
# Never access protected attributes, too bad! Only works on ID3v2.4, disabled for now!
# tagger.tags._EasyID3__id3._DictProxy__dict['TDRL'] = TDRL(encoding=3, text=track_info.tags.release_date)
if container in {ContainerEnum.mp3, ContainerEnum.aiff}:
# Use YYYY-MM-DD for consistency and convert it to DDMM
release_dd_mm = f'{track_info.tags.release_date[8:10]}{track_info.tags.release_date[5:7]}'
tagger.tags._EasyID3__id3._DictProxy__dict['TDAT'] = TDAT(encoding=3, text=release_dd_mm)
if container == ContainerEnum.mp3:
tagger.tags._EasyID3__id3._DictProxy__dict['TDAT'] = TDAT(encoding=3, text=release_dd_mm)
else:
tagger.tags['TDAT'] = TDAT(encoding=3, text=release_dd_mm)
# Now add the year tag
tagger['date'] = str(track_info.release_year)
else:
Expand Down Expand Up @@ -123,6 +177,8 @@ def tag_file(file_path: str, image_path: str, track_info: TrackInfo, credits_lis
encoding=3,
text=track_info.tags.label
)
elif container == ContainerEnum.aiff:
tagger.tags['TPUB'] = TPUB(encoding=3, text=track_info.tags.label)
elif container == ContainerEnum.m4a:
# only works with MP3TAG? https://docs.mp3tag.de/mapping/
tagger.RegisterTextKey('label', '\xa9pub')
Expand All @@ -145,6 +201,8 @@ def tag_file(file_path: str, image_path: str, track_info: TrackInfo, credits_lis
desc=u'',
text=track_info.tags.description
)
elif container == ContainerEnum.aiff:
tagger.tags['COMM'] = COMM(encoding=3, lang=u'eng', desc=u'', text=track_info.tags.description)

# add all extra_kwargs key value pairs to the (FLAC, Vorbis) file
if container in {ContainerEnum.flac, ContainerEnum.ogg}:
Expand All @@ -168,6 +226,10 @@ def tag_file(file_path: str, image_path: str, track_info: TrackInfo, credits_lis
# Create a new user-defined text frame key
tagger.tags.RegisterTXXXKey(credit.type.upper(), credit.type)
tagger[credit.type] = credit.names
elif container == ContainerEnum.aiff:
for credit in credits_list:
EasyID3.RegisterTXXXKey(credit.type.upper(), credit.type)
tagger[credit.type] = credit.names
else:
for credit in credits_list:
try:
Expand All @@ -183,6 +245,8 @@ def tag_file(file_path: str, image_path: str, track_info: TrackInfo, credits_lis
lang=u'eng', # don't assume?
text=embedded_lyrics
)
elif container == ContainerEnum.aiff:
tagger.tags['USLT'] = USLT(encoding=3, lang=u'eng', text=embedded_lyrics)
else:
tagger['lyrics'] = embedded_lyrics

Expand Down Expand Up @@ -214,6 +278,14 @@ def tag_file(file_path: str, image_path: str, track_info: TrackInfo, credits_lis
desc='Cover', # name
data=data
)
elif container == ContainerEnum.aiff:
tagger.tags['APIC'] = APIC(
encoding=3,
mime='image/jpeg',
type=3,
desc='Cover',
data=data
)
# If you want to have a cover in only a few applications, then this technically works for Opus
elif container in {ContainerEnum.ogg, ContainerEnum.opus}:
im = Image.open(image_path)
Expand All @@ -231,7 +303,12 @@ def tag_file(file_path: str, image_path: str, track_info: TrackInfo, credits_lis
f'will not have cover saved.')

try:
tagger.save(file_path, v1=2, v2_version=3, v23_sep=None) if container == ContainerEnum.mp3 else tagger.save()
if container == ContainerEnum.mp3:
tagger.save(file_path, v1=2, v2_version=3, v23_sep=None)
elif container == ContainerEnum.aiff:
tagger.save(file_path)
else:
tagger.save()
except:
logging.debug('Tagging failed.')
tag_text = '\n'.join((f'{k}: {v}' for k, v in asdict(track_info.tags).items() if v and k != 'credits' and k != 'lyrics'))
Expand Down
3 changes: 3 additions & 0 deletions utils/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class CodecEnum(Flag):
FLAC = auto() # Lossless, free
ALAC = auto() # Lossless, free, useless
WAV = auto() # Lossless (uncompressed), free, useless
AIFF = auto() # Lossless, free, uncompressed
MQA = auto() # Lossy, proprietary, terrible
OPUS = auto() # Lossy, free
VORBIS = auto() # Lossy, free
Expand All @@ -53,6 +54,7 @@ class CodecEnum(Flag):
class ContainerEnum(Flag):
flac = auto()
wav = auto()
aiff = auto()
opus = auto()
ogg = auto()
m4a = auto()
Expand Down Expand Up @@ -84,6 +86,7 @@ class CodecData:
CodecEnum.FLAC: CodecData(pretty_name='FLAC', container=ContainerEnum.flac, lossless=True, spatial=False, proprietary=False),
CodecEnum.ALAC: CodecData(pretty_name='ALAC', container=ContainerEnum.m4a, lossless=True, spatial=False, proprietary=False),
CodecEnum.WAV: CodecData(pretty_name='WAVE', container=ContainerEnum.wav, lossless=True, spatial=False, proprietary=False),
CodecEnum.AIFF: CodecData(pretty_name='AIFF', container=ContainerEnum.aiff, lossless=True, spatial=False, proprietary=False),
CodecEnum.MQA: CodecData(pretty_name='MQA', container=ContainerEnum.flac, lossless=False, spatial=False, proprietary=True),
CodecEnum.OPUS: CodecData(pretty_name='Opus', container=ContainerEnum.opus, lossless=False, spatial=False, proprietary=False),
CodecEnum.VORBIS: CodecData(pretty_name='Vorbis', container=ContainerEnum.ogg, lossless=False, spatial=False, proprietary=False),
Expand Down