Merge pull request '添加 Mac QQ 音乐 v10 的说明' (#3) from feat/mac-qqmusic-v10 into main

Reviewed-on: https://git.um-react.app/um/um-react/pulls/3
This commit is contained in:
鲁树人
2025-10-15 12:59:19 +00:00
17 changed files with 609 additions and 105 deletions
+2
View File
@@ -44,6 +44,7 @@
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.1.9",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"@types/sql.js": "^1.4.9", "@types/sql.js": "^1.4.9",
"@types/tar-stream": "^3.1.4",
"@types/wicg-file-system-access": "^2023.10.6", "@types/wicg-file-system-access": "^2023.10.6",
"@typescript-eslint/eslint-plugin": "^8.42.0", "@typescript-eslint/eslint-plugin": "^8.42.0",
"@typescript-eslint/parser": "^8.42.0", "@typescript-eslint/parser": "^8.42.0",
@@ -64,6 +65,7 @@
"sass": "^1.92.1", "sass": "^1.92.1",
"simple-git-hooks": "^2.13.1", "simple-git-hooks": "^2.13.1",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.13",
"tar-stream": "^3.1.7",
"terser": "^5.44.0", "terser": "^5.44.0",
"typescript": "^5.9.2", "typescript": "^5.9.2",
"typescript-eslint": "^8.42.0", "typescript-eslint": "^8.42.0",
+88 -16
View File
@@ -92,6 +92,9 @@ importers:
'@types/sql.js': '@types/sql.js':
specifier: ^1.4.9 specifier: ^1.4.9
version: 1.4.9 version: 1.4.9
'@types/tar-stream':
specifier: ^3.1.4
version: 3.1.4
'@types/wicg-file-system-access': '@types/wicg-file-system-access':
specifier: ^2023.10.6 specifier: ^2023.10.6
version: 2023.10.6 version: 2023.10.6
@@ -152,6 +155,9 @@ importers:
tailwindcss: tailwindcss:
specifier: ^4.1.13 specifier: ^4.1.13
version: 4.1.13 version: 4.1.13
tar-stream:
specifier: ^3.1.7
version: 3.1.7
terser: terser:
specifier: ^5.44.0 specifier: ^5.44.0
version: 5.44.0 version: 5.44.0
@@ -1533,6 +1539,9 @@ packages:
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-ep8b36RKHlgWPqjNG9ToUrPiwkhwh0AEzy883mO5Xnd+cL6VBH1EvSjBAAuxLUFF2Vn/moE3Me6v9E1Lo+48GQ==} resolution: {integrity: sha512-ep8b36RKHlgWPqjNG9ToUrPiwkhwh0AEzy883mO5Xnd+cL6VBH1EvSjBAAuxLUFF2Vn/moE3Me6v9E1Lo+48GQ==}
'@types/[email protected]':
resolution: {integrity: sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==}
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@@ -1744,6 +1753,14 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==}
peerDependencies:
react-native-b4a: '*'
peerDependenciesMeta:
react-native-b4a:
optional: true
[email protected]: [email protected]:
resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==}
peerDependencies: peerDependencies:
@@ -1762,6 +1779,14 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
[email protected]:
resolution: {integrity: sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA==}
peerDependencies:
bare-abort-controller: '*'
peerDependenciesMeta:
bare-abort-controller:
optional: true
[email protected]: [email protected]:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
@@ -2122,6 +2147,9 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
[email protected]:
resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
[email protected]: [email protected]:
resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -2129,6 +2157,9 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
[email protected]:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
[email protected]: [email protected]:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'} engines: {node: '>=8.6.0'}
@@ -2792,15 +2823,10 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
minizlib@3.0.2: minizlib@3.1.0:
resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
[email protected]:
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
engines: {node: '>=10'}
hasBin: true
[email protected]: [email protected]:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -3275,6 +3301,9 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==}
[email protected]: [email protected]:
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
engines: {node: '>=0.6.19'} engines: {node: '>=0.6.19'}
@@ -3352,8 +3381,11 @@ packages:
resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==}
engines: {node: '>=6'} engines: {node: '>=6'}
tar@7.4.3: tar[email protected]:
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
[email protected]:
resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==}
engines: {node: '>=18'} engines: {node: '>=18'}
[email protected]: [email protected]:
@@ -3373,6 +3405,9 @@ packages:
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
engines: {node: '>=18'} engines: {node: '>=18'}
[email protected]:
resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==}
[email protected]: [email protected]:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -4995,7 +5030,7 @@ snapshots:
'@tailwindcss/[email protected]': '@tailwindcss/[email protected]':
dependencies: dependencies:
detect-libc: 2.0.4 detect-libc: 2.0.4
tar: 7.4.3 tar: 7.5.1
optionalDependencies: optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.1.13 '@tailwindcss/oxide-android-arm64': 4.1.13
'@tailwindcss/oxide-darwin-arm64': 4.1.13 '@tailwindcss/oxide-darwin-arm64': 4.1.13
@@ -5115,6 +5150,10 @@ snapshots:
'@types/emscripten': 1.41.1 '@types/emscripten': 1.41.1
'@types/node': 24.3.1 '@types/node': 24.3.1
'@types/[email protected]':
dependencies:
'@types/node': 24.3.1
'@types/[email protected]': {} '@types/[email protected]': {}
'@types/[email protected]': {} '@types/[email protected]': {}
@@ -5383,6 +5422,8 @@ snapshots:
dependencies: dependencies:
possible-typed-array-names: 1.1.0 possible-typed-array-names: 1.1.0
[email protected]: {}
[email protected](@babel/[email protected]): [email protected](@babel/[email protected]):
dependencies: dependencies:
'@babel/compat-data': 7.28.4 '@babel/compat-data': 7.28.4
@@ -5409,6 +5450,8 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: [email protected]:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
@@ -5832,10 +5875,18 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
bare-events: 2.8.0
transitivePeerDependencies:
- bare-abort-controller
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: [email protected]:
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@@ -6493,12 +6544,10 @@ snapshots:
[email protected]: {} [email protected]: {}
minizlib@3.0.2: minizlib@3.1.0:
dependencies: dependencies:
minipass: 7.1.2 minipass: 7.1.2
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@@ -6982,6 +7031,15 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
internal-slot: 1.1.0 internal-slot: 1.1.0
[email protected]:
dependencies:
events-universal: 1.0.1
fast-fifo: 1.3.2
text-decoder: 1.2.3
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@@ -7079,13 +7137,21 @@ snapshots:
[email protected]: {} [email protected]: {}
tar@7.4.3: tar[email protected]:
dependencies:
b4a: 1.7.3
fast-fifo: 1.3.2
streamx: 2.23.0
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
[email protected]:
dependencies: dependencies:
'@isaacs/fs-minipass': 4.0.1 '@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0 chownr: 3.0.0
minipass: 7.1.2 minipass: 7.1.2
minizlib: 3.0.2 minizlib: 3.1.0
mkdirp: 3.0.1
yallist: 5.0.0 yallist: 5.0.0
[email protected]: {} [email protected]: {}
@@ -7110,6 +7176,12 @@ snapshots:
glob: 10.4.5 glob: 10.4.5
minimatch: 9.0.5 minimatch: 9.0.5
[email protected]:
dependencies:
b4a: 1.7.3
transitivePeerDependencies:
- react-native-b4a
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
+32
View File
@@ -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 (
<ExtLink
icon={icon ?? <IoMdArchive className="inline size-sm ml-1" />}
className={className ?? 'link-info mx-1'}
download={filename}
href={`data:${mimetype};base64,${data}`}
>
{children ?? <code>{filename}</code>}
</ExtLink>
);
}
+3 -3
View File
@@ -1,15 +1,15 @@
import type { AnchorHTMLAttributes } from 'react'; import type { AnchorHTMLAttributes, ReactNode } from 'react';
import { FiExternalLink } from 'react-icons/fi'; import { FiExternalLink } from 'react-icons/fi';
export type ExtLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & { export type ExtLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
icon?: boolean; icon?: boolean | ReactNode;
}; };
export function ExtLink({ className, icon = true, children, ...props }: ExtLinkProps) { export function ExtLink({ className, icon = true, children, ...props }: ExtLinkProps) {
return ( return (
<a rel="noreferrer noopener nofollow" target="_blank" className={`link ${className}`} {...props}> <a rel="noreferrer noopener nofollow" target="_blank" className={`link ${className}`} {...props}>
{children} {children}
{icon && <FiExternalLink className="inline size-sm ml-1" />} {icon === true ? <FiExternalLink className="inline size-sm ml-1" /> : icon}
</a> </a>
); );
} }
+5 -2
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { FileInput } from '~/components/FileInput'; import { FileInput } from '~/components/FileInput';
import { InSecretImportModalContext } from '~/context/InSecretImportModal';
export interface ImportSecretModalProps { export interface ImportSecretModalProps {
clientName?: React.ReactNode; clientName?: React.ReactNode;
@@ -31,7 +32,7 @@ export function ImportSecretModal({ clientName, children, show, onClose, onImpor
}, [show]); }, [show]);
return ( return (
<dialog ref={refModel} className="modal"> <dialog ref={refModel} className="modal" onClose={onClose}>
<div className="modal-box"> <div className="modal-box">
<form method="dialog" onSubmit={() => onClose()}> <form method="dialog" onSubmit={() => onClose()}>
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button> <button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
@@ -41,7 +42,9 @@ export function ImportSecretModal({ clientName, children, show, onClose, onImpor
<FileInput onReceiveFiles={handleFileReceived}>拖放或点我选择含有密钥的数据库文件</FileInput> <FileInput onReceiveFiles={handleFileReceived}>拖放或点我选择含有密钥的数据库文件</FileInput>
<div className="mt-2">选择你的{clientName && <>「{clientName}」</>}客户端平台以查看对应说明:</div> <div className="mt-2">选择你的{clientName && <>「{clientName}」</>}客户端平台以查看对应说明:</div>
<div>{children}</div> <InSecretImportModalContext.Provider value={true}>
<div>{children}</div>
</InSecretImportModalContext.Provider>
</div> </div>
</div> </div>
</dialog> </dialog>
+3
View File
@@ -0,0 +1,3 @@
import { createContext } from 'react';
export const InSecretImportModalContext = createContext<boolean>(false);
@@ -1,94 +1,31 @@
import { RiFileCopyLine } from 'react-icons/ri'; import { useId } from 'react';
import { toast } from 'react-toastify'; import { InstructionsMacV8 } from './InstructionsMacV8';
import { ExtLink } from '~/components/ExtLink'; import { InstructionsMacV10 } from './InstructionsMacV10';
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';
export function InstructionsMac() { export function InstructionsMac() {
const copyDbPathToClipboard = () => { const macInstructionId = useId();
navigator.clipboard
.writeText(DB_PATH)
.then(() => {
toast.success('已复制到剪贴板');
})
.catch((err) => {
toast.error(`复制失败,请手动复制\n${err}`);
});
};
return ( return (
<> <>
<p>Mac 客户端使用 mmkv 数据库储存密钥。</p> <p>Mac 客户端使用 mmkv 数据库储存密钥。</p>
<p>此外,你需要降级到 v8.8.0 版本的客户端 —— 更新的版本对密钥数据库进行了加密,目前无公开的获取方案。</p> <p>建议使用 v8.8.0 或 v10.7 版本的客户端,其中 v8.8.0 版本需要屏蔽更新。</p>
<p className="mt-4">获取 QQ 音乐 Mac 客户端 8.8.0:</p> <div className="join join-vertical bg-base-100 mt-2 max-w-full">
<ul className="list-disc pl-6"> <div className="collapse collapse-arrow join-item border-base-300 border">
<li> <input type="radio" name={macInstructionId} />
<ExtLink className="link-info" href={MAC_CLIENT_URL}> <div className="collapse-title font-semibold">使用 QQ 音乐 Mac v8.8.0</div>
通过 <code>Archive.org</code> 缓存下载(慢) <div className="collapse-content text-sm min-w-0">
</ExtLink> <InstructionsMacV8 />
</li> </div>
<li> </div>
<ExtLink className="link-info" href={MAC_CLIENT_TG_URL}> <div className="collapse collapse-arrow join-item border-base-300 border">
通过 Telegram 下载(需要账号) <input type="radio" name={macInstructionId} />
</ExtLink> <div className="collapse-title font-semibold">使用 QQ 音乐 Mac v10.7.1</div>
</li> <div className="collapse-content text-sm min-w-0">
</ul> <InstructionsMacV10 />
</div>
<p className="mt-4"> </div>
有部分用户发现现在会强制更新。你可以下载 </div>
<ExtLink
className="link-info mx-1"
download="QQ 音乐 Mac 屏蔽升级.tar.gz"
href={`data:application/gzip;base64,${BlockUpdateScript}`}
>
QQ 音乐 Mac 屏蔽升级.tar.gz
</ExtLink>
,然后执行 <code>QQ 音乐 Mac 屏蔽升级.command</code>。 其原理是修改 QQ
音乐的版本号,让其认为自己是最新版本,从而屏蔽更新。
</p>
<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>
</> </>
); );
} }
@@ -0,0 +1,65 @@
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 (
<>
<p className="mt-4">获取 QQ 音乐 Mac 客户端 10.7.1:</p>
<ul className="list-disc pl-6">
<li>
<ExtLink className="link-info" href={MAC_CLIENT_URL}>
通过 QQ 音乐官网下载(高速,但可能失效)
</ExtLink>
</li>
<li>
<ExtLink className="link-info" href={MAC_CLIENT_TG_URL}>
通过 Telegram 下载(缓存,需要账号)
</ExtLink>
</li>
</ul>
<h4 className="font-bold text-lg mt-4">导入密钥</h4>
<ol className="list-decimal pl-6">
<li>
下载 <DownloadBase64 data={DUMP_COMMAND_BASE64} filename={DUMP_COMMAND_TARBALL_NAME}></DownloadBase64>
,打开得到 <code>{DUMP_COMMAND_NAME}</code>。
</li>
<li>
<p>
双击 <code>{DUMP_COMMAND_NAME}</code> 执行。
</p>
<p>
※ 若是提示文件来自未知开发者,请右键点击该文件,选择菜单第一项
<VQuote>打开</VQuote>,在警告窗口再次选择<VQuote>打开</VQuote>。
</p>
</li>
<li>
运行后会在脚本当前目录生成 <code>qqmusic-mac-*.mmkv</code> 文件,其中 <code>*</code> 是一串随机字符。
</li>
{inSecretImportModal ? (
<li>
上传刚生成的 <code>qqmusic-mac-*.mmkv</code> 文件到上方的<VQuote>文件选择区域</VQuote>。
</li>
) : (
<li>
前往设定页面,提交生成的 <code>qqmusic-mac-*.mmkv</code> 文件。
</li>
)}
</ol>
</>
);
}
@@ -0,0 +1,89 @@
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 {
commandName as BLOCK_UPDATE_COMAND,
tarName as BLOCK_UPDATE_TAR_NAME,
tarball as BLOCK_UPDATE_BASE64,
} from './assets/qqmusic_v8.8.0_patch_update.command?&name=QQ 音乐 Mac v8.8.0 屏蔽更新.command&mac-command';
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 (
<>
<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">
部分用户可能会被强制要求更新。你可以下载
<DownloadBase64 filename={BLOCK_UPDATE_TAR_NAME} data={BLOCK_UPDATE_BASE64}></DownloadBase64>
并执行 <code>{BLOCK_UPDATE_COMAND}</code>。
<span>其原理是修改 QQ 音乐的版本号,让其认为自己是最新版本,从而达到屏蔽更新的效果。</span>
</p>
<p>
※ 若是提示文件来自未知开发者,请右键点击该文件,选择菜单第一项
<VQuote>打开</VQuote>,在警告窗口再次选择<VQuote>打开</VQuote>。
</p>
<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={() => copyToClipboard(DB_PATH)}>
<RiFileCopyLine className="text-xl" />
<span>复制</span>
</button>
<code>MMKVStreamEncryptId</code> 文件路径
</li>
<li>
{inSecretImportModal ? (
<p>
点击上方的<VQuote>文件选择区域</VQuote>,打开<VQuote>文件选择框</VQuote>
</p>
) : (
<p>前往设定页面,提交该密钥文件。</p>
)}
<p>
※ 你可以在文件选择对话框按下
<VQuote>
<ShiftKey className="mx-1" />
{'+'}
<MacCommandKey className="mx-1" />
{'+'}
<kbd className="kbd mx-1">G</kbd>
</VQuote>
组合键打开<VQuote>路径输入框</VQuote>,粘贴文件路径并回车提交。
</p>
</li>
</ol>
</>
);
}
@@ -0,0 +1,3 @@
com.tencent.QQMusicMac.plist
iData/
qqmusic-mac-*.mmkv
@@ -0,0 +1,213 @@
#!/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 os.path import dirname
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("<I", crc.read(4))
(mmkv_payload_size,) = unpack("<I", mmkv.read(4))
if mmkv_payload_size != real_size:
raise ValueError("MMKV file size mismatch")
decrypted_data = pack("<I", real_size)
decrypted_data += _aes_128_cfb_decrypt(key, iv, mmkv.read(real_size))
return decrypted_data
def _dump_udid(plist_file: PathLike):
"""Extract UDIDs from the given plist file"""
with open(plist_file, "rb") as f:
plist = f.read()
for m in re.finditer(rb"_\x10\(([0-9a-f]{40})_", plist):
yield m.group(1).decode()
def _dump_mmkv(plist_file: PathLike, data_dir: PathLike):
"""Dump all MMKV files from the given plist file and iData directory"""
for udid in _dump_udid(plist_file):
mmkv_name, mmkv_key = _derive_mmkv_config(udid, __MMKV_TYPE_STREAM_KEY)
mmkv_path = Path(data_dir) / mmkv_name
if not mmkv_path.exists() or not mmkv_path.is_file():
print(f"MMKV file not found, skipping (path={mmkv_path})", file=sys.stderr)
continue
try:
decrypted_mmkv = _decrypt_mmkv(mmkv_path, mmkv_key.encode())
except Exception as e:
print(
"Error decrypting mmkv, skipping"
f" (path={mmkv_path}, key={mmkv_key}, error={e})",
file=sys.stderr,
)
continue
yield MMKVDecryptionData(
udid=udid,
mmkv_path=mmkv_path,
mmkv_key=mmkv_key,
data=decrypted_mmkv,
)
def main():
parser = ArgumentParser(
description="QQMusic Mac MMKV Decryptor by LSR@Unlock Music"
)
parser.add_argument(
"-p",
"--plist",
type=str,
nargs="+",
help="Path to com.tencent.QQMusicMac.plist file or files",
default=[],
)
parser.add_argument(
"-i",
"--idata",
type=str,
help="Path to iData directory",
default="",
)
parser.add_argument("-f", "--force", action="store_true", help="Force overwrite")
parser.add_argument(
"-o",
"--output",
type=str,
help="Output directory for decrypted MMKV files (default: script directory)",
default=dirname(__file__),
)
parser.add_argument(
"-v", "--verbose", action="store_true", help="Enable verbose output"
)
args = parser.parse_args()
home_dir = Path.home()
app_sandbox_dir = home_dir / "Library/Containers/com.tencent.QQMusicMac/Data"
idata_dir = app_sandbox_dir / "Library/Application Support/QQMusicMac/iData"
if args.idata:
idata_dir = Path(args.idata)
plists = []
if args.plist:
plists = [Path(p) for p in args.plist]
else:
for base_dir in (home_dir, app_sandbox_dir):
plists.append(base_dir / "Library/Preferences/com.tencent.QQMusicMac.plist")
output_dir = Path(args.output)
output_dir.mkdir(parents=True, exist_ok=True)
force = args.force
verbose = args.verbose
for plist_file in plists:
if plist_file.exists() and plist_file.is_file():
for dump in _dump_mmkv(plist_file, idata_dir):
out_path = output_dir / f"qqmusic-mac-{dump.mmkv_path.name}.mmkv"
if out_path.exists() and not force:
print(f"output exists, skipping (name={out_path.name})")
continue
if verbose:
print("*** MMKV DUMP ENTRY START ***")
print(f"UDID: {dump.udid}")
print(f"MMKV Name: {dump.mmkv_path.name}")
print(f"MMKV Key: {dump.mmkv_key}")
print(f"Output: {out_path.name}")
print("**** MMKV DUMP ENTRY END ****")
else:
print(f"Dumping mmkv: {out_path.name}...")
try:
with open(out_path, "wb") as f:
f.write(dump.data)
except Exception as e:
print(f"Error writing decrypted mmkv: {e}", file=sys.stderr)
continue
if __name__ == "__main__":
main()
@@ -0,0 +1,28 @@
#!/bin/sh
echo '补丁中…'
patch_count=0
patch_qqmusic() {
SUDO="$1"
APP="$2"
if [ ! -d "$APP" ]; then
echo "路径不存在,跳过 $APP..."
return
fi
echo "修补 $APP..."
$SUDO sed -i.bak 's#<string>8.8.0</string>#<string>88.8.0</string>#' \
"$APP/Contents/Info.plist"
$SUDO codesign --force --deep --sign - "$APP"
$SUDO xattr -d com.apple.quarantine "$APP"
patch_count=$((patch_count + 1))
}
patch_qqmusic sudo "/Applications/QQMusic.app"
patch_qqmusic "" "$HOME/Applications/QQMusic.app"
echo "完成,已修补 $patch_count 个 QQ 音乐安装"
+12
View File
@@ -0,0 +1,12 @@
import { toast } from 'react-toastify';
export const copyToClipboard = (text: string) => {
navigator.clipboard
.writeText(text)
.then(() => {
toast.success('已复制到剪贴板');
})
.catch((err) => {
toast.error(`复制失败,请手动复制\n${err}`);
});
};
+6
View File
@@ -11,3 +11,9 @@ declare module '*?base64' {
const content: string; const content: string;
export default content; export default content;
} }
declare module '*&mac-command' {
export const tarball: string;
export const commandName: string;
export const tarName: string;
}
+37
View File
@@ -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`)};
`;
},
};
+2
View File
@@ -12,6 +12,7 @@ import tailwindcss from '@tailwindcss/vite';
import { tryCommand } from './support/command'; import { tryCommand } from './support/command';
import { base64Loader } from './support/b64-loader'; import { base64Loader } from './support/b64-loader';
import { macCommandLoader } from './support/mac-command-loader';
const projectRoot = url.fileURLToPath(new URL('.', import.meta.url)); const projectRoot = url.fileURLToPath(new URL('.', import.meta.url));
const pkg = JSON.parse(fs.readFileSync(projectRoot + '/package.json', 'utf-8')); const pkg = JSON.parse(fs.readFileSync(projectRoot + '/package.json', 'utf-8'));
@@ -47,6 +48,7 @@ export default defineConfig({
plugins: [ plugins: [
tailwindcss(), tailwindcss(),
base64Loader, base64Loader,
macCommandLoader,
replace({ replace({
preventAssignment: true, preventAssignment: true,
values: { values: {