diff --git a/src/App.vue b/src/App.vue index 60c8fc9..b2652f2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -60,7 +60,7 @@ import preview from "./component/preview" import {DownloadBlobMusic, RemoveBlobMusic} from "./component/util" import config from "../package" - import {IXAREA_API_ENDPOINT} from "./decrypt/util"; + import {IXAREA_API_ENDPOINT} from "@/decrypt/utils"; export default { name: 'app', diff --git a/src/decrypt/ncm.js b/src/decrypt/ncm.js index 47124e3..70f373b 100644 --- a/src/decrypt/ncm.js +++ b/src/decrypt/ncm.js @@ -4,21 +4,16 @@ import { GetArrayBuffer, GetImageFromURL, GetMetaFromFile, - SniffAudioExt + SniffAudioExt, WriteMetaToFlac, WriteMetaToMp3 } from "@/decrypt/utils.ts"; import {parseBlob as metaParseBlob} from "music-metadata-browser"; +import jimp from 'jimp'; const CryptoJS = require("crypto-js"); -const MetaFlac = require('metaflac-js'); const CORE_KEY = CryptoJS.enc.Hex.parse("687a4852416d736f356b496e62617857"); const META_KEY = CryptoJS.enc.Hex.parse("2331346C6A6B5F215C5D2630553C2728"); const MagicHeader = [0x43, 0x54, 0x45, 0x4E, 0x46, 0x44, 0x41, 0x4D]; -import jimp from 'jimp'; - -import { - WriteMp3Meta -} from "./util" export async function Decrypt(file, raw_filename, _) { const fileBuffer = await GetArrayBuffer(file); @@ -48,32 +43,27 @@ export async function Decrypt(file, raw_filename, _) { if (musicMeta.format === undefined) musicMeta.format = SniffAudioExt(audioData); const imageInfo = await GetImageFromURL(musicMeta.albumPic); - while (imageInfo && imageInfo.buffer.byteLength >= 1 << 24) { - let img = await jimp.read(imageInfo.buffer) - await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO) - imageInfo.buffer = await img.getBufferAsync("image/jpeg") - } + const mime = AudioMimeType[musicMeta.format] try { let musicBlob = new Blob([audioData], {type: mime}); const originalMeta = await metaParseBlob(musicBlob); let shouldWrite = !originalMeta.common.album && !originalMeta.common.artists && !originalMeta.common.title - if (musicMeta.format === "mp3") { - audioData = await WriteMp3Meta( - audioData, artists, info.title, musicMeta.album, imageInfo.buffer, musicMeta.albumPic, shouldWrite ? null : originalMeta) - } else if (musicMeta.format === "flac") { - const writer = new MetaFlac(Buffer.from(audioData)) - if (shouldWrite) { - writer.setTag("TITLE=" + info.title) - writer.setTag("ALBUM=" + musicMeta.album) - writer.removeTag("ARTIST") - artists.forEach(artist => writer.setTag("ARTIST=" + artist)) + if (shouldWrite || imageInfo) { + while (imageInfo && imageInfo.buffer.byteLength >= 1 << 24) { + let img = await jimp.read(Buffer.from(imageInfo.buffer)) + await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO) + imageInfo.buffer = await img.getBufferAsync("image/jpeg") + } + const newMeta = {title: info.title, artists, album: musicMeta.album, picture: imageInfo?.buffer} + if (musicMeta.format === "mp3") { + audioData = WriteMetaToMp3(audioData.buffer, newMeta, originalMeta) + } else if (musicMeta.format === "flac") { + audioData = WriteMetaToFlac(Buffer.from(audioData), newMeta, originalMeta) } - writer.importPictureFromBuffer(Buffer.from(imageInfo.buffer)) - audioData = writer.save() } } catch (e) { - console.warn("Error while appending cover image to file " + e) + console.warn("Error while appending cover image to file ", e) } const musicData = new Blob([audioData], {type: mime}) diff --git a/src/decrypt/qmc.js b/src/decrypt/qmc.js index 55b51c7..5aebc6f 100644 --- a/src/decrypt/qmc.js +++ b/src/decrypt/qmc.js @@ -1,7 +1,3 @@ -import { - IXAREA_API_ENDPOINT, - WriteMp3Meta -} from "./util"; import {QmcMaskCreate58, QmcMaskDetectMflac, QmcMaskDetectMgg, QmcMaskGetDefault} from "./qmcMask"; import {fromByteArray as Base64Encode, toByteArray as Base64Decode} from 'base64-js' import { @@ -9,13 +5,11 @@ import { GetArrayBuffer, GetCoverFromFile, GetImageFromURL, - GetMetaFromFile, - SniffAudioExt + GetMetaFromFile, IXAREA_API_ENDPOINT, + SniffAudioExt, WriteMetaToFlac, WriteMetaToMp3 } from "@/decrypt/utils.ts"; import {parseBlob as metaParseBlob} from "music-metadata-browser"; -const MetaFlac = require('metaflac-js'); - const iconv = require('iconv-lite'); const decode = iconv.decode @@ -85,15 +79,12 @@ export async function Decrypt(file, raw_filename, raw_ext) { if (imageInfo) { imgUrl = imageInfo.url try { + const newMeta = {picture: imageInfo.buffer, title: info.title, artists: info.artist.split(" _ ")} if (ext === "mp3") { - musicDecoded = await WriteMp3Meta(musicDecoded, - info.artist.split(" _ "), info.title, "", - imageInfo.buffer, "Cover", musicMeta) + musicDecoded = WriteMetaToMp3(musicDecoded, newMeta, musicMeta) musicBlob = new Blob([musicDecoded], {type: mime}); } else if (ext === 'flac') { - const writer = new MetaFlac(Buffer.from(musicDecoded)) - writer.importPictureFromBuffer(Buffer.from(imageInfo.buffer)) - musicDecoded = writer.save() + musicDecoded = WriteMetaToFlac(Buffer.from(musicDecoded), newMeta, musicMeta) musicBlob = new Blob([musicDecoded], {type: mime}); } else { console.info("writing metadata for " + ext + " is not being supported for now") diff --git a/src/decrypt/util.js b/src/decrypt/util.js deleted file mode 100644 index 03533d8..0000000 --- a/src/decrypt/util.js +++ /dev/null @@ -1,36 +0,0 @@ -const ID3Writer = require("browser-id3-writer"); - - -export const IXAREA_API_ENDPOINT = "https://stats.ixarea.com/apis" - - -export async function WriteMp3Meta(audioData, artistList, title, album, pictureData = null, pictureDesc = "Cover", originalMeta = null) { - const writer = new ID3Writer(audioData); - if (originalMeta !== null) { - artistList = originalMeta.common.artists || artistList - title = originalMeta.common.title || title - album = originalMeta.common.album || album - const frames = originalMeta.native['ID3v2.4'] || originalMeta.native['ID3v2.3'] || originalMeta.native['ID3v2.2'] || [] - frames.forEach(frame => { - if (frame.id !== 'TPE1' && frame.id !== 'TIT2' && frame.id !== 'TALB') { - try { - writer.setFrame(frame.id, frame.value) - } catch (e) { - } - } - }) - } - writer.setFrame('TPE1', artistList) - .setFrame('TIT2', title) - .setFrame('TALB', album); - if (pictureData !== null) { - writer.setFrame('APIC', { - type: 3, - data: pictureData, - description: pictureDesc, - }) - } - writer.addTag(); - return writer.arrayBuffer; -} - diff --git a/src/decrypt/utils.ts b/src/decrypt/utils.ts index 25affac..194a836 100644 --- a/src/decrypt/utils.ts +++ b/src/decrypt/utils.ts @@ -1,4 +1,6 @@ import {IAudioMetadata} from "music-metadata-browser"; +import ID3Writer from "browser-id3-writer"; +import MetaFlac from "metaflac-js"; export const FLAC_HEADER = [0x66, 0x4C, 0x61, 0x43]; export const MP3_HEADER = [0x49, 0x44, 0x33]; @@ -19,6 +21,9 @@ export const AudioMimeType: { [key: string]: string } = { wav: "audio/x-wav" }; +export const IXAREA_API_ENDPOINT = "https://stats.ixarea.com/apis" + + export function BytesHasPrefix(data: Uint8Array, prefix: number[]): boolean { if (prefix.length > data.length) return false return prefix.every((val, idx) => { @@ -104,3 +109,58 @@ export async function GetImageFromURL(src: string): console.warn(e) } } + + +export interface IMusicMeta { + title: string + artists?: string[] + album?: string + picture?: ArrayBuffer + picture_desc?: string +} + +export function WriteMetaToMp3(audioData: ArrayBuffer, info: IMusicMeta, original: IAudioMetadata) { + const writer = new ID3Writer(audioData); + + // reserve original data + const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || [] + frames.forEach(frame => { + if (frame.id !== 'TPE1' && frame.id !== 'TIT2' && frame.id !== 'TALB') { + try { + writer.setFrame(frame.id, frame.value) + } catch (e) { + } + } + }) + + const old = original.common + writer.setFrame('TPE1', old?.artists || info.artists || []) + .setFrame('TIT2', old?.title || info.title) + .setFrame('TALB', old?.album || info.album || ""); + if (info.picture) { + writer.setFrame('APIC', { + type: 3, + data: info.picture, + description: info.picture_desc || "Cover", + }) + } + return writer.addTag(); +} + +export function WriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) { + const writer = new MetaFlac(audioData) + const old = original.common + if (!old.title && !old.album && old.artists) { + writer.setTag("TITLE=" + info.title) + writer.setTag("ALBUM=" + info.album) + if (info.artists) { + writer.removeTag("ARTIST") + info.artists.forEach(artist => writer.setTag("ARTIST=" + artist)) + } + } + + if (info.picture) { + writer.importPictureFromBuffer(Buffer.from(info.picture)) + } + return writer.save() +} diff --git a/src/shims-browser-id3-writer.d.ts b/src/shims-browser-id3-writer.d.ts new file mode 100644 index 0000000..acd09de --- /dev/null +++ b/src/shims-browser-id3-writer.d.ts @@ -0,0 +1,25 @@ +declare module "browser-id3-writer" { + export default class ID3Writer { + constructor(buffer: Buffer | ArrayBuffer) + + setFrame(name: string, value: string | object | string[]) + + addTag(): ArrayBuffer + } +} + +declare module "metaflac-js" { + export default class Metaflac { + constructor(buffer: Buffer) + + setTag(field: string) + + removeTag(name: string) + + importPictureFromBuffer(picture: Buffer) + + save(): Buffer + } +} + +