mirror of
				https://git.unlock-music.dev/um/web.git
				synced 2025-11-04 15:33:28 +08:00 
			
		
		
		
	refactor(typescript): .xm & .kgm
This commit is contained in:
		
							parent
							
								
									a1eddb230f
								
							
						
					
					
						commit
						342241b379
					
				@ -7,6 +7,7 @@ import {
 | 
				
			|||||||
    SniffAudioExt
 | 
					    SniffAudioExt
 | 
				
			||||||
} from "@/decrypt/utils.ts";
 | 
					} from "@/decrypt/utils.ts";
 | 
				
			||||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
					import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
				
			||||||
 | 
					import {DecryptResult} from "@/decrypt/entity";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const VprHeader = [
 | 
					const VprHeader = [
 | 
				
			||||||
    0x05, 0x28, 0xBC, 0x96, 0xE9, 0xE4, 0x5A, 0x43,
 | 
					    0x05, 0x28, 0xBC, 0x96, 0xE9, 0xE4, 0x5A, 0x43,
 | 
				
			||||||
@ -14,27 +15,22 @@ const VprHeader = [
 | 
				
			|||||||
const KgmHeader = [
 | 
					const KgmHeader = [
 | 
				
			||||||
    0x7C, 0xD5, 0x32, 0xEB, 0x86, 0x02, 0x7F, 0x4B,
 | 
					    0x7C, 0xD5, 0x32, 0xEB, 0x86, 0x02, 0x7F, 0x4B,
 | 
				
			||||||
    0xA8, 0xAF, 0xA6, 0x8E, 0x0F, 0xFF, 0x99, 0x14]
 | 
					    0xA8, 0xAF, 0xA6, 0x8E, 0x0F, 0xFF, 0x99, 0x14]
 | 
				
			||||||
const VprMaskDiff = [0x25, 0xDF, 0xE8, 0xA6, 0x75, 0x1E, 0x75, 0x0E,
 | 
					const VprMaskDiff = [
 | 
				
			||||||
 | 
					    0x25, 0xDF, 0xE8, 0xA6, 0x75, 0x1E, 0x75, 0x0E,
 | 
				
			||||||
    0x2F, 0x80, 0xF3, 0x2D, 0xB8, 0xB6, 0xE3, 0x11,
 | 
					    0x2F, 0x80, 0xF3, 0x2D, 0xB8, 0xB6, 0xE3, 0x11,
 | 
				
			||||||
    0x00]
 | 
					    0x00]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function Decrypt(file, raw_filename, raw_ext) {
 | 
					
 | 
				
			||||||
    try {
 | 
					export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
 | 
				
			||||||
        if (window.location.protocol === "file:") {
 | 
					    if (window?.location?.protocol === "file:") {
 | 
				
			||||||
            return {
 | 
					        throw Error("请使用 <a target='_blank' href='https://github.com/unlock-music/cli'>CLI版本</a> 进行解锁")
 | 
				
			||||||
                status: false,
 | 
					 | 
				
			||||||
                message: "请使用<a target='_blank' href='https://github.com/ix64/unlock-music/wiki/其他音乐格式工具'>CLI版本</a>进行解锁"
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } catch {
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const oriData = new Uint8Array(await GetArrayBuffer(file));
 | 
					    const oriData = new Uint8Array(await GetArrayBuffer(file));
 | 
				
			||||||
    if (raw_ext === "vpr") {
 | 
					    if (raw_ext === "vpr") {
 | 
				
			||||||
        if (!BytesHasPrefix(oriData, VprHeader))
 | 
					        if (!BytesHasPrefix(oriData, VprHeader)) throw Error("Not a valid vpr file!")
 | 
				
			||||||
            return {status: false, message: "Not a valid vpr file!"}
 | 
					 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        if (!BytesHasPrefix(oriData, KgmHeader))
 | 
					        if (!BytesHasPrefix(oriData, KgmHeader)) throw Error("Not a valid kgm(a) file!")
 | 
				
			||||||
            return {status: false, message: "Not a valid kgm/kgma file!"}
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    let bHeaderLen = new DataView(oriData.slice(0x10, 0x14).buffer)
 | 
					    let bHeaderLen = new DataView(oriData.slice(0x10, 0x14).buffer)
 | 
				
			||||||
    let headerLen = bHeaderLen.getUint32(0, true)
 | 
					    let headerLen = bHeaderLen.getUint32(0, true)
 | 
				
			||||||
@ -42,18 +38,13 @@ export async function Decrypt(file, raw_filename, raw_ext) {
 | 
				
			|||||||
    let audioData = oriData.slice(headerLen)
 | 
					    let audioData = oriData.slice(headerLen)
 | 
				
			||||||
    let dataLen = audioData.length
 | 
					    let dataLen = audioData.length
 | 
				
			||||||
    if (audioData.byteLength > 1 << 26) {
 | 
					    if (audioData.byteLength > 1 << 26) {
 | 
				
			||||||
        return {
 | 
					        throw Error("文件过大,请使用 <a target='_blank' href='https://github.com/unlock-music/cli'>CLI版本</a> 进行解锁")
 | 
				
			||||||
            status: false,
 | 
					 | 
				
			||||||
            message: "文件过大,请使用<a target='_blank' href='https://github.com/ix64/unlock-music/wiki/其他音乐格式工具'>CLI版本</a>进行解锁"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let key1 = new Uint8Array(17)
 | 
					    let key1 = new Uint8Array(17)
 | 
				
			||||||
    key1.set(oriData.slice(0x1c, 0x2c), 0)
 | 
					    key1.set(oriData.slice(0x1c, 0x2c), 0)
 | 
				
			||||||
    if (MaskV2 == null) {
 | 
					    if (MaskV2.length === 0) {
 | 
				
			||||||
        if (!await LoadMaskV2()) {
 | 
					        if (!await LoadMaskV2()) throw Error("加载Kgm/Vpr Mask数据失败")
 | 
				
			||||||
            return {status: false, message: "加载Kgm/Vpr Mask数据失败"}
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (let i = 0; i < dataLen; i++) {
 | 
					    for (let i = 0; i < dataLen; i++) {
 | 
				
			||||||
@ -85,17 +76,16 @@ export async function Decrypt(file, raw_filename, raw_ext) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function GetMask(pos) {
 | 
					
 | 
				
			||||||
 | 
					function GetMask(pos: number) {
 | 
				
			||||||
    return MaskV2PreDef[pos % 272] ^ MaskV2[pos >> 4]
 | 
					    return MaskV2PreDef[pos % 272] ^ MaskV2[pos >> 4]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let MaskV2 = null;
 | 
					let MaskV2: Uint8Array = new Uint8Array(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function LoadMaskV2() {
 | 
					async function LoadMaskV2(): Promise<boolean> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        let resp = await fetch("./static/kgm.mask", {
 | 
					        const resp = await fetch("./static/kgm.mask", {method: "GET"})
 | 
				
			||||||
            method: "GET"
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        MaskV2 = new Uint8Array(await resp.arrayBuffer());
 | 
					        MaskV2 = new Uint8Array(await resp.arrayBuffer());
 | 
				
			||||||
        return true
 | 
					        return true
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
@ -1,4 +1,11 @@
 | 
				
			|||||||
import {AudioMimeType, BytesHasPrefix, GetArrayBuffer, GetMetaFromFile, SniffAudioExt} from "@/decrypt/utils.ts";
 | 
					import {
 | 
				
			||||||
 | 
					    AudioMimeType,
 | 
				
			||||||
 | 
					    BytesHasPrefix,
 | 
				
			||||||
 | 
					    GetArrayBuffer,
 | 
				
			||||||
 | 
					    GetImageFromURL,
 | 
				
			||||||
 | 
					    GetMetaFromFile,
 | 
				
			||||||
 | 
					    SniffAudioExt
 | 
				
			||||||
 | 
					} from "@/decrypt/utils.ts";
 | 
				
			||||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
					import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const CryptoJS = require("crypto-js");
 | 
					const CryptoJS = require("crypto-js");
 | 
				
			||||||
@ -10,7 +17,6 @@ const MagicHeader = [0x43, 0x54, 0x45, 0x4E, 0x46, 0x44, 0x41, 0x4D];
 | 
				
			|||||||
import jimp from 'jimp';
 | 
					import jimp from 'jimp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    GetWebImage,
 | 
					 | 
				
			||||||
    WriteMp3Meta
 | 
					    WriteMp3Meta
 | 
				
			||||||
} from "./util"
 | 
					} from "./util"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -41,8 +47,8 @@ export async function Decrypt(file, raw_filename, _) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if (musicMeta.format === undefined) musicMeta.format = SniffAudioExt(audioData);
 | 
					    if (musicMeta.format === undefined) musicMeta.format = SniffAudioExt(audioData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const imageInfo = await GetWebImage(musicMeta.albumPic);
 | 
					    const imageInfo = await GetImageFromURL(musicMeta.albumPic);
 | 
				
			||||||
    while (!!imageInfo.buffer && imageInfo.buffer.byteLength >= 16 * 1024 * 1024) {
 | 
					    while (imageInfo && imageInfo.buffer.byteLength >= 1 << 24) {
 | 
				
			||||||
        let img = await jimp.read(imageInfo.buffer)
 | 
					        let img = await jimp.read(imageInfo.buffer)
 | 
				
			||||||
        await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO)
 | 
					        await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO)
 | 
				
			||||||
        imageInfo.buffer = await img.getBufferAsync("image/jpeg")
 | 
					        imageInfo.buffer = await img.getBufferAsync("image/jpeg")
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,17 @@
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
    GetWebImage,
 | 
					 | 
				
			||||||
    IXAREA_API_ENDPOINT,
 | 
					    IXAREA_API_ENDPOINT,
 | 
				
			||||||
    WriteMp3Meta
 | 
					    WriteMp3Meta
 | 
				
			||||||
} from "./util";
 | 
					} from "./util";
 | 
				
			||||||
import {QmcMaskCreate58, QmcMaskDetectMflac, QmcMaskDetectMgg, QmcMaskGetDefault} from "./qmcMask";
 | 
					import {QmcMaskCreate58, QmcMaskDetectMflac, QmcMaskDetectMgg, QmcMaskGetDefault} from "./qmcMask";
 | 
				
			||||||
import {fromByteArray as Base64Encode, toByteArray as Base64Decode} from 'base64-js'
 | 
					import {fromByteArray as Base64Encode, toByteArray as Base64Decode} from 'base64-js'
 | 
				
			||||||
import {AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt} from "@/decrypt/utils.ts";
 | 
					import {
 | 
				
			||||||
 | 
					    AudioMimeType,
 | 
				
			||||||
 | 
					    GetArrayBuffer,
 | 
				
			||||||
 | 
					    GetCoverFromFile,
 | 
				
			||||||
 | 
					    GetImageFromURL,
 | 
				
			||||||
 | 
					    GetMetaFromFile,
 | 
				
			||||||
 | 
					    SniffAudioExt
 | 
				
			||||||
 | 
					} from "@/decrypt/utils.ts";
 | 
				
			||||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
					import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MetaFlac = require('metaflac-js');
 | 
					const MetaFlac = require('metaflac-js');
 | 
				
			||||||
@ -75,8 +81,8 @@ export async function Decrypt(file, raw_filename, raw_ext) {
 | 
				
			|||||||
    if (!imgUrl) {
 | 
					    if (!imgUrl) {
 | 
				
			||||||
        imgUrl = await queryAlbumCoverImage(info.artist, info.title, musicMeta.common.album);
 | 
					        imgUrl = await queryAlbumCoverImage(info.artist, info.title, musicMeta.common.album);
 | 
				
			||||||
        if (imgUrl !== "") {
 | 
					        if (imgUrl !== "") {
 | 
				
			||||||
            const imageInfo = await GetWebImage(imgUrl);
 | 
					            const imageInfo = await GetImageFromURL(imgUrl);
 | 
				
			||||||
            if (imageInfo.url !== "") {
 | 
					            if (imageInfo) {
 | 
				
			||||||
                imgUrl = imageInfo.url
 | 
					                imgUrl = imageInfo.url
 | 
				
			||||||
                try {
 | 
					                try {
 | 
				
			||||||
                    if (ext === "mp3") {
 | 
					                    if (ext === "mp3") {
 | 
				
			||||||
 | 
				
			|||||||
@ -4,21 +4,6 @@ const ID3Writer = require("browser-id3-writer");
 | 
				
			|||||||
export const IXAREA_API_ENDPOINT = "https://stats.ixarea.com/apis"
 | 
					export const IXAREA_API_ENDPOINT = "https://stats.ixarea.com/apis"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function GetWebImage(pic_url) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
        let resp = await fetch(pic_url);
 | 
					 | 
				
			||||||
        let mime = resp.headers.get("Content-Type");
 | 
					 | 
				
			||||||
        if (mime.startsWith("image/")) {
 | 
					 | 
				
			||||||
            let buf = await resp.arrayBuffer();
 | 
					 | 
				
			||||||
            let objBlob = new Blob([buf], {type: mime});
 | 
					 | 
				
			||||||
            let objUrl = URL.createObjectURL(objBlob);
 | 
					 | 
				
			||||||
            return {"buffer": buf, "src": pic_url, "url": objUrl, "type": mime};
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return {"buffer": null, "src": pic_url, "url": "", "type": ""}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function WriteMp3Meta(audioData, artistList, title, album, pictureData = null, pictureDesc = "Cover", originalMeta = null) {
 | 
					export async function WriteMp3Meta(audioData, artistList, title, album, pictureData = null, pictureDesc = "Cover", originalMeta = null) {
 | 
				
			||||||
    const writer = new ID3Writer(audioData);
 | 
					    const writer = new ID3Writer(audioData);
 | 
				
			||||||
    if (originalMeta !== null) {
 | 
					    if (originalMeta !== null) {
 | 
				
			||||||
 | 
				
			|||||||
@ -89,3 +89,18 @@ export function GetMetaFromFile(filename: string, exist_title?: string, exist_ar
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    return meta
 | 
					    return meta
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function GetImageFromURL(src: string):
 | 
				
			||||||
 | 
					    Promise<{ mime: string; buffer: ArrayBuffer; url: string } | undefined> {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const resp = await fetch(src);
 | 
				
			||||||
 | 
					        const mime = resp.headers.get("Content-Type");
 | 
				
			||||||
 | 
					        if (mime?.startsWith("image/")) {
 | 
				
			||||||
 | 
					            const buffer = await resp.arrayBuffer();
 | 
				
			||||||
 | 
					            const url = URL.createObjectURL(new Blob([buffer], {type: mime}))
 | 
				
			||||||
 | 
					            return {buffer, url, mime}
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					        console.warn(e)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,22 +1,23 @@
 | 
				
			|||||||
import {Decrypt as RawDecrypt} from "./raw";
 | 
					import {Decrypt as RawDecrypt} from "@/decrypt/raw";
 | 
				
			||||||
 | 
					import {DecryptResult} from "@/decrypt/entity";
 | 
				
			||||||
import {AudioMimeType, BytesHasPrefix, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile} from "@/decrypt/utils.ts";
 | 
					import {AudioMimeType, BytesHasPrefix, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile} from "@/decrypt/utils.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
					import {parseBlob as metaParseBlob} from "music-metadata-browser";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MagicHeader = [0x69, 0x66, 0x6D, 0x74]
 | 
					const MagicHeader = [0x69, 0x66, 0x6D, 0x74]
 | 
				
			||||||
const MagicHeader2 = [0xfe, 0xfe, 0xfe, 0xfe]
 | 
					const MagicHeader2 = [0xfe, 0xfe, 0xfe, 0xfe]
 | 
				
			||||||
const FileTypeMap = {
 | 
					const FileTypeMap: { [key: string]: string } = {
 | 
				
			||||||
    " WAV": ".wav",
 | 
					    " WAV": ".wav",
 | 
				
			||||||
    "FLAC": ".flac",
 | 
					    "FLAC": ".flac",
 | 
				
			||||||
    " MP3": ".mp3",
 | 
					    " MP3": ".mp3",
 | 
				
			||||||
    " A4M": ".m4a",
 | 
					    " A4M": ".m4a",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function Decrypt(file, raw_filename, raw_ext) {
 | 
					export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
 | 
				
			||||||
    const oriData = new Uint8Array(await GetArrayBuffer(file));
 | 
					    const oriData = new Uint8Array(await GetArrayBuffer(file));
 | 
				
			||||||
    if (!BytesHasPrefix(oriData, MagicHeader) || !BytesHasPrefix(oriData.slice(8, 12), MagicHeader2)) {
 | 
					    if (!BytesHasPrefix(oriData, MagicHeader) || !BytesHasPrefix(oriData.slice(8, 12), MagicHeader2)) {
 | 
				
			||||||
        if (raw_ext === "xm") {
 | 
					        if (raw_ext === "xm") {
 | 
				
			||||||
            return {status: false, message: "此xm文件已损坏"}
 | 
					            throw Error("此xm文件已损坏")
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            return await RawDecrypt(file, raw_filename, raw_ext, true)
 | 
					            return await RawDecrypt(file, raw_filename, raw_ext, true)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -24,7 +25,7 @@ export async function Decrypt(file, raw_filename, raw_ext) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    let typeText = (new TextDecoder()).decode(oriData.slice(4, 8))
 | 
					    let typeText = (new TextDecoder()).decode(oriData.slice(4, 8))
 | 
				
			||||||
    if (!FileTypeMap.hasOwnProperty(typeText)) {
 | 
					    if (!FileTypeMap.hasOwnProperty(typeText)) {
 | 
				
			||||||
        return {status: false, message: "未知的xm文件类型"}
 | 
					        throw Error("未知的.xm文件类型")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let key = oriData[0xf]
 | 
					    let key = oriData[0xf]
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user