diff --git a/Makefile.am b/Makefile.am
index cb6a4cc71..3beb8bd71 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1392,6 +1392,7 @@ libinput_a_SOURCES += \
 	src/input/plugins/QobuzErrorParser.cxx src/input/plugins/QobuzErrorParser.hxx \
 	src/input/plugins/QobuzLoginRequest.cxx src/input/plugins/QobuzLoginRequest.hxx \
 	src/input/plugins/QobuzTrackRequest.cxx src/input/plugins/QobuzTrackRequest.hxx \
+	src/input/plugins/QobuzTagScanner.cxx src/input/plugins/QobuzTagScanner.hxx \
 	src/input/plugins/QobuzInputPlugin.cxx src/input/plugins/QobuzInputPlugin.hxx
 INPUT_LIBS += $(YAJL_LIBS) $(LIBGCRYPT_LIBS)
 endif
diff --git a/src/input/plugins/QobuzClient.cxx b/src/input/plugins/QobuzClient.cxx
index 693afaf11..419f24682 100644
--- a/src/input/plugins/QobuzClient.cxx
+++ b/src/input/plugins/QobuzClient.cxx
@@ -164,6 +164,25 @@ QobuzClient::InvokeHandlers() noexcept
 	}
 }
 
+std::string
+QobuzClient::MakeUrl(const char *object, const char *method,
+		     const std::multimap<std::string, std::string> &query) const noexcept
+{
+	assert(!query.empty());
+
+	std::string uri(base_url);
+	uri += object;
+	uri.push_back('/');
+	uri += method;
+
+	QueryStringBuilder q;
+	for (const auto &i : query)
+		q(uri, i.first.c_str(), i.second.c_str());
+
+	q(uri, "app_id", app_id);
+	return uri;
+}
+
 std::string
 QobuzClient::MakeSignedUrl(const char *object, const char *method,
 			   const std::multimap<std::string, std::string> &query) const noexcept
diff --git a/src/input/plugins/QobuzClient.hxx b/src/input/plugins/QobuzClient.hxx
index 5999adb00..4edcc2c5d 100644
--- a/src/input/plugins/QobuzClient.hxx
+++ b/src/input/plugins/QobuzClient.hxx
@@ -96,6 +96,9 @@ public:
 	 */
 	QobuzSession GetSession() const;
 
+	std::string MakeUrl(const char *object, const char *method,
+			    const std::multimap<std::string, std::string> &query) const noexcept;
+
 	std::string MakeSignedUrl(const char *object, const char *method,
 				  const std::multimap<std::string, std::string> &query) const noexcept;
 
diff --git a/src/input/plugins/QobuzInputPlugin.cxx b/src/input/plugins/QobuzInputPlugin.cxx
index 0b7ed2c03..e5c4f6d64 100644
--- a/src/input/plugins/QobuzInputPlugin.cxx
+++ b/src/input/plugins/QobuzInputPlugin.cxx
@@ -21,6 +21,7 @@
 #include "QobuzInputPlugin.hxx"
 #include "QobuzClient.hxx"
 #include "QobuzTrackRequest.hxx"
+#include "QobuzTagScanner.hxx"
 #include "CurlInputPlugin.hxx"
 #include "PluginUnavailable.hxx"
 #include "input/ProxyInputStream.hxx"
@@ -192,9 +193,23 @@ OpenQobuzInput(const char *uri, Mutex &mutex, Cond &cond)
 	return std::make_unique<QobuzInputStream>(uri, track_id, mutex, cond);
 }
 
+static std::unique_ptr<RemoteTagScanner>
+ScanQobuzTags(const char *uri, RemoteTagHandler &handler)
+{
+	assert(qobuz_client != nullptr);
+
+	const char *track_id = ExtractQobuzTrackId(uri);
+	if (track_id == nullptr)
+		return nullptr;
+
+	return std::make_unique<QobuzTagScanner>(*qobuz_client, track_id,
+						 handler);
+}
+
 const InputPlugin qobuz_input_plugin = {
 	"qobuz",
 	InitQobuzInput,
 	FinishQobuzInput,
 	OpenQobuzInput,
+	ScanQobuzTags,
 };
diff --git a/src/input/plugins/QobuzTagScanner.cxx b/src/input/plugins/QobuzTagScanner.cxx
new file mode 100644
index 000000000..ce01829b4
--- /dev/null
+++ b/src/input/plugins/QobuzTagScanner.cxx
@@ -0,0 +1,292 @@
+/*
+ * 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.
+ */
+
+#include "config.h"
+#include "QobuzTagScanner.hxx"
+#include "QobuzErrorParser.hxx"
+#include "QobuzClient.hxx"
+#include "lib/yajl/Callbacks.hxx"
+#include "tag/Builder.hxx"
+#include "tag/Tag.hxx"
+
+using Wrapper = Yajl::CallbacksWrapper<QobuzTagScanner::ResponseParser>;
+static constexpr yajl_callbacks parse_callbacks = {
+	nullptr,
+	nullptr,
+	Wrapper::Integer,
+	nullptr,
+	nullptr,
+	Wrapper::String,
+	Wrapper::StartMap,
+	Wrapper::MapKey,
+	Wrapper::EndMap,
+	nullptr,
+	nullptr,
+};
+
+class QobuzTagScanner::ResponseParser final : public YajlResponseParser {
+	enum class State {
+		NONE,
+		COMPOSER,
+		COMPOSER_NAME,
+		DURATION,
+		TITLE,
+		ALBUM,
+		ALBUM_TITLE,
+		ALBUM_ARTIST,
+		ALBUM_ARTIST_NAME,
+		PERFORMER,
+		PERFORMER_NAME,
+	} 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(QobuzClient &client, const char *track_id)
+{
+	return client.MakeUrl("track", "get",
+			      {
+				      {"track_id", track_id},
+			      });
+}
+
+QobuzTagScanner::QobuzTagScanner(QobuzClient &client,
+				 const char *track_id,
+				 RemoteTagHandler &_handler)
+	:request(client.GetCurl(),
+		 MakeTrackUrl(client, track_id).c_str(),
+		 *this),
+	 handler(_handler)
+{
+}
+
+QobuzTagScanner::~QobuzTagScanner() noexcept
+{
+	request.StopIndirect();
+}
+
+std::unique_ptr<CurlResponseParser>
+QobuzTagScanner::MakeParser(unsigned status,
+			    std::multimap<std::string, std::string> &&headers)
+{
+	if (status != 200)
+		return std::make_unique<QobuzErrorParser>(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 Qobuz");
+
+	return std::make_unique<ResponseParser>();
+}
+
+void
+QobuzTagScanner::FinishParser(std::unique_ptr<CurlResponseParser> p)
+{
+	assert(dynamic_cast<ResponseParser *>(p.get()) != nullptr);
+	auto &rp = (ResponseParser &)*p;
+	handler.OnRemoteTag(rp.GetTag());
+}
+
+void
+QobuzTagScanner::OnError(std::exception_ptr e) noexcept
+{
+	handler.OnRemoteTagError(e);
+}
+
+inline bool
+QobuzTagScanner::ResponseParser::Integer(long long value) noexcept
+{
+	switch (state) {
+	case State::DURATION:
+		if (value > 0)
+			tag.SetDuration(SignedSongTime::FromS((unsigned)value));
+		break;
+
+	default:
+		break;
+	}
+
+	return true;
+}
+
+inline bool
+QobuzTagScanner::ResponseParser::String(StringView value) noexcept
+{
+	switch (state) {
+	case State::TITLE:
+		if (map_depth == 1)
+			tag.AddItem(TAG_TITLE, value);
+		break;
+
+	case State::COMPOSER_NAME:
+		if (map_depth == 2)
+			tag.AddItem(TAG_COMPOSER, value);
+		break;
+
+	case State::ALBUM_TITLE:
+		if (map_depth == 2)
+			tag.AddItem(TAG_ALBUM, value);
+		break;
+
+	case State::ALBUM_ARTIST_NAME:
+		if (map_depth == 3)
+			tag.AddItem(TAG_ALBUM_ARTIST, value);
+		break;
+
+	case State::PERFORMER_NAME:
+		if (map_depth == 2)
+			tag.AddItem(TAG_PERFORMER, value);
+		break;
+
+	default:
+		break;
+	}
+
+	return true;
+}
+
+inline bool
+QobuzTagScanner::ResponseParser::StartMap() noexcept
+{
+	++map_depth;
+	return true;
+}
+
+inline bool
+QobuzTagScanner::ResponseParser::MapKey(StringView value) noexcept
+{
+	switch (map_depth) {
+	case 1:
+		if (value.Equals("composer"))
+			state = State::COMPOSER;
+		else if (value.Equals("duration"))
+			state = State::DURATION;
+		else if (value.Equals("title"))
+			state = State::TITLE;
+		else if (value.Equals("album"))
+			state = State::ALBUM;
+		else if (value.Equals("performer"))
+			state = State::PERFORMER;
+		else
+			state = State::NONE;
+		break;
+
+	case 2:
+		switch (state) {
+		case State::NONE:
+		case State::DURATION:
+		case State::TITLE:
+			break;
+
+		case State::COMPOSER:
+		case State::COMPOSER_NAME:
+			if (value.Equals("name"))
+				state = State::COMPOSER_NAME;
+			else
+				state = State::COMPOSER;
+			break;
+
+		case State::ALBUM:
+		case State::ALBUM_TITLE:
+		case State::ALBUM_ARTIST:
+		case State::ALBUM_ARTIST_NAME:
+			if (value.Equals("title"))
+				state = State::ALBUM_TITLE;
+			else if (value.Equals("artist"))
+				state = State::ALBUM_ARTIST;
+			else
+				state = State::ALBUM;
+			break;
+
+		case State::PERFORMER:
+		case State::PERFORMER_NAME:
+			if (value.Equals("name"))
+				state = State::PERFORMER_NAME;
+			else
+				state = State::PERFORMER;
+			break;
+
+		default:
+			break;
+		}
+		break;
+
+	case 3:
+		switch (state) {
+		case State::ALBUM_ARTIST:
+		case State::ALBUM_ARTIST_NAME:
+			if (value.Equals("name"))
+				state = State::ALBUM_ARTIST_NAME;
+			else
+				state = State::ALBUM_ARTIST;
+			break;
+
+		default:
+			break;
+		}
+		break;
+	}
+
+	return true;
+}
+
+inline bool
+QobuzTagScanner::ResponseParser::EndMap() noexcept
+{
+	switch (map_depth) {
+	case 2:
+		state = State::NONE;
+		break;
+
+	case 3:
+		switch (state) {
+		case State::ALBUM_TITLE:
+		case State::ALBUM_ARTIST:
+		case State::ALBUM_ARTIST_NAME:
+			state = State::ALBUM;
+			break;
+
+		default:
+			break;
+		}
+		break;
+	}
+
+	--map_depth;
+
+	return true;
+}
diff --git a/src/input/plugins/QobuzTagScanner.hxx b/src/input/plugins/QobuzTagScanner.hxx
new file mode 100644
index 000000000..eae715da1
--- /dev/null
+++ b/src/input/plugins/QobuzTagScanner.hxx
@@ -0,0 +1,60 @@
+/*
+ * 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 QOBUZ_TAG_SCANNER
+#define QOBUZ_TAG_SCANNER
+
+#include "check.h"
+#include "lib/curl/Delegate.hxx"
+#include "lib/curl/Request.hxx"
+#include "input/RemoteTagScanner.hxx"
+
+class QobuzClient;
+
+class QobuzTagScanner final
+	: public RemoteTagScanner, DelegateCurlResponseHandler
+{
+	CurlRequest request;
+
+	RemoteTagHandler &handler;
+
+public:
+	class ResponseParser;
+
+	QobuzTagScanner(QobuzClient &client,
+			const char *track_id,
+			RemoteTagHandler &_handler);
+
+	~QobuzTagScanner() noexcept override;
+
+	void Start() noexcept 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