Merge pull request 'feat: add mac import option and help text' (#30) from feat/import-qmc2-mmkv into main

Reviewed-on: um/um-react#30
This commit is contained in:
鲁树人 2023-06-12 23:56:34 +00:00
commit f787e1c3f8
10 changed files with 273 additions and 46 deletions

View File

@ -0,0 +1,15 @@
import { Icon, Kbd } from '@chakra-ui/react';
import { BsCommand } from 'react-icons/bs';
export function MacCommandKey() {
return (
<ruby>
<Kbd>
<Icon as={BsCommand} />
</Kbd>
<rp> (</rp>
<rt>command</rt>
<rp>)</rp>
</ruby>
);
}

View File

@ -0,0 +1,15 @@
import { Icon, Kbd } from '@chakra-ui/react';
import { BsShift } from 'react-icons/bs';
export function ShiftKey() {
return (
<ruby>
<Kbd>
<Icon as={BsShift} />
</Kbd>
<rp> (</rp>
<rt>shift</rt>
<rp>)</rp>
</ruby>
);
}

View File

@ -20,13 +20,21 @@ import { qmc2ImportKeys } from '../../settingsSlice';
import { useAppDispatch } from '~/hooks';
import { DatabaseKeyExtractor } from '~/util/DatabaseKeyExtractor';
import { QMCv2AndroidInstructions } from './QMCv2AndroidInstructions';
import { InstructionsAndroid } from './InstructionsAndroid';
import { MMKVParser } from '~/util/MMKVParser';
import { getFileName } from '~/util/pathHelper';
import { InstructionsMac } from './InstructionsMac';
export interface ImportFileModalProps {
show: boolean;
onClose: () => void;
}
interface KeyEntry {
name: string;
key: string;
}
export function ImportFileModal({ onClose, show }: ImportFileModalProps) {
const dispatch = useAppDispatch();
const toast = useToast();
@ -35,22 +43,31 @@ export function ImportFileModal({ onClose, show }: ImportFileModalProps) {
const file = files[0];
const fileBuffer = await file.arrayBuffer();
let qmc2Keys: null | KeyEntry[] = null;
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',
});
qmc2Keys = extractor.extractQmAndroidDbKeys(fileBuffer);
if (!qmc2Keys) {
alert(`不是支持的 SQLite 数据库文件。\n表名${qmc2Keys}`);
return;
}
alert(`不是支持的 SQLite 数据库文件。\n表名${qmc2Keys}`);
} else if (/MMKVStreamEncryptId/i.test(file.name)) {
const fileBuffer = await file.arrayBuffer();
const map = MMKVParser.toStringMap(new DataView(fileBuffer));
qmc2Keys = Array.from(map.entries(), ([name, key]) => ({ name: getFileName(name), key }));
}
if (qmc2Keys) {
dispatch(qmc2ImportKeys(qmc2Keys));
onClose();
toast({
title: `导入成功 (${qmc2Keys.length})`,
description: '记得保存更改来应用。',
isClosable: true,
duration: 5000,
status: 'success',
});
} else {
alert(`不支持的文件:${file.name}`);
}
@ -74,14 +91,14 @@ export function ImportFileModal({ onClose, show }: ImportFileModalProps) {
<Flex as={Tabs} variant="enclosed" flexDir="column" mt={4} flex={1} minH={0}>
<TabList>
<Tab></Tab>
{/* <Tab>Two</Tab> */}
<Tab>Mac </Tab>
</TabList>
<TabPanels flex={1} overflow="auto">
<TabPanel>
<QMCv2AndroidInstructions />
<InstructionsAndroid />
</TabPanel>
<TabPanel>
<p>two!</p>
<InstructionsMac />
</TabPanel>
</TabPanels>
</Flex>

View File

@ -5,6 +5,7 @@ import {
AccordionItem,
AccordionPanel,
Box,
Code,
Heading,
Link,
ListItem,
@ -19,7 +20,7 @@ import hljsStyleGitHub from 'react-syntax-highlighter/dist/esm/styles/hljs/githu
import PowerShellAdbDumpCommand from './adb_dump.ps1?raw';
import ShellAdbDumpCommand from './adb_dump.sh?raw';
export function QMCv2AndroidInstructions() {
export function InstructionsAndroid() {
return (
<>
<Text>
@ -43,11 +44,25 @@ export function QMCv2AndroidInstructions() {
<AccordionPanel pb={4}>
<OrderedList>
<ListItem>
使 <code>root</code> 访 <code>/data/data/com.tencent.qqmusic/databases/</code>
{' 目录,将文件 '}
<code>player_process_db</code> 访
<Text>
<Code>root</Code>
</Text>
</ListItem>
<ListItem>
<Text>
访 <Code>/data/data/com.tencent.qqmusic/databases/</Code>
</Text>
</ListItem>
<ListItem>
<Text>
<Code>player_process_db</Code> 访
<br />
</Text>
</ListItem>
<ListItem>
<Text></Text>
</ListItem>
<ListItem></ListItem>
</OrderedList>
</AccordionPanel>
</AccordionItem>
@ -64,24 +79,33 @@ export function QMCv2AndroidInstructions() {
<AccordionPanel pb={4}>
<OrderedList>
<ListItem>
<code>adb</code>
<br />
💡
<Link href="https://scoop.sh/#/apps?q=adb" isExternal>
使 Scoop <ExternalLinkIcon />
</Link>
<Text>
<code>adb</code>
</Text>
<Text>
💡
<Link href="https://scoop.sh/#/apps?q=adb" isExternal>
使 Scoop <ExternalLinkIcon />
</Link>
</Text>
</ListItem>
<ListItem> PowerShell 7 </ListItem>
<ListItem></ListItem>
<ListItem>
<Text> PowerShell 7 </Text>
</ListItem>
<ListItem>
<Text></Text>
</ListItem>
<ListItem>
<Text></Text>
<SyntaxHighlighter language="ps1" style={hljsStyleGitHub}>
{PowerShellAdbDumpCommand}
</SyntaxHighlighter>
</ListItem>
<ListItem>
<code>player_process_db</code>
<Text>
<Code>player_process_db</Code>
</Text>
</ListItem>
</OrderedList>
</AccordionPanel>
@ -98,15 +122,19 @@ export function QMCv2AndroidInstructions() {
</Heading>
<AccordionPanel pb={4}>
<OrderedList>
<ListItem></ListItem>
<ListItem>
<Text></Text>
</ListItem>
<ListItem>
<Text></Text>
<SyntaxHighlighter language="bash" style={hljsStyleGitHub}>
{ShellAdbDumpCommand}
</SyntaxHighlighter>
</ListItem>
<ListItem>
<code>player_process_db</code>
<Text>
<Code>player_process_db</Code>
</Text>
</ListItem>
</OrderedList>
</AccordionPanel>

View File

@ -0,0 +1,50 @@
import { Heading, Text, Code, Kbd, OrderedList, ListItem } from '@chakra-ui/react';
import { MacCommandKey } from '~/components/Key/MacCommandKey';
import { ShiftKey } from '~/components/Key/ShiftKey';
export function InstructionsMac() {
return (
<>
<Text>Mac 使 mmkv </Text>
<Text></Text>
<Text as="pre" whiteSpace="pre-wrap" wordBreak="break-word" lang="en">
<Code>
~/Library/Containers/com.tencent.QQMusicMac/Data/Library/Application
Support/QQMusicMac/mmkv/MMKVStreamEncryptId
</Code>
</Text>
<Heading as="h3" size="md" mt="4">
</Heading>
<OrderedList>
<ListItem>
<Text>
<Code>MMKVStreamEncryptId</Code>
</Text>
</ListItem>
<ListItem>
<Text></Text>
</ListItem>
<ListItem>
<Text>
<ShiftKey />
{' + '}
<MacCommandKey />
{' + '}
<Kbd>{'G'}</Kbd>
</Text>
</ListItem>
<ListItem>
<Text>
<Code>MMKVStreamEncryptId</Code>
</Text>
</ListItem>
<ListItem>
<Text></Text>
</ListItem>
</OrderedList>
</>
);
}

View File

@ -20,16 +20,6 @@ export const theme = extendTheme({
},
},
Tabs: tabsTheme,
Heading: {
baseStyle: {
userSelect: 'none',
},
},
Text: {
baseStyle: {
userSelect: 'none',
},
},
Link: {
baseStyle: {
color: 'blue.600',

View File

@ -1,3 +1,4 @@
import { getFileName } from './pathHelper';
import { SQLDatabase, SQLStatic, loadSQL } from './sqlite';
export interface QMAndroidKeyEntry {
@ -34,7 +35,7 @@ export class DatabaseKeyExtractor {
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(/.+\//, ''),
name: getFileName(String(path)),
key: String(key),
}));
} finally {

95
src/util/MMKVParser.ts Normal file
View File

@ -0,0 +1,95 @@
const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true });
export class MMKVParser {
private offset = 8;
private length: number;
constructor(private view: DataView) {
const payloadLength = view.getUint32(0, true);
this.length = 4 + payloadLength;
}
toString() {
const offset = this.offset.toString(16).padStart(8, '0');
const length = this.length.toString(16).padStart(8, '0');
return `<MMKVParser offset=0x${offset} len=0x${length}>`;
}
get eof() {
return this.offset >= this.length;
}
peek() {
return this.view.getUint8(this.offset);
}
public readByte() {
return this.view.getUint8(this.offset++);
}
public readBigInt() {
let value = 0n;
let shift = 0n;
let b: number;
do {
b = this.readByte();
value |= BigInt(b & 0x7f) << shift;
shift += 7n;
} while ((b & 0x80) !== 0);
return value;
}
public readInt() {
const value = this.readBigInt();
if (value > Number.MAX_SAFE_INTEGER) {
throw new Error('Runtime Error: BigInt too large to cast as number');
}
return Number(value);
}
public readBytes(n: number) {
const offset = this.offset;
const end = offset + n;
this.offset = end;
return new Uint8Array(this.view.buffer.slice(offset, end));
}
public readString() {
// String [
// len: int,
// data: byte[int], # utf-8
// ]
const strByteLen = this.readInt();
const data = this.readBytes(strByteLen);
return textDecoder.decode(data).normalize();
}
public readVariantString() {
// Container [
// len: int,
// data: variant
// ]
const containerLen = this.readInt();
const newOffset = this.offset + containerLen;
const result = this.readString();
if (newOffset !== this.offset) {
throw new Error('readVariantString failed: offset does not match');
}
return result;
}
public static toStringMap(view: DataView): Map<string, string> {
const mmkv = new MMKVParser(view);
const result = new Map<string, string>();
while (!mmkv.eof) {
const key = mmkv.readString();
const value = mmkv.readVariantString();
result.set(key, value);
}
return result;
}
}

View File

@ -0,0 +1,13 @@
import { getFileName } from '../pathHelper';
test('handle linux path', () => {
expect(getFileName('/path/to/file.bin')).toEqual('file.bin');
});
test('handle win32 path', () => {
expect(getFileName('C:\\system32\\file.bin')).toEqual('file.bin');
});
test('handle file name only as well', () => {
expect(getFileName('file.bin')).toEqual('file.bin');
});

3
src/util/pathHelper.ts Normal file
View File

@ -0,0 +1,3 @@
export function getFileName(path: string) {
return path.replace(/.*[\\/]/, '');
}