feat: support for qmcv2 musicex tail
This commit is contained in:
parent
6c21150fc8
commit
fcc4b14211
@ -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",
|
||||
|
@ -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:
|
||||
|
@ -1,5 +1,6 @@
|
||||
export enum DECRYPTION_WORKER_ACTION_NAME {
|
||||
DECRYPT = 'DECRYPT',
|
||||
FIND_QMC_MUSICEX_NAME = 'FIND_QMC_MUSICEX_NAME',
|
||||
VERSION = 'VERSION',
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
|
@ -10,3 +10,8 @@ export interface DecryptCommandPayload {
|
||||
blobURI: string;
|
||||
options: DecryptCommandOptions;
|
||||
}
|
||||
|
||||
export interface FetchMusicExNamePayload {
|
||||
id: string;
|
||||
blobURI: string;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
27
src/decrypt-worker/worker/handler/qmcv2_parser.ts
Normal file
27
src/decrypt-worker/worker/handler/qmcv2_parser.ts
Normal 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';
|
||||
});
|
||||
};
|
@ -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');
|
||||
}
|
||||
|
@ -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),
|
||||
};
|
||||
|
@ -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 }));
|
||||
|
@ -1,43 +1,44 @@
|
||||
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) => {
|
||||
if (settings?.qmc2) {
|
||||
const { allowFuzzyNameSearch, keys } = settings.qmc2;
|
||||
for (const [k, v] of enumObject(keys)) {
|
||||
if (typeof v === 'string') {
|
||||
draft.qmc2.keys[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof allowFuzzyNameSearch === 'boolean') {
|
||||
draft.qmc2.allowFuzzyNameSearch = allowFuzzyNameSearch;
|
||||
const draft = deepClone(settingsSlice.getInitialState().production);
|
||||
if (settings?.qmc2) {
|
||||
const { allowFuzzyNameSearch, keys } = settings.qmc2;
|
||||
for (const [k, v] of enumObject(keys)) {
|
||||
if (typeof v === 'string') {
|
||||
draft.qmc2.keys[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
if (settings?.kwm2) {
|
||||
const { keys } = settings.kwm2;
|
||||
if (typeof allowFuzzyNameSearch === 'boolean') {
|
||||
draft.qmc2.allowFuzzyNameSearch = allowFuzzyNameSearch;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [k, v] of enumObject(keys)) {
|
||||
if (typeof v === 'string' && parseKwm2ProductionKey(k)) {
|
||||
draft.kwm2.keys[k] = v;
|
||||
}
|
||||
if (settings?.kwm2) {
|
||||
const { keys } = settings.kwm2;
|
||||
|
||||
for (const [k, v] of enumObject(keys)) {
|
||||
if (typeof v === 'string' && parseKwm2ProductionKey(k)) {
|
||||
draft.kwm2.keys[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof settings?.qtfm?.android === 'string') {
|
||||
draft.qtfm.android = settings.qtfm.android.replace(/[^0-9a-fA-F]/g, '');
|
||||
}
|
||||
});
|
||||
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
3
src/util/deepClone.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function deepClone<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
@ -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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user