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
-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))