Compare commits
15 Commits
v1.9.0-bet
...
v1.9.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d3898161b9 | ||
![]() |
5a7a9e3add | ||
![]() |
652bb1fc32 | ||
![]() |
6737e8c11b | ||
![]() |
71862538b7 | ||
![]() |
4251b94b1f | ||
![]() |
8fdda048f6 | ||
![]() |
39c7294996 | ||
![]() |
48f879cb58 | ||
![]() |
f0875ad175 | ||
![]() |
02a146e069 | ||
![]() |
2e31853ffb | ||
![]() |
a7aaf246ae | ||
![]() |
4bc0a10c09 | ||
![]() |
3645dd7d01 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -3,6 +3,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- "**/*.js"
|
- "**/*.js"
|
||||||
|
- "**/*.ts"
|
||||||
- "**/*.vue"
|
- "**/*.vue"
|
||||||
- "public/**/*"
|
- "public/**/*"
|
||||||
- "package-lock.json"
|
- "package-lock.json"
|
||||||
@@ -12,6 +13,7 @@ on:
|
|||||||
types: [ opened, synchronize, reopened ]
|
types: [ opened, synchronize, reopened ]
|
||||||
paths:
|
paths:
|
||||||
- "**/*.js"
|
- "**/*.js"
|
||||||
|
- "**/*.ts"
|
||||||
- "**/*.vue"
|
- "**/*.vue"
|
||||||
- "public/**/*"
|
- "public/**/*"
|
||||||
- "package-lock.json"
|
- "package-lock.json"
|
||||||
|
4157
package-lock.json
generated
4157
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "unlock-music",
|
"name": "unlock-music",
|
||||||
"version": "v1.9.0-beta",
|
"version": "v1.9.0",
|
||||||
"updateInfo": "新增写入本地文件系统; 优化.kwm解锁; 支持.acc嗅探; 使用Typescript重构",
|
"updateInfo": "新增写入本地文件系统; 优化.kwm解锁; 支持.acc嗅探; 使用Typescript重构",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Unlock encrypted music file in browser.",
|
"description": "Unlock encrypted music file in browser.",
|
||||||
@@ -18,19 +18,19 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"browser-id3-writer": "^4.4.0",
|
"browser-id3-writer": "^4.4.0",
|
||||||
"core-js": "^3.12.1",
|
"core-js": "^3.16.0",
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "^4.1.1",
|
||||||
"element-ui": "^2.15.1",
|
"element-ui": "^2.15.5",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"jimp": "^0.16.1",
|
"jimp": "^0.16.1",
|
||||||
"metaflac-js": "^1.0.5",
|
"metaflac-js": "^1.0.5",
|
||||||
"music-metadata-browser": "^2.2.6",
|
"music-metadata-browser": "^2.4.3",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"threads": "^1.6.4",
|
"threads": "^1.6.5",
|
||||||
"vue": "^2.6.12"
|
"vue": "^2.6.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/crypto-js": "^4.0.1",
|
"@types/crypto-js": "^4.0.2",
|
||||||
"@vue/cli-plugin-babel": "^4.5.13",
|
"@vue/cli-plugin-babel": "^4.5.13",
|
||||||
"@vue/cli-plugin-pwa": "^4.5.13",
|
"@vue/cli-plugin-pwa": "^4.5.13",
|
||||||
"@vue/cli-plugin-typescript": "^4.5.13",
|
"@vue/cli-plugin-typescript": "^4.5.13",
|
||||||
@@ -40,8 +40,8 @@
|
|||||||
"sass-loader": "^10.2.0",
|
"sass-loader": "^10.2.0",
|
||||||
"semver": "^7.3.5",
|
"semver": "^7.3.5",
|
||||||
"threads-plugin": "^1.4.0",
|
"threads-plugin": "^1.4.0",
|
||||||
"typescript": "~4.1.5",
|
"typescript": "~4.1.6",
|
||||||
"vue-cli-plugin-element": "^1.0.1",
|
"vue-cli-plugin-element": "^1.0.1",
|
||||||
"vue-template-compiler": "^2.6.12"
|
"vue-template-compiler": "^2.6.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -63,7 +63,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (window.Worker && process.env.NODE_ENV === 'production') {
|
if (window.Worker && window.location.protocol !== "file:" && process.env.NODE_ENV === 'production') {
|
||||||
console.log("Using Worker Pool")
|
console.log("Using Worker Pool")
|
||||||
this.queue = Pool(
|
this.queue = Pool(
|
||||||
() => spawn(new Worker('@/utils/worker.ts')),
|
() => spawn(new Worker('@/utils/worker.ts')),
|
||||||
|
@@ -1,37 +1,42 @@
|
|||||||
import {Decrypt as NcmDecrypt} from "@/decrypt/ncm";
|
import {Decrypt as NcmDecrypt} from "@/decrypt/ncm";
|
||||||
|
import {Decrypt as NcmCacheDecrypt} from "@/decrypt/ncmcache";
|
||||||
import {Decrypt as XmDecrypt} from "@/decrypt/xm";
|
import {Decrypt as XmDecrypt} from "@/decrypt/xm";
|
||||||
import {Decrypt as QmcDecrypt} from "@/decrypt/qmc";
|
import {Decrypt as QmcDecrypt} from "@/decrypt/qmc";
|
||||||
|
import {Decrypt as QmcCacheDecrypt} from "@/decrypt/qmccache";
|
||||||
import {Decrypt as KgmDecrypt} from "@/decrypt/kgm";
|
import {Decrypt as KgmDecrypt} from "@/decrypt/kgm";
|
||||||
import {Decrypt as KwmDecrypt} from "@/decrypt/kwm";
|
import {Decrypt as KwmDecrypt} from "@/decrypt/kwm";
|
||||||
import {Decrypt as RawDecrypt} from "@/decrypt/raw";
|
import {Decrypt as RawDecrypt} from "@/decrypt/raw";
|
||||||
import {Decrypt as TmDecrypt} from "@/decrypt/tm";
|
import {Decrypt as TmDecrypt} from "@/decrypt/tm";
|
||||||
import {DecryptResult, FileInfo} from "@/decrypt/entity";
|
import {DecryptResult, FileInfo} from "@/decrypt/entity";
|
||||||
|
import {SplitFilename} from "@/decrypt/utils";
|
||||||
|
|
||||||
|
|
||||||
export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
|
export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
|
||||||
let raw_ext = file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length).toLowerCase();
|
const raw = SplitFilename(file.name)
|
||||||
let raw_filename = file.name.substring(0, file.name.lastIndexOf("."));
|
|
||||||
let rt_data: DecryptResult;
|
let rt_data: DecryptResult;
|
||||||
switch (raw_ext) {
|
switch (raw.ext) {
|
||||||
case "ncm":// Netease Mp3/Flac
|
case "ncm":// Netease Mp3/Flac
|
||||||
rt_data = await NcmDecrypt(file.raw, raw_filename, raw_ext);
|
rt_data = await NcmDecrypt(file.raw, raw.name, raw.ext);
|
||||||
|
break;
|
||||||
|
case "uc":// Netease Cache
|
||||||
|
rt_data = await NcmCacheDecrypt(file.raw, raw.name, raw.ext);
|
||||||
break;
|
break;
|
||||||
case "kwm":// Kuwo Mp3/Flac
|
case "kwm":// Kuwo Mp3/Flac
|
||||||
rt_data = await KwmDecrypt(file.raw, raw_filename, raw_ext);
|
rt_data = await KwmDecrypt(file.raw, raw.name, raw.ext);
|
||||||
break
|
break
|
||||||
case "xm": // Xiami Wav/M4a/Mp3/Flac
|
case "xm": // Xiami Wav/M4a/Mp3/Flac
|
||||||
case "wav":// Xiami/Raw Wav
|
case "wav":// Xiami/Raw Wav
|
||||||
case "mp3":// Xiami/Raw Mp3
|
case "mp3":// Xiami/Raw Mp3
|
||||||
case "flac":// Xiami/Raw Flac
|
case "flac":// Xiami/Raw Flac
|
||||||
case "m4a":// Xiami/Raw M4a
|
case "m4a":// Xiami/Raw M4a
|
||||||
rt_data = await XmDecrypt(file.raw, raw_filename, raw_ext);
|
rt_data = await XmDecrypt(file.raw, raw.name, raw.ext);
|
||||||
break;
|
break;
|
||||||
case "ogg":// Raw Ogg
|
case "ogg":// Raw Ogg
|
||||||
rt_data = await RawDecrypt(file.raw, raw_filename, raw_ext);
|
rt_data = await RawDecrypt(file.raw, raw.name, raw.ext);
|
||||||
break;
|
break;
|
||||||
case "tm0":// QQ Music IOS Mp3
|
case "tm0":// QQ Music IOS Mp3
|
||||||
case "tm3":// QQ Music IOS Mp3
|
case "tm3":// QQ Music IOS Mp3
|
||||||
rt_data = await RawDecrypt(file.raw, raw_filename, "mp3");
|
rt_data = await RawDecrypt(file.raw, raw.name, "mp3");
|
||||||
break;
|
break;
|
||||||
case "qmc3"://QQ Music Android Mp3
|
case "qmc3"://QQ Music Android Mp3
|
||||||
case "qmc2"://QQ Music Android Ogg
|
case "qmc2"://QQ Music Android Ogg
|
||||||
@@ -48,23 +53,26 @@ export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
|
|||||||
case "6f6767"://QQ Music Weiyun Ogg
|
case "6f6767"://QQ Music Weiyun Ogg
|
||||||
case "6d3461"://QQ Music Weiyun M4a
|
case "6d3461"://QQ Music Weiyun M4a
|
||||||
case "776176"://QQ Music Weiyun Wav
|
case "776176"://QQ Music Weiyun Wav
|
||||||
rt_data = await QmcDecrypt(file.raw, raw_filename, raw_ext);
|
rt_data = await QmcDecrypt(file.raw, raw.name, raw.ext);
|
||||||
break;
|
break;
|
||||||
case "tm2":// QQ Music IOS M4a
|
case "tm2":// QQ Music IOS M4a
|
||||||
case "tm6":// QQ Music IOS M4a
|
case "tm6":// QQ Music IOS M4a
|
||||||
rt_data = await TmDecrypt(file.raw, raw_filename);
|
rt_data = await TmDecrypt(file.raw, raw.name);
|
||||||
|
break;
|
||||||
|
case "cache"://QQ Music Cache
|
||||||
|
rt_data = await QmcCacheDecrypt(file.raw, raw.name, raw.ext);
|
||||||
break;
|
break;
|
||||||
case "vpr":
|
case "vpr":
|
||||||
case "kgm":
|
case "kgm":
|
||||||
case "kgma":
|
case "kgma":
|
||||||
rt_data = await KgmDecrypt(file.raw, raw_filename, raw_ext);
|
rt_data = await KgmDecrypt(file.raw, raw.name, raw.ext);
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
throw "不支持此文件格式"
|
throw "不支持此文件格式"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rt_data.rawExt) rt_data.rawExt = raw_ext;
|
if (!rt_data.rawExt) rt_data.rawExt = raw.ext;
|
||||||
if (!rt_data.rawFilename) rt_data.rawFilename = raw_filename;
|
if (!rt_data.rawFilename) rt_data.rawFilename = raw.name;
|
||||||
console.log(rt_data);
|
console.log(rt_data);
|
||||||
return rt_data;
|
return rt_data;
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,7 @@ import {
|
|||||||
} 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";
|
import {DecryptResult} from "@/decrypt/entity";
|
||||||
|
import config from "@/../package.json"
|
||||||
|
|
||||||
const VprHeader = [
|
const VprHeader = [
|
||||||
0x05, 0x28, 0xBC, 0x96, 0xE9, 0xE4, 0x5A, 0x43,
|
0x05, 0x28, 0xBC, 0x96, 0xE9, 0xE4, 0x5A, 0x43,
|
||||||
@@ -22,9 +23,6 @@ const VprMaskDiff = [
|
|||||||
|
|
||||||
|
|
||||||
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
|
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
|
||||||
if (window?.location?.protocol === "file:") {
|
|
||||||
throw Error("请使用 <a target='_blank' href='https://github.com/unlock-music/cli'>CLI版本</a> 进行解锁")
|
|
||||||
}
|
|
||||||
|
|
||||||
const oriData = new Uint8Array(await GetArrayBuffer(file));
|
const oriData = new Uint8Array(await GetArrayBuffer(file));
|
||||||
if (raw_ext === "vpr") {
|
if (raw_ext === "vpr") {
|
||||||
@@ -84,8 +82,16 @@ function GetMask(pos: number) {
|
|||||||
let MaskV2: Uint8Array = new Uint8Array(0);
|
let MaskV2: Uint8Array = new Uint8Array(0);
|
||||||
|
|
||||||
async function LoadMaskV2(): Promise<boolean> {
|
async function LoadMaskV2(): Promise<boolean> {
|
||||||
|
let mask_url = `https://cdn.jsdelivr.net/gh/unlock-music/unlock-music@${config.version}/public/static/kgm.mask`
|
||||||
|
if (["http:", "https:"].some(v => v == self.location.protocol)) {
|
||||||
|
if (!!self.document) {// using Web Worker
|
||||||
|
mask_url = "./static/kgm.mask"
|
||||||
|
} else {// using Main thread
|
||||||
|
mask_url = "../static/kgm.mask"
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("./static/kgm.mask", {method: "GET"})
|
const resp = await fetch(mask_url, {method: "GET"})
|
||||||
MaskV2 = new Uint8Array(await resp.arrayBuffer());
|
MaskV2 = new Uint8Array(await resp.arrayBuffer());
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@@ -11,11 +11,18 @@ import {
|
|||||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
|
import {parseBlob as metaParseBlob} from "music-metadata-browser";
|
||||||
import jimp from 'jimp';
|
import jimp from 'jimp';
|
||||||
|
|
||||||
import CryptoJS from "crypto-js";
|
import AES from "crypto-js/aes";
|
||||||
|
import PKCS7 from "crypto-js/pad-pkcs7";
|
||||||
|
import ModeECB from "crypto-js/mode-ecb";
|
||||||
|
import WordArray from "crypto-js/lib-typedarrays";
|
||||||
|
import Base64 from "crypto-js/enc-base64";
|
||||||
|
import EncUTF8 from "crypto-js/enc-utf8";
|
||||||
|
import EncHex from "crypto-js/enc-hex";
|
||||||
|
|
||||||
import {DecryptResult} from "@/decrypt/entity";
|
import {DecryptResult} from "@/decrypt/entity";
|
||||||
|
|
||||||
const CORE_KEY = CryptoJS.enc.Hex.parse("687a4852416d736f356b496e62617857");
|
const CORE_KEY = EncHex.parse("687a4852416d736f356b496e62617857");
|
||||||
const META_KEY = CryptoJS.enc.Hex.parse("2331346C6A6B5F215C5D2630553C2728");
|
const META_KEY = EncHex.parse("2331346C6A6B5F215C5D2630553C2728");
|
||||||
const MagicHeader = [0x43, 0x54, 0x45, 0x4E, 0x46, 0x44, 0x41, 0x4D];
|
const MagicHeader = [0x43, 0x54, 0x45, 0x4E, 0x46, 0x44, 0x41, 0x4D];
|
||||||
|
|
||||||
|
|
||||||
@@ -67,11 +74,11 @@ class NcmDecrypt {
|
|||||||
.map(uint8 => uint8 ^ 0x64);
|
.map(uint8 => uint8 ^ 0x64);
|
||||||
this.offset += keyLen;
|
this.offset += keyLen;
|
||||||
|
|
||||||
const plainText = CryptoJS.AES.decrypt(
|
const plainText = AES.decrypt(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
{ciphertext: CryptoJS.lib.WordArray.create(cipherText)},
|
{ciphertext: WordArray.create(cipherText)},
|
||||||
CORE_KEY,
|
CORE_KEY,
|
||||||
{mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7}
|
{mode: ModeECB, padding: PKCS7}
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = new Uint8Array(plainText.sigBytes);
|
const result = new Uint8Array(plainText.sigBytes);
|
||||||
@@ -115,17 +122,18 @@ class NcmDecrypt {
|
|||||||
.map(data => data ^ 0x63);
|
.map(data => data ^ 0x63);
|
||||||
this.offset += metaDataLen;
|
this.offset += metaDataLen;
|
||||||
|
|
||||||
const plainText = CryptoJS.AES.decrypt(
|
WordArray.create()
|
||||||
//@ts-ignore
|
const plainText = AES.decrypt(
|
||||||
|
// @ts-ignore
|
||||||
{
|
{
|
||||||
ciphertext: CryptoJS.enc.Base64.parse(
|
ciphertext: Base64.parse(
|
||||||
//@ts-ignore
|
// @ts-ignore
|
||||||
CryptoJS.lib.WordArray.create(cipherText.slice(22)).toString(CryptoJS.enc.Utf8)
|
WordArray.create(cipherText.slice(22)).toString(EncUTF8)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
META_KEY,
|
META_KEY,
|
||||||
{mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7}
|
{mode: ModeECB, padding: PKCS7}
|
||||||
).toString(CryptoJS.enc.Utf8);
|
).toString(EncUTF8);
|
||||||
|
|
||||||
const labelIndex = plainText.indexOf(":");
|
const labelIndex = plainText.indexOf(":");
|
||||||
let result: NcmMusicMeta;
|
let result: NcmMusicMeta;
|
||||||
|
29
src/decrypt/ncmcache.ts
Normal file
29
src/decrypt/ncmcache.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import {AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt} from "@/decrypt/utils.ts";
|
||||||
|
|
||||||
|
import {DecryptResult} from "@/decrypt/entity";
|
||||||
|
|
||||||
|
import {parseBlob as metaParseBlob} from "music-metadata-browser";
|
||||||
|
|
||||||
|
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
|
||||||
|
: Promise<DecryptResult> {
|
||||||
|
const buffer = new Uint8Array(await GetArrayBuffer(file));
|
||||||
|
let length = buffer.length
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
buffer[i] ^= 163
|
||||||
|
}
|
||||||
|
const ext = SniffAudioExt(buffer, raw_ext);
|
||||||
|
if (ext !== raw_ext) file = new Blob([buffer], {type: AudioMimeType[ext]})
|
||||||
|
const tag = await metaParseBlob(file);
|
||||||
|
const {title, artist} = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist)
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
artist,
|
||||||
|
ext,
|
||||||
|
album: tag.common.album,
|
||||||
|
picture: GetCoverFromFile(tag),
|
||||||
|
file: URL.createObjectURL(file),
|
||||||
|
blob: file,
|
||||||
|
mime: AudioMimeType[ext]
|
||||||
|
}
|
||||||
|
}
|
@@ -22,9 +22,11 @@ interface Handler {
|
|||||||
handler(data?: Uint8Array): QmcMask | undefined
|
handler(data?: Uint8Array): QmcMask | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const HandlerMap: { [key: string]: Handler } = {
|
export const HandlerMap: { [key: string]: Handler } = {
|
||||||
"mgg": {handler: QmcMaskDetectMgg, ext: "ogg", detect: true},
|
"mgg": {handler: QmcMaskDetectMgg, ext: "ogg", detect: true},
|
||||||
"mflac": {handler: QmcMaskDetectMflac, ext: "flac", detect: true},
|
"mflac": {handler: QmcMaskDetectMflac, ext: "flac", detect: true},
|
||||||
|
"mgg.cache": {handler: QmcMaskDetectMgg, ext: "ogg", detect: false},
|
||||||
|
"mflac.cache": {handler: QmcMaskDetectMflac, ext: "flac", detect: false},
|
||||||
"qmc0": {handler: QmcMaskGetDefault, ext: "mp3", detect: false},
|
"qmc0": {handler: QmcMaskGetDefault, ext: "mp3", detect: false},
|
||||||
"qmc2": {handler: QmcMaskGetDefault, ext: "ogg", detect: false},
|
"qmc2": {handler: QmcMaskGetDefault, ext: "ogg", detect: false},
|
||||||
"qmc3": {handler: QmcMaskGetDefault, ext: "mp3", detect: false},
|
"qmc3": {handler: QmcMaskGetDefault, ext: "mp3", detect: false},
|
||||||
@@ -40,8 +42,8 @@ const HandlerMap: { [key: string]: Handler } = {
|
|||||||
"776176": {handler: QmcMaskGetDefault, ext: "wav", detect: false}
|
"776176": {handler: QmcMaskGetDefault, ext: "wav", detect: false}
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
|
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
|
||||||
if (!(raw_ext in HandlerMap)) throw "File type is incorrect!";
|
if (!(raw_ext in HandlerMap)) throw `Qmc cannot handle type: ${raw_ext}`;
|
||||||
const handler = HandlerMap[raw_ext];
|
const handler = HandlerMap[raw_ext];
|
||||||
|
|
||||||
const fileData = new Uint8Array(await GetArrayBuffer(file));
|
const fileData = new Uint8Array(await GetArrayBuffer(file));
|
||||||
@@ -57,6 +59,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
|
|||||||
} else {
|
} else {
|
||||||
audioData = fileData;
|
audioData = fileData;
|
||||||
seed = handler.handler(audioData) as QmcMask;
|
seed = handler.handler(audioData) as QmcMask;
|
||||||
|
if (!seed) throw raw_ext + "格式仅提供实验性支持";
|
||||||
}
|
}
|
||||||
let musicDecoded = seed.Decrypt(audioData);
|
let musicDecoded = seed.Decrypt(audioData);
|
||||||
|
|
||||||
|
51
src/decrypt/qmccache.ts
Normal file
51
src/decrypt/qmccache.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
AudioMimeType,
|
||||||
|
GetArrayBuffer,
|
||||||
|
GetCoverFromFile,
|
||||||
|
GetMetaFromFile,
|
||||||
|
SniffAudioExt,
|
||||||
|
SplitFilename
|
||||||
|
} from "@/decrypt/utils.ts";
|
||||||
|
|
||||||
|
import {Decrypt as QmcDecrypt, HandlerMap} from "@/decrypt/qmc";
|
||||||
|
|
||||||
|
import {DecryptResult} from "@/decrypt/entity";
|
||||||
|
|
||||||
|
import {parseBlob as metaParseBlob} from "music-metadata-browser";
|
||||||
|
|
||||||
|
export async function Decrypt(file: Blob, raw_filename: string, _: string)
|
||||||
|
: Promise<DecryptResult> {
|
||||||
|
const buffer = new Uint8Array(await GetArrayBuffer(file));
|
||||||
|
let length = buffer.length
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
buffer[i] ^= 0xf4
|
||||||
|
if (buffer[i] <= 0x3f) buffer[i] = buffer[i] * 4;
|
||||||
|
else if (buffer[i] <= 0x7f) buffer[i] = (buffer[i] - 0x40) * 4 + 1;
|
||||||
|
else if (buffer[i] <= 0xbf) buffer[i] = (buffer[i] - 0x80) * 4 + 2;
|
||||||
|
else buffer[i] = (buffer[i] - 0xc0) * 4 + 3;
|
||||||
|
}
|
||||||
|
let ext = SniffAudioExt(buffer, "");
|
||||||
|
const newName = SplitFilename(raw_filename)
|
||||||
|
let audioBlob: Blob
|
||||||
|
if (ext !== "" || newName.ext === "mp3") {
|
||||||
|
audioBlob = new Blob([buffer], {type: AudioMimeType[ext]})
|
||||||
|
} else if (newName.ext in HandlerMap) {
|
||||||
|
audioBlob = new Blob([buffer], {type: "application/octet-stream"})
|
||||||
|
return QmcDecrypt(audioBlob, newName.name, newName.ext);
|
||||||
|
} else {
|
||||||
|
throw "不支持的QQ音乐缓存格式"
|
||||||
|
}
|
||||||
|
const tag = await metaParseBlob(audioBlob);
|
||||||
|
const {title, artist} = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist)
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
artist,
|
||||||
|
ext,
|
||||||
|
album: tag.common.album,
|
||||||
|
picture: GetCoverFromFile(tag),
|
||||||
|
file: URL.createObjectURL(audioBlob),
|
||||||
|
blob: audioBlob,
|
||||||
|
mime: AudioMimeType[ext]
|
||||||
|
}
|
||||||
|
}
|
@@ -12,13 +12,16 @@ export const WMA_HEADER = [
|
|||||||
]
|
]
|
||||||
export const WAV_HEADER = [0x52, 0x49, 0x46, 0x46]
|
export const WAV_HEADER = [0x52, 0x49, 0x46, 0x46]
|
||||||
export const AAC_HEADER = [0xFF, 0xF1]
|
export const AAC_HEADER = [0xFF, 0xF1]
|
||||||
|
export const DFF_HEADER = [0x46, 0x52, 0x4D, 0x38]
|
||||||
|
|
||||||
export const AudioMimeType: { [key: string]: string } = {
|
export const AudioMimeType: { [key: string]: string } = {
|
||||||
mp3: "audio/mpeg",
|
mp3: "audio/mpeg",
|
||||||
flac: "audio/flac",
|
flac: "audio/flac",
|
||||||
m4a: "audio/mp4",
|
m4a: "audio/mp4",
|
||||||
ogg: "audio/ogg",
|
ogg: "audio/ogg",
|
||||||
wma: "audio/x-ms-wma",
|
wma: "audio/x-ms-wma",
|
||||||
wav: "audio/x-wav"
|
wav: "audio/x-wav",
|
||||||
|
dff: "audio/x-dff"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -39,6 +42,7 @@ export function SniffAudioExt(data: Uint8Array, fallback_ext: string = "mp3"): s
|
|||||||
if (BytesHasPrefix(data, WAV_HEADER)) return "wav"
|
if (BytesHasPrefix(data, WAV_HEADER)) return "wav"
|
||||||
if (BytesHasPrefix(data, WMA_HEADER)) return "wma"
|
if (BytesHasPrefix(data, WMA_HEADER)) return "wma"
|
||||||
if (BytesHasPrefix(data, AAC_HEADER)) return "aac"
|
if (BytesHasPrefix(data, AAC_HEADER)) return "aac"
|
||||||
|
if (BytesHasPrefix(data, DFF_HEADER)) return "dff"
|
||||||
return fallback_ext;
|
return fallback_ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,3 +160,11 @@ export function WriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: I
|
|||||||
}
|
}
|
||||||
return writer.save()
|
return writer.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SplitFilename(n: string): { name: string; ext: string } {
|
||||||
|
const pos = n.lastIndexOf(".")
|
||||||
|
return {
|
||||||
|
ext: n.substring(pos + 1).toLowerCase(),
|
||||||
|
name: n.substring(0, pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
9
src/shims-fs.d.ts
vendored
9
src/shims-fs.d.ts
vendored
@@ -6,6 +6,10 @@ interface FileSystemCreateWritableOptions {
|
|||||||
keepExistingData?: boolean
|
keepExistingData?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FileSystemRemoveOptions {
|
||||||
|
recursive?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
interface FileSystemFileHandle {
|
interface FileSystemFileHandle {
|
||||||
getFile(): Promise<File>;
|
getFile(): Promise<File>;
|
||||||
|
|
||||||
@@ -37,13 +41,16 @@ interface FileSystemWritableFileStream extends WritableStream {
|
|||||||
close(): Promise<undefined> // should be implemented in WritableStream
|
close(): Promise<undefined> // should be implemented in WritableStream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export declare interface FileSystemDirectoryHandle {
|
export declare interface FileSystemDirectoryHandle {
|
||||||
getFileHandle(name: string, options?: FileSystemGetFileOptions): Promise<FileSystemFileHandle>
|
getFileHandle(name: string, options?: FileSystemGetFileOptions): Promise<FileSystemFileHandle>
|
||||||
|
|
||||||
|
removeEntry(name: string, options?: FileSystemRemoveOptions): Promise<undefined>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
FileSystemDirectoryHandle
|
|
||||||
|
|
||||||
showDirectoryPicker?(): Promise<FileSystemDirectoryHandle>
|
showDirectoryPicker?(): Promise<FileSystemDirectoryHandle>
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import {fromByteArray as Base64Encode} from "base64-js";
|
import {fromByteArray as Base64Encode} from "base64-js";
|
||||||
|
|
||||||
export const IXAREA_API_ENDPOINT = "https://stats.ixarea.com/apis"
|
export const IXAREA_API_ENDPOINT = "https://um-api.ixarea.com"
|
||||||
|
|
||||||
export interface UpdateInfo {
|
export interface UpdateInfo {
|
||||||
Found: boolean
|
Found: boolean
|
||||||
|
@@ -144,9 +144,9 @@ export default {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.dir = await window.showDirectoryPicker()
|
this.dir = await window.showDirectoryPicker()
|
||||||
window.dir = this.dir
|
const test_filename = "__unlock_music_write_test.txt"
|
||||||
window.f = await this.dir.getFileHandle("write-test.txt", {create: true})
|
await this.dir.getFileHandle(test_filename, {create: true})
|
||||||
|
await this.dir.removeEntry(test_filename)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
|
@@ -26,7 +26,8 @@
|
|||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"scripthost"
|
"scripthost"
|
||||||
]
|
],
|
||||||
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
|
Reference in New Issue
Block a user