mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 03:06:38 +08:00
chore clean repository deployment files
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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/
|
||||
|
||||
@@ -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>
|
||||
@@ -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/
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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: "未分类"
|
||||
@@ -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
|
||||
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
|
||||
Vendored
-20
@@ -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
|
||||
Vendored
-89
@@ -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"
|
||||
}
|
||||
Vendored
-78
@@ -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
|
||||
Vendored
-34
@@ -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
|
||||
Vendored
-31
@@ -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[@]}"
|
||||
Vendored
-47
@@ -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
|
||||
Vendored
-148
@@ -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
|
||||
Vendored
-68
@@ -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"
|
||||
Vendored
-383
@@ -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))
|
||||
Reference in New Issue
Block a user