diff --git a/NEWS b/NEWS index 7c4d36bba..6d9857bb9 100644 --- a/NEWS +++ b/NEWS @@ -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 diff --git a/doc/protocol.xml b/doc/protocol.xml index 4355334e2..0e1c1fb83 100644 --- a/doc/protocol.xml +++ b/doc/protocol.xml @@ -1590,6 +1590,7 @@ OK TYPE WHAT ... + sort TYPE window START:END @@ -1636,6 +1637,18 @@ OK WHAT is what to find. + + sort sorts the result by the + specified tag. Without sort, 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". + + window can be used to query only a portion of the real response. The parameter is two @@ -1833,6 +1846,7 @@ OK TYPE WHAT ... + sort TYPE window START:END diff --git a/src/command/DatabaseCommands.cxx b/src/command/DatabaseCommands.cxx index b67e3c1ae..729de699e 100644 --- a/src/command/DatabaseCommands.cxx +++ b/src/command/DatabaseCommands.cxx @@ -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; } diff --git a/src/db/DatabasePrint.cxx b/src/db/DatabasePrint.cxx index 31f971cd8..0e87cc557 100644 --- a/src/db/DatabasePrint.cxx +++ b/src/db/DatabasePrint.cxx @@ -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::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::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 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::max()); } diff --git a/src/db/DatabasePrint.hxx b/src/db/DatabasePrint.hxx index 60f3e5a10..e0a27674d 100644 --- a/src/db/DatabasePrint.hxx +++ b/src/db/DatabasePrint.hxx @@ -20,6 +20,9 @@ #ifndef MPD_DB_PRINT_H #define MPD_DB_PRINT_H +#include + +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 diff --git a/src/tag/Tag.cxx b/src/tag/Tag.cxx index 000652c24..6b7faceb9 100644 --- a/src/tag/Tag.cxx +++ b/src/tag/Tag.cxx @@ -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 ""; +} diff --git a/src/tag/Tag.hxx b/src/tag/Tag.hxx index 98039c367..31658b0a5 100644 --- a/src/tag/Tag.hxx +++ b/src/tag/Tag.hxx @@ -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;