From aca8a8e996f7f44f2edc695ed84c66f5f3e05f30 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sun, 22 Mar 2026 08:32:04 -0700 Subject: [PATCH] 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. --- core/repair_worker.py | 35 ++++++++++++++++++++++++++++++++++- web_server.py | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/core/repair_worker.py b/core/repair_worker.py index 34cc9038..28d7bdc7 100644 --- a/core/repair_worker.py +++ b/core/repair_worker.py @@ -1928,7 +1928,7 @@ class RepairWorker: codec_configs = { 'mp3': ('libmp3lame', '.mp3', ['-id3v2_version', '3']), - 'opus': ('libopus', '.opus', ['-vbr', 'on']), + 'opus': ('libopus', '.opus', ['-map', '0:a', '-vbr', 'on']), 'aac': ('aac', '.m4a', ['-movflags', '+faststart']), } @@ -1997,6 +1997,39 @@ class RepairWorker: except Exception: 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 delete_original = False if self._config_manager: diff --git a/web_server.py b/web_server.py index 98743d78..c8f49ec2 100644 --- a/web_server.py +++ b/web_server.py @@ -14749,7 +14749,7 @@ def _create_lossy_copy(final_path): # Codec configuration: (ffmpeg_codec, extension, quality_label, extra_args) codec_map = { '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']), } @@ -14808,6 +14808,45 @@ def _create_lossy_copy(final_path): except Exception as 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 if config_manager.get('lossy_copy.delete_original', False): try: