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.
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.
Never walk the WebDAV root
nzbdav's .ids object store materialises all 16 hex children at every one of
its five levels regardless of contents. That is 16^5 or roughly 1,048,576
directories. Point find and du and rclone and any scanner at /content
or /nzbs or /completed-symlinks explicitly. Never at /.
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
.nfoinside 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
.mkvor.mp4from a folder name. If any stem ends that way then setretain_folder_name_extension: true. -
Two different releases sharing a stem get a
{shorthash}suffix on one of them. Find them now withls ~/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.