mirror of
https://git.um-react.app/um/um-react.git
synced 2025-11-28 11:33:02 +00:00
refactor: batch 3
This commit is contained in:
@@ -1,33 +1,28 @@
|
||||
import { Code, ListItem, OrderedList, Text, chakra } from '@chakra-ui/react';
|
||||
|
||||
const KUWO_IOS_DIR = '/var/mobile/Containers/Data/Application/<酷我数据目录>/mmkv';
|
||||
import { HiWord } from '~/components/HelpText/HiWord';
|
||||
|
||||
export function InstructionsIOS() {
|
||||
return (
|
||||
<>
|
||||
<Text>你需要越狱来访问 iOS 应用的私有数据。</Text>
|
||||
<Text>
|
||||
<p>你需要越狱来访问 iOS 应用的私有数据。</p>
|
||||
<p>
|
||||
⚠️ 请注意,越狱通常意味着你的设备
|
||||
<chakra.span color="red.400">将失去保修资格</chakra.span>。
|
||||
</Text>
|
||||
<OrderedList>
|
||||
<ListItem>
|
||||
<Text>
|
||||
访问设备的这个目录:
|
||||
<Code wordBreak="break-word">{KUWO_IOS_DIR}</Code>
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
提取密钥数据库文件 <Code>kw_ekey</Code> 至浏览器可访问的目录,如下载目录。
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
提交刚刚提取的 <Code>kw_ekey</Code> 密钥数据库。
|
||||
</Text>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
<span className="text-red-600">将失去保修资格</span>。
|
||||
</p>
|
||||
<ol className="list-decimal pl-6">
|
||||
<li>
|
||||
访问设备的这个目录:
|
||||
<br />
|
||||
<code className="break-words">
|
||||
/var/mobile/Containers/Data/Application/<HiWord className="text-nowrap">{'<酷我数据目录>'}</HiWord>/mmkv
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
提取密钥数据库文件 <code>kw_ekey</code> 至浏览器可访问的目录,如下载目录。
|
||||
</li>
|
||||
<li>
|
||||
提交刚刚提取的 <code>kw_ekey</code> 密钥数据库。
|
||||
</li>
|
||||
</ol>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import { Text } from '@chakra-ui/react';
|
||||
|
||||
export function InstructionsPC() {
|
||||
return (
|
||||
<>
|
||||
<Text>使用 Windows 客户端下载的文件不需要导入密钥。</Text>
|
||||
</>
|
||||
);
|
||||
return <p>使用 Windows 客户端下载的文件不需要导入密钥。</p>;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,16 @@
|
||||
import { Tab, TabList, TabPanel, TabPanels } from '@chakra-ui/react';
|
||||
import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction';
|
||||
import { InstructionsPC } from './InstructionsPC';
|
||||
import { InstructionsIOS } from './InstructionsIOS';
|
||||
import { InstructionsTabs, InstructionTab } from '~/components/InstructionsTabs';
|
||||
|
||||
export function KWMv2AllInstructions() {
|
||||
return (
|
||||
<>
|
||||
<TabList>
|
||||
<Tab>安卓</Tab>
|
||||
<Tab>iOS</Tab>
|
||||
<Tab>Windows</Tab>
|
||||
</TabList>
|
||||
<TabPanels flex={1} overflow="auto">
|
||||
<TabPanel>
|
||||
<AndroidADBPullInstruction
|
||||
dir="/data/data/cn.kuwo.player/files/mmkv"
|
||||
file="cn.kuwo.player.mmkv.defaultconfig"
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<InstructionsIOS />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<InstructionsPC />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</>
|
||||
);
|
||||
const ANDROID_DIR = '/data/data/cn.kuwo.player/files/mmkv';
|
||||
const ANDROID_FILE = 'cn.kuwo.player.mmkv.defaultconfig';
|
||||
const tabs: InstructionTab[] = [
|
||||
{ id: 'android', label: '安卓', content: <AndroidADBPullInstruction dir={ANDROID_DIR} file={ANDROID_FILE} /> },
|
||||
{ id: 'ios', label: 'iOS', content: <InstructionsIOS /> },
|
||||
{ id: 'windows', label: 'Windows', content: <InstructionsPC /> },
|
||||
];
|
||||
|
||||
return <InstructionsTabs tabs={tabs} />;
|
||||
}
|
||||
|
||||
@@ -1,81 +1,41 @@
|
||||
import {
|
||||
HStack,
|
||||
Icon,
|
||||
IconButton,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
InputRightElement,
|
||||
ListItem,
|
||||
Text,
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
import { MdDelete, MdVpnKey } from 'react-icons/md';
|
||||
import { PiFileAudio, PiHash } from 'react-icons/pi';
|
||||
import { kwm2DeleteKey, kwm2UpdateKey } from '../../settingsSlice';
|
||||
import { useAppDispatch } from '~/hooks';
|
||||
import { memo } from 'react';
|
||||
import { StagingKWMv2Key } from '../../keyFormats';
|
||||
import { KeyInput } from '~/components/KeyInput';
|
||||
|
||||
export const KWMv2EKeyItem = memo(({ id, ekey, quality, rid, i }: StagingKWMv2Key & { i: number }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const updateKey = (prop: keyof StagingKWMv2Key, e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(kwm2UpdateKey({ id, field: prop, value: e.target.value }));
|
||||
const deleteKey = () => dispatch(kwm2DeleteKey({ id }));
|
||||
const ekeyLen = ekey.length;
|
||||
const isValidEKey = ekeyLen === 364 || ekeyLen === 704;
|
||||
|
||||
return (
|
||||
<ListItem mt={0} pt={2} pb={2} _even={{ bg: 'gray.50' }}>
|
||||
<HStack>
|
||||
<Text w="2em" textAlign="center">
|
||||
{i + 1}
|
||||
</Text>
|
||||
|
||||
<VStack flex={1}>
|
||||
<HStack flex={1} w="full">
|
||||
<Input
|
||||
variant="flushed"
|
||||
placeholder="资源 ID"
|
||||
value={rid}
|
||||
onChange={(e) => updateKey('rid', e)}
|
||||
type="number"
|
||||
maxW="8em"
|
||||
/>
|
||||
<Input
|
||||
variant="flushed"
|
||||
placeholder="音质格式"
|
||||
value={quality}
|
||||
onChange={(e) => updateKey('quality', e)}
|
||||
flex={1}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
<InputGroup size="xs">
|
||||
<InputLeftElement pr="2">
|
||||
<Icon as={MdVpnKey} />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
variant="flushed"
|
||||
placeholder="密钥,通常包含 364 或 704 位字符,没有空格。"
|
||||
value={ekey}
|
||||
onChange={(e) => updateKey('ekey', e)}
|
||||
/>
|
||||
<InputRightElement>
|
||||
<Text pl="2" color={ekey.length ? 'green.500' : 'red.500'}>
|
||||
<code>{ekey.length || '?'}</code>
|
||||
</Text>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
</VStack>
|
||||
|
||||
<IconButton
|
||||
aria-label="删除该密钥"
|
||||
icon={<Icon as={MdDelete} boxSize={6} />}
|
||||
variant="ghost"
|
||||
colorScheme="red"
|
||||
type="button"
|
||||
onClick={deleteKey}
|
||||
/>
|
||||
</HStack>
|
||||
</ListItem>
|
||||
<KeyInput
|
||||
name={rid}
|
||||
quality={quality}
|
||||
value={ekey}
|
||||
isValidKey={isValidEKey}
|
||||
onSetName={(value) => dispatch(kwm2UpdateKey({ id, field: 'rid', value }))}
|
||||
onSetQuality={(value) => dispatch(kwm2UpdateKey({ id, field: 'quality', value }))}
|
||||
onSetValue={(value) => dispatch(kwm2UpdateKey({ id, field: 'ekey', value }))}
|
||||
onDelete={() => dispatch(kwm2DeleteKey({ id }))}
|
||||
sequence={i + 1}
|
||||
nameLabel={
|
||||
<>
|
||||
ID
|
||||
<PiHash className="hidden md:inline-block" />
|
||||
</>
|
||||
}
|
||||
qualityLabel={
|
||||
<>
|
||||
质量 <PiFileAudio className="hidden md:inline-block" />
|
||||
</>
|
||||
}
|
||||
namePlaceholder="音频哈希。不建议手动填写。"
|
||||
qualityPlaceholder="比特率 ID"
|
||||
valuePlaceholder="密钥,通常包含 364 或 704 位字符,没有空格。"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,34 +1,50 @@
|
||||
import { Code, Heading, ListItem, OrderedList, Text } from '@chakra-ui/react';
|
||||
import { RiFileCopyLine } from 'react-icons/ri';
|
||||
import { toast } from 'react-toastify';
|
||||
import { ExtLink } from '~/components/ExtLink';
|
||||
import { FilePathBlock } from '~/components/FilePathBlock.tsx';
|
||||
|
||||
export function InstructionsPC() {
|
||||
const DB_PATH = '%APPDATA%\\KuGou8\\KGMusicV3.db';
|
||||
const copyDbPathToClipboard = () => {
|
||||
navigator.clipboard
|
||||
.writeText(DB_PATH)
|
||||
.then(() => {
|
||||
toast.success('已复制到剪贴板');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(`复制失败,请手动复制\n${err}`);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text>酷狗的 Windows 客户端使用 <abbr title="SQLite w/ SQLCipher">SQLite</abbr> 数据库储存密钥。</Text>
|
||||
<Text>该密钥文件通常存储在下述路径:</Text>
|
||||
<FilePathBlock>%APPDATA%\KuGou8\KGMusicV3.db</FilePathBlock>
|
||||
<p>
|
||||
酷狗的 Windows 客户端使用魔改的
|
||||
<ExtLink className="link-info px-1" href="https://www.zetetic.net/sqlcipher/">
|
||||
SQLCipher
|
||||
</ExtLink>
|
||||
来加密储存密钥。
|
||||
</p>
|
||||
<p>该密钥数据库通常位于下述路径:</p>
|
||||
<p className="flex items-center gap-1">
|
||||
<FilePathBlock>{DB_PATH}</FilePathBlock>
|
||||
</p>
|
||||
|
||||
<Heading as="h3" size="md" mt="4">
|
||||
导入密钥
|
||||
</Heading>
|
||||
<OrderedList>
|
||||
<ListItem>
|
||||
<Text>
|
||||
选中并复制上述的 <Code>KGMusicV3.db</Code> 文件路径
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>点击上方的「文件选择区域」,打开「文件选择框」</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
在「文件名」输入框中粘贴之前复制的 <Code>KGMusicV3.db</Code> 文件路径
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>按下「回车键」确认。</Text>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
<h3 className="font-bold text-xl mt-4">导入密钥</h3>
|
||||
<ol className="list-decimal pl-6">
|
||||
<li>
|
||||
<button className="btn btn-sm btn-outline btn-accent mr-2" onClick={copyDbPathToClipboard}>
|
||||
<RiFileCopyLine className="text-xl" />
|
||||
<span>复制</span>
|
||||
</button>
|
||||
<code>KGMusicV3.db</code> 文件路径
|
||||
</li>
|
||||
<li>点击上方的「文件选择区域」,打开「文件选择框」</li>
|
||||
<li>
|
||||
在「文件名」输入框中粘贴之前复制的 <code>KGMusicV3.db</code> 文件路径
|
||||
</li>
|
||||
<li>按下「回车键」确认。</li>
|
||||
</ol>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
import { Tab, TabList, TabPanel, TabPanels } from '@chakra-ui/react';
|
||||
import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction';
|
||||
import { InstructionsPC } from './InstructionsPC';
|
||||
import { InstructionsTabs, InstructionTab } from '~/components/InstructionsTabs';
|
||||
|
||||
export function KugouAllInstructions() {
|
||||
return (
|
||||
<>
|
||||
<TabList>
|
||||
<Tab>安卓</Tab>
|
||||
<Tab>Windows</Tab>
|
||||
</TabList>
|
||||
<TabPanels flex={1} overflow="auto">
|
||||
<TabPanel>
|
||||
<AndroidADBPullInstruction
|
||||
dir="/data/data/com.kugou.android/files/mmkv"
|
||||
file="mggkey_multi_process"
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<InstructionsPC />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</>
|
||||
);
|
||||
const ANDROID_DIR = '/data/data/com.kugou.android/files/mmkv';
|
||||
const ANDROID_FILE = 'mggkey_multi_process';
|
||||
const tabs: InstructionTab[] = [
|
||||
{ id: 'android', label: '安卓', content: <AndroidADBPullInstruction dir={ANDROID_DIR} file={ANDROID_FILE} /> },
|
||||
{ id: 'windows', label: 'Windows', content: <InstructionsPC /> },
|
||||
];
|
||||
|
||||
return <InstructionsTabs tabs={tabs} />;
|
||||
}
|
||||
|
||||
@@ -1,72 +1,26 @@
|
||||
import {
|
||||
HStack,
|
||||
Icon,
|
||||
IconButton,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
InputRightElement,
|
||||
ListItem,
|
||||
Text,
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
import { MdDelete, MdVpnKey } from 'react-icons/md';
|
||||
import { kugouDeleteKey, kugouUpdateKey } from '../../settingsSlice';
|
||||
import { useAppDispatch } from '~/hooks';
|
||||
import { memo } from 'react';
|
||||
import { StagingKugouKey } from '../../keyFormats';
|
||||
import { KeyInput } from '~/components/KeyInput';
|
||||
|
||||
export const KugouEKeyItem = memo(({ id, ekey, audioHash, i }: StagingKugouKey & { i: number }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const updateKey = (prop: keyof StagingKugouKey, e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(kugouUpdateKey({ id, field: prop, value: e.target.value }));
|
||||
const deleteKey = () => dispatch(kugouDeleteKey({ id }));
|
||||
const ekeyLen = ekey.length;
|
||||
const isValidEKey = ekeyLen === 364 || ekeyLen === 704;
|
||||
|
||||
return (
|
||||
<ListItem mt={0} pt={2} pb={2} _even={{ bg: 'gray.50' }}>
|
||||
<HStack>
|
||||
<Text w="2em" textAlign="center">
|
||||
{i + 1}
|
||||
</Text>
|
||||
|
||||
<VStack flex={1}>
|
||||
<HStack flex={1} w="full">
|
||||
<Input
|
||||
variant="flushed"
|
||||
placeholder="音频哈希。不建议手动填写。"
|
||||
value={audioHash}
|
||||
onChange={(e) => updateKey('audioHash', e)}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
<InputGroup size="xs">
|
||||
<InputLeftElement pr="2">
|
||||
<Icon as={MdVpnKey} />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
variant="flushed"
|
||||
placeholder="密钥,通常包含 364 或 704 位字符,没有空格。"
|
||||
value={ekey}
|
||||
onChange={(e) => updateKey('ekey', e)}
|
||||
/>
|
||||
<InputRightElement>
|
||||
<Text pl="2" color={ekey.length ? 'green.500' : 'red.500'}>
|
||||
<code>{ekey.length || '?'}</code>
|
||||
</Text>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
</VStack>
|
||||
|
||||
<IconButton
|
||||
aria-label="删除该密钥"
|
||||
icon={<Icon as={MdDelete} boxSize={6} />}
|
||||
variant="ghost"
|
||||
colorScheme="red"
|
||||
type="button"
|
||||
onClick={deleteKey}
|
||||
/>
|
||||
</HStack>
|
||||
</ListItem>
|
||||
<KeyInput
|
||||
name={audioHash}
|
||||
value={ekey}
|
||||
isValidKey={isValidEKey}
|
||||
onSetName={(value) => dispatch(kugouUpdateKey({ id, field: 'audioHash', value }))}
|
||||
onSetValue={(value) => dispatch(kugouUpdateKey({ id, field: 'ekey', value }))}
|
||||
onDelete={() => dispatch(kugouDeleteKey({ id }))}
|
||||
sequence={i + 1}
|
||||
namePlaceholder="音频哈希。不建议手动填写。"
|
||||
valuePlaceholder="密钥,通常包含 364 或 704 位字符,没有空格。"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Box, Flex, Heading, List, Text, useToast } from '@chakra-ui/react';
|
||||
import { useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { ImportSecretModal } from '~/components/ImportSecretModal';
|
||||
@@ -12,9 +11,10 @@ import { KugouEKeyItem } from '~/features/settings/panels/Kugou/KugouEKeyItem.ts
|
||||
import { KugouAllInstructions } from '~/features/settings/panels/Kugou/KugouAllInstructions.tsx';
|
||||
import { parseAndroidKugouMMKV } from '~/util/mmkv/kugou.ts';
|
||||
import { DatabaseKeyExtractor } from '~/util/DatabaseKeyExtractor.ts';
|
||||
import { KeyListContainer } from '~/components/KeyListContainer';
|
||||
import { toastImportResult } from '~/util/toastImportResult';
|
||||
|
||||
export function PanelKGGKey() {
|
||||
const toast = useToast();
|
||||
const dispatch = useDispatch();
|
||||
const kugouKeys = useSelector(selectStagingKugouV5Keys);
|
||||
const [showImportModal, setShowImportModal] = useState(false);
|
||||
@@ -30,49 +30,34 @@ export function PanelKGGKey() {
|
||||
keys = extractor.extractKugouKeyFromEncryptedDb(await file.arrayBuffer());
|
||||
}
|
||||
|
||||
if (keys?.length === 0) {
|
||||
toast({
|
||||
title: '未导入密钥',
|
||||
description: '选择的密钥数据库文件未发现任何可用的密钥。',
|
||||
isClosable: true,
|
||||
status: 'warning',
|
||||
});
|
||||
} else if (keys) {
|
||||
if (keys && keys.length > 0) {
|
||||
dispatch(kugouImportKeys(keys));
|
||||
setShowImportModal(false);
|
||||
toast({
|
||||
title: `导入完成,共导入了 ${keys.length} 个密钥。`,
|
||||
description: '记得按下「保存」来应用。',
|
||||
isClosable: true,
|
||||
status: 'success',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: `不支持的文件:${file.name}`,
|
||||
isClosable: true,
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
toastImportResult(file.name, keys);
|
||||
};
|
||||
|
||||
const refKeyContainer = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<Flex minH={0} flexDir="column" flex={1}>
|
||||
<Heading as="h2" size="lg">
|
||||
酷狗解密密钥 (KGG / KGM v5)
|
||||
</Heading>
|
||||
<div className="container flex flex-col grow min-h-0 w-full">
|
||||
<h2 className="text-2xl font-bold">酷狗解密密钥 (KGG / KGM v5)</h2>
|
||||
|
||||
<Text>酷狗已经升级了加密方式,现在使用 KGG / KGM v5 加密。</Text>
|
||||
<p>酷狗已经升级了加密方式,现在使用 KGG / KGM v5 加密。</p>
|
||||
|
||||
<AddKey addKey={addKey} importKeyFromFile={() => setShowImportModal(true)} clearKeys={clearAll} />
|
||||
<h3 className="mt-2 text-xl font-bold">密钥管理</h3>
|
||||
<AddKey
|
||||
addKey={addKey}
|
||||
refContainer={refKeyContainer}
|
||||
importKeyFromFile={() => setShowImportModal(true)}
|
||||
clearKeys={clearAll}
|
||||
/>
|
||||
|
||||
<Box flex={1} minH={0} overflow="auto" pr="4">
|
||||
<List spacing={3}>
|
||||
{kugouKeys.map(({ id, audioHash, ekey }, i) => (
|
||||
<KugouEKeyItem key={id} id={id} ekey={ekey} audioHash={audioHash} i={i} />
|
||||
))}
|
||||
</List>
|
||||
{kugouKeys.length === 0 && <Text>还没有添加密钥。</Text>}
|
||||
</Box>
|
||||
<KeyListContainer ref={refKeyContainer} keys={kugouKeys}>
|
||||
{kugouKeys.map(({ id, audioHash, ekey }, i) => (
|
||||
<KugouEKeyItem key={id} id={id} ekey={ekey} audioHash={audioHash} i={i} />
|
||||
))}
|
||||
</KeyListContainer>
|
||||
|
||||
<ImportSecretModal
|
||||
clientName="酷狗音乐"
|
||||
@@ -82,6 +67,6 @@ export function PanelKGGKey() {
|
||||
>
|
||||
<KugouAllInstructions />
|
||||
</ImportSecretModal>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Code,
|
||||
Flex,
|
||||
HStack,
|
||||
Heading,
|
||||
Icon,
|
||||
IconButton,
|
||||
List,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuDivider,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Text,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { MdAdd, MdDeleteForever, MdExpandMore, MdFileUpload } from 'react-icons/md';
|
||||
|
||||
import { ImportSecretModal } from '~/components/ImportSecretModal';
|
||||
import { parseAndroidKuwoEKey, parseIosKuwoEKey } from '~/util/mmkv/kuwo';
|
||||
@@ -29,9 +9,11 @@ import { selectStagingKWMv2Keys } from '../settingsSelector';
|
||||
import { KWMv2EKeyItem } from './KWMv2/KWMv2EKeyItem';
|
||||
import type { StagingKWMv2Key } from '../keyFormats';
|
||||
import { KWMv2AllInstructions } from './KWMv2/KWMv2AllInstructions';
|
||||
import { AddKey } from '~/components/AddKey';
|
||||
import { KeyListContainer } from '~/components/KeyListContainer';
|
||||
import { toastImportResult } from '~/util/toastImportResult';
|
||||
|
||||
export function PanelKWMv2Key() {
|
||||
const toast = useToast();
|
||||
const dispatch = useDispatch();
|
||||
const kwm2Keys = useSelector(selectStagingKWMv2Keys);
|
||||
const [showImportModal, setShowImportModal] = useState(false);
|
||||
@@ -46,70 +28,33 @@ export function PanelKWMv2Key() {
|
||||
keys = parseIosKuwoEKey(new DataView(await file.arrayBuffer()));
|
||||
}
|
||||
|
||||
if (keys?.length === 0) {
|
||||
toast({
|
||||
title: '未导入密钥',
|
||||
description: '选择的密钥数据库文件未发现任何可用的密钥。',
|
||||
isClosable: true,
|
||||
status: 'warning',
|
||||
});
|
||||
} else if (keys) {
|
||||
if (keys && keys.length > 0) {
|
||||
dispatch(kwm2ImportKeys(keys));
|
||||
setShowImportModal(false);
|
||||
toast({
|
||||
title: `导入完成,共导入了 ${keys.length} 个密钥。`,
|
||||
description: '记得按下「保存」来应用。',
|
||||
isClosable: true,
|
||||
status: 'success',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: `不支持的文件:${file.name}`,
|
||||
isClosable: true,
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
toastImportResult(file.name, keys);
|
||||
};
|
||||
|
||||
const refKeyContainer = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<Flex minH={0} flexDir="column" flex={1}>
|
||||
<Heading as="h2" size="lg">
|
||||
酷我解密密钥(KwmV2)
|
||||
</Heading>
|
||||
<div className="container flex flex-col grow min-h-0 w-full">
|
||||
<h2 className="text-2xl font-bold">酷我解密密钥(KwmV2)</h2>
|
||||
<p>
|
||||
酷我安卓版本的「臻品音质」已经换用 V2 版,表现为加密文件的后缀名为 <code>mflac</code> 或 <code>mgg</code>。
|
||||
</p>
|
||||
<p>该格式需要提取密钥后才能正常解密。</p>
|
||||
<AddKey
|
||||
addKey={addKey}
|
||||
refContainer={refKeyContainer}
|
||||
importKeyFromFile={() => setShowImportModal(true)}
|
||||
clearKeys={clearAll}
|
||||
/>
|
||||
|
||||
<Text>
|
||||
酷我安卓版本的「臻品音质」已经换用 V2 版,后缀名为 <Code>mflac</Code> 或沿用旧的 <Code>kwm</Code>。{''}
|
||||
该格式需要提取密钥后才能正常解密。
|
||||
</Text>
|
||||
|
||||
<HStack pb={2} pt={2}>
|
||||
<ButtonGroup isAttached colorScheme="purple" variant="outline">
|
||||
<Button onClick={addKey} leftIcon={<Icon as={MdAdd} />}>
|
||||
添加一条密钥
|
||||
</Button>
|
||||
<Menu>
|
||||
<MenuButton as={IconButton} icon={<MdExpandMore />}></MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem onClick={() => setShowImportModal(true)} icon={<Icon as={MdFileUpload} boxSize={5} />}>
|
||||
从文件导入密钥…
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
<MenuItem color="red" onClick={clearAll} icon={<Icon as={MdDeleteForever} boxSize={5} />}>
|
||||
清空密钥
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</ButtonGroup>
|
||||
</HStack>
|
||||
|
||||
<Box flex={1} minH={0} overflow="auto" pr="4">
|
||||
<List spacing={3}>
|
||||
{kwm2Keys.map(({ id, ekey, quality, rid }, i) => (
|
||||
<KWMv2EKeyItem key={id} id={id} ekey={ekey} quality={quality} rid={rid} i={i} />
|
||||
))}
|
||||
</List>
|
||||
{kwm2Keys.length === 0 && <Text>还没有添加密钥。</Text>}
|
||||
</Box>
|
||||
<KeyListContainer ref={refKeyContainer} keys={kwm2Keys}>
|
||||
{kwm2Keys.map(({ id, ekey, quality, rid }, i) => (
|
||||
<KWMv2EKeyItem key={id} id={id} ekey={ekey} quality={quality} rid={rid} i={i} />
|
||||
))}
|
||||
</KeyListContainer>
|
||||
|
||||
<ImportSecretModal
|
||||
clientName="酷我音乐"
|
||||
@@ -119,6 +64,6 @@ export function PanelKWMv2Key() {
|
||||
>
|
||||
<KWMv2AllInstructions />
|
||||
</ImportSecretModal>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { qmc2AddKey, qmc2AllowFuzzyNameSearch, qmc2ClearKeys, qmc2ImportKeys } from '../settingsSlice';
|
||||
import { selectStagingQMCv2Settings } from '../settingsSelector';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { QMCv2EKeyItem } from './QMCv2/QMCv2EKeyItem';
|
||||
import { ImportSecretModal } from '~/components/ImportSecretModal';
|
||||
import { StagingQMCv2Key } from '../keyFormats';
|
||||
@@ -15,9 +14,10 @@ import { AddKey } from '~/components/AddKey';
|
||||
import { InfoModal } from '~/components/InfoModal.tsx';
|
||||
import { Ruby } from '~/components/Ruby.tsx';
|
||||
import { ExtLink } from '~/components/ExtLink.tsx';
|
||||
import { KeyListContainer } from '~/components/KeyListContainer';
|
||||
import { toastImportResult } from '~/util/toastImportResult';
|
||||
|
||||
export function PanelQMCv2Key() {
|
||||
const toast = useToast();
|
||||
const dispatch = useDispatch();
|
||||
const { keys: qmc2Keys, allowFuzzyNameSearch } = useSelector(selectStagingQMCv2Settings);
|
||||
const [showImportModal, setShowImportModal] = useState(false);
|
||||
@@ -34,46 +34,31 @@ export function PanelQMCv2Key() {
|
||||
try {
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
|
||||
let qmc2Keys: null | Omit<StagingQMCv2Key, 'id'>[] = null;
|
||||
let keys: null | Omit<StagingQMCv2Key, 'id'>[] = null;
|
||||
|
||||
if (/(player_process[_.]db|music_audio_play)(\.db)?$/i.test(file.name)) {
|
||||
const extractor = await DatabaseKeyExtractor.getInstance();
|
||||
qmc2Keys = extractor.extractQmcV2KeysFromSqliteDb(fileBuffer);
|
||||
if (!qmc2Keys) {
|
||||
alert(`不是支持的 SQLite 数据库文件。`);
|
||||
return;
|
||||
}
|
||||
keys = extractor.extractQmcV2KeysFromSqliteDb(fileBuffer);
|
||||
} else if (/MMKVStreamEncryptId|filenameEkeyMap|qmpc-mmkv-v1|(\.mmkv$)/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 }));
|
||||
keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey }));
|
||||
}
|
||||
|
||||
if (qmc2Keys?.length === 0) {
|
||||
toast({
|
||||
title: '未导入密钥',
|
||||
description: '选择的密钥数据库文件未发现任何可用的密钥。',
|
||||
isClosable: true,
|
||||
status: 'warning',
|
||||
});
|
||||
} else if (qmc2Keys) {
|
||||
dispatch(qmc2ImportKeys(qmc2Keys));
|
||||
if (keys && keys.length > 0) {
|
||||
dispatch(qmc2ImportKeys(keys));
|
||||
setShowImportModal(false);
|
||||
toast({
|
||||
title: `导入成功 (${qmc2Keys.length})`,
|
||||
description: '记得保存更改来应用。',
|
||||
isClosable: true,
|
||||
status: 'success',
|
||||
});
|
||||
} else {
|
||||
alert(`不支持的文件:${file.name}`);
|
||||
}
|
||||
|
||||
toastImportResult(file.name, keys);
|
||||
} catch (e) {
|
||||
console.error('error during import: ', e);
|
||||
alert(`导入数据库时发生错误:${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
const refKeyContainer = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-2xl font-bold">QMCv2 解密密钥</h2>
|
||||
@@ -112,19 +97,19 @@ export function PanelQMCv2Key() {
|
||||
</InfoModal>
|
||||
</div>
|
||||
|
||||
<h3 className="mt-2 text-1xl font-bold">密钥管理</h3>
|
||||
<AddKey addKey={addKey} importKeyFromFile={() => setShowImportModal(true)} clearKeys={clearAll} />
|
||||
<h3 className="mt-2 text-xl font-bold">密钥管理</h3>
|
||||
<AddKey
|
||||
addKey={addKey}
|
||||
refContainer={refKeyContainer}
|
||||
importKeyFromFile={() => setShowImportModal(true)}
|
||||
clearKeys={clearAll}
|
||||
/>
|
||||
|
||||
<div className="flex-1 min-h-0 overflow-auto pr-4 pt-3">
|
||||
{qmc2Keys.length > 0 && (
|
||||
<ul className="list bg-base-100 rounded-box shadow-md border border-base-300">
|
||||
{qmc2Keys.map(({ id, ekey, name }, i) => (
|
||||
<QMCv2EKeyItem key={id} id={id} ekey={ekey} name={name} i={i} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{qmc2Keys.length === 0 && <p className="p-4 pb-2 tracking-wide">还没有密钥。</p>}
|
||||
</div>
|
||||
<KeyListContainer ref={refKeyContainer} keys={qmc2Keys}>
|
||||
{qmc2Keys.map(({ id, ekey, name }, i) => (
|
||||
<QMCv2EKeyItem key={id} id={id} ekey={ekey} name={name} i={i} />
|
||||
))}
|
||||
</KeyListContainer>
|
||||
|
||||
<ImportSecretModal
|
||||
clientName={
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
import {
|
||||
Box,
|
||||
Code,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
Heading,
|
||||
Input,
|
||||
ListItem,
|
||||
Text,
|
||||
UnorderedList,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '~/hooks';
|
||||
import { ExtLink } from '~/components/ExtLink';
|
||||
import { ChangeEvent, ClipboardEvent } from 'react';
|
||||
import { ChangeEvent, ClipboardEvent, useId } from 'react';
|
||||
import { VQuote } from '~/components/HelpText/VQuote';
|
||||
import { selectStagingQtfmAndroidKey } from '../settingsSelector';
|
||||
import { qtfmAndroidUpdateKey } from '../settingsSlice';
|
||||
import { workerClientBus } from '~/decrypt-worker/client.ts';
|
||||
import { GetQingTingFMDeviceKeyPayload } from '~/decrypt-worker/types.ts';
|
||||
import { DECRYPTION_WORKER_ACTION_NAME } from '~/decrypt-worker/constants.ts';
|
||||
import { Ruby } from '~/components/Ruby';
|
||||
import { HiWord } from '~/components/HelpText/HiWord';
|
||||
|
||||
const QTFM_DEVICE_ID_URL = 'https://github.com/parakeet-rs/qtfm-device-id/releases/latest';
|
||||
|
||||
@@ -64,64 +52,72 @@ export function PanelQingTing() {
|
||||
setSecretKey(e.target.value);
|
||||
};
|
||||
|
||||
const idSecretKey = useId();
|
||||
|
||||
return (
|
||||
<Flex minH={0} flexDir="column" flex={1}>
|
||||
<Heading as="h2" size="lg">
|
||||
<div className="min-h-0 flex-col grow px-1">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
<VQuote>蜻蜓 FM</VQuote>
|
||||
设备密钥
|
||||
</Heading>
|
||||
</h2>
|
||||
|
||||
<Text>
|
||||
<p>
|
||||
<VQuote>蜻蜓 FM</VQuote>的安卓版本需要获取设备密钥,并以此来生成解密密钥。
|
||||
</Text>
|
||||
<Box mt={3} mb={3}>
|
||||
<FormControl>
|
||||
<FormLabel>设备密钥</FormLabel>
|
||||
<Input type="text" onPaste={handleDataPaste} value={secretKey} onChange={handleDataInput} />
|
||||
<FormHelperText>
|
||||
{'粘贴含有设备密钥的信息的内容时将自动提取密钥(如通过 '}
|
||||
</p>
|
||||
|
||||
<div className="my-4">
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend text-lg">
|
||||
<label htmlFor={idSecretKey}>设备密钥</label>
|
||||
</legend>
|
||||
<input
|
||||
id={idSecretKey}
|
||||
type="text"
|
||||
className="input font-mono"
|
||||
onPaste={handleDataPaste}
|
||||
value={secretKey}
|
||||
onChange={handleDataInput}
|
||||
/>
|
||||
<p className="label">
|
||||
粘贴含有设备密钥的信息的内容时将自动提取密钥(如通过
|
||||
<ExtLink href={QTFM_DEVICE_ID_URL}>
|
||||
<Code>qtfm-device-id</Code>
|
||||
<code>qtfm-device-id</code>
|
||||
</ExtLink>
|
||||
{' 获取的设备信息)。'}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</Box>
|
||||
获取的设备信息),不需要 root。
|
||||
</p>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<Heading as="h3" size="md" pt={3} pb={2}>
|
||||
注意事项
|
||||
</Heading>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Text>
|
||||
<h3 className="text-xl font-bold my-2">注意事项</h3>
|
||||
<ul className="list-disc pl-6">
|
||||
<li>
|
||||
<p>
|
||||
下载的文件位于
|
||||
<Code>[内部储存]/Android/data/fm.qingting.qtradio/files/Music/</Code>
|
||||
</Text>
|
||||
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Text>
|
||||
<VQuote>
|
||||
<code>
|
||||
<HiWord>[内部储存]</HiWord>/Android/data/fm.qingting.qtradio/files/Music/
|
||||
</code>
|
||||
</VQuote>
|
||||
</p>
|
||||
<ul className="list-disc pl-6">
|
||||
<li>
|
||||
<p>
|
||||
你可能需要使用有
|
||||
<ruby>
|
||||
特权
|
||||
<rp> (</rp>
|
||||
<rt>root</rt>
|
||||
<rp>)</rp>
|
||||
</ruby>
|
||||
<Ruby caption="root">特权</Ruby>
|
||||
的文件浏览器访问。
|
||||
</Text>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
音频文件文件名为「<Code>.p~!</Code>」前缀。
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>因为解密密钥与文件名相关,因此解密前请不要更改文件名。</Text>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</Flex>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
音频文件文件名为「<code>.p~!</code>」前缀。
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>因为解密密钥与文件名相关,因此解密前请不要更改文件名。</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Heading, Text, Code, Kbd, OrderedList, ListItem, Link } from '@chakra-ui/react';
|
||||
import { Heading, Text, Code, OrderedList, ListItem, Link } from '@chakra-ui/react';
|
||||
import { FilePathBlock } from '~/components/FilePathBlock';
|
||||
import { MacCommandKey } from '~/components/Key/MacCommandKey';
|
||||
import { ShiftKey } from '~/components/Key/ShiftKey';
|
||||
@@ -42,7 +42,7 @@ export function InstructionsMac() {
|
||||
{' + '}
|
||||
<MacCommandKey />
|
||||
{' + '}
|
||||
<Kbd>{'G'}</Kbd>」组合键打开「路径输入框」
|
||||
<kbd className="kbd">{'G'}</kbd>」组合键打开「路径输入框」
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
|
||||
@@ -1,16 +1,57 @@
|
||||
import { ExtLink } from '~/components/ExtLink';
|
||||
import { HiWord } from '~/components/HelpText/HiWord';
|
||||
import NoopExecutable from './assets/noop.exe?base64';
|
||||
import NoopExecutableSource from './assets/noop.asm.txt?base64';
|
||||
|
||||
export function InstructionsPC() {
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
使用 <span className="text-primary">19.51 或更低版本</span>下载的歌曲文件
|
||||
<mark>无需密钥</mark>。
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
使用 <span className="text-error">19.57 或更高版本</span>下载的歌曲文件
|
||||
<mark>需要导入密钥</mark>。
|
||||
<HiWord>需要导入密钥</HiWord>。
|
||||
<br />
|
||||
目前未公开密钥获取方式。
|
||||
目前未公开密钥获取方式,因此不支持。
|
||||
</p>
|
||||
|
||||
<p className="mt-4">
|
||||
使用 <span className="text-primary">19.51 或更低版本</span>下载的歌曲文件
|
||||
<HiWord>无需密钥</HiWord>。
|
||||
</p>
|
||||
|
||||
<p className="mt-4">
|
||||
获取 QQ 音乐 Windows <HiWord>19.51</HiWord> 客户端:
|
||||
</p>
|
||||
<ul className="list-disc pl-6">
|
||||
<li>
|
||||
<ExtLink href="https://web.archive.org/web/2023/https://dldir1v6.qq.com/music/clntupate/QQMusic_Setup_1951.exe">
|
||||
通过 <code>Archive.org</code> 缓存下载(慢)
|
||||
</ExtLink>
|
||||
</li>
|
||||
<li>
|
||||
<ExtLink href="https://t.me/um_lsr_ch/24">通过 Telegram 下载(需要账号)</ExtLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p className="mt-4">
|
||||
安装完成后可以覆盖 QQ 音乐安装目录下的
|
||||
<a
|
||||
className="link px-1"
|
||||
download="QQMusicUp.exe"
|
||||
href={`data:application/vnd.microsoft.portable-executable;base64,${NoopExecutable}`}
|
||||
>
|
||||
<code>QQMusicUp.exe</code>
|
||||
</a>
|
||||
同名文件(
|
||||
<a
|
||||
className="link px-1"
|
||||
download="QQMusicUp.asm"
|
||||
href={`data:text/x-asm;charset=utf-8;base64,${NoopExecutableSource}`}
|
||||
>
|
||||
源码
|
||||
</a>
|
||||
),屏蔽自动更新。
|
||||
</p>
|
||||
<p className="mt-2">降级后需要删除新版本下载的文件并重新使用旧版本下载。</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
16
src/features/settings/panels/QMCv2/assets/noop.asm.txt
Normal file
16
src/features/settings/panels/QMCv2/assets/noop.asm.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
; QQ 音乐更新 - 占位文件
|
||||
; 使用 FASM 编译即可。
|
||||
|
||||
format PE GUI 4.0
|
||||
entry start
|
||||
|
||||
include 'win32a.inc'
|
||||
|
||||
section '.text' code readable executable
|
||||
start:
|
||||
invoke ExitProcess, 0
|
||||
|
||||
section '.idata' import data readable writeable
|
||||
library kernel,'KERNEL32.DLL'
|
||||
import kernel,\
|
||||
ExitProcess,'ExitProcess'
|
||||
BIN
src/features/settings/panels/QMCv2/assets/noop.exe
Normal file
BIN
src/features/settings/panels/QMCv2/assets/noop.exe
Normal file
Binary file not shown.
Reference in New Issue
Block a user