diff --git a/Dockerfile b/Dockerfile index 8455a45f..4d3a4719 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,7 +39,7 @@ ENV BIND_IP='0.0.0.0' # ENV LIST_ADD_MUSIC_LOCATION_TYPE 'top' # ENV LX_USER_user1 '123.123' # ENV LX_USER_user2 '{ "password": "123.456", "maxSnapshotNum": 10, "list.addMusicLocationType": "top" }' -# ENV CONFIG_PATH '/server/config.js' +# ENV CONFIG_PATH '/server/data/config.js' # ENV WEBDAV_URL '' # ENV WEBDAV_USERNAME '' # ENV WEBDAV_PASSWORD '' diff --git a/README.md b/README.md index ed3fed11..b4c99258 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

LX Sync Server

-->

Build Status - Version + Version Node Version License
@@ -252,7 +252,7 @@ npm start ## 🛠️ 配置说明 -可以直接编辑 `config.js`。环境变量优先级最高: +系统配置文件已默认持久化在数据目录中的 `data/config.js`(Docker 部署只需挂载 `./data` 卷即可在容器更新、重启后永久保留配置,若根目录下存在旧 `config.js` 会自动平滑迁移)。环境变量优先级最高: | 环境变量 | 对应配置项 | 说明 | 默认值 | | --------------------------------------- | ------------------------------------ | ------------------------------------------------------------------ | ------------------ | @@ -265,7 +265,7 @@ npm start | `FRONTEND_PASSWORD` | `frontend.password` | Web 管理界面访问密码 | `123456` | | `SERVER_NAME` | `serverName` | 同步服务名称 | `lxserver` | | `MAX_SNAPSHOT_NUM` | `maxSnapshotNum` | 保留的最大快照数量 | `10` | -| `CONFIG_PATH` | - | 指定外部配置文件的绝对路径 | - | +| `CONFIG_PATH` | - | 指定外部配置文件的绝对路径 (默认使用 `data/config.js`) | - | | `DATA_PATH` | - | 指定数据存储目录的绝对路径 | `./data` | | `LOG_PATH` | - | 指定日志输出目录的绝对路径 | `./logs` | | `PROXY_HEADER` | `proxy.header` | 代理转发 IP 头 (如 `x-real-ip`) | - | @@ -299,7 +299,11 @@ npm start | `SUBSONIC_ONLINE_SEARCH` | `subsonic.onlineSearch` | 是否开启 Subsonic 在线全网搜索 | `true` | | `SUBSONIC_ONLINE_SEARCH_MODE` | `subsonic.onlineSearchMode` | Subsonic 在线搜索模式 (`fallback` / `merge` / `local_only`) | `fallback` | | `SUBSONIC_ONLINE_SEARCH_SOURCES` | `subsonic.onlineSearchSources` | Subsonic 在线搜索默认音源列表 | `wy,tx,kw,kg,mg` | +| `SUBSONIC_PUBLIC_LEADERBOARDS` | `subsonic.publicLeaderboards` | 是否开启 Subsonic 公开排行榜 (将在线排行榜映射为只读歌单) | `true` | +| `SUBSONIC_LEADERBOARD_SOURCE` | `subsonic.leaderboardSource` | Subsonic 公开排行榜默认音源平台 (仅支持单选, 如 `tx`, `wy` 等) | `tx` | | `SUBSONIC_LYRIC_TRANSLATION` | `subsonic.lyricTranslation` | Subsonic 歌词中是否包含翻译 | `true` | +| `SUBSONIC_CACHE_ON_PLAY` | `subsonic.cacheOnPlay` | Subsonic 播放时是否触发服务器自动缓存保存 (落盘到用户目录) | `false` | +| `SUBSONIC_PLAY_CACHE_FIRST` | `subsonic.playCacheFirst` | Subsonic 播放时是否优先使用服务器已有的本地缓存/下载文件直接传输 | `true` | | `ARTIST_MAX_FETCH_PAGES` | `artist.maxFetchPages` | 歌手歌曲最大抓取页数 | `20` | | `CACHE_NAMING_PATTERN` | `cache.namingPattern` | 缓存文件命名规则 (`simple` / `custom`) | `simple` | | `SYSTEM_ALLOW_UNSAFE_VM` | `system.allowUnsafeVM` | 是否允许运行 VM 模式自定义源脚本 (需注意安全风险) | `false` | diff --git a/README_EN.md b/README_EN.md index e5541284..074e4440 100644 --- a/README_EN.md +++ b/README_EN.md @@ -5,7 +5,7 @@

Build Status - Version + Version Node Version License
@@ -242,7 +242,7 @@ Separated frontend and backend architecture based on Node.js: ## 🛠️ Configuration -Edit `config.js` directly. Environment variables take precedence: +The configuration file is persisted by default in the data directory at `data/config.js` (Docker deployments only need to mount the `./data` volume to retain all settings across container updates and restarts; existing legacy `config.js` in root will be migrated automatically). Environment variables take precedence: | Env Variable | Config Key | Description | Default | | --- | --- | --- | --- | @@ -255,7 +255,7 @@ Edit `config.js` directly. Environment variables take precedence: | `FRONTEND_PASSWORD` | `frontend.password` | Web dashboard password | `123456` | | `SERVER_NAME` | `serverName` | Sync service name | `lxserver` | | `MAX_SNAPSHOT_NUM` | `maxSnapshotNum` | Max snapshots to keep | `10` | -| `CONFIG_PATH` | - | Absolute path to external config file | - | +| `CONFIG_PATH` | - | Absolute path to external config file (defaults to `data/config.js`) | - | | `DATA_PATH` | - | Absolute path to data storage directory | `./data` | | `LOG_PATH` | - | Absolute path to log output directory | `./logs` | | `PROXY_HEADER` | `proxy.header` | Proxy IP header (e.g., `x-real-ip`) | - | @@ -289,7 +289,11 @@ Edit `config.js` directly. Environment variables take precedence: | `SUBSONIC_ONLINE_SEARCH` | `subsonic.onlineSearch` | Enable Subsonic online search | `true` | | `SUBSONIC_ONLINE_SEARCH_MODE` | `subsonic.onlineSearchMode` | Subsonic online search mode (`fallback` / `merge` / `local_only`) | `fallback` | | `SUBSONIC_ONLINE_SEARCH_SOURCES` | `subsonic.onlineSearchSources` | Subsonic online search default platforms | `wy,tx,kw,kg,mg` | +| `SUBSONIC_PUBLIC_LEADERBOARDS` | `subsonic.publicLeaderboards` | Enable Subsonic public leaderboards (map online charts to read-only playlists) | `true` | +| `SUBSONIC_LEADERBOARD_SOURCE` | `subsonic.leaderboardSource` | Subsonic public leaderboard default platform (single platform, e.g., `tx`, `wy`) | `tx` | | `SUBSONIC_LYRIC_TRANSLATION` | `subsonic.lyricTranslation` | Include translations in Subsonic lyrics | `true` | +| `SUBSONIC_CACHE_ON_PLAY` | `subsonic.cacheOnPlay` | Trigger server caching on Subsonic playback (cached to user dir) | `false` | +| `SUBSONIC_PLAY_CACHE_FIRST` | `subsonic.playCacheFirst` | Prefer streaming local cached/downloaded files on Subsonic playback | `true` | | `ARTIST_MAX_FETCH_PAGES` | `artist.maxFetchPages` | Maximum fetch pages for artist tracks | `20` | | `CACHE_NAMING_PATTERN` | `cache.namingPattern` | Cache file naming rule (`simple` / `custom`) | `simple` | | `SYSTEM_ALLOW_UNSAFE_VM` | `system.allowUnsafeVM` | Allow VM mode custom source scripts (note security risks) | `false` | diff --git a/changelog.md b/changelog.md index cab1abb1..8c8889c3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,67 @@ # Changelog +## v2.0.2 (2026-09-07) + +### 🌟 新增功能 + +- **Subsonic 公开排行榜映射 (#348 @pyss56)**: + - **在线排行榜暴露**: 在 Subsonic 协议中将各平台(如 QQ 音乐)的在线排行榜暴露为只读虚拟歌单,歌单统一归属于虚拟用户 `lxserver` 的公开歌单中,支持通过平台设置查看不同平台的榜单数据。 + - **设置项扩展**: 在系统设置中新增了独立的“开启公开排行榜映射”开关及“排行榜音源平台”配置,界面采用现代 Tag 标签单选交互,方便一键切换目标数据源。 + - ![Subsonic Leaderboard Settings](md/log/2.0.2-2.png) + +- **Subsonic 播放缓存能力** : + + - **播放时触发服务器缓存 (#339 @pyss56)**: 新增 `subsonic.cacheOnPlay` 独立开关,支持在通过 Subsonic 协议播放音乐时,在后台自动将曲目落盘至用户的缓存目录,已缓存的会自动跳过,极大增强离线体验。(注意:开启此项可能产生较大流量) + - **服务器存在缓存优先播放**: 新增独立开关,开启后通过 Subsonic 播放曲目时,若服务器已存在该歌曲的缓存或下载文件,将直接通过本地流式传输,避免重复向源站发起请求。 + - **切歌并发控制**: 为防止用户在客户端快速切歌导致后台并行下载任务堆积跑满带宽,新增了流式响应的 Abort (取消) 控制器调度机制,在客户端断开连接时自动终止缓存任务。 + - ![Subsonic Settings](md/log/2.0.2-1.png) +- **Subsonic 红心收藏/取消收藏功能与持久化联动 (#338 @pyss56)**: + + - **红心收藏与写入**: 新增 Subsonic 协议 star 与 unstar 接口,支持单曲(id)、专辑(albumId)与歌手(artistId)收藏。歌曲实时写入/移出用户的「我的收藏(love)」歌单并生成快照持久化。 + - **在线兜底解析**: 收藏不在本地歌单中的单曲时,支持自动通过平台源在线反查元数据后写入收藏。 + - **媒体库双向联动**: 建立 `subsonic-meta.json` 扩展元数据,打通音流等客户端与 LX Web 前端原生媒体库(artists.json / albums.json)的双向收藏同步。 + - **星标状态与封面防崩**: 优化 getArtists / getArtist 及单曲对象,实时回显 starred 标记;修复专辑封面读取导致的 Node 服务异常,提升客户端交互体验与稳定性。 +- **Subsonic 在线元数据落盘持久化**: + + - 将此前仅存于内存的在线歌曲缓存升级为磁盘文件持久化(写入 `subsonic-online-cache.json`),引入防抖批量聚合与原子写入,彻底解决服务端重启导致检索元数据丢失的问题。 +- **文件管理器现代化与路由联动**: + + - **动态感知管理后台路径 (#341)**: 将文件管理器快捷操作栏的「返回主页」按钮优化为「返回后台」,通过动态读取服务端 `admin.path` 环境变量与配置(`window.CONFIG['admin.path']`),支持在自定义管理后台路径下精准跳转回对应后台控制台。 + - **引入 Material 现代主题**: 引入现代 Material Design 样式,全面升级工具栏、列表区与右键菜单视觉效果,提供更具质感的扁平与圆角交互体验。 + +### 🔧 修复与优化 + +- **播放列表回退逻辑修复 (#343)**: + - **搜索与歌单列表回退 UI 刷新**: 修复了在关闭“播放时切换歌单”相关设置后,点击搜索列表或歌单中的单曲播放时,界面播放队列依然错误显示为整个搜索列表/歌单,且当前播放索引被强制置零的 Bug。现在回退默认试听列表时 UI 会正确刷新并精确定位到刚刚播放的新歌。 +- **Subsonic 收藏 (红心) 状态全局联动修复**: + - **`getStarred` 接口精准剥离**: 修复了获取已星标收藏列表时,服务端错误地聚合所有自定义歌单的 Bug。现在客户端的“我的收藏”严格镜像服务端的 `love` 列表。 +- **Subsonic 封面代理与首页/推荐专辑封面修复 (#344 @pyss56)**: + - **抽取独立封面代理模块 (`coverProxy`)**: 将此前客户端 302 重定向拉取封面的模式重构为服务端代理流式返回,内置并发信号量调度(限制同时回源并发数)、自动失败重试(最多 3 次)与内存缓存降级(`STALE`)保护,彻底解决音流等客户端冷启动并发拉取封面被平台 CDN 封禁导致的封面大面积空白问题。 + - **恢复首页/推荐与云端专辑封面**: 新增 `buildAlbumCoverUrl` 根据 mid 动态构造云端封面,并为网易云等无法直接拼接 URL 的专辑接入 SDK 详情兜底拉取首曲封面;同时为推荐接口引入抖动自动重试机制。 + - **进程级并发队列与请求防击穿**: 将媒体库专辑/歌手补全封面的并发控制提升至模块进程级,并实现 In-flight Promise 复用,防止多客户端并发请求击穿音源;带缓存容量上限管理,预防内存泄露与 OOM。 +- **歌曲命名方式修改与重命名联动修复 (#342)**: + - **修复新命名规则无法选中及保存**: 修复前端 `SETTINGS_UI_MAP` 中 `serverCacheNamingPattern` 的 `normalize` 规则遗漏新命名模式的问题,解决选择“歌手-歌名-音质-专辑”、“歌手-歌名”或“歌名-歌手”后无法选中并自动弹回“简洁”的 Bug。 +- **配置持久化与 Docker 升级数据保留优化 (#345)**: + - **配置文件默认迁移至数据卷目录 (`data/config.js`)**: 将服务配置文件的默认持久化路径从容易随容器销毁而丢失的根目录重定向至持久化卷目录 `data/config.js`,彻底解决 Docker/Watchtower 升级或重建容器后通过后台保存的所有设置(密码、路径、用户权限等)静默重置为默认值的 Bug。 + - **平滑无感兼容迁移**: 服务启动时若 `data/config.js` 尚不存在,将自动从根目录旧 `config.js` 迁移至 `data/config.js`,无需用户手动修改挂载映射。 + - **WebDAV 备份与同步统一**: WebDAV 增量同步与全量打包天然覆盖 `data/config.js`,移除了繁琐的根目录文件特判与转移代码,增强了数据备份的稳定性。 + +- **自定义目录与本地音乐异常报错修复 (#347)**: + - **修复 songmid 类型异常导致的崩溃**: 修复了在自定义目录关联歌曲、重新渲染或操作本地音乐列表时,由于极少部分未索引歌曲的 `songmid` 为数字类型从而触发 `item.songmid.includes is not a function` 导致组件或整个页面崩溃、误报权限错误的 Bug。 + +### 💡 升级与迁移建议 (Migration Guide) + +- **标准 Docker / Docker Compose 用户(推荐)**: + - 本次更新针对 Docker 容器生命周期进行了无感平滑迁移设计。如果您按照官方推荐挂载了 `./data:/server/data` 目录: + - **直接拉取新镜像重启容器即可**(`docker compose pull && docker compose up -d` 或 Watchtower 自动更新)。 + - 容器首次启动时,程序会自动检测并初始化持久化配置文件 `data/config.js`。 + - 以后在 Web 管理后台保存的所有配置(包括自定义路径、密码、缓存策略等)都将安全持久化在 `./data/config.js` 中,**后续任何镜像更新或容器销毁重建均不会再丢失配置**。 +- **曾手动挂载过 `-v ./data/config.js:/server/config.js` 的用户**: + - 现在系统原生支持读取和写入 `data/config.js`,您可以继续保留该挂载,也可以直接精简 `docker-compose.yml`,移除单独的 `config.js` 挂载项,仅保留 `./data:/server/data` 即可。 +- **源码直接运行 / Node.js 物理机部署用户**: + - 系统启动时若发现 `data/config.js` 不存在,会自动将项目根目录现有的 `config.js` 复制到 `data/config.js` 并作为主配置生效。 + - 建议后续直接在后台管理页面修改配置,或直接编辑 `data/config.js`。如需使用自定义配置文件路径,可继续通过环境变量 `CONFIG_PATH` 显式指定。 + ## v2.0.1 (2026-07-23) ### 🌟 新增功能 diff --git a/config.js b/config.js index 03958cce..ed7f443d 100644 --- a/config.js +++ b/config.js @@ -192,6 +192,14 @@ module.exports = { // 环境变量: SUBSONIC_LYRIC_TRANSLATION "subsonic.lyricTranslation": true, + // 是否在 Subsonic 播放音乐时触发服务器缓存保存 (落盘到该用户缓存目录,已缓存自动跳过) + // 环境变量: SUBSONIC_CACHE_ON_PLAY (true/false) + "subsonic.cacheOnPlay": false, + + // 是否在 Subsonic 播放音乐时优先使用本地缓存/下载文件直接流式传输 (默认开启) + // 环境变量: SUBSONIC_PLAY_CACHE_FIRST (true/false) + "subsonic.playCacheFirst": true, + // 歌手信息源优先级 (多个源用逗号分隔,如 tx,wy) // 环境变量: SINGER_SOURCE_PRIORITY "singer.sourcePriority": [ diff --git a/docs/en/guide/configuration.md b/docs/en/guide/configuration.md index 06c039a3..3e637aee 100644 --- a/docs/en/guide/configuration.md +++ b/docs/en/guide/configuration.md @@ -9,9 +9,9 @@ LX Music Sync Server has built a unified basic model skeleton (located in `src/d The loading and merging of configurations follow the priority sequence from high to low below. High-priority options will **hardly override** the corresponding keys of low-priority ones: 1. **Runtime Environment Variables (Environment Variables)**: Has very high priority. For example, `PORT=9527`. -2. **WebDAV Cloud Data (WebDAV Cloud Data)**: If WebDAV is configured, the system will try to restore from the cloud on startup. **Restored cloud content will overwrite the local `config.js` and trigger a hot-reload**. -3. **Explicit Custom Configuration File Path (Custom Config File)**: Static JSON file specified via `CONFIG_PATH`. -4. **Global Default Entry Configuration (Global Config.js)**: The `config.js` file in the project's root directory. +2. **WebDAV Cloud Data (WebDAV Cloud Data)**: If WebDAV is configured, the system will try to restore from the cloud on startup. **Restored cloud content will overwrite persistent `data/config.js` and trigger a hot-reload**. +3. **Explicit Custom Configuration File Path (Custom Config File)**: Path specified via `CONFIG_PATH` (defaults to persistent `data/config.js` inside the data directory). +4. **Persistent Configuration (Persistent Config)**: The `data/config.js` in the data directory (if absent on first launch, will smoothly migrate from legacy root `config.js`). 5. **System-level Default Constants (Default Consts)**: Defaults in `src/defaultConfig.ts`. --- @@ -74,8 +74,8 @@ The underlying periodic polling asynchronous daemon of the service will only be | `SYNC_INTERVAL` | `60` | Integer | Cold shrinking timed parameters (unit: minutes) that trigger full thermal backup and pull comparison synchronization flow periods. | > 🔖 **Stateful Resurrection and Initialization Mechanism**: -> 1. **Cloud-First Restore**: If the variables are detected on startup, the system prioritizes pulling archives from the cloud. -> 2. **Environment-Driven Persistence**: If the cloud config is empty (e.g., first deployment in Docker/Cloud), the system will **automatically persist the current effective configuration (such as ports, passwords set via environment variables) into the local `config.js` and upload it to the cloud** for initialization. This ensures you can establish the initial cloud data solely through environment variables. +> 1. **Cloud-First Restoration**: If this set of variables is detected at startup, the system will prioritize attempting to pull archives from the cloud. +> 2. **Environment-Driven Persistence**: If the cloud config is empty (e.g., first deployment in Docker/Cloud), the system will **automatically persist the current effective configuration (such as ports, passwords set via environment variables) into `data/config.js` in the data directory and upload it to the cloud** for initialization. This ensures you can establish the initial cloud data solely through environment variables. ### IV. Web-side Composite Media Playback Space Protection Logic diff --git a/docs/en/guide/sync-server.md b/docs/en/guide/sync-server.md index 24c7fa93..17c5ef4b 100644 --- a/docs/en/guide/sync-server.md +++ b/docs/en/guide/sync-server.md @@ -7,7 +7,7 @@ LX Music Sync Server includes a Web Management Console (located at the system ro Accessing the management console (default `http://IP:9527`) requires entering the administrator password. - **Default Password**: `123456` -- **Modify Password**: You can override the default password by setting the environment variable `FRONTEND_PASSWORD`, or go to the settings page to modify it after the first login. The password will be saved in the `config.json` file on the server. +- **Modify Password**: You can override the default password by setting the environment variable `FRONTEND_PASSWORD`, or go to the settings page to modify it after the first login. The password will be persisted in the `data/config.js` file on the server. ## Functional Module Description diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 57e2e036..375be7e7 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -75,7 +75,7 @@ LX Music Sync Server 构建了统一的基础模型骨架(位于 `src/defaultC > 🔖 **状态无感迁移与初始化机制 (Stateful Resurrection)**: > 1. **云端优先恢复**:若启动时探测到此组变量,系统会优先尝试从云端拉取存档。 -> 2. **环境驱动落盘**:若云端配置为空(如首次在 Docker/云端部署),系统会**自动将当前运行环境中的生效配置(如通过环境变量设置的端口、密码等)持久化写入本地 `config.js` 并同步上传至云端**进行初始化。这确保了您仅通过环境变量即可完成云端数据的首次“开荒”建立。 +> 2. **环境驱动落盘**:若云端配置为空(如首次在 Docker/云端部署),系统会**自动将当前运行环境中的生效配置(如通过环境变量设置的端口、密码等)持久化写入数据目录 `data/config.js` 并同步上传至云端**进行初始化。这确保了您仅通过环境变量即可完成云端数据的首次“开荒”建立。 ### 四、 Web 端复合媒体播放空间防护逻辑 diff --git a/docs/guide/sync-server.md b/docs/guide/sync-server.md index 088cc5c6..79fbe267 100644 --- a/docs/guide/sync-server.md +++ b/docs/guide/sync-server.md @@ -7,7 +7,7 @@ LX Music Sync Server 包含一个 Web 管理控制台(位于系统根路径 `/ 访问管理控制台(默认 `http://IP:9527`)需要输入管理员密码。 - **默认密码**:`123456` -- **修改密码**:您可以通过设置环境变量 `FRONTEND_PASSWORD` 覆盖默认密码,或在首次登录后前往设置页面修改。密码会保存在服务端的 `config.json` 文件中。 +- **修改密码**:您可以通过设置环境变量 `FRONTEND_PASSWORD` 覆盖默认密码,或在首次登录后前往设置页面修改。密码会持久化保存在服务端的 `data/config.js` 文件中。 ## 功能模块说明 diff --git a/md/log/2.0.2-1.png b/md/log/2.0.2-1.png new file mode 100644 index 00000000..f82fade7 Binary files /dev/null and b/md/log/2.0.2-1.png differ diff --git a/md/log/2.0.2-2.png b/md/log/2.0.2-2.png new file mode 100644 index 00000000..8f85b28c Binary files /dev/null and b/md/log/2.0.2-2.png differ diff --git a/md/lxserver.md b/md/lxserver.md index ba708e09..60cf050c 100644 --- a/md/lxserver.md +++ b/md/lxserver.md @@ -6,7 +6,7 @@

Build Status - Version + Version Node Version License
diff --git a/md/lxserver_EN.md b/md/lxserver_EN.md index 20a8c4b2..2afc8a04 100644 --- a/md/lxserver_EN.md +++ b/md/lxserver_EN.md @@ -5,7 +5,7 @@

Build Status - Version + Version Node Version License
diff --git a/notice/notification.json b/notice/notification.json index c901e175..2ac86fb4 100644 --- a/notice/notification.json +++ b/notice/notification.json @@ -18,25 +18,25 @@ } }, { - "comment": "=== 版本更新 v2.0.1 ===", - "id": "update_v2_0_1", + "comment": "=== 版本更新 v2.0.2 ===", + "id": "update_v2_0_2", "status": "active", "type": "version", "logic": { - "target_version": "v2.0.1", + "target_version": "v2.0.2", "operator": "<", "interval_hours": 24 }, "ui": { - "title": "发现新版本 v2.0.1", - "date": "2026-07-23", - "message": "本次更新包含核心体验重构与多项功能增强:\n1. 📁 自定义歌曲目录与权限映射,支持多层目录索引与关联歌单\n2. 🌟 数据查看支持收藏专辑与关注歌手展示,分类筛选与列表查阅\n3. ⚙️ 自定义第三方音源平台独立开关 (#292) & 非管理员缓存下载细粒度权限控制 (#284)\n4. 🎧 Subsonic 接口与搜索响应全面优化 (#311, #325),PWA 自定义路径 404 修复与多项体验升级", + "title": "发现新版本 v2.0.2", + "date": "2026-09-08", + "message": "本次更新包含多项 Subsonic 功能增强与核心修复:\n1. 🏆 Subsonic 公开排行榜映射,在线排行榜只读暴露 (#348)\n2. 💾 Subsonic 播放时自动缓存,离线体验增强 (#339)\n3. ❤️ Subsonic 红心收藏双向联动与在线兜底解析 (#338)\n4. 📁 文件管理器界面现代化,Docker 升级数据保留无缝迁移 (#341, #345)\n5. 🐛 修复 Subsonic 封面拉取防崩、自定义目录 songmid 报错等多个问题 (#344, #347)", "confirm_text": "查看日志", "cancel_text": "稍后提醒" }, "action": { "type": "link", - "url": "https://github.com/XCQ0607/lxserver/releases/tag/v2.0.1" + "url": "https://github.com/XCQ0607/lxserver/releases/tag/v2.0.2" } } ] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index db173996..606ebc56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "lx-music-sync-server", - "version": "2.0.1", + "version": "2.0.2", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 69d3ef3c..e2fd96c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lx-music-sync-server", - "version": "2.0.1", + "version": "2.0.2", "private": true, "scripts": { "prebuild": "node scripts/download-binary.js && node scripts/update-build-hash.js", diff --git a/public/app.js b/public/app.js index 75274184..20c1b560 100644 --- a/public/app.js +++ b/public/app.js @@ -156,6 +156,10 @@ class App { document.querySelector('input[name="user.enablePublicRestriction"]')?.addEventListener('change', () => { this.togglePublicNonAdminLocalMusicVisibility(); }); + document.querySelector('input[name="subsonic.publicLeaderboards"]')?.addEventListener('change', () => { + this.toggleSubsonicLeaderboardVisibility(); + }); + this.initTagSelectors(); // 日志查看 document.getElementById('refresh-logs-btn')?.addEventListener('click', () => this.loadLogs()); @@ -2299,11 +2303,28 @@ class App { form.elements['subsonic.onlineSearchMode'].value = config['subsonic.onlineSearchMode'] || 'fallback'; } if (form.elements['subsonic.onlineSearchSources']) { - form.elements['subsonic.onlineSearchSources'].value = config['subsonic.onlineSearchSources'] || 'wy,tx,kw,kg,mg'; + const searchSources = config['subsonic.onlineSearchSources'] || 'wy,tx,kw,kg,mg'; + form.elements['subsonic.onlineSearchSources'].value = searchSources; + this.updateSearchSourcesTagUI(searchSources); + } + if (form.elements['subsonic.publicLeaderboards']) { + form.elements['subsonic.publicLeaderboards'].checked = config['subsonic.publicLeaderboards'] === true; + } + if (form.elements['subsonic.leaderboardSource']) { + const lbSource = config['subsonic.leaderboardSource'] || 'tx'; + form.elements['subsonic.leaderboardSource'].value = lbSource; + this.updateLeaderboardSourceTagUI(lbSource); } + this.toggleSubsonicLeaderboardVisibility(); if (form.elements['subsonic.lyricTranslation']) { form.elements['subsonic.lyricTranslation'].checked = config['subsonic.lyricTranslation'] !== false; } + if (form.elements['subsonic.cacheOnPlay']) { + form.elements['subsonic.cacheOnPlay'].checked = config['subsonic.cacheOnPlay'] === true; + } + if (form.elements['subsonic.playCacheFirst']) { + form.elements['subsonic.playCacheFirst'].checked = config['subsonic.playCacheFirst'] !== false; + } // 自定义歌曲目录配置 if (form.elements['user.enableCustomMusicDir']) { @@ -2318,6 +2339,80 @@ class App { } } + toggleSubsonicLeaderboardVisibility() { + const lbCb = document.querySelector('input[name="subsonic.publicLeaderboards"]'); + const childWrapper = document.getElementById('subsonic-leaderboard-options'); + if (lbCb && childWrapper) { + childWrapper.style.display = lbCb.checked ? 'block' : 'none'; + } + } + + initTagSelectors() { + // 排行榜平台单选 + const lbContainer = document.getElementById('tag-group-leaderboard-source'); + if (lbContainer) { + lbContainer.querySelectorAll('.tag-select-item').forEach(item => { + item.addEventListener('click', () => { + const val = item.getAttribute('data-value'); + const hiddenInput = document.querySelector('input[name="subsonic.leaderboardSource"]'); + if (hiddenInput) hiddenInput.value = val; + lbContainer.querySelectorAll('.tag-select-item').forEach(i => i.classList.remove('active')); + item.classList.add('active'); + }); + }); + } + + // 在线搜索平台多选 + const searchContainer = document.getElementById('tag-group-search-sources'); + if (searchContainer) { + searchContainer.querySelectorAll('.tag-select-item').forEach(item => { + item.addEventListener('click', () => { + const hiddenInput = document.querySelector('input[name="subsonic.onlineSearchSources"]'); + let activeVals = []; + item.classList.toggle('active'); + + searchContainer.querySelectorAll('.tag-select-item.active').forEach(act => { + activeVals.push(act.getAttribute('data-value')); + }); + + // 至少保留一个平台 + if (activeVals.length === 0) { + item.classList.add('active'); + activeVals.push(item.getAttribute('data-value')); + } + + if (hiddenInput) hiddenInput.value = activeVals.join(','); + }); + }); + } + } + + updateLeaderboardSourceTagUI(val) { + const container = document.getElementById('tag-group-leaderboard-source'); + if (!container) return; + container.querySelectorAll('.tag-select-item').forEach(item => { + if (item.getAttribute('data-value') === val) { + item.classList.add('active'); + } else { + item.classList.remove('active'); + } + }); + } + + updateSearchSourcesTagUI(valStr) { + const container = document.getElementById('tag-group-search-sources'); + if (!container) return; + const set = new Set((valStr || '').split(',').map(s => s.trim()).filter(Boolean)); + container.querySelectorAll('.tag-select-item').forEach(item => { + const v = item.getAttribute('data-value'); + if (set.has(v)) { + item.classList.add('active'); + } else { + item.classList.remove('active'); + } + }); + } + togglePublicNonAdminAccessVisibility() { const favCb = document.querySelector('input[name="user.enablePublicFavorites"]'); const childWrapper = document.getElementById('public-non-admin-access-wrapper'); @@ -2402,7 +2497,11 @@ class App { 'subsonic.onlineSearch': formData.get('subsonic.onlineSearch') === 'on', 'subsonic.onlineSearchMode': formData.get('subsonic.onlineSearchMode') || 'fallback', 'subsonic.onlineSearchSources': (formData.get('subsonic.onlineSearchSources') || '').trim() || 'wy,tx,kw,kg,mg', + 'subsonic.publicLeaderboards': formData.get('subsonic.publicLeaderboards') === 'on', + 'subsonic.leaderboardSource': (formData.get('subsonic.leaderboardSource') || '').trim() || 'tx', 'subsonic.lyricTranslation': formData.get('subsonic.lyricTranslation') === 'on', + 'subsonic.cacheOnPlay': formData.get('subsonic.cacheOnPlay') === 'on', + 'subsonic.playCacheFirst': formData.get('subsonic.playCacheFirst') === 'on', 'singer.sourcePriority': formData.get('singer.sourcePriority'), 'system.allowUnsafeVM': formData.get('system.allowUnsafeVM') === 'on', }; diff --git a/public/filemanager.html b/public/filemanager.html index 926cca26..d38730c6 100644 --- a/public/filemanager.html +++ b/public/filemanager.html @@ -17,7 +17,8 @@ - + + @@ -93,10 +94,7 @@ /* elFinder 样式优化 */ .elfinder { font-family: inherit !important; - } - - .elfinder-toolbar { - background: #f8f9fa !important; + border: none !important; }