commit 0e95664d966b60a45f618f4ed5baa13a50362792
parent a4f200402729dcaf8df05ed912f36a70cc4f15f0
Author: mtmn <miro@haravara.org>
Date: Tue, 7 Jul 2026 18:36:21 +0200
o2aif: fix missing metadata on aiff output files
Diffstat:
1 file changed, 64 insertions(+), 25 deletions(-)
diff --git a/modules/mixins/dotfiles/bin/o2aif b/modules/mixins/dotfiles/bin/o2aif
@@ -20,7 +20,7 @@ class ConversionStats:
def check_artwork(file_path):
- """Return True if the file contains an embedded artwork (video) stream."""
+ """Return True if the file contains a video/artwork stream."""
result = subprocess.run(
[
"ffprobe",
@@ -29,7 +29,7 @@ def check_artwork(file_path):
"-select_streams",
"v",
"-show_entries",
- "stream=codec_type",
+ "stream=codec_type,stream_disposition=attached_pic",
"-of",
"default=noprint_wrappers=1:nokey=1",
str(file_path),
@@ -37,7 +37,24 @@ def check_artwork(file_path):
capture_output=True,
text=True,
)
- return "video" in result.stdout
+ output = result.stdout
+ return "video" in output or "attached_pic=1" in output
+
+
+COVER_NAMES = ("cover", "folder", "front", "albumart", "album")
+COVER_EXTS = (".jpg", ".jpeg", ".png", ".bmp", ".gif", ".webp")
+
+
+def find_external_cover(input_path):
+ """Return the path to a cover image next to the input, or None."""
+ parent = input_path.parent
+ for name in COVER_NAMES:
+ for ext in COVER_EXTS:
+ for variant in (name + ext, name.title() + ext, name.upper() + ext):
+ candidate = parent / variant
+ if candidate.is_file():
+ return candidate
+ return None
def convert_opus_to_aiff(input_path, delete_original=True):
@@ -55,53 +72,75 @@ def convert_opus_to_aiff(input_path, delete_original=True):
print(f"Converting {input_path} to {output_path}")
+ had_embedded_artwork = check_artwork(input_path)
+ external_cover = None if had_embedded_artwork else find_external_cover(input_path)
+
try:
- result = subprocess.run(
+ ffmpeg_cmd = [
+ "ffmpeg",
+ "-i",
+ str(input_path),
+ ]
+
+ if external_cover:
+ ffmpeg_cmd.extend(["-i", str(external_cover)])
+
+ ffmpeg_cmd.extend(
[
- "ffmpeg",
- "-i",
- str(input_path),
"-c:a",
"pcm_s16be", # 16-bit PCM (AIFF default audio codec)
"-map",
"0:a", # Map audio stream
- "-map",
- "0:v?", # Map video (cover art) stream if it exists
"-map_metadata",
- "0", # Copy all metadata
+ "0:s:0", # Copy Vorbis-comment tags to global AIFF ID3 metadata
"-write_id3v2",
"1", # Write ID3v2 chunk (required: AIFF muxer defaults to off)
"-id3v2_version",
"3", # ID3v2.3 for broad compatibility
- "-f",
- "aiff",
- str(output_path),
- ],
- check=True,
- capture_output=True,
+ ]
)
- # Surface any non-fatal ffmpeg warnings (e.g. the optional 0:v? map
- # producing a "no video stream" notice).
+ if had_embedded_artwork:
+ ffmpeg_cmd.extend(["-map", "0:v?", "-c:v", "copy"])
+ elif external_cover:
+ ffmpeg_cmd.extend(["-map", "1:v", "-c:v", "copy"])
+
+ ffmpeg_cmd.extend(["-f", "aiff", str(output_path)])
+
+ result = subprocess.run(ffmpeg_cmd, check=True, capture_output=True)
+
+ # Surface any non-fatal ffmpeg warnings.
if result.stderr:
for line in result.stderr.decode(errors="replace").splitlines():
if line.strip().lower().startswith("warning"):
print(f" ffmpeg warning: {line.strip()}")
- had_artwork = check_artwork(input_path)
- if had_artwork:
- if check_artwork(output_path):
- print(f"Successfully converted {input_path} (artwork preserved)")
+ has_output_artwork = check_artwork(output_path)
+ if has_output_artwork:
+ if had_embedded_artwork:
+ print(
+ f"Successfully converted {input_path} (embedded artwork preserved)"
+ )
+ status = "artwork_preserved"
+ elif external_cover:
+ print(
+ f"Successfully converted {input_path} "
+ f"(external cover {external_cover.name} embedded)"
+ )
status = "artwork_preserved"
else:
- # Artwork existed in source but did not make it to the output.
- # Keep the original so the user can investigate / re-encode.
+ print(f"Successfully converted {input_path} (artwork preserved)")
+ status = "artwork_preserved"
+ else:
+ if had_embedded_artwork or external_cover:
+ # Artwork existed in source or as external file but did not
+ # make it to the output. Keep the original so the user can
+ # investigate / re-encode.
print(
f"Warning: Artwork did not transfer for {input_path}; "
"keeping original"
)
return True, "artwork_lost"
- else:
print(f"Successfully converted {input_path}")
status = "no_artwork"