Skip to main content
Glama
README.md
# 🎵 方糖音乐 (cubesugar-music)

> **NAS 音乐库下载壳子** — 配合 Navidrome / Plex / Emby / Subsonic 等自托管音乐服务器,**提供壳子 + 多源聚合 + MCP 协议**,让你下载的歌曲能正确写 ID3 tag、被播放器识别。

![License](https://img.shields.io/badge/license-MIT-green)
![Python](https://img.shields.io/badge/python-3.11+-blue)
![PostgreSQL](https://img.shields.io/badge/postgresql-15+-blue)

---

## ⚠️ 重要声明

**本项目不内置任何插件。** 支持 LX Music 插件格式(用户自行导入)。 

- 用户需要**自行**通过 Web UI 导入或下载到 `sources/` 目录
- 本项目仅提供:
  - ✅ 壳子(HTTP API + 数据库 + 队列)
  - ✅ 多源聚合上游
  - ✅ MCP 协议(供 AI Agent 接入)
  - ✅ ID3 tag 写入(Navidrome 识别用)
  - ✅ 批量歌单导入
  - ✅ 4 层防重
  - ✅ 本地音乐库索引
  - ❌ **不内置**任何音源插件

---

## ✨ 核心功能

| 功能 | 说明 |
|---|---|
| 🔍 **跨源搜歌** | 默认走多源聚合 |
| 📦 **插件支持** | 支持 LX Music 插件格式,通过 Web UI 导入(Node VM 沙箱执行)|
| 📥 **下载队列** | 2 worker 后台线程,原子抢任务,音质降级(hires → master → atmos → flac24bit → flac → 320k → 128k) |
| 🎵 **批量歌单** | 歌单 URL 一键导入,自动跳过已下载/已存在任务 |
| 🚫 **4 层防重** | ①文件系统 ②数据库精确 ③文件模糊匹配 ④本地索引库 |
| 🎼 **本地索引** | 自动扫描下载目录建立索引,存 PostgreSQL,跨源跨 ID 查重 |
| 🆔 **ID3 tag** | 下载后自动写 FLAC Vorbis Comments / MP3 ID3v2 — Navidrome/Plex 直接识别 |
| 🔌 **MCP 服务** | 10 个工具供 Claude / Hermes / Cursor 等 AI Agent 接入(SSE 模式 + 30 天 token) |
| 🌙 **主题切换** | 白天/夜间双主题,CSS 变量切换,localStorage 持久化 |
| 🔑 **用户管理** | JWT 鉴权、修改密码、退出登录 |

---

## 🏗️ 架构

```
┌─ Web UI (浏览器/外网)
│   ├─ 登录页 (admin/admin)
│   ├─ 搜索/下载/任务
│   └─ 设置 (插件导入 / 音乐库 / MCP / 服务状态)
│
├─ MCP (Claude/Hermes AI Agent 接入, SSE 模式)
│
├─ REST API (8765 端口)
│
└─ Backend
    ├─ upstream 多源聚合 (公开 API)
    ├─ PostgreSQL 5 表
    ├─ Worker threads × 2 (原子抢任务, FOR UPDATE SKIP LOCKED)
    ├─ 插件 (Node VM 沙箱) — 需用户自行导入
    └─ 4 层防重 + ID3 tag 自动写入
```

---

## 🚀 快速开始

### 方式 1: Docker Compose (推荐)

```bash
git clone https://github.com/fangtang0206/cubesugar-music.git
cd cubesugar-music

# 配置环境变量 (可选)
export PG_PASSWORD=your_secure_password
export JWT_SECRET=$(openssl rand -hex 32)
export DOWNLOAD_DIR=/path/to/music  # 默认 ./music

# 启动 (首次会构建镜像,需要 2-3 分钟)
docker compose up -d

# 等 30 秒让服务起来
curl http://127.0.0.1:8765/api/health
# → {"status": "ok", "runtime": "ok", "upstream": "ok", "postgres": "ok", "worker_running": true}

# 打开 Web UI
open http://127.0.0.1:8765/
# 默认账号: admin / admin (登录后立即修改!)
```

**端口占用**: 
- 8765 (Web UI + API)
- 8098 (Node runtime)
- 54321 (PostgreSQL)

如有冲突,修改 `docker-compose.yml` 里的端口映射。

### 方式 2: 直接运行 (开发)

```bash
# 1. 准备 PostgreSQL
psql -U root -c "CREATE DATABASE fangtang_music"

# 2. 安装依赖
pip3 install mutagen psycopg[binary]

# 3. 启动 Node runtime
cd node-runtime && node server.js &

# 4. 启动主服务
cd app && python3 server.py
```

---

## 🔌 导入 LX Music 插件

**项目不内置任何插件**。支持 LX Music 插件格式(.js 文件),首次使用需要导入:

### 通过 Web UI

1. 登录 → 右上角 ⚙ 设置 → 📦 插件 tab
2. 填入插件 JS 文件的 URL → 点击"URL 导入"
3. 或下载到本地 → 用"文件上传"按钮

### 通过文件系统

```bash
# 复制插件 JS 到 sources 目录
cp /path/to/plugin.js /vol1/1000/docker/cubesugar-music/sources/

# 重启服务 (或点 Web UI 刷新)
```

### 推荐插件来源


**重要提醒**:

- 插件上游 API 可能失效(Cloudflare 拦截、KEY 过期等),**多源聚合已能覆盖大部分歌曲**
- 导入前请检查插件代码安全性

---

## ⚙️ 配置

### 环境变量

| 变量 | 默认值 | 说明 |
|---|---|---|
| `PORT` | `8765` | HTTP 服务端口 |
| `DOWNLOAD_DIR` | `/vol2/1000/music` | 下载目录(host 路径, **必须可写**) |
| `SOURCES_DIR` | `/vol1/1000/docker/cubesugar-music/sources` | 插件目录(**需用户填充**) |
| `WORKER_COUNT` | `2` | 后台 worker 线程数 |
| `MAX_RETRIES` | `3` | 任务最大重试次数 |
| `PG_HOST` | `127.0.0.1` | PostgreSQL 主机 |
| `PG_PORT` | `54321` | PostgreSQL 端口 |
| `PG_USER` | `root` | 数据库用户 |
| `PG_PASSWORD` | `cubeSugar_postgresql` | 数据库密码 |
| `PG_DATABASE` | `fangtang_music` | 数据库名 |
| `LX_RUNTIME_BASE` | `http://127.0.0.1:8098` | Node runtime 地址 |
| `SELF_BASE_URL` | `http://127.0.0.1:8765` | MCP 内部调用自己时用 |
| `JWT_SECRET` | `cubesugar-music-default-secret-change-me` | **生产环境必须改** |

---

## 📚 API 文档

### 鉴权

```bash
# 登录
curl -X POST http://127.0.0.1:8765/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin"}'
# → {"token": "eyJhbG...","user": {...}}

# 后续请求带 token
curl http://127.0.0.1:8765/api/tasks/list \
  -H "Authorization: Bearer eyJhbG..."

# 签发 MCP 长 token (30 天)
curl -X POST http://127.0.0.1:8765/api/auth/mcp-token \
  -H "Authorization: Bearer <token>"
# → {"token": "eyJhbG...","expires_in": 2592000}
```

### 搜歌

```bash
# 默认跨源
curl "http://127.0.0.1:8765/search?name=海阔天空&artist=Beyond" \
  -H "Authorization: Bearer <token>"

# 指定源
curl "http://127.0.0.1:8765/search?name=test&source=tx"
```

### 提交下载任务

```bash
curl -X POST http://127.0.0.1:8765/api/tasks/submit \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "wy",
    "song_id": "1357375695",
    "name": "海阔天空",
    "artist": "Beyond",
    "quality": "flac"
  }'
```

### 批量歌单导入

```bash
curl -X POST http://127.0.0.1:8765/api/playlist/import \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://music.163.com/playlist?id=12345",
    "quality": "flac"
  }'
```

### 任务列表 (分页 + 状态筛选)

```bash
# 默认 status=all, page=1, page_size=20
curl "http://127.0.0.1:8765/api/tasks/list" -H "Authorization: Bearer <token>"

# 筛选下载成功 / 失败 / 待重试
curl "http://127.0.0.1:8765/api/tasks/list?status=success" -H "Authorization: Bearer <token>"
curl "http://127.0.0.1:8765/api/tasks/list?status=error"   -H "Authorization: Bearer <token>"
curl "http://127.0.0.1:8765/api/tasks/list?status=retry"   -H "Authorization: Bearer <token>"

# 分页
curl "http://127.0.0.1:8765/api/tasks/list?page=2&page_size=50" -H "Authorization: Bearer <token>"
```

### 本地音乐库索引

```bash
# 状态
curl "http://127.0.0.1:8765/api/library/stats" -H "Authorization: Bearer <token>"

# 查询
curl "http://127.0.0.1:8765/api/library/lookup?artist=Beyond&name=海阔天空" \
  -H "Authorization: Bearer <token>"

# 触发重建 (后台 4-5 分钟)
curl -X POST "http://127.0.0.1:8765/api/library/rescan" -H "Authorization: Bearer <token>"
```

---

## 🔌 MCP 集成 (Claude / Hermes / Cursor)

```yaml
# ~/.hermes/config.yaml 或 ~/.claude/mcp_servers.json
mcp_servers:
  cubesugar-music:
    url: http://your-host:8765/mcp/sse
    transport: sse
    headers:
      Authorization: Bearer <从 /api/auth/mcp-token 拿到的 30 天 token>
      X-Client: hermes-agent
```

### MCP 工具列表 (10 个)

| 工具 | 说明 |
|---|---|
| `search_music` | 搜歌 (跨源) |
| `download_music` | 下载单首到队列 |
| `import_playlist` | 批量导入歌单 |
| `list_tasks` | 列下载任务 (带状态过滤) |
| `get_task_status` | 单个任务状态 |
| `list_plugins` | 列插件 |
| `import_plugin` | 导入插件 (URL/文件) |
| `library_stats` | 本地音乐库统计 |
| `library_lookup` | 查本地库 (artist + name) |
| `library_rescan` | 触发重建索引 |

---

## 🗂️ 数据库结构

| 表 | 说明 |
|---|---|
| `users` | 用户 (默认 admin) |
| `plugins` | 插件元数据 |
| `download_tasks` | 下载任务 (status: pending/processing/success/error) |
| `local_library` | 本地音乐索引 (artist, name, album, file_path) |
| `index_status` | 索引状态 (最后扫描时间) |

---

## 🛠️ 开发

### 项目结构

```
cubesugar-music/
├── app/                        # Python 主服务
│   ├── server.py               # 主 HTTP 路由
│   ├── fetcher.py            # 多源聚合上游
│   ├── library_index.py        # 本地音乐库扫描器 + 索引查重
│   ├── id3_writer.py           # FLAC/MP3 ID3 tag 写入
│   ├── worker/worker.py        # 后台下歌 worker
│   ├── auth/                   # JWT + PBKDF2 密码哈希
│   ├── db/                     # PostgreSQL 连接池
│   ├── api/mcp.py              # MCP server (SSE)
│   └── playlist.py             # 歌单 URL 解析
├── node-runtime/               # Node 沙箱 (8098 端口)
├── sources/                    # (空) 插件目录 - 用户自行填充
├── docker-compose.yml          # 一键启动
├── Dockerfile
└── README.md
```

### 启动开发模式

```bash
# 1. 启动 postgres
docker run -d --name cubesugar-db \
  -e POSTGRES_PASSWORD=cubeSugar_postgresql \
  -e POSTGRES_DB=fangtang_music \
  -p 54321:5432 \
  postgres:15-alpine

# 2. 启动 runtime
cd node-runtime && node server.js &

# 3. 启动主服务
cd app
PORT=8765 DOWNLOAD_DIR=/vol2/1000/music \
PG_HOST=127.0.0.1 PG_PORT=54321 \
python3 server.py
```

---

## 🐛 常见问题

### Q: 启动后 runtime 提示 500 / plugins 列表空?

A: 项目不内置插件。需要手动导入 LX Music 格式的 .js 插件文件。
A: 正常, 仓内**不内置插件**, `sources/` 目录是空的, 需要自己导入插件 JS 文件。

### Q: 搜歌结果显示的歌曲都搜不到?
A: 多源聚合已能覆盖大部分歌曲。如果某首歌搜不到, 检查:
- upstream 是否在线 (`/api/health` 状态)
- 插件是否需要登录账号
- 公开 API 限流 (稍等再试)

### Q: 下载的文件 Navidrome/Plex 显示 "Unknown Artist"?
A: 老文件没 ID3 tag, 手动补:
```bash
cd app/
python3 -c "from id3_writer import backfill_existing_files; print(backfill_existing_files('/vol2/1000/music'))"
```
会扫所有 FLAC/MP3, 从数据库元数据 + 文件路径回推写入 ID3 (4199 首约 30 分钟)

### Q: 怎么给 Navidrome/Plex 用?
A: 两者都扫 `/vol2/1000/music/` 目录:
- 库目录指向 `/vol2/1000/music/`
- 启用"扫描 ID3 tag"选项
- 几分钟后歌名/歌手/专辑全部就位

---

## 📜 License

MIT License - 见 [LICENSE](LICENSE) 文件

---