diff --git a/NEWS b/NEWS
index 28a9acb36..3d6f0a112 100644
--- a/NEWS
+++ b/NEWS
@@ -4,6 +4,7 @@ ver 0.22.10 (not yet released)
 * database
   - simple: fix crash bug
   - simple: fix absolute paths in CUE "as_directory" entries
+  - simple: prune CUE entries from database for non-existent songs
 * input
   - curl: fix crash bug after stream with Icy metadata was closed by peer
   - tidal: remove defunct unmaintained plugin
diff --git a/src/db/plugins/simple/Directory.cxx b/src/db/plugins/simple/Directory.cxx
index 4c7bb2873..b03994730 100644
--- a/src/db/plugins/simple/Directory.cxx
+++ b/src/db/plugins/simple/Directory.cxx
@@ -109,6 +109,23 @@ Directory::FindChild(std::string_view name) const noexcept
 	return nullptr;
 }
 
+bool
+Directory::TargetExists(std::string_view _target) const noexcept
+{
+	StringView target{_target};
+
+	if (target.SkipPrefix("../")) {
+		if (parent == nullptr)
+			return false;
+
+		return parent->TargetExists(target);
+	}
+
+	/* sorry for the const_cast ... */
+	const auto lr = const_cast<Directory *>(this)->LookupDirectory(target);
+	return lr.directory->FindSong(lr.rest) != nullptr;
+}
+
 void
 Directory::PruneEmpty() noexcept
 {
diff --git a/src/db/plugins/simple/Directory.hxx b/src/db/plugins/simple/Directory.hxx
index 6b8899440..87e8351e7 100644
--- a/src/db/plugins/simple/Directory.hxx
+++ b/src/db/plugins/simple/Directory.hxx
@@ -118,13 +118,17 @@ public:
 		return new Directory(std::string(), nullptr);
 	}
 
+	bool IsPlaylist() const noexcept {
+		return device == DEVICE_PLAYLIST;
+	}
+
 	/**
 	 * Is this really a regular file which is being treated like a
 	 * directory?
 	 */
 	bool IsReallyAFile() const noexcept {
 		return device == DEVICE_INARCHIVE ||
-			device == DEVICE_PLAYLIST ||
+			IsPlaylist() ||
 			device == DEVICE_CONTAINER;
 	}
 
@@ -210,6 +214,9 @@ public:
 	gcc_pure
 	LookupResult LookupDirectory(std::string_view uri) noexcept;
 
+	[[gnu::pure]]
+	bool TargetExists(std::string_view target) const noexcept;
+
 	gcc_pure
 	bool IsEmpty() const noexcept {
 		return children.empty() &&
diff --git a/src/db/update/Walk.cxx b/src/db/update/Walk.cxx
index b1fb675e6..c5fb8230d 100644
--- a/src/db/update/Walk.cxx
+++ b/src/db/update/Walk.cxx
@@ -133,6 +133,28 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory) noexcept
 	}
 }
 
+void
+UpdateWalk::PurgeDanglingFromPlaylists(Directory &directory) noexcept
+{
+	/* recurse */
+	for (Directory &child : directory.children)
+		PurgeDanglingFromPlaylists(child);
+
+	if (!directory.IsPlaylist())
+		/* this check is only for virtual directories
+		   representing a playlist file */
+		return;
+
+	directory.ForEachSongSafe([&](Song &song){
+		if (!song.target.empty() &&
+		    !PathTraitsUTF8::IsAbsoluteOrHasScheme(song.target.c_str()) &&
+		    !directory.TargetExists(song.target)) {
+			editor.DeleteSong(directory, &song);
+			modified = true;
+		}
+	});
+}
+
 #ifndef _WIN32
 static bool
 update_directory_stat(Storage &storage, Directory &directory) noexcept
@@ -530,5 +552,10 @@ UpdateWalk::Walk(Directory &root, const char *path, bool discard) noexcept
 		UpdateDirectory(root, exclude_list, info);
 	}
 
+	{
+		const ScopeDatabaseLock protect;
+		PurgeDanglingFromPlaylists(root);
+	}
+
 	return modified;
 }
diff --git a/src/db/update/Walk.hxx b/src/db/update/Walk.hxx
index 433d1a3a1..984230ce9 100644
--- a/src/db/update/Walk.hxx
+++ b/src/db/update/Walk.hxx
@@ -85,6 +85,12 @@ private:
 
 	void PurgeDeletedFromDirectory(Directory &directory) noexcept;
 
+	/**
+	 * Remove all virtual songs inside playlists whose "target"
+	 * field points to a non-existing song file.
+	 */
+	void PurgeDanglingFromPlaylists(Directory &directory) noexcept;
+
 	void UpdateSongFile2(Directory &directory,
 			     const char *name, const char *suffix,
 			     const StorageFileInfo &info) noexcept;