#58 蜻蜓FM 安卓端支援 #59
@ -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",
|
||||||
|
1137
pnpm-lock.yaml
1137
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||||
];
|
];
|
||||||
|
25
src/decrypt-worker/crypto/qtfm/qtfm_device.ts
Normal file
25
src/decrypt-worker/crypto/qtfm/qtfm_device.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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>,
|
||||||
|
43
src/features/settings/panels/PanelQingTing.tsx
Normal file
43
src/features/settings/panels/PanelQingTing.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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');
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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' },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user