command/Database: add "sort" parameter to "find" and "search"

Implement the second part of https://bugs.musicpd.org/view.php?id=3990
This commit is contained in:
Max Kellermann 2017-02-08 09:22:15 +01:00
parent 1e0a60e73d
commit 3850716522
7 changed files with 178 additions and 9 deletions

1
NEWS
View File

@ -1,6 +1,7 @@
ver 0.21 (not yet released)
* protocol
- "tagtypes" can be used to hide tags
- "find" and "search" can sort
ver 0.20.5 (not yet released)
* tags

View File

@ -1590,6 +1590,7 @@ OK
<arg choice="req"><replaceable>TYPE</replaceable></arg>
<arg choice="req"><replaceable>WHAT</replaceable></arg>
<arg choice="opt"><replaceable>...</replaceable></arg>
<arg choice="opt">sort <replaceable>TYPE</replaceable></arg>
<arg choice="opt">window <replaceable>START</replaceable>:<replaceable>END</replaceable></arg>
</cmdsynopsis>
</term>
@ -1636,6 +1637,18 @@ OK
<varname>WHAT</varname> is what to find.
</para>
<para>
<varname>sort</varname> sorts the result by the
specified tag. Without <varname>sort</varname>, the
order is undefined. Only the first tag value will be
used, if multiple of the same type exist. To sort by
"Artist", "Album" or "AlbumArtist", you should specify
"ArtistSort", "AlbumSort" or "AlbumArtistSort" instead.
These will automatically fall back to the former if
"*Sort" doesn't exist. "AlbumArtist" falls back to just
"Artist".
</para>
<para>
<varname>window</varname> can be used to query only a
portion of the real response. The parameter is two
@ -1833,6 +1846,7 @@ OK
<arg choice="req"><replaceable>TYPE</replaceable></arg>
<arg choice="req"><replaceable>WHAT</replaceable></arg>
<arg choice="opt"><replaceable>...</replaceable></arg>
<arg choice="opt">sort <replaceable>TYPE</replaceable></arg>
<arg choice="opt">window <replaceable>START</replaceable>:<replaceable>END</replaceable></arg>
</cmdsynopsis>
</term>

View File

@ -67,6 +67,16 @@ handle_match(Client &client, Request args, Response &r, bool fold_case)
} else
window.SetAll();
TagType sort = TAG_NUM_OF_ITEM_TYPES;
if (args.size >= 2 && StringIsEqual(args[args.size - 2], "sort")) {
sort = tag_name_parse_i(args.back());
if (sort == TAG_NUM_OF_ITEM_TYPES)
throw ProtocolError(ACK_ERROR_ARG, "Unknown sort tag");
args.pop_back();
args.pop_back();
}
SongFilter filter;
if (!filter.Parse(args, fold_case)) {
r.Error(ACK_ERROR_ARG, "incorrect arguments");
@ -77,6 +87,7 @@ handle_match(Client &client, Request args, Response &r, bool fold_case)
db_selection_print(r, client.partition,
selection, true, false,
sort,
window.start, window.end);
return CommandResult::OK;
}

View File

@ -22,6 +22,7 @@
#include "Selection.hxx"
#include "SongFilter.hxx"
#include "SongPrint.hxx"
#include "DetachedSong.hxx"
#include "TimePrint.hxx"
#include "TagPrint.hxx"
#include "client/Response.hxx"
@ -139,10 +140,36 @@ PrintPlaylistFull(Response &r, bool base,
time_print(r, "Last-Modified", playlist.mtime);
}
static bool
CompareNumeric(const char *a, const char *b)
{
long a_value = strtol(a, nullptr, 10);
long b_value = strtol(b, nullptr, 10);
return a_value < b_value;
}
static bool
CompareTags(TagType type, const Tag &a, const Tag &b)
{
const char *a_value = a.GetSortValue(type);
const char *b_value = b.GetSortValue(type);
switch (type) {
case TAG_DISC:
case TAG_TRACK:
return CompareNumeric(a_value, b_value);
default:
return strcmp(a_value, b_value) < 0;
}
}
void
db_selection_print(Response &r, Partition &partition,
const DatabaseSelection &selection,
bool full, bool base,
TagType sort,
unsigned window_start, unsigned window_end)
{
const Database &db = partition.GetDatabaseOrThrow();
@ -161,16 +188,53 @@ db_selection_print(Response &r, Partition &partition,
std::ref(r), base, _1, _2)
: VisitPlaylist();
if (window_start > 0 ||
window_end < (unsigned)std::numeric_limits<int>::max())
s = [s, window_start, window_end, &i](const LightSong &song){
const bool in_window = i >= window_start && i < window_end;
++i;
if (in_window)
s(song);
};
if (sort == TAG_NUM_OF_ITEM_TYPES) {
if (window_start > 0 ||
window_end < (unsigned)std::numeric_limits<int>::max())
s = [s, window_start, window_end, &i](const LightSong &song){
const bool in_window = i >= window_start && i < window_end;
++i;
if (in_window)
s(song);
};
db.Visit(selection, d, s, p);
db.Visit(selection, d, s, p);
} else {
// TODO: allow the database plugin to sort internally
/* the client has asked us to sort the result; this is
pretty expensive, because instead of streaming the
result to the client, we need to copy it all into
this std::vector, and then sort it */
std::vector<DetachedSong> songs;
{
auto collect_songs = [&songs](const LightSong &song){
songs.emplace_back(song);
};
db.Visit(selection, d, collect_songs, p);
}
std::stable_sort(songs.begin(), songs.end(),
[sort](const DetachedSong &a, const DetachedSong &b){
return CompareTags(sort, a.GetTag(),
b.GetTag());
});
if (window_end < songs.size())
songs.erase(std::next(songs.begin(), window_end),
songs.end());
if (window_start >= songs.size())
return;
songs.erase(songs.begin(),
std::next(songs.begin(), window_start));
for (const auto &song : songs)
s((LightSong)song);
}
}
void
@ -179,6 +243,7 @@ db_selection_print(Response &r, Partition &partition,
bool full, bool base)
{
db_selection_print(r, partition, selection, full, base,
TAG_NUM_OF_ITEM_TYPES,
0, std::numeric_limits<int>::max());
}

View File

@ -20,6 +20,9 @@
#ifndef MPD_DB_PRINT_H
#define MPD_DB_PRINT_H
#include <stdint.h>
enum TagType : uint8_t;
class TagMask;
class SongFilter;
struct DatabaseSelection;
@ -39,6 +42,7 @@ void
db_selection_print(Response &r, Partition &partition,
const DatabaseSelection &selection,
bool full, bool base,
TagType sort,
unsigned window_start, unsigned window_end);
void

View File

@ -96,3 +96,68 @@ Tag::HasType(TagType type) const
{
return GetValue(type) != nullptr;
}
static TagType
DecaySort(TagType type)
{
switch (type) {
case TAG_ARTIST_SORT:
return TAG_ARTIST;
case TAG_ALBUM_SORT:
return TAG_ALBUM;
case TAG_ALBUM_ARTIST_SORT:
return TAG_ALBUM_ARTIST;
default:
return TAG_NUM_OF_ITEM_TYPES;
}
}
static TagType
Fallback(TagType type)
{
switch (type) {
case TAG_ALBUM_ARTIST:
return TAG_ARTIST;
case TAG_MUSICBRAINZ_ALBUMARTISTID:
return TAG_MUSICBRAINZ_ARTISTID;
default:
return TAG_NUM_OF_ITEM_TYPES;
}
}
const char *
Tag::GetSortValue(TagType type) const
{
const char *value = GetValue(type);
if (value != nullptr)
return value;
/* try without *_SORT */
const auto no_sort_type = DecaySort(type);
if (no_sort_type != TAG_NUM_OF_ITEM_TYPES) {
value = GetValue(no_sort_type);
if (value != nullptr)
return value;
}
/* fall back from TAG_ALBUM_ARTIST to TAG_ALBUM */
type = Fallback(type);
if (type != TAG_NUM_OF_ITEM_TYPES)
return GetSortValue(type);
if (no_sort_type != TAG_NUM_OF_ITEM_TYPES) {
type = Fallback(no_sort_type);
if (type != TAG_NUM_OF_ITEM_TYPES)
return GetSortValue(type);
}
/* finally fall back to empty string */
return "";
}

View File

@ -142,6 +142,15 @@ struct Tag {
gcc_pure
bool HasType(TagType type) const;
/**
* Returns a value for sorting on the specified type, with
* automatic fallbacks to the next best tag type
* (e.g. #TAG_ALBUM_ARTIST falls back to #TAG_ARTIST). If
* there is no such value, returns an empty string.
*/
gcc_pure
const char *GetSortValue(TagType type) const;
class const_iterator {
friend struct Tag;
const TagItem *const*cursor;