diff options
-rw-r--r-- | NEWS | 2 | ||||
-rw-r--r-- | doc/protocol.xml | 10 | ||||
-rw-r--r-- | src/command/DatabaseCommands.cxx | 18 | ||||
-rw-r--r-- | src/db/Count.cxx | 85 | ||||
-rw-r--r-- | src/db/Count.hxx | 4 |
5 files changed, 109 insertions, 10 deletions
@@ -6,7 +6,7 @@ ver 0.19 (not yet released) - "playlistadd" supports file:/// - "idle" with unrecognized event name fails - "list" on album artist falls back to the artist tag - - "list" allows grouping + - "list" and "count" allow grouping * database - proxy: forward "idle" events - proxy: copy "Last-Modified" from remote directories diff --git a/doc/protocol.xml b/doc/protocol.xml index e1fb08a84..e9c28995b 100644 --- a/doc/protocol.xml +++ b/doc/protocol.xml @@ -1493,12 +1493,15 @@ OK <title>The music database</title> <variablelist> + <varlistentry id="command_count"> <term> <cmdsynopsis> <command>count</command> <arg choice="req"><replaceable>TAG</replaceable></arg> <arg choice="req"><replaceable>NEEDLE</replaceable></arg> + <arg choice="opt">group</arg> + <arg choice="opt"><replaceable>GROUPTYPE</replaceable></arg> </cmdsynopsis> </term> <listitem> @@ -1506,8 +1509,15 @@ OK Counts the number of songs and their total playtime in the db matching <varname>TAG</varname> exactly. </para> + <para> + The <parameter>group</parameter> keyword may be used to + group the results by a tag. The following prints + per-artist counts: + </para> + <programlisting>count group artist</programlisting> </listitem> </varlistentry> + <varlistentry id="command_find"> <term> <cmdsynopsis> diff --git a/src/command/DatabaseCommands.cxx b/src/command/DatabaseCommands.cxx index 0f23dc959..f41c89c45 100644 --- a/src/command/DatabaseCommands.cxx +++ b/src/command/DatabaseCommands.cxx @@ -153,14 +153,28 @@ handle_count(Client &client, int argc, char *argv[]) { ConstBuffer<const char *> args(argv + 1, argc - 1); + TagType group = TAG_NUM_OF_ITEM_TYPES; + if (args.size >= 2 && strcmp(args[args.size - 2], "group") == 0) { + const char *s = args[args.size - 1]; + group = tag_name_parse_i(s); + if (group == TAG_NUM_OF_ITEM_TYPES) { + command_error(client, ACK_ERROR_ARG, + "Unknown tag type: %s", s); + return CommandResult::ERROR; + } + + args.pop_back(); + args.pop_back(); + } + SongFilter filter; - if (!filter.Parse(args, false)) { + if (!args.IsEmpty() && !filter.Parse(args, false)) { command_error(client, ACK_ERROR_ARG, "incorrect arguments"); return CommandResult::ERROR; } Error error; - return PrintSongCount(client, "", &filter, error) + return PrintSongCount(client, "", &filter, group, error) ? CommandResult::OK : print_error(client, error); } diff --git a/src/db/Count.cxx b/src/db/Count.cxx index 8c61e4c21..ec3eacd1f 100644 --- a/src/db/Count.cxx +++ b/src/db/Count.cxx @@ -23,8 +23,10 @@ #include "Interface.hxx" #include "client/Client.hxx" #include "LightSong.hxx" +#include "tag/Set.hxx" #include <functional> +#include <map> struct SearchStats { unsigned n_songs; @@ -34,6 +36,9 @@ struct SearchStats { :n_songs(0), total_time_s(0) {} }; +class TagCountMap : public std::map<std::string, SearchStats> { +}; + static void PrintSearchStats(Client &client, const SearchStats &stats) { @@ -43,6 +48,18 @@ PrintSearchStats(Client &client, const SearchStats &stats) stats.n_songs, stats.total_time_s); } +static void +Print(Client &client, TagType group, const TagCountMap &m) +{ + assert(unsigned(group) < TAG_NUM_OF_ITEM_TYPES); + + for (const auto &i : m) { + client_printf(client, "%s: %s\n", + tag_item_names[group], i.first.c_str()); + PrintSearchStats(client, i.second); + } +} + static bool stats_visitor_song(SearchStats &stats, const LightSong &song) { @@ -52,9 +69,45 @@ stats_visitor_song(SearchStats &stats, const LightSong &song) return true; } +static bool +CollectGroupCounts(TagCountMap &map, TagType group, const Tag &tag) +{ + bool found = false; + for (unsigned i = 0; i < tag.num_items; ++i) { + const TagItem &item = *tag.items[i]; + + if (item.type == group) { + auto r = map.insert(std::make_pair(item.value, + SearchStats())); + SearchStats &s = r.first->second; + ++s.n_songs; + if (tag.time > 0) + s.total_time_s += tag.time; + + found = true; + } + } + + return found; +} + +static bool +GroupCountVisitor(TagCountMap &map, TagType group, const LightSong &song) +{ + assert(song.tag != nullptr); + + const Tag &tag = *song.tag; + if (!CollectGroupCounts(map, group, tag) && group == TAG_ALBUM_ARTIST) + /* fall back to "Artist" if no "AlbumArtist" was found */ + CollectGroupCounts(map, TAG_ARTIST, tag); + + return true; +} + bool PrintSongCount(Client &client, const char *name, const SongFilter *filter, + TagType group, Error &error) { const Database *db = client.GetDatabase(error); @@ -63,14 +116,32 @@ PrintSongCount(Client &client, const char *name, const DatabaseSelection selection(name, true, filter); - SearchStats stats; + if (group == TAG_NUM_OF_ITEM_TYPES) { + /* no grouping */ - using namespace std::placeholders; - const auto f = std::bind(stats_visitor_song, std::ref(stats), - _1); - if (!db->Visit(selection, f, error)) - return false; + SearchStats stats; + + using namespace std::placeholders; + const auto f = std::bind(stats_visitor_song, std::ref(stats), + _1); + if (!db->Visit(selection, f, error)) + return false; + + PrintSearchStats(client, stats); + } else { + /* group by the specified tag: store counts in a + std::map */ + + TagCountMap map; + + using namespace std::placeholders; + const auto f = std::bind(GroupCountVisitor, std::ref(map), + group, _1); + if (!db->Visit(selection, f, error)) + return false; + + Print(client, group, map); + } - PrintSearchStats(client, stats); return true; } diff --git a/src/db/Count.hxx b/src/db/Count.hxx index 5c5fca1bd..d22a3210d 100644 --- a/src/db/Count.hxx +++ b/src/db/Count.hxx @@ -22,6 +22,9 @@ #include "Compiler.h" +#include <stdint.h> + +enum TagType : uint8_t; class Client; class SongFilter; class Error; @@ -30,6 +33,7 @@ gcc_nonnull(2) bool PrintSongCount(Client &client, const char *name, const SongFilter *filter, + TagType group, Error &error); #endif |