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; /// /// 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. /// 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 _logger; private readonly object _gate = new(); private List? _rules; /// /// Initializes a new instance of the class. /// /// The Jellyfin application paths. /// The logger. public MediaPolicyStore(IApplicationPaths applicationPaths, ILogger logger) { _applicationPaths = applicationPaths; _logger = logger; } /// public IReadOnlyList GetRules() { lock (_gate) { return EnsureLoaded().ToList(); } } /// 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); } } /// 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 EnsureLoaded() { if (_rules is not null) { return _rules; } var path = GetPath(); if (!File.Exists(path)) { _rules = new List(); return _rules; } try { using var stream = File.OpenRead(path); _rules = JsonSerializer.Deserialize>(stream, JsonOptions) ?? new List(); } 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(); } return _rules; } private void Save(List 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"); } }