diff --git a/NEWS b/NEWS
index 1c801f3a3..842f61edc 100644
--- a/NEWS
+++ b/NEWS
@@ -10,7 +10,7 @@ ver 0.24 (not yet released)
   - show PCRE support in "config" response
   - apply Unicode normalization to case-insensitive filter expressions
   - stickers on playlists and some tag types
-  - new command "stickernames"
+  - new commands "stickernames" and "playlistlength"
   - new "search"/"find" filter "added-since"
 * database
   - attribute "added" shows when each song was added to the database
diff --git a/doc/protocol.rst b/doc/protocol.rst
index 5fca9496b..3e657aac9 100644
--- a/doc/protocol.rst
+++ b/doc/protocol.rst
@@ -1001,6 +1001,19 @@ remote playlists (absolute URI with a supported scheme).
 
     The second parameter can be a range. [#since_0_23_3]_
 
+.. _command_playlistlength:
+
+:command:`playlistlength {NAME}`
+    Count the number of songs and their total playtime (seconds) in the
+    playlist.
+
+    Example::
+
+     playlistlength example
+     songs: 10
+     playtime: 8192
+     OK
+
 .. _command_playlistmove:
 
 :command:`playlistmove {NAME} [{FROM} | {START:END}] {TO}`
diff --git a/meson.build b/meson.build
index d56f64505..f7bac24e1 100644
--- a/meson.build
+++ b/meson.build
@@ -387,6 +387,7 @@ sources = [
   'src/PlaylistError.cxx',
   'src/PlaylistPrint.cxx',
   'src/PlaylistSave.cxx',
+  'src/playlist/Length.cxx',
   'src/playlist/PlaylistStream.cxx',
   'src/playlist/PlaylistMapper.cxx',
   'src/playlist/PlaylistAny.cxx',
diff --git a/src/command/AllCommands.cxx b/src/command/AllCommands.cxx
index ddfe9a2b9..fda8d0f69 100644
--- a/src/command/AllCommands.cxx
+++ b/src/command/AllCommands.cxx
@@ -148,6 +148,7 @@ static constexpr struct command commands[] = {
 	{ "playlistfind", PERMISSION_READ, 1, -1, handle_playlistfind },
 	{ "playlistid", PERMISSION_READ, 0, 1, handle_playlistid },
 	{ "playlistinfo", PERMISSION_READ, 0, 1, handle_playlistinfo },
+	{ "playlistlength", PERMISSION_READ, 1, 1, handle_playlistlength },
 	{ "playlistmove", PERMISSION_CONTROL, 3, 3, handle_playlistmove },
 	{ "playlistsearch", PERMISSION_READ, 1, -1, handle_playlistsearch },
 	{ "plchanges", PERMISSION_READ, 1, 2, handle_plchanges },
diff --git a/src/command/PlaylistCommands.cxx b/src/command/PlaylistCommands.cxx
index 65dbe4044..84f647eaf 100644
--- a/src/command/PlaylistCommands.cxx
+++ b/src/command/PlaylistCommands.cxx
@@ -17,6 +17,7 @@
 #include "SongLoader.hxx"
 #include "song/DetachedSong.hxx"
 #include "BulkEdit.hxx"
+#include "playlist/Length.hxx"
 #include "playlist/PlaylistQueue.hxx"
 #include "playlist/Print.hxx"
 #include "TimePrint.hxx"
@@ -149,6 +150,23 @@ handle_listplaylistinfo(Client &client, Request args, Response &r)
 	throw PlaylistError::NoSuchList();
 }
 
+CommandResult
+handle_playlistlength(Client &client, Request args, Response &r)
+{
+	const auto name = LocateUri(UriPluginKind::PLAYLIST, args.front(),
+				    &client
+#ifdef ENABLE_DATABASE
+					   , nullptr
+#endif
+					   );
+
+	if (playlist_file_length(r, client.GetPartition(), SongLoader(client),
+				name))
+		return CommandResult::OK;
+
+	throw PlaylistError::NoSuchList();
+}
+
 CommandResult
 handle_rm([[maybe_unused]] Client &client, Request args, [[maybe_unused]] Response &r)
 {
diff --git a/src/command/PlaylistCommands.hxx b/src/command/PlaylistCommands.hxx
index f90af8433..1c4ce7ce4 100644
--- a/src/command/PlaylistCommands.hxx
+++ b/src/command/PlaylistCommands.hxx
@@ -26,6 +26,9 @@ handle_listplaylist(Client &client, Request request, Response &response);
 CommandResult
 handle_listplaylistinfo(Client &client, Request request, Response &response);
 
+CommandResult
+handle_playlistlength(Client &client, Request request, Response &response);
+
 CommandResult
 handle_rm(Client &client, Request request, Response &response);
 
diff --git a/src/playlist/Length.cxx b/src/playlist/Length.cxx
new file mode 100644
index 000000000..56c03cc6f
--- /dev/null
+++ b/src/playlist/Length.cxx
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// Copyright The Music Player Daemon Project
+
+#include "config.h"
+#include "LocateUri.hxx"
+#include "Length.hxx"
+#include "PlaylistAny.hxx"
+#include "PlaylistSong.hxx"
+#include "SongEnumerator.hxx"
+#include "SongPrint.hxx"
+#include "song/DetachedSong.hxx"
+#include "song/LightSong.hxx"
+#include "fs/Traits.hxx"
+#include "thread/Mutex.hxx"
+#include "Partition.hxx"
+#include "Instance.hxx"
+
+#include <fmt/format.h>
+
+static SignedSongTime get_duration(const DetachedSong &song) {
+	const auto duration = song.GetDuration();
+	return duration.IsNegative() ? (SignedSongTime)0 : song.GetDuration();
+}
+
+static void
+playlist_provider_length(Response &r,
+			const SongLoader &loader,
+			const char *uri,
+			SongEnumerator &e) noexcept
+{
+	const auto base_uri = uri != nullptr
+		? PathTraitsUTF8::GetParent(uri)
+		: ".";
+
+	std::unique_ptr<DetachedSong> song;
+	unsigned i = 0;
+	SignedSongTime playtime = (SignedSongTime)0;
+	while ((song = e.NextSong()) != nullptr) {
+		if (playlist_check_translate_song(*song, base_uri,
+						  loader))
+			playtime += get_duration(*song);
+		i++;
+	}
+	r.Fmt(FMT_STRING("songs: {}\n"), i);
+	r.Fmt(FMT_STRING("playtime: {}\n"), playtime.RoundS());
+}
+
+bool
+playlist_file_length(Response &r, Partition &partition,
+		    const SongLoader &loader,
+		    const LocatedUri &uri)
+{
+	Mutex mutex;
+
+#ifndef ENABLE_DATABASE
+	(void)partition;
+#endif
+
+	auto playlist = playlist_open_any(uri,
+#ifdef ENABLE_DATABASE
+					  partition.instance.storage,
+#endif
+					  mutex);
+	if (playlist == nullptr)
+		return false;
+
+	playlist_provider_length(r, loader, uri.canonical_uri, *playlist);
+	return true;
+}
diff --git a/src/playlist/Length.hxx b/src/playlist/Length.hxx
new file mode 100644
index 000000000..053b784c4
--- /dev/null
+++ b/src/playlist/Length.hxx
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// Copyright The Music Player Daemon Project
+
+#ifndef MPD_PLAYLIST__LENGTH_HXX
+#define MPD_PLAYLIST__LENGTH_HXX
+
+#include "client/Response.hxx"
+
+class SongLoader;
+struct Partition;
+
+/**
+ * Count the number of songs and their total playtime (seconds) in the
+ * playlist.
+ *
+ * @param uri the URI of the playlist file in UTF-8 encoding
+ * @return true on success, false if the playlist does not exist
+ */
+bool
+playlist_file_length(Response &r, Partition &partition,
+		    const SongLoader &loader,
+		    const LocatedUri &uri);
+
+#endif