Commit 38507165 authored by Max Kellermann's avatar Max Kellermann

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

Implement the second part of https://bugs.musicpd.org/view.php?id=3990
parent 1e0a60e7
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
......
......@@ -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>
......@@ -1637,6 +1638,18 @@ OK
</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
zero-based record numbers; a start number and an end
......@@ -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>
......
......@@ -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;
}
......
......@@ -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());
}
......
......@@ -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
......
......@@ -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 "";
}
......@@ -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;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment