Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cpp/server/artwork_controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ ResponsePtr ArtworkController::getLibraryArtwork()
item.path = param<std::string>("path");
item.subsong = optionalParam<int32_t>("subsong", -1);

auto responseFuture = player_->fetchLibraryArtwork(item).then(
auto preferFolderImage = optionalParam<bool>("folderImage", false);

auto responseFuture = player_->fetchLibraryArtwork(item, preferFolderImage).then(
boost::launch::sync, [this](boost::unique_future<ArtworkResult> resultFuture) {
auto result = resultFuture.get();
return getResponse(&result);
Expand Down
6 changes: 5 additions & 1 deletion cpp/server/foobar2000/player.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,17 @@ class PlayerImpl final : public Player
LibraryNodesResult getLibraryNodes(
const LibraryQuery& query, const Range& range, ColumnsQuery* columns) override;

LibraryGroupsResult getLibraryGroups(
const LibraryGroupQuery& query, const Range& range, ColumnsQuery* columns) override;

void addLibraryItems(
const PlaylistRef& plref,
const LibraryItemQuery& query,
int32_t targetIndex,
AddItemsOptions options) override;

boost::unique_future<ArtworkResult> fetchLibraryArtwork(const LibraryItemRef& item) override;
boost::unique_future<ArtworkResult> fetchLibraryArtwork(
const LibraryItemRef& item, bool preferFolderImage) override;

boost::unique_future<ArtworkResult> fetchCurrentArtwork() override;
boost::unique_future<ArtworkResult> fetchArtwork(const ArtworkQuery& query) override;
Expand Down
255 changes: 240 additions & 15 deletions cpp/server/foobar2000/player_library.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,26 @@ std::string joinNodePath(const std::string& prefix, const std::string& name)
return prefix.empty() ? name : prefix + PATH_SEPARATOR + name;
}

// Orders names the way the platform does: case-insensitive, embedded numbers compared by value.
// Names that still compare equal (e.g. differ only in case) are kept distinct and ordered byte-wise,
// otherwise such folders would be merged together
struct NameLess
{
bool operator()(const std::string& left, const std::string& right) const
{
auto result = pfc::sysNaturalSortCompareI(left.c_str(), right.c_str());
return result != 0 ? result < 0 : left < right;
}
};

using NodeItem = std::pair<std::string, metadb_handle_ptr>;

// Single file may hold several tracks (cue sheets), keep such tracks in subsong order
void sortItems(std::vector<NodeItem>* items)
{
std::sort(items->begin(), items->end(), [](const NodeItem& left, const NodeItem& right) {
if (left.first != right.first)
return left.first < right.first;
return NameLess()(left.first, right.first);

return left.second->get_location().get_subsong() < right.second->get_location().get_subsong();
});
Expand Down Expand Up @@ -94,6 +106,54 @@ bool matchesRef(const LibraryItemRef& ref, const std::string& itemPath, const me
return isSubpath(itemPath, path);
}

// Separates levels in grouping pattern output, same as Album List views
constexpr char GROUP_SEPARATOR = '|';

// Shown for empty level values, same as for missing fields in title formatting,
// this also keeps any group distinguishable from the top level
constexpr char MISSING_VALUE[] = "?";

std::vector<std::string> splitLevels(const std::string& text)
{
std::vector<std::string> levels;
size_t start = 0;

while (true)
{
auto end = text.find(GROUP_SEPARATOR, start);
auto level = text.substr(start, end == std::string::npos ? std::string::npos : end - start);
levels.emplace_back(level.empty() ? std::string(MISSING_VALUE) : std::move(level));

if (end == std::string::npos)
return levels;

start = end + 1;
}
}

std::string joinLevels(std::vector<std::string>::const_iterator begin, std::vector<std::string>::const_iterator end)
{
std::string result;

for (auto it = begin; it != end; ++it)
{
if (it != begin)
result += GROUP_SEPARATOR;

result += *it;
}

return result;
}

struct GroupTrack
{
std::string label;
std::string path;
std::string sortKey;
metadb_handle_ptr item;
};

// Item locations are prefixed with a scheme, plain file system path is what artwork lookup needs
std::string getAbsolutePath(const metadb_handle_ptr& item)
{
Expand All @@ -108,8 +168,9 @@ std::string getAbsolutePath(const metadb_handle_ptr& item)
return std::string(path);
}

// Folders may hold their own artwork which is unrelated to artwork of the tracks below,
// this is what a folder view is expected to show
// Folders may hold an image file that represents the folder itself.
// This is not how the player resolves artwork (it uses configurable per track patterns),
// so it is only used when explicitly requested
std::string findFolderArtwork(const std::string& folderPath)
{
static const char* const names[] = {"folder", "cover", "front", "album", "artwork"};
Expand Down Expand Up @@ -182,6 +243,29 @@ bool endsWithNodePath(const std::string& absolutePath, const std::string& nodePa
return true;
}

// Absolute path of the folder an item reference points to, derived from a track below it.
// Empty when the reference points to a file or to the whole library
std::string getFolderPath(const LibraryItemRef& ref, const metadb_handle_ptr& firstItem)
{
auto path = normalizeNodePath(ref.path);
if (path.empty())
return std::string();

pfc::string8 buffer;
auto nodePath = getNodePath(library_manager::get(), firstItem, &buffer);

if (nodePath.length() <= path.length())
return std::string();

auto absolutePath = getAbsolutePath(firstItem);
auto suffixLength = nodePath.length() - path.length();

if (absolutePath.length() <= suffixLength || !endsWithNodePath(absolutePath, nodePath))
return std::string();

return absolutePath.substr(0, absolutePath.length() - suffixLength);
}

class ItemCounter : public library_manager::enum_callback
{
public:
Expand Down Expand Up @@ -402,7 +486,8 @@ void PlayerImpl::addLibraryItems(
}
}

boost::unique_future<ArtworkResult> PlayerImpl::fetchLibraryArtwork(const LibraryItemRef& item)
boost::unique_future<ArtworkResult> PlayerImpl::fetchLibraryArtwork(
const LibraryItemRef& item, bool preferFolderImage)
{
LibraryItemQuery query;
query.items.emplace_back(item);
Expand All @@ -414,20 +499,13 @@ boost::unique_future<ArtworkResult> PlayerImpl::fetchLibraryArtwork(const Librar
return boost::make_future(ArtworkResult());

const auto& firstItem = items[0];
auto path = normalizeNodePath(item.path);

pfc::string8 buffer;
auto nodePath = getNodePath(library_manager::get(), firstItem, &buffer);

// Query addresses a folder rather than a single file, prefer artwork stored in that folder
if (!path.empty() && nodePath.length() > path.length())
if (preferFolderImage)
{
auto absolutePath = getAbsolutePath(firstItem);
auto suffixLength = nodePath.length() - path.length();
auto folderPath = getFolderPath(item, firstItem);

if (absolutePath.length() > suffixLength && endsWithNodePath(absolutePath, nodePath))
if (!folderPath.empty())
{
auto folderPath = absolutePath.substr(0, absolutePath.length() - suffixLength);
auto artwork = findFolderArtwork(folderPath);

if (!artwork.empty())
Expand Down Expand Up @@ -456,7 +534,7 @@ LibraryNodesResult PlayerImpl::getLibraryNodes(
auto prefix = normalizeNodePath(query.path);
auto childOffset = prefix.empty() ? 0 : prefix.length() + 1;

std::map<std::string, int32_t> folders;
std::map<std::string, int32_t, NameLess> folders;
std::vector<NodeItem> files;

pfc::string8 buffer;
Expand Down Expand Up @@ -545,5 +623,152 @@ LibraryNodesResult PlayerImpl::getLibraryNodes(
return nodesResult;
}

LibraryGroupsResult PlayerImpl::getLibraryGroups(
const LibraryGroupQuery& query, const Range& range, ColumnsQuery* columns)
{
auto queryImpl = dynamic_cast<ColumnsQueryImpl*>(columns);
if (!queryImpl)
throw std::logic_error("ColumnsQueryImpl is required");

titleformat_object::ptr groupBy;
if (!titleFormatCompiler_->compile(groupBy, query.groupBy.c_str()))
throw InvalidRequestException("invalid format expression: " + query.groupBy);

titleformat_object::ptr sortBy;
if (!query.sortBy.empty() && !titleFormatCompiler_->compile(sortBy, query.sortBy.c_str()))
throw InvalidRequestException("invalid format expression: " + query.sortBy);

auto current = query.group.empty() ? std::vector<std::string>() : splitLevels(query.group);
auto depth = current.size();

auto libraryManager = library_manager::get();

metadb_handle_list items;
libraryManager->get_all_items(items);

if (!query.search.empty())
filterItems(&items, query.search);

std::map<std::string, int32_t, NameLess> groups;
std::vector<GroupTrack> tracks;
pfc::string8 buffer;

for (t_size i = 0; i < items.get_count(); i++)
{
const auto& item = items[i];

item->format_title(nullptr, buffer, groupBy, nullptr);
auto levels = splitLevels(std::string(buffer.get_ptr(), buffer.get_length()));

// Item belongs below current node only when it has more levels and all selected values match
if (levels.size() <= depth || !std::equal(current.begin(), current.end(), levels.begin()))
continue;

// Last level of an item is the label of the track itself
if (levels.size() == depth + 1)
{
GroupTrack track;
track.label = std::move(levels[depth]);
track.path = getNodePath(libraryManager, item, &buffer);
track.item = item;
tracks.emplace_back(std::move(track));
}
else
{
groups[levels[depth]]++;
}
}

for (auto& track : tracks)
{
if (sortBy.is_valid())
{
track.item->format_title(nullptr, buffer, sortBy, nullptr);
track.sortKey.assign(buffer.get_ptr(), buffer.get_length());
}
else
{
track.sortKey = track.label;
}
}

// Path and subsong make the order deterministic, so that paging is stable
std::sort(tracks.begin(), tracks.end(), [](const GroupTrack& left, const GroupTrack& right) {
if (left.sortKey != right.sortKey)
return NameLess()(left.sortKey, right.sortKey);

if (left.path != right.path)
return left.path < right.path;

return left.item->get_location().get_subsong() < right.item->get_location().get_subsong();
});

if (query.sortDescending)
std::reverse(tracks.begin(), tracks.end());

auto groupPath = joinLevels(current.begin(), current.end());

std::vector<LibraryNodeInfo> nodes;
std::vector<metadb_handle_ptr> handles;

nodes.reserve(groups.size() + tracks.size());
handles.reserve(groups.size() + tracks.size());

for (auto& group : groups)
{
LibraryNodeInfo node;
node.isFolder = true;
node.name = group.first;
node.group = groupPath.empty() ? group.first : groupPath + GROUP_SEPARATOR + group.first;
node.itemCount = group.second;
nodes.emplace_back(std::move(node));
handles.emplace_back();
}

for (auto& track : tracks)
{
LibraryNodeInfo node;
node.name = std::move(track.label);
node.path = std::move(track.path);
node.subsong = static_cast<int32_t>(track.item->get_location().get_subsong());
nodes.emplace_back(std::move(node));
handles.emplace_back(track.item);
}

auto totalCount = nodes.size();
auto offset = std::min(static_cast<size_t>(range.offset), totalCount);
auto endOffset = std::min(static_cast<size_t>(range.endOffset()), totalCount);

std::vector<LibraryNodeInfo> result;

if (offset < endOffset)
{
result.reserve(endOffset - offset);

for (size_t i = offset; i < endOffset; i++)
{
if (handles[i].is_valid())
nodes[i].columns = evaluateItemColumns(handles[i], queryImpl->columns, &buffer);

result.emplace_back(std::move(nodes[i]));
}
}

LibraryGroupsResult groupsResult(
static_cast<int32_t>(offset),
static_cast<int32_t>(totalCount),
std::move(result));

groupsResult.group = groupPath;

if (depth > 0)
{
groupsResult.hasParent = true;
groupsResult.parentGroup = joinLevels(current.begin(), current.end() - 1);
}

return groupsResult;
}

}
}
Loading