feat: support for qmcv2 musicex tail

This commit is contained in:
鲁树人 2023-12-24 12:15:56 +01:00
parent 6c21150fc8
commit fcc4b14211
14 changed files with 93 additions and 48 deletions

View File

@ -21,10 +21,9 @@
"@chakra-ui/react": "^2.8.2",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@jixun/libparakeet": "0.4.2",
"@jixun/libparakeet": "0.4.3",
"@reduxjs/toolkit": "^2.0.1",
"framer-motion": "^10.16.16",
"immer": "^10.0.3",
"nanoid": "^5.0.4",
"radash": "^11.0.0",
"react": "^18.2.0",

View File

@ -33,17 +33,14 @@ dependencies:
specifier: ^11.11.0
version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.45)(react@18.2.0)
'@jixun/libparakeet':
specifier: 0.4.2
version: 0.4.2
specifier: 0.4.3
version: 0.4.3
'@reduxjs/toolkit':
specifier: ^2.0.1
version: 2.0.1(react-redux@9.0.4)(react@18.2.0)
framer-motion:
specifier: ^10.16.16
version: 10.16.16(react-dom@18.2.0)(react@18.2.0)
immer:
specifier: ^10.0.3
version: 10.0.3
nanoid:
specifier: ^5.0.4
version: 5.0.4
@ -2885,8 +2882,8 @@ packages:
'@sinclair/typebox': 0.27.8
dev: true
/@jixun/libparakeet@0.4.2:
resolution: {integrity: sha512-E6XXrHeOOIexKSyWUgQwHUpZNMh5I1IoC9gbwyVQjJhBLiURy6KU7U2Fgsg62Q3BSkdi7NwbdbPERrygUFmA7Q==}
/@jixun/libparakeet@0.4.3:
resolution: {integrity: sha512-Y+h65ZXbJ604sO1RyXA+2kx1WQoA6xJSlIIFWLcmAJpbj34XY6eCpyRXnltgkzcOLsLaO89jjGqMPS7MBC4XqA==}
dev: false
/@jridgewell/gen-mapping@0.3.3:

View File

@ -1,5 +1,6 @@
export enum DECRYPTION_WORKER_ACTION_NAME {
DECRYPT = 'DECRYPT',
FIND_QMC_MUSICEX_NAME = 'FIND_QMC_MUSICEX_NAME',
VERSION = 'VERSION',
}

View File

@ -1,10 +1,9 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { CryptoBase } from '../CryptoBase';
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
import { SEED, ENC_V2_KEY_1, ENC_V2_KEY_2 } from './qmc_v2.key.ts';
import { fetchParakeet } from '@jixun/libparakeet';
import { stringToUTF8Bytes } from '~/decrypt-worker/util/utf8Encoder.ts';
import { makeQMCv2KeyCrypto } from '~/decrypt-worker/util/qmc2KeyCrypto.ts';
import { makeQMCv2FooterParser, makeQMCv2KeyCrypto } from '~/decrypt-worker/util/qmc2KeyCrypto.ts';
export class QMC2Crypto implements CryptoBase {
cryptoName = 'QMC/v2';
@ -12,7 +11,7 @@ export class QMC2Crypto implements CryptoBase {
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
const parakeet = await fetchParakeet();
const footerParser = parakeet.make.QMCv2FooterParser(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2);
const footerParser = makeQMCv2FooterParser(parakeet);
return transformBlob(buffer, (p) => p.make.QMCv2(footerParser), {
parakeet,
cleanup: () => footerParser.delete(),

View File

@ -10,3 +10,8 @@ export interface DecryptCommandPayload {
blobURI: string;
options: DecryptCommandOptions;
}
export interface FetchMusicExNamePayload {
id: string;
blobURI: string;
}

View File

@ -2,3 +2,4 @@ import type { Parakeet } from '@jixun/libparakeet';
import { SEED, ENC_V2_KEY_1, ENC_V2_KEY_2 } from '../crypto/qmc/qmc_v2.key';
export const makeQMCv2KeyCrypto = (p: Parakeet) => p.make.QMCv2KeyCrypto(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2);
export const makeQMCv2FooterParser = (p: Parakeet) => p.make.QMCv2FooterParser(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2);

View File

@ -4,9 +4,11 @@ import { DECRYPTION_WORKER_ACTION_NAME } from './constants';
import { getSDKVersion } from '@jixun/libparakeet';
import { workerDecryptHandler } from './worker/handler/decrypt';
import { workerParseMusicExMediaName } from './worker/handler/qmcv2_parser';
const bus = new WorkerServerBus();
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, getSDKVersion);

View File

@ -0,0 +1,27 @@
import { fetchParakeet, FooterParserState } from '@jixun/libparakeet';
import type { FetchMusicExNamePayload } from '~/decrypt-worker/types';
import { makeQMCv2FooterParser } from '~/decrypt-worker/util/qmc2KeyCrypto';
import { timedLogger, withGroupedLogs as withTimeGroupedLogs } from '~/util/logUtils';
export const workerParseMusicExMediaName = async ({ id, blobURI }: FetchMusicExNamePayload) => {
const label = `decrypt(${id})`;
return withTimeGroupedLogs(label, async () => {
const parakeet = await timedLogger(`${label}/init`, fetchParakeet);
const blob = await timedLogger(`${label}/fetch-src`, async () =>
fetch(blobURI, { headers: { Range: 'bytes=-1024' } }).then((r) => r.blob()),
);
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) {
return parsed.mediaName;
}
return '# N/A';
});
};

View File

@ -1,5 +1,5 @@
// This is a dummy module for vite/rollup to resolve.
export function createRequire() {
import('immer'); // we need to import something, so vite don't complain on build
import('radash'); // we need to import something, so vite don't complain on build
throw new Error('this is a dummy module. Do not use');
}

View File

@ -2,9 +2,9 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '~/store';
import type { DecryptionResult } from '~/decrypt-worker/constants';
import type { DecryptCommandOptions } from '~/decrypt-worker/types';
import { decryptionQueue } from '~/decrypt-worker/client';
import { DECRYPTION_WORKER_ACTION_NAME, type DecryptionResult } from '~/decrypt-worker/constants';
import type { DecryptCommandOptions, FetchMusicExNamePayload } 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';
@ -44,7 +44,7 @@ export interface FileListingState {
displayMode: ListingMode;
}
const initialState: FileListingState = {
files: Object.create(null),
files: {},
displayMode: ListingMode.LIST,
};
@ -64,17 +64,27 @@ export const processFile = createAsyncThunk<
thunkAPI.dispatch(setFileAsProcessing({ id: fileId }));
};
const fileHeader = await fetch(file.raw, {
headers: {
Range: 'bytes=0-1023',
},
})
const fileHeader = await fetch(file.raw, { headers: { Range: 'bytes=0-1023' } })
.then((r) => r.blob())
.then((r) => r.arrayBuffer());
.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,
blobURI: file.raw,
},
);
const options: DecryptCommandOptions = {
fileName: file.fileName,
qmc2Key: selectQMCv2KeyByFileName(state, file.fileName),
qmc2Key: selectQMCv2KeyByFileName(state, qmcv2MusicExMediaFile || file.fileName),
kwm2key: selectKWMv2Key(state, new DataView(fileHeader)),
qingTingAndroidKey: selectQtfmAndroidKey(state),
};

View File

@ -61,7 +61,7 @@ export function PanelQMCv2Key() {
alert(`不是支持的 SQLite 数据库文件。`);
return;
}
} else if (/MMKVStreamEncryptId|filenameEkeyMap/i.test(file.name)) {
} else if (/MMKVStreamEncryptId|filenameEkeyMap|qmpc-mmkv-v1/i.test(file.name)) {
const fileBuffer = await file.arrayBuffer();
const map = parseAndroidQmEKey(new DataView(fileBuffer));
qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey }));

View File

@ -1,16 +1,16 @@
import { debounce } from 'radash';
import { produce } from 'immer';
import type { AppStore } from '~/store';
import { settingsSlice, setProductionChanges, ProductionSettings } from './settingsSlice';
import { enumObject } from '~/util/objects';
import { getLogger } from '~/util/logUtils';
import { parseKwm2ProductionKey } from './keyFormats';
import { deepClone } from '~/util/deepClone';
const DEFAULT_STORAGE_KEY = 'um-react-settings';
function mergeSettings(settings: ProductionSettings): ProductionSettings {
return produce(settingsSlice.getInitialState().production, (draft) => {
const draft = deepClone(settingsSlice.getInitialState().production);
if (settings?.qmc2) {
const { allowFuzzyNameSearch, keys } = settings.qmc2;
for (const [k, v] of enumObject(keys)) {
@ -37,7 +37,8 @@ function mergeSettings(settings: ProductionSettings): ProductionSettings {
if (typeof settings?.qtfm?.android === 'string') {
draft.qtfm.android = settings.qtfm.android.replace(/[^0-9a-fA-F]/g, '');
}
});
return draft;
}
export function persistSettings(store: AppStore, storageKey = DEFAULT_STORAGE_KEY) {

3
src/util/deepClone.ts Normal file
View File

@ -0,0 +1,3 @@
export function deepClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}

View File

@ -98,7 +98,7 @@ export default defineConfig({
reacts: ['react', 'react-dom', 'react-dropzone', 'react-promise-suspense', 'react-redux', '@reduxjs/toolkit'],
chakra: ['@chakra-ui/react', '@emotion/react', '@emotion/styled', 'framer-motion'],
icons: ['react-icons', '@chakra-ui/icons'],
utility: ['radash', 'nanoid', 'immer', 'react-syntax-highlighter'],
utility: ['radash', 'nanoid', 'react-syntax-highlighter'],
},
},
},