using System; using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Plugin.Enigma2; public sealed class ChannelPreferences { public string[] FavoriteIds { get; set; } = Array.Empty(); public string[] HiddenIds { get; set; } = Array.Empty(); public string[] OrderedIds { get; set; } = Array.Empty(); public string DefaultView { get; set; } = "all"; public void Validate() { string[] Clean(string[] values) => (values ?? Array.Empty()).Take(2000).Where(v=>Guid.TryParse(v,out _)).Select(v=>Guid.Parse(v).ToString("N")).Distinct().ToArray(); FavoriteIds=Clean(FavoriteIds); HiddenIds=Clean(HiddenIds); OrderedIds=Clean(OrderedIds); if (DefaultView != "all" && DefaultView != "favorites" && DefaultView != "guide") DefaultView="all"; } } public sealed class PreferencesStore { private readonly string folder; private readonly ConcurrentDictionary locks = new(); public PreferencesStore(IApplicationPaths paths) { folder=Path.Combine(paths.DataPath,"decoder-integration","users"); } public async Task Load(Guid user, CancellationToken token) { var gate=locks.GetOrAdd(user,_=>new(1)); await gate.WaitAsync(token); try { var file=Path.Combine(folder,user.ToString("N")+".json"); if (!File.Exists(file)) return new(); var result=JsonSerializer.Deserialize(await File.ReadAllTextAsync(file,token)) ?? new(); result.Validate(); return result; } finally { gate.Release(); } } public async Task Save(Guid user, ChannelPreferences value, CancellationToken token) { value.Validate(); var gate=locks.GetOrAdd(user,_=>new(1)); await gate.WaitAsync(token); try { Directory.CreateDirectory(folder); var file=Path.Combine(folder,user.ToString("N")+".json"); var temp=file+".tmp"; await File.WriteAllTextAsync(temp,JsonSerializer.Serialize(value),token); File.Move(temp,file,true); } finally { gate.Release(); } } } [ApiController] [Route("DecoderIntegration")] [Authorize(Policy="LiveTvAccess")] public sealed class PreferencesController : ControllerBase { private readonly PreferencesStore store; private readonly InternetEpg epg; private readonly LiveStreamRelay relay; private readonly LiveStreamProbe probe; public PreferencesController(PreferencesStore store,InternetEpg epg,LiveStreamRelay relay,LiveStreamProbe probe) { this.store=store; this.epg=epg; this.relay=relay; this.probe=probe; } private Guid UserId => Guid.TryParse(User.FindFirst("Jellyfin-UserId")?.Value,out var id) ? id : Guid.Empty; [HttpGet("Channels")] [AllowAnonymous] public IActionResult Page() { var stream=typeof(Plugin).Assembly.GetManifestResourceStream("Jellyfin.Plugin.Enigma2.Configuration.channels.html"); return File(stream,"text/html; charset=utf-8"); } [HttpGet("Preferences")] public async Task> Get(CancellationToken token) => UserId == Guid.Empty ? Unauthorized() : Ok(await store.Load(UserId,token)); [HttpPut("Preferences")] public async Task Put(ChannelPreferences value,CancellationToken token) { if (UserId == Guid.Empty) return Unauthorized(); await store.Save(UserId,value,token); return NoContent(); } [HttpGet("Status")] [Authorize(Policy="RequiresElevation")] public IActionResult Status() => Ok(new { epg.LastSuccess,epg.MatchedImages,epg.LastError, Source="https://epg.polishteam.one/guide.xml", Streams=relay.Status(), ProbeError=probe.LastError }); }