diff --git a/NEWS b/NEWS
index 5f68398f1..18f6adad4 100644
--- a/NEWS
+++ b/NEWS
@@ -6,6 +6,7 @@ ver 0.23 (not yet released)
   - proxy: require MPD 0.20 or later
   - proxy: require libmpdclient 2.11 or later
   - proxy: split search into chunks to avoid exceeding the output buffer
+  - upnp: support libnpupnp instead of libupnp
 * output
   - pipewire: new plugin
   - snapcast: new plugin
diff --git a/meson_options.txt b/meson_options.txt
index 23850eb29..aaf978f92 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -62,7 +62,10 @@ option('dsd', type: 'boolean', value: true, description: 'Support the DSD audio
 #
 
 option('database', type: 'boolean', value: true, description: 'enable support for the music database')
-option('upnp', type: 'feature', description: 'UPnP client support')
+option('upnp', type: 'combo',
+       choices: ['auto', 'pupnp', 'npupnp', 'disabled'],
+       value: 'auto',
+       description: 'UPnP client support')
 option('libmpdclient', type: 'feature', description: 'libmpdclient support (for the proxy database plugin)')
 
 #
diff --git a/src/db/plugins/upnp/ContentDirectoryService.cxx b/src/db/plugins/upnp/ContentDirectoryService.cxx
index 5301c0f35..720940521 100644
--- a/src/db/plugins/upnp/ContentDirectoryService.cxx
+++ b/src/db/plugins/upnp/ContentDirectoryService.cxx
@@ -18,7 +18,10 @@
  */
 
 #include "lib/upnp/ContentDirectoryService.hxx"
-#include "lib/upnp/ixmlwrap.hxx"
+#include "config.h"
+#ifdef USING_PUPNP
+#	include "lib/upnp/ixmlwrap.hxx"
+#endif
 #include "lib/upnp/UniqueIxml.hxx"
 #include "lib/upnp/Action.hxx"
 #include "Directory.hxx"
@@ -27,6 +30,9 @@
 #include "util/ScopeExit.hxx"
 #include "util/StringFormat.hxx"
 
+#include <algorithm>
+
+#ifdef USING_PUPNP
 static void
 ReadResultTag(UPnPDirContent &dirbuf, IXML_Document *response)
 {
@@ -36,6 +42,7 @@ ReadResultTag(UPnPDirContent &dirbuf, IXML_Document *response)
 
 	dirbuf.Parse(p);
 }
+#endif
 
 inline void
 ContentDirectoryService::readDirSlice(UpnpClient_Handle hdl,
@@ -44,6 +51,7 @@ ContentDirectoryService::readDirSlice(UpnpClient_Handle hdl,
 				      unsigned &didreadp,
 				      unsigned &totalp) const
 {
+#ifdef USING_PUPNP
 	// Some devices require an empty SortCriteria, else bad params
 	IXML_Document *request =
 		MakeActionHelper("Browse", m_serviceType.c_str(),
@@ -79,6 +87,35 @@ ContentDirectoryService::readDirSlice(UpnpClient_Handle hdl,
 		totalp = ParseUnsigned(value);
 
 	ReadResultTag(dirbuf, response);
+#else
+	std::vector<std::pair<std::string, std::string>> actionParams{
+		{"ObjectID", objectId},
+		{"BrowseFlag", "BrowseDirectChildren"},
+		{"Filter", "*"},
+		{"SortCriteria", ""},
+		{"StartingIndex", StringFormat<32>("%u", offset).c_str()},
+		{"RequestedCount", StringFormat<32>("%u", count).c_str()}};
+	std::vector<std::pair<std::string, std::string>> responseData;
+	int errcode;
+	std::string errdesc;
+	int code = UpnpSendAction(hdl, "", m_actionURL, m_serviceType, "Browse",
+				  actionParams, responseData, &errcode, errdesc);
+	if (code != UPNP_E_SUCCESS)
+		throw FormatRuntimeError("UpnpSendAction() failed: %s",
+					 UpnpGetErrorMessage(code));
+	const char *p = "";
+	didreadp = 0;
+	for (const auto &entry : responseData) {
+		if (entry.first == "Result") {
+			p = entry.second.c_str();
+		} else if (entry.first == "TotalMatches") {
+			totalp = ParseUnsigned(entry.second.c_str());
+		} else if (entry.first == "NumberReturned") {
+			didreadp = ParseUnsigned(entry.second.c_str());
+		}
+	}
+	dirbuf.Parse(p);
+#endif
 }
 
 UPnPDirContent
@@ -107,6 +144,7 @@ ContentDirectoryService::search(UpnpClient_Handle hdl,
 	unsigned offset = 0, total = -1, count;
 
 	do {
+#ifdef USING_PUPNP
 		UniqueIxmlDocument request(MakeActionHelper("Search", m_serviceType.c_str(),
 							    "ContainerID", objectId,
 							    "SearchCriteria", ss,
@@ -144,6 +182,36 @@ ContentDirectoryService::search(UpnpClient_Handle hdl,
 			total = ParseUnsigned(value);
 
 		ReadResultTag(dirbuf, response.get());
+#else
+		std::vector<std::pair<std::string, std::string>> actionParams{
+			{"ContainerID", objectId},
+			{"SearchCriteria", ss},
+			{"Filter", "*"},
+			{"SortCriteria", ""},
+			{"StartingIndex", StringFormat<32>("%u", offset).c_str()},
+			{"RequestedCount", "0"}};
+		std::vector<std::pair<std::string, std::string>> responseData;
+		int errcode;
+		std::string errdesc;
+		int code = UpnpSendAction(hdl, "", m_actionURL, m_serviceType, "Search",
+					  actionParams, responseData, &errcode, errdesc);
+		if (code != UPNP_E_SUCCESS)
+			throw FormatRuntimeError("UpnpSendAction() failed: %s",
+						 UpnpGetErrorMessage(code));
+		const char *p = "";
+		count = 0;
+		for (const auto &entry : responseData) {
+			if (entry.first == "Result") {
+				p = entry.second.c_str();
+			} else if (entry.first == "TotalMatches") {
+				total = ParseUnsigned(entry.second.c_str());
+			} else if (entry.first == "NumberReturned") {
+				count = ParseUnsigned(entry.second.c_str());
+				offset += count;
+			}
+		}
+		dirbuf.Parse(p);
+#endif
 	} while (count > 0 && offset < total);
 
 	return dirbuf;
@@ -153,6 +221,7 @@ UPnPDirContent
 ContentDirectoryService::getMetadata(UpnpClient_Handle hdl,
 				     const char *objectId) const
 {
+#ifdef USING_PUPNP
 	// Create request
 	UniqueIxmlDocument request(MakeActionHelper("Browse", m_serviceType.c_str(),
 						    "ObjectID", objectId,
@@ -176,4 +245,27 @@ ContentDirectoryService::getMetadata(UpnpClient_Handle hdl,
 	UPnPDirContent dirbuf;
 	ReadResultTag(dirbuf, response.get());
 	return dirbuf;
+#else
+	std::vector<std::pair<std::string, std::string>> actionParams{
+		{"ObjectID", objectId}, {"BrowseFlag", "BrowseMetadata"},
+		{"Filter", "*"},	{"SortCriteria", ""},
+		{"StartingIndex", "0"}, {"RequestedCount", "1"}};
+	std::vector<std::pair<std::string, std::string>> responseData;
+	int errcode;
+	std::string errdesc;
+	int code = UpnpSendAction(hdl, "", m_actionURL, m_serviceType, "Browse",
+				  actionParams, responseData, &errcode, errdesc);
+	if (code != UPNP_E_SUCCESS)
+		throw FormatRuntimeError("UpnpSendAction() failed: %s",
+					 UpnpGetErrorMessage(code));
+	const char *p = "";
+	for (const auto &entry : responseData) {
+		if (entry.first == "Result") {
+			p = entry.second.c_str();
+		}
+	}
+	UPnPDirContent dirbuf;
+	dirbuf.Parse(p);
+	return dirbuf;
+#endif
 }
diff --git a/src/lib/upnp/Action.hxx b/src/lib/upnp/Action.hxx
index 8ff8d1d70..45765ae08 100644
--- a/src/lib/upnp/Action.hxx
+++ b/src/lib/upnp/Action.hxx
@@ -38,6 +38,7 @@ CountNameValuePairs([[maybe_unused]] const char *name, [[maybe_unused]] const ch
 	return 1 + CountNameValuePairs(args...);
 }
 
+#ifdef USING_PUPNP
 /**
  * A wrapper for UpnpMakeAction() that counts the number of name/value
  * pairs and adds the nullptr sentinel.
@@ -52,5 +53,6 @@ MakeActionHelper(const char *action_name, const char *service_type,
 			      args...,
 			      nullptr, nullptr);
 }
+#endif
 
 #endif
diff --git a/src/lib/upnp/ContentDirectoryService.cxx b/src/lib/upnp/ContentDirectoryService.cxx
index f887c7469..679243cd6 100644
--- a/src/lib/upnp/ContentDirectoryService.cxx
+++ b/src/lib/upnp/ContentDirectoryService.cxx
@@ -17,13 +17,21 @@
  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  */
 
+#include "config.h"
+
 #include "ContentDirectoryService.hxx"
-#include "UniqueIxml.hxx"
 #include "Device.hxx"
-#include "ixmlwrap.hxx"
-#include "util/UriRelative.hxx"
-#include "util/RuntimeError.hxx"
+#include "UniqueIxml.hxx"
+#ifdef USING_PUPNP
+#	include "ixmlwrap.hxx"
+#endif
+#include "Action.hxx"
 #include "util/IterableSplitString.hxx"
+#include "util/RuntimeError.hxx"
+#include "util/UriRelative.hxx"
+#include "util/UriUtil.hxx"
+
+#include <algorithm>
 
 #include <upnptools.h>
 
@@ -50,6 +58,7 @@ ContentDirectoryService::~ContentDirectoryService() noexcept = default;
 std::forward_list<std::string>
 ContentDirectoryService::getSearchCapabilities(UpnpClient_Handle hdl) const
 {
+#ifdef USING_PUPNP
 	UniqueIxmlDocument request(UpnpMakeAction("GetSearchCapabilities", m_serviceType.c_str(),
 						  0,
 						  nullptr, nullptr));
@@ -68,6 +77,23 @@ ContentDirectoryService::getSearchCapabilities(UpnpClient_Handle hdl) const
 
 	const char *s = ixmlwrap::getFirstElementValue(response.get(),
 						       "SearchCaps");
+#else
+	std::vector<std::pair<std::string, std::string>> responseData;
+	int errcode;
+	std::string errdesc;
+	auto code = UpnpSendAction(hdl, "", m_actionURL, m_serviceType,
+				   "GetSearchCapabilities", {}, responseData, &errcode,
+				   errdesc);
+	if (code != UPNP_E_SUCCESS)
+		throw FormatRuntimeError("UpnpSendAction() failed: %s",
+					 UpnpGetErrorMessage(code));
+	const char *s{nullptr};
+	for (auto &entry : responseData) {
+		if (entry.first == "SearchCaps") {
+			s = entry.second.c_str();
+		}
+	}
+#endif
 	if (s == nullptr || *s == 0)
 		return {};
 
diff --git a/src/lib/upnp/Init.cxx b/src/lib/upnp/Init.cxx
index 05396a402..f9f0c8515 100644
--- a/src/lib/upnp/Init.cxx
+++ b/src/lib/upnp/Init.cxx
@@ -23,7 +23,9 @@
 #include "util/RuntimeError.hxx"
 
 #include <upnptools.h>
-#include <ixml.h>
+#ifdef USING_PUPNP
+#	include <ixml.h>
+#endif
 
 #include <cassert>
 
@@ -44,8 +46,10 @@ DoInit()
 
 	UpnpSetMaxContentLength(2000*1024);
 
+#ifdef USING_PUPNP
 	// Servers sometimes make error (e.g.: minidlna returns bad utf-8)
 	ixmlRelaxParser(1);
+#endif
 }
 
 void
diff --git a/src/lib/upnp/UniqueIxml.hxx b/src/lib/upnp/UniqueIxml.hxx
index 9e0b94a9e..60da03b75 100644
--- a/src/lib/upnp/UniqueIxml.hxx
+++ b/src/lib/upnp/UniqueIxml.hxx
@@ -20,9 +20,10 @@
 #ifndef MPD_UPNP_UNIQUE_XML_HXX
 #define MPD_UPNP_UNIQUE_XML_HXX
 
-#include <ixml.h>
+#ifdef USING_PUPNP
+#	include <ixml.h>
 
-#include <memory>
+#	include <memory>
 
 struct UpnpIxmlDeleter {
 	void operator()(IXML_Document *doc) noexcept {
@@ -37,4 +38,5 @@ struct UpnpIxmlDeleter {
 typedef std::unique_ptr<IXML_Document, UpnpIxmlDeleter> UniqueIxmlDocument;
 typedef std::unique_ptr<IXML_NodeList, UpnpIxmlDeleter> UniqueIxmlNodeList;
 
+#endif /* USING_PUPNP */
 #endif
diff --git a/src/lib/upnp/ixmlwrap.cxx b/src/lib/upnp/ixmlwrap.cxx
index f811b07a6..90e8d3155 100644
--- a/src/lib/upnp/ixmlwrap.cxx
+++ b/src/lib/upnp/ixmlwrap.cxx
@@ -15,8 +15,11 @@
  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
  */
 
-#include "ixmlwrap.hxx"
-#include "UniqueIxml.hxx"
+#include "config.h"
+
+#ifdef USING_PUPNP
+#	include "ixmlwrap.hxx"
+#	include "UniqueIxml.hxx"
 
 namespace ixmlwrap {
 
@@ -39,3 +42,4 @@ getFirstElementValue(IXML_Document *doc, const char *name) noexcept
 }
 
 } // namespace ixmlwrap
+#endif
diff --git a/src/lib/upnp/ixmlwrap.hxx b/src/lib/upnp/ixmlwrap.hxx
index 2ef142ac7..25ac8695d 100644
--- a/src/lib/upnp/ixmlwrap.hxx
+++ b/src/lib/upnp/ixmlwrap.hxx
@@ -17,7 +17,8 @@
 #ifndef _IXMLWRAP_H_INCLUDED_
 #define _IXMLWRAP_H_INCLUDED_
 
-#include <ixml.h>
+#ifdef USING_PUPNP
+#	include <ixml.h>
 
 namespace ixmlwrap {
 	/**
@@ -30,4 +31,5 @@ namespace ixmlwrap {
 
 }
 
+#endif /* USING_PUPNP */
 #endif /* _IXMLWRAP_H_INCLUDED_ */
diff --git a/src/lib/upnp/meson.build b/src/lib/upnp/meson.build
index f7570eb1c..bdc248e6c 100644
--- a/src/lib/upnp/meson.build
+++ b/src/lib/upnp/meson.build
@@ -1,4 +1,22 @@
-upnp_dep = dependency('libupnp', version: '>= 1.8', required: get_option('upnp'))
+upnp_option = get_option('upnp')
+
+if upnp_option == 'auto'
+  upnp_dep = dependency('libupnp', version: '>= 1.8', required: false)
+  conf.set('USING_PUPNP', upnp_dep.found())
+  if not upnp_dep.found()
+    upnp_dep = dependency('libnpupnp', version: '>= 1.8', required: false)
+  endif
+elif upnp_option == 'pupnp'
+  upnp_dep = dependency('libupnp', version: '>= 1.8', required: true)
+  conf.set('USING_PUPNP', true)
+elif upnp_option == 'npupnp'
+  upnp_dep = dependency('libnpupnp', required: true)
+  conf.set('USING_PUPNP', false)
+elif upnp_option == 'disabled'
+  upnp_dep = dependency('', required: false)
+  subdir_done()
+endif
+
 conf.set('ENABLE_UPNP', upnp_dep.found())
 if not upnp_dep.found()
   subdir_done()