Upgrade to use @unlock-music/crypto #78
@ -23,7 +23,7 @@
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@unlock-music/crypto": "0.0.0-alpha.16",
|
||||
"@unlock-music/crypto": "0.0.0-alpha.18",
|
||||
"framer-motion": "^10.16.16",
|
||||
"nanoid": "^5.0.4",
|
||||
"radash": "^11.0.0",
|
||||
|
@ -39,8 +39,8 @@ importers:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1(react-redux@9.0.4(@types/react@18.2.45)(react@18.2.0)(redux@5.0.0))(react@18.2.0)
|
||||
'@unlock-music/crypto':
|
||||
specifier: 0.0.0-alpha.16
|
||||
version: 0.0.0-alpha.16
|
||||
specifier: 0.0.0-alpha.18
|
||||
version: 0.0.0-alpha.18
|
||||
framer-motion:
|
||||
specifier: ^10.16.16
|
||||
version: 10.16.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@ -1921,8 +1921,8 @@ packages:
|
||||
'@ungap/structured-clone@1.2.0':
|
||||
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
||||
|
||||
'@unlock-music/crypto@0.0.0-alpha.16':
|
||||
resolution: {integrity: sha512-QDhDWTsZOOkSQ64d3+pBwPCfesit6MiO63SzV7TQB8rQ/lPb78MMz6C4ECIjP9ijCBXds4PT4CBXp8vJDifAEg==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.16/crypto-0.0.0-alpha.16.tgz}
|
||||
'@unlock-music/crypto@0.0.0-alpha.18':
|
||||
resolution: {integrity: sha512-/2VgU15WCW+GnWOgYd18GylQTkCGgAW7cf/qBcjXWU+2qtsvv9heiN8LkIWWz+NMbO0XDRONXijAKdKYf38o/g==, tarball: https://git.unlock-music.dev/api/packages/um/npm/%40unlock-music%2Fcrypto/-/0.0.0-alpha.18/crypto-0.0.0-alpha.18.tgz}
|
||||
|
||||
'@vitejs/plugin-react@4.2.1':
|
||||
resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==}
|
||||
@ -6110,7 +6110,7 @@ snapshots:
|
||||
|
||||
'@ungap/structured-clone@1.2.0': {}
|
||||
|
||||
'@unlock-music/crypto@0.0.0-alpha.16': {}
|
||||
'@unlock-music/crypto@0.0.0-alpha.18': {}
|
||||
|
||||
'@vitejs/plugin-react@4.2.1(vite@5.0.10(@types/node@20.10.5)(sass@1.69.5)(terser@5.27.0))':
|
||||
dependencies:
|
||||
|
@ -1,27 +0,0 @@
|
||||
import { bytesToUTF8String } from '~/decrypt-worker/util/utf8Encoder';
|
||||
import { strlen } from './strlen';
|
||||
|
||||
export interface KuwoHeader {
|
||||
rid: string; // uint64
|
||||
encVersion: 1 | 2; // uint32
|
||||
quality: string;
|
||||
}
|
||||
|
||||
const KUWO_MAGIC_HDRS = new Set(['yeelion-kuwo\x00\x00\x00\x00', 'yeelion-kuwo-tme']);
|
||||
|
||||
export function parseKuwoHeader(view: DataView): KuwoHeader | null {
|
||||
const magic = view.buffer.slice(view.byteOffset, view.byteOffset + 0x10);
|
||||
if (!KUWO_MAGIC_HDRS.has(bytesToUTF8String(magic))) {
|
||||
return null; // not kuwo-encrypted file
|
||||
}
|
||||
|
||||
const qualityBytes = new Uint8Array(view.buffer.slice(view.byteOffset + 0x30, view.byteOffset + 0x40));
|
||||
const qualityLen = strlen(qualityBytes);
|
||||
const quality = bytesToUTF8String(qualityBytes.slice(0, qualityLen));
|
||||
|
||||
return {
|
||||
encVersion: view.getUint32(0x10, true) as 1 | 2,
|
||||
rid: view.getUint32(0x18, true).toString(),
|
||||
quality,
|
||||
};
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
export function strlen(data: Uint8Array): number {
|
||||
const n = data.byteLength;
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (data[i] === 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
export enum DECRYPTION_WORKER_ACTION_NAME {
|
||||
DECRYPT = 'DECRYPT',
|
||||
FIND_QMC_MUSICEX_NAME = 'FIND_QMC_MUSICEX_NAME',
|
||||
KUWO_PARSE_HEADER = 'KUWO_PARSE_HEADER',
|
||||
QINGTING_FM_GET_DEVICE_KEY = 'QINGTING_FM_GET_DEVICE_KEY',
|
||||
VERSION = 'VERSION',
|
||||
}
|
||||
|
@ -12,10 +12,18 @@ export interface DecryptCommandPayload {
|
||||
}
|
||||
|
||||
export interface FetchMusicExNamePayload {
|
||||
id: string;
|
||||
blobURI: string;
|
||||
}
|
||||
|
||||
export interface ParseKuwoHeaderPayload {
|
||||
blobURI: string;
|
||||
}
|
||||
|
||||
export type ParseKuwoHeaderResponse = null | {
|
||||
resourceId: number;
|
||||
qualityId: number;
|
||||
};
|
||||
|
||||
export interface GetQingTingFMDeviceKeyPayload {
|
||||
product: string;
|
||||
device: string;
|
||||
|
@ -5,6 +5,7 @@ import { getUmcVersion } from '@unlock-music/crypto';
|
||||
import { workerDecryptHandler } from './worker/decrypt.ts';
|
||||
import { workerParseMusicExMediaName } from './worker/qmcv2_parser.ts';
|
||||
import { workerGetQtfmDeviceKey } from '~/decrypt-worker/worker/qtfm_device_key.ts';
|
||||
import { workerParseKuwoHeader } from '~/decrypt-worker/worker/kuwo_header_parse.ts';
|
||||
|
||||
const bus = new WorkerServerBus();
|
||||
onmessage = bus.onmessage;
|
||||
@ -12,4 +13,5 @@ onmessage = bus.onmessage;
|
||||
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.DECRYPT, workerDecryptHandler);
|
||||
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.FIND_QMC_MUSICEX_NAME, workerParseMusicExMediaName);
|
||||
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.VERSION, getUmcVersion);
|
||||
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.KUWO_PARSE_HEADER, workerParseKuwoHeader);
|
||||
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.QINGTING_FM_GET_DEVICE_KEY, workerGetQtfmDeviceKey);
|
||||
|
17
src/decrypt-worker/worker/kuwo_header_parse.ts
Normal file
17
src/decrypt-worker/worker/kuwo_header_parse.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { FetchMusicExNamePayload, ParseKuwoHeaderResponse } from '~/decrypt-worker/types.ts';
|
||||
import { KuwoHeader } from '@unlock-music/crypto';
|
||||
|
||||
export const workerParseKuwoHeader = async ({ blobURI }: FetchMusicExNamePayload): Promise<ParseKuwoHeaderResponse> => {
|
||||
const blob = await fetch(blobURI, { headers: { Range: 'bytes=0-1023' } }).then((r) => r.blob());
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
|
||||
try {
|
||||
const buffer = new Uint8Array(arrayBuffer.slice(0, 1024));
|
||||
const kwm = KuwoHeader.parse(buffer);
|
||||
const { qualityId, resourceId } = kwm;
|
||||
kwm.free();
|
||||
return { qualityId, resourceId };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
@ -1,10 +1,7 @@
|
||||
import type { FetchMusicExNamePayload } from '~/decrypt-worker/types.ts';
|
||||
import { timedLogger } from '~/util/logUtils.ts';
|
||||
import { QMCFooter } from '@unlock-music/crypto';
|
||||
|
||||
export const workerParseMusicExMediaName = async ({ id, blobURI }: FetchMusicExNamePayload) => {
|
||||
const label = `qmcMusixExDetectName(${id.replace('://', ':')})`;
|
||||
return timedLogger(label, async () => {
|
||||
export const workerParseMusicExMediaName = async ({ blobURI }: FetchMusicExNamePayload) => {
|
||||
const blob = await fetch(blobURI, { headers: { Range: 'bytes=-1024' } }).then((r) => r.blob());
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
|
||||
@ -15,5 +12,4 @@ export const workerParseMusicExMediaName = async ({ id, blobURI }: FetchMusicExN
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -1,12 +1,17 @@
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
||||
import type { RootState } from '~/store';
|
||||
|
||||
import { DECRYPTION_WORKER_ACTION_NAME, type DecryptionResult } from '~/decrypt-worker/constants';
|
||||
import type { DecryptCommandOptions, FetchMusicExNamePayload } from '~/decrypt-worker/types';
|
||||
import type {
|
||||
DecryptCommandOptions,
|
||||
FetchMusicExNamePayload,
|
||||
ParseKuwoHeaderPayload,
|
||||
ParseKuwoHeaderResponse,
|
||||
} from '~/decrypt-worker/types';
|
||||
import { decryptionQueue, workerClientBus } from '~/decrypt-worker/client';
|
||||
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
|
||||
import { selectQMCv2KeyByFileName, selectKWMv2Key, selectQtfmAndroidKey } from '../settings/settingsSelector';
|
||||
import { selectKWMv2Key, selectQMCv2KeyByFileName, selectQtfmAndroidKey } from '../settings/settingsSelector';
|
||||
|
||||
export enum ProcessState {
|
||||
QUEUED = 'QUEUED',
|
||||
@ -43,6 +48,7 @@ export interface FileListingState {
|
||||
files: Record<string, DecryptedAudioFile>;
|
||||
displayMode: ListingMode;
|
||||
}
|
||||
|
||||
const initialState: FileListingState = {
|
||||
files: {},
|
||||
displayMode: ListingMode.LIST,
|
||||
@ -64,28 +70,20 @@ export const processFile = createAsyncThunk<
|
||||
thunkAPI.dispatch(setFileAsProcessing({ id: fileId }));
|
||||
};
|
||||
|
||||
const fileHeader = await fetch(file.raw, { headers: { Range: 'bytes=0-1023' } })
|
||||
.then((r) => r.blob())
|
||||
.then((r) => r.arrayBuffer())
|
||||
.then((r) => {
|
||||
if (r.byteLength > 1024) {
|
||||
return r.slice(0, 1024);
|
||||
}
|
||||
return r;
|
||||
});
|
||||
|
||||
const qmcv2MusicExMediaFile = await workerClientBus.request<string, FetchMusicExNamePayload>(
|
||||
DECRYPTION_WORKER_ACTION_NAME.FIND_QMC_MUSICEX_NAME,
|
||||
{
|
||||
id: fileId,
|
||||
const [qmcv2MusicExMediaFile, kuwoHdr] = await Promise.all([
|
||||
workerClientBus.request<string, FetchMusicExNamePayload>(DECRYPTION_WORKER_ACTION_NAME.FIND_QMC_MUSICEX_NAME, {
|
||||
blobURI: file.raw,
|
||||
},
|
||||
);
|
||||
}),
|
||||
workerClientBus.request<ParseKuwoHeaderResponse, ParseKuwoHeaderPayload>(
|
||||
DECRYPTION_WORKER_ACTION_NAME.KUWO_PARSE_HEADER,
|
||||
{ blobURI: file.raw },
|
||||
),
|
||||
]);
|
||||
|
||||
const options: DecryptCommandOptions = {
|
||||
fileName: file.fileName,
|
||||
qmc2Key: selectQMCv2KeyByFileName(state, qmcv2MusicExMediaFile || file.fileName),
|
||||
kwm2key: selectKWMv2Key(state, new DataView(fileHeader)),
|
||||
kwm2key: selectKWMv2Key(state, kuwoHdr),
|
||||
qingTingAndroidKey: selectQtfmAndroidKey(state),
|
||||
};
|
||||
return decryptionQueue.add({ id: fileId, blobURI: file.raw, options }, onPreProcess);
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { parseKuwoHeader } from '~/crypto/parseKuwo';
|
||||
import type { RootState } from '~/store';
|
||||
import { closestByLevenshtein } from '~/util/levenshtein';
|
||||
import { hasOwn } from '~/util/objects';
|
||||
import { kwm2StagingToProductionKey } from './keyFormats';
|
||||
import type { ParseKuwoHeaderResponse } from '~/decrypt-worker/types.ts';
|
||||
|
||||
export const selectIsSettingsNotSaved = (state: RootState) => state.settings.dirty;
|
||||
|
||||
@ -31,14 +31,16 @@ export const selectQMCv2KeyByFileName = (state: RootState, name: string): string
|
||||
return ekey;
|
||||
};
|
||||
|
||||
export const selectKWMv2Key = (state: RootState, headerView: DataView): string | undefined => {
|
||||
const hdr = parseKuwoHeader(headerView);
|
||||
export const selectKWMv2Key = (state: RootState, hdr: ParseKuwoHeaderResponse): string | undefined => {
|
||||
if (!hdr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const quality = String(hdr.qualityId);
|
||||
const rid = String(hdr.resourceId);
|
||||
|
||||
const keys = selectFinalKWMv2Keys(state);
|
||||
const lookupKey = kwm2StagingToProductionKey({ id: '', ekey: '', quality: hdr.quality, rid: hdr.rid });
|
||||
const lookupKey = kwm2StagingToProductionKey({ id: '', ekey: '', quality, rid });
|
||||
|
||||
let ekey: string | undefined;
|
||||
if (hasOwn(keys, lookupKey)) {
|
||||
|
Loading…
Reference in New Issue
Block a user