Files
blackBarRemover/JellyfinPlugin/PlaybackInfoInterceptorStartupFilter.cs
T

198 lines
8.4 KiB
C#

using System;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace JellyfinPlugin
{
public class PlaybackInfoInterceptorStartupFilter : IStartupFilter
{
private readonly ILogger<PlaybackInfoInterceptorStartupFilter> _logger;
private readonly MediaBrowser.Controller.Library.ILibraryManager _libraryManager;
public PlaybackInfoInterceptorStartupFilter(ILogger<PlaybackInfoInterceptorStartupFilter> logger, MediaBrowser.Controller.Library.ILibraryManager libraryManager)
{
_logger = logger;
_libraryManager = libraryManager;
}
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
app.Use(InvokeAsync);
next(app);
};
}
private string? GetCropFromId(string itemId)
{
if (Guid.TryParse(itemId, out Guid id))
{
var item = _libraryManager.GetItemById(id);
if (item != null && item.ProviderIds != null)
{
if (item.ProviderIds.TryGetValue("BlackBarCrop", out string? cropData))
{
return cropData;
}
}
}
return null;
}
private async Task InvokeAsync(HttpContext context, Func<Task> nextMw)
{
var path = context.Request.Path.Value ?? string.Empty;
var config = Plugin.Instance?.Configuration;
if (config == null || !config.IsEnabled || config.PlaybackMode == "Client")
{
await nextMw().ConfigureAwait(false);
return;
}
var isAutoMode = config.PlaybackMode == "Auto" || config.PlaybackMode == "Both";
if (isAutoMode && IsJellyfinWebClient(context))
{
// W trybie Auto dla przeglądarki Web pozwalamy na Direct Play (przycięcie wykona skrypt CSS w przeglądarce).
await nextMw().ConfigureAwait(false);
return;
}
// --- HLS STREAM QUERY REWRITE ---
// If the client requests the m3u8 playlist, we simply tell Jellyfin NOT to allow video stream copy.
// This elegantly forces transcoding for the BEST supported codec without breaking FFmpeg tags/filters!
if (path.Contains("/Videos/", StringComparison.OrdinalIgnoreCase) &&
(path.Contains(".m3u8", StringComparison.OrdinalIgnoreCase) || path.Contains("/stream", StringComparison.OrdinalIgnoreCase)))
{
var matchId = Regex.Match(path, @"/Videos/([^/]+)/", RegexOptions.IgnoreCase);
if (matchId.Success)
{
var idStr = matchId.Groups[1].Value;
var crop = GetCropFromId(idStr);
if (crop != null)
{
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(context.Request.QueryString.Value);
var items = query.SelectMany(x => x.Value, (col, val) => new System.Collections.Generic.KeyValuePair<string, string>(col.Key, val)).ToList();
var newQueryString = "?";
foreach (var kvp in items)
{
if (!kvp.Key.Equals("allowVideoStreamCopy", StringComparison.OrdinalIgnoreCase) &&
!kvp.Key.Equals("transcodeReasons", StringComparison.OrdinalIgnoreCase))
{
newQueryString += $"{kvp.Key}={System.Net.WebUtility.UrlEncode(kvp.Value)}&";
}
}
// Force disable stream copy AND provide a valid reason for the dashboard
newQueryString += "allowVideoStreamCopy=false&transcodeReasons=VideoResolutionNotSupported";
context.Request.QueryString = new QueryString(newQueryString);
_logger.LogInformation($"[Black Bar Remover DEBUG] Intercepted HLS request for {idStr}. Appended allowVideoStreamCopy=false and transcodeReasons=VideoResolutionNotSupported.");
}
}
}
var match = Regex.Match(path, @"/Items/([^/]+)/PlaybackInfo", RegexOptions.IgnoreCase);
if (!match.Success)
{
await nextMw().ConfigureAwait(false);
return;
}
var itemId = match.Groups[1].Value;
var cropData = GetCropFromId(itemId);
if (cropData == null)
{
_logger.LogInformation($"[Black Bar Remover DEBUG] PlaybackInfo request for {itemId}, but no crop found in DB. Skipping interception.");
await nextMw().ConfigureAwait(false);
return;
}
_logger.LogInformation($"[Black Bar Remover DEBUG] Intercepting PlaybackInfo request for {itemId}. Found crop in DB: {cropData}. Disabling DirectPlayProfiles in incoming request.");
// Modify incoming request body to clear DirectPlayProfiles
context.Request.EnableBuffering();
string reqJson = string.Empty;
using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true))
{
reqJson = await reader.ReadToEndAsync().ConfigureAwait(false);
}
JsonNode rootNode;
try
{
rootNode = string.IsNullOrWhiteSpace(reqJson) ? new JsonObject() : (JsonNode.Parse(reqJson) ?? new JsonObject());
}
catch
{
rootNode = new JsonObject();
}
if (rootNode["DeviceProfile"] is not JsonObject devProfile)
{
devProfile = new JsonObject();
rootNode["DeviceProfile"] = devProfile;
}
devProfile["DirectPlayProfiles"] = new JsonArray();
var newReqBytes = Encoding.UTF8.GetBytes(rootNode.ToJsonString());
var reqMs = new MemoryStream(newReqBytes);
context.Request.Body = reqMs;
context.Request.ContentLength = newReqBytes.Length;
await nextMw().ConfigureAwait(false);
}
private static bool IsJellyfinWebClient(HttpContext context)
{
if (context.Request.Headers.TryGetValue("X-Emby-Authorization", out var authHeader) ||
context.Request.Headers.TryGetValue("Authorization", out authHeader))
{
var authStr = authHeader.ToString();
if (authStr.Contains("Client=\"Jellyfin Web\"", StringComparison.OrdinalIgnoreCase) ||
authStr.Contains("Client=\"Jellyfin%20Web\"", StringComparison.OrdinalIgnoreCase) ||
authStr.Contains("Client=Jellyfin Web", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
if (context.Request.Headers.TryGetValue("User-Agent", out var uaHeader))
{
var ua = uaHeader.ToString();
if (ua.Contains("JellyfinMediaPlayer", StringComparison.OrdinalIgnoreCase) ||
ua.Contains("Jellyfin-Android", StringComparison.OrdinalIgnoreCase) ||
ua.Contains("ExoPlayer", StringComparison.OrdinalIgnoreCase) ||
ua.Contains("okhttp", StringComparison.OrdinalIgnoreCase) ||
ua.Contains("Kodi", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (ua.Contains("Mozilla", StringComparison.OrdinalIgnoreCase) ||
ua.Contains("Chrome", StringComparison.OrdinalIgnoreCase) ||
ua.Contains("Safari", StringComparison.OrdinalIgnoreCase) ||
ua.Contains("Firefox", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
}
}