mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-08 22:36:37 +08:00
优化
优化
This commit is contained in:
+1
-1
@@ -18,7 +18,7 @@ verify-downloads/
|
|||||||
.tmp_*
|
.tmp_*
|
||||||
.tmp-deploy-*
|
.tmp-deploy-*
|
||||||
.tmp-deploy-data/
|
.tmp-deploy-data/
|
||||||
.mediastation.pid
|
.mmtl.pid
|
||||||
*.db
|
*.db
|
||||||
*.db-journal
|
*.db-journal
|
||||||
*.db-shm
|
*.db-shm
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Telegram MediaStationGo 交流群
|
- name: Telegram MMTL 交流群
|
||||||
url: https://t.me/MediaStationGo
|
url: https://t.me/MMTL
|
||||||
about: 适合快速交流部署经验、使用问题和排查线索。
|
about: 适合快速交流部署经验、使用问题和排查线索。
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ assignees: ""
|
|||||||
|
|
||||||
## 期望方案
|
## 期望方案
|
||||||
|
|
||||||
请描述你希望 MediaStationGo 如何工作。
|
请描述你希望 MMTL 如何工作。
|
||||||
|
|
||||||
## 使用场景
|
## 使用场景
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: MediaStationGo CodeQL
|
name: MMTL CodeQL
|
||||||
|
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- internal/service/fileid_other.go
|
- internal/service/fileid_other.go
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
|
|
||||||
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
||||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "tag=MediaStationGo-v${NEW_VERSION}" >> $GITHUB_OUTPUT
|
echo "tag=MMTL-v${NEW_VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# 3. 更新 VERSION 文件
|
# 3. 更新 VERSION 文件
|
||||||
- name: Update version file
|
- name: Update version file
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: 'Image tag to publish, for example MediaStationGo-v0.0.32'
|
description: 'Image tag to publish, for example MMTL-v0.0.32'
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
ref:
|
ref:
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
# 自动使用当前仓库所有者
|
# 自动使用当前仓库所有者
|
||||||
images: ghcr.io/${{ github.repository_owner }}/mediastation-go
|
images: ghcr.io/${{ github.repository_owner }}/mmtl
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
type=raw,value=${{ env.RELEASE_VERSION }}
|
type=raw,value=${{ env.RELEASE_VERSION }}
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ logs/
|
|||||||
.tmp-deploy-cache/
|
.tmp-deploy-cache/
|
||||||
.tmp-deploy-server.*
|
.tmp-deploy-server.*
|
||||||
.tmp-live-server.*
|
.tmp-live-server.*
|
||||||
.mediastation.pid
|
.mmtl.pid
|
||||||
*.log
|
*.log
|
||||||
*.db
|
*.db
|
||||||
*.db-journal
|
*.db-journal
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
# 贡献规范
|
# 贡献规范
|
||||||
|
|
||||||
感谢你愿意帮助 MediaStationGo 变得更稳定。这个项目主要面向 NAS、Docker 部署、媒体库整理、订阅下载和多端播放场景;提交 Issue 或 Pull Request 时,请尽量提供可复现、可验证的信息。
|
感谢你愿意帮助 MMTL 变得更稳定。这个项目主要面向 NAS、Docker 部署、媒体库整理、订阅下载和多端播放场景;提交 Issue 或 Pull Request 时,请尽量提供可复现、可验证的信息。
|
||||||
|
|
||||||
## Issue 提交规范
|
## Issue 提交规范
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
- 期望行为:你认为正确结果应该是什么。
|
- 期望行为:你认为正确结果应该是什么。
|
||||||
- 复现步骤:从哪个页面、点击什么、填写什么、触发什么任务。
|
- 复现步骤:从哪个页面、点击什么、填写什么、触发什么任务。
|
||||||
- 部署方式:Docker 第一档 / 第二档 / 第三档、裸机运行、反代方式等。
|
- 部署方式:Docker 第一档 / 第二档 / 第三档、裸机运行、反代方式等。
|
||||||
- 环境信息:NAS 型号或系统、Docker / Compose 版本、浏览器、MediaStationGo 镜像版本。
|
- 环境信息:NAS 型号或系统、Docker / Compose 版本、浏览器、MMTL 镜像版本。
|
||||||
- 相关配置:路径映射、下载器保存路径、媒体库路径、站点类型等。请隐藏 Cookie、API Key、密码和 Token。
|
- 相关配置:路径映射、下载器保存路径、媒体库路径、站点类型等。请隐藏 Cookie、API Key、密码和 Token。
|
||||||
- 日志和任务信息:优先提供应用日志、任务队列详情、浏览器控制台错误、网络请求错误。
|
- 日志和任务信息:优先提供应用日志、任务队列详情、浏览器控制台错误、网络请求错误。
|
||||||
|
|
||||||
@@ -28,14 +28,14 @@ Docker 部署常用命令:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose ps
|
docker compose ps
|
||||||
docker compose logs --tail=300 mediastation-go
|
docker compose logs --tail=300 mmtl
|
||||||
docker compose exec mediastation-go sh -lc 'ls -la /data/logs || true'
|
docker compose exec mmtl sh -lc 'ls -la /data/logs || true'
|
||||||
```
|
```
|
||||||
|
|
||||||
PostgreSQL 部署查询示例:
|
PostgreSQL 部署查询示例:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose exec postgres psql -U mediastation -d mediastation -c "select key,value,updated_at from settings order by updated_at desc limit 30;"
|
docker compose exec postgres psql -U mmtl -d mmtl -c "select key,value,updated_at from settings order by updated_at desc limit 30;"
|
||||||
```
|
```
|
||||||
|
|
||||||
请勿公开粘贴以下敏感信息:
|
请勿公开粘贴以下敏感信息:
|
||||||
|
|||||||
+12
-12
@@ -1,6 +1,6 @@
|
|||||||
# syntax=docker/dockerfile:1.6
|
# syntax=docker/dockerfile:1.6
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Multi-architecture build for MediaStationGo.
|
# Multi-architecture build for MMTL.
|
||||||
#
|
#
|
||||||
# Stage 1 (frontend) : Node 20 -> static SPA bundle
|
# Stage 1 (frontend) : Node 20 -> static SPA bundle
|
||||||
# Stage 2 (backend) : Go 1.25 -> single static binary (CGO_ENABLED=0)
|
# Stage 2 (backend) : Go 1.25 -> single static binary (CGO_ENABLED=0)
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
#
|
#
|
||||||
# Build:
|
# Build:
|
||||||
# docker buildx build --platform linux/amd64,linux/arm64 \
|
# docker buildx build --platform linux/amd64,linux/arm64 \
|
||||||
# --build-arg VERSION=MediaStationGo-v0.1.16 -t mediastation-go:latest --push .
|
# --build-arg VERSION=MMTL-v0.1.16 -t mmtl:latest --push .
|
||||||
#
|
#
|
||||||
# Optional Intel VAAPI/QSV runtime packages:
|
# Optional Intel VAAPI/QSV runtime packages:
|
||||||
# docker buildx build --build-arg WITH_VAAPI=true ...
|
# docker buildx build --build-arg WITH_VAAPI=true ...
|
||||||
@@ -39,7 +39,7 @@ COPY . .
|
|||||||
COPY --from=frontend /app/web/dist ./web/dist
|
COPY --from=frontend /app/web/dist ./web/dist
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
|
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
|
||||||
go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o mediastation-go ./cmd/server
|
go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o mmtl ./cmd/server
|
||||||
|
|
||||||
# ---- Stage 3: runtime ------------------------------------------------------
|
# ---- Stage 3: runtime ------------------------------------------------------
|
||||||
FROM alpine:3.23
|
FROM alpine:3.23
|
||||||
@@ -64,22 +64,22 @@ RUN apk add --no-cache \
|
|||||||
&& rm -rf /var/cache/apk/*
|
&& rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
# Non-root user for the long-running process.
|
# Non-root user for the long-running process.
|
||||||
RUN addgroup -S mediastation && adduser -S mediastation -G mediastation
|
RUN addgroup -S mmtl && adduser -S mmtl -G mmtl
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=backend /app/mediastation-go /usr/local/bin/mediastation-go
|
COPY --from=backend /app/mmtl /usr/local/bin/mmtl
|
||||||
COPY --from=frontend /app/web/dist /app/web/dist
|
COPY --from=frontend /app/web/dist /app/web/dist
|
||||||
|
|
||||||
RUN mkdir -p /data /cache /media \
|
RUN mkdir -p /data /cache /media \
|
||||||
&& chown -R mediastation:mediastation /data /cache /media
|
&& chown -R mmtl:mmtl /data /cache /media
|
||||||
|
|
||||||
# Default environment (overridable via docker-compose / `docker run -e`).
|
# Default environment (overridable via docker-compose / `docker run -e`).
|
||||||
ENV MEDIASTATION_APP_PORT=8080 \
|
ENV MMTL_APP_PORT=8080 \
|
||||||
MEDIASTATION_APP_DATA_DIR=/data \
|
MMTL_APP_DATA_DIR=/data \
|
||||||
MEDIASTATION_APP_WEB_DIR=/app/web/dist \
|
MMTL_APP_WEB_DIR=/app/web/dist \
|
||||||
MEDIASTATION_DATABASE_DB_PATH=/data/mediastation.db \
|
MMTL_DATABASE_DB_PATH=/data/mmtl.db \
|
||||||
MEDIASTATION_CACHE_CACHE_DIR=/cache \
|
MMTL_CACHE_CACHE_DIR=/cache \
|
||||||
MEDIASTATION_LOGGING_LEVEL=info \
|
MMTL_LOGGING_LEVEL=info \
|
||||||
TZ=Asia/Shanghai
|
TZ=Asia/Shanghai
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
## 项目简介
|
## 项目简介
|
||||||
|
|
||||||
MediaStationGo 是一个自托管媒体管理系统,面向 NAS、小主机、家庭影音和多用户共享场景。它把媒体库、刮削、下载整理、订阅、网盘播放、Emby 协议兼容、用户权限和 Bot 通知放在一个后台里,目标是让用户只维护一套服务,就能给网页端、手机端、电视端和第三方播放器使用。
|
MMTL 是一个自托管媒体管理系统,面向 NAS、小主机、家庭影音和多用户共享场景。它把媒体库、刮削、下载整理、订阅、网盘播放、Emby 协议兼容、用户权限和 Bot 通知放在一个后台里,目标是让用户只维护一套服务,就能给网页端、手机端、电视端和第三方播放器使用。
|
||||||
|
|
||||||
核心能力:
|
核心能力:
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ GHCR:ghcr.io/truewhile/mmtl:latest
|
|||||||
|
|
||||||
## 部署档位
|
## 部署档位
|
||||||
|
|
||||||
MediaStationGo 推荐按机器资源和用户规模选择部署档位。每份 Compose 文件都是完整文件,不需要再叠加多个 `-f`。想一个镜像跑起来就选单镜像档(SQLite);需要多用户 / 高并发时再用 PostgreSQL 三档。Redis 和 OpenSearch 是增强组件,不替代 PostgreSQL。
|
MMTL 推荐按机器资源和用户规模选择部署档位。每份 Compose 文件都是完整文件,不需要再叠加多个 `-f`。想一个镜像跑起来就选单镜像档(SQLite);需要多用户 / 高并发时再用 PostgreSQL 三档。Redis 和 OpenSearch 是增强组件,不替代 PostgreSQL。
|
||||||
|
|
||||||
| 档位 | 完整配置文件 | 组件 | 适合场景 |
|
| 档位 | 完整配置文件 | 组件 | 适合场景 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
@@ -119,12 +119,12 @@ volumes:
|
|||||||
关键数据目录:
|
关键数据目录:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
./data JWT 密钥、运行配置、SQLite 主数据库(mediastation.db)——必须备份
|
./data JWT 密钥、运行配置、SQLite 主数据库(mmtl.db)——必须备份
|
||||||
./cache 海报/临时缓存,可重建
|
./cache 海报/临时缓存,可重建
|
||||||
./media 媒体库
|
./media 媒体库
|
||||||
```
|
```
|
||||||
|
|
||||||
> 单镜像模式请不要配置 `MEDIASTATION_DATABASE_DSN`;一旦填了 DSN 就会切回 PostgreSQL。
|
> 单镜像模式请不要配置 `MMTL_DATABASE_DSN`;一旦填了 DSN 就会切回 PostgreSQL。
|
||||||
|
|
||||||
### 第一档:PostgreSQL
|
### 第一档:PostgreSQL
|
||||||
|
|
||||||
@@ -176,10 +176,10 @@ OpenSearch 数据目录是 `./opensearch`。搜索索引可重建,但重建大
|
|||||||
仓库内提供四份推荐 Compose 文件:
|
仓库内提供四份推荐 Compose 文件:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
docker-compose.simple.yml 单镜像档:MediaStationGo + 内置 SQLite
|
docker-compose.simple.yml 单镜像档:MMTL + 内置 SQLite
|
||||||
docker-compose.yml 第一档:MediaStationGo + PostgreSQL
|
docker-compose.yml 第一档:MMTL + PostgreSQL
|
||||||
docker-compose.standard.yml 第二档:MediaStationGo + PostgreSQL + Redis
|
docker-compose.standard.yml 第二档:MMTL + PostgreSQL + Redis
|
||||||
docker-compose.search.yml 第三档:MediaStationGo + PostgreSQL + Redis + OpenSearch
|
docker-compose.search.yml 第三档:MMTL + PostgreSQL + Redis + OpenSearch
|
||||||
```
|
```
|
||||||
|
|
||||||
仓库只保留面向用户部署和项目维护的必要文件。旧的本地部署脚本、发包脚本、开发机辅助脚本、`.env` 示例和旧高级 Compose 模板已经移除;Linux / Docker 用户按上面四个 Compose 文件部署即可。开发者本地生成的 `bin/`、`data/`、`cache/`、`logs/`、`.tmp/`、`tools/` 等目录已列入 `.gitignore`,不应提交到仓库。
|
仓库只保留面向用户部署和项目维护的必要文件。旧的本地部署脚本、发包脚本、开发机辅助脚本、`.env` 示例和旧高级 Compose 模板已经移除;Linux / Docker 用户按上面四个 Compose 文件部署即可。开发者本地生成的 `bin/`、`data/`、`cache/`、`logs/`、`.tmp/`、`tools/` 等目录已列入 `.gitignore`,不应提交到仓库。
|
||||||
@@ -202,7 +202,7 @@ docker compose -f docker-compose.search.yml up -d
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
mediastation-go:
|
mmtl:
|
||||||
image: ghcr.io/truewhile/MMTL:latest
|
image: ghcr.io/truewhile/MMTL:latest
|
||||||
ports:
|
ports:
|
||||||
# 左边是宿主机访问端口,右边是容器内端口。
|
# 左边是宿主机访问端口,右边是容器内端口。
|
||||||
@@ -220,17 +220,17 @@ services:
|
|||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
|
|
||||||
# PostgreSQL 主数据库。
|
# PostgreSQL 主数据库。
|
||||||
MEDIASTATION_DATABASE_TYPE: postgres
|
MMTL_DATABASE_TYPE: postgres
|
||||||
MEDIASTATION_DATABASE_DSN: postgres://mediastation:mediastation@postgres:5432/mediastation?sslmode=disable
|
MMTL_DATABASE_DSN: postgres://mmtl:mmtl@postgres:5432/mmtl?sslmode=disable
|
||||||
|
|
||||||
# 旧 SQLite 迁移源:只在从旧版 data/mediastation.db 导入时使用。
|
# 旧 SQLite 迁移源:只在从旧版 data/mmtl.db 导入时使用。
|
||||||
MEDIASTATION_DATABASE_DB_PATH: /data/mediastation.db
|
MMTL_DATABASE_DB_PATH: /data/mmtl.db
|
||||||
|
|
||||||
# 路径换算:宿主机路径和容器路径必须一一对应。
|
# 路径换算:宿主机路径和容器路径必须一一对应。
|
||||||
MEDIASTATION_MEDIA_DIR: /vol1/1000/Media
|
MMTL_MEDIA_DIR: /vol1/1000/Media
|
||||||
MEDIASTATION_MEDIA_CONTAINER_DIR: /media
|
MMTL_MEDIA_CONTAINER_DIR: /media
|
||||||
MEDIASTATION_DOWNLOAD_DIR: /vol1/1000/Downloads
|
MMTL_DOWNLOAD_DIR: /vol1/1000/Downloads
|
||||||
MEDIASTATION_DOWNLOAD_CONTAINER_DIR: /downloads
|
MMTL_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||||
```
|
```
|
||||||
|
|
||||||
## 路径映射
|
## 路径映射
|
||||||
@@ -244,10 +244,10 @@ volumes:
|
|||||||
- /vol1/1000/Docker/moviepilot-v2/media:/vol1/1000/Docker/moviepilot-v2/media
|
- /vol1/1000/Docker/moviepilot-v2/media:/vol1/1000/Docker/moviepilot-v2/media
|
||||||
- /vol1/1000/qBittorrent/downloads:/vol1/1000/qBittorrent/downloads
|
- /vol1/1000/qBittorrent/downloads:/vol1/1000/qBittorrent/downloads
|
||||||
environment:
|
environment:
|
||||||
MEDIASTATION_MEDIA_DIR: /vol1/1000/Docker/moviepilot-v2/media
|
MMTL_MEDIA_DIR: /vol1/1000/Docker/moviepilot-v2/media
|
||||||
MEDIASTATION_MEDIA_CONTAINER_DIR: /vol1/1000/Docker/moviepilot-v2/media
|
MMTL_MEDIA_CONTAINER_DIR: /vol1/1000/Docker/moviepilot-v2/media
|
||||||
MEDIASTATION_DOWNLOAD_DIR: /vol1/1000/qBittorrent/downloads
|
MMTL_DOWNLOAD_DIR: /vol1/1000/qBittorrent/downloads
|
||||||
MEDIASTATION_DOWNLOAD_CONTAINER_DIR: /vol1/1000/qBittorrent/downloads
|
MMTL_DOWNLOAD_CONTAINER_DIR: /vol1/1000/qBittorrent/downloads
|
||||||
```
|
```
|
||||||
|
|
||||||
Windows Docker Desktop 示例:
|
Windows Docker Desktop 示例:
|
||||||
@@ -256,20 +256,20 @@ Windows Docker Desktop 示例:
|
|||||||
volumes:
|
volumes:
|
||||||
- D:/Media:/media
|
- D:/Media:/media
|
||||||
environment:
|
environment:
|
||||||
MEDIASTATION_MEDIA_DIR: D:/Media
|
MMTL_MEDIA_DIR: D:/Media
|
||||||
MEDIASTATION_MEDIA_CONTAINER_DIR: /media
|
MMTL_MEDIA_CONTAINER_DIR: /media
|
||||||
```
|
```
|
||||||
|
|
||||||
如果后台添加媒体库时填的是 `/vol1/...`,Compose 里也建议把同一个 `/vol1/...` 挂进容器,避免自动整理和下载入库时路径不可访问。
|
如果后台添加媒体库时填的是 `/vol1/...`,Compose 里也建议把同一个 `/vol1/...` 挂进容器,避免自动整理和下载入库时路径不可访问。
|
||||||
|
|
||||||
## 旧 SQLite 迁移
|
## 旧 SQLite 迁移
|
||||||
|
|
||||||
新版推荐 PostgreSQL 作为主数据库。`MEDIASTATION_DATABASE_DB_PATH` 不是主库路径,而是旧 SQLite 数据的迁移源。
|
新版推荐 PostgreSQL 作为主数据库。`MMTL_DATABASE_DB_PATH` 不是主库路径,而是旧 SQLite 数据的迁移源。
|
||||||
|
|
||||||
迁移步骤:
|
迁移步骤:
|
||||||
|
|
||||||
1. 把旧版 `mediastation.db` 放到 `./data/mediastation.db`。
|
1. 把旧版 `mmtl.db` 放到 `./data/mmtl.db`。
|
||||||
2. 保持 `MEDIASTATION_DATABASE_DB_PATH: /data/mediastation.db`。
|
2. 保持 `MMTL_DATABASE_DB_PATH: /data/mmtl.db`。
|
||||||
3. 启动一次,确认日志显示迁移完成,网页数据正常。
|
3. 启动一次,确认日志显示迁移完成,网页数据正常。
|
||||||
4. 备份 `./postgres` 和 `./data`。
|
4. 备份 `./postgres` 和 `./data`。
|
||||||
5. 确认不再需要 SQLite 后,把迁移源改成不存在的路径,例如:
|
5. 确认不再需要 SQLite 后,把迁移源改成不存在的路径,例如:
|
||||||
@@ -277,7 +277,7 @@ environment:
|
|||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
# 已完成 SQLite 迁移后,建议改成不存在的路径,避免下次启动重复检查旧库。
|
# 已完成 SQLite 迁移后,建议改成不存在的路径,避免下次启动重复检查旧库。
|
||||||
MEDIASTATION_DATABASE_DB_PATH: /data/no-sqlite-migration.db
|
MMTL_DATABASE_DB_PATH: /data/no-sqlite-migration.db
|
||||||
```
|
```
|
||||||
|
|
||||||
不要删除 `./postgres`。PostgreSQL 已经是主数据库,删除它会丢失账号、媒体库、订阅、配置和历史数据。
|
不要删除 `./postgres`。PostgreSQL 已经是主数据库,删除它会丢失账号、媒体库、订阅、配置和历史数据。
|
||||||
@@ -287,36 +287,36 @@ environment:
|
|||||||
Compose 模板默认把完整应用日志写入 `./data/logs/app.log`,同时拆分 `./data/logs/warn.log` 和 `./data/logs/error.log`。Docker 自身日志也会保留 10 个 50MB 文件:
|
Compose 模板默认把完整应用日志写入 `./data/logs/app.log`,同时拆分 `./data/logs/warn.log` 和 `./data/logs/error.log`。Docker 自身日志也会保留 10 个 50MB 文件:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose logs -f mediastation-go
|
docker compose logs -f mmtl
|
||||||
tail -f ./data/logs/app.log
|
tail -f ./data/logs/app.log
|
||||||
tail -f ./data/logs/error.log
|
tail -f ./data/logs/error.log
|
||||||
```
|
```
|
||||||
|
|
||||||
如果要排查订阅、站点搜索、自动整理或 STRM 生成问题,保持 `MEDIASTATION_LOGGING_LEVEL: info`;需要更细日志时临时改成 `debug`,确认后再改回 `info`。
|
如果要排查订阅、站点搜索、自动整理或 STRM 生成问题,保持 `MMTL_LOGGING_LEVEL: info`;需要更细日志时临时改成 `debug`,确认后再改回 `info`。
|
||||||
|
|
||||||
STRM 输出目录请使用容器内可写路径,例如 `/data/strm`,或你已经挂载进容器的媒体目录。旧版本保存过 `/app/data/strm` 的部署会在生成时自动迁移到当前 `MEDIASTATION_APP_DATA_DIR`,默认就是 `/data`。
|
STRM 输出目录请使用容器内可写路径,例如 `/data/strm`,或你已经挂载进容器的媒体目录。旧版本保存过 `/app/data/strm` 的部署会在生成时自动迁移到当前 `MMTL_APP_DATA_DIR`,默认就是 `/data`。
|
||||||
|
|
||||||
## 更新与备份
|
## 更新与备份
|
||||||
|
|
||||||
更新镜像:
|
更新镜像:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose pull mediastation-go
|
docker compose pull mmtl
|
||||||
docker compose up -d --no-deps mediastation-go
|
docker compose up -d --no-deps mmtl
|
||||||
```
|
```
|
||||||
|
|
||||||
不要执行裸 `docker compose pull` 做日常更新。PostgreSQL / Redis / OpenSearch 是数据与缓存基础组件,compose 已设置为 `pull_policy: missing`,首次部署缺镜像时会拉取,日常更新只建议拉取 `mediastation-go`。需要升级这些基础组件时,请先备份 `./postgres`,再手动修改镜像版本并单独拉取。
|
不要执行裸 `docker compose pull` 做日常更新。PostgreSQL / Redis / OpenSearch 是数据与缓存基础组件,compose 已设置为 `pull_policy: missing`,首次部署缺镜像时会拉取,日常更新只建议拉取 `mmtl`。需要升级这些基础组件时,请先备份 `./postgres`,再手动修改镜像版本并单独拉取。
|
||||||
|
|
||||||
如果第二档或第三档保留了原始文件名,更新时指定对应完整文件:
|
如果第二档或第三档保留了原始文件名,更新时指定对应完整文件:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 第二档
|
# 第二档
|
||||||
docker compose -f docker-compose.standard.yml pull mediastation-go
|
docker compose -f docker-compose.standard.yml pull mmtl
|
||||||
docker compose -f docker-compose.standard.yml up -d --no-deps mediastation-go
|
docker compose -f docker-compose.standard.yml up -d --no-deps mmtl
|
||||||
|
|
||||||
# 第三档
|
# 第三档
|
||||||
docker compose -f docker-compose.search.yml pull mediastation-go
|
docker compose -f docker-compose.search.yml pull mmtl
|
||||||
docker compose -f docker-compose.search.yml up -d --no-deps mediastation-go
|
docker compose -f docker-compose.search.yml up -d --no-deps mmtl
|
||||||
```
|
```
|
||||||
|
|
||||||
必须备份:
|
必须备份:
|
||||||
@@ -338,7 +338,7 @@ docker compose -f docker-compose.search.yml up -d --no-deps mediastation-go
|
|||||||
|
|
||||||
**启动后还是反复迁移 SQLite?**
|
**启动后还是反复迁移 SQLite?**
|
||||||
|
|
||||||
确认旧数据已经迁移成功后,把 `MEDIASTATION_DATABASE_DB_PATH` 改成不存在的路径,例如 `/data/no-sqlite-migration.db`,然后重启容器。
|
确认旧数据已经迁移成功后,把 `MMTL_DATABASE_DB_PATH` 改成不存在的路径,例如 `/data/no-sqlite-migration.db`,然后重启容器。
|
||||||
|
|
||||||
**扫库或入库速度很慢?**
|
**扫库或入库速度很慢?**
|
||||||
|
|
||||||
@@ -346,7 +346,7 @@ docker compose -f docker-compose.search.yml up -d --no-deps mediastation-go
|
|||||||
|
|
||||||
**qBittorrent 下载完成后无法整理?**
|
**qBittorrent 下载完成后无法整理?**
|
||||||
|
|
||||||
确认 qBittorrent 保存路径已经通过 `volumes` 挂载进 MediaStationGo 容器,并且 `MEDIASTATION_DOWNLOAD_DIR` 与 `MEDIASTATION_DOWNLOAD_CONTAINER_DIR` 对应正确。
|
确认 qBittorrent 保存路径已经通过 `volumes` 挂载进 MMTL 容器,并且 `MMTL_DOWNLOAD_DIR` 与 `MMTL_DOWNLOAD_CONTAINER_DIR` 对应正确。
|
||||||
|
|
||||||
**硬链接目录在 Docker / NAS 上看不到内容?**
|
**硬链接目录在 Docker / NAS 上看不到内容?**
|
||||||
|
|
||||||
@@ -354,7 +354,7 @@ docker compose -f docker-compose.search.yml up -d --no-deps mediastation-go
|
|||||||
|
|
||||||
**第三方播放器无法连接?**
|
**第三方播放器无法连接?**
|
||||||
|
|
||||||
确认播放器填写的是 `http://服务器IP:18080`,账号密码使用 MediaStationGo 用户账号。反代部署时需要正确设置外部访问地址和 HTTPS 头。
|
确认播放器填写的是 `http://服务器IP:18080`,账号密码使用 MMTL 用户账号。反代部署时需要正确设置外部访问地址和 HTTPS 头。
|
||||||
|
|
||||||
## 开发构建
|
## 开发构建
|
||||||
|
|
||||||
@@ -400,11 +400,11 @@ http://127.0.0.1:8080/api/health
|
|||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
<a href="https://www.star-history.com/?repos=ShukeBta%2FMediaStationGo&type=date&legend=top-left">
|
<a href="https://www.star-history.com/?repos=ShukeBta%2FMMTL&type=date&legend=top-left">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=ShukeBta/MediaStationGo&type=date&theme=dark&legend=top-left" />
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=ShukeBta/MMTL&type=date&theme=dark&legend=top-left" />
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=ShukeBta/MediaStationGo&type=date&legend=top-left" />
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=ShukeBta/MMTL&type=date&legend=top-left" />
|
||||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=ShukeBta/MediaStationGo&type=date&legend=top-left" />
|
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=ShukeBta/MMTL&type=date&legend=top-left" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
+69
-69
@@ -29,14 +29,14 @@
|
|||||||
|
|
||||||
## What is it?
|
## What is it?
|
||||||
|
|
||||||
MediaStationGo is a self-hosted media center for personal libraries, home NAS, and home-theater users.
|
MMTL is a self-hosted media center for personal libraries, home NAS, and home-theater users.
|
||||||
|
|
||||||
It helps you:
|
It helps you:
|
||||||
|
|
||||||
- Manage movies, TV shows, anime, variety shows, music, and adult libraries.
|
- Manage movies, TV shows, anime, variety shows, music, and adult libraries.
|
||||||
- Create multiple user accounts for family members, friends, or different devices.
|
- Create multiple user accounts for family members, friends, or different devices.
|
||||||
- Scan files and enrich posters, summaries, years, seasons, and episodes.
|
- Scan files and enrich posters, summaries, years, seasons, and episodes.
|
||||||
- Play in the web UI, or log in with a MediaStationGo account from Emby-protocol apps such as Infuse, VidHub, SenPlayer, and Emby clients.
|
- Play in the web UI, or log in with a MMTL account from Emby-protocol apps such as Infuse, VidHub, SenPlayer, and Emby clients.
|
||||||
- Connect qBittorrent for search, subscriptions, downloads, and post-download organization.
|
- Connect qBittorrent for search, subscriptions, downloads, and post-download organization.
|
||||||
- Connect OpenList, CloudDrive2, WebDAV, and other storage backends with STRMURL or 302 redirect playback.
|
- 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.
|
- Run on NAS, mini PCs, VPS, Linux, Windows Docker Desktop, or any Docker-friendly host.
|
||||||
@@ -47,8 +47,8 @@ It helps you:
|
|||||||
|
|
||||||
## Key Highlights
|
## Key Highlights
|
||||||
|
|
||||||
- **One server, many clients**: deploy MediaStationGo once; you do not need to run a separate Emby server.
|
- **One server, many clients**: deploy MMTL once; you do not need to run a separate Emby server.
|
||||||
- **Emby-protocol compatibility**: add the server in third-party players as an Emby/Jellyfin-compatible server, then log in with your MediaStationGo username and password.
|
- **Emby-protocol compatibility**: add the server in third-party players as an Emby/Jellyfin-compatible server, then log in with your MMTL username and password.
|
||||||
- **Multi-user management**: supports admins, regular users, account enable/disable, expiry dates, device management, Bot registration, and redeem codes.
|
- **Multi-user management**: supports admins, regular users, account enable/disable, expiry dates, device management, Bot registration, and redeem codes.
|
||||||
- **Local + cloud media in one place**: manage local disks, download folders, OpenList, CloudDrive2, WebDAV, and other storage backends from one panel.
|
- **Local + cloud media in one place**: manage local disks, download folders, OpenList, CloudDrive2, WebDAV, and other storage backends from one panel.
|
||||||
- **Download-to-library workflow**: connect qBittorrent for search, subscriptions, download completion organization, and metadata matching.
|
- **Download-to-library workflow**: connect qBittorrent for search, subscriptions, download completion organization, and metadata matching.
|
||||||
@@ -61,7 +61,7 @@ It helps you:
|
|||||||
- **Beginners** who want to edit one `docker-compose.yml` and start the service.
|
- **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.
|
- **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.
|
- **PT/download users** who want downloads, organization, metadata, and playback in one panel.
|
||||||
- **External-player users** who want to log in to Emby-protocol third-party apps with one MediaStationGo account.
|
- **External-player users** who want to log in to Emby-protocol third-party apps with one MMTL account.
|
||||||
- **Family-sharing users** who want separate user accounts without deploying a separate media server for each person.
|
- **Family-sharing users** who want separate user accounts without deploying a separate media server for each person.
|
||||||
- **Developers** who want to study or extend a Go + React self-hosted media app.
|
- **Developers** who want to study or extend a Go + React self-hosted media app.
|
||||||
|
|
||||||
@@ -82,12 +82,12 @@ It helps you:
|
|||||||
Docker Compose is the recommended path. Beginners do not need `.env`, bare-metal binaries, or source builds. Use the single-image SQLite template if you want the smallest possible setup.
|
Docker Compose is the recommended path. Beginners do not need `.env`, bare-metal binaries, or source builds. Use the single-image SQLite template if you want the smallest possible setup.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p MediaStationGo
|
mkdir -p MMTL
|
||||||
cd MediaStationGo
|
cd MMTL
|
||||||
# Simplest option: one MediaStationGo container + SQLite
|
# Simplest option: one MMTL container + SQLite
|
||||||
curl -fsSL https://raw.githubusercontent.com/ShukeBta/MediaStationGo/main/docker-compose.simple.yml -o docker-compose.yml
|
curl -fsSL https://raw.githubusercontent.com/ShukeBta/MMTL/main/docker-compose.simple.yml -o docker-compose.yml
|
||||||
# Or tier 1: MediaStationGo + PostgreSQL
|
# Or tier 1: MMTL + PostgreSQL
|
||||||
# curl -fsSL https://raw.githubusercontent.com/ShukeBta/MediaStationGo/main/docker-compose.yml -o docker-compose.yml
|
# curl -fsSL https://raw.githubusercontent.com/ShukeBta/MMTL/main/docker-compose.yml -o docker-compose.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
Edit `docker-compose.yml`:
|
Edit `docker-compose.yml`:
|
||||||
@@ -119,9 +119,9 @@ Password: admin123
|
|||||||
|
|
||||||
## Docker Compose Recommended
|
## Docker Compose Recommended
|
||||||
|
|
||||||
The repository `docker-compose.yml` is the lightweight recommended template: no `.env` required, and by default it only starts `MediaStationGo + PostgreSQL`. This is the best starting point for most NAS users.
|
The repository `docker-compose.yml` is the lightweight recommended template: no `.env` required, and by default it only starts `MMTL + PostgreSQL`. This is the best starting point for most NAS users.
|
||||||
|
|
||||||
If you already have an older `./data/mediastation.db`, the first start with the new compose file automatically imports it into PostgreSQL. Keep `./data`; it still stores the JWT secret, runtime data, and the old SQLite migration source.
|
If you already have an older `./data/mmtl.db`, the first start with the new compose file automatically imports it into PostgreSQL. Keep `./data`; it still stores the JWT secret, runtime data, and the old SQLite migration source.
|
||||||
|
|
||||||
### Deployment modes
|
### Deployment modes
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ If you already have an older `./data/mediastation.db`, the first start with the
|
|||||||
|
|
||||||
Each compose file is standalone. Do not stack multiple `-f` files together.
|
Each compose file is standalone. Do not stack multiple `-f` files together.
|
||||||
|
|
||||||
The single-image `docker-compose.simple.yml` runs only MediaStationGo with a built-in SQLite database — the simplest starting point. Do not set `MEDIASTATION_DATABASE_DSN` there, or it switches back to PostgreSQL. Move up to the PostgreSQL modes for multi-user or high-concurrency use (keep `./data` when you switch). Redis and OpenSearch are enhancement layers, not source databases. Do not enable OpenSearch by default on low-memory NAS devices.
|
The single-image `docker-compose.simple.yml` runs only MMTL with a built-in SQLite database — the simplest starting point. Do not set `MMTL_DATABASE_DSN` there, or it switches back to PostgreSQL. Move up to the PostgreSQL modes for multi-user or high-concurrency use (keep `./data` when you switch). Redis and OpenSearch are enhancement layers, not source databases. Do not enable OpenSearch by default on low-memory NAS devices.
|
||||||
|
|
||||||
### Database Choice And Disabling SQLite
|
### Database Choice And Disabling SQLite
|
||||||
|
|
||||||
@@ -142,14 +142,14 @@ The current Docker Compose setup uses PostgreSQL by default. SQLite is no longer
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
MEDIASTATION_DATABASE_TYPE: postgres
|
MMTL_DATABASE_TYPE: postgres
|
||||||
MEDIASTATION_DATABASE_DSN: postgres://mediastation:mediastation@postgres:5432/mediastation?sslmode=disable
|
MMTL_DATABASE_DSN: postgres://mmtl:mmtl@postgres:5432/mmtl?sslmode=disable
|
||||||
```
|
```
|
||||||
|
|
||||||
`MEDIASTATION_DATABASE_DB_PATH` is only used as a one-time migration source for old SQLite data:
|
`MMTL_DATABASE_DB_PATH` is only used as a one-time migration source for old SQLite data:
|
||||||
|
|
||||||
- Fresh installs: `docker compose up -d` uses PostgreSQL and does not create a new SQLite primary database.
|
- Fresh installs: `docker compose up -d` uses PostgreSQL and does not create a new SQLite primary database.
|
||||||
- Upgrades: if `./data/mediastation.db` exists, the first start with the new compose file imports it into PostgreSQL.
|
- Upgrades: if `./data/mmtl.db` exists, the first start with the new compose file imports it into PostgreSQL.
|
||||||
- Migration fills missing rows by primary key and skips rows that already exist. If it fails partway through, a later start continues the remaining tables.
|
- Migration fills missing rows by primary key and skips rows that already exist. If it fails partway through, a later start continues the remaining tables.
|
||||||
- After a successful import, PostgreSQL gets a completion marker in the `settings` table, so the old SQLite file is not imported again.
|
- After a successful import, PostgreSQL gets a completion marker in the `settings` table, so the old SQLite file is not imported again.
|
||||||
- Redis is a hot cache and OpenSearch is a search index; neither is a source database.
|
- Redis is a hot cache and OpenSearch is a search index; neither is a source database.
|
||||||
@@ -157,9 +157,9 @@ environment:
|
|||||||
Recommended SQLite to PostgreSQL upgrade flow:
|
Recommended SQLite to PostgreSQL upgrade flow:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose pull mediastation-go
|
docker compose pull mmtl
|
||||||
docker compose up -d --no-deps mediastation-go
|
docker compose up -d --no-deps mmtl
|
||||||
docker compose logs -f mediastation-go
|
docker compose logs -f mmtl
|
||||||
```
|
```
|
||||||
|
|
||||||
After you see `sqlite data migrated to postgres`, or after the web UI shows your users, libraries, and settings correctly, you can stop using the old SQLite file as a migration source.
|
After you see `sqlite data migrated to postgres`, or after the web UI shows your users, libraries, and settings correctly, you can stop using the old SQLite file as a migration source.
|
||||||
@@ -170,15 +170,15 @@ To make the deployment PostgreSQL-only after migration, keep PostgreSQL selected
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
MEDIASTATION_DATABASE_TYPE: postgres
|
MMTL_DATABASE_TYPE: postgres
|
||||||
MEDIASTATION_DATABASE_DSN: postgres://mediastation:mediastation@postgres:5432/mediastation?sslmode=disable
|
MMTL_DATABASE_DSN: postgres://mmtl:mmtl@postgres:5432/mmtl?sslmode=disable
|
||||||
MEDIASTATION_DATABASE_DB_PATH: /data/disabled-sqlite-migration.db
|
MMTL_DATABASE_DB_PATH: /data/disabled-sqlite-migration.db
|
||||||
```
|
```
|
||||||
|
|
||||||
Then rename or move the old host-side SQLite file as an offline backup:
|
Then rename or move the old host-side SQLite file as an offline backup:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mv data/mediastation.db data/mediastation.sqlite.bak
|
mv data/mmtl.db data/mmtl.sqlite.bak
|
||||||
```
|
```
|
||||||
|
|
||||||
For bare-metal or custom `config.yaml` deployments, use the same idea:
|
For bare-metal or custom `config.yaml` deployments, use the same idea:
|
||||||
@@ -186,7 +186,7 @@ For bare-metal or custom `config.yaml` deployments, use the same idea:
|
|||||||
```yaml
|
```yaml
|
||||||
database:
|
database:
|
||||||
type: postgres
|
type: postgres
|
||||||
dsn: postgres://mediastation:mediastation@127.0.0.1:5432/mediastation?sslmode=disable
|
dsn: postgres://mmtl:mmtl@127.0.0.1:5432/mmtl?sslmode=disable
|
||||||
db_path: ""
|
db_path: ""
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -198,15 +198,15 @@ Both image sources are supported. Pick one and put it in `image:`:
|
|||||||
|
|
||||||
| Source | Image | Best for |
|
| Source | Image | Best for |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| GitHub Container Registry (GHCR) | `ghcr.io/shukebta/mediastation-go:latest` | Recommended default, follows repository releases |
|
| GitHub Container Registry (GHCR) | `ghcr.io/shukebta/mmtl:latest` | Recommended default, follows repository releases |
|
||||||
| Docker Hub | `shukbet/mediastationgo:latest` | Backup source when GHCR is slow or unavailable |
|
| Docker Hub | `shukbet/mmtl:latest` | Backup source when GHCR is slow or unavailable |
|
||||||
|
|
||||||
To pin a version, first confirm the tag exists on the repository Packages page. Use this format:
|
To pin a version, first confirm the tag exists on the repository Packages page. Use this format:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
image: ghcr.io/shukebta/mediastation-go:<version-tag>
|
image: ghcr.io/shukebta/mmtl:<version-tag>
|
||||||
# If GHCR does not have that tag, use Docker Hub as the backup:
|
# If GHCR does not have that tag, use Docker Hub as the backup:
|
||||||
# image: shukbet/mediastationgo:MediaStationGo-v0.0.72
|
# image: shukbet/mmtl:MMTL-v0.0.72
|
||||||
```
|
```
|
||||||
|
|
||||||
For the simplest setup, keep GHCR `latest`.
|
For the simplest setup, keep GHCR `latest`.
|
||||||
@@ -215,10 +215,10 @@ Manual pull examples:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# GitHub Container Registry
|
# GitHub Container Registry
|
||||||
docker pull ghcr.io/shukebta/mediastation-go:latest
|
docker pull ghcr.io/shukebta/mmtl:latest
|
||||||
|
|
||||||
# Docker Hub backup
|
# Docker Hub backup
|
||||||
docker pull shukbet/mediastationgo:latest
|
docker pull shukbet/mmtl:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Focus on this part:
|
Focus on this part:
|
||||||
@@ -260,8 +260,8 @@ volumes:
|
|||||||
- /vol1/1000/Downloads:/downloads
|
- /vol1/1000/Downloads:/downloads
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
MEDIASTATION_MEDIA_DIR: /vol1/1000/Media
|
MMTL_MEDIA_DIR: /vol1/1000/Media
|
||||||
MEDIASTATION_DOWNLOAD_DIR: /vol1/1000/Downloads
|
MMTL_DOWNLOAD_DIR: /vol1/1000/Downloads
|
||||||
```
|
```
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
@@ -279,12 +279,12 @@ The root `docker-compose.yml` follows this style:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
mediastation-go:
|
mmtl:
|
||||||
# Pick one image source:
|
# Pick one image source:
|
||||||
# GitHub Container Registry (GHCR):
|
# GitHub Container Registry (GHCR):
|
||||||
image: ghcr.io/shukebta/mediastation-go:latest
|
image: ghcr.io/shukebta/mmtl:latest
|
||||||
# Docker Hub backup:
|
# Docker Hub backup:
|
||||||
# image: shukbet/mediastationgo:latest
|
# image: shukbet/mmtl:latest
|
||||||
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
init: true
|
init: true
|
||||||
@@ -326,38 +326,38 @@ services:
|
|||||||
PUID: "1000"
|
PUID: "1000"
|
||||||
PGID: "1000"
|
PGID: "1000"
|
||||||
|
|
||||||
MEDIASTATION_APP_HOST: 0.0.0.0
|
MMTL_APP_HOST: 0.0.0.0
|
||||||
MEDIASTATION_APP_PORT: 8080
|
MMTL_APP_PORT: 8080
|
||||||
MEDIASTATION_APP_WEB_DIR: /app/web/dist
|
MMTL_APP_WEB_DIR: /app/web/dist
|
||||||
MEDIASTATION_APP_DATA_DIR: /data
|
MMTL_APP_DATA_DIR: /data
|
||||||
|
|
||||||
# Lightweight mode uses PostgreSQL by default.
|
# Lightweight mode uses PostgreSQL by default.
|
||||||
# Old SQLite data migrates from this path on first start.
|
# Old SQLite data migrates from this path on first start.
|
||||||
MEDIASTATION_DATABASE_TYPE: postgres
|
MMTL_DATABASE_TYPE: postgres
|
||||||
MEDIASTATION_DATABASE_DSN: postgres://mediastation:mediastation@postgres:5432/mediastation?sslmode=disable
|
MMTL_DATABASE_DSN: postgres://mmtl:mmtl@postgres:5432/mmtl?sslmode=disable
|
||||||
# After migration, change this to /data/disabled-sqlite-migration.db to disable the SQLite migration source.
|
# After migration, change this to /data/disabled-sqlite-migration.db to disable the SQLite migration source.
|
||||||
MEDIASTATION_DATABASE_DB_PATH: /data/mediastation.db
|
MMTL_DATABASE_DB_PATH: /data/mmtl.db
|
||||||
MEDIASTATION_CACHE_CACHE_DIR: /cache
|
MMTL_CACHE_CACHE_DIR: /cache
|
||||||
|
|
||||||
# Use /media and /downloads in the web UI and downloader by default.
|
# Use /media and /downloads in the web UI and downloader by default.
|
||||||
# Only set MEDIASTATION_*_DIR to real host paths when migrating old
|
# Only set MMTL_*_DIR to real host paths when migrating old
|
||||||
# libraries/tasks that already stored host paths.
|
# libraries/tasks that already stored host paths.
|
||||||
MEDIASTATION_MEDIA_DIR: /media
|
MMTL_MEDIA_DIR: /media
|
||||||
MEDIASTATION_MEDIA_CONTAINER_DIR: /media
|
MMTL_MEDIA_CONTAINER_DIR: /media
|
||||||
MEDIASTATION_DOWNLOAD_DIR: /downloads
|
MMTL_DOWNLOAD_DIR: /downloads
|
||||||
MEDIASTATION_DOWNLOAD_CONTAINER_DIR: /downloads
|
MMTL_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: mediastation
|
POSTGRES_DB: mmtl
|
||||||
POSTGRES_USER: mediastation
|
POSTGRES_USER: mmtl
|
||||||
POSTGRES_PASSWORD: mediastation
|
POSTGRES_PASSWORD: mmtl
|
||||||
volumes:
|
volumes:
|
||||||
- ./postgres:/var/lib/postgresql/data
|
- ./postgres:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U mediastation -d mediastation"]
|
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U mmtl -d mmtl"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
@@ -386,7 +386,7 @@ services:
|
|||||||
4. **Use external players**
|
4. **Use external players**
|
||||||
- Add the server as an Emby/Jellyfin-compatible server.
|
- Add the server as an Emby/Jellyfin-compatible server.
|
||||||
- Server URL: `http://SERVER_IP:18080`.
|
- Server URL: `http://SERVER_IP:18080`.
|
||||||
- Use the username and password created in MediaStationGo. No separate Emby server is required.
|
- Use the username and password created in MMTL. No separate Emby server is required.
|
||||||
- Admins can create regular users in the web UI or Bot so each person can log in with their own account.
|
- Admins can create regular users in the web UI or Bot so each person can log in with their own account.
|
||||||
|
|
||||||
5. **Use cloud playback**
|
5. **Use cloud playback**
|
||||||
@@ -401,21 +401,21 @@ services:
|
|||||||
### Update
|
### Update
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose pull mediastation-go
|
docker compose pull mmtl
|
||||||
docker compose up -d --no-deps mediastation-go
|
docker compose up -d --no-deps mmtl
|
||||||
```
|
```
|
||||||
|
|
||||||
### Logs
|
### Logs
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose logs -f mediastation-go
|
docker compose logs -f mmtl
|
||||||
tail -f ./data/logs/app.log
|
tail -f ./data/logs/app.log
|
||||||
tail -f ./data/logs/error.log
|
tail -f ./data/logs/error.log
|
||||||
```
|
```
|
||||||
|
|
||||||
The compose templates keep full application logs in `./data/logs/app.log` and split warnings/errors into `warn.log` and `error.log`. Keep `MEDIASTATION_LOGGING_LEVEL=info` while diagnosing subscription, site search, organizer, or STRM generation issues; temporarily switch to `debug` only when deeper tracing is needed.
|
The compose templates keep full application logs in `./data/logs/app.log` and split warnings/errors into `warn.log` and `error.log`. Keep `MMTL_LOGGING_LEVEL=info` while diagnosing subscription, site search, organizer, or STRM generation issues; temporarily switch to `debug` only when deeper tracing is needed.
|
||||||
|
|
||||||
Use a writable container path for STRM output, such as `/data/strm` or a mounted media path. Deployments that previously saved `/app/data/strm` are migrated automatically to the configured `MEDIASTATION_APP_DATA_DIR`, which defaults to `/data`.
|
Use a writable container path for STRM output, such as `/data/strm` or a mounted media path. Deployments that previously saved `/app/data/strm` are migrated automatically to the configured `MMTL_APP_DATA_DIR`, which defaults to `/data`.
|
||||||
|
|
||||||
### Backup
|
### Backup
|
||||||
|
|
||||||
@@ -435,7 +435,7 @@ redis/ # hot cache, safe to rebuild
|
|||||||
opensearch/ # search index, rebuildable; backing it up can save reindex time on huge libraries
|
opensearch/ # search index, rebuildable; backing it up can save reindex time on huge libraries
|
||||||
```
|
```
|
||||||
|
|
||||||
`cache/` is usually not important. If you explicitly still use `database.type=sqlite`, the primary database remains `data/mediastation.db`.
|
`cache/` is usually not important. If you explicitly still use `database.type=sqlite`, the primary database remains `data/mmtl.db`.
|
||||||
|
|
||||||
### Stop
|
### Stop
|
||||||
|
|
||||||
@@ -453,7 +453,7 @@ Check the container:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker ps
|
docker ps
|
||||||
docker compose logs --tail=100 mediastation-go
|
docker compose logs --tail=100 mmtl
|
||||||
```
|
```
|
||||||
|
|
||||||
Then open:
|
Then open:
|
||||||
@@ -503,7 +503,7 @@ Beginners should not. Editing `docker-compose.yml` directly is easier to underst
|
|||||||
| Libraries | Movies, TV shows, anime, variety, music, adult content |
|
| Libraries | Movies, TV shows, anime, variety, music, adult content |
|
||||||
| Metadata | NFO, local artwork, TMDb, TheTVDB, Bangumi, Douban, Fanart, JavBus/JavDB |
|
| Metadata | NFO, local artwork, TMDb, TheTVDB, Bangumi, Douban, Fanart, JavBus/JavDB |
|
||||||
| Playback | Web playback, HTTP Range, HLS transcoding, direct links, STRMURL, 302 redirect |
|
| Playback | Web playback, HTTP Range, HLS transcoding, direct links, STRMURL, 302 redirect |
|
||||||
| External clients | Emby-protocol compatible APIs; MediaStationGo accounts can log in to third-party players |
|
| External clients | Emby-protocol compatible APIs; MMTL accounts can log in to third-party players |
|
||||||
| User management | Multi-user accounts, admin/regular users, expiry dates, device management, Bot registration and redeem codes |
|
| User management | Multi-user accounts, admin/regular users, expiry dates, device management, Bot registration and redeem codes |
|
||||||
| Downloads | qBittorrent, site search, subscriptions, post-download organization |
|
| Downloads | qBittorrent, site search, subscriptions, post-download organization |
|
||||||
| File manager | Browse, organize, copy, move, hardlink, symlink |
|
| File manager | Browse, organize, copy, move, hardlink, symlink |
|
||||||
@@ -543,7 +543,7 @@ cd web && npm run build
|
|||||||
|
|
||||||
## Community and Friends
|
## Community and Friends
|
||||||
|
|
||||||
- Telegram group: <https://t.me/MediaStationGo>
|
- Telegram group: <https://t.me/MMTL>
|
||||||
- NodeSeek: [https://www.nodeseek.com/](https://www.nodeseek.com/)
|
- NodeSeek: [https://www.nodeseek.com/](https://www.nodeseek.com/)
|
||||||
- LINUX DO: [https://linux.do/](https://linux.do/)
|
- LINUX DO: [https://linux.do/](https://linux.do/)
|
||||||
|
|
||||||
@@ -551,7 +551,7 @@ cd web && npm run build
|
|||||||
|
|
||||||
## Donation
|
## Donation
|
||||||
|
|
||||||
If MediaStationGo saves you time, feel free to buy the author a bowl of noodles.
|
If MMTL saves you time, feel free to buy the author a bowl of noodles.
|
||||||
|
|
||||||
<img width="200" height="200" alt="WeChat Donation QR" src="https://github.com/user-attachments/assets/d6077de5-8305-400d-8b82-470ef05d926e" />
|
<img width="200" height="200" alt="WeChat Donation QR" src="https://github.com/user-attachments/assets/d6077de5-8305-400d-8b82-470ef05d926e" />
|
||||||
|
|
||||||
@@ -559,11 +559,11 @@ If MediaStationGo saves you time, feel free to buy the author a bowl of noodles.
|
|||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
<a href="https://www.star-history.com/?repos=ShukeBta%2FMediaStationGo&type=date&legend=top-left">
|
<a href="https://www.star-history.com/?repos=ShukeBta%2FMMTL&type=date&legend=top-left">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=ShukeBta/MediaStationGo&type=date&theme=dark&legend=top-left" />
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=ShukeBta/MMTL&type=date&theme=dark&legend=top-left" />
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=ShukeBta/MediaStationGo&type=date&legend=top-left" />
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=ShukeBta/MMTL&type=date&legend=top-left" />
|
||||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=ShukeBta/MediaStationGo&type=date&legend=top-left" />
|
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=ShukeBta/MMTL&type=date&legend=top-left" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -1,13 +1,13 @@
|
|||||||
# 安全策略
|
# 安全策略
|
||||||
|
|
||||||
MediaStationGo 是自托管媒体系统,常部署在 NAS、家庭网络、Docker、反向代理和第三方下载器环境中。安全问题通常会同时涉及应用代码、容器配置、路径映射、站点 Cookie / API Key、下载器凭据和外部访问入口。请按本策略报告和处理安全问题。
|
MMTL 是自托管媒体系统,常部署在 NAS、家庭网络、Docker、反向代理和第三方下载器环境中。安全问题通常会同时涉及应用代码、容器配置、路径映射、站点 Cookie / API Key、下载器凭据和外部访问入口。请按本策略报告和处理安全问题。
|
||||||
|
|
||||||
## 支持范围
|
## 支持范围
|
||||||
|
|
||||||
我们优先支持以下版本和部署方式的安全修复:
|
我们优先支持以下版本和部署方式的安全修复:
|
||||||
|
|
||||||
- 当前 `main` 分支。
|
- 当前 `main` 分支。
|
||||||
- 最新发布镜像:`ghcr.io/shukebta/mediastation-go:latest`。
|
- 最新发布镜像:`ghcr.io/shukebta/mmtl:latest`。
|
||||||
- README 中推荐的 Docker Compose 第一档、第二档、第三档部署方式。
|
- README 中推荐的 Docker Compose 第一档、第二档、第三档部署方式。
|
||||||
|
|
||||||
历史版本、私有魔改镜像、未公开补丁分支和非标准部署仍可报告,但维护者可能要求先在最新 `main` 或最新镜像中复现。
|
历史版本、私有魔改镜像、未公开补丁分支和非标准部署仍可报告,但维护者可能要求先在最新 `main` 或最新镜像中复现。
|
||||||
@@ -16,7 +16,7 @@ MediaStationGo 是自托管媒体系统,常部署在 NAS、家庭网络、Dock
|
|||||||
|
|
||||||
请不要在公开 Issue、PR、讨论区或群聊中披露可利用细节。优先使用 GitHub Security Advisory 私密报告:
|
请不要在公开 Issue、PR、讨论区或群聊中披露可利用细节。优先使用 GitHub Security Advisory 私密报告:
|
||||||
|
|
||||||
<https://github.com/ShukeBta/MediaStationGo/security/advisories/new>
|
<https://github.com/ShukeBta/MMTL/security/advisories/new>
|
||||||
|
|
||||||
如果无法使用 GitHub 私密报告,可以先通过项目 README 中的社区入口联系维护者,说明“需要私下报告安全问题”,不要直接贴出利用细节、密钥或完整日志。
|
如果无法使用 GitHub 私密报告,可以先通过项目 README 中的社区入口联系维护者,说明“需要私下报告安全问题”,不要直接贴出利用细节、密钥或完整日志。
|
||||||
|
|
||||||
@@ -68,14 +68,14 @@ MediaStationGo 是自托管媒体系统,常部署在 NAS、家庭网络、Dock
|
|||||||
|
|
||||||
## 自托管安全基线
|
## 自托管安全基线
|
||||||
|
|
||||||
部署 MediaStationGo 时建议:
|
部署 MMTL 时建议:
|
||||||
|
|
||||||
- 首次登录后立即修改默认 `admin / admin123`。
|
- 首次登录后立即修改默认 `admin / admin123`。
|
||||||
- 不要把 PostgreSQL、Redis、OpenSearch、qBittorrent WebUI 暴露到公网。
|
- 不要把 PostgreSQL、Redis、OpenSearch、qBittorrent WebUI 暴露到公网。
|
||||||
- 反向代理公网访问时启用 HTTPS,并限制管理后台访问来源。
|
- 反向代理公网访问时启用 HTTPS,并限制管理后台访问来源。
|
||||||
- 使用强随机的 JWT / 加密密钥,妥善备份 `./data` 和数据库。
|
- 使用强随机的 JWT / 加密密钥,妥善备份 `./data` 和数据库。
|
||||||
- 不要在 Issue、PR、截图或日志中公开站点 Cookie、API Key、Passkey、下载器密码。
|
- 不要在 Issue、PR、截图或日志中公开站点 Cookie、API Key、Passkey、下载器密码。
|
||||||
- Docker `volumes` 只挂载 MediaStationGo 需要访问的目录,媒体库目录需要写入时再授予写权限。
|
- Docker `volumes` 只挂载 MMTL 需要访问的目录,媒体库目录需要写入时再授予写权限。
|
||||||
- 定期更新镜像,并在升级前备份 `./postgres` 和 `./data`。
|
- 定期更新镜像,并在升级前备份 `./postgres` 和 `./data`。
|
||||||
|
|
||||||
## 安全修复 PR
|
## 安全修复 PR
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultLogMaxSizeMB = 20
|
const defaultLogMaxSizeMB = 20
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newLogger 根据 cfg.Logging 构建 Zap。
|
// newLogger 根据 cfg.Logging 构建 Zap。
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProductionLoggerWritesConfiguredInfoToAppLogAndSplitsWarnError(t *testing.T) {
|
func TestProductionLoggerWritesConfiguredInfoToAppLogAndSplitsWarnError(t *testing.T) {
|
||||||
|
|||||||
+7
-7
@@ -1,6 +1,6 @@
|
|||||||
// Package main is the MediaStationGo HTTP server entry point.
|
// Package main is the MMTL HTTP server entry point.
|
||||||
//
|
//
|
||||||
// MediaStationGo is a Go rewrite of the legacy Python implementation,
|
// MMTL is a Go rewrite of the legacy Python implementation,
|
||||||
// adopting the same tech stack as cropflre/nowen-video:
|
// adopting the same tech stack as cropflre/nowen-video:
|
||||||
//
|
//
|
||||||
// Backend: Go 1.25 + Gin + GORM + PostgreSQL/SQLite + Viper + Zap + JWT
|
// Backend: Go 1.25 + Gin + GORM + PostgreSQL/SQLite + Viper + Zap + JWT
|
||||||
@@ -24,10 +24,10 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/database"
|
"github.com/ShukeBta/MMTL/internal/database"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
"github.com/ShukeBta/MMTL/internal/repository"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// version is overwritten at build time via -ldflags="-X main.version=...".
|
// version is overwritten at build time via -ldflags="-X main.version=...".
|
||||||
@@ -38,7 +38,7 @@ func effectiveVersion(buildVersion string) string {
|
|||||||
if buildVersion != "" && buildVersion != "dev" {
|
if buildVersion != "" && buildVersion != "dev" {
|
||||||
return buildVersion
|
return buildVersion
|
||||||
}
|
}
|
||||||
if envVersion := strings.TrimSpace(os.Getenv("MEDIASTATION_VERSION")); envVersion != "" {
|
if envVersion := strings.TrimSpace(os.Getenv("MMTL_VERSION")); envVersion != "" {
|
||||||
return envVersion
|
return envVersion
|
||||||
}
|
}
|
||||||
if buildVersion == "" {
|
if buildVersion == "" {
|
||||||
|
|||||||
@@ -12,23 +12,23 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestEffectiveVersionPrefersBuildVersion(t *testing.T) {
|
func TestEffectiveVersionPrefersBuildVersion(t *testing.T) {
|
||||||
t.Setenv("MEDIASTATION_VERSION", "MediaStationGo-v0.1.15")
|
t.Setenv("MMTL_VERSION", "MMTL-v0.1.15")
|
||||||
|
|
||||||
if got := effectiveVersion("MediaStationGo-v0.1.16"); got != "MediaStationGo-v0.1.16" {
|
if got := effectiveVersion("MMTL-v0.1.16"); got != "MMTL-v0.1.16" {
|
||||||
t.Fatalf("effectiveVersion = %q, want MediaStationGo-v0.1.16", got)
|
t.Fatalf("effectiveVersion = %q, want MMTL-v0.1.16", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEffectiveVersionUsesEnvWhenBuildVersionIsDev(t *testing.T) {
|
func TestEffectiveVersionUsesEnvWhenBuildVersionIsDev(t *testing.T) {
|
||||||
t.Setenv("MEDIASTATION_VERSION", " MediaStationGo-v0.1.16 ")
|
t.Setenv("MMTL_VERSION", " MMTL-v0.1.16 ")
|
||||||
|
|
||||||
if got := effectiveVersion("dev"); got != "MediaStationGo-v0.1.16" {
|
if got := effectiveVersion("dev"); got != "MMTL-v0.1.16" {
|
||||||
t.Fatalf("effectiveVersion = %q, want MediaStationGo-v0.1.16", got)
|
t.Fatalf("effectiveVersion = %q, want MMTL-v0.1.16", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEffectiveVersionDefaultsToDev(t *testing.T) {
|
func TestEffectiveVersionDefaultsToDev(t *testing.T) {
|
||||||
t.Setenv("MEDIASTATION_VERSION", "")
|
t.Setenv("MMTL_VERSION", "")
|
||||||
|
|
||||||
if got := effectiveVersion(""); got != "dev" {
|
if got := effectiveVersion(""); got != "dev" {
|
||||||
t.Fatalf("effectiveVersion = %q, want dev", got)
|
t.Fatalf("effectiveVersion = %q, want dev", got)
|
||||||
@@ -85,7 +85,7 @@ func TestServeSPAServesAssetsImmutableAndBypassesAPIRoutes(t *testing.T) {
|
|||||||
if err := os.WriteFile(filepath.Join(webDir, "assets", "app.js"), []byte("console.log('ok')"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(webDir, "assets", "app.js"), []byte("console.log('ok')"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(webDir, "brand", "mediastationgo-logo.svg"), []byte("<svg></svg>"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(webDir, "brand", "mmtl-logo.svg"), []byte("<svg></svg>"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(webDir, "artwork-cache-sw.js"), []byte("self.addEventListener('fetch', () => {})"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(webDir, "artwork-cache-sw.js"), []byte("self.addEventListener('fetch', () => {})"), 0o644); err != nil {
|
||||||
@@ -105,7 +105,7 @@ func TestServeSPAServesAssetsImmutableAndBypassesAPIRoutes(t *testing.T) {
|
|||||||
t.Fatalf("asset Cache-Control = %q, want immutable", got)
|
t.Fatalf("asset Cache-Control = %q, want immutable", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
brandReq := httptest.NewRequest(http.MethodGet, "/brand/mediastationgo-logo.svg", nil)
|
brandReq := httptest.NewRequest(http.MethodGet, "/brand/mmtl-logo.svg", nil)
|
||||||
brandResp := httptest.NewRecorder()
|
brandResp := httptest.NewRecorder()
|
||||||
router.ServeHTTP(brandResp, brandReq)
|
router.ServeHTTP(brandResp, brandReq)
|
||||||
if brandResp.Code != http.StatusOK {
|
if brandResp.Code != http.StatusOK {
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/handler"
|
"github.com/ShukeBta/MMTL/internal/handler"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildRouter(cfg *config.Config, logger *zap.Logger, svc *service.Container) *gin.Engine {
|
func buildRouter(cfg *config.Config, logger *zap.Logger, svc *service.Container) *gin.Engine {
|
||||||
@@ -79,7 +79,7 @@ func serveNoCacheFile(filePath string) gin.HandlerFunc {
|
|||||||
func serveSPAIndex(c *gin.Context, indexPath string) {
|
func serveSPAIndex(c *gin.Context, indexPath string) {
|
||||||
setNoCacheHeaders(c)
|
setNoCacheHeaders(c)
|
||||||
if _, err := os.Stat(indexPath); err != nil {
|
if _, err := os.Stat(indexPath); err != nil {
|
||||||
c.String(http.StatusNotFound, "MediaStationGo web UI not found: %s", indexPath)
|
c.String(http.StatusNotFound, "MMTL web UI not found: %s", indexPath)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.File(indexPath)
|
c.File(indexPath)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func applyCPUThreadLimit(cfg *config.Config, logger *zap.Logger) {
|
func applyCPUThreadLimit(cfg *config.Config, logger *zap.Logger) {
|
||||||
|
|||||||
+39
-39
@@ -1,7 +1,7 @@
|
|||||||
# MediaStationGo 第三档完整 Docker Compose 部署文件
|
# MMTL 第三档完整 Docker Compose 部署文件
|
||||||
#
|
#
|
||||||
# 组件:
|
# 组件:
|
||||||
# MediaStationGo + PostgreSQL + Redis + OpenSearch
|
# MMTL + PostgreSQL + Redis + OpenSearch
|
||||||
#
|
#
|
||||||
# 使用方式二选一:
|
# 使用方式二选一:
|
||||||
# 1. 保存为 docker-compose.yml 后执行:
|
# 1. 保存为 docker-compose.yml 后执行:
|
||||||
@@ -19,12 +19,12 @@
|
|||||||
# admin / admin123
|
# admin / admin123
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mediastation-go:
|
mmtl:
|
||||||
# 镜像二选一:
|
# 镜像二选一:
|
||||||
# 方式一:GitHub 仓库镜像 GHCR(默认,推荐)
|
# 方式一:GitHub 仓库镜像 GHCR(默认,推荐)
|
||||||
image: ghcr.io/shukebta/mediastation-go:latest
|
image: ghcr.io/shukebta/mmtl:latest
|
||||||
# 方式二:Docker Hub 备用(GHCR 拉取慢或不可用时使用)
|
# 方式二:Docker Hub 备用(GHCR 拉取慢或不可用时使用)
|
||||||
# image: shukbet/mediastationgo:latest
|
# image: shukbet/mmtl:latest
|
||||||
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
init: true
|
init: true
|
||||||
@@ -78,45 +78,45 @@ services:
|
|||||||
PUID: "1000"
|
PUID: "1000"
|
||||||
PGID: "1000"
|
PGID: "1000"
|
||||||
|
|
||||||
MEDIASTATION_APP_HOST: 0.0.0.0
|
MMTL_APP_HOST: 0.0.0.0
|
||||||
MEDIASTATION_APP_PORT: 8080
|
MMTL_APP_PORT: 8080
|
||||||
MEDIASTATION_APP_WEB_DIR: /app/web/dist
|
MMTL_APP_WEB_DIR: /app/web/dist
|
||||||
MEDIASTATION_APP_DATA_DIR: /data
|
MMTL_APP_DATA_DIR: /data
|
||||||
MEDIASTATION_LOGGING_LEVEL: info
|
MMTL_LOGGING_LEVEL: info
|
||||||
MEDIASTATION_LOGGING_FORMAT: console
|
MMTL_LOGGING_FORMAT: console
|
||||||
MEDIASTATION_LOGGING_OUTPUT_PATH: /data/logs
|
MMTL_LOGGING_OUTPUT_PATH: /data/logs
|
||||||
MEDIASTATION_LOGGING_MAX_SIZE_MB: "50"
|
MMTL_LOGGING_MAX_SIZE_MB: "50"
|
||||||
MEDIASTATION_LOGGING_MAX_BACKUPS: "20"
|
MMTL_LOGGING_MAX_BACKUPS: "20"
|
||||||
MEDIASTATION_LOGGING_MAX_AGE_DAYS: "30"
|
MMTL_LOGGING_MAX_AGE_DAYS: "30"
|
||||||
|
|
||||||
MEDIASTATION_DATABASE_TYPE: postgres
|
MMTL_DATABASE_TYPE: postgres
|
||||||
MEDIASTATION_DATABASE_DSN: postgres://mediastation:mediastation@postgres:5432/mediastation?sslmode=disable
|
MMTL_DATABASE_DSN: postgres://mmtl:mmtl@postgres:5432/mmtl?sslmode=disable
|
||||||
MEDIASTATION_DATABASE_DB_PATH: /data/mediastation.db
|
MMTL_DATABASE_DB_PATH: /data/mmtl.db
|
||||||
|
|
||||||
MEDIASTATION_CACHE_REDIS_URL: redis://redis:6379/0
|
MMTL_CACHE_REDIS_URL: redis://redis:6379/0
|
||||||
MEDIASTATION_CACHE_CACHE_DIR: /cache
|
MMTL_CACHE_CACHE_DIR: /cache
|
||||||
|
|
||||||
# OpenSearch 只做搜索索引,主数据仍以 PostgreSQL 为准。
|
# OpenSearch 只做搜索索引,主数据仍以 PostgreSQL 为准。
|
||||||
MEDIASTATION_SEARCH_BACKEND: opensearch
|
MMTL_SEARCH_BACKEND: opensearch
|
||||||
MEDIASTATION_SEARCH_OPENSEARCH_URL: http://opensearch:9200
|
MMTL_SEARCH_OPENSEARCH_URL: http://opensearch:9200
|
||||||
MEDIASTATION_SEARCH_INDEX: mediastation_media
|
MMTL_SEARCH_INDEX: mmtl_media
|
||||||
|
|
||||||
MEDIASTATION_UPDATE_IMAGE: ghcr.io/shukebta/mediastation-go:latest
|
MMTL_UPDATE_IMAGE: ghcr.io/shukebta/mmtl:latest
|
||||||
|
|
||||||
# 默认推荐在网页里使用容器路径 /media。
|
# 默认推荐在网页里使用容器路径 /media。
|
||||||
# 如果旧媒体库已经保存了宿主机路径 /vol1/1000/Media,
|
# 如果旧媒体库已经保存了宿主机路径 /vol1/1000/Media,
|
||||||
# 再把这里改成同一个宿主机真实路径用于旧路径换算。
|
# 再把这里改成同一个宿主机真实路径用于旧路径换算。
|
||||||
MEDIASTATION_MEDIA_DIR: /media
|
MMTL_MEDIA_DIR: /media
|
||||||
MEDIASTATION_MEDIA_CONTAINER_DIR: /media
|
MMTL_MEDIA_CONTAINER_DIR: /media
|
||||||
MEDIASTATION_DOWNLOAD_DIR: /downloads
|
MMTL_DOWNLOAD_DIR: /downloads
|
||||||
MEDIASTATION_DOWNLOAD_CONTAINER_DIR: /downloads
|
MMTL_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||||
|
|
||||||
MEDIASTATION_TRANSCODER_ENABLED: "true"
|
MMTL_TRANSCODER_ENABLED: "true"
|
||||||
MEDIASTATION_TRANSCODER_HARDWARE_ACCEL: "false"
|
MMTL_TRANSCODER_HARDWARE_ACCEL: "false"
|
||||||
MEDIASTATION_TRANSCODER_REALTIME: "true"
|
MMTL_TRANSCODER_REALTIME: "true"
|
||||||
MEDIASTATION_TRANSCODER_THREADS: "2"
|
MMTL_TRANSCODER_THREADS: "2"
|
||||||
MEDIASTATION_TRANSCODER_MAX_CONCURRENT: "1"
|
MMTL_TRANSCODER_MAX_CONCURRENT: "1"
|
||||||
MEDIASTATION_TRANSCODER_IDLE_TIMEOUT_SECONDS: "120"
|
MMTL_TRANSCODER_IDLE_TIMEOUT_SECONDS: "120"
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "busybox wget -qO- http://127.0.0.1:8080/api/health || exit 1"]
|
test: ["CMD-SHELL", "busybox wget -qO- http://127.0.0.1:8080/api/health || exit 1"]
|
||||||
@@ -133,18 +133,18 @@ services:
|
|||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
# 首次部署允许拉取;日常更新请只 pull mediastation-go。
|
# 首次部署允许拉取;日常更新请只 pull mmtl。
|
||||||
pull_policy: missing
|
pull_policy: missing
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: mediastation
|
POSTGRES_DB: mmtl
|
||||||
POSTGRES_USER: mediastation
|
POSTGRES_USER: mmtl
|
||||||
POSTGRES_PASSWORD: mediastation
|
POSTGRES_PASSWORD: mmtl
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
volumes:
|
volumes:
|
||||||
- ./postgres:/var/lib/postgresql/data
|
- ./postgres:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U mediastation -d mediastation"]
|
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U mmtl -d mmtl"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|||||||
+23
-23
@@ -1,4 +1,4 @@
|
|||||||
# MediaStationGo 单镜像部署模板(SQLite)
|
# MMTL 单镜像部署模板(SQLite)
|
||||||
#
|
#
|
||||||
# 适合:新手、单人使用、低配 NAS / 小主机。
|
# 适合:新手、单人使用、低配 NAS / 小主机。
|
||||||
# 特点:只有一个镜像,不需要 PostgreSQL / Redis / .env。
|
# 特点:只有一个镜像,不需要 PostgreSQL / Redis / .env。
|
||||||
@@ -12,12 +12,12 @@
|
|||||||
# 首次登录后请立刻修改密码。
|
# 首次登录后请立刻修改密码。
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mediastation-go:
|
mmtl:
|
||||||
image: ghcr.io/shukebta/mediastation-go:latest
|
image: ghcr.io/shukebta/mmtl:latest
|
||||||
# Docker Hub 备用:
|
# Docker Hub 备用:
|
||||||
# image: shukbet/mediastationgo:latest
|
# image: shukbet/mmtl:latest
|
||||||
|
|
||||||
container_name: mediastation-go
|
container_name: mmtl
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
init: true
|
init: true
|
||||||
|
|
||||||
@@ -64,29 +64,29 @@ services:
|
|||||||
PUID: "1000"
|
PUID: "1000"
|
||||||
PGID: "1000"
|
PGID: "1000"
|
||||||
|
|
||||||
MEDIASTATION_APP_HOST: 0.0.0.0
|
MMTL_APP_HOST: 0.0.0.0
|
||||||
MEDIASTATION_APP_PORT: 8080
|
MMTL_APP_PORT: 8080
|
||||||
MEDIASTATION_APP_WEB_DIR: /app/web/dist
|
MMTL_APP_WEB_DIR: /app/web/dist
|
||||||
MEDIASTATION_APP_DATA_DIR: /data
|
MMTL_APP_DATA_DIR: /data
|
||||||
|
|
||||||
# 单镜像档固定使用 SQLite。主数据库文件:./data/mediastation.db。
|
# 单镜像档固定使用 SQLite。主数据库文件:./data/mmtl.db。
|
||||||
MEDIASTATION_DATABASE_TYPE: sqlite
|
MMTL_DATABASE_TYPE: sqlite
|
||||||
MEDIASTATION_DATABASE_DB_PATH: /data/mediastation.db
|
MMTL_DATABASE_DB_PATH: /data/mmtl.db
|
||||||
MEDIASTATION_CACHE_CACHE_DIR: /cache
|
MMTL_CACHE_CACHE_DIR: /cache
|
||||||
|
|
||||||
# 路径映射保持容器内统一,网页和下载器里优先使用 /media、/downloads。
|
# 路径映射保持容器内统一,网页和下载器里优先使用 /media、/downloads。
|
||||||
MEDIASTATION_MEDIA_DIR: /media
|
MMTL_MEDIA_DIR: /media
|
||||||
MEDIASTATION_MEDIA_CONTAINER_DIR: /media
|
MMTL_MEDIA_CONTAINER_DIR: /media
|
||||||
MEDIASTATION_DOWNLOAD_DIR: /downloads
|
MMTL_DOWNLOAD_DIR: /downloads
|
||||||
MEDIASTATION_DOWNLOAD_CONTAINER_DIR: /downloads
|
MMTL_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||||
|
|
||||||
# 完整应用日志默认写入 ./data/logs/app.log;排查复杂问题时可临时改成 debug。
|
# 完整应用日志默认写入 ./data/logs/app.log;排查复杂问题时可临时改成 debug。
|
||||||
MEDIASTATION_LOGGING_LEVEL: info
|
MMTL_LOGGING_LEVEL: info
|
||||||
MEDIASTATION_LOGGING_FORMAT: console
|
MMTL_LOGGING_FORMAT: console
|
||||||
MEDIASTATION_LOGGING_OUTPUT_PATH: /data/logs
|
MMTL_LOGGING_OUTPUT_PATH: /data/logs
|
||||||
MEDIASTATION_LOGGING_MAX_SIZE_MB: "50"
|
MMTL_LOGGING_MAX_SIZE_MB: "50"
|
||||||
MEDIASTATION_LOGGING_MAX_BACKUPS: "20"
|
MMTL_LOGGING_MAX_BACKUPS: "20"
|
||||||
MEDIASTATION_LOGGING_MAX_AGE_DAYS: "30"
|
MMTL_LOGGING_MAX_AGE_DAYS: "30"
|
||||||
|
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
# 容器访问宿主机服务用,例如 qBittorrent: http://host.docker.internal:8085
|
# 容器访问宿主机服务用,例如 qBittorrent: http://host.docker.internal:8085
|
||||||
|
|||||||
+36
-36
@@ -1,7 +1,7 @@
|
|||||||
# MediaStationGo 第二档完整 Docker Compose 部署文件
|
# MMTL 第二档完整 Docker Compose 部署文件
|
||||||
#
|
#
|
||||||
# 组件:
|
# 组件:
|
||||||
# MediaStationGo + PostgreSQL + Redis
|
# MMTL + PostgreSQL + Redis
|
||||||
#
|
#
|
||||||
# 使用方式二选一:
|
# 使用方式二选一:
|
||||||
# 1. 保存为 docker-compose.yml 后执行:
|
# 1. 保存为 docker-compose.yml 后执行:
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
# admin / admin123
|
# admin / admin123
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mediastation-go:
|
mmtl:
|
||||||
# 镜像二选一:
|
# 镜像二选一:
|
||||||
# 方式一:GitHub 仓库镜像 GHCR(默认,推荐)
|
# 方式一:GitHub 仓库镜像 GHCR(默认,推荐)
|
||||||
image: ghcr.io/shukebta/mediastation-go:latest
|
image: ghcr.io/shukebta/mmtl:latest
|
||||||
# 方式二:Docker Hub 备用(GHCR 拉取慢或不可用时使用)
|
# 方式二:Docker Hub 备用(GHCR 拉取慢或不可用时使用)
|
||||||
# image: shukbet/mediastationgo:latest
|
# image: shukbet/mmtl:latest
|
||||||
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
init: true
|
init: true
|
||||||
@@ -73,39 +73,39 @@ services:
|
|||||||
PUID: "1000"
|
PUID: "1000"
|
||||||
PGID: "1000"
|
PGID: "1000"
|
||||||
|
|
||||||
MEDIASTATION_APP_HOST: 0.0.0.0
|
MMTL_APP_HOST: 0.0.0.0
|
||||||
MEDIASTATION_APP_PORT: 8080
|
MMTL_APP_PORT: 8080
|
||||||
MEDIASTATION_APP_WEB_DIR: /app/web/dist
|
MMTL_APP_WEB_DIR: /app/web/dist
|
||||||
MEDIASTATION_APP_DATA_DIR: /data
|
MMTL_APP_DATA_DIR: /data
|
||||||
MEDIASTATION_LOGGING_LEVEL: info
|
MMTL_LOGGING_LEVEL: info
|
||||||
MEDIASTATION_LOGGING_FORMAT: console
|
MMTL_LOGGING_FORMAT: console
|
||||||
MEDIASTATION_LOGGING_OUTPUT_PATH: /data/logs
|
MMTL_LOGGING_OUTPUT_PATH: /data/logs
|
||||||
MEDIASTATION_LOGGING_MAX_SIZE_MB: "50"
|
MMTL_LOGGING_MAX_SIZE_MB: "50"
|
||||||
MEDIASTATION_LOGGING_MAX_BACKUPS: "20"
|
MMTL_LOGGING_MAX_BACKUPS: "20"
|
||||||
MEDIASTATION_LOGGING_MAX_AGE_DAYS: "30"
|
MMTL_LOGGING_MAX_AGE_DAYS: "30"
|
||||||
|
|
||||||
MEDIASTATION_DATABASE_TYPE: postgres
|
MMTL_DATABASE_TYPE: postgres
|
||||||
MEDIASTATION_DATABASE_DSN: postgres://mediastation:mediastation@postgres:5432/mediastation?sslmode=disable
|
MMTL_DATABASE_DSN: postgres://mmtl:mmtl@postgres:5432/mmtl?sslmode=disable
|
||||||
MEDIASTATION_DATABASE_DB_PATH: /data/mediastation.db
|
MMTL_DATABASE_DB_PATH: /data/mmtl.db
|
||||||
|
|
||||||
# Redis 只做热缓存,源数据仍在 PostgreSQL;Redis 丢失可自动重建。
|
# Redis 只做热缓存,源数据仍在 PostgreSQL;Redis 丢失可自动重建。
|
||||||
MEDIASTATION_CACHE_REDIS_URL: redis://redis:6379/0
|
MMTL_CACHE_REDIS_URL: redis://redis:6379/0
|
||||||
MEDIASTATION_CACHE_CACHE_DIR: /cache
|
MMTL_CACHE_CACHE_DIR: /cache
|
||||||
|
|
||||||
MEDIASTATION_UPDATE_IMAGE: ghcr.io/shukebta/mediastation-go:latest
|
MMTL_UPDATE_IMAGE: ghcr.io/shukebta/mmtl:latest
|
||||||
|
|
||||||
# 路径换算配置。左边宿主机真实路径要和 volumes 左边保持一致。
|
# 路径换算配置。左边宿主机真实路径要和 volumes 左边保持一致。
|
||||||
MEDIASTATION_MEDIA_DIR: /media
|
MMTL_MEDIA_DIR: /media
|
||||||
MEDIASTATION_MEDIA_CONTAINER_DIR: /media
|
MMTL_MEDIA_CONTAINER_DIR: /media
|
||||||
MEDIASTATION_DOWNLOAD_DIR: /downloads
|
MMTL_DOWNLOAD_DIR: /downloads
|
||||||
MEDIASTATION_DOWNLOAD_CONTAINER_DIR: /downloads
|
MMTL_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||||
|
|
||||||
MEDIASTATION_TRANSCODER_ENABLED: "true"
|
MMTL_TRANSCODER_ENABLED: "true"
|
||||||
MEDIASTATION_TRANSCODER_HARDWARE_ACCEL: "false"
|
MMTL_TRANSCODER_HARDWARE_ACCEL: "false"
|
||||||
MEDIASTATION_TRANSCODER_REALTIME: "true"
|
MMTL_TRANSCODER_REALTIME: "true"
|
||||||
MEDIASTATION_TRANSCODER_THREADS: "2"
|
MMTL_TRANSCODER_THREADS: "2"
|
||||||
MEDIASTATION_TRANSCODER_MAX_CONCURRENT: "1"
|
MMTL_TRANSCODER_MAX_CONCURRENT: "1"
|
||||||
MEDIASTATION_TRANSCODER_IDLE_TIMEOUT_SECONDS: "120"
|
MMTL_TRANSCODER_IDLE_TIMEOUT_SECONDS: "120"
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "busybox wget -qO- http://127.0.0.1:8080/api/health || exit 1"]
|
test: ["CMD-SHELL", "busybox wget -qO- http://127.0.0.1:8080/api/health || exit 1"]
|
||||||
@@ -122,18 +122,18 @@ services:
|
|||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
# 首次部署允许拉取;日常更新请只 pull mediastation-go。
|
# 首次部署允许拉取;日常更新请只 pull mmtl。
|
||||||
pull_policy: missing
|
pull_policy: missing
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: mediastation
|
POSTGRES_DB: mmtl
|
||||||
POSTGRES_USER: mediastation
|
POSTGRES_USER: mmtl
|
||||||
POSTGRES_PASSWORD: mediastation
|
POSTGRES_PASSWORD: mmtl
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
volumes:
|
volumes:
|
||||||
- ./postgres:/var/lib/postgresql/data
|
- ./postgres:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U mediastation -d mediastation"]
|
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U mmtl -d mmtl"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|||||||
+36
-36
@@ -1,4 +1,4 @@
|
|||||||
# MediaStationGo 最简单 Docker Compose 部署文件
|
# MMTL 最简单 Docker Compose 部署文件
|
||||||
#
|
#
|
||||||
# 新手建议:
|
# 新手建议:
|
||||||
# 1. 不用 .env。
|
# 1. 不用 .env。
|
||||||
@@ -15,12 +15,12 @@
|
|||||||
# admin / admin123
|
# admin / admin123
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mediastation-go:
|
mmtl:
|
||||||
# 镜像二选一:
|
# 镜像二选一:
|
||||||
# 方式一:GitHub 仓库镜像 GHCR(默认,推荐)
|
# 方式一:GitHub 仓库镜像 GHCR(默认,推荐)
|
||||||
image: ghcr.io/shukebta/mediastation-go:latest
|
image: ghcr.io/shukebta/mmtl:latest
|
||||||
# 方式二:Docker Hub 备用(GHCR 拉取慢或不可用时使用)
|
# 方式二:Docker Hub 备用(GHCR 拉取慢或不可用时使用)
|
||||||
# image: shukbet/mediastationgo:latest
|
# image: shukbet/mmtl:latest
|
||||||
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
init: true
|
init: true
|
||||||
@@ -87,52 +87,52 @@ services:
|
|||||||
PGID: "1000"
|
PGID: "1000"
|
||||||
|
|
||||||
# 程序基础配置,通常不用改。
|
# 程序基础配置,通常不用改。
|
||||||
MEDIASTATION_APP_HOST: 0.0.0.0
|
MMTL_APP_HOST: 0.0.0.0
|
||||||
MEDIASTATION_APP_PORT: 8080
|
MMTL_APP_PORT: 8080
|
||||||
MEDIASTATION_APP_WEB_DIR: /app/web/dist
|
MMTL_APP_WEB_DIR: /app/web/dist
|
||||||
MEDIASTATION_APP_DATA_DIR: /data
|
MMTL_APP_DATA_DIR: /data
|
||||||
# 详细应用日志会保存在 ./data/logs/app.log,warn/error 也会拆分保存。
|
# 详细应用日志会保存在 ./data/logs/app.log,warn/error 也会拆分保存。
|
||||||
# 排查复杂问题时可临时改成 debug。
|
# 排查复杂问题时可临时改成 debug。
|
||||||
MEDIASTATION_LOGGING_LEVEL: info
|
MMTL_LOGGING_LEVEL: info
|
||||||
MEDIASTATION_LOGGING_FORMAT: console
|
MMTL_LOGGING_FORMAT: console
|
||||||
MEDIASTATION_LOGGING_OUTPUT_PATH: /data/logs
|
MMTL_LOGGING_OUTPUT_PATH: /data/logs
|
||||||
MEDIASTATION_LOGGING_MAX_SIZE_MB: "50"
|
MMTL_LOGGING_MAX_SIZE_MB: "50"
|
||||||
MEDIASTATION_LOGGING_MAX_BACKUPS: "20"
|
MMTL_LOGGING_MAX_BACKUPS: "20"
|
||||||
MEDIASTATION_LOGGING_MAX_AGE_DAYS: "30"
|
MMTL_LOGGING_MAX_AGE_DAYS: "30"
|
||||||
|
|
||||||
# 轻量模式默认只使用 PostgreSQL,适合大多数 NAS。
|
# 轻量模式默认只使用 PostgreSQL,适合大多数 NAS。
|
||||||
# 旧版 ./data/mediastation.db 存在时,首次启动会自动迁移到 PostgreSQL。
|
# 旧版 ./data/mmtl.db 存在时,首次启动会自动迁移到 PostgreSQL。
|
||||||
MEDIASTATION_DATABASE_TYPE: postgres
|
MMTL_DATABASE_TYPE: postgres
|
||||||
MEDIASTATION_DATABASE_DSN: postgres://mediastation:mediastation@postgres:5432/mediastation?sslmode=disable
|
MMTL_DATABASE_DSN: postgres://mmtl:mmtl@postgres:5432/mmtl?sslmode=disable
|
||||||
|
|
||||||
# SQLite 旧库迁移源:
|
# SQLite 旧库迁移源:
|
||||||
# - 首次从旧版 ./data/mediastation.db 导入时保持此路径。
|
# - 首次从旧版 ./data/mmtl.db 导入时保持此路径。
|
||||||
# - 确认迁移完成后,建议改成 /data/no-sqlite-migration.db 这类不存在的路径。
|
# - 确认迁移完成后,建议改成 /data/no-sqlite-migration.db 这类不存在的路径。
|
||||||
MEDIASTATION_DATABASE_DB_PATH: /data/mediastation.db
|
MMTL_DATABASE_DB_PATH: /data/mmtl.db
|
||||||
MEDIASTATION_CACHE_CACHE_DIR: /cache
|
MMTL_CACHE_CACHE_DIR: /cache
|
||||||
|
|
||||||
# 管理面板热更新默认拉取此镜像,并用 Watchtower 一次性重建当前容器。
|
# 管理面板热更新默认拉取此镜像,并用 Watchtower 一次性重建当前容器。
|
||||||
MEDIASTATION_UPDATE_IMAGE: ghcr.io/shukebta/mediastation-go:latest
|
MMTL_UPDATE_IMAGE: ghcr.io/shukebta/mmtl:latest
|
||||||
|
|
||||||
# 路径换算配置。
|
# 路径换算配置。
|
||||||
# 默认推荐在网页里使用容器路径 /media。
|
# 默认推荐在网页里使用容器路径 /media。
|
||||||
# 如果旧媒体库已经保存了宿主机路径 /vol1/1000/Media,
|
# 如果旧媒体库已经保存了宿主机路径 /vol1/1000/Media,
|
||||||
# 再把这里改成同一个宿主机真实路径用于旧路径换算。
|
# 再把这里改成同一个宿主机真实路径用于旧路径换算。
|
||||||
MEDIASTATION_MEDIA_DIR: /media
|
MMTL_MEDIA_DIR: /media
|
||||||
MEDIASTATION_MEDIA_CONTAINER_DIR: /media
|
MMTL_MEDIA_CONTAINER_DIR: /media
|
||||||
|
|
||||||
# 默认推荐下载器保存路径使用 /downloads。
|
# 默认推荐下载器保存路径使用 /downloads。
|
||||||
# 如果下载器只能返回宿主机路径,再改成同一个宿主机真实路径。
|
# 如果下载器只能返回宿主机路径,再改成同一个宿主机真实路径。
|
||||||
MEDIASTATION_DOWNLOAD_DIR: /downloads
|
MMTL_DOWNLOAD_DIR: /downloads
|
||||||
MEDIASTATION_DOWNLOAD_CONTAINER_DIR: /downloads
|
MMTL_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||||
|
|
||||||
# NAS 友好的低负载默认值。
|
# NAS 友好的低负载默认值。
|
||||||
MEDIASTATION_TRANSCODER_ENABLED: "true"
|
MMTL_TRANSCODER_ENABLED: "true"
|
||||||
MEDIASTATION_TRANSCODER_HARDWARE_ACCEL: "false"
|
MMTL_TRANSCODER_HARDWARE_ACCEL: "false"
|
||||||
MEDIASTATION_TRANSCODER_REALTIME: "true"
|
MMTL_TRANSCODER_REALTIME: "true"
|
||||||
MEDIASTATION_TRANSCODER_THREADS: "2"
|
MMTL_TRANSCODER_THREADS: "2"
|
||||||
MEDIASTATION_TRANSCODER_MAX_CONCURRENT: "1"
|
MMTL_TRANSCODER_MAX_CONCURRENT: "1"
|
||||||
MEDIASTATION_TRANSCODER_IDLE_TIMEOUT_SECONDS: "120"
|
MMTL_TRANSCODER_IDLE_TIMEOUT_SECONDS: "120"
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "busybox wget -qO- http://127.0.0.1:8080/api/health || exit 1"]
|
test: ["CMD-SHELL", "busybox wget -qO- http://127.0.0.1:8080/api/health || exit 1"]
|
||||||
@@ -150,20 +150,20 @@ services:
|
|||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
# 首次部署允许拉取;日常更新请只 pull mediastation-go。
|
# 首次部署允许拉取;日常更新请只 pull mmtl。
|
||||||
# 如需升级 PostgreSQL,请先备份 ./postgres 后再手动调整镜像版本并拉取。
|
# 如需升级 PostgreSQL,请先备份 ./postgres 后再手动调整镜像版本并拉取。
|
||||||
pull_policy: missing
|
pull_policy: missing
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: mediastation
|
POSTGRES_DB: mmtl
|
||||||
POSTGRES_USER: mediastation
|
POSTGRES_USER: mmtl
|
||||||
POSTGRES_PASSWORD: mediastation
|
POSTGRES_PASSWORD: mmtl
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
volumes:
|
volumes:
|
||||||
# PostgreSQL 主数据目录。升级/重建容器时必须保留。
|
# PostgreSQL 主数据目录。升级/重建容器时必须保留。
|
||||||
- ./postgres:/var/lib/postgresql/data
|
- ./postgres:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U mediastation -d mediastation"]
|
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U mmtl -d mmtl"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|||||||
Vendored
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
run_uid="${PUID:-$(id -u mediastation 2>/dev/null || echo 1000)}"
|
run_uid="${PUID:-$(id -u mmtl 2>/dev/null || echo 1000)}"
|
||||||
run_gid="${PGID:-$(id -g mediastation 2>/dev/null || echo 1000)}"
|
run_gid="${PGID:-$(id -g mmtl 2>/dev/null || echo 1000)}"
|
||||||
|
|
||||||
case "$run_uid" in
|
case "$run_uid" in
|
||||||
''|*[!0-9]*)
|
''|*[!0-9]*)
|
||||||
@@ -19,10 +19,10 @@ case "$run_gid" in
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
if [ "$run_uid" = "0" ]; then
|
if [ "$run_uid" = "0" ]; then
|
||||||
exec mediastation-go
|
exec mmtl
|
||||||
fi
|
fi
|
||||||
|
|
||||||
chown -R "$run_uid:$run_gid" /data /cache 2>/dev/null || true
|
chown -R "$run_uid:$run_gid" /data /cache 2>/dev/null || true
|
||||||
chown "$run_uid:$run_gid" /media 2>/dev/null || true
|
chown "$run_uid:$run_gid" /media 2>/dev/null || true
|
||||||
|
|
||||||
exec su-exec "$run_uid:$run_gid" mediastation-go
|
exec su-exec "$run_uid:$run_gid" mmtl
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
module github.com/ShukeBta/MediaStationGo
|
module github.com/ShukeBta/MMTL
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// 1. 内置默认值
|
// 1. 内置默认值
|
||||||
// 2. 工作目录中的 config.yaml(嵌套格式)
|
// 2. 工作目录中的 config.yaml(嵌套格式)
|
||||||
// 3. config/*.yaml 分片文件(按模块)
|
// 3. config/*.yaml 分片文件(按模块)
|
||||||
// 4. 以 MEDIASTATION_ 为前缀的环境变量
|
// 4. 以 MMTL_ 为前缀的环境变量
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// EnvPrefix 是所有环境变量驱动的覆盖使用的前缀。
|
// EnvPrefix 是所有环境变量驱动的覆盖使用的前缀。
|
||||||
const EnvPrefix = "MEDIASTATION"
|
const EnvPrefix = "MMTL"
|
||||||
|
|
||||||
// Load 从默认值 / 文件 / 环境读取配置。
|
// Load 从默认值 / 文件 / 环境读取配置。
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -40,15 +40,15 @@ func TestLoadDefaults(t *testing.T) {
|
|||||||
if cfg.Database.MaxOpenConns != defaultDatabaseMaxOpenConns {
|
if cfg.Database.MaxOpenConns != defaultDatabaseMaxOpenConns {
|
||||||
t.Fatalf("expected default MaxOpenConns %d, got %d", defaultDatabaseMaxOpenConns, cfg.Database.MaxOpenConns)
|
t.Fatalf("expected default MaxOpenConns %d, got %d", defaultDatabaseMaxOpenConns, cfg.Database.MaxOpenConns)
|
||||||
}
|
}
|
||||||
if cfg.Cache.RedisPrefix != "mediastationgo" {
|
if cfg.Cache.RedisPrefix != "mmtl" {
|
||||||
t.Fatalf("expected default redis prefix, got %q", cfg.Cache.RedisPrefix)
|
t.Fatalf("expected default redis prefix, got %q", cfg.Cache.RedisPrefix)
|
||||||
}
|
}
|
||||||
if cfg.Cache.MediaTTLSeconds != 15 {
|
if cfg.Cache.MediaTTLSeconds != 15 {
|
||||||
t.Fatalf("expected default media cache ttl 15, got %d", cfg.Cache.MediaTTLSeconds)
|
t.Fatalf("expected default media cache ttl 15, got %d", cfg.Cache.MediaTTLSeconds)
|
||||||
}
|
}
|
||||||
if cfg.Search.Index != "mediastation_media" {
|
if cfg.Search.Index != "mmtl_media" {
|
||||||
t.Fatalf("expected default search index, got %q", cfg.Search.Index)
|
t.Fatalf("expected default search index, got %q", cfg.Search.Index)
|
||||||
}
|
}
|
||||||
if cfg.Database.MaxIdleConns != defaultDatabaseMaxIdleConns {
|
if cfg.Database.MaxIdleConns != defaultDatabaseMaxIdleConns {
|
||||||
t.Fatalf("expected default MaxIdleConns %d, got %d", defaultDatabaseMaxIdleConns, cfg.Database.MaxIdleConns)
|
t.Fatalf("expected default MaxIdleConns %d, got %d", defaultDatabaseMaxIdleConns, cfg.Database.MaxIdleConns)
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ func TestLoadDefaults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestEnvOverride checks that MEDIASTATION_* env vars override the defaults.
|
// TestEnvOverride checks that MMTL_* env vars override the defaults.
|
||||||
func TestEnvOverride(t *testing.T) {
|
func TestEnvOverride(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
wd, _ := os.Getwd()
|
wd, _ := os.Getwd()
|
||||||
@@ -83,16 +83,16 @@ func TestEnvOverride(t *testing.T) {
|
|||||||
t.Fatalf("chdir: %v", err)
|
t.Fatalf("chdir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Setenv("MEDIASTATION_APP_PORT", "9090")
|
t.Setenv("MMTL_APP_PORT", "9090")
|
||||||
t.Setenv("MEDIASTATION_DATABASE_TYPE", "postgres")
|
t.Setenv("MMTL_DATABASE_TYPE", "postgres")
|
||||||
t.Setenv("MEDIASTATION_DATABASE_DSN", "postgres://msgo:secret@postgres:5432/msgo?sslmode=disable")
|
t.Setenv("MMTL_DATABASE_DSN", "postgres://mmtl:secret@postgres:5432/mmtl?sslmode=disable")
|
||||||
t.Setenv("MEDIASTATION_CACHE_REDIS_URL", "redis://redis:6379/0")
|
t.Setenv("MMTL_CACHE_REDIS_URL", "redis://redis:6379/0")
|
||||||
t.Setenv("MEDIASTATION_CACHE_MEDIA_TTL_SECONDS", "30")
|
t.Setenv("MMTL_CACHE_MEDIA_TTL_SECONDS", "30")
|
||||||
t.Setenv("MEDIASTATION_SEARCH_BACKEND", "opensearch")
|
t.Setenv("MMTL_SEARCH_BACKEND", "opensearch")
|
||||||
t.Setenv("MEDIASTATION_SEARCH_OPENSEARCH_URL", "http://opensearch:9200")
|
t.Setenv("MMTL_SEARCH_OPENSEARCH_URL", "http://opensearch:9200")
|
||||||
t.Setenv("MEDIASTATION_LICENSE_SERVER_URL", "https://license.example.com")
|
t.Setenv("MMTL_LICENSE_SERVER_URL", "https://license.example.com")
|
||||||
t.Setenv("MEDIASTATION_LICENSE_HMAC_SECRET", "override-secret")
|
t.Setenv("MMTL_LICENSE_HMAC_SECRET", "override-secret")
|
||||||
t.Setenv("MEDIASTATION_LICENSE_PUBLIC_KEY", "override-public-key")
|
t.Setenv("MMTL_LICENSE_PUBLIC_KEY", "override-public-key")
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Load() error: %v", err)
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func setDefaults(v *viper.Viper) {
|
|||||||
v.SetDefault("app.server_url", "")
|
v.SetDefault("app.server_url", "")
|
||||||
|
|
||||||
v.SetDefault("database.type", "auto")
|
v.SetDefault("database.type", "auto")
|
||||||
v.SetDefault("database.db_path", "./data/mediastation.db")
|
v.SetDefault("database.db_path", "./data/mmtl.db")
|
||||||
v.SetDefault("database.dsn", "")
|
v.SetDefault("database.dsn", "")
|
||||||
v.SetDefault("database.wal_mode", true)
|
v.SetDefault("database.wal_mode", true)
|
||||||
v.SetDefault("database.busy_timeout", 5000)
|
v.SetDefault("database.busy_timeout", 5000)
|
||||||
@@ -45,12 +45,12 @@ func setDefaults(v *viper.Viper) {
|
|||||||
v.SetDefault("cache.cache_dir", "./cache")
|
v.SetDefault("cache.cache_dir", "./cache")
|
||||||
v.SetDefault("cache.cleanup_interval_min", 60)
|
v.SetDefault("cache.cleanup_interval_min", 60)
|
||||||
v.SetDefault("cache.redis_url", "")
|
v.SetDefault("cache.redis_url", "")
|
||||||
v.SetDefault("cache.redis_prefix", "mediastationgo")
|
v.SetDefault("cache.redis_prefix", "mmtl")
|
||||||
v.SetDefault("cache.media_ttl_seconds", 15)
|
v.SetDefault("cache.media_ttl_seconds", 15)
|
||||||
|
|
||||||
v.SetDefault("search.backend", "")
|
v.SetDefault("search.backend", "")
|
||||||
v.SetDefault("search.opensearch_url", "")
|
v.SetDefault("search.opensearch_url", "")
|
||||||
v.SetDefault("search.index", "mediastation_media")
|
v.SetDefault("search.index", "mmtl_media")
|
||||||
v.SetDefault("search.username", "")
|
v.SetDefault("search.username", "")
|
||||||
v.SetDefault("search.password", "")
|
v.SetDefault("search.password", "")
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ func setDefaults(v *viper.Viper) {
|
|||||||
|
|
||||||
v.SetDefault("flaresolverr.enabled", false)
|
v.SetDefault("flaresolverr.enabled", false)
|
||||||
v.SetDefault("flaresolverr.url", "http://localhost:8191")
|
v.SetDefault("flaresolverr.url", "http://localhost:8191")
|
||||||
v.SetDefault("flaresolverr.session", "mediastation")
|
v.SetDefault("flaresolverr.session", "mmtl")
|
||||||
v.SetDefault("flaresolverr.timeout", 60)
|
v.SetDefault("flaresolverr.timeout", 60)
|
||||||
|
|
||||||
v.SetDefault("downloads.smart_classify", true)
|
v.SetDefault("downloads.smart_classify", true)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ func (c *Config) normalize() error {
|
|||||||
c.App.DataDir = "./data"
|
c.App.DataDir = "./data"
|
||||||
}
|
}
|
||||||
if c.Database.DBPath == "" {
|
if c.Database.DBPath == "" {
|
||||||
c.Database.DBPath = filepath.Join(c.App.DataDir, "mediastation.db")
|
c.Database.DBPath = filepath.Join(c.App.DataDir, "mmtl.db")
|
||||||
}
|
}
|
||||||
if c.Database.Type == "" {
|
if c.Database.Type == "" {
|
||||||
c.Database.Type = "auto"
|
c.Database.Type = "auto"
|
||||||
@@ -45,14 +45,14 @@ func (c *Config) normalize() error {
|
|||||||
c.Cache.CacheDir = filepath.Join(c.App.DataDir, "cache")
|
c.Cache.CacheDir = filepath.Join(c.App.DataDir, "cache")
|
||||||
}
|
}
|
||||||
if c.Cache.RedisPrefix == "" {
|
if c.Cache.RedisPrefix == "" {
|
||||||
c.Cache.RedisPrefix = "mediastationgo"
|
c.Cache.RedisPrefix = "mmtl"
|
||||||
}
|
}
|
||||||
if c.Cache.MediaTTLSeconds < 1 {
|
if c.Cache.MediaTTLSeconds < 1 {
|
||||||
c.Cache.MediaTTLSeconds = 15
|
c.Cache.MediaTTLSeconds = 15
|
||||||
}
|
}
|
||||||
c.Search.Backend = strings.ToLower(strings.TrimSpace(c.Search.Backend))
|
c.Search.Backend = strings.ToLower(strings.TrimSpace(c.Search.Backend))
|
||||||
if c.Search.Index == "" {
|
if c.Search.Index == "" {
|
||||||
c.Search.Index = "mediastation_media"
|
c.Search.Index = "mmtl_media"
|
||||||
}
|
}
|
||||||
if c.Secrets.JWTSecret == "" {
|
if c.Secrets.JWTSecret == "" {
|
||||||
// 持久化自动生成的密钥以在操作员忘记配置时保持会话稳定。
|
// 持久化自动生成的密钥以在操作员忘记配置时保持会话稳定。
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ type AIConfig struct {
|
|||||||
MaxConcurrent int `mapstructure:"max_concurrent"`
|
MaxConcurrent int `mapstructure:"max_concurrent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LicenseConfig configures the optional MediaStationGo license server bridge.
|
// LicenseConfig configures the optional MMTL license server bridge.
|
||||||
type LicenseConfig struct {
|
type LicenseConfig struct {
|
||||||
ServerURL string `mapstructure:"server_url"`
|
ServerURL string `mapstructure:"server_url"`
|
||||||
HMACSecret string `mapstructure:"hmac_secret"`
|
HMACSecret string `mapstructure:"hmac_secret"`
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Open initialises the configured GORM database. database.type=auto chooses
|
// Open initialises the configured GORM database. database.type=auto chooses
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestOpenRequiresConfig(t *testing.T) {
|
func TestOpenRequiresConfig(t *testing.T) {
|
||||||
@@ -28,7 +28,7 @@ func TestOpenRequiresConfig(t *testing.T) {
|
|||||||
func TestOpenSQLiteWithNilLoggerConfiguresPool(t *testing.T) {
|
func TestOpenSQLiteWithNilLoggerConfiguresPool(t *testing.T) {
|
||||||
cfg := &config.Config{}
|
cfg := &config.Config{}
|
||||||
cfg.Database.Type = "sqlite"
|
cfg.Database.Type = "sqlite"
|
||||||
cfg.Database.DBPath = filepath.Join(t.TempDir(), "mediastation.db")
|
cfg.Database.DBPath = filepath.Join(t.TempDir(), "mmtl.db")
|
||||||
cfg.Database.WALMode = true
|
cfg.Database.WALMode = true
|
||||||
cfg.Database.BusyTimeout = 5000
|
cfg.Database.BusyTimeout = 5000
|
||||||
cfg.Database.CacheSize = -2000
|
cfg.Database.CacheSize = -2000
|
||||||
@@ -278,7 +278,7 @@ func TestSQLiteMigrationCompleteMarker(t *testing.T) {
|
|||||||
|
|
||||||
func TestSQLiteMigrationFallsBackToDataDirDefaultPath(t *testing.T) {
|
func TestSQLiteMigrationFallsBackToDataDirDefaultPath(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
sqlitePath := filepath.Join(dir, "mediastation.db")
|
sqlitePath := filepath.Join(dir, "mmtl.db")
|
||||||
src, err := gorm.Open(sqlite.Open(sqlitePath), &gorm.Config{})
|
src, err := gorm.Open(sqlite.Open(sqlitePath), &gorm.Config{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -362,7 +362,7 @@ func TestSQLiteMigrationFallsBackToDataDirDefaultPath(t *testing.T) {
|
|||||||
|
|
||||||
func TestOpenSQLiteMigrationSourceUsesFallbackSourcePath(t *testing.T) {
|
func TestOpenSQLiteMigrationSourceUsesFallbackSourcePath(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
sqlitePath := filepath.Join(dir, "mediastation.db")
|
sqlitePath := filepath.Join(dir, "mmtl.db")
|
||||||
src, err := gorm.Open(sqlite.Open(sqlitePath), &gorm.Config{})
|
src, err := gorm.Open(sqlite.Open(sqlitePath), &gorm.Config{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ensureLibraryRootsCompatibility(db *gorm.DB) error {
|
func ensureLibraryRootsCompatibility(db *gorm.DB) error {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package database
|
|||||||
import (
|
import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AutoMigrate creates tables for every model registered in the model package.
|
// AutoMigrate creates tables for every model registered in the model package.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MigrateSQLiteToCurrentIfNeeded copies an existing SQLite database into
|
// MigrateSQLiteToCurrentIfNeeded copies an existing SQLite database into
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resetBootstrapTargetBeforeSQLiteMigrationIfSafe(src, target *gorm.DB, log *zap.Logger) error {
|
func resetBootstrapTargetBeforeSQLiteMigrationIfSafe(src, target *gorm.DB, log *zap.Logger) error {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
"gorm.io/gorm/schema"
|
"gorm.io/gorm/schema"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func copyModelTables(src, target *gorm.DB, batchSize int) (int64, error) {
|
func copyModelTables(src, target *gorm.DB, batchSize int) (int64, error) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func openSQLiteMigrationSource(cfg *config.Config, sqlitePath string) (*gorm.DB, error) {
|
func openSQLiteMigrationSource(cfg *config.Config, sqlitePath string) (*gorm.DB, error) {
|
||||||
@@ -36,7 +36,7 @@ func sqliteMigrationSourcePath(cfg *config.Config, log *zap.Logger) (string, err
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fallback := filepath.Join(strings.TrimSpace(cfg.App.DataDir), "mediastation.db")
|
fallback := filepath.Join(strings.TrimSpace(cfg.App.DataDir), "mmtl.db")
|
||||||
if fallback == "" || sameCleanPath(configured, fallback) {
|
if fallback == "" || sameCleanPath(configured, fallback) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import (
|
|||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func installSQLiteWriteGate(db *gorm.DB) {
|
func installSQLiteWriteGate(db *gorm.DB) {
|
||||||
if db == nil {
|
if db == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const lockedKey = "mediastation:sqlite_write_locked"
|
const lockedKey = "mmtl:sqlite_write_locked"
|
||||||
gate := newSQLiteWriteGate()
|
gate := newSQLiteWriteGate()
|
||||||
lock := func(tx *gorm.DB) {
|
lock := func(tx *gorm.DB) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -32,14 +32,14 @@ func installSQLiteWriteGate(db *gorm.DB) {
|
|||||||
gate.Unlock()
|
gate.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = db.Callback().Create().Before("gorm:create").Register("mediastation:sqlite_write_lock", lock)
|
_ = db.Callback().Create().Before("gorm:create").Register("mmtl:sqlite_write_lock", lock)
|
||||||
_ = db.Callback().Create().After("gorm:create").Register("mediastation:sqlite_write_unlock", unlock)
|
_ = db.Callback().Create().After("gorm:create").Register("mmtl:sqlite_write_unlock", unlock)
|
||||||
_ = db.Callback().Update().Before("gorm:update").Register("mediastation:sqlite_write_lock", lock)
|
_ = db.Callback().Update().Before("gorm:update").Register("mmtl:sqlite_write_lock", lock)
|
||||||
_ = db.Callback().Update().After("gorm:update").Register("mediastation:sqlite_write_unlock", unlock)
|
_ = db.Callback().Update().After("gorm:update").Register("mmtl:sqlite_write_unlock", unlock)
|
||||||
_ = db.Callback().Delete().Before("gorm:delete").Register("mediastation:sqlite_write_lock", lock)
|
_ = db.Callback().Delete().Before("gorm:delete").Register("mmtl:sqlite_write_lock", lock)
|
||||||
_ = db.Callback().Delete().After("gorm:delete").Register("mediastation:sqlite_write_unlock", unlock)
|
_ = db.Callback().Delete().After("gorm:delete").Register("mmtl:sqlite_write_unlock", unlock)
|
||||||
_ = db.Callback().Raw().Before("gorm:raw").Register("mediastation:sqlite_write_lock", lock)
|
_ = db.Callback().Raw().Before("gorm:raw").Register("mmtl:sqlite_write_lock", lock)
|
||||||
_ = db.Callback().Raw().After("gorm:raw").Register("mediastation:sqlite_write_unlock", unlock)
|
_ = db.Callback().Raw().After("gorm:raw").Register("mmtl:sqlite_write_unlock", unlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sqliteWriteGate serializes in-process SQLite writes while respecting the
|
// sqliteWriteGate serializes in-process SQLite writes while respecting the
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
const embyCtxUserName = "emby_user_name"
|
const embyCtxUserName = "emby_user_name"
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func listUsersHandler(svc *service.Container) gin.HandlerFunc {
|
func listUsersHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type settingReq struct {
|
type settingReq struct {
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
"github.com/ShukeBta/MMTL/internal/repository"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDeleteUserRefusesRecentRealtimeSession(t *testing.T) {
|
func TestDeleteUserRefusesRecentRealtimeSession(t *testing.T) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func listAPIConfigsHandler(svc *service.Container) gin.HandlerFunc {
|
func listAPIConfigsHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ApiConfigHandler API 配置 HTTP 处理。
|
// ApiConfigHandler API 配置 HTTP 处理。
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type loginReq struct {
|
type loginReq struct {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setAccessTokenCookie(c *gin.Context, token string, maxAgeSeconds int) {
|
func setAccessTokenCookie(c *gin.Context, token string, maxAgeSeconds int) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSetAccessTokenCookie(t *testing.T) {
|
func TestSetAccessTokenCookie(t *testing.T) {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// refreshHandler returns a fresh token signed for the current user.
|
// refreshHandler returns a fresh token signed for the current user.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createBackupHandler(svc *service.Container) gin.HandlerFunc {
|
func createBackupHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// getDanmakuHandler returns danmaku for a media. ?kw= optionally overrides the
|
// getDanmakuHandler returns danmaku for a media. ?kw= optionally overrides the
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func dlnaListHandler(svc *service.Container) gin.HandlerFunc {
|
func dlnaListHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// dlnaControlPath maps the action name to the AVTransport SOAP body.
|
// dlnaControlPath maps the action name to the AVTransport SOAP body.
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// embyError 返回 Emby 风格的错误(顶层 Code/Message)。
|
// embyError 返回 Emby 风格的错误(顶层 Code/Message)。
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
"github.com/ShukeBta/MMTL/internal/repository"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseEmbyAuthByNameReqAcceptsLowercaseJSON(t *testing.T) {
|
func TestParseEmbyAuthByNameReqAcceptsLowercaseJSON(t *testing.T) {
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
"github.com/ShukeBta/MMTL/internal/repository"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestEmbyLoginThenAuthedRequest simulates the exact flow an Emby-compatible
|
// TestEmbyLoginThenAuthedRequest simulates the exact flow an Emby-compatible
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
"github.com/ShukeBta/MMTL/internal/repository"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEmbyWithRequestAddressUsesHost(t *testing.T) {
|
func TestEmbyWithRequestAddressUsesHost(t *testing.T) {
|
||||||
@@ -24,7 +24,7 @@ func TestEmbyWithRequestAddressUsesHost(t *testing.T) {
|
|||||||
c, _ := gin.CreateTestContext(w)
|
c, _ := gin.CreateTestContext(w)
|
||||||
c.Request = httptest.NewRequest(http.MethodGet, "http://192.168.1.4:18080/System/Info/Public", nil)
|
c.Request = httptest.NewRequest(http.MethodGet, "http://192.168.1.4:18080/System/Info/Public", nil)
|
||||||
|
|
||||||
payload := embyWithRequestAddress(c, map[string]any{"Id": "mediastation-go-001"})
|
payload := embyWithRequestAddress(c, map[string]any{"Id": "mmtl-001"})
|
||||||
|
|
||||||
if payload["LocalAddress"] != "http://192.168.1.4:18080" {
|
if payload["LocalAddress"] != "http://192.168.1.4:18080" {
|
||||||
t.Fatalf("unexpected LocalAddress: %#v", payload["LocalAddress"])
|
t.Fatalf("unexpected LocalAddress: %#v", payload["LocalAddress"])
|
||||||
@@ -42,7 +42,7 @@ func TestEmbyWithRequestAddressHonorsForwardedHeaders(t *testing.T) {
|
|||||||
c.Request.Header.Set("X-Forwarded-Proto", "https")
|
c.Request.Header.Set("X-Forwarded-Proto", "https")
|
||||||
c.Request.Header.Set("X-Forwarded-Host", "media.example.test")
|
c.Request.Header.Set("X-Forwarded-Host", "media.example.test")
|
||||||
|
|
||||||
payload := embyWithRequestAddress(c, map[string]any{"Id": "mediastation-go-001"})
|
payload := embyWithRequestAddress(c, map[string]any{"Id": "mmtl-001"})
|
||||||
|
|
||||||
if payload["LocalAddress"] != "https://media.example.test" {
|
if payload["LocalAddress"] != "https://media.example.test" {
|
||||||
t.Fatalf("unexpected LocalAddress: %#v", payload["LocalAddress"])
|
t.Fatalf("unexpected LocalAddress: %#v", payload["LocalAddress"])
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
var embyPlaceholderPNG = []byte{
|
var embyPlaceholderPNG = []byte{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseEmbyItemsParams(c *gin.Context) service.ItemsParams {
|
func parseEmbyItemsParams(c *gin.Context) service.ItemsParams {
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
"github.com/ShukeBta/MMTL/internal/repository"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEmbyItemImageServesWithoutAPIAuth(t *testing.T) {
|
func TestEmbyItemImageServesWithoutAPIAuth(t *testing.T) {
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
"github.com/ShukeBta/MMTL/internal/repository"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEmbyVirtualFoldersRouteReturnsJSON(t *testing.T) {
|
func TestEmbyVirtualFoldersRouteReturnsJSON(t *testing.T) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func embyPlaybackInfoHandler(svc *service.Container) gin.HandlerFunc {
|
func embyPlaybackInfoHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
"github.com/ShukeBta/MMTL/internal/repository"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEmbyLowercasePlaybackInfoRouteReturnsJSON(t *testing.T) {
|
func TestEmbyLowercasePlaybackInfoRouteReturnsJSON(t *testing.T) {
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
"github.com/ShukeBta/MMTL/internal/repository"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEmbyLowercaseVideoStreamRouteServesMedia(t *testing.T) {
|
func TestEmbyLowercaseVideoStreamRouteServesMedia(t *testing.T) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type embyPlayingReq struct {
|
type embyPlayingReq struct {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// registerEmbyRoutes 在 r 上挂双前缀("" + "/emby")的 Emby 兼容路由。
|
// registerEmbyRoutes 在 r 上挂双前缀("" + "/emby")的 Emby 兼容路由。
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerLowercaseEmbyAuthRoutes(auth *gin.RouterGroup, svc *service.Container) {
|
func registerLowercaseEmbyAuthRoutes(auth *gin.RouterGroup, svc *service.Container) {
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
"github.com/ShukeBta/MMTL/internal/repository"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEmbyMarkPlayedRefreshesPlaybackDevice(t *testing.T) {
|
func TestEmbyMarkPlayedRefreshesPlaybackDevice(t *testing.T) {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func embySessionsHandler(svc *service.Container) gin.HandlerFunc {
|
func embySessionsHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
@@ -29,7 +29,7 @@ func embySessionsHandler(svc *service.Container) gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
row := gin.H{
|
row := gin.H{
|
||||||
"Id": sess.ID,
|
"Id": sess.ID,
|
||||||
"ServerId": "mediastation-go-001",
|
"ServerId": "mmtl-001",
|
||||||
"Client": sess.Client,
|
"Client": sess.Client,
|
||||||
"DeviceId": sess.DeviceID,
|
"DeviceId": sess.DeviceID,
|
||||||
"DeviceName": sess.DeviceName,
|
"DeviceName": sess.DeviceName,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEmbySessionsReturnsRealtimeSession(t *testing.T) {
|
func TestEmbySessionsReturnsRealtimeSession(t *testing.T) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func embyNoContentHandler(_ *service.Container) gin.HandlerFunc {
|
func embyNoContentHandler(_ *service.Container) gin.HandlerFunc {
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
"github.com/ShukeBta/MMTL/internal/repository"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestEmbySubtitleOfficialRouteServesRawASS verifies that the official Emby
|
// TestEmbySubtitleOfficialRouteServesRawASS verifies that the official Emby
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func embySystemInfoHandler(svc *service.Container) gin.HandlerFunc {
|
func embySystemInfoHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
@@ -84,9 +84,9 @@ func embyPublicSystemInfoPayload(c *gin.Context, svc *service.Container) map[str
|
|||||||
return embyWithRequestAddress(c, svc.Emby.SystemInfoPublic())
|
return embyWithRequestAddress(c, svc.Emby.SystemInfoPublic())
|
||||||
}
|
}
|
||||||
return embyWithRequestAddress(c, map[string]any{
|
return embyWithRequestAddress(c, map[string]any{
|
||||||
"Id": "mediastation-go-001",
|
"Id": "mmtl-001",
|
||||||
"ServerId": "mediastation-go-001",
|
"ServerId": "mmtl-001",
|
||||||
"ServerName": "MediaStationGo",
|
"ServerName": "MMTL",
|
||||||
"Version": "4.8.10.0",
|
"Version": "4.8.10.0",
|
||||||
"ServerVersion": "4.8.10.0",
|
"ServerVersion": "4.8.10.0",
|
||||||
"ProductName": "Emby Server",
|
"ProductName": "Emby Server",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
func signedTestToken(t *testing.T, secret string) string {
|
func signedTestToken(t *testing.T, secret string) string {
|
||||||
@@ -23,7 +23,7 @@ func signedTestTokenWithPurpose(t *testing.T, secret, purpose string) string {
|
|||||||
Purpose: purpose,
|
Purpose: purpose,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||||
Issuer: "mediastationgo-test",
|
Issuer: "mmtl-test",
|
||||||
Subject: "user-1",
|
Subject: "user-1",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ─── Users / Auth ────────────────────────────────────────────────────────────
|
// ─── Users / Auth ────────────────────────────────────────────────────────────
|
||||||
@@ -67,7 +67,7 @@ func embyAuthByNameHandler(svc *service.Container) gin.HandlerFunc {
|
|||||||
embyRememberCompatSession(c, accessToken)
|
embyRememberCompatSession(c, accessToken)
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"AccessToken": accessToken,
|
"AccessToken": accessToken,
|
||||||
"ServerId": "mediastation-go-001",
|
"ServerId": "mmtl-001",
|
||||||
"User": userPayload,
|
"User": userPayload,
|
||||||
"SessionInfo": gin.H{
|
"SessionInfo": gin.H{
|
||||||
"Id": resp.User.ID,
|
"Id": resp.User.ID,
|
||||||
@@ -150,12 +150,12 @@ func embyGetUserByIDHandler(svc *service.Container) gin.HandlerFunc {
|
|||||||
|
|
||||||
func embyFallbackUser(id string) gin.H {
|
func embyFallbackUser(id string) gin.H {
|
||||||
if strings.TrimSpace(id) == "" {
|
if strings.TrimSpace(id) == "" {
|
||||||
id = "mediastation-user"
|
id = "mmtl-user"
|
||||||
}
|
}
|
||||||
return gin.H{
|
return gin.H{
|
||||||
"Id": id,
|
"Id": id,
|
||||||
"Name": "MediaStationGo",
|
"Name": "MMTL",
|
||||||
"ServerId": "mediastation-go-001",
|
"ServerId": "mmtl-001",
|
||||||
"HasPassword": true,
|
"HasPassword": true,
|
||||||
"HasConfiguredPassword": true,
|
"HasConfiguredPassword": true,
|
||||||
"HasConfiguredEasyPassword": false,
|
"HasConfiguredEasyPassword": false,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func embyViewsHandler(svc *service.Container) gin.HandlerFunc {
|
func embyViewsHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fileFolderReq struct {
|
type fileFolderReq struct {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register attaches every API route to the engine.
|
// Register attaches every API route to the engine.
|
||||||
@@ -46,7 +46,7 @@ func healthCheck(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func versionInfo(c *gin.Context) {
|
func versionInfo(c *gin.Context) {
|
||||||
c.JSON(200, gin.H{"name": "MediaStationGo", "version": "0.1.0"})
|
c.JSON(200, gin.H{"name": "MMTL", "version": "0.1.0"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 权限 Handler 包装 ────────────────────────────────────────────────────────
|
// ─── 权限 Handler 包装 ────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type manualScrapeApplyReq struct {
|
type manualScrapeApplyReq struct {
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type createLibraryReq struct {
|
type createLibraryReq struct {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func recentMediaHandler(svc *service.Container) gin.HandlerFunc {
|
func recentMediaHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// addMediaFavoriteHandler ensures the (user, media) row exists. If it
|
// addMediaFavoriteHandler ensures the (user, media) row exists. If it
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func listLibraryRootsHandler(svc *service.Container) gin.HandlerFunc {
|
func listLibraryRootsHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func scanLibraryHandler(svc *service.Container) gin.HandlerFunc {
|
func scanLibraryHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
"github.com/ShukeBta/MMTL/internal/repository"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestQueueLibraryRootScanImportsNewLibraryMedia(t *testing.T) {
|
func TestQueueLibraryRootScanImportsNewLibraryMedia(t *testing.T) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
var metadataNoiseRE = regexp.MustCompile(`(?i)(自动订阅|订阅|全集|合集|complete|batch|season\s*\d+|s\d{1,2}|s\d{1,2}e\d{1,3}|第\s*\d+\s*季|第\s*\d+\s*[集话話期]|2160p|1080p|720p|4k|uhd|bluray|blu-ray|web-?dl|hdtv|remux|x26[45]|h\.?26[45]|hevc|avc|hdr10?\+?|dovi|dv|atmos|aac|ddp?5\.1|truehd|flac)`)
|
var metadataNoiseRE = regexp.MustCompile(`(?i)(自动订阅|订阅|全集|合集|complete|batch|season\s*\d+|s\d{1,2}|s\d{1,2}e\d{1,3}|第\s*\d+\s*季|第\s*\d+\s*[集话話期]|2160p|1080p|720p|4k|uhd|bluray|blu-ray|web-?dl|hdtv|remux|x26[45]|h\.?26[45]|hevc|avc|hdr10?\+?|dovi|dv|atmos|aac|ddp?5\.1|truehd|flac)`)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func exportNFOHandler(svc *service.Container) gin.HandlerFunc {
|
func exportNFOHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// organizeReq carries optional per-request overrides. 留空则沿用系统设置。
|
// organizeReq carries optional per-request overrides. 留空则沿用系统设置。
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PermissionHandler 权限相关 HTTP 处理。
|
// PermissionHandler 权限相关 HTTP 处理。
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func requirePermission(svc *service.Container, key string) gin.HandlerFunc {
|
func requirePermission(svc *service.Container, key string) gin.HandlerFunc {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type verifyPlayProfilePINReq struct {
|
type verifyPlayProfilePINReq struct {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ─── History ────────────────────────────────────────────────────────────────
|
// ─── History ────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// playbackInfoHandler returns the media row + a `stream_url` the React
|
// playbackInfoHandler returns the media row + a `stream_url` the React
|
||||||
@@ -141,7 +141,7 @@ func externalPlaybackURL(c *gin.Context, svc *service.Container, path string) st
|
|||||||
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
headerOrigin := sanitizedPublicOrigin(c.GetHeader("X-MediaStation-Public-Origin"))
|
headerOrigin := sanitizedPublicOrigin(c.GetHeader("X-MMTL-Public-Origin"))
|
||||||
if headerOrigin != "" && !isLocalPublicOrigin(headerOrigin) {
|
if headerOrigin != "" && !isLocalPublicOrigin(headerOrigin) {
|
||||||
return joinOriginPath(headerOrigin, path)
|
return joinOriginPath(headerOrigin, path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
"github.com/ShukeBta/MMTL/internal/config"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
"github.com/ShukeBta/MMTL/internal/repository"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestExternalURLUsesMediaScopedPlaybackToken(t *testing.T) {
|
func TestExternalURLUsesMediaScopedPlaybackToken(t *testing.T) {
|
||||||
@@ -109,7 +109,7 @@ func TestExternalURLPrefersBrowserPublicOriginOverForwardedSource(t *testing.T)
|
|||||||
req.Header.Set("Authorization", "Bearer "+loginToken)
|
req.Header.Set("Authorization", "Bearer "+loginToken)
|
||||||
req.Header.Set("X-Forwarded-Proto", "https")
|
req.Header.Set("X-Forwarded-Proto", "https")
|
||||||
req.Header.Set("X-Forwarded-Host", "media.v6.agonyz.dpdns.org")
|
req.Header.Set("X-Forwarded-Host", "media.v6.agonyz.dpdns.org")
|
||||||
req.Header.Set("X-MediaStation-Public-Origin", "https://media.agonyz.dpdns.org")
|
req.Header.Set("X-MMTL-Public-Origin", "https://media.agonyz.dpdns.org")
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
router.ServeHTTP(w, req)
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ func TestExternalPlayersSanitizeBrowserPublicOrigin(t *testing.T) {
|
|||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "http://origin.internal/api/playback/media-1/external-players", nil)
|
req := httptest.NewRequest(http.MethodGet, "http://origin.internal/api/playback/media-1/external-players", nil)
|
||||||
req.Header.Set("Authorization", "Bearer "+loginToken)
|
req.Header.Set("Authorization", "Bearer "+loginToken)
|
||||||
req.Header.Set("X-MediaStation-Public-Origin", "https://user:pass@media.agonyz.dpdns.org/sneaky/path?x=1#frag")
|
req.Header.Set("X-MMTL-Public-Origin", "https://user:pass@media.agonyz.dpdns.org/sneaky/path?x=1#frag")
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
router.ServeHTTP(w, req)
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
@@ -184,7 +184,7 @@ func TestExternalURLFallsBackToConfiguredPublicServerURL(t *testing.T) {
|
|||||||
req.Header.Set("Authorization", "Bearer "+loginToken)
|
req.Header.Set("Authorization", "Bearer "+loginToken)
|
||||||
req.Header.Set("X-Forwarded-Proto", "https")
|
req.Header.Set("X-Forwarded-Proto", "https")
|
||||||
req.Header.Set("X-Forwarded-Host", "source.example.test")
|
req.Header.Set("X-Forwarded-Host", "source.example.test")
|
||||||
req.Header.Set("X-MediaStation-Public-Origin", "javascript:alert(1)")
|
req.Header.Set("X-MMTL-Public-Origin", "javascript:alert(1)")
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
router.ServeHTTP(w, req)
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ func TestExternalURLPrefersConfiguredPublicServerURLOverLocalBrowserOrigin(t *te
|
|||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:8080/api/playback/media-1/external-url", nil)
|
req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:8080/api/playback/media-1/external-url", nil)
|
||||||
req.Header.Set("Authorization", "Bearer "+loginToken)
|
req.Header.Set("Authorization", "Bearer "+loginToken)
|
||||||
req.Header.Set("X-MediaStation-Public-Origin", "http://127.0.0.1:8080")
|
req.Header.Set("X-MMTL-Public-Origin", "http://127.0.0.1:8080")
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
router.ServeHTTP(w, req)
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func enforceScopedPlaybackToken(c *gin.Context, mediaID string) bool {
|
func enforceScopedPlaybackToken(c *gin.Context, mediaID string) bool {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type reorderReq struct {
|
type reorderReq struct {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/middleware"
|
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func updateProfileHandler(svc *service.Container) gin.HandlerFunc {
|
func updateProfileHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
"github.com/ShukeBta/MMTL/internal/repository"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProfileHideAdultRequiresPasswordOnlyWhenChanged(t *testing.T) {
|
func TestProfileHideAdultRequiresPasswordOnlyWhenChanged(t *testing.T) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
const hideCommunityLinksSettingKey = "ui.hide_community_links_for_users"
|
const hideCommunityLinksSettingKey = "ui.hide_community_links_for_users"
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
"github.com/ShukeBta/MMTL/internal/repository"
|
||||||
"github.com/ShukeBta/MediaStationGo/internal/service"
|
"github.com/ShukeBta/MMTL/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPublicUIConfigExposesCommunityLinkVisibility(t *testing.T) {
|
func TestPublicUIConfigExposesCommunityLinkVisibility(t *testing.T) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user