Compare commits

..

28 Commits

Author SHA1 Message Date
CodeDevMLH
d3f6641158 Update manifest.json for release v1.6.6.0 [skip ci] 2026-02-19 01:01:21 +00:00
CodeDevMLH
c214a620e4 Bump version to 1.6.6.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-19 01:52:31 +01:00
CodeDevMLH
f0c9462878 Implement manual mapping for MediaBarIsEnabled and enhance video backdrop handling 2026-02-19 01:52:17 +01:00
CodeDevMLH
e12a5b56a2 Update manifest.json for release v1.6.5.2 [skip ci] 2026-02-16 23:57:58 +00:00
CodeDevMLH
51ff0f2623 Bump version to 1.6.5.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-17 00:57:07 +01:00
CodeDevMLH
2c907debc8 Enhance video handling: release HTTP connections and manage lazy loading for trailers 2026-02-17 00:56:47 +01:00
CodeDevMLH
7b30f8c9e9 typos [skip ci] 2026-02-16 23:06:45 +01:00
CodeDevMLH
3a90605112 Update manifest.json for release v1.6.5.1 [skip ci] 2026-02-16 21:49:25 +00:00
CodeDevMLH
5772d670ff Bump version to 1.6.5.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-16 22:48:33 +01:00
CodeDevMLH
e558594c52 Enhance seasonal section layout and adjust input field width in configuration 2026-02-16 22:48:14 +01:00
CodeDevMLH
343436ac56 Update manifest.json for release v1.6.5.0 [skip ci] 2026-02-16 18:38:41 +00:00
CodeDevMLH
6075e20a11 Update version to 1.6.5.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-16 19:37:46 +01:00
CodeDevMLH
8b7def809b Refactor configuration tabs and enhance seasonal section functionality 2026-02-16 19:36:46 +01:00
CodeDevMLH
43950eac60 Add release existence check to automation workflow 2026-02-16 00:06:15 +01:00
CodeDevMLH
c09f265b26 Update manifest.json for release v1.6.4.1 [skip ci] 2026-02-15 22:56:18 +00:00
CodeDevMLH
379c370b4a ..
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-15 23:55:28 +01:00
CodeDevMLH
b567307003 Update manifest.json for release v1.6.4.0 [skip ci] 2026-02-15 22:53:31 +00:00
CodeDevMLH
ff9ea9eff0 Bump version to 1.6.4.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-15 23:52:41 +01:00
CodeDevMLH
9427e3e535 Remove autoplay and loop attributes from video backdrop configuration 2026-02-15 23:52:34 +01:00
CodeDevMLH
19318a916d Update manifest.json for release v1.6.4.0 [skip ci] 2026-02-15 22:39:48 +00:00
CodeDevMLH
5d85284df8 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/jellyfin-plugin-media-bar-enhanced
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-02-15 23:38:58 +01:00
CodeDevMLH
2382f850b6 Bump version to 1.6.4.0 2026-02-15 23:38:56 +01:00
CodeDevMLH
22041293f6 Update manifest.json for release v1.6.3.0 [skip ci] 2026-02-15 22:38:19 +00:00
CodeDevMLH
5595158f9d Update field description for Prefer Local Backdrops setting
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-15 23:25:01 +01:00
CodeDevMLH
39f85e0c9b fix backdrop issue? 2026-02-15 23:24:56 +01:00
CodeDevMLH
18a9980a0a Update manifest.json for release v1.6.3.0 [skip ci] 2026-02-15 01:21:08 +00:00
CodeDevMLH
deb426833d Bump version to 1.6.3.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-02-15 02:19:47 +01:00
CodeDevMLH
bf4b6da0f0 fix path issue on subpath installations 2026-02-15 02:19:37 +01:00
7 changed files with 237 additions and 54 deletions

View File

@@ -51,7 +51,31 @@ jobs:
echo "$CHANGELOG" >> $GITHUB_ENV echo "$CHANGELOG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
- name: Check if Release Already Exists
id: check_release
shell: bash
run: |
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
VERSION="${{ env.VERSION }}"
TAG="v$VERSION"
SERVER_URL="https://git.mahom03-spacecloud.de"
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$SERVER_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$TAG")
if [ "$HTTP_STATUS" -eq 200 ]; then
echo "Release $TAG already exists. Skipping release-related steps."
echo "release_exists=true" >> $GITHUB_OUTPUT
elif [ "$HTTP_STATUS" -eq 404 ]; then
echo "No existing release for $TAG. Continuing."
echo "release_exists=false" >> $GITHUB_OUTPUT
else
echo "Unexpected response when checking release: $HTTP_STATUS"
exit 1
fi
- name: Build and Zip - name: Build and Zip
if: steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
# Inject version from manifest into the build # Inject version from manifest into the build
@@ -71,6 +95,7 @@ jobs:
echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.MediaBarEnhanced.zip" >> $GITHUB_ENV echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.MediaBarEnhanced.zip" >> $GITHUB_ENV
- name: Update manifest.json - name: Update manifest.json
if: steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
REPO_OWNER="${{ github.repository_owner }}" REPO_OWNER="${{ github.repository_owner }}"
@@ -90,12 +115,14 @@ jobs:
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit manifest.json - name: Commit manifest.json
if: steps.check_release.outputs.release_exists == 'false'
uses: stefanzweifel/git-auto-commit-action@v7 uses: stefanzweifel/git-auto-commit-action@v7
with: with:
commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]" commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]"
file_pattern: manifest.json file_pattern: manifest.json
- name: Create Release - name: Create Release
if: steps.check_release.outputs.release_exists == 'false'
uses: akkuman/gitea-release-action@v1 uses: akkuman/gitea-release-action@v1
with: with:
server_url: "https://git.mahom03-spacecloud.de" server_url: "https://git.mahom03-spacecloud.de"
@@ -109,6 +136,7 @@ jobs:
# Update Message in Remote Repository # Update Message in Remote Repository
- name: Checkout Central Manifest Repo - name: Checkout Central Manifest Repo
if: steps.check_release.outputs.release_exists == 'false'
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest
@@ -116,6 +144,7 @@ jobs:
token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }} token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
- name: Update Central Manifest - name: Update Central Manifest
if: steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
cd central-manifest cd central-manifest
@@ -171,6 +200,7 @@ jobs:
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit and Push Central Manifest - name: Commit and Push Central Manifest
if: steps.check_release.outputs.release_exists == 'false'
run: | run: |
cd central-manifest cd central-manifest
git config user.name "CodeDevMLH" git config user.name "CodeDevMLH"

View File

@@ -52,7 +52,32 @@ jobs:
echo "$CHANGELOG" >> $GITHUB_ENV echo "$CHANGELOG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
- name: Check if Release Already Exists
id: check_release
shell: bash
run: |
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
VERSION="${{ env.VERSION }}"
TAG="v$VERSION"
API_URL="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$TAG"
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$API_URL")
if [ "$HTTP_STATUS" -eq 200 ]; then
echo "Release $TAG already exists. Skipping release-related steps."
echo "release_exists=true" >> $GITHUB_OUTPUT
elif [ "$HTTP_STATUS" -eq 404 ]; then
echo "No existing release for $TAG. Continuing."
echo "release_exists=false" >> $GITHUB_OUTPUT
else
echo "Unexpected response when checking release: $HTTP_STATUS"
exit 1
fi
- name: Build and Zip - name: Build and Zip
if: steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
# Inject version from manifest into the build # Inject version from manifest into the build
@@ -72,6 +97,7 @@ jobs:
echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.MediaBarEnhanced.zip" >> $GITHUB_ENV echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.MediaBarEnhanced.zip" >> $GITHUB_ENV
- name: Update manifest.json - name: Update manifest.json
if: steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
REPO_OWNER="${{ github.repository_owner }}" REPO_OWNER="${{ github.repository_owner }}"
@@ -91,13 +117,14 @@ jobs:
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit manifest.json - name: Commit manifest.json
if: steps.check_release.outputs.release_exists == 'false'
uses: stefanzweifel/git-auto-commit-action@v7 uses: stefanzweifel/git-auto-commit-action@v7
with: with:
commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]" commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]"
file_pattern: manifest.json file_pattern: manifest.json
- name: Generate Commit Log - name: Generate Commit Log
if: success() if: success() && steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
echo "Generating commit log since last tag..." echo "Generating commit log since last tag..."
@@ -131,6 +158,7 @@ jobs:
cat release_body.txt cat release_body.txt
- name: Create Release - name: Create Release
if: steps.check_release.outputs.release_exists == 'false'
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: "v${{ env.VERSION }}" tag_name: "v${{ env.VERSION }}"
@@ -145,6 +173,7 @@ jobs:
# Update Message in Remote Repository # Update Message in Remote Repository
- name: Checkout Central Manifest Repo - name: Checkout Central Manifest Repo
if: steps.check_release.outputs.release_exists == 'false'
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest
@@ -152,6 +181,7 @@ jobs:
token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }} token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
- name: Update Central Manifest - name: Update Central Manifest
if: steps.check_release.outputs.release_exists == 'false'
shell: bash shell: bash
run: | run: |
cd central-manifest cd central-manifest
@@ -207,6 +237,7 @@ jobs:
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit and Push Central Manifest - name: Commit and Push Central Manifest
if: steps.check_release.outputs.release_exists == 'false'
run: | run: |
cd central-manifest cd central-manifest
git config user.name "CodeDevMLH" git config user.name "CodeDevMLH"

View File

@@ -22,15 +22,15 @@
<hr style="max-width: 800px; margin: 1em 0;"> <hr style="max-width: 800px; margin: 1em 0;">
<div style="margin-bottom: 1.5em;"> <div style="margin-bottom: 1.5em;">
<button class="jellyfin-tab-button active" onclick="showTab('basic', this)" <button class="jellyfin-tab-button active" onclick="showTab('media-bar-enhanced-basic', this)"
style="background: none; border: none; color: #fff; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid #00a4dc;"> style="background: none; border: none; color: #fff; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid #00a4dc;">
<h3>General Settings</h3> <h3>General Settings</h3>
</button> </button>
<button class="jellyfin-tab-button" onclick="showTab('custom', this)" <button class="jellyfin-tab-button" onclick="showTab('media-bar-enhanced-custom', this)"
style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid transparent;"> style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid transparent;">
<h3>Custom Content</h3> <h3>Custom Content</h3>
</button> </button>
<button class="jellyfin-tab-button" onclick="showTab('advanced', this)" <button class="jellyfin-tab-button" onclick="showTab('media-bar-enhanced-advanced', this)"
style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid transparent;"> style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; padding: 0.5em 1em; border-bottom: 2px solid transparent;">
<h3>Advanced Settings</h3> <h3>Advanced Settings</h3>
</button> </button>
@@ -39,11 +39,11 @@
<form id="mediaBarEnhancedConfigForm"> <form id="mediaBarEnhancedConfigForm">
<!-- BASIC TAB --> <!-- BASIC TAB -->
<div id="basic" class="tab-content"> <div id="media-bar-enhanced-basic" class="tab-content">
<h2 class="sectionTitle">Main Plugin Settings</h2> <h2 class="sectionTitle">Main Plugin Settings</h2>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="IsEnabled" name="IsEnabled" /> <input is="emby-checkbox" type="checkbox" id="MediaBarIsEnabled" name="MediaBarIsEnabled" />
<span>Enable Media Bar Enhanced Plugin</span> <span>Enable Media Bar Enhanced Plugin</span>
</label> </label>
<div class="fieldDescription">Enable or disable the entire plugin functionality.</div> <div class="fieldDescription">Enable or disable the entire plugin functionality.</div>
@@ -71,7 +71,7 @@
name="PreferLocalBackdrops" /> name="PreferLocalBackdrops" />
<span>Prefer Local Backdrops / Theme Videos</span> <span>Prefer Local Backdrops / Theme Videos</span>
</label> </label>
<div class="fieldDescription">If enabled, local backdrop videos (Theme Videos) will be preferred over trailers.</div> <div class="fieldDescription">If enabled, local backdrop videos (Theme Videos) will be preferred over remote and local trailers.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -101,7 +101,7 @@
</div> </div>
<!-- CUSTOM CONTENT TAB --> <!-- CUSTOM CONTENT TAB -->
<div id="custom" class="tab-content" style="display:none;"> <div id="media-bar-enhanced-custom" class="tab-content" style="display:none;">
<!-- Default Custom Media IDs --> <!-- Default Custom Media IDs -->
<h2 class="sectionTitle">Custom Media IDs</h2> <h2 class="sectionTitle">Custom Media IDs</h2>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
@@ -146,7 +146,7 @@
Example: Example:
<code>https://your-jellyfin-url/web/#/details?id=<b style="color:red;">your-item-id</b>&serverId=your-server-id</code><br><br> <code>https://your-jellyfin-url/web/#/details?id=<b style="color:red;">your-item-id</b>&serverId=your-server-id</code><br><br>
You can also insert a name of a collection or playlist to fetch the IDs of all items in You can also insert a name of a collection or playlist to fetch the IDs of all items in
it (will take the first hit.<br><b>Note:</b> there is currently no feedback if the name it (will take the first hit.<br><b>Note:</b> There is currently no feedback if the name
resolution succeeded, you will have to look if the bar displays the correct items). resolution succeeded, you will have to look if the bar displays the correct items).
</p> </p>
</div> </div>
@@ -164,6 +164,12 @@
during their active date ranges. If no season matches the current date, the default Custom Media IDs above or random selection are used as fallback.</div> during their active date ranges. If no season matches the current date, the default Custom Media IDs above or random selection are used as fallback.</div>
</div> </div>
<div id="seasonalContentContainer" style="display: none;"> <div id="seasonalContentContainer" style="display: none;">
<div style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;">
<i class="material-icons" style="color: #00a4dc; font-size: 24px;">info</i>
<div>Define seasonal rules to automatically select a selection of items based on the date. Rules are evaluated from top to bottom. The first matching rule wins.</div>
</div>
<div id="seasonalSectionsList"></div> <div id="seasonalSectionsList"></div>
<button is="emby-button" type="button" id="addSeasonBtn" class="raised emby-button" <button is="emby-button" type="button" id="addSeasonBtn" class="raised emby-button"
style="margin-top: 1em; display: inline-flex; align-items: center; gap: 0.4em;"> style="margin-top: 1em; display: inline-flex; align-items: center; gap: 0.4em;">
@@ -175,7 +181,7 @@
</div> </div>
<!-- ADVANCED TAB --> <!-- ADVANCED TAB -->
<div id="advanced" class="tab-content" style="display:none;"> <div id="media-bar-enhanced-advanced" class="tab-content" style="display:none;">
<h2 class="sectionTitle">Features</h2> <h2 class="sectionTitle">Features</h2>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -443,7 +449,7 @@
ApiClient.getPluginConfiguration(MediaBarEnhancedConfigurationPage.pluginId).then(function (config) { ApiClient.getPluginConfiguration(MediaBarEnhancedConfigurationPage.pluginId).then(function (config) {
var keys = [ var keys = [
'IsEnabled', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance',
'LoadingCheckInterval', 'MaxPlotLength', 'MaxMovies', 'MaxTvShows', 'LoadingCheckInterval', 'MaxPlotLength', 'MaxMovies', 'MaxTvShows',
'MaxItems', 'PreloadCount', 'FadeTransitionDuration', 'MaxPaginationDots', 'MaxItems', 'PreloadCount', 'FadeTransitionDuration', 'MaxPaginationDots',
'SlideAnimationEnabled', 'EnableVideoBackdrop', 'UseSponsorBlock', 'SlideAnimationEnabled', 'EnableVideoBackdrop', 'UseSponsorBlock',
@@ -456,6 +462,12 @@
'IncludeWatchedContent' 'IncludeWatchedContent'
]; ];
// Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins
var mediaBarEnabledCheckbox = page.querySelector('#MediaBarIsEnabled');
if (mediaBarEnabledCheckbox) {
mediaBarEnabledCheckbox.checked = config.IsEnabled;
}
keys.forEach(function (key) { keys.forEach(function (key) {
var el = page.querySelector('#' + key); var el = page.querySelector('#' + key);
if (el) { if (el) {
@@ -533,8 +545,15 @@
if (seasonalInput) seasonalInput.value = sectionsJson; if (seasonalInput) seasonalInput.value = sectionsJson;
var config = {}; var config = {};
// Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins
var mediaBarEnabledCheckbox = page.querySelector('#MediaBarIsEnabled');
if (mediaBarEnabledCheckbox) {
config.IsEnabled = mediaBarEnabledCheckbox.checked;
}
var keys = [ var keys = [
'IsEnabled', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance', 'ShuffleInterval', 'RetryInterval', 'MinSwipeDistance',
'LoadingCheckInterval', 'MaxPlotLength', 'MaxMovies', 'MaxTvShows', 'LoadingCheckInterval', 'MaxPlotLength', 'MaxMovies', 'MaxTvShows',
'MaxItems', 'PreloadCount', 'FadeTransitionDuration', 'MaxPaginationDots', 'MaxItems', 'PreloadCount', 'FadeTransitionDuration', 'MaxPaginationDots',
'SlideAnimationEnabled', 'EnableVideoBackdrop', 'UseSponsorBlock', 'SlideAnimationEnabled', 'EnableVideoBackdrop', 'UseSponsorBlock',
@@ -612,14 +631,18 @@
div.innerHTML = div.innerHTML =
'<div class="inputContainer" style="margin-bottom: 0.5em;">' + '<div class="inputContainer" style="margin-bottom: 0.5em;">' +
' <label class="inputLabel" style="font-size: 1.2em; font-weight: bold; margin-bottom:0.5em; display:block;">' + labelText + '</label>' + ' <div style="display: flex; align-items: center; justify-content: space-between;">' +
' <div style="display: flex; align-items: center;">' + ' <label class="inputLabel section-title" style="font-size: 1.2em; font-weight: bold; margin-bottom:0.5em; display:block;">' + labelText + '</label>' +
' <div style="flex-grow:1;">' + ' <div style="display: flex; gap: 0.5em;">' +
' <input is="emby-input" type="text" class="emby-input section-name" value="' + (data.Name || '') + '" />' + ' <button type="button" is="paper-icon-button-light" class="btn-move-up" title="Move Up"><i class="material-icons">arrow_upward</i></button>' +
' <button type="button" is="paper-icon-button-light" class="btn-move-down" title="Move Down"><i class="material-icons">arrow_downward</i></button>' +
' <button type="button" is="paper-icon-button-light" class="btn-remove" title="Remove" style="color: #a94442;"><i class="material-icons">delete</i></button>' +
' </div>' + ' </div>' +
' <button type="button" class="raised emby-button remove-section" style="background: #a94442; min-width: unset; margin-left: 1em;">Remove</button>' +
' </div>' + ' </div>' +
' <div class="inputContainer">' +
' <input is="emby-input" type="text" class="emby-input section-name" style="width: 60%;" value="' + (data.Name || '') + '" />' +
' <div class="fieldDescription">Name of the season</div>' + ' <div class="fieldDescription">Name of the season</div>' +
' </div>' +
'</div>' + '</div>' +
'<div class="inputContainer" style="margin-bottom: 1em;">' + '<div class="inputContainer" style="margin-bottom: 1em;">' +
' <label class="inputLabel" style="margin-bottom:0.5em; display:block;">Active Period</label>' + ' <label class="inputLabel" style="margin-bottom:0.5em; display:block;">Active Period</label>' +
@@ -639,13 +662,38 @@
' <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 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>';
div.querySelector('.remove-section').addEventListener('click', function() { div.querySelector('.btn-remove').addEventListener('click', function() {
div.remove(); div.remove();
MediaBarEnhancedConfigurationPage.updateSectionTitles(container);
});
div.querySelector('.btn-move-up').addEventListener('click', function() {
if (div.previousElementSibling) {
container.insertBefore(div, div.previousElementSibling);
MediaBarEnhancedConfigurationPage.updateSectionTitles(container);
}
});
div.querySelector('.btn-move-down').addEventListener('click', function() {
if (div.nextElementSibling) {
container.insertBefore(div.nextElementSibling, div);
MediaBarEnhancedConfigurationPage.updateSectionTitles(container);
}
}); });
container.appendChild(div); container.appendChild(div);
}, },
updateSectionTitles: function(container) {
var sections = container.querySelectorAll('.seasonal-section');
sections.forEach(function(section, index) {
var title = section.querySelector('.section-title');
if (title) {
title.innerText = 'Season list #' + (index + 1);
}
});
},
getSeasonalSectionsFromUI: function(page) { getSeasonalSectionsFromUI: function(page) {
var sections = []; var sections = [];
var els = page.querySelectorAll('.seasonal-section'); var els = page.querySelectorAll('.seasonal-section');

View File

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

View File

@@ -18,8 +18,8 @@ namespace Jellyfin.Plugin.MediaBarEnhanced
{ {
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly ILogger<ScriptInjector> _logger; private readonly ILogger<ScriptInjector> _logger;
public const string ScriptTag = "<script src=\"/MediaBarEnhanced/Resources/mediaBarEnhanced.js\" defer></script>"; public const string ScriptTag = "<script src=\"../MediaBarEnhanced/Resources/mediaBarEnhanced.js\" defer></script>";
public const string CssTag = "<link rel=\"stylesheet\" href=\"/MediaBarEnhanced/Resources/mediaBarEnhanced.css\" />"; public const string CssTag = "<link rel=\"stylesheet\" href=\"../MediaBarEnhanced/Resources/mediaBarEnhanced.css\" />";
public const string ScriptMarker = "</body>"; public const string ScriptMarker = "</body>";
public const string CssMarker = "</head>"; public const string CssMarker = "</head>";

View File

@@ -63,6 +63,7 @@ const CONFIG = {
sortOrder: "Ascending", sortOrder: "Ascending",
applyLimitsToCustomIds: false, applyLimitsToCustomIds: false,
seasonalSections: "[]", seasonalSections: "[]",
isEnabled: true,
}; };
// State management // State management
@@ -437,7 +438,7 @@ const waitForApiClientAndInitialize = () => {
const fetchPluginConfig = async () => { const fetchPluginConfig = async () => {
try { try {
const response = await fetch('/MediaBarEnhanced/Config'); const response = await fetch('../MediaBarEnhanced/Config');
if (response.ok) { if (response.ok) {
const pluginConfig = await response.json(); const pluginConfig = await response.json();
if (pluginConfig) { if (pluginConfig) {
@@ -1638,6 +1639,7 @@ const SlideCreator = {
"data-item-id": itemId, "data-item-id": itemId,
}); });
let videoBackdrop;
let backdrop; let backdrop;
let isVideo = false; let isVideo = false;
let trailerUrl = null; let trailerUrl = null;
@@ -1721,7 +1723,7 @@ const SlideCreator = {
// Create container for YouTube API // Create container for YouTube API
const videoClass = CONFIG.fullWidthVideo ? "video-backdrop-full" : "video-backdrop-default"; const videoClass = CONFIG.fullWidthVideo ? "video-backdrop-full" : "video-backdrop-default";
backdrop = SlideUtils.createElement("div", { videoBackdrop = SlideUtils.createElement("div", {
className: `backdrop video-backdrop ${videoClass}`, className: `backdrop video-backdrop ${videoClass}`,
id: `youtube-player-${itemId}` id: `youtube-player-${itemId}`
}); });
@@ -1833,11 +1835,7 @@ const SlideCreator = {
if (event.data === YT.PlayerState.ENDED) { if (event.data === YT.PlayerState.ENDED) {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
if (slide && slide.classList.contains('active')) { if (slide && slide.classList.contains('active')) {
if (CONFIG.waitForTrailerToEnd) {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
} else {
event.target.playVideo(); // Loop if trailer is shorter than slide duration
}
} }
} }
}, },
@@ -1857,24 +1855,23 @@ const SlideCreator = {
} else if (!isYoutube) { } else if (!isYoutube) {
isVideo = true; isVideo = true;
const videoSrc = (typeof trailerUrl === 'object' ? trailerUrl.url : trailerUrl);
const videoAttributes = { const videoAttributes = {
className: "backdrop video-backdrop", className: "backdrop video-backdrop",
src: (typeof trailerUrl === 'object' ? trailerUrl.url : trailerUrl), preload: "none",
autoplay: false,
preload: "auto",
loop: false,
disablePictureInPicture: true, disablePictureInPicture: true,
"data-src": videoSrc,
style: "object-fit: cover; object-position: center center; width: 100%; height: 100%; position: absolute; top: 0; left: 0; pointer-events: none;" style: "object-fit: cover; object-position: center center; width: 100%; height: 100%; position: absolute; top: 0; left: 0; pointer-events: none;"
}; };
videoAttributes.muted = ""; videoAttributes.muted = "";
backdrop = SlideUtils.createElement("video", videoAttributes); videoBackdrop = SlideUtils.createElement("video", videoAttributes);
backdrop.volume = 0.4; videoBackdrop.volume = 0.4;
STATE.slideshow.videoPlayers[itemId] = backdrop; STATE.slideshow.videoPlayers[itemId] = videoBackdrop;
backdrop.addEventListener('play', (event) => { videoBackdrop.addEventListener('play', (event) => {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`);
if (!slide || !slide.classList.contains('active')) { if (!slide || !slide.classList.contains('active')) {
console.log(`Local video ${itemId} started playing but slide is not active, pausing.`); console.log(`Local video ${itemId} started playing but slide is not active, pausing.`);
@@ -1887,29 +1884,35 @@ const SlideCreator = {
} }
}); });
backdrop.addEventListener('ended', () => { videoBackdrop.addEventListener('ended', (event) => {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); const slide = event.target.closest('.slide');
if (slide && slide.classList.contains('active')) { if (slide && slide.classList.contains('active')) {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
} }
}); });
backdrop.addEventListener('error', () => { videoBackdrop.addEventListener('error', (event) => {
const slide = document.querySelector(`.slide[data-item-id="${itemId}"]`); console.warn(`Local video error for item ${itemId}`);
if (CONFIG.waitForTrailerToEnd && slide && slide.classList.contains('active')) { const slide = event.target.closest('.slide');
if (slide && slide.classList.contains('active')) {
SlideshowManager.nextSlide(); SlideshowManager.nextSlide();
} }
}); });
} }
} }
if (!isVideo) { // Always create a static backdrop image (to show while video loads or if no video)
backdrop = SlideUtils.createElement("img", { backdrop = SlideUtils.createElement("img", {
className: "backdrop high-quality", className: "backdrop high-quality",
src: this.buildImageUrl(item, "Backdrop", 0, serverAddress, 60), src: this.buildImageUrl(item, "Backdrop", 0, serverAddress, 60),
alt: LocalizationUtils.getLocalizedString('Backdrop', 'Backdrop'), alt: LocalizationUtils.getLocalizedString('Backdrop', 'Backdrop'),
loading: "eager", loading: "eager",
}); });
// If video, static backdrop should be strictly a background (no animation)
if (isVideo) {
backdrop.style.animation = "none";
backdrop.style.transition = "none";
} }
const backdropOverlay = SlideUtils.createElement("div", { const backdropOverlay = SlideUtils.createElement("div", {
@@ -1919,8 +1922,14 @@ const SlideCreator = {
const backdropContainer = SlideUtils.createElement("div", { const backdropContainer = SlideUtils.createElement("div", {
className: "backdrop-container" + (isVideo && CONFIG.fullWidthVideo ? " full-width-video" : ""), className: "backdrop-container" + (isVideo && CONFIG.fullWidthVideo ? " full-width-video" : ""),
}); });
backdropContainer.append(backdrop, backdropOverlay); backdropContainer.append(backdrop, backdropOverlay);
// If video exists, append on top of static backdrop
if (isVideo && videoBackdrop) {
backdropContainer.appendChild(videoBackdrop);
}
const logo = SlideUtils.createElement("img", { const logo = SlideUtils.createElement("img", {
className: "logo high-quality", className: "logo high-quality",
src: this.buildImageUrl(item, "Logo", undefined, serverAddress, 40), src: this.buildImageUrl(item, "Logo", undefined, serverAddress, 40),
@@ -2361,7 +2370,7 @@ const SlideshowManager = {
currentSlide.classList.add("active"); currentSlide.classList.add("active");
// Manage Video Playback: Stop others, Play current // Manage Video Playback: Stop others, Play current
// 1. Stop all other YouTube players and local video elements // 1. Stop all other YouTube players and local video elements, release connections
if (STATE.slideshow.videoPlayers) { if (STATE.slideshow.videoPlayers) {
Object.keys(STATE.slideshow.videoPlayers).forEach(id => { Object.keys(STATE.slideshow.videoPlayers).forEach(id => {
if (id !== currentItemId) { if (id !== currentItemId) {
@@ -2371,11 +2380,17 @@ const SlideshowManager = {
if (typeof p.pauseVideo === 'function') { if (typeof p.pauseVideo === 'function') {
p.pauseVideo(); p.pauseVideo();
} }
// HTML5 <video> element (local trailers) // HTML5 <video> element (local trailers), release HTTP connection
if (p instanceof HTMLVideoElement) { if (p instanceof HTMLVideoElement) {
p.pause(); p.pause();
p.muted = true; p.muted = true;
p.currentTime = 0; p.currentTime = 0;
// Save src to data-src and release the HTTP streaming connection
if (p.src && !p.getAttribute('data-src')) {
p.setAttribute('data-src', p.src);
}
p.removeAttribute('src');
p.load();
} }
} }
}); });
@@ -2412,6 +2427,12 @@ const SlideshowManager = {
if (videoBackdrop) { if (videoBackdrop) {
if (videoBackdrop.tagName === 'VIDEO') { if (videoBackdrop.tagName === 'VIDEO') {
// Restore src from data-src if it was deactivated to release connections
const lazySrc = videoBackdrop.getAttribute('data-src');
if (lazySrc && !videoBackdrop.src) {
videoBackdrop.src = lazySrc;
}
videoBackdrop.currentTime = 0; videoBackdrop.currentTime = 0;
videoBackdrop.muted = STATE.slideshow.isMuted; videoBackdrop.muted = STATE.slideshow.isMuted;
@@ -2610,7 +2631,7 @@ const SlideshowManager = {
*/ */
pruneSlideCache() { pruneSlideCache() {
const currentIndex = STATE.slideshow.currentSlideIndex; const currentIndex = STATE.slideshow.currentSlideIndex;
const keepRange = 5; const keepRange = CONFIG.preloadCount + 1;
let prunedAny = false; let prunedAny = false;
Object.keys(STATE.slideshow.createdSlides).forEach((itemId) => { Object.keys(STATE.slideshow.createdSlides).forEach((itemId) => {
@@ -2628,7 +2649,17 @@ const SlideshowManager = {
if (STATE.slideshow.videoPlayers[itemId]) { if (STATE.slideshow.videoPlayers[itemId]) {
const player = STATE.slideshow.videoPlayers[itemId]; const player = STATE.slideshow.videoPlayers[itemId];
if (typeof player.destroy === 'function') { if (typeof player.destroy === 'function') {
// YouTube player
player.destroy(); player.destroy();
} else if (player instanceof HTMLVideoElement) {
// HTML5 video, release HTTP streaming connection
player.pause();
// Save src to data-src and release the HTTP streaming connection
if (player.src && !player.getAttribute('data-src')) {
player.setAttribute('data-src', player.src);
}
player.removeAttribute('src');
player.load();
} }
delete STATE.slideshow.videoPlayers[itemId]; delete STATE.slideshow.videoPlayers[itemId];
} }
@@ -2807,7 +2838,7 @@ const SlideshowManager = {
}); });
} }
// 2. Stop and mute all HTML5 videos // 2. Stop and mute all HTML5 videos, release connections
const container = document.getElementById("slides-container"); const container = document.getElementById("slides-container");
if (container) { if (container) {
container.querySelectorAll('video').forEach(video => { container.querySelectorAll('video').forEach(video => {
@@ -2815,6 +2846,12 @@ const SlideshowManager = {
video.pause(); video.pause();
video.muted = true; video.muted = true;
video.currentTime = 0; video.currentTime = 0;
// Save src and release HTTP streaming connection
if (video.src && !video.getAttribute('data-src')) {
video.setAttribute('data-src', video.src);
}
video.removeAttribute('src');
video.load();
} catch (e) { } catch (e) {
console.warn("Error stopping HTML5 video:", e); console.warn("Error stopping HTML5 video:", e);
} }
@@ -2847,9 +2884,14 @@ const SlideshowManager = {
return; return;
} }
// HTML5 video: just resume, don't reset currentTime // HTML5 video: restore src if needed, then resume
const html5Video = currentSlide.querySelector('video.video-backdrop'); const html5Video = currentSlide.querySelector('video.video-backdrop');
if (html5Video) { if (html5Video) {
// Restore src from data-src if it was cleared to release connections
const lazySrc = html5Video.getAttribute('data-src');
if (lazySrc && !html5Video.src) {
html5Video.src = lazySrc;
}
html5Video.muted = STATE.slideshow.isMuted; html5Video.muted = STATE.slideshow.isMuted;
if (!STATE.slideshow.isMuted) html5Video.volume = 0.4; if (!STATE.slideshow.isMuted) html5Video.volume = 0.4;
html5Video.play().catch(e => console.warn("Error resuming HTML5 video:", e)); html5Video.play().catch(e => console.warn("Error resuming HTML5 video:", e));

View File

@@ -8,6 +8,38 @@
"category": "General", "category": "General",
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png", "imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
"versions": [ "versions": [
{
"version": "1.6.6.0",
"changelog": "- feat: add static backdrop also for video backdrops\n- fix: renaming issue of settings (avoiding conflict with other plugins)",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.6.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "af624d779b5e43cc6e21899eb24db26d",
"timestamp": "2026-02-19T01:01:20Z"
},
{
"version": "1.6.5.2",
"changelog": "- refactored seasonal UI settings",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.5.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "552cb3376c77ede5a0664ced56bf7d1e",
"timestamp": "2026-02-16T23:57:57Z"
},
{
"version": "1.6.4.1",
"changelog": "- fix slide transition when using local/backdrop videos",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.4.1/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "a9c5a863427de84639eca082483936da",
"timestamp": "2026-02-15T22:56:17Z"
},
{
"version": "1.6.3.1",
"changelog": "- fix path issue on subpath installations",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.6.3.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "6a952445bfb80ba4603017358e48da91",
"timestamp": "2026-02-15T22:38:19Z"
},
{ {
"version": "1.6.2.3", "version": "1.6.2.3",
"changelog": "- feat: add options for local backdrops (theme videos) support and randomization features for local trailer/backdrops if more than one is available\n- feat: add option to include watched content in the random selection", "changelog": "- feat: add options for local backdrops (theme videos) support and randomization features for local trailer/backdrops if more than one is available\n- feat: add option to include watched content in the random selection",