mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Compare commits
8 Commits
67b4fac64c
...
6f91361966
| Author | SHA1 | Date | |
|---|---|---|---|
|
6f91361966
|
|||
|
d4036095f1
|
|||
|
6620b39357
|
|||
|
dcaa89171a
|
|||
|
1889dc6e19
|
|||
|
615ad58bc6
|
|||
|
6176777d0f
|
|||
|
a339574f05
|
@@ -169,31 +169,28 @@ public class JellyfinController : ControllerBase
|
||||
var (localSongs, localAlbums, localArtists) = _modelMapper.ParseItemsResponse(jellyfinResult);
|
||||
|
||||
// Score and filter Jellyfin results by relevance
|
||||
var scoredLocalSongs = ScoreSearchResults(cleanQuery, localSongs, s => s.Title, s => s.Artist, isExternal: false);
|
||||
var scoredLocalAlbums = ScoreSearchResults(cleanQuery, localAlbums, a => a.Title, a => a.Artist, isExternal: false);
|
||||
var scoredLocalArtists = ScoreSearchResults(cleanQuery, localArtists, a => a.Name, _ => null, 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, _ => null, isExternal: false);
|
||||
var scoredLocalArtists = ScoreSearchResults(cleanQuery, localArtists, a => a.Name, _ => null, _ => null, isExternal: false);
|
||||
|
||||
// Score external results with a small boost
|
||||
var scoredExternalSongs = ScoreSearchResults(cleanQuery, externalResult.Songs, s => s.Title, s => s.Artist, isExternal: true);
|
||||
var scoredExternalAlbums = ScoreSearchResults(cleanQuery, externalResult.Albums, a => a.Title, a => a.Artist, isExternal: true);
|
||||
var scoredExternalArtists = ScoreSearchResults(cleanQuery, externalResult.Artists, a => a.Name, _ => null, 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, _ => 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)
|
||||
.Where(x => x.Score >= 40)
|
||||
.OrderByDescending(x => x.Score)
|
||||
.Select(x => x.Item)
|
||||
.ToList();
|
||||
|
||||
var allAlbums = scoredLocalAlbums.Concat(scoredExternalAlbums)
|
||||
.Where(x => x.Score >= 40)
|
||||
.OrderByDescending(x => x.Score)
|
||||
.Select(x => x.Item)
|
||||
.ToList();
|
||||
|
||||
// Dedupe artists by name, keeping highest scored version
|
||||
var artistScores = scoredLocalArtists.Concat(scoredExternalArtists)
|
||||
.Where(x => x.Score >= 40)
|
||||
.GroupBy(x => x.Item.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => g.OrderByDescending(x => x.Score).First())
|
||||
.OrderByDescending(x => x.Score)
|
||||
@@ -210,7 +207,6 @@ public class JellyfinController : ControllerBase
|
||||
{
|
||||
var scoredPlaylists = playlistResult
|
||||
.Select(p => new { Playlist = p, Score = FuzzyMatcher.CalculateSimilarity(cleanQuery, p.Name) })
|
||||
.Where(x => x.Score >= 40)
|
||||
.OrderByDescending(x => x.Score)
|
||||
.Select(x => _responseBuilder.ConvertPlaylistToJellyfinItem(x.Playlist))
|
||||
.ToList();
|
||||
@@ -778,6 +774,23 @@ public class JellyfinController : ControllerBase
|
||||
|
||||
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)
|
||||
{
|
||||
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>(
|
||||
string query,
|
||||
List<T> items,
|
||||
Func<T, string> primaryField,
|
||||
Func<T, string?> secondaryField,
|
||||
Func<T, string> titleField,
|
||||
Func<T, string?> artistField,
|
||||
Func<T, string?> albumField,
|
||||
bool isExternal = false)
|
||||
{
|
||||
return items.Select(item =>
|
||||
{
|
||||
var primary = primaryField(item) ?? "";
|
||||
var secondary = secondaryField(item) ?? "";
|
||||
var title = titleField(item) ?? "";
|
||||
var artist = artistField(item) ?? "";
|
||||
var album = albumField(item) ?? "";
|
||||
|
||||
// Score against primary field (title/name)
|
||||
var primaryScore = FuzzyMatcher.CalculateSimilarity(query, primary);
|
||||
// Token-based fuzzy matching: split query and fields into words
|
||||
var queryTokens = query.ToLower()
|
||||
.Split(new[] { ' ', '-', '_' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.ToList();
|
||||
|
||||
// Score against secondary field (artist) if provided
|
||||
var secondaryScore = string.IsNullOrEmpty(secondary)
|
||||
? 0
|
||||
: FuzzyMatcher.CalculateSimilarity(query, secondary);
|
||||
var fieldText = $"{title} {artist} {album}".ToLower();
|
||||
var fieldTokens = fieldText
|
||||
.Split(new[] { ' ', '-', '_' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.ToList();
|
||||
|
||||
// Use the better of the two scores
|
||||
var baseScore = Math.Max(primaryScore, secondaryScore);
|
||||
if (queryTokens.Count == 0) return (item, 0);
|
||||
|
||||
// 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
|
||||
// This means external results will rank slightly higher when scores are close
|
||||
var finalScore = isExternal ? Math.Min(100, baseScore + 5) : baseScore;
|
||||
|
||||
return (item, finalScore);
|
||||
|
||||
@@ -297,8 +297,10 @@ public class JellyfinProxyService
|
||||
{
|
||||
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;
|
||||
_logger.LogDebug("Forwarded X-Emby-Authorization from client");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -309,21 +311,38 @@ public class JellyfinProxyService
|
||||
{
|
||||
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;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For login requests without auth headers, provide a minimal client auth header
|
||||
if (!authHeaderAdded)
|
||||
// For non-auth requests without headers, use API key
|
||||
// For auth requests, client MUST provide their own client info
|
||||
if (!authHeaderAdded && !endpoint.Contains("Authenticate", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var clientAuthHeader = $"MediaBrowser Client=\"{_settings.ClientName}\", " +
|
||||
$"Device=\"{_settings.DeviceName}\", " +
|
||||
$"DeviceId=\"{_settings.DeviceId}\", " +
|
||||
$"Version=\"{_settings.ClientVersion}\"";
|
||||
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"));
|
||||
|
||||
@@ -304,11 +304,11 @@ public class JellyfinResponseBuilder
|
||||
/// </summary>
|
||||
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;
|
||||
if (!album.IsLocal)
|
||||
{
|
||||
albumName = $"{album.Title} - SW";
|
||||
albumName = $"{album.Title} - S";
|
||||
}
|
||||
|
||||
var item = new Dictionary<string, object?>
|
||||
@@ -371,11 +371,11 @@ public class JellyfinResponseBuilder
|
||||
/// </summary>
|
||||
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;
|
||||
if (!artist.IsLocal)
|
||||
{
|
||||
artistName = $"{artist.Name} - SW";
|
||||
artistName = $"{artist.Name} - S";
|
||||
}
|
||||
|
||||
var item = new Dictionary<string, object?>
|
||||
|
||||
@@ -187,9 +187,17 @@ public class SquidWTFMetadataService : IMusicMetadataService
|
||||
playlistObj.TryGetProperty("items", out var items))
|
||||
{
|
||||
foreach(var playlist in items.EnumerateArray())
|
||||
{
|
||||
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;
|
||||
}, new List<ExternalPlaylist>());
|
||||
|
||||
Reference in New Issue
Block a user