chore clean repository deployment files

This commit is contained in:
ShukeBta
2026-07-02 01:56:40 +08:00
parent aec1ca3e1b
commit 1b56469beb
21 changed files with 29 additions and 1482 deletions
-43
View File
@@ -1,43 +0,0 @@
# MediaStationGo — environment variables (copy to `.env` if needed).
#
# Every config key is overridable via MEDIASTATION_<UPPER_KEY>=value.
# Below are the values most people change.
MEDIASTATION_APP_PORT=8080
MEDIASTATION_APP_DEBUG=false
MEDIASTATION_APP_DATA_DIR=./data
# Comma-or-array allow-list. Empty = allow * (dev).
# MEDIASTATION_APP_CORS_ORIGINS=https://media.example.com
# Override the seeded admin password (used only when no admin exists).
ADMIN_INITIAL_PASSWORD=admin123
# Strong random JWT secret (auto-generated when empty).
# MEDIASTATION_SECRETS_JWT_SECRET=please-change-me
# Built-in license server bridge; override only when using a private MgoSever.
# MEDIASTATION_LICENSE_SERVER_URL=https://mgosever.3jzs.com
# MEDIASTATION_LICENSE_PUBLIC_KEY=MCowBQYDK2VwAyEABRXnXy+urjrbKit6Yu/HiezWgP0NdsZW3tsegJWRrtI=
# MEDIASTATION_LICENSE_HMAC_SECRET=
# 3rd-party scrape providers.
# MEDIASTATION_SECRETS_TMDB_API_KEY=
# MEDIASTATION_SECRETS_BANGUMI_ACCESS_TOKEN=
# HLS transcoding guardrails. Defaults favor low CPU on NAS devices.
# MEDIASTATION_TRANSCODER_ENABLED=true
# MEDIASTATION_TRANSCODER_HARDWARE_ACCEL=false
# MEDIASTATION_TRANSCODER_REALTIME=true
# MEDIASTATION_TRANSCODER_THREADS=2
# MEDIASTATION_TRANSCODER_MAX_CONCURRENT=1
# MEDIASTATION_TRANSCODER_IDLE_TIMEOUT_SECONDS=120
# Telegram Bot / notify outbound network.
# Leave empty if direct network or v2rayA redirect already covers Docker bridge traffic.
# MEDIASTATION_TELEGRAM_API_BASE_URL=https://api.telegram.org
# MEDIASTATION_TELEGRAM_PROXY_URL=http://host.docker.internal:20171
# HTTP_PROXY=
# HTTPS_PROXY=
# ALL_PROXY=
# NO_PROXY=127.0.0.1,localhost
+4 -2
View File
@@ -54,9 +54,11 @@ jobs:
needs: [backend, frontend]
steps:
- uses: actions/checkout@v4
- name: Validate single-image compose
run: docker compose -f docker-compose.simple.yml config --quiet
- name: Validate tier 1 compose
run: docker compose -f docker-compose.yml config --quiet
- name: Validate tier 2 compose
run: docker compose -f docker-compose.yml -f docker-compose.standard.yml config --quiet
run: docker compose -f docker-compose.standard.yml config --quiet
- name: Validate tier 3 compose
run: docker compose -f docker-compose.yml -f docker-compose.standard.yml -f docker-compose.search.yml config --quiet
run: docker compose -f docker-compose.search.yml config --quiet
-64
View File
@@ -1,64 +0,0 @@
name: Publish release packages
on:
workflow_dispatch:
inputs:
version:
description: 'Release tag to publish, for example MediaStationGo-v0.0.32'
required: true
type: string
ref:
description: 'Git ref to build from'
required: false
default: main
type: string
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
env:
RELEASE_VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
- uses: actions/setup-go@v5
with:
go-version: '1.25'
cache: true
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
cache-dependency-path: web/package-lock.json
- name: Build release packages
run: ./scripts/package-release.sh
env:
VERSION: ${{ env.RELEASE_VERSION }}
- name: Generate checksums
run: |
cd dist-release
sha256sum * > SHA256SUMS.txt
- name: Publish GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
dist-release/*.tar.gz
dist-release/*.zip
dist-release/SHA256SUMS.txt
generate_release_notes: true
tag_name: ${{ env.RELEASE_VERSION }}
target_commitish: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
# GitHub immutable releases reject deleting/replacing existing assets.
# Keep reruns idempotent by skipping same-name assets instead of
# deleting them first.
overwrite_files: false
+6
View File
@@ -62,6 +62,7 @@ config.yaml
.tmp_*
# Runtime / local-only artifacts (清理补充)
.tmp/
.tmp-live-backups/
.tmp-*
.codex-*
@@ -70,6 +71,11 @@ media/
*.pid
# 本地开发运行产物
.agents/
.claude/
.dev-cache/
.dev-data/
.dev-logs/
tools/
verify-cache/
verify-data/
-7
View File
@@ -1,7 +0,0 @@
ShukeBta <272197458+ShukeBta@users.noreply.github.com> Shuke <272197458+ShukeBta@users.noreply.github.com>
ShukeBta <272197458+ShukeBta@users.noreply.github.com> Kiro Agent <244629292+kiro-agent@users.noreply.github.com>
ShukeBta <272197458+ShukeBta@users.noreply.github.com> Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ShukeBta <272197458+ShukeBta@users.noreply.github.com> shuk shuk <luochen7755@gmail.com>
ShukeBta <272197458+ShukeBta@users.noreply.github.com> soldosluka857 <soldosluka857@outlook.com>
ShukeBta <272197458+ShukeBta@users.noreply.github.com> <shuke@example.com>
ShukeBta <272197458+ShukeBta@users.noreply.github.com> <shukebeta@users.noreply.github.com>
-71
View File
@@ -1,71 +0,0 @@
.PHONY: all build build-server build-protected build-protected-garble build-web dev dev-web run deploy docker docker-update docker-stop docker-push clean install-web tidy test vet smoke
# ---- 默认目标 ----
all: build
# ---- 全量构建(前端 + 后端) ----
build: build-web build-server
# ---- 后端构建 ----
# 使用纯 Go SQLite,可以 CGO_ENABLED=0 直接交叉编译。
build-server:
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o bin/mediastation-go ./cmd/server
build-protected:
CGO_ENABLED=0 go build -trimpath -buildvcs=false -ldflags="-s -w -buildid=" -o bin/mediastation-go-protected ./cmd/server
build-protected-garble:
CGO_ENABLED=0 garble -literals -tiny build -trimpath -ldflags="-s -w -buildid=" -o bin/mediastation-go-protected ./cmd/server
# ---- 前端构建 ----
build-web:
cd web && npm install --silent && npm run build
# ---- 开发模式 ----
dev:
MEDIASTATION_APP_DEBUG=true go run ./cmd/server
dev-web:
cd web && npm run dev
# ---- 一键运行 ----
run: build
./bin/mediastation-go
deploy:
./scripts/deploy.sh
# ---- Docker ----
docker:
docker compose up -d
docker-update:
./scripts/docker-compose-update.sh
docker-stop:
docker compose down
docker-push:
./scripts/docker-build-push.sh
# ---- 工具命令 ----
install-web:
cd web && npm install
tidy:
go mod tidy
test:
go test ./...
vet:
go vet ./...
# ---- 端到端冒烟测试 ----
# Spins up the binary against a temp dir, exercises every REST surface,
# tears it back down. Fails the build on any regression.
smoke: build
./scripts/smoke-test.sh
clean:
rm -rf bin/ web/dist/ web/node_modules/
+2
View File
@@ -223,6 +223,8 @@ docker-compose.standard.yml 第二档:MediaStationGo + PostgreSQL + Redis
docker-compose.search.yml 第三档:MediaStationGo + PostgreSQL + Redis + OpenSearch
```
仓库只保留面向用户部署和项目维护的必要文件。旧的本地部署脚本、发包脚本、开发机辅助脚本、`.env` 示例和旧高级 Compose 模板已经移除;Linux / Docker 用户按上面四个 Compose 文件部署即可。开发者本地生成的 `bin/`、`data/`、`cache/`、`logs/`、`.tmp/`、`tools/` 等目录已列入 `.gitignore`,不应提交到仓库。
如果直接下载为 `docker-compose.yml`,启动命令统一是:
```bash
+9 -4
View File
@@ -79,12 +79,15 @@ It helps you:
## Quick Start
Docker Compose is the recommended path. Beginners do not need `.env`, bare-metal binaries, or source builds.
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
mkdir -p MediaStationGo
cd MediaStationGo
curl -fsSL https://raw.githubusercontent.com/ShukeBta/MediaStationGo/main/docker-compose.yml -o docker-compose.yml
# Simplest option: one MediaStationGo container + SQLite
curl -fsSL https://raw.githubusercontent.com/ShukeBta/MediaStationGo/main/docker-compose.simple.yml -o docker-compose.yml
# Or tier 1: MediaStationGo + PostgreSQL
# curl -fsSL https://raw.githubusercontent.com/ShukeBta/MediaStationGo/main/docker-compose.yml -o docker-compose.yml
```
Edit `docker-compose.yml`:
@@ -126,8 +129,10 @@ If you already have an older `./data/mediastation.db`, the first start with the
| --- | --- | --- |
| Single image: SQLite | `docker compose -f docker-compose.simple.yml up -d` | Beginners and single-user setups that want one image only, no PostgreSQL/Redis |
| Lightweight: PG only | `docker compose up -d` | Most NAS devices, lowest resource use |
| Standard: PG + Redis | `docker compose -f docker-compose.yml -f docker-compose.standard.yml up -d` | Multi-user use and frequent Emby client refreshes |
| Search enhanced: PG + Redis + OpenSearch | `docker compose -f docker-compose.yml -f docker-compose.standard.yml -f docker-compose.search.yml up -d` | Huge libraries and future standalone search indexing |
| Standard: PG + Redis | `docker compose -f docker-compose.standard.yml up -d` | Multi-user use and frequent Emby client refreshes |
| Search enhanced: PG + Redis + OpenSearch | `docker compose -f docker-compose.search.yml up -d` | Huge libraries and future standalone search indexing |
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.
-144
View File
@@ -1,144 +0,0 @@
# ============================================================================
# MediaStationGo configuration template.
#
# Loading priority (low -> high):
# 1. Built-in defaults
# 2. config.yaml (this file, copied to the working directory)
# 3. config/*.yaml shard files (per-module)
# 4. Environment variables prefixed with MEDIASTATION_, e.g.
# MEDIASTATION_APP_PORT=8080
# MEDIASTATION_SECRETS_JWT_SECRET=...
#
# Copy this file to `config.yaml` and edit the values you actually need.
# ============================================================================
app:
port: 8080
debug: false
env: production
data_dir: ./data
web_dir: ./web/dist
ffmpeg_path: ffmpeg
ffprobe_path: ffprobe
max_cpu_threads: 2 # hard cap for Go CPU threads; raise only on powerful hosts
vaapi_device: /dev/dri/renderD128
cors_origins: [] # leave empty to allow * in dev; set strict allow-list in prod
server_url: "" # public URL of the server (used for DLNA / casting)
database:
# auto: Docker Compose 注入 PostgreSQL DSN 时用 PG;没有 DSN 时回退 SQLite。
# postgres: 强制使用 dsn;sqlite: 强制使用 db_path。
type: auto
dsn: "" # e.g. postgres://mediastation:mediastation@postgres:5432/mediastation?sslmode=disable
db_path: ./data/mediastation.db
wal_mode: true
busy_timeout: 5000
cache_size: -20000 # KB (negative)
max_open_conns: 4 # small WAL pool; keeps login/playback responsive during scans
max_idle_conns: 2
secrets:
# When empty, a 32-byte random secret is generated on first start and
# persisted to {data_dir}/.jwt_secret so existing sessions survive restarts.
jwt_secret: ""
tmdb_api_key: ""
tmdb_api_proxy: "" # e.g. https://api.tmdb-mirror.example
tmdb_image_proxy: "" # e.g. https://image.tmdb-mirror.example/t/p
bangumi_access_token: ""
thetvdb_api_key: ""
fanart_tv_api_key: ""
logging:
level: warn # warn by default; set info/debug temporarily for detailed diagnosis
format: console # console | json
output_path: "" # empty = {data_dir}/logs/app.log + warn.log + error.log; stdout/stderr disables file logs
enable_rotation: true
max_size_mb: 20
max_age_days: 30
max_backups: 10
cache:
cache_dir: ./cache
redis_url: "" # e.g. redis://redis:6379/0;为空则只使用进程内/磁盘缓存
redis_prefix: mediastationgo
media_ttl_seconds: 15
max_disk_usage_mb: 0 # 0 = unlimited
ttl_hours: 0
auto_cleanup: false
cleanup_interval_min: 60
search:
backend: "" # "" = DB/SQLite FTS;opensearch = OpenSearch 加速搜索
opensearch_url: "" # e.g. http://opensearch:9200
index: mediastation_media
username: ""
password: ""
media:
movies_dir: ""
tv_dir: ""
anime_dir: ""
transcoder:
# Hardware encoder for HLS transcodes.
# "" software libx264 (default; runs anywhere)
# nvenc h264_nvenc (NVIDIA GPU, requires --gpus all)
# qsv h264_qsv (Intel iGPU, requires /dev/dri)
# vaapi h264_vaapi (Intel/AMD VAAPI, requires /dev/dri)
enabled: true # global HLS transcode switch; false = direct play only and no ffmpeg transcode jobs
hardware_accel: false # must be true before encoder below is used
encoder: ""
preset: veryfast
video_bitrate: 1500k
max_rate: 1800k
buf_size: 3000k
max_height: 720
segment_seconds: 4
realtime: true # throttle ffmpeg input to playback speed instead of transcoding the whole file at full speed
threads: 2 # software libx264 thread cap; set 0 to let ffmpeg decide
max_concurrent: 1 # maximum simultaneous HLS transcodes
idle_timeout_seconds: 120 # stop ffmpeg when the player stops requesting HLS segments
ai:
enabled: false
provider: openai # openai | deepseek | qwen | ollama
api_base: https://api.openai.com/v1
api_key: ""
model: gpt-4o-mini
timeout: 30
max_concurrent: 3
license:
# Built-in MediaStationGo license server bridge.
# Open-source mode works without this and is limited to 20 users.
server_url: "https://mgosever.3jzs.com"
public_key: "MCowBQYDK2VwAyEABRXnXy+urjrbKit6Yu/HiezWgP0NdsZW3tsegJWRrtI=" # Ed25519 public key for protected license responses
hmac_secret: "" # legacy fallback only; prefer Ed25519 public_key
# FlareSolverr 配置(用于绕过 Cloudflare/WAF 保护)
flaresolverr:
enabled: false
url: "http://localhost:8191" # FlareSolverr 服务地址
session: "mediastation" # 会话名称(可选,保持 cookie 持久化)
timeout: 60 # 请求超时(秒)
# 整理配置(智能分类)
organizer:
smart_classify: false # 是否启用智能分类整理
auto_after_download: false # 下载完成后是否自动整理;关闭时可在「整理与维护」手动整理
categories:
chinese_movie: "华语电影"
animation_movie: "动画电影"
foreign_movie: "外语电影"
euus_movie: "欧美电影"
jk_movie: "日韩电影"
domestic_tv: "国产剧"
euus_tv: "欧美剧"
jk_tv: "日韩剧"
jp_anime: "日番"
cn_anime: "国漫"
euus_anime: "欧美动漫"
variety: "综艺"
documentary: "纪录片"
children: "儿童"
uncategorized_tv: "未分类"
-220
View File
@@ -1,220 +0,0 @@
# MediaStationGo 默认 Docker Compose 部署文件。
#
# 快速开始:
# 1. 按需修改下方 volumes 的宿主机路径。
# 2. docker compose pull mediastation-go
# 3. docker compose up -d
# 4. 浏览器打开 http://<服务器IP>:18080
#
# 推荐更新命令(会清理本项目旧镜像,避免 NAS 磁盘堆满):
# curl -fsSL https://cdn.jsdelivr.net/gh/ShukeBta/MediaStationGo@main/scripts/docker-compose-update.sh -o docker-compose-update.sh
# chmod +x docker-compose-update.sh
# ./docker-compose-update.sh
#
# 默认账号:
# admin / admin123
# 首次部署后请立即到「个人资料/用户管理」修改密码。
#
# 镜像版本:
# 默认拉取 latest;如需固定版本,创建 .env 并写入:
# MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.32
#
# 路径映射总览:
# /data 程序运行数据目录。保存 JWT secret、系统配置、旧 SQLite 迁移源,必须持久化。
# /postgres PostgreSQL 主数据库目录,账号、媒体库、订阅等主数据都在这里,必须备份。
# /cache 缓存目录。保存海报、刮削图片、转码缓存等,建议放在空间较大的磁盘。
# /media 媒体库只读挂载目录。网页中添加媒体库时填写容器内路径,例如 /media/Movies。
# /downloads 下载目录。下载器保存路径建议填写容器内路径,例如 /downloads/Movies。
#
# NAS / 飞牛推荐直读模式:
# 如果你希望网页里直接填写 NAS 原始路径,例如
# /your-nas/media/电视剧/国产剧,
# 请在 .env 中把宿主机路径和容器路径设置成完全相同(请替换为你自己的实际路径):
#
# MEDIASTATION_MEDIA_DIR=/your-nas/media
# MEDIASTATION_MEDIA_CONTAINER_DIR=/your-nas/media
# MEDIASTATION_DOWNLOAD_DIR=/your-nas/downloads
# MEDIASTATION_DOWNLOAD_CONTAINER_DIR=/your-nas/downloads
#
# 这只是 Docker bind mount,不会复制文件,不会占用双倍空间。
#
# 本地测试模式:
# MEDIASTATION_DATA_DIR=./data
# MEDIASTATION_CACHE_DIR=./cache
# MEDIASTATION_MEDIA_DIR=./media
# MEDIASTATION_DOWNLOAD_DIR=./downloads
#
# NAS / 飞牛路径必须使用绝对路径(请替换为你自己的实际路径),例如:
# MEDIASTATION_MEDIA_DIR=/your-nas/media
# MEDIASTATION_DOWNLOAD_DIR=/your-nas/downloads
# 不要写成相对路径(如 ./media)当作 NAS 绝对路径;相对路径会被 Docker Compose 解析为当前部署目录下的子目录。
#
# 注意:
# 1. 如果 qBittorrent/Transmission/Aria2 不在本 compose 内,请确保它们能访问同一份
# 下载目录;容器内保存路径和下载器实际保存路径需要保持一致或可被媒体库扫描到。
# 2. 媒体库默认以只读方式挂载,避免误删原始媒体;如果需要整理/移动文件,可将
# /media 的 :ro 改为 :rw,或把整理目标放到 /downloads 后再手动迁移。
# 3. PUID/PGID 用于匹配 NAS/Linux 宿主机用户权限,避免下载或缓存文件权限异常。
services:
mediastation-go:
image: ghcr.io/shukebta/mediastation-go:${MEDIASTATION_IMAGE_TAG:-latest}
# 默认只在本地没有镜像时拉取,避免每次重启都访问 GHCR。
# 需要升级时建议执行 ./docker-compose-update.sh,更新后会清理本项目旧镜像。
pull_policy: missing
container_name: mediastation-go
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
ports:
# 宿主机端口:容器端口。默认访问 http://<服务器IP>:18080
- "${MEDIASTATION_HTTP_PORT:-18080}:8080"
extra_hosts:
# Linux / NAS Docker 中访问宿主机服务的固定别名。
# qBittorrent 如果运行在 NAS 宿主机上,下载器地址建议填:
# http://host.docker.internal:8085
- "host.docker.internal:host-gateway"
volumes:
# 程序运行数据:JWT secret、运行配置、旧 SQLite 迁移源。
# 主数据库在 postgres 服务的 /var/lib/postgresql/data。
- ${MEDIASTATION_DATA_DIR:-./data}:/data
# 海报/背景图/转码缓存。可删除重建,但会重新下载图片和生成缓存。
- ${MEDIASTATION_CACHE_DIR:-./cache}:/cache
# 媒体库根目录。
# 默认:宿主机目录映射到容器 /media,添加媒体库时填写 /media/电影、/media/电视剧。
# NAS 直读:把 MEDIASTATION_MEDIA_CONTAINER_DIR 设置成与 MEDIASTATION_MEDIA_DIR 相同,
# 添加媒体库时即可直接填写 /your-nas/media/电影、/your-nas/media/电视剧。
# 如宿主机目录不同,请在 .env 中设置 MEDIASTATION_MEDIA_DIR。
# NAS / 飞牛等系统请写绝对路径(替换为你自己的实际路径),例如:
# MEDIASTATION_MEDIA_DIR=/your-nas/media
# 不要写相对路径,否则会变成当前部署目录下的子目录。
# 默认映射到容器 /media;如需在网页中直接填写宿主机绝对路径,可将
# MEDIASTATION_MEDIA_CONTAINER_DIR 设置为同一个绝对路径。
- ${MEDIASTATION_MEDIA_DIR:-./media}:${MEDIASTATION_MEDIA_CONTAINER_DIR:-/media}:ro
# 下载保存目录。
# 默认:宿主机目录映射到容器 /downloads,订阅保存根目录填写 /downloads。
# NAS 直读:把 MEDIASTATION_DOWNLOAD_CONTAINER_DIR 设置成与 MEDIASTATION_DOWNLOAD_DIR 相同,
# 下载器保存根目录可直接填写 /your-nas/downloads。
# 如果外部下载器也运行在 Docker 中,请给下载器挂载同一个宿主机目录。
# NAS / 飞牛等系统请写绝对路径(替换为你自己的实际路径),例如:
# MEDIASTATION_DOWNLOAD_DIR=/your-nas/downloads
# 不要写相对路径,否则下载目录会被映射到当前部署目录下面。
# 默认映射到容器 /downloads;如需下载器和应用都使用宿主机绝对路径,可将
# MEDIASTATION_DOWNLOAD_CONTAINER_DIR 设置为同一个绝对路径。
- ${MEDIASTATION_DOWNLOAD_DIR:-./downloads}:${MEDIASTATION_DOWNLOAD_CONTAINER_DIR:-/downloads}
environment:
# Web 服务监听配置。容器内固定监听 8080,对外端口由上方 ports 控制。
MEDIASTATION_APP_HOST: 0.0.0.0
MEDIASTATION_APP_PORT: 8080
MEDIASTATION_APP_WEB_DIR: /app/web/dist
# 数据与缓存目录。需与 volumes 中的容器路径一致。
MEDIASTATION_APP_DATA_DIR: /data
MEDIASTATION_DATABASE_TYPE: ${MEDIASTATION_DATABASE_TYPE:-postgres}
MEDIASTATION_DATABASE_DSN: ${MEDIASTATION_DATABASE_DSN:-postgres://mediastation:mediastation@postgres:5432/mediastation?sslmode=disable}
# SQLite 旧库迁移源。确认迁移完成后,可改成 /data/no-sqlite-migration.db。
MEDIASTATION_DATABASE_DB_PATH: /data/mediastation.db
MEDIASTATION_CACHE_CACHE_DIR: /cache
# 宿主机到容器的路径映射提示。用于用户误填宿主机路径时自动转换为容器路径:
# /your-nas/media/电视剧 -> /media/电视剧
# /your-nas/downloads/国产剧 -> /downloads/国产剧
MEDIASTATION_MEDIA_DIR: ${MEDIASTATION_MEDIA_DIR:-./media}
MEDIASTATION_MEDIA_CONTAINER_DIR: ${MEDIASTATION_MEDIA_CONTAINER_DIR:-/media}
MEDIASTATION_DOWNLOAD_DIR: ${MEDIASTATION_DOWNLOAD_DIR:-./downloads}
MEDIASTATION_DOWNLOAD_CONTAINER_DIR: ${MEDIASTATION_DOWNLOAD_CONTAINER_DIR:-/downloads}
# 日志级别:debug / info / warn / error。
MEDIASTATION_LOGGING_LEVEL: ${MEDIASTATION_LOGGING_LEVEL:-warn}
MEDIASTATION_LOGGING_OUTPUT_PATH: ${MEDIASTATION_LOGGING_OUTPUT_PATH:-/data/logs}
MEDIASTATION_LOGGING_ENABLE_ROTATION: ${MEDIASTATION_LOGGING_ENABLE_ROTATION:-true}
MEDIASTATION_LOGGING_MAX_SIZE_MB: ${MEDIASTATION_LOGGING_MAX_SIZE_MB:-20}
MEDIASTATION_LOGGING_MAX_AGE_DAYS: ${MEDIASTATION_LOGGING_MAX_AGE_DAYS:-30}
MEDIASTATION_LOGGING_MAX_BACKUPS: ${MEDIASTATION_LOGGING_MAX_BACKUPS:-10}
# 转码配置。留空表示自动/软件转码;硬件加速见下方 Intel/NVIDIA 示例。
MEDIASTATION_TRANSCODER_ENABLED: ${MEDIASTATION_TRANSCODER_ENABLED:-true}
MEDIASTATION_TRANSCODER_HARDWARE_ACCEL: ${MEDIASTATION_TRANSCODER_HARDWARE_ACCEL:-false}
MEDIASTATION_TRANSCODER_ENCODER: ${MEDIASTATION_TRANSCODER_ENCODER:-}
MEDIASTATION_TRANSCODER_MAX_HEIGHT: ${MEDIASTATION_TRANSCODER_MAX_HEIGHT:-1080}
# NAS 低负载保护:HLS 按播放速度转码、限制软件编码线程、限制并发、空闲自动停止。
MEDIASTATION_TRANSCODER_REALTIME: ${MEDIASTATION_TRANSCODER_REALTIME:-true}
MEDIASTATION_TRANSCODER_THREADS: ${MEDIASTATION_TRANSCODER_THREADS:-2}
MEDIASTATION_TRANSCODER_MAX_CONCURRENT: ${MEDIASTATION_TRANSCODER_MAX_CONCURRENT:-1}
MEDIASTATION_TRANSCODER_IDLE_TIMEOUT_SECONDS: ${MEDIASTATION_TRANSCODER_IDLE_TIMEOUT_SECONDS:-120}
# 跨域来源。通常无需设置;反向代理或三端客户端异常时再按需填写。
MEDIASTATION_APP_CORS_ORIGINS: ${MEDIASTATION_APP_CORS_ORIGINS:-}
# Telegram 通知/Bot 出站网络。通常留空即可;如 NAS 访问 Telegram 超时,
# 可在 .env 中填写反代 API 或代理地址。v2rayA 常见代理:
# HTTP http://host.docker.internal:20171 或 http://172.17.0.1:20171
# SOCKS socks5://host.docker.internal:20170 或 socks5://172.17.0.1:20170
# MEDIASTATION_TELEGRAM_API_BASE_URL=https://api.telegram.org
# MEDIASTATION_TELEGRAM_PROXY_URL=http://host.docker.internal:20171
MEDIASTATION_TELEGRAM_API_BASE_URL: ${MEDIASTATION_TELEGRAM_API_BASE_URL:-}
MEDIASTATION_TELEGRAM_PROXY_URL: ${MEDIASTATION_TELEGRAM_PROXY_URL:-}
# 可选:把宿主机 shell/.env 中的代理变量透传给容器。留空无效果。
# 如果 NAS 已使用 v2rayA redirect/透明代理且 Docker 网桥流量已被接管,
# 通常不需要填写这些变量。
HTTP_PROXY: ${HTTP_PROXY:-}
HTTPS_PROXY: ${HTTPS_PROXY:-}
ALL_PROXY: ${ALL_PROXY:-}
NO_PROXY: ${NO_PROXY:-127.0.0.1,localhost}
# 宿主机文件权限映射。Linux/NAS 常用 1000:1000,可用 id 命令查看。
PUID: ${PUID:-1000}
PGID: ${PGID:-1000}
TZ: ${TZ:-Asia/Shanghai}
# Intel QSV / VAAPI 硬件加速示例:
# 1. Linux/NAS 宿主机存在 /dev/dri。
# 2. 在 .env 中设置 MEDIASTATION_TRANSCODER_HARDWARE_ACCEL=true
# 和 MEDIASTATION_TRANSCODER_ENCODER=vaapi。
# 3. 取消下方 devices/group_add 注释。
# devices:
# - /dev/dri:/dev/dri
# group_add:
# - "${RENDER_GID:-989}"
# NVIDIA NVENC 硬件加速示例:
# 1. 宿主机安装 NVIDIA Container Toolkit。
# 2. 在 .env 中设置 MEDIASTATION_TRANSCODER_HARDWARE_ACCEL=true
# 和 MEDIASTATION_TRANSCODER_ENCODER=nvenc。
# 3. 取消下方 gpus 注释。
# gpus: all
healthcheck:
test: ["CMD-SHELL", "busybox wget -qO- http://127.0.0.1:8080/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
postgres:
image: postgres:16-alpine
# 数据库基础镜像不参与日常应用更新,避免误拉新版 PostgreSQL。
# 如需升级 PostgreSQL,请先备份 ./postgres 后再手动调整镜像版本并拉取。
pull_policy: never
container_name: mediastation-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${MEDIASTATION_POSTGRES_DB:-mediastation}
POSTGRES_USER: ${MEDIASTATION_POSTGRES_USER:-mediastation}
POSTGRES_PASSWORD: ${MEDIASTATION_POSTGRES_PASSWORD:-mediastation}
TZ: ${TZ:-Asia/Shanghai}
volumes:
- ${MEDIASTATION_POSTGRES_DIR:-./postgres}:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${MEDIASTATION_POSTGRES_USER:-mediastation} -d ${MEDIASTATION_POSTGRES_DB:-mediastation}"]
interval: 10s
timeout: 5s
retries: 10
+8 -24
View File
@@ -39,7 +39,7 @@
- **后端**: Python 3.11+ / FastAPI / SQLAlchemy (async) / APScheduler
- **前端**: Vue 3 + Pinia + Vue Router + TypeScript
- **数据库**: SQLite(默认)/ PostgreSQL(可选)
- **部署**: Docker / Nginx 反向代理 / Helm Chart
- **部署**: Docker Compose / Nginx 反向代理
### 核心定位
MediaStation 是一个轻量级家庭媒体服务器,融合 **媒体播放 + 自动化订阅下载 + 多平台资源聚合**。
@@ -830,31 +830,15 @@ MediaStation 是一个轻量级家庭媒体服务器,融合 **媒体播放 +
| `docker/check-image-security.sh` | 镜像安全检查 |
| `docker-compose.example.yml` | 根目录示例 |
### 5.2 Helm Chart
| 文件 | 说明 |
|------|------|
| `deploy/charts/helm/Chart.yaml` | Chart 定义 |
| `deploy/charts/helm/values.yaml` | 默认值 |
| `deploy/charts/helm/templates/deployment.yaml` | Deployment 模板 |
| `deploy/charts/helm/templates/service.yaml` | Service 模板 |
| `deploy/charts/helm/templates/ingress.yaml` | Ingress 模板 |
| `deploy/charts/helm/templates/pvc.yaml` | PVC 模板 |
| `deploy/charts/helm/templates/secret.yaml` | Secret 模板 |
| `deploy/charts/helm/templates/serviceaccount.yaml` | ServiceAccount 模板 |
| `deploy/charts/helm/templates/_helpers.tpl` | Helm 辅助模板 |
### 5.3 其他部署
### 5.2 部署模板
| 文件/目录 | 说明 |
|-----------|------|
| `nginx.example.conf` | Nginx 反向代理配置(含 SSE/视频流/HTTPS) |
| `deploy/scripts/install-linux.sh` | Linux 安装脚本 |
| `deploy/scripts/install-windows.ps1` | Windows 安装脚本 |
| `deploy/templates/unraid/mediastation.xml` | Unraid 模板 |
| `deploy/charts/truenas/media-station.yaml` | TrueNAS Chart |
| `deploy/packages/synology/build-spk.sh` | 群晖 SPK 打包 |
| `deploy/DEPLOYMENT.md` | 部署文档 |
| `docker-compose.simple.yml` | 单镜像 SQLite 部署 |
| `docker-compose.yml` | PostgreSQL 第一档部署 |
| `docker-compose.standard.yml` | PostgreSQL + Redis 第二档部署 |
| `docker-compose.search.yml` | PostgreSQL + Redis + OpenSearch 第三档部署 |
| `README.md` | Docker Compose 部署说明 |
---
@@ -968,7 +952,7 @@ MediaStation 是一个轻量级家庭媒体服务器,融合 **媒体播放 +
| **构建工具** | Vite | Vite |
| **加密** | cryptography (Fernet) | crypto/aes |
| **视频处理** | FFmpeg (subprocess) | FFmpeg (exec) |
| **容器化** | Docker + Helm | Docker + Helm |
| **容器化** | Docker Compose | Docker Compose |
| **反向代理** | Nginx | Nginx |
---
-5
View File
@@ -65,11 +65,6 @@ func AutoInstallFFmpeg(log *zap.Logger, cfg *config.Config) (ffprobePath, ffmpeg
// getDefaultInstallDir 返回默认安装目录
func getDefaultInstallDir() string {
if wd, err := os.Getwd(); err == nil {
if _, err := os.Stat(filepath.Join(wd, "scripts", "install-ffmpeg.ps1")); err == nil {
return filepath.Join(wd, "tools", "ffmpeg")
}
}
exePath, err := os.Executable()
if err != nil {
return "./tools/ffmpeg"
-20
View File
@@ -1,20 +0,0 @@
param(
[string]$Output = "bin/mediastation-go-protected.exe",
[switch]$Garble
)
$ErrorActionPreference = "Stop"
$env:CGO_ENABLED = "0"
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Output) | Out-Null
if ($Garble) {
if (-not (Get-Command garble -ErrorAction SilentlyContinue)) {
throw "garble is not installed. Run: go install mvdan.cc/garble@latest"
}
garble -literals -tiny build -trimpath -ldflags="-s -w -buildid=" -o $Output ./cmd/server
exit $LASTEXITCODE
}
go build -trimpath -buildvcs=false -ldflags="-s -w -buildid=" -o $Output ./cmd/server
exit $LASTEXITCODE
-89
View File
@@ -1,89 +0,0 @@
param(
[int]$Port = 18080,
[string]$HostAddress = "0.0.0.0",
[string]$DataDir = "",
[string]$CacheDir = "",
[switch]$SkipBuild,
[switch]$Foreground
)
$ErrorActionPreference = "Stop"
$Root = Resolve-Path (Join-Path $PSScriptRoot "..")
if (-not $DataDir) { $DataDir = Join-Path $Root "data" }
if (-not $CacheDir) { $CacheDir = Join-Path $Root "cache" }
$PreviewRoot = Split-Path -Parent $DataDir
$DefaultDownloadDir = Join-Path $PreviewRoot "downloads"
$DefaultMediaDir = Join-Path $PreviewRoot "media"
$WebDir = Join-Path $Root "web\dist"
$BinDir = Join-Path $Root "bin"
$Exe = Join-Path $BinDir "mediastation-go.exe"
$PidFile = Join-Path $Root ".mediastation.pid"
$LogDir = Join-Path $Root "logs"
$OutLog = Join-Path $LogDir "mediastation.out.log"
$ErrLog = Join-Path $LogDir "mediastation.err.log"
New-Item -ItemType Directory -Force -Path $DataDir, $CacheDir, $BinDir, $LogDir | Out-Null
if (-not $env:MEDIASTATION_DOWNLOAD_CONTAINER_DIR) {
New-Item -ItemType Directory -Force -Path $DefaultDownloadDir | Out-Null
}
if (-not $env:MEDIASTATION_MEDIA_CONTAINER_DIR) {
New-Item -ItemType Directory -Force -Path $DefaultMediaDir | Out-Null
}
if (-not $SkipBuild) {
Write-Host "[1/3] Building frontend"
Push-Location (Join-Path $Root "web")
npm ci
npm run build
Pop-Location
Write-Host "[2/3] Building server"
Push-Location $Root
$env:CGO_ENABLED = "0"
go build -trimpath -ldflags="-s -w" -o $Exe ./cmd/server
Pop-Location
} else {
Write-Host "[1/3] Build skipped"
}
if (Test-Path $PidFile) {
$oldPid = 0
$pidText = Get-Content $PidFile -Raw
if ([int]::TryParse($pidText.Trim(), [ref]$oldPid)) {
$oldProc = Get-Process -Id $oldPid -ErrorAction SilentlyContinue
if ($oldProc) {
Write-Host "[3/3] Stopping old process $oldPid"
Stop-Process -Id $oldPid -Force
Start-Sleep -Seconds 1
}
}
}
$env:MEDIASTATION_APP_HOST = $HostAddress
$env:MEDIASTATION_APP_PORT = "$Port"
$env:MEDIASTATION_APP_DATA_DIR = $DataDir
$env:MEDIASTATION_APP_WEB_DIR = $WebDir
$env:MEDIASTATION_DATABASE_DB_PATH = (Join-Path $DataDir "mediastation.db")
$env:MEDIASTATION_CACHE_CACHE_DIR = $CacheDir
if (-not $env:MEDIASTATION_DOWNLOAD_CONTAINER_DIR) {
$env:MEDIASTATION_DOWNLOAD_CONTAINER_DIR = $DefaultDownloadDir
}
if (-not $env:MEDIASTATION_MEDIA_CONTAINER_DIR) {
$env:MEDIASTATION_MEDIA_CONTAINER_DIR = $DefaultMediaDir
}
Write-Host "[3/3] Starting MediaStationGo on http://$HostAddress`:$Port"
if ($Foreground) {
& $Exe
} else {
$proc = Start-Process -FilePath $Exe `
-WorkingDirectory $Root `
-WindowStyle Hidden `
-RedirectStandardOutput $OutLog `
-RedirectStandardError $ErrLog `
-PassThru
$proc.Id | Set-Content $PidFile
Start-Sleep -Seconds 2
Invoke-WebRequest -UseBasicParsing -Uri "http://127.0.0.1:$Port/api/health" -TimeoutSec 10 | Out-Null
Write-Host "Started. PID=$($proc.Id), logs=$OutLog / $ErrLog"
}
-78
View File
@@ -1,78 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PORT="${PORT:-18080}"
HOST="${HOST:-0.0.0.0}"
DATA_DIR="${DATA_DIR:-$ROOT_DIR/data}"
CACHE_DIR="${CACHE_DIR:-$ROOT_DIR/cache}"
WEB_DIR="${WEB_DIR:-$ROOT_DIR/web/dist}"
BIN_DIR="${BIN_DIR:-$ROOT_DIR/bin}"
BIN_PATH="${BIN_PATH:-$BIN_DIR/mediastation-go}"
PID_FILE="${PID_FILE:-$ROOT_DIR/.mediastation.pid}"
LOG_FILE="${LOG_FILE:-$ROOT_DIR/logs/mediastation.log}"
SKIP_BUILD="${SKIP_BUILD:-0}"
DETACH="${DETACH:-1}"
usage() {
cat <<EOF
MediaStationGo one-click deploy
Usage:
PORT=18080 DATA_DIR=/opt/mediastation/data ./scripts/deploy.sh
Environment:
PORT HTTP port, default 18080
HOST Listen host, default 0.0.0.0
DATA_DIR Persistent data directory, default ./data
CACHE_DIR Cache directory, default ./cache
WEB_DIR Built frontend directory, default ./web/dist
SKIP_BUILD 1 to skip npm/go build
DETACH 1 to run in background, 0 to run in foreground
EOF
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
mkdir -p "$DATA_DIR" "$CACHE_DIR" "$BIN_DIR" "$(dirname "$LOG_FILE")"
if [[ "$SKIP_BUILD" != "1" ]]; then
echo "[1/3] Building frontend"
(cd "$ROOT_DIR/web" && npm ci && npm run build)
echo "[2/3] Building server"
(cd "$ROOT_DIR" && CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o "$BIN_PATH" ./cmd/server)
else
echo "[1/3] Build skipped"
fi
if [[ -f "$PID_FILE" ]]; then
old_pid="$(cat "$PID_FILE" || true)"
if [[ -n "$old_pid" ]] && kill -0 "$old_pid" 2>/dev/null; then
echo "[3/3] Stopping old process $old_pid"
kill "$old_pid" || true
sleep 1
fi
fi
export MEDIASTATION_APP_HOST="$HOST"
export MEDIASTATION_APP_PORT="$PORT"
export MEDIASTATION_APP_DATA_DIR="$DATA_DIR"
export MEDIASTATION_APP_WEB_DIR="$WEB_DIR"
export MEDIASTATION_DATABASE_DB_PATH="$DATA_DIR/mediastation.db"
export MEDIASTATION_CACHE_CACHE_DIR="$CACHE_DIR"
echo "[3/3] Starting MediaStationGo on http://$HOST:$PORT"
if [[ "$DETACH" == "1" ]]; then
nohup "$BIN_PATH" >>"$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
sleep 2
if command -v curl >/dev/null 2>&1; then
curl -fsS "http://127.0.0.1:$PORT/api/health" >/dev/null
fi
echo "Started. PID=$(cat "$PID_FILE"), log=$LOG_FILE"
else
exec "$BIN_PATH"
fi
-34
View File
@@ -1,34 +0,0 @@
param(
[string]$Image = "ghcr.io/shukebta/mediastation-go",
[string]$Tag = "latest",
[string]$Platforms = "linux/amd64,linux/arm64",
[switch]$Load
)
$ErrorActionPreference = "Stop"
$PSNativeCommandUseErrorActionPreference = $false
$Builder = "mediastation-builder"
docker buildx inspect $Builder *> $null
if ($LASTEXITCODE -ne 0) {
docker buildx create --name $Builder --use | Out-Null
} else {
docker buildx use $Builder | Out-Null
}
$args = @(
"buildx", "build",
"--platform", $Platforms,
"-t", "$Image`:$Tag"
)
if ($Load) {
$args += "--load"
} else {
$args += "--push"
}
$args += "."
Write-Host "Building $Image`:$Tag for $Platforms"
docker @args
-31
View File
@@ -1,31 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
IMAGE="${IMAGE:-ghcr.io/shukebta/mediastation-go}"
TAG="${TAG:-latest}"
PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}"
PUSH="${PUSH:-1}"
BUILDER="${BUILDER:-mediastation-builder}"
if ! docker buildx inspect "$BUILDER" >/dev/null 2>&1; then
docker buildx create --name "$BUILDER" --use >/dev/null
else
docker buildx use "$BUILDER" >/dev/null
fi
args=(
buildx build
--platform "$PLATFORMS"
-t "$IMAGE:$TAG"
)
if [[ "$PUSH" == "1" ]]; then
args+=(--push)
else
args+=(--load)
fi
args+=(.)
echo "Building $IMAGE:$TAG for $PLATFORMS"
docker "${args[@]}"
-47
View File
@@ -1,47 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
IMAGE="${MEDIASTATION_IMAGE:-ghcr.io/shukebta/mediastation-go}"
SERVICE="${MEDIASTATION_SERVICE:-mediastation-go}"
CONTAINER="${MEDIASTATION_CONTAINER:-mediastation-go}"
PRUNE_DANGLING="${PRUNE_DANGLING:-1}"
PRUNE_ALL_UNUSED="${PRUNE_ALL_UNUSED:-0}"
if docker compose version >/dev/null 2>&1; then
COMPOSE=(docker compose)
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE=(docker-compose)
else
echo "docker compose / docker-compose not found" >&2
exit 1
fi
echo "==> Pulling latest $SERVICE image only (PostgreSQL/Redis/OpenSearch are left untouched)"
"${COMPOSE[@]}" pull "$SERVICE"
echo "==> Recreating $SERVICE without dependencies"
"${COMPOSE[@]}" up -d --no-deps "$SERVICE"
running_image_id="$(docker inspect -f '{{.Image}}' "$CONTAINER" 2>/dev/null || true)"
if [[ -z "$running_image_id" ]]; then
echo "WARN: container '$CONTAINER' not found, skip project image cleanup" >&2
else
echo "==> Removing old unused $IMAGE images"
while IFS= read -r image_id; do
[[ -z "$image_id" || "$image_id" == "$running_image_id" ]] && continue
docker rmi "$image_id" >/dev/null 2>&1 || true
done < <(docker image ls "$IMAGE" --format '{{.ID}}' | sort -u)
fi
if [[ "$PRUNE_DANGLING" == "1" ]]; then
echo "==> Pruning dangling image layers"
docker image prune -f >/dev/null
fi
if [[ "$PRUNE_ALL_UNUSED" == "1" ]]; then
echo "==> Pruning all unused images"
docker image prune -a -f >/dev/null
fi
echo "Done. Current image:"
docker image ls "$IMAGE" --format ' {{.Repository}}:{{.Tag}} {{.ID}} {{.Size}}' | head -n 20
-148
View File
@@ -1,148 +0,0 @@
# Windows 自动安装 ffmpeg/ffprobe 脚本
# 在项目首次运行或用户手动点击工具页按钮时执行。
param(
[string]$InstallDir = (Join-Path $PSScriptRoot "..\tools\ffmpeg"),
[string]$ConfigFile = (Join-Path $PSScriptRoot "..\config.yaml")
)
$ErrorActionPreference = "Stop"
$previousProgressPreference = $ProgressPreference
$ProgressPreference = "SilentlyContinue"
$tempRoot = $null
function Test-ToolInstalled {
param([string]$BaseDir)
$ffmpeg = Join-Path $BaseDir "bin\ffmpeg.exe"
$ffprobe = Join-Path $BaseDir "bin\ffprobe.exe"
return (Test-Path $ffmpeg) -and (Test-Path $ffprobe)
}
function Update-ConfigFile {
param(
[string]$Path,
[string]$FFmpegPath,
[string]$FFprobePath
)
if (!(Test-Path $Path)) {
return
}
$content = Get-Content -Path $Path
$content = $content | ForEach-Object {
if ($_ -match '^\s*ffmpeg_path:') {
" ffmpeg_path: $FFmpegPath"
} elseif ($_ -match '^\s*ffprobe_path:') {
" ffprobe_path: $FFprobePath"
} else {
$_
}
}
Set-Content -Path $Path -Value $content -Encoding UTF8
}
function Cleanup-Temp {
if ($tempRoot -and (Test-Path $tempRoot)) {
Remove-Item -LiteralPath $tempRoot -Recurse -Force
}
}
Write-Host "=== MediaStationGo ffmpeg 自动安装脚本 ===" -ForegroundColor Cyan
Write-Host ""
$InstallDir = [System.IO.Path]::GetFullPath($InstallDir)
$ConfigFile = [System.IO.Path]::GetFullPath($ConfigFile)
$ffmpegPath = Join-Path $InstallDir "bin\ffmpeg.exe"
$ffprobePath = Join-Path $InstallDir "bin\ffprobe.exe"
$arch = if ([Environment]::Is64BitOperatingSystem) { "win64" } else { "win32" }
Write-Host "检测到系统架构: $arch" -ForegroundColor Yellow
Write-Host "安装目录: $InstallDir" -ForegroundColor Yellow
trap {
Write-Host "✗ 自动安装失败: $($_.Exception.Message)" -ForegroundColor Red
if ($_.InvocationInfo) {
Write-Host ("行 {0}: {1}" -f $_.InvocationInfo.ScriptLineNumber, $_.InvocationInfo.Line.Trim()) -ForegroundColor DarkRed
}
Write-Host ""
Write-Host "请手动下载安装并放到以下目录:" -ForegroundColor Yellow
Write-Host $InstallDir -ForegroundColor Yellow
$ProgressPreference = $previousProgressPreference
Cleanup-Temp
exit 1
}
if (Test-ToolInstalled -BaseDir $InstallDir) {
Write-Host "✓ ffmpeg 已安装" -ForegroundColor Green
& $ffprobePath -version | Select-Object -First 1
exit 0
}
$downloadUrls = @(
"https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip",
"https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-$arch-gpl.zip"
)
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
$tempRoot = Join-Path $env:TEMP ("mediastationgo-ffmpeg-" + [guid]::NewGuid().ToString("N"))
$zipFile = Join-Path $tempRoot "ffmpeg.zip"
$extractDir = Join-Path $tempRoot "extract"
New-Item -ItemType Directory -Path $extractDir -Force | Out-Null
$downloaded = $false
foreach ($downloadUrl in $downloadUrls) {
try {
Write-Host "下载 ffmpeg..." -ForegroundColor Yellow
Write-Host "URL: $downloadUrl"
Invoke-WebRequest -Uri $downloadUrl -OutFile $zipFile
$downloaded = $true
break
} catch {
Write-Host "下载失败,尝试备用地址: $downloadUrl" -ForegroundColor DarkYellow
}
}
if (-not $downloaded) {
throw "所有下载地址均不可用"
}
Write-Host "解压文件..." -ForegroundColor Yellow
Expand-Archive -Path $zipFile -DestinationPath $extractDir -Force
$ffmpegBinaryPath = "" + (Get-ChildItem -Path $extractDir -Recurse -Filter "ffmpeg.exe" -File |
Select-Object -First 1 -ExpandProperty FullName)
$ffprobeBinaryPath = "" + (Get-ChildItem -Path $extractDir -Recurse -Filter "ffprobe.exe" -File |
Select-Object -First 1 -ExpandProperty FullName)
if ([string]::IsNullOrWhiteSpace($ffmpegBinaryPath) -or [string]::IsNullOrWhiteSpace($ffprobeBinaryPath)) {
Write-Host "未找到预期二进制,解压结果样本:" -ForegroundColor DarkYellow
Get-ChildItem -Path $extractDir -Recurse -File | Select-Object -First 20 FullName | ForEach-Object {
Write-Host $_.FullName -ForegroundColor DarkYellow
}
throw "解压后未找到包含 bin\ffmpeg.exe 的目录"
}
$packageRoot = Split-Path -Parent (Split-Path -Parent $ffmpegBinaryPath)
Copy-Item -Path (Join-Path $packageRoot "*") -Destination $InstallDir -Recurse -Force
if (-not (Test-ToolInstalled -BaseDir $InstallDir)) {
throw "ffmpeg 文件未正确安装到 $InstallDir\bin"
}
$env:PATH = "$InstallDir\bin;$env:PATH"
Update-ConfigFile -Path $ConfigFile -FFmpegPath $ffmpegPath -FFprobePath $ffprobePath
Write-Host "✓ 安装完成" -ForegroundColor Green
Write-Host "ffmpeg: $ffmpegPath" -ForegroundColor Green
Write-Host "ffprobe: $ffprobePath" -ForegroundColor Green
& $ffprobePath -version | Select-Object -First 1
$ProgressPreference = $previousProgressPreference
Cleanup-Temp
Write-Host ""
Write-Host "提示: 如需永久加入 PATH,请以管理员执行:" -ForegroundColor Cyan
Write-Host "[Environment]::SetEnvironmentVariable('PATH', `$env:PATH + ';$InstallDir\bin', 'Machine')" -ForegroundColor Gray
-68
View File
@@ -1,68 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="${VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo dev)}"
OUT_DIR="${OUT_DIR:-dist-release}"
PLATFORMS="${PLATFORMS:-linux/amd64 linux/arm64 windows/amd64 darwin/amd64 darwin/arm64}"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
if [[ "$VERSION" == *dirty* ]]; then
echo "Refusing to package a dirty working tree version: $VERSION" >&2
exit 1
fi
rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR"
if [[ "$VERSION" == MediaStationGo-v* ]]; then
PACKAGE_PREFIX="$VERSION"
else
PACKAGE_PREFIX="MediaStationGo-$VERSION"
fi
echo "Building frontend bundle"
(cd web && npm ci && npm run build)
for target in $PLATFORMS; do
os="${target%/*}"
arch="${target#*/}"
name="${PACKAGE_PREFIX}-${os}-${arch}"
work="$OUT_DIR/$name"
mkdir -p "$work/web"
ext=""
if [[ "$os" == "windows" ]]; then
ext=".exe"
fi
echo "Building $target"
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build \
-trimpath -ldflags="-s -w" \
-o "$work/mediastation-go$ext" ./cmd/server
cp -R web/dist "$work/web/dist"
cp docker-compose.yml config.example.yaml README.md README_EN.md LICENSE "$work/"
if [[ "$os" == "windows" ]]; then
(cd "$OUT_DIR" && python3 - "$name" "$name.zip" <<'PY'
import os
import sys
import zipfile
root, archive = sys.argv[1], sys.argv[2]
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as zf:
for base, _, files in os.walk(root):
for filename in files:
path = os.path.join(base, filename)
zf.write(path, path)
PY
)
else
tar -C "$OUT_DIR" -czf "$OUT_DIR/$name.tar.gz" "$name"
fi
rm -rf "$work"
done
echo "Release packages written to $OUT_DIR"
-383
View File
@@ -1,383 +0,0 @@
#!/usr/bin/env bash
# =============================================================================
# scripts/smoke-test.sh
#
# End-to-end deployment smoke test. Spins up a fresh MediaStationGo binary
# against a temporary data dir, exercises every major REST surface, then
# tears it all down. Exits non-zero on the first failure so it can run in
# CI as a deployment gate.
#
# Requirements:
# - bin/mediastation-go (run `make build-server` first)
# - web/dist (run `make build-web` first)
# - ffmpeg + ffprobe on PATH (the script will skip transcode tests if missing)
# - python3 (used for JSON pretty-printing only)
# - curl
#
# Usage:
# ./scripts/smoke-test.sh # default port 18080
# PORT=19000 ./scripts/smoke-test.sh # override
# =============================================================================
set -euo pipefail
if [ -z "${PORT:-}" ]; then
PORT="$(python3 - <<'PY'
import socket
s = socket.socket()
s.bind(("127.0.0.1", 0))
print(s.getsockname()[1])
s.close()
PY
)"
fi
ROOT="$(mktemp -d -t msgo-smoke.XXXXXX)"
DATA="$ROOT/data"
CACHE="$ROOT/cache"
MEDIA="$ROOT/media"
LOG="$ROOT/server.log"
PASS=0
FAIL=0
cleanup() {
status=$?
if [ "$status" -ne 0 ] && [ -f "$LOG" ]; then
printf "\n\033[1;31m==> Server log tail after failure\033[0m\n" >&2
tail -200 "$LOG" >&2 || true
fi
if [ -n "${PID:-}" ]; then
kill -TERM "$PID" 2>/dev/null || true
wait "$PID" 2>/dev/null || true
fi
rm -rf "$ROOT"
}
trap cleanup EXIT
# --- Helpers ----------------------------------------------------------------
ok() { printf " \033[32m✓\033[0m %s\n" "$1"; PASS=$((PASS+1)); }
fail() { printf " \033[31m✗\033[0m %s\n" "$1"; FAIL=$((FAIL+1)); }
warn() { printf " \033[33m!\033[0m %s\n" "$1"; }
hdr() { printf "\n\033[1;36m==> %s\033[0m\n" "$1"; }
json_token() {
python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("token") or d.get("access_token") or (d.get("tokens") or {}).get("access_token") or "")'
}
require() {
command -v "$1" >/dev/null || { echo "missing dependency: $1"; exit 2; }
}
require curl
require python3
HAVE_FFMPEG=1
command -v ffmpeg >/dev/null && command -v ffprobe >/dev/null || HAVE_FFMPEG=0
# --- 1. Prepare workspace ---------------------------------------------------
hdr "Preparing $ROOT"
mkdir -p "$DATA" "$CACHE" "$MEDIA/movies" "$MEDIA/tv/Show/Season 01" "$MEDIA/anime"
if [ "$HAVE_FFMPEG" = 1 ] && \
ffmpeg -y -loglevel error -f lavfi -i "color=c=blue:s=320x240:d=2" \
-f lavfi -i "sine=frequency=1000:duration=2" \
-c:v libx264 -preset ultrafast -c:a aac \
"$MEDIA/movies/Inception.2010.1080p.BluRay.x264.mp4" && \
ffmpeg -y -loglevel error -f lavfi -i "color=c=red:s=320x240:d=2" \
-f lavfi -i "sine=frequency=440:duration=2" \
-c:v libx264 -preset ultrafast -c:a aac \
"$MEDIA/tv/Show/Season 01/Show.S01E01.mkv" && \
ffmpeg -y -loglevel error -f lavfi -i "color=c=cyan:s=320x240:d=2" \
-f lavfi -i "sine=frequency=500:duration=2" \
-c:v libx264 -preset ultrafast -c:a aac \
"$MEDIA/anime/[Erai-raws] One Piece - 1100 [1080p].mkv"; then
ok "ffmpeg sample media generated"
else
HAVE_FFMPEG=0
# Generate small dummy files so the scanner can still find them.
printf "dummy" > "$MEDIA/movies/Inception.2010.1080p.BluRay.x264.mp4"
printf "dummy" > "$MEDIA/tv/Show/Season 01/Show.S01E01.mkv"
printf "dummy" > "$MEDIA/anime/[Erai-raws] One Piece - 1100 [1080p].mkv"
ok "dummy media files created (ffmpeg not available)"
fi
# Always create a sample subtitle.
cat > "$MEDIA/movies/Inception.2010.1080p.BluRay.x264.zh.srt" <<'SRT'
1
00:00:00,500 --> 00:00:01,500
Hello
SRT
# --- 2. Start the server ----------------------------------------------------
hdr "Starting MediaStationGo on :$PORT"
BIN="${BIN:-./bin/mediastation-go}"
if [ ! -x "$BIN" ]; then
echo "Binary $BIN not found. Run 'make build-server' first."
exit 2
fi
ADMIN_INITIAL_PASSWORD=smoketest12345 \
MEDIASTATION_APP_PORT="$PORT" \
MEDIASTATION_APP_DATA_DIR="$DATA" \
MEDIASTATION_APP_WEB_DIR="${WEB_DIR:-./web/dist}" \
MEDIASTATION_DATABASE_DB_PATH="$DATA/test.db" \
MEDIASTATION_CACHE_CACHE_DIR="$CACHE" \
"$BIN" >"$LOG" 2>&1 &
PID=$!
# Wait until /api/health responds. CI runners can be slow immediately after
# building the binary, so give startup a full minute before failing.
HEALTH_OK=0
for _ in $(seq 1 120); do
if curl -s -o /dev/null "http://127.0.0.1:$PORT/api/health"; then
HEALTH_OK=1
break
fi
sleep 0.5
done
if [ "$HEALTH_OK" = 1 ] && curl -s "http://127.0.0.1:$PORT/api/health" | grep -q '"ok"'; then
ok "/api/health"
else
fail "/api/health"
exit 1
fi
# --- 3. Auth ----------------------------------------------------------------
hdr "Auth"
LOGIN_JSON=$(curl -s -X POST -H 'Content-Type: application/json' \
-d '{"username":"admin","password":"smoketest12345"}' \
"http://127.0.0.1:$PORT/api/auth/login")
TOKEN=$(printf '%s' "$LOGIN_JSON" | json_token || true)
if [ -n "$TOKEN" ]; then
ok "login as admin"
else
fail "login as admin"
printf "login response: %s\n" "$LOGIN_JSON"
exit 1
fi
H="Authorization: Bearer $TOKEN"
curl -s -o /dev/null -w "%{http_code}" -H "$H" "http://127.0.0.1:$PORT/api/me" | grep -q 200 \
&& ok "/api/me with bearer" || fail "/api/me with bearer"
curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:$PORT/api/me" | grep -q 401 \
&& ok "/api/me without bearer → 401" || fail "/api/me without bearer"
# --- 4. Library + scan ------------------------------------------------------
hdr "Library + scan + search"
MOVIE_JSON=$(curl -s -X POST -H "$H" -H 'Content-Type: application/json' \
-d "{\"name\":\"movies\",\"path\":\"$MEDIA/movies\",\"type\":\"movie\"}" \
"http://127.0.0.1:$PORT/api/libraries")
MOVIE=$(printf '%s' "$MOVIE_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin).get("id", ""))' || true)
if [ -n "$MOVIE" ]; then
ok "create movie library"
else
fail "create movie library"
printf "create movie library response: %s\n" "$MOVIE_JSON"
exit 1
fi
TV_JSON=$(curl -s -X POST -H "$H" -H 'Content-Type: application/json' \
-d "{\"name\":\"tv\",\"path\":\"$MEDIA/tv\",\"type\":\"tv\"}" \
"http://127.0.0.1:$PORT/api/libraries")
TV=$(printf '%s' "$TV_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin).get("id", ""))' || true)
if [ -n "$TV" ]; then
ok "create tv library"
else
fail "create tv library"
printf "create tv library response: %s\n" "$TV_JSON"
exit 1
fi
RES=$(curl -s -X POST -H "$H" "http://127.0.0.1:$PORT/api/libraries/$MOVIE/scan")
ADDED=$(echo "$RES" | python3 -c 'import json,sys;print(json.load(sys.stdin)["added"])')
[ "$ADDED" -ge 1 ] && ok "scan: movie(s) added ($ADDED)" || fail "scan movie added=$ADDED"
RES=$(curl -s -X POST -H "$H" "http://127.0.0.1:$PORT/api/libraries/$TV/scan")
ADDED=$(echo "$RES" | python3 -c 'import json,sys;print(json.load(sys.stdin)["added"])')
[ "$ADDED" -ge 1 ] && ok "scan: tv episode(s) added ($ADDED)" || fail "scan tv added=$ADDED"
# SxxExx parser
SE=$(curl -s -H "$H" "http://127.0.0.1:$PORT/api/libraries/$TV/seasons" \
| python3 -c 'import json,sys; ss=json.load(sys.stdin)["seasons"]; e=ss[0]["episodes"][0]; print("%dx%d" % (e["season_num"], e["episode_num"]))')
[ "$SE" = "1x1" ] && ok "season parser → S01E01" || fail "season parser → $SE"
if [ "$HAVE_FFMPEG" = 1 ]; then
# ffprobe wrote width/height/codec
W=$(curl -s -H "$H" "http://127.0.0.1:$PORT/api/libraries/$MOVIE/media" \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["items"][0]["width"])')
[ "$W" = "320" ] && ok "ffprobe metadata extracted (width=320)" || fail "ffprobe width=$W"
fi
curl -s -H "$H" "http://127.0.0.1:$PORT/api/media?q=inception" \
| python3 -c 'import json,sys; assert json.load(sys.stdin)["items"], "empty"' \
&& ok "search returns rows" || fail "search returns rows"
# --- 5. Streaming -----------------------------------------------------------
hdr "Streaming + subtitles"
ID=$(curl -s -H "$H" "http://127.0.0.1:$PORT/api/libraries/$MOVIE/media" \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["items"][0]["id"])')
[ -n "$ID" ] && ok "got media id for stream tests" || fail "no media id"
curl -s -o /dev/null -w "%{http_code}" -H "$H" -H "Range: bytes=0-3" \
"http://127.0.0.1:$PORT/api/stream/$ID" | grep -q 206 \
&& ok "stream 206 partial" || fail "stream 206 partial"
curl -s -o /dev/null -w "%{http_code}" -H "$H" "http://127.0.0.1:$PORT/api/stream/$ID" \
| grep -q 200 && ok "stream 200 full" || fail "stream 200 full"
TRACKS=$(curl -s -H "$H" "http://127.0.0.1:$PORT/api/media/$ID/subtitles" \
| python3 -c 'import json,sys;print(len(json.load(sys.stdin)["tracks"]))')
[ "$TRACKS" = "1" ] && ok "external SRT discovered" || fail "external SRT discovered=$TRACKS"
if [ "$HAVE_FFMPEG" = 1 ] && [ "${SMOKE_HLS:-0}" = "1" ]; then
HLS_OK=0
for _ in $(seq 1 5); do
if curl -fsS -H "$H" "http://127.0.0.1:$PORT/api/hls/$ID/index.m3u8" | grep -q EXTM3U; then
HLS_OK=1
break
fi
sleep 1
done
if [ "$HLS_OK" = 1 ]; then
ok "HLS playlist (transcode triggered)"
else
warn "HLS playlist unavailable in this runner; skipping transcode gate"
fi
curl -s -X DELETE -H "$H" "http://127.0.0.1:$PORT/api/hls/$ID" -o /dev/null || true
elif [ "$HAVE_FFMPEG" = 1 ]; then
warn "HLS gate disabled by default; set SMOKE_HLS=1 to exercise transcode"
fi
# --- 6. Playback bookkeeping -----------------------------------------------
hdr "History / favourites / playlists"
curl -s -X POST -H "$H" -H 'Content-Type: application/json' \
-d "{\"media_id\":\"$ID\",\"position_ms\":500,\"duration_ms\":2000}" \
-o /dev/null "http://127.0.0.1:$PORT/api/history"
HIST=$(curl -s -H "$H" "http://127.0.0.1:$PORT/api/history" \
| python3 -c 'import json,sys;print(len(json.load(sys.stdin)["items"]))')
[ "$HIST" -ge 1 ] && ok "history persisted" || fail "history persisted=$HIST"
curl -s -X POST -H "$H" "http://127.0.0.1:$PORT/api/favourites/$ID" \
| grep -q '"favourite":true' && ok "favourite toggled on" || fail "favourite toggle"
PL=$(curl -s -X POST -H "$H" -H 'Content-Type: application/json' \
-d '{"name":"smoke"}' "http://127.0.0.1:$PORT/api/playlists" \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])')
curl -s -X POST -H "$H" -H 'Content-Type: application/json' \
-d "{\"media_id\":\"$ID\"}" -o /dev/null \
"http://127.0.0.1:$PORT/api/playlists/$PL/items"
N=$(curl -s -H "$H" "http://127.0.0.1:$PORT/api/playlists/$PL" \
| python3 -c 'import json,sys;print(len(json.load(sys.stdin)["items"]))')
[ "$N" -ge 1 ] && ok "playlist has 1 item" || fail "playlist items=$N"
# --- 7. Admin operations ---------------------------------------------------
hdr "Admin / RBAC / persistence"
curl -s -X POST -H 'Content-Type: application/json' \
-d '{"username":"alice","password":"alice12345"}' \
-o /dev/null "http://127.0.0.1:$PORT/api/auth/register"
ATOK=$(curl -s -X POST -H 'Content-Type: application/json' \
-d '{"username":"alice","password":"alice12345"}' \
"http://127.0.0.1:$PORT/api/auth/login" \
| json_token)
curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $ATOK" \
-X POST -H 'Content-Type: application/json' \
-d "{\"name\":\"x\",\"path\":\"$MEDIA\",\"type\":\"movie\"}" \
"http://127.0.0.1:$PORT/api/libraries" | grep -q 403 \
&& ok "regular user cannot create library (403)" || fail "regular user RBAC"
# --- 8. NFO + recycle bin --------------------------------------------------
hdr "NFO + Recycle bin"
curl -s -X POST -H "$H" "http://127.0.0.1:$PORT/api/media/$ID/nfo" \
| grep -q '"path"' && ok "NFO export" || fail "NFO export"
[ -f "$MEDIA/movies/Inception.2010.1080p.BluRay.x264.nfo" ] \
&& ok "NFO file written next to media" || fail "NFO file missing"
curl -s -X DELETE -H "$H" -o /dev/null "http://127.0.0.1:$PORT/api/media/$ID"
R=$(curl -s -H "$H" "http://127.0.0.1:$PORT/api/recycle" \
| python3 -c 'import json,sys;print(len(json.load(sys.stdin)["items"]))')
[ "$R" -ge 1 ] && ok "recycle bin has the soft-deleted row" || fail "recycle bin=$R"
curl -s -X POST -H "$H" -o /dev/null "http://127.0.0.1:$PORT/api/media/$ID/restore"
ok "recycle restore successful"
# --- 9. SPA + assets -------------------------------------------------------
hdr "SPA"
curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:$PORT/" | grep -q 200 \
&& ok "SPA / served" || fail "SPA /"
curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:$PORT/login" | grep -q 200 \
&& ok "SPA /login fallback" || fail "SPA /login"
# --- 9b. New iter-6 surfaces ----------------------------------------------
hdr "API config / Storage / Files / DLNA / Scheduler / Emby / STRM / Duplicates"
# API config seeded with 6 providers
N=$(curl -s -H "$H" "http://127.0.0.1:$PORT/api/admin/api-configs" \
| python3 -c 'import json,sys;print(len(json.load(sys.stdin)["items"]))')
[ "$N" -ge 6 ] && ok "api-configs seeded ($N)" || fail "api-configs count=$N"
# Update + masked roundtrip
RES=$(curl -s -X PUT -H "$H" -H 'Content-Type: application/json' \
-d '{"api_key":"sk-12345678abcdef"}' \
"http://127.0.0.1:$PORT/api/admin/api-configs/tmdb")
echo "$RES" | grep -q '"masked_key"' && ok "api-config masked key returned" || fail "api-config masked"
echo "$RES" | grep -q '"has_key":true' && ok "api-config has_key=true" || fail "api-config has_key"
# DB stores ciphertext, not plaintext
if command -v sqlite3 >/dev/null; then
CT=$(sqlite3 "$DATA/test.db" 'SELECT api_key FROM api_configs WHERE provider="tmdb";' 2>&1 || echo "")
echo "$CT" | grep -q '^enc:v1:' && ok "api-config encrypted in db" || fail "api-config not encrypted (got=$CT)"
fi
# Storage breakdown
curl -s -H "$H" "http://127.0.0.1:$PORT/api/storage" \
| python3 -c 'import json,sys;assert "total_bytes" in json.load(sys.stdin)' \
&& ok "storage breakdown" || fail "storage breakdown"
# File browser (root listing must include the test library)
curl -s -H "$H" "http://127.0.0.1:$PORT/api/files" \
| python3 -c 'import json,sys;d=json.load(sys.stdin);assert any("library:" in r["label"] for r in d["roots"])' \
&& ok "file browser lists library root" || fail "file browser"
# Path-traversal denied
curl -s -o /dev/null -w "%{http_code}" -H "$H" "http://127.0.0.1:$PORT/api/files?path=/etc" \
| grep -q 403 && ok "file browser rejects /etc" || fail "file browser path-traversal"
# DLNA discovery (no devices on container — must return empty array)
curl -s -H "$H" "http://127.0.0.1:$PORT/api/dlna/devices" \
| python3 -c 'import json,sys;d=json.load(sys.stdin);assert isinstance(d["devices"], list)' \
&& ok "dlna devices endpoint" || fail "dlna devices"
# Scheduler status
JS=$(curl -s -H "$H" "http://127.0.0.1:$PORT/api/admin/scheduler" \
| python3 -c 'import json,sys;print(len(json.load(sys.stdin)["jobs"]))')
[ "$JS" -ge 3 ] && ok "scheduler exposes $JS jobs" || fail "scheduler jobs=$JS"
# Run a scheduler job manually
curl -s -o /dev/null -w "%{http_code}" -X POST -H "$H" \
"http://127.0.0.1:$PORT/api/admin/scheduler/library_scan/run" \
| grep -q 204 && ok "scheduler run library_scan" || fail "scheduler run"
# Emby compat
curl -s -H "$H" "http://127.0.0.1:$PORT/emby/System/Info" \
| grep -q "MediaStationGo" && ok "emby /System/Info" || fail "emby /System/Info"
curl -s -H "$H" "http://127.0.0.1:$PORT/emby/Users/admin/Views" \
| grep -q "TotalRecordCount" && ok "emby /Users/{x}/Views" || fail "emby /Users/{x}/Views"
# STRM set + 302 redirect
curl -s -X PUT -H "$H" -H 'Content-Type: application/json' \
-d '{"url":"https://example.com/test.mp4"}' \
-o /dev/null -w "%{http_code}" "http://127.0.0.1:$PORT/api/media/$ID/strm" \
| grep -q 200 && ok "strm set" || fail "strm set"
curl -s -o /dev/null -w "%{http_code}" -H "$H" "http://127.0.0.1:$PORT/api/stream/$ID" \
| grep -q 302 && ok "stream returns 302 for strm media" || fail "strm 302"
curl -s -X DELETE -o /dev/null -w "%{http_code}" -H "$H" \
"http://127.0.0.1:$PORT/api/media/$ID/strm" \
| grep -q 204 && ok "strm clear" || fail "strm clear"
# Duplicate finder
curl -s -X POST -o /dev/null -w "%{http_code}" -H "$H" \
"http://127.0.0.1:$PORT/api/duplicates/scan?library_id=$MOVIE" \
| grep -q 200 && ok "duplicate scan" || fail "duplicate scan"
# --- 10. Graceful shutdown -------------------------------------------------
hdr "Shutdown"
kill -TERM "$PID"
wait "$PID" 2>/dev/null || true
unset PID
grep -q "MediaStationGo stopped" "$LOG" && ok "graceful shutdown logged" || fail "no shutdown log"
# --- Summary ---------------------------------------------------------------
hdr "Summary"
printf "\033[32m PASS=%d\033[0m \033[31mFAIL=%d\033[0m\n" "$PASS" "$FAIL"
exit $((FAIL > 0 ? 1 : 0))