From e5f23678ca1bdaaff3dba81af89d5d3fe9be594a Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Wed, 22 May 2019 18:24:45 +0200
Subject: [PATCH 1/6] Log: use GetFullMessage() to print exceptions

Print all nested exceptions on a single line to avoid confusion.
---
 src/Log.cxx | 44 ++++++--------------------------------------
 1 file changed, 6 insertions(+), 38 deletions(-)

diff --git a/src/Log.cxx b/src/Log.cxx
index 622f79b69..6e47512c4 100644
--- a/src/Log.cxx
+++ b/src/Log.cxx
@@ -19,8 +19,7 @@
 
 #include "LogV.hxx"
 #include "util/Domain.hxx"
-
-#include <exception>
+#include "util/Exception.hxx"
 
 #include <stdio.h>
 #include <string.h>
@@ -94,31 +93,13 @@ FormatError(const Domain &domain, const char *fmt, ...) noexcept
 void
 LogError(const std::exception &e) noexcept
 {
-	Log(exception_domain, LogLevel::ERROR, e.what());
-
-	try {
-		std::rethrow_if_nested(e);
-	} catch (const std::exception &nested) {
-		LogError(nested, "nested");
-	} catch (...) {
-		Log(exception_domain, LogLevel::ERROR,
-		    "Unrecognized nested exception");
-	}
+	LogError(exception_domain, GetFullMessage(e).c_str());
 }
 
 void
 LogError(const std::exception &e, const char *msg) noexcept
 {
-	FormatError(exception_domain, "%s: %s", msg, e.what());
-
-	try {
-		std::rethrow_if_nested(e);
-	} catch (const std::exception &nested) {
-		LogError(nested);
-	} catch (...) {
-		Log(exception_domain, LogLevel::ERROR,
-		    "Unrecognized nested exception");
-	}
+	FormatError(exception_domain, "%s: %s", msg, GetFullMessage(e).c_str());
 }
 
 void
@@ -136,27 +117,14 @@ FormatError(const std::exception &e, const char *fmt, ...) noexcept
 void
 LogError(const std::exception_ptr &ep) noexcept
 {
-	try {
-		std::rethrow_exception(ep);
-	} catch (const std::exception &e) {
-		LogError(e);
-	} catch (...) {
-		Log(exception_domain, LogLevel::ERROR,
-		    "Unrecognized exception");
-	}
+	LogError(exception_domain, GetFullMessage(ep).c_str());
 }
 
 void
 LogError(const std::exception_ptr &ep, const char *msg) noexcept
 {
-	try {
-		std::rethrow_exception(ep);
-	} catch (const std::exception &e) {
-		LogError(e, msg);
-	} catch (...) {
-		FormatError(exception_domain,
-			    "%s: Unrecognized exception", msg);
-	}
+	FormatError(exception_domain, "%s: %s", msg,
+		    GetFullMessage(ep).c_str());
 }
 
 void

From 36e6079c57787a3599de8be80e508eb497d0de1e Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Wed, 22 May 2019 18:38:25 +0200
Subject: [PATCH 2/6] Log: make LogLevel the first parameter

Prepare for templated functions.
---
 src/Log.cxx                                   | 22 +++++++++----------
 src/Log.hxx                                   | 16 +++++++-------
 src/LogBackend.cxx                            |  4 ++--
 src/LogBackend.hxx                            |  2 +-
 src/LogV.hxx                                  |  4 ++--
 .../plugins/FluidsynthDecoderPlugin.cxx       |  4 ++--
 src/lib/ffmpeg/LogCallback.cxx                |  2 +-
 src/system/FatalError.cxx                     |  2 +-
 test/test_translate_song.cxx                  |  2 +-
 9 files changed, 29 insertions(+), 29 deletions(-)

diff --git a/src/Log.cxx b/src/Log.cxx
index 6e47512c4..befe238c9 100644
--- a/src/Log.cxx
+++ b/src/Log.cxx
@@ -1,5 +1,5 @@
 /*
- * Copyright 2003-2018 The Music Player Daemon Project
+ * Copyright 2003-2019 The Music Player Daemon Project
  * http://www.musicpd.org
  *
  * This program is free software; you can redistribute it and/or modify
@@ -28,20 +28,20 @@
 static constexpr Domain exception_domain("exception");
 
 void
-LogFormatV(const Domain &domain, LogLevel level,
+LogFormatV(LogLevel level, const Domain &domain,
 	   const char *fmt, va_list ap) noexcept
 {
 	char msg[1024];
 	vsnprintf(msg, sizeof(msg), fmt, ap);
-	Log(domain, level, msg);
+	Log(level, domain, msg);
 }
 
 void
-LogFormat(const Domain &domain, LogLevel level, const char *fmt, ...) noexcept
+LogFormat(LogLevel level, const Domain &domain, const char *fmt, ...) noexcept
 {
 	va_list ap;
 	va_start(ap, fmt);
-	LogFormatV(domain, level, fmt, ap);
+	LogFormatV(level, domain, fmt, ap);
 	va_end(ap);
 }
 
@@ -50,7 +50,7 @@ FormatDebug(const Domain &domain, const char *fmt, ...) noexcept
 {
 	va_list ap;
 	va_start(ap, fmt);
-	LogFormatV(domain, LogLevel::DEBUG, fmt, ap);
+	LogFormatV(LogLevel::DEBUG, domain, fmt, ap);
 	va_end(ap);
 }
 
@@ -59,7 +59,7 @@ FormatInfo(const Domain &domain, const char *fmt, ...) noexcept
 {
 	va_list ap;
 	va_start(ap, fmt);
-	LogFormatV(domain, LogLevel::INFO, fmt, ap);
+	LogFormatV(LogLevel::INFO, domain, fmt, ap);
 	va_end(ap);
 }
 
@@ -68,7 +68,7 @@ FormatDefault(const Domain &domain, const char *fmt, ...) noexcept
 {
 	va_list ap;
 	va_start(ap, fmt);
-	LogFormatV(domain, LogLevel::DEFAULT, fmt, ap);
+	LogFormatV(LogLevel::DEFAULT, domain, fmt, ap);
 	va_end(ap);
 }
 
@@ -77,7 +77,7 @@ FormatWarning(const Domain &domain, const char *fmt, ...) noexcept
 {
 	va_list ap;
 	va_start(ap, fmt);
-	LogFormatV(domain, LogLevel::WARNING, fmt, ap);
+	LogFormatV(LogLevel::WARNING, domain, fmt, ap);
 	va_end(ap);
 }
 
@@ -86,7 +86,7 @@ FormatError(const Domain &domain, const char *fmt, ...) noexcept
 {
 	va_list ap;
 	va_start(ap, fmt);
-	LogFormatV(domain, LogLevel::ERROR, fmt, ap);
+	LogFormatV(LogLevel::ERROR, domain, fmt, ap);
 	va_end(ap);
 }
 
@@ -142,7 +142,7 @@ FormatError(const std::exception_ptr &ep, const char *fmt, ...) noexcept
 void
 LogErrno(const Domain &domain, int e, const char *msg) noexcept
 {
-	LogFormat(domain, LogLevel::ERROR, "%s: %s", msg, strerror(e));
+	LogFormat(LogLevel::ERROR, domain, "%s: %s", msg, strerror(e));
 }
 
 void
diff --git a/src/Log.hxx b/src/Log.hxx
index 2e8c0c6ee..7454d96ea 100644
--- a/src/Log.hxx
+++ b/src/Log.hxx
@@ -1,5 +1,5 @@
 /*
- * Copyright 2003-2018 The Music Player Daemon Project
+ * Copyright 2003-2019 The Music Player Daemon Project
  * http://www.musicpd.org
  *
  * This program is free software; you can redistribute it and/or modify
@@ -28,16 +28,16 @@
 class Domain;
 
 void
-Log(const Domain &domain, LogLevel level, const char *msg) noexcept;
+Log(LogLevel level, const Domain &domain, const char *msg) noexcept;
 
 gcc_printf(3,4)
 void
-LogFormat(const Domain &domain, LogLevel level, const char *fmt, ...) noexcept;
+LogFormat(LogLevel level, const Domain &domain, const char *fmt, ...) noexcept;
 
 static inline void
 LogDebug(const Domain &domain, const char *msg) noexcept
 {
-	Log(domain, LogLevel::DEBUG, msg);
+	Log(LogLevel::DEBUG, domain, msg);
 }
 
 gcc_printf(2,3)
@@ -47,7 +47,7 @@ FormatDebug(const Domain &domain, const char *fmt, ...) noexcept;
 static inline void
 LogInfo(const Domain &domain, const char *msg) noexcept
 {
-	Log(domain, LogLevel::INFO, msg);
+	Log(LogLevel::INFO, domain, msg);
 }
 
 gcc_printf(2,3)
@@ -57,7 +57,7 @@ FormatInfo(const Domain &domain, const char *fmt, ...) noexcept;
 static inline void
 LogDefault(const Domain &domain, const char *msg) noexcept
 {
-	Log(domain, LogLevel::DEFAULT, msg);
+	Log(LogLevel::DEFAULT, domain, msg);
 }
 
 gcc_printf(2,3)
@@ -67,7 +67,7 @@ FormatDefault(const Domain &domain, const char *fmt, ...) noexcept;
 static inline void
 LogWarning(const Domain &domain, const char *msg) noexcept
 {
-	Log(domain, LogLevel::WARNING, msg);
+	Log(LogLevel::WARNING, domain, msg);
 }
 
 gcc_printf(2,3)
@@ -77,7 +77,7 @@ FormatWarning(const Domain &domain, const char *fmt, ...) noexcept;
 static inline void
 LogError(const Domain &domain, const char *msg) noexcept
 {
-	Log(domain, LogLevel::ERROR, msg);
+	Log(LogLevel::ERROR, domain, msg);
 }
 
 void
diff --git a/src/LogBackend.cxx b/src/LogBackend.cxx
index 874da1af0..f7760ee23 100644
--- a/src/LogBackend.cxx
+++ b/src/LogBackend.cxx
@@ -1,5 +1,5 @@
 /*
- * Copyright 2003-2018 The Music Player Daemon Project
+ * Copyright 2003-2019 The Music Player Daemon Project
  * http://www.musicpd.org
  *
  * This program is free software; you can redistribute it and/or modify
@@ -176,7 +176,7 @@ FileLog(const Domain &domain, const char *message) noexcept
 #endif /* !ANDROID */
 
 void
-Log(const Domain &domain, LogLevel level, const char *msg) noexcept
+Log(LogLevel level, const Domain &domain, const char *msg) noexcept
 {
 #ifdef ANDROID
 	__android_log_print(ToAndroidLogLevel(level), "MPD",
diff --git a/src/LogBackend.hxx b/src/LogBackend.hxx
index 76d7b1910..b90354755 100644
--- a/src/LogBackend.hxx
+++ b/src/LogBackend.hxx
@@ -1,5 +1,5 @@
 /*
- * Copyright 2003-2018 The Music Player Daemon Project
+ * Copyright 2003-2019 The Music Player Daemon Project
  * http://www.musicpd.org
  *
  * This program is free software; you can redistribute it and/or modify
diff --git a/src/LogV.hxx b/src/LogV.hxx
index 23e27d931..7e55d3d24 100644
--- a/src/LogV.hxx
+++ b/src/LogV.hxx
@@ -1,5 +1,5 @@
 /*
- * Copyright 2003-2018 The Music Player Daemon Project
+ * Copyright 2003-2019 The Music Player Daemon Project
  * http://www.musicpd.org
  *
  * This program is free software; you can redistribute it and/or modify
@@ -25,7 +25,7 @@
 #include <stdarg.h>
 
 void
-LogFormatV(const Domain &domain, LogLevel level,
+LogFormatV(LogLevel level, const Domain &domain,
 	   const char *fmt, va_list ap) noexcept;
 
 #endif /* LOG_H */
diff --git a/src/decoder/plugins/FluidsynthDecoderPlugin.cxx b/src/decoder/plugins/FluidsynthDecoderPlugin.cxx
index ad09c6f3d..905aa55be 100644
--- a/src/decoder/plugins/FluidsynthDecoderPlugin.cxx
+++ b/src/decoder/plugins/FluidsynthDecoderPlugin.cxx
@@ -70,8 +70,8 @@ fluidsynth_mpd_log_function(int level,
 			    char *message,
 			    void *)
 {
-	Log(fluidsynth_domain,
-	    fluidsynth_level_to_mpd(fluid_log_level(level)),
+	Log(fluidsynth_level_to_mpd(fluid_log_level(level)),
+	    fluidsynth_domain,
 	    message);
 }
 
diff --git a/src/lib/ffmpeg/LogCallback.cxx b/src/lib/ffmpeg/LogCallback.cxx
index f236b0eea..0dcdb5e6c 100644
--- a/src/lib/ffmpeg/LogCallback.cxx
+++ b/src/lib/ffmpeg/LogCallback.cxx
@@ -62,6 +62,6 @@ FfmpegLogCallback(gcc_unused void *ptr, int level, const char *fmt, va_list vl)
 					 ffmpeg_domain.GetName(),
 					 cls->item_name(ptr));
 		const Domain d(domain);
-		LogFormatV(d, FfmpegImportLogLevel(level), fmt, vl);
+		LogFormatV(FfmpegImportLogLevel(level), d, fmt, vl);
 	}
 }
diff --git a/src/system/FatalError.cxx b/src/system/FatalError.cxx
index b846dc830..dc056ca3c 100644
--- a/src/system/FatalError.cxx
+++ b/src/system/FatalError.cxx
@@ -54,7 +54,7 @@ FormatFatalError(const char *fmt, ...)
 {
 	va_list ap;
 	va_start(ap, fmt);
-	LogFormatV(fatal_error_domain, LogLevel::ERROR, fmt, ap);
+	LogFormatV(LogLevel::ERROR, fatal_error_domain, fmt, ap);
 	va_end(ap);
 
 	Abort();
diff --git a/test/test_translate_song.cxx b/test/test_translate_song.cxx
index 8c2b41ebb..6afcc3ca4 100644
--- a/test/test_translate_song.cxx
+++ b/test/test_translate_song.cxx
@@ -25,7 +25,7 @@
 #include <stdio.h>
 
 void
-Log(const Domain &domain, gcc_unused LogLevel level, const char *msg) noexcept
+Log(LogLevel, const Domain &domain, const char *msg) noexcept
 {
 	fprintf(stderr, "[%s] %s\n", domain.GetName(), msg);
 }

From e7c5a42821ebe7767f76aabebf1247fddd3c6376 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Thu, 23 May 2019 12:23:28 +0200
Subject: [PATCH 3/6] Log: add Log() and LogFormat() overloads with
 std::exception_ptr

Make LogError()/FormatError() wrappers for those.  Now we can log
exceptions with a lower level.
---
 src/Log.cxx | 26 ++++++++++----------
 src/Log.hxx | 69 ++++++++++++++++++++++++++++++++++++++++++-----------
 2 files changed, 68 insertions(+), 27 deletions(-)

diff --git a/src/Log.cxx b/src/Log.cxx
index befe238c9..3c29ef9a3 100644
--- a/src/Log.cxx
+++ b/src/Log.cxx
@@ -91,19 +91,19 @@ FormatError(const Domain &domain, const char *fmt, ...) noexcept
 }
 
 void
-LogError(const std::exception &e) noexcept
+Log(LogLevel level, const std::exception &e) noexcept
 {
-	LogError(exception_domain, GetFullMessage(e).c_str());
+	Log(level, exception_domain, GetFullMessage(e).c_str());
 }
 
 void
-LogError(const std::exception &e, const char *msg) noexcept
+Log(LogLevel level, const std::exception &e, const char *msg) noexcept
 {
-	FormatError(exception_domain, "%s: %s", msg, GetFullMessage(e).c_str());
+	LogFormat(level, exception_domain, "%s: %s", msg, GetFullMessage(e).c_str());
 }
 
 void
-FormatError(const std::exception &e, const char *fmt, ...) noexcept
+LogFormat(LogLevel level, const std::exception &e, const char *fmt, ...) noexcept
 {
 	char msg[1024];
 	va_list ap;
@@ -111,24 +111,24 @@ FormatError(const std::exception &e, const char *fmt, ...) noexcept
 	vsnprintf(msg, sizeof(msg), fmt, ap);
 	va_end(ap);
 
-	LogError(e, msg);
+	Log(level, e, msg);
 }
 
 void
-LogError(const std::exception_ptr &ep) noexcept
+Log(LogLevel level, const std::exception_ptr &ep) noexcept
 {
-	LogError(exception_domain, GetFullMessage(ep).c_str());
+	Log(level, exception_domain, GetFullMessage(ep).c_str());
 }
 
 void
-LogError(const std::exception_ptr &ep, const char *msg) noexcept
+Log(LogLevel level, const std::exception_ptr &ep, const char *msg) noexcept
 {
-	FormatError(exception_domain, "%s: %s", msg,
-		    GetFullMessage(ep).c_str());
+	LogFormat(level, exception_domain, "%s: %s", msg,
+		  GetFullMessage(ep).c_str());
 }
 
 void
-FormatError(const std::exception_ptr &ep, const char *fmt, ...) noexcept
+LogFormat(LogLevel level, const std::exception_ptr &ep, const char *fmt, ...) noexcept
 {
 	char msg[1024];
 	va_list ap;
@@ -136,7 +136,7 @@ FormatError(const std::exception_ptr &ep, const char *fmt, ...) noexcept
 	vsnprintf(msg, sizeof(msg), fmt, ap);
 	va_end(ap);
 
-	LogError(ep, msg);
+	Log(level, ep, msg);
 }
 
 void
diff --git a/src/Log.hxx b/src/Log.hxx
index 7454d96ea..f116ca439 100644
--- a/src/Log.hxx
+++ b/src/Log.hxx
@@ -34,6 +34,28 @@ gcc_printf(3,4)
 void
 LogFormat(LogLevel level, const Domain &domain, const char *fmt, ...) noexcept;
 
+void
+Log(LogLevel level, const std::exception &e) noexcept;
+
+void
+Log(LogLevel level, const std::exception &e, const char *msg) noexcept;
+
+gcc_printf(3,4)
+void
+LogFormat(LogLevel level, const std::exception &e,
+	  const char *fmt, ...) noexcept;
+
+void
+Log(LogLevel level, const std::exception_ptr &ep) noexcept;
+
+void
+Log(LogLevel level, const std::exception_ptr &ep, const char *msg) noexcept;
+
+gcc_printf(3,4)
+void
+LogFormat(LogLevel level, const std::exception_ptr &ep,
+	  const char *fmt, ...) noexcept;
+
 static inline void
 LogDebug(const Domain &domain, const char *msg) noexcept
 {
@@ -80,25 +102,44 @@ LogError(const Domain &domain, const char *msg) noexcept
 	Log(LogLevel::ERROR, domain, msg);
 }
 
-void
-LogError(const std::exception &e) noexcept;
+inline void
+LogError(const std::exception &e) noexcept
+{
+	Log(LogLevel::ERROR, e);
+}
 
-void
-LogError(const std::exception &e, const char *msg) noexcept;
+inline void
+LogError(const std::exception &e, const char *msg) noexcept
+{
+	Log(LogLevel::ERROR, e, msg);
+}
 
-gcc_printf(2,3)
-void
-FormatError(const std::exception &e, const char *fmt, ...) noexcept;
+template<typename... Args>
+inline void
+FormatError(const std::exception &e, const char *fmt, Args&&... args) noexcept
+{
+	LogFormat(LogLevel::ERROR, e, fmt, std::forward<Args>(args)...);
+}
 
-void
-LogError(const std::exception_ptr &ep) noexcept;
+inline void
+LogError(const std::exception_ptr &ep) noexcept
+{
+	Log(LogLevel::ERROR, ep);
+}
 
-void
-LogError(const std::exception_ptr &ep, const char *msg) noexcept;
+inline void
+LogError(const std::exception_ptr &ep, const char *msg) noexcept
+{
+	Log(LogLevel::ERROR, ep, msg);
+}
 
-gcc_printf(2,3)
-void
-FormatError(const std::exception_ptr &ep, const char *fmt, ...) noexcept;
+template<typename... Args>
+inline void
+FormatError(const std::exception_ptr &ep,
+	    const char *fmt, Args&&... args) noexcept
+{
+	LogFormat(LogLevel::ERROR, ep, fmt, std::forward<Args>(args)...);
+}
 
 gcc_printf(2,3)
 void

From 5ece9685c2dc0441bb0bf1aef3b3e5e548a8ee0c Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 6 Jul 2020 20:58:33 +0200
Subject: [PATCH 4/6] PluginUnavailable: backport class PluginUnconfigured from
 master

Stop bothering people about the Tidal/Qobuz plugins.
---
 src/PluginUnavailable.hxx              | 13 ++++++++++++-
 src/input/Init.cxx                     |  5 +++++
 src/input/plugins/QobuzInputPlugin.cxx |  8 ++++----
 src/input/plugins/TidalInputPlugin.cxx |  6 +++---
 4 files changed, 24 insertions(+), 8 deletions(-)

diff --git a/src/PluginUnavailable.hxx b/src/PluginUnavailable.hxx
index 379bd87bf..a29361ad4 100644
--- a/src/PluginUnavailable.hxx
+++ b/src/PluginUnavailable.hxx
@@ -27,9 +27,20 @@
  * that this plugin is unavailable.  It will be disabled, and MPD can
  * continue initialization.
  */
-class PluginUnavailable final : public std::runtime_error {
+class PluginUnavailable : public std::runtime_error {
 public:
 	using std::runtime_error::runtime_error;
 };
 
+/**
+ * Like #PluginUnavailable, but denotes that the plugin is not
+ * available because it was not explicitly enabled in the
+ * configuration.  The message may describe the necessary steps to
+ * enable it.
+ */
+class PluginUnconfigured : public PluginUnavailable {
+public:
+	using PluginUnavailable::PluginUnavailable;
+};
+
 #endif
diff --git a/src/input/Init.cxx b/src/input/Init.cxx
index 3b714bf5c..e944ff697 100644
--- a/src/input/Init.cxx
+++ b/src/input/Init.cxx
@@ -58,6 +58,11 @@ input_stream_global_init(const ConfigData &config, EventLoop &event_loop)
 			if (plugin->init != nullptr)
 				plugin->init(event_loop, *block);
 			input_plugins_enabled[i] = true;
+		} catch (const PluginUnconfigured &e) {
+			LogFormat(LogLevel::INFO, e,
+				  "Input plugin '%s' is not configured",
+				  plugin->name);
+			continue;
 		} catch (const PluginUnavailable &e) {
 			FormatError(e,
 				    "Input plugin '%s' is unavailable",
diff --git a/src/input/plugins/QobuzInputPlugin.cxx b/src/input/plugins/QobuzInputPlugin.cxx
index 31f60cb24..372a38752 100644
--- a/src/input/plugins/QobuzInputPlugin.cxx
+++ b/src/input/plugins/QobuzInputPlugin.cxx
@@ -133,11 +133,11 @@ InitQobuzInput(EventLoop &event_loop, const ConfigBlock &block)
 
 	const char *app_id = block.GetBlockValue("app_id");
 	if (app_id == nullptr)
-		throw PluginUnavailable("No Qobuz app_id configured");
+		throw PluginUnconfigured("No Qobuz app_id configured");
 
 	const char *app_secret = block.GetBlockValue("app_secret");
 	if (app_secret == nullptr)
-		throw PluginUnavailable("No Qobuz app_secret configured");
+		throw PluginUnconfigured("No Qobuz app_secret configured");
 
 	const char *device_manufacturer_id = block.GetBlockValue("device_manufacturer_id",
 								 "df691fdc-fa36-11e7-9718-635337d7df8f");
@@ -145,11 +145,11 @@ InitQobuzInput(EventLoop &event_loop, const ConfigBlock &block)
 	const char *username = block.GetBlockValue("username");
 	const char *email = block.GetBlockValue("email");
 	if (username == nullptr && email == nullptr)
-		throw PluginUnavailable("No Qobuz username configured");
+		throw PluginUnconfigured("No Qobuz username configured");
 
 	const char *password = block.GetBlockValue("password");
 	if (password == nullptr)
-		throw PluginUnavailable("No Qobuz password configured");
+		throw PluginUnconfigured("No Qobuz password configured");
 
 	const char *format_id = block.GetBlockValue("format_id", "5");
 
diff --git a/src/input/plugins/TidalInputPlugin.cxx b/src/input/plugins/TidalInputPlugin.cxx
index 3592262b2..426244af9 100644
--- a/src/input/plugins/TidalInputPlugin.cxx
+++ b/src/input/plugins/TidalInputPlugin.cxx
@@ -170,15 +170,15 @@ InitTidalInput(EventLoop &event_loop, const ConfigBlock &block)
 
 	const char *token = block.GetBlockValue("token");
 	if (token == nullptr)
-		throw PluginUnavailable("No Tidal application token configured");
+		throw PluginUnconfigured("No Tidal application token configured");
 
 	const char *username = block.GetBlockValue("username");
 	if (username == nullptr)
-		throw PluginUnavailable("No Tidal username configured");
+		throw PluginUnconfigured("No Tidal username configured");
 
 	const char *password = block.GetBlockValue("password");
 	if (password == nullptr)
-		throw PluginUnavailable("No Tidal password configured");
+		throw PluginUnconfigured("No Tidal password configured");
 
 	FormatWarning(tidal_domain, "The Tidal input plugin is deprecated because Tidal has changed the protocol and doesn't share documentation");
 

From 00789de7d42429f0fdd8f5282d54ec5e41d567d6 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 6 Jul 2020 21:35:31 +0200
Subject: [PATCH 5/6] db/upnp/Object: root nodes are allowed to omit parent_id
 and name

This fixes compatibility with Plex DLNA.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/851
---
 NEWS                           |  1 +
 src/db/plugins/upnp/Object.hxx | 11 ++++++++++-
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/NEWS b/NEWS
index 054059bfb..258723782 100644
--- a/NEWS
+++ b/NEWS
@@ -3,6 +3,7 @@ ver 0.21.25 (not yet released)
   - fix crash when using "rangeid" while playing
 * database
   - simple: automatically scan new mounts
+  - upnp: fix compatibility with Plex DLNA
 * storage
   - fix disappearing mounts after mounting twice
   - udisks: fix reading ".mpdignore"
diff --git a/src/db/plugins/upnp/Object.hxx b/src/db/plugins/upnp/Object.hxx
index e17cdfbdd..e27fcfc2d 100644
--- a/src/db/plugins/upnp/Object.hxx
+++ b/src/db/plugins/upnp/Object.hxx
@@ -89,9 +89,18 @@ public:
 		tag.Clear();
 	}
 
+	gcc_pure
+	bool IsRoot() const noexcept {
+		return type == Type::CONTAINER && id == "0";
+	}
+
 	gcc_pure
 	bool Check() const noexcept {
-		return !id.empty() && !parent_id.empty() && !name.empty() &&
+		return !id.empty() &&
+			/* root nodes don't need a parent id and a
+			   name */
+			(IsRoot() || (!parent_id.empty() &&
+				      !name.empty())) &&
 			(type != UPnPDirObject::Type::ITEM ||
 			 item_class != UPnPDirObject::ItemClass::UNKNOWN);
 	}

From c67372f8af0419da2012bae743d96e272594b7f4 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 6 Jul 2020 21:41:53 +0200
Subject: [PATCH 6/6] release v0.21.25

---
 NEWS | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/NEWS b/NEWS
index 258723782..e831815b5 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,4 @@
-ver 0.21.25 (not yet released)
+ver 0.21.25 (2020/07/06)
 * protocol:
   - fix crash when using "rangeid" while playing
 * database