using System; using System.Linq; using System.Net.Mime; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace JellyfinPlugin.Api { [ApiController] [Route("BlackBarRemover")] public class BlackBarController : ControllerBase { private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; public BlackBarController(ILibraryManager libraryManager, ILogger logger) { _libraryManager = libraryManager; _logger = logger; } [HttpGet("CropByPath")] [Produces("text/plain")] public ContentResult GetCropByPath([FromQuery] string path) { if (string.IsNullOrEmpty(path)) return Content(string.Empty, "text/plain"); var mode = Plugin.Instance!.Configuration.PlaybackMode; if (mode == "Client") return Content(string.Empty, "text/plain"); var item = _libraryManager.RootFolder.GetRecursiveChildren().FirstOrDefault(x => x.Path == path); if (item != null && item.ProviderIds != null && item.ProviderIds.TryGetValue("BlackBarCrop", out string? cropData)) { return Content(cropData ?? string.Empty, "text/plain"); } return Content(string.Empty, "text/plain"); } [HttpGet("CropById")] [Produces("text/plain")] public ContentResult GetCropById([FromQuery] string itemId) { if (string.IsNullOrEmpty(itemId)) return Content(string.Empty, "text/plain"); var mode = Plugin.Instance!.Configuration.PlaybackMode; if (mode == "Server") return Content(string.Empty, "text/plain"); if (Guid.TryParse(itemId, out Guid id)) { var item = _libraryManager.GetItemById(id); if (item != null && item.ProviderIds != null && item.ProviderIds.TryGetValue("BlackBarCrop", out string? cropData)) { return Content(cropData ?? string.Empty, "text/plain"); } } return Content(string.Empty, "text/plain"); } [HttpGet("ClientScript.js")] [Produces("application/javascript")] public ContentResult GetClientScript() { string js = @" (function() { console.log('Black Bar Remover: Script loaded'); let currentItemId = null; let interceptedItemId = null; function applyCrop(cropData) { const video = document.querySelector('video'); if (!video) return; const parts = cropData.split(':'); if (parts.length === 4) { const w = parseInt(parts[0]); const h = parseInt(parts[1]); const x = parseInt(parts[2]); const y = parseInt(parts[3]); const topPercent = (y / 1080) * 100; const bottomPercent = ((1080 - (y + h)) / 1080) * 100; const leftPercent = (x / 1920) * 100; const rightPercent = ((1920 - (x + w)) / 1920) * 100; video.style.clipPath = `inset(${topPercent}% ${rightPercent}% ${bottomPercent}% ${leftPercent}%)`; const scaleY = 1 / (1 - (topPercent + bottomPercent)/100); const scaleX = 1 / (1 - (leftPercent + rightPercent)/100); const scale = Math.max(scaleY, scaleX); if (scale > 1.01) { video.style.transform = `scale(${scale})`; } console.log('Black Bar Remover: Applied crop', cropData, 'scale', scale); } } function fetchCrop(itemId) { console.log('Black Bar Remover: Fetching crop data for item', itemId); window._originalFetch('/BlackBarRemover/CropById?itemId=' + itemId) .then(r => r.text()) .then(crop => { console.log('Black Bar Remover: Received crop data:', crop); if (crop) { setTimeout(() => applyCrop(crop), 500); setTimeout(() => applyCrop(crop), 2000); } }) .catch(e => console.error('Black Bar Remover: API fetch error:', e)); } if (!window._originalFetch) { window._originalFetch = window.fetch; window.fetch = async function(...args) { const url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url); if (url) { const match = url.match(/\/Items\/([a-f0-9]{32}|[a-f0-9\-]{36})/i); if (match) { const id = match[1]; if (id && id !== interceptedItemId) { interceptedItemId = id; console.log('Black Bar Remover: Intercepted itemId from fetch:', id); } } } return window._originalFetch.apply(this, args); }; } if (!window._originalXHR) { window._originalXHR = window.XMLHttpRequest; window.XMLHttpRequest = function() { const xhr = new window._originalXHR(); const originalOpen = xhr.open; xhr.open = function(method, url, ...rest) { if (url && typeof url === 'string') { const match = url.match(/\/Items\/([a-f0-9]{32}|[a-f0-9\-]{36})/i); if (match) { const id = match[1]; if (id && id !== interceptedItemId) { interceptedItemId = id; console.log('Black Bar Remover: Intercepted itemId from XHR:', id); } } } return originalOpen.apply(this, [method, url, ...rest]); }; return xhr; }; } setInterval(() => { const video = document.querySelector('video'); if (video && interceptedItemId && interceptedItemId !== currentItemId) { currentItemId = interceptedItemId; console.log('Black Bar Remover: Video element found for intercepted item:', currentItemId); fetchCrop(currentItemId); } else if (!video) { currentItemId = null; } }, 2000); })(); "; return Content(js, "application/javascript"); } } }