mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
fix: improve auth, search, and stability
This commit is contained in:
@@ -169,31 +169,28 @@ public class JellyfinController : ControllerBase
|
|||||||
var (localSongs, localAlbums, localArtists) = _modelMapper.ParseItemsResponse(jellyfinResult);
|
var (localSongs, localAlbums, localArtists) = _modelMapper.ParseItemsResponse(jellyfinResult);
|
||||||
|
|
||||||
// Score and filter Jellyfin results by relevance
|
// Score and filter Jellyfin results by relevance
|
||||||
var scoredLocalSongs = ScoreSearchResults(cleanQuery, localSongs, s => s.Title, s => s.Artist, isExternal: false);
|
var scoredLocalSongs = ScoreSearchResults(cleanQuery, localSongs, s => s.Title, s => s.Artist, s => s.Album, isExternal: false);
|
||||||
var scoredLocalAlbums = ScoreSearchResults(cleanQuery, localAlbums, a => a.Title, a => a.Artist, isExternal: false);
|
var scoredLocalAlbums = ScoreSearchResults(cleanQuery, localAlbums, a => a.Title, a => a.Artist, _ => null, isExternal: false);
|
||||||
var scoredLocalArtists = ScoreSearchResults(cleanQuery, localArtists, a => a.Name, _ => null, isExternal: false);
|
var scoredLocalArtists = ScoreSearchResults(cleanQuery, localArtists, a => a.Name, _ => null, _ => null, isExternal: false);
|
||||||
|
|
||||||
// Score external results with a small boost
|
// Score external results with a small boost
|
||||||
var scoredExternalSongs = ScoreSearchResults(cleanQuery, externalResult.Songs, s => s.Title, s => s.Artist, isExternal: true);
|
var scoredExternalSongs = ScoreSearchResults(cleanQuery, externalResult.Songs, s => s.Title, s => s.Artist, s => s.Album, isExternal: true);
|
||||||
var scoredExternalAlbums = ScoreSearchResults(cleanQuery, externalResult.Albums, a => a.Title, a => a.Artist, isExternal: true);
|
var scoredExternalAlbums = ScoreSearchResults(cleanQuery, externalResult.Albums, a => a.Title, a => a.Artist, _ => null, isExternal: true);
|
||||||
var scoredExternalArtists = ScoreSearchResults(cleanQuery, externalResult.Artists, a => a.Name, _ => null, isExternal: true);
|
var scoredExternalArtists = ScoreSearchResults(cleanQuery, externalResult.Artists, a => a.Name, _ => null, _ => null, isExternal: true);
|
||||||
|
|
||||||
// Merge and sort by score (only include items with score >= 40)
|
// Merge and sort by score (no filtering - just reorder by relevance)
|
||||||
var allSongs = scoredLocalSongs.Concat(scoredExternalSongs)
|
var allSongs = scoredLocalSongs.Concat(scoredExternalSongs)
|
||||||
.Where(x => x.Score >= 40)
|
|
||||||
.OrderByDescending(x => x.Score)
|
.OrderByDescending(x => x.Score)
|
||||||
.Select(x => x.Item)
|
.Select(x => x.Item)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var allAlbums = scoredLocalAlbums.Concat(scoredExternalAlbums)
|
var allAlbums = scoredLocalAlbums.Concat(scoredExternalAlbums)
|
||||||
.Where(x => x.Score >= 40)
|
|
||||||
.OrderByDescending(x => x.Score)
|
.OrderByDescending(x => x.Score)
|
||||||
.Select(x => x.Item)
|
.Select(x => x.Item)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Dedupe artists by name, keeping highest scored version
|
// Dedupe artists by name, keeping highest scored version
|
||||||
var artistScores = scoredLocalArtists.Concat(scoredExternalArtists)
|
var artistScores = scoredLocalArtists.Concat(scoredExternalArtists)
|
||||||
.Where(x => x.Score >= 40)
|
|
||||||
.GroupBy(x => x.Item.Name, StringComparer.OrdinalIgnoreCase)
|
.GroupBy(x => x.Item.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
.Select(g => g.OrderByDescending(x => x.Score).First())
|
.Select(g => g.OrderByDescending(x => x.Score).First())
|
||||||
.OrderByDescending(x => x.Score)
|
.OrderByDescending(x => x.Score)
|
||||||
@@ -210,7 +207,6 @@ public class JellyfinController : ControllerBase
|
|||||||
{
|
{
|
||||||
var scoredPlaylists = playlistResult
|
var scoredPlaylists = playlistResult
|
||||||
.Select(p => new { Playlist = p, Score = FuzzyMatcher.CalculateSimilarity(cleanQuery, p.Name) })
|
.Select(p => new { Playlist = p, Score = FuzzyMatcher.CalculateSimilarity(cleanQuery, p.Name) })
|
||||||
.Where(x => x.Score >= 40)
|
|
||||||
.OrderByDescending(x => x.Score)
|
.OrderByDescending(x => x.Score)
|
||||||
.Select(x => _responseBuilder.ConvertPlaylistToJellyfinItem(x.Playlist))
|
.Select(x => _responseBuilder.ConvertPlaylistToJellyfinItem(x.Playlist))
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -778,6 +774,23 @@ public class JellyfinController : ControllerBase
|
|||||||
|
|
||||||
var contentType = response.Content.Headers.ContentType?.ToString() ?? "audio/mpeg";
|
var contentType = response.Content.Headers.ContentType?.ToString() ?? "audio/mpeg";
|
||||||
|
|
||||||
|
// Forward caching headers for client-side caching
|
||||||
|
if (response.Headers.ETag != null)
|
||||||
|
{
|
||||||
|
Response.Headers["ETag"] = response.Headers.ETag.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.Content.Headers.LastModified.HasValue)
|
||||||
|
{
|
||||||
|
Response.Headers["Last-Modified"] = response.Content.Headers.LastModified.Value.ToString("R");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.Headers.CacheControl != null)
|
||||||
|
{
|
||||||
|
Response.Headers["Cache-Control"] = response.Headers.CacheControl.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward range headers for seeking
|
||||||
if (response.Content.Headers.ContentRange != null)
|
if (response.Content.Headers.ContentRange != null)
|
||||||
{
|
{
|
||||||
Response.Headers["Content-Range"] = response.Content.Headers.ContentRange.ToString();
|
Response.Headers["Content-Range"] = response.Content.Headers.ContentRange.ToString();
|
||||||
@@ -1761,28 +1774,52 @@ public class JellyfinController : ControllerBase
|
|||||||
private static List<(T Item, int Score)> ScoreSearchResults<T>(
|
private static List<(T Item, int Score)> ScoreSearchResults<T>(
|
||||||
string query,
|
string query,
|
||||||
List<T> items,
|
List<T> items,
|
||||||
Func<T, string> primaryField,
|
Func<T, string> titleField,
|
||||||
Func<T, string?> secondaryField,
|
Func<T, string?> artistField,
|
||||||
|
Func<T, string?> albumField,
|
||||||
bool isExternal = false)
|
bool isExternal = false)
|
||||||
{
|
{
|
||||||
return items.Select(item =>
|
return items.Select(item =>
|
||||||
{
|
{
|
||||||
var primary = primaryField(item) ?? "";
|
var title = titleField(item) ?? "";
|
||||||
var secondary = secondaryField(item) ?? "";
|
var artist = artistField(item) ?? "";
|
||||||
|
var album = albumField(item) ?? "";
|
||||||
|
|
||||||
// Score against primary field (title/name)
|
// Token-based fuzzy matching: split query and fields into words
|
||||||
var primaryScore = FuzzyMatcher.CalculateSimilarity(query, primary);
|
var queryTokens = query.ToLower()
|
||||||
|
.Split(new[] { ' ', '-', '_' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
// Score against secondary field (artist) if provided
|
var fieldText = $"{title} {artist} {album}".ToLower();
|
||||||
var secondaryScore = string.IsNullOrEmpty(secondary)
|
var fieldTokens = fieldText
|
||||||
? 0
|
.Split(new[] { ' ', '-', '_' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
: FuzzyMatcher.CalculateSimilarity(query, secondary);
|
.ToList();
|
||||||
|
|
||||||
// Use the better of the two scores
|
if (queryTokens.Count == 0) return (item, 0);
|
||||||
var baseScore = Math.Max(primaryScore, secondaryScore);
|
|
||||||
|
// Count how many query tokens match field tokens (with fuzzy tolerance)
|
||||||
|
var matchedTokens = 0;
|
||||||
|
foreach (var queryToken in queryTokens)
|
||||||
|
{
|
||||||
|
// Check if any field token matches this query token
|
||||||
|
var hasMatch = fieldTokens.Any(fieldToken =>
|
||||||
|
{
|
||||||
|
// Exact match or substring match
|
||||||
|
if (fieldToken.Contains(queryToken) || queryToken.Contains(fieldToken))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Fuzzy match with Levenshtein distance
|
||||||
|
var similarity = FuzzyMatcher.CalculateSimilarity(queryToken, fieldToken);
|
||||||
|
return similarity >= 70; // 70% similarity threshold for individual words
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasMatch) matchedTokens++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score = percentage of query tokens that matched
|
||||||
|
var baseScore = (matchedTokens * 100) / queryTokens.Count;
|
||||||
|
|
||||||
// Give external results a small boost (+5 points) to prioritize the larger catalog
|
// Give external results a small boost (+5 points) to prioritize the larger catalog
|
||||||
// This means external results will rank slightly higher when scores are close
|
|
||||||
var finalScore = isExternal ? Math.Min(100, baseScore + 5) : baseScore;
|
var finalScore = isExternal ? Math.Min(100, baseScore + 5) : baseScore;
|
||||||
|
|
||||||
return (item, finalScore);
|
return (item, finalScore);
|
||||||
|
|||||||
@@ -297,8 +297,10 @@ public class JellyfinProxyService
|
|||||||
{
|
{
|
||||||
if (header.Key.Equals("X-Emby-Authorization", StringComparison.OrdinalIgnoreCase))
|
if (header.Key.Equals("X-Emby-Authorization", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
request.Headers.TryAddWithoutValidation("X-Emby-Authorization", header.Value.ToString());
|
var headerValue = header.Value.ToString();
|
||||||
|
request.Headers.TryAddWithoutValidation("X-Emby-Authorization", headerValue);
|
||||||
authHeaderAdded = true;
|
authHeaderAdded = true;
|
||||||
|
_logger.LogDebug("Forwarded X-Emby-Authorization from client");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,21 +311,38 @@ public class JellyfinProxyService
|
|||||||
{
|
{
|
||||||
if (header.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase))
|
if (header.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
request.Headers.TryAddWithoutValidation("Authorization", header.Value.ToString());
|
var headerValue = header.Value.ToString();
|
||||||
|
|
||||||
|
// Check if it's MediaBrowser/Jellyfin format
|
||||||
|
if (headerValue.Contains("MediaBrowser", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
headerValue.Contains("Client=", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Forward as X-Emby-Authorization
|
||||||
|
request.Headers.TryAddWithoutValidation("X-Emby-Authorization", headerValue);
|
||||||
|
_logger.LogDebug("Converted Authorization to X-Emby-Authorization");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Standard Bearer token
|
||||||
|
request.Headers.TryAddWithoutValidation("Authorization", headerValue);
|
||||||
|
_logger.LogDebug("Forwarded Authorization header");
|
||||||
|
}
|
||||||
authHeaderAdded = true;
|
authHeaderAdded = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For login requests without auth headers, provide a minimal client auth header
|
// For non-auth requests without headers, use API key
|
||||||
if (!authHeaderAdded)
|
// For auth requests, client MUST provide their own client info
|
||||||
|
if (!authHeaderAdded && !endpoint.Contains("Authenticate", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var clientAuthHeader = $"MediaBrowser Client=\"{_settings.ClientName}\", " +
|
var clientAuthHeader = $"MediaBrowser Client=\"{_settings.ClientName}\", " +
|
||||||
$"Device=\"{_settings.DeviceName}\", " +
|
$"Device=\"{_settings.DeviceName}\", " +
|
||||||
$"DeviceId=\"{_settings.DeviceId}\", " +
|
$"DeviceId=\"{_settings.DeviceId}\", " +
|
||||||
$"Version=\"{_settings.ClientVersion}\"";
|
$"Version=\"{_settings.ClientVersion}\"";
|
||||||
request.Headers.TryAddWithoutValidation("X-Emby-Authorization", clientAuthHeader);
|
request.Headers.TryAddWithoutValidation("X-Emby-Authorization", clientAuthHeader);
|
||||||
|
_logger.LogDebug("Using server API key for non-auth request");
|
||||||
}
|
}
|
||||||
|
|
||||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
|||||||
@@ -304,11 +304,11 @@ public class JellyfinResponseBuilder
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<string, object?> ConvertAlbumToJellyfinItem(Album album)
|
public Dictionary<string, object?> ConvertAlbumToJellyfinItem(Album album)
|
||||||
{
|
{
|
||||||
// Add " - SW" suffix to external album names
|
// Add " - S" suffix to external album names (S = SquidWTF)
|
||||||
var albumName = album.Title;
|
var albumName = album.Title;
|
||||||
if (!album.IsLocal)
|
if (!album.IsLocal)
|
||||||
{
|
{
|
||||||
albumName = $"{album.Title} - SW";
|
albumName = $"{album.Title} - S";
|
||||||
}
|
}
|
||||||
|
|
||||||
var item = new Dictionary<string, object?>
|
var item = new Dictionary<string, object?>
|
||||||
@@ -371,11 +371,11 @@ public class JellyfinResponseBuilder
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<string, object?> ConvertArtistToJellyfinItem(Artist artist)
|
public Dictionary<string, object?> ConvertArtistToJellyfinItem(Artist artist)
|
||||||
{
|
{
|
||||||
// Add " - SW" suffix to external artist names
|
// Add " - S" suffix to external artist names (S = SquidWTF)
|
||||||
var artistName = artist.Name;
|
var artistName = artist.Name;
|
||||||
if (!artist.IsLocal)
|
if (!artist.IsLocal)
|
||||||
{
|
{
|
||||||
artistName = $"{artist.Name} - SW";
|
artistName = $"{artist.Name} - S";
|
||||||
}
|
}
|
||||||
|
|
||||||
var item = new Dictionary<string, object?>
|
var item = new Dictionary<string, object?>
|
||||||
|
|||||||
@@ -188,7 +188,15 @@ public class SquidWTFMetadataService : IMusicMetadataService
|
|||||||
{
|
{
|
||||||
foreach(var playlist in items.EnumerateArray())
|
foreach(var playlist in items.EnumerateArray())
|
||||||
{
|
{
|
||||||
playlists.Add(ParseTidalPlaylist(playlist));
|
try
|
||||||
|
{
|
||||||
|
playlists.Add(ParseTidalPlaylist(playlist));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Failed to parse playlist, skipping");
|
||||||
|
// Skip this playlist and continue with others
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return playlists;
|
return playlists;
|
||||||
|
|||||||
Reference in New Issue
Block a user