103 lines
5.3 KiB
C#
103 lines
5.3 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(); }
|
|
}
|
|
}
|
|
[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;
|
|
public PreferencesController(PreferencesStore store,InternetEpg epg,LiveStreamRelay relay,LiveStreamProbe probe,ILibraryManager library,IUserManager users) { this.store=store; this.epg=epg; this.relay=relay; this.probe=probe; this.library=library; this.users=users; }
|
|
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 });
|
|
}
|