Compare commits

..

27 Commits

Author SHA1 Message Date
CodeDevMLH
1e18c22937 Update manifest.json for release v1.6.1.22 [skip ci] 2026-02-14 00:43:45 +00:00
CodeDevMLH
a83913d15c Bump version to 1.6.1.22
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-14 01:42:53 +01:00
CodeDevMLH
2f50931beb Fix YouTube player readiness checks and improve polling logic for video playback 2026-02-14 01:42:42 +01:00
CodeDevMLH
5b14bdba35 Update manifest.json for release v1.6.1.21 [skip ci] 2026-02-14 00:35:03 +00:00
CodeDevMLH
9ba3b1e49f Bump version to 1.6.1.21
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-14 01:34:12 +01:00
CodeDevMLH
bf7c7fb8e8 Enhance video backdrop handling to support YouTube iframe integration and improve video playback logic 2026-02-14 01:33:54 +01:00
CodeDevMLH
39e29046de Update manifest.json for release v1.6.1.20 [skip ci] 2026-02-14 00:10:58 +00:00
CodeDevMLH
18260f8eac Bump version to 1.6.1.20
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-14 01:10:09 +01:00
CodeDevMLH
59c07f3c45 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/jellyfin-plugin-media-bar-enhanced
Some checks failed
Auto Release Plugin / build-and-release (push) Has been cancelled
2026-02-14 01:09:50 +01:00
CodeDevMLH
b06d1e9375 Enhance video playback logic to handle pending play state and improve pause behavior for video backdrops 2026-02-14 01:09:48 +01:00
CodeDevMLH
e5bf23a7bc Enhance README with detailed configuration options for Custom Media IDs, Content Sorting, and Content Limits 2026-02-14 01:09:29 +01:00
CodeDevMLH
0d7113969b Improve clarity and consistency in configuration page labels and descriptions 2026-02-14 01:09:16 +01:00
CodeDevMLH
f69f676a68 Update manifest.json for release v1.6.1.19 [skip ci] 2026-02-13 18:44:00 +00:00
CodeDevMLH
f448c89ef2 Bump version to 1.6.1.19
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-13 19:43:04 +01:00
CodeDevMLH
daf26fe53a Refactor slide playback logic to remove redundant checks and improve clarity 2026-02-13 19:42:49 +01:00
CodeDevMLH
26ef307838 Update manifest.json for release v1.6.1.18 [skip ci] 2026-02-13 18:35:24 +00:00
CodeDevMLH
c296483583 Bump version to 1.6.1.18
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-13 19:34:31 +01:00
CodeDevMLH
7992e20715 Refactor slide playback logic to improve active slide checks and remove commented code 2026-02-13 19:34:13 +01:00
CodeDevMLH
1ae59f5da5 Update manifest.json for release v1.6.1.17 [skip ci] 2026-02-13 13:49:23 +00:00
CodeDevMLH
92eaf91173 Bump version to 1.6.1.17
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m2s
2026-02-13 14:48:24 +01:00
CodeDevMLH
e7410ec22a Fix slideshow preloading logic to prevent redundant slide creation 2026-02-13 14:48:07 +01:00
CodeDevMLH
bb43d1e679 Update manifest.json for release v1.6.1.16 [skip ci] 2026-02-13 02:53:20 +00:00
CodeDevMLH
b6609d23a2 Bump version to 1.6.1.16
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-13 03:52:27 +01:00
CodeDevMLH
9d4cbf37d3 Refactor slideshow state management and improve video playback logic 2026-02-13 03:52:07 +01:00
CodeDevMLH
b5e63ef3b7 Update manifest.json for release v1.6.1.15 [skip ci] 2026-02-13 02:41:24 +00:00
CodeDevMLH
22f9906188 Bump version to 1.6.1.15
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-13 03:40:35 +01:00
CodeDevMLH
ae54ab41a8 Refactor video playback logic to simplify conditions for active slides 2026-02-13 03:40:23 +01:00
5 changed files with 172 additions and 50 deletions

View File

@@ -115,14 +115,10 @@
</div>
<div id="customMediaIdsContainer">
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Default Media/Collection/Playlist
IDs
(Newline or Comma separated)</label>
<label class="inputLabel inputLabelUnfocused" for="CustomMediaIds">Default Media/Collection/Playlist IDs (Newline or Comma-separated)</label>
<textarea class="emby-textarea" is="emby-textarea" id="CustomMediaIds" name="CustomMediaIds"
style="width: 100%; height: 150px; font-family: monospace;"></textarea>
<div class="fieldDescription">Enter the IDs of the items you want to show in the slideshow as
your default content.
You can separate them by new line or comma.
<div class="fieldDescription">Enter the IDs of the items you want to show in the slideshow as your default content. You can separate them by new line or comma.
<br><br>
<b>Manual Trailer/Video Override:</b> You can specify a YouTube URL <b>OR</b> a Jellyfin Item ID (e.g. for a Theme Video) for an item by adding it in
brackets: <br> <code>e.g. ID DESCRIPTION [https://youtu.be/...]</code> or <code>ID [JellyfinItemId] DESCRIPTION</code>.
@@ -132,11 +128,9 @@
<li><b>YouTube URL:</b> Play a remote trailer from YouTube.</li>
<li><b>Jellyfin Item ID (GUID):</b> Play the video of another library item (e.g. a Theme Video or Backdrop Video) using the native player.</li>
</ul>
You can also add a description after the ID using any separator like space, pipe
(|) or dash (-): <br>e.g. <code>ID DESCRIPTION</code> or <code>ID | DESCRIPTION</code>
You can also add a description after the ID using any separator like space, pipe (|) or dash (-): <br>e.g. <code>ID DESCRIPTION</code> or <code>ID | DESCRIPTION</code>
<br><br>
<b>Note:</b> If using a <b>Collection Name</b> (instead of an ID) combined with a description, you <b>MUST</b> use
the pipe (|) separator.
<b>Note:</b> If using a <b>Collection Name</b> (instead of an ID) combined with a description, you <b>MUST</b> use the pipe (|) separator.
<br>
<b>Note:</b> The separator <b>MUST NOT</b> be a hex character (0-9, a-f).
</div>
@@ -198,7 +192,7 @@
<input is="emby-checkbox" type="checkbox" id="UseSponsorBlock" name="UseSponsorBlock" />
<span>Use SponsorBlock</span>
</label>
<div class="fieldDescription">Skip intro/outro segments in YouTube trailers.</div>
<div class="fieldDescription">Skip intro/outro segments in YouTube trailers (if data is available).</div>
</div>
<div class="selectContainer">
<label class="selectLabel" for="PreferredVideoQuality">Preferred YouTube Quality</label>
@@ -603,7 +597,7 @@
'<div class="inputContainer">' +
' <label class="inputLabel" style="margin-bottom:0.5em; display:block;">Media IDs</label>' +
' <textarea is="emby-textarea" class="emby-textarea section-ids" style="width: 100%; height: 80px; font-family: monospace;">' + (data.MediaIds || '') + '</textarea>' +
' <div class="fieldDescription">Comma-separated list of Movie/Series/Collection IDs to show during this season.</div>' +
' <div class="fieldDescription">Comma-separated or Newline separated list of Movie/Series/Collection IDs to show during this season.<br>Same options available as for the default media IDs.</div>' +
'</div>';
div.querySelector('.remove-section').addEventListener('click', function() {

View File

@@ -12,7 +12,7 @@
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
<Title>Jellyfin Media Bar Enhanced Plugin</Title>
<Authors>CodeDevMLH</Authors>
<Version>1.6.1.14</Version>
<Version>1.6.1.22</Version>
<RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-media-bar-enhanced</RepositoryUrl>
</PropertyGroup>

View File

@@ -1490,7 +1490,6 @@ const VisibilityObserver = {
* Initializes visibility observer
*/
init() {
// MARK: Mark
const observer = new MutationObserver(() => this.updateVisibility());
// let debounceTimer = null;
// const observer = new MutationObserver(() => {
@@ -1720,6 +1719,13 @@ const SlideCreator = {
if (iframe) {
iframe.setAttribute('tabindex', '-1');
iframe.setAttribute('inert', '');
// Preserve video-backdrop class on the iframe (YT API replaces the original div)
iframe.classList.add('backdrop', 'video-backdrop');
if (CONFIG.fullWidthVideo) {
iframe.classList.add('video-backdrop-full');
} else {
iframe.classList.add('video-backdrop-default');
}
}
// Store start/end time and videoId for later use
@@ -1742,14 +1748,14 @@ const SlideCreator = {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
const isVideoPlayerOpen = document.querySelector('.videoPlayerContainer') || document.querySelector('.youtubePlayerContainer');
if (slide && slide.classList.contains('active') && !document.hidden && (!isVideoPlayerOpen || isVideoPlayerOpen.classList.contains('hide'))) {
const currentIndex = STATE.slideshow.currentSlideIndex;
const currentItemId = STATE.slideshow.itemIds[currentIndex];
if (currentItemId !== itemId) {
console.log(`Slide ${itemId} is no longer active (current: ${currentItemId}), aborting playback.`);
event.target.mute(); // Mute just in case
return;
}
// Check _pendingPlay flag (set by playCurrentVideo when player wasn't ready yet)
const container = document.getElementById(`youtube-player-${itemId}`);
const hasPendingPlay = container && container._pendingPlay;
if (container) container._pendingPlay = false;
const isActiveAndVisible = slide && slide.classList.contains('active') && !document.hidden && (!isVideoPlayerOpen || isVideoPlayerOpen.classList.contains('hide'));
if ((isActiveAndVisible || hasPendingPlay) && !STATE.slideshow.isPaused) {
event.target.playVideo();
// Check if it actually started playing after a short delay (handling autoplay blocks)
@@ -1826,10 +1832,8 @@ const SlideCreator = {
backdrop.addEventListener('play', (event) => {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
const currentIndex = STATE.slideshow.currentSlideIndex;
const currentItemId = STATE.slideshow.itemIds[currentIndex];
if (!slide || !slide.classList.contains('active') || currentItemId !== itemId) {
if (!slide || !slide.classList.contains('active')) {
console.log(`Local video ${itemId} started playing but is not active, pausing.`);
event.target.pause();
event.target.currentTime = 0;
@@ -2314,8 +2318,6 @@ const SlideshowManager = {
}
currentSlide.classList.add("active");
STATE.slideshow.currentSlideIndex = index;
// Restore focus for TV mode navigation continuity
requestAnimationFrame(() => {
@@ -2337,7 +2339,34 @@ const SlideshowManager = {
// Manage Video Playback: Stop others, Play current
this.pauseOtherVideos(currentItemId);
this.playCurrentVideo(currentSlide, currentItemId);
// Check for video backdrop (also check by YouTube player ID since YT replaces div with iframe)
const hasVideoBackdrop = !!(currentSlide.querySelector('.video-backdrop') ||
currentSlide.querySelector(`#youtube-player-${currentItemId}`) ||
(STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]));
// If paused and new slide has video, un-pause for video playback.
// If paused and new slide has only images, stay paused.
if (STATE.slideshow.isPaused && hasVideoBackdrop) {
STATE.slideshow.isPaused = false;
const pauseButton = document.querySelector('.pause-button');
if (pauseButton) {
pauseButton.innerHTML = '<i class="material-icons">pause</i>';
const pauseLabel = LocalizationUtils.getLocalizedString('ButtonPause', 'Pause');
pauseButton.setAttribute('aria-label', pauseLabel);
pauseButton.setAttribute('title', pauseLabel);
}
}
if (!STATE.slideshow.isPaused) {
this.playCurrentVideo(currentSlide, currentItemId);
} else {
// Still update mute button visibility based on video presence
const muteButton = document.querySelector('.mute-button');
if (muteButton) {
muteButton.style.display = hasVideoBackdrop ? 'block' : 'none';
}
}
const enableAnimations = MediaBarEnhancedSettingsManager.getSetting('slideAnimations', CONFIG.slideAnimationEnabled);
@@ -2350,6 +2379,8 @@ const SlideshowManager = {
if (logo) logo.classList.add("animate");
}
STATE.slideshow.currentSlideIndex = index;
if (index === 0 || !previousVisibleSlide) {
const dotsContainer = container.querySelector(".dots-container");
if (dotsContainer) {
@@ -2370,7 +2401,9 @@ const SlideshowManager = {
this.updateDots();
// Only restart interval if we are NOT waiting for a video to end
const hasVideo = currentSlide.querySelector('.video-backdrop');
const hasVideo = currentSlide.querySelector('.video-backdrop') ||
currentSlide.querySelector(`#youtube-player-${currentItemId}`) ||
(STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]);
if (STATE.slideshow.slideInterval && !STATE.slideshow.isPaused) {
if (CONFIG.waitForTrailerToEnd && hasVideo) {
STATE.slideshow.slideInterval.stop();
@@ -2433,6 +2466,8 @@ const SlideshowManager = {
// Preload next slides
for (let i = 1; i <= preloadCount; i++) {
const nextIndex = (currentIndex + i) % totalItems;
if (nextIndex === currentIndex) break;
const itemId = STATE.slideshow.itemIds[nextIndex];
SlideCreator.createSlideForItemId(itemId);
}
@@ -2440,6 +2475,8 @@ const SlideshowManager = {
// Preload previous slides
for (let i = 1; i <= preloadCount; i++) {
const prevIndex = (currentIndex - i + totalItems) % totalItems;
if (prevIndex === currentIndex) break;
const prevItemId = STATE.slideshow.itemIds[prevIndex];
SlideCreator.createSlideForItemId(prevItemId);
}
@@ -2628,7 +2665,9 @@ const SlideshowManager = {
// Only restart interval if we are NOT waiting for a video to end
const currentItemId = STATE.slideshow.itemIds[STATE.slideshow.currentSlideIndex];
const currentSlide = document.querySelector(`.slide[data-item-id="${currentItemId}"]`);
const hasVideo = currentSlide && currentSlide.querySelector('.video-backdrop');
const hasVideo = currentSlide && (currentSlide.querySelector('.video-backdrop') ||
currentSlide.querySelector(`#youtube-player-${currentItemId}`) ||
(STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]));
if (!CONFIG.waitForTrailerToEnd || !hasVideo) {
STATE.slideshow.slideInterval.start();
@@ -2687,17 +2726,24 @@ const SlideshowManager = {
* @returns {boolean} Whether a video was found and playback attempted
*/
playCurrentVideo(slide, itemId) {
const videoBackdrop = slide.querySelector('.video-backdrop');
// Try finding by class first, then fall back to YouTube player container by ID
let videoBackdrop = slide.querySelector('.video-backdrop');
if (!videoBackdrop) {
// YouTube API replaces the div with an iframe, which may not have the class yet
videoBackdrop = slide.querySelector(`#youtube-player-${itemId}`);
}
// Also check if a player exists in the registry even if no DOM element found
const hasRegisteredPlayer = !!(STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[itemId]);
// Update mute button visibility
const muteButton = document.querySelector('.mute-button');
if (muteButton) {
muteButton.style.display = videoBackdrop ? 'block' : 'none';
muteButton.style.display = (videoBackdrop || hasRegisteredPlayer) ? 'block' : 'none';
}
if (!videoBackdrop) return false;
if (!videoBackdrop && !hasRegisteredPlayer) return false;
if (videoBackdrop.tagName === 'VIDEO') {
if (videoBackdrop && videoBackdrop.tagName === 'VIDEO') {
videoBackdrop.currentTime = 0;
videoBackdrop.muted = STATE.slideshow.isMuted;
if (!STATE.slideshow.isMuted) videoBackdrop.volume = 0.4;
@@ -2715,8 +2761,10 @@ const SlideshowManager = {
}
// YouTube player
const player = STATE.slideshow.videoPlayers?.[itemId];
if (player && typeof player.loadVideoById === 'function' && player._videoId) {
const player = STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[itemId];
const playerIsReady = player && typeof player.loadVideoById === 'function' && player._videoId;
if (playerIsReady) {
player.loadVideoById({
videoId: player._videoId,
startSeconds: player._startTime || 0,
@@ -2742,11 +2790,64 @@ const SlideshowManager = {
}
}, 1000);
return true;
} else if (player && typeof player.seekTo === 'function') {
// Fallback if loadVideoById is not available or videoId missing but player object exists
const startTime = player._startTime || 0;
player.seekTo(startTime);
player.playVideo();
}
// YouTube player exists but NOT fully ready yet.
// new YT.Player() returns an object immediately, but API methods like
// loadVideoById and _videoId aren't available until onReady fires.
// onReady may have ALREADY fired (during preload), so we can't rely on
// _pendingPlay alone. Instead, poll for readiness.
const ytContainer = videoBackdrop || slide.querySelector(`[id^="youtube-player-"]`);
if (ytContainer && (ytContainer.tagName === 'IFRAME' || (ytContainer.id && ytContainer.id.startsWith('youtube-player-')))) {
console.log(`YouTube player for ${itemId} not ready yet, polling for readiness...`);
// Also set _pendingPlay as fallback in case onReady hasn't fired yet
ytContainer._pendingPlay = true;
let attempts = 0;
const maxAttempts = 25; // 25 * 200ms = 5 seconds max
const pollInterval = setInterval(() => {
attempts++;
// Stop if slide is no longer active (user navigated away)
if (!slide.classList.contains('active')) {
clearInterval(pollInterval);
return;
}
const p = STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[itemId];
if (p && typeof p.loadVideoById === 'function' && p._videoId) {
clearInterval(pollInterval);
console.log(`YouTube player for ${itemId} now ready (after ${attempts * 200}ms), starting playback`);
if (STATE.slideshow.isPaused) return;
p.loadVideoById({
videoId: p._videoId,
startSeconds: p._startTime || 0,
endSeconds: p._endTime
});
if (STATE.slideshow.isMuted) {
p.mute();
} else {
p.unMute();
p.setVolume(40);
}
// Pause slideshow timer when video starts if configured
if (CONFIG.waitForTrailerToEnd && STATE.slideshow.slideInterval) {
STATE.slideshow.slideInterval.stop();
}
return;
}
if (attempts >= maxAttempts) {
clearInterval(pollInterval);
console.warn(`YouTube player for ${itemId} failed to become ready after ${maxAttempts * 200}ms`);
}
}, 200);
return true;
}

View File

@@ -155,21 +155,48 @@ Configure the plugin via **Dashboard** > **Plugins** > **Media Bar Enhanced**.
Define exactly what shows up in your bar.
* **Enable Custom Media IDs**: Restrict the slideshow to a specific list of IDs.
* **Manual Trailer Override**: Add `[YouTube_URL]` after an ID to force a specific trailer.
* **Manual Trailer Override**: Add `[YouTube_URL]` or `[Jellyfin_ID]` after an ID to force a specific trailer/video.
* Example ID: `a1b2c3d4e5... [https://www.youtube.com/watch?v=VIDEO_ID]`
* Example ID: `z1b2c3d4e5... [Jellyfin_ID]`
* **Example Mixed List**:
```
a1b2c3d4e5f6... <-- Plays local item video
6bdu812812hd... [https://youtu.be/...] <-- Item metadata + Custom YouTube Trailer
12h44h124sf7... [hdc78127z4ff...] <-- Item metadata + Custom Jellyfin Trailer/Video etc.
```
* Example Collection Name: `Halloween Collection [https://...] | My Description` (Note: Use `|` to separate description from name if using a name instead of an ID)
* **Apply Limits to Custom IDs**: If enabled, the "Content Limits" (see below) will also apply to your Custom Media IDs list. By default, custom lists show all listed items regardless of limits.
* **Enable Seasonal Content Mode**: Advanced date-based scheduling.
* Format: `DD.MM-DD.MM | Name | ID1, ID2, ID3`
* Example: `20.10-31.10 | Halloween | <ID_OF_HALLOWEEN_COLLECTION>`
* If the current date matches a range, those IDs are used. Otherwise, it defaults to standard behavior or the Custom Media IDs list.
* **GUI Configuration**: You can easily add "Seasons" via the **Add Season** button.
* **Active Period**: Select the Start and End Day/Month for each season.
* **Media IDs**: Enter the Comma-separated list of IDs (Movies, Series, Collections) for that season.
* **Priority**: If the current date matches a defined season, those IDs are used. If multiple seasons overlap, the first matching one is used. If no season matches, it falls back to the Default Custom Media IDs.
**How to get IDs:**
Check the URL of an item in the web interface:
`.../web/#/details?id=YOUR_ITEM_ID_HERE&...`
### Content Sorting
Customize the order of slides in the Media Bar.
* **Sort By**: Choose criteria like *Random*, *Premiere Date*, *Production Year*, *Critic Rating*, *Community Rating*, *Name*, or *Runtime*.
* **Sort Order**: Ascending or Descending.
* **Note**: Sorting applies to both server-fetched content AND Custom Media IDs. Select **Original** to preserve the exact order of your Custom Media IDs list.
### Content Limits
Fine-tune performance by limiting the number of items fetched from the server.
* **Total Max Items**: Maximum total items to fetch (combined).
* **Max Movies**: Maximum movies to include (for random selection).
* **Max Tv Shows**: Maximum TV shows to include (for random selection).
* **Preload Count**: Number of slides to preload for smooth transitions.
* *Intelligent Preloading*: The plugin uses a safe preloading strategy that respects this count but handles small lists gracefully to avoid playback issues.
* **Max Pagination Dots**: Maximum number of dots to show. If exceeded, it switches to a counter (e.g., 1/20).
### Advanced Settings
* **Slide Animations**: Enable/disable the "Zoom In" effect.
* **Use SponsorBlock**: Skips non-content segments in YouTube trailers (if the data exists).
* **Preferred YouTube Quality**: Select your preferred resolution (*Auto*, *Maximum*, *1080p*, *720p*).
* **Start Muted**: Videos start without sound (user can unmute).
* **Full Width Video**: Stretches video to cover the entire width (good for desktop, crop on mobile).
* **Enable Loading Screen**: Enable/disable the loading indicator while the bar initializes.

View File

@@ -9,12 +9,12 @@
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
"versions": [
{
"version": "1.6.1.14",
"version": "1.6.1.22",
"changelog": "- fix tv mode issue\n- refactor video playback management",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.1.14/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "6af6dd96995a90bf91d811467c88da04",
"timestamp": "2026-02-13T02:23:28Z"
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.1.22/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "aec9d9b10eba6e4a540b8373c37fcd98",
"timestamp": "2026-02-14T00:43:44Z"
},
{
"version": "1.6.0.2",