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 + +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 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