feat: initial implementation of qtfm android

This commit is contained in:
鲁树人 2023-11-29 23:45:56 +00:00
parent 85ab69d41d
commit 18d02a906b
12 changed files with 871 additions and 416 deletions

View File

@ -21,7 +21,7 @@
"@chakra-ui/react": "^2.8.1", "@chakra-ui/react": "^2.8.1",
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@jixun/libparakeet": "0.3.0", "@jixun/libparakeet": "0.4.1",
"@reduxjs/toolkit": "^1.9.7", "@reduxjs/toolkit": "^1.9.7",
"framer-motion": "^10.16.4", "framer-motion": "^10.16.4",
"immer": "^10.0.3", "immer": "^10.0.3",

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ import { XimalayaAndroidCrypto } from './xmly/xmly_android';
import { KWMCrypto } from './kwm/kwm'; import { KWMCrypto } from './kwm/kwm';
import { MiguCrypto } from './migu/migu3d_keyless'; import { MiguCrypto } from './migu/migu3d_keyless';
import { TransparentCrypto } from './transparent/transparent'; import { TransparentCrypto } from './transparent/transparent';
import { QingTingFM$Device } from './qtfm/qtfm_device';
export const allCryptoFactories: CryptoFactory[] = [ export const allCryptoFactories: CryptoFactory[] = [
// Xiami (*.xm) // Xiami (*.xm)
@ -40,6 +41,9 @@ export const allCryptoFactories: CryptoFactory[] = [
XimalayaAndroidCrypto.makeX2M, XimalayaAndroidCrypto.makeX2M,
XimalayaAndroidCrypto.makeX3M, XimalayaAndroidCrypto.makeX3M,
// QingTingFM (Android)
QingTingFM$Device.make,
// Transparent crypto (not encrypted) // Transparent crypto (not encrypted)
TransparentCrypto.make, TransparentCrypto.make,
]; ];

View File

@ -0,0 +1,25 @@
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
import type { CryptoBase } from '../CryptoBase';
import { DecryptCommandOptions } from '~/decrypt-worker/types';
export class QingTingFM$Device implements CryptoBase {
cryptoName = 'QingTing FM/Device ID';
checkByDecryptHeader = false;
async checkBySignature(_buffer: ArrayBuffer, options: DecryptCommandOptions) {
return Boolean(/^\.p~?!.*\.qta$/.test(options.fileName) && options.qingTingAndroidDevice);
}
async decrypt(buffer: ArrayBuffer, options: DecryptCommandOptions): Promise<Blob> {
const { fileName: name, qingTingAndroidDevice: qingTingDevice } = options;
if (!qingTingDevice) {
throw new Error('QingTingFM Device Info was not provided');
}
return transformBlob(buffer, (p) => p.make.QingTingFM(name, qingTingDevice));
}
public static make() {
return new QingTingFM$Device();
}
}

View File

@ -1,6 +1,10 @@
import type { QingTingDeviceInfo } from '@jixun/libparakeet';
export interface DecryptCommandOptions { export interface DecryptCommandOptions {
fileName: string;
qmc2Key?: string; qmc2Key?: string;
kwm2key?: string; kwm2key?: string;
qingTingAndroidDevice?: QingTingDeviceInfo;
} }
export interface DecryptCommandPayload { export interface DecryptCommandPayload {

View File

@ -6,7 +6,7 @@ import type { DecryptionResult } from '~/decrypt-worker/constants';
import type { DecryptCommandOptions } from '~/decrypt-worker/types'; import type { DecryptCommandOptions } from '~/decrypt-worker/types';
import { decryptionQueue } from '~/decrypt-worker/client'; import { decryptionQueue } from '~/decrypt-worker/client';
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError'; import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
import { selectQMCv2KeyByFileName, selectKWMv2Key } from '../settings/settingsSelector'; import { selectQMCv2KeyByFileName, selectKWMv2Key, selectQtfmAndroidDevice } from '../settings/settingsSelector';
export enum ProcessState { export enum ProcessState {
QUEUED = 'QUEUED', QUEUED = 'QUEUED',
@ -73,8 +73,10 @@ export const processFile = createAsyncThunk<
.then((r) => r.arrayBuffer()); .then((r) => r.arrayBuffer());
const options: DecryptCommandOptions = { const options: DecryptCommandOptions = {
fileName: file.fileName,
qmc2Key: selectQMCv2KeyByFileName(state, file.fileName), qmc2Key: selectQMCv2KeyByFileName(state, file.fileName),
kwm2key: selectKWMv2Key(state, new DataView(fileHeader)), kwm2key: selectKWMv2Key(state, new DataView(fileHeader)),
qingTingAndroidDevice: selectQtfmAndroidDevice(state),
}; };
return decryptionQueue.add({ id: fileId, blobURI: file.raw, options }, onPreProcess); return decryptionQueue.add({ id: fileId, blobURI: file.raw, options }, onPreProcess);
}); });

View File

@ -30,10 +30,12 @@ import { useAppDispatch, useAppSelector } from '~/hooks';
import { commitStagingChange, discardStagingChanges } from './settingsSlice'; import { commitStagingChange, discardStagingChanges } from './settingsSlice';
import { PanelKWMv2Key } from './panels/PanelKWMv2Key'; import { PanelKWMv2Key } from './panels/PanelKWMv2Key';
import { selectIsSettingsNotSaved } from './settingsSelector'; import { selectIsSettingsNotSaved } from './settingsSelector';
import { PanelQingTing } from './panels/PanelQingTing';
const TABS: { name: string; Tab: () => JSX.Element }[] = [ const TABS: { name: string; Tab: () => JSX.Element }[] = [
{ name: 'QMCv2 密钥', Tab: PanelQMCv2Key }, { name: 'QMCv2 密钥', Tab: PanelQMCv2Key },
{ name: 'KWMv2 密钥', Tab: PanelKWMv2Key }, { name: 'KWMv2 密钥', Tab: PanelKWMv2Key },
{ name: '蜻蜓 FM', Tab: PanelQingTing },
{ {
name: '其它/待定', name: '其它/待定',
Tab: () => <Text></Text>, Tab: () => <Text></Text>,

View File

@ -0,0 +1,43 @@
import { Box, Flex, Heading, Input, Table, Tbody, Td, Text, Th, Tr } from '@chakra-ui/react';
import { qtfmAndroidUpdateKey } from '../settingsSlice';
import { useAppDispatch, useAppSelector } from '~/hooks';
import { selectStagingQtfmAndroidDevice } from '../settingsSelector';
import type { QingTingDeviceInfo } from '@jixun/libparakeet';
const QTFM_ANDROID_DEVICE_PROPS = ['product', 'device', 'manufacturer', 'brand', 'board', 'model'] as const;
export function PanelQingTing() {
const dispatch = useAppDispatch();
const qingTingAndroidDeviceInfo = useAppSelector(selectStagingQtfmAndroidDevice);
const handleChangeDeviceInfo = (name: keyof QingTingDeviceInfo) => (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(qtfmAndroidUpdateKey({ field: name, value: e.target.value }));
};
return (
<Flex minH={0} flexDir="column" flex={1}>
<Heading as="h2" size="lg">
FM
</Heading>
<Text> FM</Text>
<Box flex={1} minH={0} overflow="auto" pr="4">
<Table>
<Tbody>
{QTFM_ANDROID_DEVICE_PROPS.map((name) => (
<Tr key={name}>
<Th w="1px" p={1} textAlign="right">
{name}
</Th>
<Td pl={1} pr={1}>
<Input value={qingTingAndroidDeviceInfo[name]} onChange={handleChangeDeviceInfo(name)} />
</Td>
</Tr>
))}
</Tbody>
</Table>
</Box>
</Flex>
);
}

View File

@ -2,10 +2,11 @@ import { debounce } from 'radash';
import { produce } from 'immer'; import { produce } from 'immer';
import type { AppStore } from '~/store'; import type { AppStore } from '~/store';
import { settingsSlice, setProductionChanges, ProductionSettings } from './settingsSlice'; import { settingsSlice, setProductionChanges, ProductionSettings, QINGTING_DEVICE_INFO_KEY } 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 type { QingTingDeviceInfo } from '@jixun/libparakeet';
const DEFAULT_STORAGE_KEY = 'um-react-settings'; const DEFAULT_STORAGE_KEY = 'um-react-settings';
@ -33,6 +34,16 @@ function mergeSettings(settings: ProductionSettings): ProductionSettings {
} }
} }
} }
if (settings?.qtfm?.android) {
const qtfmAndroid = settings.qtfm.android;
draft.qtfm.android = Object.fromEntries(
QINGTING_DEVICE_INFO_KEY.map((key) => {
const value = qtfmAndroid[key];
return [key, typeof value === 'string' ? value : ''];
}),
) as unknown as QingTingDeviceInfo;
}
}); });
} }
@ -58,6 +69,6 @@ export function persistSettings(store: AppStore, storageKey = DEFAULT_STORAGE_KE
localStorage.setItem(storageKey, JSON.stringify(currentSettings)); localStorage.setItem(storageKey, JSON.stringify(currentSettings));
getLogger().debug('settings saved'); getLogger().debug('settings saved');
} }
}) }),
); );
} }

View File

@ -47,3 +47,6 @@ export const selectKWMv2Key = (state: RootState, headerView: DataView): string |
return ekey; return ekey;
}; };
export const selectStagingQtfmAndroidDevice = (state: RootState) => state.settings.staging.qtfm.android;
export const selectQtfmAndroidDevice = (state: RootState) => state.settings.production.qtfm.android;

View File

@ -1,3 +1,4 @@
import type { QingTingDeviceInfo } from '@jixun/libparakeet';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
@ -16,6 +17,24 @@ import {
stagingKeyToProduction, stagingKeyToProduction,
} from './keyFormats'; } from './keyFormats';
export const QINGTING_DEVICE_INFO_KEY: (keyof QingTingDeviceInfo)[] = [
'product',
'device',
'manufacturer',
'brand',
'board',
'model',
];
const DEFAULT_QINGTING_ANDROID_DEVICE_INFO: QingTingDeviceInfo = {
product: '',
device: '',
manufacturer: '',
brand: '',
board: '',
model: '',
};
export interface StagingSettings { export interface StagingSettings {
qmc2: { qmc2: {
keys: StagingQMCv2Key[]; keys: StagingQMCv2Key[];
@ -24,6 +43,9 @@ export interface StagingSettings {
kwm2: { kwm2: {
keys: StagingKWMv2Key[]; keys: StagingKWMv2Key[];
}; };
qtfm: {
android: QingTingDeviceInfo;
};
} }
export interface ProductionSettings { export interface ProductionSettings {
@ -34,6 +56,9 @@ export interface ProductionSettings {
kwm2: { kwm2: {
keys: ProductionKWMv2Keys; // { [`${rid}-${quality}`]: ekey } keys: ProductionKWMv2Keys; // { [`${rid}-${quality}`]: ekey }
}; };
qtfm: {
android: QingTingDeviceInfo;
};
} }
export interface SettingsState { export interface SettingsState {
@ -46,10 +71,12 @@ const initialState: SettingsState = {
staging: { staging: {
qmc2: { allowFuzzyNameSearch: true, keys: [] }, qmc2: { allowFuzzyNameSearch: true, keys: [] },
kwm2: { keys: [] }, kwm2: { keys: [] },
qtfm: { android: DEFAULT_QINGTING_ANDROID_DEVICE_INFO },
}, },
production: { production: {
qmc2: { allowFuzzyNameSearch: true, keys: {} }, qmc2: { allowFuzzyNameSearch: true, keys: {} },
kwm2: { keys: {} }, kwm2: { keys: {} },
qtfm: { android: DEFAULT_QINGTING_ANDROID_DEVICE_INFO },
}, },
}; };
@ -61,6 +88,7 @@ const stagingToProduction = (staging: StagingSettings): ProductionSettings => ({
kwm2: { kwm2: {
keys: stagingKeyToProduction(staging.kwm2.keys, kwm2StagingToProductionKey, kwm2StagingToProductionValue), keys: stagingKeyToProduction(staging.kwm2.keys, kwm2StagingToProductionKey, kwm2StagingToProductionValue),
}, },
qtfm: staging.qtfm,
}); });
const productionToStaging = (production: ProductionSettings): StagingSettings => ({ const productionToStaging = (production: ProductionSettings): StagingSettings => ({
@ -71,6 +99,7 @@ const productionToStaging = (production: ProductionSettings): StagingSettings =>
kwm2: { kwm2: {
keys: productionKeyToStaging(production.kwm2.keys, kwm2ProductionToStaging), keys: productionKeyToStaging(production.kwm2.keys, kwm2ProductionToStaging),
}, },
qtfm: production.qtfm,
}); });
export const settingsSlice = createSlice({ export const settingsSlice = createSlice({
@ -101,7 +130,7 @@ export const settingsSlice = createSlice({
}, },
qmc2UpdateKey( qmc2UpdateKey(
state, state,
{ payload: { id, field, value } }: PayloadAction<{ id: string; field: keyof StagingQMCv2Key; value: string }> { payload: { id, field, value } }: PayloadAction<{ id: string; field: keyof StagingQMCv2Key; value: string }>,
) { ) {
const keyItem = state.staging.qmc2.keys.find((item) => item.id === id); const keyItem = state.staging.qmc2.keys.find((item) => item.id === id);
if (keyItem) { if (keyItem) {
@ -134,7 +163,7 @@ export const settingsSlice = createSlice({
}, },
kwm2UpdateKey( kwm2UpdateKey(
state, state,
{ payload: { id, field, value } }: PayloadAction<{ id: string; field: keyof StagingKWMv2Key; value: string }> { payload: { id, field, value } }: PayloadAction<{ id: string; field: keyof StagingKWMv2Key; value: string }>,
) { ) {
const keyItem = state.staging.kwm2.keys.find((item) => item.id === id); const keyItem = state.staging.kwm2.keys.find((item) => item.id === id);
if (keyItem) { if (keyItem) {
@ -142,6 +171,13 @@ export const settingsSlice = createSlice({
state.dirty = true; state.dirty = true;
} }
}, },
qtfmAndroidUpdateKey(
state,
{ payload: { field, value } }: PayloadAction<{ field: keyof QingTingDeviceInfo; value: string }>,
) {
state.staging.qtfm.android[field] = value;
state.dirty = true;
},
kwm2ClearKeys(state) { kwm2ClearKeys(state) {
state.staging.kwm2.keys = []; state.staging.kwm2.keys = [];
state.dirty = true; state.dirty = true;
@ -183,6 +219,8 @@ export const {
kwm2ClearKeys, kwm2ClearKeys,
kwm2ImportKeys, kwm2ImportKeys,
qtfmAndroidUpdateKey,
commitStagingChange, commitStagingChange,
discardStagingChanges, discardStagingChanges,
} = settingsSlice.actions; } = settingsSlice.actions;

View File

@ -22,12 +22,14 @@ test('should be able to forward request to worker client bus', async () => {
); );
const queue = new DecryptionQueue(bus, 1); const queue = new DecryptionQueue(bus, 1);
await expect(queue.add({ id: 'file://1', blobURI: 'blob://mock-file', options: {} })).resolves.toEqual({ await expect(
queue.add({ id: 'file://1', blobURI: 'blob://mock-file', options: { fileName: 'test.bin' } }),
).resolves.toEqual({
actionName: DECRYPTION_WORKER_ACTION_NAME.DECRYPT, actionName: DECRYPTION_WORKER_ACTION_NAME.DECRYPT,
payload: { payload: {
blobURI: 'blob://mock-file', blobURI: 'blob://mock-file',
id: 'file://1', id: 'file://1',
options: {}, options: { fileName: 'test.bin' },
}, },
}); });
}); });