From cd093a60141077b91693d4865c6e48ed2b912dfa Mon Sep 17 00:00:00 2001
From: gd <gd@iotide.com>
Date: Sun, 2 Oct 2022 11:19:26 +0300
Subject: [PATCH] Added new optional argument MODE to command 'save' to be able
 to append or replace an existing playlist

---
 doc/protocol.rst                 | 13 ++++++++++++-
 src/PlaylistSave.cxx             | 23 ++++++++++++++++-------
 src/PlaylistSave.hxx             | 13 ++++++++-----
 src/command/AllCommands.cxx      |  2 +-
 src/command/PlaylistCommands.cxx | 17 ++++++++++++++++-
 5 files changed, 53 insertions(+), 15 deletions(-)

diff --git a/doc/protocol.rst b/doc/protocol.rst
index 69bdada87..9d8125698 100644
--- a/doc/protocol.rst
+++ b/doc/protocol.rst
@@ -1008,10 +1008,21 @@ remote playlists (absolute URI with a supported scheme).
 
 .. _command_save:
 
-:command:`save {NAME}`
+:command:`save {NAME} [MODE]`
     Saves the queue to
     `NAME.m3u` in the playlist directory.
 
+    ``MODE`` [#since_0_24]_
+        Optional argument. One of `create`, `append`, or `replace`.
+
+        `create`
+            The default. Create a new playlist.
+            Fail if a playlist with name ``NAME`` already exists.
+
+        `append`, `replace`
+            Append or replace an existing playlist.
+            Fail if a playlist with name ``NAME`` doesn\'t already exist.
+
 The music database
 ==================
 
diff --git a/src/PlaylistSave.cxx b/src/PlaylistSave.cxx
index 6bcedb2d7..73157ca79 100644
--- a/src/PlaylistSave.cxx
+++ b/src/PlaylistSave.cxx
@@ -84,16 +84,25 @@ playlist_print_uri(BufferedOutputStream &os, const char *uri)
 }
 
 void
-spl_save_queue(const char *name_utf8, const Queue &queue)
+spl_save_queue(const char *name_utf8, PlaylistSaveMode save_mode, const Queue &queue)
 {
 	const auto path_fs = spl_map_to_fs(name_utf8);
 	assert(!path_fs.IsNull());
 
-	if (FileExists(path_fs))
-		throw PlaylistError(PlaylistResult::LIST_EXISTS,
-				    "Playlist already exists");
+	if (save_mode == PlaylistSaveMode::CREATE) {
+		if (FileExists(path_fs)) {
+			throw PlaylistError(PlaylistResult::LIST_EXISTS, "Playlist already exists");
+		}
+	}
+	else if (!FileExists(path_fs)) {
+		throw PlaylistError(PlaylistResult::NO_SUCH_LIST, "No such playlist");
+	}
+
+	FileOutputStream fos(path_fs,
+			     save_mode == PlaylistSaveMode::APPEND
+			     ? FileOutputStream::Mode::APPEND_EXISTING
+			     : FileOutputStream::Mode::CREATE);
 
-	FileOutputStream fos(path_fs);
 	BufferedOutputStream bos(fos);
 
 	for (unsigned i = 0; i < queue.GetLength(); i++)
@@ -106,7 +115,7 @@ spl_save_queue(const char *name_utf8, const Queue &queue)
 }
 
 void
-spl_save_playlist(const char *name_utf8, const playlist &playlist)
+spl_save_playlist(const char *name_utf8, PlaylistSaveMode save_mode, const playlist &playlist)
 {
-	spl_save_queue(name_utf8, playlist.queue);
+	spl_save_queue(name_utf8, save_mode, playlist.queue);
 }
diff --git a/src/PlaylistSave.hxx b/src/PlaylistSave.hxx
index eebd9ed14..f0063783f 100644
--- a/src/PlaylistSave.hxx
+++ b/src/PlaylistSave.hxx
@@ -31,16 +31,19 @@ playlist_print_song(BufferedOutputStream &os, const DetachedSong &song);
 void
 playlist_print_uri(BufferedOutputStream &os, const char *uri);
 
-/**
- * Saves a queue object into a stored playlist file.
- */
+enum class PlaylistSaveMode {
+	CREATE,
+	APPEND,
+	REPLACE
+};
+
 void
-spl_save_queue(const char *name_utf8, const Queue &queue);
+spl_save_queue(const char *name_utf8, PlaylistSaveMode save_mode, const Queue &queue);
 
 /**
  * Saves a playlist object into a stored playlist file.
  */
 void
-spl_save_playlist(const char *name_utf8, const playlist &playlist);
+spl_save_playlist(const char *name_utf8, PlaylistSaveMode save_mode, const playlist &playlist);
 
 #endif
diff --git a/src/command/AllCommands.cxx b/src/command/AllCommands.cxx
index 30f698640..ca2f29a9a 100644
--- a/src/command/AllCommands.cxx
+++ b/src/command/AllCommands.cxx
@@ -184,7 +184,7 @@ static constexpr struct command commands[] = {
 	  handle_replay_gain_status },
 	{ "rescan", PERMISSION_CONTROL, 0, 1, handle_rescan },
 	{ "rm", PERMISSION_CONTROL, 1, 1, handle_rm },
-	{ "save", PERMISSION_CONTROL, 1, 1, handle_save },
+	{ "save", PERMISSION_CONTROL, 1, 2, handle_save },
 #ifdef ENABLE_DATABASE
 	{ "search", PERMISSION_READ, 1, -1, handle_search },
 	{ "searchadd", PERMISSION_ADD, 1, -1, handle_searchadd },
diff --git a/src/command/PlaylistCommands.cxx b/src/command/PlaylistCommands.cxx
index b6311b4f9..c3654d30f 100644
--- a/src/command/PlaylistCommands.cxx
+++ b/src/command/PlaylistCommands.cxx
@@ -66,7 +66,22 @@ print_spl_list(Response &r, const PlaylistVector &list)
 CommandResult
 handle_save(Client &client, Request args, [[maybe_unused]] Response &r)
 {
-	spl_save_playlist(args.front(), client.GetPlaylist());
+	PlaylistSaveMode mode = PlaylistSaveMode::CREATE;
+
+	const char *mode_arg = args.GetOptional(1);
+	if (mode_arg != nullptr) {
+		if (StringIsEqual(mode_arg, "create"))
+			mode = PlaylistSaveMode::CREATE;
+		else if (StringIsEqual(mode_arg, "append"))
+			mode = PlaylistSaveMode::APPEND;
+		else if (StringIsEqual(mode_arg, "replace"))
+			mode = PlaylistSaveMode::REPLACE;
+		else
+			throw std::invalid_argument("Unrecognized save mode, expected one of 'create', 'append', 'replace'");
+	}
+
+	spl_save_playlist(args.front(), mode, client.GetPlaylist());
+
 	return CommandResult::OK;
 }