Move mutagen PP to ffmpeg PP (WIP)

This commit is contained in:
7x11x13 2024-12-15 17:03:43 -05:00
parent a8ba5954ab
commit bac8ee69a7
4 changed files with 155 additions and 211 deletions

View file

@ -690,10 +690,7 @@ def get_postprocessors(opts):
'add_chapters': opts.addchapters, 'add_chapters': opts.addchapters,
'add_metadata': opts.addmetadata, 'add_metadata': opts.addmetadata,
'add_infojson': opts.embed_infojson, 'add_infojson': opts.embed_infojson,
} 'prefer_mutagen': opts.prefer_mutagen,
if opts.prefer_mutagen:
yield {
'key': 'Mutagen',
} }
# Deprecated # Deprecated
# This should be above EmbedThumbnail since sponskrub removes the thumbnail attachment # This should be above EmbedThumbnail since sponskrub removes the thumbnail attachment

View file

@ -30,7 +30,6 @@ from .metadataparser import (
) )
from .modify_chapters import ModifyChaptersPP from .modify_chapters import ModifyChaptersPP
from .movefilesafterdownload import MoveFilesAfterDownloadPP from .movefilesafterdownload import MoveFilesAfterDownloadPP
from .mutagen import MutagenPP
from .sponskrub import SponSkrubPP from .sponskrub import SponSkrubPP
from .sponsorblock import SponsorBlockPP from .sponsorblock import SponsorBlockPP
from .xattrpp import XAttrMetadataPP from .xattrpp import XAttrMetadataPP

View file

@ -32,6 +32,26 @@ from ..utils import (
variadic, variadic,
write_json_file, write_json_file,
) )
from ..dependencies import mutagen
if mutagen:
import mutagen
from mutagen import (
FileType,
aiff,
dsdiff,
dsf,
flac,
id3,
mp3,
mp4,
oggopus,
oggspeex,
oggtheora,
oggvorbis,
trueaudio,
wave,
)
EXT_TO_OUT_FORMATS = { EXT_TO_OUT_FORMATS = {
'aac': 'adts', 'aac': 'adts',
@ -668,11 +688,56 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
class FFmpegMetadataPP(FFmpegPostProcessor): class FFmpegMetadataPP(FFmpegPostProcessor):
def __init__(self, downloader, add_metadata=True, add_chapters=True, add_infojson='if_exists'): _MUTAGEN_SUPPORTED_EXTS = ('alac', 'aiff', 'flac', 'mp3', 'm4a', 'ogg', 'opus', 'vorbis', 'wav')
_VORBIS_METADATA = {
'title': 'title',
'artist': 'artist',
'genre': 'genre',
'date': 'date',
'album': 'album',
'albumartist': 'album_artist',
'description': 'description',
'comment': 'comment',
'composer': 'composer',
'tracknumber': 'track',
'WWWAUDIOFILE': 'purl', # https://getmusicbee.com/forum/index.php?topic=39759.0
}
_ID3_METADATA = {
'TIT2': 'title',
'TPE1': 'artist',
'COMM': 'description',
'TCON': 'genre',
'WFED': 'purl',
'WOAF': 'purl',
'TDAT': 'date',
'TALB': 'album',
'TPE2': 'album_artist',
'TRCK': 'track',
'TCOM': 'composer',
'TPOS': 'disc',
}
_MP4_METADATA = {
'\251ART': 'artist',
'\251nam': 'title',
'\251gen': 'genre',
'\251day': 'date',
'\251alb': 'album',
'aART': 'album_artist',
'\251cmt': 'description',
'\251wrt': 'composer',
'disk': 'disc',
'tvsh': 'show',
'tvsn': 'season_number',
'egid': 'episode_id',
'tven': 'episode_sort',
}
def __init__(self, downloader, add_metadata=True, add_chapters=True, add_infojson='if_exists', prefer_mutagen=False):
FFmpegPostProcessor.__init__(self, downloader) FFmpegPostProcessor.__init__(self, downloader)
self._add_metadata = add_metadata self._add_metadata = add_metadata
self._add_chapters = add_chapters self._add_chapters = add_chapters
self._add_infojson = add_infojson self._add_infojson = add_infojson
self._prefer_mutagen = prefer_mutagen
@staticmethod @staticmethod
def _options(target_ext): def _options(target_ext):
@ -681,8 +746,91 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
if audio_only: if audio_only:
yield from ('-vn', '-acodec', 'copy') yield from ('-vn', '-acodec', 'copy')
def _use_mutagen(self, info):
if not self._prefer_mutagen:
return False
if info['ext'] not in self._MUTAGEN_SUPPORTED_EXTS:
return False
if self._add_chapters and info.get('chapters'):
# mutagen can't handle adding chapters to M4A
return False
if not mutagen:
self.report_warning('module mutagen was not found. Please install using `python3 -m pip install mutagen`')
return False
return True
if mutagen:
@functools.singledispatchmethod
def _assemble_metadata(self, file: FileType, meta: dict) -> None:
raise FFmpegPostProcessorError(f'Filetype {file.__class__.__name__} is not currently supported')
@_assemble_metadata.register(oggvorbis.OggVorbis)
@_assemble_metadata.register(oggtheora.OggTheora)
@_assemble_metadata.register(oggspeex.OggSpeex)
@_assemble_metadata.register(oggopus.OggOpus)
@_assemble_metadata.register(flac.FLAC)
def _(self, file: oggopus.OggOpus, meta: dict) -> None:
for file_key, meta_key in self._VORBIS_METADATA.items():
if meta.get(meta_key):
file[file_key] = meta[meta_key]
@_assemble_metadata.register(trueaudio.TrueAudio)
@_assemble_metadata.register(dsf.DSF)
@_assemble_metadata.register(dsdiff.DSDIFF)
@_assemble_metadata.register(aiff.AIFF)
@_assemble_metadata.register(mp3.MP3)
@_assemble_metadata.register(wave.WAVE)
def _(self, file: wave.WAVE, meta: dict) -> None:
for file_key, meta_key in self._ID3_METADATA.items():
if meta.get(meta_key):
id3_class = getattr(id3, file_key)
if issubclass(id3_class, id3.UrlFrame):
file[file_key] = id3_class(url=meta[meta_key])
else:
file[file_key] = id3_class(encoding=id3.Encoding.UTF8, text=meta[meta_key])
@_assemble_metadata.register(mp4.MP4)
def _(self, file: mp4.MP4, meta: dict) -> None:
for file_key, meta_key in self._MP4_METADATA.items():
if meta.get(meta_key):
file[file_key] = meta[meta_key]
if meta.get('purl'):
# https://getmusicbee.com/forum/index.php?topic=39759.0
file['----:com.apple.iTunes:WWWAUDIOFILE'] = meta['purl'].encode()
file['purl'] = meta['purl'].encode()
if meta.get('track'):
file['trkn'] = [(meta['track'], 0)]
def _run_mutagen(self, info):
self.to_screen('Using mutagen to embed metadata')
filename = info['filepath']
metadata = self._get_metadata_dict(info)['common']
if not metadata:
self.to_screen('There isn\'t any metadata to add')
return [], info
self.to_screen(f'Adding metadata to "{filename}"')
try:
f = mutagen.File(filename)
self._assemble_metadata(f, metadata)
f.save()
except Exception as err:
raise FFmpegPostProcessorError(f'Unable to embed metadata; {err}')
return [], info
@PostProcessor._restrict_to(images=False) @PostProcessor._restrict_to(images=False)
def run(self, info): def run(self, info):
if self._use_mutagen(info):
try:
self._run_mutagen(info)
return [], info
except Exception as err:
self.report_warning(f'Unable to embed metadata using mutagen; {err}')
self._fixup_chapters(info) self._fixup_chapters(info)
filename, metadata_filename = info['filepath'], None filename, metadata_filename = info['filepath'], None
files_to_delete, options = [], [] files_to_delete, options = [], []
@ -732,7 +880,7 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
f.write(metadata_file_content) f.write(metadata_file_content)
yield ('-map_metadata', '1') yield ('-map_metadata', '1')
def _get_metadata_opts(self, info): def _get_metadata_dict(self, info):
meta_prefix = 'meta' meta_prefix = 'meta'
metadata = collections.defaultdict(dict) metadata = collections.defaultdict(dict)
@ -774,6 +922,10 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
mobj = re.fullmatch(meta_regex, key) mobj = re.fullmatch(meta_regex, key)
if value is not None and mobj: if value is not None and mobj:
metadata[mobj.group('i') or 'common'][mobj.group('key')] = value.replace('\0', '') metadata[mobj.group('i') or 'common'][mobj.group('key')] = value.replace('\0', '')
return metadata
def _get_metadata_opts(self, info):
metadata = self._get_metadata_dict(info)
# Write id3v1 metadata also since Windows Explorer can't handle id3v2 tags # Write id3v1 metadata also since Windows Explorer can't handle id3v2 tags
yield ('-write_id3v1', '1') yield ('-write_id3v1', '1')

View file

@ -1,204 +0,0 @@
from __future__ import annotations
import collections
from functools import singledispatchmethod
import re
from typing import TypedDict
from yt_dlp.utils._utils import PostProcessingError, variadic
from ..dependencies import mutagen
if mutagen:
import mutagen
from mutagen import (
FileType,
aiff,
dsdiff,
dsf,
flac,
id3,
mp3,
mp4,
oggopus,
oggspeex,
oggtheora,
oggvorbis,
trueaudio,
wave,
)
from yt_dlp.postprocessor.common import PostProcessor
class MutagenPPError(PostProcessingError):
pass
class MutagenPP(PostProcessor):
def __init__(self, downloader=None):
PostProcessor.__init__(self, downloader)
class MetadataInfo(TypedDict):
title: str | None
date: str | None
description: str | None
synopsis: str | None
purl: str | None
comment: str | None
track: str | None
artist: str | None
composer: str | None
genre: str | None
album: str | None
album_artist: str | None
disc: str | None
show: str | None
season_number: str | None
episode_id: str | None
episode_sort: str | None
if mutagen:
@singledispatchmethod
@staticmethod
def _assemble_metadata(file: FileType, meta: MetadataInfo) -> None:
raise MutagenPPError(f'Filetype {file.__class__.__name__} is not currently supported')
@staticmethod
def _set_metadata(file: FileType, meta: MetadataInfo, file_name: str, meta_name: str):
if meta[meta_name]:
file[file_name] = meta[meta_name]
@_assemble_metadata.register(oggvorbis.OggVorbis)
@_assemble_metadata.register(oggtheora.OggTheora)
@_assemble_metadata.register(oggspeex.OggSpeex)
@_assemble_metadata.register(oggopus.OggOpus)
@_assemble_metadata.register(flac.FLAC)
@staticmethod
def _(file: oggopus.OggOpus, meta: MetadataInfo) -> None:
MutagenPP._set_metadata(file, meta, 'artist', 'artist')
MutagenPP._set_metadata(file, meta, 'title', 'title')
MutagenPP._set_metadata(file, meta, 'genre', 'genre')
MutagenPP._set_metadata(file, meta, 'date', 'date')
MutagenPP._set_metadata(file, meta, 'album', 'album')
MutagenPP._set_metadata(file, meta, 'albumartist', 'album_artist')
MutagenPP._set_metadata(file, meta, 'description', 'description')
MutagenPP._set_metadata(file, meta, 'comment', 'comment')
MutagenPP._set_metadata(file, meta, 'composer', 'composer')
MutagenPP._set_metadata(file, meta, 'tracknumber', 'track')
# https://getmusicbee.com/forum/index.php?topic=39759.0
MutagenPP._set_metadata(file, meta, 'WWWAUDIOFILE', 'purl')
@_assemble_metadata.register(trueaudio.TrueAudio)
@_assemble_metadata.register(dsf.DSF)
@_assemble_metadata.register(dsdiff.DSDIFF)
@_assemble_metadata.register(aiff.AIFF)
@_assemble_metadata.register(mp3.MP3)
@_assemble_metadata.register(wave.WAVE)
@staticmethod
def _(file: wave.WAVE, meta: MetadataInfo) -> None:
def _set_metadata(file_name: str, meta_name: str):
if meta[meta_name]:
id3_class = getattr(id3, file_name)
file[file_name] = id3_class(encoding=id3.Encoding.UTF8, text=meta[meta_name])
_set_metadata('TIT2', 'title')
_set_metadata('TPE1', 'artist')
_set_metadata('COMM', 'description')
_set_metadata('TCON', 'genre')
_set_metadata('WFED', 'purl')
_set_metadata('WOAF', 'purl')
_set_metadata('TDAT', 'date')
_set_metadata('TALB', 'album')
_set_metadata('TPE2', 'album_artist')
_set_metadata('TRCK', 'track')
_set_metadata('TCOM', 'composer')
_set_metadata('TPOS', 'disc')
@_assemble_metadata.register(mp4.MP4)
@staticmethod
def _(file: mp4.MP4, meta: MetadataInfo) -> None:
MutagenPP._set_metadata(file, meta, '\251ART', 'artist')
MutagenPP._set_metadata(file, meta, '\251nam', 'title')
MutagenPP._set_metadata(file, meta, '\251gen', 'genre')
MutagenPP._set_metadata(file, meta, '\251day', 'date')
MutagenPP._set_metadata(file, meta, '\251alb', 'album')
MutagenPP._set_metadata(file, meta, 'aART', 'album_artist')
MutagenPP._set_metadata(file, meta, '\251cmt', 'description')
MutagenPP._set_metadata(file, meta, '\251wrt', 'composer')
MutagenPP._set_metadata(file, meta, 'disk', 'disc')
MutagenPP._set_metadata(file, meta, 'tvsh', 'show')
MutagenPP._set_metadata(file, meta, 'tvsn', 'season_number')
MutagenPP._set_metadata(file, meta, 'egid', 'episode_id')
MutagenPP._set_metadata(file, meta, 'tven', 'episode_sort')
if meta['purl']:
# https://getmusicbee.com/forum/index.php?topic=39759.0
file['----:com.apple.iTunes:WWWAUDIOFILE'] = meta['purl'].encode()
file['purl'] = meta['purl'].encode()
if meta['track']:
file['trkn'] = [(meta['track'], 0)]
def _get_metadata_from_info(self, info) -> MetadataInfo:
meta_prefix = 'meta'
metadata: dict[str, self.MetadataInfo] = collections.defaultdict(
lambda: collections.defaultdict(lambda: None),
)
def add(meta_list, info_list=None):
value = next((
info[key] for key in [f'{meta_prefix}_', *variadic(info_list or meta_list)]
if info.get(key) is not None), None)
if value not in ('', None):
value = ', '.join(map(str, variadic(value)))
value = value.replace('\0', '') # nul character cannot be passed in command line
metadata['common'].update({meta_f: value for meta_f in variadic(meta_list)})
add('title', ('track', 'title'))
add('date', 'upload_date')
add(('description', 'synopsis'), 'description')
add(('purl', 'comment'), 'webpage_url')
add('track', 'track_number')
add('artist', ('artist', 'artists', 'creator', 'creators', 'uploader', 'uploader_id'))
add('composer', ('composer', 'composers'))
add('genre', ('genre', 'genres'))
add('album')
add('album_artist', ('album_artist', 'album_artists'))
add('disc', 'disc_number')
add('show', 'series')
add('season_number')
add('episode_id', ('episode', 'episode_id'))
add('episode_sort', 'episode_number')
if 'embed-metadata' in self.get_param('compat_opts', []):
add('comment', 'description')
metadata['common'].pop('synopsis', None)
meta_regex = rf'{re.escape(meta_prefix)}(?P<i>\d+)?_(?P<key>.+)'
for key, value in info.items():
mobj = re.fullmatch(meta_regex, key)
if value is not None and mobj:
metadata[mobj.group('i') or 'common'][mobj.group('key')] = value.replace('\0', '')
return metadata['common']
@PostProcessor._restrict_to(video=False, images=False)
def run(self, info):
if not mutagen:
raise MutagenPPError('module mutagen was not found. Please install using `python3 -m pip install mutagen`')
filename = info['filepath']
metadata = self._get_metadata_from_info(info)
if not metadata:
self.to_screen('There isn\'t any metadata to add')
return [], info
self.to_screen(f'Adding metadata to "{filename}"')
try:
f = mutagen.File(filename)
metadata = self._get_metadata_from_info(info)
self._assemble_metadata(f, metadata)
f.save()
except Exception as err:
raise MutagenPPError(f'Unable to embed metadata; {err}')
return [], info