mirror of
https://git.um-react.app/um/um-react.git
synced 2025-11-28 03:23:02 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acb7a634b1 | ||
|
|
ce969af57f | ||
|
|
ec4bd16b03 | ||
|
|
531930a6ec | ||
|
|
3862f2d38e | ||
|
|
ddc073fbcc | ||
|
|
82dbfc2d1f | ||
|
|
87d2d71193 | ||
|
|
759252cec5 | ||
|
|
afc65fd5d0 | ||
|
|
9f587212bc | ||
|
|
9ede00037e | ||
|
|
0951963f46 | ||
|
|
c57bc9cfbb | ||
|
|
b16e3bf3ea | ||
|
|
e9a95d1bd6 | ||
|
|
00813957d6 | ||
|
|
b26e62e8d9 | ||
|
|
9fed1ee610 | ||
|
|
5e890bca77 |
35
.drone.yml
35
.drone.yml
@@ -1,35 +0,0 @@
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: test & build
|
||||
image: node:20.10.0-bookworm
|
||||
commands:
|
||||
# - git config --global --add safe.directory "/drone/src"
|
||||
- corepack enable
|
||||
- corepack prepare pnpm@latest --activate
|
||||
- pnpm i --frozen-lockfile
|
||||
- pnpm build
|
||||
environment:
|
||||
# 让 npm 使用淘宝源
|
||||
npm_config_registry: https://registry.npmmirror.com
|
||||
|
||||
- name: publish
|
||||
image: node:20.10.0-bookworm
|
||||
environment:
|
||||
DRONE_GITEA_SERVER: https://git.unlock-music.dev
|
||||
GITEA_API_KEY:
|
||||
from_secret: GITEA_API_KEY
|
||||
NETLIFY_SITE_ID:
|
||||
from_secret: NETLIFY_SITE_ID
|
||||
NETLIFY_API_KEY:
|
||||
from_secret: NETLIFY_API_KEY
|
||||
commands:
|
||||
- |
|
||||
python3 -m zipfile -c um-react.zip dist/.
|
||||
cp um-react.zip dist/"release-${DRONE_COMMIT_SHA}.zip"
|
||||
python3 -m zipfile -c um-react-site.zip dist/.
|
||||
- ./scripts/publish.sh
|
||||
- ./scripts/deploy.sh
|
||||
@@ -11,5 +11,5 @@ charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{{c,m,}js{x,on,},ts{x,}}]
|
||||
[*.{{c,m,}js{x,on,},ts{x,},y{,a}ml}]
|
||||
indent_size = 2
|
||||
|
||||
34
.gitea/workflows/build.yaml
Normal file
34
.gitea/workflows/build.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Build and Deploy
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
npm_config_registry: https://registry.npmmirror.com
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.0.0
|
||||
with:
|
||||
standalone: true
|
||||
run_install: |
|
||||
- args: [--frozen-lockfile, --strict-peer-dependencies]
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
- name: Prepare for deployment
|
||||
run: |
|
||||
python3 -m zipfile -c um-react.zip dist/.
|
||||
cp um-react.zip dist/"release-${GITHUB_SHA}.zip"
|
||||
python3 -m zipfile -c um-react-site.zip dist/.
|
||||
- name: Publish Artifact
|
||||
uses: christopherhx/gitea-upload-artifact@v4
|
||||
with:
|
||||
name: site
|
||||
path: dist/
|
||||
- name: Deploy
|
||||
env:
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
NETLIFY_API_KEY: ${{ secrets.NETLIFY_API_KEY }}
|
||||
run: ./scripts/deploy.sh
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
pnpm exec lint-staged
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
pnpm test
|
||||
4
.npmrc
4
.npmrc
@@ -1,5 +1,3 @@
|
||||
use-node-version=20.10.0
|
||||
node-version=20.10.0
|
||||
use-node-version=22.12.0
|
||||
engine-strict=true
|
||||
@um:registry=https://git.unlock-music.dev/api/packages/um/npm/
|
||||
@unlock-music:registry=https://git.unlock-music.dev/api/packages/um/npm/
|
||||
|
||||
35
README.MD
35
README.MD
@@ -7,14 +7,17 @@
|
||||
- Unlock Music 项目是以学习和技术研究的初衷创建的,修改、再分发时请遵循[授权协议]。
|
||||
- Unlock Music 的 CLI 版本可以在 [unlock-music/cli] 找到,大批量转换建议使用 CLI 版本。
|
||||
- 我们新建了 Telegram 群组 [`@unlock_music_chat`] ,欢迎加入!
|
||||
- CI 自动构建已经部署,可以在 [Packages][um-react-packages] 下载。
|
||||
- CI 自动构建已经部署,可以在 [Actions][um-react-actions] 寻找对应的<ruby>构建产物<rp>(</rp><rt>Artifact</rt><rp>)</rp> </ruby>下载。
|
||||
- [常见问题参考](./docs/faq_zh-hans.md)
|
||||
|
||||
> **WARNING**
|
||||
> 在本站 fork 不会起到备份的作用,只会浪费服务器储存空间。如无必要请勿 fork 该仓库。
|
||||
|
||||
[授权协议]: https://git.unlock-music.dev/um/um-react/src/branch/main/LICENSE
|
||||
[um-vue]: https://git.unlock-music.dev/um/web
|
||||
[unlock-music/cli]: https://git.unlock-music.dev/um/cli
|
||||
[`@unlock_music_chat`]: https://t.me/unlock_music_chat
|
||||
[um-react-packages]: https://git.unlock-music.dev/um/-/packages/generic/um-react/
|
||||
[um-react-actions]: https://git.unlock-music.dev/um/um-react/actions
|
||||
|
||||
⚠️ 手机端浏览器支持有限,请使用最新版本的 Chrome 或 Firefox 官方浏览器。
|
||||
|
||||
@@ -30,20 +33,28 @@
|
||||
- [x] 虾米音乐 (`.xm`)
|
||||
- [x] 酷我音乐 (`.kwm`)
|
||||
- [x] 酷狗音乐 (`.kgm` / `.vpr`)
|
||||
- [x] 喜马拉雅 Android 端 (`.x2m` / `.x3m`)
|
||||
- [x] 喜马拉雅 (`.x2m` / `.x3m` / `.xm`)
|
||||
- [x] 咪咕音乐格式 (`.mg3d`)
|
||||
- [x] 蜻蜓 FM (`.qta`)
|
||||
- [ ] ~~<ruby>QQ 音乐海外版<rt>JOOX Music</rt></ruby> (`.ofl_en`)~~
|
||||
|
||||
[^qm-key-pc]: PC 客户端仅支持 v19.43 或更低版本。
|
||||
|
||||
[^qm-key-android]: 需要获取超级管理员权限后提取密钥数据库,并导入后使用。
|
||||
|
||||
[^qm-key-ios]: 需要越狱获取密钥数据库,或对设备进行完整备份后提取密钥数据库,并导入后使用。
|
||||
|
||||
[^qm-key-mac]: 需要导入密钥数据库。
|
||||
|
||||
不支持的格式?请提交样本(加密文件)与客户端信息(或一并上传其安装包)到[仓库的问题追踪区][project-issues]
|
||||
。如果文件太大,请上传到不需要登入下载的网盘,如 [mega.nz](https://mega.nz)、[OneDrive](https://www.onedrive.com/) 等。
|
||||
## 错误报告
|
||||
|
||||
如果遇到解密出错的情况,请一并携带错误信息并简单描述错误的重现过程。
|
||||
有不支持的格式?请提交样本(加密文件)与客户端信息版本信息(如系统版本、下载渠道),或一并上传其安装包到[仓库的问题追踪区][project-issues]。
|
||||
|
||||
⚠️ 如果文件太大,请上传到不需要登入下载的网盘,如 [mega.nz](https://mega.nz)、[OneDrive](https://www.onedrive.com/) 等。
|
||||
|
||||
遇到解密出错的情况,请一并携带错误信息(诊断信息)并简单描述错误的重现过程。
|
||||
|
||||
待实现的算法支持可[追踪 `crypto` 标签](https://git.unlock-music.dev/um/um-react/issues?labels=67)。
|
||||
|
||||
[project-issues]: https://git.unlock-music.dev/um/um-react/issues/new
|
||||
|
||||
@@ -55,7 +66,7 @@
|
||||
|
||||
⚠️ 如果只是进行前端方面的更改,你可以跳过该节。
|
||||
|
||||
请参考文档「[面向 `@unlock-music/crypto` 开发](./docs/develop-with-um_crypto.zh)」。
|
||||
请参考文档「[面向 `@unlock-music/crypto` 开发](./docs/develop-with-um_crypto.zh.md)」。
|
||||
|
||||
### 架构
|
||||
|
||||
@@ -87,13 +98,3 @@
|
||||
[webview2_redist]: https://go.microsoft.com/fwlink/p/?LinkId=2124703
|
||||
|
||||
有新的项目提交?欢迎[提交 issue][project-issues],请带上项目名称和链接。
|
||||
|
||||
## TODO
|
||||
|
||||
- 待定
|
||||
- [ ] 各类算法 [追踪 `crypto` 标签](https://git.unlock-music.dev/um/um-react/issues?labels=67)
|
||||
- 完成
|
||||
- [x] #7 ~~简易元数据编辑器~~ 放弃
|
||||
- [x] #8 ~~添加单元测试~~ 框架加上了,以后慢慢添加更多测试即可。
|
||||
- [x] #2 解密内容探测 (解密过程)
|
||||
- [x] #6 文件拖放 (利用 `react-dropzone`?)
|
||||
|
||||
82
package.json
82
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "um-react",
|
||||
"private": true,
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
@@ -14,66 +14,71 @@
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"preview": "vite preview",
|
||||
"preview:coverage": "vite preview --outDir coverage --port 5175",
|
||||
"prepare": "husky install"
|
||||
"prepare": "simple-git-hooks"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/anatomy": "^2.2.2",
|
||||
"@chakra-ui/icons": "^2.1.1",
|
||||
"@chakra-ui/react": "^2.8.2",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@unlock-music/crypto": "0.1.0",
|
||||
"framer-motion": "^11.5.6",
|
||||
"nanoid": "^5.0.7",
|
||||
"@chakra-ui/anatomy": "^2.3.4",
|
||||
"@chakra-ui/icons": "^2.2.4",
|
||||
"@chakra-ui/react": "^2.10.4",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@reduxjs/toolkit": "^2.5.0",
|
||||
"@unlock-music/crypto": "0.1.2",
|
||||
"framer-motion": "^11.14.4",
|
||||
"nanoid": "^5.0.9",
|
||||
"radash": "^12.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-promise-suspense": "^0.3.4",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"sass": "^1.79.3",
|
||||
"sql.js": "^1.11.0"
|
||||
"react-redux": "^9.2.0",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"sass": "^1.83.0",
|
||||
"sql.js": "^1.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-replace": "^6.0.1",
|
||||
"@testing-library/jest-dom": "^6.5.0",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/node": "^22.6.1",
|
||||
"@types/react": "^18.3.9",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^18.3.16",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/sql.js": "^1.4.9",
|
||||
"@typescript-eslint/eslint-plugin": "^8.7.0",
|
||||
"@typescript-eslint/parser": "^8.7.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"@vitest/coverage-v8": "^2.1.1",
|
||||
"@vitest/ui": "^2.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
||||
"@typescript-eslint/parser": "^8.18.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "^2.1.8",
|
||||
"@vitest/ui": "^2.1.8",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.12",
|
||||
"husky": "^9.1.6",
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^25.0.1",
|
||||
"lint-staged": "^15.2.10",
|
||||
"prettier": "^3.3.3",
|
||||
"rollup": "^4.22.4",
|
||||
"terser": "^5.33.0",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.7",
|
||||
"lint-staged": "^15.2.11",
|
||||
"prettier": "^3.4.2",
|
||||
"rollup": "^4.28.1",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"terser": "^5.37.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-pwa": "^0.20.5",
|
||||
"vite-plugin-top-level-await": "^1.4.4",
|
||||
"vite-plugin-wasm": "^3.3.0",
|
||||
"vitest": "^2.1.1",
|
||||
"workbox-window": "^7.1.0"
|
||||
"vitest": "^2.1.8",
|
||||
"workbox-window": "^7.3.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "prettier --write --ignore-unknown",
|
||||
"*.{js,jsx,ts,tsx}": "eslint --fix --report-unused-disable-directives --max-warnings 0"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm exec lint-staged",
|
||||
"pre-push": "pnpm test"
|
||||
},
|
||||
"prettier": {
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
@@ -88,5 +93,6 @@
|
||||
"rollup-plugin-terser": "npm:@rollup/plugin-terser@0.4.3",
|
||||
"sourcemap-codec": "npm:@jridgewell/sourcemap-codec@1.4.15"
|
||||
}
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4"
|
||||
}
|
||||
|
||||
5337
pnpm-lock.yaml
generated
5337
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -103,7 +103,7 @@ deploy_netlify() {
|
||||
|
||||
# For deployment, we care a bit less
|
||||
if [[ -n "${NETLIFY_API_KEY}" && -n "${NETLIFY_SITE_ID}" ]]; then
|
||||
echo "Deploy to netlify..."
|
||||
echo "Deploy to netlify (branch: ${BRANCH_NAME})..."
|
||||
deploy_netlify um-react-site.zip
|
||||
else
|
||||
echo "skip netlify deployment."
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Heading } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
export interface Header3Props {
|
||||
children: React.ReactNode;
|
||||
id?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Header3({ children, className, id }: Header3Props) {
|
||||
return (
|
||||
<Heading
|
||||
as="h3"
|
||||
id={id}
|
||||
className={className}
|
||||
pt={3}
|
||||
pb={1}
|
||||
borderBottom={'1px solid'}
|
||||
borderColor="gray.300"
|
||||
color="gray.800"
|
||||
size="lg"
|
||||
>
|
||||
{children}
|
||||
</Heading>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Heading } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
export interface Header4Props {
|
||||
children: React.ReactNode;
|
||||
id?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Header4({ children, className, id }: Header4Props) {
|
||||
return (
|
||||
<Heading as="h4" id={id} className={className} pt={3} pb={1} color="gray.700" size="md">
|
||||
{children}
|
||||
</Heading>
|
||||
);
|
||||
}
|
||||
42
src/components/HelpText/Headers.tsx
Normal file
42
src/components/HelpText/Headers.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Heading } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
export interface HeaderProps {
|
||||
children: React.ReactNode;
|
||||
id?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Header3({ children, className, id }: HeaderProps) {
|
||||
return (
|
||||
<Heading
|
||||
as="h3"
|
||||
id={id}
|
||||
className={className}
|
||||
pt={3}
|
||||
pb={1}
|
||||
borderBottom={'1px solid'}
|
||||
borderColor="gray.300"
|
||||
color="gray.800"
|
||||
size="lg"
|
||||
>
|
||||
{children}
|
||||
</Heading>
|
||||
);
|
||||
}
|
||||
|
||||
export function Header4({ children, className, id }: HeaderProps) {
|
||||
return (
|
||||
<Heading as="h4" id={id} className={className} pt={3} pb={1} color="gray.700" size="md">
|
||||
{children}
|
||||
</Heading>
|
||||
);
|
||||
}
|
||||
|
||||
export function Header5({ children, className, id }: HeaderProps) {
|
||||
return (
|
||||
<Heading as="h5" id={id} className={className} pt={3} pb={1} color="gray.700" size="sm">
|
||||
{children}
|
||||
</Heading>
|
||||
);
|
||||
}
|
||||
@@ -38,20 +38,32 @@ export class QQMusicV2Decipher implements DecipherInstance {
|
||||
this.cipherName = `QQMusic/QMC2(user_key=${+useUserKey})`;
|
||||
}
|
||||
|
||||
async decrypt(buffer: Uint8Array, options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> {
|
||||
parseFooter(buffer: Uint8Array): { size: number; ekey?: undefined | string } {
|
||||
const footer = QMCFooter.parse(buffer.subarray(buffer.byteLength - 1024));
|
||||
if (!footer) {
|
||||
|
||||
if (footer) {
|
||||
const { size, ekey } = footer;
|
||||
footer.free();
|
||||
return { size, ekey };
|
||||
}
|
||||
|
||||
// No footer, and we don't accept user key:
|
||||
if (!this.useUserKey) {
|
||||
throw new UnsupportedSourceFile('Not QMC2 File');
|
||||
}
|
||||
|
||||
const audioBuffer = buffer.slice(0, buffer.byteLength - footer.size);
|
||||
return { size: 0 };
|
||||
}
|
||||
|
||||
async decrypt(buffer: Uint8Array, options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> {
|
||||
const footer = this.parseFooter(buffer.subarray(buffer.byteLength - 1024));
|
||||
const ekey = this.useUserKey ? options.qmc2Key : footer.ekey;
|
||||
footer.free();
|
||||
if (!ekey) {
|
||||
throw new Error('EKey missing');
|
||||
throw new Error('EKey required');
|
||||
}
|
||||
|
||||
const qmc2 = new QMC2(ekey);
|
||||
const audioBuffer = buffer.slice(0, buffer.byteLength - footer.size);
|
||||
for (const [block, offset] of chunkBuffer(audioBuffer)) {
|
||||
qmc2.decrypt(block, offset);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export const toArrayBuffer = async (src: Blob | ArrayBuffer) => (src instanceof Blob ? await src.arrayBuffer() : src);
|
||||
export const toBlob = (src: Blob | ArrayBuffer) => (src instanceof Blob ? src : new Blob([src]));
|
||||
export const toArrayBuffer = async (src: Blob | ArrayBuffer | Uint8Array<ArrayBufferLike>) =>
|
||||
src instanceof Blob ? await src.arrayBuffer() : src;
|
||||
export const toBlob = (src: Blob | ArrayBuffer | Uint8Array<ArrayBufferLike>) =>
|
||||
src instanceof Blob ? src : new Blob([src]);
|
||||
|
||||
export function* chunkBuffer(buffer: Uint8Array, blockLen = 4096): Generator<[Uint8Array, number], void> {
|
||||
const len = buffer.byteLength;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Alert, AlertIcon, Container, Flex, List, ListItem, Text, chakra } from '@chakra-ui/react';
|
||||
import { Header4 } from '~/components/HelpText/Header4';
|
||||
import { Alert, AlertIcon, Container, Flex, List, ListItem, Text } from '@chakra-ui/react';
|
||||
import { Header4 } from '~/components/HelpText/Headers';
|
||||
import { VQuote } from '~/components/HelpText/VQuote';
|
||||
import { SegmentTryOfficialPlayer } from './SegmentTryOfficialPlayer';
|
||||
import { HiWord } from '~/components/HelpText/HiWord';
|
||||
@@ -15,9 +15,6 @@ export function KuwoFAQ() {
|
||||
<SegmentTryOfficialPlayer />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
<chakra.strong>2、检查您的平台</chakra.strong>
|
||||
</Text>
|
||||
<Text>
|
||||
日前,仅<HiWord>手机客户端</HiWord>下载的
|
||||
<VQuote>
|
||||
@@ -38,10 +35,10 @@ export function KuwoFAQ() {
|
||||
<Flex flexDir="column">
|
||||
<Text>安卓用户提取密钥需要 root 权限,或注入文件提供器。</Text>
|
||||
<Text>
|
||||
<strong>注意</strong>:已知部分第三方修改版会破坏密钥写入功能,导致无法正常导入密钥。
|
||||
<strong>注意</strong>:已知部分第三方修改版会破坏密钥写入功能,导致无法提取密钥。
|
||||
</Text>
|
||||
<Text>
|
||||
<strong>注意</strong>:项目组不提倡使用第三方修改版应用亦不会提供,使用前请自行评估风险。
|
||||
<strong>注意</strong>:项目组不提倡使用、也不提供第三方修改版。使用前请自行评估风险。
|
||||
</Text>
|
||||
</Flex>
|
||||
</Alert>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Alert, AlertIcon, Code, Container, Flex, Img, ListItem, Text, UnorderedList } from '@chakra-ui/react';
|
||||
import { ExtLink } from '~/components/ExtLink';
|
||||
import { Header4 } from '~/components/HelpText/Header4';
|
||||
import { Header4 } from '~/components/HelpText/Headers';
|
||||
import { VQuote } from '~/components/HelpText/VQuote';
|
||||
import { ProjectIssue } from '~/components/ProjectIssue';
|
||||
import LdPlayerSettingsScreen from './assets/ld_settings_misc.webp';
|
||||
@@ -63,7 +63,7 @@ export function OtherFAQ() {
|
||||
<AlertIcon />
|
||||
<Flex flexDir="column">
|
||||
<Text>
|
||||
<strong>注意</strong>:根据应用厂商的风控策略,使用模拟器登录的账号<strong>有可能会被封锁</strong>
|
||||
<strong>注意</strong>:根据应用的风控策略,使用模拟器登录的账号<strong>有可能会导致账号被封锁</strong>
|
||||
{';使用前请自行评估风险。'}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -1,63 +1,159 @@
|
||||
import { Alert, AlertIcon, Container, Flex, List, ListItem, Text, UnorderedList, chakra } from '@chakra-ui/react';
|
||||
import { Header4 } from '~/components/HelpText/Header4';
|
||||
import { Accordion, AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Box } from '@chakra-ui/react';
|
||||
import { Alert, AlertIcon, Container, Flex, ListItem, Text, UnorderedList } from '@chakra-ui/react';
|
||||
import { Header4 } from '~/components/HelpText/Headers';
|
||||
import { SegmentTryOfficialPlayer } from './SegmentTryOfficialPlayer';
|
||||
import { QMCv2QQMusicAllInstructions } from '~/features/settings/panels/QMCv2/QMCv2QQMusicAllInstructions';
|
||||
import { SegmentKeyImportInstructions } from './SegmentKeyImportInstructions';
|
||||
import { ExtLink } from '~/components/ExtLink';
|
||||
import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction';
|
||||
import { InstructionsIOS } from '~/features/settings/panels/QMCv2/InstructionsIOS';
|
||||
import { InstructionsMac } from '~/features/settings/panels/QMCv2/InstructionsMac';
|
||||
|
||||
export function QQMusicFAQ() {
|
||||
return (
|
||||
<>
|
||||
<Header4>解锁失败</Header4>
|
||||
<List spacing={2}>
|
||||
<ListItem>
|
||||
<SegmentTryOfficialPlayer />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
<chakra.strong>2、检查您的平台</chakra.strong>
|
||||
</Text>
|
||||
<Text>
|
||||
日前,仅 Windows 客户端 19.43 或更低版本下载的歌曲文件无需密钥,其余平台的官方正式版本均需要提取密钥。
|
||||
你可以通过下方的链接获取 QQ 音乐 Windows 客户端 v19.43 的安装程序:
|
||||
</Text>
|
||||
<UnorderedList pl={3}>
|
||||
<ListItem>
|
||||
<Text>
|
||||
<ExtLink href="https://dldir1v6.qq.com/music/clntupate/QQMusic_Setup_1943.exe">
|
||||
<code>qq.com</code> 官方下载地址(推荐)
|
||||
</ExtLink>
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
<ExtLink href="https://web.archive.org/web/2023/https://dldir1v6.qq.com/music/clntupate/QQMusic_Setup_1943.exe">
|
||||
<code>Archive.org</code> 存档
|
||||
</ExtLink>
|
||||
</Text>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
<SegmentTryOfficialPlayer />
|
||||
<Text>重复下载同一首的歌曲不重复扣下载配额,但是同一首歌的两个版本会重复扣下载配额,请仔细分辨。</Text>
|
||||
<Text>
|
||||
部分平台获取的加密文件未包含密钥。选择你<strong>下载文件时</strong>使用的客户端来查看说明。
|
||||
</Text>
|
||||
<Accordion allowToggle my={2}>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
Windows
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4}>
|
||||
<Text>
|
||||
目前 Windows 客户端 19.51 或更低版本下载的歌曲文件无需密钥,其余平台的官方正式版本均需要提取密钥。
|
||||
</Text>
|
||||
<Text>你可以通过下方的链接获取 QQ 音乐 Windows 客户端 v19.51 的安装程序:</Text>
|
||||
<UnorderedList pl={3}>
|
||||
<ListItem>
|
||||
<Text>
|
||||
<ExtLink href="https://dldir1v6.qq.com/music/clntupate/QQMusic_Setup_1951.exe">
|
||||
<code>qq.com</code> 官方下载地址(推荐)
|
||||
</ExtLink>
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
<ExtLink href="https://web.archive.org/web/2023/https://dldir1v6.qq.com/music/clntupate/QQMusic_Setup_1951.exe">
|
||||
<code>Archive.org</code> 存档
|
||||
</ExtLink>
|
||||
</Text>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
<Container p={2}>
|
||||
<Alert status="warning" borderRadius={5}>
|
||||
<AlertIcon />
|
||||
<Flex flexDir="column">
|
||||
<Text>iOS 用户提取歌曲困难,建议换用电脑操作;</Text>
|
||||
<Text>安卓用户提取密钥需要root,也建议用电脑操作。</Text>
|
||||
</Flex>
|
||||
</Alert>
|
||||
</Container>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
Mac
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4}>
|
||||
<Container p={2}>
|
||||
<Alert status="warning" borderRadius={5}>
|
||||
<AlertIcon />
|
||||
<Flex flexDir="column">
|
||||
<Text>Mac 需要降级到 8.8.0 或以下版本。</Text>
|
||||
<Text>
|
||||
<ExtLink href="https://web.archive.org/web/20230903/https://dldir1.qq.com/music/clntupate/mac/QQMusicMac_Mgr.dmg">
|
||||
<code>Archive.org</code> 存档
|
||||
</ExtLink>
|
||||
</Text>
|
||||
</Flex>
|
||||
</Alert>
|
||||
</Container>
|
||||
|
||||
<Container p={2} pt={0}>
|
||||
<Alert status="info" borderRadius={5}>
|
||||
<AlertIcon />
|
||||
重复下载同一首的歌曲不重复扣下载配额,但是同一首歌的两个版本会重复扣下载配额,请仔细分辨。
|
||||
</Alert>
|
||||
</Container>
|
||||
<SegmentKeyImportInstructions
|
||||
tab="QMCv2 密钥"
|
||||
keyInstructionText="查看密钥提取说明:"
|
||||
clientInstructions={
|
||||
<Box p={2}>
|
||||
<InstructionsMac />
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
<SegmentKeyImportInstructions tab="QMCv2 密钥" clientInstructions={<QMCv2QQMusicAllInstructions />} />
|
||||
</ListItem>
|
||||
</List>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
安卓 (Android)
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4}>
|
||||
<Container p={2}>
|
||||
<Alert status="warning" borderRadius={5}>
|
||||
<AlertIcon />
|
||||
<Flex flexDir="column">
|
||||
<Text>安卓提取密钥需要 root 特权,建议用电脑操作。</Text>
|
||||
</Flex>
|
||||
</Alert>
|
||||
</Container>
|
||||
|
||||
<Text>QQ 音乐官方版本需要提取密钥才能解密。</Text>
|
||||
<Text>
|
||||
你也可以尝试使用【QQ 音乐简洁版】或 OEM 定制版(如小米、魅族定制版)。简洁、定制版本目前不需要提取密钥。
|
||||
</Text>
|
||||
|
||||
<SegmentKeyImportInstructions
|
||||
tab="QMCv2 密钥"
|
||||
keyInstructionText="查看密钥提取说明:"
|
||||
clientInstructions={
|
||||
<Box p={2}>
|
||||
<AndroidADBPullInstruction dir="/data/data/com.tencent.qqmusic/databases" file="player_process_db" />
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
iOS (iPhone, iPad)
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4}>
|
||||
<Container p={2}>
|
||||
<Alert status="warning" borderRadius={5}>
|
||||
<AlertIcon />
|
||||
<Flex flexDir="column">
|
||||
<Text>iOS 用户提取歌曲困难,建议换用电脑操作;</Text>
|
||||
</Flex>
|
||||
</Alert>
|
||||
</Container>
|
||||
|
||||
<SegmentKeyImportInstructions
|
||||
tab="QMCv2 密钥"
|
||||
keyInstructionText="查看密钥提取说明:"
|
||||
clientInstructions={
|
||||
<Box p={2}>
|
||||
<InstructionsIOS />
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,9 +8,14 @@ import { MdFileUpload } from 'react-icons/md';
|
||||
export interface SegmentKeyImportInstructionsProps {
|
||||
clientInstructions: React.ReactNode;
|
||||
tab: string;
|
||||
keyInstructionText?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function SegmentKeyImportInstructions({ clientInstructions, tab }: SegmentKeyImportInstructionsProps) {
|
||||
export function SegmentKeyImportInstructions({
|
||||
clientInstructions,
|
||||
tab,
|
||||
keyInstructionText = '选择你的客户端平台来查看密钥提取说明:',
|
||||
}: SegmentKeyImportInstructionsProps) {
|
||||
return (
|
||||
<>
|
||||
<Text>导入密钥可以参考下面的步骤:</Text>
|
||||
@@ -33,7 +38,7 @@ export function SegmentKeyImportInstructions({ clientInstructions, tab }: Segmen
|
||||
</Flex>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>选择你的客户端平台来查看密钥提取说明:</Text>
|
||||
<Text>{keyInstructionText}</Text>
|
||||
<Tabs display="flex" flexDir="column" border="1px solid" borderColor="gray.300" borderRadius={5}>
|
||||
{clientInstructions}
|
||||
</Tabs>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Text, chakra } from '@chakra-ui/react';
|
||||
import { Alert, AlertIcon, Container } from '@chakra-ui/react';
|
||||
|
||||
export function SegmentTryOfficialPlayer() {
|
||||
return (
|
||||
<>
|
||||
<Text>
|
||||
<chakra.strong>1、请检查您的文件</chakra.strong>
|
||||
</Text>
|
||||
<Text>尝试用下载音乐的设备播放一次看看,如果官方客户端都无法播放,那解锁肯定会失败哦。</Text>
|
||||
</>
|
||||
<Container p={2} my={2} pt={0}>
|
||||
<Alert status="info" borderRadius={5}>
|
||||
<AlertIcon />
|
||||
尝试用下载音乐的设备播放一次看看,如果官方客户端都无法播放,那解锁肯定会失败哦。
|
||||
</Alert>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
chakra,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
chakra,
|
||||
Flex,
|
||||
HStack,
|
||||
Icon,
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
TabPanels,
|
||||
Tabs,
|
||||
Text,
|
||||
VStack,
|
||||
useBreakpointValue,
|
||||
useToast,
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
import { PanelQMCv2Key } from './panels/PanelQMCv2Key';
|
||||
import { useState } from 'react';
|
||||
@@ -145,7 +145,7 @@ export function Settings() {
|
||||
onClick={handleResetSettings}
|
||||
colorScheme="red"
|
||||
variant="ghost"
|
||||
title="放弃未储存的更改,将设定还原为储存前的状态。"
|
||||
title="放弃未储存的更改,将设定还原未储存前的状态。"
|
||||
aria-label="放弃未储存的更改"
|
||||
/>
|
||||
<Button onClick={handleApplySettings}>保存</Button>
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import { Heading, Text, Code, Kbd, OrderedList, ListItem } from '@chakra-ui/react';
|
||||
import { Heading, Text, Code, Kbd, OrderedList, ListItem, Link } from '@chakra-ui/react';
|
||||
import { FilePathBlock } from '~/components/FilePathBlock';
|
||||
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';
|
||||
|
||||
export function InstructionsMac() {
|
||||
return (
|
||||
<>
|
||||
<Text>Mac 客户端使用 mmkv 数据库储存密钥。</Text>
|
||||
<Text>
|
||||
{'此外,你需要降级到 '}
|
||||
<Link isExternal href={MAC_CLIENT_URL}>
|
||||
2023.09.03 版本的客户端
|
||||
</Link>
|
||||
{'。'}
|
||||
新版本对 mmkv 数据库进行了加密处理。
|
||||
</Text>
|
||||
<Text>该密钥文件通常存储在下述路径:</Text>
|
||||
<FilePathBlock>
|
||||
~/Library/Containers/com.tencent.QQMusicMac/Data/Library/Application Support/QQMusicMac/mmkv/MMKVStreamEncryptId
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Text } from '@chakra-ui/react';
|
||||
export function InstructionsPC() {
|
||||
return (
|
||||
<>
|
||||
<Text>使用 Windows 19.43 或更低版本下载的歌曲文件无需密钥。</Text>
|
||||
<Text>使用 Windows 19.51 或更高版本下载的歌曲文件需要导入密钥,但方法尚未公开。</Text>
|
||||
<Text>使用 Windows 19.51 或更低版本下载的歌曲文件无需密钥。</Text>
|
||||
<Text>使用 Windows 19.57 或更高版本下载的歌曲文件需要导入密钥,但方法尚未公开。</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Center, Container, Heading } from '@chakra-ui/react';
|
||||
import { Header3 } from '~/components/HelpText/Header3';
|
||||
import { Center, Container, Heading, Link, ListItem, UnorderedList } from '@chakra-ui/react';
|
||||
import { Header3 } from '~/components/HelpText/Headers';
|
||||
import { KuwoFAQ } from '~/faq/KuwoFAQ';
|
||||
import { OtherFAQ } from '~/faq/OtherFAQ';
|
||||
import { QQMusicFAQ } from '~/faq/QQMusicFAQ';
|
||||
@@ -10,11 +10,23 @@ export function FaqTab() {
|
||||
<Center>
|
||||
<Heading as="h2">常见问题解答</Heading>
|
||||
</Center>
|
||||
<Header3>QQ 音乐</Header3>
|
||||
<Header3>答疑目录</Header3>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="#faq-qqmusic">QQ 音乐</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="#faq-kuwo">酷我音乐</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="#faq-other">其它问题</Link>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
<Header3 id="faq-qqmusic">QQ 音乐</Header3>
|
||||
<QQMusicFAQ />
|
||||
<Header3>酷我音乐</Header3>
|
||||
<Header3 id="faq-kuwo">酷我音乐</Header3>
|
||||
<KuwoFAQ />
|
||||
<Header3>其它问题</Header3>
|
||||
<Header3 id="faq-other">其它问题</Header3>
|
||||
<OtherFAQ />
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
import { Box, VStack } from '@chakra-ui/react';
|
||||
import { Alert, AlertIcon, Box, Button, Flex, Text, VStack } from '@chakra-ui/react';
|
||||
import { SelectFile } from '../components/SelectFile';
|
||||
|
||||
import { FileListing } from '~/features/file-listing/FileListing';
|
||||
import { useAppDispatch, useAppSelector } from '~/hooks.ts';
|
||||
import { selectIsSettingsNotSaved } from '~/features/settings/settingsSelector.ts';
|
||||
import { commitStagingChange } from '~/features/settings/settingsSlice.ts';
|
||||
|
||||
export function MainTab() {
|
||||
const dispatch = useAppDispatch();
|
||||
const isSettingsNotSaved = useAppSelector(selectIsSettingsNotSaved);
|
||||
const onClickSaveSettings = () => {
|
||||
dispatch(commitStagingChange());
|
||||
};
|
||||
|
||||
return (
|
||||
<Box h="full" w="full" pt="4">
|
||||
<VStack gap="3">
|
||||
{isSettingsNotSaved && (
|
||||
<Alert borderRadius={7} maxW={400} status="warning">
|
||||
<AlertIcon />
|
||||
<Flex flexDir="row" alignItems="center" flexGrow={1} justifyContent="space-between">
|
||||
<Text m={0}>
|
||||
有尚未储存的设置,
|
||||
<br />
|
||||
设定将在保存后生效
|
||||
</Text>
|
||||
<Button type="button" ml={3} size="md" onClick={onClickSaveSettings}>
|
||||
立即储存
|
||||
</Button>
|
||||
</Flex>
|
||||
</Alert>
|
||||
)}
|
||||
<SelectFile />
|
||||
|
||||
<Box w="full">
|
||||
|
||||
@@ -101,7 +101,7 @@ test('it should move on to the next item in the queue once failed', async () =>
|
||||
await expect(badPromise).rejects.toThrowError('dummy error');
|
||||
|
||||
queuedResolver.forEach((resolve) => resolve());
|
||||
expect(Promise.all(promises)).resolves.toEqual([1, 2, 4, 5]);
|
||||
await expect(Promise.all(promises)).resolves.toEqual([1, 2, 4, 5]);
|
||||
} finally {
|
||||
vi.spyOn(queue, 'handler').mockRejectedValue(new Error('handler ran too late'));
|
||||
queuedResolver.forEach((resolve) => resolve());
|
||||
|
||||
Reference in New Issue
Block a user