From ce1b234b176599325f5a31bccf151e44342a0124 Mon Sep 17 00:00:00 2001 From: ShukeBta <272197458+ShukeBta@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:24:50 +0800 Subject: [PATCH] docs: simplify deployment docs and restrict telegram group commands --- README.md | 1171 +++++--------------- README_EN.md | 1148 +++++-------------- docker-compose.simple.env.example | 10 +- docker-compose.yml | 102 +- internal/service/telegram_api_test.go | 58 +- internal/service/telegram_bot.go | 34 + internal/service/telegram_bot_user_test.go | 68 ++ internal/service/telegram_commands.go | 87 +- internal/service/telegram_menu.go | 50 + 9 files changed, 877 insertions(+), 1851 deletions(-) diff --git a/README.md b/README.md index 860b222..ab6b2fb 100644 --- a/README.md +++ b/README.md @@ -4,1005 +4,368 @@ MediaStationGo Logo

-

轻量、漂亮、NAS 友好的私人媒体中心

+

轻量、好看、适合 NAS 的私人媒体中心

- Go 单二进制后端 · React 现代化前端 · Docker 一键部署 · Emby API 兼容 · 多源刮削 · PT 订阅下载 + Docker 一键部署 · 媒体库 · 刮削 · 播放 · 下载整理 · Emby 客户端兼容 · 网盘播放

English · - 快速开始 · - Docker 部署 · - 界面预览 · + 快速开始 · + Docker 部署 · + 常见问题 · 在线演示

-## 支持友链 - -## NodeSeek:[NodeSeek](https://www.nodeseek.com/) -## LINUX DO:[LINUX DO](https://linux.do/) -

Go React - TypeScript Docker License - Use

--- -## ✨ 项目简介 +## 一句话介绍 -MediaStationGo 是一个面向个人、家庭 NAS 与影音爱好者的开源媒体中心。它把「媒体库管理、自动刮削、在线播放、外部客户端兼容、PT 站点检索、订阅下载、AI 推荐」放进一个轻量的 Go 服务里,配合 React 前端提供统一、简洁、漂亮的三端体验。 +MediaStationGo 是一个给个人、家庭 NAS、影音爱好者使用的媒体管理系统。 -它适合这些场景: +你可以用它做这些事: -- 家里有 NAS / Windows 主机 / Linux 小主机,希望统一管理电影、剧集、动漫、综艺、成人内容。 -- 想使用 TMDb、豆瓣、Bangumi、TheTVDB、Fanart、JavBus/JavDB 等多源元数据补全海报、简介、分季分集信息。 -- 想把 PT 站点搜索、订阅、下载器、下载后整理集中到一个 Web 面板中。 -- 想让 Infuse、VidHub、SenPlayer 等外部客户端通过 Emby 风格接口访问媒体库。 -- 想要一个部署简单、便于二次开发、不会把密钥和私有 Token 暴露到前端的开源项目。 +- 把电影、电视剧、动漫、综艺、音乐整理成漂亮的媒体库。 +- 自动识别文件、补全海报、简介、年份、季集信息。 +- 在网页里播放,也可以用 Infuse、VidHub、SenPlayer、Emby 客户端等第三方播放器看。 +- 连接 qBittorrent,做搜索、订阅、下载、整理入库。 +- 接入 OpenList / CloudDrive2 / WebDAV 等外部存储,支持 STRMURL 与 302 反代播放。 +- 在 NAS、小主机、VPS、Windows Docker Desktop 上用 Docker Compose 快速运行。 -> 当前项目仍在快速迭代中,建议固定镜像版本部署,并定期备份 `/data` 目录。 +> 项目还在快速迭代。重要数据都在 `data` 目录,升级前建议先备份。 --- -## 🌱 开源承诺 +## 适合谁 -MediaStationGo 采用完全开源路线,核心媒体库、刮削、播放、订阅、下载、外部客户端兼容与运维能力均在本仓库持续迭代。项目参考了 MoviePilot 等优秀开源项目在「站点聚合、订阅下载、媒体整理、Emby/Jellyfin 客户端兼容」上的产品思路,但本项目会保持独立实现,不直接复制不兼容代码。 - -本项目当前基础许可证为 `GPL-3.0`,欢迎基于 GPL-3.0 协议参与改造、适配站点、提交刮削规则与优化 UI。项目作者同时倡议:本项目面向个人学习、家庭 NAS、自建影音与非商业场景使用,未经作者明确书面许可,不得将本项目或其衍生版本用于商业售卖、商业托管、付费 SaaS、预装售卖设备、闭源二次分发或其他商业化牟利用途。 - -> 说明:GPL-3.0 是自由软件许可证,其正式授权范围以仓库 [LICENSE](LICENSE) 文件为准;上方「非商用承诺」表达项目维护者的使用边界与商业合作要求。如需商业合作、企业部署或二次发行,请先联系作者获得额外授权。 - -### 源码开放与 Docker 部署边界 - -- 当前公开仓库继续以 `GPL-3.0` 作为基础许可证;如果代码包含 GPL 派生实现,不能通过“只发布 Docker 镜像”规避对应源码提供义务。 -- 可以把官方部署策略收敛为 **Docker-first / Docker-only support**:即项目只承诺维护 Docker Compose、GHCR 镜像和容器部署文档,裸机运行与二进制包可作为社区自助能力。 -- 若未来需要部分闭源,可能将闭源能力拆成独立插件、独立服务或私有模块,并确保该部分为作者自有或兼容许可证的干净实现;GPL 覆盖代码仍应按 GPL 公开。 -- README 中的非商用声明是维护者的使用边界与商业授权要求;正式代码授权仍以 [LICENSE](LICENSE) 为准。 +- **新手用户**:只想复制一份 `docker-compose.yml`,改几个路径就跑起来。 +- **NAS 用户**:想用低资源占用的媒体中心管理本地硬盘和网盘资源。 +- **PT / 下载用户**:想把下载、整理、刮削、播放放到一个后台。 +- **外部播放器用户**:想让第三方 APP 通过 Emby 风格接口读取媒体库。 +- **开发者**:想研究 Go + React 的自托管媒体项目。 --- -## 🚀 在线演示 +## 在线演示 -- 演示站:[https://mgo.3jzs.com](https://mgo.3jzs.com) -- 默认账号:`admin` -- 默认密码:`admin123` +- 地址:[https://mgo.3jzs.com](https://mgo.3jzs.com) +- 账号:`admin` +- 密码:`admin123` -> 演示环境仅用于功能体验,请勿上传真实隐私信息或配置私人 API Key。 +> 演示站只用于看功能,请不要填写私人 API Key、站点 Cookie 或真实隐私信息。 --- -## 🧭 功能总览 +## 快速开始 -| 模块 | 能力 | -| --- | --- | -| 媒体库 | 电影、电视剧、动漫、综艺、音乐、成人内容;支持文件夹封面、合集、季、集展示 | -| 扫描识别 | 递归扫描、ffprobe 探测、文件名解析、季集识别、综艺节目识别、重复扫描去重 | -| 本地元数据 | 优先读取 NFO、poster、fanart、season poster、episode image、本地成人影片图片 | -| 在线刮削 | TMDb、TheTVDB、Bangumi、豆瓣、Fanart.tv、JavBus/JavDB 页面直爬补全 | -| 播放体验 | 直链播放、HTTP Range 拖动、HLS 转码、外挂字幕、播放进度、继续观看、外部播放器 | -| 发现与搜索 | TMDb / 豆瓣 / Bangumi 多源推荐,智能搜索,详情页订阅入口 | -| PT 站点 | 站点管理、M-Team API Token、站点搜索、种子链接解析、下载器联动 | -| 订阅下载 | RSS / 站点搜索订阅,分辨率/质量/特效/发布组/排除词规则,洗版开关与优先级 | -| 下载中心 | qBittorrent 任务状态、速度、进度、上传下载体积、小卡片海报展示、私有 URL 脱敏 | -| 外部兼容 | Emby/Jellyfin 风格 API,兼容 Infuse、VidHub、SenPlayer 等外部客户端 | -| AI 能力 | OpenAI Compatible API 配置,AI 搜索、推荐、运维助手 | -| 运维工具 | 运行状态、任务队列、重复文件、回收站、文件管理、存储配置、通知渠道 | - ---- - - - -## 🖼️ 界面预览 - -> 以下截图使用 Codex 内置浏览器从当前运行实例采集,并已对个人媒体内容、本地路径、账号信息、API Key/Token/密钥等敏感信息做图像级打码处理。 - -
-核心体验 - -| 登录与首页 | 媒体库总览 | -| --- | --- | -| 登录界面 | 系统首页 | -| 媒体库总览 | 媒体库详情 | -| 媒体库总览 | 媒体库详情 | -| 海报墙 | 媒体详情 | -| 海报墙 | 媒体详情 | -| 播放器 | 精彩发现 | -| 播放器 | 精彩发现 | -| 智能搜索 | DLNA 投屏 | -| 智能搜索 | DLNA 投屏 | - -
- -
-个人空间与播放管理 - -| AI 助理 | 我的收藏 | -| --- | --- | -| AI 助理 | 我的收藏 | -| 播放列表 | 观看历史 | -| 播放列表 | 观看历史 | -| 账号信息 | 下载中心 | -| 账号信息 | 下载中心 | - -
- -
-下载订阅与站点 - -| 下载器管理 | 订阅管理 | -| --- | --- | -| 下载器管理 | 订阅管理 | -| 站点检索 | 站点与下载器 | -| 站点检索 | 站点与下载器 | - -
- -
-管理与运维 - -| 媒体与用户 | 整理与维护 | -| --- | --- | -| 媒体与用户 | 整理与维护 | -| 存储与文件 | 运行状态 | -| 存储与文件 | 运行状态 | -| 系统设置 | 任务队列 | -| 系统设置 | 任务队列 | -| 重复文件 | 回收站 | -| 重复文件 | 回收站 | -| 调度任务 | 文件管理 | -| 调度任务 | 文件管理 | -| STRM 管理 | 存储配置 | -| STRM 管理 | 存储配置 | -| 通知渠道 | AI 运维助手 | -| 通知渠道 | AI 运维助手 | - -
- ---- - -## 🧱 技术栈 - -| 层级 | 技术 | 说明 | -| --- | --- | --- | -| 后端语言 | Go 1.25+ | 单二进制部署,启动快,资源占用低 | -| Web 框架 | Gin | REST API、鉴权中间件、静态资源托管 | -| 数据库 | SQLite + GORM | 适合个人/NAS 场景,数据文件易备份 | -| 前端框架 | React 18 + TypeScript | 组件化 UI,类型安全 | -| 构建工具 | Vite | 前端快速开发与生产打包 | -| 样式系统 | Tailwind CSS | 统一浅色高级视觉方案与响应式布局 | -| 状态管理 | Zustand | 轻量全局状态与鉴权状态维护 | -| 播放链路 | HTML5 Video / HLS / FFmpeg | 直链、Range、HLS 转码、字幕 | -| 元数据 | TMDb / 豆瓣 / Bangumi / TheTVDB / Fanart / JavBus / JavDB | 多源补全海报、简介、评分、分季分集 | -| 下载联动 | qBittorrent / PT Site Adapter | 站点搜索、订阅、下载任务展示与脱敏 | -| 外部兼容 | Emby-style API / DLNA | 面向外部播放器与三端客户端 | -| 部署 | Docker / Docker Compose / Shell / PowerShell | NAS、Linux、Windows 均可部署 | -| CI/CD | GitHub Actions / GHCR | 版本标签或手动 Actions 自动发布多架构镜像与 Release 包 | - ---- - - - -## 📦 快速开始 - - - -### Docker Compose 部署(推荐) - -默认部署主线改为**直接编辑 `docker-compose.yml`**:把 NAS/服务器真实目录写进 compose 文件里,路径最直观,也最适合刚接触 NAS、VPS 或 Docker 的用户。 - -#### 1. 创建部署目录 - -```bash -mkdir -p ~/MediaStationGo -cd ~/MediaStationGo -mkdir -p data cache media downloads -``` - -#### 2. 下载 compose 文件 +最推荐新手使用 Docker Compose。不要一开始就折腾 `.env`、裸机运行、源码编译。 ```bash +mkdir -p MediaStationGo +cd MediaStationGo curl -fsSL https://raw.githubusercontent.com/ShukeBta/MediaStationGo/main/docker-compose.yml -o docker-compose.yml ``` -如果 GitHub Raw 访问慢,可以手动创建 `docker-compose.yml`,内容以仓库根目录的 `docker-compose.yml` 为准。 - -#### 3. 编辑真实路径 - -打开 compose: +编辑 `docker-compose.yml`: ```bash vi docker-compose.yml ``` -先找到 `volumes` 里的媒体库和下载目录,把左边改成你的 NAS/服务器真实路径,例如: - -```yaml - volumes: - - ./data:/data - - ./cache:/cache - - /vol1/1000/Docker/moviepilot-v2/media:/media:ro - - /vol1/1000/qBittorrent/downloads:/downloads -``` - -再找到 `environment` 里的路径提示,把同样的真实路径写进去: - -```yaml - environment: - MEDIASTATION_MEDIA_DIR: /vol1/1000/Docker/moviepilot-v2/media - MEDIASTATION_MEDIA_CONTAINER_DIR: /media - MEDIASTATION_DOWNLOAD_DIR: /vol1/1000/qBittorrent/downloads - MEDIASTATION_DOWNLOAD_CONTAINER_DIR: /downloads -``` - -路径怎么理解: - -| 位置 | 填什么 | -| --- | --- | -| `volumes` 左侧媒体路径 | NAS/服务器上的真实媒体目录,例如 `/volume1/media`、`/mnt/media`、`/vol1/1000/Docker/moviepilot-v2/media` | -| `volumes` 左侧下载路径 | NAS/服务器上的真实下载目录,例如 `/volume1/downloads`、`/mnt/downloads`、`/vol1/1000/qBittorrent/downloads` | -| `MEDIASTATION_MEDIA_DIR` | 和媒体库 `volumes` 左侧保持一致 | -| `MEDIASTATION_DOWNLOAD_DIR` | 和下载目录 `volumes` 左侧保持一致 | - -> 不要写成 `./vol1/...`。带 `./` 是相对路径,会变成当前部署目录下面的文件夹。 - -#### 4. 启动 +然后启动: ```bash -docker compose pull docker compose up -d ``` -如果你的系统只有旧版命令: - -```bash -docker-compose pull -docker-compose up -d -``` - -#### 5. 访问 +浏览器打开: ```text -http://<服务器IP>:18080 +http://服务器IP:18080 ``` -默认账号: +默认登录: ```text -admin / admin123 +账号:admin +密码:admin123 ``` -首次登录后请立即修改管理员密码。 - -#### 6. 页面里路径怎么填 - -默认 compose 会把你的宿主机目录映射为容器路径: - -| 宿主机真实目录 | 容器内路径 | 页面里建议填写 | -| --- | --- | --- | -| `MEDIASTATION_MEDIA_DIR` | `/media` | `/media/电影`、`/media/电视剧`、`/media/电视剧/国产剧` | -| `MEDIASTATION_DOWNLOAD_DIR` | `/downloads` | 下载器保存根目录填 `/downloads` | - -这只是 Docker 绑定挂载,不会复制文件,不会占用双倍空间。MediaStationGo 读取的是原目录内容,只是在容器里显示成 `/media` 和 `/downloads`。 - -#### 7. 更新镜像 - -推荐使用仓库脚本,它会更新容器并清理本项目旧镜像,避免 NAS 磁盘被旧镜像占满: - -```bash -curl -fsSL https://cdn.jsdelivr.net/gh/ShukeBta/MediaStationGo@main/scripts/docker-compose-update.sh -o docker-compose-update.sh -chmod +x docker-compose-update.sh -./docker-compose-update.sh -``` - -#### 8. 查看状态和日志 - -```bash -docker compose ps -docker compose logs -f mediastation-go -``` - -#### 高级配置 - -默认文件只保留新手必需项。需要让容器内也显示宿主机原始路径、Telegram 代理、硬件加速、更多转码环境变量时,参考仓库中的高级示例: - -```text -docker-compose.advanced.yml -``` - -### 可选:使用 `.env` 管理路径 - -如果你已经熟悉 Docker Compose,或者同一份 compose 要在多台机器复用,也可以把路径写进 `.env`。这是可选玩法,不是新手主线: - -```bash -cat > .env <<'EOF' -MEDIASTATION_MEDIA_DIR=/vol1/1000/Docker/moviepilot-v2/media -MEDIASTATION_DOWNLOAD_DIR=/vol1/1000/qBittorrent/downloads -MEDIASTATION_HTTP_PORT=18080 -TZ=Asia/Shanghai -PUID=1000 -PGID=1000 -EOF -``` - -使用 `.env` 时,`docker-compose.yml` 中保持默认变量写法即可: - -```yaml -- ${MEDIASTATION_MEDIA_DIR:-./media}:/media:ro -- ${MEDIASTATION_DOWNLOAD_DIR:-./downloads}:/downloads -``` - -### 固定版本部署 - -生产环境可以固定镜像版本,避免 `latest` 自动变化。不使用 `.env` 时,直接把 compose 里的镜像改成固定版本: - -```yaml -image: ghcr.io/shukebta/mediastation-go:MediaStationGo-v0.0.32 -``` - -如果使用 `.env`,也可以写: - -```env -MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.32 -``` - -然后执行: - -```bash -docker compose pull -docker compose up -d -``` - -### 外网访问与 v2rayA 说明 - -如果 NAS 已经开启 v2rayA 的 `redirect` / 透明代理,并使用大陆白名单分流,MediaStationGo 通常不需要额外配置 `HTTP_PROXY` / `HTTPS_PROXY`。 - -不建议在 `docker-compose.yml` 或 `.env` 中重复配置代理环境变量,因为这会让容器网络和 Docker 拉镜像路径叠加代理,反而可能导致 GHCR、M-Team、TMDB 等访问异常。 - -### qBittorrent 连接怎么填 - -如果 qBittorrent 运行在同一台 NAS/宿主机上,MediaStationGo 容器里不要填 `127.0.0.1`;推荐在「下载器管理」中填写: - -```text -http://host.docker.internal:8085 -``` - -如果 qBittorrent 也运行在 Docker 中,建议让 qBittorrent 和 MediaStationGo 都把同一个宿主机下载目录映射为 `/downloads`。订阅保存根目录填写: - -```text -/downloads -``` - -启用智能分类后,下载会自动进入 `/downloads/动画电影`、`/downloads/国产剧`、`/downloads/综艺` 等分类目录。 - -### 网盘、OpenList 与 CloudDrive2 - -MediaStationGo 内置 OpenList、Alist、WebDAV、115、夸克和 CloudDrive2 外部存储配置。推荐新手优先使用 OpenList、CloudDrive2 或 Alist 作为桥接层:它们可以对接 115、123、阿里、夸克等多种网盘,项目通过 OpenList/Alist API 或 WebDAV 入口即可完成浏览、挂载媒体库、本地媒体转存和反代播放。 - -OpenList 推荐用法: - -1. 在 OpenList 中挂载你的 115 / 123 / 阿里 / 夸克等网盘。 -2. 在 MediaStationGo 的「外部存储」中选择 `OpenList`。 -3. `OpenList Server URL` 填管理/API 地址,例如 `http://NAS-IP:5244`。 -4. `WebDAV URL` 填 WebDAV 地址,例如 `http://NAS-IP:5244/dav/`。 -5. 如果 OpenList 没有配置 HTTPS 反向代理,不要填写 `https://`。`Propfind "https://...": http: server gave HTTP response to HTTPS client` 表示你把 HTTP 服务误填成了 HTTPS。 - -CloudDrive2 推荐用法: - -1. 在 CloudDrive2 中挂载你的 115 / 123 / 阿里 / 夸克等网盘。 -2. 在 MediaStationGo 的「外部存储」中选择 `CloudDrive2`。 -3. 填写 CloudDrive2 WebDAV 地址,例如 `http://host.docker.internal:19798/dav` 或 `http://NAS-IP:19798/dav`。 -4. 保存后可直接浏览网盘目录,或挂载为媒体库进行扫描播放。 - -115 原生接口保留 Cookie / 扫码登录、目录浏览和 302 播放能力;本地文件上传建议优先走 OpenList / CloudDrive2 / Alist 桥接,避免在项目内维护各网盘私有分片上传协议。 - --- -## 🐳 Docker Compose 配置示例 +## Docker Compose 推荐部署 -项目默认内置极简 `docker-compose.yml`。新手推荐直接在 `volumes` 左侧写真实路径,并让 `MEDIASTATION_MEDIA_DIR`、`MEDIASTATION_DOWNLOAD_DIR` 与左侧路径保持一致;`.env` 变量适合进阶用户复用配置: +仓库里的 `docker-compose.yml` 已经是最简单模板:默认不用 `.env`。 -| 变量 | 默认值 | 说明 | +你只需要重点看 `volumes` 这一段: + +```yaml +volumes: + - ./data:/data + - ./cache:/cache + - ./media:/media:ro + - ./downloads:/downloads +``` + +含义很简单: + +| 左边 | 右边 | 说明 | | --- | --- | --- | -| `MEDIASTATION_IMAGE_TAG` | `latest` | 镜像标签,建议固定为 Release 版本 | -| `MEDIASTATION_HTTP_PORT` | `18080` | 宿主机访问端口 | -| `MEDIASTATION_DATA_DIR` | `./data` | 数据持久化目录 | -| `MEDIASTATION_CACHE_DIR` | `./cache` | 图片和转码缓存目录 | -| `MEDIASTATION_MEDIA_DIR` | `./media` | 媒体库宿主机目录;NAS 建议写 `/your-nas/media` 这种绝对路径 | -| `MEDIASTATION_DOWNLOAD_DIR` | `./downloads` | 下载保存宿主机目录;NAS 建议写 `/your-nas/downloads` 这种绝对路径 | -| `PUID` / `PGID` | `1000` / `1000` | Linux/NAS 文件权限映射 | -| `TZ` | `Asia/Shanghai` | 容器时区 | +| `./data` | `/data` | 程序数据库、配置、账号信息;一定要备份 | +| `./cache` | `/cache` | 缓存目录;可清理 | +| `./media` | `/media` | 媒体库目录;网页里添加媒体库时填 `/media/...` | +| `./downloads` | `/downloads` | 下载目录;文件管理和自动整理会用 | -查看日志: +如果你的媒体在 NAS 真实目录,例如: + +```text +/vol1/1000/Media +/vol1/1000/Downloads +``` + +就把 compose 改成: + +```yaml +volumes: + - ./data:/data + - ./cache:/cache + - /vol1/1000/Media:/media:ro + - /vol1/1000/Downloads:/downloads + +environment: + MEDIASTATION_MEDIA_DIR: /vol1/1000/Media + MEDIASTATION_DOWNLOAD_DIR: /vol1/1000/Downloads +``` + +注意: + +- `volumes` 左边是宿主机 / NAS 的真实路径。 +- `volumes` 右边是容器里的路径,建议固定用 `/media` 和 `/downloads`。 +- 在网页里新建媒体库时,填容器路径,例如 `/media/电影`、`/media/电视剧`。 +- 不要把 NAS 绝对路径写成 `./vol1/...`,`./` 表示当前部署目录下面的相对路径。 +- Windows Docker Desktop 可以写成 `D:/Media:/media:ro`、`D:/Downloads:/downloads`。 + +### 最简单 compose 示例 + +仓库根目录的 `docker-compose.yml` 就是这个思路。你也可以手动创建: + +```yaml +services: + mediastation-go: + image: ghcr.io/shukebta/mediastation-go:latest + container_name: mediastation-go + restart: unless-stopped + init: true + + # 访问端口:浏览器打开 http://服务器IP:18080 + ports: + - "18080:8080" + + # 让容器可以访问宿主机上的 qBittorrent: + # qB 地址可填 http://host.docker.internal:8085 + extra_hosts: + - "host.docker.internal:host-gateway" + + volumes: + # 程序数据,升级前备份这个目录。 + - ./data:/data + - ./cache:/cache + + # 新手先用当前目录下的 media/downloads。 + # NAS 用户把左边改成真实绝对路径。 + - ./media:/media:ro + - ./downloads:/downloads + + environment: + TZ: Asia/Shanghai + PUID: "1000" + PGID: "1000" + + MEDIASTATION_APP_HOST: 0.0.0.0 + MEDIASTATION_APP_PORT: 8080 + MEDIASTATION_APP_WEB_DIR: /app/web/dist + MEDIASTATION_APP_DATA_DIR: /data + MEDIASTATION_DATABASE_DB_PATH: /data/mediastation.db + MEDIASTATION_CACHE_CACHE_DIR: /cache + + # 如果上面的 ./media / ./downloads 改成 NAS 真实路径, + # 这里也改成同样的宿主机真实路径。 + MEDIASTATION_MEDIA_DIR: ./media + MEDIASTATION_MEDIA_CONTAINER_DIR: /media + MEDIASTATION_DOWNLOAD_DIR: ./downloads + MEDIASTATION_DOWNLOAD_CONTAINER_DIR: /downloads +``` + +--- + +## 首次进入后怎么配置 + +1. **新建媒体库** + - 进入「媒体库」页面。 + - 路径填容器路径,例如 `/media/电影`。 + - 点扫描。 + +2. **配置下载器** + - 进入「下载器管理」。 + - 如果 qBittorrent 在宿主机上,地址通常填 `http://host.docker.internal:8085`。 + +3. **配置刮削源** + - 进入「系统设置 / 外部 API」。 + - 按需填写 TMDb、Bangumi、TheTVDB、Fanart、豆瓣等配置。 + +4. **配置外部播放器** + - 第三方客户端按 Emby/Jellyfin 方式添加服务器。 + - 地址填 `http://服务器IP:18080`。 + - 使用 MediaStationGo 的账号密码登录。 + +5. **配置网盘播放** + - 进入「外部存储」配置 OpenList、CloudDrive2、WebDAV 等。 + - 后台播放策略可以选择 STRMURL 或 302 反代。 + - 开启哪个,就优先走哪个;都关闭时走普通服务端播放链路。 + +--- + +## 更新、备份、日志 + +### 更新 + +```bash +docker compose pull +docker compose up -d +``` + +### 查看日志 ```bash docker logs -f mediastation-go ``` -更新镜像: +### 备份 -```bash -curl -fsSL https://cdn.jsdelivr.net/gh/ShukeBta/MediaStationGo@main/scripts/docker-compose-update.sh -o docker-compose-update.sh -chmod +x docker-compose-update.sh -./docker-compose-update.sh +重点备份: + +```text +data/ ``` -说明:普通 `docker compose pull && docker compose up -d` 只会切换到新镜像,不会自动删除旧镜像;上面的脚本会保留当前正在运行的 MediaStationGo 镜像,删除同仓库未使用的旧镜像,并执行 `docker image prune -f` 清理 dangling 层。如需进一步清理所有未使用镜像,可临时执行 `PRUNE_ALL_UNUSED=1 ./docker-compose-update.sh`。 +这里面有数据库、用户、设置、部分运行状态。`cache/` 通常不用备份。 -停止服务: +### 停止 ```bash docker compose down ``` -备份数据: - -```bash -tar -czf mediastationgo-data-backup.tgz ./data -``` - --- -## 🖥️ 一键脚本部署 +## 常见问题 -如果不想使用 Docker,也可以裸机运行。脚本会自动构建前端、编译后端、启动服务并检查健康状态。 +### 1. 页面打不开? -### Linux / macOS - -```bash -git clone https://github.com/ShukeBta/MediaStationGo.git -cd MediaStationGo -chmod +x scripts/deploy.sh -PORT=18080 DATA_DIR=/opt/mediastation/data CACHE_DIR=/opt/mediastation/cache ./scripts/deploy.sh -``` - -### Windows PowerShell - -```powershell -git clone https://github.com/ShukeBta/MediaStationGo.git -cd MediaStationGo -.\scripts\deploy.ps1 -Port 18080 -DataDir D:\MediaStationGo\data -CacheDir D:\MediaStationGo\cache -``` - -脚本执行内容: - -1. 安装前端依赖并构建 `web/dist` -2. 编译 Go 服务端到 `bin/` -3. 创建数据目录和缓存目录 -4. 停止旧进程并启动新进程 -5. 请求 `/api/health` 验证服务状态 - ---- - -## 🧩 Release 包部署 - -每个 Release 会提供多平台压缩包: - -| 平台 | 包名示例 | -| --- | --- | -| Linux x86_64 | `MediaStationGo-v0.0.32-linux-amd64.tar.gz` | -| Linux ARM64 | `MediaStationGo-v0.0.32-linux-arm64.tar.gz` | -| Windows x86_64 | `MediaStationGo-v0.0.32-windows-amd64.zip` | -| macOS Intel | `MediaStationGo-v0.0.32-darwin-amd64.tar.gz` | -| macOS Apple Silicon | `MediaStationGo-v0.0.32-darwin-arm64.tar.gz` | - -部署步骤: - -```bash -# Linux 示例 -tar -xzf MediaStationGo-v0.0.32-linux-amd64.tar.gz -cd MediaStationGo-v0.0.32-linux-amd64 -MEDIASTATION_APP_PORT=18080 ./mediastation-go -``` - -Windows: - -```powershell -Expand-Archive .\MediaStationGo-v0.0.32-windows-amd64.zip -cd .\MediaStationGo-v0.0.32-windows-amd64 -$env:MEDIASTATION_APP_PORT = "18080" -.\mediastation-go.exe -``` - -> Release 二进制默认监听 `8080`,如果希望和 Docker 示例保持一致,请按上方设置 `MEDIASTATION_APP_PORT=18080`。 - ---- - -## 🛠️ 本地开发 - -### 环境要求 - -| 组件 | 版本 | 用途 | -| --- | --- | --- | -| Go | 1.25+ | 后端编译与测试 | -| Node.js | 20+ | 前端构建 | -| FFmpeg / ffprobe | 推荐安装 | 媒体探测与转码 | -| Docker | 可选 | 容器部署与多架构镜像 | -| qBittorrent | 可选 | 下载器联动测试 | - -### 本地构建 - -```bash -cp config.example.yaml config.yaml -cd web -npm ci -npm run build -cd .. -go build -o bin/mediastation-go ./cmd/server -./bin/mediastation-go -``` - -Windows: - -```powershell -Copy-Item config.example.yaml config.yaml -Set-Location web -npm ci -npm run build -Set-Location .. -go build -o bin\mediastation-go.exe .\cmd\server -.\bin\mediastation-go.exe -``` - -### 常用命令 - -```bash -make build # 构建前后端 -make test # 运行 Go 测试 -make smoke # 冒烟测试 -make docker # docker compose up -d -make deploy # Linux 一键部署 -make docker-push # buildx 多架构推送 -``` - ---- - -## 🏗️ 项目结构 - -```text -MediaStationGo/ -├── cmd/server/ # 服务入口 -├── internal/ -│ ├── config/ # 配置加载与默认值 -│ ├── database/ # SQLite 初始化与迁移 -│ ├── handler/ # HTTP API / Emby API / 管理接口 -│ ├── middleware/ # 鉴权、权限、日志中间件 -│ ├── model/ # GORM 数据模型 -│ ├── repository/ # 数据访问层 -│ └── service/ # 扫描、刮削、播放、下载、订阅等业务逻辑 -├── web/ -│ ├── public/ # favicon 等静态资源 -│ ├── src/ # React 页面、组件、API、状态管理 -│ └── dist/ # 前端构建产物,默认不入库 -├── scripts/ # 部署、打包、Docker 构建脚本 -├── docs/ # 设计文档、截图与架构说明 -├── docker-compose.yml # 默认 Docker Compose 部署文件 -├── Dockerfile # 多阶段镜像构建 -├── config.example.yaml # 配置模板 -└── README.md / README_EN.md # 项目文档 -``` - ---- - -## ⚙️ 配置说明 - -配置优先级从低到高: - -1. 内置默认值 -2. `config.yaml` -3. `config/*.yaml` -4. `MEDIASTATION_` 环境变量 -5. 后台数据库运行时配置 - -常用环境变量: - -| 变量 | 默认值 | 说明 | -| --- | --- | --- | -| `MEDIASTATION_APP_HOST` | `0.0.0.0` | 服务监听地址 | -| `MEDIASTATION_APP_PORT` | `8080` | 服务监听端口 | -| `MEDIASTATION_APP_WEB_DIR` | `./web/dist` | 前端静态资源目录 | -| `MEDIASTATION_APP_DATA_DIR` | `./data` | 程序数据目录 | -| `MEDIASTATION_DATABASE_DB_PATH` | `./data/mediastation.db` | SQLite 数据库路径 | -| `MEDIASTATION_CACHE_CACHE_DIR` | `./cache` | 图片/转码缓存目录 | -| `MEDIASTATION_SECRETS_JWT_SECRET` | 自动生成 | JWT 和敏感配置加密种子 | -| `MEDIASTATION_APP_CORS_ORIGINS` | 空 | 额外允许的跨域来源 | -| `MEDIASTATION_TELEGRAM_API_BASE_URL` | `https://api.telegram.org` | Telegram Bot API 地址;网络受限时可填写反代地址 | -| `MEDIASTATION_TELEGRAM_PROXY_URL` | 空 | Telegram 出站代理,例如 `http://172.17.0.1:7890` 或 `socks5://172.17.0.1:1080` | - -后台可运行时配置: - -- API Key:TMDb、Bangumi、TheTVDB、Fanart、OpenAI Compatible 等。 -- 站点:M-Team、NexusPHP、Unit3D、自定义 RSS 等。 -- 下载器:qBittorrent、Transmission、Aria2。 -- 通知渠道:Telegram、Bark、Webhook、Email 等。Telegram 渠道支持单独配置 API 反代与代理地址,测试通知失败时错误信息会自动隐藏 Bot Token。 -- 播放配置、权限配置、调度任务、存储配置。 - ---- - -## 👥 用户与权限策略 - -- 默认管理员由系统首次启动创建,默认账号为 `admin / admin123`;该默认管理员可以改用户名,但不能删除,也不能降级,始终拥有最高权限。 -- 开源版默认最多允许 20 个用户,避免家庭 NAS 或公开测试环境被滥用;绑定私有授权服务后可按授权策略提升用户额度。 -- 管理后台新增用户默认为“观看用户”:允许登录 Web 与 Emby 兼容客户端、浏览媒体库、播放媒体、使用外部播放器、收藏与记录观看历史。 -- 普通观看用户默认不能扫描媒体库、重新刮削、删除媒体、探测媒体轨、写出 NFO、管理文件、管理 STRM、管理下载器、创建下载任务或订阅下载。 -- 由于视频流播放本身需要向客户端传输媒体数据,系统可以禁止“下载任务”和管理型下载入口,但无法从协议层完全阻止外部播放器或浏览器保存已授权播放的数据流。 - ---- - -## 🔐 私有授权服务 - -MediaStationGo 已预留并接入私有独立的 `MediaStationLicenseServer`: - -- 授权服务器:`ShukeBta/MediaStationLicenseServer`,本地备份路径示例为 `C:\Users\Administrator\WorkBuddy\license_server_backup`。 -- 主项目后端提供 `/api/license/activate`、`/api/license/status`、`/api/license/heartbeat`,由服务端代理调用 License Server,不在浏览器暴露 HMAC 密钥。 -- License Server 公共接口使用 `/api/v1/activate`、`/api/v1/status/:fingerprint`、`/api/v1/heartbeat`。 -- 在「系统设置 → 授权服务」填写 `license.server_url` 与 `license.hmac_secret`,然后在「授权许可」页面绑定授权码。 -- 未绑定或授权失效时保持开源版能力;授权有效时当前实现将用户额度提升到授权版额度。 - -环境变量示例: - -```bash -MEDIASTATION_LICENSE_SERVER_URL=http://127.0.0.1:8001 -MEDIASTATION_LICENSE_HMAC_SECRET=与 License Server 的 LICENSE_HMAC_SECRET 一致 -``` - ---- - -## 🎞️ FFmpeg / ffprobe 按需运行 - -MediaStationGo 不会把 `ffmpeg` 或 `ffprobe` 作为常驻守护进程启动。它们只在以下场景被临时调用: - -- 扫描或手动探测媒体轨时调用 `ffprobe`。 -- 浏览器无法直放、需要 HLS 转码时调用 `ffmpeg`。 -- 管理后台手动检测工具状态或手动安装工具时短暂调用版本检测/安装逻辑。 - -播放停止、转码任务取消或服务退出时,后台会结束对应转码任务。空闲状态下如果没有扫描、探测或转码,`ffmpeg/ffprobe` 不应持续占用 CPU。 - -默认 HLS 转码使用 NAS 友好的低负载策略:`MEDIASTATION_TRANSCODER_ENABLED=true` 是总开关,关闭后不会启动 ffmpeg 转码;`MEDIASTATION_TRANSCODER_HARDWARE_ACCEL=false` 是硬件加速总开关,只有开启后才会使用 `MEDIASTATION_TRANSCODER_ENCODER=nvenc/qsv/vaapi`;`MEDIASTATION_TRANSCODER_REALTIME=true` 按播放速度处理输入,`MEDIASTATION_TRANSCODER_THREADS=2` 限制软件编码线程,`MEDIASTATION_TRANSCODER_MAX_CONCURRENT=1` 限制同时转码数量,`MEDIASTATION_TRANSCODER_IDLE_TIMEOUT_SECONDS=120` 在播放器停止请求分片后自动结束 ffmpeg。 - -默认 Docker 镜像使用精简运行层,不再内置 Intel VAAPI / mesa 驱动依赖,以降低 Docker Hub 漏洞扫描暴露面。需要自构建 Intel VAAPI/QSV 镜像时,可使用 `docker buildx build --build-arg WITH_VAAPI=true ...`;NVIDIA NVENC 仍主要依赖宿主机安装 NVIDIA Container Toolkit 并在运行容器时启用 GPU。 - ---- - -## 🔍 刮削与元数据策略 - -MediaStationGo 的刮削顺序尽量避免重复请求和错误覆盖: - -1. 优先读取本地 NFO、poster、fanart、season poster、episode image。 -2. 根据文件名识别电影、剧集、动漫、综艺、成人内容。 -3. 使用 TMDb / TheTVDB / Bangumi / 豆瓣补全缺失元数据。 -4. 使用 Fanart.tv 补充更高清的艺术图。 -5. 成人内容优先读取本地 NFO 与图片,再通过 JavBus/JavDB 等公开页面补全。 -6. 已有本地元数据不会被无意义重复刮削覆盖。 - -推荐目录结构: - -```text -/media/Movies/Inception (2010)/Inception (2010).mkv -/media/TV/Some Show/Season 01/Some Show S01E01.mkv -/media/Anime/Anime Title/Season 01/Anime Title S01E01.mkv -/media/Variety/Show Name/Season 2026/Show Name S2026E01.mkv -/media/Adult/ABCD-123/ABCD-123.mp4 -``` - -本地图片常见命名: - -```text -poster.jpg -fanart.jpg -folder.jpg -season01-poster.jpg -S01E01-thumb.jpg -movie.nfo -tvshow.nfo -episode.nfo -``` - -### 智能分类目录规则 - -MediaStationGo 的智能分类分为两个阶段: - -1. 下载阶段:根据订阅类型、搜索结果分类和标题特征,把任务保存到下载器根目录下的分类子目录。 -2. 整理阶段:用户可选择手动整理,或开启自动整理;整理时先进入媒体库一级目录,再进入二级分类目录。 - -推荐宿主机目录: - -```text -/your-nas/downloads -/your-nas/media/电影 -/your-nas/media/电视剧 -``` - -对应容器内目录: - -```text -/downloads -/media/电影 -/media/电视剧 -``` - -下载器智能分类示例: - -```text -/downloads/动画电影 -/downloads/国产剧 -/downloads/国漫 -/downloads/华语电影 -/downloads/日番 -/downloads/外语电影 -/downloads/综艺 -``` - -整理后媒体库示例: - -```text -/media/电视剧/国产剧/剧名 (2026)/Season 01/剧名 - S01E01 - 第 1 集.mkv -/media/电视剧/国漫/动画名 (2026)/Season 01/动画名 - S01E01 - 第 1 集.mkv -/media/电视剧/欧美剧/剧名 (2026)/Season 01/剧名 - S01E01 - 第 1 集.mkv -/media/电视剧/日番/番剧名 (2026)/Season 01/番剧名 - S01E01 - 第 1 集.mkv -/media/电视剧/日韩剧/剧名 (2026)/Season 01/剧名 - S01E01 - 第 1 集.mkv -/media/电视剧/综艺/综艺名 (2026)/Season 2026/综艺名 - S2026E01 - 第 1 集.mp4 -/media/电影/动画电影/电影名 (2026)/电影名 (2026) - 1080p.mkv -/media/电影/华语电影/电影名 (2026)/电影名 (2026) - 1080p.mkv -/media/电影/外语电影/电影名 (2026)/电影名 (2026) - 1080p.mkv -``` - -如果媒体库根目录直接设置为 `/media`,整理器会在启用智能分类时自动补上 `电影/` 或 `电视剧/` 一级目录;如果你已经把媒体库设置为 `/media/电影` 或 `/media/电视剧`,则不会重复追加一级目录。 - -自动整理与手动整理是两件事: - -- `downloads.smart_classify`:控制订阅下载 / 站点搜索下载是否自动按媒体分类写入下载目录和 qB 分类,默认开启。 -- `organizer.smart_classify`:只控制是否使用智能分类目录。 -- `organizer.auto_after_download` / `organize.auto`:控制下载完成后是否自动整理。 -- 未开启自动整理时,可以在「整理与维护」页面手动整理媒体库或单个媒体。 - -### 整理与刮削命名模板 - -整理规则建议按媒体类型拆分。剧集、动漫、综艺都属于连续剧集类,应保留剧名、年份、季目录、季集号和分集标题;电影类则保留片名、年份、分段和视频规格。 - -剧集 / 动漫 / 综艺推荐模板(参考MPV2模板): - -```jinja -{{title}}{% if year %} ({{year}}){% endif %}/Season {{season}}/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}} 集{% endif %}{{fileExt}} -``` - -输出示例: - -```text -孤独的美食家 (2024)/Season 01/孤独的美食家 - S01E01 - 第 1 集.mkv -某动画 (2025)/Season 02/某动画 - S02E03 - 第 3 集.mkv -某综艺 (2026)/Season 2026/某综艺 - S2026E01 - 第 1 集.mp4 -``` - -电影推荐模板: - -```jinja -{{title}}{% if year %} ({{year}}){% endif %}/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}{{fileExt}} -``` - -输出示例: - -```text -盗梦空间 (2010)/盗梦空间 (2010) - 1080p.mkv -沙丘 (2021)/沙丘 (2021)-CD1 - 2160p.mkv -``` - -常用变量说明: - -| 变量 | 说明 | -| --- | --- | -| `title` | 媒体标题,优先使用本地 NFO / 在线元数据识别后的标题 | -| `year` | 年份,存在时追加到目录和文件名中 | -| `season` | 季号,剧集/动漫/综艺用于生成 `Season 01` 等目录 | -| `season_episode` | 季集号,例如 `S01E01`、`S2026E01` | -| `episode` | 分集序号,用于中文分集标题 | -| `part` | 分段标记,例如 `CD1`、`Part1` | -| `videoFormat` | 视频规格,例如 `1080p`、`2160p`、`WEB-DL` | -| `fileExt` | 原始文件扩展名,例如 `.mkv`、`.mp4` | - ---- - -## 🔎 发现、搜索与订阅下载 - -### 多源发现 - -精彩发现支持: - -- TMDb:趋势、热门电影、热门剧集、高分电影。 -- 豆瓣:热门电影、高分电影、热门剧集。 -- Bangumi:每日放送、动漫条目。 - -### 智能搜索 - -智能搜索会同时考虑: - -- 本地媒体库已有内容。 -- TMDb / 豆瓣 / Bangumi 等在线结果。 -- 可订阅关键词与媒体类型。 - -### 订阅规则 - -订阅支持以下规则: - -| 规则 | 说明 | -| --- | --- | -| 媒体类型 | 电影、剧集、动漫、综艺,支持自动识别 | -| 搜索模式 | 标题关键词或 IMDB ID | -| 分辨率 | 自动择优、2160p、1080p、720p | -| 质量 | REMUX、BluRay、WEB-DL、HDTV 等 | -| 特效 | HDR、Dolby Vision、Atmos 等 | -| 发布组 | 白名单发布组 | -| 排除词 | 排除 CAM、TS、枪版等低质资源 | -| 洗版 | 默认关闭,可按分辨率、质量、特效、做种数优先 | - -下载与订阅卡片只展示安全标题、海报、进度、速度、体积等信息,不展示原始种子 URL,避免多用户场景泄露私人 Tracker Token。 - ---- - -## 🔌 外部客户端与 Emby 兼容 - -项目提供 Emby/Jellyfin 风格 API,用于外部客户端连接: - -```text -http://<服务器IP>:18080 -``` - -可尝试的客户端: - -- Infuse -- VidHub -- SenPlayer -- 其他支持 Emby/Jellyfin 服务器的播放器 - -建议检查: - -1. Docker 端口是否映射为 `18080:8080`。 -2. 防火墙是否允许局域网访问 `18080`。 -3. 账号密码是否正确。 -4. 反向代理是否正确转发 `/api`、视频流和 Range 请求。 - -### 与 MoviePilot 的功能参考关系 - -MediaStationGo 在外部客户端兼容与媒体生态联动上参考了 MoviePilot 的成熟产品路径:通过统一媒体库、订阅下载、下载后整理和 Emby/Jellyfin 兼容接口,把 Web 管理端与 Infuse、VidHub、SenPlayer 等客户端串起来。本项目的目标不是替代 Emby/Jellyfin,而是在最极少的资源占用的轻量 Go 服务中提供足够常用的媒体浏览、播放、海报墙、剧集分季分集、播放进度和外部客户端访问能力。 - -当前兼容重点: - -- 媒体库、合集、季、集的层级输出。 -- 海报、背景图、简介、年份、评分等基础元数据输出。 -- 视频流地址、HTTP Range、播放进度与继续观看。 -- 外部客户端登录、媒体浏览和播放所需的 Emby/Jellyfin 风格接口。 - -仍在持续补齐: - -- 更完整的 Emby/Jellyfin 设备能力协商。 -- 更细的转码 Profile 与字幕能力声明。 -- 多用户权限、媒体库过滤与播放历史同步。 -- 与订阅下载、自动整理、洗版规则之间的闭环联动。 - -> MoviePilot 项目使用 GPL-3.0 许可,本项目只参考其公开产品思路与交互路径,不复制私有数据、密钥、站点账号或不兼容实现。 - ---- - -## 🧠 AI 与外部服务配置 - -在后台「外部 API 配置」中可配置: - -| 服务 | 作用 | -| --- | --- | -| TMDb | 电影、剧集、海报、背景图、简介 | -| Bangumi | 动漫、番剧、中文条目 | -| TheTVDB | 剧集季集补充 | -| Fanart.tv | 高清 Logo、背景图、艺术图 | -| 豆瓣 | 中文影视搜索与推荐补充 | -| OpenAI Compatible | AI 搜索、推荐、运维助手 | - -M-Team 建议使用 API Access Token: - -```text -控制台 → 实验室 → 存取令牌 -HTTP Header: x-api-key -``` - -不建议使用 Cookie 调用开放 API,避免账号风险。 - ---- - -## 🔐 隐私与安全 - -默认不会提交以下数据: - -- `data/`、`cache/`、`logs/` -- `.tmp-deploy-data/`、`.tmp-deploy-server.*` -- `.mediastation.pid` -- `config.yaml`、`.env*` -- `*.db`、`*.db-wal`、`*.log` -- `web/dist/`、`node_modules/`、`bin/` -- API Key、Cookie、Token、密码、证书等敏感文件 - -提交前建议检查: - -```bash -git status --short -git ls-files | grep -E 'data/|cache/|\.db|\.log|jwt_secret|config.yaml|\.env|token|apikey|password' || true -``` - ---- - -## ❓ 常见问题 - -### Q: 拉取 GHCR 镜像时出现 `EOF` 怎么办? - -`EOF` 通常表示服务器到 GHCR 的网络连接中途断开,不是 compose 文件语法错误。建议按顺序处理: - -```bash -# 1. 清理可能异常的 GHCR 登录状态 -docker logout ghcr.io || true - -# 2. 单独拉取镜像,确认是网络/ registry 问题还是 compose 问题 -docker pull ghcr.io/shukebta/mediastation-go:latest - -# 3. 如果是 x86_64/AMD64 主机,也可以显式指定平台重试 -docker pull --platform linux/amd64 ghcr.io/shukebta/mediastation-go:latest - -# 4. 拉取成功后再启动 -docker compose up -d -``` - -如果服务器在国内网络或 NAS 网络环境中,建议为 Docker daemon 配置可访问 GHCR 的代理;仅设置终端代理通常不一定会被 Docker 服务进程继承。默认 compose 已使用 `pull_policy: missing`,避免容器重启时反复访问 GHCR。 - -另外,你示例中的路径如果是 NAS 绝对路径,建议写成 `/your-nas/...`,不要写 `./your-nas/...`;前者是系统根目录路径,后者是当前 compose 目录下面的相对路径。 - -### Q: Docker 部署后浏览器打不开? - -检查容器状态和端口: +先看容器是否启动: ```bash docker ps -docker logs -f mediastation-go +docker logs --tail=100 mediastation-go ``` -确认访问的是宿主机端口,例如 `http://你的IP:18080`。 +确认浏览器访问的是: -### Q: 外部客户端提示服务器未响应? +```text +http://服务器IP:18080 +``` -优先检查防火墙、Docker 端口映射、反向代理和局域网 IP。容器内监听 `8080`,宿主机默认映射为 `18080`。 +### 2. 媒体库扫描不到文件? -### Q: 媒体库没有海报? +最常见原因是路径写错。 -请确认: +- Docker `volumes` 右边是 `/media`。 +- 网页媒体库路径就应该填 `/media/电影`,不要填 NAS 原始路径。 +- 如果 qB 下载目录是 `/downloads`,自动整理源目录也优先填 `/downloads`。 -1. 本地是否有 `poster.jpg`、`fanart.jpg`、NFO。 -2. TMDb / Bangumi / 豆瓣是否可连接。 -3. 代理是否正确配置。 -4. 媒体文件名是否包含清晰标题、年份、季集信息。 +### 3. qBittorrent 连不上? -### Q: 下载任务为什么不显示原始链接? +如果 qB 在宿主机上,地址试试: -PT 下载 URL 常包含私有 Token。下载中心和订阅管理会主动隐藏原始 URL,只显示安全标题、海报、速度、进度、体积等信息。 +```text +http://host.docker.internal:8085 +``` +如果 qB 在另一台机器上,填那台机器的局域网 IP。 +### 4. NAS CPU 占用高? -## 🗺️ 路线图 +建议先在系统设置里确认: -- 更完整的 Emby/Jellyfin 客户端兼容。 -- 更强的成人内容本地元数据和公开页面补全。 -- 更细粒度的订阅洗版和下载后整理规则。 -- 更完善的移动端/电视端交互。 -- 插件化站点适配器和通知渠道。 -- 更完整的端到端测试与截图自动化。 +- `ffprobe.max_concurrent` 设为 `1`。 +- 自动整理、扫描后刮削、启动后扫描网盘按需开启。 +- 大媒体库不要频繁全量扫描,优先手动扫描或夜间同步。 + +### 5. 要不要用 `.env`? + +新手不建议。直接改 `docker-compose.yml` 最直观。 + +`.env` 适合进阶用户在多台机器复用同一份 compose。仓库保留 `docker-compose.simple.env.example`,但它不是推荐主线。 --- -## 🤝 贡献 +## 功能概览 -欢迎提交 Issue、Pull Request、站点适配、刮削规则、UI 改进与文档修正。 +| 分类 | 功能 | +| --- | --- | +| 媒体库 | 电影、电视剧、动漫、综艺、音乐、成人内容 | +| 元数据 | NFO、本地图片、TMDb、TheTVDB、Bangumi、豆瓣、Fanart、JavBus/JavDB | +| 播放 | Web 播放、Range 拖动、HLS 转码、直链、STRMURL、302 反代 | +| 外部客户端 | Emby 风格 API,支持多种第三方播放器接入 | +| 下载 | qBittorrent、站点搜索、订阅、下载完成后整理 | +| 文件管理 | 浏览、整理、复制、移动、硬链接、软链接 | +| 运维 | 任务队列、回收站、重复文件、通知渠道、运行日志 | +| AI | OpenAI Compatible API、AI 搜索、推荐、助手 | -建议贡献前先运行: +--- + +## 截图 + +
+界面预览 + +| 登录 | 首页 | +| --- | --- | +| 登录 | 首页 | + +| 媒体库 | 播放器 | +| --- | --- | +| 媒体库 | 播放器 | + +
+ +--- + +## 开发者运行 + +普通用户请优先使用 Docker。开发者可以这样运行: + +```bash +go run ./cmd/server +``` + +前端: + +```bash +cd web +npm install +npm run dev +``` + +测试: ```bash go test ./... @@ -1011,13 +374,15 @@ cd web && npm run build --- -## 👥 开发群组 +## 社区与友链 -- Telegram: +- Telegram 群组: +- NodeSeek:[https://www.nodeseek.com/](https://www.nodeseek.com/) +- LINUX DO:[https://linux.do/](https://linux.do/) --- -## 🍜 赞赏 +## 赞赏 如果这个项目节省了你的时间,欢迎请作者吃桶泡面。 @@ -1025,7 +390,7 @@ cd web && npm run build --- -## ⭐ Star History +## Star History @@ -1037,9 +402,11 @@ cd web && npm run build --- -## 📄 许可证与非商用声明 +## 许可证与非商用声明 -本项目基础许可证遵循 `GPL-3.0`,详见 [LICENSE](LICENSE)。项目维护者同时声明并倡议: +本项目基础许可证遵循 `GPL-3.0`,详见 [LICENSE](LICENSE)。 + +项目维护者同时声明并倡议: - 本项目主要面向个人学习、家庭 NAS、自建影音、非商业研究与社区共建场景。 - 未经作者明确书面许可,不得将本项目或衍生版本用于商业售卖、商业托管、付费 SaaS、预装售卖设备、闭源二次分发或其他商业化牟利用途。 diff --git a/README_EN.md b/README_EN.md index 9e6e0d4..a4f8b36 100644 --- a/README_EN.md +++ b/README_EN.md @@ -7,987 +7,365 @@

A lightweight, polished, NAS-friendly private media center

- Go single-binary backend · React frontend · Docker-first deployment · Emby-compatible APIs · Multi-source metadata · PT subscriptions + Docker-first setup · Media library · Metadata · Playback · Downloads · Emby-compatible clients · Cloud playback

中文 · Quick Start · - Docker Deploy · - Screenshots · + Docker Compose · + FAQ · Live Demo

Go React - TypeScript Docker License - Use

--- -## ✨ Overview +## What is it? -MediaStationGo is an open-source media center for personal libraries, family NAS setups, and home-theater enthusiasts. It combines library management, metadata scraping, web playback, third-party client compatibility, PT site search, subscription-based downloads, and AI-assisted recommendations into one lightweight Go service with a polished React interface. +MediaStationGo is a self-hosted media center for personal libraries, home NAS, and home-theater users. -It is designed for users who want to: +It helps you: -- Manage movies, TV shows, anime, variety shows, adult content, and music from one place. -- Prefer local NFO/images first, then enrich missing metadata from TMDb, Douban, Bangumi, TheTVDB, Fanart, JavBus, and JavDB. -- Search PT sites, subscribe to resources, enqueue downloads, and organize completed files from one panel. -- Connect external clients such as Infuse, VidHub, SenPlayer, and other Emby/Jellyfin-compatible apps. -- Deploy easily on NAS, Windows, Linux, macOS, or Docker without leaking private tracker tokens to the frontend. +- Manage movies, TV shows, anime, variety shows, music, and adult libraries. +- Scan files and enrich posters, summaries, years, seasons, and episodes. +- Play in the web UI or external apps such as Infuse, VidHub, SenPlayer, and Emby clients. +- Connect qBittorrent for search, subscriptions, downloads, and post-download organization. +- Connect OpenList, CloudDrive2, WebDAV, and other storage backends with STRMURL or 302 redirect playback. +- Run on NAS, mini PCs, VPS, Linux, Windows Docker Desktop, or any Docker-friendly host. -> The project is under active development. For production use, pin a release image tag and back up `/data` regularly. +> The project is moving fast. Keep a backup of the `data` directory before upgrades. --- -## 🌱 Open Source Promise +## Who is it for? -MediaStationGo follows a fully open-source path. The core media library, metadata scraping, playback, subscriptions, downloads, external-client compatibility, and operations features are developed in this repository. The project learns from excellent open-source projects such as MoviePilot in areas like site aggregation, subscription downloads, post-download organization, and Emby/Jellyfin client compatibility, while keeping MediaStationGo as an independent implementation and avoiding incompatible code reuse. - -The current base license is `GPL-3.0`, and contributions are welcome under that license: site adapters, scraping rules, UI improvements, documentation, and bug fixes all help the project grow. The maintainers also state the intended usage boundary: MediaStationGo is provided for personal learning, home NAS, self-hosted media, and non-commercial scenarios. Without explicit written permission from the author, do not use this project or derivative versions for commercial resale, paid hosting, paid SaaS, pre-installed commercial devices, closed-source redistribution, or other profit-oriented commercial use. - -> Note: GPL-3.0 is a free software license, and the formal grant is defined by the repository [LICENSE](LICENSE) file. The non-commercial commitment above expresses the maintainer's intended usage boundary and commercial cooperation requirements. For commercial cooperation, enterprise deployment, or redistribution, contact the author for additional authorization first. - -### Source Availability and Docker Support Boundary - -- The public repository currently uses `GPL-3.0` as its base license. If a component is GPL-derived, distributing it only as a Docker image does not remove the corresponding source-distribution obligations. -- The project can still define its official support scope as **Docker-first / Docker-only support**: Docker Compose, GHCR images, and container deployment docs are maintained as the supported path, while bare-metal binaries can be community/best-effort. -- If some future functionality needs to be closed-source, keep it as a separate plugin, private service, or independently implemented module whose license boundary is clean. GPL-covered code should remain available under GPL terms. -- The README non-commercial statement describes the maintainer's intended usage boundary and commercial authorization requirement; the formal code license remains governed by [LICENSE](LICENSE). +- **Beginners** who want to edit one `docker-compose.yml` and start the service. +- **NAS users** who want a low-resource media center for local disks and cloud storage. +- **PT/download users** who want downloads, organization, metadata, and playback in one panel. +- **External-player users** who want an Emby-style API for third-party apps. +- **Developers** who want to study or extend a Go + React self-hosted media app. --- -## 🚀 Live Demo +## Live Demo -- Demo: [https://mgo.3jzs.com](https://mgo.3jzs.com) +- URL: [https://mgo.3jzs.com](https://mgo.3jzs.com) - Username: `admin` - Password: `admin123` -> The demo is for feature preview only. Do not store private API keys, tracker tokens, or personal media information there. +> The demo is for feature preview only. Do not save private API keys, tracker cookies, or personal data there. --- -## 🧭 Feature Overview +## Quick Start -| Area | Capabilities | -| --- | --- | -| Libraries | Movies, TV, anime, variety, music, adult content; folder covers, series, seasons, and episodes | -| Scanning | Recursive scanning, ffprobe probing, filename parsing, season/episode recognition, duplicate prevention | -| Local metadata | NFO, poster, fanart, season poster, episode image, local adult artwork first | -| Online metadata | TMDb, TheTVDB, Bangumi, Douban, Fanart.tv, JavBus/JavDB page scraping | -| Playback | Direct streaming, HTTP Range seeking, HLS transcoding, external subtitles, progress, resume, external players | -| Discovery | TMDb / Douban / Bangumi recommendation rails and subscription entry points | -| PT sites | Site management, M-Team API token, cross-site search, download URL resolution | -| Subscriptions | RSS/search subscriptions, resolution/quality/effects/release-group rules, wash toggle and priorities | -| Downloads | qBittorrent task cards, status, speed, progress, uploaded/downloaded size, private URL redaction | -| Compatibility | Emby-style APIs for Infuse, VidHub, SenPlayer, and similar clients | -| Operations | Runtime status, task queue, duplicate files, recycle bin, file manager, storage settings, notifications | -| AI | OpenAI-compatible API settings, AI search, recommendations, and operations assistant | - ---- - - - -## 🖼️ Screenshots - -> Screenshots below were captured from a running local instance. Personal media, local paths, accounts, API keys, tokens, and secrets have been visually redacted. - -
-Core Experience - -| Login & Home | Library Overview | -| --- | --- | -| Login | Home | -| Libraries | Library Detail | -| Libraries | Library Detail | -| Poster Wall | Media Detail | -| Poster Wall | Media Detail | -| Player | Discover | -| Player | Discover | -| Smart Search | DLNA Cast | -| Smart Search | DLNA | - -
- -
-Personal Space & Playback - -| AI Assistant | Favorites | -| --- | --- | -| AI Assistant | Favorites | -| Playlists | Watch History | -| Playlists | Watch History | -| Profile | Downloads | -| Profile | Downloads | - -
- -
-Downloads, Subscriptions & Sites - -| Download Clients | Subscriptions | -| --- | --- | -| Download Clients | Subscriptions | -| Site Search | Sites & Downloaders | -| Site Search | Sites | - -
- -
-Administration & Operations - -| Media & Users | Tools | -| --- | --- | -| Admin | Tools | -| Storage & Files | Runtime Status | -| Storage | Stats | -| Settings | Tasks | -| Settings | Tasks | -| Duplicates | Recycle Bin | -| Duplicates | Recycle Bin | -| Scheduler | File Manager | -| Scheduler | Files | -| STRM | Storage Config | -| STRM | Storage Config | -| Notifications | Operations Assistant | -| Notifications | Operations Assistant | - -
- ---- - -## 🧱 Tech Stack - -| Layer | Technology | Notes | -| --- | --- | --- | -| Backend | Go 1.25+ | Single-binary deployment, low resource usage | -| Web framework | Gin | REST APIs, auth middleware, static file serving | -| Database | SQLite + GORM | Simple backup and migration for personal/NAS usage | -| Frontend | React 18 + TypeScript | Typed component-based UI | -| Build tool | Vite | Fast frontend development and production builds | -| Styling | Tailwind CSS | Unified bright visual system and responsive layout | -| State | Zustand | Lightweight auth and permission state | -| Playback | HTML5 Video / HLS / FFmpeg | Direct play, Range seek, HLS transcoding, subtitles | -| Metadata | TMDb / Douban / Bangumi / TheTVDB / Fanart / JavBus / JavDB | Posters, backdrops, descriptions, ratings, episode metadata | -| Downloads | qBittorrent / PT site adapters | Search, subscriptions, task cards, private URL redaction | -| Compatibility | Emby-style API / DLNA | External clients and player integrations | -| Deployment | Docker / Docker Compose / Shell / PowerShell | NAS, Linux, Windows, and macOS friendly | -| CI/CD | GitHub Actions / GHCR | Version tags or manual Actions runs publish multi-arch Docker images and release packages | - ---- - - - -## 📦 Quick Start - - - -### Docker Compose (Recommended) - -The recommended beginner path is now **editing real paths directly in `docker-compose.yml`**. This is easier for new NAS, VPS, and Docker users because the host paths are visible in one file. - -#### 1. Create a deployment directory - -```bash -mkdir -p ~/MediaStationGo -cd ~/MediaStationGo -mkdir -p data cache media downloads -``` - -#### 2. Download compose +Docker Compose is the recommended path. Beginners do not need `.env`, bare-metal binaries, or source builds. ```bash +mkdir -p MediaStationGo +cd MediaStationGo curl -fsSL https://raw.githubusercontent.com/ShukeBta/MediaStationGo/main/docker-compose.yml -o docker-compose.yml ``` -If GitHub Raw is slow, create `docker-compose.yml` manually and paste the template from the repository root. - -#### 3. Edit real host paths - -Open the compose file: +Edit `docker-compose.yml`: ```bash vi docker-compose.yml ``` -Find the media and download volume lines, then replace the left side with your real NAS/server paths: - -```yaml - volumes: - - ./data:/data - - ./cache:/cache - - /vol1/1000/Docker/moviepilot-v2/media:/media:ro - - /vol1/1000/qBittorrent/downloads:/downloads -``` - -Then find the path mapping variables under `environment` and write the same real paths there: - -```yaml - environment: - MEDIASTATION_MEDIA_DIR: /vol1/1000/Docker/moviepilot-v2/media - MEDIASTATION_MEDIA_CONTAINER_DIR: /media - MEDIASTATION_DOWNLOAD_DIR: /vol1/1000/qBittorrent/downloads - MEDIASTATION_DOWNLOAD_CONTAINER_DIR: /downloads -``` - -| Location | Meaning | -| --- | --- | -| Media volume left side | Real host/NAS media directory, e.g. `/volume1/media`, `/mnt/media`, `/vol1/1000/Docker/moviepilot-v2/media` | -| Download volume left side | Real host/NAS download directory, e.g. `/volume1/downloads`, `/mnt/downloads`, `/vol1/1000/qBittorrent/downloads` | -| `MEDIASTATION_MEDIA_DIR` | Must match the media volume left side | -| `MEDIASTATION_DOWNLOAD_DIR` | Must match the download volume left side | - -> Do not write NAS absolute paths as `./vol1/...`. A leading `./` means a directory under the current compose project. - -#### 4. Start +Start: ```bash -docker compose pull docker compose up -d ``` -If your system only has the legacy command: - -```bash -docker-compose pull -docker-compose up -d -``` - -#### 5. Open the app +Open: ```text -http://:18080 +http://SERVER_IP:18080 ``` -Default account: +Default login: ```text -admin / admin123 +Username: admin +Password: admin123 ``` -Change the admin password immediately after first login. - -#### 6. Paths inside the web UI - -The simple compose maps host directories to stable container paths: - -| Host directory | Container path | Use in the web UI | -| --- | --- | --- | -| `MEDIASTATION_MEDIA_DIR` | `/media` | `/media/Movies`, `/media/TV`, `/media/TV/CDrama` | -| `MEDIASTATION_DOWNLOAD_DIR` | `/downloads` | Downloader save root: `/downloads` | - -This is a Docker bind mount. It does not copy files and does not consume double disk space. - -#### 7. Update - -Use the update helper to pull the new image and remove old MediaStationGo image layers: - -```bash -curl -fsSL https://cdn.jsdelivr.net/gh/ShukeBta/MediaStationGo@main/scripts/docker-compose-update.sh -o docker-compose-update.sh -chmod +x docker-compose-update.sh -./docker-compose-update.sh -``` - -#### Advanced options - -The default compose intentionally stays small. If you need the container to expose the same raw host paths, Telegram proxy variables, hardware acceleration, or additional transcoding options, see: - -```text -docker-compose.advanced.yml -``` - -### Optional: Use `.env` for Paths - -If you already know Docker Compose well, or if you reuse the same compose file across multiple machines, you can put paths in `.env`. This is optional, not the beginner path: - -```bash -cat > .env <<'EOF' -MEDIASTATION_MEDIA_DIR=/vol1/1000/Docker/moviepilot-v2/media -MEDIASTATION_DOWNLOAD_DIR=/vol1/1000/qBittorrent/downloads -MEDIASTATION_HTTP_PORT=18080 -TZ=Asia/Shanghai -PUID=1000 -PGID=1000 -EOF -``` - -When using `.env`, keep the default variable-style volume lines in `docker-compose.yml`: - -```yaml -- ${MEDIASTATION_MEDIA_DIR:-./media}:/media:ro -- ${MEDIASTATION_DOWNLOAD_DIR:-./downloads}:/downloads -``` - -### Fixed Version Deployment - -For production, pin a release tag so `latest` does not change unexpectedly. Without `.env`, edit the image line directly: - -```yaml -image: ghcr.io/shukebta/mediastation-go:MediaStationGo-v0.0.32 -``` - -If you use `.env`, you can also set: - -```env -MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.32 -``` - -Then run: - -```bash -docker compose pull -docker compose up -d -``` - -### External Network and v2rayA - -If your NAS already uses v2rayA redirect / transparent proxy with routing rules, MediaStationGo usually does not need extra `HTTP_PROXY` / `HTTPS_PROXY` variables. - -Avoid stacking proxy variables in `.env`, `docker-compose.yml`, or the Docker daemon unless you intentionally use an application-level proxy. Stacked proxies can break GHCR pulls and site APIs. - -### qBittorrent URL - -If qBittorrent runs on the same NAS/host, do not use `127.0.0.1` from inside MediaStationGo. Use: - -```text -http://host.docker.internal:8085 -``` - -If qBittorrent also runs in Docker, mount the same host download directory as `/downloads` in both containers. Use `/downloads` as the subscription save root. - -With smart classification enabled, downloads are saved to folders such as `/downloads/动画电影`, `/downloads/国产剧`, and `/downloads/综艺`. - -### Cloud Drives, OpenList, and CloudDrive2 - -MediaStationGo supports OpenList, Alist, WebDAV, 115, Quark, and CloudDrive2 as external storage backends. For most users, OpenList, CloudDrive2, or Alist is the recommended bridge layer: they can integrate many cloud drives such as 115, 123Pan, Aliyun Drive, and Quark, while MediaStationGo can use OpenList/Alist APIs or WebDAV endpoints for browsing, library mounting, local-media upload, and authenticated proxy playback. - -Typical OpenList setup: - -1. Mount your 115 / 123Pan / Aliyun / Quark drive inside OpenList. -2. Select `OpenList` in MediaStationGo → External Storage. -3. Set `OpenList Server URL` to the management/API endpoint, for example `http://NAS-IP:5244`. -4. Set `WebDAV URL` to the WebDAV endpoint, for example `http://NAS-IP:5244/dav/`. -5. Do not use `https://` unless OpenList is actually behind an HTTPS reverse proxy. `Propfind "https://...": http: server gave HTTP response to HTTPS client` means an HTTP service was entered as HTTPS. - -Typical CloudDrive2 setup: - -1. Mount your 115 / 123Pan / Aliyun / Quark drive inside CloudDrive2. -2. Select `CloudDrive2` in MediaStationGo → External Storage. -3. Fill the CloudDrive2 WebDAV URL, for example `http://host.docker.internal:19798/dav` or `http://NAS-IP:19798/dav`. -4. Save the config, then browse the cloud directory or mount it as a media library. - -The native 115 adapter still supports cookie / QR login, directory browsing, and 302 playback. For local file upload, prefer OpenList / CloudDrive2 / Alist bridging instead of maintaining every provider's private chunk-upload protocol inside this project. - --- -## 🐳 Docker Compose Configuration +## Docker Compose Recommended -The default repository `docker-compose.yml` is intentionally simple. Beginners should write real host paths directly on the left side of `volumes`, then keep `MEDIASTATION_MEDIA_DIR` and `MEDIASTATION_DOWNLOAD_DIR` aligned with those same paths. `.env` variables are optional for advanced reuse: +The repository `docker-compose.yml` is intentionally simple and does not require `.env`. -| Variable | Default | Description | +Focus on this part: + +```yaml +volumes: + - ./data:/data + - ./cache:/cache + - ./media:/media:ro + - ./downloads:/downloads +``` + +Meaning: + +| Host path | Container path | Purpose | | --- | --- | --- | -| `MEDIASTATION_IMAGE_TAG` | `latest` | Image tag. Pin a release for production. | -| `MEDIASTATION_HTTP_PORT` | `18080` | Host access port | -| `MEDIASTATION_DATA_DIR` | `./data` | Persistent data directory | -| `MEDIASTATION_CACHE_DIR` | `./cache` | Image and transcoding cache | -| `MEDIASTATION_MEDIA_DIR` | `./media` | Host media library root; on NAS use an absolute path such as `/your-nas/media` | -| `MEDIASTATION_DOWNLOAD_DIR` | `./downloads` | Host download target; on NAS use an absolute path such as `/your-nas/downloads` | -| `PUID` / `PGID` | `1000` / `1000` | Linux/NAS file permission mapping | -| `TZ` | `Asia/Shanghai` | Container timezone | +| `./data` | `/data` | Database, users, settings; back this up | +| `./cache` | `/cache` | Cache; safe to clean when needed | +| `./media` | `/media` | Media libraries; use `/media/...` in the web UI | +| `./downloads` | `/downloads` | Download directory and organization source | -View logs: +If your NAS paths are: + +```text +/vol1/1000/Media +/vol1/1000/Downloads +``` + +change the compose file to: + +```yaml +volumes: + - ./data:/data + - ./cache:/cache + - /vol1/1000/Media:/media:ro + - /vol1/1000/Downloads:/downloads + +environment: + MEDIASTATION_MEDIA_DIR: /vol1/1000/Media + MEDIASTATION_DOWNLOAD_DIR: /vol1/1000/Downloads +``` + +Rules: + +- The left side of `volumes` is the real path on your host/NAS. +- The right side is the container path. Keep `/media` and `/downloads` unless you know why you are changing them. +- In the web UI, create libraries with container paths such as `/media/Movies` or `/media/TV`. +- Do not write NAS absolute paths as `./vol1/...`; `./` means a folder under the current compose directory. +- On Windows Docker Desktop, paths like `D:/Media:/media:ro` and `D:/Downloads:/downloads` are fine. + +### Minimal compose example + +The root `docker-compose.yml` follows this style: + +```yaml +services: + mediastation-go: + image: ghcr.io/shukebta/mediastation-go:latest + container_name: mediastation-go + restart: unless-stopped + init: true + + # Browser: http://SERVER_IP:18080 + ports: + - "18080:8080" + + # Let the container reach qBittorrent running on the host: + # qB URL example: http://host.docker.internal:8085 + extra_hosts: + - "host.docker.internal:host-gateway" + + volumes: + # Application data. Back this up before upgrades. + - ./data:/data + - ./cache:/cache + + # Beginners can keep ./media and ./downloads. + # NAS users should replace the left side with real absolute paths. + - ./media:/media:ro + - ./downloads:/downloads + + environment: + TZ: Asia/Shanghai + PUID: "1000" + PGID: "1000" + + MEDIASTATION_APP_HOST: 0.0.0.0 + MEDIASTATION_APP_PORT: 8080 + MEDIASTATION_APP_WEB_DIR: /app/web/dist + MEDIASTATION_APP_DATA_DIR: /data + MEDIASTATION_DATABASE_DB_PATH: /data/mediastation.db + MEDIASTATION_CACHE_CACHE_DIR: /cache + + # If you changed ./media or ./downloads above, + # set these to the same real host paths. + MEDIASTATION_MEDIA_DIR: ./media + MEDIASTATION_MEDIA_CONTAINER_DIR: /media + MEDIASTATION_DOWNLOAD_DIR: ./downloads + MEDIASTATION_DOWNLOAD_CONTAINER_DIR: /downloads +``` + +--- + +## First-time Setup + +1. **Create a library** + - Go to the library page. + - Use a container path such as `/media/Movies`. + - Start a scan. + +2. **Connect qBittorrent** + - Go to download client settings. + - If qBittorrent runs on the host, try `http://host.docker.internal:8085`. + +3. **Configure metadata providers** + - Go to system settings / external APIs. + - Add TMDb, Bangumi, TheTVDB, Fanart, Douban, or other providers when needed. + +4. **Use external players** + - Add the server as an Emby/Jellyfin-compatible server. + - Server URL: `http://SERVER_IP:18080`. + - Log in with your MediaStationGo account. + +5. **Use cloud playback** + - Configure OpenList, CloudDrive2, WebDAV, or another provider in storage settings. + - Choose STRMURL or 302 redirect playback in the admin settings. + - The enabled option takes priority. If both are disabled, playback falls back to the normal server playback path. + +--- + +## Update, Backup, Logs + +### Update + +```bash +docker compose pull +docker compose up -d +``` + +### Logs ```bash docker logs -f mediastation-go ``` -Update: +### Backup -```bash -curl -fsSL https://cdn.jsdelivr.net/gh/ShukeBta/MediaStationGo@main/scripts/docker-compose-update.sh -o docker-compose-update.sh -chmod +x docker-compose-update.sh -./docker-compose-update.sh +Back up: + +```text +data/ ``` -Note: plain `docker compose pull && docker compose up -d` switches to the new image but does not remove old images. The helper keeps the currently running MediaStationGo image, removes unused older images from the same repository, and runs `docker image prune -f` for dangling layers. To aggressively remove all unused images, run `PRUNE_ALL_UNUSED=1 ./docker-compose-update.sh`. +It contains the database, users, settings, and runtime state. `cache/` is usually not important. -Stop: +### Stop ```bash docker compose down ``` -Back up data: - -```bash -tar -czf mediastationgo-data-backup.tgz ./data -``` - --- -## 🖥️ One-Click Deployment Scripts +## FAQ -If you do not want Docker, run MediaStationGo directly on the host. The scripts build the frontend, compile the backend, start the service, and verify `/api/health`. +### 1. The web page does not open -### Linux / macOS - -```bash -git clone https://github.com/ShukeBta/MediaStationGo.git -cd MediaStationGo -chmod +x scripts/deploy.sh -PORT=18080 DATA_DIR=/opt/mediastation/data CACHE_DIR=/opt/mediastation/cache ./scripts/deploy.sh -``` - -### Windows PowerShell - -```powershell -git clone https://github.com/ShukeBta/MediaStationGo.git -cd MediaStationGo -.\scripts\deploy.ps1 -Port 18080 -DataDir D:\MediaStationGo\data -CacheDir D:\MediaStationGo\cache -``` - -The scripts will: - -1. Install frontend dependencies and build `web/dist` -2. Compile the Go backend into `bin/` -3. Create data and cache directories -4. Stop any previous process and start a new one -5. Verify the service through `/api/health` - ---- - -## 🧩 Release Package Deployment - -Each release provides multi-platform archives: - -| Platform | Package example | -| --- | --- | -| Linux x86_64 | `MediaStationGo-v0.0.32-linux-amd64.tar.gz` | -| Linux ARM64 | `MediaStationGo-v0.0.32-linux-arm64.tar.gz` | -| Windows x86_64 | `MediaStationGo-v0.0.32-windows-amd64.zip` | -| macOS Intel | `MediaStationGo-v0.0.32-darwin-amd64.tar.gz` | -| macOS Apple Silicon | `MediaStationGo-v0.0.32-darwin-arm64.tar.gz` | - -Linux example: - -```bash -tar -xzf MediaStationGo-v0.0.32-linux-amd64.tar.gz -cd MediaStationGo-v0.0.32-linux-amd64 -MEDIASTATION_APP_PORT=18080 ./mediastation-go -``` - -Windows example: - -```powershell -Expand-Archive .\MediaStationGo-v0.0.32-windows-amd64.zip -cd .\MediaStationGo-v0.0.32-windows-amd64 -$env:MEDIASTATION_APP_PORT = "18080" -.\mediastation-go.exe -``` - -> Release binaries listen on `8080` by default. Set `MEDIASTATION_APP_PORT=18080` as shown above if you want the same port as the Docker examples. - ---- - -## 🛠️ Local Development - -### Requirements - -| Component | Version | Purpose | -| --- | --- | --- | -| Go | 1.25+ | Backend build and tests | -| Node.js | 20+ | Frontend build | -| FFmpeg / ffprobe | Recommended | Media probing and transcoding | -| Docker | Optional | Container deployment and multi-arch builds | -| qBittorrent | Optional | Download integration testing | - -### Build Locally - -```bash -cp config.example.yaml config.yaml -cd web -npm ci -npm run build -cd .. -go build -o bin/mediastation-go ./cmd/server -./bin/mediastation-go -``` - -Windows: - -```powershell -Copy-Item config.example.yaml config.yaml -Set-Location web -npm ci -npm run build -Set-Location .. -go build -o bin\mediastation-go.exe .\cmd\server -.\bin\mediastation-go.exe -``` - -### Common Commands - -```bash -make build # Build frontend and backend -make test # Run Go tests -make smoke # Smoke test -make docker # docker compose up -d -make deploy # Linux one-click deploy -make docker-push # Multi-arch buildx push -``` - ---- - -## 🏗️ Repository Layout - -```text -MediaStationGo/ -├── cmd/server/ # Server entry point -├── internal/ -│ ├── config/ # Config loading and defaults -│ ├── database/ # SQLite initialization and migrations -│ ├── handler/ # HTTP API, Emby API, admin endpoints -│ ├── middleware/ # Auth, permission, logging middleware -│ ├── model/ # GORM models -│ ├── repository/ # Data access layer -│ └── service/ # Scanner, scraper, playback, downloads, subscriptions -├── web/ -│ ├── public/ # favicon and static public assets -│ ├── src/ # React pages, components, API clients, stores -│ └── dist/ # Frontend build output, ignored by git -├── scripts/ # Deploy, package, Docker scripts -├── docs/ # Design docs, screenshots, architecture notes -├── docker-compose.yml # Default Docker Compose deployment -├── Dockerfile # Multi-stage image build -├── config.example.yaml # Config template -└── README.md / README_EN.md # Documentation -``` - ---- - -## ⚙️ Configuration - -Configuration precedence, from low to high: - -1. Built-in defaults -2. `config.yaml` -3. `config/*.yaml` -4. `MEDIASTATION_` environment variables -5. Runtime settings stored in the database - -Common variables: - -| Variable | Default | Description | -| --- | --- | --- | -| `MEDIASTATION_APP_HOST` | `0.0.0.0` | Listen address | -| `MEDIASTATION_APP_PORT` | `8080` | Listen port | -| `MEDIASTATION_APP_WEB_DIR` | `./web/dist` | Frontend static bundle | -| `MEDIASTATION_APP_DATA_DIR` | `./data` | App data directory | -| `MEDIASTATION_DATABASE_DB_PATH` | `./data/mediastation.db` | SQLite database path | -| `MEDIASTATION_CACHE_CACHE_DIR` | `./cache` | Image/transcode cache | -| `MEDIASTATION_SECRETS_JWT_SECRET` | Auto-generated | JWT and encrypted settings seed | -| `MEDIASTATION_APP_CORS_ORIGINS` | empty | Extra CORS origins | -| `MEDIASTATION_TELEGRAM_API_BASE_URL` | `https://api.telegram.org` | Telegram Bot API base URL; use a reverse proxy when Telegram is blocked or slow | -| `MEDIASTATION_TELEGRAM_PROXY_URL` | empty | Telegram outbound proxy, e.g. `http://172.17.0.1:7890` or `socks5://172.17.0.1:1080` | - -Runtime settings from the admin UI: - -- API keys: TMDb, Bangumi, TheTVDB, Fanart, OpenAI Compatible. -- Sites: M-Team, NexusPHP, Unit3D, custom RSS. -- Download clients: qBittorrent, Transmission, Aria2. -- Notifications: Telegram, Bark, Webhook, Email. Telegram channels can use a per-channel API base URL or proxy, and test errors redact Bot Tokens automatically. -- Playback profiles, permissions, scheduler tasks, storage settings. - ---- - -## 👥 Users and Permissions - -- The default administrator is created on first startup as `admin / admin123`. This account can be renamed, but it cannot be deleted or demoted and always keeps the highest privileges. -- The open-source edition allows up to 20 users by default to reduce abuse on home NAS or public test instances. Binding a private license server can raise the quota according to the activated license policy. -- Users created from the admin panel are “viewer users” by default: they can log in through the Web UI and Emby-compatible clients, browse libraries, play media, use external players, favorite items, and keep watch history. -- Viewer users cannot scan libraries, rescrape metadata, delete media, probe media tracks, export NFO files, manage files, manage STRM links, manage download clients, create download tasks, or create/run subscriptions. -- Because playback necessarily streams media data to the client, MediaStationGo can block download-management features and torrent/download tasks, but it cannot fully prevent an authorized browser or external player from saving an already authorized stream at the protocol level. - ---- - -## 🔐 Private License Server - -MediaStationGo includes a server-side bridge for the private standalone `MediaStationLicenseServer`: - -- License server: `ShukeBta/MediaStationLicenseServer`; a local backup may live at `C:\Users\Administrator\WorkBuddy\license_server_backup`. -- MediaStationGo exposes `/api/license/activate`, `/api/license/status`, and `/api/license/heartbeat`; these backend routes proxy the License Server and do not expose the HMAC secret to browsers. -- License Server public endpoints are `/api/v1/activate`, `/api/v1/status/:fingerprint`, and `/api/v1/heartbeat`. -- Configure `license.server_url` and `license.hmac_secret` under Settings → License Server, then bind a key on the License page. -- Without a valid license, MediaStationGo stays in open-source mode. With a valid license, the current implementation raises the user quota to the licensed tier. - -Example environment variables: - -```bash -MEDIASTATION_LICENSE_SERVER_URL=http://127.0.0.1:8001 -MEDIASTATION_LICENSE_HMAC_SECRET=must-match-LICENSE_HMAC_SECRET -``` - ---- - -## 🎞️ On-Demand FFmpeg / ffprobe - -MediaStationGo does not keep `ffmpeg` or `ffprobe` running as resident daemons. They are launched only when needed: - -- `ffprobe` runs during library scanning or manual media-track probing. -- `ffmpeg` runs when browser direct play is not suitable and HLS transcoding is required. -- Admin tool-status checks or manual tool installation may briefly execute version checks/install logic. - -When playback stops, a transcode job is cancelled, or the service shuts down, the corresponding transcoding process is stopped. If there is no scanning, probing, or transcoding, `ffmpeg/ffprobe` should not continuously consume CPU. - -The default HLS profile is NAS-friendly: `MEDIASTATION_TRANSCODER_ENABLED=true` is the global switch, and disabling it prevents ffmpeg transcode jobs from starting; `MEDIASTATION_TRANSCODER_HARDWARE_ACCEL=false` is the hardware acceleration switch, and hardware encoders are used only when it is enabled together with `MEDIASTATION_TRANSCODER_ENCODER=nvenc/qsv/vaapi`; `MEDIASTATION_TRANSCODER_REALTIME=true` throttles input to playback speed, `MEDIASTATION_TRANSCODER_THREADS=2` caps software encoding threads, `MEDIASTATION_TRANSCODER_MAX_CONCURRENT=1` limits simultaneous transcodes, and `MEDIASTATION_TRANSCODER_IDLE_TIMEOUT_SECONDS=120` stops ffmpeg after the player stops requesting segments. - -The default Docker image now uses a trimmed runtime layer and does not bundle Intel VAAPI / mesa driver packages by default, reducing the Docker Hub vulnerability-scan surface. If you need a custom Intel VAAPI/QSV image, build with `docker buildx build --build-arg WITH_VAAPI=true ...`; NVIDIA NVENC still mainly depends on NVIDIA Container Toolkit on the host and GPU access when the container runs. - ---- - -## 🔍 Metadata Strategy - -MediaStationGo avoids unnecessary repeated scraping and tries not to overwrite good local metadata: - -1. Read local NFO, poster, fanart, season poster, and episode images first. -2. Parse media type, title, year, season, and episode from file paths. -3. Fill missing data through TMDb, TheTVDB, Bangumi, and Douban. -4. Use Fanart.tv for higher-quality artwork when available. -5. Adult content prefers local NFO/images, then enriches from public JavBus/JavDB pages. -6. Existing local metadata is not blindly overwritten. - -Recommended layout: - -```text -/media/Movies/Inception (2010)/Inception (2010).mkv -/media/TV/Some Show/Season 01/Some Show S01E01.mkv -/media/Anime/Anime Title/Season 01/Anime Title S01E01.mkv -/media/Variety/Show Name/Season 2026/Show Name S2026E01.mkv -/media/Adult/ABCD-123/ABCD-123.mp4 -``` - -Common local artwork names: - -```text -poster.jpg -fanart.jpg -folder.jpg -season01-poster.jpg -S01E01-thumb.jpg -movie.nfo -tvshow.nfo -episode.nfo -``` - -### Smart Classification Directory Rules - -MediaStationGo separates download classification from final media organization: - -1. Download stage: subscriptions and site-search downloads are saved under the downloader root by category. -2. Organization stage: manual or automatic organization moves files into the media library root, then into type and category folders. - -Recommended host directories: - -```text -/your-nas/downloads -/your-nas/media/电影 -/your-nas/media/电视剧 -``` - -Container paths: - -```text -/downloads -/media/电影 -/media/电视剧 -``` - -Download classification examples: - -```text -/downloads/动画电影 -/downloads/国产剧 -/downloads/国漫 -/downloads/华语电影 -/downloads/日番 -/downloads/外语电影 -/downloads/综艺 -``` - -Organized media examples: - -```text -/media/电视剧/国产剧/Show Name (2026)/Season 01/Show Name - S01E01 - 第 1 集.mkv -/media/电视剧/国漫/Anime Name (2026)/Season 01/Anime Name - S01E01 - 第 1 集.mkv -/media/电视剧/欧美剧/Show Name (2026)/Season 01/Show Name - S01E01 - 第 1 集.mkv -/media/电视剧/日番/Anime Name (2026)/Season 01/Anime Name - S01E01 - 第 1 集.mkv -/media/电视剧/日韩剧/Show Name (2026)/Season 01/Show Name - S01E01 - 第 1 集.mkv -/media/电视剧/综艺/Variety Name (2026)/Season 2026/Variety Name - S2026E01 - 第 1 集.mp4 -/media/电影/动画电影/Movie Name (2026)/Movie Name (2026) - 1080p.mkv -/media/电影/华语电影/Movie Name (2026)/Movie Name (2026) - 1080p.mkv -/media/电影/外语电影/Movie Name (2026)/Movie Name (2026) - 1080p.mkv -``` - -If the library root is set directly to `/media`, the organizer automatically adds the `电影/` or `电视剧/` type folder when smart classification is enabled. If the library root is already `/media/电影` or `/media/电视剧`, it will not add the type folder again. - -Automatic and manual organization are separate switches: - -- `downloads.smart_classify`: controls whether subscription/site-search downloads are automatically routed into category-specific save paths and qB categories; enabled by default. -- `organizer.smart_classify`: controls smart category folders only. -- `organizer.auto_after_download` / `organize.auto`: controls whether completed downloads are organized automatically. -- If auto organization is disabled, use the Tools page to organize a library or a single media item manually. - -### Organization and Scraping Naming Templates - -Use separate organization templates by media type. TV shows, anime, and variety shows should keep title, year, season folder, season/episode number, and episode title. Movies should keep title, year, part marker, and video format. - -Recommended template for TV / anime / variety: - -```jinja -{{title}}{% if year %} ({{year}}){% endif %}/Season {{season}}/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}} 集{% endif %}{{fileExt}} -``` - -Example output: - -```text -Some Show (2024)/Season 01/Some Show - S01E01 - 第 1 集.mkv -Some Anime (2025)/Season 02/Some Anime - S02E03 - 第 3 集.mkv -Some Variety (2026)/Season 2026/Some Variety - S2026E01 - 第 1 集.mp4 -``` - -Recommended template for movies: - -```jinja -{{title}}{% if year %} ({{year}}){% endif %}/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}{{fileExt}} -``` - -Example output: - -```text -Inception (2010)/Inception (2010) - 1080p.mkv -Dune (2021)/Dune (2021)-CD1 - 2160p.mkv -``` - -Common variables: - -| Variable | Description | -| --- | --- | -| `title` | Media title, preferably from local NFO or online metadata | -| `year` | Year, appended when available | -| `season` | Season number, used for `Season 01` folders | -| `season_episode` | Season/episode code, such as `S01E01` or `S2026E01` | -| `episode` | Episode number, used for Chinese episode titles | -| `part` | Part marker, such as `CD1` or `Part1` | -| `videoFormat` | Video format, such as `1080p`, `2160p`, or `WEB-DL` | -| `fileExt` | Original file extension, such as `.mkv` or `.mp4` | - ---- - -## 🔎 Discovery, Search & Subscriptions - -### Multi-source discovery - -Discover supports: - -- TMDb: trending, popular movies, popular TV, top-rated movies. -- Douban: hot movies, top movies, hot TV. -- Bangumi: calendar and anime entries. - -### Smart search - -Smart search can combine: - -- Existing local library content. -- Online results from TMDb, Douban, and Bangumi. -- Subscription keywords and media types. - -### Subscription rules - -| Rule | Description | -| --- | --- | -| Media type | Movie, TV, anime, variety, or auto-detect | -| Search mode | Keyword or IMDB ID | -| Resolution | Best, 2160p, 1080p, 720p | -| Quality | REMUX, BluRay, WEB-DL, HDTV | -| Effects | HDR, Dolby Vision, Atmos | -| Release groups | Preferred release groups | -| Exclude words | Filter CAM, TS, low-quality releases | -| Wash | Disabled by default; can prioritize resolution, quality, effects, or seeders | - -Download and subscription cards show only safe display metadata such as title, poster, speed, progress, and size. Raw torrent URLs are hidden to prevent tracker token leaks in multi-user deployments. - ---- - -## 🔌 External Clients & Emby Compatibility - -MediaStationGo exposes Emby/Jellyfin-style APIs for clients such as: - -- Infuse -- VidHub -- SenPlayer -- Other Emby/Jellyfin-compatible players - -Server URL: - -```text -http://:18080 -``` - -If a client cannot connect, check: - -1. Docker port mapping, typically `18080:8080`. -2. Firewall access from LAN to port `18080`. -3. Username/password. -4. Reverse proxy handling of `/api`, video streams, and Range requests. - -### Functional Reference to MoviePilot - -For external-client compatibility and media workflow integration, MediaStationGo references MoviePilot's mature product direction: a unified media library, subscription downloads, post-download organization, and Emby/Jellyfin-compatible APIs that connect the Web management interface with clients such as Infuse, VidHub, and SenPlayer. MediaStationGo does not aim to replace Emby/Jellyfin; instead, it provides the common browsing, playback, poster wall, season/episode hierarchy, progress tracking, and external-client access capabilities inside a lightweight Go service. - -Current compatibility focus: - -- Library, collection, season, and episode hierarchy output. -- Basic metadata output such as posters, backdrops, descriptions, year, and ratings. -- Stream URLs, HTTP Range support, playback progress, and resume. -- Emby/Jellyfin-style endpoints required for external-client login, browsing, and playback. - -Still being improved: - -- More complete Emby/Jellyfin device capability negotiation. -- More detailed transcoding profiles and subtitle capability declarations. -- Multi-user permissions, library filtering, and playback-history synchronization. -- Closed-loop integration with subscriptions, post-download organization, and upgrade rules. - -> MoviePilot is licensed under GPL-3.0. MediaStationGo references its public product ideas and interaction patterns only, and does not copy private data, secrets, tracker accounts, or incompatible implementations. - ---- - -## 🧠 AI and External Services - -Configure external services in the admin UI: - -| Service | Purpose | -| --- | --- | -| TMDb | Movie/TV posters, backdrops, descriptions | -| Bangumi | Anime and Chinese anime metadata | -| TheTVDB | Additional TV/season/episode metadata | -| Fanart.tv | High-quality logos and artwork | -| Douban | Chinese movie/TV search and recommendation supplement | -| OpenAI Compatible | AI search, recommendations, operations assistant | - -For M-Team, use an API Access Token: - -```text -Control Panel → Lab → Access Token -HTTP Header: x-api-key -``` - -Avoid using cookies for open API calls to reduce account risk. - ---- - ---- - -## 🔐 Privacy and Safety - -The repository ignores personal/runtime data by default: - -- `data/`, `cache/`, `logs/` -- `.tmp-deploy-data/`, `.tmp-deploy-server.*` -- `.mediastation.pid` -- `config.yaml`, `.env*` -- `*.db`, `*.db-wal`, `*.log` -- `web/dist/`, `node_modules/`, `bin/` -- API keys, cookies, tokens, passwords, certificates, and secret files - -Before pushing, run: - -```bash -git status --short -git ls-files | grep -E 'data/|cache/|\.db|\.log|jwt_secret|config.yaml|\.env|token|apikey|password' || true -``` - ---- - -## ❓ FAQ - -### Pulling the GHCR image fails with `EOF`. - -`EOF` usually means the connection from your server/NAS to GHCR was interrupted. It is normally a network or registry connectivity issue, not a compose syntax issue. Try: - -```bash -# 1. Clear any stale GHCR login state -docker logout ghcr.io || true - -# 2. Pull the image directly -docker pull ghcr.io/shukebta/mediastation-go:latest - -# 3. On x86_64/AMD64 hosts, retry with an explicit platform -docker pull --platform linux/amd64 ghcr.io/shukebta/mediastation-go:latest - -# 4. Start after the pull succeeds -docker compose up -d -``` - -If the server is behind a restricted network, configure a Docker daemon proxy that can reach GHCR. A shell-only proxy is often not inherited by the Docker service. The default compose file uses `pull_policy: missing` to avoid contacting GHCR on every container restart. - -If your media path is an absolute NAS path, use `/your-nas/...` instead of `./your-nas/...`; the latter is relative to the compose directory. - -### The Docker deployment starts but the browser cannot open the site. - -Check container status and logs: +Check the container: ```bash docker ps -docker logs -f mediastation-go +docker logs --tail=100 mediastation-go ``` -Use the host port, usually `http://:18080`. +Then open: -### External clients report that the server does not respond. +```text +http://SERVER_IP:18080 +``` -Check firewall rules, Docker port mappings, reverse proxy configuration, and LAN access to `18080`. The container listens on `8080`; the host default is `18080`. +### 2. The library cannot find files -### Posters are missing. +Most cases are path mistakes. -Check: +- Docker maps media to `/media`. +- In the web UI, use `/media/Movies`, not the original NAS path. +- Docker maps downloads to `/downloads`; use `/downloads` as the organization source when possible. -1. Local `poster.jpg`, `fanart.jpg`, and NFO files. -2. TMDb / Bangumi / Douban connectivity. -3. Proxy settings if the host is behind a restricted network. -4. Whether file names contain clear title, year, season, and episode information. +### 3. qBittorrent cannot connect -### Why are raw download URLs hidden? +If qBittorrent is on the host, try: -PT download URLs often include private tokens. Download and subscription views intentionally hide raw URLs and only show safe metadata such as title, poster, speed, progress, and size. +```text +http://host.docker.internal:8085 +``` -### Which Docker package should be kept? +If qBittorrent is on another machine, use that machine's LAN IP. -Keep `ghcr.io/shukebta/mediastation-go`. The old `mediastationgo` package can be removed to avoid users pulling the wrong image. +### 4. NAS CPU usage is high + +Suggested settings: + +- Set `ffprobe.max_concurrent` to `1`. +- Enable automatic organization, scrape-after-scan, and boot cloud scan only when you really need them. +- Avoid frequent full-library scans on large libraries. Prefer manual scan or scheduled night sync. + +### 5. Should I use `.env`? + +Beginners should not. Editing `docker-compose.yml` directly is easier to understand. + +`.env` is useful only for advanced users who reuse the same compose file on multiple machines. The repository keeps `docker-compose.simple.env.example`, but it is not the main path. --- -## 🗺️ Roadmap +## Features -- Broader Emby/Jellyfin client compatibility. -- Stronger adult metadata handling from local files and public pages. -- More granular subscription wash and post-download organization rules. -- Better mobile and TV interaction patterns. -- Plugin-style site adapters and notification providers. -- More end-to-end tests and automated screenshot generation. +| Area | Features | +| --- | --- | +| Libraries | Movies, TV shows, anime, variety, music, adult content | +| Metadata | NFO, local artwork, TMDb, TheTVDB, Bangumi, Douban, Fanart, JavBus/JavDB | +| Playback | Web playback, HTTP Range, HLS transcoding, direct links, STRMURL, 302 redirect | +| External clients | Emby-style APIs for many third-party apps | +| Downloads | qBittorrent, site search, subscriptions, post-download organization | +| File manager | Browse, organize, copy, move, hardlink, symlink | +| Operations | Task queue, recycle bin, duplicate files, notifications, logs | +| AI | OpenAI-compatible API, AI search, recommendations, assistant | --- -## 🤝 Contributing +## Screenshots -Issues, pull requests, site adapters, scraping rules, UI improvements, and documentation fixes are welcome. +
+Preview -Before submitting changes, please run: +| Login | Home | +| --- | --- | +| Login | Home | + +| Libraries | Player | +| --- | --- | +| Libraries | Player | + +
+ +--- + +## Development + +Regular users should use Docker. Developers can run: + +```bash +go run ./cmd/server +``` + +Frontend: + +```bash +cd web +npm install +npm run dev +``` + +Tests: ```bash go test ./... @@ -996,13 +374,15 @@ cd web && npm run build --- -## 👥 Developer Group +## Community and Friends -- Telegram: +- Telegram group: +- NodeSeek: [https://www.nodeseek.com/](https://www.nodeseek.com/) +- LINUX DO: [https://linux.do/](https://linux.do/) --- -## 🍜 Donation +## Donation If MediaStationGo saves you time, feel free to buy the author a bowl of noodles. @@ -1010,7 +390,7 @@ If MediaStationGo saves you time, feel free to buy the author a bowl of noodles. --- -## ⭐ Star History +## Star History @@ -1022,14 +402,16 @@ If MediaStationGo saves you time, feel free to buy the author a bowl of noodles. --- -## 📄 License and Non-Commercial Statement +## License and Non-Commercial Statement -This project uses `GPL-3.0` as its base license. See [LICENSE](LICENSE) for details. The maintainers also state and request the following usage boundary: +This project uses `GPL-3.0` as its base license. See [LICENSE](LICENSE). + +The maintainers also state and request: - The project is intended for personal learning, home NAS, self-hosted media, non-commercial research, and community collaboration. - Without explicit written permission from the author, do not use this project or derivative versions for commercial resale, paid hosting, paid SaaS, pre-installed commercial devices, closed-source redistribution, or other profit-oriented commercial use. -- For commercial cooperation, enterprise deployment, custom development, integrated redistribution, or commercial authorization, contact the author first to confirm the authorization scope. -- If there is any interpretive difference between the README non-commercial statement and the formal `GPL-3.0` license text, the code license is governed by [LICENSE](LICENSE); commercial usage should additionally obtain author permission. +- For commercial cooperation, enterprise deployment, custom development, integrated redistribution, or commercial authorization, contact the author first. +- If there is any interpretive difference between this README and the formal `GPL-3.0` license text, the code license is governed by [LICENSE](LICENSE); commercial usage should additionally obtain author permission. --- diff --git a/docker-compose.simple.env.example b/docker-compose.simple.env.example index e839ded..03c1887 100644 --- a/docker-compose.simple.env.example +++ b/docker-compose.simple.env.example @@ -1,10 +1,14 @@ -# MediaStationGo 新手部署 .env 示例 -# 只改下面两个路径即可;必须是宿主机/NAS真实路径。 +# MediaStationGo 可选 .env 示例 +# +# 新手不建议使用 .env;请直接改 docker-compose.yml,更直观。 +# 这个文件只给进阶用户复用多台机器配置时参考。 +# +# 如果你确实要用 .env,下面两个路径必须是宿主机/NAS真实路径。 MEDIASTATION_MEDIA_DIR=/your-nas/media MEDIASTATION_DOWNLOAD_DIR=/your-nas/downloads -# 可选:固定镜像版本。不写则使用 latest。 +# 可选:固定镜像版本。不写则使用 compose 里的 latest。 # MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.32 # 可选:修改访问端口。不写则使用 18080。 diff --git a/docker-compose.yml b/docker-compose.yml index d77f696..fa6768b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,53 +1,68 @@ -# MediaStationGo 极简 Docker Compose 部署文件 +# MediaStationGo 最简单 Docker Compose 部署文件 # -# 新手推荐:直接在本文件里填写 NAS/服务器真实路径,不必先创建 .env。 -# 需要改两组位置: -# 1. volumes 里媒体库和下载目录左侧的宿主机路径。 -# 2. environment 里 MEDIASTATION_MEDIA_DIR / MEDIASTATION_DOWNLOAD_DIR 的同名真实路径。 +# 新手建议: +# 1. 不用 .env。 +# 2. 直接改本文件。 +# 3. 第一次可以不改路径,先用当前目录下的 ./media 和 ./downloads 体验。 # -# 示例: -# - /vol1/1000/Docker/moviepilot-v2/media:/media:ro -# - /vol1/1000/qBittorrent/downloads:/downloads -# MEDIASTATION_MEDIA_DIR: /vol1/1000/Docker/moviepilot-v2/media -# MEDIASTATION_DOWNLOAD_DIR: /vol1/1000/qBittorrent/downloads +# 启动: +# docker compose up -d # -# .env 是可选高级写法,适合多环境复用;README 后半部分有说明。 +# 访问: +# http://服务器IP:18080 # -# 启动:docker compose up -d -# 访问:http://<服务器IP>:18080 -# 默认账号:admin / admin123 +# 默认账号: +# admin / admin123 services: mediastation-go: - image: ghcr.io/shukebta/mediastation-go:${MEDIASTATION_IMAGE_TAG:-latest} + image: ghcr.io/shukebta/mediastation-go:latest container_name: mediastation-go restart: unless-stopped init: true - pull_policy: missing + # 浏览器访问端口。 + # 如果 18080 被占用,可以改成 "19011:8080" 之类。 ports: - - "${MEDIASTATION_HTTP_PORT:-18080}:8080" + - "18080:8080" - # 让容器可以用 http://host.docker.internal:端口 访问宿主机上的 qBittorrent 等服务。 + # 让容器可以访问宿主机上的 qBittorrent。 + # qB 地址通常可填:http://host.docker.internal:8085 extra_hosts: - "host.docker.internal:host-gateway" volumes: - # 程序数据与缓存:放在部署目录下,方便备份。 - - ${MEDIASTATION_DATA_DIR:-./data}:/data - - ${MEDIASTATION_CACHE_DIR:-./cache}:/cache + # 程序数据:数据库、账号、设置都在这里。升级前主要备份它。 + - ./data:/data - # 媒体库与下载目录:左边是宿主机/NAS真实路径,右边是容器内路径。 - # 新手部署时推荐直接把左边改成真实绝对路径,不要写 ./vol1/...。 - # Web 页面里添加媒体库请填写 /media/电影、/media/电视剧 等容器路径。 - - ${MEDIASTATION_MEDIA_DIR:-./media}:/media:ro - - ${MEDIASTATION_DOWNLOAD_DIR:-./downloads}:/downloads + # 缓存目录:海报缓存、临时文件等。通常不用备份。 + - ./cache:/cache + + # 媒体库目录。 + # 新手可先把影片放到当前目录的 ./media。 + # NAS 用户把左边改成真实路径,例如: + # - /vol1/1000/Media:/media:ro + # Windows Docker Desktop 示例: + # - D:/Media:/media:ro + - ./media:/media:ro + + # 下载目录。 + # qB 下载目录、手动整理、自动整理会经常用到。 + # NAS 示例: + # - /vol1/1000/Downloads:/downloads + # Windows Docker Desktop 示例: + # - D:/Downloads:/downloads + - ./downloads:/downloads environment: - TZ: ${TZ:-Asia/Shanghai} - PUID: ${PUID:-1000} - PGID: ${PGID:-1000} + TZ: Asia/Shanghai + # Linux/NAS 用户权限。一般 1000 就可以。 + # 如果写入文件权限不对,再改成宿主机实际用户的 uid/gid。 + PUID: "1000" + PGID: "1000" + + # 程序基础配置,通常不用改。 MEDIASTATION_APP_HOST: 0.0.0.0 MEDIASTATION_APP_PORT: 8080 MEDIASTATION_APP_WEB_DIR: /app/web/dist @@ -55,20 +70,24 @@ services: MEDIASTATION_DATABASE_DB_PATH: /data/mediastation.db MEDIASTATION_CACHE_CACHE_DIR: /cache - # 路径提示:这里要和 volumes 左侧真实路径保持一致。 - # 用于把宿主机路径自动换算成容器路径,避免误填 NAS 原始路径时报不可访问。 - MEDIASTATION_MEDIA_DIR: ${MEDIASTATION_MEDIA_DIR:-./media} + # 路径换算配置。 + # 如果上面 volumes 的 ./media 改成 /vol1/1000/Media, + # 这里也要改成同一个宿主机真实路径。 + MEDIASTATION_MEDIA_DIR: ./media MEDIASTATION_MEDIA_CONTAINER_DIR: /media - MEDIASTATION_DOWNLOAD_DIR: ${MEDIASTATION_DOWNLOAD_DIR:-./downloads} + + # 如果上面 volumes 的 ./downloads 改成 /vol1/1000/Downloads, + # 这里也要改成同一个宿主机真实路径。 + MEDIASTATION_DOWNLOAD_DIR: ./downloads MEDIASTATION_DOWNLOAD_CONTAINER_DIR: /downloads - # NAS 友好的低负载转码默认值;可在系统设置里继续调整。 - MEDIASTATION_TRANSCODER_ENABLED: ${MEDIASTATION_TRANSCODER_ENABLED:-true} - MEDIASTATION_TRANSCODER_HARDWARE_ACCEL: ${MEDIASTATION_TRANSCODER_HARDWARE_ACCEL:-false} - MEDIASTATION_TRANSCODER_REALTIME: ${MEDIASTATION_TRANSCODER_REALTIME:-true} - MEDIASTATION_TRANSCODER_THREADS: ${MEDIASTATION_TRANSCODER_THREADS:-2} - MEDIASTATION_TRANSCODER_MAX_CONCURRENT: ${MEDIASTATION_TRANSCODER_MAX_CONCURRENT:-1} - MEDIASTATION_TRANSCODER_IDLE_TIMEOUT_SECONDS: ${MEDIASTATION_TRANSCODER_IDLE_TIMEOUT_SECONDS:-120} + # NAS 友好的低负载默认值。 + MEDIASTATION_TRANSCODER_ENABLED: "true" + MEDIASTATION_TRANSCODER_HARDWARE_ACCEL: "false" + MEDIASTATION_TRANSCODER_REALTIME: "true" + MEDIASTATION_TRANSCODER_THREADS: "2" + MEDIASTATION_TRANSCODER_MAX_CONCURRENT: "1" + MEDIASTATION_TRANSCODER_IDLE_TIMEOUT_SECONDS: "120" healthcheck: test: ["CMD-SHELL", "busybox wget -qO- http://127.0.0.1:8080/api/health || exit 1"] @@ -77,8 +96,7 @@ services: retries: 5 start_period: 30s - # 限制容器日志体积:默认 json-file 驱动不设上限, - # 长期运行会持续吃宿主机磁盘与 IO。 + # 限制 Docker 日志大小,避免长期运行把磁盘写满。 logging: driver: json-file options: diff --git a/internal/service/telegram_api_test.go b/internal/service/telegram_api_test.go index 2ed8a26..026e3e8 100644 --- a/internal/service/telegram_api_test.go +++ b/internal/service/telegram_api_test.go @@ -96,14 +96,20 @@ func TestTelegramTargetChatIDsUsesLegacyPrivateChatID(t *testing.T) { func TestRegisterTelegramBotCommands(t *testing.T) { var gotPath string - var payload struct { + var payloads []struct { Commands []telegramBotCommand `json:"commands"` + Scope map[string]any `json:"scope"` } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path + var payload struct { + Commands []telegramBotCommand `json:"commands"` + Scope map[string]any `json:"scope"` + } if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { t.Fatalf("decode payload: %v", err) } + payloads = append(payloads, payload) _, _ = w.Write([]byte(`{"ok":true}`)) })) defer server.Close() @@ -118,9 +124,55 @@ func TestRegisterTelegramBotCommands(t *testing.T) { if gotPath != "/bot123456:ABC/setMyCommands" { t.Fatalf("path = %q", gotPath) } - if len(payload.Commands) == 0 || payload.Commands[0].Command != "start" { - t.Fatalf("commands not registered: %#v", payload.Commands) + if len(payloads) < 3 { + t.Fatalf("expected default/private/group command registrations, got %d", len(payloads)) } + if len(payloads[0].Commands) == 0 || payloads[0].Commands[0].Command != "start" { + t.Fatalf("commands not registered: %#v", payloads[0].Commands) + } + var groupCommands []telegramBotCommand + for _, payload := range payloads { + if payload.Scope["type"] == "all_group_chats" { + groupCommands = payload.Commands + break + } + } + if len(groupCommands) == 0 { + t.Fatal("group command scope was not registered") + } + for _, command := range groupCommands { + if command.Command == "users" || command.Command == "status" || command.Command == "cleanup" || command.Command == "register" || command.Command == "redeem" { + t.Fatalf("group commands must not expose private/admin command %q", command.Command) + } + } +} + +func TestTelegramCommandMenusSeparateGroupAndAdminCommands(t *testing.T) { + groupNames := telegramCommandNames(telegramGroupBotCommandMenu()) + for _, forbidden := range []string{"status", "search", "downloads", "stats", "users", "cleanup", "cleanup_rule", "register", "redeem"} { + if groupNames[forbidden] { + t.Fatalf("group menu should not expose %s", forbidden) + } + } + for _, required := range []string{"start", "menu", "help", "account", "signin", "devices", "kick", "hideadult"} { + if !groupNames[required] { + t.Fatalf("group menu should include %s", required) + } + } + adminNames := telegramCommandNames(telegramAdminBotCommandMenu()) + for _, required := range []string{"users", "status", "cleanup_rule"} { + if !adminNames[required] { + t.Fatalf("admin menu should include %s", required) + } + } +} + +func telegramCommandNames(commands []telegramBotCommand) map[string]bool { + names := make(map[string]bool, len(commands)) + for _, command := range commands { + names[command.Command] = true + } + return names } func TestTelegramProxyCandidatesDefaultLocalFallbacks(t *testing.T) { diff --git a/internal/service/telegram_bot.go b/internal/service/telegram_bot.go index 8126d9c..54d414a 100644 --- a/internal/service/telegram_bot.go +++ b/internal/service/telegram_bot.go @@ -251,12 +251,31 @@ func telegramCommandName(text string) string { return cmd } +func telegramIsGroupChat(chatType string) bool { + return chatType != "" && chatType != "private" +} + +func telegramGroupPrivateAdminHint() string { + return "管理命令请私聊 Bot 使用 /menu 或对应管理员命令,避免在群组公开管理面板。" +} + +func telegramGroupPrivateUserHint(action string) string { + action = strings.TrimSpace(action) + if action == "" { + action = "此操作" + } + return action + "包含账号凭据或敏感信息,请私聊 Bot 操作;群组内仅开放账号状态、签到、设备与成人目录开关。" +} + // cmdStart 处理 /start 命令。 func (s *TelegramBotService) cmdStart(ctx context.Context, msg *TelegramMessage, args []string) telegramCommandReply { name := msg.From.FirstName if msg.From.Username != "" { name = "@" + msg.From.Username } + if telegramIsGroupChat(msg.Chat.Type) && len(args) > 0 { + return telegramCommandReply{Text: telegramGroupPrivateUserHint("绑定账号")} + } if len(args) == 0 { if binding := s.telegramBinding(ctx, msg.From.ID); binding != nil { user, _ := s.repo.User.FindByID(ctx, binding.UserID) @@ -420,6 +439,21 @@ func (s *TelegramBotService) cmdRegistrationToggle(ctx context.Context, args []s // cmdHelp 处理 /help 命令。 func (s *TelegramBotService) cmdHelp(ctx context.Context, msg *TelegramMessage) string { channel := s.findChannelForMessage(ctx, msg) + if telegramIsGroupChat(msg.Chat.Type) { + adminHint := "" + if s.telegramUserIsAdmin(ctx, channel, msg.From.ID) { + adminHint = "\n\n管理员命令和管理面板请私聊 Bot 使用,避免在群组公开。" + } + return "MediaStationGo 群组可用命令\n\n" + + "/menu — 打开群组自助菜单\n" + + "/account — 查看账号状态\n" + + "/signin — 签到\n" + + "/devices — 查看登录设备\n" + + "/kick all|编号 — 踢下线设备\n" + + "/hideadult on|off — 隐藏或显示成人目录\n\n" + + "绑定、注册、兑换、改名、改密等包含敏感信息的操作请私聊 Bot。" + + adminHint + } if !s.telegramUserIsAdmin(ctx, channel, msg.From.ID) { register := "" if s.openRegEnabled(ctx) { diff --git a/internal/service/telegram_bot_user_test.go b/internal/service/telegram_bot_user_test.go index ca92824..1de7c5d 100644 --- a/internal/service/telegram_bot_user_test.go +++ b/internal/service/telegram_bot_user_test.go @@ -134,6 +134,74 @@ func TestTelegramRegisterRespectsAdminToggle(t *testing.T) { } } +func TestTelegramGroupHidesAdminPanelFromRegularUsers(t *testing.T) { + ctx := t.Context() + _, bot := newBotTestService(t) + channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"group_chat_id":"-100123","admin_user_ids":"9001"}`} + msg := &TelegramMessage{ + From: TelegramUser{ID: 9002, Username: "viewer", FirstName: "Viewer"}, + Chat: TelegramChat{ID: -100123, Type: "supergroup"}, + } + + menu := bot.mainMenu(ctx, channel, msg) + if strings.Contains(menu.Text, "管理员") || telegramReplyHasButtonPrefix(menu, "adm_") { + t.Fatalf("regular group user must not see admin panel: text=%q buttons=%#v", menu.Text, menu.Buttons) + } + + reply, err := bot.executeCommand(ctx, channel, msg, "/users") + if err != nil { + t.Fatal(err) + } + if reply.Text != "" || len(reply.Buttons) != 0 { + t.Fatalf("regular group user admin command should be ignored, got %#v", reply) + } + + reply, err = bot.executeCommand(ctx, channel, msg, "/start viewer secret-pass") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(reply.Text, "请私聊 Bot") { + t.Fatalf("group credential command should point to private chat, got %q", reply.Text) + } +} + +func TestTelegramGroupAdminMenuDoesNotExposeButtonsInGroup(t *testing.T) { + ctx := t.Context() + _, bot := newBotTestService(t) + channel := &model.NotifyChannel{Name: "Telegram", Type: "telegram", Enabled: true, Config: `{"group_chat_id":"-100123","admin_user_ids":"9001"}`} + msg := &TelegramMessage{ + From: TelegramUser{ID: 9001, Username: "admin", FirstName: "Admin"}, + Chat: TelegramChat{ID: -100123, Type: "group"}, + } + + menu := bot.mainMenu(ctx, channel, msg) + if telegramReplyHasButtonPrefix(menu, "adm_") { + t.Fatalf("admin group menu must not expose admin buttons publicly: %#v", menu.Buttons) + } + if !strings.Contains(menu.Text, "请私聊 Bot") { + t.Fatalf("admin group menu should tell admins to use private chat, got %q", menu.Text) + } + + reply, handled := bot.handleMenuCallback(ctx, channel, msg, "adm_users") + if !handled { + t.Fatal("admin callback should be handled") + } + if !strings.Contains(reply.Text, "请私聊 Bot") || telegramReplyHasButtonPrefix(reply, "adm_") { + t.Fatalf("group admin callback should not render admin panel publicly: %#v", reply) + } +} + +func telegramReplyHasButtonPrefix(reply telegramCommandReply, prefix string) bool { + for _, row := range reply.Buttons { + for _, button := range row { + if strings.HasPrefix(button.Data, prefix) { + return true + } + } + } + return false +} + func TestTelegramStartClearsStaleUserBinding(t *testing.T) { repos, auth, _, _ := newAuthTestServices(t) if err := repos.DB.Create(&model.TelegramBinding{ diff --git a/internal/service/telegram_commands.go b/internal/service/telegram_commands.go index a69be43..5d51b57 100644 --- a/internal/service/telegram_commands.go +++ b/internal/service/telegram_commands.go @@ -16,31 +16,32 @@ type telegramCommandDefinition struct { Aliases []string AdminOnly bool AdminOnlyText string + GroupAllowed bool Handle telegramCommandHandler } func (s *TelegramBotService) telegramCommandDefinitions(ctx context.Context, channel *model.NotifyChannel, msg *TelegramMessage) []telegramCommandDefinition { adminOnly := "此命令仅管理员可用。" return []telegramCommandDefinition{ - {Aliases: []string{"/start"}, Handle: func(args []string) (telegramCommandReply, error) { + {Aliases: []string{"/start"}, GroupAllowed: true, Handle: func(args []string) (telegramCommandReply, error) { if len(args) == 0 { return s.mainMenu(ctx, channel, msg), nil } return s.cmdStart(ctx, msg, args), nil }}, - {Aliases: []string{"/menu"}, Handle: func(args []string) (telegramCommandReply, error) { return s.mainMenu(ctx, channel, msg), nil }}, - {Aliases: []string{"/cancel"}, Handle: func(args []string) (telegramCommandReply, error) { + {Aliases: []string{"/menu"}, GroupAllowed: true, Handle: func(args []string) (telegramCommandReply, error) { return s.mainMenu(ctx, channel, msg), nil }}, + {Aliases: []string{"/cancel"}, GroupAllowed: true, Handle: func(args []string) (telegramCommandReply, error) { s.takePending(int64(msg.From.ID)) return telegramCommandReply{Text: "已取消当前操作。"}, nil }}, - {Aliases: []string{"/help"}, Handle: func(args []string) (telegramCommandReply, error) { + {Aliases: []string{"/help"}, GroupAllowed: true, Handle: func(args []string) (telegramCommandReply, error) { return telegramCommandReply{Text: s.cmdHelp(ctx, msg)}, nil }}, - {Aliases: []string{"/hideadult", "/hide_adult", "/adult"}, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdHideAdult(ctx, msg, args), nil }}, - {Aliases: []string{"/account", "/me"}, Handle: func(args []string) (telegramCommandReply, error) { return s.replyAccount(ctx, msg), nil }}, - {Aliases: []string{"/signin", "/checkin"}, Handle: func(args []string) (telegramCommandReply, error) { return s.replySignIn(ctx, msg), nil }}, - {Aliases: []string{"/devices"}, Handle: func(args []string) (telegramCommandReply, error) { return s.replyDevices(ctx, msg), nil }}, - {Aliases: []string{"/kick"}, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdKick(ctx, msg, args), nil }}, + {Aliases: []string{"/hideadult", "/hide_adult", "/adult"}, GroupAllowed: true, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdHideAdult(ctx, msg, args), nil }}, + {Aliases: []string{"/account", "/me"}, GroupAllowed: true, Handle: func(args []string) (telegramCommandReply, error) { return s.replyAccount(ctx, msg), nil }}, + {Aliases: []string{"/signin", "/checkin"}, GroupAllowed: true, Handle: func(args []string) (telegramCommandReply, error) { return s.replySignIn(ctx, msg), nil }}, + {Aliases: []string{"/devices"}, GroupAllowed: true, Handle: func(args []string) (telegramCommandReply, error) { return s.replyDevices(ctx, msg), nil }}, + {Aliases: []string{"/kick"}, GroupAllowed: true, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdKick(ctx, msg, args), nil }}, {Aliases: []string{"/setname", "/rename"}, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSetName(ctx, msg, args), nil }}, {Aliases: []string{"/setpass", "/passwd", "/password"}, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdSetPass(ctx, msg, args), nil }}, {Aliases: []string{"/redeem"}, Handle: func(args []string) (telegramCommandReply, error) { return s.cmdRedeem(ctx, channel, msg, args), nil }}, @@ -102,6 +103,12 @@ func (s *TelegramBotService) executeCommand(ctx context.Context, channel *model. if !ok { return telegramCommandReply{Text: fmt.Sprintf("未知命令: %s\n\n输入 /help 查看可用命令列表。", cmd)}, nil } + if telegramIsGroupChat(msg.Chat.Type) && !def.GroupAllowed { + if def.AdminOnly && s.telegramUserIsAdmin(ctx, channel, msg.From.ID) { + return telegramCommandReply{Text: telegramGroupPrivateAdminHint()}, nil + } + return telegramCommandReply{}, nil + } if def.AdminOnly && !s.telegramUserIsAdmin(ctx, channel, msg.From.ID) { return telegramCommandReply{Text: def.AdminOnlyText}, nil } @@ -133,6 +140,10 @@ type telegramBotCommand struct { } func telegramBotCommandMenu() []telegramBotCommand { + return telegramPrivateBotCommandMenu() +} + +func telegramPrivateBotCommandMenu() []telegramBotCommand { return []telegramBotCommand{ {Command: "start", Description: "绑定账号或打开主菜单"}, {Command: "menu", Description: "打开功能菜单"}, @@ -144,22 +155,62 @@ func telegramBotCommandMenu() []telegramBotCommand { {Command: "hideadult", Description: "隐藏/显示成人媒体库"}, {Command: "redeem", Description: "兑换注册码或续期码"}, {Command: "register", Description: "注册新账号"}, - {Command: "status", Description: "系统运行状态(管理员)"}, - {Command: "search", Description: "搜索媒体库(管理员)"}, - {Command: "downloads", Description: "下载列表(管理员)"}, - {Command: "stats", Description: "媒体库统计(管理员)"}, - {Command: "users", Description: "用户管理(管理员)"}, - {Command: "cleanup", Description: "删号规则巡检(管理员)"}, - {Command: "cleanup_rule", Description: "保号规则管理(管理员)"}, } } +func telegramGroupBotCommandMenu() []telegramBotCommand { + return []telegramBotCommand{ + {Command: "start", Description: "打开群组自助菜单"}, + {Command: "menu", Description: "打开群组自助菜单"}, + {Command: "help", Description: "查看群组可用命令"}, + {Command: "account", Description: "查看账号状态"}, + {Command: "signin", Description: "签到"}, + {Command: "devices", Description: "查看登录设备"}, + {Command: "kick", Description: "踢下线设备"}, + {Command: "hideadult", Description: "隐藏/显示成人媒体库"}, + } +} + +func telegramAdminBotCommandMenu() []telegramBotCommand { + commands := append([]telegramBotCommand{}, telegramPrivateBotCommandMenu()...) + commands = append(commands, + telegramBotCommand{Command: "status", Description: "系统运行状态(管理员)"}, + telegramBotCommand{Command: "search", Description: "搜索媒体库(管理员)"}, + telegramBotCommand{Command: "downloads", Description: "下载列表(管理员)"}, + telegramBotCommand{Command: "stats", Description: "媒体库统计(管理员)"}, + telegramBotCommand{Command: "users", Description: "用户管理(管理员)"}, + telegramBotCommand{Command: "cleanup", Description: "删号规则巡检(管理员)"}, + telegramBotCommand{Command: "cleanup_rule", Description: "保号规则管理(管理员)"}, + ) + return commands +} + func registerTelegramBotCommands(ctx context.Context, cfg map[string]string) error { if strings.TrimSpace(cfg["bot_token"]) == "" { return nil } - payload := map[string]interface{}{ - "commands": telegramBotCommandMenu(), + normalizeTelegramConfig(cfg) + if err := telegramSetBotCommands(ctx, cfg, telegramPrivateBotCommandMenu(), nil); err != nil { + return err + } + if err := telegramSetBotCommands(ctx, cfg, telegramPrivateBotCommandMenu(), map[string]interface{}{"type": "all_private_chats"}); err != nil { + return err + } + if err := telegramSetBotCommands(ctx, cfg, telegramGroupBotCommandMenu(), map[string]interface{}{"type": "all_group_chats"}); err != nil { + return err + } + + adminCommands := telegramAdminBotCommandMenu() + for _, adminID := range telegramConfiguredUserIDs(cfg["admin_user_ids"]) { + _ = telegramSetBotCommands(ctx, cfg, adminCommands, map[string]interface{}{"type": "chat", "chat_id": adminID}) + } + return nil +} + +func telegramSetBotCommands(ctx context.Context, cfg map[string]string, commands []telegramBotCommand, scope map[string]interface{}) error { + payload := map[string]interface{}{"commands": commands} + if scope != nil { + payload["scope"] = scope } return telegramPostJSON(ctx, cfg, "setMyCommands", payload, 15*time.Second) } diff --git a/internal/service/telegram_menu.go b/internal/service/telegram_menu.go index a2dc0bb..539a62f 100644 --- a/internal/service/telegram_menu.go +++ b/internal/service/telegram_menu.go @@ -55,11 +55,36 @@ func (s *TelegramBotService) boundUser(ctx context.Context, telegramUserID int) // extra management section. func (s *TelegramBotService) mainMenu(ctx context.Context, channel *model.NotifyChannel, msg *TelegramMessage) telegramCommandReply { isAdmin := s.telegramUserIsAdmin(ctx, channel, msg.From.ID) + isGroup := telegramIsGroupChat(msg.Chat.Type) user := s.boundUser(ctx, msg.From.ID) var rows [][]telegramInlineButton var header string + if isGroup { + if user == nil { + header = "MediaStationGo 群组自助菜单\n\n你还没有绑定媒体中心账号。绑定、注册、兑换等包含敏感信息的操作请私聊 Bot。" + } else { + adult := map[bool]string{true: "已隐藏", false: "已显示"}[user.HideAdult] + header = fmt.Sprintf("MediaStationGo 群组自助菜单\n\n账号:%s\n到期:%s\n成人目录:%s", + user.Username, formatExpiry(user.ExpiredAt), adult) + rows = append(rows, + []telegramInlineButton{ + {Text: "👤 我的账号", Data: "act_account"}, + {Text: "📅 签到", Data: "act_signin"}, + }, + []telegramInlineButton{ + {Text: "📱 我的设备", Data: "act_devices"}, + {Text: map[bool]string{true: "🔞 显示成人目录", false: "🔞 隐藏成人目录"}[user.HideAdult], Data: "adult_toggle"}, + }, + ) + } + if isAdmin { + header += "\n\n" + telegramGroupPrivateAdminHint() + } + return telegramCommandReply{Text: header, Buttons: rows} + } + if user == nil { header = "MediaStationGo\n\n你还没有绑定媒体中心账号。" rows = append(rows, []telegramInlineButton{{Text: "🔗 绑定账号", Data: "act_bind"}}) @@ -109,6 +134,7 @@ func (s *TelegramBotService) mainMenu(ctx context.Context, channel *model.Notify // handleMenuCallback routes inline-button taps. Returns (reply, handled). func (s *TelegramBotService) handleMenuCallback(ctx context.Context, channel *model.NotifyChannel, msg *TelegramMessage, data string) (telegramCommandReply, bool) { isAdmin := s.telegramUserIsAdmin(ctx, channel, msg.From.ID) + isGroup := telegramIsGroupChat(msg.Chat.Type) switch { case data == "noop": @@ -116,17 +142,29 @@ func (s *TelegramBotService) handleMenuCallback(ctx context.Context, channel *mo case data == "menu_main": return s.mainMenu(ctx, channel, msg), true case data == "act_bind": + if isGroup { + return telegramCommandReply{Text: telegramGroupPrivateUserHint("绑定账号")}, true + } return telegramCommandReply{Text: "请发送:/start 用户名 密码 绑定已有账号。"}, true case data == "act_register": + if isGroup { + return telegramCommandReply{Text: telegramGroupPrivateUserHint("注册账号")}, true + } if !s.openRegEnabled(ctx) { return telegramCommandReply{Text: "注册功能未开放,请联系管理员。"}, true } s.setPending(int64(msg.From.ID), "register") return telegramCommandReply{Text: "请发送新账号的 用户名 密码(空格分隔),例如:alice mypass123"}, true case data == "act_redeem_register": + if isGroup { + return telegramCommandReply{Text: telegramGroupPrivateUserHint("兑换码注册")}, true + } s.setPending(int64(msg.From.ID), "redeem_register") return telegramCommandReply{Text: "请发送你的注册兑换码,例如:ABCD2345EFGH\n(兑换后会要求设置用户名密码)"}, true case data == "act_redeem_renew": + if isGroup { + return telegramCommandReply{Text: telegramGroupPrivateUserHint("兑换码续期")}, true + } s.setPending(int64(msg.From.ID), "redeem_renew") return telegramCommandReply{Text: "请发送你的续期兑换码,将为当前绑定账号续期。"}, true case data == "act_account": @@ -136,9 +174,15 @@ func (s *TelegramBotService) handleMenuCallback(ctx context.Context, channel *mo case data == "act_devices": return s.replyDevices(ctx, msg), true case data == "act_setname": + if isGroup { + return telegramCommandReply{Text: telegramGroupPrivateUserHint("修改用户名")}, true + } s.setPending(int64(msg.From.ID), "setname") return telegramCommandReply{Text: "请发送:当前密码 新用户名。"}, true case data == "act_setpass": + if isGroup { + return telegramCommandReply{Text: telegramGroupPrivateUserHint("修改密码")}, true + } s.setPending(int64(msg.From.ID), "setpass") return telegramCommandReply{Text: "请发送:当前密码 新密码(新密码至少 6 位)。"}, true case strings.HasPrefix(data, "kick:"): @@ -146,6 +190,12 @@ func (s *TelegramBotService) handleMenuCallback(ctx context.Context, channel *mo } // ── 管理员专属 ── + if isGroup { + if isAdmin { + return telegramCommandReply{Text: telegramGroupPrivateAdminHint()}, true + } + return telegramCommandReply{}, true + } if !isAdmin { return telegramCommandReply{Text: "此功能仅管理员可用。"}, true }