Compare commits

..

No commits in common. "1b116a8db3576c868238d611153faa7f9805cff4" and "a75ca7aabb37dd3d62fae7dcfaf350e02b058915" have entirely different histories.

9 changed files with 47 additions and 68 deletions

View File

@ -24,7 +24,7 @@
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@reduxjs/toolkit": "^2.0.1", "@reduxjs/toolkit": "^2.0.1",
"@um/libparakeet": "0.4.5", "@um/libparakeet": "0.4.5",
"@unlock-music/crypto": "0.0.0-alpha.12", "@unlock-music/crypto": "0.0.0-alpha.11",
"framer-motion": "^10.16.16", "framer-motion": "^10.16.16",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"radash": "^11.0.0", "radash": "^11.0.0",

View File

@ -42,8 +42,8 @@ importers:
specifier: 0.4.5 specifier: 0.4.5
version: 0.4.5 version: 0.4.5
'@unlock-music/crypto': '@unlock-music/crypto':
specifier: 0.0.0-alpha.12 specifier: 0.0.0-alpha.11
version: 0.0.0-alpha.12 version: 0.0.0-alpha.11
framer-motion: framer-motion:
specifier: ^10.16.16 specifier: ^10.16.16
version: 10.16.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0) version: 10.16.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@ -1927,8 +1927,8 @@ packages:
'@ungap/structured-clone@1.2.0': '@ungap/structured-clone@1.2.0':
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
'@unlock-music/crypto@0.0.0-alpha.12': '@unlock-music/crypto@0.0.0-alpha.11':
resolution: {integrity: sha512-Q24cq653CmD8sj/D1M6wHYtXJIX3YIgnvbPtO+aHnY07J0ZXvkqNh+6a3hBrGGLYzcSWioAw2xxf2rFEQ3q35A==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.12/crypto-0.0.0-alpha.12.tgz} resolution: {integrity: sha512-lA3xryziHULhkPbuQFI2HrfwDREUD9YoaZOTMQqcu/8mKF2/hA3sCK0Uoq0miYr+7VUbE5sMBvl9dcrnCI1UWA==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.11/crypto-0.0.0-alpha.11.tgz}
'@vitejs/plugin-react@4.2.1': '@vitejs/plugin-react@4.2.1':
resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==}
@ -6118,7 +6118,7 @@ snapshots:
'@ungap/structured-clone@1.2.0': {} '@ungap/structured-clone@1.2.0': {}
'@unlock-music/crypto@0.0.0-alpha.12': {} '@unlock-music/crypto@0.0.0-alpha.11': {}
'@vitejs/plugin-react@4.2.1(vite@5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.27.0))': '@vitejs/plugin-react@4.2.1(vite@5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.27.0))':
dependencies: dependencies:

View File

@ -3,7 +3,6 @@ import { TransparentDecipher } from './decipher/Transparent.ts';
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts'; import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
import { QQMusicV1Decipher, QQMusicV2Decipher } from '~/decrypt-worker/decipher/QQMusic.ts'; import { QQMusicV1Decipher, QQMusicV2Decipher } from '~/decrypt-worker/decipher/QQMusic.ts';
import { KuwoMusicDecipher } from '~/decrypt-worker/decipher/KuwoMusic.ts'; import { KuwoMusicDecipher } from '~/decrypt-worker/decipher/KuwoMusic.ts';
import { KugouMusicDecipher } from '~/decrypt-worker/decipher/KugouMusic.ts';
export enum Status { export enum Status {
OK = 0, OK = 0,
@ -41,7 +40,7 @@ export const allCryptoFactories: DecipherFactory[] = [
NetEaseCloudMusicDecipher.make, NetEaseCloudMusicDecipher.make,
// KGM (*.kgm, *.vpr) // KGM (*.kgm, *.vpr)
KugouMusicDecipher.make, // KGMCrypto.make,
// KWMv1 (*.kwm) // KWMv1 (*.kwm)
KuwoMusicDecipher.make, KuwoMusicDecipher.make,

View File

@ -1,36 +0,0 @@
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers';
import { KuGouDecipher, KuGouHeader } from '@unlock-music/crypto';
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
export class KugouMusicDecipher implements DecipherInstance {
cipherName = 'Kugou';
async decrypt(buffer: Uint8Array, _options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> {
let kgm: KuGouDecipher | undefined;
let header: KuGouHeader | undefined;
try {
header = KuGouHeader.parse(buffer.subarray(0, 0x400));
kgm = new KuGouDecipher(header);
const audioBuffer = new Uint8Array(buffer.subarray(0x400));
for (const [block, offset] of chunkBuffer(audioBuffer)) {
kgm.decrypt(block, offset);
}
return {
status: Status.OK,
cipherName: this.cipherName,
data: audioBuffer,
};
} finally {
kgm?.free();
header?.free();
}
}
public static make() {
return new KugouMusicDecipher();
}
}

View File

@ -1,5 +1,5 @@
import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers'; import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-worker/Deciphers';
import { KuwoHeader, KWMDecipher } from '@unlock-music/crypto'; import { KuwoHeader, KWMCipher } from '@unlock-music/crypto';
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts'; import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts'; import { chunkBuffer } from '~/decrypt-worker/util/buffer.ts';
@ -8,11 +8,11 @@ export class KuwoMusicDecipher implements DecipherInstance {
async decrypt(buffer: Uint8Array, options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> { async decrypt(buffer: Uint8Array, options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> {
let header: KuwoHeader | undefined; let header: KuwoHeader | undefined;
let kwm: KWMDecipher | undefined; let kwm: KWMCipher | undefined;
try { try {
header = KuwoHeader.parse(buffer.subarray(0, 0x400)); header = KuwoHeader.parse(buffer.subarray(0, 0x400));
kwm = new KWMDecipher(header, options.kwm2key); kwm = header.makeCipher(options.kwm2key);
const audioBuffer = new Uint8Array(buffer.subarray(0x400)); const audioBuffer = new Uint8Array(buffer.subarray(0x400));
for (const [block, offset] of chunkBuffer(audioBuffer)) { for (const [block, offset] of chunkBuffer(audioBuffer)) {

View File

@ -19,7 +19,7 @@ class DecryptCommandHandler {
this.label = `DecryptCommandHandler(${label})`; this.label = `DecryptCommandHandler(${label})`;
} }
log<R>(label: string, fn: () => Promise<R>): Promise<R> { log<R>(label: string, fn: () => R): R {
return timedLogger(`${this.label}: ${label}`, fn); return timedLogger(`${this.label}: ${label}`, fn);
} }
@ -52,7 +52,7 @@ class DecryptCommandHandler {
} }
async tryDecryptWith(decipher: DecipherInstance) { async tryDecryptWith(decipher: DecipherInstance) {
const result = await this.log(`try decrypt with ${decipher.cipherName}`, async () => const result = await this.log(`decrypt ${decipher.cipherName}`, async () =>
decipher.decrypt(this.buffer, this.options), decipher.decrypt(this.buffer, this.options),
); );
switch (result.status) { switch (result.status) {
@ -79,9 +79,8 @@ class DecryptCommandHandler {
} }
} }
export const workerDecryptHandler = async ({ id: payloadId, blobURI, options }: DecryptCommandPayload) => { export const workerDecryptHandler = async ({ id, blobURI, options }: DecryptCommandPayload) => {
await umCryptoReady; await umCryptoReady;
const id = payloadId.replace('://', ':');
const label = `decrypt(${id})`; const label = `decrypt(${id})`;
return withTimeGroupedLogs(label, async () => { return withTimeGroupedLogs(label, async () => {
const buffer = await fetch(blobURI).then((r) => r.arrayBuffer()); const buffer = await fetch(blobURI).then((r) => r.arrayBuffer());

View File

@ -1,16 +1,26 @@
import { fetchParakeet, FooterParserState } from '@um/libparakeet'; import { fetchParakeet, FooterParserState } from '@um/libparakeet';
import type { FetchMusicExNamePayload } from '~/decrypt-worker/types'; import type { FetchMusicExNamePayload } from '~/decrypt-worker/types';
import { makeQMCv2FooterParser } from '~/decrypt-worker/util/qmc2KeyCrypto'; import { makeQMCv2FooterParser } from '~/decrypt-worker/util/qmc2KeyCrypto';
import { timedLogger } from '~/util/logUtils.ts'; import { timedLogger, withGroupedLogs as withTimeGroupedLogs } from '~/util/logUtils';
export const workerParseMusicExMediaName = async ({ id, blobURI }: FetchMusicExNamePayload) => { export const workerParseMusicExMediaName = async ({ id, blobURI }: FetchMusicExNamePayload) => {
const label = `qmcMusixEx(${id.replace('://', ':')})`; const label = `decrypt(${id})`;
return timedLogger(label, async () => { return withTimeGroupedLogs(label, async () => {
const parakeet = await fetchParakeet(); const parakeet = await timedLogger(`${label}/init`, fetchParakeet);
const blob = await fetch(blobURI, { headers: { Range: 'bytes=-1024' } }).then((r) => r.blob()); const blob = await timedLogger(`${label}/fetch-src`, async () =>
const buffer = await blob.arrayBuffer(); fetch(blobURI, { headers: { Range: 'bytes=-1024' } }).then((r) => r.blob()),
);
const parsed = makeQMCv2FooterParser(parakeet).parse(buffer.slice(-1024)); const buffer = await timedLogger(`${label}/read-src`, async () => {
// Firefox: the range header does not work...?
const blobBuffer = await blob.arrayBuffer();
if (blobBuffer.byteLength > 1024) {
return blobBuffer.slice(-1024);
}
return blobBuffer;
});
const parsed = makeQMCv2FooterParser(parakeet).parse(buffer);
if (parsed.state === FooterParserState.OK) { if (parsed.state === FooterParserState.OK) {
return parsed.mediaName; return parsed.mediaName;
} }

View File

@ -1,13 +1,20 @@
export async function wrapFunctionCall<R = unknown>( function isPromise<T = unknown>(p: unknown): p is Promise<T> {
pre: () => void, return !!p && typeof p === 'object' && 'then' in p && 'catch' in p && 'finally' in p;
post: () => void, }
fn: () => Promise<R>,
): Promise<R> { export function wrapFunctionCall<R = unknown>(pre: () => void, post: () => void, fn: () => R): R {
pre(); pre();
try { try {
return await fn(); const result = fn();
} finally {
if (isPromise(result)) {
result.finally(post);
}
return result;
} catch (e) {
post(); post();
throw e;
} }
} }

View File

@ -1,6 +1,6 @@
import { wrapFunctionCall } from './fnWrapper'; import { wrapFunctionCall } from './fnWrapper';
export async function timedLogger<R = unknown>(label: string, fn: () => Promise<R>): Promise<R> { export function timedLogger<R = unknown>(label: string, fn: () => R): R {
if (import.meta.env.VITE_ENABLE_PERF_LOG !== '1') { if (import.meta.env.VITE_ENABLE_PERF_LOG !== '1') {
return fn(); return fn();
} else { } else {
@ -12,13 +12,13 @@ export async function timedLogger<R = unknown>(label: string, fn: () => Promise<
} }
} }
export async function withGroupedLogs<R = unknown>(label: string, fn: () => Promise<R>): Promise<R> { export function withGroupedLogs<R = unknown>(label: string, fn: () => R): R {
if (import.meta.env.VITE_ENABLE_PERF_LOG !== '1') { if (import.meta.env.VITE_ENABLE_PERF_LOG !== '1') {
return fn(); return fn();
} else { } else {
return wrapFunctionCall( return wrapFunctionCall(
() => console.group(label), () => console.group(label),
() => console.groupEnd(), () => (console.groupEnd as (label: string) => void)(label),
() => timedLogger(`${label}/total`, fn), () => timedLogger(`${label}/total`, fn),
); );
} }