diff --git a/package.json b/package.json index 2c73f60..3d06882 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@types/react-dom": "^19.1.9", "@types/react-syntax-highlighter": "^15.5.13", "@types/sql.js": "^1.4.9", + "@types/tar-stream": "^3.1.4", "@types/wicg-file-system-access": "^2023.10.6", "@typescript-eslint/eslint-plugin": "^8.42.0", "@typescript-eslint/parser": "^8.42.0", @@ -64,6 +65,7 @@ "sass": "^1.92.1", "simple-git-hooks": "^2.13.1", "tailwindcss": "^4.1.13", + "tar-stream": "^3.1.7", "terser": "^5.44.0", "typescript": "^5.9.2", "typescript-eslint": "^8.42.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c13c0fa..455b691 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: '@types/sql.js': specifier: ^1.4.9 version: 1.4.9 + '@types/tar-stream': + specifier: ^3.1.4 + version: 3.1.4 '@types/wicg-file-system-access': specifier: ^2023.10.6 version: 2023.10.6 @@ -152,6 +155,9 @@ importers: tailwindcss: specifier: ^4.1.13 version: 4.1.13 + tar-stream: + specifier: ^3.1.7 + version: 3.1.7 terser: specifier: ^5.44.0 version: 5.44.0 @@ -1533,6 +1539,9 @@ packages: '@types/sql.js@1.4.9': resolution: {integrity: sha512-ep8b36RKHlgWPqjNG9ToUrPiwkhwh0AEzy883mO5Xnd+cL6VBH1EvSjBAAuxLUFF2Vn/moE3Me6v9E1Lo+48GQ==} + '@types/tar-stream@3.1.4': + resolution: {integrity: sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -1744,6 +1753,14 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + b4a@1.7.3: + resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + babel-plugin-polyfill-corejs2@0.4.14: resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} peerDependencies: @@ -1762,6 +1779,14 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bare-events@2.8.0: + resolution: {integrity: sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -2122,6 +2147,9 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} @@ -2129,6 +2157,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -2792,15 +2823,10 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@3.0.2: - resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} - mkdirp@3.0.1: - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} - engines: {node: '>=10'} - hasBin: true - mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -3275,6 +3301,9 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + streamx@2.23.0: + resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -3352,8 +3381,11 @@ packages: resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} engines: {node: '>=6'} - tar@7.4.3: - resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + + tar@7.5.1: + resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} engines: {node: '>=18'} temp-dir@2.0.0: @@ -3373,6 +3405,9 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -4995,7 +5030,7 @@ snapshots: '@tailwindcss/oxide@4.1.13': dependencies: detect-libc: 2.0.4 - tar: 7.4.3 + tar: 7.5.1 optionalDependencies: '@tailwindcss/oxide-android-arm64': 4.1.13 '@tailwindcss/oxide-darwin-arm64': 4.1.13 @@ -5115,6 +5150,10 @@ snapshots: '@types/emscripten': 1.41.1 '@types/node': 24.3.1 + '@types/tar-stream@3.1.4': + dependencies: + '@types/node': 24.3.1 + '@types/trusted-types@2.0.7': {} '@types/unist@2.0.11': {} @@ -5383,6 +5422,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + b4a@1.7.3: {} + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.4): dependencies: '@babel/compat-data': 7.28.4 @@ -5409,6 +5450,8 @@ snapshots: balanced-match@1.0.2: {} + bare-events@2.8.0: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -5832,10 +5875,18 @@ snapshots: eventemitter3@5.0.1: {} + events-universal@1.0.1: + dependencies: + bare-events: 2.8.0 + transitivePeerDependencies: + - bare-abort-controller + expect-type@1.2.2: {} fast-deep-equal@3.1.3: {} + fast-fifo@1.3.2: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6493,12 +6544,10 @@ snapshots: minipass@7.1.2: {} - minizlib@3.0.2: + minizlib@3.1.0: dependencies: minipass: 7.1.2 - mkdirp@3.0.1: {} - mrmime@2.0.1: {} ms@2.1.3: {} @@ -6982,6 +7031,15 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + streamx@2.23.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.3 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + string-argv@0.3.2: {} string-width@4.2.3: @@ -7079,13 +7137,21 @@ snapshots: tapable@2.2.3: {} - tar@7.4.3: + tar-stream@3.1.7: + dependencies: + b4a: 1.7.3 + fast-fifo: 1.3.2 + streamx: 2.23.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + tar@7.5.1: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 minipass: 7.1.2 - minizlib: 3.0.2 - mkdirp: 3.0.1 + minizlib: 3.1.0 yallist: 5.0.0 temp-dir@2.0.0: {} @@ -7110,6 +7176,12 @@ snapshots: glob: 10.4.5 minimatch: 9.0.5 + text-decoder@1.2.3: + dependencies: + b4a: 1.7.3 + transitivePeerDependencies: + - react-native-b4a + tinybench@2.9.0: {} tinyexec@0.3.2: {} diff --git a/src/components/DownloadBase64.tsx b/src/components/DownloadBase64.tsx new file mode 100644 index 0000000..e7a13c3 --- /dev/null +++ b/src/components/DownloadBase64.tsx @@ -0,0 +1,32 @@ +import type { ReactNode } from 'react'; +import { ExtLink } from './ExtLink'; +import { IoMdArchive } from 'react-icons/io'; + +export type DownloadBase64Props = { + data: string; + filename: string; + mimetype?: string; + className?: string; + icon?: boolean | ReactNode; + children?: ReactNode; +}; + +export function DownloadBase64({ + className, + children, + data, + filename, + icon, + mimetype = 'application/octet-stream', +}: DownloadBase64Props) { + return ( + } + className={className ?? 'link-info mx-1'} + download={filename} + href={`data:${mimetype};base64,${data}`} + > + {children ?? {filename}} + + ); +} diff --git a/src/components/ExtLink.tsx b/src/components/ExtLink.tsx index d3daa3c..4d6a6de 100644 --- a/src/components/ExtLink.tsx +++ b/src/components/ExtLink.tsx @@ -1,15 +1,15 @@ -import type { AnchorHTMLAttributes } from 'react'; +import type { AnchorHTMLAttributes, ReactNode } from 'react'; import { FiExternalLink } from 'react-icons/fi'; export type ExtLinkProps = AnchorHTMLAttributes & { - icon?: boolean; + icon?: boolean | ReactNode; }; export function ExtLink({ className, icon = true, children, ...props }: ExtLinkProps) { return ( {children} - {icon && } + {icon === true ? : icon} ); } diff --git a/src/components/ImportSecretModal.tsx b/src/components/ImportSecretModal.tsx index 1af16a3..dbc275b 100644 --- a/src/components/ImportSecretModal.tsx +++ b/src/components/ImportSecretModal.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef } from 'react'; import { FileInput } from '~/components/FileInput'; +import { InSecretImportModalContext } from '~/context/InSecretImportModal'; export interface ImportSecretModalProps { clientName?: React.ReactNode; @@ -31,7 +32,7 @@ export function ImportSecretModal({ clientName, children, show, onClose, onImpor }, [show]); return ( - +
onClose()}> @@ -41,7 +42,9 @@ export function ImportSecretModal({ clientName, children, show, onClose, onImpor 拖放或点我选择含有密钥的数据库文件
选择你的{clientName && <>「{clientName}」}客户端平台以查看对应说明:
-
{children}
+ +
{children}
+
diff --git a/src/context/InSecretImportModal.tsx b/src/context/InSecretImportModal.tsx new file mode 100644 index 0000000..86eaed2 --- /dev/null +++ b/src/context/InSecretImportModal.tsx @@ -0,0 +1,3 @@ +import { createContext } from 'react'; + +export const InSecretImportModalContext = createContext(false); diff --git a/src/features/settings/panels/QMCv2/InstructionsMac.tsx b/src/features/settings/panels/QMCv2/InstructionsMac.tsx index 56ad266..26f5986 100644 --- a/src/features/settings/panels/QMCv2/InstructionsMac.tsx +++ b/src/features/settings/panels/QMCv2/InstructionsMac.tsx @@ -1,94 +1,31 @@ -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'; - -import BlockUpdateScript from './assets/QQ 音乐 Mac 屏蔽升级.tar.gz?base64'; - -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'; +import { useId } from 'react'; +import { InstructionsMacV8 } from './InstructionsMacV8'; +import { InstructionsMacV10 } from './InstructionsMacV10'; export function InstructionsMac() { - const copyDbPathToClipboard = () => { - navigator.clipboard - .writeText(DB_PATH) - .then(() => { - toast.success('已复制到剪贴板'); - }) - .catch((err) => { - toast.error(`复制失败,请手动复制\n${err}`); - }); - }; + const macInstructionId = useId(); return ( <>

Mac 客户端使用 mmkv 数据库储存密钥。

-

此外,你需要降级到 v8.8.0 版本的客户端 —— 更新的版本对密钥数据库进行了加密,目前无公开的获取方案。

+

建议使用 v8.8.0 或 v10.7 版本的客户端,其中 v8.8.0 版本需要屏蔽更新。

-

获取 QQ 音乐 Mac 客户端 8.8.0:

-
    -
  • - - 通过 Archive.org 缓存下载(慢) - -
  • -
  • - - 通过 Telegram 下载(需要账号) - -
  • -
- -

- 有部分用户发现现在会强制更新。你可以下载 - - QQ 音乐 Mac 屏蔽升级.tar.gz - - ,然后执行 QQ 音乐 Mac 屏蔽升级.command。 其原理是修改 QQ - 音乐的版本号,让其认为自己是最新版本,从而屏蔽更新。 -

- -

密钥文件通常存储在下述路径:

- {DB_PATH} - -

导入密钥

-
    -
  1. - - MMKVStreamEncryptId 文件路径 -
  2. -
  3. - 点击上方的文件选择区域,打开文件选择框 -
  4. -
  5. - 按下 - - - {'+'} - - {'+'} - G - - 组合键打开路径输入框 -
  6. -
  7. - 粘贴之前复制的 MMKVStreamEncryptId 文件路径 -
  8. -
  9. 按下「回车键」确认。
  10. -
+
+
+ +
使用 QQ 音乐 Mac v8.8.0
+
+ +
+
+
+ +
使用 QQ 音乐 Mac v10.7.1
+
+ +
+
+
); } diff --git a/src/features/settings/panels/QMCv2/InstructionsMacV10.tsx b/src/features/settings/panels/QMCv2/InstructionsMacV10.tsx new file mode 100644 index 0000000..30ab96b --- /dev/null +++ b/src/features/settings/panels/QMCv2/InstructionsMacV10.tsx @@ -0,0 +1,59 @@ +import { ExtLink } from '~/components/ExtLink'; + +import { + commandName as DUMP_COMMAND_NAME, + tarName as DUMP_COMMAND_TARBALL_NAME, + tarball as DUMP_COMMAND_BASE64, +} from './assets/qqmusic_v10.7_dump.command?&name=QQ 音乐 Mac v10 密钥提取.command&mac-command'; +import { DownloadBase64 } from '~/components/DownloadBase64'; +import { VQuote } from '~/components/HelpText/VQuote'; +import { InSecretImportModalContext } from '~/context/InSecretImportModal'; +import { useContext } from 'react'; + +const MAC_CLIENT_URL = + 'https://c.y.qq.com/cgi-bin/file_redirect.fcg?bid=dldir&file=ecosfile%2Fmusic_clntupate%2Fmac%2Fother%2FQQMusicMac10.7.1Build00.dmg&sign=1-0cb9ee4c40e7447e2113cfdee2dc11c88487b0e31fe37cfe1c59e12c20956dce-689e9373'; +const MAC_CLIENT_TG_URL = 'https://t.me/um_lsr_ch/30'; + +export function InstructionsMacV10() { + const inSecretImportModal = useContext(InSecretImportModalContext); + return ( + <> +

获取 QQ 音乐 Mac 客户端 10.7.1:

+
    +
  • + + 通过 QQ 音乐官网下载(高速,但可能失效) + +
  • +
  • + + 通过 Telegram 下载(缓存,需要账号) + +
  • +
+ +

导入密钥

+
    +
  1. + 下载 + ,打开得到 {DUMP_COMMAND_NAME}。 +
  2. +
  3. + 双击运行 {DUMP_COMMAND_NAME},如果提示访问目录,请允许其访问。 +
  4. +
  5. + 运行后会在当前目录生成 qqmusic-mac-*.mmkv 文件,其中 * 是一串随机字符。 +
  6. + {inSecretImportModal ? ( +
  7. + 上传刚生成的 qqmusic-mac-*.mmkv 文件到上方的文件选择区域。 +
  8. + ) : ( +
  9. + 前往设定页面,提交生成的 qqmusic-mac-*.mmkv 文件。 +
  10. + )} +
+ + ); +} diff --git a/src/features/settings/panels/QMCv2/InstructionsMacV8.tsx b/src/features/settings/panels/QMCv2/InstructionsMacV8.tsx new file mode 100644 index 0000000..4bce9ec --- /dev/null +++ b/src/features/settings/panels/QMCv2/InstructionsMacV8.tsx @@ -0,0 +1,81 @@ +import { RiFileCopyLine } from 'react-icons/ri'; +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'; +import { copyToClipboard } from '~/util/clipboard'; + +import BlockUpdateScript from './assets/QQ 音乐 Mac 屏蔽升级.tar.gz?base64'; +import { DownloadBase64 } from '~/components/DownloadBase64'; +import { useContext } from 'react'; +import { InSecretImportModalContext } from '~/context/InSecretImportModal'; + +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 InstructionsMacV8() { + const inSecretImportModal = useContext(InSecretImportModalContext); + + return ( + <> +

获取 QQ 音乐 Mac 客户端 8.8.0:

+
    +
  • + + 通过 Archive.org 缓存下载(慢) + +
  • +
  • + + 通过 Telegram 下载(需要账号) + +
  • +
+ +

+ 部分用户可能会被强制要求更新。你可以下载 + + 并执行 QQ 音乐 Mac 屏蔽升级.command。 + 其原理是修改 QQ 音乐的版本号,让其认为自己是最新版本,从而达到屏蔽更新的效果。 +

+ +

密钥文件通常存储在下述路径:

+ {DB_PATH} + +

导入密钥

+
    +
  1. + + MMKVStreamEncryptId 文件路径 +
  2. +
  3. + {inSecretImportModal ? ( +

    + 点击上方的文件选择区域,打开文件选择框 +

    + ) : ( +

    前往设定页面,提交该密钥文件。

    + )} +

    + ※ 你可以在文件选择对话框按下 + + + {'+'} + + {'+'} + G + + 组合键打开路径输入框,粘贴文件路径并回车提交。 +

    +
  4. +
+ + ); +} diff --git a/src/features/settings/panels/QMCv2/assets/.gitignore b/src/features/settings/panels/QMCv2/assets/.gitignore new file mode 100644 index 0000000..13772d6 --- /dev/null +++ b/src/features/settings/panels/QMCv2/assets/.gitignore @@ -0,0 +1,3 @@ +com.tencent.QQMusicMac.plist +iData/ +qqmusic-mac-*.mmkv diff --git a/src/features/settings/panels/QMCv2/assets/qqmusic_v10.7_dump.command b/src/features/settings/panels/QMCv2/assets/qqmusic_v10.7_dump.command new file mode 100755 index 0000000..516e8b3 --- /dev/null +++ b/src/features/settings/panels/QMCv2/assets/qqmusic_v10.7_dump.command @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 + +# QQMusic Mac MMKV Decryptor by LSR@Unlock Music + +import hashlib +import re +import sys +from argparse import ArgumentParser +from dataclasses import dataclass +from os import PathLike +from pathlib import Path +from struct import pack, unpack + + +@dataclass +class MMKVDecryptionData: + udid: str + mmkv_path: Path + mmkv_key: str + data: bytes + + @property + def mmkv_name(self) -> str: + return self.mmkv_path.name + + +def _aes_128_cfb_decrypt(key: bytes, iv: bytes, ciphertext: bytes) -> bytes: + """Decrypt using `Crypto.Cipher.AES` _or_ fallback to `OpenSSL` otherwise""" + try: + from Crypto.Cipher import AES # pyright: ignore[reportMissingImports] + + aes = AES.new(key[:16], AES.MODE_CFB, iv=iv, segment_size=128) + return aes.decrypt(ciphertext) + except ImportError: + from subprocess import PIPE, Popen + + process = Popen( + ["openssl", "enc", "-aes-128-cfb", "-d", "-K", key.hex(), "-iv", iv.hex()], + stdin=PIPE, + stdout=PIPE, + stderr=PIPE, + text=False, + ) + stdout, stderr = process.communicate(input=ciphertext) + if process.returncode != 0: + raise RuntimeError( + f"OpenSSL error (install PyCryptodome instead): {stderr.decode()}" + ) + return stdout + + +def _caesar(text: str, shift: int) -> str: + """A simple Caesar cipher implementation for alphanumeric characters""" + result = "" + for char in text: + if char.isalpha(): + base = ord("A") if char.isupper() else ord("a") + result += chr((ord(char) - base + shift) % 26 + base) + elif char.isdigit(): + result += chr((ord(char) - ord("0") + shift) % 10 + ord("0")) + else: + result += char + return result + + +__MMKV_TYPE_STREAM_KEY = 1 + + +def _derive_mmkv_config(udid: str, mmkv_type: int): + """Derive MMKV name and key from UDID, return (name, key)""" + str1 = _caesar(udid, mmkv_type + 3) + int1 = int(udid[5:7], 16) + int2 = 5 + (int1 + mmkv_type) % 4 + mmkv_name = str1[0:int2] + + int3 = mmkv_type + 0xA546 + str3 = f"{udid}{int3:04x}" + mmkv_key = hashlib.md5(str3.encode()).hexdigest() + + return mmkv_name, mmkv_key + + +def _decrypt_mmkv(path: PathLike, key: bytes): + """Decrypt MMKV file using the given key, return decrypted data""" + with open(path, "rb") as mmkv, open(str(path) + ".crc", "rb") as crc: + crc.seek(12) + iv = crc.read(16) + (real_size,) = unpack(" { + navigator.clipboard + .writeText(text) + .then(() => { + toast.success('已复制到剪贴板'); + }) + .catch((err) => { + toast.error(`复制失败,请手动复制\n${err}`); + }); +}; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index e73ae3a..02deb07 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -11,3 +11,9 @@ declare module '*?base64' { const content: string; export default content; } + +declare module '*&mac-command' { + export const tarball: string; + export const commandName: string; + export const tarName: string; +} diff --git a/support/mac-command-loader.ts b/support/mac-command-loader.ts new file mode 100644 index 0000000..31047e7 --- /dev/null +++ b/support/mac-command-loader.ts @@ -0,0 +1,37 @@ +import { basename } from 'node:path'; +import { readFile } from 'node:fs/promises'; +import { Plugin } from 'vite'; +import tar from 'tar-stream'; + +export const macCommandLoader: Plugin = { + name: 'mac-command-loader', + async transform(_: unknown, id: string) { + const [path, query] = id.split('?'); + if (!query || !query.includes('mac-command')) return null; + + const params = new URLSearchParams(query); + + // Mac .command packer. + // - Create a tarball with the given file (a+x) + // - Encode to base64 + + const tarball = tar.pack(); + const name = params.get('name') || `${basename(path)}.command`; + const data = await readFile(path); + tarball.entry({ name, mode: 0o755 }, data); + tarball.finalize(); + + const chunks: Buffer[] = []; + for await (const chunk of tarball) { + chunks.push(chunk as Buffer); + } + const dataBuffer = Buffer.concat(chunks); + const base64 = dataBuffer.toString('base64'); + + return ` + export const tarball = ${JSON.stringify(base64)}; + export const commandName = ${JSON.stringify(name)}; + export const tarName = ${JSON.stringify(`${name}.tar`)}; + `; + }, +}; diff --git a/vite.config.ts b/vite.config.ts index 3711dcc..97af5d2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,6 +12,7 @@ import tailwindcss from '@tailwindcss/vite'; import { tryCommand } from './support/command'; import { base64Loader } from './support/b64-loader'; +import { macCommandLoader } from './support/mac-command-loader'; const projectRoot = url.fileURLToPath(new URL('.', import.meta.url)); const pkg = JSON.parse(fs.readFileSync(projectRoot + '/package.json', 'utf-8')); @@ -47,6 +48,7 @@ export default defineConfig({ plugins: [ tailwindcss(), base64Loader, + macCommandLoader, replace({ preventAssignment: true, values: {