Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@ All notable changes to ShuffleMuse are documented in this file.

The project follows [Semantic Versioning](https://semver.org/).

## [0.1.1] - 2026-07-24

### Added

- Support for case-insensitive `folder.jpg` and `folder.png` directory artwork
after the existing `cover.jpg` and `cover.png` candidates.
- Lazy display of embedded TITLE metadata in Now Playing while preserving the
original relative file path.
- Deployment guidance for trusted real-IP headers and combined LAN plus
Cloudflare Tunnel access.

### Changed

- Metadata and embedded-cover discovery now share one bounded ffprobe,
file-identity cache, and in-flight request.

### Fixed

- Prevented the playlist navigation strip from exposing list content through a
spacing seam.
- Kept playback and stream-mode controls from overlapping at intermediate
viewport widths.
- Made the codec-side file path in the bottom player link to its Browse
directory.

## [0.1.0] - 2026-07-23

### Added
Expand All @@ -19,4 +44,5 @@ The project follows [Semantic Versioning](https://semver.org/).
- Multi-architecture GHCR image publication for `linux/amd64` and
`linux/arm64`.

[0.1.1]: https://github.com/ColderCoder/ShuffleMuse/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/ColderCoder/ShuffleMuse/tree/v0.1.0
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ ShuffleMuse 是一个面向个人和小型自托管场景的轻量音乐库播
## 快速部署

要求 Docker Engine 和 Docker Compose 插件。默认配置拉取公开的
`ghcr.io/coldercoder/shufflemuse:0.1.0`。把音乐文件放入项目根目录的
`ghcr.io/coldercoder/shufflemuse:0.1.1`。把音乐文件放入项目根目录的
`music/` 后执行:

```bash
Expand Down Expand Up @@ -177,7 +177,7 @@ Tags 页的 CSV 用于查看和外部处理,没有对应的导入功能,不

## 版本与镜像

- 稳定版本由对应 Git 标签发布;`v0.1.0` 对应镜像标签 `0.1.0`、`0.1`、
- 稳定版本由对应 Git 标签发布;`v0.1.1` 对应镜像标签 `0.1.1`、`0.1`、
`0` 和 `latest`。
- 支持 `linux/amd64` 与 `linux/arm64`。
- `shufflemuse --version` 输出版本、Git commit 与构建时间。
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: shufflemuse

services:
shufflemuse:
image: ghcr.io/coldercoder/shufflemuse:0.1.0
image: ghcr.io/coldercoder/shufflemuse:0.1.1
ports:
- "127.0.0.1:8080:8080"
volumes:
Expand Down
9 changes: 5 additions & 4 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,24 +306,25 @@ HTTP/1.1 202 Accepted

```json
{
"title": "君玉",
"codec": "FLAC",
"bitrateKbps": 986,
"bitrateApproximate": false,
"durationSeconds": 245.32
}
```

ffprobe 优先使用第一条音轨 bitrate,其次使用容器 bitrate;两者都缺失时按文件大小和时长估算,并设置 `bitrateApproximate:true`。
`title` 来自容器 TITLE,缺失时回退第一条音轨的 TITLE;两者都为空时省略该字段。标题去除首尾空白并限制为 512 个 UTF-8 字节。ffprobe 优先使用第一条音轨 bitrate,其次使用容器 bitrate;两者都缺失时按文件大小和时长估算,并设置 `bitrateApproximate:true`。

错误:`404 NOT_FOUND`、`503 MEDIA_BUSY`、`504 MEDIA_TIMEOUT`、`422 METADATA_ERROR`。

### `GET|HEAD /api/covers/directory?dir=...`

返回指定相对目录同级、大小写不敏感的 `cover.jpg` `cover.png`,JPEG 优先。`dir` 必须恰好出现一次并使用干净的相对路径;路径穿越、绝对路径、反斜杠和任一目录 symlink 返回 `400 INVALID_DIR`。目录不存在或没有可用封面返回 `404 COVER_NOT_FOUND`。该端点不会递归查找子目录,也不会探测音频内嵌封面
`cover.jpg` `cover.png` → `folder.jpg` → `folder.png` 返回指定相对目录同级、大小写不敏感的封面。`dir` 必须恰好出现一次并使用干净的相对路径;路径穿越、绝对路径、反斜杠和任一目录 symlink 返回 `400 INVALID_DIR`。目录不存在或没有可用封面返回 `404 COVER_NOT_FOUND`。该端点不会递归查找子目录、扩展到其他文件格式或探测音频内嵌封面

### `GET|HEAD /api/files/{id}/cover`

顺序:同目录 `cover.jpg` → `cover.png` → 音频内嵌第一视频流。外置封面不接受 symlink。文件严格超过 20 MiB、任一边严格超过 8192 或总像素严格超过 40 MP 时返回 `404 COVER_NOT_FOUND`。
顺序:同目录 `cover.jpg` → `cover.png` → `folder.jpg` → `folder.png` → 音频内嵌第一视频流;目录文件名大小写不敏感。内嵌 descriptor 与 metadata 共享同一次 ffprobe、文件身份缓存和 singleflight,不会再启动独立 descriptor probe。外置封面不接受 symlink。文件严格超过 20 MiB、任一边严格超过 8192 或总像素严格超过 40 MP 时返回 `404 COVER_NOT_FOUND`。

外置封面任一边严格超过 1536 或文件严格超过 1 MiB 时,实时转换为最长边 1024、不放大的 JPEG q3;PNG 先合成白底。JPEG 转换结果不超过原文件 85% 才采用,否则本次请求发送原 JPEG。小型外置封面原样发送,因此未触发转换的 PNG 保留透明度。内嵌封面固定输出最长边 1024 的 JPEG q3。

Expand All @@ -333,7 +334,7 @@ ffprobe 优先使用第一条音轨 bitrate,其次使用容器 bitrate;两
- `Content-Disposition: inline`;转换输出文件名为 `cover.jpg`;
- `Cache-Control: private, max-age=3600`;
- `ETag`:由源路径、大小、mtime、阈值和编码规格生成;
- `X-Cover-Source: embedded|cover.jpg|cover.png`
- `X-Cover-Source`:`embedded` 或命中的实际目录封面文件名(保留原始大小写)

HEAD 和能得到 304 的条件请求只发现 descriptor,不启动 FFmpeg。可能转换的 JPEG/PNG 在 HEAD 中预先声明 `image/jpeg`,但不声明未知的转换后 `Content-Length`。每个非缓存 GET 都重新转换;仅并发中的相同转换 singleflight,不建立服务端图片结果缓存。错误:`404 NOT_FOUND`、`404 COVER_NOT_FOUND`、`503 MEDIA_BUSY`、`504 MEDIA_TIMEOUT`、`500 COVER_ERROR`。等待队列满或等待超时返回 `503 MEDIA_BUSY`;15 秒执行 deadline 返回 504,FFmpeg 失败不会降级发送超大原图。

Expand Down
8 changes: 4 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ flowchart LR
API --> Queue[Bounded server queue cache]
API --> Manager[Strict media lane manager]
Manager --> FFmpeg[ffmpeg transcoding / cover]
Manager --> FFprobe[ffprobe metadata]
Manager --> FFprobe[ffprobe metadata / embedded cover descriptor]
Rescanner[Rescanner] -->|atomic publish| Snapshot
Rescanner --> Files
Rescanner -->|legacy tag migration| Tags
Expand Down Expand Up @@ -213,15 +213,15 @@ Original 使用 `http.ServeContent`:
- 转码 lane:最多 1 个长 Opus;
- 辅助 lane:最多 1 个 metadata、cover descriptor 或 cover render。

两个 lane 有独立等待队列。辅助任务中 metadata/descriptor 为高优先级,连续最多 4 个后若封面转换等待则让出一次。相同文件身份的 metadata/封面在 Acquire singleflight;Manager 统一追踪 active task、取消和关机 WaitGroup。总数为 1 且未显式设置辅助保留时使用旧共享模式。
两个 lane 有独立等待队列。辅助任务中 metadata/descriptor 为高优先级,连续最多 4 个后若封面转换等待则让出一次。同一文件身份的 metadata 与内嵌封面 descriptor 合并为一次 ffprobe,并在 Acquire 前共享 singleflight;Manager 统一追踪 active task、取消和关机 WaitGroup。总数为 1 且未显式设置辅助保留时使用旧共享模式。

### Metadata LRU

metadata 缓存键包含绝对路径、大小和 mtime成功 LRU 固定默认 4096 条;确定性解析失败进入有界 30 秒负缓存,busy、deadline、取消和临时 I/O 不缓存。底层任务使用独立 deadline;单个 waiter 取消不影响其他 waiter,全部离开才取消底层任务。
共享 probe 一次读取 TITLE、第一音轨 codec/bitrate/duration 与第一视频流尺寸,stdout 上限 64 KiB。缓存键包含绝对路径、大小和 mtime成功 LRU 固定默认 4096 条;确定性命令或 JSON 失败进入有界 30 秒负缓存,busy、deadline、取消和临时 I/O 不缓存。音频 metadata 与封面尺寸分别校验,任一部分无效不隐藏另一部分。底层任务使用独立 deadline;单个 waiter 取消不影响其他 waiter,全部离开才取消底层任务。

### 封面

Loader 先寻找同目录、大小写不敏感的 `cover.jpg`/`cover.png`(JPEG 优先),不存在时才用 ffprobe 发现内嵌封面。外置源超过 20 MiB、8192 单边或 40 MP 时稳定返回 not-found;超过 1536 单边或 1 MiB 才进入 `AuxRender`。FFmpeg 单线程输出最长边 1024、不放大的 JPEG q3,PNG 和带透明度的内嵌图先合成白底。JPEG 结果只有达到 15% 节省才替代原文件。
Loader `cover.jpg``cover.png` → `folder.jpg` → `folder.png` 寻找同目录、大小写不敏感的外置封面,不存在时才消费共享 probe 已发现的内嵌封面 descriptor。查找不递归,也不扩展到其他文件格式。外置源超过 20 MiB、8192 单边或 40 MP 时稳定返回 not-found;超过 1536 单边或 1 MiB 才进入 `AuxRender`。FFmpeg 单线程输出最长边 1024、不放大的 JPEG q3,PNG 和带透明度的内嵌图先合成白底。JPEG 结果只有达到 15% 节省才替代原文件。

HEAD/304 只读取 descriptor,不启动 FFmpeg。未转换外置图使用 `Open` + `ServeContent` 原样发送;转换字节只在当前请求或同一 in-flight 请求组中存在,结束后立即释放,不进入 LRU。服务端只保留小型 descriptor LRU 和 30 秒负缓存。ETag 包含源身份、阈值与编码规格,成功响应允许浏览器私有缓存 1 小时。

Expand Down
2 changes: 1 addition & 1 deletion docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ ShuffleMuse 只从进程环境读取配置,不读取 `.env`。配置在启动
| `MUSIC_MEDIA_TASK_SECONDS` | `15` | `15` | metadata、封面底层任务和 Opus 首字节最长秒数,必须大于 0 |
| `MUSIC_STREAM_WRITE_IDLE_SECONDS` | `60` | `60` | Opus 首字节后滚动写空闲 deadline,必须大于 0 |
| `MUSIC_MEDIA_NEGATIVE_CACHE_SECONDS` | `30` | `30` | 确定性 metadata 失败和封面未找到的负缓存 TTL |
| `MUSIC_METADATA_CACHE_ENTRIES` | `4096` | `4096` | 元数据 LRU 最大条目数,必须大于 0 |
| `MUSIC_METADATA_CACHE_ENTRIES` | `4096` | `4096` | metadata 与内嵌封面 descriptor 共享 probe LRU 的最大条目数,必须大于 0 |
| `MUSIC_COVER_CACHE_ENTRIES` | `128` | `128` | 兼容变量;小型封面 descriptor LRU 条目上限,不缓存图片字节 |
| `MUSIC_COVER_CACHE_BYTES` | `67108864` | `67108864` | 兼容变量;descriptor 估算内存上限,不缓存图片字节 |
| `MUSIC_QUEUE_CACHE_MAX_QUEUES` | `64` | `64` | 服务端随机队列数量上限 |
Expand Down
4 changes: 2 additions & 2 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ Tags 文件行播放按钮已覆盖 hover、按钮 `:focus-visible` 和行 `:foc

## 当前测试范围

后端测试覆盖配置、认证、代理、严格请求、扫描、重扫、Browse、stream、media queue、tags、Graveyard、CSV 和静态资源缓存。前端当前有 17 个测试文件、60 个用例,覆盖主要 stores、分页/取消竞态、登录封禁、路由、Search 语义、Playlist 分块、Tags 二级导航与导出、Graveyard 和 Modal 焦点
后端测试覆盖配置、认证、代理、严格请求、扫描、重扫、Browse、stream、media queue、tags、Graveyard、CSV 和静态资源缓存。前端当前有 17 个测试文件、64 个用例,覆盖主要 stores、分页/取消竞态、登录封禁、路由、Search 语义、Playlist 分块、Tags 二级导航与导出、Graveyard、Modal 焦点和 metadata 标题切换

当前明确缺口:

Expand All @@ -268,7 +268,7 @@ Tags 文件行播放按钮已覆盖 hover、按钮 `:focus-visible` 和行 `:foc
## 依赖与项目元数据

- Go module 为 `github.com/ColderCoder/ShuffleMuse`。
- 前端和后端版本均为 `0.1.0`;容器构建通过 linker flags 注入版本、commit
- 前端和后端发布版本均为 `0.1.1`;容器构建通过 linker flags 注入版本、commit
和构建时间,`shufflemuse --version` 可直接读取。
- 发布与治理入口包括 LICENSE、CHANGELOG、CONTRIBUTING、SECURITY policy、
完整 CI 和仅 GHCR 的标签发布 workflow。
Expand Down
21 changes: 11 additions & 10 deletions docs/PROJECT_AUDIT.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 项目审计

审计日期:2026-07-23
审计日期:2026-07-24

审计对象:当前工作树,包括尚未提交的后端、前端、Compose 和测试变更。

Expand All @@ -20,6 +20,8 @@

2026-07-23 的全项目逻辑与性能复核又关闭了八项问题:默认 Compose 实际暴露范围与文档重新一致;Tags 不再预取完整标签集合;Search/Browse/Preview/Rescan 的过期工作可取消;Browse 的任意深页码不再扩大内存上界;队列选曲不再持有全局锁完成线性扫描;静态哈希资源获得长期缓存且缺失 asset 不再回退 HTML;损坏的本地音量值会被安全归一化;favorite 筛选也不再夹带当前非收藏曲目。

2026-07-24 的 `0.1.1` 发布复核进一步检查了自 `v0.1.0` 起的全部提交与工作树差异。metadata TITLE 与内嵌封面描述已合并为单次有界 ffprobe,目录封面加入 `folder.jpg`/`folder.png`,播放器中间宽度布局、底栏路径链接和 Playlist 接缝均通过自动化与隔离 Chromium 验证。默认及大陆 Dockerfile 的发布候选镜像均完成构建并正确注入 `0.1.1` 版本。

## 审计范围与方法

本次逐项检查了:
Expand Down Expand Up @@ -47,15 +49,14 @@
| `go test -race -count=1 ./...` | 通过 | 覆盖当前 Go 测试触达的并发路径 |
| `go vet ./...` | 通过 | 无 vet 报告 |
| `go test -cover -count=1 ./internal/...` | 通过 | 包级覆盖率见下表 |
| `cd web && bun run test:run` | 通过 | 17 个测试文件、61 个用例 |
| `cd web && bun run test:run` | 通过 | 17 个测试文件、64 个用例 |
| `cd web && bun run build` | 通过 | 包含 `vue-tsc -b` 与 Vite production build |
| 三份 `docker compose ... config --quiet` | 通过 | GHCR、官方源码构建和大陆源码构建配置均能完成解析和插值 |
| `docker compose build --no-cache` | 外部网络失败 | 官方 npm 冷安装与前端构建通过;`proxy.golang.org` 下载 bbolt module 时连接超时 |
| `docker compose build --no-cache --build-arg GOPROXY=https://goproxy.cn,direct` | 外部网络未完成 | Go 下载、官方 npm 冷安装及前后端编译通过;官方 Alpine 仓库停在 FFmpeg 依赖 38/107,连续 3 分钟无进展后人工取消 |
| 默认 Dockerfile 发布候选构建 | 通过 | 官方 Go proxy 首次连接超时;改用受支持的 `GOPROXY=https://goproxy.cn,direct` 参数后完成镜像、版本和 labels 检查 |
| `docker compose -f docker-compose.build-cn.yml build --no-cache` | 通过 | DaoCloud、npmmirror、Goproxy.cn、`sum.golang.google.cn` 与阿里云 APK 镜像的完整无缓存构建通过 |
| 默认 Dockerfile + `GOPROXY=https://goproxy.cn,direct` | 通过 | 完整构建、版本信息、OCI labels 与非 root 用户均已核对 |
| 大陆 Dockerfile `0.1.1` 发布候选构建 | 通过 | 前后端构建、FFmpeg 安装、版本信息、OCI labels 与非 root 用户均已核对 |
| 非 root 临时卷 tar 备份/恢复 smoke test | 通过 | 在只读根、`cap_drop ALL`、`no-new-privileges` 下完成跨卷 round trip,恢复文件属于 `shufflemuse` |
| 隔离 Chromium 实机冒烟 | 通过 | favorite 只循环收藏曲目;音频请求早于延迟封面;同目录换曲复用 URL/DOM/请求;刷新后封面 `transferSize=0`;无 console/page error |
| 隔离 Chromium 实机冒烟 | 通过 | 在 1240、1100、961/960、761/760 和 375 px 验证布局;TITLE、原始路径、Browse 链接及 Original/Opus 正常;无 console/page error |
| `git diff --check` | 通过 | 当前 diff 无空白错误 |

`internal` 包级语句覆盖率快照:
Expand All @@ -65,11 +66,11 @@
| `internal/api` | 81.9% |
| `internal/auth` | 92.2% |
| `internal/config` | 84.3% |
| `internal/cover` | 78.5% |
| `internal/cover` | 79.1% |
| `internal/index` | 76.0% |
| `internal/mediaexec` | 78.2% |
| `internal/playqueue` | 80.9% |
| `internal/stream` | 84.7% |
| `internal/stream` | 85.5% |
| `internal/tags` | 76.9% |

覆盖率只能说明哪些语句被执行,不能代替端到端行为验证。尤其是进程启动/关机、真实反向代理链、浏览器可访问性、Docker 网络和长期内存增长,不能从这些数字推导为安全。
Expand Down Expand Up @@ -288,8 +289,8 @@ E2E 优先覆盖登录失效、播放、Tags CSV、Graveyard 与 Modal 焦点。

状态:2026-07-23 已修复

Go module 已改为 `github.com/ColderCoder/ShuffleMuse`,前后端版本统一为
`0.1.0`,构建注入 commit/build time 并提供 `shufflemuse --version`。
Go module 已改为 `github.com/ColderCoder/ShuffleMuse`,前后端发布版本统一为
`0.1.1`,构建注入 commit/build time 并提供 `shufflemuse --version`。
仓库加入 MIT LICENSE、CHANGELOG、CONTRIBUTING、SECURITY policy、完整 CI
和仅向 GHCR 发布 amd64/arm64 镜像的标签 workflow。`.dockerignore` 也排除
本地二进制、测试产物和 TypeScript build info。
Expand Down
4 changes: 2 additions & 2 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ HTTP 服务会先开始监听,再在后台完成首次扫描:
Home 显示:

- 可选标签过滤器;
- 当前曲名和相对路径;
- 同目录 `cover.jpg` / `cover.png`,不存在时回退内嵌封面;两者都不可用时显示缺省封面
- 当前曲名和相对路径;曲名先显示文件名,metadata TITLE 可用后自动切换,缺失或读取失败时保持文件名;
- 按顺序使用同目录 `cover.jpg` / `cover.png` / `folder.jpg` / `folder.png`(文件名大小写不敏感),不存在时回退内嵌封面;都不可用时显示缺省封面
- 当前路径到 Browse 目录的链接。

切换标签过滤器后,队列严格只包含该标签的曲目。当前歌曲已经带有该标签时,
Expand Down
Loading
Loading