Skip to content

故障排除

本文档整理自后端源码(internal/pkg/tag/)、AGENTS.md 业务踩坑总结、FAQ 文档,以及各模块的错误定义与配置逻辑。

目录


扫描问题

章节来源internal/services/scanner.gointernal/services/scan_progress.gointernal/services/auto_scan.goAGENTS.md 业务踩坑总结

文件找不到 / 扫描结果为空

症状可能原因解决方案
扫描后歌曲数为 0音乐目录路径未配置或不正确通过 PUT /api/v1/settings/music-path 设置正确的绝对路径
部分文件未出现文件格式不在支持列表中检查 scan_config 中的 SupportedFormats 配置
子目录文件缺失目录被排除规则命中检查 ExcludeDirs(按名称匹配)和 ExcludePaths(精确路径匹配)
Docker 环境下文件不可见卷挂载路径不正确使用绝对路径挂载:-v /absolute/path/to/music:/app/music

元数据提取失败

扫描依赖 pkg/tag 纯 Go 库提取音频元数据(无外部依赖)。可选安装 ffprobe 以获取更精确的技术参数(时长、比特率、采样率)。Docker 镜像中已包含 ffprobe。

常见提取失败场景:

  • 文件损坏或不完整:下载中断、传输错误导致的截断文件
  • 非标准 tag 格式:某些软件写入的非标准 ID3v2 帧可能无法解析
  • 嵌入封面过大:超大封面图片可能导致内存占用显著增加

支持的音频格式

默认支持:MP3、FLAC、WAV、APE、OGG、M4A、WMA、AIF/AIFF。可通过数据库配置 scan_configSupportedFormats 字段自定义。

标题规则踩坑

扫描标题提取遵循以下规则:

  1. tag 中有 title 字段 --> 直接使用 tag.Title
  2. tag 中无 title 字段 --> 使用文件名去掉扩展名

注意:不会做"最长公共子串去重 + 拼接"处理。历史版本曾实现此逻辑,导致产生类似"艺术家 - 标题"的冗余标题,将艺术家信息错误混入标题字段。


播放问题

章节来源internal/services/source/errors.gointernal/services/source/validator.goAGENTS.md HLS 电台代理模式

远程歌曲 URL 过期

远程歌曲的播放 URL 可能因源站 token 过期而失效。系统通过源编排(Orchestrator)机制自动处理:

  1. 主源 URL 返回网络错误(NetworkError)或下载文件校验失败(InvalidAudioError
  2. 系统判定错误是否可 fallback(IsFallbackable 函数)
  3. 若可 fallback,Resolver 跨插件 fan-out 搜索备选音源
  4. 所有候选源都失败时,返回终态 AllSourcesFailedError

可 fallback 的错误类型

  • InvalidAudioError:文件下载成功但未通过完整性校验(时长过短、码率过低、时长与预期不符)
  • NetworkError:HTTP 层失败(DNS/连接/超时/非 2xx 状态码)
  • PluginInvocationError:插件 music/url 接口调用失败

HLS 电台无法播放

症状原因解决方案
电台播放直接失败源站 Referer/UA 防盗链开启 HLS 代理模式
Web 嵌入模式电台不播CORS 限制阻塞开启 HLS 代理模式
播放器选错 MediaSourceURL 缺少 .m3u8 后缀系统已强制 HLS 电台 URL 带 .m3u8 后缀

开启 HLS 代理模式:

bash
curl -X PUT http://localhost:58091/api/v1/settings/hls-proxy \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled": true}'

代理模式下所有切片通过服务端中转,注意带宽成本。默认关闭(false),仅在源站防盗链或 CORS 导致播放失败时开启。

音频校验失败

源编排的 Validate 函数在下载后校验文件完整性,以下情况会触发校验失败并尝试 fallback:

校验原因含义默认阈值
probe_failedffprobe / tag 无法读取文件--
too_short实测时长低于绝对下限30 秒
duration_mismatch_low实测时长 < 预期 x 容忍比预期 x 0.85
duration_mismatch_high实测时长 > 预期 x 上限预期 x 1.5
bitrate_too_low平均码率过低8 kbps

可通过 source_validation 配置 key 调整阈值,或设置 enabled: false 灰度关闭校验。


认证问题

章节来源internal/services/auth_service.gointernal/middleware/auth.go

Token 过期

Token 类型有效期处理方式
Access Token7 天使用 Refresh Token 刷新
Refresh Token30 天重新登录获取
插件内部 Token100 年(永久)进程重启自动重新生成

刷新 Token:

bash
curl -X POST http://localhost:58091/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "YOUR_REFRESH_TOKEN"}'

默认凭证

默认账号密码为 admin / admin。修改方式:

  • Docker:环境变量 ADMIN_USERNAME / ADMIN_PASSWORD
  • 二进制:命令行参数 -password your_password

JWT Secret 丢失恢复

JWT Secret 存储在数据库 configs 表中(key 为 jwt_secret),初始迁移时自动生成 32 字节随机密钥。如果数据库损坏导致 Secret 丢失:

  1. 启动时 NewAuthService 会尝试从数据库读取 jwt_secret,失败则服务无法启动
  2. 恢复方式:从备份恢复数据库,或删除 data/songloft.db 重新初始化(会丢失所有用户数据和配置)
  3. Secret 变更后所有已签发的 Token 自动失效,所有客户端需重新登录

插件问题

章节来源internal/jsplugin/health.gointernal/jsplugin/manager.goAGENTS.md JS 插件章节

插件加载失败

常见原因:

  • plugin.json 格式错误或缺少必要字段
  • 文件 SHA256 指纹校验不通过(双层校验:manifest 校验 + 运行时加载校验)
  • QuickJS 沙盒初始化失败(onInit 脚本执行错误)
  • 权限声明缺失:manifest 中的 permissions 未声明所需能力(netstoragefs:music 等)

健康检查失败导致自动禁用

插件健康检查器每 60 秒执行一轮检查,直连 VM 探针(绕开 scheduler 串行队列,避免被长 fetch 阻塞导致假阳性):

状态含义处理
healthyVM 存活且响应正常重置失败计数
busyVM 正在处理长请求(持锁中)不计失败;连续 5 轮(约 5 分钟)仍 busy 则升级为 unhealthy
unhealthyVM 无法响应或 env 已销毁累计连续 3 次失败后标记为 error 状态并卸载

自动恢复机制:被标记为 error 的插件进入指数退避恢复序列:1 分钟 --> 5 分钟 --> 15 分钟 --> 30 分钟 --> 60 分钟。恢复成功后自动切回 active 状态。

手动恢复:通过 POST /api/v1/plugins/{id}/recover 手动触发恢复,会清空退避计数从头开始。

权限不足

插件运行时权限由 manifest 中的 permissions 字段声明,internal/jsplugin 在运行时校验。如果插件尝试未声明的操作(如未声明 net 权限却调用 songloft.net.udpBind),请求会被拒绝。

解决方案:检查插件 plugin.json 中的 permissions 数组,确保包含所需权限。

热更新不生效

插件使用文件指纹(SHA256)检测变更,自动触发热更新。如果更新不生效:

  1. 确认新版本的文件指纹确实发生了变化
  2. 检查插件目录写权限
  3. 查看日志中是否有热更新相关的错误信息
  4. 手动禁用再启用插件强制重新加载

空闲插件自动卸载

插件空闲超过 10 分钟后自动卸载 VM 释放资源(数据库状态保持 active,下次请求时懒加载恢复)。带有定时器的插件会在定时器触发前 2 分钟提前唤醒。拥有活跃 WebSocket 连接或运行中子进程的插件不会被休眠。


缓存问题

章节来源AGENTS.md 音乐缓存章节、internal/services/file_move.go

磁盘空间不足

缓存服务使用 LRU 淘汰策略管理磁盘空间:

  • 默认上限 max_size 为 1 GB,超出时按最后访问时间淘汰最久未使用的文件
  • 设置 max_size=0 表示不限制缓存大小
  • 查看缓存状态:GET /api/v1/cache-manage/stats
  • 手动清理:POST /api/v1/cache-manage/clean
  • 调整上限:PUT /api/v1/cache-manage/config

缓存目录不可写

设置自定义缓存目录前,使用 validate-dir 端点预先验证:

bash
curl -X POST http://localhost:58091/api/v1/cache-manage/validate-dir \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"cache_dir": "/mnt/data/music_cache"}'

该端点执行三项检查:自动创建目录、可写性测试、返回磁盘剩余空间。

跨设备 rename 错误(EXDEV)

典型场景:临时文件创建在系统 /tmp(tmpfs),目标缓存目录挂载在独立磁盘或 Docker volume,os.Rename 返回 syscall.EXDEV(cross-device link)。

解决方案:系统内部统一使用 internal/services.moveFile(src, dst) 替代裸 os.Rename。该函数先尝试 rename,EXDEV 时自动回退到 copy + remove。

注意pkg/tag 的原子写入不受此问题影响,因为它使用 os.CreateTemp(dir, ...) 在源文件同目录创建临时文件,rename 一定在同设备内。


数据库问题

章节来源internal/database/sqlite.gointernal/database/errors.goAGENTS.md 数据库规范

SQLITE_BUSY 错误

SQLite 并发写入时可能出现 SQLITE_BUSY。系统已通过以下措施缓解:

  • WAL 模式:允许读写并发,读不被写阻塞
  • busy_timeout(10000):遇到锁时最多等待 10 秒,避免直接 SQLITE_BUSY
  • 连接池限制MaxOpenConns=10MaxIdleConns=5

如果仍然出现 SQLITE_BUSY,通常是因为跨表写入未正确使用事务:

# 正确做法:使用 RunInTx 获取同一 *sql.Tx 下的 UnitOfWork
db.RunInTx(ctx, func(ctx context.Context, uow *UnitOfWork) error {
    // uow.Songs / uow.Playlists 共享同一事务
})

# 错误做法:service 层手动 BeginTx(会导致 SQLITE_BUSY)

迁移失败

数据库迁移使用 goose,启动时自动执行 goose.Up。迁移失败的常见原因:

原因解决方案
数据库文件被锁确保没有其他进程在访问同一数据库文件
迁移 SQL 语法错误检查 internal/database/migrations/ 中对应的 SQL 文件
磁盘空间不足清理磁盘空间后重启
手动 ALTER 导致 schema 不一致从备份恢复数据库;禁止手动 ALTER data/songloft.db

错误语义

仓储层使用统一的哨兵错误:

  • database.ErrNotFound:记录不存在
  • database.ErrConflict:UNIQUE 约束冲突或写入冲突

service 层应使用 errors.Is 判别并翻译为业务语义。


网络问题

章节来源internal/services/whitelist.goAGENTS.md HLS 电台代理模式 / HTTP Proxy 章节

CORS 问题

Web 嵌入模式下播放远程资源可能遇到 CORS 限制。解决方案:

  • HLS 电台:开启 HLS 代理模式(PUT /api/v1/settings/hls-proxy),服务端代理所有请求
  • 远程歌曲:缓存服务自动将远程文件缓存到本地,缓存命中后通过本域返回

SSRF 拦截

HLS 代理和相关网络功能内置 SSRF 防护(services.IsHostnameAllowed),采用内网封禁策略:

被阻止的地址

  • localhost*.local 域名
  • 回环地址:127.0.0.0/8::1
  • 私有地址:10.0.0.0/8172.16.0.0/12192.168.0.0/16fc00::/7
  • 链路本地:169.254.0.0/16fe80::/10
  • 未指定地址:0.0.0.0::

DNS 解析失败时放行,交由后续 HTTP 请求自行报错。

HLS 代理还额外执行同源校验(scheme + host + port 与 song.URL 严格相等),非同源 URL 保持原样不改写,避免成为开放代理。

HTTP Proxy 配置

配置后端外发请求通过 HTTP 代理转发:

bash
curl -X PUT http://localhost:58091/api/v1/settings/http-proxy \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"proxy": "http://192.168.1.1:7890"}'

支持 HTTP/HTTPS/SOCKS5 代理协议。设置即时生效,无需重启。

注意事项

  • loopback 地址(localhost/127.0.0.1/::1)自动跳过代理
  • 与 GitHub 镜像加速(github_proxy URL 前缀拼接)共存:先拼接镜像前缀再经 HTTP Proxy 转发
  • 影响范围:插件注册表拉取、插件下载/更新、系统升级检查/下载

Tag 写入问题

章节来源pkg/tag/write.goAGENTS.md tag 写入章节

格式支持状态

tag.WriteTag(filePath, opts) 按文件扩展名分发写入实现:

格式写入方式状态
.mp3ID3v2.3(TIT2, TPE1, TPE2, TALB, TYER, TCON, USLT, APIC)支持
.flacVorbis Comment + PICTURE block支持
.apeAPEv2 tag支持
.wavRIFF INFO / ID3支持
.m4a / .mp4 / .m4biTunes-style atoms支持
.ogg / .ogaVorbis Comment (Ogg container)支持
.aif / .aiffID3v2.3 (ID3 chunk) + NAME/AUTH native chunks支持
其他格式--返回 ErrUnsupportedWrite

写入失败处理

  • 不支持的格式返回 ErrUnsupportedWrite,调用方必须降级为日志记录,不得阻塞主流程
  • 写入采用原子化策略:先写临时文件(在源文件同目录创建),再 os.Rename 覆盖原文件
  • 原子写入失败(如磁盘空间不足、权限不足)时,原文件保持不变

平台特定问题

章节来源AGENTS.md 平台适配踩坑章节、docs/faq.md

macOS

问题原因解决方案
Token 存储报错secure_storage 在未签名沙盒下无法使用 Keychain系统自动降级到 SharedPreferences,无需处理
升级检查不可用仅 Docker 部署支持在线升级手动下载新版本替换

Android

问题原因解决方案
构建失败SDK 许可证未接受执行 sdkmanager --licenses
通知不显示Android 13+ 需运行时通知权限在应用设置中授予通知权限
HyperOS3 后台被杀系统激进回收前台服务配置 androidStopForegroundOnPause: false

Windows / Linux

问题原因解决方案
音频播放无声/崩溃音频后端依赖 libmpv安装 just_audio_media_kit 所需的 libmpv 库
Windows 无法修改密码双击 exe 无法传参创建 songloft.bat,写入 songloft.exe -password your_password

Docker

问题原因解决方案
定时任务时间错误容器时区与宿主不一致设置环境变量 TZ=Asia/Shanghai
音乐文件不可见卷挂载路径非绝对路径使用 -v /absolute/path:/app/music
二进制未更新热替换规则判定保留 data 版本参见 Docker 热替换规则表

性能调优

章节来源internal/database/sqlite.goAGENTS.md 音乐缓存章节

SQLite 优化参数

系统启动时通过 DSN 参数自动配置以下优化,通常无需手动调整:

参数作用
journal_modeWAL读写并发,读不被写阻塞
busy_timeout10000 (10s)遇锁等待而非直接失败
synchronousNORMALWAL 模式下已足够安全,减少 fsync
cache_size10000页缓存约 40 MB
foreign_keys1启用外键约束
MaxOpenConns10连接池上限
MaxIdleConns5空闲连接上限
ConnMaxLifetime30 min连接最大存活时间

扫描大量文件

大规模音乐库扫描建议:

  • 使用 ExcludeDirs 排除非音乐目录(如 .gitnode_modules、缩略图目录)
  • 使用 ExcludePaths 精确排除特定路径
  • 扫描是异步执行的,可通过进度 API 查看状态,也可取消正在进行的扫描
  • 首次扫描后增量扫描速度显著提升

缓存 LRU 淘汰策略

配置项默认值说明
max_size1 GB缓存总大小上限;0 表示不限制
cache_dir{data_dir}/music_cache/可自定义为任意绝对路径

切换缓存目录时会自动重建 LRU 索引,但不迁移旧文件。inflight 去重机制确保同一歌曲的并发请求只下载一次。


调试技巧

章节来源internal/handlers/log.gointernal/services/source/metrics.gointernal/jsplugin/health.go

运行时日志级别

支持运行时动态切换日志级别,无需重启:

bash
# 查看当前级别
curl http://localhost:58091/api/v1/settings/log-level \
  -H "Authorization: Bearer TOKEN"

# 切换到 debug 级别
curl -X PUT http://localhost:58091/api/v1/settings/log-level \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"level": "debug"}'

支持的级别:debuginfo(默认)、warnerror。修改后即时生效且持久化到数据库,重启后自动恢复。

访问日志

后端使用标准库 slog 输出结构化日志。排查问题时建议临时切换到 debug 级别以获取详细的请求处理信息,排查完毕后切回 info 减少日志量。

插件健康检查 API

查看所有插件的健康状态:

bash
curl http://localhost:58091/api/v1/plugins/health \
  -H "Authorization: Bearer TOKEN"

返回每个插件的健康度分类(green/yellow/red)、成功率、采样数和最近失败记录。

源编排 Metrics API

查看各音源插件的 Fetch 成功率与健康度:

bash
curl http://localhost:58091/api/v1/plugins/health \
  -H "Authorization: Bearer TOKEN"

返回的 PluginHealthSnapshot 包含:

字段说明
class健康度分类:green(成功率>=80%) / yellow(中间态或样本不足) / red(成功率<40%)
success_rate最近 200 次的成功率
samples当前采样数(<10 时不会被判定为 red,避免冷启动误杀)
last_failures最近失败记录及原因(network_fail / probe_fail / validation_fail / plugin_invocation_fail

健康度影响 Resolver 排序权重:score = baseScore * (0.3 + 0.7 * successRate)。样本不足时取中性值 0.8,不会让冷启动插件被零优先级。

Swagger 文档

开发模式(make run)启动后,访问 http://localhost:58091/swagger/index.html 查看交互式 API 文档。生产版本不包含 Swagger。

pprof 性能分析

开发模式构建(-tags dev)包含 pprof 端点,可用于 CPU/内存/goroutine 分析。生产版本中此功能不可用。