From da563940b477955d78d4fb7352e0d879395fab7e Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 22 Oct 2018 08:32:32 +0200
Subject: [PATCH 01/13] python/build/libs.py: upgrade libmpdclient to 2.16

---
 python/build/libs.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/python/build/libs.py b/python/build/libs.py
index e4f621daf..81b6cd79d 100644
--- a/python/build/libs.py
+++ b/python/build/libs.py
@@ -9,8 +9,8 @@ from build.ffmpeg import FfmpegProject
 from build.boost import BoostProject
 
 libmpdclient = MesonProject(
-    'https://www.musicpd.org/download/libmpdclient/2/libmpdclient-2.14.tar.xz',
-    '0a84e2791bfe3077cf22ee1784c805d5bb550803dffe56a39aa3690a38061372',
+    'https://www.musicpd.org/download/libmpdclient/2/libmpdclient-2.16.tar.xz',
+    'fa6bdab67c0e0490302b38f00c27b4959735c3ec8aef7a88327adb1407654464',
     'lib/libmpdclient.a',
 )
 

From 850d208b7b3de08acdd7b8a09dce6595add9b6ea Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 22 Oct 2018 08:44:11 +0200
Subject: [PATCH 02/13] python/build/libs.py: upgrade Opus to 1.3

---
 python/build/libs.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/python/build/libs.py b/python/build/libs.py
index 81b6cd79d..eed00fdbe 100644
--- a/python/build/libs.py
+++ b/python/build/libs.py
@@ -38,8 +38,8 @@ libvorbis = AutotoolsProject(
 )
 
 opus = AutotoolsProject(
-    'https://archive.mozilla.org/pub/opus/opus-1.2.1.tar.gz',
-    'cfafd339ccd9c5ef8d6ab15d7e1a412c054bf4cb4ecbbbcc78c12ef2def70732',
+    'https://archive.mozilla.org/pub/opus/opus-1.3.tar.gz',
+    '4f3d69aefdf2dbaf9825408e452a8a414ffc60494c70633560700398820dc550',
     'lib/libopus.a',
     [
         '--disable-shared', '--enable-static',

From 3f3f0af543e33ede947bdb19809243064224f4d7 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 22 Oct 2018 08:53:30 +0200
Subject: [PATCH 03/13] python/build/libs.py: upgrade CURL to 7.61.1

---
 python/build/libs.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/python/build/libs.py b/python/build/libs.py
index eed00fdbe..025d933df 100644
--- a/python/build/libs.py
+++ b/python/build/libs.py
@@ -341,8 +341,8 @@ ffmpeg = FfmpegProject(
 )
 
 curl = AutotoolsProject(
-    'http://curl.haxx.se/download/curl-7.61.0.tar.xz',
-    'ef6e55192d04713673b4409ccbcb4cb6cd723137d6e10ca45b0c593a454e1720',
+    'http://curl.haxx.se/download/curl-7.61.1.tar.xz',
+    '3d5913d6a39bd22e68e34dff697fd6e4c3c81563f580c76fca2009315cd81891',
     'lib/libcurl.a',
     [
         '--disable-shared', '--enable-static',

From ff58b8d2551eb15810180e3fca0fac7f761c9d47 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 22 Oct 2018 10:03:47 +0200
Subject: [PATCH 04/13] tag/Builder: move code to AddItemUnchecked()

---
 src/tag/TagBuilder.cxx | 22 ++++++++++++----------
 src/tag/TagBuilder.hxx |  6 ++++++
 2 files changed, 18 insertions(+), 10 deletions(-)

diff --git a/src/tag/TagBuilder.cxx b/src/tag/TagBuilder.cxx
index 43cf3cc6f..e88fc6edd 100644
--- a/src/tag/TagBuilder.cxx
+++ b/src/tag/TagBuilder.cxx
@@ -188,6 +188,16 @@ TagBuilder::Complement(const Tag &other)
 	tag_pool_lock.unlock();
 }
 
+void
+TagBuilder::AddItemUnchecked(TagType type, StringView value) noexcept
+{
+	tag_pool_lock.lock();
+	auto i = tag_pool_get_item(type, value);
+	tag_pool_lock.unlock();
+
+	items.push_back(i);
+}
+
 inline void
 TagBuilder::AddItemInternal(TagType type, StringView value)
 {
@@ -197,13 +207,9 @@ TagBuilder::AddItemInternal(TagType type, StringView value)
 	if (!f.IsNull())
 		value = { f.data, f.size };
 
-	tag_pool_lock.lock();
-	auto i = tag_pool_get_item(type, value);
-	tag_pool_lock.unlock();
+	AddItemUnchecked(type, value);
 
 	free(f.data);
-
-	items.push_back(i);
 }
 
 void
@@ -229,11 +235,7 @@ TagBuilder::AddItem(TagType type, const char *value)
 void
 TagBuilder::AddEmptyItem(TagType type)
 {
-	tag_pool_lock.lock();
-	auto i = tag_pool_get_item(type, StringView::Empty());
-	tag_pool_lock.unlock();
-
-	items.push_back(i);
+	AddItemUnchecked(type, StringView::Empty());
 }
 
 void
diff --git a/src/tag/TagBuilder.hxx b/src/tag/TagBuilder.hxx
index b69d23689..82ed0227b 100644
--- a/src/tag/TagBuilder.hxx
+++ b/src/tag/TagBuilder.hxx
@@ -132,6 +132,12 @@ public:
 	 */
 	void Complement(const Tag &other);
 
+	/**
+	 * A variant of AddItem() which does not attempt to fix up the
+	 * value and does not check whether the tag type is disabled.
+	 */
+	void AddItemUnchecked(TagType type, StringView value) noexcept;
+
 	/**
 	 * Appends a new tag item.
 	 *

From 4bc5333995528d2d9f4d8ad39f820a5b96fac178 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 22 Oct 2018 10:06:04 +0200
Subject: [PATCH 05/13] tag/Set: use TagBuilder::AddItemUnchecked()

This improves the workaround from commit
b5ba94f1de06c621da937241eedfcfb100f26a09 and actually gives a useful
result for "list" with a disabled tag.
---
 src/tag/Set.cxx | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/src/tag/Set.cxx b/src/tag/Set.cxx
index 34e823c89..81e9eb9b6 100644
--- a/src/tag/Set.cxx
+++ b/src/tag/Set.cxx
@@ -20,6 +20,7 @@
 #include "Set.hxx"
 #include "TagBuilder.hxx"
 #include "Settings.hxx"
+#include "util/StringView.hxx"
 
 #include <assert.h>
 
@@ -70,10 +71,7 @@ TagSet::InsertUnique(const Tag &src, TagType type, const char *value,
 		     tag_mask_t group_mask) noexcept
 {
 	TagBuilder builder;
-	if (value == nullptr)
-		builder.AddEmptyItem(type);
-	else
-		builder.AddItem(type, value);
+	builder.AddItemUnchecked(type, value);
 	CopyTagMask(builder, src, group_mask);
 #if CLANG_OR_GCC_VERSION(4,8)
 	emplace(builder.Commit());
@@ -110,8 +108,7 @@ TagSet::InsertUnique(const Tag &tag,
 
 	if (!CheckUnique(type, tag, type, group_mask) &&
 	    (type != TAG_ALBUM_ARTIST ||
-	     !IsTagEnabled(TAG_ALBUM_ARTIST) ||
 	     /* fall back to "Artist" if no "AlbumArtist" was found */
 	     !CheckUnique(type, tag, TAG_ARTIST, group_mask)))
-		InsertUnique(tag, type, nullptr, group_mask);
+		InsertUnique(tag, type, "", group_mask);
 }

From 6b9966e9694aa304cff4ac46308c464ae830d586 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 22 Oct 2018 10:09:56 +0200
Subject: [PATCH 06/13] tag/Set: include cleanup

---
 src/tag/Set.cxx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/tag/Set.cxx b/src/tag/Set.cxx
index 81e9eb9b6..fa4ecaf65 100644
--- a/src/tag/Set.cxx
+++ b/src/tag/Set.cxx
@@ -19,7 +19,6 @@
 
 #include "Set.hxx"
 #include "TagBuilder.hxx"
-#include "Settings.hxx"
 #include "util/StringView.hxx"
 
 #include <assert.h>

From 94aed92e9ad3da465a0f9aa6a65ad1746061fc62 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 22 Oct 2018 09:43:16 +0200
Subject: [PATCH 07/13] tag/Set: move code to ApplyTagWithFallback()

---
 Makefile.am          |  1 +
 src/tag/Fallback.hxx | 43 +++++++++++++++++++++++++++++++++++++++++++
 src/tag/Set.cxx      | 22 +++++++++++++---------
 3 files changed, 57 insertions(+), 9 deletions(-)
 create mode 100644 src/tag/Fallback.hxx

diff --git a/Makefile.am b/Makefile.am
index ca800617a..f746d25aa 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -977,6 +977,7 @@ libtag_a_SOURCES =\
 	src/tag/TagItem.hxx \
 	src/tag/TagHandler.cxx src/tag/TagHandler.hxx \
 	src/tag/Mask.hxx \
+	src/tag/Fallback.hxx \
 	src/tag/Settings.cxx src/tag/Settings.hxx \
 	src/tag/TagConfig.cxx src/tag/TagConfig.hxx \
 	src/tag/TagNames.c \
diff --git a/src/tag/Fallback.hxx b/src/tag/Fallback.hxx
new file mode 100644
index 000000000..5c75e8bee
--- /dev/null
+++ b/src/tag/Fallback.hxx
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2003-2018 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 MPD_TAG_FALLBACK_HXX
+#define MPD_TAG_FALLBACK_HXX
+
+#include <utility>
+
+template<typename F>
+bool
+ApplyTagFallback(TagType type, F &&f) noexcept
+{
+	if (type == TAG_ALBUM_ARTIST)
+		/* fall back to "Artist" if no "AlbumArtist" was found */
+		return f(TAG_ARTIST);
+
+	return false;
+}
+
+template<typename F>
+bool
+ApplyTagWithFallback(TagType type, F &&f) noexcept
+{
+	return f(type) || ApplyTagFallback(type, std::forward<F>(f));
+}
+
+#endif
diff --git a/src/tag/Set.cxx b/src/tag/Set.cxx
index fa4ecaf65..d52d517aa 100644
--- a/src/tag/Set.cxx
+++ b/src/tag/Set.cxx
@@ -18,17 +18,20 @@
  */
 
 #include "Set.hxx"
+#include "Fallback.hxx"
 #include "TagBuilder.hxx"
 #include "util/StringView.hxx"
 
+#include <functional>
+
 #include <assert.h>
 
 /**
  * Copy all tag items of the specified type.
  */
 static bool
-CopyTagItem(TagBuilder &dest, TagType dest_type,
-	    const Tag &src, TagType src_type)
+CopyTagItem2(TagBuilder &dest, TagType dest_type,
+	     const Tag &src, TagType src_type)
 {
 	bool found = false;
 
@@ -49,9 +52,9 @@ CopyTagItem(TagBuilder &dest, TagType dest_type,
 static void
 CopyTagItem(TagBuilder &dest, const Tag &src, TagType type)
 {
-	if (!CopyTagItem(dest, type, src, type) &&
-	    type == TAG_ALBUM_ARTIST)
-		CopyTagItem(dest, type, src, TAG_ARTIST);
+	ApplyTagWithFallback(type,
+			     std::bind(CopyTagItem2, std::ref(dest), type,
+				       std::cref(src), std::placeholders::_1));
 }
 
 /**
@@ -105,9 +108,10 @@ TagSet::InsertUnique(const Tag &tag,
 
 	assert((group_mask & (tag_mask_t(1) << unsigned(type))) == 0);
 
-	if (!CheckUnique(type, tag, type, group_mask) &&
-	    (type != TAG_ALBUM_ARTIST ||
-	     /* fall back to "Artist" if no "AlbumArtist" was found */
-	     !CheckUnique(type, tag, TAG_ARTIST, group_mask)))
+	if (!ApplyTagWithFallback(type,
+				  std::bind(&TagSet::CheckUnique, this,
+					    type, std::cref(tag),
+					    std::placeholders::_1,
+					    group_mask)))
 		InsertUnique(tag, type, "", group_mask);
 }

From 0340b01392a750a086b19abfdc47672308e0a2da Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 22 Oct 2018 10:44:36 +0200
Subject: [PATCH 08/13] db/Count: use ApplyTagFallback()

---
 src/db/Count.cxx | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/db/Count.cxx b/src/db/Count.cxx
index 9c3945677..72f492ee3 100644
--- a/src/db/Count.cxx
+++ b/src/db/Count.cxx
@@ -25,6 +25,7 @@
 #include "client/Response.hxx"
 #include "LightSong.hxx"
 #include "tag/Tag.hxx"
+#include "tag/Fallback.hxx"
 
 #include <functional>
 #include <map>
@@ -98,9 +99,9 @@ GroupCountVisitor(TagCountMap &map, TagType group, const LightSong &song)
 	assert(song.tag != nullptr);
 
 	const Tag &tag = *song.tag;
-	if (!CollectGroupCounts(map, group, tag) && group == TAG_ALBUM_ARTIST)
-		/* fall back to "Artist" if no "AlbumArtist" was found */
-		CollectGroupCounts(map, TAG_ARTIST, tag);
+	ApplyTagWithFallback(group,
+			     std::bind(CollectGroupCounts, std::ref(map),
+				       std::placeholders::_1, std::cref(tag)));
 }
 
 void

From 21adc78713881793958cd7606f96b099a93e020e Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 22 Oct 2018 10:41:39 +0200
Subject: [PATCH 09/13] SongFilter: use ApplyTagFallback()

---
 src/SongFilter.cxx | 26 ++++++++++++++++++--------
 1 file changed, 18 insertions(+), 8 deletions(-)

diff --git a/src/SongFilter.cxx b/src/SongFilter.cxx
index 2e0d6751a..faf5deb2a 100644
--- a/src/SongFilter.cxx
+++ b/src/SongFilter.cxx
@@ -22,6 +22,7 @@
 #include "db/LightSong.hxx"
 #include "DetachedSong.hxx"
 #include "tag/Tag.hxx"
+#include "tag/Fallback.hxx"
 #include "util/ConstBuffer.hxx"
 #include "util/StringAPI.hxx"
 #include "util/StringCompare.hxx"
@@ -118,14 +119,23 @@ SongFilter::Item::Match(const Tag &_tag) const noexcept
 		if (value.empty())
 			return true;
 
-		if (tag == TAG_ALBUM_ARTIST && visited_types[TAG_ARTIST]) {
-			/* if we're looking for "album artist", but
-			   only "artist" exists, use that */
-			for (const auto &item : _tag)
-				if (item.type == TAG_ARTIST &&
-				    StringMatch(item.value))
-					return true;
-		}
+		bool result = false;
+		if (ApplyTagFallback(TagType(tag),
+				     [&](TagType tag2) {
+			     if (!visited_types[tag2])
+				     return false;
+
+			     for (const auto &item : _tag) {
+				     if (item.type == tag2 &&
+					 StringMatch(item.value)) {
+					     result = true;
+					     break;
+				     }
+			     }
+
+			     return true;
+		     }))
+			return result;
 	}
 
 	return false;

From 53448e463383eca5633dc1d87e2004cae5dceaae Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 22 Oct 2018 10:52:42 +0200
Subject: [PATCH 10/13] tag/Fallback: add tag fallbacks for AlbumArtistSort,
 ArtistSort

Just like AlbumArtist falls back to Artist, AlbumArtistSort should
fall back tom AlbumArtist, ArtistSort and finally Artist.

Closes #355
---
 NEWS                 |  2 ++
 src/tag/Fallback.hxx | 14 ++++++++++++--
 2 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/NEWS b/NEWS
index 69adaf45e..9507057e7 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,6 @@
 ver 0.20.22 (not yet released)
+* protocol
+  - add tag fallbacks for AlbumArtistSort, ArtistSort
 * storage
   - curl: URL-encode paths
 * Android
diff --git a/src/tag/Fallback.hxx b/src/tag/Fallback.hxx
index 5c75e8bee..08e4dc80a 100644
--- a/src/tag/Fallback.hxx
+++ b/src/tag/Fallback.hxx
@@ -26,8 +26,18 @@ template<typename F>
 bool
 ApplyTagFallback(TagType type, F &&f) noexcept
 {
-	if (type == TAG_ALBUM_ARTIST)
-		/* fall back to "Artist" if no "AlbumArtist" was found */
+	if (type == TAG_ALBUM_ARTIST_SORT) {
+		/* fall back to "AlbumArtist", "ArtistSort" and
+		   "Artist" if no "AlbumArtistSort" was found */
+		if (f(TAG_ALBUM_ARTIST))
+			return true;
+
+		return ApplyTagFallback(TAG_ARTIST_SORT, std::forward<F>(f));
+	}
+
+	if (type == TAG_ALBUM_ARTIST || type == TAG_ARTIST_SORT)
+		/* fall back to "Artist" if no
+		   "AlbumArtist"/"ArtistSort" was found */
 		return f(TAG_ARTIST);
 
 	return false;

From 6c06244e83a7accb8fece35d755ad69f7102cb4a Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 22 Oct 2018 11:50:51 +0200
Subject: [PATCH 11/13] db/Count: move code to tag/VisitCallback.hxx

---
 Makefile.am               |  1 +
 src/db/Count.cxx          | 32 +++++++++---------------
 src/tag/VisitFallback.hxx | 52 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 65 insertions(+), 20 deletions(-)
 create mode 100644 src/tag/VisitFallback.hxx

diff --git a/Makefile.am b/Makefile.am
index f746d25aa..1061d838d 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -978,6 +978,7 @@ libtag_a_SOURCES =\
 	src/tag/TagHandler.cxx src/tag/TagHandler.hxx \
 	src/tag/Mask.hxx \
 	src/tag/Fallback.hxx \
+	src/tag/VisitFallback.hxx \
 	src/tag/Settings.cxx src/tag/Settings.hxx \
 	src/tag/TagConfig.cxx src/tag/TagConfig.hxx \
 	src/tag/TagNames.c \
diff --git a/src/db/Count.cxx b/src/db/Count.cxx
index 72f492ee3..ba0d443e7 100644
--- a/src/db/Count.cxx
+++ b/src/db/Count.cxx
@@ -25,7 +25,7 @@
 #include "client/Response.hxx"
 #include "LightSong.hxx"
 #include "tag/Tag.hxx"
-#include "tag/Fallback.hxx"
+#include "tag/VisitFallback.hxx"
 
 #include <functional>
 #include <map>
@@ -73,24 +73,15 @@ stats_visitor_song(SearchStats &stats, const LightSong &song)
 		stats.total_duration += duration;
 }
 
-static bool
-CollectGroupCounts(TagCountMap &map, TagType group, const Tag &tag)
+static void
+CollectGroupCounts(TagCountMap &map, const Tag &tag,
+		   const char *value) noexcept
 {
-	bool found = false;
-	for (const auto &item : tag) {
-		if (item.type == group) {
-			auto r = map.insert(std::make_pair(item.value,
-							   SearchStats()));
-			SearchStats &s = r.first->second;
-			++s.n_songs;
-			if (!tag.duration.IsNegative())
-				s.total_duration += tag.duration;
-
-			found = true;
-		}
-	}
-
-	return found;
+	auto r = map.insert(std::make_pair(value, SearchStats()));
+	SearchStats &s = r.first->second;
+	++s.n_songs;
+	if (!tag.duration.IsNegative())
+		s.total_duration += tag.duration;
 }
 
 static void
@@ -99,9 +90,10 @@ GroupCountVisitor(TagCountMap &map, TagType group, const LightSong &song)
 	assert(song.tag != nullptr);
 
 	const Tag &tag = *song.tag;
-	ApplyTagWithFallback(group,
+	VisitTagWithFallback(tag, group,
 			     std::bind(CollectGroupCounts, std::ref(map),
-				       std::placeholders::_1, std::cref(tag)));
+				       std::cref(tag),
+				       std::placeholders::_1));
 }
 
 void
diff --git a/src/tag/VisitFallback.hxx b/src/tag/VisitFallback.hxx
new file mode 100644
index 000000000..3ad486f5f
--- /dev/null
+++ b/src/tag/VisitFallback.hxx
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2003-2018 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 MPD_TAG_VISIT_FALLBACK_HXX
+#define MPD_TAG_VISIT_FALLBACK_HXX
+
+#include "Fallback.hxx"
+#include "Tag.hxx"
+
+template<typename F>
+bool
+VisitTagType(const Tag &tag, TagType type, F &&f) noexcept
+{
+	bool found = false;
+
+	for (const auto &item : tag) {
+		if (item.type == type) {
+			found = true;
+			f(item.value);
+		}
+	}
+
+	return found;
+}
+
+template<typename F>
+bool
+VisitTagWithFallback(const Tag &tag, TagType type, F &&f) noexcept
+{
+	return ApplyTagWithFallback(type,
+				    [&](TagType type2) {
+					    return VisitTagType(tag, type2, f);
+				    });
+}
+
+#endif

From 7cfe929c3692e74f4e6073960eaf0e08a9989811 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 22 Oct 2018 12:42:18 +0200
Subject: [PATCH 12/13] db/Count: print empty group if song without grouped tag
 exists

Be consistent with "list" responses.
---
 NEWS                      | 1 +
 doc/protocol.xml          | 5 +++++
 src/db/Count.cxx          | 8 ++++----
 src/tag/VisitFallback.hxx | 8 ++++++++
 4 files changed, 18 insertions(+), 4 deletions(-)

diff --git a/NEWS b/NEWS
index 9507057e7..5b8a3b48c 100644
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,7 @@
 ver 0.20.22 (not yet released)
 * protocol
   - add tag fallbacks for AlbumArtistSort, ArtistSort
+  - "count group ..." can print an empty group
 * storage
   - curl: URL-encode paths
 * Android
diff --git a/doc/protocol.xml b/doc/protocol.xml
index 4b9a4c95a..75cdb5478 100644
--- a/doc/protocol.xml
+++ b/doc/protocol.xml
@@ -1599,6 +1599,11 @@ OK
               per-artist counts:
             </para>
             <programlisting>count group artist</programlisting>
+            <para>
+              A group with an empty value contains counts of matching
+              song which don't this group tag.  It exists only if at
+              least one such song is found.
+            </para>
           </listitem>
         </varlistentry>
 
diff --git a/src/db/Count.cxx b/src/db/Count.cxx
index ba0d443e7..dccb3e8c7 100644
--- a/src/db/Count.cxx
+++ b/src/db/Count.cxx
@@ -90,10 +90,10 @@ GroupCountVisitor(TagCountMap &map, TagType group, const LightSong &song)
 	assert(song.tag != nullptr);
 
 	const Tag &tag = *song.tag;
-	VisitTagWithFallback(tag, group,
-			     std::bind(CollectGroupCounts, std::ref(map),
-				       std::cref(tag),
-				       std::placeholders::_1));
+	VisitTagWithFallbackOrEmpty(tag, group,
+				    std::bind(CollectGroupCounts, std::ref(map),
+					      std::cref(tag),
+					      std::placeholders::_1));
 }
 
 void
diff --git a/src/tag/VisitFallback.hxx b/src/tag/VisitFallback.hxx
index 3ad486f5f..23992d44b 100644
--- a/src/tag/VisitFallback.hxx
+++ b/src/tag/VisitFallback.hxx
@@ -49,4 +49,12 @@ VisitTagWithFallback(const Tag &tag, TagType type, F &&f) noexcept
 				    });
 }
 
+template<typename F>
+void
+VisitTagWithFallbackOrEmpty(const Tag &tag, TagType type, F &&f) noexcept
+{
+	if (!VisitTagWithFallback(tag, type, f))
+		f("");
+}
+
 #endif

From db27bb76e280c69d70bc14f0b0161e89d496a3c8 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 22 Oct 2018 11:35:22 +0200
Subject: [PATCH 13/13] db: fix broken command "list ... group"

Grouping in the "list" command was completely broken from the start,
unlike "count group".  I have no idea what I have been thinking when I
wrote commit ae178c77bdc47c954fd9a4b32ffc07fe6c4a8a49, but it didn't
make any sense.

This commit is a rewrite of the feature.

For clients to be able to detect this feature, this commit also
increments the protocol version.
---
 Makefile.am                                   |   1 -
 NEWS                                          |   1 +
 configure.ac                                  |   2 +-
 src/command/DatabaseCommands.cxx              |  17 ++-
 src/db/DatabasePrint.cxx                      |  41 +++---
 src/db/DatabasePrint.hxx                      |   4 +-
 src/db/Interface.hxx                          |  14 +--
 src/db/UniqueTags.cxx                         |  48 ++++---
 src/db/UniqueTags.hxx                         |  15 ++-
 src/db/plugins/ProxyDatabasePlugin.cxx        | 103 +++++++--------
 .../plugins/simple/SimpleDatabasePlugin.cxx   |   9 +-
 .../plugins/simple/SimpleDatabasePlugin.hxx   |   6 +-
 src/db/plugins/upnp/UpnpDatabasePlugin.cxx    |  26 ++--
 src/tag/Set.cxx                               | 117 ------------------
 src/tag/Set.hxx                               |  73 -----------
 15 files changed, 145 insertions(+), 332 deletions(-)
 delete mode 100644 src/tag/Set.cxx
 delete mode 100644 src/tag/Set.hxx

diff --git a/Makefile.am b/Makefile.am
index 1061d838d..50b98da76 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -985,7 +985,6 @@ libtag_a_SOURCES =\
 	src/tag/TagString.cxx src/tag/TagString.hxx \
 	src/tag/TagPool.cxx src/tag/TagPool.hxx \
 	src/tag/TagTable.cxx src/tag/TagTable.hxx \
-	src/tag/Set.cxx src/tag/Set.hxx \
 	src/tag/Format.cxx src/tag/Format.hxx \
 	src/tag/VorbisComment.cxx src/tag/VorbisComment.hxx \
 	src/tag/ReplayGain.cxx src/tag/ReplayGain.hxx \
diff --git a/NEWS b/NEWS
index 5b8a3b48c..8b0029784 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,7 @@ ver 0.20.22 (not yet released)
 * protocol
   - add tag fallbacks for AlbumArtistSort, ArtistSort
   - "count group ..." can print an empty group
+  - fix broken command "list ... group"
 * storage
   - curl: URL-encode paths
 * Android
diff --git a/configure.ac b/configure.ac
index 4e9f8b460..687823dc4 100644
--- a/configure.ac
+++ b/configure.ac
@@ -14,7 +14,7 @@ AM_SILENT_RULES
 AC_CONFIG_HEADERS(config.h)
 AC_CONFIG_MACRO_DIR([m4])
 
-AC_DEFINE(PROTOCOL_VERSION, "0.20.0", [The MPD protocol version])
+AC_DEFINE(PROTOCOL_VERSION, "0.20.22", [The MPD protocol version])
 
 GIT_COMMIT=`cd "$srcdir" && git describe --dirty --always 2>/dev/null`
 if test x$GIT_COMMIT != x; then
diff --git a/src/command/DatabaseCommands.cxx b/src/command/DatabaseCommands.cxx
index 42f79b8e2..3f7d18324 100644
--- a/src/command/DatabaseCommands.cxx
+++ b/src/command/DatabaseCommands.cxx
@@ -191,7 +191,7 @@ handle_list(Client &client, Request args, Response &r)
 	}
 
 	std::unique_ptr<SongFilter> filter;
-	tag_mask_t group_mask = 0;
+	TagType group = TAG_NUM_OF_ITEM_TYPES;
 
 	if (args.size == 1) {
 		/* for compatibility with < 0.12.0 */
@@ -206,18 +206,16 @@ handle_list(Client &client, Request args, Response &r)
 					    args.shift()));
 	}
 
-	while (args.size >= 2 &&
-	       StringIsEqual(args[args.size - 2], "group")) {
+	if  (args.size >= 2 &&
+	     StringIsEqual(args[args.size - 2], "group")) {
 		const char *s = args[args.size - 1];
-		TagType gt = tag_name_parse_i(s);
-		if (gt == TAG_NUM_OF_ITEM_TYPES) {
+		group = tag_name_parse_i(s);
+		if (group == TAG_NUM_OF_ITEM_TYPES) {
 			r.FormatError(ACK_ERROR_ARG,
 				      "Unknown tag type: %s", s);
 			return CommandResult::ERROR;
 		}
 
-		group_mask |= tag_mask_t(1) << unsigned(gt);
-
 		args.pop_back();
 		args.pop_back();
 	}
@@ -230,14 +228,13 @@ handle_list(Client &client, Request args, Response &r)
 		}
 	}
 
-	if (tagType < TAG_NUM_OF_ITEM_TYPES &&
-	    group_mask & (tag_mask_t(1) << tagType)) {
+	if (tagType < TAG_NUM_OF_ITEM_TYPES && tagType == group) {
 		r.Error(ACK_ERROR_ARG, "Conflicting group");
 		return CommandResult::ERROR;
 	}
 
 	PrintUniqueTags(r, client.partition,
-			tagType, group_mask, filter.get());
+			tagType, group, filter.get());
 	return CommandResult::OK;
 }
 
diff --git a/src/db/DatabasePrint.cxx b/src/db/DatabasePrint.cxx
index 87d441d20..d4d386912 100644
--- a/src/db/DatabasePrint.cxx
+++ b/src/db/DatabasePrint.cxx
@@ -187,22 +187,34 @@ PrintSongURIVisitor(Response &r, Partition &partition, const LightSong &song)
 }
 
 static void
-PrintUniqueTag(Response &r, TagType tag_type,
-	       const Tag &tag)
+PrintUniqueTags(Response &r, TagType tag_type,
+		const std::set<std::string> &values)
 {
-	const char *value = tag.GetValue(tag_type);
-	assert(value != nullptr);
-	r.Format("%s: %s\n", tag_item_names[tag_type], value);
+	const char *const name = tag_item_names[tag_type];
+	for (const auto &i : values)
+		r.Format("%s: %s\n", name, i.c_str());
+}
 
-	for (const auto &item : tag)
-		if (item.type != tag_type)
-			r.Format("%s: %s\n",
-				 tag_item_names[item.type], item.value);
+static void
+PrintGroupedUniqueTags(Response &r, TagType tag_type, TagType group,
+		       const std::map<std::string, std::set<std::string>> &groups)
+{
+	if (group == TAG_NUM_OF_ITEM_TYPES) {
+		for (const auto &i : groups)
+			PrintUniqueTags(r, tag_type, i.second);
+		return;
+	}
+
+	const char *const group_name = tag_item_names[group];
+	for (const auto &i : groups) {
+		r.Format("%s: %s\n", group_name, i.first.c_str());
+		PrintUniqueTags(r, tag_type, i.second);
+	}
 }
 
 void
 PrintUniqueTags(Response &r, Partition &partition,
-		unsigned type, tag_mask_t group_mask,
+		unsigned type, TagType group,
 		const SongFilter *filter)
 {
 	const Database &db = partition.GetDatabaseOrThrow();
@@ -217,10 +229,9 @@ PrintUniqueTags(Response &r, Partition &partition,
 	} else {
 		assert(type < TAG_NUM_OF_ITEM_TYPES);
 
-		using namespace std::placeholders;
-		const auto f = std::bind(PrintUniqueTag, std::ref(r),
-					 (TagType)type, _1);
-		db.VisitUniqueTags(selection, (TagType)type,
-				   group_mask, f);
+		PrintGroupedUniqueTags(r, TagType(type), group,
+				       db.CollectUniqueTags(selection,
+							    TagType(type),
+							    group));
 	}
 }
diff --git a/src/db/DatabasePrint.hxx b/src/db/DatabasePrint.hxx
index 51b9a92cd..bbb52b101 100644
--- a/src/db/DatabasePrint.hxx
+++ b/src/db/DatabasePrint.hxx
@@ -20,7 +20,7 @@
 #ifndef MPD_DB_PRINT_H
 #define MPD_DB_PRINT_H
 
-#include "tag/Mask.hxx"
+#include "tag/TagType.h"
 
 class SongFilter;
 struct DatabaseSelection;
@@ -44,7 +44,7 @@ db_selection_print(Response &r, Partition &partition,
 
 void
 PrintUniqueTags(Response &r, Partition &partition,
-		unsigned type, tag_mask_t group_mask,
+		unsigned type, TagType group,
 		const SongFilter *filter);
 
 #endif
diff --git a/src/db/Interface.hxx b/src/db/Interface.hxx
index a61c323d3..656dfaa76 100644
--- a/src/db/Interface.hxx
+++ b/src/db/Interface.hxx
@@ -22,9 +22,12 @@
 
 #include "Visitor.hxx"
 #include "tag/TagType.h"
-#include "tag/Mask.hxx"
 #include "Compiler.h"
 
+#include <map>
+#include <set>
+#include <string>
+
 #include <time.h>
 
 struct DatabasePlugin;
@@ -99,12 +102,9 @@ public:
 		return Visit(selection, VisitDirectory(), visit_song);
 	}
 
-	/**
-	 * Visit all unique tag values.
-	 */
-	virtual void VisitUniqueTags(const DatabaseSelection &selection,
-				     TagType tag_type, tag_mask_t group_mask,
-				     VisitTag visit_tag) const = 0;
+	virtual std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
+									       TagType tag_type,
+									       TagType group=TAG_NUM_OF_ITEM_TYPES) const = 0;
 
 	virtual DatabaseStats GetStats(const DatabaseSelection &selection) const = 0;
 
diff --git a/src/db/UniqueTags.cxx b/src/db/UniqueTags.cxx
index c888fe7b4..4e33f5df6 100644
--- a/src/db/UniqueTags.cxx
+++ b/src/db/UniqueTags.cxx
@@ -20,34 +20,42 @@
 #include "UniqueTags.hxx"
 #include "Interface.hxx"
 #include "LightSong.hxx"
-#include "tag/Set.hxx"
+#include "tag/VisitFallback.hxx"
 
 #include <functional>
 
 #include <assert.h>
 
 static void
-CollectTags(TagSet &set, TagType tag_type, tag_mask_t group_mask,
-	    const LightSong &song)
+CollectTags(std::set<std::string> &result,
+	    const Tag &tag,
+	    TagType tag_type) noexcept
 {
-	assert(song.tag != nullptr);
-	const Tag &tag = *song.tag;
-
-	set.InsertUnique(tag, tag_type, group_mask);
+	VisitTagWithFallbackOrEmpty(tag, tag_type, [&result](const char *value){
+			result.emplace(value);
+		});
 }
 
-void
-VisitUniqueTags(const Database &db, const DatabaseSelection &selection,
-		TagType tag_type, tag_mask_t group_mask,
-		VisitTag visit_tag)
+static void
+CollectGroupTags(std::map<std::string, std::set<std::string>> &result,
+		 const Tag &tag,
+		 TagType tag_type,
+		 TagType group) noexcept
 {
-	TagSet set;
-
-	using namespace std::placeholders;
-	const auto f = std::bind(CollectTags, std::ref(set),
-				 tag_type, group_mask, _1);
-	db.Visit(selection, f);
-
-	for (const auto &value : set)
-		visit_tag(value);
+	VisitTagWithFallbackOrEmpty(tag, group, [&](const char *group_name){
+			CollectTags(result[group_name], tag, tag_type);
+		});
+}
+
+std::map<std::string, std::set<std::string>>
+CollectUniqueTags(const Database &db, const DatabaseSelection &selection,
+		  TagType tag_type, TagType group)
+{
+	std::map<std::string, std::set<std::string>> result;
+
+	db.Visit(selection, [&result, tag_type, group](const LightSong &song){
+			CollectGroupTags(result, *song.tag, tag_type, group);
+		});
+
+	return result;
 }
diff --git a/src/db/UniqueTags.hxx b/src/db/UniqueTags.hxx
index efb2a089d..83e6e7f09 100644
--- a/src/db/UniqueTags.hxx
+++ b/src/db/UniqueTags.hxx
@@ -20,16 +20,19 @@
 #ifndef MPD_DB_UNIQUE_TAGS_HXX
 #define MPD_DB_UNIQUE_TAGS_HXX
 
-#include "Visitor.hxx"
 #include "tag/TagType.h"
-#include "tag/Mask.hxx"
+#include "Compiler.h"
+
+#include <map>
+#include <set>
+#include <string>
 
 class Database;
 struct DatabaseSelection;
 
-void
-VisitUniqueTags(const Database &db, const DatabaseSelection &selection,
-		TagType tag_type, tag_mask_t group_mask,
-		VisitTag visit_tag);
+gcc_pure
+std::map<std::string, std::set<std::string>>
+CollectUniqueTags(const Database &db, const DatabaseSelection &selection,
+		  TagType tag_type, TagType group);
 
 #endif
diff --git a/src/db/plugins/ProxyDatabasePlugin.cxx b/src/db/plugins/ProxyDatabasePlugin.cxx
index b3d2d0a05..d6954968f 100644
--- a/src/db/plugins/ProxyDatabasePlugin.cxx
+++ b/src/db/plugins/ProxyDatabasePlugin.cxx
@@ -120,9 +120,9 @@ public:
 		   VisitSong visit_song,
 		   VisitPlaylist visit_playlist) const override;
 
-	void VisitUniqueTags(const DatabaseSelection &selection,
-			     TagType tag_type, tag_mask_t group_mask,
-			     VisitTag visit_tag) const override;
+	std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
+								       TagType tag_type,
+								       TagType group) const override;
 
 	DatabaseStats GetStats(const DatabaseSelection &selection) const override;
 
@@ -334,28 +334,19 @@ SendConstraints(mpd_connection *connection, const DatabaseSelection &selection)
 }
 
 static bool
-SendGroupMask(mpd_connection *connection, tag_mask_t mask)
+SendGroup(mpd_connection *connection, TagType group)
 {
+	if (group == TAG_NUM_OF_ITEM_TYPES)
+		return true;
+
 #if LIBMPDCLIENT_CHECK_VERSION(2,12,0)
-	for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i) {
-		if ((mask & (tag_mask_t(1) << i)) == 0)
-			continue;
+	const auto tag = Convert(group);
+	if (tag == MPD_TAG_COUNT)
+		throw std::runtime_error("Unsupported tag");
 
-		const auto tag = Convert(TagType(i));
-		if (tag == MPD_TAG_COUNT)
-			throw std::runtime_error("Unsupported tag");
-
-		if (!mpd_search_add_group_tag(connection, tag))
-			return false;
-	}
-
-	return true;
+	return mpd_search_add_group_tag(connection, tag);
 #else
 	(void)connection;
-	(void)mask;
-
-	if (mask != 0)
-		throw std::runtime_error("Grouping requires libmpdclient 2.12");
 
 	return true;
 #endif
@@ -799,11 +790,9 @@ ProxyDatabase::Visit(const DatabaseSelection &selection,
 		visit_directory, visit_song, visit_playlist);
 }
 
-void
-ProxyDatabase::VisitUniqueTags(const DatabaseSelection &selection,
-			       TagType tag_type,
-			       tag_mask_t group_mask,
-			       VisitTag visit_tag) const
+std::map<std::string, std::set<std::string>>
+ProxyDatabase::CollectUniqueTags(const DatabaseSelection &selection,
+				 TagType tag_type, TagType group) const
 try {
 	// TODO: eliminate the const_cast
 	const_cast<ProxyDatabase *>(this)->EnsureConnected();
@@ -814,54 +803,56 @@ try {
 
 	if (!mpd_search_db_tags(connection, tag_type2) ||
 	    !SendConstraints(connection, selection) ||
-	    !SendGroupMask(connection, group_mask))
+	    !SendGroup(connection, group))
 		ThrowError(connection);
 
 	if (!mpd_search_commit(connection))
 		ThrowError(connection);
 
-	TagBuilder builder;
+	std::map<std::string, std::set<std::string>> result;
 
-	while (auto *pair = mpd_recv_pair(connection)) {
-		AtScopeExit(this, pair) {
-			mpd_return_pair(connection, pair);
-		};
+	if (group == TAG_NUM_OF_ITEM_TYPES) {
+		auto &values = result[std::string()];
 
-		const auto current_type = tag_name_parse_i(pair->name);
-		if (current_type == TAG_NUM_OF_ITEM_TYPES)
-			continue;
+		while (auto *pair = mpd_recv_pair(connection)) {
+			AtScopeExit(this, pair) {
+				mpd_return_pair(connection, pair);
+			};
 
-		if (current_type == tag_type && !builder.IsEmpty()) {
-			try {
-				visit_tag(builder.Commit());
-			} catch (...) {
-				mpd_response_finish(connection);
-				throw;
-			}
+			const auto current_type = tag_name_parse_i(pair->name);
+			if (current_type == TAG_NUM_OF_ITEM_TYPES)
+				continue;
+
+			if (current_type == tag_type)
+				values.emplace(pair->value);
 		}
+	} else {
+		std::set<std::string> *current_group = nullptr;
 
-		builder.AddItem(current_type, pair->value);
+		while (auto *pair = mpd_recv_pair(connection)) {
+			AtScopeExit(this, pair) {
+				mpd_return_pair(connection, pair);
+			};
 
-		if (!builder.HasType(current_type))
-			/* if no tag item has been added, then the
-			   given value was not acceptable
-			   (e.g. empty); forcefully insert an empty
-			   tag in this case, as the caller expects the
-			   given tag type to be present */
-			builder.AddEmptyItem(current_type);
-	}
+			const auto current_type = tag_name_parse_i(pair->name);
+			if (current_type == TAG_NUM_OF_ITEM_TYPES)
+				continue;
 
-	if (!builder.IsEmpty()) {
-		try {
-			visit_tag(builder.Commit());
-		} catch (...) {
-			mpd_response_finish(connection);
-			throw;
+			if (current_type == tag_type) {
+				if (current_group == nullptr)
+					current_group = &result[std::string()];
+
+				current_group->emplace(pair->value);
+			} else if (current_type == group) {
+				current_group = &result[pair->value];
+			}
 		}
 	}
 
 	if (!mpd_response_finish(connection))
 		ThrowError(connection);
+
+	return result;
 } catch (...) {
 	if (connection != nullptr)
 		mpd_search_cancel(connection);
diff --git a/src/db/plugins/simple/SimpleDatabasePlugin.cxx b/src/db/plugins/simple/SimpleDatabasePlugin.cxx
index a27c070c6..1e5bf8f7e 100644
--- a/src/db/plugins/simple/SimpleDatabasePlugin.cxx
+++ b/src/db/plugins/simple/SimpleDatabasePlugin.cxx
@@ -312,12 +312,11 @@ SimpleDatabase::Visit(const DatabaseSelection &selection,
 			    "No such directory");
 }
 
-void
-SimpleDatabase::VisitUniqueTags(const DatabaseSelection &selection,
-				TagType tag_type, tag_mask_t group_mask,
-				VisitTag visit_tag) const
+std::map<std::string, std::set<std::string>>
+SimpleDatabase::CollectUniqueTags(const DatabaseSelection &selection,
+				  TagType tag_type, TagType group) const
 {
-	::VisitUniqueTags(*this, selection, tag_type, group_mask, visit_tag);
+	return ::CollectUniqueTags(*this, selection, tag_type, group);
 }
 
 DatabaseStats
diff --git a/src/db/plugins/simple/SimpleDatabasePlugin.hxx b/src/db/plugins/simple/SimpleDatabasePlugin.hxx
index 4336fb58b..4c58c73d3 100644
--- a/src/db/plugins/simple/SimpleDatabasePlugin.hxx
+++ b/src/db/plugins/simple/SimpleDatabasePlugin.hxx
@@ -119,9 +119,9 @@ public:
 		   VisitSong visit_song,
 		   VisitPlaylist visit_playlist) const override;
 
-	void VisitUniqueTags(const DatabaseSelection &selection,
-			     TagType tag_type, tag_mask_t group_mask,
-			     VisitTag visit_tag) const override;
+	std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
+								       TagType tag_type,
+								       TagType group) const override;
 
 	DatabaseStats GetStats(const DatabaseSelection &selection) const override;
 
diff --git a/src/db/plugins/upnp/UpnpDatabasePlugin.cxx b/src/db/plugins/upnp/UpnpDatabasePlugin.cxx
index c49d8a725..81548f98e 100644
--- a/src/db/plugins/upnp/UpnpDatabasePlugin.cxx
+++ b/src/db/plugins/upnp/UpnpDatabasePlugin.cxx
@@ -87,9 +87,9 @@ public:
 		   VisitSong visit_song,
 		   VisitPlaylist visit_playlist) const override;
 
-	void VisitUniqueTags(const DatabaseSelection &selection,
-			     TagType tag_type, tag_mask_t group_mask,
-			     VisitTag visit_tag) const override;
+	std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
+								       TagType tag_type,
+								       TagType group) const override;
 
 	DatabaseStats GetStats(const DatabaseSelection &selection) const override;
 
@@ -603,17 +603,15 @@ UpnpDatabase::Visit(const DatabaseSelection &selection,
 		    visit_directory, visit_song, visit_playlist);
 }
 
-void
-UpnpDatabase::VisitUniqueTags(const DatabaseSelection &selection,
-			      TagType tag, gcc_unused tag_mask_t group_mask,
-			      VisitTag visit_tag) const
+std::map<std::string, std::set<std::string>>
+UpnpDatabase::CollectUniqueTags(const DatabaseSelection &selection,
+				TagType tag, TagType group) const
 {
-	// TODO: use group_mask
+	(void)group; // TODO: use group
 
-	if (!visit_tag)
-		return;
+	std::map<std::string, std::set<std::string>> result;
+	auto &values = result[std::string()];
 
-	std::set<std::string> values;
 	for (auto& server : discovery->GetDirectories()) {
 		const auto dirbuf = SearchSongs(server, rootid, selection);
 
@@ -633,11 +631,7 @@ UpnpDatabase::VisitUniqueTags(const DatabaseSelection &selection,
 		}
 	}
 
-	for (const auto& value : values) {
-		TagBuilder builder;
-		builder.AddItem(tag, value.c_str());
-		visit_tag(builder.Commit());
-	}
+	return result;
 }
 
 DatabaseStats
diff --git a/src/tag/Set.cxx b/src/tag/Set.cxx
deleted file mode 100644
index d52d517aa..000000000
--- a/src/tag/Set.cxx
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright 2003-2017 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 "Set.hxx"
-#include "Fallback.hxx"
-#include "TagBuilder.hxx"
-#include "util/StringView.hxx"
-
-#include <functional>
-
-#include <assert.h>
-
-/**
- * Copy all tag items of the specified type.
- */
-static bool
-CopyTagItem2(TagBuilder &dest, TagType dest_type,
-	     const Tag &src, TagType src_type)
-{
-	bool found = false;
-
-	for (const auto &item : src) {
-		if (item.type == src_type) {
-			dest.AddItem(dest_type, item.value);
-			found = true;
-		}
-	}
-
-	return found;
-}
-
-/**
- * Copy all tag items of the specified type.  Fall back to "Artist" if
- * there is no "AlbumArtist".
- */
-static void
-CopyTagItem(TagBuilder &dest, const Tag &src, TagType type)
-{
-	ApplyTagWithFallback(type,
-			     std::bind(CopyTagItem2, std::ref(dest), type,
-				       std::cref(src), std::placeholders::_1));
-}
-
-/**
- * Copy all tag items of the types in the mask.
- */
-static void
-CopyTagMask(TagBuilder &dest, const Tag &src, tag_mask_t mask)
-{
-	for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i)
-		if ((mask & (tag_mask_t(1) << i)) != 0)
-			CopyTagItem(dest, src, TagType(i));
-}
-
-void
-TagSet::InsertUnique(const Tag &src, TagType type, const char *value,
-		     tag_mask_t group_mask) noexcept
-{
-	TagBuilder builder;
-	builder.AddItemUnchecked(type, value);
-	CopyTagMask(builder, src, group_mask);
-#if CLANG_OR_GCC_VERSION(4,8)
-	emplace(builder.Commit());
-#else
-	insert(builder.Commit());
-#endif
-}
-
-bool
-TagSet::CheckUnique(TagType dest_type,
-		    const Tag &tag, TagType src_type,
-		    tag_mask_t group_mask) noexcept
-{
-	bool found = false;
-
-	for (const auto &item : tag) {
-		if (item.type == src_type) {
-			InsertUnique(tag, dest_type, item.value, group_mask);
-			found = true;
-		}
-	}
-
-	return found;
-}
-
-void
-TagSet::InsertUnique(const Tag &tag,
-		     TagType type, tag_mask_t group_mask) noexcept
-{
-	static_assert(sizeof(group_mask) * 8 >= TAG_NUM_OF_ITEM_TYPES,
-		      "Mask is too small");
-
-	assert((group_mask & (tag_mask_t(1) << unsigned(type))) == 0);
-
-	if (!ApplyTagWithFallback(type,
-				  std::bind(&TagSet::CheckUnique, this,
-					    type, std::cref(tag),
-					    std::placeholders::_1,
-					    group_mask)))
-		InsertUnique(tag, type, "", group_mask);
-}
diff --git a/src/tag/Set.hxx b/src/tag/Set.hxx
deleted file mode 100644
index 8e19b7ca2..000000000
--- a/src/tag/Set.hxx
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright 2003-2017 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 MPD_TAG_SET_HXX
-#define MPD_TAG_SET_HXX
-
-#include "Compiler.h"
-#include "Tag.hxx"
-#include "Mask.hxx"
-
-#include <set>
-
-#include <string.h>
-
-/**
- * Helper class for #TagSet which compares two #Tag objects.
- */
-struct TagLess {
-	gcc_pure
-	bool operator()(const Tag &a, const Tag &b) const noexcept {
-		if (a.num_items != b.num_items)
-			return a.num_items < b.num_items;
-
-		const unsigned n = a.num_items;
-		for (unsigned i = 0; i < n; ++i) {
-			const TagItem &ai = *a.items[i];
-			const TagItem &bi = *b.items[i];
-			if (ai.type != bi.type)
-				return unsigned(ai.type) < unsigned(bi.type);
-
-			const int cmp = strcmp(ai.value, bi.value);
-			if (cmp != 0)
-				return cmp < 0;
-		}
-
-		return false;
-	}
-};
-
-/**
- * A set of #Tag objects.
- */
-class TagSet : public std::set<Tag, TagLess> {
-public:
-	void InsertUnique(const Tag &tag,
-			  TagType type, tag_mask_t group_mask) noexcept;
-
-private:
-	void InsertUnique(const Tag &src, TagType type, const char *value,
-			  tag_mask_t group_mask) noexcept;
-
-	bool CheckUnique(TagType dest_type,
-			 const Tag &tag, TagType src_type,
-			 tag_mask_t group_mask) noexcept;
-};
-
-#endif