Merge pull request '从安卓客户端的密钥数据库读取密钥' (#24) from feat/import-from-android into main
Reviewed-on: #24
This commit is contained in:
commit
10aa8ee20a
12
package.json
12
package.json
@ -32,7 +32,10 @@
|
|||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-icons": "^4.9.0",
|
"react-icons": "^4.9.0",
|
||||||
"react-promise-suspense": "^0.3.4",
|
"react-promise-suspense": "^0.3.4",
|
||||||
"react-redux": "^8.0.5"
|
"react-redux": "^8.0.5",
|
||||||
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
|
"sass": "^1.63.3",
|
||||||
|
"sql.js": "^1.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-replace": "^5.0.2",
|
"@rollup/plugin-replace": "^5.0.2",
|
||||||
@ -42,6 +45,8 @@
|
|||||||
"@types/node": "^20.2.5",
|
"@types/node": "^20.2.5",
|
||||||
"@types/react": "^18.2.7",
|
"@types/react": "^18.2.7",
|
||||||
"@types/react-dom": "^18.2.4",
|
"@types/react-dom": "^18.2.4",
|
||||||
|
"@types/react-syntax-highlighter": "^15.5.7",
|
||||||
|
"@types/sql.js": "^1.4.4",
|
||||||
"@types/testing-library__jest-dom": "^5.14.6",
|
"@types/testing-library__jest-dom": "^5.14.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.7",
|
"@typescript-eslint/eslint-plugin": "^5.59.7",
|
||||||
"@typescript-eslint/parser": "^5.59.7",
|
"@typescript-eslint/parser": "^5.59.7",
|
||||||
@ -71,5 +76,10 @@
|
|||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"tabWidth": 2
|
"tabWidth": 2
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"patchedDependencies": {
|
||||||
|
"sql.js@1.8.0": "patches/sql.js@1.8.0.patch"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
patches/sql.js@1.8.0.patch
Normal file
12
patches/sql.js@1.8.0.patch
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
diff --git a/dist/sql-wasm.js b/dist/sql-wasm.js
|
||||||
|
index e0da60ba096433d9af1c7025d2ffb9c521f190ed..89a5da6af23e1a644106d38dafe7cfa85500a8c4 100644
|
||||||
|
--- a/dist/sql-wasm.js
|
||||||
|
+++ b/dist/sql-wasm.js
|
||||||
|
@@ -192,3 +192,7 @@ else if (typeof define === 'function' && define['amd']) {
|
||||||
|
else if (typeof exports === 'object'){
|
||||||
|
exports["Module"] = initSqlJs;
|
||||||
|
}
|
||||||
|
+
|
||||||
|
+var module;
|
||||||
|
+export default initSqlJs;
|
||||||
|
+
|
1995
pnpm-lock.yaml
1995
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
43
src/components/FileInput.tsx
Normal file
43
src/components/FileInput.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { useDropzone } from 'react-dropzone';
|
||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export interface FileInputProps {
|
||||||
|
onReceiveFiles: (files: File[]) => void;
|
||||||
|
multiple?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FileInput({ children, onReceiveFiles }: FileInputProps) {
|
||||||
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
|
multiple: true,
|
||||||
|
onDropAccepted: onReceiveFiles,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
{...getRootProps()}
|
||||||
|
w="100%"
|
||||||
|
maxW={480}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderRadius="lg"
|
||||||
|
transitionDuration="0.5s"
|
||||||
|
p="6"
|
||||||
|
cursor="pointer"
|
||||||
|
display="flex"
|
||||||
|
flexDir="column"
|
||||||
|
alignItems="center"
|
||||||
|
_hover={{
|
||||||
|
borderColor: 'gray.400',
|
||||||
|
bg: 'gray.50',
|
||||||
|
}}
|
||||||
|
{...(isDragActive && {
|
||||||
|
bg: 'blue.50',
|
||||||
|
borderColor: 'blue.700',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@ -1,77 +1,52 @@
|
|||||||
import { useDropzone } from 'react-dropzone';
|
import { Box, Text } from '@chakra-ui/react';
|
||||||
import { chakra, Box, Text } from '@chakra-ui/react';
|
|
||||||
import { UnlockIcon } from '@chakra-ui/icons';
|
import { UnlockIcon } from '@chakra-ui/icons';
|
||||||
|
|
||||||
import { useAppDispatch } from '~/hooks';
|
import { useAppDispatch } from '~/hooks';
|
||||||
import { addNewFile, processFile } from '~/features/file-listing/fileListingSlice';
|
import { addNewFile, processFile } from '~/features/file-listing/fileListingSlice';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import { FileInput } from './FileInput';
|
||||||
|
|
||||||
export function SelectFile() {
|
export function SelectFile() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
const handleFileReceived = (files: File[]) => {
|
||||||
multiple: true,
|
console.debug(
|
||||||
onDropAccepted(files, _event) {
|
'react-dropzone/onDropAccepted(%o, %o)',
|
||||||
console.debug(
|
files.length,
|
||||||
'react-dropzone/onDropAccepted(%o, %o)',
|
files.map((x) => x.name)
|
||||||
files.length,
|
);
|
||||||
files.map((x) => x.name)
|
|
||||||
|
for (const file of files) {
|
||||||
|
const blobURI = URL.createObjectURL(file);
|
||||||
|
const fileName = file.name;
|
||||||
|
const fileId = 'file://' + nanoid();
|
||||||
|
|
||||||
|
// FIXME: this should be a single action/thunk that first adds the item, then updates it.
|
||||||
|
dispatch(
|
||||||
|
addNewFile({
|
||||||
|
id: fileId,
|
||||||
|
blobURI,
|
||||||
|
fileName,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
dispatch(processFile({ fileId }));
|
||||||
for (const file of files) {
|
}
|
||||||
const blobURI = URL.createObjectURL(file);
|
};
|
||||||
const fileName = file.name;
|
|
||||||
const fileId = 'file://' + nanoid();
|
|
||||||
|
|
||||||
// FIXME: this should be a single action/thunk that first adds the item, then updates it.
|
|
||||||
dispatch(
|
|
||||||
addNewFile({
|
|
||||||
id: fileId,
|
|
||||||
blobURI,
|
|
||||||
fileName,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
dispatch(processFile({ fileId }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<FileInput multiple onReceiveFiles={handleFileReceived}>
|
||||||
{...getRootProps()}
|
|
||||||
w="100%"
|
|
||||||
maxW={480}
|
|
||||||
borderWidth="1px"
|
|
||||||
borderRadius="lg"
|
|
||||||
transitionDuration="0.5s"
|
|
||||||
p="6"
|
|
||||||
cursor="pointer"
|
|
||||||
display="flex"
|
|
||||||
flexDir="column"
|
|
||||||
alignItems="center"
|
|
||||||
_hover={{
|
|
||||||
borderColor: 'gray.400',
|
|
||||||
bg: 'gray.50',
|
|
||||||
}}
|
|
||||||
{...(isDragActive && {
|
|
||||||
bg: 'blue.50',
|
|
||||||
borderColor: 'blue.700',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<input {...getInputProps()} />
|
|
||||||
|
|
||||||
<Box pb={3}>
|
<Box pb={3}>
|
||||||
<UnlockIcon boxSize={8} />
|
<UnlockIcon boxSize={8} />
|
||||||
</Box>
|
</Box>
|
||||||
<Text textAlign="center">
|
<Text as="div" textAlign="center">
|
||||||
拖放或
|
拖放或
|
||||||
<chakra.span as="span" color="teal.400">
|
<Text as="span" color="teal.400">
|
||||||
点我选择
|
点我选择
|
||||||
</chakra.span>
|
</Text>
|
||||||
需要解密的文件
|
需要解密的文件
|
||||||
<Text fontSize="sm" opacity="50%">
|
<Text fontSize="sm" opacity="50%">
|
||||||
在浏览器内对文件进行解锁,零上传
|
在浏览器内对文件进行解锁,零上传
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</FileInput>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -24,12 +24,14 @@ import {
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { qmc2AddKey, qmc2ClearKeys, qmc2DeleteKey, qmc2UpdateKey } from '../settingsSlice';
|
import { qmc2AddKey, qmc2ClearKeys, qmc2DeleteKey, qmc2UpdateKey } from '../settingsSlice';
|
||||||
import { selectStagingQMCv2Settings } from '../settingsSelector';
|
import { selectStagingQMCv2Settings } from '../settingsSelector';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { MdAdd, MdDelete, MdDeleteForever, MdExpandMore, MdFileUpload, MdVpnKey } from 'react-icons/md';
|
import { MdAdd, MdDelete, MdDeleteForever, MdExpandMore, MdFileUpload, MdVpnKey } from 'react-icons/md';
|
||||||
|
import { ImportFileModal } from './QMCv2/ImportFileModal';
|
||||||
|
|
||||||
export function PanelQMCv2Key() {
|
export function PanelQMCv2Key() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const qmc2Keys = useSelector(selectStagingQMCv2Settings).keys;
|
const qmc2Keys = useSelector(selectStagingQMCv2Settings).keys;
|
||||||
|
const [showImportModal, setShowImportModal] = useState(false);
|
||||||
|
|
||||||
const addKey = () => dispatch(qmc2AddKey());
|
const addKey = () => dispatch(qmc2AddKey());
|
||||||
const updateKey = (prop: 'name' | 'key', id: string, e: React.ChangeEvent<HTMLInputElement>) =>
|
const updateKey = (prop: 'name' | 'key', id: string, e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
@ -53,12 +55,10 @@ export function PanelQMCv2Key() {
|
|||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton as={IconButton} icon={<MdExpandMore />}></MenuButton>
|
<MenuButton as={IconButton} icon={<MdExpandMore />}></MenuButton>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
{/* 目前的想法是弹出一个 modal,给用户一些信息(如期待的格式、如何导出或寻找对应的文件) */}
|
<MenuItem onClick={() => setShowImportModal(true)} icon={<Icon as={MdFileUpload} boxSize={5} />}>
|
||||||
{/* 但是这样的话就不太方便放在这个分支里面做了,下次一定。 */}
|
|
||||||
<MenuItem hidden onClick={() => alert('TODO!')} icon={<Icon as={MdFileUpload} boxSize={5} />}>
|
|
||||||
从文件导入
|
从文件导入
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuDivider hidden />
|
<MenuDivider />
|
||||||
<MenuItem color="red" onClick={clearAll} icon={<Icon as={MdDeleteForever} boxSize={5} />}>
|
<MenuItem color="red" onClick={clearAll} icon={<Icon as={MdDeleteForever} boxSize={5} />}>
|
||||||
清空
|
清空
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -111,6 +111,8 @@ export function PanelQMCv2Key() {
|
|||||||
</List>
|
</List>
|
||||||
{qmc2Keys.length === 0 && <Text>还没有添加密钥。</Text>}
|
{qmc2Keys.length === 0 && <Text>还没有添加密钥。</Text>}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<ImportFileModal show={showImportModal} onClose={() => setShowImportModal(false)} />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
92
src/features/settings/panels/QMCv2/ImportFileModal.tsx
Normal file
92
src/features/settings/panels/QMCv2/ImportFileModal.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import {
|
||||||
|
Center,
|
||||||
|
Flex,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
TabPanels,
|
||||||
|
Tabs,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { FileInput } from '~/components/FileInput';
|
||||||
|
import { qmc2ImportKeys } from '../../settingsSlice';
|
||||||
|
import { useAppDispatch } from '~/hooks';
|
||||||
|
import { DatabaseKeyExtractor } from '~/util/DatabaseKeyExtractor';
|
||||||
|
|
||||||
|
import { QMCv2AndroidInstructions } from './QMCv2AndroidInstructions';
|
||||||
|
|
||||||
|
export interface ImportFileModalProps {
|
||||||
|
show: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImportFileModal({ onClose, show }: ImportFileModalProps) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const toast = useToast();
|
||||||
|
const handleFileReceived = async (files: File[]) => {
|
||||||
|
try {
|
||||||
|
const file = files[0];
|
||||||
|
const fileBuffer = await file.arrayBuffer();
|
||||||
|
|
||||||
|
if (/[_.]db$/i.test(file.name)) {
|
||||||
|
const extractor = await DatabaseKeyExtractor.getInstance();
|
||||||
|
const qmc2Keys = extractor.extractQmAndroidDbKeys(fileBuffer);
|
||||||
|
if (qmc2Keys) {
|
||||||
|
dispatch(qmc2ImportKeys(qmc2Keys));
|
||||||
|
onClose();
|
||||||
|
toast({
|
||||||
|
title: `导入成功 (${qmc2Keys.length})`,
|
||||||
|
description: '记得保存更改来应用。',
|
||||||
|
isClosable: true,
|
||||||
|
duration: 5000,
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert(`不是支持的 SQLite 数据库文件。\n表名:${qmc2Keys}`);
|
||||||
|
} else {
|
||||||
|
alert(`不支持的文件:${file.name}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('error during import: ', e);
|
||||||
|
alert(`导入数据库时发生错误:${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={show} onClose={onClose} closeOnOverlayClick={false} scrollBehavior="inside" size="xl">
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>导入密钥数据库</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<Flex as={ModalBody} gap={2} flexDir="column" flex={1}>
|
||||||
|
<Center>
|
||||||
|
<FileInput onReceiveFiles={handleFileReceived}>拖放或点我选择含有密钥的数据库文件</FileInput>
|
||||||
|
</Center>
|
||||||
|
|
||||||
|
<Flex as={Tabs} variant="enclosed" flexDir="column" mt={4} flex={1} minH={0}>
|
||||||
|
<TabList>
|
||||||
|
<Tab>安卓客户端</Tab>
|
||||||
|
{/* <Tab>Two</Tab> */}
|
||||||
|
</TabList>
|
||||||
|
<TabPanels flex={1} overflow="auto">
|
||||||
|
<TabPanel>
|
||||||
|
<QMCv2AndroidInstructions />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
<p>two!</p>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
117
src/features/settings/panels/QMCv2/QMCv2AndroidInstructions.tsx
Normal file
117
src/features/settings/panels/QMCv2/QMCv2AndroidInstructions.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionButton,
|
||||||
|
AccordionIcon,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionPanel,
|
||||||
|
Box,
|
||||||
|
Heading,
|
||||||
|
Link,
|
||||||
|
ListItem,
|
||||||
|
OrderedList,
|
||||||
|
Text,
|
||||||
|
chakra,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||||
|
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
|
import hljsStyleGitHub from 'react-syntax-highlighter/dist/esm/styles/hljs/github';
|
||||||
|
|
||||||
|
import PowerShellAdbDumpCommand from './adb_dump.ps1?raw';
|
||||||
|
import ShellAdbDumpCommand from './adb_dump.sh?raw';
|
||||||
|
|
||||||
|
export function QMCv2AndroidInstructions() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Text>
|
||||||
|
你需要 <code>root</code> 访问权限来访问安卓应用的私有数据。
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
⚠️ 请注意,获取 <code>root</code> 通常意味着你的安卓设备
|
||||||
|
<chakra.span color="red.400">将失去保修资格</chakra.span>。
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Accordion allowToggle mt="2">
|
||||||
|
<AccordionItem>
|
||||||
|
<Heading as="h3" size="md">
|
||||||
|
<AccordionButton>
|
||||||
|
<Box as="span" flex="1" textAlign="left">
|
||||||
|
在安卓手机端操作
|
||||||
|
</Box>
|
||||||
|
<AccordionIcon />
|
||||||
|
</AccordionButton>
|
||||||
|
</Heading>
|
||||||
|
<AccordionPanel pb={4}>
|
||||||
|
<OrderedList>
|
||||||
|
<ListItem>
|
||||||
|
使用具有 <code>root</code> 权限的文件浏览器,访问 <code>/data/data/com.tencent.qqmusic/databases/</code>
|
||||||
|
{' 目录,将文件 '}
|
||||||
|
<code>player_process_db</code> 复制到正常模式下用户可访问的目录(如下载目录)。
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>提交该数据库文件。</ListItem>
|
||||||
|
</OrderedList>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem>
|
||||||
|
<Heading as="h3" size="md">
|
||||||
|
<AccordionButton>
|
||||||
|
<Box as="span" flex="1" textAlign="left">
|
||||||
|
在 PC 端操作(ADB / PowerShell)
|
||||||
|
</Box>
|
||||||
|
<AccordionIcon />
|
||||||
|
</AccordionButton>
|
||||||
|
</Heading>
|
||||||
|
<AccordionPanel pb={4}>
|
||||||
|
<OrderedList>
|
||||||
|
<ListItem>
|
||||||
|
确保 <code>adb</code> 命令可用。
|
||||||
|
<br />
|
||||||
|
💡 如果没有,可以
|
||||||
|
<Link href="https://scoop.sh/#/apps?q=adb" isExternal color="blue.600">
|
||||||
|
使用 Scoop 安装 <ExternalLinkIcon />
|
||||||
|
</Link>
|
||||||
|
。
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>启动终端并进入 PowerShell 7 环境。</ListItem>
|
||||||
|
<ListItem>将安卓设备连接到电脑,并允许调试。</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
粘贴执行下述代码。若设备提示「超级用户请求」请允许:
|
||||||
|
<SyntaxHighlighter language="ps1" style={hljsStyleGitHub}>
|
||||||
|
{PowerShellAdbDumpCommand}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
提交当前目录下的 <code>player_process_db</code> 文件。
|
||||||
|
</ListItem>
|
||||||
|
</OrderedList>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem>
|
||||||
|
<Heading as="h3" size="md">
|
||||||
|
<AccordionButton>
|
||||||
|
<Box as="span" flex="1" textAlign="left">
|
||||||
|
在 Linux / Mac 系统下操作(ADB / Shell)
|
||||||
|
</Box>
|
||||||
|
<AccordionIcon />
|
||||||
|
</AccordionButton>
|
||||||
|
</Heading>
|
||||||
|
<AccordionPanel pb={4}>
|
||||||
|
<OrderedList>
|
||||||
|
<ListItem>将安卓设备连接到电脑,并允许调试。</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
粘贴执行下述代码。若设备提示「超级用户请求」请允许:
|
||||||
|
<SyntaxHighlighter language="bash" style={hljsStyleGitHub}>
|
||||||
|
{ShellAdbDumpCommand}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
提交当前目录下的 <code>player_process_db</code> 文件。
|
||||||
|
</ListItem>
|
||||||
|
</OrderedList>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
11
src/features/settings/panels/QMCv2/adb_dump.ps1
Normal file
11
src/features/settings/panels/QMCv2/adb_dump.ps1
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
try {
|
||||||
|
$gz_b64 = adb shell su -c "cat '/data/data/com.tencent.qqmusic/databases/player_process_db' | gzip | base64" | Out-String
|
||||||
|
$bStream = New-Object System.IO.MemoryStream(,[System.Convert]::FromBase64String($gz_b64))
|
||||||
|
$decoded = New-Object System.IO.Compression.GzipStream($bStream, [System.IO.Compression.CompressionMode]::Decompress)
|
||||||
|
$outFile = New-Object System.IO.FileStream("player_process_db", [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write)
|
||||||
|
$decoded.CopyTo($outFile)
|
||||||
|
} finally {
|
||||||
|
if ($outFile -ne $null) { $outFile.Dispose() }
|
||||||
|
if ($decoded -ne $null) { $decoded.Dispose() }
|
||||||
|
if ($bStream -ne $null) { $bStream.Dispose() }
|
||||||
|
}
|
2
src/features/settings/panels/QMCv2/adb_dump.sh
Normal file
2
src/features/settings/panels/QMCv2/adb_dump.sh
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
sh adb shell su -c "cat '/data/data/com.tencent.qqmusic/databases/player_process_db' | gzip | base64" \
|
||||||
|
| base64 -d | gzip -d player_process_db
|
@ -6,9 +6,10 @@ export const selectStagingQMCv2Settings = (state: RootState) => state.settings.s
|
|||||||
export const selectFinalQMCv2Settings = (state: RootState) => state.settings.production.qmc2;
|
export const selectFinalQMCv2Settings = (state: RootState) => state.settings.production.qmc2;
|
||||||
|
|
||||||
export const selectDecryptOptionByFile = (state: RootState, name: string): DecryptCommandOptions => {
|
export const selectDecryptOptionByFile = (state: RootState, name: string): DecryptCommandOptions => {
|
||||||
|
const normalizedName = name.normalize();
|
||||||
const qmc2Keys = selectFinalQMCv2Settings(state).keys;
|
const qmc2Keys = selectFinalQMCv2Settings(state).keys;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
qmc2Key: hasOwn(qmc2Keys, name) ? qmc2Keys[name] : undefined,
|
qmc2Key: hasOwn(qmc2Keys, normalizedName) ? qmc2Keys[normalizedName] : undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -36,8 +36,8 @@ const stagingToProduction = (staging: StagingSettings): ProductionSettings => ({
|
|||||||
qmc2: {
|
qmc2: {
|
||||||
keys: objectify(
|
keys: objectify(
|
||||||
staging.qmc2.keys,
|
staging.qmc2.keys,
|
||||||
(item) => item.name,
|
(item) => item.name.normalize(),
|
||||||
(item) => item.key
|
(item) => item.key.trim()
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -61,6 +61,10 @@ export const settingsSlice = createSlice({
|
|||||||
qmc2AddKey(state) {
|
qmc2AddKey(state) {
|
||||||
state.staging.qmc2.keys.push({ id: nanoid(), name: '', key: '' });
|
state.staging.qmc2.keys.push({ id: nanoid(), name: '', key: '' });
|
||||||
},
|
},
|
||||||
|
qmc2ImportKeys(state, { payload }: PayloadAction<{ name: string; key: string }[]>) {
|
||||||
|
const newItems = payload.map((item) => ({ id: nanoid(), ...item }));
|
||||||
|
state.staging.qmc2.keys.push(...newItems);
|
||||||
|
},
|
||||||
qmc2DeleteKey(state, { payload: { id } }: PayloadAction<{ id: string }>) {
|
qmc2DeleteKey(state, { payload: { id } }: PayloadAction<{ id: string }>) {
|
||||||
const qmc2 = state.staging.qmc2;
|
const qmc2 = state.staging.qmc2;
|
||||||
qmc2.keys = qmc2.keys.filter((item) => item.id !== id);
|
qmc2.keys = qmc2.keys.filter((item) => item.id !== id);
|
||||||
@ -102,6 +106,7 @@ export const {
|
|||||||
qmc2UpdateKey,
|
qmc2UpdateKey,
|
||||||
qmc2DeleteKey,
|
qmc2DeleteKey,
|
||||||
qmc2ClearKeys,
|
qmc2ClearKeys,
|
||||||
|
qmc2ImportKeys,
|
||||||
|
|
||||||
commitStagingChange,
|
commitStagingChange,
|
||||||
discardStagingChanges,
|
discardStagingChanges,
|
||||||
|
@ -3,6 +3,13 @@ import ReactDOM from 'react-dom/client';
|
|||||||
|
|
||||||
import { AppRoot } from './components/AppRoot';
|
import { AppRoot } from './components/AppRoot';
|
||||||
|
|
||||||
|
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
|
import hljsSyntaxPowerShell from 'react-syntax-highlighter/dist/esm/languages/hljs/powershell';
|
||||||
|
import hljsSyntaxBash from 'react-syntax-highlighter/dist/esm/languages/hljs/bash';
|
||||||
|
|
||||||
|
SyntaxHighlighter.registerLanguage('ps1', hljsSyntaxPowerShell);
|
||||||
|
SyntaxHighlighter.registerLanguage('bash', hljsSyntaxBash);
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<AppRoot />
|
<AppRoot />
|
||||||
|
44
src/util/DatabaseKeyExtractor.ts
Normal file
44
src/util/DatabaseKeyExtractor.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { SQLDatabase, SQLStatic, loadSQL } from './sqlite';
|
||||||
|
|
||||||
|
export interface QMAndroidKeyEntry {
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DatabaseKeyExtractor {
|
||||||
|
private static _instance: DatabaseKeyExtractor;
|
||||||
|
|
||||||
|
static async getInstance() {
|
||||||
|
if (!DatabaseKeyExtractor._instance) {
|
||||||
|
DatabaseKeyExtractor._instance = new DatabaseKeyExtractor(await loadSQL());
|
||||||
|
}
|
||||||
|
return DatabaseKeyExtractor._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private SQL: SQLStatic) {}
|
||||||
|
|
||||||
|
private hasTable(db: SQLDatabase, name: string): boolean {
|
||||||
|
const tables = db.exec('SELECT name FROM sqlite_master WHERE type="table"')[0].values.map((x) => x[0]);
|
||||||
|
return tables.includes(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
extractQmAndroidDbKeys(buffer: ArrayBuffer): null | QMAndroidKeyEntry[] {
|
||||||
|
let db: SQLDatabase | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
db = new this.SQL.Database(new Uint8Array(buffer));
|
||||||
|
if (!this.hasTable(db, 'audio_file_ekey_table')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = db.exec('select file_path, ekey from `audio_file_ekey_table`')[0].values;
|
||||||
|
return keys.map(([path, key]) => ({
|
||||||
|
// strip dir name
|
||||||
|
name: String(path).replace(/.+\//, ''),
|
||||||
|
key: String(key),
|
||||||
|
}));
|
||||||
|
} finally {
|
||||||
|
db?.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
src/util/sqlite.ts
Normal file
15
src/util/sqlite.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import * as initSqlite from 'sql.js';
|
||||||
|
|
||||||
|
const urlWasm = new URL('@nm/sql.js/dist/sql-wasm.wasm', import.meta.url).toString();
|
||||||
|
|
||||||
|
export type SQLStatic = Awaited<ReturnType<(typeof initSqlite)['default']>>;
|
||||||
|
export type SQLDatabase = SQLStatic['Database']['prototype'];
|
||||||
|
|
||||||
|
let sqlLoaderPromise: Promise<SQLStatic> | void;
|
||||||
|
|
||||||
|
export async function loadSQL() {
|
||||||
|
if (!sqlLoaderPromise) {
|
||||||
|
sqlLoaderPromise = initSqlite.default({ locateFile: () => urlWasm });
|
||||||
|
}
|
||||||
|
return await sqlLoaderPromise;
|
||||||
|
}
|
@ -1,15 +1,8 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"lib": [
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
"DOM",
|
"types": ["vitest/globals", "@testing-library/jest-dom"],
|
||||||
"DOM.Iterable",
|
|
||||||
"ESNext"
|
|
||||||
],
|
|
||||||
"types": [
|
|
||||||
"vitest/globals",
|
|
||||||
"@testing-library/jest-dom"
|
|
||||||
],
|
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
@ -27,17 +20,14 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": [
|
"~/*": ["./src/*"],
|
||||||
"./src/*"
|
"@nm/*": ["./node_modules/*"]
|
||||||
]
|
}
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src"],
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "./tsconfig.node.json"
|
"path": "./tsconfig.node.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
base: './',
|
base: './',
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
exclude: ['@jixun/libparakeet'],
|
exclude: ['@jixun/libparakeet', 'sql.js'],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
replace({
|
replace({
|
||||||
@ -88,6 +88,7 @@ export default defineConfig({
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'~': path.resolve(__dirname, 'src'),
|
'~': path.resolve(__dirname, 'src'),
|
||||||
|
'@nm': path.resolve(__dirname, 'node_modules'),
|
||||||
module: path.resolve(__dirname, 'src', 'dummy.mjs'),
|
module: path.resolve(__dirname, 'src', 'dummy.mjs'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -95,8 +96,10 @@ export default defineConfig({
|
|||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks: {
|
manualChunks: {
|
||||||
reacts: ['react', 'react-dom', 'react-promise-suspense', 'react-redux', '@reduxjs/toolkit'],
|
reacts: ['react', 'react-dom', 'react-dropzone', 'react-promise-suspense', 'react-redux', '@reduxjs/toolkit'],
|
||||||
chakra: ['@chakra-ui/icons', '@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'],
|
||||||
|
utility: ['radash', 'nanoid', 'immer', 'react-syntax-highlighter'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -12,6 +12,10 @@ export default defineConfig({
|
|||||||
replacement: 'src/$1',
|
replacement: 'src/$1',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
// workaround: sql.js is not ESModule friendly, yet...
|
||||||
|
deps: {
|
||||||
|
inline: ['sql.js'],
|
||||||
|
},
|
||||||
api: {
|
api: {
|
||||||
port: 5174, // vite port + 1
|
port: 5174, // vite port + 1
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user