refactor: batch 4

This commit is contained in:
鲁树人
2025-05-18 09:58:34 +09:00
parent 2e4e57be45
commit 9518b813bd
15 changed files with 12109 additions and 1485 deletions
+11870
View File
File diff suppressed because it is too large Load Diff
+4 -11
View File
@@ -6,7 +6,7 @@
"scripts": {
"start": "vite",
"build": "tsc -p tsconfig.prod.json && vite build && pnpm build:finalize",
"build:finalize": "node scripts/write-version.mjs && node scripts/minify-mjs.mjs",
"build:finalize": "node scripts/write-version.mjs",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"format": "prettier -w .",
"test": "vitest run",
@@ -17,17 +17,10 @@
"prepare": "simple-git-hooks"
},
"dependencies": {
"@chakra-ui/anatomy": "^2.3.4",
"@chakra-ui/icons": "^2.2.4",
"@chakra-ui/react": "^2.10.8",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@reduxjs/toolkit": "^2.8.2",
"@unlock-music/crypto": "0.1.10",
"classnames": "^2.5.1",
"framer-motion": "^12.12.1",
"nanoid": "^5.1.5",
"next-themes": "^0.4.6",
"radash": "^12.1.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
@@ -37,9 +30,7 @@
"react-router": "^7.6.0",
"react-syntax-highlighter": "^15.6.1",
"react-toastify": "^11.0.5",
"sass": "^1.89.0",
"sql.js": "^1.13.0",
"workbox-build": "^7.3.0"
"sql.js": "^1.13.0"
},
"devDependencies": {
"@eslint/js": "^9.26.0",
@@ -69,6 +60,7 @@
"lint-staged": "^16.0.0",
"prettier": "^3.5.3",
"rollup": "^4.40.2",
"sass": "^1.89.0",
"simple-git-hooks": "^2.13.0",
"tailwindcss": "^4.1.7",
"terser": "^5.39.2",
@@ -79,6 +71,7 @@
"vite-plugin-top-level-await": "^1.5.0",
"vite-plugin-wasm": "^3.4.1",
"vitest": "^3.1.3",
"workbox-build": "^7.3.0",
"workbox-window": "^7.3.0"
},
"lint-staged": {
+61 -1129
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,9 +1,9 @@
import { BsCommand } from 'react-icons/bs';
import { Ruby } from '../Ruby';
export function MacCommandKey() {
export function MacCommandKey({ className }: { className?: string }) {
return (
<Ruby caption="command">
<Ruby caption="command" className={className}>
<kbd className="kbd">
<BsCommand className="text-sm" />
</kbd>
+2 -2
View File
@@ -1,9 +1,9 @@
import { BsShift } from 'react-icons/bs';
import { Ruby } from '../Ruby';
export function ShiftKey() {
export function ShiftKey({ className }: { className?: string }) {
return (
<Ruby caption="shift">
<Ruby caption="shift" className={className}>
<kbd className="kbd">
<BsShift className="text-sm" />
</kbd>
@@ -1,27 +0,0 @@
import { Grid, chakra } from '@chakra-ui/react';
export const FileRowResponsiveGrid = chakra(Grid, {
baseStyle: {
gridTemplateAreas: {
base: `
"cover"
"title"
"meta"
"action"
`,
md: `
"cover title action"
"cover meta action"
`,
},
gridTemplateRows: {
base: 'repeat(auto-fill)',
md: 'min-content 1fr',
},
gridTemplateColumns: {
base: '1fr',
md: '160px 1fr',
},
gap: 3,
},
});
@@ -26,9 +26,7 @@ export function InstructionsPC() {
来加密储存密钥。
</p>
<p>该密钥数据库通常位于下述路径:</p>
<p className="flex items-center gap-1">
<FilePathBlock>{DB_PATH}</FilePathBlock>
</p>
<FilePathBlock>{DB_PATH}</FilePathBlock>
<h3 className="font-bold text-xl mt-4">导入密钥</h3>
<ol className="list-decimal pl-6">
@@ -56,10 +56,7 @@ export function PanelQingTing() {
return (
<div className="min-h-0 flex-col grow px-1">
<h2 className="text-2xl font-bold mb-4">
<VQuote>蜻蜓 FM</VQuote>
设备密钥
</h2>
<h2 className="text-2xl font-bold mb-4">蜻蜓 FM</h2>
<p>
<VQuote>蜻蜓 FM</VQuote>的安卓版本需要获取设备密钥,并以此来生成解密密钥。
@@ -1,51 +1,36 @@
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Heading,
Text,
} from '@chakra-ui/react';
import { InstructionsIOSCondition } from './InstructionsIOSCondition';
import { useId } from 'react';
export function InstructionsIOS() {
const iosInstructionId = useId();
return (
<>
<Box>
<Text>iOS 设备获取应用私有文件比较麻烦,你需要越狱或使用一台 PC 或 Mac 来对 iOS 设备进行完整备份。</Text>
<Text>因此,建议换用 PC 或 Mac 重新下载音乐文件然后再尝试解密。</Text>
</Box>
<Accordion allowToggle mt="2">
<AccordionItem>
<Heading as="h3" size="md">
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
我的 iOS 设备已经越狱
</Box>
<AccordionIcon />
</AccordionButton>
</Heading>
<AccordionPanel pb={4}>
<InstructionsIOSCondition jailbreak={true} />
</AccordionPanel>
</AccordionItem>
<div>
<p>iOS 设备获取应用私有文件比较麻烦,你需要越狱或使用一台 PC 或 Mac 来对 iOS 设备进行完整备份。</p>
<p>因此,建议换用 PC 或 Mac 重新下载音乐文件然后再尝试解密。</p>
</div>
<AccordionItem>
<Heading as="h3" size="md">
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
我的 iOS 设备没有越狱
</Box>
<AccordionIcon />
</AccordionButton>
</Heading>
<AccordionPanel pb={4}>
<div className="join join-vertical bg-base-100 mt-2 max-w-full">
<div className="collapse collapse-arrow join-item border-base-300 border">
<input type="radio" name={iosInstructionId} />
<div className="collapse-title font-semibold">
我的 iOS 设备<strong>已经越狱</strong>{' '}
</div>
<div className="collapse-content text-sm min-w-0">
<InstructionsIOSCondition jailbreak={true} />
</div>
</div>
<div className="collapse collapse-arrow join-item border-base-300 border">
<input type="radio" name={iosInstructionId} />
<div className="collapse-title font-semibold">
我的 iOS 设备<strong>没有越狱</strong>
</div>
<div className="collapse-content text-sm min-w-0">
<InstructionsIOSCondition jailbreak={false} />
</AccordionPanel>
</AccordionItem>
</Accordion>
</div>
</div>
</div>
</>
);
}
@@ -1,6 +1,6 @@
import { Box, Code, Heading, Image, ListItem, OrderedList, Text } from '@chakra-ui/react';
import iosAllowBackup from './iosAllowBackup.webp';
import { FilePathBlock } from '~/components/FilePathBlock';
import { HiWord } from '~/components/HelpText/HiWord';
const EXAMPLE_MEDIA_ID = '0011wjLv1bIkvv';
const EXAMPLE_NAME_IOS = '333407709-0011wjLv1bIkvv-1.mgalaxy';
@@ -10,92 +10,77 @@ export function InstructionsIOSCondition({ jailbreak }: { jailbreak: boolean })
const useJailbreak = jailbreak;
const useBackup = !jailbreak;
const pathPrefix = jailbreak ? '/var/mobile/Containers/Data/Application/<随机>/' : '/AppDomain-';
const pathPrefix = jailbreak ? (
<>
/var/mobile/Containers/Data/Application/<HiWord className="text-nowrap">[随机字符]</HiWord>/
</>
) : (
'/AppDomain-'
);
return (
<>
<Heading as="h3" size="md">
获取密钥数据库文件
</Heading>
<OrderedList>
<h4 className="text-lg font-semibold">获取密钥数据库文件</h4>
<ol className="list-decimal pl-4">
{useBackup && (
<ListItem>
<Text>首先需要在 iOS 客户端的设定允许备份:</Text>
<Image src={iosAllowBackup}></Image>
</ListItem>
<li>
首先需要在 iOS 客户端的设定允许备份:
<br />
<img src={iosAllowBackup}></img>
</li>
)}
{useBackup && (
<ListItem>
<Text>使用你喜欢的备份软件对 iOS 设备进行完整备份;</Text>
</ListItem>
)}
<ListItem>
{useBackup && <Text>打开备份文件,并导航到下述目录:</Text>}
{useJailbreak && <Text>访问下述目录:</Text>}
{useBackup && <li>使用你喜欢的备份软件对 iOS 设备进行完整备份</li>}
<li>
{useBackup && <span>打开备份文件,并导航到下述目录:</span>}
{useJailbreak && <span>访问下述目录:</span>}
<FilePathBlock>{pathPrefix}com.tencent.QQMusic/Documents/mmkv/</FilePathBlock>
</ListItem>
<ListItem>
<Text>
提取或导出密钥数据库文件 <Code>filenameEkeyMap</Code>;
</Text>
</ListItem>
<ListItem>
<Text>
提交导出的 <Code>filenameEkeyMap</Code> 文件;
</Text>
</ListItem>
<ListItem>
<Text>按下「保存」来应用更改。</Text>
</ListItem>
</OrderedList>
</li>
<li>
提取或导出密钥数据库文件 <code>filenameEkeyMap</code>
</li>
<li>
提交导出的 <code>filenameEkeyMap</code> 文件
</li>
<li>按下「保存」来应用更改。</li>
</ol>
<Heading as="h3" size="md" mt="3">
获取离线文件
</Heading>
<Box>
<Text>访问下述目录:</Text>
<h3 className="text-lg font-semibold mt-3">获取离线文件</h3>
<section>
<p>访问下述目录:</p>
<FilePathBlock>
{pathPrefix}com.tencent.QQMusic/Library/Application Support/com.tencent.QQMusic/iData/iMusic
</FilePathBlock>
<Text>
该目录又存在数个子目录,其子目录下保存的「<Code>[字符].m[字符]</Code>」文件则是最终的加密文件。
</Text>
<Text>
格式:<Code>[song_id]-[mid]-[随机数字].m[后缀]</Code>
</Text>
<Text>
&#x3000;例:<Code>{EXAMPLE_NAME_IOS}</Code>
</Text>
</Box>
<p>
该目录又存在数个子目录,其子目录下保存的「<code>[字符].m[字符]</code>」文件则是最终的加密文件。
</p>
<p>
格式:<code>[song_id]-[mid]-[随机数字].m[后缀]</code>
</p>
<p>
&#x3000;例:<code>{EXAMPLE_NAME_IOS}</code>
</p>
</section>
<Heading as="h3" size="md" mt="3">
解密离线文件
</Heading>
<Text>勾选设定界面的「使用近似文件名匹配」可跳过该节内容。</Text>
<Text>⚠ 注意:若密钥过多,匹配过程可能会造成浏览器卡顿或无响应。</Text>
<OrderedList>
<ListItem>
<Text>
提取文件的 <Code>[mid]</Code> 部分,如 <Code>{EXAMPLE_MEDIA_ID}</Code>;
</Text>
</ListItem>
<ListItem>
<Text>
查找密钥表,得到文件名「<Code>{EXAMPLE_NAME_DB}</Code>」;
</Text>
</ListItem>
<ListItem>
<Text>
将文件更名为对应的文件名,如<Code display="inline">{EXAMPLE_NAME_IOS}</Code> ➔
<Code display="inline">{EXAMPLE_NAME_DB}</Code>;
</Text>
</ListItem>
<ListItem>
<Text>
回到主界面,提交文件「<Code>{EXAMPLE_NAME_DB}</Code>」。
</Text>
</ListItem>
</OrderedList>
<h4 className="text-lg font-semibold mt-3">解密离线文件</h4>
<p>勾选设定界面的「使用近似文件名匹配」可跳过该节内容。</p>
<p>⚠ 注意:若密钥过多,匹配过程可能会造成浏览器卡顿或无响应。</p>
<ol className="list-decimal pl-4 mt-1">
<li>
提取文件的 <code>[mid]</code> 部分,如 <code>{EXAMPLE_MEDIA_ID}</code>
</li>
<li>
查找密钥表,得到文件名「<code>{EXAMPLE_NAME_DB}</code>」
</li>
<li>
将文件更名为对应的文件名,如
<br />
<code>{EXAMPLE_NAME_IOS}</code>
<br />➔ <code>{EXAMPLE_NAME_DB}</code>
</li>
<li>
回到主界面,提交文件「<code>{EXAMPLE_NAME_DB}</code>」。
</li>
</ol>
</>
);
}
@@ -1,59 +1,79 @@
import { Heading, Text, Code, OrderedList, ListItem, Link } 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';
import { VQuote } from '~/components/HelpText/VQuote';
import { MacCommandKey } from '~/components/Key/MacCommandKey';
import { ShiftKey } from '~/components/Key/ShiftKey';
const MAC_CLIENT_URL =
'https://web.archive.org/web/20230903/https://dldir1.qq.com/music/clntupate/mac/QQMusicMac_Mgr.dmg';
const MAC_CLIENT_TG_URL = 'https://t.me/um_lsr_ch/21';
const DB_PATH =
'~/Library/Containers/com.tencent.QQMusicMac/Data/Library/Application Support/QQMusicMac/mmkv/MMKVStreamEncryptId';
export function InstructionsMac() {
const copyDbPathToClipboard = () => {
navigator.clipboard
.writeText(DB_PATH)
.then(() => {
toast.success('已复制到剪贴板');
})
.catch((err) => {
toast.error(`复制失败,请手动复制\n${err}`);
});
};
return (
<>
<Text>Mac 客户端使用 mmkv 数据库储存密钥。</Text>
<Text>
{'此外,你需要降级到 '}
<Link isExternal href={MAC_CLIENT_URL}>
2023.09.03 版本的客户端
</Link>
{'。'}
新版本对 mmkv 数据库进行了加密处理。
</Text>
<Text>该密钥文件通常存储在下述路径:</Text>
<FilePathBlock>
~/Library/Containers/com.tencent.QQMusicMac/Data/Library/Application Support/QQMusicMac/mmkv/MMKVStreamEncryptId
</FilePathBlock>
<p>Mac 客户端使用 mmkv 数据库储存密钥。</p>
<p>此外,你需要降级到 v8.8.0 版本的客户端 —— 更新的版本对密钥数据库进行了加密,目前无公开的获取方案。</p>
<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 className="kbd">{'G'}</kbd>」组合键打开「路径输入框」
</Text>
</ListItem>
<ListItem>
<Text>
粘贴之前复制的 <Code>MMKVStreamEncryptId</Code> 文件路径
</Text>
</ListItem>
<ListItem>
<Text>按下「回车键」确认。</Text>
</ListItem>
</OrderedList>
<p className="mt-4">获取 QQ 音乐 Mac 客户端 8.8.0:</p>
<ul className="list-disc pl-6">
<li>
<ExtLink className="link-info" href={MAC_CLIENT_URL}>
通过 <code>Archive.org</code> 缓存下载(慢)
</ExtLink>
</li>
<li>
<ExtLink className="link-info" href={MAC_CLIENT_TG_URL}>
通过 Telegram 下载(需要账号)
</ExtLink>
</li>
</ul>
<p className="mt-4">密钥文件通常存储在下述路径:</p>
<FilePathBlock>{DB_PATH}</FilePathBlock>
<h4 className="font-bold text-lg mt-4">导入密钥</h4>
<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>MMKVStreamEncryptId</code> 文件路径
</li>
<li>
点击上方的<VQuote>文件选择区域</VQuote>,打开<VQuote>文件选择框</VQuote>
</li>
<li>
按下
<VQuote>
<ShiftKey className="mx-1" />
{'+'}
<MacCommandKey className="mx-1" />
{'+'}
<kbd className="kbd mx-1">G</kbd>
</VQuote>
组合键打开<VQuote>路径输入框</VQuote>
</li>
<li>
粘贴之前复制的 <code>MMKVStreamEncryptId</code> 文件路径
</li>
<li>按下「回车键」确认。</li>
</ol>
</>
);
}
@@ -3,6 +3,9 @@ import { HiWord } from '~/components/HelpText/HiWord';
import NoopExecutable from './assets/noop.exe?base64';
import NoopExecutableSource from './assets/noop.asm.txt?base64';
const PC_CLIENT_URL = 'https://web.archive.org/web/2023/https://dldir1v6.qq.com/music/clntupate/QQMusic_Setup_1951.exe';
const PC_CLIENT_TG_URL = 'https://t.me/um_lsr_ch/24';
export function InstructionsPC() {
return (
<>
@@ -23,33 +26,35 @@ export function InstructionsPC() {
</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">
<ExtLink className="link-info" href={PC_CLIENT_URL}>
通过 <code>Archive.org</code> 缓存下载(慢)
</ExtLink>
</li>
<li>
<ExtLink href="https://t.me/um_lsr_ch/24">通过 Telegram 下载(需要账号)</ExtLink>
<ExtLink className="link-info" href={PC_CLIENT_TG_URL}>
通过 Telegram 下载(需要账号)
</ExtLink>
</li>
</ul>
<p className="mt-4">
安装完成后可以覆盖 QQ 音乐安装目录下的
<a
className="link px-1"
className="link link-info mx-1"
download="QQMusicUp.exe"
href={`data:application/vnd.microsoft.portable-executable;base64,${NoopExecutable}`}
>
<code>QQMusicUp.exe</code>
</a>
同名文件(
同名文件,屏蔽自动更新(
<a
className="link px-1"
className="link"
download="QQMusicUp.asm"
href={`data:text/x-asm;charset=utf-8;base64,${NoopExecutableSource}`}
>
源码
</a>
),屏蔽自动更新。
)。
</p>
<p className="mt-2">降级后需要删除新版本下载的文件并重新使用旧版本下载。</p>
</>
-61
View File
@@ -1,61 +0,0 @@
import { extendTheme } from '@chakra-ui/react';
import { tabsTheme } from './themes/Tabs';
export const theme = extendTheme({
fonts: {
body: [
'-system-ui,-apple-system,BlinkMacSystemFont',
'Source Han Sans CN,Noto Sans CJK SC',
'Segoe UI,Helvetica,Arial,sans-serif',
'Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol',
].join(','),
mono: [
'SFMono-Regular,Menlo,Monaco',
'"Sarasa Mono CJK SC"',
'Consolas,"Liberation Mono","Courier New",monospace',
'"Microsoft YaHei UI"',
].join(','),
},
components: {
Button: {
baseStyle: {
fontWeight: 'normal',
},
defaultProps: {
colorScheme: 'teal',
},
},
Tabs: tabsTheme,
Link: {
baseStyle: {
color: 'blue.600',
},
},
Text: {
baseStyle: {
mt: 1,
},
},
Header: {
baseStyle: {
mt: 3,
},
},
},
styles: {
global: {
'#root': {
minHeight: '100vh',
maxHeight: '100vh',
display: 'flex',
flexDirection: 'column',
},
},
},
sizes: {
footer: {
container: '5rem',
content: '4rem',
},
},
});
-74
View File
@@ -1,74 +0,0 @@
import { tabsAnatomy } from '@chakra-ui/anatomy';
import { createMultiStyleConfigHelpers, cssVar } from '@chakra-ui/react';
const $fg = cssVar('tabs-color');
const $bg = cssVar('tabs-bg');
const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers(tabsAnatomy.keys);
const variantLineInvert = definePartsStyle((props) => {
const { colorScheme: c, orientation } = props;
const isVertical = orientation === 'vertical';
const borderProp = isVertical ? 'borderEnd' : 'borderTop';
const marginProp = isVertical ? 'marginEnd' : 'marginTop';
return {
tablist: {
[borderProp]: '2px solid',
borderColor: 'inherit',
},
tabpanels: {
flex: 1,
minH: 0,
},
tabpanel: {
padding: 0,
},
tab: {
[borderProp]: '2px solid',
borderColor: 'transparent',
[marginProp]: '-2px',
justifyContent: 'flex-end',
_selected: {
[$fg.variable]: `colors.${c}.600`,
_dark: {
[$fg.variable]: `colors.${c}.300`,
},
borderColor: 'currentColor',
},
_active: {
[$bg.variable]: 'colors.gray.200',
_dark: {
[$bg.variable]: 'colors.whiteAlpha.300',
},
},
_disabled: {
_active: { bg: 'none' },
},
color: $fg.reference,
bg: $bg.reference,
},
root: {
display: 'flex',
flexDir: isVertical ? 'row' : 'column',
gap: 8,
minH: 0,
},
};
});
export const tabsTheme = defineMultiStyleConfig({
baseStyle: {
tablist: {
userSelect: 'none',
},
tabpanel: {
minHeight: 0,
overflow: 'auto',
maxHeight: '100%',
},
},
variants: {
'line-i': variantLineInvert,
},
});
+5 -4
View File
@@ -96,13 +96,14 @@ export default defineConfig({
},
},
build: {
minify: true,
rollupOptions: {
output: {
manualChunks: {
reacts: ['react', 'react-dom', 'react-dropzone', 'react-redux', '@reduxjs/toolkit'],
chakra: ['@chakra-ui/react', '@emotion/react', '@emotion/styled', 'framer-motion'],
icons: ['react-icons', '@chakra-ui/icons'],
utility: ['radash', 'nanoid', 'react-syntax-highlighter'],
core: ['react', 'react-dom'],
router: ['react-router'],
store: ['react-redux', '@reduxjs/toolkit'],
extras: ['react-dropzone', 'react-toastify'],
},
},
},