mirror of
https://github.com/MrSleeps/jellyfin-scene-search.git
synced 2026-11-14 22:11:27 +00:00
Add SceneSearchProvider.cs
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Plugin.SceneSearch.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.SceneSearch;
|
||||
|
||||
/// <summary>
|
||||
/// External Jellyfin search provider backed by jellyfin-scene-search.
|
||||
/// </summary>
|
||||
public sealed class SceneSearchProvider : IExternalSearchProvider
|
||||
{
|
||||
private static readonly HashSet<BaseItemKind> SupportedKinds =
|
||||
[
|
||||
BaseItemKind.Movie,
|
||||
BaseItemKind.Episode,
|
||||
BaseItemKind.Video
|
||||
];
|
||||
|
||||
private readonly SceneSearchClient _client;
|
||||
private readonly ILogger<SceneSearchProvider> _logger;
|
||||
|
||||
public SceneSearchProvider(
|
||||
SceneSearchClient client,
|
||||
ILogger<SceneSearchProvider> logger)
|
||||
{
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Scene Search";
|
||||
|
||||
/// <inheritdoc />
|
||||
public MetadataPluginType Type => MetadataPluginType.SearchProvider;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Priority => 20;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanSearch(SearchProviderQuery query)
|
||||
{
|
||||
if (!_client.IsConfigured || string.IsNullOrWhiteSpace(query.SearchTerm))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var config = Configuration;
|
||||
if (config.RequirePrefix && !HasPrefix(query.SearchTerm, config.SearchPrefix))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (query.IncludeItemTypes.Length > 0
|
||||
&& !query.IncludeItemTypes.Any(SupportedKinds.Contains))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<SearchResult> SearchAsync(
|
||||
SearchProviderQuery query,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
if (!CanSearch(query))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var config = Configuration;
|
||||
var searchTerm = StripPrefix(query.SearchTerm, config);
|
||||
if (string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var limit = Math.Min(query.Limit ?? config.MaxResults, config.MaxResults);
|
||||
var results = await _client.SearchAsync(
|
||||
searchTerm,
|
||||
Math.Max(limit, 1),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
if (!Guid.TryParse(result.JellyfinItemId, out var itemId) || itemId == Guid.Empty)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Ignoring Scene Search result with invalid Jellyfin item id {ItemId}",
|
||||
result.JellyfinItemId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!MatchesRequestedType(result, query))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var score = result.RankingScore ?? result.SiglipScore ?? 0.5;
|
||||
yield return new SearchResult(itemId, (float)Math.Clamp(score, 0.0, 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async Task<IReadOnlyList<SearchResult>> ISearchProvider.SearchAsync(
|
||||
SearchProviderQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var results = new List<SearchResult>();
|
||||
await foreach (var result in SearchAsync(query, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
results.Add(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static PluginConfiguration Configuration
|
||||
=> Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
||||
|
||||
private static bool HasPrefix(string term, string prefix)
|
||||
=> !string.IsNullOrWhiteSpace(prefix)
|
||||
&& term.TrimStart().StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string StripPrefix(string term, PluginConfiguration config)
|
||||
{
|
||||
var trimmed = term.Trim();
|
||||
if (!config.RequirePrefix || string.IsNullOrWhiteSpace(config.SearchPrefix))
|
||||
{
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
return trimmed.StartsWith(config.SearchPrefix, StringComparison.OrdinalIgnoreCase)
|
||||
? trimmed[config.SearchPrefix.Length..].Trim()
|
||||
: trimmed;
|
||||
}
|
||||
|
||||
private static bool MatchesRequestedType(
|
||||
SceneSearchItem result,
|
||||
SearchProviderQuery query)
|
||||
{
|
||||
if (!Enum.TryParse<BaseItemKind>(
|
||||
result.JellyfinItemType,
|
||||
ignoreCase: true,
|
||||
out var kind))
|
||||
{
|
||||
return query.IncludeItemTypes.Length == 0;
|
||||
}
|
||||
|
||||
if (!SupportedKinds.Contains(kind))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (query.IncludeItemTypes.Length > 0 && !query.IncludeItemTypes.Contains(kind))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (query.IncludeItemTypes.Length == 0 && query.ExcludeItemTypes.Contains(kind))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user