mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
chore clean repository deployment files
This commit is contained in:
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