From e2da13b0d3f5029741cb1a02013dadc63f84861b Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 12 Aug 2019 14:01:14 +0200
Subject: [PATCH] command/file: add command "readpicture"

Closes https://github.com/MusicPlayerDaemon/MPD/issues/42
---
 NEWS                         |  1 +
 doc/protocol.rst             | 24 +++++++++++++++
 src/command/AllCommands.cxx  |  1 +
 src/command/FileCommands.cxx | 60 ++++++++++++++++++++++++++++++++++++
 src/command/FileCommands.hxx |  3 ++
 5 files changed, 89 insertions(+)

diff --git a/NEWS b/NEWS
index 060e98fb6..baea3bf76 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,7 @@ ver 0.22 (not yet released)
 * protocol
   - "findadd"/"searchadd"/"searchaddpl" support the "sort" and
     "window" parameters
+  - add command "readpicture" to download embedded pictures
 * tags
   - new tags "Grouping" (for ID3 "TIT1") and "Work"
 * input
diff --git a/doc/protocol.rst b/doc/protocol.rst
index 9c580259c..72520d67e 100644
--- a/doc/protocol.rst
+++ b/doc/protocol.rst
@@ -1005,6 +1005,30 @@ The music database
     decoder plugins support it.  For example, on Ogg files,
     this lists the Vorbis comments.
 
+:command:`readpicture {URI} {OFFSET}`
+    Locate a picture for the given song and return a chunk of the
+    image file at offset ``OFFSET``.  This is usually implemented by
+    reading embedded pictures from binary tags (e.g. ID3v2's ``APIC``
+    tag).
+
+    Returns the following values:
+
+    - ``size``: the total file size
+    - ``type``: the file's MIME type (optional)
+    - ``binary``: see :ref:`binary`
+
+    If the song file was recognized, but there is no picture, the
+    response is successful, but is otherwise empty.
+
+    Example::
+
+     readpicture foo/bar.ogg 0
+     size: 1024768
+     type: image/jpeg
+     binary: 8192
+     <8192 bytes>
+     OK
+
 .. _command_search:
 
 :command:`search {FILTER} [sort {TYPE}] [window {START:END}]`
diff --git a/src/command/AllCommands.cxx b/src/command/AllCommands.cxx
index dde17d85c..005d6245e 100644
--- a/src/command/AllCommands.cxx
+++ b/src/command/AllCommands.cxx
@@ -168,6 +168,7 @@ static constexpr struct command commands[] = {
 	{ "rangeid", PERMISSION_ADD, 2, 2, handle_rangeid },
 	{ "readcomments", PERMISSION_READ, 1, 1, handle_read_comments },
 	{ "readmessages", PERMISSION_READ, 0, 0, handle_read_messages },
+	{ "readpicture", PERMISSION_READ, 2, 2, handle_read_picture },
 	{ "rename", PERMISSION_CONTROL, 2, 2, handle_rename },
 	{ "repeat", PERMISSION_CONTROL, 1, 1, handle_repeat },
 	{ "replay_gain_mode", PERMISSION_CONTROL, 1, 1,
diff --git a/src/command/FileCommands.cxx b/src/command/FileCommands.cxx
index 578bf3385..301b309fd 100644
--- a/src/command/FileCommands.cxx
+++ b/src/command/FileCommands.cxx
@@ -26,6 +26,7 @@
 #include "client/Client.hxx"
 #include "client/Response.hxx"
 #include "util/CharUtil.hxx"
+#include "util/OffsetPointer.hxx"
 #include "util/StringView.hxx"
 #include "util/UriExtract.hxx"
 #include "tag/Handler.hxx"
@@ -270,3 +271,62 @@ handle_album_art(Client &client, Request args, Response &r)
 	return CommandResult::ERROR;
 }
 
+class PrintPictureHandler final : public NullTagHandler {
+	Response &response;
+
+	const size_t offset;
+
+	bool found = false;
+
+	bool bad_offset = false;
+
+public:
+	PrintPictureHandler(Response &_response, size_t _offset) noexcept
+		:NullTagHandler(WANT_PICTURE), response(_response),
+		 offset(_offset) {}
+
+	void RethrowError() const {
+		if (bad_offset)
+			throw ProtocolError(ACK_ERROR_ARG, "Bad file offset");
+	}
+
+	void OnPicture(const char *mime_type,
+		       ConstBuffer<void> buffer) noexcept override {
+		if (found)
+			/* only use the first picture */
+			return;
+
+		found = true;
+
+		if (offset > buffer.size) {
+			bad_offset = true;
+			return;
+		}
+
+		response.Format("size: %" PRIoffset "\n", buffer.size);
+
+		if (mime_type != nullptr)
+			response.Format("type: %s\n", mime_type);
+
+		buffer.size -= offset;
+		if (buffer.size > Response::MAX_BINARY_SIZE)
+			buffer.size = Response::MAX_BINARY_SIZE;
+		buffer.data = OffsetPointer(buffer.data, offset);
+
+		response.WriteBinary(buffer);
+	}
+};
+
+CommandResult
+handle_read_picture(Client &client, Request args, Response &r)
+{
+	assert(args.size == 2);
+
+	const char *const uri = args.front();
+	const size_t offset = args.ParseUnsigned(1);
+
+	PrintPictureHandler handler(r, offset);
+	TagScanAny(client, uri, handler);
+	handler.RethrowError();
+	return CommandResult::OK;
+}
diff --git a/src/command/FileCommands.hxx b/src/command/FileCommands.hxx
index 25d7c2c77..2483452b1 100644
--- a/src/command/FileCommands.hxx
+++ b/src/command/FileCommands.hxx
@@ -36,4 +36,7 @@ handle_read_comments(Client &client, Request request, Response &response);
 CommandResult
 handle_album_art(Client &client, Request request, Response &response);
 
+CommandResult
+handle_read_picture(Client &client, Request request, Response &response);
+
 #endif