Migrating from AltMount to zurg

Read the shared page first. The trash guard lives there. So does the reason your library lands in __magic__ rather than in a symlink farm.

AltMount serves the cleanest tree of the five. Exactly one file per release. No thumbnails and no adverts and no sample and no leftovers. The migration is correspondingly small because there is very little there to disagree about.

Measured against zurg on the same five releases.

   
zurg gains the sample clip, the .nfo, thumbnail.jpg, the .url adverts, the archive's inner directory, and every release AltMount refused
zurg loses nothing
renamed archive (RAR) releases only

The bigger issue is what AltMount never imported

AltMount refused a 10-episode season pack in every one of four runs. The articles were verified present on the news server beforehand.

fast-fail segment check inconclusive: 93 segment(s) remained
unverified after 3 attempts: context deadline exceeded

The check is not deterministic. The same release came back with 196 unverified segments and then 93 and then 76. It is also size-biased. Setting import.segment_sample_percentage: 1 samples 1% of segments so a bigger release needs more STATs to land inside the deadline. Repeated against a faster news host it refused more rather than less. That run lost the season pack and a 41 GB remux both. Latency is not the explanation.

The practical consequence for you is this. The releases AltMount quietly never published are the ones zurg will add. They have no Plex item today so nothing is preserved and nothing is at risk. They simply appear. If your library is smaller than your NZB collection then this is why.

If you want to keep AltMount importing while you migrate then lowering segment_sample_percentage is the knob to try. It was not exercised here.

What your library looks like afterwards

Everything is re-added because the library root moves to __magic__. See the shared page for why there is no cheaper path.

What changes on the way through is small. Plain single-file releases keep the filename AltMount gave them so most of the library reads the same. Archive releases come back under the poster's name and one directory deeper. That is strictly better for an *arr. The name father.brown.2013.s02e05.hdtv.x264-tla.mp4 carries the quality and source and group that FatherBrown.mp4 threw away.

Find the archive releases by looking for a mount folder whose single file is named after the folder.

AM=/mnt/altmount
for d in "$AM"/*/; do
  r=$(basename "$d")
  for f in "$d"*; do
    b=$(basename "$f")
    [ "${b%.*}" = "$r" ] && echo "RENAMED BY ALTMOUNT: $f"
  done
done

What new content shows up

AltMount publishes none of this so everything in the shared page's list applies to you.

  • Sample clips inside Sample/. This is the one worth configuring for. Set only_show_the_biggest_file: true if you do not want a 9 MB sample turning up as a second version.

  • thumbnail.jpg and the .nfo and the poster's .url adverts. All inert.

  • Whole releases AltMount refused arriving as new library items.

  • Empty folders where an archive chain is broken.

Get your NZBs out

AltMount took ownership of every NZB you fed it. The original was moved away and a compressed copy kept at <config>/.nzbs/<category>/<queueID>-<name>.nzbz. The mapping from mount folder to .nzbz is in the .meta files under <config>/metadata/complete/<Release>/. Those are binary with magic AM3 and the .nzbz path sits in there as a plain string.

zurg wants one .nzb per release named after the release and flat in nzbs/. Subdirectories are skipped. The metadata/complete/<Release> directory name is the mount folder name and that is exactly the name the file should get.

AM=/path/to/altmount/config          # holds altmount.db, .nzbs/, metadata/
OUT=./recovered-nzbs
mkdir -p "$OUT"
for dir in "$AM"/metadata/complete/*/; do
  release=$(basename "$dir")
  meta=$(find "$dir" -maxdepth 1 -name '*.meta' | head -1)
  [ -n "$meta" ] || { echo "NO META: $release" >&2; continue; }
  nzbz=$(strings "$meta" | grep -m1 '\.nzbz$')
  case "$nzbz" in "") echo "NO NZBZ REF: $release" >&2; continue ;;
                  /*) src=$nzbz ;; *) src="$AM/$nzbz" ;; esac
  [ -f "$src" ] || { echo "NZBZ MISSING: $release" >&2; continue; }
  [ -e "$OUT/$release.nzb" ] && { echo "NAME COLLISION: $release" >&2; continue; }
  if zcat "$src" > "$OUT/$release.nzb" 2>/dev/null && grep -q '<nzb' "$OUT/$release.nzb"; then
    echo "OK: $release"
  else
    rm -f "$OUT/$release.nzb"
    echo "NOT A GZIPPED NZB: $release. Use the API export instead" >&2
  fi
done

On the measured v0.3.2 install the .nzbz decompressed as a gzip copy of the imported NZB. The source also carries a zstd-protobuf path for these files so the zcat and grep check is not paranoia. Anything it rejects goes through the API instead. GET /api/files/export-nzb?path=<virtual path> regenerates a faithful NZB from the store and POST /api/files/export-batch returns a ZIP. Both need a JWT rather than the API key.

curl -c jar -s -X POST http://localhost:8585/api/auth/login \
  -H 'Content-Type: application/json' -d '{"username":"admin","password":"…"}'
curl -b jar -s -G http://localhost:8585/api/files/export-nzb \
  --data-urlencode "path=/complete/<Release>/<file>" -o "$OUT/<Release>.nzb"

Take virtual paths from the AltMount UI's file view or from /api/files/info rather than guessing the format. The download is named <queueID>-<name>.nzb. Rename it to <Release>.nzb so zurg names the folder correctly.

One sanity check. The recovered .nzb count should equal the metadata/complete/*/ directory count.

Build zurg alongside

AltMount keeps serving throughout. Run zurg on port 9999. There is no clash with AltMount's 8585.

zurg: v1
providers:
  - type: nzb
    nntp:
      host: news.example.com   # copy host/port/TLS/credentials from AltMount
      port: 563
      tls: true
      username: USER
      password: PASS
      connections: 30          # your plan's real allowance
rclone_enabled: true
rclone_binary: bin/rclone
mount_path: "/mnt/zurg"
mkdir -p nzbs && cp recovered-nzbs/*.nzb nzbs/
./zurg
ls /mnt/zurg/__all__/ | wc -l

Expect more folders here than AltMount had. That is the refused-import class arriving.

Cut over

One path whatever you have today. Nothing here moves bytes.

1. Turn on __magic__ and restart. The routes are registered at startup so a dashboard toggle needs a restart before the namespace answers.

magic:
  enabled: true

2. Make the root folders. They go at the root of __magic__, beside the __all__ zurg keeps there. Not __magic__/__all__, not __magic__ itself and not the mount root. Both *arrs raise a health check for those; siblings trip neither.

mkdir -p /mnt/zurg/__magic__/tv /mnt/zurg/__magic__/movies
ls /mnt/zurg/__magic__/__all__/   # your releases are already listed here

__magic__/__all__ mirrors the library so every release is present before you organise anything.

3. Organise it. A mv inside __magic__ writes a row and moves no bytes.

mv "/mnt/zurg/__magic__/__all__/Some.Release.S01E01.1080p/ep1.mkv" \
   "/mnt/zurg/__magic__/tv/The Show/Season 01/S01E01.mkv"

If Sonarr and Radarr organised your old library then move it in with one script. It renames every file the old library links to into a new root folder under __magic__. Each one lands at the path the *arr already expects. Nothing is copied. Then you switch the *arr's root folder without letting it move anything. The steps and the script are on the shared page. Never let an *arr move the files itself. That crosses the mount boundary. It is a copy and it downloads your library.

The script needs a zurg_path function for AltMount's links. Save this as zurg_path.sh beside it and set OLDMOUNT to where AltMount is mounted.

OLDMOUNT=/mnt/altmount          # AltMount's mount, where the old links point
zurg_path() {
  case "$1" in "$OLDMOUNT"/*) ;; *) return ;; esac   # not a link into AltMount
  local name=${1##*/} f videos
  local release; release=$(basename "$(dirname "$1")")
  [ -f "$ZURG/__all__/$release/$name" ] && { echo "$ZURG/__all__/$release/$name"; return; }
  # AltMount named a lone video or an unpacked archive after the release. Take its one video.
  case "$name" in "$release".*) ;; *) return ;; esac
  videos=$(find "$ZURG/__all__/$release" -type f \( -iname '*.mkv' -o -iname '*.mp4' -o -iname '*.avi' \) ! -ipath '*sample*')
  [ "$(printf '%s\n' "$videos" | grep -c .)" -eq 1 ] && echo "$videos"
}

The function expects the release folder just above the file in each link's target. The exact path AltMount's links use was not checked for this guide. Run readlink on one link first. Where AltMount named a lone video or an unpacked archive after the release the function takes the one video zurg serves in that release. A release with more than one video is never guessed at. It prints NOT IN ZURG instead.

4. Point Plex at the root folders you made — __magic__/tv and __magic__/movies — and at those only. Never the root of __magic__, which holds the whole library at __magic__/__all__ as well, and never __magic__ and a filter directory, or every episode is found twice. Confirm autoEmptyTrash is 0. Add the new location and scan. Then remove the old one. That is your *arr library path or the AltMount mount if Plex read it directly.

5. Empty the trash once you have spot-checked a few items. Watch state comes back through the GUID match. Collections and artwork choices do not.

Leave AltMount running until then. It costs nothing and it is your rollback. When you are done stop it.

sudo systemctl stop altmount

Afterwards

Do not decommission AltMount until you have streamed from a representative sample. Its config directory holds the .nzbz store and that is the only other copy of your NZBs.

Now think about acquisition. zurg has an opt-in SABnzbd-compatible endpoint that Sonarr and Radarr can grab through. It replaces AltMount as their download client. zurg checks each grab before it reports it finished. It asks the news servers about the start and the end of every file. It also downloads the first article of each file. That is up to sixty-four of them plus a few more of a release with only a few files. Some news servers still say an article exists after its content was taken down and only a download shows that. zurg rebuilds a release with articles gone from its PAR2 files where it can. One it cannot rebuild is reported Failed. The *arr then blocklists it and grabs another. The check is a sample and not the whole post. A post that lost a stretch the sample did not reach still reports Completed and fails on the read that reaches the gap. Keep the *arrs' root folders inside __magic__. A root folder anywhere else gets a full copy of every import. In Docker give the *arr one volume that holds zurg's whole mount. A download folder and root folders on two volumes copy every import too. See one volume.