mirror of
https://git.um-react.app/um/um-react.git
synced 2025-11-28 03:23:02 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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:
|
||||
|
||||
4
.npmrc
4
.npmrc
@@ -1,3 +1,3 @@
|
||||
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
|
||||
|
||||
38
README.MD
38
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,21 @@
|
||||
|
||||
满足上述条件后发起 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)
|
||||
|
||||
有新的项目提交?欢迎[提交 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 浏览器
|
||||
|
||||
可能会遇到的问题包括:
|
||||
|
||||
- 网页白屏
|
||||
- 无法下载解密后内容
|
||||
- 下载的文件名错误
|
||||
|
||||
### 新版解锁网站没有批量下载
|
||||
|
||||
目前没有做。抱歉。
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "um-react",
|
||||
"private": true,
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "tsc -p tsconfig.prod.json && vite build",
|
||||
"build": "tsc -p tsconfig.prod.json && vite build && node scripts/write-version.mjs",
|
||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"format": "prettier -w .",
|
||||
"test": "vitest run",
|
||||
@@ -21,10 +21,9 @@
|
||||
"@chakra-ui/react": "^2.8.2",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@jixun/libparakeet": "0.4.2",
|
||||
"@jixun/libparakeet": "0.4.3",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"framer-motion": "^10.16.16",
|
||||
"immer": "^10.0.3",
|
||||
"nanoid": "^5.0.4",
|
||||
"radash": "^11.0.0",
|
||||
"react": "^18.2.0",
|
||||
|
||||
11
pnpm-lock.yaml
generated
11
pnpm-lock.yaml
generated
@@ -33,17 +33,14 @@ dependencies:
|
||||
specifier: ^11.11.0
|
||||
version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.45)(react@18.2.0)
|
||||
'@jixun/libparakeet':
|
||||
specifier: 0.4.2
|
||||
version: 0.4.2
|
||||
specifier: 0.4.3
|
||||
version: 0.4.3
|
||||
'@reduxjs/toolkit':
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1(react-redux@9.0.4)(react@18.2.0)
|
||||
framer-motion:
|
||||
specifier: ^10.16.16
|
||||
version: 10.16.16(react-dom@18.2.0)(react@18.2.0)
|
||||
immer:
|
||||
specifier: ^10.0.3
|
||||
version: 10.0.3
|
||||
nanoid:
|
||||
specifier: ^5.0.4
|
||||
version: 5.0.4
|
||||
@@ -2885,8 +2882,8 @@ packages:
|
||||
'@sinclair/typebox': 0.27.8
|
||||
dev: true
|
||||
|
||||
/@jixun/libparakeet@0.4.2:
|
||||
resolution: {integrity: sha512-E6XXrHeOOIexKSyWUgQwHUpZNMh5I1IoC9gbwyVQjJhBLiURy6KU7U2Fgsg62Q3BSkdi7NwbdbPERrygUFmA7Q==}
|
||||
/@jixun/libparakeet@0.4.3:
|
||||
resolution: {integrity: sha512-Y+h65ZXbJ604sO1RyXA+2kx1WQoA6xJSlIIFWLcmAJpbj34XY6eCpyRXnltgkzcOLsLaO89jjGqMPS7MBC4XqA==}
|
||||
dev: false
|
||||
|
||||
/@jridgewell/gen-mapping@0.3.3:
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { 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;
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ import type { Parakeet } from '@jixun/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);
|
||||
|
||||
@@ -4,9 +4,11 @@ import { DECRYPTION_WORKER_ACTION_NAME } from './constants';
|
||||
import { getSDKVersion } from '@jixun/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);
|
||||
|
||||
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 '@jixun/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,95 @@ 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>
|
||||
</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 }));
|
||||
|
||||
@@ -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,43 +1,44 @@
|
||||
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) => {
|
||||
if (settings?.qmc2) {
|
||||
const { allowFuzzyNameSearch, keys } = settings.qmc2;
|
||||
for (const [k, v] of enumObject(keys)) {
|
||||
if (typeof v === 'string') {
|
||||
draft.qmc2.keys[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof allowFuzzyNameSearch === 'boolean') {
|
||||
draft.qmc2.allowFuzzyNameSearch = allowFuzzyNameSearch;
|
||||
const draft = deepClone(settingsSlice.getInitialState().production);
|
||||
if (settings?.qmc2) {
|
||||
const { allowFuzzyNameSearch, keys } = settings.qmc2;
|
||||
for (const [k, v] of enumObject(keys)) {
|
||||
if (typeof v === 'string') {
|
||||
draft.qmc2.keys[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
if (settings?.kwm2) {
|
||||
const { keys } = settings.kwm2;
|
||||
if (typeof allowFuzzyNameSearch === 'boolean') {
|
||||
draft.qmc2.allowFuzzyNameSearch = allowFuzzyNameSearch;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [k, v] of enumObject(keys)) {
|
||||
if (typeof v === 'string' && parseKwm2ProductionKey(k)) {
|
||||
draft.kwm2.keys[k] = v;
|
||||
}
|
||||
if (settings?.kwm2) {
|
||||
const { keys } = settings.kwm2;
|
||||
|
||||
for (const [k, v] of enumObject(keys)) {
|
||||
if (typeof v === 'string' && parseKwm2ProductionKey(k)) {
|
||||
draft.kwm2.keys[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof settings?.qtfm?.android === 'string') {
|
||||
draft.qtfm.android = settings.qtfm.android.replace(/[^0-9a-fA-F]/g, '');
|
||||
}
|
||||
});
|
||||
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));
|
||||
}
|
||||
@@ -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