198 lines
8.4 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|