mirror of
https://git.um-react.app/um/um-react.git
synced 2025-11-28 11:33:02 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1e17992e9 | ||
|
|
f478ca8818 | ||
|
|
8e4367fbf9 | ||
|
|
1ae2f93e99 | ||
|
|
741e302ea7 | ||
|
|
f09aa84984 | ||
|
|
1d2296a02a | ||
|
|
f6703160e7 | ||
|
|
bea2f4b7d4 | ||
|
|
0a3dac9d3d | ||
|
|
602d6865f5 | ||
|
|
8d4194772e | ||
|
|
1ef1db30ab | ||
|
|
80fc595833 | ||
|
|
be9a1b6724 | ||
|
|
6cccb722ce | ||
|
|
2f9cfaa763 | ||
|
|
5d7f5b76ef | ||
|
|
2d50a45ef2 | ||
|
|
fcc4b14211 | ||
|
|
6c21150fc8 | ||
|
|
e98470cb70 | ||
|
|
bb8f69f137 | ||
|
|
f194dfd135 | ||
|
|
7e741412a8 | ||
|
|
fe39ac6604 | ||
|
|
d781767dd0 | ||
|
|
a7158a75e9 | ||
|
|
2bd35f899d | ||
|
|
bb37da5066 | ||
|
|
27a91a67bb | ||
|
|
067ad6e40b | ||
|
|
c95bcd7eda | ||
|
|
caed717755 | ||
|
|
16bb7cb0fb |
10
.drone.yml
10
.drone.yml
@@ -5,7 +5,7 @@ name: default
|
||||
|
||||
steps:
|
||||
- name: test & build
|
||||
image: node:20.8.1-bookworm
|
||||
image: node:20.10.0-bookworm
|
||||
commands:
|
||||
# - git config --global --add safe.directory "/drone/src"
|
||||
- corepack enable
|
||||
@@ -17,7 +17,7 @@ steps:
|
||||
npm_config_registry: https://registry.npmmirror.com
|
||||
|
||||
- name: publish
|
||||
image: node:20.8.1-bookworm
|
||||
image: node:20.10.0-bookworm
|
||||
environment:
|
||||
DRONE_GITEA_SERVER: https://git.unlock-music.dev
|
||||
GITEA_API_KEY:
|
||||
@@ -27,7 +27,9 @@ steps:
|
||||
NETLIFY_API_KEY:
|
||||
from_secret: NETLIFY_API_KEY
|
||||
commands:
|
||||
# - git config --global --add safe.directory "/drone/src"
|
||||
- python3 -m zipfile -c um-react.zip dist/.
|
||||
- |
|
||||
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
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -27,3 +27,8 @@ dist-ssr
|
||||
# Files created when running "drone exec" locally
|
||||
/.pnpm-store/
|
||||
/*.zip
|
||||
|
||||
/um-react-wry-*
|
||||
/um-react*.exe
|
||||
|
||||
/win64/
|
||||
|
||||
5
.npmrc
5
.npmrc
@@ -1,3 +1,4 @@
|
||||
use-node-version=20.8.1
|
||||
node-version=20.8.1
|
||||
use-node-version=20.10.0
|
||||
node-version=20.10.0
|
||||
engine-strict=true
|
||||
@um:registry=https://git.unlock-music.dev/api/packages/um/npm/
|
||||
|
||||
42
README.MD
42
README.MD
@@ -16,21 +16,24 @@
|
||||
[`@unlock_music_chat`]: https://t.me/unlock_music_chat
|
||||
[um-react-packages]: https://git.unlock-music.dev/um/-/packages/generic/um-react/
|
||||
|
||||
⚠️ 手机端浏览器支持有限,请使用最新版本的 Chrome 或 Firefox 官方浏览器。
|
||||
|
||||
## 支持的格式
|
||||
|
||||
- [x] QQ 音乐 QMCv1 (..qmc3/.qmcflac 等)
|
||||
- [x] QQ 音乐 QMCv1 (`.qmc3` / `.qmcflac` 等)
|
||||
- [x] QQ 音乐 QMCv2
|
||||
- PC 客户端 (.mflac/.mgg 等) [^qm-key-pc]
|
||||
- 安卓客户端 (.mflac0/.mgg1/.mggl 等) [^qm-key-android]
|
||||
- iOS 客户端 (.mgalaxy 等) [^qm-key-ios]
|
||||
- Mac 客户端 (.mflach 等) [^qm-key-mac]
|
||||
- [x] 网易云音乐 (.ncm)
|
||||
- [x] 虾米音乐 (.xm)
|
||||
- [x] 酷我音乐 (.kwm)
|
||||
- [x] 酷狗音乐 (.kgm/.vpr)
|
||||
- [x] 喜马拉雅 Android 端 (.x2m/.x3m)
|
||||
- [x] 咪咕音乐格式 (.mg3d)
|
||||
- [ ] ~~<ruby>QQ 音乐海外版<rt>JOOX Music</rt></ruby> (.ofl_en)~~
|
||||
- PC 客户端 (`.mflac` / `.mgg` 等) [^qm-key-pc]
|
||||
- 安卓客户端 (`.mflac0` / `.mgg1` / `.mggl` 等) [^qm-key-android]
|
||||
- iOS 客户端 (`.mgalaxy` 等) [^qm-key-ios]
|
||||
- Mac 客户端 (`.mflach` 等) [^qm-key-mac]
|
||||
- [x] 网易云音乐 (`.ncm`)
|
||||
- [x] 虾米音乐 (`.xm`)
|
||||
- [x] 酷我音乐 (`.kwm`)
|
||||
- [x] 酷狗音乐 (`.kgm` / `.vpr`)
|
||||
- [x] 喜马拉雅 Android 端 (`.x2m` / `.x3m`)
|
||||
- [x] 咪咕音乐格式 (`.mg3d`)
|
||||
- [x] 蜻蜓 FM (`.qta`)
|
||||
- [ ] ~~<ruby>QQ 音乐海外版<rt>JOOX Music</rt></ruby> (`.ofl_en`)~~
|
||||
|
||||
[^qm-key-pc]: PC 客户端仅支持 v19.43 或更低版本。
|
||||
[^qm-key-android]: 需要获取超级管理员权限后提取密钥数据库,并导入后使用。
|
||||
@@ -70,12 +73,25 @@
|
||||
|
||||
满足上述条件后发起 Pull Request,仓库管理员审阅后将合并到主分支。
|
||||
|
||||
## 相关项目
|
||||
|
||||
- [Unlock Music (Web)](https://git.unlock-music.dev/um/web) - 原始项目
|
||||
- [Unlock Music (Cli)](https://git.unlock-music.dev/um/cli) - 命令行批量处理版
|
||||
- [um-react (Electron 前端)](https://github.com/CarlGao4/um-react-electron) - 使用 Electron 框架封装的本地可执行文件。
|
||||
- [GitHub 下载](https://github.com/CarlGao4/um-react-electron/releases/latest) | [仓库镜像](https://git.unlock-music.dev/CarlGao4/um-react-electron)
|
||||
- [um-react-wry](https://git.unlock-music.dev/um/um-react-wry) - 使用 WRY 框架封装的 Win64 单文件 (需要[安装 Edge WebView2 运行时][webview2_redist],Win10+ 操作系统自带)
|
||||
- [本地下载](https://git.unlock-music.dev/um/um-react/releases/latest) | 寻找文件名为 `um-react-win64-` 开头的附件
|
||||
|
||||
[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)
|
||||
- [ ] #7 简易元数据编辑器
|
||||
- 完成
|
||||
- [x] #7 ~~简易元数据编辑器~~ 放弃
|
||||
- [x] #8 ~~添加单元测试~~ 框架加上了,以后慢慢添加更多测试即可。
|
||||
- [x] #2 解密内容探测 (解密过程)
|
||||
- [x] #6 文件拖放 (利用 `react-dropzone`?)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
#### 2、检查您的平台。
|
||||
|
||||
日前,<mark>仅 Windows 客户端</mark>下载的歌曲无需密钥,其余平台的官方正式版本均需要提取密钥。
|
||||
日前,<mark>仅 Windows 客户端 v19.43 或以下版本</mark>下载的歌曲无需密钥,其余平台的官方正式版本均需要提取密钥。
|
||||
|
||||
> iOS 用户提取歌曲困难,建议换用电脑操作;Android 用户提取密钥需要 root,也建议用电脑操作。
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
|
||||
日前,<mark>仅手机客户端</mark>下载的歌曲**至臻全景声**及**至臻母带**为新版加密,手机平台的其他音质暂时不需要提取密钥,PC 平台暂未推出使用新版加密的音质。
|
||||
|
||||
※ 已知部分第三方修改版会破坏密钥写出功能,导致无法导入密钥。请使用官方版本。
|
||||
|
||||
> Android 用户提取密钥需要 root,或者注入文件提供器。
|
||||
|
||||
提取密钥教程请访问[新版解锁网站](https://um-react.netlify.app/),前往网站内的设置 →<mark>切换密钥为 KWMv2 密钥</mark>→“添加一条密钥”旁的<mark>**下拉按钮**</mark>→ 从文件导入密钥…→ 选择您对应的平台查看具体教程。
|
||||
@@ -54,6 +56,37 @@
|
||||
|
||||
目前新版没有做歌曲信息匹配与编辑,所以歌曲如果自己没有写入歌曲信息,解出来就是没有的。
|
||||
|
||||
### 安卓 root 相关
|
||||
|
||||
对安卓设备获取 root 特权通常会破坏系统的完整性并导致部分功能无法使用。
|
||||
例如部分厂商的安卓设备会在解锁后丧失保修资格,或导致无法使用 NFC 移动支付功能等限制。
|
||||
|
||||
如果希望不破坏系统完整性,你可以考虑使用模拟器。
|
||||
|
||||
※ **注意**:根据应用厂商的风控策略,使用模拟器登录的账号**有可能会被封锁**;使用前请自行评估风险。
|
||||
|
||||
目前常见的带有 root 特权支持的的安卓模拟器方案,分别是雷电模拟器(※ 官方版有内置广告)和微软在 Windows 11 开始支援的适用于 Android™ 的 Windows 子系统 (WSA)。
|
||||
|
||||
- WSA 可以参考 [MagiskOnWSALocal](https://github.com/LSPosed/MagiskOnWSALocal) 的说明操作。
|
||||
- 雷电模拟器可以在「模拟器设置」 → 「其他设置」中启用 root 特权。
|
||||

|
||||
|
||||
### Via 等浏览器无法正常解密/下载
|
||||
|
||||
⚠️ 手机端浏览器支持有限,请使用最新版本的 Chrome 或 Firefox 官方浏览器。
|
||||
|
||||
已知有问题的浏览器:
|
||||
|
||||
- Via 浏览器
|
||||
- 夸克浏览器
|
||||
- UC 浏览器
|
||||
|
||||
可能会遇到的问题包括:
|
||||
|
||||
- 网页白屏
|
||||
- 无法下载解密后内容
|
||||
- 下载的文件名错误
|
||||
|
||||
### 新版解锁网站没有批量下载
|
||||
|
||||
目前没有做。抱歉。
|
||||
|
||||
@@ -33,3 +33,31 @@ pnpm build
|
||||
如果需要预览构建版本,运行 `pnpm preview` 然后打开[项目预览页面][vite-preview-url]即可。
|
||||
|
||||
[vite-preview-url]: http://localhost:4173/
|
||||
|
||||
## 打包 `.zip`
|
||||
|
||||
建议在 Linux 环境下执行,可参考 `.drone.yml` CI 文件。
|
||||
|
||||
1. 确保上述的构建步骤已完成。
|
||||
2. 确保 `python3` 已安装。
|
||||
3. 执行下述代码
|
||||
```sh
|
||||
python3 -m zipfile -c um-react.zip dist/.
|
||||
```
|
||||
|
||||
## 打包 win64 单文件
|
||||
|
||||
利用 Windows 系统自带的 [Edge WebView2 组件](https://learn.microsoft.com/zh-cn/microsoft-edge/webview2/)
|
||||
和 [wry](https://github.com/tauri-apps/wry) 进行一个单文件的打包。
|
||||
|
||||
大部分 Windows 10 或以上版本的操作系统已经集成了 WebView2 运行时。若无法正常启动,请[下载并安装 Edge WebView2 运行时](https://go.microsoft.com/fwlink/p/?LinkId=2124703)。
|
||||
|
||||
其它系统兼容性未知。
|
||||
|
||||
1. 确保你现在在 `linux-amd64` 环境下。
|
||||
2. 确保上述的 `um-react.zip` 构建已完成。
|
||||
3. 执行下述代码
|
||||
```sh
|
||||
./scripts/make-win64.sh
|
||||
```
|
||||
4. 等待提示 `[Build OK]` 即可。
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "um-react",
|
||||
"private": true,
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.8",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "tsc -p tsconfig.prod.json && vite build",
|
||||
"build": "tsc -p tsconfig.prod.json && vite build && pnpm build:finalize",
|
||||
"build:finalize": "node scripts/write-version.mjs && node scripts/minify-mjs.mjs",
|
||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"format": "prettier -w .",
|
||||
"test": "vitest run",
|
||||
@@ -21,10 +22,9 @@
|
||||
"@chakra-ui/react": "^2.8.2",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@jixun/libparakeet": "0.4.2",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@um/libparakeet": "0.4.5",
|
||||
"framer-motion": "^10.16.16",
|
||||
"immer": "^10.0.3",
|
||||
"nanoid": "^5.0.4",
|
||||
"radash": "^11.0.0",
|
||||
"react": "^18.2.0",
|
||||
@@ -60,6 +60,7 @@
|
||||
"jsdom": "^23.0.1",
|
||||
"lint-staged": "^15.2.0",
|
||||
"prettier": "^3.1.1",
|
||||
"terser": "^5.27.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.10",
|
||||
"vite-plugin-pwa": "^0.17.4",
|
||||
|
||||
9085
pnpm-lock.yaml
generated
9085
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -104,7 +104,7 @@ deploy_netlify() {
|
||||
# For deployment, we care a bit less
|
||||
if [[ -n "${NETLIFY_API_KEY}" && -n "${NETLIFY_SITE_ID}" ]]; then
|
||||
echo "Deploy to netlify..."
|
||||
deploy_netlify um-react.zip
|
||||
deploy_netlify um-react-site.zip
|
||||
else
|
||||
echo "skip netlify deployment."
|
||||
fi
|
||||
|
||||
33
scripts/make-win64.sh
Executable file
33
scripts/make-win64.sh
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
|
||||
# sudo apt install -y jq zip
|
||||
|
||||
pushd "$(dirname "${BASH_SOURCE[0]}")/../"
|
||||
|
||||
WRY_VER="0.1.1"
|
||||
|
||||
mkdir -p win64/{deps,dist}
|
||||
dl_file() {
|
||||
local FILE="$1"
|
||||
if [[ ! -f "win64/deps/$FILE" ]]; then
|
||||
curl -fsL "https://um-react.app/files/${FILE}.gz" | gzip -d >"win64/deps/${FILE}"
|
||||
fi
|
||||
}
|
||||
|
||||
dl_file "um-react-wry-builder-${WRY_VER}-linux-amd64"
|
||||
dl_file "um-react-wry-stub-${WRY_VER}-win64.exe"
|
||||
chmod a+x win64/deps/um-react-wry-builder-${WRY_VER}-linux-amd64
|
||||
|
||||
APP_VERSION="$(jq -r '.version' <package.json)"
|
||||
EXE_NAME="um-react-win64-${APP_VERSION}.exe"
|
||||
ZIP_NAME="um-react-win64-${APP_VERSION}.zip"
|
||||
"./win64/deps/um-react-wry-builder-${WRY_VER}-linux-amd64" \
|
||||
-t "win64/deps/um-react-wry-stub-${WRY_VER}-win64.exe" \
|
||||
-r um-react.zip \
|
||||
-o "win64/dist/${EXE_NAME}"
|
||||
|
||||
touch -d 1970-01-01T00:00:00Z "win64/dist/${EXE_NAME}"
|
||||
zip -9oX "win64/dist/${ZIP_NAME}" -- "win64/dist/${EXE_NAME}"
|
||||
echo "[Build OK] 'win64/dist/${ZIP_NAME}'."
|
||||
|
||||
popd
|
||||
19
scripts/minify-mjs.mjs
Normal file
19
scripts/minify-mjs.mjs
Normal file
@@ -0,0 +1,19 @@
|
||||
import { minify } from 'terser';
|
||||
import { readFileSync, writeFileSync, readdirSync } from 'fs';
|
||||
|
||||
for (const file of readdirSync('dist/assets')) {
|
||||
if (!/\.(mjs|js)$/.test(file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`minifying ${file}...`);
|
||||
const isModule = /\.mjs$/.test(file);
|
||||
|
||||
const output = await minify(readFileSync(`dist/assets/${file}`, 'utf-8'), {
|
||||
compress: true,
|
||||
mangle: true,
|
||||
module: isModule,
|
||||
});
|
||||
|
||||
writeFileSync(`dist/assets/${file}`, output.code);
|
||||
}
|
||||
14
scripts/write-version.mjs
Normal file
14
scripts/write-version.mjs
Normal file
@@ -0,0 +1,14 @@
|
||||
/* eslint-env node */
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const commitHash = execSync('git rev-parse --short HEAD').toString('utf-8').trim();
|
||||
|
||||
const pkgJson = JSON.parse(readFileSync(__dirname + '/../package.json', 'utf-8'));
|
||||
const pkgVer = `${pkgJson.version ?? 'unknown'}-${commitHash ?? 'unknown'}` + '\n';
|
||||
writeFileSync(__dirname + '/../dist/version.txt', pkgVer, 'utf-8');
|
||||
@@ -1,5 +1,6 @@
|
||||
export enum DECRYPTION_WORKER_ACTION_NAME {
|
||||
DECRYPT = 'DECRYPT',
|
||||
FIND_QMC_MUSICEX_NAME = 'FIND_QMC_MUSICEX_NAME',
|
||||
VERSION = 'VERSION',
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { CryptoBase } from '../CryptoBase';
|
||||
import { KWM_KEY } from './kwm.key';
|
||||
import { DecryptCommandOptions } from '~/decrypt-worker/types';
|
||||
import { makeQMCv2KeyCrypto } from '~/decrypt-worker/util/qmc2KeyCrypto';
|
||||
import { fetchParakeet } from '@jixun/libparakeet';
|
||||
import { fetchParakeet } from '@um/libparakeet';
|
||||
import { stringToUTF8Bytes } from '~/decrypt-worker/util/utf8Encoder';
|
||||
|
||||
// v1 only
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
|
||||
import type { CryptoBase } from '../CryptoBase';
|
||||
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
|
||||
import { SEED, ENC_V2_KEY_1, ENC_V2_KEY_2 } from './qmc_v2.key.ts';
|
||||
import { fetchParakeet } from '@jixun/libparakeet';
|
||||
import { fetchParakeet } from '@um/libparakeet';
|
||||
import { stringToUTF8Bytes } from '~/decrypt-worker/util/utf8Encoder.ts';
|
||||
import { makeQMCv2KeyCrypto } from '~/decrypt-worker/util/qmc2KeyCrypto.ts';
|
||||
import { makeQMCv2FooterParser, makeQMCv2KeyCrypto } from '~/decrypt-worker/util/qmc2KeyCrypto.ts';
|
||||
|
||||
export class QMC2Crypto implements CryptoBase {
|
||||
cryptoName = 'QMC/v2';
|
||||
@@ -12,7 +11,7 @@ export class QMC2Crypto implements CryptoBase {
|
||||
|
||||
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
|
||||
const parakeet = await fetchParakeet();
|
||||
const footerParser = parakeet.make.QMCv2FooterParser(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2);
|
||||
const footerParser = makeQMCv2FooterParser(parakeet);
|
||||
return transformBlob(buffer, (p) => p.make.QMCv2(footerParser), {
|
||||
parakeet,
|
||||
cleanup: () => footerParser.delete(),
|
||||
|
||||
@@ -10,3 +10,8 @@ export interface DecryptCommandPayload {
|
||||
blobURI: string;
|
||||
options: DecryptCommandOptions;
|
||||
}
|
||||
|
||||
export interface FetchMusicExNamePayload {
|
||||
id: string;
|
||||
blobURI: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Parakeet } from '@jixun/libparakeet';
|
||||
import type { Parakeet } from '@um/libparakeet';
|
||||
import { SEED, ENC_V2_KEY_1, ENC_V2_KEY_2 } from '../crypto/qmc/qmc_v2.key';
|
||||
|
||||
export const makeQMCv2KeyCrypto = (p: Parakeet) => p.make.QMCv2KeyCrypto(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2);
|
||||
export const makeQMCv2FooterParser = (p: Parakeet) => p.make.QMCv2FooterParser(SEED, ENC_V2_KEY_1, ENC_V2_KEY_2);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Transformer, Parakeet, TransformResult, fetchParakeet } from '@jixun/libparakeet';
|
||||
import { Transformer, Parakeet, TransformResult, fetchParakeet } from '@um/libparakeet';
|
||||
import { toArrayBuffer } from './buffer';
|
||||
import { UnsupportedSourceFile } from './DecryptError';
|
||||
|
||||
export async function transformBlob(
|
||||
blob: Blob | ArrayBuffer,
|
||||
transformerFactory: (p: Parakeet) => Transformer | Promise<Transformer>,
|
||||
{ cleanup, parakeet }: { cleanup?: () => void; parakeet?: Parakeet } = {}
|
||||
{ cleanup, parakeet }: { cleanup?: () => void; parakeet?: Parakeet } = {},
|
||||
) {
|
||||
const registeredCleanupFns: (() => void)[] = [];
|
||||
if (cleanup) {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { WorkerServerBus } from '~/util/WorkerEventBus';
|
||||
import { DECRYPTION_WORKER_ACTION_NAME } from './constants';
|
||||
|
||||
import { getSDKVersion } from '@jixun/libparakeet';
|
||||
import { getSDKVersion } from '@um/libparakeet';
|
||||
|
||||
import { workerDecryptHandler } from './worker/handler/decrypt';
|
||||
import { workerParseMusicExMediaName } from './worker/handler/qmcv2_parser';
|
||||
|
||||
const bus = new WorkerServerBus();
|
||||
onmessage = bus.onmessage;
|
||||
|
||||
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.DECRYPT, workerDecryptHandler);
|
||||
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.FIND_QMC_MUSICEX_NAME, workerParseMusicExMediaName);
|
||||
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.VERSION, getSDKVersion);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Parakeet, fetchParakeet } from '@jixun/libparakeet';
|
||||
import { Parakeet, fetchParakeet } from '@um/libparakeet';
|
||||
import { timedLogger, withGroupedLogs as withTimeGroupedLogs } from '~/util/logUtils';
|
||||
import type { DecryptCommandOptions, DecryptCommandPayload } from '~/decrypt-worker/types';
|
||||
import { allCryptoFactories } from '../../crypto/CryptoFactory';
|
||||
|
||||
30
src/decrypt-worker/worker/handler/qmcv2_parser.ts
Normal file
30
src/decrypt-worker/worker/handler/qmcv2_parser.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { fetchParakeet, FooterParserState } from '@um/libparakeet';
|
||||
import type { FetchMusicExNamePayload } from '~/decrypt-worker/types';
|
||||
import { makeQMCv2FooterParser } from '~/decrypt-worker/util/qmc2KeyCrypto';
|
||||
import { timedLogger, withGroupedLogs as withTimeGroupedLogs } from '~/util/logUtils';
|
||||
|
||||
export const workerParseMusicExMediaName = async ({ id, blobURI }: FetchMusicExNamePayload) => {
|
||||
const label = `decrypt(${id})`;
|
||||
return withTimeGroupedLogs(label, async () => {
|
||||
const parakeet = await timedLogger(`${label}/init`, fetchParakeet);
|
||||
const blob = await timedLogger(`${label}/fetch-src`, async () =>
|
||||
fetch(blobURI, { headers: { Range: 'bytes=-1024' } }).then((r) => r.blob()),
|
||||
);
|
||||
|
||||
const buffer = await timedLogger(`${label}/read-src`, async () => {
|
||||
// Firefox: the range header does not work...?
|
||||
const blobBuffer = await blob.arrayBuffer();
|
||||
if (blobBuffer.byteLength > 1024) {
|
||||
return blobBuffer.slice(-1024);
|
||||
}
|
||||
return blobBuffer;
|
||||
});
|
||||
|
||||
const parsed = makeQMCv2FooterParser(parakeet).parse(buffer);
|
||||
if (parsed.state === FooterParserState.OK) {
|
||||
return parsed.mediaName;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
// This is a dummy module for vite/rollup to resolve.
|
||||
export function createRequire() {
|
||||
import('immer'); // we need to import something, so vite don't complain on build
|
||||
import('radash'); // we need to import something, so vite don't complain on build
|
||||
throw new Error('this is a dummy module. Do not use');
|
||||
}
|
||||
|
||||
@@ -37,7 +37,12 @@ export function KuwoFAQ() {
|
||||
<AlertIcon />
|
||||
<Flex flexDir="column">
|
||||
<Text>安卓用户提取密钥需要 root 权限,或注入文件提供器。</Text>
|
||||
<Text>请注意:项目组不提倡使用第三方修改版应用亦不会提供,使用前请自行评估风险。</Text>
|
||||
<Text>
|
||||
<strong>注意</strong>:已知部分第三方修改版会破坏密钥写入功能,导致无法正常导入密钥。
|
||||
</Text>
|
||||
<Text>
|
||||
<strong>注意</strong>:项目组不提倡使用第三方修改版应用亦不会提供,使用前请自行评估风险。
|
||||
</Text>
|
||||
</Flex>
|
||||
</Alert>
|
||||
</Container>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Text } from '@chakra-ui/react';
|
||||
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 { VQuote } from '~/components/HelpText/VQuote';
|
||||
import { ProjectIssue } from '~/components/ProjectIssue';
|
||||
import LdPlayerSettingsScreen from './assets/ld_settings_misc.webp';
|
||||
|
||||
export function OtherFAQ() {
|
||||
return (
|
||||
@@ -9,11 +11,123 @@ export function OtherFAQ() {
|
||||
<Header4>解密后没有封面等信息</Header4>
|
||||
<Text>该项目进行解密处理。如果加密前的资源没有内嵌元信息或封面,解密的文件也没有。</Text>
|
||||
<Text>请使用第三方工具进行编辑或管理元信息。</Text>
|
||||
<Header4>如何批量下载</Header4>
|
||||
|
||||
<Header4>批量下载</Header4>
|
||||
<Text>
|
||||
暂时没有实现,不过你可以在 <ProjectIssue id={34} title="[UI] 全部下载功能" /> 以及{' '}
|
||||
<ProjectIssue id={43} title="批量下载" /> 追踪该问题。
|
||||
{'暂时没有实现,不过你可以在 '}
|
||||
<ProjectIssue id={34} title="[UI] 全部下载功能" />
|
||||
{' 以及 '}
|
||||
<ProjectIssue id={43} title="批量下载" />
|
||||
{' 追踪该问题。'}
|
||||
</Text>
|
||||
|
||||
<Header4>安卓: 浏览器支持说明</Header4>
|
||||
<Text>⚠️ 手机端浏览器支持有限,请使用最新版本的 Chrome 或 Firefox 官方浏览器。</Text>
|
||||
<Text>已知有问题的浏览器:</Text>
|
||||
<UnorderedList>
|
||||
<ListItem>Via 浏览器</ListItem>
|
||||
<ListItem>夸克浏览器</ListItem>
|
||||
<ListItem>UC 浏览器</ListItem>
|
||||
</UnorderedList>
|
||||
<Text>可能会遇到的问题包括:</Text>
|
||||
<UnorderedList>
|
||||
<ListItem>网页白屏</ListItem>
|
||||
<ListItem>无法下载解密后内容</ListItem>
|
||||
<ListItem>下载的文件名错误</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<Header4>安卓: root 相关说明</Header4>
|
||||
<Text>
|
||||
对安卓设备获取 root 特权通常会破坏系统的完整性并导致部分功能无法使用。
|
||||
例如部分厂商的安卓设备会在解锁后丧失保修资格,或导致无法使用 NFC 移动支付功能等限制。
|
||||
</Text>
|
||||
<Text>如果希望不破坏系统完整性,你可以考虑使用模拟器。</Text>
|
||||
<Text>
|
||||
目前常见的带有 root 特权支持的的安卓模拟器方案,分别是雷电模拟器(※ 官方版有内置广告)和微软在 Windows 11
|
||||
开始支援的
|
||||
<ExtLink href="https://learn.microsoft.com/zh-cn/windows/android/wsa/">
|
||||
<ruby>
|
||||
适用于 Android™ 的 Windows 子系统 (WSA)
|
||||
<rp> (</rp>
|
||||
<rt>
|
||||
<code>Windows Subsystem for Android</code>
|
||||
</rt>
|
||||
<rp>)</rp>
|
||||
</ruby>
|
||||
</ExtLink>
|
||||
。
|
||||
</Text>
|
||||
|
||||
<Container p={2}>
|
||||
<Alert status="warning" borderRadius={5}>
|
||||
<AlertIcon />
|
||||
<Flex flexDir="column">
|
||||
<Text>
|
||||
<strong>注意</strong>:根据应用厂商的风控策略,使用模拟器登录的账号<strong>有可能会被封锁</strong>
|
||||
{';使用前请自行评估风险。'}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Alert>
|
||||
</Container>
|
||||
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Text>
|
||||
{'WSA 可以参考 '}
|
||||
<ExtLink href="https://github.com/LSPosed/MagiskOnWSALocal">MagiskOnWSALocal</ExtLink>
|
||||
{' 的说明操作。'}
|
||||
</Text>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
雷电模拟器可以在<VQuote>模拟器设置</VQuote> → <VQuote>其他设置</VQuote>中启用 root 特权。
|
||||
</Text>
|
||||
<Img borderRadius={5} border="1px solid #ccc" src={LdPlayerSettingsScreen}></Img>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<Header4>相关项目</Header4>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Text>
|
||||
<ExtLink href="https://github.com/CarlGao4/um-react-electron">
|
||||
<strong>
|
||||
<Code>um-react-electron</Code>
|
||||
</strong>
|
||||
</ExtLink>
|
||||
:利用 Electron 框架打包的本地版,提供适用于 Windows、Linux 和 Mac 平台的可执行文件。
|
||||
</Text>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Text>
|
||||
<ExtLink href="https://github.com/CarlGao4/um-react-electron/releases/latest">GitHub 下载</ExtLink>
|
||||
</Text>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text>
|
||||
<ExtLink href="https://git.unlock-music.dev/um/um-react-wry">
|
||||
<strong>
|
||||
<Code>um-react-wry</Code>
|
||||
</strong>
|
||||
</ExtLink>
|
||||
: 使用 WRY 框架封装的 Win64 单文件(需要
|
||||
<ExtLink href="https://go.microsoft.com/fwlink/p/?LinkId=2124703">安装 Edge WebView2 运行时</ExtLink>
|
||||
{',Win10+ 操作系统自带)'}
|
||||
</Text>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Text>
|
||||
<ExtLink href="https://git.unlock-music.dev/um/um-react/releases/latest">仓库下载</ExtLink>
|
||||
{' | 寻找文件名为 '}
|
||||
<Code>um-react-win64-</Code> 开头的附件
|
||||
</Text>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<Header4>有更多问题?</Header4>
|
||||
<Text>
|
||||
{'欢迎进入 '}
|
||||
|
||||
BIN
src/faq/assets/ld_settings_misc.webp
Normal file
BIN
src/faq/assets/ld_settings_misc.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
@@ -2,9 +2,9 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { RootState } from '~/store';
|
||||
|
||||
import type { DecryptionResult } from '~/decrypt-worker/constants';
|
||||
import type { DecryptCommandOptions } from '~/decrypt-worker/types';
|
||||
import { decryptionQueue } from '~/decrypt-worker/client';
|
||||
import { DECRYPTION_WORKER_ACTION_NAME, type DecryptionResult } from '~/decrypt-worker/constants';
|
||||
import type { DecryptCommandOptions, FetchMusicExNamePayload } from '~/decrypt-worker/types';
|
||||
import { decryptionQueue, workerClientBus } from '~/decrypt-worker/client';
|
||||
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
|
||||
import { selectQMCv2KeyByFileName, selectKWMv2Key, selectQtfmAndroidKey } from '../settings/settingsSelector';
|
||||
|
||||
@@ -44,7 +44,7 @@ export interface FileListingState {
|
||||
displayMode: ListingMode;
|
||||
}
|
||||
const initialState: FileListingState = {
|
||||
files: Object.create(null),
|
||||
files: {},
|
||||
displayMode: ListingMode.LIST,
|
||||
};
|
||||
|
||||
@@ -64,17 +64,27 @@ export const processFile = createAsyncThunk<
|
||||
thunkAPI.dispatch(setFileAsProcessing({ id: fileId }));
|
||||
};
|
||||
|
||||
const fileHeader = await fetch(file.raw, {
|
||||
headers: {
|
||||
Range: 'bytes=0-1023',
|
||||
},
|
||||
})
|
||||
const fileHeader = await fetch(file.raw, { headers: { Range: 'bytes=0-1023' } })
|
||||
.then((r) => r.blob())
|
||||
.then((r) => r.arrayBuffer());
|
||||
.then((r) => r.arrayBuffer())
|
||||
.then((r) => {
|
||||
if (r.byteLength > 1024) {
|
||||
return r.slice(0, 1024);
|
||||
}
|
||||
return r;
|
||||
});
|
||||
|
||||
const qmcv2MusicExMediaFile = await workerClientBus.request<string, FetchMusicExNamePayload>(
|
||||
DECRYPTION_WORKER_ACTION_NAME.FIND_QMC_MUSICEX_NAME,
|
||||
{
|
||||
id: fileId,
|
||||
blobURI: file.raw,
|
||||
},
|
||||
);
|
||||
|
||||
const options: DecryptCommandOptions = {
|
||||
fileName: file.fileName,
|
||||
qmc2Key: selectQMCv2KeyByFileName(state, file.fileName),
|
||||
qmc2Key: selectQMCv2KeyByFileName(state, qmcv2MusicExMediaFile || file.fileName),
|
||||
kwm2key: selectKWMv2Key(state, new DataView(fileHeader)),
|
||||
qingTingAndroidKey: selectQtfmAndroidKey(state),
|
||||
};
|
||||
|
||||
@@ -61,7 +61,7 @@ export function PanelQMCv2Key() {
|
||||
alert(`不是支持的 SQLite 数据库文件。`);
|
||||
return;
|
||||
}
|
||||
} else if (/MMKVStreamEncryptId|filenameEkeyMap/i.test(file.name)) {
|
||||
} else if (/MMKVStreamEncryptId|filenameEkeyMap|qmpc-mmkv-v1/i.test(file.name)) {
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
const map = parseAndroidQmEKey(new DataView(fileBuffer));
|
||||
qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey }));
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '~/hooks';
|
||||
import { fetchParakeet } from '@jixun/libparakeet';
|
||||
import { fetchParakeet } from '@um/libparakeet';
|
||||
import { ExtLink } from '~/components/ExtLink';
|
||||
import { ChangeEvent, ClipboardEvent } from 'react';
|
||||
import { VQuote } from '~/components/HelpText/VQuote';
|
||||
|
||||
@@ -3,7 +3,8 @@ import { Text } from '@chakra-ui/react';
|
||||
export function InstructionsPC() {
|
||||
return (
|
||||
<>
|
||||
<Text>使用 Windows 客户端下载的文件不需要导入密钥。</Text>
|
||||
<Text>使用 Windows 19.43 或更低版本下载的歌曲文件无需密钥。</Text>
|
||||
<Text>使用 Windows 19.51 或更高版本下载的歌曲文件需要导入密钥,但方法尚未公开。</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { debounce } from 'radash';
|
||||
import { produce } from 'immer';
|
||||
|
||||
import type { AppStore } from '~/store';
|
||||
import { settingsSlice, setProductionChanges, ProductionSettings } from './settingsSlice';
|
||||
import { enumObject } from '~/util/objects';
|
||||
import { getLogger } from '~/util/logUtils';
|
||||
import { parseKwm2ProductionKey } from './keyFormats';
|
||||
import { deepClone } from '~/util/deepClone';
|
||||
|
||||
const DEFAULT_STORAGE_KEY = 'um-react-settings';
|
||||
|
||||
function mergeSettings(settings: ProductionSettings): ProductionSettings {
|
||||
return produce(settingsSlice.getInitialState().production, (draft) => {
|
||||
const draft = deepClone(settingsSlice.getInitialState().production);
|
||||
if (settings?.qmc2) {
|
||||
const { allowFuzzyNameSearch, keys } = settings.qmc2;
|
||||
for (const [k, v] of enumObject(keys)) {
|
||||
@@ -37,7 +37,8 @@ function mergeSettings(settings: ProductionSettings): ProductionSettings {
|
||||
if (typeof settings?.qtfm?.android === 'string') {
|
||||
draft.qtfm.android = settings.qtfm.android.replace(/[^0-9a-fA-F]/g, '');
|
||||
}
|
||||
});
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
||||
export function persistSettings(store: AppStore, storageKey = DEFAULT_STORAGE_KEY) {
|
||||
|
||||
10
src/theme.ts
10
src/theme.ts
@@ -31,6 +31,16 @@ export const theme = extendTheme({
|
||||
color: 'blue.600',
|
||||
},
|
||||
},
|
||||
Text: {
|
||||
baseStyle: {
|
||||
mt: 1,
|
||||
},
|
||||
},
|
||||
Header: {
|
||||
baseStyle: {
|
||||
mt: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
styles: {
|
||||
global: {
|
||||
|
||||
3
src/util/deepClone.ts
Normal file
3
src/util/deepClone.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function deepClone<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export default defineConfig({
|
||||
},
|
||||
base: './',
|
||||
optimizeDeps: {
|
||||
exclude: ['@jixun/libparakeet', 'sql.js'],
|
||||
exclude: ['@um/libparakeet', 'sql.js'],
|
||||
},
|
||||
plugins: [
|
||||
replace({
|
||||
@@ -57,7 +57,7 @@ export default defineConfig({
|
||||
registerType: 'prompt',
|
||||
workbox: {
|
||||
// Cache everything from dist
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,wasm}'],
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,wasm,webp}'],
|
||||
},
|
||||
manifest: {
|
||||
display: 'standalone',
|
||||
@@ -98,7 +98,7 @@ export default defineConfig({
|
||||
reacts: ['react', 'react-dom', 'react-dropzone', 'react-promise-suspense', 'react-redux', '@reduxjs/toolkit'],
|
||||
chakra: ['@chakra-ui/react', '@emotion/react', '@emotion/styled', 'framer-motion'],
|
||||
icons: ['react-icons', '@chakra-ui/icons'],
|
||||
utility: ['radash', 'nanoid', 'immer', 'react-syntax-highlighter'],
|
||||
utility: ['radash', 'nanoid', 'react-syntax-highlighter'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user