Files
jellyfin-plugin-enigma2/Jellyfin.Plugin.Enigma2/PreferencesController.cs
T

121 lines
6.6 KiB
C#

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Plugin.Enigma2;
public sealed class WebPlaybackPolicy
{
public bool IsEnigmaChannel { get; set; }
public bool PreferH264 { get; set; }
public static WebPlaybackPolicy For(BaseItem item, bool enabled)
{
var owned = item is LiveTvChannel && string.Equals(item.ServiceName, "Enigma2", StringComparison.OrdinalIgnoreCase);
return new() { IsEnigmaChannel = owned, PreferH264 = owned && enabled };
}
}
public sealed class ChannelPreferences
{
public string[] FavoriteIds { get; set; } = Array.Empty<string>();
public string[] HiddenIds { get; set; } = Array.Empty<string>();
public string[] OrderedIds { get; set; } = Array.Empty<string>();
public string DefaultView { get; set; } = "all";
public void Validate()
{
string[] Clean(string[] values) => (values ?? Array.Empty<string>()).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<Guid,SemaphoreSlim> locks = new();
public PreferencesStore(IApplicationPaths paths) { folder=Path.Combine(paths.DataPath,"decoder-integration","users"); }
public async Task<ChannelPreferences> 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<ChannelPreferences>(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(); }
}
public async Task Remap(Guid user, System.Collections.Generic.IReadOnlyDictionary<string,string> mapping, 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;
var value=JsonSerializer.Deserialize<ChannelPreferences>(await File.ReadAllTextAsync(file,token))??new();value.Validate();
var before=JsonSerializer.Serialize(value);
string[] Map(string[] ids)=>ids.Select(id=>mapping.TryGetValue(id,out var target)?target:id).Distinct().ToArray();
value.FavoriteIds=Map(value.FavoriteIds);value.HiddenIds=Map(value.HiddenIds);value.OrderedIds=Map(value.OrderedIds);
var after=JsonSerializer.Serialize(value);
if(before==after)return;
await File.WriteAllTextAsync(file+".tmp",after,token);File.Move(file+".tmp",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;
private readonly ILibraryManager library;
private readonly IUserManager users;
private readonly ChannelSyncTask channelSync;
public PreferencesController(PreferencesStore store,InternetEpg epg,LiveStreamRelay relay,LiveStreamProbe probe,ILibraryManager library,IUserManager users,ChannelSyncTask channelSync=null) { this.store=store; this.epg=epg; this.relay=relay; this.probe=probe; this.library=library; this.users=users; this.channelSync=channelSync; }
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<ActionResult<ChannelPreferences>> Get(CancellationToken token) => UserId == Guid.Empty ? Unauthorized() : Ok(await store.Load(UserId,token));
[HttpPut("Preferences")]
public async Task<IActionResult> Put(ChannelPreferences value,CancellationToken token)
{ if (UserId == Guid.Empty) return Unauthorized(); await store.Save(UserId,value,token); return NoContent(); }
[HttpGet("WebPlaybackPolicy")]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public ActionResult<WebPlaybackPolicy> GetWebPlaybackPolicy([FromQuery] Guid channelId)
{
// Resolve the authenticated user explicitly: the native library lookup with a
// null user would skip visibility checks. Never accept a userId from the query.
if (UserId == Guid.Empty) return Unauthorized();
var user = users.GetUserById(UserId);
if (user == null) return Unauthorized();
if (!user.HasPermission(PermissionKind.EnableLiveTvAccess)) return Forbid();
var item = channelId == Guid.Empty ? null : library.GetItemById<BaseItem>(channelId, user);
return WebPlaybackPolicy.For(item, Plugin.Instance?.Configuration?.PreferH264ForWebLiveTv ?? false);
}
[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, ChannelSync=channelSync==null?null:new {channelSync.LastCheckedUtc,channelSync.LastSynchronizedUtc,channelSync.LastError} });
}