Add SceneSearchClient.cs

This commit is contained in:
Sleeps
2026-10-06 16:52:44 +01:00
parent 4ad2acdc7e
commit 4164cce292
@@ -0,0 +1,114 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Jellyfin.Plugin.SceneSearch.Configuration;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.SceneSearch;
/// <summary>
/// HTTP client for the local scene-search service.
/// </summary>
public sealed class SceneSearchClient : IDisposable
{
private readonly HttpClient _httpClient;
private readonly ILogger<SceneSearchClient> _logger;
public SceneSearchClient(ILogger<SceneSearchClient> logger)
{
_logger = logger;
_httpClient = new HttpClient();
}
/// <summary>
/// Gets a value indicating whether enough configuration exists to search.
/// </summary>
public bool IsConfigured
{
get
{
var config = Configuration;
return config.Enabled
&& Uri.TryCreate(config.ApiUrl, UriKind.Absolute, out _);
}
}
private static PluginConfiguration Configuration
=> Plugin.Instance?.Configuration ?? new PluginConfiguration();
/// <summary>
/// Searches the local scene-search API.
/// </summary>
public async Task<IReadOnlyList<SceneSearchItem>> SearchAsync(
string searchTerm,
int limit,
CancellationToken cancellationToken)
{
var config = Configuration;
if (!config.Enabled || string.IsNullOrWhiteSpace(config.ApiUrl))
{
return [];
}
var safeLimit = Math.Clamp(limit, 1, Math.Clamp(config.MaxResults, 1, 50));
var requestUri = string.Concat(
config.ApiUrl.TrimEnd('/'),
"/api/search?q=",
Uri.EscapeDataString(searchTerm),
"&scene_limit=",
Math.Min(safeLimit * 4, 100).ToString(System.Globalization.CultureInfo.InvariantCulture),
"&item_limit=",
safeLimit.ToString(System.Globalization.CultureInfo.InvariantCulture));
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
if (!string.IsNullOrWhiteSpace(config.ApiKey))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", config.ApiKey);
}
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeout.CancelAfter(TimeSpan.FromSeconds(Math.Clamp(config.TimeoutSeconds, 5, 300)));
try
{
using var response = await _httpClient.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
timeout.Token).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
"Scene Search API returned HTTP {StatusCode} for '{SearchTerm}'",
(int)response.StatusCode,
searchTerm);
return [];
}
var payload = await response.Content.ReadFromJsonAsync<SceneSearchResponse>(
cancellationToken: timeout.Token).ConfigureAwait(false);
return payload?.Items ?? [];
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
_logger.LogWarning(
"Scene Search API timed out after {TimeoutSeconds}s for '{SearchTerm}'",
config.TimeoutSeconds,
searchTerm);
return [];
}
catch (HttpRequestException ex)
{
_logger.LogWarning(ex, "Scene Search API request failed for '{SearchTerm}'", searchTerm);
return [];
}
}
public void Dispose()
{
_httpClient.Dispose();
GC.SuppressFinalize(this);
}
}