自托管音乐库整理工具
扫描、浏览、重组本地音乐收藏,提供简洁的 Web 界面。
自动读取元数据、检测重复文件、批量整理、批量刮削、歌词编辑与打轴、定时任务。
功能特性 ·
快速开始 ·
配置 ·
API 一览 ·
命名规则
- 目录扫描 -- 递归扫描音乐根目录,增量更新(仅处理变更文件),自动关联艺术家和专辑信息
- 多音乐库管理 -- 支持创建多个音乐库,切换当前库,设置默认库
- Web 浏览 -- 按艺术家、专辑、曲目浏览,单页前端界面
- 待定文件 -- 列出缺少完整元数据的文件,支持在线编辑元数据
- 元数据读取 -- 通过 mutagen 提取 ID3v2 / Vorbis Comment 标签、内嵌封面和歌词
- 元数据刮削 -- 自动从网易云音乐、酷狗、QQ音乐获取缺失的标签、歌词和封面,支持多源并行搜索与智能匹配评分,支持在文件浏览视图批量勾选刮削
- 专辑封面 -- 支持专辑封面自动提取和整理
- 定时任务 -- 支持定时自动扫描和整理音乐库,可配置执行间隔(最小5分钟),支持手动触发
- 歌词编辑 -- 支持在线编辑歌词,包括时间轴同步,支持网页加载音乐边听歌边快速编辑时间轴,支持快捷键和自动跳转,支持编辑进度缓存
- 在线播放 -- 支持在浏览器中直接播放音乐,支持 Range 请求断点续传
- 文件下载 -- 支持单曲、专辑、艺术家、批量、目录等多种下载方式(单曲直传,多曲自动打包 ZIP)
- 文件上传 -- 支持音频文件上传,自动处理文件名冲突(跳过、替换、重命名)
- 文件格式化 -- 预览并执行批量重命名与移动,整理为
{艺术家}/{专辑}/ 目录结构,支持多艺术家批量操作
- 艺术家封面 -- 支持上传、刮削、删除艺术家封面图片,自动转换为 JPEG 格式;支持自定义关键词搜索艺术家头像(网易云+QQ音乐双平台,最多各5条结果),双击应用
- 重复检测 -- 识别音乐库中的重复文件
- 统计分析 -- 艺术家/专辑统计页面,包含相似艺术家检测、歌曲时间轴树展示
- 访问控制 -- 基于 Token 的简单认证




















| 分类 |
技术 |
版本 |
| 语言 |
Python |
3.13 |
| Web 框架 |
Flask |
3.0+ |
| 模板引擎 |
Jinja2 |
-- |
| 数据库 |
SQLite3 (WAL mode) |
-- |
| 音频元数据 |
mutagen |
1.47+ |
| 图像处理 |
Pillow |
10.0+ |
| HTTP 客户端 |
requests |
2.31+ |
| 加密库 |
cryptography |
-- |
| WSGI 服务器 |
Gunicorn / Waitress |
-- |
| 进程管理 |
Supervisor |
-- |
# 1. 进入后端目录
cd python
# 2. 安装依赖
pip install -r requirements.txt
# 3. 设置音乐根目录(默认 /music,可通过环境变量覆盖)
# Windows
set MUSIC_ROOT=D:\\Music
# Linux / macOS
export MUSIC_ROOT=/path/to/your/music
# 4. 启动(开发模式)
python app.py
浏览器打开 http://localhost:5000,默认密钥 tunetree-2026
# Linux / macOS
gunicorn wsgi:application -w 4 -b 0.0.0.0:5000
# Windows
pip install waitress
waitress-serve --host=0.0.0.0 --port=5000 wsgi:application
# 构建镜像
docker build -t tune-tree:1.0.0 .
# 修改 docker-compose.yml 中的卷挂载路径后启动
docker-compose up -d
访问 http://localhost:15000
在 python/config.py 中修改,或通过环境变量覆盖:
| 变量 |
环境变量 |
默认值 |
说明 |
ACCESS_KEY |
ACCESS_KEY |
tunetree-2026 |
登录密钥 |
SECRET_KEY |
-- |
change-me-in-production-please |
Flask 会话密钥 |
DB_ROOT |
DB_ROOT |
instance/ |
数据库目录 |
DB_PATH |
-- |
{DB_ROOT}/library.db |
数据库路径 |
安全提醒:生产环境必须修改 ACCESS_KEY 和 SECRET_KEY
tune-tree/
├── python/ # 后端主目录
│ ├── api/ # API 路由层
│ │ └── routes.py # API 端点定义
│ ├── services/ # 业务逻辑层
│ │ ├── scan_service.py # 目录扫描服务(多线程并行)
│ │ ├── format_service.py # 文件格式化服务
│ │ ├── metadata_scraper.py # 元数据刮削服务(多源并行+评分)
│ │ ├── similarity_service.py # 相似艺术家检测服务(自适应相似度)
│ │ ├── task_service.py # 定时任务服务
│ │ ├── netease_api.py # 网易云音乐 API 客户端
│ │ ├── kugou_api.py # 酷狗音乐 API 客户端
│ │ └── qqmusic_api.py # QQ音乐 API 客户端
│ ├── repository/ # 数据访问层
│ │ ├── artist_repository.py # 艺术家数据操作
│ │ ├── album_repository.py # 专辑数据操作
│ │ ├── track_repository.py # 曲目数据操作
│ │ ├── library_repository.py # 音乐库数据操作
│ │ └── task_repository.py # 定时任务数据操作
│ ├── models/ # 数据模型层
│ │ └── db.py # 数据库连接与初始化(WAL 模式)
│ ├── utils/ # 工具层
│ │ ├── metadata.py # 元数据读写工具(含 Unicode 规范化)
│ │ ├── formatting.py # 跨平台文件名安全处理
│ │ └── qrc_decrypt.py # QRC 歌词解密工具(3DES + zlib)
│ ├── static/ # 静态资源
│ │ ├── css/main.css
│ │ ├── js/app.js # 前端入口
│ │ └── js/modules/ # 前端模块
│ │ ├── api.js # API 请求封装
│ │ ├── auth.js # 认证模块
│ │ ├── artist.js # 艺术家视图
│ │ ├── artist-stats.js # 艺术家统计面板
│ │ ├── album-stats.js # 专辑统计面板
│ │ ├── batch-scrape.js # 批量刮削
│ │ ├── detail.js # 曲目详情
│ │ ├── files.js # 目录浏览
│ │ ├── format.js # 格式化操作
│ │ ├── image-viewer.js # 图片查看器
│ │ ├── logs.js # 操作日志
│ │ ├── lrc-parser.js # LRC 歌词解析
│ │ ├── lyrics-editor.js # 歌词编辑器(含打轴)
│ │ ├── metadata-edit.js # 元数据编辑
│ │ ├── pending.js # 待定文件
│ │ ├── settings.js # 设置页面
│ │ ├── state.js # 全局状态管理
│ │ ├── stats.js # 统计概览
│ │ ├── ui.js # UI 组件
│ │ └── utils.js # 工具函数
│ ├── templates/
│ │ └── index.html # 单页前端
│ ├── instance/ # 应用数据
│ │ ├── library.db # SQLite 数据库
│ │ └── tunetree.log # 操作日志(按日轮转,保留30天)
│ ├── app.py # Flask 应用入口
│ ├── config.py # 配置文件
│ ├── requirements.txt
│ └── wsgi.py # Gunicorn / Waitress 入口
├── conf/ # 配置文件
│ └── supervisord.conf # Supervisor 进程管理配置
├── screenshots/ # 平台预览截图
├── Dockerfile # Docker 构建文件
├── docker-compose.yml # Docker Compose 配置
└── LICENSE # GPLv3
所有接口需要 X-Token 请求头(值为 ACCESS_KEY),部分接口也支持 ?token= 查询参数。
| 方法 |
路径 |
说明 |
| POST |
/api/auth/verify |
验证 token |
| 方法 |
路径 |
说明 |
| POST |
/api/scan |
扫描音乐目录 |
| GET |
/api/scan/status |
查询扫描状态 |
| 方法 |
路径 |
说明 |
| GET |
/api/artists |
获取艺术家列表(支持 ?q= 搜索) |
| GET |
/api/artists/<int:artist_id>/albums |
获取专辑列表 |
| GET |
/api/albums/<int:album_id>/tracks |
获取曲目列表 |
| GET |
/api/artists/<int:artist_id>/full |
获取艺术家完整信息(含所有专辑及曲目) |
| 方法 |
路径 |
说明 |
| GET |
/api/artists/<int:artist_id>/cover |
获取艺术家封面 |
| POST |
/api/artists/<int:artist_id>/cover |
上传艺术家封面 |
| DELETE |
/api/artists/<int:artist_id>/cover |
删除艺术家封面 |
| GET |
/api/artists/<int:artist_id>/cover/exists |
检查艺术家封面是否存在 |
| POST |
/api/artists/<int:artist_id>/scrape-cover |
从网易云刮削艺术家头像 |
| POST |
/api/artists/search-avatars |
搜索艺术家头像({ keyword },返回网易云和QQ音乐各最多5条结果) |
| POST |
/api/artists/<int:artist_id>/apply-avatar |
应用头像URL为艺术家封面({ picUrl }) |
| 方法 |
路径 |
说明 |
| GET |
/api/albums/<int:album_id>/cover |
获取专辑封面 |
| POST |
/api/albums/<int:album_id>/cover |
上传专辑封面 |
| GET |
/api/albums/<int:album_id>/cover/exists |
检查专辑封面是否存在 |
| 方法 |
路径 |
说明 |
| GET |
/api/tracks/<int:track_id> |
获取单曲详情(含歌词) |
| DELETE |
/api/tracks/<int:track_id> |
删除单曲(同时删除本地文件) |
| PUT |
/api/tracks/<int:track_id>/metadata |
更新曲目元数据标签 |
| PUT |
/api/tracks/<int:track_id>/cover |
上传曲目封面 |
| PUT |
/api/tracks/<int:track_id>/lyrics |
更新曲目歌词 |
| POST |
/api/tracks/<int:track_id>/export-lrc |
导出歌词为 .lrc 文件 |
| GET |
/api/tracks/<int:track_id>/audio |
在线播放(支持 Range) |
| GET |
/api/tracks/<int:track_id>/download |
下载单曲 |
| GET |
/api/tracks/by-path?path= |
按路径查找曲目 |
| POST |
/api/tracks/batch-delete |
批量删除曲目({ track_ids: [] }) |
| POST |
/api/tracks/download-batch |
批量下载曲目({ track_ids: [] }) |
| POST |
/api/tracks/batch-scrape |
批量刮削元数据({ track_ids: [], user_inputs: { id: { title, artist, album } } }) |
| 方法 |
路径 |
说明 |
| POST |
/api/tracks/<int:track_id>/scrape |
刮削元数据(支持指定 API) |
| POST |
/api/tracks/<int:track_id>/apply-scrape |
应用刮削结果 |
| POST |
/api/tracks/<int:track_id>/scrape-all |
批量搜索所有 API(支持 exclude_ids、title、artist、album 参数) |
| 方法 |
路径 |
说明 |
| POST |
/api/lyrics/search |
搜索歌词 |
| GET |
/api/lyrics/{song_id} |
获取歌词(含翻译) |
| 方法 |
路径 |
说明 |
| GET |
/api/cover/<int:track_id> |
获取专辑封面 |
| 方法 |
路径 |
说明 |
| GET |
/api/artists/<int:artist_id>/download |
下载艺术家所有曲目(ZIP) |
| GET |
/api/albums/<int:album_id>/download |
下载专辑所有曲目(ZIP) |
| GET |
/api/files/download?path= |
下载文件或目录(ZIP) |
| 方法 |
路径 |
说明 |
| GET |
/api/files?path= |
目录浏览(支持 limit、offset、sort、search、folders_first 参数) |
| GET |
/api/files/audio-count |
获取音频文件统计 |
| POST |
/api/files/upload-check |
检查上传文件冲突 |
| POST |
/api/files/upload-commit |
提交上传文件(含冲突处理:跳过/替换/重命名) |
| POST |
/api/files/upload-cancel |
取消上传,清理临时文件 |
| 方法 |
路径 |
说明 |
| POST |
/api/format/preview |
格式化预览 |
| POST |
/api/format/execute |
执行格式化 |
| POST |
/api/format/batch-preview |
批量格式化预览 |
| POST |
/api/format/batch-execute |
批量执行格式化 |
| 方法 |
路径 |
说明 |
| GET |
/api/stats |
统计数据 |
| GET |
/api/stats/artists |
艺术家统计数据 |
| GET |
/api/stats/albums |
专辑统计数据 |
| GET |
/api/stats/similar-artists |
相似艺术家检测结果 |
| GET |
/api/stats/similar-artists/<int:artist_a_id>/<int:artist_b_id> |
相似艺术家详情对比 |
| POST |
/api/artists/batch-scrape-covers |
批量刮削艺术家封面 |
| GET |
/api/pending |
待定文件 |
| GET |
/api/duplicates |
重复文件 |
| GET |
/api/logs |
操作日志 |
| DELETE |
/api/logs |
清空日志 |
| 方法 |
路径 |
说明 |
| GET |
/api/task/config |
获取任务配置 |
| POST |
/api/task/config |
设置任务配置({ scrape_enabled, organize_enabled, interval_minutes }) |
| GET |
/api/task/status |
获取任务状态(刮削、整理、定时任务) |
| POST |
/api/task/execute |
手动执行任务({ task_type: "scrape" | "organize" | "both" }) |
| GET |
/api/task/running |
检查是否有任务正在运行 |
| 方法 |
路径 |
说明 |
| GET |
/api/libraries |
获取所有音乐库列表 |
| GET |
/api/libraries/current |
获取当前音乐库信息 |
| POST |
/api/libraries |
创建新音乐库({ name, path, is_default }) |
| PUT |
/api/libraries/<int:library_id> |
更新音乐库信息 |
| DELETE |
/api/libraries/<int:library_id> |
删除音乐库(含关联数据) |
| POST |
/api/libraries/<int:library_id>/switch |
切换到指定音乐库 |
格式化后文件名:曲序号. 曲目名[ - feat. 客串].扩展名
示例:
{MUSIC_ROOT}
├── Michael Jackson
│ └── HIStory_ Past, Present and Future, Book I
│ └── 05. Earth Song.flac
└── Babyface_Kenny G
└── Love Ballads
└── 04. Every Time I Close My Eyes.flac
目录结构:{MUSIC_ROOT}/{艺术家}/{专辑}/
歌词编辑器提供完整的 LRC 歌词编辑和时间轴打轴功能,支持在线播放音乐并同步编辑歌词时间戳。
| 功能 |
说明 |
| LRC 解析展示 |
解析标准 LRC 格式,支持多行歌词、时间轴、元数据标签 |
| 行编辑 |
支持主行/附行编辑,可添加、删除、修改歌词行 |
| 音乐播放 |
内置音频播放器,支持播放/暂停、快进/快退、进度条定位 |
| 打轴功能 |
边听歌边打轴,一键设置时间戳,自动跳转下一行 |
| 缓存持久化 |
编辑进度自动保存到 localStorage,刷新页面不丢失 |
| 歌词搜索导入 |
从网易云音乐搜索并导入歌词 |
| 文件导入导出 |
支持从本地文件导入(拖拽上传),导出为 LRC 文件到浏览器或歌曲目录 |
从曲目详情页点击「编辑歌词」按钮打开编辑器。
| 操作 |
说明 |
| 选择行 |
点击任意歌词行选中,被选中的行会高亮显示 |
| 修改时间戳 |
点击时间戳输入框直接编辑,格式为 [mm:ss.xx] |
| 修改歌词内容 |
点击歌词文本输入框编辑内容 |
| 添加附行 |
点击行右侧的 + 按钮,为当前时间轴添加第二行歌词(翻译/和声等) |
| 在下方添加行 |
点击 ↓ 按钮在当前行下方插入新行,时间戳自动递增 0.1 秒 |
| 删除行 |
点击 × 按钮删除整行;点击行内 × 按钮删除主行或附行 |
| 控件 |
功能 |
| 播放/暂停 |
点击播放按钮或使用快捷键切换 |
| 快进/快退 |
点击左右箭头按钮,每次跳转 5 秒 |
| 进度条 |
点击进度条任意位置跳转播放 |
| 音量调节 |
通过滑块调整播放音量 |
打轴是歌词编辑的核心功能,用于为每句歌词设置准确的时间戳:
- 点击选中要设置时间戳的歌词行
- 播放音乐,当歌词开始演唱时点击「打轴」按钮或按快捷键
- 系统自动设置当前播放时间为该行的时间戳,并自动跳转到下一行
- 继续为下一行打轴
点击「搜索歌词」按钮打开搜索面板:
- 自动根据当前歌曲名和艺术家搜索
- 显示搜索结果列表,双击应用歌词
- 搜索结果缓存到本地,重复搜索无需重新请求
点击「导入歌词」按钮打开导入面板,支持两种方式:
| 方式 |
说明 |
| 文本导入 |
直接粘贴歌词文本到输入框,支持 LRC 格式和纯文本 |
| 文件导入 |
点击或拖拽 .lrc 或 .txt 文件到上传区域 |
点击「导出」按钮提供两种导出方式:
| 方式 |
说明 |
| 浏览器下载 |
将歌词保存为 .lrc 文件下载到本地 |
| 保存到歌曲目录 |
将歌词文件保存到服务器端歌曲所在目录 |
| 按钮 |
功能 |
| 重置 |
放弃所有修改,恢复到打开编辑器时的状态 |
| 复制 |
将当前歌词复制到剪贴板 |
| 确认保存 |
保存修改并关闭编辑器(触发 onLyricsConfirmed 回调) |
| 快捷键组合 |
功能 |
Shift + Enter |
打轴:为当前选中行设置时间戳,并自动选中下一行 |
Shift + Space |
播放/暂停音频 |
Shift + → |
快进 5 秒 |
Shift + ← |
快退 5 秒 |
↑ / ↓ |
在歌词行间上下切换选中状态(光标在输入框首尾时也可触发) |
歌词数据采用分组结构,每个分组包含:
{
timestamp: 123.45, // 时间戳(秒)
timestampStr: "[02:03.45]", // 格式化时间戳字符串
primary: { text: "歌词内容", role: "primary" }, // 主行
secondary: null | { text: "翻译", role: "secondary" } // 附行
}
编辑过程中每一次修改都会自动保存到 localStorage,缓存键格式为 tt-lyrics-edit-{trackId},包含:
- 歌词分组数据 (
groups)
- 元数据信息 (
metadata)
- 是否包含时间戳 (
hasTimestamp)
- 保存时间戳 (
timestamp)
关闭编辑器并确认保存后,缓存会被清除;若取消编辑,下次打开时会恢复缓存内容。
| 格式 |
标签标准 |
封面 |
歌词 |
.mp3 |
ID3v2 |
APIC |
USLT |
.flac |
Vorbis Comment |
内嵌 |
歌词字段 |
系统使用三张核心表存储音乐数据:
- artists:艺术家表(id, name, dir_name)
- albums:专辑表(id, title, dir_name, artist_id, cover_path, year)
- tracks:曲目表(id, title, artist, album, artist_id, album_id, path, ...)
当更新曲目元数据(艺术家、专辑)时,系统自动处理关联关系:
更新元数据
├─ 仅专辑名变化
│ ├─ 新专辑存在?→ 直接更新 album_id
│ └─ 新专辑不存在 → 创建专辑 → 更新 album_id
│ └─ 旧专辑无曲目 → 删除专辑记录
│
├─ 仅艺术家变化
│ ├─ 新艺术家存在?
│ │ ├─ 新艺术家有同名专辑?→ 直接更新 artist_id, album_id
│ │ └─ 新艺术家无同名专辑 → 创建专辑 → 更新 artist_id, album_id
│ └─ 新艺术家不存在 → 创建艺术家 → 创建专辑 → 更新 artist_id, album_id
│ └─ 清理旧数据
│ ├─ 旧艺术家+旧专辑无曲目 → 删除专辑记录
│ └─ 旧艺术家无专辑 → 删除艺术家记录
│
└─ 艺术家和专辑都变化
├─ 新艺术家存在?
│ ├─ 新艺术家有新专辑?→ 直接更新 artist_id, album_id
│ └─ 新艺术家无新专辑 → 创建专辑 → 更新 artist_id, album_id
└─ 新艺术家不存在 → 创建艺术家 → 创建专辑 → 更新 artist_id, album_id
└─ 清理旧数据
├─ 旧艺术家+旧专辑无曲目 → 删除专辑记录
└─ 旧艺术家无专辑 → 删除艺术家记录
清理规则:
支持跨平台处理特殊字符限制。
以下字符会被自动替换为下划线 _:
| 字符 |
Windows |
Linux |
说明 |
|
\ |
❌ |
✅ |
反斜杠(目录分隔符) |
|
/ |
❌ |
❌ |
正斜杠(目录分隔符) |
|
: |
❌ |
✅ |
冒号 |
|
* |
❌ |
✅ |
星号(通配符) |
|
? |
❌ |
✅ |
问号(通配符) |
|
" |
❌ |
✅ |
双引号 |
|
< |
❌ |
✅ |
小于号 |
|
> |
❌ |
✅ |
大于号 |
|
| ` |
` |
❌ |
✅ |
竖线 |
- 控制字符:ASCII
\x00-\x1f 和 \x7f 会被移除
- 不可见 Unicode 字符:零宽空格、BOM、全角空格等不可见字符会被移除
- Unicode 规范化:使用 NFKC 规范化处理日文假名等字符的不同表示形式
- 首字符处理:
- 以
- 开头会被替换为 _(避免被误认为命令行选项)
- 尾字符处理:
- 以
. 结尾会被替换为 _(Windows 不允许目录名以点号结尾)
- 保留名称:
- Windows 保留名称(
CON, PRN, AUX, NUL, COM1-9, LPT1-9)会自动添加下划线后缀
- 空名称处理:
- 如果处理后名称为空或只有特殊字符,会使用
Unknown 作为默认名称
原始名称 → 处理后名称
------------------------ → ------------------------
Michael: Jackson → Michael_Jackson
Album / Best Of → Album _ Best Of
"Classic" Hits → _Classic_ Hits
-rock ballads → _rock ballads
README. → README_
CON → CON_
aux → aux_
特殊字符处理逻辑位于 python/utils/formatting.py:
def safe_dirname(name: str) -> str:
# 1. 替换跨平台非法字符(Windows: \/:*?"<>|,Linux: /)
result = re.sub(r'[\\/:*?"<>|]', "_", name)
# 2. 移除控制字符(ASCII 0-31 和 127)
result = re.sub(r"[\x00-\x1f\x7f]", "", result)
# 3. 去除首尾空格
result = result.strip()
# 4. 处理 Linux 特殊开头字符(以 - 开头)
if result.startswith("-"):
result = "_" + result[1:]
# 5. 将结尾的点号替换为下划线
result = re.sub(r"\.+$", "_", result)
# 6. 检查 Windows 保留名称
windows_reserved = {
"con", "prn", "aux", "nul",
"com1-9", "lpt1-9"
}
if result.lower() in windows_reserved:
result = result + "_"
# 7. 如果处理后为空或只有特殊字符,返回 Unknown
return result or "Unknown"
GPLv3