Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3accee2
feat: add OpenList storage, Alidrive integration, card-based registra…
XCQ0607 Aug 2, 2026
65e5291
fix: load OpenList/Alidrive on tab switch; add NAS deploy tooling
XCQ0607 Aug 2, 2026
ecce15c
fix: OpenList 播放卡死 - needle 流式代理 bug 修复 + 本地边播边缓存
XCQ0607 Aug 3, 2026
47a6ef4
feat: OpenList 整合本地音乐模块 - 目录扫描、合并列表、内嵌目录树与收藏播放
XCQ0607 Aug 3, 2026
0e5a4f0
docs: README 部署章节重排为 NAS Docker Compose 优先并替换链接归属
XCQ0607 Aug 4, 2026
d8a19aa
docs: 修正 Docker 部署方式为自行构建镜像(镜像未发布到 Docker Hub)
XCQ0607 Aug 4, 2026
6c143b2
docs: Docker 部署改为使用 ghcr.io 镜像并保留源码构建方式
XCQ0607 Aug 4, 2026
0b17e15
fix: 强制 DNS 优先 IPv4, 修复 OpenList/WebDAV 连接 ENETUNREACH
XCQ0607 Aug 4, 2026
d7b55d8
chore: update build hash
XCQ0607 Aug 4, 2026
b5cc135
feat: remove alidrive & fix local playback & intranet mount
XCQ0607 Aug 5, 2026
e847118
Merge pull request #1 from boy6656598/260805-feat-remove-alidrive-fix…
boy6656598 Aug 5, 2026
d9888db
docs: WebDAV 音乐挂载功能需求与技术设计(边播边缓存+目录歌单)
XCQ0607 Aug 5, 2026
8721e1a
feat: WebDAV 音乐挂载(边播边缓存+目录歌单)
XCQ0607 Aug 5, 2026
9d2d721
docs: 记录 WebDAV 挂载构建/测试/镜像知识
XCQ0607 Aug 5, 2026
6ba2f10
fix: WebDAV restore 分层列举+请求超时, 防止 dav 无响应导致启动崩溃
XCQ0607 Aug 5, 2026
71970c8
fix: 修复缓存文件同步上云自我污染、WebDAV 目录树路径翻倍与播放源丢失
XCQ0607 Aug 6, 2026
aa71351
fix: WebDAV/OpenList 歌曲播放与歌词直连, 不再走在线自定义源
XCQ0607 Aug 6, 2026
66c8c66
fix: 收藏/歌单内 WebDAV 歌曲恢复直链播放字段, 服务端按路径兜底恢复挂载
XCQ0607 Aug 6, 2026
4664fbd
ci: push docker image to ghcr.io only
XCQ0607 Aug 5, 2026
c3207e7
fix(docker): remove nonexistent config.js from build output, include …
XCQ0607 Aug 5, 2026
110e487
ci: use PACKAGES_TOKEN to push to ghcr
XCQ0607 Aug 5, 2026
ac4ba0e
fix(docker): skip postinstall scripts to avoid bufferutil build failu…
XCQ0607 Aug 5, 2026
91908ce
fix(docker): add host.docker.internal mapping for accessing host serv…
XCQ0607 Aug 5, 2026
dc021e4
docs(docker): use published ghcr.io image in compose, keep source bui…
XCQ0607 Aug 5, 2026
fb7a5bd
fix: openlist stream 实时刷新 sign 避免收藏旧 sign 失效; webdav-mounts 列表放行播放器用户
XCQ0607 Aug 6, 2026
53934f3
fix: 服务端出站请求自动解析 host.docker.internal, 不再依赖容器 DNS
XCQ0607 Aug 7, 2026
dc0f8f2
chore(docker): 在 compose 中显式设置 LX_HOST_ADDR=192.168.2.100 兜底宿主机地址
XCQ0607 Aug 7, 2026
b54f29a
chore(docker): 移除 LX_HOST_ADDR 错误兜底, 改用 host-gateway 自动探测
XCQ0607 Aug 7, 2026
e7b1048
docs: 自述文件补充 WebDAV/OpenList 音乐挂载功能与配置说明
XCQ0607 Aug 7, 2026
1421048
fix(webdav): 修复刷新页面后 webdav 歌曲播放失败
XCQ0607 Aug 7, 2026
25bd8f1
fix(subsonic): 修复 Subsonic 客户端无法播放 OpenList/WebDAV 挂载音乐
XCQ0607 Aug 8, 2026
30688d8
chore(docker): 排除 downloads 目录避免 APK 等下载物进入镜像
XCQ0607 Aug 8, 2026
59ffada
fix(subsonic): 修复 WebDAV/OpenList 挂载歌曲在 Subsonic 客户端无法播放
XCQ0607 Aug 8, 2026
799fad4
fix(subsonic): 内部流路由支持 HEAD 请求
XCQ0607 Aug 9, 2026
a902ccf
feat(subsonic): 支持 WebDAV/OpenList 挂载歌曲读取同目录 .lrc 歌词
XCQ0607 Aug 9, 2026
486f521
docs: 记录挂载歌词编码与 needle 解码限制
XCQ0607 Aug 9, 2026
40ece6e
fix(lyric): 用 iconv-lite 解码 GBK 歌词,兼容 Docker/Alpine 的 small-icu node
XCQ0607 Aug 9, 2026
05a6a16
docs: 更新歌词编码记忆(Alpine small-icu 需用 iconv-lite)
XCQ0607 Aug 9, 2026
3d35b68
chore: 版本升级至 v3.0.1(WebDAV/OpenList 挂载、Subsonic 播放、同目录歌词)
XCQ0607 Aug 10, 2026
9f95d3f
docs: 新增对外开放注册与 TG 机器人功能需求与技术设计
XCQ0607 Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ logs
lx-music-desktop-master
dist-electron
build
downloads
13 changes: 2 additions & 11 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ env:
REGISTRY: ghcr.io
# 使用 github.repository_owner 获取用户名,并指定镜像名为 lxserver
IMAGE_NAME: ${{ github.repository_owner }}/lxserver
# Docker Hub 仓库名
DOCKER_HUB_REPO: ${{ secrets.DOCKER_USERNAME }}/lxserver

jobs:
build_and_push:
Expand All @@ -33,13 +31,7 @@ jobs:
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
password: ${{ secrets.PACKAGES_TOKEN }}

# Extract project version from config.js
- name: Read version from config
Expand All @@ -64,7 +56,6 @@ jobs:
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
${{ env.DOCKER_HUB_REPO }}
tags: |
type=raw,value=latest
type=raw,value=${{ env.PROJECT_VERSION }}
Expand Down Expand Up @@ -96,7 +87,7 @@ jobs:
fi
done
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
continue-on-error: true

- name: Build and push Docker image
Expand Down
Binary file modified .gitignore
Binary file not shown.
78 changes: 78 additions & 0 deletions .monkeycode/MEMORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# User Instruction Memory

This file records user instructions, preferences, and teachings for reference in future interactions.

## Format

### User Instruction Entry
User instruction entries should follow this format:

[User Instruction Summary]
- Date: [YYYY-MM-DD]
- Context: [Mentioned scenario or time]
- Instructions:
- [Content of user teaching or instruction, described line by line]

### Project Knowledge Entry
Entries discovered by the Agent during task execution should follow this format:

[Project Knowledge Summary]
- Date: [YYYY-MM-DD]
- Context: Discovered by Agent while performing [specific task description]
- Category: [Operations & Deployment|Build Methods|Testing Methods|Troubleshooting & Debugging|Workflow & Collaboration|Environment Configuration]
- Instructions:
- [Specific knowledge points, described line by line]

## Deduplication Strategy
- Before adding a new entry, check for similar or identical instructions.
- If a duplicate is found, skip the new entry or merge it with the existing one.
- When merging, update the context or date information.
- This helps avoid redundant entries and keeps the memory file tidy.

## Entries

[Project Knowledge Summary]
- Date: 2026-08-01
- Context: Discovered by Agent while building and deploying lxserver music sync server
- Category: Operations & Deployment
- Instructions:
- Build: `npm run build`(prebuild 会自动下载 fpcalc 二进制并更新 build hash 到 config.js)
- Start: `npm start`,服务器监听 `0.0.0.0:9527`;开发时用 background terminal 启动,避免阻塞
- 管理员后台入口 `/`,前端密码(`frontend.password`)默认 `123456`;用户密码登录播放器
- 测试账号:admin/password(管理员)、testuser/123456;管理员鉴权头 `X-Frontend-Auth: <frontend.password>`
- 强制登录开启时(player.forceLogin),播放器静态资源未登录会 302 到 `/music/login`;登录接口 `/api/user/login` 同时下发 `lx_player_session` 与 user token cookie
- 卡密与阿里云盘配置分别持久化在 dataPath 下的 `cards.json`、`alidrive.json`,需配置 ClientID/ClientSecret 并在后台扫码绑定后才能使用云盘功能

[Project Knowledge Summary]
- Date: 2026-08-03
- Context: Discovered by Agent while fixing OpenList 播放卡死问题
- Category: Troubleshooting & Debugging
- Instructions:
- **needle 3.x 流式下载 bug**:`needle.get()` 在响应约 130KB(130896 字节)后会卡死不再输出数据,导致 `openlist.stream` 代理播放几秒就卡死。修复:改用 Node 原生 `http/https.request`(`src/server/openlist.ts` 的 `stream` 函数)。任何新的流式代理代码禁止使用 needle 转发大文件。
- OpenList 播放已支持"边播边缓存":首次播放把数据同时写入 `<dataPath>/openlist-cache/<serverId>/<hash>.ext`,完整后 rename 落盘,之后播放/拖拽直接读本地(秒开)。缓存状态接口:`/api/openlist/cache/check`(单文件)、`/api/openlist/cache/status`(汇总)、`/api/openlist/cache/clear`(管理员)。前端 openlist_manager 会显示"已缓存/缓存中"徽标。
- 真实 OpenList 上游速度实测约 360KB/s-1.3MB/s(此前 needle 卡死误判为上游限速 4KB/s),足够流畅播放。
- config.js 含真实凭据,不进入 git 提交;NAS 部署用 `scripts/migrate-to-nas.sh` 生成清洗后的部署包。
- OpenList `/d/` 直链会 302 到对象存储/CDN(如阿里云盘 OSS 签名 URL),代理必须服务端跟随重定向(最多 5 跳),否则播放器拿到无 Location 的 302 无法播放。OSS 签名 URL 可直接访问,无需转发 Authorization。
- 远程目录树扫描必须加防护:单目录 listFiles 加 20s 超时(needle 对超大目录可能永久挂起)、整体 60s 截止、目录数上限 800、子目录并发 6,否则真实 OpenList(含大量网盘挂载)递归扫描会把进程拖死。
- 本地音乐整合 OpenList:`/api/openlist/local-list?server=&refresh=` 递归扫描生成索引(TTL 120s);`/api/music/cache/list` 后端合并 folder='openlist' 条目;前端 local_music.js 过滤 tab 加 openlist 选项,内嵌目录树面板(`lm-ol-*` 元素 + LocalMusicManager.ol* 方法),收藏走 openlist 字段(url/serverId/path/sign)恢复播放。

[Project Knowledge Summary]
- Date: 2026-08-05
- Context: Discovered by Agent while implementing WebDAV 音乐挂载功能(边播边缓存+目录歌单)
- Category: Build Methods / Troubleshooting & Debugging
- Instructions:
- 测试框架:node:test + `npx tsx --test <file>`(项目无 vitest/jest);mock WebDAV 服务器须注意 PROPFIND 目录 key 去尾部斜杠归一化、响应过滤 `.`/`..`、GET 支持 Range。
- 新增 `src/server/webdavMount.ts`(挂载源 CRUD 持久化 webdav-mounts.json、密码脱敏 hasPassword、目录扫描防护同 openlist、边播边缓存 .tmp->rename、本地 Range 206/416);`subsonic.ts handleStream` 对 webdav_/openlist_/local source 走内部流 302,由内部流路由统一承担缓存优先(避免重复实现)。
- `stream` 返回同步 ClientRequest,server.ts 路由用 try/catch 包裹而非 `.then()`(误用 `.then` 会 TS2339)。
- 服务启动时 config.js 的 webdav.* 备份 restore 会阻塞监听(当前环境 host.docker.internal 不可达,需等网络超时约 2-3 分钟才完成启动);冒烟测试前先 curl 首页确认 200。
- 构建/推送镜像:`docker build -t lxserver:webdav . && docker tag ... ghcr.io/boy6656598/lxserver:latest && docker push`;容器内产物路径为 `/server/server/server/*.js`(根目录是 `/server` 非 `/app`)。

[Project Knowledge Summary]
- Date: 2026-08-09
- Context: Discovered by Agent while implementing 挂载歌曲同目录 .lrc 歌词读取(Subsonic getLyricsBySongId)
- Category: Troubleshooting & Debugging
- Instructions:
- needle 3.x 始终将 body 解码为 string(即使 `decode_response:false`/`output:buffer`),GBK 编码的 .lrc 经 needle 读取后字节已损坏为 U+FFFD,无法恢复。读取远端歌词/文本必须用原生 `http/https.get` 拿原始 Buffer(openlist.ts 的 `httpGetBuffer`)。
- 远端 .lrc 常见 GBK/GB18030 编码;解码策略:先 `new TextDecoder('utf-8',{fatal:true})` 严格解码,异常或含 `\uFFFD` 则回退 `iconv-lite` 的 gb18030。**禁止用 `TextDecoder('gb18030')`**:Docker 镜像(Alpine node,small-icu)不支持该编码,会回退 utf-8 导致乱码;iconv-lite 为纯 JS 实现且已随镜像打包,可用。
- Subsonic 歌词端点:`handleGetLyricsBySongId` 对 `webdav`/`openlist` 源不查在线 SDK(musicSdk 无此二源,会返回错误 70),改为读歌曲同目录同名 .lrc;`resolveMountedLyric` 先按索引匹配、失败再兜底解析歌单固化 url 的 server/path/sign,多候选路径逐个尝试。
- webdav 索引 path 为相对 baseUrl 的路径(baseUrl 已指向挂载根);openlist 索引 path 为含 rootPath 的完整 fs 路径,二者语义不同。
175 changes: 175 additions & 0 deletions .monkeycode/specs/public-user-access-and-telegram-bot/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# 技术设计:对外开放用户注册与 TG 机器人

Feature Name: public-user-access-and-telegram-bot
Updated: 2026-08-10

## Description

将 lx-music-sync-server 改造为对外开放注册形态:

1. Web 播放端仅管理员(后台密码 `frontend.password`)可登录,普通用户被拒。
2. 账号生命周期管理:有效期(7/30/365/永久)、30 天周期活跃 ≥5 分钟自动续期、管理后台手动续期、超期/封禁统一拦截。
3. 记录并展示用户活跃时间;可配置 N 天无活跃自动封禁。
4. 新增 TG 机器人:卡密注册、`/bind` 绑定、改密、获取线路、接收歌单(文本/LX JSON/平台链接)并自动下载生成用户收藏歌单。

## Architecture

### 模块总览

```mermaid
graph TD
A["管理员 Web 后台"] -->|"x-frontend-auth"| B["server.ts HTTP Router"]
C["TG 用户"] -->|"Telegram Bot API"| D["telegramBot.ts"]
E["Subsonic 客户端"] -->|"/rest"| F["subsonic.ts"]
B --> G["userAccount.ts"]
D --> G
F --> G
G --> H["users.json + 定时任务"]
D --> I["playlistParser.ts"]
I --> J["musicSdk 搜索"]
D --> K["serverDownloadQueue.enqueue"]
K --> L["fileCache.downloadAndCache"]
K --> M["ListManage 生成收藏歌单"]
```

### TG 歌单处理数据流

```mermaid
graph TD
S1["TG 收到文本/LX JSON/平台链接"] --> P["playlistParser 解析"]
P --> R["对比全服务器本地曲库索引"]
R -->|"缺失歌曲"| Q["搜索解析为 songInfo"]
Q --> DQ["serverDownloadQueue.enqueue"]
DQ --> DL["下载至 /music/username"]
DQ --> UL["ListManage 写入 TG 歌单"]
UL --> R2["TG 回复统计"]
```

## Components and Interfaces

### 1. `src/server/userAccount.ts`(新增,账号状态中枢)

集中账号生命周期策略,被 server.ts、subsonic.ts、telegramBot.ts 调用。

| 接口 | 说明 |
|---|---|
| `initAccountManager()` | 加载扩展字段(兼容旧数据默认值),启动定时任务(每日扫描:自动续期 + 自动封禁) |
| `registerUser(name, password, cardCode, telegramId?)` | 卡密校验 + 创建用户 + 初始化账号字段,返回结果(复用现有注册逻辑) |
| `bindTelegram(name, password, telegramId)` | 校验密码并绑定 TG ID |
| `recordActivity(name)` | 刷新 `lastActiveAt` 并累加活跃秒(节流写入) |
| `checkUserAccess(name): AccessResult` | 返回 `{ok, reason}`,校验 banned / 过期 |
| `renewExpire(name, days)` | 管理后台手动续期 |
| `setExpire(name, expireAt)` | 设置有效期(含永久) |
| `setBanned(name, banned)` | 封禁/解封 |
| `listAccounts(): UserAccount[]` | 返回含扩展字段的用户列表(后台展示用) |

### 2. `src/server/telegramBot.ts`(新增,grammY 长轮询)

- 初始化读取 `telegram.botToken` / `telegram.enable`,`bot.start()` 长轮询。
- 命令路由:`/start`、`/register`、`/bind`、`/changepassword`、`/server`、`/status`。
- 非命令消息/文档/链接 → 歌单流程。
- 未绑定用户仅允许 `/start`、`/register`、`/bind`。
- 所有用户级操作前调用 `userAccount.checkUserAccess`。

### 3. `src/server/playlistParser.ts`(新增,歌单解析)

| 入参 | 行为 |
|---|---|
| 文本 | 按行解析 `歌手 - 歌名`,无分隔符时按整行歌名搜索 |
| LX JSON | 直接读 `list` 数组为 `MusicInfo[]` |
| 平台链接 | 识别网易云/QQ 等域名 → musicSdk 获取歌单歌曲 |

解析结果统一为待搜索文本列表 → 调 musicSdk 搜索得到 `songInfo[]`。

### 4. 本地曲库对比

复用现有 `fileCache` 的索引:合并所有用户 `music_index.json` 生成全量 `(歌手 - 歌名)` 集合,按归一化标题匹配,判断缺失。

### 5. 下载编排(telegramBot 内)

- 缺失歌曲 → `serverDownloadQueue.enqueue(username, tasks)`(复用现有 resolver `resolveServerSong`)。
- 轮询队列状态,全部结束(finished/exists/error)后汇总回复。
- 生成收藏歌单:调用 `ListManage` 在该用户 `userList` 写入"TG 歌单"。

### 6. `server.ts` 改动

- `saveUsers` / 注册逻辑:保存并初始化扩展字段(`expireAt/banned/lastActiveAt/activeSeconds/periodStart/telegramId`)。
- Web 请求活跃记录:在用户已鉴权(`x-user-token` / session cookie)的 API 入口调用 `recordActivity`。
- 播放端登录拦截:`/api/user/login` 对普通用户返回拒绝(仅管理员后台密码可进入播放端);保留 `/api/music/auth` 校验 `frontend.password`。
- 管理后台 API 扩展:`GET /api/users` 返回扩展字段;新增设置时长/手动续期/封禁/解封接口;`config` API 支持 `telegram.*`、`user.autoBanInactiveDays`、`server.publicUrl`。
- 启动时初始化 accountManager 与 telegramBot。

### 7. `subsonic.ts` 改动

在用户密码认证建立会话处插入 `userAccount.checkUserAccess(username)`,拒绝到期/封禁用户。

## Data Models

### 用户扩展字段(存于 `users.json`)

```ts
interface UserAccount {
name: string
password: string
expireAt: number | null // null = 永久
banned: boolean
lastActiveAt: number // 上次活跃时间戳
activeSeconds: number // 当前周期累计活跃秒
periodStart: number // 当前续期周期起始时间戳
telegramId: number | null
dataPath?: string
}
```

- 注册:`expireAt = now + card.expireDays`(永久卡为 null),`periodStart = now`,`activeSeconds = 0`,`banned = false`。
- 活跃续期判定(每日扫描 + 到期拦截时):`now - periodStart >= 30d` 时,若 `activeSeconds >= 300` 则 `expireAt += 30d`;随后 `activeSeconds = 0`,`periodStart = now`。
- 自动封禁:`autoBanInactiveDays > 0` 且非永久且 `now - lastActiveAt > N 天` → `banned = true`。

### 配置新增键(defaultConfig + config.js)

```ts
'telegram.enable': false,
'telegram.botToken': '',
'user.autoBanInactiveDays': 0, // 0 = 关闭自动封禁
'server.publicUrl': '', // 对外地址,/server 命令使用
```

## Correctness Properties

1. 永久账号(`expireAt === null`)SHALL 不受到期拦截、续期与自动封禁影响。
2. 活跃累加 SHALL 节流(同一用户名每秒最多累计一次差值,单次差值上限 60 秒),防止高频轮询刷时长。
3. 到期/封禁检查 SHALL 在所有入口(Web 播放端登录、Subsonic 会话、TG 用户命令)一致生效。
4. `saveUsers` SHALL 保留新增字段,热重载 SHALL 不丢失(加载时缺失字段补默认值)。
5. 卡密 SHALL 在注册时消耗一次且不可重复使用(沿用 `consumeCard`)。
6. 管理员(frontend.password)SHALL 不受账号有效期/封禁限制。

## Error Handling

| 场景 | 处理 |
|---|---|
| 卡密无效/已用/过期 | TG `/register` 回复对应错误文案 |
| 用户名重复 | TG `/register` 回复"用户名已存在" |
| 账号到期/封禁 | Subsonic 返回认证失败;TG 用户命令回复"账号已到期/被封禁" |
| TG 歌单无法解析/为空 | 回复解析失败提示,不进入下载 |
| 下载任务部分失败 | 汇总回复统计(总数/已存在/已排队/失败),失败原因附在列表 |
| 未配置 botToken | telegramBot 跳过启动并记录日志,不影响其他功能 |
| users.json 损坏 | 沿用现有兜底(置空数组),扩展字段初始化默认值 |

## Test Strategy

1. **单元测试**:`playlistParser`(文本/LX JSON/链接三种格式)、活跃续期判定(30 天周期 + 5 分钟阈值边界)、封禁判定(N 天边界)。
2. **集成验证**(本地 `node ./index.js` + curl):
- `/api/auth/register` 注册后 `expireAt/banned` 初始化正确,`users.json` 字段齐全。
- 手动将 `expireAt` 置为过去 → Subsonic 登录失败、`/api/user/login` 拒绝。
- 活跃记录:模拟请求后 `lastActiveAt/activeSeconds` 更新。
- TG 命令:使用测试 botToken 验证 `/register`、`/bind`、`/server`、歌单文本下载链路(与现有 openlist/webdav 播放验证一致的本地流程)。
3. **构建与镜像**:`npm run build` 通过后构建 `ghcr.io/boy6656598/lxserver:3.0.2` 并推送;TG 库 grammY 需在 Alpine 容器可安装运行。

## References

[^1]: (src/server/cards.ts) - 卡密生成/消耗:`consumeCard` L97、`generateCards` L56
[^2]: (src/server/server.ts) - 注册接口 L4326、`saveUsers` L499、用户管理 `/api/users` L1210
[^3]: (src/server/serverDownloadQueue.ts) - 下载队列 `enqueue`/`resolver` 注入 L7704
[^4]: (src/server/fileCache.ts) - `downloadAndCache` L1855、本地索引 `syncCacheIndex` L655
[^5]: (src/modules/list/manage.ts) - 用户收藏歌单 `ListManage`
[^6]: (src/defaultConfig.ts) - 配置默认值(player.enableRegister L48 等)
Loading