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",
|
"@chakra-ui/react": "^2.8.2",
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@jixun/libparakeet": "0.4.2",
|
"@jixun/libparakeet": "0.4.3",
|
||||||
"@reduxjs/toolkit": "^2.0.1",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"framer-motion": "^10.16.16",
|
"framer-motion": "^10.16.16",
|
||||||
"immer": "^10.0.3",
|
|
||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
"radash": "^11.0.0",
|
"radash": "^11.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
@ -33,17 +33,14 @@ dependencies:
|
|||||||
specifier: ^11.11.0
|
specifier: ^11.11.0
|
||||||
version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.45)(react@18.2.0)
|
version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.45)(react@18.2.0)
|
||||||
'@jixun/libparakeet':
|
'@jixun/libparakeet':
|
||||||
specifier: 0.4.2
|
specifier: 0.4.3
|
||||||
version: 0.4.2
|
version: 0.4.3
|
||||||
'@reduxjs/toolkit':
|
'@reduxjs/toolkit':
|
||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.0.1(react-redux@9.0.4)(react@18.2.0)
|
version: 2.0.1(react-redux@9.0.4)(react@18.2.0)
|
||||||
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)
|
version: 10.16.16(react-dom@18.2.0)(react@18.2.0)
|
||||||
immer:
|
|
||||||
specifier: ^10.0.3
|
|
||||||
version: 10.0.3
|
|
||||||
nanoid:
|
nanoid:
|
||||||
specifier: ^5.0.4
|
specifier: ^5.0.4
|
||||||
version: 5.0.4
|
version: 5.0.4
|
||||||
@ -2885,8 +2882,8 @@ packages:
|
|||||||
'@sinclair/typebox': 0.27.8
|
'@sinclair/typebox': 0.27.8
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@jixun/libparakeet@0.4.2:
|
/@jixun/libparakeet@0.4.3:
|
||||||
resolution: {integrity: sha512-E6XXrHeOOIexKSyWUgQwHUpZNMh5I1IoC9gbwyVQjJhBLiURy6KU7U2Fgsg62Q3BSkdi7NwbdbPERrygUFmA7Q==}
|
resolution: {integrity: sha512-Y+h65ZXbJ604sO1RyXA+2kx1WQoA6xJSlIIFWLcmAJpbj34XY6eCpyRXnltgkzcOLsLaO89jjGqMPS7MBC4XqA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@jridgewell/gen-mapping@0.3.3:
|
/@jridgewell/gen-mapping@0.3.3:
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export enum DECRYPTION_WORKER_ACTION_NAME {
|
export enum DECRYPTION_WORKER_ACTION_NAME {
|
||||||
DECRYPT = 'DECRYPT',
|
DECRYPT = 'DECRYPT',
|
||||||
|
FIND_QMC_MUSICEX_NAME = 'FIND_QMC_MUSICEX_NAME',
|
||||||
VERSION = 'VERSION',
|
VERSION = 'VERSION',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
|
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
|
||||||
import type { CryptoBase } from '../CryptoBase';
|
import type { CryptoBase } from '../CryptoBase';
|
||||||
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
|
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 { fetchParakeet } from '@jixun/libparakeet';
|
||||||
import { stringToUTF8Bytes } from '~/decrypt-worker/util/utf8Encoder.ts';
|
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 {
|
export class QMC2Crypto implements CryptoBase {
|
||||||
cryptoName = 'QMC/v2';
|
cryptoName = 'QMC/v2';
|
||||||
@ -12,7 +11,7 @@ export class QMC2Crypto implements CryptoBase {
|
|||||||
|
|
||||||
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
|
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
|
||||||
const parakeet = await fetchParakeet();
|
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), {
|
return transformBlob(buffer, (p) => p.make.QMCv2(footerParser), {
|
||||||
parakeet,
|
parakeet,
|
||||||
cleanup: () => footerParser.delete(),
|
cleanup: () => footerParser.delete(),
|
||||||
|
@ -10,3 +10,8 @@ export interface DecryptCommandPayload {
|
|||||||
blobURI: string;
|
blobURI: string;
|
||||||
options: DecryptCommandOptions;
|
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';
|
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 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 { getSDKVersion } from '@jixun/libparakeet';
|
||||||
|
|
||||||
import { workerDecryptHandler } from './worker/handler/decrypt';
|
import { workerDecryptHandler } from './worker/handler/decrypt';
|
||||||
|
import { workerParseMusicExMediaName } from './worker/handler/qmcv2_parser';
|
||||||
|
|
||||||
const bus = new WorkerServerBus();
|
const bus = new WorkerServerBus();
|
||||||
onmessage = bus.onmessage;
|
onmessage = bus.onmessage;
|
||||||
|
|
||||||
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.DECRYPT, workerDecryptHandler);
|
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);
|
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.
|
// This is a dummy module for vite/rollup to resolve.
|
||||||
export function createRequire() {
|
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');
|
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 { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import type { RootState } from '~/store';
|
import type { RootState } from '~/store';
|
||||||
|
|
||||||
import type { DecryptionResult } from '~/decrypt-worker/constants';
|
import { DECRYPTION_WORKER_ACTION_NAME, type DecryptionResult } from '~/decrypt-worker/constants';
|
||||||
import type { DecryptCommandOptions } from '~/decrypt-worker/types';
|
import type { DecryptCommandOptions, FetchMusicExNamePayload } from '~/decrypt-worker/types';
|
||||||
import { decryptionQueue } from '~/decrypt-worker/client';
|
import { decryptionQueue, workerClientBus } from '~/decrypt-worker/client';
|
||||||
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
|
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
|
||||||
import { selectQMCv2KeyByFileName, selectKWMv2Key, selectQtfmAndroidKey } from '../settings/settingsSelector';
|
import { selectQMCv2KeyByFileName, selectKWMv2Key, selectQtfmAndroidKey } from '../settings/settingsSelector';
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ export interface FileListingState {
|
|||||||
displayMode: ListingMode;
|
displayMode: ListingMode;
|
||||||
}
|
}
|
||||||
const initialState: FileListingState = {
|
const initialState: FileListingState = {
|
||||||
files: Object.create(null),
|
files: {},
|
||||||
displayMode: ListingMode.LIST,
|
displayMode: ListingMode.LIST,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -64,17 +64,27 @@ export const processFile = createAsyncThunk<
|
|||||||
thunkAPI.dispatch(setFileAsProcessing({ id: fileId }));
|
thunkAPI.dispatch(setFileAsProcessing({ id: fileId }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileHeader = await fetch(file.raw, {
|
const fileHeader = await fetch(file.raw, { headers: { Range: 'bytes=0-1023' } })
|
||||||
headers: {
|
|
||||||
Range: 'bytes=0-1023',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((r) => r.blob())
|
.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 = {
|
const options: DecryptCommandOptions = {
|
||||||
fileName: file.fileName,
|
fileName: file.fileName,
|
||||||
qmc2Key: selectQMCv2KeyByFileName(state, file.fileName),
|
qmc2Key: selectQMCv2KeyByFileName(state, qmcv2MusicExMediaFile || file.fileName),
|
||||||
kwm2key: selectKWMv2Key(state, new DataView(fileHeader)),
|
kwm2key: selectKWMv2Key(state, new DataView(fileHeader)),
|
||||||
qingTingAndroidKey: selectQtfmAndroidKey(state),
|
qingTingAndroidKey: selectQtfmAndroidKey(state),
|
||||||
};
|
};
|
||||||
|
@ -61,7 +61,7 @@ export function PanelQMCv2Key() {
|
|||||||
alert(`不是支持的 SQLite 数据库文件。`);
|
alert(`不是支持的 SQLite 数据库文件。`);
|
||||||
return;
|
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 fileBuffer = await file.arrayBuffer();
|
||||||
const map = parseAndroidQmEKey(new DataView(fileBuffer));
|
const map = parseAndroidQmEKey(new DataView(fileBuffer));
|
||||||
qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey }));
|
qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey }));
|
||||||
|
@ -1,43 +1,44 @@
|
|||||||
import { debounce } from 'radash';
|
import { debounce } from 'radash';
|
||||||
import { produce } from 'immer';
|
|
||||||
|
|
||||||
import type { AppStore } from '~/store';
|
import type { AppStore } from '~/store';
|
||||||
import { settingsSlice, setProductionChanges, ProductionSettings } from './settingsSlice';
|
import { settingsSlice, setProductionChanges, ProductionSettings } from './settingsSlice';
|
||||||
import { enumObject } from '~/util/objects';
|
import { enumObject } from '~/util/objects';
|
||||||
import { getLogger } from '~/util/logUtils';
|
import { getLogger } from '~/util/logUtils';
|
||||||
import { parseKwm2ProductionKey } from './keyFormats';
|
import { parseKwm2ProductionKey } from './keyFormats';
|
||||||
|
import { deepClone } from '~/util/deepClone';
|
||||||
|
|
||||||
const DEFAULT_STORAGE_KEY = 'um-react-settings';
|
const DEFAULT_STORAGE_KEY = 'um-react-settings';
|
||||||
|
|
||||||
function mergeSettings(settings: ProductionSettings): ProductionSettings {
|
function mergeSettings(settings: ProductionSettings): ProductionSettings {
|
||||||
return produce(settingsSlice.getInitialState().production, (draft) => {
|
const draft = deepClone(settingsSlice.getInitialState().production);
|
||||||
if (settings?.qmc2) {
|
if (settings?.qmc2) {
|
||||||
const { allowFuzzyNameSearch, keys } = settings.qmc2;
|
const { allowFuzzyNameSearch, keys } = settings.qmc2;
|
||||||
for (const [k, v] of enumObject(keys)) {
|
for (const [k, v] of enumObject(keys)) {
|
||||||
if (typeof v === 'string') {
|
if (typeof v === 'string') {
|
||||||
draft.qmc2.keys[k] = v;
|
draft.qmc2.keys[k] = v;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof allowFuzzyNameSearch === 'boolean') {
|
|
||||||
draft.qmc2.allowFuzzyNameSearch = allowFuzzyNameSearch;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings?.kwm2) {
|
if (typeof allowFuzzyNameSearch === 'boolean') {
|
||||||
const { keys } = settings.kwm2;
|
draft.qmc2.allowFuzzyNameSearch = allowFuzzyNameSearch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [k, v] of enumObject(keys)) {
|
if (settings?.kwm2) {
|
||||||
if (typeof v === 'string' && parseKwm2ProductionKey(k)) {
|
const { keys } = settings.kwm2;
|
||||||
draft.kwm2.keys[k] = v;
|
|
||||||
}
|
for (const [k, v] of enumObject(keys)) {
|
||||||
|
if (typeof v === 'string' && parseKwm2ProductionKey(k)) {
|
||||||
|
draft.kwm2.keys[k] = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof settings?.qtfm?.android === 'string') {
|
if (typeof settings?.qtfm?.android === 'string') {
|
||||||
draft.qtfm.android = settings.qtfm.android.replace(/[^0-9a-fA-F]/g, '');
|
draft.qtfm.android = settings.qtfm.android.replace(/[^0-9a-fA-F]/g, '');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
return draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function persistSettings(store: AppStore, storageKey = DEFAULT_STORAGE_KEY) {
|
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'],
|
reacts: ['react', 'react-dom', 'react-dropzone', 'react-promise-suspense', 'react-redux', '@reduxjs/toolkit'],
|
||||||
chakra: ['@chakra-ui/react', '@emotion/react', '@emotion/styled', 'framer-motion'],
|
chakra: ['@chakra-ui/react', '@emotion/react', '@emotion/styled', 'framer-motion'],
|
||||||
icons: ['react-icons', '@chakra-ui/icons'],
|
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