From 1afa33c3c766af22c35b02ba58e84693243a4f3e Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Thu, 21 Jan 2021 13:14:27 +0100
Subject: [PATCH] db/simple/Song: Export() merges tags with "target"

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1048
---
 NEWS                                          |  2 +-
 .../plugins/simple/SimpleDatabasePlugin.cxx   |  2 +-
 src/db/plugins/simple/Song.cxx                | 83 +++++++++++++++++--
 3 files changed, 80 insertions(+), 7 deletions(-)

diff --git a/NEWS b/NEWS
index 16353791c..d24c90f8c 100644
--- a/NEWS
+++ b/NEWS
@@ -1,7 +1,7 @@
 ver 0.22.4 (not yet released)
 * protocol
   - fix "readpicture" on 32 bit machines
-  - show duration of songs in virtual playlist (CUE) folders
+  - show duration and tags of songs in virtual playlist (CUE) folders
 * storage
   - curl: fix several WebDAV protocol bugs
 * decoder
diff --git a/src/db/plugins/simple/SimpleDatabasePlugin.cxx b/src/db/plugins/simple/SimpleDatabasePlugin.cxx
index 1e126f3da..bb8230723 100644
--- a/src/db/plugins/simple/SimpleDatabasePlugin.cxx
+++ b/src/db/plugins/simple/SimpleDatabasePlugin.cxx
@@ -233,12 +233,12 @@ SimpleDatabase::GetSong(std::string_view uri) const
 				    "No such song");
 
 	const Song *song = r.directory->FindSong(r.rest);
-	protect.unlock();
 	if (song == nullptr)
 		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
 				    "No such song");
 
 	exported_song.Construct(song->Export());
+	protect.unlock();
 
 #ifndef NDEBUG
 	++borrowed_song_count;
diff --git a/src/db/plugins/simple/Song.cxx b/src/db/plugins/simple/Song.cxx
index 519a1a3d7..8db47d03f 100644
--- a/src/db/plugins/simple/Song.cxx
+++ b/src/db/plugins/simple/Song.cxx
@@ -21,9 +21,12 @@
 #include "ExportedSong.hxx"
 #include "Directory.hxx"
 #include "tag/Tag.hxx"
+#include "tag/Builder.hxx"
 #include "song/DetachedSong.hxx"
 #include "song/LightSong.hxx"
 #include "fs/Traits.hxx"
+#include "time/ChronoUtil.hxx"
+#include "util/IterableSplitString.hxx"
 
 Song::Song(DetachedSong &&other, Directory &_parent) noexcept
 	:tag(std::move(other.WritableTag())),
@@ -54,17 +57,87 @@ Song::GetURI() const noexcept
 	}
 }
 
+/**
+ * Path name traversal of a #Directory.
+ */
+gcc_pure
+static const Directory *
+FindTargetDirectory(const Directory &base, StringView path) noexcept
+{
+	const auto *directory = &base;
+	for (const StringView name : IterableSplitString(path, '/')) {
+		if (name.empty() || name.Equals("."))
+			continue;
+
+		directory = name.Equals("..")
+			? directory->parent
+			: directory->FindChild(name);
+		if (directory == nullptr)
+			break;
+	}
+
+	return directory;
+}
+
+/**
+ * Path name traversal of a #Song.
+ */
+gcc_pure
+static const Song *
+FindTargetSong(const Directory &_directory, StringView target) noexcept
+{
+	auto [path, last] = target.SplitLast('/');
+	if (last == nullptr) {
+		last = path;
+		path = nullptr;
+	}
+
+	if (last.empty())
+		return nullptr;
+
+	const auto *directory = FindTargetDirectory(_directory, path);
+	if (directory == nullptr)
+		return nullptr;
+
+	return directory->FindSong(last);
+}
+
 ExportedSong
 Song::Export() const noexcept
 {
-	ExportedSong dest(filename.c_str(), tag);
+	const auto *target_song = !target.empty()
+		? FindTargetSong(parent, (std::string_view)target)
+		: nullptr;
+
+	Tag merged_tag;
+	if (target_song != nullptr) {
+		/* if we found the target song (which may be the
+		   underlying song file of a CUE file), merge the tags
+		   from that song with this song's tags (from the CUE
+		   file) */
+		TagBuilder builder(tag);
+		builder.Complement(target_song->tag);
+		merged_tag = builder.Commit();
+	}
+
+	ExportedSong dest = merged_tag.IsDefined()
+		? ExportedSong(filename.c_str(), std::move(merged_tag))
+		: ExportedSong(filename.c_str(), tag);
 	if (!parent.IsRoot())
 		dest.directory = parent.GetPath();
 	if (!target.empty())
 		dest.real_uri = target.c_str();
-	dest.mtime = mtime;
-	dest.start_time = start_time;
-	dest.end_time = end_time;
-	dest.audio_format = audio_format;
+	dest.mtime = IsNegative(mtime) && target_song != nullptr
+		? target_song->mtime
+		: mtime;
+	dest.start_time = start_time.IsZero() && target_song != nullptr
+		? target_song->start_time
+		: start_time;
+	dest.end_time = end_time.IsZero() && target_song != nullptr
+		? target_song->end_time
+		: end_time;
+	dest.audio_format = audio_format.IsDefined() || target_song == nullptr
+		? audio_format
+		: target_song->audio_format;
 	return dest;
 }