From eb630ca655c7380abebc38a9675e9485afb917b5 Mon Sep 17 00:00:00 2001
From: Naglis Jonaitis <naglis@mailbox.org>
Date: Thu, 24 Jun 2021 16:17:54 +0300
Subject: [PATCH 01/16] doc/user.rst: rectify admin permission

Updating the database no longer requires the `admin` permission, only
`control` is needed (changed in 2abad0f479).

See also: #1124
---
 doc/user.rst | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/doc/user.rst b/doc/user.rst
index 515f57bd7..2e9d8e710 100644
--- a/doc/user.rst
+++ b/doc/user.rst
@@ -628,7 +628,8 @@ By default, all clients are unauthenticated and have a full set of permissions.
    * - **control**
      - Allows all other player and playlist manipulations.
    * - **admin**
-     - Allows database updates and allows shutting down :program:`MPD`.
+     - Allows manipulating outputs, stickers and partitions,
+       mounting/unmounting storage and shutting down :program:`MPD`.
 
 :code:`local_permissions` may be used to assign other permissions to clients connecting on a local socket.
 

From 716784f632148ff5da1a3cd6d235f5b93191c5ab Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 16 Jul 2021 07:22:45 +0200
Subject: [PATCH 02/16] increment version number to 0.22.10

---
 NEWS                        | 2 ++
 android/AndroidManifest.xml | 4 ++--
 doc/conf.py                 | 2 +-
 meson.build                 | 2 +-
 4 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/NEWS b/NEWS
index 64c3d23d2..7a34b69ea 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,5 @@
+ver 0.22.10 (not yet released)
+
 ver 0.22.9 (2021/06/23)
 * database
   - simple: load all .mpdignore files of all parent directories
diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
index 36e636804..0139560e3 100644
--- a/android/AndroidManifest.xml
+++ b/android/AndroidManifest.xml
@@ -2,8 +2,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="org.musicpd"
           android:installLocation="auto"
-          android:versionCode="57"
-          android:versionName="0.22.9">
+          android:versionCode="58"
+          android:versionName="0.22.10">
 
   <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29"/>
 
diff --git a/doc/conf.py b/doc/conf.py
index 5062830b2..652e8ffb9 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -38,7 +38,7 @@ author = 'Max Kellermann'
 # built documents.
 #
 # The short X.Y version.
-version = '0.22.9'
+version = '0.22.10'
 # The full version, including alpha/beta/rc tags.
 release = version
 
diff --git a/meson.build b/meson.build
index 5ba58c5b8..09b6908d9 100644
--- a/meson.build
+++ b/meson.build
@@ -1,7 +1,7 @@
 project(
   'mpd',
   ['c', 'cpp'],
-  version: '0.22.9',
+  version: '0.22.10',
   meson_version: '>= 0.49.0',
   default_options: [
     'c_std=c11',

From 5eab2d96f499ba0dce3df1800e566c7b52b3d05f Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 16 Jul 2021 08:30:28 +0200
Subject: [PATCH 03/16] output/winmm: fix struct/class mismatch

---
 src/output/plugins/WinmmOutputPlugin.hxx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/output/plugins/WinmmOutputPlugin.hxx b/src/output/plugins/WinmmOutputPlugin.hxx
index 5e29121b8..bf8a2e34a 100644
--- a/src/output/plugins/WinmmOutputPlugin.hxx
+++ b/src/output/plugins/WinmmOutputPlugin.hxx
@@ -29,7 +29,7 @@
 #include <windef.h>
 #include <mmsystem.h>
 
-struct WinmmOutput;
+class WinmmOutput;
 
 extern const struct AudioOutputPlugin winmm_output_plugin;
 

From 809a18913a179c1581cea463daaac3cbdae2231f Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 16 Jul 2021 08:20:18 +0200
Subject: [PATCH 04/16] fs/Traits: add overload GetParent(string_view)

---
 src/fs/Traits.cxx | 28 ++++++++++++++++++++++++++++
 src/fs/Traits.hxx | 31 +++++++++++++++++++++++++++++++
 2 files changed, 59 insertions(+)

diff --git a/src/fs/Traits.cxx b/src/fs/Traits.cxx
index f3598fdc1..35d7fa1fd 100644
--- a/src/fs/Traits.cxx
+++ b/src/fs/Traits.cxx
@@ -84,6 +84,22 @@ GetParentPathImpl(typename Traits::const_pointer p) noexcept
 	return {p, size_t(sep - p)};
 }
 
+template<typename Traits>
+typename Traits::string_view
+GetParentPathImpl(typename Traits::string_view p) noexcept
+{
+	auto sep = Traits::FindLastSeparator(p);
+	if (sep == nullptr)
+		return Traits::CURRENT_DIRECTORY;
+	if (sep == p.data())
+		return p.substr(0, 1);
+#ifdef _WIN32
+	if (Traits::IsDrive(p) && sep == p.data() + 2)
+		return p.substr(0, 3);
+#endif
+	return p.substr(0, sep - p.data());
+}
+
 template<typename Traits>
 typename Traits::const_pointer
 RelativePathImpl(typename Traits::string_view base,
@@ -166,6 +182,12 @@ PathTraitsFS::GetParent(PathTraitsFS::const_pointer p) noexcept
 	return GetParentPathImpl<PathTraitsFS>(p);
 }
 
+PathTraitsFS::string_view
+PathTraitsFS::GetParent(string_view p) noexcept
+{
+	return GetParentPathImpl<PathTraitsFS>(p);
+}
+
 PathTraitsFS::const_pointer
 PathTraitsFS::Relative(string_view base, const_pointer other) noexcept
 {
@@ -210,6 +232,12 @@ PathTraitsUTF8::GetParent(const_pointer p) noexcept
 	return GetParentPathImpl<PathTraitsUTF8>(p);
 }
 
+PathTraitsUTF8::string_view
+PathTraitsUTF8::GetParent(string_view p) noexcept
+{
+	return GetParentPathImpl<PathTraitsUTF8>(p);
+}
+
 PathTraitsUTF8::const_pointer
 PathTraitsUTF8::Relative(string_view base, const_pointer other) noexcept
 {
diff --git a/src/fs/Traits.hxx b/src/fs/Traits.hxx
index 9f22e2f7a..a5495a29f 100644
--- a/src/fs/Traits.hxx
+++ b/src/fs/Traits.hxx
@@ -88,6 +88,18 @@ struct PathTraitsFS {
 #endif
 	}
 
+	[[gnu::pure]]
+	static const_pointer FindLastSeparator(string_view p) noexcept {
+#ifdef _WIN32
+		const_pointer pos = p.data() + p.size();
+		while (p.data() != pos && !IsSeparator(*pos))
+			--pos;
+		return IsSeparator(*pos) ? pos : nullptr;
+#else
+		return StringFindLast(p.data(), SEPARATOR, p.size());
+#endif
+	}
+
 	gcc_pure
 	static const_pointer GetFilenameSuffix(const_pointer filename) noexcept {
 		const_pointer dot = StringFindLast(filename, '.');
@@ -106,6 +118,10 @@ struct PathTraitsFS {
 	static constexpr bool IsDrive(const_pointer p) noexcept {
 		return IsAlphaASCII(p[0]) && p[1] == ':';
 	}
+
+	static constexpr bool IsDrive(string_view p) noexcept {
+		return p.size() >= 2 && IsAlphaASCII(p[0]) && p[1] == ':';
+	}
 #endif
 
 	gcc_pure gcc_nonnull_all
@@ -153,6 +169,9 @@ struct PathTraitsFS {
 	gcc_pure gcc_nonnull_all
 	static string_view GetParent(const_pointer p) noexcept;
 
+	[[gnu::pure]]
+	static string_view GetParent(string_view p) noexcept;
+
 	/**
 	 * Determine the relative part of the given path to this
 	 * object, not including the directory separator.  Returns an
@@ -212,6 +231,11 @@ struct PathTraitsUTF8 {
 		return std::strrchr(p, SEPARATOR);
 	}
 
+	[[gnu::pure]]
+	static const_pointer FindLastSeparator(string_view p) noexcept {
+		return StringFindLast(p.data(), SEPARATOR, p.size());
+	}
+
 	gcc_pure
 	static const_pointer GetFilenameSuffix(const_pointer filename) noexcept {
 		const_pointer dot = StringFindLast(filename, '.');
@@ -230,6 +254,10 @@ struct PathTraitsUTF8 {
 	static constexpr bool IsDrive(const_pointer p) noexcept {
 		return IsAlphaASCII(p[0]) && p[1] == ':';
 	}
+
+	static constexpr bool IsDrive(string_view p) noexcept {
+		return p.size() >= 2 && IsAlphaASCII(p[0]) && p[1] == ':';
+	}
 #endif
 
 	gcc_pure gcc_nonnull_all
@@ -277,6 +305,9 @@ struct PathTraitsUTF8 {
 	gcc_pure gcc_nonnull_all
 	static string_view GetParent(const_pointer p) noexcept;
 
+	[[gnu::pure]]
+	static string_view GetParent(string_view p) noexcept;
+
 	/**
 	 * Determine the relative part of the given path to this
 	 * object, not including the directory separator.  Returns an

From 19a44076cfd31c4c6e2c58d0efc86244970fc2e8 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 16 Jul 2021 07:52:54 +0200
Subject: [PATCH 05/16] command/file: pass directory uri to read_stream_art()

---
 src/command/FileCommands.cxx | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/src/command/FileCommands.cxx b/src/command/FileCommands.cxx
index e5638a10a..52d8abae2 100644
--- a/src/command/FileCommands.cxx
+++ b/src/command/FileCommands.cxx
@@ -187,10 +187,9 @@ find_stream_art(std::string_view directory, Mutex &mutex)
 }
 
 static CommandResult
-read_stream_art(Response &r, const char *uri, size_t offset)
+read_stream_art(Response &r, const std::string_view art_directory,
+		size_t offset)
 {
-	const auto art_directory = PathTraitsUTF8::GetParent(uri);
-
 	// TODO: eliminate this const_cast
 	auto &client = const_cast<Client &>(r.GetClient());
 
@@ -252,7 +251,8 @@ read_db_art(Client &client, Response &r, const char *uri, const uint64_t offset)
 		return CommandResult::ERROR;
 	}
 	std::string uri2 = storage->MapUTF8(uri);
-	return read_stream_art(r, uri2.c_str(), offset);
+	return read_stream_art(r, PathTraitsUTF8::GetParent(uri2.c_str()),
+			       offset);
 }
 #endif
 
@@ -273,7 +273,10 @@ handle_album_art(Client &client, Request args, Response &r)
 	switch (located_uri.type) {
 	case LocatedUri::Type::ABSOLUTE:
 	case LocatedUri::Type::PATH:
-		return read_stream_art(r, located_uri.canonical_uri, offset);
+		return read_stream_art(r,
+				       PathTraitsUTF8::GetParent(located_uri.canonical_uri),
+				       offset);
+
 	case LocatedUri::Type::RELATIVE:
 #ifdef ENABLE_DATABASE
 		return read_db_art(client, r, located_uri.canonical_uri, offset);

From 64fa76c56826cdb89a0eb9b24553bdf6293fe434 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 16 Jul 2021 07:50:53 +0200
Subject: [PATCH 06/16] command/file: support "albumart" for virtual tracks in
 CUE sheets

Instead of checking for "cover.jpg" in the virtual directory
representing the CUE sheet, check its enclosing directory.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1206
---
 NEWS                         |  2 ++
 src/command/FileCommands.cxx | 48 ++++++++++++++++++++++++++++++++++--
 2 files changed, 48 insertions(+), 2 deletions(-)

diff --git a/NEWS b/NEWS
index 7a34b69ea..7b0c94c90 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,6 @@
 ver 0.22.10 (not yet released)
+* protocol
+  - support "albumart" for virtual tracks in CUE sheets
 
 ver 0.22.9 (2021/06/23)
 * database
diff --git a/src/command/FileCommands.cxx b/src/command/FileCommands.cxx
index 52d8abae2..1a2b3b8c4 100644
--- a/src/command/FileCommands.cxx
+++ b/src/command/FileCommands.cxx
@@ -27,11 +27,15 @@
 #include "client/Response.hxx"
 #include "util/CharUtil.hxx"
 #include "util/OffsetPointer.hxx"
+#include "util/ScopeExit.hxx"
+#include "util/StringCompare.hxx"
 #include "util/StringView.hxx"
 #include "util/UriExtract.hxx"
 #include "tag/Handler.hxx"
 #include "tag/Generic.hxx"
 #include "TagAny.hxx"
+#include "db/Interface.hxx"
+#include "song/LightSong.hxx"
 #include "storage/StorageInterface.hxx"
 #include "fs/AllocatedPath.hxx"
 #include "fs/FileInfo.hxx"
@@ -242,6 +246,41 @@ read_stream_art(Response &r, const std::string_view art_directory,
 }
 
 #ifdef ENABLE_DATABASE
+
+/**
+ * Attempt to locate the "real" directory where the given song is
+ * stored.  This attempts to resolve "virtual" directories/songs,
+ * e.g. expanded CUE sheet contents.
+ */
+[[gnu::pure]]
+static std::string_view
+RealDirectoryOfSong(Client &client, const char *song_uri,
+		    std::string_view directory_uri) noexcept
+try {
+	const auto *db = client.GetDatabase();
+	if (db == nullptr)
+		return directory_uri;
+
+	const auto *song = db->GetSong(song_uri);
+	if (song == nullptr)
+		return directory_uri;
+
+	AtScopeExit(db, song) { db->ReturnSong(song); };
+
+	const char *real_uri = song->real_uri;
+
+	/* this is a simplification which is just enough for CUE
+	   sheets (but may be incomplete): for each "../", go one
+	   level up */
+	while ((real_uri = StringAfterPrefix(real_uri, "../")) != nullptr)
+		directory_uri = PathTraitsUTF8::GetParent(directory_uri);
+
+	return directory_uri;
+} catch (...) {
+	/* ignore all exceptions from Database::GetSong() */
+	return directory_uri;
+}
+
 static CommandResult
 read_db_art(Client &client, Response &r, const char *uri, const uint64_t offset)
 {
@@ -251,8 +290,13 @@ read_db_art(Client &client, Response &r, const char *uri, const uint64_t offset)
 		return CommandResult::ERROR;
 	}
 	std::string uri2 = storage->MapUTF8(uri);
-	return read_stream_art(r, PathTraitsUTF8::GetParent(uri2.c_str()),
-			       offset);
+
+	std::string_view directory_uri =
+		RealDirectoryOfSong(client,
+				    uri,
+				    PathTraitsUTF8::GetParent(uri2.c_str()));
+
+	return read_stream_art(r, directory_uri, offset);
 }
 #endif
 

From 5355335f190016e2611a2029a982755c0cfa817b Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 30 Jul 2021 13:08:34 +0200
Subject: [PATCH 07/16] db/simple/ExportedSong: check src.OwnsTag(), not
 this->OwnsTag()

this->OwnsTag() accesses fields that are not yet initialized.
---
 NEWS                                   | 2 ++
 src/db/plugins/simple/ExportedSong.hxx | 2 +-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/NEWS b/NEWS
index 7b0c94c90..e18e3ba81 100644
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,8 @@
 ver 0.22.10 (not yet released)
 * protocol
   - support "albumart" for virtual tracks in CUE sheets
+* database
+  - simple: fix crash bug
 
 ver 0.22.9 (2021/06/23)
 * database
diff --git a/src/db/plugins/simple/ExportedSong.hxx b/src/db/plugins/simple/ExportedSong.hxx
index 9a2d54a85..31f3946f2 100644
--- a/src/db/plugins/simple/ExportedSong.hxx
+++ b/src/db/plugins/simple/ExportedSong.hxx
@@ -53,7 +53,7 @@ public:
 			      moved-from instance also owned the Tag
 			      which its LightSong::tag field refers
 			      to */
-			   OwnsTag() ? tag_buffer : src.tag),
+			   src.OwnsTag() ? tag_buffer : src.tag),
 		 tag_buffer(std::move(src.tag_buffer)) {}
 
 	ExportedSong &operator=(ExportedSong &&) = delete;

From 9fa3984a2fd9ef6ca4a8843be78592c072e1705e Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 2 Aug 2021 16:33:33 +0200
Subject: [PATCH 08/16] input/icy: adjust offset at end of stream in Read()

ProxyInputStream::Read() assigns the `offset` field, which is the
wrong offset because it does not consider Icy metadata removed from
the stream.  Therefore, after every ProxyInputStream::Read() call,
IcyInputStream::Read() needs to override this offset.  This was
missing at the end of the stream, when Read()==0.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1216
---
 NEWS                         | 2 ++
 src/input/IcyInputStream.cxx | 5 ++++-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/NEWS b/NEWS
index e18e3ba81..a9cae45d1 100644
--- a/NEWS
+++ b/NEWS
@@ -3,6 +3,8 @@ ver 0.22.10 (not yet released)
   - support "albumart" for virtual tracks in CUE sheets
 * database
   - simple: fix crash bug
+* input
+  - curl: fix crash bug after stream with Icy metadata was closed by peer
 
 ver 0.22.9 (2021/06/23)
 * database
diff --git a/src/input/IcyInputStream.cxx b/src/input/IcyInputStream.cxx
index f24399e85..46ebeaca4 100644
--- a/src/input/IcyInputStream.cxx
+++ b/src/input/IcyInputStream.cxx
@@ -104,8 +104,11 @@ IcyInputStream::Read(std::unique_lock<Mutex> &lock,
 
 	while (true) {
 		size_t nbytes = ProxyInputStream::Read(lock, ptr, read_size);
-		if (nbytes == 0)
+		if (nbytes == 0) {
+			assert(IsEOF());
+			offset = override_offset;
 			return 0;
+		}
 
 		size_t result = parser->ParseInPlace(ptr, nbytes);
 		if (result > 0) {

From 97c43954e8df3f772899cda951ad5a7dd4a24f25 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Thu, 5 Aug 2021 13:50:04 +0200
Subject: [PATCH 09/16] input/tidal: remove defunct unmaintained plugin

This plugin has been defunct for several years.  Tidal has not ever
replied to any of my emails, so they're apparently not interested in
MPD support.
---
 NEWS                                      |   1 +
 doc/plugins.rst                           |  31 ---
 meson_options.txt                         |   1 -
 src/input/InputStream.cxx                 |   1 -
 src/input/Registry.cxx                    |   4 -
 src/input/plugins/TidalError.hxx          |  62 ------
 src/input/plugins/TidalErrorParser.cxx    | 117 ----------
 src/input/plugins/TidalErrorParser.hxx    |  69 ------
 src/input/plugins/TidalInputPlugin.cxx    | 256 ----------------------
 src/input/plugins/TidalInputPlugin.hxx    |  25 ---
 src/input/plugins/TidalLoginRequest.cxx   | 155 -------------
 src/input/plugins/TidalLoginRequest.hxx   |  74 -------
 src/input/plugins/TidalSessionManager.cxx | 118 ----------
 src/input/plugins/TidalSessionManager.hxx | 161 --------------
 src/input/plugins/TidalTagScanner.cxx     | 244 ---------------------
 src/input/plugins/TidalTagScanner.hxx     |  61 ------
 src/input/plugins/TidalTrackRequest.cxx   | 160 --------------
 src/input/plugins/TidalTrackRequest.hxx   |  76 -------
 src/input/plugins/meson.build             |  21 --
 19 files changed, 1 insertion(+), 1636 deletions(-)
 delete mode 100644 src/input/plugins/TidalError.hxx
 delete mode 100644 src/input/plugins/TidalErrorParser.cxx
 delete mode 100644 src/input/plugins/TidalErrorParser.hxx
 delete mode 100644 src/input/plugins/TidalInputPlugin.cxx
 delete mode 100644 src/input/plugins/TidalInputPlugin.hxx
 delete mode 100644 src/input/plugins/TidalLoginRequest.cxx
 delete mode 100644 src/input/plugins/TidalLoginRequest.hxx
 delete mode 100644 src/input/plugins/TidalSessionManager.cxx
 delete mode 100644 src/input/plugins/TidalSessionManager.hxx
 delete mode 100644 src/input/plugins/TidalTagScanner.cxx
 delete mode 100644 src/input/plugins/TidalTagScanner.hxx
 delete mode 100644 src/input/plugins/TidalTrackRequest.cxx
 delete mode 100644 src/input/plugins/TidalTrackRequest.hxx

diff --git a/NEWS b/NEWS
index a9cae45d1..810379466 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,7 @@ ver 0.22.10 (not yet released)
   - simple: fix crash bug
 * input
   - curl: fix crash bug after stream with Icy metadata was closed by peer
+  - tidal: remove defunct unmaintained plugin
 
 ver 0.22.9 (2021/06/23)
 * database
diff --git a/doc/plugins.rst b/doc/plugins.rst
index ee637f20a..bac76f106 100644
--- a/doc/plugins.rst
+++ b/doc/plugins.rst
@@ -295,37 +295,6 @@ in the form ``qobuz://track/ID``, e.g.:
    * - **format_id N**
      - The `Qobuz format identifier <https://github.com/Qobuz/api-documentation/blob/master/endpoints/track/getFileUrl.md#parameters>`_, i.e. a number which chooses the format and quality to be requested from Qobuz. The default is "5" (320 kbit/s MP3).
 
-tidal
------
-
-Play songs from the commercial streaming service `Tidal
-<http://tidal.com/>`_. It plays URLs in the form ``tidal://track/ID``,
-e.g.:
-
-.. warning::
-
-   This plugin is currently defunct because Tidal has changed the
-   protocol and decided not to share documentation.
-
-.. code-block:: none
-
-    mpc add tidal://track/59727857
-
-.. list-table::
-   :widths: 20 80
-   :header-rows: 1
-
-   * - Setting
-     - Description
-   * - **token TOKEN**
-     - The Tidal application token. Since Tidal is unwilling to assign a token to MPD, this needs to be reverse-engineered from another (approved) Tidal client.
-   * - **username USERNAME**
-     - The Tidal user name.
-   * - **password PASSWORD**
-     - The Tidal password.
-   * - **audioquality Q**
-     - The Tidal "audioquality" parameter. Possible values: HI_RES, LOSSLESS, HIGH, LOW. Default is HIGH.
-
 .. _decoder_plugins:
      
 Decoder plugins
diff --git a/meson_options.txt b/meson_options.txt
index 36214692d..89f889b57 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -104,7 +104,6 @@ option('smbclient', type: 'feature', value: 'disabled', description: 'SMB suppor
 
 option('qobuz', type: 'feature', description: 'Qobuz client')
 option('soundcloud', type: 'feature', description: 'SoundCloud client')
-option('tidal', type: 'feature', description: 'Tidal client')
 
 #
 # Archive plugins
diff --git a/src/input/InputStream.cxx b/src/input/InputStream.cxx
index 5af7d681b..322138914 100644
--- a/src/input/InputStream.cxx
+++ b/src/input/InputStream.cxx
@@ -57,7 +57,6 @@ static bool
 ExpensiveSeeking(const char *uri) noexcept
 {
 	return StringStartsWithCaseASCII(uri, "http://") ||
-		StringStartsWithCaseASCII(uri, "tidal://") ||
 		StringStartsWithCaseASCII(uri, "qobuz://") ||
 		StringStartsWithCaseASCII(uri, "https://");
 }
diff --git a/src/input/Registry.cxx b/src/input/Registry.cxx
index 4a234a88c..d00a620cc 100644
--- a/src/input/Registry.cxx
+++ b/src/input/Registry.cxx
@@ -20,7 +20,6 @@
 #include "Registry.hxx"
 #include "InputPlugin.hxx"
 #include "input/Features.h"
-#include "plugins/TidalInputPlugin.hxx"
 #include "plugins/QobuzInputPlugin.hxx"
 #include "config.h"
 
@@ -56,9 +55,6 @@ const InputPlugin *const input_plugins[] = {
 #ifdef ENABLE_ALSA
 	&input_plugin_alsa,
 #endif
-#ifdef ENABLE_TIDAL
-	&tidal_input_plugin,
-#endif
 #ifdef ENABLE_QOBUZ
 	&qobuz_input_plugin,
 #endif
diff --git a/src/input/plugins/TidalError.hxx b/src/input/plugins/TidalError.hxx
deleted file mode 100644
index 036f12998..000000000
--- a/src/input/plugins/TidalError.hxx
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright 2003-2021 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#ifndef TIDAL_ERROR_HXX
-#define TIDAL_ERROR_HXX
-
-#include <stdexcept>
-
-/**
- * An error condition reported by the server.
- *
- * See http://developer.tidal.com/technical/errors/ for details (login
- * required).
- */
-class TidalError : public std::runtime_error {
-	/**
-	 * The HTTP status code.
-	 */
-	unsigned status;
-
-	/**
-	 * The Tidal-specific "subStatus".  0 if none was found in the
-	 * JSON response.
-	 */
-	unsigned sub_status;
-
-public:
-	template<typename W>
-	TidalError(unsigned _status, unsigned _sub_status, W &&_what) noexcept
-		:std::runtime_error(std::forward<W>(_what)),
-		 status(_status), sub_status(_sub_status) {}
-
-	unsigned GetStatus() const noexcept {
-		return status;
-	}
-
-	unsigned GetSubStatus() const noexcept {
-		return sub_status;
-	}
-
-	bool IsInvalidSession() const noexcept {
-		return sub_status == 6001 || sub_status == 6002;
-	}
-};
-
-#endif
diff --git a/src/input/plugins/TidalErrorParser.cxx b/src/input/plugins/TidalErrorParser.cxx
deleted file mode 100644
index 8049113d4..000000000
--- a/src/input/plugins/TidalErrorParser.cxx
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright 2003-2021 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#include "TidalErrorParser.hxx"
-#include "TidalError.hxx"
-#include "lib/yajl/Callbacks.hxx"
-#include "util/RuntimeError.hxx"
-
-using Wrapper = Yajl::CallbacksWrapper<TidalErrorParser>;
-static constexpr yajl_callbacks tidal_error_parser_callbacks = {
-	nullptr,
-	nullptr,
-	Wrapper::Integer,
-	nullptr,
-	nullptr,
-	Wrapper::String,
-	nullptr,
-	Wrapper::MapKey,
-	Wrapper::EndMap,
-	nullptr,
-	nullptr,
-};
-
-TidalErrorParser::TidalErrorParser(unsigned _status,
-				   const std::multimap<std::string, std::string> &headers)
-	:YajlResponseParser(&tidal_error_parser_callbacks, nullptr, this),
-	 status(_status)
-{
-	auto i = headers.find("content-type");
-	if (i == headers.end() || i->second.find("/json") == i->second.npos)
-		throw FormatRuntimeError("Status %u from Tidal", status);
-}
-
-void
-TidalErrorParser::OnEnd()
-{
-	YajlResponseParser::OnEnd();
-
-	char what[1024];
-
-	if (!message.empty())
-		snprintf(what, sizeof(what), "Error from Tidal: %s",
-			 message.c_str());
-	else
-		snprintf(what, sizeof(what), "Status %u from Tidal", status);
-
-	throw TidalError(status, sub_status, what);
-}
-
-inline bool
-TidalErrorParser::Integer(long long value) noexcept
-{
-	switch (state) {
-	case State::NONE:
-	case State::USER_MESSAGE:
-		break;
-
-	case State::SUB_STATUS:
-		sub_status = value;
-		break;
-	}
-
-	return true;
-}
-
-inline bool
-TidalErrorParser::String(StringView value) noexcept
-{
-	switch (state) {
-	case State::NONE:
-	case State::SUB_STATUS:
-		break;
-
-	case State::USER_MESSAGE:
-		message.assign(value.data, value.size);
-		break;
-	}
-
-	return true;
-}
-
-inline bool
-TidalErrorParser::MapKey(StringView value) noexcept
-{
-	if (value.Equals("userMessage"))
-		state = State::USER_MESSAGE;
-	else if (value.Equals("subStatus"))
-		state = State::SUB_STATUS;
-	else
-		state = State::NONE;
-
-	return true;
-}
-
-inline bool
-TidalErrorParser::EndMap() noexcept
-{
-	state = State::NONE;
-
-	return true;
-}
diff --git a/src/input/plugins/TidalErrorParser.hxx b/src/input/plugins/TidalErrorParser.hxx
deleted file mode 100644
index 6291d130f..000000000
--- a/src/input/plugins/TidalErrorParser.hxx
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2003-2021 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#ifndef TIDAL_ERROR_PARSER_HXX
-#define TIDAL_ERROR_PARSER_HXX
-
-#include "lib/yajl/ResponseParser.hxx"
-
-#include <string>
-#include <map>
-
-template<typename T> struct ConstBuffer;
-struct StringView;
-
-/**
- * Parse an error JSON response and throw a #TidalError upon
- * completion.
- */
-class TidalErrorParser final : public YajlResponseParser {
-	const unsigned status;
-
-	enum class State {
-		NONE,
-		USER_MESSAGE,
-		SUB_STATUS,
-	} state = State::NONE;
-
-	unsigned sub_status = 0;
-
-	std::string message;
-
-public:
-	/**
-	 * May throw if there is a formal error in the response
-	 * headers.
-	 */
-	TidalErrorParser(unsigned status,
-			 const std::multimap<std::string, std::string> &headers);
-
-protected:
-	/* virtual methods from CurlResponseParser */
-	[[noreturn]]
-	void OnEnd() override;
-
-public:
-	/* yajl callbacks */
-	bool Integer(long long value) noexcept;
-	bool String(StringView value) noexcept;
-	bool MapKey(StringView value) noexcept;
-	bool EndMap() noexcept;
-};
-
-#endif
diff --git a/src/input/plugins/TidalInputPlugin.cxx b/src/input/plugins/TidalInputPlugin.cxx
deleted file mode 100644
index 1781d16ae..000000000
--- a/src/input/plugins/TidalInputPlugin.cxx
+++ /dev/null
@@ -1,256 +0,0 @@
-/*
- * Copyright 2003-2021 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#include "TidalInputPlugin.hxx"
-#include "TidalSessionManager.hxx"
-#include "TidalTrackRequest.hxx"
-#include "TidalTagScanner.hxx"
-#include "TidalError.hxx"
-#include "CurlInputPlugin.hxx"
-#include "PluginUnavailable.hxx"
-#include "input/ProxyInputStream.hxx"
-#include "input/FailingInputStream.hxx"
-#include "input/InputPlugin.hxx"
-#include "config/Block.hxx"
-#include "thread/Mutex.hxx"
-#include "util/Domain.hxx"
-#include "util/Exception.hxx"
-#include "util/StringCompare.hxx"
-#include "Log.hxx"
-
-#include <memory>
-#include <utility>
-
-static constexpr Domain tidal_domain("tidal");
-
-static TidalSessionManager *tidal_session;
-static const char *tidal_audioquality;
-
-class TidalInputStream final
-	: public ProxyInputStream, TidalSessionHandler, TidalTrackHandler {
-
-	const std::string track_id;
-
-	std::unique_ptr<TidalTrackRequest> track_request;
-
-	std::exception_ptr error;
-
-	/**
-	 * Retry to login if TidalError::IsInvalidSession() returns
-	 * true?
-	 */
-	bool retry_login = true;
-
-public:
-	TidalInputStream(const char *_uri, const char *_track_id,
-			 Mutex &_mutex) noexcept
-		:ProxyInputStream(_uri, _mutex),
-		 track_id(_track_id)
-	{
-		tidal_session->AddLoginHandler(*this);
-	}
-
-	~TidalInputStream() override {
-		tidal_session->RemoveLoginHandler(*this);
-	}
-
-	/* virtual methods from InputStream */
-
-	void Check() override {
-		if (error)
-			std::rethrow_exception(error);
-	}
-
-private:
-	void Failed(const std::exception_ptr& e) {
-		SetInput(std::make_unique<FailingInputStream>(GetURI(), e,
-							      mutex));
-	}
-
-	/* virtual methods from TidalSessionHandler */
-	void OnTidalSession() noexcept override;
-
-	/* virtual methods from TidalTrackHandler */
-	void OnTidalTrackSuccess(std::string url) noexcept override;
-	void OnTidalTrackError(std::exception_ptr error) noexcept override;
-};
-
-void
-TidalInputStream::OnTidalSession() noexcept
-{
-	const std::lock_guard<Mutex> protect(mutex);
-
-	try {
-		TidalTrackHandler &h = *this;
-		track_request = std::make_unique<TidalTrackRequest>(tidal_session->GetCurl(),
-								    tidal_session->GetBaseUrl(),
-								    tidal_session->GetToken(),
-								    tidal_session->GetSession().c_str(),
-								    track_id.c_str(),
-								    tidal_audioquality,
-								    h);
-		track_request->Start();
-	} catch (...) {
-		Failed(std::current_exception());
-	}
-}
-
-void
-TidalInputStream::OnTidalTrackSuccess(std::string url) noexcept
-{
-	FormatDebug(tidal_domain, "Tidal track '%s' resolves to %s",
-		    track_id.c_str(), url.c_str());
-
-	const std::lock_guard<Mutex> protect(mutex);
-
-	track_request.reset();
-
-	try {
-		SetInput(OpenCurlInputStream(url.c_str(), {},
-					     mutex));
-	} catch (...) {
-		Failed(std::current_exception());
-	}
-}
-
-gcc_pure
-static bool
-IsInvalidSession(std::exception_ptr e) noexcept
-{
-	try {
-		std::rethrow_exception(std::move(e));
-	} catch (const TidalError &te) {
-		return te.IsInvalidSession();
-	} catch (...) {
-		return false;
-	}
-}
-
-void
-TidalInputStream::OnTidalTrackError(std::exception_ptr e) noexcept
-{
-	const std::lock_guard<Mutex> protect(mutex);
-
-	if (retry_login && IsInvalidSession(e)) {
-		/* the session has expired - obtain a new session id
-		   by logging in again */
-
-		FormatInfo(tidal_domain, "Session expired ('%s'), retrying to log in",
-			   GetFullMessage(e).c_str());
-
-		retry_login = false;
-		tidal_session->AddLoginHandler(*this);
-		return;
-	}
-
-	Failed(e);
-}
-
-static void
-InitTidalInput(EventLoop &event_loop, const ConfigBlock &block)
-{
-	const char *base_url = block.GetBlockValue("base_url",
-						   "https://api.tidal.com/v1");
-
-	const char *token = block.GetBlockValue("token");
-	if (token == nullptr)
-		throw PluginUnconfigured("No Tidal application token configured");
-
-	const char *username = block.GetBlockValue("username");
-	if (username == nullptr)
-		throw PluginUnconfigured("No Tidal username configured");
-
-	const char *password = block.GetBlockValue("password");
-	if (password == nullptr)
-		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");
-
-	tidal_audioquality = block.GetBlockValue("audioquality", "HIGH");
-
-	tidal_session = new TidalSessionManager(event_loop, base_url, token,
-						username, password);
-}
-
-static void
-FinishTidalInput() noexcept
-{
-	delete tidal_session;
-}
-
-gcc_pure
-static const char *
-ExtractTidalTrackId(const char *uri)
-{
-	const char *track_id = StringAfterPrefix(uri, "tidal://track/");
-	if (track_id == nullptr) {
-		track_id = StringAfterPrefix(uri, "https://listen.tidal.com/track/");
-		if (track_id == nullptr)
-			return nullptr;
-	}
-
-	if (*track_id == 0)
-		return nullptr;
-
-	return track_id;
-}
-
-static InputStreamPtr
-OpenTidalInput(const char *uri, Mutex &mutex)
-{
-	assert(tidal_session != nullptr);
-
-	const char *track_id = ExtractTidalTrackId(uri);
-	if (track_id == nullptr)
-		return nullptr;
-
-	// TODO: validate track_id
-
-	return std::make_unique<TidalInputStream>(uri, track_id, mutex);
-}
-
-static std::unique_ptr<RemoteTagScanner>
-ScanTidalTags(const char *uri, RemoteTagHandler &handler)
-{
-	assert(tidal_session != nullptr);
-
-	const char *track_id = ExtractTidalTrackId(uri);
-	if (track_id == nullptr)
-		return nullptr;
-
-	return std::make_unique<TidalTagScanner>(tidal_session->GetCurl(),
-						 tidal_session->GetBaseUrl(),
-						 tidal_session->GetToken(),
-						 track_id, handler);
-}
-
-static constexpr const char *tidal_prefixes[] = {
-	"tidal://",
-	nullptr
-};
-
-const InputPlugin tidal_input_plugin = {
-	"tidal",
-	tidal_prefixes,
-	InitTidalInput,
-	FinishTidalInput,
-	OpenTidalInput,
-	nullptr,
-	ScanTidalTags,
-};
diff --git a/src/input/plugins/TidalInputPlugin.hxx b/src/input/plugins/TidalInputPlugin.hxx
deleted file mode 100644
index ba88bf68b..000000000
--- a/src/input/plugins/TidalInputPlugin.hxx
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright 2003-2021 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#ifndef INPUT_TIDAL_HXX
-#define INPUT_TIDAL_HXX
-
-extern const struct InputPlugin tidal_input_plugin;
-
-#endif
diff --git a/src/input/plugins/TidalLoginRequest.cxx b/src/input/plugins/TidalLoginRequest.cxx
deleted file mode 100644
index ae7133734..000000000
--- a/src/input/plugins/TidalLoginRequest.cxx
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright 2003-2021 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#include "TidalLoginRequest.hxx"
-#include "TidalErrorParser.hxx"
-#include "lib/curl/Form.hxx"
-#include "lib/yajl/Callbacks.hxx"
-#include "lib/yajl/ResponseParser.hxx"
-
-#include <cassert>
-
-using Wrapper = Yajl::CallbacksWrapper<TidalLoginRequest::ResponseParser>;
-static constexpr yajl_callbacks parse_callbacks = {
-	nullptr,
-	nullptr,
-	nullptr,
-	nullptr,
-	nullptr,
-	Wrapper::String,
-	nullptr,
-	Wrapper::MapKey,
-	Wrapper::EndMap,
-	nullptr,
-	nullptr,
-};
-
-class TidalLoginRequest::ResponseParser final : public YajlResponseParser {
-	enum class State {
-		NONE,
-		SESSION_ID,
-	} state = State::NONE;
-
-	std::string session;
-
-public:
-	explicit ResponseParser() noexcept
-		:YajlResponseParser(&parse_callbacks, nullptr, this) {}
-
-	std::string &&GetSession() {
-		if (session.empty())
-			throw std::runtime_error("No sessionId in login response");
-
-		return std::move(session);
-	}
-
-	/* yajl callbacks */
-	bool String(StringView value) noexcept;
-	bool MapKey(StringView value) noexcept;
-	bool EndMap() noexcept;
-};
-
-static std::string
-MakeLoginUrl(const char *base_url)
-{
-	return std::string(base_url) + "/login/username";
-}
-
-TidalLoginRequest::TidalLoginRequest(CurlGlobal &curl,
-				     const char *base_url, const char *token,
-				     const char *username, const char *password,
-				     TidalLoginHandler &_handler)
-	:request(curl, MakeLoginUrl(base_url).c_str(), *this),
-	 handler(_handler)
-{
-	request_headers.Append((std::string("X-Tidal-Token:")
-				+ token).c_str());
-	request.SetOption(CURLOPT_HTTPHEADER, request_headers.Get());
-
-	request.SetOption(CURLOPT_COPYPOSTFIELDS,
-			  EncodeForm(request.Get(),
-				     {{"username", username}, {"password", password}}).c_str());
-}
-
-TidalLoginRequest::~TidalLoginRequest() noexcept
-{
-	request.StopIndirect();
-}
-
-std::unique_ptr<CurlResponseParser>
-TidalLoginRequest::MakeParser(unsigned status,
-			      std::multimap<std::string, std::string> &&headers)
-{
-	if (status != 200)
-		return std::make_unique<TidalErrorParser>(status, headers);
-
-	auto i = headers.find("content-type");
-	if (i == headers.end() || i->second.find("/json") == i->second.npos)
-		throw std::runtime_error("Not a JSON response from Tidal");
-
-	return std::make_unique<ResponseParser>();
-}
-
-void
-TidalLoginRequest::FinishParser(std::unique_ptr<CurlResponseParser> p)
-{
-	assert(dynamic_cast<ResponseParser *>(p.get()) != nullptr);
-	auto &rp = (ResponseParser &)*p;
-	handler.OnTidalLoginSuccess(rp.GetSession());
-}
-
-void
-TidalLoginRequest::OnError(std::exception_ptr e) noexcept
-{
-	handler.OnTidalLoginError(e);
-}
-
-inline bool
-TidalLoginRequest::ResponseParser::String(StringView value) noexcept
-{
-	switch (state) {
-	case State::NONE:
-		break;
-
-	case State::SESSION_ID:
-		session.assign(value.data, value.size);
-		break;
-	}
-
-	return true;
-}
-
-inline bool
-TidalLoginRequest::ResponseParser::MapKey(StringView value) noexcept
-{
-	if (value.Equals("sessionId"))
-		state = State::SESSION_ID;
-	else
-		state = State::NONE;
-
-	return true;
-}
-
-inline bool
-TidalLoginRequest::ResponseParser::EndMap() noexcept
-{
-	state = State::NONE;
-
-	return true;
-}
diff --git a/src/input/plugins/TidalLoginRequest.hxx b/src/input/plugins/TidalLoginRequest.hxx
deleted file mode 100644
index b830b2f2c..000000000
--- a/src/input/plugins/TidalLoginRequest.hxx
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2003-2021 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#ifndef TIDAL_LOGIN_REQUEST_HXX
-#define TIDAL_LOGIN_REQUEST_HXX
-
-#include "lib/curl/Delegate.hxx"
-#include "lib/curl/Slist.hxx"
-#include "lib/curl/Request.hxx"
-
-/**
- * Callback class for #TidalLoginRequest.
- *
- * Its methods must be thread-safe.
- */
-class TidalLoginHandler {
-public:
-	virtual void OnTidalLoginSuccess(std::string session) noexcept = 0;
-	virtual void OnTidalLoginError(std::exception_ptr error) noexcept = 0;
-};
-
-/**
- * An asynchronous Tidal "login/username" request.
- *
- * After construction, call Start() to initiate the request.
- */
-class TidalLoginRequest final : DelegateCurlResponseHandler {
-	CurlSlist request_headers;
-
-	CurlRequest request;
-
-	TidalLoginHandler &handler;
-
-public:
-	class ResponseParser;
-
-	TidalLoginRequest(CurlGlobal &curl,
-			  const char *base_url, const char *token,
-			  const char *username, const char *password,
-			  TidalLoginHandler &_handler);
-
-	~TidalLoginRequest() noexcept;
-
-	void Start() {
-		request.StartIndirect();
-	}
-
-private:
-	/* virtual methods from DelegateCurlResponseHandler */
-	std::unique_ptr<CurlResponseParser> MakeParser(unsigned status,
-						       std::multimap<std::string, std::string> &&headers) override;
-	void FinishParser(std::unique_ptr<CurlResponseParser> p) override;
-
-	/* virtual methods from CurlResponseHandler */
-	void OnError(std::exception_ptr e) noexcept override;
-};
-
-#endif
diff --git a/src/input/plugins/TidalSessionManager.cxx b/src/input/plugins/TidalSessionManager.cxx
deleted file mode 100644
index c454429eb..000000000
--- a/src/input/plugins/TidalSessionManager.cxx
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright 2003-2021 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#include "TidalSessionManager.hxx"
-#include "util/Domain.hxx"
-
-#include "Log.hxx"
-
-static constexpr Domain tidal_domain("tidal");
-
-TidalSessionManager::TidalSessionManager(EventLoop &event_loop,
-					 const char *_base_url, const char *_token,
-					 const char *_username,
-					 const char *_password)
-	:base_url(_base_url), token(_token),
-	 username(_username), password(_password),
-	 curl(event_loop),
-	 defer_invoke_handlers(event_loop,
-			       BIND_THIS_METHOD(InvokeHandlers))
-{
-}
-
-TidalSessionManager::~TidalSessionManager() noexcept
-{
-	assert(handlers.empty());
-}
-
-void
-TidalSessionManager::AddLoginHandler(TidalSessionHandler &h) noexcept
-{
-	const std::lock_guard<Mutex> protect(mutex);
-	assert(!h.is_linked());
-
-	const bool was_empty = handlers.empty();
-	handlers.push_front(h);
-
-	if (!was_empty || login_request)
-		return;
-
-	if (session.empty()) {
-		// TODO: throttle login attempts?
-
-		LogDebug(tidal_domain, "Sending login request");
-
-		std::string login_uri(base_url);
-		login_uri += "/login/username";
-
-		try {
-			TidalLoginHandler &handler = *this;
-			login_request =
-				std::make_unique<TidalLoginRequest>(*curl, base_url,
-								    token,
-								    username, password,
-								    handler);
-			login_request->Start();
-		} catch (...) {
-			error = std::current_exception();
-			ScheduleInvokeHandlers();
-			return;
-		}
-	} else
-		ScheduleInvokeHandlers();
-}
-
-void
-TidalSessionManager::OnTidalLoginSuccess(std::string _session) noexcept
-{
-	FormatDebug(tidal_domain, "Login successful, session=%s", _session.c_str());
-
-	{
-		const std::lock_guard<Mutex> protect(mutex);
-		login_request.reset();
-		session = std::move(_session);
-	}
-
-	ScheduleInvokeHandlers();
-}
-
-void
-TidalSessionManager::OnTidalLoginError(std::exception_ptr e) noexcept
-{
-	{
-		const std::lock_guard<Mutex> protect(mutex);
-		login_request.reset();
-		error = e;
-	}
-
-	ScheduleInvokeHandlers();
-}
-
-void
-TidalSessionManager::InvokeHandlers() noexcept
-{
-	const std::lock_guard<Mutex> protect(mutex);
-	while (!handlers.empty()) {
-		auto &h = handlers.front();
-		handlers.pop_front();
-
-		const ScopeUnlock unlock(mutex);
-		h.OnTidalSession();
-	}
-}
diff --git a/src/input/plugins/TidalSessionManager.hxx b/src/input/plugins/TidalSessionManager.hxx
deleted file mode 100644
index 2d4159b32..000000000
--- a/src/input/plugins/TidalSessionManager.hxx
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright 2003-2021 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#ifndef TIDAL_SESSION_MANAGER_HXX
-#define TIDAL_SESSION_MANAGER_HXX
-
-#include "TidalLoginRequest.hxx"
-#include "lib/curl/Init.hxx"
-#include "thread/Mutex.hxx"
-#include "event/DeferEvent.hxx"
-
-#include <boost/intrusive/list.hpp>
-
-#include <memory>
-#include <string>
-
-/**
- * Callback class for #TidalSessionManager.
- *
- * Its methods must be thread-safe.
- */
-class TidalSessionHandler
-	: public boost::intrusive::list_base_hook<boost::intrusive::link_mode<boost::intrusive::safe_link>>
-{
-public:
-	/**
-	 * TidalSessionHandler::AddLoginHandler() has completed
-	 * (successful or failed).  This method may now call
-	 * #TidalSessionHandler::GetSession().
-	 */
-	virtual void OnTidalSession() noexcept = 0;
-};
-
-class TidalSessionManager final : TidalLoginHandler {
-	/**
-	 * The Tidal API base URL.
-	 */
-	const char *const base_url;
-
-	/**
-	 * The configured Tidal application token.
-	 */
-	const char *const token;
-
-	/**
-	 * The configured Tidal user name.
-	 */
-	const char *const username;
-
-	/**
-	 * The configured Tidal password.
-	 */
-	const char *const password;
-
-	CurlInit curl;
-
-	DeferEvent defer_invoke_handlers;
-
-	/**
-	 * Protects #session, #error and #handlers.
-	 */
-	mutable Mutex mutex;
-
-	std::exception_ptr error;
-
-	/**
-	 * The current Tidal session id, empty if none.
-	 */
-	std::string session;
-
-	typedef boost::intrusive::list<TidalSessionHandler,
-				       boost::intrusive::constant_time_size<false>> LoginHandlerList;
-
-	LoginHandlerList handlers;
-
-	std::unique_ptr<TidalLoginRequest> login_request;
-
-public:
-	TidalSessionManager(EventLoop &event_loop,
-			    const char *_base_url, const char *_token,
-			    const char *_username,
-			    const char *_password);
-
-	~TidalSessionManager() noexcept;
-
-	auto &GetEventLoop() const noexcept {
-		return defer_invoke_handlers.GetEventLoop();
-	}
-
-	CurlGlobal &GetCurl() noexcept {
-		return *curl;
-	}
-
-	const char *GetBaseUrl() const noexcept {
-		return base_url;
-	}
-
-	/**
-	 * Ask the object to call back once the login to Tidal has
-	 * completed.  If no session exists currently, then one is
-	 * created.  Since the callback may occur in another thread,
-	 * the it may have been completed already before this method
-	 * returns.
-	 */
-	void AddLoginHandler(TidalSessionHandler &h) noexcept;
-
-	void RemoveLoginHandler(TidalSessionHandler &h) noexcept {
-		const std::lock_guard<Mutex> protect(mutex);
-		if (h.is_linked())
-			handlers.erase(handlers.iterator_to(h));
-	}
-
-	const char *GetToken() const noexcept {
-		return token;
-	}
-
-	/**
-	 * Get the Tidal session id, or rethrows an exception if an
-	 * error has occurred while logging in.
-	 */
-	std::string GetSession() const {
-		const std::lock_guard<Mutex> protect(mutex);
-
-		if (error)
-			std::rethrow_exception(error);
-
-		if (session.empty())
-			throw std::runtime_error("No session");
-
-		return session;
-	}
-
-private:
-	void InvokeHandlers() noexcept;
-
-	void ScheduleInvokeHandlers() noexcept {
-		defer_invoke_handlers.Schedule();
-	}
-
-	/* virtual methods from TidalLoginHandler */
-	void OnTidalLoginSuccess(std::string session) noexcept override;
-	void OnTidalLoginError(std::exception_ptr error) noexcept override;
-};
-
-#endif
diff --git a/src/input/plugins/TidalTagScanner.cxx b/src/input/plugins/TidalTagScanner.cxx
deleted file mode 100644
index 4b345dc35..000000000
--- a/src/input/plugins/TidalTagScanner.cxx
+++ /dev/null
@@ -1,244 +0,0 @@
-/*
- * Copyright 2003-2021 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#include "TidalTagScanner.hxx"
-#include "TidalErrorParser.hxx"
-#include "lib/yajl/Callbacks.hxx"
-#include "tag/Builder.hxx"
-#include "tag/Tag.hxx"
-
-#include <cassert>
-
-using Wrapper = Yajl::CallbacksWrapper<TidalTagScanner::ResponseParser>;
-static constexpr yajl_callbacks parse_callbacks = {
-	nullptr,
-	nullptr,
-	Wrapper::Integer,
-	nullptr,
-	nullptr,
-	Wrapper::String,
-	Wrapper::StartMap,
-	Wrapper::MapKey,
-	Wrapper::EndMap,
-	nullptr,
-	nullptr,
-};
-
-class TidalTagScanner::ResponseParser final : public YajlResponseParser {
-	enum class State {
-		NONE,
-		TITLE,
-		DURATION,
-		ARTIST,
-		ARTIST_NAME,
-		ALBUM,
-		ALBUM_TITLE,
-	} state = State::NONE;
-
-	unsigned map_depth = 0;
-
-	TagBuilder tag;
-
-public:
-	explicit ResponseParser() noexcept
-		:YajlResponseParser(&parse_callbacks, nullptr, this) {}
-
-	Tag GetTag() {
-		return tag.Commit();
-	}
-
-	/* yajl callbacks */
-	bool Integer(long long value) noexcept;
-	bool String(StringView value) noexcept;
-	bool StartMap() noexcept;
-	bool MapKey(StringView value) noexcept;
-	bool EndMap() noexcept;
-};
-
-static std::string
-MakeTrackUrl(const char *base_url, const char *track_id)
-{
-	return std::string(base_url)
-		+ "/tracks/"
-		+ track_id
-		// TODO: configurable countryCode?
-		+ "?countryCode=US";
-}
-
-TidalTagScanner::TidalTagScanner(CurlGlobal &curl,
-				 const char *base_url, const char *token,
-				 const char *track_id,
-				 RemoteTagHandler &_handler)
-	:request(curl, MakeTrackUrl(base_url, track_id).c_str(), *this),
-	 handler(_handler)
-{
-	request_headers.Append((std::string("X-Tidal-Token:")
-				+ token).c_str());
-	request.SetOption(CURLOPT_HTTPHEADER, request_headers.Get());
-}
-
-TidalTagScanner::~TidalTagScanner() noexcept
-{
-	request.StopIndirect();
-}
-
-std::unique_ptr<CurlResponseParser>
-TidalTagScanner::MakeParser(unsigned status,
-			      std::multimap<std::string, std::string> &&headers)
-{
-	if (status != 200)
-		return std::make_unique<TidalErrorParser>(status, headers);
-
-	auto i = headers.find("content-type");
-	if (i == headers.end() || i->second.find("/json") == i->second.npos)
-		throw std::runtime_error("Not a JSON response from Tidal");
-
-	return std::make_unique<ResponseParser>();
-}
-
-void
-TidalTagScanner::FinishParser(std::unique_ptr<CurlResponseParser> p)
-{
-	assert(dynamic_cast<ResponseParser *>(p.get()) != nullptr);
-	auto &rp = (ResponseParser &)*p;
-	handler.OnRemoteTag(rp.GetTag());
-}
-
-void
-TidalTagScanner::OnError(std::exception_ptr e) noexcept
-{
-	handler.OnRemoteTagError(e);
-}
-
-inline bool
-TidalTagScanner::ResponseParser::Integer(long long value) noexcept
-{
-	switch (state) {
-	case State::NONE:
-	case State::TITLE:
-	case State::ARTIST:
-	case State::ARTIST_NAME:
-	case State::ALBUM:
-	case State::ALBUM_TITLE:
-		break;
-
-	case State::DURATION:
-		if (map_depth == 1 && value > 0)
-			tag.SetDuration(SignedSongTime::FromS((unsigned)value));
-		break;
-	}
-
-	return true;
-}
-
-inline bool
-TidalTagScanner::ResponseParser::String(StringView value) noexcept
-{
-	switch (state) {
-	case State::NONE:
-	case State::DURATION:
-	case State::ARTIST:
-	case State::ALBUM:
-		break;
-
-	case State::TITLE:
-		if (map_depth == 1)
-			tag.AddItem(TAG_TITLE, value);
-		break;
-
-	case State::ARTIST_NAME:
-		if (map_depth == 2)
-			tag.AddItem(TAG_ARTIST, value);
-		break;
-
-	case State::ALBUM_TITLE:
-		if (map_depth == 2)
-			tag.AddItem(TAG_ALBUM, value);
-		break;
-	}
-
-	return true;
-}
-
-inline bool
-TidalTagScanner::ResponseParser::StartMap() noexcept
-{
-	++map_depth;
-	return true;
-}
-
-inline bool
-TidalTagScanner::ResponseParser::MapKey(StringView value) noexcept
-{
-	switch (map_depth) {
-	case 1:
-		if (value.Equals("title"))
-			state = State::TITLE;
-		else if (value.Equals("duration"))
-			state = State::DURATION;
-		else if (value.Equals("artist"))
-			state = State::ARTIST;
-		else if (value.Equals("album"))
-			state = State::ALBUM;
-		else
-			state = State::NONE;
-		break;
-
-	case 2:
-		switch (state) {
-		case State::NONE:
-		case State::TITLE:
-		case State::DURATION:
-			break;
-
-		case State::ARTIST:
-		case State::ARTIST_NAME:
-			if (value.Equals("name"))
-				state = State::ARTIST_NAME;
-			else
-				state = State::ARTIST;
-			break;
-
-		case State::ALBUM:
-		case State::ALBUM_TITLE:
-			if (value.Equals("title"))
-				state = State::ALBUM_TITLE;
-			else
-				state = State::ALBUM;
-			break;
-		}
-		break;
-	}
-
-	return true;
-}
-
-inline bool
-TidalTagScanner::ResponseParser::EndMap() noexcept
-{
-	switch (map_depth) {
-	case 2:
-		state = State::NONE;
-		break;
-	}
-
-	--map_depth;
-
-	return true;
-}
diff --git a/src/input/plugins/TidalTagScanner.hxx b/src/input/plugins/TidalTagScanner.hxx
deleted file mode 100644
index b7c8cd3bc..000000000
--- a/src/input/plugins/TidalTagScanner.hxx
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright 2003-2021 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#ifndef TIDAL_TAG_SCANNER_HXX
-#define TIDAL_TAG_SCANNER_HXX
-
-#include "lib/curl/Delegate.hxx"
-#include "lib/curl/Slist.hxx"
-#include "lib/curl/Request.hxx"
-#include "input/RemoteTagScanner.hxx"
-
-class TidalTagScanner final
-	: public RemoteTagScanner, DelegateCurlResponseHandler
-{
-	CurlSlist request_headers;
-
-	CurlRequest request;
-
-	RemoteTagHandler &handler;
-
-public:
-	class ResponseParser;
-
-	TidalTagScanner(CurlGlobal &curl,
-			const char *base_url, const char *token,
-			const char *track_id,
-			RemoteTagHandler &_handler);
-
-	~TidalTagScanner() noexcept override;
-
-	void Start() override {
-		request.StartIndirect();
-	}
-
-private:
-	/* virtual methods from DelegateCurlResponseHandler */
-	std::unique_ptr<CurlResponseParser> MakeParser(unsigned status,
-						       std::multimap<std::string, std::string> &&headers) override;
-	void FinishParser(std::unique_ptr<CurlResponseParser> p) override;
-
-	/* virtual methods from CurlResponseHandler */
-	void OnError(std::exception_ptr e) noexcept override;
-};
-
-#endif
diff --git a/src/input/plugins/TidalTrackRequest.cxx b/src/input/plugins/TidalTrackRequest.cxx
deleted file mode 100644
index bced95ef9..000000000
--- a/src/input/plugins/TidalTrackRequest.cxx
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright 2003-2021 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#include "TidalTrackRequest.hxx"
-#include "TidalErrorParser.hxx"
-#include "lib/yajl/Callbacks.hxx"
-
-#include <cassert>
-
-using Wrapper = Yajl::CallbacksWrapper<TidalTrackRequest::ResponseParser>;
-static constexpr yajl_callbacks parse_callbacks = {
-	nullptr,
-	nullptr,
-	nullptr,
-	nullptr,
-	nullptr,
-	Wrapper::String,
-	nullptr,
-	Wrapper::MapKey,
-	Wrapper::EndMap,
-	nullptr,
-	nullptr,
-};
-
-class TidalTrackRequest::ResponseParser final : public YajlResponseParser {
-	enum class State {
-		NONE,
-		URLS,
-	} state = State::NONE;
-
-	std::string url;
-
-public:
-	explicit ResponseParser() noexcept
-		:YajlResponseParser(&parse_callbacks, nullptr, this) {}
-
-	std::string &&GetUrl() {
-		if (url.empty())
-			throw std::runtime_error("No url in track response");
-
-		return std::move(url);
-	}
-
-	/* yajl callbacks */
-	bool String(StringView value) noexcept;
-	bool MapKey(StringView value) noexcept;
-	bool EndMap() noexcept;
-};
-
-static std::string
-MakeTrackUrl(const char *base_url, const char *track_id,
-	     const char *audioquality) noexcept
-{
-	return std::string(base_url)
-		+ "/tracks/"
-		+ track_id
-		+ "/urlpostpaywall?assetpresentation=FULL&audioquality="
-		+ audioquality + "&urlusagemode=STREAM";
-}
-
-TidalTrackRequest::TidalTrackRequest(CurlGlobal &curl,
-				     const char *base_url, const char *token,
-				     const char *session,
-				     const char *track_id,
-				     const char *audioquality,
-				     TidalTrackHandler &_handler)
-	:request(curl, MakeTrackUrl(base_url, track_id, audioquality).c_str(),
-		 *this),
-	 handler(_handler)
-{
-	request_headers.Append((std::string("X-Tidal-Token:")
-				+ token).c_str());
-	request_headers.Append((std::string("X-Tidal-SessionId:")
-				+ session).c_str());
-	request.SetOption(CURLOPT_HTTPHEADER, request_headers.Get());
-}
-
-TidalTrackRequest::~TidalTrackRequest() noexcept
-{
-	request.StopIndirect();
-}
-
-std::unique_ptr<CurlResponseParser>
-TidalTrackRequest::MakeParser(unsigned status,
-			      std::multimap<std::string, std::string> &&headers)
-{
-	if (status != 200)
-		return std::make_unique<TidalErrorParser>(status, headers);
-
-	auto i = headers.find("content-type");
-	if (i == headers.end() || i->second.find("/json") == i->second.npos)
-		throw std::runtime_error("Not a JSON response from Tidal");
-
-	return std::make_unique<ResponseParser>();
-}
-
-void
-TidalTrackRequest::FinishParser(std::unique_ptr<CurlResponseParser> p)
-{
-	assert(dynamic_cast<ResponseParser *>(p.get()) != nullptr);
-	auto &rp = (ResponseParser &)*p;
-	handler.OnTidalTrackSuccess(rp.GetUrl());
-}
-
-void
-TidalTrackRequest::OnError(std::exception_ptr e) noexcept
-{
-	handler.OnTidalTrackError(e);
-}
-
-inline bool
-TidalTrackRequest::ResponseParser::String(StringView value) noexcept
-{
-	switch (state) {
-	case State::NONE:
-		break;
-
-	case State::URLS:
-		if (url.empty())
-			url.assign(value.data, value.size);
-		break;
-	}
-
-	return true;
-}
-
-inline bool
-TidalTrackRequest::ResponseParser::MapKey(StringView value) noexcept
-{
-	if (value.Equals("urls"))
-		state = State::URLS;
-	else
-		state = State::NONE;
-
-	return true;
-}
-
-inline bool
-TidalTrackRequest::ResponseParser::EndMap() noexcept
-{
-	state = State::NONE;
-
-	return true;
-}
diff --git a/src/input/plugins/TidalTrackRequest.hxx b/src/input/plugins/TidalTrackRequest.hxx
deleted file mode 100644
index 44f3dad6b..000000000
--- a/src/input/plugins/TidalTrackRequest.hxx
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 2003-2021 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#ifndef TIDAL_TRACK_REQUEST_HXX
-#define TIDAL_TRACK_REQUEST_HXX
-
-#include "lib/curl/Delegate.hxx"
-#include "lib/curl/Slist.hxx"
-#include "lib/curl/Request.hxx"
-
-/**
- * Callback class for #TidalTrackRequest.
- *
- * Its methods must be thread-safe.
- */
-class TidalTrackHandler {
-public:
-	virtual void OnTidalTrackSuccess(std::string url) noexcept = 0;
-	virtual void OnTidalTrackError(std::exception_ptr error) noexcept = 0;
-};
-
-/**
- * An asynchronous request for the streaming URL of a Tidal track.
- *
- * After construction, call Start() to initiate the request.
- */
-class TidalTrackRequest final : DelegateCurlResponseHandler {
-	CurlSlist request_headers;
-
-	CurlRequest request;
-
-	TidalTrackHandler &handler;
-
-public:
-	class ResponseParser;
-
-	TidalTrackRequest(CurlGlobal &curl,
-			  const char *base_url, const char *token,
-			  const char *session,
-			  const char *track_id,
-			  const char *audioquality,
-			  TidalTrackHandler &_handler);
-
-	~TidalTrackRequest() noexcept;
-
-	void Start() {
-		request.StartIndirect();
-	}
-
-private:
-	/* virtual methods from DelegateCurlResponseHandler */
-	std::unique_ptr<CurlResponseParser> MakeParser(unsigned status,
-						       std::multimap<std::string, std::string> &&headers) override;
-	void FinishParser(std::unique_ptr<CurlResponseParser> p) override;
-
-	/* virtual methods from CurlResponseHandler */
-	void OnError(std::exception_ptr e) noexcept override;
-};
-
-#endif
diff --git a/src/input/plugins/meson.build b/src/input/plugins/meson.build
index 73223654f..9802e942e 100644
--- a/src/input/plugins/meson.build
+++ b/src/input/plugins/meson.build
@@ -63,27 +63,6 @@ if enable_qobuz
   ]
 endif
 
-tidal_feature = get_option('tidal')
-if tidal_feature.disabled()
-  enable_tidal = false
-else
-  enable_tidal = curl_dep.found() and yajl_dep.found()
-  if not enable_tidal and tidal_feature.enabled()
-    error('Tidal requires CURL and libyajl')
-  endif
-endif
-input_features.set('ENABLE_TIDAL', enable_tidal)
-if enable_tidal
-  input_plugins_sources += [
-    'TidalErrorParser.cxx',
-    'TidalLoginRequest.cxx',
-    'TidalSessionManager.cxx',
-    'TidalTrackRequest.cxx',
-    'TidalTagScanner.cxx',
-    'TidalInputPlugin.cxx',
-  ]
-endif
-
 input_plugins = static_library(
   'input_plugins',
   input_plugins_sources,

From 4b2d9e544c168c0c876e366e806416df31dc8607 Mon Sep 17 00:00:00 2001
From: Cebtenzzre <cebtenzzre@gmail.com>
Date: Wed, 23 Jun 2021 17:39:30 -0400
Subject: [PATCH 10/16] tag/Pool: add [[nodiscard]]

---
 src/tag/Pool.hxx | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/tag/Pool.hxx b/src/tag/Pool.hxx
index 54df15c89..9ec9ab0cb 100644
--- a/src/tag/Pool.hxx
+++ b/src/tag/Pool.hxx
@@ -28,9 +28,11 @@ extern Mutex tag_pool_lock;
 struct TagItem;
 struct StringView;
 
+[[nodiscard]]
 TagItem *
 tag_pool_get_item(TagType type, StringView value) noexcept;
 
+[[nodiscard]]
 TagItem *
 tag_pool_dup_item(TagItem *item) noexcept;
 

From ba13b4b5d617aa1073051ac73df9e41b42dbbb21 Mon Sep 17 00:00:00 2001
From: Cebtenzzre <cebtenzzre@gmail.com>
Date: Wed, 23 Jun 2021 17:39:30 -0400
Subject: [PATCH 11/16] tag/Builder: use RemoveAll() to give up references

---
 src/tag/Builder.cxx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/tag/Builder.cxx b/src/tag/Builder.cxx
index 5a60a6e78..3f265f5e2 100644
--- a/src/tag/Builder.cxx
+++ b/src/tag/Builder.cxx
@@ -92,7 +92,7 @@ TagBuilder::operator=(Tag &&other) noexcept
 	/* move all TagItem pointers from the Tag object; we don't
 	   need to contact the tag pool, because all we do is move
 	   references */
-	items.clear();
+	RemoveAll();
 	items.reserve(other.num_items);
 	std::copy_n(other.items, other.num_items, std::back_inserter(items));
 

From 624c77ab43e7c4c845b2a01c0a716d3f4a4d409e Mon Sep 17 00:00:00 2001
From: Cebtenzzre <cebtenzzre@gmail.com>
Date: Wed, 23 Jun 2021 17:39:30 -0400
Subject: [PATCH 12/16] tag/Builder: another missing RemoveAll() call

---
 src/tag/Builder.cxx | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/tag/Builder.cxx b/src/tag/Builder.cxx
index 3f265f5e2..8f38ae172 100644
--- a/src/tag/Builder.cxx
+++ b/src/tag/Builder.cxx
@@ -63,6 +63,8 @@ TagBuilder::operator=(const TagBuilder &other) noexcept
 	/* copy all attributes */
 	duration = other.duration;
 	has_playlist = other.has_playlist;
+
+	RemoveAll();
 	items = other.items;
 
 	/* increment the tag pool refcounters */

From 551c941b5a5cd9beea2e2970103839d5535e2d11 Mon Sep 17 00:00:00 2001
From: Cebtenzzre <cebtenzzre@gmail.com>
Date: Wed, 23 Jun 2021 17:39:30 -0400
Subject: [PATCH 13/16] tag/Builder: don't ignore the result of
 tag_pool_dup_item

Also, use RemoveAll() instead of directly clearing TagBuilder::items in
most cases, as its elements represent references that must be released.

Closes #1023
---
 NEWS                | 2 ++
 src/tag/Builder.cxx | 6 ++++--
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/NEWS b/NEWS
index 810379466..f42c8545a 100644
--- a/NEWS
+++ b/NEWS
@@ -6,6 +6,8 @@ ver 0.22.10 (not yet released)
 * input
   - curl: fix crash bug after stream with Icy metadata was closed by peer
   - tidal: remove defunct unmaintained plugin
+* tags
+  - fix crash caused by bug in TagBuilder and a few potential reference leaks
 
 ver 0.22.9 (2021/06/23)
 * database
diff --git a/src/tag/Builder.cxx b/src/tag/Builder.cxx
index 8f38ae172..76b404499 100644
--- a/src/tag/Builder.cxx
+++ b/src/tag/Builder.cxx
@@ -69,8 +69,8 @@ TagBuilder::operator=(const TagBuilder &other) noexcept
 
 	/* increment the tag pool refcounters */
 	const std::lock_guard<Mutex> protect(tag_pool_lock);
-	for (auto i : items)
-		tag_pool_dup_item(i);
+	for (auto &i : items)
+		i = tag_pool_dup_item(i);
 
 	return *this;
 }
@@ -80,6 +80,8 @@ TagBuilder::operator=(TagBuilder &&other) noexcept
 {
 	duration = other.duration;
 	has_playlist = other.has_playlist;
+
+	RemoveAll();
 	items = std::move(other.items);
 
 	return *this;

From 96875921b72949592ce4fa07a5618df80b668443 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Thu, 5 Aug 2021 14:27:30 +0200
Subject: [PATCH 14/16] tag/Builder: use std::swap() in move operator

This way, we save the overhead for acquiring the tag_pool_lock.
---
 src/tag/Builder.cxx | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/tag/Builder.cxx b/src/tag/Builder.cxx
index 76b404499..e5e380399 100644
--- a/src/tag/Builder.cxx
+++ b/src/tag/Builder.cxx
@@ -78,11 +78,14 @@ TagBuilder::operator=(const TagBuilder &other) noexcept
 TagBuilder &
 TagBuilder::operator=(TagBuilder &&other) noexcept
 {
+	using std::swap;
+
 	duration = other.duration;
 	has_playlist = other.has_playlist;
 
-	RemoveAll();
-	items = std::move(other.items);
+	/* swap the two TagItem lists so we don't need to touch the
+	   tag pool just yet */
+	swap(items, other.items);
 
 	return *this;
 }

From bde64a13e24f1bfc6d9319b916b304ad434cd027 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Thu, 5 Aug 2021 14:30:07 +0200
Subject: [PATCH 15/16] tag/Builder: do not acquire tag_pool_lock if TagItem
 list is empty

---
 src/tag/Builder.cxx | 41 +++++++++++++++++++++++++++--------------
 1 file changed, 27 insertions(+), 14 deletions(-)

diff --git a/src/tag/Builder.cxx b/src/tag/Builder.cxx
index e5e380399..b2887c903 100644
--- a/src/tag/Builder.cxx
+++ b/src/tag/Builder.cxx
@@ -36,10 +36,12 @@ TagBuilder::TagBuilder(const Tag &other) noexcept
 {
 	items.reserve(other.num_items);
 
-	const std::lock_guard<Mutex> protect(tag_pool_lock);
-
-	for (unsigned i = 0, n = other.num_items; i != n; ++i)
-		items.push_back(tag_pool_dup_item(other.items[i]));
+	const std::size_t n = other.num_items;
+	if (n > 0) {
+		const std::lock_guard<Mutex> protect(tag_pool_lock);
+		for (std::size_t i = 0; i != n; ++i)
+			items.push_back(tag_pool_dup_item(other.items[i]));
+	}
 }
 
 TagBuilder::TagBuilder(Tag &&other) noexcept
@@ -65,12 +67,15 @@ TagBuilder::operator=(const TagBuilder &other) noexcept
 	has_playlist = other.has_playlist;
 
 	RemoveAll();
-	items = other.items;
 
-	/* increment the tag pool refcounters */
-	const std::lock_guard<Mutex> protect(tag_pool_lock);
-	for (auto &i : items)
-		i = tag_pool_dup_item(i);
+	if (!other.items.empty()) {
+		items = other.items;
+
+		/* increment the tag pool refcounters */
+		const std::lock_guard<Mutex> protect(tag_pool_lock);
+		for (auto &i : items)
+			i = tag_pool_dup_item(i);
+	}
 
 	return *this;
 }
@@ -181,11 +186,14 @@ TagBuilder::Complement(const Tag &other) noexcept
 
 	items.reserve(items.size() + other.num_items);
 
-	const std::lock_guard<Mutex> protect(tag_pool_lock);
-	for (unsigned i = 0, n = other.num_items; i != n; ++i) {
-		TagItem *item = other.items[i];
-		if (!present[item->type])
-			items.push_back(tag_pool_dup_item(item));
+	const std::size_t n = other.num_items;
+	if (n > 0) {
+		const std::lock_guard<Mutex> protect(tag_pool_lock);
+		for (std::size_t i = 0; i != n; ++i) {
+			TagItem *item = other.items[i];
+			if (!present[item->type])
+				items.push_back(tag_pool_dup_item(item));
+		}
 	}
 }
 
@@ -245,6 +253,11 @@ TagBuilder::AddEmptyItem(TagType type) noexcept
 void
 TagBuilder::RemoveAll() noexcept
 {
+	if (items.empty())
+		/* don't acquire the tag_pool_lock if we're not going
+		   to call tag_pool_put_item() anyway */
+		return;
+
 	{
 		const std::lock_guard<Mutex> protect(tag_pool_lock);
 		for (auto i : items)

From 17b0ac75ca9d8d81e4eabaa8f11bec68830eb9ba Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Thu, 5 Aug 2021 15:07:27 +0200
Subject: [PATCH 16/16] output/oss: always enable PcmExport for
 alsa_channel_order

We need this even when AFMT_S24_PACKED is not available, for the
correct channel order in multi-channel files.  Internally, MPD uses
FLAC channel order, but OSS uses the same channel order as ALSA.
---
 NEWS                                   |  2 +
 src/output/plugins/OssOutputPlugin.cxx | 65 ++++++--------------------
 2 files changed, 17 insertions(+), 50 deletions(-)

diff --git a/NEWS b/NEWS
index f42c8545a..a56c573e9 100644
--- a/NEWS
+++ b/NEWS
@@ -8,6 +8,8 @@ ver 0.22.10 (not yet released)
   - tidal: remove defunct unmaintained plugin
 * tags
   - fix crash caused by bug in TagBuilder and a few potential reference leaks
+* output
+  - oss: fix channel order of multi-channel files
 
 ver 0.22.9 (2021/06/23)
 * database
diff --git a/src/output/plugins/OssOutputPlugin.cxx b/src/output/plugins/OssOutputPlugin.cxx
index 13dbcbcbe..5598140b4 100644
--- a/src/output/plugins/OssOutputPlugin.cxx
+++ b/src/output/plugins/OssOutputPlugin.cxx
@@ -20,11 +20,13 @@
 #include "OssOutputPlugin.hxx"
 #include "../OutputAPI.hxx"
 #include "mixer/MixerList.hxx"
+#include "pcm/Export.hxx"
 #include "io/UniqueFileDescriptor.hxx"
 #include "system/Error.hxx"
 #include "util/ConstBuffer.hxx"
 #include "util/Domain.hxx"
 #include "util/ByteOrder.hxx"
+#include "util/Manual.hxx"
 #include "Log.hxx"
 
 #include <cassert>
@@ -53,15 +55,8 @@
 #undef AFMT_S24_NE
 #endif
 
-#ifdef AFMT_S24_PACKED
-#include "pcm/Export.hxx"
-#include "util/Manual.hxx"
-#endif
-
 class OssOutput final : AudioOutput {
-#ifdef AFMT_S24_PACKED
 	Manual<PcmExport> pcm_export;
-#endif
 
 	FileDescriptor fd = FileDescriptor::Undefined();
 	const char *device;
@@ -78,11 +73,7 @@ class OssOutput final : AudioOutput {
 	 */
 	int oss_format;
 
-#ifdef AFMT_S24_PACKED
 	static constexpr unsigned oss_flags = FLAG_ENABLE_DISABLE;
-#else
-	static constexpr unsigned oss_flags = 0;
-#endif
 
 public:
 	explicit OssOutput(const char *_device=nullptr)
@@ -92,7 +83,6 @@ public:
 	static AudioOutput *Create(EventLoop &event_loop,
 				   const ConfigBlock &block);
 
-#ifdef AFMT_S24_PACKED
 	void Enable() override {
 		pcm_export.Construct();
 	}
@@ -100,7 +90,6 @@ public:
 	void Disable() noexcept override {
 		pcm_export.Destruct();
 	}
-#endif
 
 	void Open(AudioFormat &audio_format) override;
 
@@ -428,11 +417,8 @@ sample_format_from_oss(int format) noexcept
 static bool
 oss_probe_sample_format(FileDescriptor fd, SampleFormat sample_format,
 			SampleFormat *sample_format_r,
-			int *oss_format_r
-#ifdef AFMT_S24_PACKED
-			, PcmExport &pcm_export
-#endif
-			)
+			int *oss_format_r,
+			PcmExport &pcm_export)
 {
 	int oss_format = sample_format_to_oss(sample_format);
 	if (oss_format == AFMT_QUERY)
@@ -464,15 +450,15 @@ oss_probe_sample_format(FileDescriptor fd, SampleFormat sample_format,
 	*sample_format_r = sample_format;
 	*oss_format_r = oss_format;
 
-#ifdef AFMT_S24_PACKED
 	PcmExport::Params params;
 	params.alsa_channel_order = true;
+#ifdef AFMT_S24_PACKED
 	params.pack24 = oss_format == AFMT_S24_PACKED;
 	params.reverse_endian = oss_format == AFMT_S24_PACKED &&
 		!IsLittleEndian();
+#endif
 
 	pcm_export.Open(sample_format, 0, params);
-#endif
 
 	return true;
 }
@@ -483,19 +469,13 @@ oss_probe_sample_format(FileDescriptor fd, SampleFormat sample_format,
  */
 static void
 oss_setup_sample_format(FileDescriptor fd, AudioFormat &audio_format,
-			int *oss_format_r
-#ifdef AFMT_S24_PACKED
-			, PcmExport &pcm_export
-#endif
-			)
+			int *oss_format_r,
+			PcmExport &pcm_export)
 {
 	SampleFormat mpd_format;
 	if (oss_probe_sample_format(fd, audio_format.format,
-				    &mpd_format, oss_format_r
-#ifdef AFMT_S24_PACKED
-				    , pcm_export
-#endif
-				    )) {
+				    &mpd_format, oss_format_r,
+				    pcm_export)) {
 		audio_format.format = mpd_format;
 		return;
 	}
@@ -518,11 +498,8 @@ oss_setup_sample_format(FileDescriptor fd, AudioFormat &audio_format,
 			continue;
 
 		if (oss_probe_sample_format(fd, mpd_format,
-					    &mpd_format, oss_format_r
-#ifdef AFMT_S24_PACKED
-					    , pcm_export
-#endif
-					    )) {
+					    &mpd_format, oss_format_r,
+					    pcm_export)) {
 			audio_format.format = mpd_format;
 			return;
 		}
@@ -536,11 +513,7 @@ OssOutput::Setup(AudioFormat &_audio_format)
 {
 	oss_setup_channels(fd, _audio_format);
 	oss_setup_sample_rate(fd, _audio_format);
-	oss_setup_sample_format(fd, _audio_format, &oss_format
-#ifdef AFMT_S24_PACKED
-				, pcm_export
-#endif
-				);
+	oss_setup_sample_format(fd, _audio_format, &oss_format, pcm_export);
 }
 
 /**
@@ -595,9 +568,7 @@ OssOutput::Cancel() noexcept
 		DoClose();
 	}
 
-#ifdef AFMT_S24_PACKED
 	pcm_export->Reset();
-#endif
 }
 
 size_t
@@ -611,23 +582,17 @@ OssOutput::Play(const void *chunk, size_t size)
 	if (!fd.IsDefined())
 		Reopen();
 
-#ifdef AFMT_S24_PACKED
 	const auto e = pcm_export->Export({chunk, size});
 	if (e.empty())
 		return size;
 
 	chunk = e.data;
 	size = e.size;
-#endif
 
 	while (true) {
 		ret = fd.Write(chunk, size);
-		if (ret > 0) {
-#ifdef AFMT_S24_PACKED
-			ret = pcm_export->CalcInputSize(ret);
-#endif
-			return ret;
-		}
+		if (ret > 0)
+			return pcm_export->CalcInputSize(ret);
 
 		if (ret < 0 && errno != EINTR)
 			throw FormatErrno("Write error on %s", device);