Migrating from nzbdav 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.

nzbdav-dev's last release is v0.6.4 from April 2026 and the upstream repository is no longer maintained. v0.6.4 itself works fine. So this migration is about getting off unmaintained software rather than about broken content. Take your time. Nothing here needs a rushed cutover.

This is the easiest of the five migrations. Measured against zurg on the same five releases nzbdav's tree differs by exactly two things.

   
zurg gains the release's .nfo file
zurg loses 5 hash-named par2 volumes inside obfuscated releases

Nothing is renamed. Every filename nzbdav serves is a filename zurg serves under the same name and in the same directory shape. That holds inside RAR archives too. The two servers agreed on all 20 non-par2 files in the corpus.

What your library looks like afterwards

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

The good news is that nzbdav is the one migration where nothing you see changes but the prefix. Every filename zurg serves is the one nzbdav served. An *arr re-importing from __magic__ parses exactly what it parsed before. A folder you organise by hand looks the same as it always did.

That holds whichever setup you have. On Windows and anywhere else Plex reads the mount directly the filenames are identical. If Sonarr and Radarr organise a symlink library then the filenames were already theirs and the source they parse is unchanged. So this migration alters nothing about how your library reads. It only changes where it lives.

What new content shows up

  • The .nfo inside RAR releases.

  • Sample clips inside Sample/. nzbdav publishes the directory and the clip and so does zurg. This one is not new for you. nzbdav is the only server of the four that already had it.

  • Empty folders for releases whose archive chain is broken. nzbdav created nothing for those.

  • par2 and sfv stop being visible.

None of it produces a Plex item except the sample clip and nzbdav already gave you that. If it was not bothering you before then nothing changes.

Get your NZBs out

zurg is fed by a directory of .nzb files and nzbdav still has all of yours at /nzbs/<category>/. Copy them. Do not move them. Deleting from /nzbs/ cancels the job.

mkdir -p ~/migration/nzbs
for cat in uncategorized movies tv audio software; do
  rclone copy "nzbdav:/nzbs/$cat" ~/migration/nzbs/ --include '*.nzb'
done
ls ~/migration/nzbs | wc -l

Both servers name the release folder after the NZB filename. So the stems you recovered should equal the folders nzbdav serves. Check that and rename any that drifted.

rclone lsf "nzbdav:/content/movies" --dirs-only | sed 's:/$::' | sort > ~/migration/folders-old.txt
ls ~/migration/nzbs | sed 's/\.nzb$//' | sort > ~/migration/stems.txt
diff ~/migration/folders-old.txt ~/migration/stems.txt

Two zurg naming rules are worth checking against that list.

  • zurg strips a trailing .mkv or .mp4 from a folder name. If any stem ends that way then set retain_folder_name_extension: true.

  • Two different releases sharing a stem get a {shorthash} suffix on one of them. Find them now with ls ~/migration/nzbs | sed 's/\.nzb$//' | sort | uniq -d.

Build zurg alongside

nzbdav keeps serving throughout. Copy its provider block into zurg's config. Give zurg its own mountpoint. Drop the NZBs in and let it scan. The watch directory is picked up every 15 seconds or so.

zurg: v1
providers:
  - type: nzb
    nntp:
      host: news.example.com   # copy from nzbdav's provider settings
      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 ~/migration/nzbs/*.nzb nzbs/
./zurg
ls /mnt/zurg/__all__/ | wc -l     # should approach your release count

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 nzbdav's links. Save this as zurg_path.sh beside it and set OLDMOUNT to where nzbdav is mounted.

OLDMOUNT=/mnt/nzbdav            # nzbdav's mount, where the old links point
# The map from each .ids name to its release and file. Built once while nzbdav runs.
[ -s uuid-map.tsv ] || find "$OLDMOUNT/completed-symlinks" -type l | while IFS= read -r l; do
  rel=${l#"$OLDMOUNT"/completed-symlinks/}      # <category>/<release>/<file>
  printf '%s\t%s\n' "$(basename "$(readlink "$l")")" "${rel#*/}"
done | sort -u > uuid-map.tsv
zurg_path() {
  local rel release name videos
  rel=$(awk -F'\t' -v u="$(basename "$1")" '$1==u{print $2; exit}' uuid-map.tsv)
  [ -n "$rel" ] || return
  [ -f "$ZURG/__all__/$rel" ] && { echo "$ZURG/__all__/$rel"; return; }
  # A file under another name. Take the release's one video.
  release=${rel%%/*} name=${rel##*/}
  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"
}

nzbdav's links point into its .ids store and zurg has no such thing. So the function reads nzbdav's completed-symlinks tree once to learn which release and file each .ids name stands for. Keep nzbdav running until the script is done. nzbdav renames nothing so every name it finds is one zurg serves.

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 nzbdav 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 nzbdav running until then. It costs nothing and it is your rollback. When you are done stop it.

sudo systemctl stop nzbdav

Afterwards

Keep nzbdav's config directory until you have streamed from a representative sample. It 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 nzbdav as their download client. The grab lands in __magic__/__all__ and the import moves it out by rename. Nothing is copied. 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.