Files
home-server/macos/Jellyfin-SMB.md

316 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Jellyfin + macOS: Persistent NFS Mount (Fix for Libraries Randomly “Clearing”)
This README documents the working fix I applied when Jellyfin (on a Mac mini) periodically “lost” or cleared my Movies/TV libraries that live on a NAS mounted over NFS.
It includes the exact commands, files, and rationale so I can reproduce it later.
---
## Problem Summary
- Symptom: Every day or two, Jellyfin showed empty Movies/TV libraries.
- Media location: NFS share at `/Volumes/media` from NAS `192.168.50.105:/media`.
- Root cause: macOS was using autofs (`/- /etc/auto_nfs`). autofs can unmount after inactivity or brief network blips. When the mount disappears during a Jellyfin scan/file-watch, Jellyfin sees files as missing and removes them from its DB.
## Solution Summary
- Stop using autofs for this path.
- Create a persistent mount at boot using a LaunchDaemon and a small networkaware mount script.
- The script:
- Is idempotent: does nothing if already mounted.
- Checks NAS reachability first.
- Logs to `/var/log/mount_media.(out|err)`.
- Optionally restarts Jellyfin (Homebrew service) if the mount comes back.
---
## Prerequisites / Assumptions
- macOS with admin (sudo) access.
- NFS server: `192.168.50.105` exporting `/media` (adjust as needed).
- Mount point: `/Volumes/media` (adjust as needed).
- Jellyfin installed (optional Homebrew service restart in script).
> Tip: If your NAS requires privileged source ports for NFSv4, `resvport` helps. The script falls back to `noresvport` if needed.
---
## Steps (copy/paste commands)
### 1) Disable autofs for this path and unmount any automounted share
```
# Backup and comment out the direct map for NFS
sudo cp /etc/auto_master /etc/auto_master.bak.$(date +%F_%H%M%S)
sudo sed -i.bak 's|^/- /etc/auto_nfs|#/- /etc/auto_nfs|' /etc/auto_master
# Reload automountd (will unmount /Volumes/media if it was automounted)
sudo automount -vc
# Ensure the mountpoint is not currently mounted (ignore errors if already unmounted)
sudo umount /Volumes/media 2>/dev/null || sudo umount -f /Volumes/media 2>/dev/null || true
```
> Note: If `chown`/`chmod` say “Operation not permitted,” the path is still mounted (or your NAS has root-squash). Unmount first.
---
### 2) Create the networkaware mount script
```
sudo mkdir -p /usr/local/sbin
sudo tee /usr/local/sbin/mount_media_nfs.sh > /dev/null <<'SH'
#!/bin/sh
set -eu
LOG="/var/log/mount_media.out"
ERR="/var/log/mount_media.err"
MOUNT="/Volumes/media"
# SMB server settings — use domain name (FQDN)
HOST="nas.example.local" # <- change to your domain
SHARE="media" # <- change share name if needed
# Optional auth:
# - If SMB_USER is set, script will try authenticated mount.
# - Supply SMB_PASS (environment) OR set SMB_KEYCHAIN_ITEM to fetch password from Keychain.
SMB_USER="${SMB_USER:-}"
SMB_PASS="${SMB_PASS:-}"
SMB_KEYCHAIN_ITEM="${SMB_KEYCHAIN_ITEM:-}"
# Ensure mountpoint exists
[ -d "$MOUNT" ] || mkdir -p "$MOUNT"
# If already mounted on the mountpoint, exit quietly
if mount | awk '{print $3}' | grep -qx "$MOUNT"; then
echo "$(date) already mounted: $MOUNT" >>"$LOG"
exit 0
fi
# Preflight: only try to mount when SMB port is reachable (try 445 then 139)
if ! ( /usr/bin/nc -G 2 -z "$HOST" 445 >/dev/null 2>&1 || /usr/bin/nc -G 2 -z "$HOST" 139 >/dev/null 2>&1 ); then
echo "$(date) NAS not reachable on SMB ports (445/139), skipping mount" >>"$LOG"
exit 0
fi
# Helpful server listing for debugging (doesn't include credentials)
echo "$(date) smbutil listing for debugging" >>"$LOG" 2>>"$ERR"
smbutil view "//$HOST" >>"$LOG" 2>>"$ERR" || true
smbutil view "//guest@$HOST" >>"$LOG" 2>>"$ERR" || true
# Helper: function to verify mount and restart Jellyfin if needed
verify_and_exit() {
if mount | awk '{print $3}' | grep -qx "$MOUNT"; then
echo "$(date) mount OK: $MOUNT" >>"$LOG"
if command -v brew >/dev/null 2>&1 && brew services list | grep -q '^jellyfin\b'; then
echo "$(date) restarting Jellyfin (brew services)" >>"$LOG"
brew services restart jellyfin >>"$LOG" 2>>"$ERR" || true
fi
exit 0
fi
}
# Try authenticated mount if SMB_USER provided
if [ -n "$SMB_USER" ]; then
# Retrieve password from Keychain if requested and SMB_PASS not set
if [ -z "$SMB_PASS" ] && [ -n "$SMB_KEYCHAIN_ITEM" ]; then
# Try to read password from Keychain (service name = SMB_KEYCHAIN_ITEM, account = SMB_USER)
# The -w flag prints only the password
SMB_PASS="$(security find-generic-password -s "$SMB_KEYCHAIN_ITEM" -a "$SMB_USER" -w 2>/dev/null || true)"
fi
if [ -n "$SMB_PASS" ]; then
# Use password via stdin to avoid exposing it in process list
echo "$(date) attempting authenticated mount as user '$SMB_USER' -> $MOUNT" >>"$LOG"
# Do NOT include the password in the URL or logs.
MOUNT_URL="//${SMB_USER}@${HOST}/${SHARE}"
# Send password followed by newline to mount_smbfs which will read it from stdin
if printf '%s\n' "$SMB_PASS" | /sbin/mount_smbfs "$MOUNT_URL" "$MOUNT" >>"$LOG" 2>>"$ERR"; then
verify_and_exit
else
echo "$(date) authenticated mount attempt FAILED" >>"$ERR"
# Fall through to guest attempts
fi
else
# No password available for authenticated mount
echo "$(date) SMB_USER set but no SMB_PASS or Keychain entry found -> will try guest" >>"$LOG"
fi
fi
# If we reach here, try guest access (null/anonymous session)
echo "$(date) trying guest/null session (mount_smbfs -N) -> $MOUNT" >>"$LOG"
if /sbin/mount_smbfs -N "//$HOST/$SHARE" "$MOUNT" >>"$LOG" 2>>"$ERR"; then
verify_and_exit
fi
echo "$(date) trying explicit guest user (guest@$HOST) -> $MOUNT" >>"$LOG"
if /sbin/mount_smbfs "//guest@$HOST/$SHARE" "$MOUNT" >>"$LOG" 2>>"$ERR"; then
verify_and_exit
fi
# If we reached here, all attempts failed
echo "$(date) ALL SMB mount attempts FAILED" >>"$ERR"
echo "------ smbutil status ------" >>"$ERR"
smbutil statshares -a >>"$ERR" 2>&1 || true
echo "------ mount table ------" >>"$ERR"
mount >>"$ERR" 2>&1 || true
exit 1
SH
sudo chmod 755 /usr/local/sbin/mount_media_smb.sh
```
---
### 3) Create the LaunchDaemon (mount at boot, re-check periodically, networkaware)
```
sudo tee /Library/LaunchDaemons/com.local.mountmedia.plist > /dev/null <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.local.mountmedia</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/sbin/mount_media_smb.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>300</integer>
<key>KeepAlive</key>
<dict>
<key>NetworkState</key>
<true/>
</dict>
<key>StandardOutPath</key>
<string>/var/log/mount_media.out</string>
<key>StandardErrorPath</key>
<string>/var/log/mount_media.err</string>
</dict>
</plist>
PLIST
sudo chown root:wheel /Library/LaunchDaemons/com.local.mountmedia.plist
sudo chmod 644 /Library/LaunchDaemons/com.local.mountmedia.plist
sudo plutil -lint /Library/LaunchDaemons/com.local.mountmedia.plist
sudo launchctl bootout system /Library/LaunchDaemons/com.local.mountmedia.plist 2>/dev/null || true
sudo launchctl bootstrap system /Library/LaunchDaemons/com.local.mountmedia.plist
sudo launchctl enable system/com.local.mountmedia
sudo launchctl kickstart -k system/com.local.mountmedia
```
---
### 4) Run once and verify
```
# Run once now (idempotent; logs "already mounted" if present)
sudo /usr/local/sbin/mount_media_smb.sh
# LaunchDaemon status
sudo launchctl print system/com.local.mountmedia | egrep 'state|last exit|PID' || true
# Mount status (should NOT say "automounted")
mount | grep " on /Volumes/media "
# SMB mount parameters
smbstat -m | sed -n '/\/Volumes\/media/,+12p'
# Script logs
tail -n 100 /var/log/mount_media.out /var/log/mount_media.err 2>/dev/null || true
```
---
### 5) Jellyfin settings
- Temporarily disable “Enable real-time monitoring” for libraries under `/Volumes/media` until you confirm the mount stays stable.
- Then run “Scan Library Files” to repopulate anything previously removed.
---
### 6) Reboot test (recommended)
```
sudo shutdown -r now
```
After reboot:
```
mount | grep " on /Volumes/media " || echo "Not mounted yet"
sudo launchctl print system/com.local.mountmedia | egrep 'state|last exit' || true
tail -n 100 /var/log/mount_media.out /var/log/mount_media.err 2>/dev/null || true
```
---
## Rationale for Key Choices
- Persistent mount (LaunchDaemon) instead of autofs:
- autofs can unmount after inactivity; Jellyfin then removes items it thinks are gone.
- LaunchDaemon ensures the mount is present before scans and remains mounted.
- smb options:
- `hard`: Blocks I/O until server responds, avoiding spurious “file missing” errors.
- `nfsvers=4.0`: Matches typical NAS defaults and the clients chosen version.
- `resvport` then fallback `noresvport`: Some servers require privileged ports; the script tries both.
- Network preflight:
- Check TCP/2049 reachability to avoid “Network is unreachable” failures (exit code 51) at boot or during link flaps.
- Logging:
- `/var/log/mount_media.out` and `.err` make it easy to correlate with Jellyfin logs.
---
## Troubleshooting
- “Operation not permitted” when `chown`/`chmod`:
- The path is mounted over NFS, and root-squash likely prevents ownership changes from the client. Unmount first or change ownership on the NAS.
- LaunchDaemon errors:
- Validate plist: `sudo plutil -lint /Library/LaunchDaemons/com.local.mountmedia.plist`
- Service state: `sudo launchctl print system/com.local.mountmedia`
- Mount health:
- `nfsstat -m` should show vers=4.0, hard, resvport/noresvport.
- Network/power:
- Prevent system sleep that drops the NIC; enable “Wake for network access.”
---
## Optional: If you must keep autofs
Increase the autofs timeout so it doesnt unmount on brief inactivity (less ideal than the LaunchDaemon approach):
```
sudo cp /etc/auto_master /etc/auto_master.bak.$(date +%F_%H%M%S)
sudo sed -i.bak -E 's|^/-[[:space:]]+/etc/auto_nfs$|/- -timeout=604800 /etc/auto_nfs|' /etc/auto_master
sudo automount -vc
```
---
## Reverting
To revert to autofs:
```
# Stop and remove LaunchDaemon
sudo launchctl bootout system /Library/LaunchDaemons/com.local.mountmedia.plist
sudo rm -f /Library/LaunchDaemons/com.local.mountmedia.plist
# Restore /etc/auto_master (uncomment direct map) and reload
sudo sed -i.bak 's|^#/- /etc/auto_nfs|/- /etc/auto_nfs|' /etc/auto_master
sudo automount -vc
```
---
## Notes
- Change permissions/ownership on the NFS export from the NAS, not the macOS client (root-squash).
- `showmount` may fail against NFSv4-only servers; its not needed here.
- Adjust `SERVER`, `MOUNT`, and `StartInterval` to suit your environment.