mirror of
https://git.um-react.app/um/um-react.git
synced 2025-11-28 19:43:02 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d514a87198 | ||
|
|
5c537ab8d9 | ||
|
|
48f10e8e30 | ||
|
|
dfac382cbd | ||
|
|
7b2558c585 | ||
|
|
fb52b0197c | ||
|
|
f49f629917 | ||
|
|
8093d30579 | ||
|
|
4fe6efec1f | ||
|
|
c4fe9ce938 | ||
|
|
045bea8084 | ||
|
|
c68195eb9a |
@@ -20,8 +20,13 @@ body:
|
|||||||
|
|
||||||
目前 Mac 客户端仅支持 v8.8.0 或更低版本下载的歌曲文件。
|
目前 Mac 客户端仅支持 v8.8.0 或更低版本下载的歌曲文件。
|
||||||
|
|
||||||
|
* [web.archive.org 镜像](https://web.archive.org/web/20230903/https://dldir1.qq.com/music/clntupate/mac/QQMusicMac_Mgr.dmg)
|
||||||
* [通过 Telegram 下载](https://t.me/um_lsr_ch/21)
|
* [通过 Telegram 下载](https://t.me/um_lsr_ch/21)
|
||||||
|
|
||||||
|
安装好客户端后可以加装更新屏蔽更新:
|
||||||
|
|
||||||
|
* [屏蔽更新](https://t.me/um_lsr_ch/29)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
如果你确定你的客户端版本符合上述描述,并遇到了问题,请继续填写下面的表单。
|
如果你确定你的客户端版本符合上述描述,并遇到了问题,请继续填写下面的表单。
|
||||||
|
|||||||
@@ -26,6 +26,35 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: site
|
name: site
|
||||||
path: dist/
|
path: dist/
|
||||||
|
- name: Create Release Package
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
run: |
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/v}
|
||||||
|
mkdir release/
|
||||||
|
python3 -m zipfile -c "release/um-react-${VERSION}.zip" dist/.
|
||||||
|
cp win64/dist/*.zip "release/um-react-win64-${VERSION}.zip"
|
||||||
|
- name: Create Draft Release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
uses: akkuman/gitea-release-action@v1
|
||||||
|
with:
|
||||||
|
draft: true
|
||||||
|
files: |
|
||||||
|
release/um-react-*.zip
|
||||||
|
release/um-react-win64-*.zip
|
||||||
|
body: |
|
||||||
|
上个版本:[v0.0.0](https://git.um-react.app/um/um-react/releases/tag/v0.0.0)
|
||||||
|
|
||||||
|
## 🐛 修正
|
||||||
|
|
||||||
|
- 修正内容
|
||||||
|
|
||||||
|
## ✨ 新增
|
||||||
|
|
||||||
|
- 新增内容
|
||||||
|
|
||||||
|
## 🔧 维护
|
||||||
|
|
||||||
|
- 维护内容
|
||||||
- name: Prepare for deployment
|
- name: Prepare for deployment
|
||||||
run: |
|
run: |
|
||||||
cp um-react.zip dist/"release-${GITHUB_SHA}.zip"
|
cp um-react.zip dist/"release-${GITHUB_SHA}.zip"
|
||||||
|
|||||||
11870
package-lock.json
generated
11870
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "um-react",
|
"name": "um-react",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.5.1",
|
"version": "0.6.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^2.9.0",
|
"@reduxjs/toolkit": "^2.9.0",
|
||||||
"@unlock-music/crypto": "^0.1.11",
|
"@unlock-music/crypto": "^0.1.12",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"radash": "^12.1.1",
|
"radash": "^12.1.1",
|
||||||
@@ -33,25 +33,26 @@
|
|||||||
"sql.js": "^1.13.0"
|
"sql.js": "^1.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.34.0",
|
"@eslint/js": "^9.35.0",
|
||||||
"@rollup/plugin-replace": "^6.0.2",
|
"@rollup/plugin-replace": "^6.0.2",
|
||||||
"@tailwindcss/vite": "^4.1.12",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
"@testing-library/jest-dom": "^6.8.0",
|
"@testing-library/jest-dom": "^6.8.0",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/node": "^24.3.0",
|
"@types/node": "^24.3.1",
|
||||||
"@types/react": "^19.1.12",
|
"@types/react": "^19.1.12",
|
||||||
"@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",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"daisyui": "^5.1.6",
|
"daisyui": "^5.1.8",
|
||||||
"eslint": "^9.34.0",
|
"eslint": "^9.35.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
@@ -60,14 +61,15 @@
|
|||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"lint-staged": "^16.1.6",
|
"lint-staged": "^16.1.6",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"rollup": "^4.50.0",
|
"rollup": "^4.50.1",
|
||||||
"sass": "^1.92.0",
|
"sass": "^1.92.1",
|
||||||
"simple-git-hooks": "^2.13.1",
|
"simple-git-hooks": "^2.13.1",
|
||||||
"tailwindcss": "^4.1.12",
|
"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",
|
||||||
"vite": "^7.1.4",
|
"vite": "^7.1.5",
|
||||||
"vite-plugin-pwa": "^1.0.3",
|
"vite-plugin-pwa": "^1.0.3",
|
||||||
"vite-plugin-top-level-await": "^1.6.0",
|
"vite-plugin-top-level-await": "^1.6.0",
|
||||||
"vite-plugin-wasm": "^3.5.0",
|
"vite-plugin-wasm": "^3.5.0",
|
||||||
@@ -90,11 +92,9 @@
|
|||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@rollup/plugin-terser": "patches/@rollup__plugin-terser.patch",
|
|
||||||
"sql.js": "patches/sql.js.patch"
|
"sql.js": "patches/sql.js.patch"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"rollup-plugin-terser": "npm:@rollup/plugin-terser@0.4.3",
|
|
||||||
"sourcemap-codec": "npm:@jridgewell/sourcemap-codec@1.4.15"
|
"sourcemap-codec": "npm:@jridgewell/sourcemap-codec@1.4.15"
|
||||||
},
|
},
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
|||||||
1418
pnpm-lock.yaml
generated
1418
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
|||||||
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
import { CodeHighlight } from '../CodeHighlight';
|
||||||
import hljsStyleGitHub from 'react-syntax-highlighter/dist/esm/styles/hljs/github';
|
|
||||||
import { ExtLink } from '../ExtLink';
|
import { ExtLink } from '../ExtLink';
|
||||||
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';
|
||||||
@@ -45,9 +44,7 @@ export function AdbInstructionTemplate({ dir, file, platform }: AdbInstructionTe
|
|||||||
<li>将安卓设备连接到电脑。</li>
|
<li>将安卓设备连接到电脑。</li>
|
||||||
<li>
|
<li>
|
||||||
<p>粘贴执行下述代码执行。若设备提示「是否允许 USB 调试」或「超级用户请求」,选择允许:</p>
|
<p>粘贴执行下述代码执行。若设备提示「是否允许 USB 调试」或「超级用户请求」,选择允许:</p>
|
||||||
<SyntaxHighlighter language={language} style={hljsStyleGitHub}>
|
<CodeHighlight language={language}>{command}</CodeHighlight>
|
||||||
{command}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
<br />※ 安卓模拟器可能需要额外操作,如
|
<br />※ 安卓模拟器可能需要额外操作,如
|
||||||
<ExtLink className="text-nowrap" href="https://g.126.fm/04jewvw">
|
<ExtLink className="text-nowrap" href="https://g.126.fm/04jewvw">
|
||||||
网易 MuMu 模拟器
|
网易 MuMu 模拟器
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ export function RootExplorerGuide() {
|
|||||||
<div className="flex flex-col items-start gap-4 @md:flex-row">
|
<div className="flex flex-col items-start gap-4 @md:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<Header5 className="[&]:mt-0 [&]:pt-0">Amaze 文件浏览器</Header5>
|
<Header5 className="[&]:mt-0 [&]:pt-0">Amaze 文件浏览器</Header5>
|
||||||
<ul className="ml-2 list-disc list-inside">
|
<ul className="ml-2 list-disc">
|
||||||
<li>
|
<li>
|
||||||
<div className="inline-flex items-center gap-1">
|
<div className="inline-flex items-center flex-wrap">
|
||||||
点触主界面左上角的 <FiMenu /> 打开侧边栏
|
点触主界面左上角的 <FiMenu className="m-1" /> 打开侧边栏
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -41,16 +41,16 @@ export function RootExplorerGuide() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Header5 className="[&]:mt-0 [&]:pt-0">MT 管理器</Header5>
|
<Header5 className="[&]:mt-0 [&]:pt-0">MT 管理器</Header5>
|
||||||
<ul className="ml-2 list-disc list-inside">
|
<ul className="ml-2 list-disc">
|
||||||
<li>
|
<li>
|
||||||
<div className="inline-flex items-center gap-1">
|
<div className="inline-flex items-center flex-wrap">
|
||||||
点触主界面左上角的 <FiMenu /> 打开侧边栏
|
点触主界面左上角的 <FiMenu className="m-1" /> 打开侧边栏
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div className="inline-flex items-center">
|
<div className="inline-flex items-center flex-wrap">
|
||||||
点触侧边栏右上方的 <FiMoreVertical className="ml-1" />
|
点触侧边栏右上方的 <FiMoreVertical className="ml-1" />
|
||||||
,点触<VQuote>设置</VQuote>
|
弹出菜单,点触<VQuote>设置</VQuote>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
10
src/components/CodeHighlight.tsx
Normal file
10
src/components/CodeHighlight.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Light as SyntaxHighlighter, type SyntaxHighlighterProps } from 'react-syntax-highlighter';
|
||||||
|
import hljsStyleGitHub from 'react-syntax-highlighter/dist/esm/styles/hljs/github';
|
||||||
|
|
||||||
|
export function CodeHighlight({ children, ...props }: SyntaxHighlighterProps) {
|
||||||
|
return (
|
||||||
|
<SyntaxHighlighter style={hljsStyleGitHub} {...props}>
|
||||||
|
{children}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
src/components/DownloadBase64.tsx
Normal file
32
src/components/DownloadBase64.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,7 +5,7 @@ export function Footer() {
|
|||||||
const appVersionShort = '__APP_VERSION_SHORT__';
|
const appVersionShort = '__APP_VERSION_SHORT__';
|
||||||
return (
|
return (
|
||||||
<footer className="flex flex-col text-center p-4 bg-base-200">
|
<footer className="flex flex-col text-center p-4 bg-base-200">
|
||||||
<p className="flex flex-row justify-center items-center h-[1em]">
|
<div className="flex flex-row justify-center items-center h-[1em]">
|
||||||
<a className="link link-info mr-1" href="https://git.um-react.app/um/um-react">
|
<a className="link link-info mr-1" href="https://git.um-react.app/um/um-react">
|
||||||
音乐解锁
|
音乐解锁
|
||||||
</a>
|
</a>
|
||||||
@@ -19,7 +19,7 @@ export function Footer() {
|
|||||||
</a>
|
</a>
|
||||||
, v{appVersionShort}
|
, v{appVersionShort}
|
||||||
<SDKVersion />)
|
<SDKVersion />)
|
||||||
</p>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
{'© 2019 - '}
|
{'© 2019 - '}
|
||||||
<CurrentYear />
|
<CurrentYear />
|
||||||
|
|||||||
@@ -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
src/context/InSecretImportModal.tsx
Normal file
3
src/context/InSecretImportModal.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export const InSecretImportModalContext = createContext<boolean>(false);
|
||||||
@@ -31,6 +31,7 @@ class DecryptCommandHandler {
|
|||||||
const [result, error] = await go(this.tryDecryptWith(decipher));
|
const [result, error] = await go(this.tryDecryptWith(decipher));
|
||||||
if (!error) {
|
if (!error) {
|
||||||
if (result) {
|
if (result) {
|
||||||
|
console.debug(`[${decipher.cipherName}] Decryption OK`);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
errors.push(`${decipher.cipherName}: no response`);
|
errors.push(`${decipher.cipherName}: no response`);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function OtherFAQ() {
|
|||||||
<div className="flex flex-col md:flex-row gap-2 md:gap-8">
|
<div className="flex flex-col md:flex-row gap-2 md:gap-8">
|
||||||
<div>
|
<div>
|
||||||
<Header4>已知有问题的浏览器</Header4>
|
<Header4>已知有问题的浏览器</Header4>
|
||||||
<ul className="list-disc list-inside pl-2">
|
<ul className="list-disc pl-8">
|
||||||
<li>Via 浏览器</li>
|
<li>Via 浏览器</li>
|
||||||
<li>夸克浏览器</li>
|
<li>夸克浏览器</li>
|
||||||
<li>UC 浏览器</li>
|
<li>UC 浏览器</li>
|
||||||
@@ -25,7 +25,7 @@ export function OtherFAQ() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Header4>可能会遇到的问题包括</Header4>
|
<Header4>可能会遇到的问题包括</Header4>
|
||||||
<ul className="list-disc list-inside pl-2">
|
<ul className="list-disc pl-8">
|
||||||
<li>网页白屏</li>
|
<li>网页白屏</li>
|
||||||
<li>无法下载解密后内容</li>
|
<li>无法下载解密后内容</li>
|
||||||
<li>下载的文件名错误</li>
|
<li>下载的文件名错误</li>
|
||||||
|
|||||||
@@ -1,79 +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';
|
|
||||||
|
|
||||||
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">密钥文件通常存储在下述路径:</p>
|
</div>
|
||||||
<FilePathBlock>{DB_PATH}</FilePathBlock>
|
</div>
|
||||||
|
|
||||||
<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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/features/settings/panels/QMCv2/InstructionsMacV10.tsx
Normal file
65
src/features/settings/panels/QMCv2/InstructionsMacV10.tsx
Normal file
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
89
src/features/settings/panels/QMCv2/InstructionsMacV8.tsx
Normal file
89
src/features/settings/panels/QMCv2/InstructionsMacV8.tsx
Normal file
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
src/features/settings/panels/QMCv2/assets/.gitignore
vendored
Normal file
3
src/features/settings/panels/QMCv2/assets/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
com.tencent.QQMusicMac.plist
|
||||||
|
iData/
|
||||||
|
qqmusic-mac-*.mmkv
|
||||||
213
src/features/settings/panels/QMCv2/assets/qqmusic_v10.7_dump.command
Executable file
213
src/features/settings/panels/QMCv2/assets/qqmusic_v10.7_dump.command
Executable file
@@ -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
src/util/clipboard.ts
Normal file
12
src/util/clipboard.ts
Normal 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
src/vite-env.d.ts
vendored
6
src/vite-env.d.ts
vendored
@@ -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
support/mac-command-loader.ts
Normal file
37
support/mac-command-loader.ts
Normal 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`)};
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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'));
|
||||||
@@ -32,6 +33,7 @@ export default defineConfig({
|
|||||||
// strict: false,
|
// strict: false,
|
||||||
|
|
||||||
allow: [
|
allow: [
|
||||||
|
'index.html',
|
||||||
'src',
|
'src',
|
||||||
'node_modules',
|
'node_modules',
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ export default defineConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
base64Loader,
|
base64Loader,
|
||||||
|
macCommandLoader,
|
||||||
replace({
|
replace({
|
||||||
preventAssignment: true,
|
preventAssignment: true,
|
||||||
values: {
|
values: {
|
||||||
|
|||||||
Reference in New Issue
Block a user