Compare commits
13 Commits
v0.1.19-beta
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 86ec128ec8 | |||
| 526ee3d165 | |||
| d1691655a4 | |||
| d0a6669179 | |||
| 359aa21394 | |||
| 0d5c55c843 | |||
| d88f910b58 | |||
| 512dabc611 | |||
| 78e100c99e | |||
| b2885aa163 | |||
| 73f4bb488a | |||
| 9042643383 | |||
| 739c96c376 |
@@ -61,19 +61,218 @@ namespace JellyfinPlugin.Api
|
|||||||
return Content(string.Empty, "text/plain");
|
return Content(string.Empty, "text/plain");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("Config")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public IActionResult GetConfig()
|
||||||
|
{
|
||||||
|
var config = Plugin.Instance!.Configuration;
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
cropDetectLimit = config.CropDetectLimit,
|
||||||
|
playbackMode = config.PlaybackMode,
|
||||||
|
subtitleOffsetVh = config.SubtitleOffsetVh,
|
||||||
|
maxParallelScans = config.MaxParallelScans
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("ClientScript.js")]
|
[HttpGet("ClientScript.js")]
|
||||||
[Produces("application/javascript")]
|
[Produces("application/javascript")]
|
||||||
public ContentResult GetClientScript()
|
public ContentResult GetClientScript()
|
||||||
{
|
{
|
||||||
|
double defaultSubOffset = Plugin.Instance!.Configuration.SubtitleOffsetVh;
|
||||||
|
string defaultSubOffsetStr = defaultSubOffset.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
string js = @"
|
string js = @"
|
||||||
(function() {
|
(function() {
|
||||||
console.log('Black Bar Remover: Script loaded');
|
console.log('Black Bar Remover: Script loaded');
|
||||||
let currentItemId = null;
|
let currentItemId = null;
|
||||||
let interceptedItemId = null;
|
let interceptedItemId = null;
|
||||||
|
let currentCropData = null;
|
||||||
|
window._currentPlayMethod = null;
|
||||||
|
window._isVideoDirect = null;
|
||||||
|
window._configuredSubtitleOffsetVh = {SUB_OFFSET};
|
||||||
|
window._customSubtitleOffsetVh = null;
|
||||||
|
|
||||||
|
function isTranscoding() {
|
||||||
|
if (window._isVideoDirect === false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (window._isVideoDirect === true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchServerConfig() {
|
||||||
|
if (!window._originalFetch) return;
|
||||||
|
window._originalFetch('/BlackBarRemover/Config')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data && typeof data.subtitleOffsetVh === 'number') {
|
||||||
|
const newOffset = data.subtitleOffsetVh;
|
||||||
|
if (newOffset !== window._configuredSubtitleOffsetVh) {
|
||||||
|
window._configuredSubtitleOffsetVh = newOffset;
|
||||||
|
console.log('Black Bar Remover: Synced configured subtitle offset:', newOffset + 'vh');
|
||||||
|
if (window._customSubtitleOffsetVh === null && lastBottomPercent > 0) {
|
||||||
|
updateSubtitleStyles(lastBottomPercent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSubtitleToast(msg) {
|
||||||
|
let toastEl = document.getElementById('black-bar-remover-sub-toast');
|
||||||
|
if (!toastEl) {
|
||||||
|
toastEl = document.createElement('div');
|
||||||
|
toastEl.id = 'black-bar-remover-sub-toast';
|
||||||
|
toastEl.style.position = 'fixed';
|
||||||
|
toastEl.style.bottom = '15%';
|
||||||
|
toastEl.style.left = '50%';
|
||||||
|
toastEl.style.transform = 'translateX(-50%)';
|
||||||
|
toastEl.style.backgroundColor = 'rgba(0, 0, 0, 0.85)';
|
||||||
|
toastEl.style.color = '#fff';
|
||||||
|
toastEl.style.padding = '8px 16px';
|
||||||
|
toastEl.style.borderRadius = '20px';
|
||||||
|
toastEl.style.fontSize = '0.95rem';
|
||||||
|
toastEl.style.fontWeight = 'bold';
|
||||||
|
toastEl.style.zIndex = '999999';
|
||||||
|
toastEl.style.pointerEvents = 'none';
|
||||||
|
toastEl.style.transition = 'opacity 0.3s ease';
|
||||||
|
document.body.appendChild(toastEl);
|
||||||
|
}
|
||||||
|
toastEl.textContent = msg;
|
||||||
|
toastEl.style.opacity = '1';
|
||||||
|
if (window._toastTimeout) clearTimeout(window._toastTimeout);
|
||||||
|
window._toastTimeout = setTimeout(() => {
|
||||||
|
toastEl.style.opacity = '0';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveOffsetVh() {
|
||||||
|
if (window._customSubtitleOffsetVh !== null && window._customSubtitleOffsetVh !== undefined) {
|
||||||
|
return window._customSubtitleOffsetVh;
|
||||||
|
}
|
||||||
|
if (window._configuredSubtitleOffsetVh !== null && window._configuredSubtitleOffsetVh !== undefined) {
|
||||||
|
return window._configuredSubtitleOffsetVh;
|
||||||
|
}
|
||||||
|
return 5.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window._subHotkeyBound) {
|
||||||
|
window._subHotkeyBound = true;
|
||||||
|
window.addEventListener('keydown', function(e) {
|
||||||
|
if (e.altKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
||||||
|
const video = document.querySelector('video');
|
||||||
|
if (!video) return;
|
||||||
|
e.preventDefault();
|
||||||
|
let current = getActiveOffsetVh();
|
||||||
|
if (e.key === 'ArrowUp') {
|
||||||
|
current = Math.min(25.0, current + 0.5);
|
||||||
|
} else if (e.key === 'ArrowDown') {
|
||||||
|
current = Math.max(0.0, current - 0.5);
|
||||||
|
}
|
||||||
|
window._customSubtitleOffsetVh = Math.round(current * 10) / 10;
|
||||||
|
showSubtitleToast('Subtitle Offset: ' + window._customSubtitleOffsetVh.toFixed(1) + ' vh (Alt+Up/Down)');
|
||||||
|
if (lastBottomPercent > 0) {
|
||||||
|
updateSubtitleStyles(lastBottomPercent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureBoundingBoxHook(video) {
|
||||||
|
if (!video._origGetBoundingClientRect) {
|
||||||
|
video._origGetBoundingClientRect = video.getBoundingClientRect;
|
||||||
|
video.getBoundingClientRect = function() {
|
||||||
|
if (video._isCroppedByPlugin && video.parentElement) {
|
||||||
|
const rect = video.parentElement.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
top: rect.top,
|
||||||
|
right: rect.right,
|
||||||
|
bottom: rect.bottom,
|
||||||
|
left: rect.left,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
x: rect.x !== undefined ? rect.x : rect.left,
|
||||||
|
y: rect.y !== undefined ? rect.y : rect.top,
|
||||||
|
toJSON: function() { return this; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return video._origGetBoundingClientRect.apply(this, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastBottomPercent = 0;
|
||||||
|
|
||||||
|
function updateSubtitleStyles(bottomPercent) {{
|
||||||
|
lastBottomPercent = bottomPercent;
|
||||||
|
let styleEl = document.getElementById('black-bar-remover-sub-style');
|
||||||
|
if (!styleEl) {{
|
||||||
|
styleEl = document.createElement('style');
|
||||||
|
styleEl.id = 'black-bar-remover-sub-style';
|
||||||
|
document.head.appendChild(styleEl);
|
||||||
|
}}
|
||||||
|
|
||||||
|
if (bottomPercent > 0) {{
|
||||||
|
const activeOffset = getActiveOffsetVh();
|
||||||
|
const offsetVh = activeOffset.toFixed(2);
|
||||||
|
styleEl.textContent =
|
||||||
|
'.videoSubtitles {' +
|
||||||
|
'display: flex !important;' +
|
||||||
|
'flex-direction: column !important;' +
|
||||||
|
'justify-content: flex-end !important;' +
|
||||||
|
'transform: translateY(-' + offsetVh + 'vh) !important;' +
|
||||||
|
'}' +
|
||||||
|
'.track-text-output, .custom-subtitle-overlay, .subtitleSyncContainer, .videoSubtitlesContainer, .libassjs-canvas-parent, canvas.libassjs-canvas, .subtitles-octopus-canvas, .htmlvideoplayer::-webkit-media-text-track-container, .htmlvideoplayer::-webkit-media-text-track-display {' +
|
||||||
|
'transform: translateY(-' + offsetVh + 'vh) !important;' +
|
||||||
|
'}';
|
||||||
|
console.log('Black Bar Remover: Subtitle CSS style updated translateY(-' + offsetVh + 'vh)');
|
||||||
|
}} else {{
|
||||||
|
styleEl.textContent = '';
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
function adjustVttCues(bottomPercent) {{
|
||||||
|
const video = document.querySelector('video');
|
||||||
|
if (!video || !video.textTracks) return;
|
||||||
|
const activeOffset = getActiveOffsetVh();
|
||||||
|
const targetLine = Math.max(10, 95 - Math.round(activeOffset * 1.5));
|
||||||
|
for (let i = 0; i < video.textTracks.length; i++) {{
|
||||||
|
const track = video.textTracks[i];
|
||||||
|
if (track && track.cues) {{
|
||||||
|
for (let j = 0; j < track.cues.length; j++) {{
|
||||||
|
const cue = track.cues[j];
|
||||||
|
if (cue && !cue._pluginAdjusted) {{
|
||||||
|
cue.snapToLines = false;
|
||||||
|
cue.line = targetLine;
|
||||||
|
cue._pluginAdjusted = true;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
function applyCrop(cropData) {
|
function applyCrop(cropData) {
|
||||||
const video = document.querySelector('video');
|
const video = document.querySelector('video');
|
||||||
if (!video) return;
|
if (!video) return;
|
||||||
|
|
||||||
|
if (isTranscoding()) {
|
||||||
|
console.log('Black Bar Remover: Video is transcoded by server (IsVideoDirect: false), skipping CSS crop.');
|
||||||
|
video.style.position = '';
|
||||||
|
video.style.objectFit = '';
|
||||||
|
video.style.width = '';
|
||||||
|
video.style.height = '';
|
||||||
|
video.style.top = '';
|
||||||
|
video.style.left = '';
|
||||||
|
video.style.clipPath = '';
|
||||||
|
video.style.transform = '';
|
||||||
|
updateSubtitleStyles(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const parts = cropData.split(':');
|
const parts = cropData.split(':');
|
||||||
if (parts.length === 4) {
|
if (parts.length === 4) {
|
||||||
const w = parseInt(parts[0]);
|
const w = parseInt(parts[0]);
|
||||||
@@ -81,21 +280,28 @@ namespace JellyfinPlugin.Api
|
|||||||
const x = parseInt(parts[2]);
|
const x = parseInt(parts[2]);
|
||||||
const y = parseInt(parts[3]);
|
const y = parseInt(parts[3]);
|
||||||
|
|
||||||
const topPercent = (y / 1080) * 100;
|
if (h > 0 && h < 1080) {
|
||||||
|
const heightPercent = (1080 / h) * 100;
|
||||||
|
const topPercent = -(y / h) * 100;
|
||||||
const bottomPercent = ((1080 - (y + h)) / 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}%)`;
|
video.style.position = 'absolute';
|
||||||
|
video.style.objectFit = 'fill';
|
||||||
|
video.style.width = '100%';
|
||||||
|
video.style.height = heightPercent.toFixed(2) + '%';
|
||||||
|
video.style.top = topPercent.toFixed(2) + '%';
|
||||||
|
video.style.left = '0%';
|
||||||
|
video.style.transform = 'none';
|
||||||
|
video.style.clipPath = 'none';
|
||||||
|
|
||||||
const scaleY = 1 / (1 - (topPercent + bottomPercent)/100);
|
if (video.parentElement) {
|
||||||
const scaleX = 1 / (1 - (leftPercent + rightPercent)/100);
|
video.parentElement.style.overflow = 'hidden';
|
||||||
const scale = Math.max(scaleY, scaleX);
|
}
|
||||||
|
|
||||||
if (scale > 1.01) {
|
updateSubtitleStyles(bottomPercent);
|
||||||
video.style.transform = `scale(${scale})`;
|
adjustVttCues(bottomPercent);
|
||||||
|
console.log('Black Bar Remover: Applied non-scaling crop height:', heightPercent.toFixed(2) + '% top:', topPercent.toFixed(2) + '% subOffset:', (bottomPercent * 0.4 + 2.5).toFixed(2) + 'vh');
|
||||||
}
|
}
|
||||||
console.log('Black Bar Remover: Applied crop', cropData, 'scale', scale);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +312,7 @@ namespace JellyfinPlugin.Api
|
|||||||
.then(crop => {
|
.then(crop => {
|
||||||
console.log('Black Bar Remover: Received crop data:', crop);
|
console.log('Black Bar Remover: Received crop data:', crop);
|
||||||
if (crop) {
|
if (crop) {
|
||||||
|
currentCropData = crop;
|
||||||
setTimeout(() => applyCrop(crop), 500);
|
setTimeout(() => applyCrop(crop), 500);
|
||||||
setTimeout(() => applyCrop(crop), 2000);
|
setTimeout(() => applyCrop(crop), 2000);
|
||||||
}
|
}
|
||||||
@@ -117,17 +324,39 @@ namespace JellyfinPlugin.Api
|
|||||||
window._originalFetch = window.fetch;
|
window._originalFetch = window.fetch;
|
||||||
window.fetch = async function(...args) {
|
window.fetch = async function(...args) {
|
||||||
const url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url);
|
const url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url);
|
||||||
if (url) {
|
if (url && typeof url === 'string') {
|
||||||
const match = url.match(/\/Items\/([a-f0-9]{32}|[a-f0-9\-]{36})/i);
|
const match = url.match(/\/Items\/([a-f0-9]{32}|[a-f0-9\-]{36})/i);
|
||||||
if (match) {
|
if (match) {
|
||||||
const id = match[1];
|
const id = match[1];
|
||||||
if (id && id !== interceptedItemId) {
|
if (id && id !== interceptedItemId) {
|
||||||
interceptedItemId = id;
|
interceptedItemId = id;
|
||||||
|
window._currentPlayMethod = null;
|
||||||
|
window._isVideoDirect = null;
|
||||||
console.log('Black Bar Remover: Intercepted itemId from fetch:', id);
|
console.log('Black Bar Remover: Intercepted itemId from fetch:', id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return window._originalFetch.apply(this, args);
|
|
||||||
|
const res = await window._originalFetch.apply(this, args);
|
||||||
|
|
||||||
|
if (url && typeof url === 'string' && url.includes('/PlaybackInfo')) {
|
||||||
|
try {
|
||||||
|
const clone = res.clone();
|
||||||
|
clone.json().then(data => {
|
||||||
|
if (data && data.MediaSources && data.MediaSources[0]) {
|
||||||
|
const ms = data.MediaSources[0];
|
||||||
|
window._currentPlayMethod = ms.PlayMethod;
|
||||||
|
window._isVideoDirect = ms.IsVideoDirect !== false;
|
||||||
|
console.log('Black Bar Remover: PlaybackInfo detected -> PlayMethod:', window._currentPlayMethod, 'IsVideoDirect:', window._isVideoDirect);
|
||||||
|
if (currentCropData) {
|
||||||
|
applyCrop(currentCropData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(e => {});
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,9 +372,27 @@ namespace JellyfinPlugin.Api
|
|||||||
const id = match[1];
|
const id = match[1];
|
||||||
if (id && id !== interceptedItemId) {
|
if (id && id !== interceptedItemId) {
|
||||||
interceptedItemId = id;
|
interceptedItemId = id;
|
||||||
|
window._currentPlayMethod = null;
|
||||||
|
window._isVideoDirect = null;
|
||||||
console.log('Black Bar Remover: Intercepted itemId from XHR:', id);
|
console.log('Black Bar Remover: Intercepted itemId from XHR:', id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (url.includes('/PlaybackInfo')) {
|
||||||
|
xhr.addEventListener('load', function() {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(xhr.responseText);
|
||||||
|
if (data && data.MediaSources && data.MediaSources[0]) {
|
||||||
|
const ms = data.MediaSources[0];
|
||||||
|
window._currentPlayMethod = ms.PlayMethod;
|
||||||
|
window._isVideoDirect = ms.IsVideoDirect !== false;
|
||||||
|
console.log('Black Bar Remover: XHR PlaybackInfo detected -> PlayMethod:', window._currentPlayMethod, 'IsVideoDirect:', window._isVideoDirect);
|
||||||
|
if (currentCropData) {
|
||||||
|
applyCrop(currentCropData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return originalOpen.apply(this, [method, url, ...rest]);
|
return originalOpen.apply(this, [method, url, ...rest]);
|
||||||
};
|
};
|
||||||
@@ -220,19 +467,25 @@ namespace JellyfinPlugin.Api
|
|||||||
playbackInfoObserver.observe(document.body, { childList: true, subtree: true, characterData: true });
|
playbackInfoObserver.observe(document.body, { childList: true, subtree: true, characterData: true });
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
|
fetchServerConfig();
|
||||||
const video = document.querySelector('video');
|
const video = document.querySelector('video');
|
||||||
if (video && interceptedItemId && interceptedItemId !== currentItemId) {
|
if (video && interceptedItemId && interceptedItemId !== currentItemId) {
|
||||||
currentItemId = interceptedItemId;
|
currentItemId = interceptedItemId;
|
||||||
console.log('Black Bar Remover: Video element found for intercepted item:', currentItemId);
|
console.log('Black Bar Remover: Video element found for intercepted item:', currentItemId);
|
||||||
fetchCrop(currentItemId);
|
fetchCrop(currentItemId);
|
||||||
checkServerCrop(currentItemId);
|
checkServerCrop(currentItemId);
|
||||||
|
} else if (video && lastBottomPercent > 0) {
|
||||||
|
updateSubtitleStyles(lastBottomPercent);
|
||||||
|
adjustVttCues(lastBottomPercent);
|
||||||
} else if (!video) {
|
} else if (!video) {
|
||||||
currentItemId = null;
|
currentItemId = null;
|
||||||
|
lastBottomPercent = 0;
|
||||||
window._isServerCropping = false;
|
window._isServerCropping = false;
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
})();
|
})();
|
||||||
";
|
";
|
||||||
|
js = js.Replace("{SUB_OFFSET}", defaultSubOffsetStr);
|
||||||
return Content(js, "application/javascript");
|
return Content(js, "application/javascript");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,13 @@ namespace JellyfinPlugin.Configuration
|
|||||||
// Czy wtyczka jest aktywna
|
// Czy wtyczka jest aktywna
|
||||||
public bool IsEnabled { get; set; } = true;
|
public bool IsEnabled { get; set; } = true;
|
||||||
|
|
||||||
// Tryb odtwarzania: "Server" (FFmpeg wrapper), "Client" (CSS injection w Web UI), "Both" (Obydwa naraz)
|
// Tryb odtwarzania: "Server" (FFmpeg wrapper), "Client" (CSS injection w Web UI), "Auto" (Automatyczny: CSS dla Web, FFmpeg dla aplikacji natywnych)
|
||||||
public string PlaybackMode { get; set; } = "Both";
|
public string PlaybackMode { get; set; } = "Auto";
|
||||||
|
|
||||||
|
// Maksymalna liczba równoległych wątków skanowania czarnych pasów (1 - 16, domyślnie 4)
|
||||||
|
public int MaxParallelScans { get; set; } = 4;
|
||||||
|
|
||||||
|
// Przesunięcie pionowe napisów w vh w trybie klienckim CSS (0.0 - 25.0, domyślnie 5.0)
|
||||||
|
public double SubtitleOffsetVh { get; set; } = 5.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,11 +26,23 @@
|
|||||||
<div class="selectContainer">
|
<div class="selectContainer">
|
||||||
<label class="selectLabel" for="selectPlaybackMode">Black Bar Removal Method:</label>
|
<label class="selectLabel" for="selectPlaybackMode">Black Bar Removal Method:</label>
|
||||||
<select is="emby-select" id="selectPlaybackMode" class="emby-select-withcolor">
|
<select is="emby-select" id="selectPlaybackMode" class="emby-select-withcolor">
|
||||||
|
<option value="Auto">Auto (CSS clip in Web, Transcode on Native Apps / Transcoded streams)</option>
|
||||||
<option value="Server">Server Only (FFmpeg Transcoding)</option>
|
<option value="Server">Server Only (FFmpeg Transcoding)</option>
|
||||||
<option value="Client">Client Only (CSS Clip in Jellyfin Web)</option>
|
<option value="Client">Client Only (CSS Clip in Jellyfin Web)</option>
|
||||||
<option value="Both">Both (Transcoding or CSS, depending on client)</option>
|
|
||||||
</select>
|
</select>
|
||||||
<div class="fieldDescription">Choose how the plugin should remove black bars. Server option forces transcoding (CPU/GPU load), Client option is lightweight but only works in web browsers.</div>
|
<div class="fieldDescription">Choose how the plugin should remove black bars. Auto mode uses lightweight CSS clipping for Web browser DirectPlay, and falls back to FFmpeg transcoding for native apps or unsupported video formats.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="txtMaxParallelScans">Max Parallel Scans (1 - 16):</label>
|
||||||
|
<input type="number" step="1" min="1" max="16" id="txtMaxParallelScans" is="emby-input" required />
|
||||||
|
<div class="fieldDescription">Number of files to analyze concurrently during library scheduled tasks. Default is 4. Higher values speed up library scans on multi-core CPUs.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="txtSubtitleOffsetVh">Client Subtitle Offset (0.0 - 25.0 vh):</label>
|
||||||
|
<input type="number" step="0.5" min="0.0" max="25.0" id="txtSubtitleOffsetVh" is="emby-input" required />
|
||||||
|
<div class="fieldDescription">Vertical offset shift for subtitles in Jellyfin Web player (in viewport height % / vh). Default is 5.0. Increase to move subtitles higher up into the video, or decrease if they appear too high.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -53,7 +65,11 @@
|
|||||||
ApiClient.getPluginConfiguration(BlackBarRemoverConfig.pluginUniqueId).then(function (config) {
|
ApiClient.getPluginConfiguration(BlackBarRemoverConfig.pluginUniqueId).then(function (config) {
|
||||||
$('#chkEnablePlugin', page).prop('checked', config.IsEnabled !== false);
|
$('#chkEnablePlugin', page).prop('checked', config.IsEnabled !== false);
|
||||||
$('#txtCropLimit', page).val(config.CropDetectLimit || 0.094);
|
$('#txtCropLimit', page).val(config.CropDetectLimit || 0.094);
|
||||||
$('#selectPlaybackMode', page).val(config.PlaybackMode || "Both");
|
var mode = config.PlaybackMode || "Auto";
|
||||||
|
if (mode === "Both") mode = "Auto";
|
||||||
|
$('#selectPlaybackMode', page).val(mode);
|
||||||
|
$('#txtMaxParallelScans', page).val(config.MaxParallelScans || 4);
|
||||||
|
$('#txtSubtitleOffsetVh', page).val(config.SubtitleOffsetVh !== undefined ? config.SubtitleOffsetVh : 5.0);
|
||||||
Dashboard.hideLoadingMsg();
|
Dashboard.hideLoadingMsg();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -67,6 +83,8 @@
|
|||||||
config.IsEnabled = $('#chkEnablePlugin', form).prop('checked');
|
config.IsEnabled = $('#chkEnablePlugin', form).prop('checked');
|
||||||
config.CropDetectLimit = parseFloat($('#txtCropLimit', form).val());
|
config.CropDetectLimit = parseFloat($('#txtCropLimit', form).val());
|
||||||
config.PlaybackMode = $('#selectPlaybackMode', form).val();
|
config.PlaybackMode = $('#selectPlaybackMode', form).val();
|
||||||
|
config.MaxParallelScans = parseInt($('#txtMaxParallelScans', form).val(), 10) || 4;
|
||||||
|
config.SubtitleOffsetVh = parseFloat($('#txtSubtitleOffsetVh', form).val());
|
||||||
|
|
||||||
ApiClient.updatePluginConfiguration(BlackBarRemoverConfig.pluginUniqueId, config).then(function (result) {
|
ApiClient.updatePluginConfiguration(BlackBarRemoverConfig.pluginUniqueId, config).then(function (result) {
|
||||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ namespace JellyfinPlugin
|
|||||||
return;
|
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 ---
|
// --- HLS STREAM QUERY REWRITE ---
|
||||||
// If the client requests the m3u8 playlist, we simply tell Jellyfin NOT to allow video stream copy.
|
// 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!
|
// This elegantly forces transcoding for the BEST supported codec without breaking FFmpeg tags/filters!
|
||||||
@@ -147,5 +155,43 @@ namespace JellyfinPlugin
|
|||||||
|
|
||||||
await nextMw().ConfigureAwait(false);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,15 +42,21 @@ namespace JellyfinPlugin.Tasks
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
int total = items.Count;
|
int total = items.Count;
|
||||||
|
if (total == 0) return;
|
||||||
|
|
||||||
int processed = 0;
|
int processed = 0;
|
||||||
|
int maxParallel = Math.Clamp(config.MaxParallelScans, 1, 16);
|
||||||
|
|
||||||
foreach (var item in items)
|
var parallelOptions = new ParallelOptions
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
MaxDegreeOfParallelism = maxParallel,
|
||||||
|
CancellationToken = cancellationToken
|
||||||
// Wymuszamy skanowanie, więc nie sprawdzamy czy ma już BlackBarCrop
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("Starting Force Rescan of {Total} items with {MaxParallel} parallel workers.", total, maxParallel);
|
||||||
|
|
||||||
|
await Parallel.ForEachAsync(items, parallelOptions, async (item, ct) =>
|
||||||
|
{
|
||||||
var path = item.Path;
|
var path = item.Path;
|
||||||
if (!string.IsNullOrEmpty(path) && File.Exists(path))
|
if (!string.IsNullOrEmpty(path) && File.Exists(path))
|
||||||
{
|
{
|
||||||
@@ -65,7 +71,7 @@ namespace JellyfinPlugin.Tasks
|
|||||||
item.ProviderIds = new Dictionary<string, string>();
|
item.ProviderIds = new Dictionary<string, string>();
|
||||||
}
|
}
|
||||||
item.ProviderIds["BlackBarCrop"] = cropData;
|
item.ProviderIds["BlackBarCrop"] = cropData;
|
||||||
await _libraryManager.UpdateItemAsync(item, item, ItemUpdateType.MetadataEdit, cancellationToken);
|
await _libraryManager.UpdateItemAsync(item, item, ItemUpdateType.MetadataEdit, ct);
|
||||||
_logger.LogInformation("Found black bars {Crop} in {Path}", cropData, path);
|
_logger.LogInformation("Found black bars {Crop} in {Path}", cropData, path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,9 +81,9 @@ namespace JellyfinPlugin.Tasks
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processed++;
|
int current = Interlocked.Increment(ref processed);
|
||||||
progress.Report((processed / (double)total) * 100);
|
progress.Report((current / (double)total) * 100);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||||
|
|||||||
@@ -42,19 +42,28 @@ namespace JellyfinPlugin.Tasks
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
int total = items.Count;
|
int total = items.Count;
|
||||||
|
if (total == 0) return;
|
||||||
|
|
||||||
int processed = 0;
|
int processed = 0;
|
||||||
|
int maxParallel = Math.Clamp(config.MaxParallelScans, 1, 16);
|
||||||
|
|
||||||
foreach (var item in items)
|
var parallelOptions = new ParallelOptions
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
MaxDegreeOfParallelism = maxParallel,
|
||||||
|
CancellationToken = cancellationToken
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("Starting Retroactive Scan of {Total} items with {MaxParallel} parallel workers.", total, maxParallel);
|
||||||
|
|
||||||
|
await Parallel.ForEachAsync(items, parallelOptions, async (item, ct) =>
|
||||||
|
{
|
||||||
if (item.ProviderIds != null &&
|
if (item.ProviderIds != null &&
|
||||||
item.ProviderIds.TryGetValue("BlackBarCrop", out string? existingCrop) &&
|
item.ProviderIds.TryGetValue("BlackBarCrop", out string? existingCrop) &&
|
||||||
!string.IsNullOrEmpty(existingCrop))
|
!string.IsNullOrEmpty(existingCrop))
|
||||||
{
|
{
|
||||||
processed++;
|
int currentSkipped = Interlocked.Increment(ref processed);
|
||||||
progress.Report((processed / (double)total) * 100);
|
progress.Report((currentSkipped / (double)total) * 100);
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = item.Path;
|
var path = item.Path;
|
||||||
@@ -71,7 +80,7 @@ namespace JellyfinPlugin.Tasks
|
|||||||
item.ProviderIds = new Dictionary<string, string>();
|
item.ProviderIds = new Dictionary<string, string>();
|
||||||
}
|
}
|
||||||
item.ProviderIds["BlackBarCrop"] = cropData;
|
item.ProviderIds["BlackBarCrop"] = cropData;
|
||||||
await _libraryManager.UpdateItemAsync(item, item, ItemUpdateType.MetadataEdit, cancellationToken);
|
await _libraryManager.UpdateItemAsync(item, item, ItemUpdateType.MetadataEdit, ct);
|
||||||
_logger.LogInformation("Found black bars {Crop} in {Path}", cropData, path);
|
_logger.LogInformation("Found black bars {Crop} in {Path}", cropData, path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,9 +90,9 @@ namespace JellyfinPlugin.Tasks
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processed++;
|
int currentDone = Interlocked.Increment(ref processed);
|
||||||
progress.Report((processed / (double)total) * 100);
|
progress.Report((currentDone / (double)total) * 100);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ if [ -n ""$INPUT_FILE"" ]; then
|
|||||||
if [ $next_idx -le $# ]; then
|
if [ $next_idx -le $# ]; then
|
||||||
next_arg=""${{!next_idx}}""
|
next_arg=""${{!next_idx}}""
|
||||||
fi
|
fi
|
||||||
NEW_ARGS+=(""$next_arg,crop=$CROP_DATA"")
|
NEW_ARGS+=(""crop=$CROP_DATA,$next_arg"")
|
||||||
let i++
|
let i++
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -13,19 +13,31 @@ A Jellyfin plugin that automatically detects and removes black bars (letterboxin
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Jellyfin Server **12.0** or newer (.NET 10).
|
- Jellyfin Server **10.11** or **12.0+** (.NET 9 / .NET 10).
|
||||||
- FFmpeg installed on the server.
|
- FFmpeg installed on the server.
|
||||||
|
|
||||||
|
## Tested Versions
|
||||||
|
|
||||||
|
This plugin has been verified and tested on:
|
||||||
|
- **Jellyfin 10.11.x** (.NET 9.0)
|
||||||
|
- **Jellyfin 12.0 / 13.0 Unstable** (.NET 10.0)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Method 1: Jellyfin Repository (Recommended & Easiest)
|
### Method 1: Jellyfin Repository (Recommended & Easiest)
|
||||||
|
|
||||||
1. Open your Jellyfin Dashboard and navigate to **Plugins** -> **Repositories**.
|
> [!TIP]
|
||||||
2. Click the `+` icon to add a new repository.
|
> **Plugin Repository URL**:
|
||||||
3. Enter `Black Bar Remover` as the name.
|
> ```text
|
||||||
4. Enter `https://gitea.labus.uk/karol/blackBarRemover/raw/branch/manifest/manifest.json` as the Repository URL and save.
|
> https://gitea.labus.uk/karol/blackBarRemover/raw/branch/master/manifest.json
|
||||||
5. Go back to the **Catalog** tab, find "Black Bar Remover" under the Playback category, and click **Install**.
|
> ```
|
||||||
6. Restart the Jellyfin server.
|
|
||||||
|
1. Open your Jellyfin Dashboard and navigate to **Dashboard** -> **Plugins** -> **Repositories** (`Repositories` tab).
|
||||||
|
2. Click the **`+`** icon to add a new repository.
|
||||||
|
3. Enter **`Black Bar Remover`** as the Repository Name.
|
||||||
|
4. Copy and paste the **Repository URL** above into the **Repository URL** field, then click **Save**.
|
||||||
|
5. Switch to the **Catalog** tab, find **Black Bar Remover** (under the *Playback* category), and click **Install**.
|
||||||
|
6. Restart your Jellyfin server.
|
||||||
|
|
||||||
### Method 2: Manual Installation from Releases
|
### Method 2: Manual Installation from Releases
|
||||||
|
|
||||||
@@ -37,22 +49,34 @@ A Jellyfin plugin that automatically detects and removes black bars (letterboxin
|
|||||||
### Method 3: Building from Source
|
### Method 3: Building from Source
|
||||||
|
|
||||||
1. Clone this repository.
|
1. Clone this repository.
|
||||||
2. Build the plugin using the .NET 9 SDK:
|
2. Ensure you have the **.NET 9.0 SDK** and/or **.NET 10.0 SDK** installed (the project multi-targets both `net9.0` and `net10.0`).
|
||||||
|
3. Build the plugin for all targets:
|
||||||
```bash
|
```bash
|
||||||
dotnet build JellyfinPlugin/JellyfinPlugin.csproj -c Release
|
dotnet build JellyfinPlugin/JellyfinPlugin.csproj -c Release
|
||||||
```
|
```
|
||||||
3. Copy the compiled DLLs from `JellyfinPlugin/bin/Release/net9.0/` into a new folder named `BlackBarRemover` inside your Jellyfin plugins directory.
|
Or build for a specific target framework:
|
||||||
4. Restart the Jellyfin server.
|
```bash
|
||||||
|
# For Jellyfin 10.11 (.NET 9.0)
|
||||||
|
dotnet build JellyfinPlugin/JellyfinPlugin.csproj -c Release -f net9.0
|
||||||
|
|
||||||
|
# For Jellyfin 12.0 / 13.0 Unstable (.NET 10.0)
|
||||||
|
dotnet build JellyfinPlugin/JellyfinPlugin.csproj -c Release -f net10.0
|
||||||
|
```
|
||||||
|
4. Copy the compiled DLLs from `JellyfinPlugin/bin/Release/net9.0/` (for Jellyfin 10.11) or `JellyfinPlugin/bin/Release/net10.0/` (for Jellyfin 12.0+) into a new folder named `BlackBarRemover` inside your Jellyfin plugins directory (e.g., `/var/lib/jellyfin/plugins/BlackBarRemover/`).
|
||||||
|
5. Restart the Jellyfin server.
|
||||||
|
|
||||||
### Configuration & Initial Scan
|
### Configuration & Initial Scan
|
||||||
|
|
||||||
1. Go to **Dashboard** -> **Plugins** -> **Black Bar Remover**.
|
1. Go to **Dashboard** -> **Plugins** -> **Black Bar Remover**.
|
||||||
2. Set your **Crop Detect Limit** (Default is 0.15). If your web-dl rips have noisy black bars that are not being cropped, increase this value to 0.20 or 0.25.
|
2. Set your **Crop Detect Limit** (Default is `0.094`). If your web-dl rips have noisy black bars that are not being cropped, increase this value to `0.15` or `0.20`.
|
||||||
3. Select your preferred **Playback Mode**:
|
3. Set **Max Parallel Scans** (Default is `4`, Range `1 - 16`). Controls how many video files are scanned concurrently during library tasks to maximize multi-core CPU utilization.
|
||||||
|
4. Set **Client Subtitle Offset** (Default is `5.0 vh`, Range `0.0 - 25.0 vh`). Adjusts the vertical offset of subtitles when CSS crop is active in the Web player.
|
||||||
|
5. Select your preferred **Playback Mode**:
|
||||||
|
- `Auto`: Lightweight CSS clipping for Web browser DirectPlay; FFmpeg transcoding for native apps.
|
||||||
- `Client`: Uses CSS injection in the web player.
|
- `Client`: Uses CSS injection in the web player.
|
||||||
- `Server`: Requires setting up the FFmpeg wrapper.
|
- `Server`: Requires setting up the FFmpeg wrapper.
|
||||||
- `Both`: Attempts both methods depending on the client.
|
6. **Live Subtitle Adjustment during Playback**: While watching a video in Jellyfin Web with CSS crop enabled, you can press **`Alt + ArrowUp`** or **`Alt + ArrowDown`** to adjust the subtitle vertical position on the fly in real-time with on-screen feedback!
|
||||||
4. **Important for existing libraries**: The plugin automatically scans *new* files when they are added to Jellyfin. For your existing library, go to **Dashboard -> Scheduled Tasks** and run the **"Scan Missing Black Bars (Black Bar Remover)"** task manually. It will scan all existing movies and episodes in the background.
|
7. **Important for existing libraries**: The plugin automatically scans *new* files when they are added to Jellyfin. For your existing library, go to **Dashboard -> Scheduled Tasks** and run the **"Scan Missing Black Bars (Black Bar Remover)"** task manually. It will scan all existing movies and episodes in the background.
|
||||||
|
|
||||||
### Activating the Server-Side Wrapper (Automatic)
|
### Activating the Server-Side Wrapper (Automatic)
|
||||||
|
|
||||||
|
|||||||
+208
-8
@@ -9,20 +9,220 @@
|
|||||||
"imageUrl": "",
|
"imageUrl": "",
|
||||||
"versions": [
|
"versions": [
|
||||||
{
|
{
|
||||||
"version": "0.1.17",
|
"version": "0.1.22",
|
||||||
"changelog": "Automated Release v0.1.17-beta (Jellyfin 12.0)",
|
"changelog": "Release v0.1.22-beta (Jellyfin 12.0)",
|
||||||
"targetAbi": "12.0.0.0",
|
"targetAbi": "12.0.0.0",
|
||||||
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.17-beta/BlackBarRemover_v0.1.17-beta_jf12.0.zip",
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.22-beta/BlackBarRemover_v0.1.22-beta_jf12.0.zip",
|
||||||
"checksum": "319a6a68e2b28f02f96787e6193eaf50",
|
"checksum": "55d64618d81ea1091200c5566cd771e5",
|
||||||
"timestamp": "2026-09-17T19:33:27Z"
|
"timestamp": "2026-09-19T20:27:28Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.22",
|
||||||
|
"changelog": "Release v0.1.22-beta (Jellyfin 10.11)",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.22-beta/BlackBarRemover_v0.1.22-beta_jf10.11.zip",
|
||||||
|
"checksum": "e18d3599973ebedba08de6494cfed3cb",
|
||||||
|
"timestamp": "2026-09-19T20:27:28Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.21",
|
||||||
|
"changelog": "Release v0.1.21-beta (Jellyfin 12.0)",
|
||||||
|
"targetAbi": "12.0.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.21-beta/BlackBarRemover_v0.1.21-beta_jf12.0.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-18T00:14:20+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.21",
|
||||||
|
"changelog": "Release v0.1.21-beta (Jellyfin 10.11)",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.21-beta/BlackBarRemover_v0.1.21-beta_jf10.11.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-18T00:14:20+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.20",
|
||||||
|
"changelog": "Release v0.1.20-beta (Jellyfin 12.0)",
|
||||||
|
"targetAbi": "12.0.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.20-beta/BlackBarRemover_v0.1.20-beta_jf12.0.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T23:45:19+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.20",
|
||||||
|
"changelog": "Release v0.1.20-beta (Jellyfin 10.11)",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.20-beta/BlackBarRemover_v0.1.20-beta_jf10.11.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T23:45:19+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.19",
|
||||||
|
"changelog": "Release v0.1.19-beta (Jellyfin 12.0)",
|
||||||
|
"targetAbi": "12.0.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.19-beta/BlackBarRemover_v0.1.19-beta_jf12.0.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T21:56:15+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.19",
|
||||||
|
"changelog": "Release v0.1.19-beta (Jellyfin 10.11)",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.19-beta/BlackBarRemover_v0.1.19-beta_jf10.11.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T21:56:15+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.18",
|
||||||
|
"changelog": "Release v0.1.18-beta (Jellyfin 12.0)",
|
||||||
|
"targetAbi": "12.0.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.18-beta/BlackBarRemover_v0.1.18-beta_jf12.0.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T21:51:42+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.18",
|
||||||
|
"changelog": "Release v0.1.18-beta (Jellyfin 10.11)",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.18-beta/BlackBarRemover_v0.1.18-beta_jf10.11.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T21:51:42+02:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"version": "0.1.17",
|
"version": "0.1.17",
|
||||||
"changelog": "Automated Release v0.1.17-beta (Jellyfin 10.11)",
|
"changelog": "Release v0.1.17-beta (Jellyfin 12.0)",
|
||||||
|
"targetAbi": "12.0.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.17-beta/BlackBarRemover_v0.1.17-beta_jf12.0.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T21:32:32+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.17",
|
||||||
|
"changelog": "Release v0.1.17-beta (Jellyfin 10.11)",
|
||||||
"targetAbi": "10.11.0.0",
|
"targetAbi": "10.11.0.0",
|
||||||
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.17-beta/BlackBarRemover_v0.1.17-beta_jf10.11.zip",
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.17-beta/BlackBarRemover_v0.1.17-beta_jf10.11.zip",
|
||||||
"checksum": "6e8368d4964eeca0573c9fb1e98ecd72",
|
"checksum": "",
|
||||||
"timestamp": "2026-09-17T19:33:27Z"
|
"timestamp": "2026-09-17T21:32:32+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.11",
|
||||||
|
"changelog": "Release v0.1.11-beta (Jellyfin 12.0)",
|
||||||
|
"targetAbi": "12.0.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.11-beta/BlackBarRemover_v0.1.11-beta_jf12.0.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T21:05:29+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.11",
|
||||||
|
"changelog": "Release v0.1.11-beta (Jellyfin 10.11)",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.11-beta/BlackBarRemover_v0.1.11-beta_jf10.11.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T21:05:29+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.10",
|
||||||
|
"changelog": "Release v0.1.10-beta (Jellyfin 12.0)",
|
||||||
|
"targetAbi": "12.0.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.10-beta/BlackBarRemover_v0.1.10-beta_jf12.0.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T19:54:01+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.10",
|
||||||
|
"changelog": "Release v0.1.10-beta (Jellyfin 10.11)",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.10-beta/BlackBarRemover_v0.1.10-beta_jf10.11.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T19:54:01+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.9",
|
||||||
|
"changelog": "Release v0.1.9-beta (Jellyfin 12.0)",
|
||||||
|
"targetAbi": "12.0.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.9-beta/BlackBarRemover_v0.1.9-beta_jf12.0.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T19:39:22+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.9",
|
||||||
|
"changelog": "Release v0.1.9-beta (Jellyfin 10.11)",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.9-beta/BlackBarRemover_v0.1.9-beta_jf10.11.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T19:39:22+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.8",
|
||||||
|
"changelog": "Release v0.1.8-beta (Jellyfin 12.0)",
|
||||||
|
"targetAbi": "12.0.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.8-beta/BlackBarRemover_v0.1.8-beta_jf12.0.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T18:29:04+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.8",
|
||||||
|
"changelog": "Release v0.1.8-beta (Jellyfin 10.11)",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.8-beta/BlackBarRemover_v0.1.8-beta_jf10.11.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T18:29:04+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.7",
|
||||||
|
"changelog": "Release v0.1.7-beta (Jellyfin 12.0)",
|
||||||
|
"targetAbi": "12.0.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.7-beta/BlackBarRemover_v0.1.7-beta_jf12.0.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T18:23:27+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.7",
|
||||||
|
"changelog": "Release v0.1.7-beta (Jellyfin 10.11)",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.7-beta/BlackBarRemover_v0.1.7-beta_jf10.11.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T18:23:27+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.6",
|
||||||
|
"changelog": "Release v0.1.6-beta (Jellyfin 12.0)",
|
||||||
|
"targetAbi": "12.0.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.6-beta/BlackBarRemover_v0.1.6-beta_jf12.0.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T18:11:37+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.6",
|
||||||
|
"changelog": "Release v0.1.6-beta (Jellyfin 10.11)",
|
||||||
|
"targetAbi": "10.11.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.6-beta/BlackBarRemover_v0.1.6-beta_jf10.11.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T18:11:37+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.2",
|
||||||
|
"changelog": "Release v0.1.2",
|
||||||
|
"targetAbi": "10.9.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.2/BlackBarRemover_v0.1.2.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T14:52:19+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.1",
|
||||||
|
"changelog": "Release v0.1.1",
|
||||||
|
"targetAbi": "10.9.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1.1/BlackBarRemover_v0.1.1.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T14:45:08+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1",
|
||||||
|
"changelog": "Release v0.1-beta",
|
||||||
|
"targetAbi": "10.9.0.0",
|
||||||
|
"sourceUrl": "https://gitea.labus.uk/karol/blackBarRemover/releases/download/v0.1-beta/BlackBarRemover_v0.1-beta.zip",
|
||||||
|
"checksum": "",
|
||||||
|
"timestamp": "2026-09-17T14:37:16+02:00"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user