Compare commits

...

2 Commits

Author SHA1 Message Date
fa9739bfaa docs: update README
Some checks failed
Docker Build & Push / build-and-test (push) Has been cancelled
Docker Build & Push / docker (push) Has been cancelled
2026-01-31 11:16:00 -05:00
0ba51e2b30 fix: improve auth, search, and stability
Some checks failed
Docker Build & Push / build-and-test (push) Has been cancelled
Docker Build & Push / docker (push) Has been cancelled
2026-01-31 01:14:53 -05:00
5 changed files with 105 additions and 36 deletions

View File

@@ -16,13 +16,18 @@ Please report all bugs as soon as possible, as the Jellyfin addition is entirely
Using Docker (recommended):
```bash
# 1. Pull the latest image
docker-compose pull
# 1. Download the docker-compose.yml file and the .env.example file to a folder on the machine you have Docker
curl -O https://raw.githubusercontent.com/SoPat712/allstarr/refs/heads/main/docker-compose.yml \
-O https://raw.githubusercontent.com/SoPat712/allstarr/refs/heads/main/.env.example
# 2. Configure environment
cp .env.example .env
vi .env # Edit with your settings
# 3. Pull the latest image
docker-compose pull
# 3. Start services
docker-compose up -d
@@ -35,7 +40,7 @@ The proxy will be available at `http://localhost:5274`.
### Nginx Proxy Setup (Required)
This service only exposes ports internally. You **must** use nginx to proxy to it:
This service only exposes ports internally. You can use nginx to proxy to it, however PLEASE take significant precautions before exposing this! Everyone decides their own level of risk, but this is currently untested, potentially dangerous software, with almost unfettered access to your Jellyfin server. My recommendation is use Tailscale or something similar!
```nginx
server {

View File

@@ -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);

View File

@@ -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"));

View File

@@ -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?>

View File

@@ -188,7 +188,15 @@ public class SquidWTFMetadataService : IMusicMetadataService
{
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;