From 751995ab95298bbc0afad6958bfccce535edf53c Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@duempel.org>
Date: Sat, 12 Jul 2014 03:00:01 +0200
Subject: [PATCH] QueueCommands: new command "rangeid"

Manipulates the playback range of a queued song.
---
 NEWS                          |  2 +-
 doc/protocol.xml              | 22 ++++++++++++++
 src/command/AllCommands.cxx   |  1 +
 src/command/QueueCommands.cxx | 54 +++++++++++++++++++++++++++++++++
 src/command/QueueCommands.hxx |  3 ++
 src/queue/Playlist.hxx        |  8 +++++
 src/queue/PlaylistEdit.cxx    | 57 +++++++++++++++++++++++++++++++++++
 7 files changed, 146 insertions(+), 1 deletion(-)

diff --git a/NEWS b/NEWS
index 82dcdaa52..ab5139a5b 100644
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,6 @@
 ver 0.19 (not yet released)
 * protocol
-  - new commands "addtagid", "cleartagid", "listfiles"
+  - new commands "addtagid", "cleartagid", "listfiles", "rangeid"
   - "lsinfo" and "readcomments" allowed for remote files
   - "listneighbors" lists file servers on the local network
   - "playlistadd" supports file:///
diff --git a/doc/protocol.xml b/doc/protocol.xml
index 360d2d22a..cec92e180 100644
--- a/doc/protocol.xml
+++ b/doc/protocol.xml
@@ -1213,6 +1213,28 @@ OK
           </listitem>
         </varlistentry>
 
+        <varlistentry id="command_rangeid">
+          <term>
+            <cmdsynopsis>
+              <command>rangeid</command>
+              <arg choice="req"><replaceable>ID</replaceable></arg>
+              <arg choice="req"><replaceable>START:END</replaceable></arg>
+            </cmdsynopsis>
+          </term>
+          <listitem>
+            <para>
+              <footnote id="since_0_19"><simpara>Since MPD
+              0.19</simpara></footnote> Specifies the portion of the
+              song that shall be played.  <varname>START</varname> and
+              <varname>END</varname> are offsets in seconds
+              (fractional seconds allowed); both are optional.
+              Omitting both (i.e. sending just ":") means "remove the
+              range, play everything".  A song that is currently
+              playing cannot be manipulated this way.
+            </para>
+          </listitem>
+        </varlistentry>
+
         <varlistentry id="command_shuffle">
           <term>
             <cmdsynopsis>
diff --git a/src/command/AllCommands.cxx b/src/command/AllCommands.cxx
index 6143dacdf..d29950eeb 100644
--- a/src/command/AllCommands.cxx
+++ b/src/command/AllCommands.cxx
@@ -150,6 +150,7 @@ static const struct command commands[] = {
 	{ "prio", PERMISSION_CONTROL, 2, -1, handle_prio },
 	{ "prioid", PERMISSION_CONTROL, 2, -1, handle_prioid },
 	{ "random", PERMISSION_CONTROL, 1, 1, handle_random },
+	{ "rangeid", PERMISSION_ADD, 2, 2, handle_rangeid },
 	{ "readcomments", PERMISSION_READ, 1, 1, handle_read_comments },
 	{ "readmessages", PERMISSION_READ, 0, 0, handle_read_messages },
 	{ "rename", PERMISSION_CONTROL, 2, 2, handle_rename },
diff --git a/src/command/QueueCommands.cxx b/src/command/QueueCommands.cxx
index 1ff7a732b..c99a6687a 100644
--- a/src/command/QueueCommands.cxx
+++ b/src/command/QueueCommands.cxx
@@ -34,6 +34,7 @@
 #include "ls.hxx"
 #include "util/ConstBuffer.hxx"
 #include "util/UriUtil.hxx"
+#include "util/NumberParser.hxx"
 #include "util/Error.hxx"
 #include "fs/AllocatedPath.hxx"
 
@@ -118,6 +119,59 @@ handle_addid(Client &client, unsigned argc, char *argv[])
 	return CommandResult::OK;
 }
 
+/**
+ * Parse a string in the form "START:END", both being (optional)
+ * fractional non-negative time offsets in seconds.  Returns both in
+ * integer milliseconds.  Omitted values are zero.
+ */
+static bool
+parse_time_range(const char *p, unsigned &start_ms, unsigned &end_ms)
+{
+	char *endptr;
+
+	const float start = ParseFloat(p, &endptr);
+	if (*endptr != ':' || start < 0)
+		return false;
+
+	start_ms = endptr > p
+		? unsigned(start * 1000u)
+		: 0u;
+
+	p = endptr + 1;
+
+	const float end = ParseFloat(p, &endptr);
+	if (*endptr != 0 || end < 0)
+		return false;
+
+	end_ms = endptr > p
+		? unsigned(end * 1000u)
+		: 0u;
+
+	return end_ms == 0 || end_ms > start_ms;
+}
+
+CommandResult
+handle_rangeid(Client &client, gcc_unused unsigned argc, char *argv[])
+{
+	unsigned id;
+	if (!check_unsigned(client, &id, argv[1]))
+		return CommandResult::ERROR;
+
+	unsigned start_ms, end_ms;
+	if (!parse_time_range(argv[2], start_ms, end_ms)) {
+		command_error(client, ACK_ERROR_ARG, "Bad range");
+		return CommandResult::ERROR;
+	}
+
+	Error error;
+	if (!client.partition.playlist.SetSongIdRange(client.partition.pc,
+						      id, start_ms, end_ms,
+						      error))
+		return print_error(client, error);
+
+	return CommandResult::OK;
+}
+
 CommandResult
 handle_delete(Client &client, gcc_unused unsigned argc, char *argv[])
 {
diff --git a/src/command/QueueCommands.hxx b/src/command/QueueCommands.hxx
index ece543cfd..f98f7bad2 100644
--- a/src/command/QueueCommands.hxx
+++ b/src/command/QueueCommands.hxx
@@ -30,6 +30,9 @@ handle_add(Client &client, unsigned argc, char *argv[]);
 CommandResult
 handle_addid(Client &client, unsigned argc, char *argv[]);
 
+CommandResult
+handle_rangeid(Client &client, unsigned argc, char *argv[]);
+
 CommandResult
 handle_delete(Client &client, unsigned argc, char *argv[]);
 
diff --git a/src/queue/Playlist.hxx b/src/queue/Playlist.hxx
index f2d778382..0f73a0513 100644
--- a/src/queue/Playlist.hxx
+++ b/src/queue/Playlist.hxx
@@ -225,6 +225,14 @@ public:
 	PlaylistResult SetPriorityId(PlayerControl &pc,
 				     unsigned song_id, uint8_t priority);
 
+	/**
+	 * Sets the start_ms and end_ms attributes on the song
+	 * with the specified id.
+	 */
+	bool SetSongIdRange(PlayerControl &pc, unsigned id,
+			    unsigned start_ms, unsigned end_ms,
+			    Error &error);
+
 	bool AddSongIdTag(unsigned id, TagType tag_type, const char *value,
 			  Error &error);
 	bool ClearSongIdTag(unsigned id, TagType tag_type, Error &error);
diff --git a/src/queue/PlaylistEdit.cxx b/src/queue/PlaylistEdit.cxx
index 4804a30aa..2ac015f6f 100644
--- a/src/queue/PlaylistEdit.cxx
+++ b/src/queue/PlaylistEdit.cxx
@@ -431,3 +431,60 @@ playlist::Shuffle(PlayerControl &pc, unsigned start, unsigned end)
 	UpdateQueuedSong(pc, queued_song);
 	OnModified();
 }
+
+bool
+playlist::SetSongIdRange(PlayerControl &pc, unsigned id,
+			 unsigned start_ms, unsigned end_ms,
+			 Error &error)
+{
+	assert(end_ms == 0 || start_ms < end_ms);
+
+	int position = queue.IdToPosition(id);
+	if (position < 0) {
+		error.Set(playlist_domain, int(PlaylistResult::NO_SUCH_SONG),
+			  "No such song");
+		return false;
+	}
+
+	if (playing) {
+		if (position == current) {
+			error.Set(playlist_domain, int(PlaylistResult::DENIED),
+				  "Cannot edit the current song");
+			return false;
+		}
+
+		if (position == queued) {
+			/* if we're manipulating the "queued" song,
+			   the decoder thread may be decoding it
+			   already; cancel that */
+			pc.Cancel();
+			queued = -1;
+		}
+	}
+
+	DetachedSong &song = queue.Get(position);
+	if (song.GetTag().time > 0) {
+		/* validate the offsets */
+
+		const unsigned duration = song.GetTag().time;
+		if (start_ms / 1000u > duration) {
+			error.Set(playlist_domain,
+				  int(PlaylistResult::BAD_RANGE),
+				  "Invalid start offset");
+			return false;
+		}
+
+		if (end_ms / 1000u > duration)
+			end_ms = 0;
+	}
+
+	/* edit it */
+	song.SetStartMS(start_ms);
+	song.SetEndMS(end_ms);
+
+	/* announce the change to all interested subsystems */
+	UpdateQueuedSong(pc, nullptr);
+	queue.ModifyAtPosition(position);
+	OnModified();
+	return true;
+}