From 329c66d681bf06b002898aac95ff641e01d45f99 Mon Sep 17 00:00:00 2001 From: axyn45 Date: Tue, 16 Jun 2026 21:05:26 +1000 Subject: [PATCH 1/3] fix(web): embed missing metadata and cover art when downloading songs in browser - Introduce browser-id3-writer for tag injection when downloading MP3 songs in browser (non-Electron environment). - Fetch song and cover image ArrayBuffers asynchronously to write TIT2, TPE1, TALB, TPE2, and USLT frames. - Add network and CORS fault tolerance, automatically falling back to original URL-based download on error to guarantee reliable delivery. --- package.json | 1 + pnpm-lock.yaml | 9 +++ src/core/resource/DownloadManager.ts | 96 +++++++++++++++++++++++++++- 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 2e7167a2a..2097040c2 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "electron-store": "^11.0.2", "electron-updater": "^6.8.3", "file-saver": "^2.0.5", + "browser-id3-writer": "^6.3.1", "font-list": "^2.1.0", "fuse.js": "^7.4.1", "get-port": "^7.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c3f50ff1..40d3229ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,9 @@ importers: better-sqlite3: specifier: ^12.10.0 version: 12.10.0 + browser-id3-writer: + specifier: ^6.3.1 + version: 6.3.1 change-case: specifier: ^5.4.4 version: 5.4.4 @@ -2836,6 +2839,7 @@ packages: binary-install@1.1.2: resolution: {integrity: sha512-ZS2cqFHPZOy4wLxvzqfQvDjCOifn+7uCPqNmYRIBM/03+yllON+4fNnsD0VJdW0p97y+E+dTRNPStWNqMBq+9g==} engines: {node: '>=10'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -2881,6 +2885,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browser-id3-writer@6.3.1: + resolution: {integrity: sha512-sRA4Uq9Q3NsmXiVpLvIDxzomtgCdbw6SY85A6fw7dUQGRVoOBg1/buFv6spPhYiSo6FlVtN5OJQTvvhbmfx9rQ==} + browserslist@4.28.2: resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -8605,6 +8612,8 @@ snapshots: dependencies: fill-range: 7.1.1 + browser-id3-writer@6.3.1: {} + browserslist@4.28.2: dependencies: baseline-browser-mapping: 2.10.33 diff --git a/src/core/resource/DownloadManager.ts b/src/core/resource/DownloadManager.ts index 25252192e..f4eb586eb 100644 --- a/src/core/resource/DownloadManager.ts +++ b/src/core/resource/DownloadManager.ts @@ -577,7 +577,101 @@ class DownloadManager { } else { // 浏览器端兜底处理 if (!strategy.downloadUrl) throw new Error("Download URL missing"); - saveAs(strategy.downloadUrl, config.fileName + "." + config.fileType); + + const axios = (await import("axios")).default; + const {ID3Writer} = await import("browser-id3-writer"); + + const mixedContentSafeURL = (downloadUrl:string)=>{ + if (window.location.protocol === "https:" && downloadUrl.startsWith("http://")) { + console.warn(`Rewrote HTTP request to HTTPS: ${downloadUrl}`); + return downloadUrl.replace(/^http:\/\//, "https://"); + } else { + return downloadUrl; + } + }; + + let downloaded = false; + if (config.downloadMeta && config.fileType.toLowerCase() === "mp3" && config.songData) { + try { + // 获取歌曲数据 + const songResponse = await axios.get(mixedContentSafeURL(strategy.downloadUrl), { + responseType: "arraybuffer", + }); + const songBuffer = songResponse.data; + + // 写入标签 + const writer = new ID3Writer(songBuffer); + + // 设置标题、歌手、专辑 + const artists = config.songData.artists; + let artistNames: string[] = []; + if (typeof artists === "string") { + artistNames = [artists]; + } else if (Array.isArray(artists)) { + artistNames = artists.map((a: any) => (typeof a === "string" ? a : a.name || "")); + } + const artist = artistNames.join(", "); + writer + .setFrame("TIT2", config.songData.name) + .setFrame("TPE1", [artist]) + .setFrame( + "TALB", + (typeof config.songData.album === "string" + ? config.songData.album + : config.songData.album?.name), + ); + + // 设置专辑歌手 + const albumArtist = config.albumArtists?.join(", "); + if (albumArtist) { + writer.setFrame("TPE2", albumArtist); + } + + // 设置歌词 + if (config.downloadLyric && config.lyric) { + writer.setFrame("USLT", { + description: "", + lyrics: config.lyric, + language: "eng", + }); + } + + // 获取并嵌入封面图片 + if (config.downloadCover) { + const coverUrl = config.songData.coverSize?.l || config.songData.cover; + if (coverUrl) { + try { + const coverResponse = await axios.get(mixedContentSafeURL(coverUrl), { + responseType: "arraybuffer", + }); + const coverBuffer = coverResponse.data; + writer.setFrame("APIC", { + type: 3, + data: coverBuffer, + description: "Cover", + useUnicodeEncoding: true, + }); + } catch (coverErr) { + console.error("Failed to download or embed cover:", coverErr); + } + } + } + + writer.addTag(); + const taggedBlob = writer.getBlob(); + saveAs(taggedBlob, config.fileName + "." + config.fileType); + downloaded = true; + } catch (metaErr) { + console.error("Failed to inject metadata, falling back to basic download:", metaErr); + } + } + + // 兜底下载 + if (!downloaded) { + console.warn("Cannot download song with metadata. Trying fallback method..."); + saveAs(mixedContentSafeURL(strategy.downloadUrl), config.fileName + "." + config.fileType); + } + dataStore.removeDownloadingSong(strategy.id); } } catch (error: any) { From a896fb26212861f79741ff153110da150091dbee Mon Sep 17 00:00:00 2001 From: axyn45 Date: Wed, 17 Jun 2026 02:16:44 +1000 Subject: [PATCH 2/3] fix: support for multiple artists in metadata --- src/core/resource/DownloadManager.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/core/resource/DownloadManager.ts b/src/core/resource/DownloadManager.ts index f4eb586eb..69a709cd3 100644 --- a/src/core/resource/DownloadManager.ts +++ b/src/core/resource/DownloadManager.ts @@ -65,7 +65,7 @@ class SongDownloadStrategy implements DownloadStrategy { constructor( public readonly song: SongType, private quality: SongLevelType, - ) {} + ) { } get id() { return this.song.id; @@ -579,9 +579,9 @@ class DownloadManager { if (!strategy.downloadUrl) throw new Error("Download URL missing"); const axios = (await import("axios")).default; - const {ID3Writer} = await import("browser-id3-writer"); + const { ID3Writer } = await import("browser-id3-writer"); - const mixedContentSafeURL = (downloadUrl:string)=>{ + const mixedContentSafeURL = (downloadUrl: string) => { if (window.location.protocol === "https:" && downloadUrl.startsWith("http://")) { console.warn(`Rewrote HTTP request to HTTPS: ${downloadUrl}`); return downloadUrl.replace(/^http:\/\//, "https://"); @@ -589,7 +589,7 @@ class DownloadManager { return downloadUrl; } }; - + let downloaded = false; if (config.downloadMeta && config.fileType.toLowerCase() === "mp3" && config.songData) { try { @@ -610,10 +610,9 @@ class DownloadManager { } else if (Array.isArray(artists)) { artistNames = artists.map((a: any) => (typeof a === "string" ? a : a.name || "")); } - const artist = artistNames.join(", "); writer .setFrame("TIT2", config.songData.name) - .setFrame("TPE1", [artist]) + .setFrame("TPE1", artistNames) .setFrame( "TALB", (typeof config.songData.album === "string" @@ -622,7 +621,7 @@ class DownloadManager { ); // 设置专辑歌手 - const albumArtist = config.albumArtists?.join(", "); + const albumArtist = config.albumArtists?.join("; "); if (albumArtist) { writer.setFrame("TPE2", albumArtist); } From ff509b551c27dc131c0a0cee63f4a75fc4166462 Mon Sep 17 00:00:00 2001 From: axyn45 Date: Wed, 17 Jun 2026 03:48:42 +1000 Subject: [PATCH 3/3] fix(web): add missing metadata support for FLAC downloads - Fix missing album artist metadata when downloading FLAC audio files in browser environment. - Add albumArtist option to FlacMetadata interface and write ALBUMARTIST tag to Vorbis Comment. --- src/core/resource/DownloadManager.ts | 58 ++---- src/core/resource/FlacMetadataWriter.ts | 260 ++++++++++++++++++++++++ src/core/resource/MetadataWriter.ts | 78 +++++++ 3 files changed, 357 insertions(+), 39 deletions(-) create mode 100644 src/core/resource/FlacMetadataWriter.ts create mode 100644 src/core/resource/MetadataWriter.ts diff --git a/src/core/resource/DownloadManager.ts b/src/core/resource/DownloadManager.ts index 69a709cd3..8a463c98b 100644 --- a/src/core/resource/DownloadManager.ts +++ b/src/core/resource/DownloadManager.ts @@ -579,7 +579,7 @@ class DownloadManager { if (!strategy.downloadUrl) throw new Error("Download URL missing"); const axios = (await import("axios")).default; - const { ID3Writer } = await import("browser-id3-writer"); + const { injectAudioMetadata } = await import("./MetadataWriter"); const mixedContentSafeURL = (downloadUrl: string) => { if (window.location.protocol === "https:" && downloadUrl.startsWith("http://")) { @@ -591,7 +591,7 @@ class DownloadManager { }; let downloaded = false; - if (config.downloadMeta && config.fileType.toLowerCase() === "mp3" && config.songData) { + if (config.downloadMeta && config.songData) { try { // 获取歌曲数据 const songResponse = await axios.get(mixedContentSafeURL(strategy.downloadUrl), { @@ -599,10 +599,7 @@ class DownloadManager { }); const songBuffer = songResponse.data; - // 写入标签 - const writer = new ID3Writer(songBuffer); - - // 设置标题、歌手、专辑 + // 获取歌手名字列表 const artists = config.songData.artists; let artistNames: string[] = []; if (typeof artists === "string") { @@ -610,32 +607,14 @@ class DownloadManager { } else if (Array.isArray(artists)) { artistNames = artists.map((a: any) => (typeof a === "string" ? a : a.name || "")); } - writer - .setFrame("TIT2", config.songData.name) - .setFrame("TPE1", artistNames) - .setFrame( - "TALB", - (typeof config.songData.album === "string" - ? config.songData.album - : config.songData.album?.name), - ); - - // 设置专辑歌手 - const albumArtist = config.albumArtists?.join("; "); - if (albumArtist) { - writer.setFrame("TPE2", albumArtist); - } - // 设置歌词 - if (config.downloadLyric && config.lyric) { - writer.setFrame("USLT", { - description: "", - lyrics: config.lyric, - language: "eng", - }); - } + const albumString = + typeof config.songData.album === "string" + ? config.songData.album + : config.songData.album?.name; // 获取并嵌入封面图片 + let coverBuffer: ArrayBuffer | undefined = undefined; if (config.downloadCover) { const coverUrl = config.songData.coverSize?.l || config.songData.cover; if (coverUrl) { @@ -643,21 +622,22 @@ class DownloadManager { const coverResponse = await axios.get(mixedContentSafeURL(coverUrl), { responseType: "arraybuffer", }); - const coverBuffer = coverResponse.data; - writer.setFrame("APIC", { - type: 3, - data: coverBuffer, - description: "Cover", - useUnicodeEncoding: true, - }); + coverBuffer = coverResponse.data; } catch (coverErr) { - console.error("Failed to download or embed cover:", coverErr); + console.error("Failed to download cover:", coverErr); } } } - writer.addTag(); - const taggedBlob = writer.getBlob(); + const taggedBlob = await injectAudioMetadata(songBuffer, config.fileType, { + title: config.songData.name, + artists: artistNames, + album: albumString || undefined, + lyric: config.downloadLyric && config.lyric ? config.lyric : undefined, + coverBuffer: coverBuffer, + albumArtist: config.albumArtists?.join("; ") || undefined, + }); + saveAs(taggedBlob, config.fileName + "." + config.fileType); downloaded = true; } catch (metaErr) { diff --git a/src/core/resource/FlacMetadataWriter.ts b/src/core/resource/FlacMetadataWriter.ts new file mode 100644 index 000000000..33866ad0c --- /dev/null +++ b/src/core/resource/FlacMetadataWriter.ts @@ -0,0 +1,260 @@ +/** + * 辅助写入 32 位大端无符号整数 (Big-Endian) + */ +function writeUInt32BE(target: Uint8Array, offset: number, value: number) { + target[offset] = (value >>> 24) & 0xff; + target[offset + 1] = (value >>> 16) & 0xff; + target[offset + 2] = (value >>> 8) & 0xff; + target[offset + 3] = value & 0xff; +} + +/** + * 辅助写入 32 位小端无符号整数 (Little-Endian) + */ +function writeUInt32LE(target: Uint8Array, offset: number, value: number) { + target[offset] = value & 0xff; + target[offset + 1] = (value >>> 8) & 0xff; + target[offset + 2] = (value >>> 16) & 0xff; + target[offset + 3] = (value >>> 24) & 0xff; +} + +/** + * 辅助写入 24 位大端无符号整数 (Big-Endian) + */ +function writeUInt24BE(target: Uint8Array, offset: number, value: number) { + target[offset] = (value >>> 16) & 0xff; + target[offset + 1] = (value >>> 8) & 0xff; + target[offset + 2] = value & 0xff; +} + +/** + * 自动分析图片前几个字节以返回对应的 MIME 类型 + */ +function getMimeType(bytes: Uint8Array): string { + if ( + bytes.length >= 8 && + bytes[0] === 0x89 && + bytes[1] === 0x50 && + bytes[2] === 0x4e && + bytes[3] === 0x47 && + bytes[4] === 0x0d && + bytes[5] === 0x0a && + bytes[6] === 0x1a && + bytes[7] === 0x0a + ) { + return "image/png"; + } + if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xd8) { + return "image/jpeg"; + } + if (bytes.length >= 3 && bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) { + return "image/gif"; + } + return "image/jpeg"; // 默认 jpeg +} + +interface FlacMetadata { + title?: string; + artists?: string[]; + album?: string; + lyric?: string; + coverBuffer?: ArrayBuffer; + albumArtist?: string; +} + + +/** + * 在内存中为 FLAC 二进制 Buffer 注入元数据 + * @param songBuffer 原始音频二进制流 + * @param metadata 标题、歌手、专辑、歌词及封面图等元数据 + */ +export function writeFlacMetadata(songBuffer: ArrayBuffer, metadata: FlacMetadata): Uint8Array { + const bytes = new Uint8Array(songBuffer); + if ( + bytes.length < 4 || + bytes[0] !== 102 || + bytes[1] !== 76 || + bytes[2] !== 97 || + bytes[3] !== 67 + ) { + // "fLaC" + throw new Error("不是合法的 FLAC 文件"); + } + + // 1. 解析原有的元数据块 + let offset = 4; + let isLast = false; + const preservedBlocks: { type: number; data: Uint8Array }[] = []; + + while (!isLast) { + if (offset + 4 > bytes.length) break; + const headerByte = bytes[offset++]; + isLast = (headerByte & 0x80) !== 0; + const blockType = headerByte & 0x7f; + const blockLength = (bytes[offset] << 16) | (bytes[offset + 1] << 8) | bytes[offset + 2]; + offset += 3; + + if (offset + blockLength > bytes.length) break; + const blockData = bytes.slice(offset, offset + blockLength); + offset += blockLength; + + // 过滤掉原有的 VORBIS_COMMENT(4)、PICTURE(6) 和 PADDING(1) 块 + if (blockType !== 4 && blockType !== 6 && blockType !== 1) { + preservedBlocks.push({ type: blockType, data: blockData }); + } + } + + const audioFrames = bytes.slice(offset); + + // 2. 构建新的 VORBIS_COMMENT 块 + const encoder = new TextEncoder(); + const vendorString = "reference libFLAC 1.3.0"; + const vendorBytes = encoder.encode(vendorString); + + // 整理要写入的 Comments + const comments: string[] = []; + if (metadata.title) comments.push(`TITLE=${metadata.title}`); + if (metadata.artists && metadata.artists.length > 0) { + for (const artist of metadata.artists) { + comments.push(`ARTIST=${artist}`); + } + } + if (metadata.album) comments.push(`ALBUM=${metadata.album}`); + if (metadata.albumArtist) comments.push(`ALBUMARTIST=${metadata.albumArtist}`); + if (metadata.lyric) comments.push(`LYRICS=${metadata.lyric}`); + + // 计算 VORBIS_COMMENT 内容的大小并写入 + const commentBytesList = comments.map((c) => encoder.encode(c)); + const vorbisContentSize = + 4 + vendorBytes.length + 4 + commentBytesList.reduce((acc, c) => acc + 4 + c.length, 0); + const vorbisBlockData = new Uint8Array(vorbisContentSize); + + let vOffset = 0; + writeUInt32LE(vorbisBlockData, vOffset, vendorBytes.length); + vOffset += 4; + vorbisBlockData.set(vendorBytes, vOffset); + vOffset += vendorBytes.length; + + writeUInt32LE(vorbisBlockData, vOffset, comments.length); + vOffset += 4; + + for (const cBytes of commentBytesList) { + writeUInt32LE(vorbisBlockData, vOffset, cBytes.length); + vOffset += 4; + vorbisBlockData.set(cBytes, vOffset); + vOffset += cBytes.length; + } + + // 3. 构建新的 PICTURE 块 + let pictureBlockData: Uint8Array | null = null; + if (metadata.coverBuffer && metadata.coverBuffer.byteLength > 0) { + const coverBytes = new Uint8Array(metadata.coverBuffer); + const mimeString = getMimeType(coverBytes); + const mimeBytes = encoder.encode(mimeString); + const descString = "Cover"; + const descBytes = encoder.encode(descString); + + const pictureContentSize = + 4 + + 4 + + mimeBytes.length + + 4 + + descBytes.length + + 4 + + 4 + + 4 + + 4 + + 4 + + coverBytes.length; + pictureBlockData = new Uint8Array(pictureContentSize); + + let pOffset = 0; + // Picture type: 3 (Front Cover) + writeUInt32BE(pictureBlockData, pOffset, 3); + pOffset += 4; + // Mime length + writeUInt32BE(pictureBlockData, pOffset, mimeBytes.length); + pOffset += 4; + // Mime string + pictureBlockData.set(mimeBytes, pOffset); + pOffset += mimeBytes.length; + // Description length + writeUInt32BE(pictureBlockData, pOffset, descBytes.length); + pOffset += 4; + // Description string + pictureBlockData.set(descBytes, pOffset); + pOffset += descBytes.length; + // Width, Height (直接填0) + writeUInt32BE(pictureBlockData, pOffset, 0); + pOffset += 4; + writeUInt32BE(pictureBlockData, pOffset, 0); + pOffset += 4; + // Color depth (色深,通常为24) + writeUInt32BE(pictureBlockData, pOffset, 24); + pOffset += 4; + // Colors + writeUInt32BE(pictureBlockData, pOffset, 0); + pOffset += 4; + // Data length + writeUInt32BE(pictureBlockData, pOffset, coverBytes.length); + pOffset += 4; + // Data + pictureBlockData.set(coverBytes, pOffset); + } + + // 4. 构建 PADDING 块 (预留 4096 字节) + const paddingBlockData = new Uint8Array(4096); + + // 5. 组合最终的所有元数据块 + const finalBlocks: { type: number; data: Uint8Array }[] = []; + // 保留的原有块 + finalBlocks.push(...preservedBlocks); + // 新的 VORBIS_COMMENT + finalBlocks.push({ type: 4, data: vorbisBlockData }); + // 新的 PICTURE + if (pictureBlockData) { + finalBlocks.push({ type: 6, data: pictureBlockData }); + } + // 填充 Padding 块 + finalBlocks.push({ type: 1, data: paddingBlockData }); + + // 6. 重新打包,输出整体二进制数据 + let totalMetadataSize = 4 + finalBlocks.length * 4; + for (const block of finalBlocks) { + totalMetadataSize += block.data.length; + } + const totalSize = totalMetadataSize + audioFrames.length; + const result = new Uint8Array(totalSize); + + // 写入 "fLaC" 标志 + result[0] = 102; // 'f' + result[1] = 76; // 'L' + result[2] = 97; // 'a' + result[3] = 67; // 'C' + + let writeOffset = 4; + for (let i = 0; i < finalBlocks.length; i++) { + const block = finalBlocks[i]; + const isLastBlock = i === finalBlocks.length - 1; + + // Header 字节 1: 1位 isLast + 7位 blockType + let typeByte = block.type & 0x7f; + if (isLastBlock) { + typeByte |= 0x80; + } + result[writeOffset++] = typeByte; + + // Header 字节 2-4: 24位长度 + writeUInt24BE(result, writeOffset, block.data.length); + writeOffset += 3; + + // 块数据 + result.set(block.data, writeOffset); + writeOffset += block.data.length; + } + + // 写入音频帧 + result.set(audioFrames, writeOffset); + + return result; +} diff --git a/src/core/resource/MetadataWriter.ts b/src/core/resource/MetadataWriter.ts new file mode 100644 index 000000000..d77b0e687 --- /dev/null +++ b/src/core/resource/MetadataWriter.ts @@ -0,0 +1,78 @@ +import { writeFlacMetadata } from "./FlacMetadataWriter"; + +interface WriteMetadataOptions { + title?: string; + artists?: string[]; + album?: string; + lyric?: string; + coverBuffer?: ArrayBuffer; + albumArtist?: string; +} + +/** + * 在内存中为音频 Buffer 嵌入元数据(支持 MP3 和 FLAC)并返回对应的 Blob + * @param songBuffer 原始音频二进制流 + * @param fileType 音频格式后缀名(如 "mp3", "flac") + * @param options 元数据配置(标题、歌手列表、专辑、歌词、封面及专辑歌手等) + */ +export async function injectAudioMetadata( + songBuffer: ArrayBuffer, + fileType: string, + options: WriteMetadataOptions +): Promise { + const fileTypeLower = fileType.toLowerCase(); + + if (fileTypeLower === "mp3") { + const { ID3Writer } = await import("browser-id3-writer"); + const writer = new ID3Writer(songBuffer); + + if (options.title) { + writer.setFrame("TIT2", options.title); + } + if (options.artists && options.artists.length > 0) { + writer.setFrame("TPE1", options.artists); + } + if (options.album) { + writer.setFrame("TALB", options.album); + } + if (options.albumArtist) { + writer.setFrame("TPE2", options.albumArtist); + } + + // 设置歌词 + if (options.lyric) { + writer.setFrame("USLT", { + description: "", + lyrics: options.lyric, + language: "eng", + }); + } + + // 设置封面图片 + if (options.coverBuffer) { + writer.setFrame("APIC", { + type: 3, + data: options.coverBuffer, + description: "Cover", + useUnicodeEncoding: true, + }); + } + + writer.addTag(); + return writer.getBlob(); + } else if (fileTypeLower === "flac") { + const modifiedBytes = writeFlacMetadata(songBuffer, { + title: options.title, + artists: options.artists, + album: options.album, + lyric: options.lyric, + coverBuffer: options.coverBuffer, + albumArtist: options.albumArtist, + }); + // 使用 any 避开严格 TS DOM 环境下的 BlobPart 兼容性限制 + return new Blob([modifiedBytes as any], { type: "audio/flac" }); + } + + // 兜底格式直接返回原 Buffer 包装的 Blob + return new Blob([songBuffer]); +}