From e6f3c07eea9b3b01b296dfadee5db6d46d9cda3b Mon Sep 17 00:00:00 2001 From: mwjdot888 Date: Sun, 3 May 2026 21:52:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BAOGG=E5=B0=81=E9=9D=A2?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=EF=BC=8C=E6=94=AF=E6=8C=81FLAC=E4=BA=8C?= =?UTF-8?q?=E8=BF=9B=E5=88=B6=E7=BB=93=E6=9E=84=E7=9A=84metadata=5Fblock?= =?UTF-8?q?=5Fpicture=20(#851)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 支持自定义temp_path路径,临时文件不再依赖music_path挂载 以前临时文件需挂载在music_path下才能访问,现在temp/前缀的文件直接从 config.temp_path提供访问,支持自定义任意临时目录路径。 Co-Authored-By: Claude Opus 4.7 * feat: 增强OGG封面解析,支持FLAC二进制结构的metadata_block_picture 解析metadata_block_picture标签时,先尝试JSON格式再尝试FLAC二进制结构格式, 兼容更多工具生成的OGG文件封面数据。 Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: mwjdot888 Co-authored-by: Claude Opus 4.7 --- xiaomusic/utils/music_utils.py | 52 ++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/xiaomusic/utils/music_utils.py b/xiaomusic/utils/music_utils.py index 6f3d5ab..8bb4518 100644 --- a/xiaomusic/utils/music_utils.py +++ b/xiaomusic/utils/music_utils.py @@ -11,6 +11,7 @@ import mimetypes import os import re import shutil +import struct import subprocess import tempfile from dataclasses import ( @@ -412,6 +413,48 @@ def _get_alltag_value(tags, k: str) -> str: return "" +def _parse_metadata_block_picture(tag_value: str) -> bytes | None: + """解析 OGG Vorbis 的 metadata_block_picture 标签 + + 先尝试 JSON 格式(部分工具使用),再尝试 FLAC 二进制结构格式。 + FLAC METADATA_BLOCK_PICTURE 二进制结构: + 4B picture type | 4B MIME length + MIME | 4B desc length + desc + | 4B width | 4B height | 4B depth | 4B colors | 4B data length + data + """ + raw = base64.b64decode(tag_value) + + # 尝试 JSON 格式 + try: + picture = json.loads(raw) + if isinstance(picture, dict) and "data" in picture: + return base64.b64decode(picture["data"]) + except (json.JSONDecodeError, UnicodeDecodeError): + pass + + # 尝试 FLAC 二进制结构格式 + try: + offset = 0 + # picture type (4B) + offset += 4 + # MIME type + mime_len = struct.unpack_from(">I", raw, offset)[0] + offset += 4 + mime_len + # description + desc_len = struct.unpack_from(">I", raw, offset)[0] + offset += 4 + desc_len + # width, height, depth, colors (各4B) + offset += 16 + # picture data + data_len = struct.unpack_from(">I", raw, offset)[0] + offset += 4 + return raw[offset : offset + data_len] + except (struct.error, IndexError): + pass + + log.warning("Failed to parse metadata_block_picture") + return None + + def _save_picture(picture_data: bytes, save_root: str, file_path: str) -> str: """保存图片""" # 计算文件名的哈希值 @@ -542,10 +585,13 @@ def extract_audio_metadata(file_path: str, save_root: str) -> dict: metadata.year = _get_tag_value(tags, "DATE") metadata.genre = _get_tag_value(tags, "GENRE") if "metadata_block_picture" in tags: - picture = json.loads(base64.b64decode(tags["metadata_block_picture"][0])) - metadata.picture = _save_picture( - base64.b64decode(picture["data"]), save_root, file_path + picture_data = _parse_metadata_block_picture( + tags["metadata_block_picture"][0] ) + if picture_data: + metadata.picture = _save_picture( + picture_data, save_root, file_path + ) elif isinstance(audio, ASF): metadata.title = _get_tag_value(tags, "Title")