20 Commits

Author SHA1 Message Date
鲁树人
4fe6efec1f 0.5.2 2025-09-08 21:31:34 +09:00
鲁树人
c4fe9ce938 fix: update dependencies 2025-09-08 21:31:34 +09:00
鲁树人
045bea8084 chore: log which decipher was used 2025-09-08 20:47:13 +09:00
鲁树人
c68195eb9a feat: add instructions to block update for qqmusic mac 8.8.0 2025-09-05 22:15:15 +09:00
鲁树人
b986a2ef99 0.5.1 2025-09-03 21:32:27 +09:00
鲁树人
b48d9b0079 chore: update major versions 2025-09-03 21:32:16 +09:00
鲁树人
62e49804a5 chore: update deps 2025-09-03 21:27:38 +09:00
鲁树人
c41e5ae531 build: sort out deps 2025-09-03 21:21:01 +09:00
鲁树人
27c33a7d20 fix: replace link to new git repo 2025-09-03 21:19:37 +09:00
鲁树人
bbb557eafd Merge pull request 'fix(downloadAll): 一点点修改' (#92) from awalol/um-react:fix-downloadall into main
Reviewed-on: https://git.unlock-music.dev/um/um-react/pulls/92
2025-07-16 10:05:52 +00:00
awalol
befe35e5bc ui(DownloadAll): button position 2025-07-15 18:33:37 +00:00
awalol
1fb6526cdb docs: update faq 2025-07-15 18:33:37 +00:00
awalol
d122eaecf5 refactor(DownloadAll): 调整按钮位置 2025-07-15 18:33:37 +00:00
awalol
2da766168c refactor(DownloadAll): 并行下载 2025-07-15 18:33:37 +00:00
awalol
17200150dd feat(DownloadAll): 并行下载 2025-07-15 18:33:37 +00:00
awalol
2c461df5fc fix(downloadAll): 防止未解密文件导致的下载失败 优化目录选择框的弹出 优化权限请求 2025-07-15 18:33:37 +00:00
鲁树人
b493391371 Merge pull request '为解密文件添加 MIME 类型' (#95) from awalol/um-react:fix-mime into main
Reviewed-on: https://git.unlock-music.dev/um/um-react/pulls/95
2025-07-14 11:27:00 +00:00
awalol
13b67f40aa refactor(MIME): add getMimeTypeFromExt function 2025-07-14 01:51:28 +08:00
awalol
fbe8ef8ba1 fix: add MIME type for the decrypted file 2025-07-11 23:40:31 +08:00
鲁树人
54f784d778 ci: fix wry build 2025-07-05 06:45:43 +09:00
30 changed files with 1403 additions and 1393 deletions

View File

@@ -20,8 +20,13 @@ body:
目前 Mac 客户端仅支持 v8.8.0 或更低版本下载的歌曲文件。 目前 Mac 客户端仅支持 v8.8.0 或更低版本下载的歌曲文件。
* [web.archive.org 镜像](https://web.archive.org/web/20230903/https://dldir1.qq.com/music/clntupate/mac/QQMusicMac_Mgr.dmg)
* [通过 Telegram 下载](https://t.me/um_lsr_ch/21) * [通过 Telegram 下载](https://t.me/um_lsr_ch/21)
安装好客户端后可以加装更新屏蔽更新:
* [屏蔽更新](https://t.me/um_lsr_ch/29)
--- ---
如果你确定你的客户端版本符合上述描述,并遇到了问题,请继续填写下面的表单。 如果你确定你的客户端版本符合上述描述,并遇到了问题,请继续填写下面的表单。

4
.npmrc
View File

@@ -1,3 +1,3 @@
use-node-version=22.12.0 use-node-version=24.7.0
engine-strict=true engine-strict=true
@unlock-music:registry=https://git.unlock-music.dev/api/packages/um/npm/ @unlock-music:registry=https://git.um-react.app/api/packages/um/npm/

View File

@@ -1,6 +1,6 @@
# Unlock Music 音乐解锁 (React) # Unlock Music 音乐解锁 (React)
[![Build Status](https://git.unlock-music.dev/um/um-react/actions/workflows/build.yaml/badge.svg)][um-react-actions] [![Build Status](https://git.um-react.app/um/um-react/actions/workflows/build.yaml/badge.svg)][um-react-actions]
- 在浏览器中解锁加密的音乐文件。 Unlock encrypted music file in the browser. - 在浏览器中解锁加密的音乐文件。 Unlock encrypted music file in the browser.
- 查看[原基于 Vue 的 Unlock Music 项目][um-vue] - 查看[原基于 Vue 的 Unlock Music 项目][um-vue]
@@ -13,11 +13,11 @@
> **WARNING** > **WARNING**
> 在本站 fork 不会起到备份的作用,只会浪费服务器储存空间。如无必要请勿 fork 该仓库。 > 在本站 fork 不会起到备份的作用,只会浪费服务器储存空间。如无必要请勿 fork 该仓库。
[授权协议]: https://git.unlock-music.dev/um/um-react/src/branch/main/LICENSE [授权协议]: https://git.um-react.app/um/um-react/src/branch/main/LICENSE
[um-vue]: https://git.unlock-music.dev/um/web [um-vue]: https://git.um-react.app/um/web
[unlock-music/cli]: https://git.unlock-music.dev/um/cli [unlock-music/cli]: https://git.um-react.app/um/cli
[`@unlock_music_chat`]: https://t.me/unlock_music_chat [`@unlock_music_chat`]: https://t.me/unlock_music_chat
[um-react-actions]: https://git.unlock-music.dev/um/um-react/actions?workflow=build.yaml [um-react-actions]: https://git.um-react.app/um/um-react/actions?workflow=build.yaml
⚠️ 手机端浏览器支持有限,请使用最新版本的 Chrome 或 Firefox 官方浏览器。 ⚠️ 手机端浏览器支持有限,请使用最新版本的 Chrome 或 Firefox 官方浏览器。
@@ -55,16 +55,16 @@
遇到解密出错的情况,请一并携带错误信息(诊断信息)并简单描述错误的重现过程。 遇到解密出错的情况,请一并携带错误信息(诊断信息)并简单描述错误的重现过程。
待实现的算法支持可[追踪 `crypto` 标签](https://git.unlock-music.dev/um/um-react/issues?labels=67)。 待实现的算法支持可[追踪 `crypto` 标签](https://git.um-react.app/um/um-react/issues?labels=67)。
[project-issues]: https://git.unlock-music.dev/um/um-react/issues/new [project-issues]: https://git.um-react.app/um/um-react/issues/new
## 使用 Docker 构建、部署 (Linux) ## 使用 Docker 构建、部署 (Linux)
首先克隆仓库并进入目录: 首先克隆仓库并进入目录:
```sh ```sh
git clone https://git.unlock-music.dev/um/um-react.git git clone https://git.um-react.app/um/um-react.git
cd um-react cd um-react
``` ```
@@ -115,15 +115,15 @@ docker run -d -p 8080:80 --name um-react um-react
## 相关项目 ## 相关项目
- [Unlock Music (Web)](https://git.unlock-music.dev/um/web) - 原始项目 - [Unlock Music (Web)](https://git.um-react.app/um/web) - 原始项目
- [Unlock Music (Cli)](https://git.unlock-music.dev/um/cli) - 命令行批量处理版 - [Unlock Music (Cli)](https://git.um-react.app/um/cli) - 命令行批量处理版
- [lib_um_crypto_rust](https://git.unlock-music.dev/um/lib_um_crypto_rust) - 项目引入的解密算法实现 - [lib_um_crypto_rust](https://git.um-react.app/um/lib_um_crypto_rust) - 项目引入的解密算法实现
- [NPM 包](https://git.unlock-music.dev/um/-/packages/npm/@unlock-music%2Fcrypto) - [NPM 包](https://git.um-react.app/um/-/packages/npm/@unlock-music%2Fcrypto)
- [um-react (Electron 前端)](https://github.com/CarlGao4/um-react-electron) - 使用 Electron 框架封装的本地可执行文件。 - [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) - [GitHub 下载](https://github.com/CarlGao4/um-react-electron/releases/latest)
- [um-react-wry](https://git.unlock-music.dev/um/um-react-wry) - 使用 WRY 框架封装的 Win64 单文件 ( - [um-react-wry](https://git.um-react.app/um/um-react-wry) - 使用 WRY 框架封装的 Win64 单文件 (
需要[安装 Edge WebView2 运行时][webview2_redist]Win10+ 操作系统自带) 需要[安装 Edge WebView2 运行时][webview2_redist]Win10+ 操作系统自带)
- [本地下载](https://git.unlock-music.dev/um/um-react/releases/latest) | 寻找文件名为 `um-react-win64-` 开头的附件 - [本地下载](https://git.um-react.app/um/um-react/releases/latest) | 寻找文件名为 `um-react-win64-` 开头的附件
[webview2_redist]: https://go.microsoft.com/fwlink/p/?LinkId=2124703 [webview2_redist]: https://go.microsoft.com/fwlink/p/?LinkId=2124703

View File

@@ -16,7 +16,7 @@
- 进入上层目录:`cd ..` - 进入上层目录:`cd ..`
- 克隆 `lib_um_crypto_rust` 仓库 - 克隆 `lib_um_crypto_rust` 仓库
- `git clone https://git.unlock-music.dev/um/lib_um_crypto_rust.git` - `git clone https://git.um-react.app/um/lib_um_crypto_rust.git`
- 进入 SDK 目录:`cd lib_um_crypto_rust ; cd um_wasm_loader` - 进入 SDK 目录:`cd lib_um_crypto_rust ; cd um_wasm_loader`
- 安装所有 Node 以来:`pnpm i` - 安装所有 Node 以来:`pnpm i`
- 构建:`pnpm build` - 构建:`pnpm build`

View File

@@ -69,7 +69,7 @@
- WSA 可以参考 [MagiskOnWSALocal](https://github.com/LSPosed/MagiskOnWSALocal) 的说明操作。 - WSA 可以参考 [MagiskOnWSALocal](https://github.com/LSPosed/MagiskOnWSALocal) 的说明操作。
- 雷电模拟器可以在「模拟器设置」 → 「其他设置」中启用 root 特权。 - 雷电模拟器可以在「模拟器设置」 → 「其他设置」中启用 root 特权。
![雷电模拟器 其他设置](../src/faq/assets/ld_settings_misc.webp) ![雷电模拟器 其他设置](../src/faq/assets/ld_settings_misc@2x.webp)
### Via 等浏览器无法正常解密/下载 ### Via 等浏览器无法正常解密/下载
@@ -87,10 +87,6 @@
- 无法下载解密后内容 - 无法下载解密后内容
- 下载的文件名错误 - 下载的文件名错误
### 新版解锁网站没有批量下载
目前没有做。抱歉。
## 仍有问题? ## 仍有问题?
欢迎进入[Telegram 交流群](https://t.me/unlock_music_chat),一起探讨。 欢迎进入[Telegram 交流群](https://t.me/unlock_music_chat),一起探讨。

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "um-react", "name": "um-react",
"version": "0.5.0", "version": "0.5.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "um-react", "name": "um-react",
"version": "0.5.0", "version": "0.5.2",
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.8.2",
"@unlock-music/crypto": "0.1.10", "@unlock-music/crypto": "0.1.10",

View File

@@ -1,7 +1,7 @@
{ {
"name": "um-react", "name": "um-react",
"private": true, "private": true,
"version": "0.5.0", "version": "0.5.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "vite", "start": "vite",
@@ -17,60 +17,60 @@
"prepare": "simple-git-hooks" "prepare": "simple-git-hooks"
}, },
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.9.0",
"@unlock-music/crypto": "0.1.10", "@unlock-music/crypto": "^0.1.12",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"radash": "^12.1.1", "radash": "^12.1.1",
"react": "^19.1.0", "react": "^19.1.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.1",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router": "^7.6.3", "react-router": "^7.8.2",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.6",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"sql.js": "^1.13.0" "sql.js": "^1.13.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.30.1", "@eslint/js": "^9.35.0",
"@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-replace": "^6.0.2",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.13",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/node": "^24.0.10", "@types/node": "^24.3.1",
"@types/react": "^19.1.8", "@types/react": "^19.1.12",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.9",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"@types/sql.js": "^1.4.9", "@types/sql.js": "^1.4.9",
"@types/wicg-file-system-access": "^2023.10.6", "@types/wicg-file-system-access": "^2023.10.6",
"@typescript-eslint/eslint-plugin": "^8.35.1", "@typescript-eslint/eslint-plugin": "^8.42.0",
"@typescript-eslint/parser": "^8.35.1", "@typescript-eslint/parser": "^8.42.0",
"@vitejs/plugin-react": "^4.6.0", "@vitejs/plugin-react": "^5.0.2",
"@vitest/coverage-v8": "^3.2.4", "@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4", "@vitest/ui": "^3.2.4",
"daisyui": "^5.0.43", "daisyui": "^5.1.8",
"eslint": "^9.30.1", "eslint": "^9.35.0",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0", "globals": "^16.3.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"jsdom": "^26.1.0", "jsdom": "^26.1.0",
"lint-staged": "^16.1.2", "lint-staged": "^16.1.6",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"rollup": "^4.44.2", "rollup": "^4.50.1",
"sass": "^1.89.2", "sass": "^1.92.1",
"simple-git-hooks": "^2.13.0", "simple-git-hooks": "^2.13.1",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.13",
"terser": "^5.43.1", "terser": "^5.44.0",
"typescript": "^5.8.3", "typescript": "^5.9.2",
"typescript-eslint": "^8.35.1", "typescript-eslint": "^8.42.0",
"vite": "^6.3.5", "vite": "^7.1.5",
"vite-plugin-pwa": "^1.0.1", "vite-plugin-pwa": "^1.0.3",
"vite-plugin-top-level-await": "^1.5.0", "vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-wasm": "^3.4.1", "vite-plugin-wasm": "^3.5.0",
"vitest": "^3.2.4", "vitest": "^3.2.4",
"workbox-build": "^7.3.0", "workbox-build": "^7.3.0",
"workbox-window": "^7.3.0" "workbox-window": "^7.3.0"
@@ -90,13 +90,17 @@
}, },
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
"@rollup/plugin-terser": "patches/@rollup__plugin-terser.patch",
"sql.js": "patches/sql.js.patch" "sql.js": "patches/sql.js.patch"
}, },
"overrides": { "overrides": {
"rollup-plugin-terser": "npm:@rollup/plugin-terser@0.4.3",
"sourcemap-codec": "npm:@jridgewell/sourcemap-codec@1.4.15" "sourcemap-codec": "npm:@jridgewell/sourcemap-codec@1.4.15"
} },
"onlyBuiltDependencies": [
"@swc/core",
"@tailwindcss/oxide",
"esbuild",
"simple-git-hooks"
]
}, },
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4" "packageManager": "pnpm@10.15.1"
} }

2497
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
pushd "$(dirname "${BASH_SOURCE[0]}")/../" pushd "$(dirname "${BASH_SOURCE[0]}")/../"
WRY_VER="0.1.1" WRY_VER="0.1.2"
mkdir -p win64/{deps,dist} mkdir -p win64/{deps,dist}
dl_file() { dl_file() {

View File

@@ -65,3 +65,12 @@ h6 {
opacity: 0.75; opacity: 0.75;
} }
} }
#downloadAll {
position: absolute;
right: 0.5em;
bottom: 72px;
width: 48px;
height: 48px;
margin: 10px;
}

View File

@@ -1,5 +1,4 @@
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; import { CodeHighlight } from '../CodeHighlight';
import hljsStyleGitHub from 'react-syntax-highlighter/dist/esm/styles/hljs/github';
import { ExtLink } from '../ExtLink'; import { ExtLink } from '../ExtLink';
import PowerShellAdbDumpCommandTemplate from './adb_dump.ps1?raw'; import PowerShellAdbDumpCommandTemplate from './adb_dump.ps1?raw';
import ShellAdbDumpCommandTemplate from './adb_dump.sh?raw'; import ShellAdbDumpCommandTemplate from './adb_dump.sh?raw';
@@ -45,9 +44,7 @@ export function AdbInstructionTemplate({ dir, file, platform }: AdbInstructionTe
<li></li> <li></li>
<li> <li>
<p> USB </p> <p> USB </p>
<SyntaxHighlighter language={language} style={hljsStyleGitHub}> <CodeHighlight language={language}>{command}</CodeHighlight>
{command}
</SyntaxHighlighter>
<br /> <br />
<ExtLink className="text-nowrap" href="https://g.126.fm/04jewvw"> <ExtLink className="text-nowrap" href="https://g.126.fm/04jewvw">
MuMu MuMu

View File

@@ -15,7 +15,6 @@ import { Bounce, ToastContainer } from 'react-toastify';
import { SettingsHome } from '~/features/settings/SettingsHome'; import { SettingsHome } from '~/features/settings/SettingsHome';
import { FAQ_PAGES } from '~/faq/FAQPages'; import { FAQ_PAGES } from '~/faq/FAQPages';
import { FaqHome } from '~/faq/FaqHome'; import { FaqHome } from '~/faq/FaqHome';
import { DownloadAll } from '~/components/DownloadAll.tsx';
// Private to this file only. // Private to this file only.
const store = setupStore(); const store = setupStore();
@@ -72,7 +71,6 @@ export function AppRoot() {
transition={Bounce} transition={Bounce}
/> />
<DownloadAll />
<Footer /> <Footer />
</Provider> </Provider>
</BrowserRouter> </BrowserRouter>

View File

@@ -0,0 +1,10 @@
import { Light as SyntaxHighlighter, type SyntaxHighlighterProps } from 'react-syntax-highlighter';
import hljsStyleGitHub from 'react-syntax-highlighter/dist/esm/styles/hljs/github';
export function CodeHighlight({ children, ...props }: SyntaxHighlighterProps) {
return (
<SyntaxHighlighter style={hljsStyleGitHub} {...props}>
{children}
</SyntaxHighlighter>
);
}

View File

@@ -1,49 +1,68 @@
import { DecryptedAudioFile, selectFiles } from '~/features/file-listing/fileListingSlice'; import { DecryptedAudioFile, ProcessState, selectFiles } from '~/features/file-listing/fileListingSlice';
import { FaDownload } from 'react-icons/fa'; import { FaDownload } from 'react-icons/fa';
import { useAppSelector } from '~/hooks'; import { useAppSelector } from '~/hooks';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
export function DownloadAll() { export function DownloadAll() {
const files = useAppSelector(selectFiles); const files = useAppSelector(selectFiles);
const filesLength = Object.keys(files).length;
const onClickDownloadAll = async () => { const onClickDownloadAll = async () => {
console.time('DownloadAll'); //开始计时
const fileCount = Object.keys(files).length;
if (fileCount === 0) {
toast.warning('未添加文件');
return;
}
//判断所有文件是否处理完成
const allComplete = Object.values(files).every((file) => file.state !== ProcessState.PROCESSING);
if (!allComplete) {
toast.warning('请等待所有文件解密完成');
return;
}
//过滤处理失败的文件
const completeFiles = Object.values(files).filter((file) => file.state === ProcessState.COMPLETE);
//开始下载
let dir: FileSystemDirectoryHandle | undefined; let dir: FileSystemDirectoryHandle | undefined;
let success = 0;
try { try {
dir = await window.showDirectoryPicker(); dir = await window.showDirectoryPicker({ mode: 'readwrite' });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
if (e instanceof Error && e.name === 'AbortError') { if (e instanceof Error && e.name === 'AbortError') {
return; return;
} }
} }
for (const [_, file] of Object.entries(files)) { toast.warning('开始下载,请稍候');
const promises = Object.values(completeFiles).map(async (file) => {
console.log(`开始下载: ${file.fileName}`);
try { try {
if (dir) { if (dir) {
await DownloadNew(dir, file); await DownloadNew(dir, file);
} else { } else {
await DownloadOld(file); await DownloadOld(file);
} }
success++; console.log(`成功下载: ${file.fileName}`);
} catch (e) { } catch (e) {
console.error(`下载失败: ${file.fileName}`, e); console.error(`下载失败: ${file.fileName}`, e);
toast.error(`出现错误: ${e}`); toast.error(`出现错误: ${e}`);
throw e;
} }
} });
if (success === filesLength) { await Promise.allSettled(promises).then((f) => {
toast.success(`成功下载: ${success}/${filesLength}`); const success = f.filter((result) => result.status === 'fulfilled').length;
} else { if (success === fileCount) {
toast.error(`成功下载: ${success}/${filesLength}`); toast.success(`成功下载: ${success}/${fileCount}`);
} } else {
toast.warning(`成功下载: ${success}/${fileCount}`);
}
});
console.timeEnd('DownloadAll'); //停止计时
}; };
return ( return (
<button <button className="btn btn-primary" id="downloadAll" onClick={onClickDownloadAll} title="下载全部">
style={{ width: '48px', height: '48px', paddingInline: '0px', margin: '10px', marginLeft: 'auto' }}
className="btn btn-primary"
onClick={onClickDownloadAll}
title="下载全部"
>
<FaDownload /> <FaDownload />
</button> </button>
); );

View File

@@ -6,14 +6,14 @@ export function Footer() {
return ( return (
<footer className="flex flex-col text-center p-4 bg-base-200"> <footer className="flex flex-col text-center p-4 bg-base-200">
<p className="flex flex-row justify-center items-center h-[1em]"> <p className="flex flex-row justify-center items-center h-[1em]">
<a className="link link-info mr-1" href="https://git.unlock-music.dev/um/um-react"> <a className="link link-info mr-1" href="https://git.um-react.app/um/um-react">
</a> </a>
( (
<a <a
title="使用 MIT 授权协议" title="使用 MIT 授权协议"
className="link link-info" className="link link-info"
href="https://git.unlock-music.dev/um/um-react/src/branch/main/LICENSE" href="https://git.um-react.app/um/um-react/src/branch/main/LICENSE"
> >
MIT MIT
</a> </a>
@@ -23,7 +23,7 @@ export function Footer() {
<p> <p>
{'© 2019 - '} {'© 2019 - '}
<CurrentYear /> <CurrentYear />
<a className="ml-1 link link-info" href="https://git.unlock-music.dev/um"> <a className="ml-1 link link-info" href="https://git.um-react.app/um">
Unlock Music Unlock Music
</a> </a>
</p> </p>

View File

@@ -7,7 +7,7 @@ export interface ProjectIssueProps {
export function ProjectIssue({ id, title }: ProjectIssueProps) { export function ProjectIssue({ id, title }: ProjectIssueProps) {
return ( return (
<ExtLink target="_blank" href={`https://git.unlock-music.dev/um/um-react/issues/${id}`}> <ExtLink target="_blank" href={`https://git.um-react.app/um/um-react/issues/${id}`}>
{`#${id}`} {`#${id}`}
{title && ` - ${title}`} {title && ` - ${title}`}
</ExtLink> </ExtLink>

View File

@@ -25,7 +25,7 @@ export interface DecipherNotOK {
export interface DecipherOK { export interface DecipherOK {
status: Status.OK; status: Status.OK;
message?: string; message?: string;
data: Uint8Array; data: Uint8Array<ArrayBuffer>;
overrideExtension?: string; overrideExtension?: string;
cipherName: string; cipherName: string;
} }

View File

@@ -3,7 +3,7 @@ import { DecipherInstance, DecipherOK, DecipherResult, Status } from '~/decrypt-
export class TransparentDecipher implements DecipherInstance { export class TransparentDecipher implements DecipherInstance {
cipherName = 'none'; cipherName = 'none';
async decrypt(buffer: Uint8Array): Promise<DecipherResult | DecipherOK> { async decrypt(buffer: Uint8Array<ArrayBuffer>): Promise<DecipherResult | DecipherOK> {
return { return {
cipherName: 'None', cipherName: 'None',
status: Status.OK, status: Status.OK,

View File

@@ -24,3 +24,17 @@ export function isDataLooksLikeAudio(buffer: Uint8Array): boolean {
detectResult.free(); detectResult.free();
return ok; return ok;
} }
const AudioMimeType: Record<string, string> = {
mp3: 'audio/mpeg',
flac: 'audio/flac',
m4a: 'audio/mp4',
ogg: 'audio/ogg',
wma: 'audio/x-ms-wma',
wav: 'audio/x-wav',
dff: 'audio/x-dff',
};
export function getMimeTypeFromExt(ext: string) {
return AudioMimeType[ext] || 'application/octet-stream';
}

View File

@@ -1,7 +1,6 @@
export const toArrayBuffer = async (src: Blob | ArrayBuffer | Uint8Array<ArrayBufferLike>) => export const toArrayBuffer = async (src: Blob | BlobPart) => (src instanceof Blob ? await src.arrayBuffer() : src);
src instanceof Blob ? await src.arrayBuffer() : src; export const toBlob = (src: Blob | BlobPart, mimeType?: string) =>
export const toBlob = (src: Blob | ArrayBuffer | Uint8Array<ArrayBufferLike>) => src instanceof Blob ? src : new Blob([src], { type: mimeType ?? 'application/octet-stream' });
src instanceof Blob ? src : new Blob([src]);
export function* chunkBuffer(buffer: Uint8Array, blockLen = 4096): Generator<[Uint8Array, number], void> { export function* chunkBuffer(buffer: Uint8Array, blockLen = 4096): Generator<[Uint8Array, number], void> {
const len = buffer.byteLength; const len = buffer.byteLength;

View File

@@ -6,7 +6,7 @@ import { DecipherFactory, DecipherInstance, Status } from '~/decrypt-worker/Deci
import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError.ts'; import { UnsupportedSourceFile } from '~/decrypt-worker/util/DecryptError.ts';
import { ready as umCryptoReady } from '@unlock-music/crypto'; import { ready as umCryptoReady } from '@unlock-music/crypto';
import { go } from '~/util/go.ts'; import { go } from '~/util/go.ts';
import { detectAudioExtension } from '~/decrypt-worker/util/audioType.ts'; import { getMimeTypeFromExt, detectAudioExtension } from '~/decrypt-worker/util/audioType.ts';
class DecryptCommandHandler { class DecryptCommandHandler {
private readonly label: string; private readonly label: string;
@@ -31,6 +31,7 @@ class DecryptCommandHandler {
const [result, error] = await go(this.tryDecryptWith(decipher)); const [result, error] = await go(this.tryDecryptWith(decipher));
if (!error) { if (!error) {
if (result) { if (result) {
console.debug(`[${decipher.cipherName}] Decryption OK`);
return result; return result;
} }
errors.push(`${decipher.cipherName}: no response`); errors.push(`${decipher.cipherName}: no response`);
@@ -75,7 +76,7 @@ class DecryptCommandHandler {
audioExt = 'm4a'; audioExt = 'm4a';
} }
return { decrypted: URL.createObjectURL(toBlob(result.data)), ext: audioExt }; return { decrypted: URL.createObjectURL(toBlob(result.data, getMimeTypeFromExt(audioExt))), ext: audioExt };
} }
} }

View File

@@ -9,31 +9,31 @@ export function FAQAboutProject() {
<Header3 id="failed">um-react </Header3> <Header3 id="failed">um-react </Header3>
<p> <p>
um-react um-react
<a className="mx-1 link link-info" href="https://git.unlock-music.dev/um"> <a className="mx-1 link link-info" href="https://git.um-react.app/um">
Unlock Music Unlock Music
</a> </a>
React 使 React 使
<a className="mx-1 link link-info" href="https://git.unlock-music.dev/um/um-react/src/branch/main/LICENSE"> <a className="mx-1 link link-info" href="https://git.um-react.app/um/um-react/src/branch/main/LICENSE">
MIT MIT
</a> </a>
</p> </p>
<p> <p>
<FaRust className="inline" /> <FaRust className="inline" />
<a className="mx-1 link link-info" href="https://git.unlock-music.dev/um/lib_um_crypto_rust"> <a className="mx-1 link link-info" href="https://git.um-react.app/um/lib_um_crypto_rust">
<code>lib_um_crypto_rust</code> <code>lib_um_crypto_rust</code>
</a> </a>
使 使
<a <a
className="mx-1 link link-info" className="mx-1 link link-info"
href="https://git.unlock-music.dev/um/lib_um_crypto_rust/src/branch/main/LICENSE_MIT" href="https://git.um-react.app/um/lib_um_crypto_rust/src/branch/main/LICENSE_MIT"
> >
MIT MIT
</a> </a>
+ +
<a <a
className="mx-1 link link-info" className="mx-1 link link-info"
href="https://git.unlock-music.dev/um/lib_um_crypto_rust/src/branch/main/LICENSE_APACHE" href="https://git.um-react.app/um/lib_um_crypto_rust/src/branch/main/LICENSE_APACHE"
> >
Apache Apache
</a> </a>

View File

@@ -1,6 +1,5 @@
import { ExtLink } from '~/components/ExtLink'; import { ExtLink } from '~/components/ExtLink';
import { Header2, Header3, Header4 } from '~/components/HelpText/Headers'; import { Header2, Header3, Header4 } from '~/components/HelpText/Headers';
import { ProjectIssue } from '~/components/ProjectIssue';
import { NavLink } from 'react-router'; import { NavLink } from 'react-router';
@@ -12,15 +11,6 @@ export function OtherFAQ() {
<p></p> <p></p>
<p>使</p> <p>使</p>
<Header3 id="batch-dl"></Header3>
<p>
{'暂时没有实现,不过你可以在 '}
<ProjectIssue id={34} title="[UI] 全部下载功能" />
{' 以及 '}
<ProjectIssue id={43} title="批量下载" />
{' 追踪该问题。'}
</p>
<Header3 id="android-browsers">安卓: 浏览器支持说明</Header3> <Header3 id="android-browsers">安卓: 浏览器支持说明</Header3>
<p> 使 Chrome Firefox </p> <p> 使 Chrome Firefox </p>
<div className="flex flex-col md:flex-row gap-2 md:gap-8"> <div className="flex flex-col md:flex-row gap-2 md:gap-8">
@@ -77,7 +67,7 @@ export function OtherFAQ() {
</li> </li>
<li> <li>
<p> <p>
<ExtLink className="mr-2" href="https://git.unlock-music.dev/um/um-react-wry"> <ExtLink className="mr-2" href="https://git.um-react.app/um/um-react-wry">
<strong> <strong>
<code>um-react-wry</code> <code>um-react-wry</code>
</strong> </strong>
@@ -89,7 +79,7 @@ export function OtherFAQ() {
<ul className="list-disc pl-6"> <ul className="list-disc pl-6">
<li> <li>
<p> <p>
<ExtLink href="https://git.unlock-music.dev/um/um-react/releases/latest"></ExtLink> <ExtLink href="https://git.um-react.app/um/um-react/releases/latest"></ExtLink>
{' | 寻找文件名为 '} {' | 寻找文件名为 '}
<code>um-react-win64-</code> <code>um-react-win64-</code>
</p> </p>

View File

@@ -6,6 +6,8 @@ import { VQuote } from '~/components/HelpText/VQuote';
import { MacCommandKey } from '~/components/Key/MacCommandKey'; import { MacCommandKey } from '~/components/Key/MacCommandKey';
import { ShiftKey } from '~/components/Key/ShiftKey'; import { ShiftKey } from '~/components/Key/ShiftKey';
import BlockUpdateScript from './assets/QQ 音乐 Mac 屏蔽升级.tar.gz?base64';
const MAC_CLIENT_URL = const MAC_CLIENT_URL =
'https://web.archive.org/web/20230903/https://dldir1.qq.com/music/clntupate/mac/QQMusicMac_Mgr.dmg'; 'https://web.archive.org/web/20230903/https://dldir1.qq.com/music/clntupate/mac/QQMusicMac_Mgr.dmg';
const MAC_CLIENT_TG_URL = 'https://t.me/um_lsr_ch/21'; const MAC_CLIENT_TG_URL = 'https://t.me/um_lsr_ch/21';
@@ -43,6 +45,19 @@ export function InstructionsMac() {
</li> </li>
</ul> </ul>
<p className="mt-4">
<ExtLink
className="link-info mx-1"
download="QQ 音乐 Mac 屏蔽升级.tar.gz"
href={`data:application/gzip;base64,${BlockUpdateScript}`}
>
QQ Mac .tar.gz
</ExtLink>
<code>QQ Mac .command</code> QQ
</p>
<p className="mt-4"></p> <p className="mt-4"></p>
<FilePathBlock>{DB_PATH}</FilePathBlock> <FilePathBlock>{DB_PATH}</FilePathBlock>

View File

@@ -1,5 +1,6 @@
import { RiErrorWarningLine } from 'react-icons/ri'; import { RiErrorWarningLine } from 'react-icons/ri';
import { SelectFile } from '../components/SelectFile'; import { SelectFile } from '../components/SelectFile';
import { DownloadAll } from '~/components/DownloadAll.tsx';
import { FileListing } from '~/features/file-listing/FileListing'; import { FileListing } from '~/features/file-listing/FileListing';
import { useAppDispatch, useAppSelector } from '~/hooks.ts'; import { useAppDispatch, useAppSelector } from '~/hooks.ts';
@@ -39,6 +40,7 @@ export function MainTab() {
<div className="w-full mt-4"> <div className="w-full mt-4">
<FileListing /> <FileListing />
</div> </div>
<DownloadAll />
</div> </div>
</div> </div>
); );

View File

@@ -5,7 +5,7 @@ export class MMKVParser {
private offset = 4; private offset = 4;
private length: number; private length: number;
constructor(private view: DataView) { constructor(private view: DataView<ArrayBuffer>) {
const payloadLength = view.getUint32(0, true); const payloadLength = view.getUint32(0, true);
this.length = 4 + payloadLength; this.length = 4 + payloadLength;

View File

@@ -1,7 +1,7 @@
import type { StagingKugouKey } from '~/features/settings/keyFormats'; import type { StagingKugouKey } from '~/features/settings/keyFormats';
import { MMKVParser } from '../MMKVParser'; import { MMKVParser } from '../MMKVParser';
export function parseAndroidKugouMMKV(view: DataView): Omit<StagingKugouKey, 'id'>[] { export function parseAndroidKugouMMKV(view: DataView<ArrayBuffer>): Omit<StagingKugouKey, 'id'>[] {
const mmkv = new MMKVParser(view); const mmkv = new MMKVParser(view);
const result: Omit<StagingKugouKey, 'id'>[] = []; const result: Omit<StagingKugouKey, 'id'>[] = [];
while (!mmkv.eof) { while (!mmkv.eof) {

View File

@@ -1,7 +1,7 @@
import type { StagingKWMv2Key } from '~/features/settings/keyFormats'; import type { StagingKWMv2Key } from '~/features/settings/keyFormats';
import { MMKVParser } from '../MMKVParser'; import { MMKVParser } from '../MMKVParser';
export function parseAndroidKuwoEKey(view: DataView): Omit<StagingKWMv2Key, 'id'>[] { export function parseAndroidKuwoEKey(view: DataView<ArrayBuffer>): Omit<StagingKWMv2Key, 'id'>[] {
const mmkv = new MMKVParser(view); const mmkv = new MMKVParser(view);
const result: Omit<StagingKWMv2Key, 'id'>[] = []; const result: Omit<StagingKWMv2Key, 'id'>[] = [];
while (!mmkv.eof) { while (!mmkv.eof) {
@@ -21,7 +21,7 @@ export function parseAndroidKuwoEKey(view: DataView): Omit<StagingKWMv2Key, 'id'
return result; return result;
} }
export function parseIosKuwoEKey(view: DataView): Omit<StagingKWMv2Key, 'id'>[] { export function parseIosKuwoEKey(view: DataView<ArrayBuffer>): Omit<StagingKWMv2Key, 'id'>[] {
const mmkv = new MMKVParser(view); const mmkv = new MMKVParser(view);
const result: Omit<StagingKWMv2Key, 'id'>[] = []; const result: Omit<StagingKWMv2Key, 'id'>[] = [];
while (!mmkv.eof) { while (!mmkv.eof) {

View File

@@ -1,6 +1,6 @@
import { MMKVParser } from '../MMKVParser'; import { MMKVParser } from '../MMKVParser';
export function parseAndroidQmEKey(view: DataView): Map<string, string> { export function parseAndroidQmEKey(view: DataView<ArrayBuffer>): Map<string, string> {
const mmkv = new MMKVParser(view); const mmkv = new MMKVParser(view);
const result = new Map<string, string>(); const result = new Map<string, string>();
while (!mmkv.eof) { while (!mmkv.eof) {