Files
allstarr/octo-fiesta/Services/Subsonic/SubsonicProxyService.cs
V1ck3s 9245dac99e refactor: extract subsonic controller logic into specialized services
- Extract SubsonicRequestParser for HTTP parameter extraction
- Extract SubsonicResponseBuilder for XML/JSON response formatting
- Extract SubsonicModelMapper for search result parsing and merging
- Extract SubsonicProxyService for upstream Subsonic server communication
- Add comprehensive test coverage (45 tests) for all new services
- Reduce SubsonicController from 1174 to 666 lines (-43%)

All tests passing. Build succeeds with 0 errors.
2026-01-08 21:47:05 +01:00

101 lines
3.4 KiB
C#

using Microsoft.AspNetCore.Mvc;
using octo_fiesta.Models.Settings;
namespace octo_fiesta.Services.Subsonic;
/// <summary>
/// Handles proxying requests to the underlying Subsonic server.
/// </summary>
public class SubsonicProxyService
{
private readonly HttpClient _httpClient;
private readonly SubsonicSettings _subsonicSettings;
public SubsonicProxyService(
IHttpClientFactory httpClientFactory,
Microsoft.Extensions.Options.IOptions<SubsonicSettings> subsonicSettings)
{
_httpClient = httpClientFactory.CreateClient();
_subsonicSettings = subsonicSettings.Value;
}
/// <summary>
/// Relays a request to the Subsonic server and returns the response.
/// </summary>
public async Task<(byte[] Body, string? ContentType)> RelayAsync(
string endpoint,
Dictionary<string, string> parameters)
{
var query = string.Join("&", parameters.Select(kv =>
$"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}"));
var url = $"{_subsonicSettings.Url}/{endpoint}?{query}";
HttpResponseMessage response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsByteArrayAsync();
var contentType = response.Content.Headers.ContentType?.ToString();
return (body, contentType);
}
/// <summary>
/// Safely relays a request to the Subsonic server, returning null on failure.
/// </summary>
public async Task<(byte[]? Body, string? ContentType, bool Success)> RelaySafeAsync(
string endpoint,
Dictionary<string, string> parameters)
{
try
{
var result = await RelayAsync(endpoint, parameters);
return (result.Body, result.ContentType, true);
}
catch
{
return (null, null, false);
}
}
/// <summary>
/// Relays a stream request to the Subsonic server with range processing support.
/// </summary>
public async Task<IActionResult> RelayStreamAsync(
Dictionary<string, string> parameters,
CancellationToken cancellationToken)
{
try
{
var query = string.Join("&", parameters.Select(kv =>
$"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}"));
var url = $"{_subsonicSettings.Url}/rest/stream?{query}";
using var request = new HttpRequestMessage(HttpMethod.Get, url);
var response = await _httpClient.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
if (!response.IsSuccessStatusCode)
{
return new StatusCodeResult((int)response.StatusCode);
}
var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
var contentType = response.Content.Headers.ContentType?.ToString() ?? "audio/mpeg";
return new FileStreamResult(stream, contentType)
{
EnableRangeProcessing = true
};
}
catch (Exception ex)
{
return new ObjectResult(new { error = $"Error streaming from Subsonic: {ex.Message}" })
{
StatusCode = 500
};
}
}
}