feat: prioritise and blacklist media and overview in setting page of progress in adding info
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Plugin.JRay.Models;
|
||||
|
||||
namespace Jellyfin.Plugin.JRay.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the prioritise/ignore rules that shape the work-discovery API
|
||||
/// (<c>GET /Plugins/JRay/Tasks/Pending</c>). Rules can target a genre, a
|
||||
/// series, or a single item; more specific scopes override broader ones.
|
||||
/// </summary>
|
||||
public interface IMediaPolicyStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all currently configured rules.
|
||||
/// </summary>
|
||||
/// <returns>The list of rules (a copy; safe to enumerate).</returns>
|
||||
IReadOnlyList<MediaPolicyRule> GetRules();
|
||||
|
||||
/// <summary>
|
||||
/// Adds or replaces the rule for a given scope+value. If a rule already
|
||||
/// exists for the same scope and value, its action and label are updated,
|
||||
/// so a target can never hold two conflicting actions.
|
||||
/// </summary>
|
||||
/// <param name="rule">The rule to set.</param>
|
||||
void SetRule(MediaPolicyRule rule);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the rule matching the given scope and value, if present.
|
||||
/// </summary>
|
||||
/// <param name="scope">The scope of the rule to remove.</param>
|
||||
/// <param name="value">The value of the rule to remove.</param>
|
||||
/// <returns><c>true</c> if a rule was removed.</returns>
|
||||
bool RemoveRule(PolicyScope scope, string value);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Plugin.JRay.Models;
|
||||
using Jellyfin.Plugin.JRay.Services.Interfaces;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.JRay.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Persists the prioritise/ignore rules to a single JSON file under the
|
||||
/// plugin's configuration directory, and keeps an in-memory copy for fast
|
||||
/// reads. All access is synchronised so the work-discovery API and the
|
||||
/// config page can touch it concurrently.
|
||||
/// </summary>
|
||||
public sealed class MediaPolicyStore : IMediaPolicyStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
private readonly IApplicationPaths _applicationPaths;
|
||||
private readonly ILogger<MediaPolicyStore> _logger;
|
||||
private readonly object _gate = new();
|
||||
private List<MediaPolicyRule>? _rules;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MediaPolicyStore"/> class.
|
||||
/// </summary>
|
||||
/// <param name="applicationPaths">The Jellyfin application paths.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public MediaPolicyStore(IApplicationPaths applicationPaths, ILogger<MediaPolicyStore> logger)
|
||||
{
|
||||
_applicationPaths = applicationPaths;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<MediaPolicyRule> GetRules()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return EnsureLoaded().ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetRule(MediaPolicyRule rule)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(rule);
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
var rules = EnsureLoaded();
|
||||
rules.RemoveAll(r => r.Scope == rule.Scope && ValueEquals(r.Value, rule.Value));
|
||||
rules.Add(rule);
|
||||
Save(rules);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool RemoveRule(PolicyScope scope, string value)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var rules = EnsureLoaded();
|
||||
var removed = rules.RemoveAll(r => r.Scope == scope && ValueEquals(r.Value, value));
|
||||
if (removed > 0)
|
||||
{
|
||||
Save(rules);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ValueEquals(string a, string b)
|
||||
{
|
||||
// Genre names are matched case-insensitively; ids happen to be
|
||||
// case-insensitive too (GUID strings), so a single comparison suffices.
|
||||
return string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private List<MediaPolicyRule> EnsureLoaded()
|
||||
{
|
||||
if (_rules is not null)
|
||||
{
|
||||
return _rules;
|
||||
}
|
||||
|
||||
var path = GetPath();
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
_rules = new List<MediaPolicyRule>();
|
||||
return _rules;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
_rules = JsonSerializer.Deserialize<List<MediaPolicyRule>>(stream, JsonOptions)
|
||||
?? new List<MediaPolicyRule>();
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or JsonException)
|
||||
{
|
||||
_logger.LogWarning(ex, "JRay: failed to read media policy file {Path}; starting empty", path);
|
||||
_rules = new List<MediaPolicyRule>();
|
||||
}
|
||||
|
||||
return _rules;
|
||||
}
|
||||
|
||||
private void Save(List<MediaPolicyRule> rules)
|
||||
{
|
||||
var path = GetPath();
|
||||
var directory = Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Media policy path has no directory.");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
var tempPath = path + ".tmp";
|
||||
using (var stream = File.Create(tempPath))
|
||||
{
|
||||
JsonSerializer.Serialize(stream, rules, JsonOptions);
|
||||
}
|
||||
|
||||
File.Move(tempPath, path, overwrite: true);
|
||||
}
|
||||
|
||||
private string GetPath()
|
||||
{
|
||||
return Path.Combine(_applicationPaths.PluginConfigurationsPath, "JRay", "policy.json");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Plugin.JRay.Models;
|
||||
|
||||
namespace Jellyfin.Plugin.JRay.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the effective prioritise/ignore action for an item from the
|
||||
/// configured rules. More specific scopes win: an Item rule overrides a
|
||||
/// Series rule, which overrides a Genre rule. Because a given scope+value can
|
||||
/// hold only one action, the only conflicts possible are across scopes, and
|
||||
/// specificity resolves those.
|
||||
/// </summary>
|
||||
public static class PolicyResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes the effective action for an item, or <c>null</c> if no rule matches.
|
||||
/// </summary>
|
||||
/// <param name="rules">The configured rules.</param>
|
||||
/// <param name="itemId">The item's id.</param>
|
||||
/// <param name="seriesId">The item's series id, or <see cref="Guid.Empty"/> if it is not an episode.</param>
|
||||
/// <param name="genres">The item's genres.</param>
|
||||
/// <returns>The effective <see cref="PolicyAction"/>, or <c>null</c> when unruled.</returns>
|
||||
public static PolicyAction? Resolve(
|
||||
IReadOnlyList<MediaPolicyRule> rules,
|
||||
Guid itemId,
|
||||
Guid seriesId,
|
||||
IReadOnlyList<string> genres)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(rules);
|
||||
|
||||
PolicyAction? itemAction = null;
|
||||
PolicyAction? seriesAction = null;
|
||||
PolicyAction? genreAction = null;
|
||||
|
||||
var itemIdStr = itemId.ToString("D");
|
||||
var seriesIdStr = seriesId == Guid.Empty ? null : seriesId.ToString("D");
|
||||
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
switch (rule.Scope)
|
||||
{
|
||||
case PolicyScope.Item:
|
||||
if (string.Equals(rule.Value, itemIdStr, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
itemAction = rule.Action;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case PolicyScope.Series:
|
||||
if (seriesIdStr is not null && string.Equals(rule.Value, seriesIdStr, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
seriesAction = rule.Action;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case PolicyScope.Genre:
|
||||
if (genres is not null && MatchesGenre(genres, rule.Value))
|
||||
{
|
||||
genreAction = rule.Action;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return itemAction ?? seriesAction ?? genreAction;
|
||||
}
|
||||
|
||||
private static bool MatchesGenre(IReadOnlyList<string> genres, string value)
|
||||
{
|
||||
for (var i = 0; i < genres.Count; i++)
|
||||
{
|
||||
if (string.Equals(genres[i], value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user