Files
allstarr/allstarr/Services/Common/FuzzyMatcher.cs
Josh Patra e18840cddf feat: Fork octo-fiestarr as allstarr with Jellyfin proxy improvements
Major changes:
- Rename project from octo-fiesta to allstarr
- Add Jellyfin proxy support alongside Subsonic/Navidrome
- Implement fuzzy search with relevance scoring and Levenshtein distance
- Add POST body logging for debugging playback progress issues
- Separate local and external artists in search results
- Add +5 score boost for external results to prioritize larger catalog(probably gonna reverse it)
- Create FuzzyMatcher utility for intelligent search result scoring
- Add ConvertPlaylistToJellyfinItem method for playlist support
- Rename keys folder to apis and update gitignore
- Filter search results by relevance score (>= 40)
- Add Redis caching support with configurable settings
- Update environment configuration with backend selection
- Improve external provider integration (SquidWTF, Deezer, Qobuz)
- Add tests for all services
2026-01-29 17:36:53 -05:00

105 lines
2.8 KiB
C#

namespace allstarr.Services.Common;
/// <summary>
/// Provides fuzzy string matching for search result scoring.
/// </summary>
public static class FuzzyMatcher
{
/// <summary>
/// Calculates a similarity score between two strings (0-100).
/// Higher score means better match.
/// </summary>
public static int CalculateSimilarity(string query, string target)
{
if (string.IsNullOrWhiteSpace(query) || string.IsNullOrWhiteSpace(target))
{
return 0;
}
var queryLower = query.ToLowerInvariant().Trim();
var targetLower = target.ToLowerInvariant().Trim();
// Exact match
if (queryLower == targetLower)
{
return 100;
}
// Starts with query
if (targetLower.StartsWith(queryLower))
{
return 90;
}
// Contains query as whole word
if (targetLower.Contains($" {queryLower} ") ||
targetLower.StartsWith($"{queryLower} ") ||
targetLower.EndsWith($" {queryLower}"))
{
return 80;
}
// Contains query anywhere
if (targetLower.Contains(queryLower))
{
return 70;
}
// Calculate Levenshtein distance for fuzzy matching
var distance = LevenshteinDistance(queryLower, targetLower);
var maxLength = Math.Max(queryLower.Length, targetLower.Length);
if (maxLength == 0)
{
return 100;
}
// Convert distance to similarity score (0-60 range for fuzzy matches)
var similarity = (1.0 - (double)distance / maxLength) * 60;
return (int)Math.Max(0, similarity);
}
/// <summary>
/// Calculates Levenshtein distance between two strings.
/// </summary>
private static int LevenshteinDistance(string source, string target)
{
if (string.IsNullOrEmpty(source))
{
return target?.Length ?? 0;
}
if (string.IsNullOrEmpty(target))
{
return source.Length;
}
var sourceLength = source.Length;
var targetLength = target.Length;
var distance = new int[sourceLength + 1, targetLength + 1];
for (var i = 0; i <= sourceLength; i++)
{
distance[i, 0] = i;
}
for (var j = 0; j <= targetLength; j++)
{
distance[0, j] = j;
}
for (var i = 1; i <= sourceLength; i++)
{
for (var j = 1; j <= targetLength; j++)
{
var cost = target[j - 1] == source[i - 1] ? 0 : 1;
distance[i, j] = Math.Min(
Math.Min(distance[i - 1, j] + 1, distance[i, j - 1] + 1),
distance[i - 1, j - 1] + cost);
}
}
return distance[sourceLength, targetLength];
}
}