diff --git a/apps/admin-ui/src/app/nodes/page.tsx b/apps/admin-ui/src/app/nodes/page.tsx index 8f7eecd..9f0fe61 100644 --- a/apps/admin-ui/src/app/nodes/page.tsx +++ b/apps/admin-ui/src/app/nodes/page.tsx @@ -48,6 +48,7 @@ export default function NodesPage() { const [nodes, setNodes] = useState([]); const [shelves, setShelves] = useState([]); const [showEnroll, setShowEnroll] = useState(false); + const [enrollPlatform, setEnrollPlatform] = useState<"unraid" | "synology">("unraid"); const [enrollName, setEnrollName] = useState(""); const [enrollLocation, setEnrollLocation] = useState(""); const [enrollToken, setEnrollToken] = useState(""); @@ -94,15 +95,39 @@ export default function NodesPage() { setEnrollToken(data.token); } + function setPlatform(platform: "unraid" | "synology") { + setEnrollPlatform(platform); + if (platform === "synology") { + setMoviesPath((p) => (p.startsWith("/mnt/") || !p ? "/volume1/Media/Films/Films" : p)); + setSeriesPath((p) => (p.startsWith("/mnt/") || !p ? "/volume1/Media/Series/Series" : p)); + if (!enrollName || enrollName.startsWith("Unraid")) { + setEnrollName("Synology-Node-01"); + } + } else { + setMoviesPath((p) => (p.startsWith("/volume") || !p ? "/mnt/user/movies" : p)); + setSeriesPath((p) => (p.startsWith("/volume") || !p ? "/mnt/user/series" : p)); + if (!enrollName || enrollName.startsWith("Synology")) { + setEnrollName("Unraid-Node-01"); + } + } + } + function installCommand() { + const script = enrollPlatform === "synology" ? "synology.sh" : "unraid.sh"; + const defaultName = + enrollPlatform === "synology" ? "Synology-Node-01" : "Unraid-Node-01"; const series = seriesPath.trim() ? ` \\\n --series='${seriesPath.trim()}'` : ""; - return `curl -fsSL ${MASTER_URL}/install/unraid.sh | bash -s -- \\ + const mediaRoot = + enrollPlatform === "synology" + ? ` \\\n --media-root='/volume1'` + : ""; + return `curl -fsSL ${MASTER_URL}/install/${script} | bash -s -- \\ --token='${enrollToken}' \\ - --name='${enrollName || "Unraid-Node-01"}' \\ + --name='${enrollName || defaultName}' \\ --public-url='${publicUrl.trim()}' \\ - --movies='${moviesPath.trim()}'${series}`; + --movies='${moviesPath.trim()}'${series}${mediaRoot}`; } async function copyInstall() { @@ -243,15 +268,53 @@ export default function NodesPage() { {showEnroll && (

- Unraid-node toevoegen + Node toevoegen

+
+ +
+ + +
+

- Eenmalig installeren met brede /mnt/user-mount. Daarna: planken in Instellingen, - scanpaden + plankkeuze hier onder Edit. + {enrollPlatform === "synology" ? ( + <> + SSH + Container Manager. Brede mount /volume1 — daarna scanpaden via Edit. + Meerdere NAS'en: eigen naam + publieke URL per box. + + ) : ( + <> + Eenmalig installeren met brede /mnt/user-mount. Daarna: planken in Instellingen, + scanpaden + plankkeuze hier onder Edit. + + )}

- setEnrollName(e.target.value)} placeholder="Unraid-Node-01" /> + setEnrollName(e.target.value)} + placeholder={enrollPlatform === "synology" ? "Synology-Node-01" : "Unraid-Node-01"} + />
@@ -263,18 +326,30 @@ export default function NodesPage() {
- setMoviesPath(e.target.value)} placeholder="/mnt/user/movies" /> + setMoviesPath(e.target.value)} + placeholder={ + enrollPlatform === "synology" ? "/volume1/Media/Films/Films" : "/mnt/user/movies" + } + />
- setSeriesPath(e.target.value)} placeholder="/mnt/user/series" /> + setSeriesPath(e.target.value)} + placeholder={ + enrollPlatform === "synology" ? "/volume1/Media/Series/Series" : "/mnt/user/series" + } + />
{enrollToken && ( <>
{enrollToken}

- Unraid-terminal + {enrollPlatform === "synology" ? "Synology SSH" : "Unraid-terminal"}

                   {installCommand()}
diff --git a/apps/master-api/src/install/routes.ts b/apps/master-api/src/install/routes.ts
index c7ca979..27b2ea2 100644
--- a/apps/master-api/src/install/routes.ts
+++ b/apps/master-api/src/install/routes.ts
@@ -10,24 +10,45 @@ function installDir(): string {
     "/app/install",
   ];
   for (const dir of candidates) {
-    if (fs.existsSync(path.join(dir, "unraid.sh"))) return dir;
+    if (
+      fs.existsSync(path.join(dir, "unraid.sh")) ||
+      fs.existsSync(path.join(dir, "synology.sh"))
+    ) {
+      return dir;
+    }
   }
   return candidates[0];
 }
 
+function sendInstallScript(
+  reply: { header: (k: string, v: string) => unknown; status: (c: number) => { send: (b: string) => unknown }; send: (b: string) => unknown },
+  dir: string,
+  filename: string,
+  publicUrl: string
+) {
+  const file = path.join(dir, filename);
+  if (!fs.existsSync(file)) {
+    return reply
+      .status(404)
+      .send("Install script not found — redeploy Master with install assets.");
+  }
+  let script = fs.readFileSync(file, "utf8");
+  script = script.replaceAll("__MASTER_PUBLIC_URL__", publicUrl.replace(/\/$/, ""));
+  reply.header("Content-Type", "text/x-shellscript; charset=utf-8");
+  reply.header("Content-Disposition", `inline; filename="${filename}"`);
+  return reply.send(script);
+}
+
 export async function registerInstallRoutes(app: FastifyInstance, config: Config) {
   const dir = installDir();
+  const publicUrl = config.PUBLIC_URL;
 
   app.get("/install/unraid.sh", async (_request, reply) => {
-    const file = path.join(dir, "unraid.sh");
-    if (!fs.existsSync(file)) {
-      return reply.status(404).send("Install script not found — redeploy Master with install assets.");
-    }
-    let script = fs.readFileSync(file, "utf8");
-    script = script.replaceAll("__MASTER_PUBLIC_URL__", config.PUBLIC_URL.replace(/\/$/, ""));
-    reply.header("Content-Type", "text/x-shellscript; charset=utf-8");
-    reply.header("Content-Disposition", 'inline; filename="unraid.sh"');
-    return reply.send(script);
+    return sendInstallScript(reply, dir, "unraid.sh", publicUrl);
+  });
+
+  app.get("/install/synology.sh", async (_request, reply) => {
+    return sendInstallScript(reply, dir, "synology.sh", publicUrl);
   });
 
   for (const bin of ["media-node-linux-amd64", "media-node-linux-arm64"] as const) {
diff --git a/deploy/docker/Dockerfile.master-api b/deploy/docker/Dockerfile.master-api
index ae5c36f..12378c7 100644
--- a/deploy/docker/Dockerfile.master-api
+++ b/deploy/docker/Dockerfile.master-api
@@ -42,6 +42,7 @@ COPY --from=builder /out/local-packages ./local-packages
 COPY --from=node-build /media-node-linux-amd64 /app/install/media-node-linux-amd64
 COPY --from=node-build /media-node-linux-arm64 /app/install/media-node-linux-arm64
 COPY deploy/node/install-unraid.sh /app/install/unraid.sh
+COPY deploy/node/install-synology.sh /app/install/synology.sh
 
 # Remove workspace protocol deps before npm install
 RUN node -e "\
diff --git a/deploy/node/install-synology.sh b/deploy/node/install-synology.sh
new file mode 100644
index 0000000..5afe09a
--- /dev/null
+++ b/deploy/node/install-synology.sh
@@ -0,0 +1,222 @@
+#!/bin/bash
+# Synology media-node installer — geen image build, alleen download + docker run
+# Werkt op DSM 7 + Container Manager (SSH). Meerdere NAS'en: unieke --name + --public-url.
+set -euo pipefail
+
+MASTER_URL="${MASTER_URL:-__MASTER_PUBLIC_URL__}"
+TOKEN=""
+NAME=""
+PUBLIC_URL=""
+MOVIES=""
+SERIES=""
+PORT="${PORT:-8080}"
+APPDATA="${APPDATA:-/volume1/docker/media-node}"
+MEDIA_ROOT="${MEDIA_ROOT:-/volume1}"
+PUID="${PUID:-1026}"
+PGID="${PGID:-100}"
+
+usage() {
+  cat </dev/null 2>&1; then
+    docker "$@"
+    return
+  fi
+  if command -v sudo >/dev/null 2>&1 && sudo docker info >/dev/null 2>&1; then
+    sudo docker "$@"
+    return
+  fi
+  echo "Docker niet gevonden. Op Synology: Container Manager installeren + SSH inschakelen."
+  echo "Draai dit script als root of een user in de docker-groep."
+  exit 1
+}
+
+prompt() {
+  local label="$1" var="$2" current="$3" flag="$4"
+  if [ -n "$current" ]; then
+    eval "$var=\"\$current\""
+    return
+  fi
+  if [ -t 0 ]; then
+    local val=""
+    printf "%s" "$label"
+    read -r val
+    eval "$var=\"\$val\""
+    return
+  fi
+  echo "Ontbrekende optie: $flag (bij curl|bash alle flags meegeven)."
+  usage
+  exit 1
+}
+
+echo "=== Media Node — Synology install ==="
+echo "Master: $MASTER_URL"
+echo
+
+prompt "Enrollment token: " TOKEN "$TOKEN" "--token"
+prompt "Node-naam [Synology-Node-01]: " NAME "${NAME}" "--name"
+if [ -z "$NAME" ]; then NAME="Synology-Node-01"; fi
+prompt "Publieke URL (https://node02....): " PUBLIC_URL "$PUBLIC_URL" "--public-url"
+prompt "Films-pad [/volume1/Media/Films/Films]: " MOVIES "${MOVIES}" "--movies"
+if [ -z "$MOVIES" ] && [ -t 0 ]; then MOVIES="/volume1/Media/Films/Films"; fi
+if [ -z "$MOVIES" ]; then
+  echo "Ontbrekende optie: --movies"
+  exit 1
+fi
+if [ -t 0 ] && [ -z "$SERIES" ]; then
+  printf "Series-pad (leeg = overslaan): "
+  read -r SERIES || true
+fi
+
+TOKEN="$(echo -n "$TOKEN" | tr -d '\r\n')"
+NAME="$(echo -n "$NAME" | tr -d '\r\n')"
+PUBLIC_URL="$(echo -n "$PUBLIC_URL" | sed 's:/*$::')"
+MOVIES="$(echo -n "$MOVIES" | tr -d '\r\n')"
+SERIES="$(echo -n "$SERIES" | tr -d '\r\n')"
+MEDIA_ROOT="$(echo -n "$MEDIA_ROOT" | sed 's:/*$::')"
+MASTER_URL="$(echo -n "$MASTER_URL" | sed 's:/*$::')"
+APPDATA="$(echo -n "$APPDATA" | sed 's:/*$::')"
+
+if [ -z "$TOKEN" ] || [ -z "$NAME" ] || [ -z "$PUBLIC_URL" ] || [ -z "$MOVIES" ]; then
+  echo "Token, naam, public-url en movies zijn verplicht."
+  exit 1
+fi
+
+if [ ! -d "$MEDIA_ROOT" ]; then
+  echo "Fout: media-root bestaat niet: $MEDIA_ROOT"
+  echo "Gebruik bijv. --media-root=/volume1 of /volume2"
+  exit 1
+fi
+if [ ! -d "$MOVIES" ]; then
+  echo "Waarschuwing: films-pad bestaat niet: $MOVIES"
+fi
+if [ -n "$SERIES" ] && [ ! -d "$SERIES" ]; then
+  echo "Waarschuwing: series-pad bestaat niet: $SERIES"
+fi
+
+case "$MOVIES" in
+  "$MEDIA_ROOT"|"$MEDIA_ROOT"/*) ;;
+  *) echo "Waarschuwing: --movies ligt niet onder --media-root ($MEDIA_ROOT). Admin-paden moeten onder die root vallen." ;;
+esac
+
+ARCH="$(uname -m)"
+case "$ARCH" in
+  x86_64|amd64) BIN_NAME="media-node-linux-amd64" ;;
+  aarch64|arm64) BIN_NAME="media-node-linux-arm64" ;;
+  *) echo "Niet-ondersteunde architectuur: $ARCH"; exit 1 ;;
+esac
+
+mkdir -p "$APPDATA/config/credentials" "$APPDATA/data" "$APPDATA/bin"
+chown -R "${PUID}:${PGID}" "$APPDATA" 2>/dev/null || true
+
+BIN_PATH="$APPDATA/bin/media-node"
+echo "Binary downloaden ($BIN_NAME)..."
+curl -fsSL "$MASTER_URL/install/$BIN_NAME" -o "$BIN_PATH"
+chmod +x "$BIN_PATH"
+chown "${PUID}:${PGID}" "$BIN_PATH" 2>/dev/null || true
+
+docker_cmd rm -f media-node >/dev/null 2>&1 || true
+
+VOLUMES=(
+  -v "$APPDATA/config:/etc/media-node"
+  -v "$APPDATA/data:/var/lib/media-node"
+  -v "$BIN_PATH:/usr/local/bin/media-node:ro"
+  -v "$MEDIA_ROOT:$MEDIA_ROOT:ro"
+)
+if [ -d /etc/ssl/certs ]; then
+  VOLUMES+=(-v /etc/ssl/certs:/etc/ssl/certs:ro)
+fi
+# Admin → Herstart via docker.sock (DSM: vaak root:root — group-add helpt niet altijd)
+if [ -S /var/run/docker.sock ]; then
+  VOLUMES+=(-v /var/run/docker.sock:/var/run/docker.sock)
+fi
+
+ENV_SERIES=""
+if [ -n "$SERIES" ]; then
+  ENV_SERIES="$SERIES"
+fi
+
+DOCKER_GID=""
+if [ -S /var/run/docker.sock ]; then
+  DOCKER_GID="$(stat -c '%g' /var/run/docker.sock 2>/dev/null || true)"
+fi
+GROUP_ARGS=()
+if [ -n "$DOCKER_GID" ] && [ "$DOCKER_GID" != "0" ]; then
+  GROUP_ARGS+=(--group-add "$DOCKER_GID")
+fi
+
+echo "Container starten (media-root=$MEDIA_ROOT, user=${PUID}:${PGID})..."
+docker_cmd run -d \
+  --name media-node \
+  --restart unless-stopped \
+  -p "${PORT}:8080" \
+  -e "MEDIA_NODE_NAME=$NAME" \
+  -e "MEDIA_NODE_MASTER_URL=$MASTER_URL" \
+  -e "MEDIA_NODE_PUBLIC_URL=$PUBLIC_URL" \
+  -e "MEDIA_NODE_MOVIES=$MOVIES" \
+  -e "MEDIA_NODE_SERIES=$ENV_SERIES" \
+  -e "MEDIA_NODE_ENROLLMENT_TOKEN=$TOKEN" \
+  -e "MEDIA_NODE_FULL_SCAN_INTERVAL=off" \
+  -e "MEDIA_NODE_FULL_SCAN_AT=03:30" \
+  -e "MEDIA_NODE_CONTAINER_NAME=media-node" \
+  -e "TZ=${TZ:-Europe/Amsterdam}" \
+  --user "${PUID}:${PGID}" \
+  "${GROUP_ARGS[@]}" \
+  "${VOLUMES[@]}" \
+  --entrypoint /usr/local/bin/media-node \
+  alpine:3.20 \
+  run
+
+echo
+echo "Klaar. Logs:  docker logs -f media-node"
+echo "Status:        in Admin → Nodes (moet online worden)"
+echo "NPM:           $PUBLIC_URL → Synology-LAN-IP:$PORT (buffering uit, Force SSL)"
+echo "Paden:         Admin → Nodes → Edit (host-paden onder $MEDIA_ROOT)"
+echo "UID/GID:       bij permission denied: --uid=/--gid= van 'id jouwuser'"
+echo
+echo "Na succes: credentials staan in $APPDATA — herstart zonder nieuwe token is ok."
+echo "Meerdere NAS'en: elk een eigen --name, --public-url en (indien nodig) --port."
diff --git a/docs/dokploy.md b/docs/dokploy.md
index 8b03ccb..61c5874 100644
--- a/docs/dokploy.md
+++ b/docs/dokploy.md
@@ -139,8 +139,8 @@ Browsers roepen alleen `/api/...` op het admin-domein aan — dat is correct.
 ## Na succesvolle deploy
 
 1. Admin UI → **Instellingen** → Stremio addon token genereren  
-2. Admin UI → **Nodes** → token + **Kopieer install-commando** (Unraid: één curl|bash)  
-3. Op Unraid: zie [unraid-node.md](./unraid-node.md). Op Linux: `sudo media-node install` met  
+2. Admin UI → **Nodes** → platform Unraid/Synology → token + **Kopieer install-commando**
+3. Op Unraid: zie [unraid-node.md](./unraid-node.md). Op Synology: [synology-node.md](./synology-node.md). Op Linux: `sudo media-node install` met
    Master URL = `https://master.jouwdomein.nl`
 
 ## Alternatief: Dokploy + jouw NPM
diff --git a/docs/synology-node.md b/docs/synology-node.md
new file mode 100644
index 0000000..ce7ed13
--- /dev/null
+++ b/docs/synology-node.md
@@ -0,0 +1,43 @@
+# Media Node op Synology (DSM 7)
+
+Zelfde model als Unraid: één curl-commando, geen image build.
+
+## Voorwaarden
+
+1. **Container Manager** geïnstalleerd  
+2. **SSH** aan (Configuratiescherm → Terminal & SNMP)  
+3. **NPM**: Proxy Host `nodeXX.…` → Synology LAN-IP `:8080`, Force SSL, buffering uit  
+
+## Installeren
+
+1. Admin → Nodes → **Node toevoegen** → kies platform **Synology** → token + paden → kopieer commando  
+2. SSH naar de NAS (admin/root) en plak het commando  
+
+Of handmatig:
+
+```bash
+curl -fsSL https://master.vonas.nl/install/synology.sh | bash -s -- \
+  --token='ENR-...' \
+  --name='Synology-Node-01' \
+  --public-url='https://node2.example.com' \
+  --movies='/volume1/Media/Films/Films' \
+  --series='/volume1/Media/Series/Series' \
+  --media-root='/volume1'
+```
+
+Defaults: appdata `/volume1/docker/media-node`, user `1026:100`.
+
+Bij **permission denied** op media:
+
+```bash
+id jouw_dsm_user
+# daarna opnieuw met --uid=... --gid=...
+```
+
+## Meerdere Synology’s
+
+Elke NAS: eigen `--name`, eigen `--public-url` (NPM), eventueel andere `--port` als ze hetzelfde LAN delen achter één router zonder aparte hosts.
+
+Paden in Admin zijn altijd de **echte Synology-paden** onder `--media-root` (bijv. `/volume1/...`).
+
+Controle: `docker logs -f media-node` → Admin → Nodes **online**.