mirror of
https://git.um-react.app/um/um-react.git
synced 2025-11-28 11:33:02 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d50a45ef2 | ||
|
|
fcc4b14211 | ||
|
|
6c21150fc8 | ||
|
|
e98470cb70 | ||
|
|
bb8f69f137 | ||
|
|
f194dfd135 | ||
|
|
7e741412a8 | ||
|
|
fe39ac6604 | ||
|
|
d781767dd0 | ||
|
|
a7158a75e9 | ||
|
|
2bd35f899d | ||
|
|
bb37da5066 | ||
|
|
27a91a67bb | ||
|
|
067ad6e40b | ||
|
|
c95bcd7eda | ||
|
|
caed717755 | ||
|
|
16bb7cb0fb |
@@ -5,7 +5,7 @@ name: default
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: test & build
|
- name: test & build
|
||||||
image: node:20.8.1-bookworm
|
image: node:20.10.0-bookworm
|
||||||
commands:
|
commands:
|
||||||
# - git config --global --add safe.directory "/drone/src"
|
# - git config --global --add safe.directory "/drone/src"
|
||||||
- corepack enable
|
- corepack enable
|
||||||
@@ -17,7 +17,7 @@ steps:
|
|||||||
npm_config_registry: https://registry.npmmirror.com
|
npm_config_registry: https://registry.npmmirror.com
|
||||||
|
|
||||||
- name: publish
|
- name: publish
|
||||||
image: node:20.8.1-bookworm
|
image: node:20.10.0-bookworm
|
||||||
environment:
|
environment:
|
||||||
DRONE_GITEA_SERVER: https://git.unlock-music.dev
|
DRONE_GITEA_SERVER: https://git.unlock-music.dev
|
||||||
GITEA_API_KEY:
|
GITEA_API_KEY:
|
||||||
|
|||||||
4
.npmrc
4
.npmrc
@@ -1,3 +1,3 @@
|
|||||||
use-node-version=20.8.1
|
use-node-version=20.10.0
|
||||||
node-version=20.8.1
|
node-version=20.10.0
|
||||||
engine-strict=true
|
engine-strict=true
|
||||||
|
|||||||
27
README.MD
27
README.MD
@@ -16,21 +16,24 @@
|
|||||||
[`@unlock_music_chat`]: https://t.me/unlock_music_chat
|
[`@unlock_music_chat`]: https://t.me/unlock_music_chat
|
||||||
[um-react-packages]: https://git.unlock-music.dev/um/-/packages/generic/um-react/
|
[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
|
- [x] QQ 音乐 QMCv2
|
||||||
- PC 客户端 (.mflac/.mgg 等) [^qm-key-pc]
|
- PC 客户端 (`.mflac` / `.mgg` 等) [^qm-key-pc]
|
||||||
- 安卓客户端 (.mflac0/.mgg1/.mggl 等) [^qm-key-android]
|
- 安卓客户端 (`.mflac0` / `.mgg1` / `.mggl` 等) [^qm-key-android]
|
||||||
- iOS 客户端 (.mgalaxy 等) [^qm-key-ios]
|
- iOS 客户端 (`.mgalaxy` 等) [^qm-key-ios]
|
||||||
- Mac 客户端 (.mflach 等) [^qm-key-mac]
|
- Mac 客户端 (`.mflach` 等) [^qm-key-mac]
|
||||||
- [x] 网易云音乐 (.ncm)
|
- [x] 网易云音乐 (`.ncm`)
|
||||||
- [x] 虾米音乐 (.xm)
|
- [x] 虾米音乐 (`.xm`)
|
||||||
- [x] 酷我音乐 (.kwm)
|
- [x] 酷我音乐 (`.kwm`)
|
||||||
- [x] 酷狗音乐 (.kgm/.vpr)
|
- [x] 酷狗音乐 (`.kgm` / `.vpr`)
|
||||||
- [x] 喜马拉雅 Android 端 (.x2m/.x3m)
|
- [x] 喜马拉雅 Android 端 (`.x2m` / `.x3m`)
|
||||||
- [x] 咪咕音乐格式 (.mg3d)
|
- [x] 咪咕音乐格式 (`.mg3d`)
|
||||||
- [ ] ~~<ruby>QQ 音乐海外版<rt>JOOX Music</rt></ruby> (.ofl_en)~~
|
- [x] 蜻蜓 FM (`.qta`)
|
||||||
|
- [ ] ~~<ruby>QQ 音乐海外版<rt>JOOX Music</rt></ruby> (`.ofl_en`)~~
|
||||||
|
|
||||||
[^qm-key-pc]: PC 客户端仅支持 v19.43 或更低版本。
|
[^qm-key-pc]: PC 客户端仅支持 v19.43 或更低版本。
|
||||||
[^qm-key-android]: 需要获取超级管理员权限后提取密钥数据库,并导入后使用。
|
[^qm-key-android]: 需要获取超级管理员权限后提取密钥数据库,并导入后使用。
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
#### 2、检查您的平台。
|
#### 2、检查您的平台。
|
||||||
|
|
||||||
日前,<mark>仅 Windows 客户端</mark>下载的歌曲无需密钥,其余平台的官方正式版本均需要提取密钥。
|
日前,<mark>仅 Windows 客户端 v19.43 或以下版本</mark>下载的歌曲无需密钥,其余平台的官方正式版本均需要提取密钥。
|
||||||
|
|
||||||
> iOS 用户提取歌曲困难,建议换用电脑操作;Android 用户提取密钥需要 root,也建议用电脑操作。
|
> iOS 用户提取歌曲困难,建议换用电脑操作;Android 用户提取密钥需要 root,也建议用电脑操作。
|
||||||
|
|
||||||
@@ -34,6 +34,8 @@
|
|||||||
|
|
||||||
日前,<mark>仅手机客户端</mark>下载的歌曲**至臻全景声**及**至臻母带**为新版加密,手机平台的其他音质暂时不需要提取密钥,PC 平台暂未推出使用新版加密的音质。
|
日前,<mark>仅手机客户端</mark>下载的歌曲**至臻全景声**及**至臻母带**为新版加密,手机平台的其他音质暂时不需要提取密钥,PC 平台暂未推出使用新版加密的音质。
|
||||||
|
|
||||||
|
※ 已知部分第三方修改版会破坏密钥写出功能,导致无法导入密钥。请使用官方版本。
|
||||||
|
|
||||||
> Android 用户提取密钥需要 root,或者注入文件提供器。
|
> Android 用户提取密钥需要 root,或者注入文件提供器。
|
||||||
|
|
||||||
提取密钥教程请访问[新版解锁网站](https://um-react.netlify.app/),前往网站内的设置 →<mark>切换密钥为 KWMv2 密钥</mark>→“添加一条密钥”旁的<mark>**下拉按钮**</mark>→ 从文件导入密钥…→ 选择您对应的平台查看具体教程。
|
提取密钥教程请访问[新版解锁网站](https://um-react.netlify.app/),前往网站内的设置 →<mark>切换密钥为 KWMv2 密钥</mark>→“添加一条密钥”旁的<mark>**下拉按钮**</mark>→ 从文件导入密钥…→ 选择您对应的平台查看具体教程。
|
||||||
@@ -54,6 +56,35 @@
|
|||||||
|
|
||||||
目前新版没有做歌曲信息匹配与编辑,所以歌曲如果自己没有写入歌曲信息,解出来就是没有的。
|
目前新版没有做歌曲信息匹配与编辑,所以歌曲如果自己没有写入歌曲信息,解出来就是没有的。
|
||||||
|
|
||||||
|
### 安卓 root 相关
|
||||||
|
|
||||||
|
对安卓设备获取 root 特权通常会破坏系统的完整性并导致部分功能无法使用。
|
||||||
|
例如部分厂商的安卓设备会在解锁后丧失保修资格,或导致无法使用 NFC 移动支付功能等限制。
|
||||||
|
|
||||||
|
如果希望不破坏系统完整性,你可以考虑使用模拟器。
|
||||||
|
|
||||||
|
目前常见的带有 root 特权支持的的安卓模拟器方案,分别是雷电模拟器(※ 官方版有内置广告)和微软在 Windows 11 开始支援的适用于 Android™ 的 Windows 子系统 (WSA)。
|
||||||
|
|
||||||
|
- WSA 可以参考 [MagiskOnWSALocal](https://github.com/LSPosed/MagiskOnWSALocal) 的说明操作。
|
||||||
|
- 雷电模拟器可以在「模拟器设置」 → 「其他设置」中启用 root 特权。
|
||||||
|

|
||||||
|
|
||||||
|
### Via 等浏览器无法正常解密/下载
|
||||||
|
|
||||||
|
⚠️ 手机端浏览器支持有限,请使用最新版本的 Chrome 或 Firefox 官方浏览器。
|
||||||
|
|
||||||
|
已知有问题的浏览器:
|
||||||
|
|
||||||
|
- Via 浏览器
|
||||||
|
- 夸克浏览器
|
||||||
|
- UC 浏览器
|
||||||
|
|
||||||
|
可能会遇到的问题包括:
|
||||||
|
|
||||||
|
- 网页白屏
|
||||||
|
- 无法下载解密后内容
|
||||||
|
- 下载的文件名错误
|
||||||
|
|
||||||
### 新版解锁网站没有批量下载
|
### 新版解锁网站没有批量下载
|
||||||
|
|
||||||
目前没有做。抱歉。
|
目前没有做。抱歉。
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "um-react",
|
"name": "um-react",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.2",
|
"version": "0.2.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
@@ -21,10 +21,9 @@
|
|||||||
"@chakra-ui/react": "^2.8.2",
|
"@chakra-ui/react": "^2.8.2",
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@jixun/libparakeet": "0.4.2",
|
"@jixun/libparakeet": "0.4.3",
|
||||||
"@reduxjs/toolkit": "^2.0.1",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"framer-motion": "^10.16.16",
|
"framer-motion": "^10.16.16",
|
||||||
"immer": "^10.0.3",
|
|
||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
"radash": "^11.0.0",
|
"radash": "^11.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
11
pnpm-lock.yaml
generated
11
pnpm-lock.yaml
generated
@@ -33,17 +33,14 @@ dependencies:
|
|||||||
specifier: ^11.11.0
|
specifier: ^11.11.0
|
||||||
version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.45)(react@18.2.0)
|
version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.45)(react@18.2.0)
|
||||||
'@jixun/libparakeet':
|
'@jixun/libparakeet':
|
||||||
specifier: 0.4.2
|
specifier: 0.4.3
|
||||||
version: 0.4.2
|
version: 0.4.3
|
||||||
'@reduxjs/toolkit':
|
'@reduxjs/toolkit':
|
||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.0.1(react-redux@9.0.4)(react@18.2.0)
|
version: 2.0.1(react-redux@9.0.4)(react@18.2.0)
|
||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^10.16.16
|
specifier: ^10.16.16
|
||||||
version: 10.16.16(react-dom@18.2.0)(react@18.2.0)
|
version: 10.16.16(react-dom@18.2.0)(react@18.2.0)
|
||||||
immer:
|
|
||||||
specifier: ^10.0.3
|
|
||||||
version: 10.0.3
|
|
||||||
nanoid:
|
nanoid:
|
||||||
specifier: ^5.0.4
|
specifier: ^5.0.4
|
||||||
version: 5.0.4
|
version: 5.0.4
|
||||||
@@ -2885,8 +2882,8 @@ packages:
|
|||||||
'@sinclair/typebox': 0.27.8
|
'@sinclair/typebox': 0.27.8
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@jixun/libparakeet@0.4.2:
|
/@jixun/libparakeet@0.4.3:
|
||||||
resolution: {integrity: sha512-E6XXrHeOOIexKSyWUgQwHUpZNMh5I1IoC9gbwyVQjJhBLiURy6KU7U2Fgsg62Q3BSkdi7NwbdbPERrygUFmA7Q==}
|
resolution: {integrity: sha512-Y+h65ZXbJ604sO1RyXA+2kx1WQoA6xJSlIIFWLcmAJpbj34XY6eCpyRXnltgkzcOLsLaO89jjGqMPS7MBC4XqA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@jridgewell/gen-mapping@0.3.3:
|
/@jridgewell/gen-mapping@0.3.3:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export enum DECRYPTION_WORKER_ACTION_NAME {
|
export enum DECRYPTION_WORKER_ACTION_NAME {
|
||||||
DECRYPT = 'DECRYPT',
|
DECRYPT = 'DECRYPT',
|
||||||
|
FIND_QMC_MUSICEX_NAME = 'FIND_QMC_MUSICEX_NAME',
|
||||||
VERSION = 'VERSION',
|
VERSION = 'VERSION',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
|
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
|
||||||
import type { CryptoBase } from '../CryptoBase';
|
import type { CryptoBase } from '../CryptoBase';
|
||||||
import type { DecryptCommandOptions } from '~/decrypt-worker/types.ts';
|
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 '@jixun/libparakeet';
|
||||||
import { stringToUTF8Bytes } from '~/decrypt-worker/util/utf8Encoder.ts';
|
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 {
|
export class QMC2Crypto implements CryptoBase {
|
||||||
cryptoName = 'QMC/v2';
|
cryptoName = 'QMC/v2';
|
||||||
@@ -12,7 +11,7 @@ export class QMC2Crypto implements CryptoBase {
|
|||||||
|
|
||||||
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
|
async decrypt(buffer: ArrayBuffer): Promise<Blob> {
|
||||||
const parakeet = await fetchParakeet();
|
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), {
|
return transformBlob(buffer, (p) => p.make.QMCv2(footerParser), {
|
||||||
parakeet,
|
parakeet,
|
||||||
cleanup: () => footerParser.delete(),
|
cleanup: () => footerParser.delete(),
|
||||||
|
|||||||
@@ -10,3 +10,8 @@ export interface DecryptCommandPayload {
|
|||||||
blobURI: string;
|
blobURI: string;
|
||||||
options: DecryptCommandOptions;
|
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';
|
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 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 { getSDKVersion } from '@jixun/libparakeet';
|
||||||
|
|
||||||
import { workerDecryptHandler } from './worker/handler/decrypt';
|
import { workerDecryptHandler } from './worker/handler/decrypt';
|
||||||
|
import { workerParseMusicExMediaName } from './worker/handler/qmcv2_parser';
|
||||||
|
|
||||||
const bus = new WorkerServerBus();
|
const bus = new WorkerServerBus();
|
||||||
onmessage = bus.onmessage;
|
onmessage = bus.onmessage;
|
||||||
|
|
||||||
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.DECRYPT, workerDecryptHandler);
|
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);
|
bus.addEventHandler(DECRYPTION_WORKER_ACTION_NAME.VERSION, getSDKVersion);
|
||||||
|
|||||||
27
src/decrypt-worker/worker/handler/qmcv2_parser.ts
Normal file
27
src/decrypt-worker/worker/handler/qmcv2_parser.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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 '# N/A';
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// This is a dummy module for vite/rollup to resolve.
|
// This is a dummy module for vite/rollup to resolve.
|
||||||
export function createRequire() {
|
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');
|
throw new Error('this is a dummy module. Do not use');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,12 @@ export function KuwoFAQ() {
|
|||||||
<AlertIcon />
|
<AlertIcon />
|
||||||
<Flex flexDir="column">
|
<Flex flexDir="column">
|
||||||
<Text>安卓用户提取密钥需要 root 权限,或注入文件提供器。</Text>
|
<Text>安卓用户提取密钥需要 root 权限,或注入文件提供器。</Text>
|
||||||
<Text>请注意:项目组不提倡使用第三方修改版应用亦不会提供,使用前请自行评估风险。</Text>
|
<Text>
|
||||||
|
<strong>注意</strong>:已知部分第三方修改版会破坏密钥写入功能,导致无法正常导入密钥。
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
<strong>注意</strong>:项目组不提倡使用第三方修改版应用亦不会提供,使用前请自行评估风险。
|
||||||
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Alert>
|
</Alert>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Text } from '@chakra-ui/react';
|
import { Img, ListItem, Text, UnorderedList } from '@chakra-ui/react';
|
||||||
import { ExtLink } from '~/components/ExtLink';
|
import { ExtLink } from '~/components/ExtLink';
|
||||||
import { Header4 } from '~/components/HelpText/Header4';
|
import { Header4 } from '~/components/HelpText/Header4';
|
||||||
|
import { VQuote } from '~/components/HelpText/VQuote';
|
||||||
import { ProjectIssue } from '~/components/ProjectIssue';
|
import { ProjectIssue } from '~/components/ProjectIssue';
|
||||||
|
import LdPlayerSettingsScreen from './assets/ld_settings_misc.webp';
|
||||||
|
|
||||||
export function OtherFAQ() {
|
export function OtherFAQ() {
|
||||||
return (
|
return (
|
||||||
@@ -9,11 +11,68 @@ export function OtherFAQ() {
|
|||||||
<Header4>解密后没有封面等信息</Header4>
|
<Header4>解密后没有封面等信息</Header4>
|
||||||
<Text>该项目进行解密处理。如果加密前的资源没有内嵌元信息或封面,解密的文件也没有。</Text>
|
<Text>该项目进行解密处理。如果加密前的资源没有内嵌元信息或封面,解密的文件也没有。</Text>
|
||||||
<Text>请使用第三方工具进行编辑或管理元信息。</Text>
|
<Text>请使用第三方工具进行编辑或管理元信息。</Text>
|
||||||
<Header4>如何批量下载</Header4>
|
|
||||||
|
<Header4>批量下载</Header4>
|
||||||
<Text>
|
<Text>
|
||||||
暂时没有实现,不过你可以在 <ProjectIssue id={34} title="[UI] 全部下载功能" /> 以及{' '}
|
{'暂时没有实现,不过你可以在 '}
|
||||||
<ProjectIssue id={43} title="批量下载" /> 追踪该问题。
|
<ProjectIssue id={34} title="[UI] 全部下载功能" />
|
||||||
|
{' 以及 '}
|
||||||
|
<ProjectIssue id={43} title="批量下载" />
|
||||||
|
{' 追踪该问题。'}
|
||||||
</Text>
|
</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>
|
||||||
|
<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>
|
<Header4>有更多问题?</Header4>
|
||||||
<Text>
|
<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 { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import type { RootState } from '~/store';
|
import type { RootState } from '~/store';
|
||||||
|
|
||||||
import type { DecryptionResult } from '~/decrypt-worker/constants';
|
import { DECRYPTION_WORKER_ACTION_NAME, type DecryptionResult } from '~/decrypt-worker/constants';
|
||||||
import type { DecryptCommandOptions } from '~/decrypt-worker/types';
|
import type { DecryptCommandOptions, FetchMusicExNamePayload } from '~/decrypt-worker/types';
|
||||||
import { decryptionQueue } from '~/decrypt-worker/client';
|
import { decryptionQueue, workerClientBus } from '~/decrypt-worker/client';
|
||||||
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
|
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
|
||||||
import { selectQMCv2KeyByFileName, selectKWMv2Key, selectQtfmAndroidKey } from '../settings/settingsSelector';
|
import { selectQMCv2KeyByFileName, selectKWMv2Key, selectQtfmAndroidKey } from '../settings/settingsSelector';
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ export interface FileListingState {
|
|||||||
displayMode: ListingMode;
|
displayMode: ListingMode;
|
||||||
}
|
}
|
||||||
const initialState: FileListingState = {
|
const initialState: FileListingState = {
|
||||||
files: Object.create(null),
|
files: {},
|
||||||
displayMode: ListingMode.LIST,
|
displayMode: ListingMode.LIST,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,17 +64,27 @@ export const processFile = createAsyncThunk<
|
|||||||
thunkAPI.dispatch(setFileAsProcessing({ id: fileId }));
|
thunkAPI.dispatch(setFileAsProcessing({ id: fileId }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileHeader = await fetch(file.raw, {
|
const fileHeader = await fetch(file.raw, { headers: { Range: 'bytes=0-1023' } })
|
||||||
headers: {
|
|
||||||
Range: 'bytes=0-1023',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((r) => r.blob())
|
.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 = {
|
const options: DecryptCommandOptions = {
|
||||||
fileName: file.fileName,
|
fileName: file.fileName,
|
||||||
qmc2Key: selectQMCv2KeyByFileName(state, file.fileName),
|
qmc2Key: selectQMCv2KeyByFileName(state, qmcv2MusicExMediaFile || file.fileName),
|
||||||
kwm2key: selectKWMv2Key(state, new DataView(fileHeader)),
|
kwm2key: selectKWMv2Key(state, new DataView(fileHeader)),
|
||||||
qingTingAndroidKey: selectQtfmAndroidKey(state),
|
qingTingAndroidKey: selectQtfmAndroidKey(state),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function PanelQMCv2Key() {
|
|||||||
alert(`不是支持的 SQLite 数据库文件。`);
|
alert(`不是支持的 SQLite 数据库文件。`);
|
||||||
return;
|
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 fileBuffer = await file.arrayBuffer();
|
||||||
const map = parseAndroidQmEKey(new DataView(fileBuffer));
|
const map = parseAndroidQmEKey(new DataView(fileBuffer));
|
||||||
qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey }));
|
qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey }));
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { Text } from '@chakra-ui/react';
|
|||||||
export function InstructionsPC() {
|
export function InstructionsPC() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Text>使用 Windows 客户端下载的文件不需要导入密钥。</Text>
|
<Text>使用 Windows 19.43 或更低版本下载的歌曲文件无需密钥。</Text>
|
||||||
|
<Text>使用 Windows 19.51 或更高版本下载的歌曲文件需要导入密钥,但方法尚未公开。</Text>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,44 @@
|
|||||||
import { debounce } from 'radash';
|
import { debounce } from 'radash';
|
||||||
import { produce } from 'immer';
|
|
||||||
|
|
||||||
import type { AppStore } from '~/store';
|
import type { AppStore } from '~/store';
|
||||||
import { settingsSlice, setProductionChanges, ProductionSettings } from './settingsSlice';
|
import { settingsSlice, setProductionChanges, ProductionSettings } from './settingsSlice';
|
||||||
import { enumObject } from '~/util/objects';
|
import { enumObject } from '~/util/objects';
|
||||||
import { getLogger } from '~/util/logUtils';
|
import { getLogger } from '~/util/logUtils';
|
||||||
import { parseKwm2ProductionKey } from './keyFormats';
|
import { parseKwm2ProductionKey } from './keyFormats';
|
||||||
|
import { deepClone } from '~/util/deepClone';
|
||||||
|
|
||||||
const DEFAULT_STORAGE_KEY = 'um-react-settings';
|
const DEFAULT_STORAGE_KEY = 'um-react-settings';
|
||||||
|
|
||||||
function mergeSettings(settings: ProductionSettings): ProductionSettings {
|
function mergeSettings(settings: ProductionSettings): ProductionSettings {
|
||||||
return produce(settingsSlice.getInitialState().production, (draft) => {
|
const draft = deepClone(settingsSlice.getInitialState().production);
|
||||||
if (settings?.qmc2) {
|
if (settings?.qmc2) {
|
||||||
const { allowFuzzyNameSearch, keys } = settings.qmc2;
|
const { allowFuzzyNameSearch, keys } = settings.qmc2;
|
||||||
for (const [k, v] of enumObject(keys)) {
|
for (const [k, v] of enumObject(keys)) {
|
||||||
if (typeof v === 'string') {
|
if (typeof v === 'string') {
|
||||||
draft.qmc2.keys[k] = v;
|
draft.qmc2.keys[k] = v;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof allowFuzzyNameSearch === 'boolean') {
|
|
||||||
draft.qmc2.allowFuzzyNameSearch = allowFuzzyNameSearch;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings?.kwm2) {
|
if (typeof allowFuzzyNameSearch === 'boolean') {
|
||||||
const { keys } = settings.kwm2;
|
draft.qmc2.allowFuzzyNameSearch = allowFuzzyNameSearch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [k, v] of enumObject(keys)) {
|
if (settings?.kwm2) {
|
||||||
if (typeof v === 'string' && parseKwm2ProductionKey(k)) {
|
const { keys } = settings.kwm2;
|
||||||
draft.kwm2.keys[k] = v;
|
|
||||||
}
|
for (const [k, v] of enumObject(keys)) {
|
||||||
|
if (typeof v === 'string' && parseKwm2ProductionKey(k)) {
|
||||||
|
draft.kwm2.keys[k] = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof settings?.qtfm?.android === 'string') {
|
if (typeof settings?.qtfm?.android === 'string') {
|
||||||
draft.qtfm.android = settings.qtfm.android.replace(/[^0-9a-fA-F]/g, '');
|
draft.qtfm.android = settings.qtfm.android.replace(/[^0-9a-fA-F]/g, '');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
return draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function persistSettings(store: AppStore, storageKey = DEFAULT_STORAGE_KEY) {
|
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',
|
color: 'blue.600',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Text: {
|
||||||
|
baseStyle: {
|
||||||
|
mt: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Header: {
|
||||||
|
baseStyle: {
|
||||||
|
mt: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
styles: {
|
styles: {
|
||||||
global: {
|
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',
|
registerType: 'prompt',
|
||||||
workbox: {
|
workbox: {
|
||||||
// Cache everything from dist
|
// Cache everything from dist
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,wasm}'],
|
globPatterns: ['**/*.{js,css,html,ico,png,svg,wasm,webp}'],
|
||||||
},
|
},
|
||||||
manifest: {
|
manifest: {
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
@@ -98,7 +98,7 @@ export default defineConfig({
|
|||||||
reacts: ['react', 'react-dom', 'react-dropzone', 'react-promise-suspense', 'react-redux', '@reduxjs/toolkit'],
|
reacts: ['react', 'react-dom', 'react-dropzone', 'react-promise-suspense', 'react-redux', '@reduxjs/toolkit'],
|
||||||
chakra: ['@chakra-ui/react', '@emotion/react', '@emotion/styled', 'framer-motion'],
|
chakra: ['@chakra-ui/react', '@emotion/react', '@emotion/styled', 'framer-motion'],
|
||||||
icons: ['react-icons', '@chakra-ui/icons'],
|
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