using System; using System.Globalization; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.Jellypod.Api; /// /// Controller for proxying podcast audio streams. /// [ApiController] [Route("Jellypod/Stream")] public class StreamProxyController : ControllerBase { private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; /// /// Initializes a new instance of the class. /// /// Logger instance. /// HTTP client factory. public StreamProxyController( ILogger logger, IHttpClientFactory httpClientFactory) { _logger = logger; _httpClientFactory = httpClientFactory; } /// /// Proxies an audio stream from a remote URL. /// /// Base64-encoded URL to stream. /// Cancellation token. /// The audio stream. [HttpGet("{url}")] [AllowAnonymous] public async Task GetStream([FromRoute] string url, CancellationToken cancellationToken) { try { // Decode the URL from base64 var decodedUrl = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(url)); _logger.LogDebug("Proxying audio stream from: {Url}", decodedUrl); var client = _httpClientFactory.CreateClient("Jellypod"); // Make a streaming request var request = new HttpRequestMessage(HttpMethod.Get, decodedUrl); var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); if (!response.IsSuccessStatusCode) { _logger.LogWarning("Failed to fetch audio stream: {StatusCode}", response.StatusCode); return StatusCode((int)response.StatusCode); } // Get content type var contentType = response.Content.Headers.ContentType?.MediaType ?? "audio/mpeg"; var contentLength = response.Content.Headers.ContentLength; // Stream the response var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); if (contentLength.HasValue) { Response.Headers["Content-Length"] = contentLength.Value.ToString(CultureInfo.InvariantCulture); } Response.Headers["Accept-Ranges"] = "bytes"; return File(stream, contentType, enableRangeProcessing: true); } catch (Exception ex) { _logger.LogError(ex, "Error proxying audio stream"); return StatusCode(500, "Failed to proxy audio stream"); } } }