Fix Opus cover art and quality: map audio only, embed art via Mutagen
Opus ffmpeg command now uses -map 0:a to extract only the audio stream, matching the user's working command and preventing picture stream interference with the encoder. After conversion, cover art is embedded from the source FLAC using Mutagen: Opus gets METADATA_BLOCK_PICTURE (base64-encoded per OGG spec), AAC gets MP4Cover. Applied to both the post-download lossy copy and the repair job fix handler.
This commit is contained in:
parent
b0b1c22f80
commit
aca8a8e996
2 changed files with 74 additions and 2 deletions
|
|
@ -1928,7 +1928,7 @@ class RepairWorker:
|
||||||
|
|
||||||
codec_configs = {
|
codec_configs = {
|
||||||
'mp3': ('libmp3lame', '.mp3', ['-id3v2_version', '3']),
|
'mp3': ('libmp3lame', '.mp3', ['-id3v2_version', '3']),
|
||||||
'opus': ('libopus', '.opus', ['-vbr', 'on']),
|
'opus': ('libopus', '.opus', ['-map', '0:a', '-vbr', 'on']),
|
||||||
'aac': ('aac', '.m4a', ['-movflags', '+faststart']),
|
'aac': ('aac', '.m4a', ['-movflags', '+faststart']),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1997,6 +1997,39 @@ class RepairWorker:
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Embed cover art from source FLAC
|
||||||
|
if codec in ('opus', 'aac'):
|
||||||
|
try:
|
||||||
|
from mutagen import File as MutagenFile
|
||||||
|
from mutagen.flac import FLAC as MutagenFLAC
|
||||||
|
source_audio = MutagenFLAC(resolved)
|
||||||
|
if source_audio and source_audio.pictures:
|
||||||
|
pic = source_audio.pictures[0]
|
||||||
|
dest_audio = MutagenFile(out_path)
|
||||||
|
if dest_audio is not None:
|
||||||
|
if codec == 'opus':
|
||||||
|
import base64, struct
|
||||||
|
from mutagen.oggopus import OggOpus
|
||||||
|
if isinstance(dest_audio, OggOpus):
|
||||||
|
picture_data = (
|
||||||
|
struct.pack('>II', pic.type, len(pic.mime.encode('utf-8')))
|
||||||
|
+ pic.mime.encode('utf-8')
|
||||||
|
+ struct.pack('>I', len(pic.desc.encode('utf-8')))
|
||||||
|
+ pic.desc.encode('utf-8')
|
||||||
|
+ struct.pack('>IIII', pic.width, pic.height, pic.depth, pic.colors)
|
||||||
|
+ struct.pack('>I', len(pic.data))
|
||||||
|
+ pic.data
|
||||||
|
)
|
||||||
|
dest_audio['METADATA_BLOCK_PICTURE'] = [base64.b64encode(picture_data).decode('ascii')]
|
||||||
|
dest_audio.save()
|
||||||
|
elif codec == 'aac':
|
||||||
|
from mutagen.mp4 import MP4Cover
|
||||||
|
fmt = MP4Cover.FORMAT_JPEG if 'jpeg' in pic.mime else MP4Cover.FORMAT_PNG
|
||||||
|
dest_audio['covr'] = [MP4Cover(pic.data, imageformat=fmt)]
|
||||||
|
dest_audio.save()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Blasphemy Mode — uses the job's own setting, not the global lossy_copy one
|
# Blasphemy Mode — uses the job's own setting, not the global lossy_copy one
|
||||||
delete_original = False
|
delete_original = False
|
||||||
if self._config_manager:
|
if self._config_manager:
|
||||||
|
|
|
||||||
|
|
@ -14749,7 +14749,7 @@ def _create_lossy_copy(final_path):
|
||||||
# Codec configuration: (ffmpeg_codec, extension, quality_label, extra_args)
|
# Codec configuration: (ffmpeg_codec, extension, quality_label, extra_args)
|
||||||
codec_map = {
|
codec_map = {
|
||||||
'mp3': ('libmp3lame', '.mp3', f'MP3-{bitrate}', ['-id3v2_version', '3']),
|
'mp3': ('libmp3lame', '.mp3', f'MP3-{bitrate}', ['-id3v2_version', '3']),
|
||||||
'opus': ('libopus', '.opus', f'OPUS-{bitrate}', ['-vbr', 'on']),
|
'opus': ('libopus', '.opus', f'OPUS-{bitrate}', ['-map', '0:a', '-vbr', 'on']),
|
||||||
'aac': ('aac', '.m4a', f'AAC-{bitrate}', ['-movflags', '+faststart']),
|
'aac': ('aac', '.m4a', f'AAC-{bitrate}', ['-movflags', '+faststart']),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -14808,6 +14808,45 @@ def _create_lossy_copy(final_path):
|
||||||
except Exception as tag_err:
|
except Exception as tag_err:
|
||||||
print(f"⚠️ [Lossy Copy] Could not update QUALITY tag: {tag_err}")
|
print(f"⚠️ [Lossy Copy] Could not update QUALITY tag: {tag_err}")
|
||||||
|
|
||||||
|
# Embed cover art from source FLAC into the lossy copy
|
||||||
|
# Opus/OGG can't inherit FLAC cover art via ffmpeg -map_metadata alone
|
||||||
|
if codec in ('opus', 'aac'):
|
||||||
|
try:
|
||||||
|
from mutagen import File as MutagenFile
|
||||||
|
from mutagen.flac import FLAC as MutagenFLAC
|
||||||
|
source_audio = MutagenFLAC(final_path)
|
||||||
|
if source_audio and source_audio.pictures:
|
||||||
|
pic = source_audio.pictures[0]
|
||||||
|
dest_audio = MutagenFile(out_path)
|
||||||
|
if dest_audio is not None:
|
||||||
|
if codec == 'opus':
|
||||||
|
import base64
|
||||||
|
from mutagen.oggopus import OggOpus
|
||||||
|
if isinstance(dest_audio, OggOpus):
|
||||||
|
# OGG stores pictures as base64-encoded METADATA_BLOCK_PICTURE
|
||||||
|
import struct
|
||||||
|
# Build METADATA_BLOCK_PICTURE block
|
||||||
|
picture_data = (
|
||||||
|
struct.pack('>II', pic.type, len(pic.mime.encode('utf-8')))
|
||||||
|
+ pic.mime.encode('utf-8')
|
||||||
|
+ struct.pack('>I', len(pic.desc.encode('utf-8')))
|
||||||
|
+ pic.desc.encode('utf-8')
|
||||||
|
+ struct.pack('>IIII', pic.width, pic.height, pic.depth, pic.colors)
|
||||||
|
+ struct.pack('>I', len(pic.data))
|
||||||
|
+ pic.data
|
||||||
|
)
|
||||||
|
dest_audio['METADATA_BLOCK_PICTURE'] = [base64.b64encode(picture_data).decode('ascii')]
|
||||||
|
dest_audio.save()
|
||||||
|
print(f"🎨 [Lossy Copy] Embedded cover art in Opus file")
|
||||||
|
elif codec == 'aac':
|
||||||
|
from mutagen.mp4 import MP4Cover
|
||||||
|
fmt = MP4Cover.FORMAT_JPEG if 'jpeg' in pic.mime else MP4Cover.FORMAT_PNG
|
||||||
|
dest_audio['covr'] = [MP4Cover(pic.data, imageformat=fmt)]
|
||||||
|
dest_audio.save()
|
||||||
|
print(f"🎨 [Lossy Copy] Embedded cover art in M4A file")
|
||||||
|
except Exception as art_err:
|
||||||
|
print(f"⚠️ [Lossy Copy] Could not embed cover art: {art_err}")
|
||||||
|
|
||||||
# Blasphemy Mode: delete original FLAC if enabled and output is verified
|
# Blasphemy Mode: delete original FLAC if enabled and output is verified
|
||||||
if config_manager.get('lossy_copy.delete_original', False):
|
if config_manager.get('lossy_copy.delete_original', False):
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue