feat: add basic instruction to paste device key from qtfm-device-id
This commit is contained in:
parent
4498ab6592
commit
83b06dbe60
@ -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.4.1",
|
"@jixun/libparakeet": "0.4.2",
|
||||||
"@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",
|
||||||
|
@ -33,8 +33,8 @@ dependencies:
|
|||||||
specifier: ^11.11.0
|
specifier: ^11.11.0
|
||||||
version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.28)(react@18.2.0)
|
version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.28)(react@18.2.0)
|
||||||
'@jixun/libparakeet':
|
'@jixun/libparakeet':
|
||||||
specifier: 0.4.1
|
specifier: 0.4.2
|
||||||
version: 0.4.1
|
version: 0.4.2
|
||||||
'@reduxjs/toolkit':
|
'@reduxjs/toolkit':
|
||||||
specifier: ^1.9.7
|
specifier: ^1.9.7
|
||||||
version: 1.9.7(react-redux@8.1.3)(react@18.2.0)
|
version: 1.9.7(react-redux@8.1.3)(react@18.2.0)
|
||||||
@ -3080,8 +3080,8 @@ packages:
|
|||||||
'@sinclair/typebox': 0.27.8
|
'@sinclair/typebox': 0.27.8
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@jixun/libparakeet@0.4.1:
|
/@jixun/libparakeet@0.4.2:
|
||||||
resolution: {integrity: sha512-tPzeXLJDMZnlcB37IsxduCPsHQxI15mYFAo2q5msRO9AZjx8kSgB0XCsGnG1xjcAEUP4WaGbQ4xURRX3f5e7kA==}
|
resolution: {integrity: sha512-E6XXrHeOOIexKSyWUgQwHUpZNMh5I1IoC9gbwyVQjJhBLiURy6KU7U2Fgsg62Q3BSkdi7NwbdbPERrygUFmA7Q==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@jridgewell/gen-mapping@0.3.3:
|
/@jridgewell/gen-mapping@0.3.3:
|
||||||
|
@ -7,16 +7,16 @@ export class QingTingFM$Device implements CryptoBase {
|
|||||||
checkByDecryptHeader = false;
|
checkByDecryptHeader = false;
|
||||||
|
|
||||||
async checkBySignature(_buffer: ArrayBuffer, options: DecryptCommandOptions) {
|
async checkBySignature(_buffer: ArrayBuffer, options: DecryptCommandOptions) {
|
||||||
return Boolean(/^\.p~?!.*\.qta$/.test(options.fileName) && options.qingTingAndroidDevice);
|
return Boolean(/^\.p~?!.*\.qta$/.test(options.fileName) && options.qingTingAndroidKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
async decrypt(buffer: ArrayBuffer, options: DecryptCommandOptions): Promise<Blob> {
|
async decrypt(buffer: ArrayBuffer, options: DecryptCommandOptions): Promise<Blob> {
|
||||||
const { fileName: name, qingTingAndroidDevice: qingTingDevice } = options;
|
const { fileName: name, qingTingAndroidKey } = options;
|
||||||
if (!qingTingDevice) {
|
if (!qingTingAndroidKey) {
|
||||||
throw new Error('QingTingFM Device Info was not provided');
|
throw new Error('QingTingFM Android Device Key was not provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
return transformBlob(buffer, (p) => p.make.QingTingFM(name, qingTingDevice));
|
return transformBlob(buffer, (p) => p.make.QingTingFM(name, qingTingAndroidKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static make() {
|
public static make() {
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import type { QingTingDeviceInfo } from '@jixun/libparakeet';
|
|
||||||
|
|
||||||
export interface DecryptCommandOptions {
|
export interface DecryptCommandOptions {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
qmc2Key?: string;
|
qmc2Key?: string;
|
||||||
kwm2key?: string;
|
kwm2key?: string;
|
||||||
qingTingAndroidDevice?: QingTingDeviceInfo;
|
qingTingAndroidKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
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, selectQtfmAndroidDevice } from '../settings/settingsSelector';
|
import { selectQMCv2KeyByFileName, selectKWMv2Key, selectQtfmAndroidKey } from '../settings/settingsSelector';
|
||||||
|
|
||||||
export enum ProcessState {
|
export enum ProcessState {
|
||||||
QUEUED = 'QUEUED',
|
QUEUED = 'QUEUED',
|
||||||
@ -76,7 +76,7 @@ export const processFile = createAsyncThunk<
|
|||||||
fileName: file.fileName,
|
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),
|
qingTingAndroidKey: selectQtfmAndroidKey(state),
|
||||||
};
|
};
|
||||||
return decryptionQueue.add({ id: fileId, blobURI: file.raw, options }, onPreProcess);
|
return decryptionQueue.add({ id: fileId, blobURI: file.raw, options }, onPreProcess);
|
||||||
});
|
});
|
||||||
|
@ -1,43 +1,118 @@
|
|||||||
import { Box, Flex, Heading, Input, Table, Tbody, Td, Text, Th, Tr } from '@chakra-ui/react';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Code,
|
||||||
|
Flex,
|
||||||
|
FormControl,
|
||||||
|
FormHelperText,
|
||||||
|
FormLabel,
|
||||||
|
Heading,
|
||||||
|
Icon,
|
||||||
|
Input,
|
||||||
|
Text,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
import { qtfmAndroidUpdateKey } from '../settingsSlice';
|
|
||||||
import { useAppDispatch, useAppSelector } from '~/hooks';
|
import { useAppDispatch, useAppSelector } from '~/hooks';
|
||||||
import { selectStagingQtfmAndroidDevice } from '../settingsSelector';
|
import { fetchParakeet } from '@jixun/libparakeet';
|
||||||
import type { QingTingDeviceInfo } from '@jixun/libparakeet';
|
import { MdLock } from 'react-icons/md';
|
||||||
|
import { ExtLink } from '~/components/ExtLink';
|
||||||
|
import { ChangeEvent, ClipboardEvent } from 'react';
|
||||||
|
import { VQuote } from '~/components/HelpText/VQuote';
|
||||||
|
import { selectStagingQtfmAndroidKey } from '../settingsSelector';
|
||||||
|
import { qtfmAndroidUpdateKey } from '../settingsSlice';
|
||||||
|
|
||||||
const QTFM_ANDROID_DEVICE_PROPS = ['product', 'device', 'manufacturer', 'brand', 'board', 'model'] as const;
|
const QTFM_DEVICE_ID_URL = 'https://github.com/parakeet-rs/qtfm-device-id/releases/latest';
|
||||||
|
|
||||||
export function PanelQingTing() {
|
export function PanelQingTing() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const qingTingAndroidDeviceInfo = useAppSelector(selectStagingQtfmAndroidDevice);
|
const secretKey = useAppSelector(selectStagingQtfmAndroidKey);
|
||||||
const handleChangeDeviceInfo = (name: keyof QingTingDeviceInfo) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
const setSecretKey = (secretKey: string) => {
|
||||||
dispatch(qtfmAndroidUpdateKey({ field: name, value: e.target.value }));
|
dispatch(qtfmAndroidUpdateKey({ deviceKey: secretKey }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDataPaste = (e: ClipboardEvent<HTMLInputElement>) => {
|
||||||
|
const plainText = e.clipboardData.getData('text/plain');
|
||||||
|
const matchDeviceSecret = plainText.match(/^DEVICE_SECRET: ([0-9a-fA-F]+)/m);
|
||||||
|
if (matchDeviceSecret) {
|
||||||
|
e.preventDefault();
|
||||||
|
setSecretKey(matchDeviceSecret[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataMap = new Map();
|
||||||
|
for (const [_unused, key, value] of plainText.matchAll(
|
||||||
|
/^(PRODUCT|DEVICE|MANUFACTURER|BRAND|BOARD|MODEL): (.+)/gim,
|
||||||
|
)) {
|
||||||
|
dataMap.set(key.toLowerCase(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const product = dataMap.get('product') ?? null;
|
||||||
|
const device = dataMap.get('device') ?? null;
|
||||||
|
const manufacturer = dataMap.get('manufacturer') ?? null;
|
||||||
|
const brand = dataMap.get('brand') ?? null;
|
||||||
|
const board = dataMap.get('board') ?? null;
|
||||||
|
const model = dataMap.get('model') ?? null;
|
||||||
|
if (
|
||||||
|
product !== null &&
|
||||||
|
device !== null &&
|
||||||
|
manufacturer !== null &&
|
||||||
|
brand !== null &&
|
||||||
|
board !== null &&
|
||||||
|
model !== null
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
fetchParakeet().then((parakeet) => {
|
||||||
|
setSecretKey(parakeet.qtfm.createDeviceKey(product, device, manufacturer, brand, board, model));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDataInput = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSecretKey(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex minH={0} flexDir="column" flex={1}>
|
<Flex minH={0} flexDir="column" flex={1}>
|
||||||
<Heading as="h2" size="lg">
|
<Heading as="h2" size="lg">
|
||||||
「蜻蜓 FM」设备信息
|
<VQuote>蜻蜓 FM</VQuote>
|
||||||
|
设备密钥
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Text>「蜻蜓 FM」安卓版本需要获取设备信息用于生成解密密钥。</Text>
|
<Text>
|
||||||
|
<VQuote>蜻蜓 FM</VQuote>安卓版本需要获取设备密钥,并以此来生成解密密钥。
|
||||||
<Box flex={1} minH={0} overflow="auto" pr="4">
|
</Text>
|
||||||
<Table>
|
<Box display="none">
|
||||||
<Tbody>
|
{/* TODO: 解密弹窗、带步骤说明 */}
|
||||||
{QTFM_ANDROID_DEVICE_PROPS.map((name) => (
|
<Box p={2} pt={4} pb={4}>
|
||||||
<Tr key={name}>
|
<Button onClick={() => {}} leftIcon={<Icon as={MdLock} boxSize={5} />} variant="outline">
|
||||||
<Th w="1px" p={1} textAlign="right">
|
获取解密密钥
|
||||||
{name}
|
</Button>
|
||||||
</Th>
|
</Box>
|
||||||
<Td pl={1} pr={1}>
|
|
||||||
<Input value={qingTingAndroidDeviceInfo[name]} onChange={handleChangeDeviceInfo(name)} />
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
))}
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box mt={3} mb={3}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>解密密钥</FormLabel>
|
||||||
|
<Input type="text" onPaste={handleDataPaste} value={secretKey} onChange={handleDataInput} />
|
||||||
|
<FormHelperText>
|
||||||
|
{'粘贴含有密钥的信息时将自动提取密钥(如通过 '}
|
||||||
|
<ExtLink href={QTFM_DEVICE_ID_URL}>
|
||||||
|
<Code>qtfm-device-id</Code>
|
||||||
|
</ExtLink>
|
||||||
|
{' 获取的内容)。'}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* TODO: 填入内部储存开始的完整路径 */}
|
||||||
|
<Text>
|
||||||
|
注:<VQuote>蜻蜓 FM</VQuote>下载的文件储存在 <Code>QTDownloadRadio</Code> 目录下,并使用
|
||||||
|
<VQuote>
|
||||||
|
<Code>.</Code>
|
||||||
|
</VQuote>
|
||||||
|
开始的文件名。
|
||||||
|
</Text>
|
||||||
|
<Text>因为解密密钥与文件名相关,因此解密前请不要更改文件名。</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
// TODO: Popup dialog for QingTing instructions
|
@ -2,11 +2,10 @@ 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, QINGTING_DEVICE_INFO_KEY } 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 type { QingTingDeviceInfo } from '@jixun/libparakeet';
|
|
||||||
|
|
||||||
const DEFAULT_STORAGE_KEY = 'um-react-settings';
|
const DEFAULT_STORAGE_KEY = 'um-react-settings';
|
||||||
|
|
||||||
@ -35,14 +34,8 @@ function mergeSettings(settings: ProductionSettings): ProductionSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings?.qtfm?.android) {
|
if (typeof settings?.qtfm?.android === 'string') {
|
||||||
const qtfmAndroid = settings.qtfm.android;
|
draft.qtfm.android = settings.qtfm.android.replace(/[^0-9a-fA-F]/g, '');
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -48,5 +48,5 @@ export const selectKWMv2Key = (state: RootState, headerView: DataView): string |
|
|||||||
return ekey;
|
return ekey;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const selectStagingQtfmAndroidDevice = (state: RootState) => state.settings.staging.qtfm.android;
|
export const selectStagingQtfmAndroidKey = (state: RootState) => state.settings.staging.qtfm.android;
|
||||||
export const selectQtfmAndroidDevice = (state: RootState) => state.settings.production.qtfm.android;
|
export const selectQtfmAndroidKey = (state: RootState) => state.settings.production.qtfm.android;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
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';
|
||||||
@ -17,24 +16,6 @@ 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[];
|
||||||
@ -44,7 +25,7 @@ export interface StagingSettings {
|
|||||||
keys: StagingKWMv2Key[];
|
keys: StagingKWMv2Key[];
|
||||||
};
|
};
|
||||||
qtfm: {
|
qtfm: {
|
||||||
android: QingTingDeviceInfo;
|
android: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +38,7 @@ export interface ProductionSettings {
|
|||||||
keys: ProductionKWMv2Keys; // { [`${rid}-${quality}`]: ekey }
|
keys: ProductionKWMv2Keys; // { [`${rid}-${quality}`]: ekey }
|
||||||
};
|
};
|
||||||
qtfm: {
|
qtfm: {
|
||||||
android: QingTingDeviceInfo;
|
android: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,12 +52,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 },
|
qtfm: { android: '' },
|
||||||
},
|
},
|
||||||
production: {
|
production: {
|
||||||
qmc2: { allowFuzzyNameSearch: true, keys: {} },
|
qmc2: { allowFuzzyNameSearch: true, keys: {} },
|
||||||
kwm2: { keys: {} },
|
kwm2: { keys: {} },
|
||||||
qtfm: { android: DEFAULT_QINGTING_ANDROID_DEVICE_INFO },
|
qtfm: { android: '' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -171,11 +152,8 @@ export const settingsSlice = createSlice({
|
|||||||
state.dirty = true;
|
state.dirty = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
qtfmAndroidUpdateKey(
|
qtfmAndroidUpdateKey(state, { payload: { deviceKey } }: PayloadAction<{ deviceKey: string }>) {
|
||||||
state,
|
state.staging.qtfm.android = deviceKey;
|
||||||
{ payload: { field, value } }: PayloadAction<{ field: keyof QingTingDeviceInfo; value: string }>,
|
|
||||||
) {
|
|
||||||
state.staging.qtfm.android[field] = value;
|
|
||||||
state.dirty = true;
|
state.dirty = true;
|
||||||
},
|
},
|
||||||
kwm2ClearKeys(state) {
|
kwm2ClearKeys(state) {
|
||||||
|
Loading…
Reference in New Issue
Block a user