Compare commits
No commits in common. "4ff58091883973b130719cd0bc20a302b8d57912" and "57c0a247c0d21b9b59c984d38fdab377bd5f44df" have entirely different histories.
4ff5809188
...
57c0a247c0
@ -7,6 +7,7 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Code,
|
Code,
|
||||||
Heading,
|
Heading,
|
||||||
|
Link,
|
||||||
ListItem,
|
ListItem,
|
||||||
OrderedList,
|
OrderedList,
|
||||||
Text,
|
Text,
|
||||||
@ -18,7 +19,6 @@ import hljsStyleGitHub from 'react-syntax-highlighter/dist/esm/styles/hljs/githu
|
|||||||
|
|
||||||
import PowerShellAdbDumpCommandTemplate from './adb_dump.ps1?raw';
|
import PowerShellAdbDumpCommandTemplate from './adb_dump.ps1?raw';
|
||||||
import ShellAdbDumpCommandTemplate from './adb_dump.sh?raw';
|
import ShellAdbDumpCommandTemplate from './adb_dump.sh?raw';
|
||||||
import { ExtLink } from '../ExtLink';
|
|
||||||
|
|
||||||
const applyTemplate = (tpl: string, values: Record<string, unknown>) => {
|
const applyTemplate = (tpl: string, values: Record<string, unknown>) => {
|
||||||
return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key) => (Object.hasOwn(values, key) ? String(values[key]) : '<nil>'));
|
return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key) => (Object.hasOwn(values, key) ? String(values[key]) : '<nil>'));
|
||||||
@ -36,19 +36,10 @@ export function AndroidADBPullInstruction({ dir, file }: AndroidADBPullInstructi
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Text>
|
<Text>
|
||||||
你需要
|
你需要 <code>root</code> 访问权限来访问安卓应用的私有数据。
|
||||||
<ruby>
|
|
||||||
超级管理员
|
|
||||||
<rp> (</rp>
|
|
||||||
<rt>
|
|
||||||
<code>root</code>
|
|
||||||
</rt>
|
|
||||||
<rp>)</rp>
|
|
||||||
</ruby>
|
|
||||||
访问权限来访问安卓应用的私有数据。
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
⚠️ 请注意,获取管理员权限通常意味着你的安卓设备
|
⚠️ 请注意,获取 <code>root</code> 通常意味着你的安卓设备
|
||||||
<chakra.span color="red.400">将失去保修资格</chakra.span>。
|
<chakra.span color="red.400">将失去保修资格</chakra.span>。
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@ -101,13 +92,13 @@ export function AndroidADBPullInstruction({ dir, file }: AndroidADBPullInstructi
|
|||||||
<OrderedList>
|
<OrderedList>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<Text>
|
<Text>
|
||||||
确保 <Code>adb</Code> 命令可用。
|
确保 <code>adb</code> 命令可用。
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
💡 如果没有,可以
|
💡 如果没有,可以
|
||||||
<ExtLink href="https://scoop.sh/#/apps?q=adb">
|
<Link href="https://scoop.sh/#/apps?q=adb" isExternal>
|
||||||
使用 Scoop 安装 <ExternalLinkIcon />
|
使用 Scoop 安装 <ExternalLinkIcon />
|
||||||
</ExtLink>
|
</Link>
|
||||||
。
|
。
|
||||||
</Text>
|
</Text>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@ -143,11 +134,6 @@ export function AndroidADBPullInstruction({ dir, file }: AndroidADBPullInstructi
|
|||||||
</Heading>
|
</Heading>
|
||||||
<AccordionPanel pb={4}>
|
<AccordionPanel pb={4}>
|
||||||
<OrderedList>
|
<OrderedList>
|
||||||
<ListItem>
|
|
||||||
<Text>
|
|
||||||
确保 <Code>adb</Code> 命令可用。
|
|
||||||
</Text>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<Text>将安卓设备连接到电脑,并允许调试。</Text>
|
<Text>将安卓设备连接到电脑,并允许调试。</Text>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import type { AnchorHTMLAttributes } from 'react';
|
|
||||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
|
||||||
import { Link } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
export function ExtLink({ children, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) {
|
|
||||||
return (
|
|
||||||
<Link isExternal {...props} rel="noreferrer noopener nofollow">
|
|
||||||
{children}
|
|
||||||
<ExternalLinkIcon />
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
@ -35,9 +35,7 @@ export function ImportSecretModal({ clientName, children, show, onClose, onImpor
|
|||||||
<FileInput onReceiveFiles={handleFileReceived}>拖放或点我选择含有密钥的数据库文件</FileInput>
|
<FileInput onReceiveFiles={handleFileReceived}>拖放或点我选择含有密钥的数据库文件</FileInput>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
||||||
<Text as="div" mt={2}>
|
<Text mt={2}>选择你的{clientName && <>「{clientName}」</>}客户端平台以查看对应说明:</Text>
|
||||||
选择你的{clientName && <>「{clientName}」</>}客户端平台以查看对应说明:
|
|
||||||
</Text>
|
|
||||||
<Flex as={Tabs} variant="enclosed" flexDir="column" flex={1} minH={0}>
|
<Flex as={Tabs} variant="enclosed" flexDir="column" flex={1} minH={0}>
|
||||||
{children}
|
{children}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Text } from '@chakra-ui/react';
|
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||||
import { ExtLink } from '~/components/ExtLink';
|
import { Link, Text } from '@chakra-ui/react';
|
||||||
import { Header4 } from '~/components/HelpText/Header4';
|
import { Header4 } from '~/components/HelpText/Header4';
|
||||||
import { ProjectIssue } from '~/components/ProjectIssue';
|
import { ProjectIssue } from '~/components/ProjectIssue';
|
||||||
|
|
||||||
@ -17,7 +17,10 @@ export function OtherFAQ() {
|
|||||||
<Header4>有更多问题?</Header4>
|
<Header4>有更多问题?</Header4>
|
||||||
<Text>
|
<Text>
|
||||||
{'欢迎进入 '}
|
{'欢迎进入 '}
|
||||||
<ExtLink href={'https://t.me/unlock_music_chat'}>Telegram “音乐解锁-交流” 交流群</ExtLink>
|
<Link href={'https://t.me/unlock_music_chat'} isExternal>
|
||||||
|
Telegram “音乐解锁-交流” 交流群
|
||||||
|
<ExternalLinkIcon />
|
||||||
|
</Link>
|
||||||
{' 一起探讨。'}
|
{' 一起探讨。'}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { Alert, AlertIcon, Container, Flex, List, ListItem, Text, UnorderedList, chakra } from '@chakra-ui/react';
|
import { Alert, AlertIcon, Container, Flex, List, ListItem, Text, chakra } from '@chakra-ui/react';
|
||||||
import { Header4 } from '~/components/HelpText/Header4';
|
import { Header4 } from '~/components/HelpText/Header4';
|
||||||
import { SegmentTryOfficialPlayer } from './SegmentTryOfficialPlayer';
|
import { SegmentTryOfficialPlayer } from './SegmentTryOfficialPlayer';
|
||||||
import { QMCv2QQMusicAllInstructions } from '~/features/settings/panels/QMCv2/QMCv2QQMusicAllInstructions';
|
import { QMCv2AllInstructions } from '~/features/settings/panels/QMCv2/QMCv2AllInstructions';
|
||||||
import { SegmentKeyImportInstructions } from './SegmentKeyImportInstructions';
|
import { SegmentKeyImportInstructions } from './SegmentKeyImportInstructions';
|
||||||
import { ExtLink } from '~/components/ExtLink';
|
|
||||||
|
|
||||||
export function QQMusicFAQ() {
|
export function QQMusicFAQ() {
|
||||||
return (
|
return (
|
||||||
@ -17,26 +16,7 @@ export function QQMusicFAQ() {
|
|||||||
<Text>
|
<Text>
|
||||||
<chakra.strong>2、检查您的平台</chakra.strong>
|
<chakra.strong>2、检查您的平台</chakra.strong>
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>日前,仅Windows客户端下载的歌曲无需密钥,其余平台的官方正式版本均需要提取密钥。</Text>
|
||||||
日前,仅 Windows 客户端 19.43 或更低版本下载的歌曲文件无需密钥,其余平台的官方正式版本均需要提取密钥。
|
|
||||||
你可以通过下方的链接获取 QQ 音乐 Windows 客户端 v19.43 的安装程序:
|
|
||||||
</Text>
|
|
||||||
<UnorderedList pl={3}>
|
|
||||||
<ListItem>
|
|
||||||
<Text>
|
|
||||||
<ExtLink href="https://dldir1v6.qq.com/music/clntupate/QQMusic_Setup_1943.exe">
|
|
||||||
<code>qq.com</code> 官方下载地址(推荐)
|
|
||||||
</ExtLink>
|
|
||||||
</Text>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>
|
|
||||||
<Text>
|
|
||||||
<ExtLink href="https://web.archive.org/web/2023/https://dldir1v6.qq.com/music/clntupate/QQMusic_Setup_1943.exe">
|
|
||||||
<code>Archive.org</code> 存档
|
|
||||||
</ExtLink>
|
|
||||||
</Text>
|
|
||||||
</ListItem>
|
|
||||||
</UnorderedList>
|
|
||||||
|
|
||||||
<Container p={2}>
|
<Container p={2}>
|
||||||
<Alert status="warning" borderRadius={5}>
|
<Alert status="warning" borderRadius={5}>
|
||||||
@ -55,7 +35,7 @@ export function QQMusicFAQ() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<SegmentKeyImportInstructions tab="QMCv2 密钥" clientInstructions={<QMCv2QQMusicAllInstructions />} />
|
<SegmentKeyImportInstructions tab="QMCv2 密钥" clientInstructions={<QMCv2AllInstructions />} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
</>
|
</>
|
||||||
|
@ -92,7 +92,7 @@ export function FileRow({ id, file }: FileRowProps) {
|
|||||||
</WrapItem>
|
</WrapItem>
|
||||||
<WrapItem>
|
<WrapItem>
|
||||||
{file.decrypted && (
|
{file.decrypted && (
|
||||||
<Link href={file.decrypted} download={decryptedName}>
|
<Link isExternal href={file.decrypted} download={decryptedName}>
|
||||||
<Button as="span">下载</Button>
|
<Button as="span">下载</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
@ -14,7 +14,6 @@ import {
|
|||||||
MenuDivider,
|
MenuDivider,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuList,
|
MenuList,
|
||||||
Select,
|
|
||||||
Text,
|
Text,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
useToast,
|
useToast,
|
||||||
@ -31,15 +30,13 @@ import { StagingQMCv2Key } from '../keyFormats';
|
|||||||
import { DatabaseKeyExtractor } from '~/util/DatabaseKeyExtractor';
|
import { DatabaseKeyExtractor } from '~/util/DatabaseKeyExtractor';
|
||||||
import { MMKVParser } from '~/util/MMKVParser';
|
import { MMKVParser } from '~/util/MMKVParser';
|
||||||
import { getFileName } from '~/util/pathHelper';
|
import { getFileName } from '~/util/pathHelper';
|
||||||
import { QMCv2QQMusicAllInstructions } from './QMCv2/QMCv2QQMusicAllInstructions';
|
import { QMCv2AllInstructions } from './QMCv2/QMCv2AllInstructions';
|
||||||
import { QMCv2DoubanAllInstructions } from './QMCv2/QMCv2DoubanAllInstructions';
|
|
||||||
|
|
||||||
export function PanelQMCv2Key() {
|
export function PanelQMCv2Key() {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { keys: qmc2Keys, allowFuzzyNameSearch } = useSelector(selectStagingQMCv2Settings);
|
const { keys: qmc2Keys, allowFuzzyNameSearch } = useSelector(selectStagingQMCv2Settings);
|
||||||
const [showImportModal, setShowImportModal] = useState(false);
|
const [showImportModal, setShowImportModal] = useState(false);
|
||||||
const [secretType, setSecretType] = useState<'qm' | 'douban'>('qm');
|
|
||||||
|
|
||||||
const addKey = () => dispatch(qmc2AddKey());
|
const addKey = () => dispatch(qmc2AddKey());
|
||||||
const clearAll = () => dispatch(qmc2ClearKeys());
|
const clearAll = () => dispatch(qmc2ClearKeys());
|
||||||
@ -54,11 +51,11 @@ export function PanelQMCv2Key() {
|
|||||||
|
|
||||||
let qmc2Keys: null | Omit<StagingQMCv2Key, 'id'>[] = null;
|
let qmc2Keys: null | Omit<StagingQMCv2Key, 'id'>[] = null;
|
||||||
|
|
||||||
if (/(player_process[_.]db|music_audio_play)(\.db)?$/i.test(file.name)) {
|
if (/[_.]db$/i.test(file.name)) {
|
||||||
const extractor = await DatabaseKeyExtractor.getInstance();
|
const extractor = await DatabaseKeyExtractor.getInstance();
|
||||||
qmc2Keys = extractor.extractQmcV2KeysFromSqliteDb(fileBuffer);
|
qmc2Keys = extractor.extractQmAndroidDbKeys(fileBuffer);
|
||||||
if (!qmc2Keys) {
|
if (!qmc2Keys) {
|
||||||
alert(`不是支持的 SQLite 数据库文件。`);
|
alert(`不是支持的 SQLite 数据库文件。\n表名:${qmc2Keys}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (/MMKVStreamEncryptId|filenameEkeyMap/i.test(file.name)) {
|
} else if (/MMKVStreamEncryptId|filenameEkeyMap/i.test(file.name)) {
|
||||||
@ -99,8 +96,8 @@ export function PanelQMCv2Key() {
|
|||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Text>
|
<Text>
|
||||||
QQ 音乐、豆瓣 FM 目前采用的加密方案(QMCv2)。在使用「QQ 音乐」安卓、Mac 或 iOS 客户端,以及在使用「豆瓣
|
QQ 音乐目前采用的加密方案(QMCv2)。在使用「QQ 音乐」安卓、Mac 或 iOS
|
||||||
FM」安卓客户端的情况下,其「离线加密文件」对应的「密钥」储存在独立的数据库文件内。
|
客户端的情况下,其「离线加密文件」对应的「密钥」储存在独立的数据库文件内。
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<HStack pb={2} pt={2}>
|
<HStack pb={2} pt={2}>
|
||||||
@ -158,28 +155,16 @@ export function PanelQMCv2Key() {
|
|||||||
<QMCv2EKeyItem key={id} id={id} ekey={ekey} name={name} i={i} />
|
<QMCv2EKeyItem key={id} id={id} ekey={ekey} name={name} i={i} />
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
{qmc2Keys.length === 0 && <Text>还没有密钥。</Text>}
|
{qmc2Keys.length === 0 && <Text>还没有添加密钥。</Text>}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<ImportSecretModal
|
<ImportSecretModal
|
||||||
clientName={
|
clientName="QQ 音乐"
|
||||||
<Select
|
|
||||||
value={secretType}
|
|
||||||
onChange={(e) => setSecretType(e.target.value as 'qm' | 'douban')}
|
|
||||||
variant="flushed"
|
|
||||||
display="inline"
|
|
||||||
css={{ paddingLeft: '0.75rem', width: 'auto' }}
|
|
||||||
>
|
|
||||||
<option value="qm">QQ 音乐</option>
|
|
||||||
<option value="douban">豆瓣 FM</option>
|
|
||||||
</Select>
|
|
||||||
}
|
|
||||||
show={showImportModal}
|
show={showImportModal}
|
||||||
onClose={() => setShowImportModal(false)}
|
onClose={() => setShowImportModal(false)}
|
||||||
onImport={handleSecretImport}
|
onImport={handleSecretImport}
|
||||||
>
|
>
|
||||||
{secretType === 'qm' && <QMCv2QQMusicAllInstructions />}
|
<QMCv2AllInstructions />
|
||||||
{secretType === 'douban' && <QMCv2DoubanAllInstructions />}
|
|
||||||
</ImportSecretModal>
|
</ImportSecretModal>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,7 @@ import { InstructionsIOS } from './InstructionsIOS';
|
|||||||
import { InstructionsMac } from './InstructionsMac';
|
import { InstructionsMac } from './InstructionsMac';
|
||||||
import { InstructionsPC } from './InstructionsPC';
|
import { InstructionsPC } from './InstructionsPC';
|
||||||
|
|
||||||
export function QMCv2QQMusicAllInstructions() {
|
export function QMCv2AllInstructions() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TabList>
|
<TabList>
|
@ -1,17 +0,0 @@
|
|||||||
import { Tab, TabList, TabPanel, TabPanels } from '@chakra-ui/react';
|
|
||||||
import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction';
|
|
||||||
|
|
||||||
export function QMCv2DoubanAllInstructions() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<TabList>
|
|
||||||
<Tab>安卓</Tab>
|
|
||||||
</TabList>
|
|
||||||
<TabPanels flex={1} overflow="auto">
|
|
||||||
<TabPanel>
|
|
||||||
<AndroidADBPullInstruction dir="/data/data/com.douban.radio/databases" file="music_audio_play" />
|
|
||||||
</TabPanel>
|
|
||||||
</TabPanels>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -23,21 +23,16 @@ export class DatabaseKeyExtractor {
|
|||||||
return tables.includes(name);
|
return tables.includes(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
extractQmcV2KeysFromSqliteDb(buffer: ArrayBuffer): null | QMAndroidKeyEntry[] {
|
extractQmAndroidDbKeys(buffer: ArrayBuffer): null | QMAndroidKeyEntry[] {
|
||||||
let db: SQLDatabase | null = null;
|
let db: SQLDatabase | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db = new this.SQL.Database(new Uint8Array(buffer));
|
db = new this.SQL.Database(new Uint8Array(buffer));
|
||||||
|
if (!this.hasTable(db, 'audio_file_ekey_table')) {
|
||||||
let sql: undefined | string;
|
return null;
|
||||||
if (this.hasTable(db, 'audio_file_ekey_table')) {
|
|
||||||
sql = 'select file_path, ekey from audio_file_ekey_table';
|
|
||||||
} else if (this.hasTable(db, 'EKeyFileInfo')) {
|
|
||||||
sql = 'select filePath, eKey from EKeyFileInfo';
|
|
||||||
}
|
}
|
||||||
if (!sql) return null;
|
|
||||||
|
|
||||||
const result = db.exec(sql);
|
const result = db.exec('select file_path, ekey from audio_file_ekey_table');
|
||||||
if (result.length === 0) {
|
if (result.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user