mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
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
249 lines
8.2 KiB
C#
249 lines
8.2 KiB
C#
using allstarr.Services.Local;
|
|
using allstarr.Models.Domain;
|
|
using allstarr.Models.Settings;
|
|
using allstarr.Models.Download;
|
|
using allstarr.Models.Search;
|
|
using allstarr.Models.Subsonic;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Moq;
|
|
using Moq.Protected;
|
|
using System.Net;
|
|
|
|
namespace allstarr.Tests;
|
|
|
|
public class LocalLibraryServiceTests : IDisposable
|
|
{
|
|
private readonly LocalLibraryService _service;
|
|
private readonly string _testDownloadPath;
|
|
private readonly Mock<IHttpClientFactory> _mockHttpClientFactory;
|
|
|
|
public LocalLibraryServiceTests()
|
|
{
|
|
_testDownloadPath = Path.Combine(Path.GetTempPath(), "allstarr-tests-" + Guid.NewGuid());
|
|
Directory.CreateDirectory(_testDownloadPath);
|
|
|
|
var configuration = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["Library:DownloadPath"] = _testDownloadPath
|
|
})
|
|
.Build();
|
|
|
|
// Mock HttpClient
|
|
var mockHandler = new Mock<HttpMessageHandler>();
|
|
mockHandler.Protected()
|
|
.Setup<Task<HttpResponseMessage>>("SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)
|
|
{
|
|
Content = new StringContent("{\"subsonic-response\":{\"status\":\"ok\",\"scanStatus\":{\"scanning\":false,\"count\":100}}}")
|
|
});
|
|
|
|
var httpClient = new HttpClient(mockHandler.Object);
|
|
_mockHttpClientFactory = new Mock<IHttpClientFactory>();
|
|
_mockHttpClientFactory.Setup(x => x.CreateClient(It.IsAny<string>())).Returns(httpClient);
|
|
|
|
var subsonicSettings = Options.Create(new SubsonicSettings { Url = "http://localhost:4533" });
|
|
var mockLogger = new Mock<ILogger<LocalLibraryService>>();
|
|
|
|
_service = new LocalLibraryService(configuration, _mockHttpClientFactory.Object, subsonicSettings, mockLogger.Object);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (Directory.Exists(_testDownloadPath))
|
|
{
|
|
Directory.Delete(_testDownloadPath, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseSongId_WithExternalId_ReturnsCorrectParts()
|
|
{
|
|
// Act
|
|
var (isExternal, provider, externalId) = _service.ParseSongId("ext-deezer-123456");
|
|
|
|
// Assert
|
|
Assert.True(isExternal);
|
|
Assert.Equal("deezer", provider);
|
|
Assert.Equal("123456", externalId);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseSongId_WithLocalId_ReturnsNotExternal()
|
|
{
|
|
// Act
|
|
var (isExternal, provider, externalId) = _service.ParseSongId("local-789");
|
|
|
|
// Assert
|
|
Assert.False(isExternal);
|
|
Assert.Null(provider);
|
|
Assert.Null(externalId);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseSongId_WithNumericId_ReturnsNotExternal()
|
|
{
|
|
// Act
|
|
var (isExternal, provider, externalId) = _service.ParseSongId("12345");
|
|
|
|
// Assert
|
|
Assert.False(isExternal);
|
|
Assert.Null(provider);
|
|
Assert.Null(externalId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetLocalPathForExternalSongAsync_WhenNotRegistered_ReturnsNull()
|
|
{
|
|
// Act
|
|
var result = await _service.GetLocalPathForExternalSongAsync("deezer", "nonexistent");
|
|
|
|
// Assert
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RegisterDownloadedSongAsync_ThenGetLocalPath_ReturnsPath()
|
|
{
|
|
// Arrange
|
|
var song = new Song
|
|
{
|
|
Id = "ext-deezer-123456",
|
|
Title = "Test Song",
|
|
Artist = "Test Artist",
|
|
Album = "Test Album",
|
|
ExternalProvider = "deezer",
|
|
ExternalId = "123456"
|
|
};
|
|
var localPath = Path.Combine(_testDownloadPath, "test-song.mp3");
|
|
|
|
// Create the file
|
|
await File.WriteAllTextAsync(localPath, "fake audio content");
|
|
|
|
// Act
|
|
await _service.RegisterDownloadedSongAsync(song, localPath);
|
|
var result = await _service.GetLocalPathForExternalSongAsync("deezer", "123456");
|
|
|
|
// Assert
|
|
Assert.Equal(localPath, result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetLocalPathForExternalSongAsync_WhenFileDeleted_ReturnsNull()
|
|
{
|
|
// Arrange
|
|
var song = new Song
|
|
{
|
|
Id = "ext-deezer-999999",
|
|
Title = "Deleted Song",
|
|
Artist = "Test Artist",
|
|
Album = "Test Album",
|
|
ExternalProvider = "deezer",
|
|
ExternalId = "999999"
|
|
};
|
|
var localPath = Path.Combine(_testDownloadPath, "deleted-song.mp3");
|
|
|
|
// Create and then delete the file
|
|
await File.WriteAllTextAsync(localPath, "fake audio content");
|
|
await _service.RegisterDownloadedSongAsync(song, localPath);
|
|
File.Delete(localPath);
|
|
|
|
// Act
|
|
var result = await _service.GetLocalPathForExternalSongAsync("deezer", "999999");
|
|
|
|
// Assert
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RegisterDownloadedSongAsync_WithNullProvider_DoesNothing()
|
|
{
|
|
// Arrange
|
|
var song = new Song
|
|
{
|
|
Id = "local-123",
|
|
Title = "Local Song",
|
|
Artist = "Local Artist",
|
|
Album = "Local Album",
|
|
ExternalProvider = null,
|
|
ExternalId = null
|
|
};
|
|
var localPath = Path.Combine(_testDownloadPath, "local-song.mp3");
|
|
|
|
// Act - should not throw
|
|
await _service.RegisterDownloadedSongAsync(song, localPath);
|
|
|
|
// Assert - nothing to assert, just checking it doesn't throw
|
|
Assert.True(true);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TriggerLibraryScanAsync_ReturnsTrue()
|
|
{
|
|
// Act
|
|
var result = await _service.TriggerLibraryScanAsync();
|
|
|
|
// Assert
|
|
Assert.True(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetScanStatusAsync_ReturnsScanStatus()
|
|
{
|
|
// Act
|
|
var result = await _service.GetScanStatusAsync();
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.False(result.Scanning);
|
|
Assert.Equal(100, result.Count);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("ext-deezer-123", true, "deezer", "123")]
|
|
[InlineData("ext-spotify-abc123", true, "spotify", "abc123")]
|
|
[InlineData("ext-tidal-999-888", true, "tidal", "999-888")]
|
|
[InlineData("ext-deezer-song-123456", true, "deezer", "123456")] // New format - extracts numeric ID
|
|
[InlineData("123456", false, null, null)]
|
|
[InlineData("", false, null, null)]
|
|
[InlineData("ext-", false, null, null)]
|
|
[InlineData("ext-deezer", false, null, null)]
|
|
public void ParseSongId_VariousInputs_ReturnsExpected(string songId, bool expectedIsExternal, string? expectedProvider, string? expectedExternalId)
|
|
{
|
|
// Act
|
|
var (isExternal, provider, externalId) = _service.ParseSongId(songId);
|
|
|
|
// Assert
|
|
Assert.Equal(expectedIsExternal, isExternal);
|
|
Assert.Equal(expectedProvider, provider);
|
|
Assert.Equal(expectedExternalId, externalId);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("ext-deezer-song-123456", true, "deezer", "song", "123456")]
|
|
[InlineData("ext-deezer-album-789012", true, "deezer", "album", "789012")]
|
|
[InlineData("ext-deezer-artist-259", true, "deezer", "artist", "259")]
|
|
[InlineData("ext-spotify-song-abc123", true, "spotify", "song", "abc123")]
|
|
[InlineData("ext-deezer-123", true, "deezer", "song", "123")] // Legacy format defaults to song
|
|
[InlineData("ext-tidal-999", true, "tidal", "song", "999")] // Legacy format defaults to song
|
|
[InlineData("123456", false, null, null, null)]
|
|
[InlineData("", false, null, null, null)]
|
|
[InlineData("ext-", false, null, null, null)]
|
|
[InlineData("ext-deezer", false, null, null, null)]
|
|
public void ParseExternalId_VariousInputs_ReturnsExpected(string id, bool expectedIsExternal, string? expectedProvider, string? expectedType, string? expectedExternalId)
|
|
{
|
|
// Act
|
|
var (isExternal, provider, type, externalId) = _service.ParseExternalId(id);
|
|
|
|
// Assert
|
|
Assert.Equal(expectedIsExternal, isExternal);
|
|
Assert.Equal(expectedProvider, provider);
|
|
Assert.Equal(expectedType, type);
|
|
Assert.Equal(expectedExternalId, externalId);
|
|
}
|
|
}
|