Merge branch 'main' into upstream

This commit is contained in:
Zach Ross-Clyne authored and GitHub committed 2026-01-15 00:19:28 +00:00
commit f4986c3aed
19 files changed
+968 -14

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
version: "1"
rules:
- base: main
upstream: MakD:Jellyfin-Media-Bar
mergeMethod: merge
mergeUnstable: false
assignees:
- IAmParadox27
reviewers:
- IAmParadox27
conflictReviewers:
- IAmParadox27
label: ":arrow_heading_down: pull"
conflictLabel: "merge-conflict"
+78 -11
View File
@@ -1,15 +1,80 @@
<h1 align="center">Media Bar</h1>
<h2 align="center">A Jellyfin Plugin</h2>
<p align="center">
<img alt="Logo" src="https://raw.githubusercontent.com/IAmParadox27/jellyfin-plugin-media-bar/main/src/logo.png" />
<br />
<br />
<a href="https://github.com/IAmParadox27/jellyfin-plugin-media-bar/?tab=License-1-ov-file">
<img alt="DBAD" src="https://img.shields.io/badge/license-DBAD-blue" />
</a>
<a href="https://github.com/IAmParadox27/jellyfin-plugin-media-bar/releases">
<img alt="Current Release" src="https://img.shields.io/github/release/IAmParadox27/jellyfin-plugin-media-bar.svg" />
</a>
</p>
## Reporting Issues
If you face issues relating to the visuals or behaviour of buttons added by the Media Bar please report them on MakD's repo (the one this is forked from). This plugin pulls the content from their repo directly and is only in control of adding it without the need for modifying your JF install files.
Any issues with plugin's settings (including using a playlist as your avatar's playlist) should be made here.
## Development Update - 20th August 2025
Hey all! Things are changing with my plugins are more and more people start to use them and report issues. In order to make it easier for me to manage I'm splitting bugs and features into different areas. For feature requests please head over to <a href="https://features.iamparadox.dev/">https://features.iamparadox.dev/</a> where you'll be able to signin with GitHub and make a feature request. For bugs please report them on the relevant GitHub repo and they will be added to the <a href="https://github.com/users/IAmParadox27/projects/1/views/1">project board</a> when I've seen them. I've found myself struggling to know when issues are made and such recently so I'm also planning to create a system that will monitor a particular view for new issues that come up and send me a notification which should hopefully allow me to keep more up to date and act faster on various issues.
As with a lot of devs, I am very momentum based in my personal life coding and there are often times when these projects may appear dormant, I assure you now that I don't plan to let these projects go stale for a long time, there just might be times where there isn't an update or response for a couple weeks, but I'll try to keep that better than it has been. With all new releases to Jellyfin I will be updating as soon as possible, I have already made a start on 10.11.0 and will release an update to my plugins hopefully not long after that version is officially released!
## Installation
### Prerequisites
- This plugin is based on Jellyfin Version `10.10.7`
- The following plugins are required to also be installed, please following their installation guides:
- File Transformation (https://github.com/IAmParadox27/jellyfin-plugin-file-transformation) at least v2.2.1.0
### Installation
1. Add `https://www.iamparadox.dev/jellyfin/plugins/manifest.json` to your plugin repositories.
2. Install `Media Bar` from the Catalogue.
3. Restart Jellyfin.
4. Force refresh your webpage (or app) and you should see your new Media Bar at the top of the home page.
## Upcoming Features/Known Issues
If you find an issue with any of the sections or usage of the plugin, please open an issue on GitHub.
### FAQ
#### I've updated Jellyfin to latest version but I can't see the plugin available in the catalogue
The likelihood is the plugin hasn't been updated for that version of Jellyfin and the plugins are strictly 1 version compatible. Please wait until an update has been pushed. If you can see the version number in the release assets then please make an issue, but if its not in the assets, please wait. I know Jellyfin has updated, I'll update when I can.
#### I've installed the plugins and the media bar doesn't appear. How do I fix?
This is common, particularly on a fresh install. The first thing you should try is the following
1. Launch your browsers developer tools
![image](https://github.com/user-attachments/assets/e8781a69-464e-430e-a07c-5172a620ef84)
3. Open the **Network** tab across the top bar
4. Check the **Disable cache** checkbox
5. Refresh the page **while the dev tools are still open**
![image](https://github.com/user-attachments/assets/6f8c3fc7-89a3-4475-b8a6-cd4a58d51b84)
## Credits
Credits for this plugin go to @MakD for his original work and to @BobHasNoSoul and @SethBacon for their influence to MakD. For full credits see below in the original README content
## Original README
<details>
<summary>Original README.md from MakD</summary>
# Jellyfin-Media-Bar - Now with Play Now Function
![jsDelivr hits (GitHub)](https://img.shields.io/jsdelivr/gh/hm/makd/Jellyfin-Media-Bar?style=for-the-badge&logo=javascript&logoColor=white&labelColor=F0DB4F&color=323330&cacheSeconds=3600)
**IMP UPDATE — We have dropped support for the normal CSS version (for now). _(It still works, but there will be no further updates till the fullscreen mode is stabilized)_**
**IMP UPDATE — We have dropped support for the normal CSS version (for now). _(It still works, but there will be no further updates till the fullscreen mode is stabilized)_**
The fullscreen version has a new look (in beta), and support for different screen sizes has been added. For any visual goof-ups, please open a bug report, including the device being used and whether it is encountered in portrait or landscape mode.
Thanks to the Man, the Legend [BobHasNoSoul](https://github.com/BobHasNoSoul) for his work on the [jellyfinfeatured](https://github.com/BobHasNoSoul/jellyfin-featured) and [SethBacon](https://forum.jellyfin.org/u-sethbacon) and [TedHinklater](https://github.com/tedhinklater) for their take on the [Jellyfin-Featured-Content-Bar](https://github.com/tedhinklater/Jellyfin-Featured-Content-Bar).
Thanks to the Man, the Legend [BobHasNoSoul](https://github.com/BobHasNoSoul) for his work on the [jellyfinfeatured](https://github.com/BobHasNoSoul/jellyfin-featured) and [SethBacon](https://forum.jellyfin.org/u-sethbacon) and [TedHinklater](https://github.com/tedhinklater) for their take on the [Jellyfin-Featured-Content-Bar](https://github.com/tedhinklater/Jellyfin-Featured-Content-Bar).
Here I present my version with some code improvements, loading optimizations, and security enhancements. Works best with the [Zombie theme](https://github.com/MakD/zombie-release) (_Shameless Plug_ `@import url(https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/zombie_revived.css);`, visit the repo for more color schemes).
@@ -18,15 +83,15 @@ Here I present my version with some code improvements, loading optimizations, an
<details>
<summary> Desktop Layout </summary>
![Jellyfin Desktop Layout](https://raw.githubusercontent.com/MakD/Jellyfin-Media-Bar/refs/heads/main/img/Jelly-Web%20-%20Fullscreen%20Mode.png)
</details>
<details>
<summary> Mobile Layout </summary>
![Jellyfin Mobile Layout](https://raw.githubusercontent.com/MakD/Jellyfin-Media-Bar/refs/heads/main/img/Jelly-Mobile-Fullscreen.png)
</details>
@@ -34,12 +99,12 @@ Here I present my version with some code improvements, loading optimizations, an
# Prepping the files
<details>
<summary>index.html</summary>
1. Navigate to your `jellyfin-web` folder and search for the file index.html. (you can use any code editor, just remember to open with administrator privileges.
2. Search for `</head>`
3. Just before the `</head>`, plug the below code
1. Navigate to your `jellyfin-web` folder and search for the file index.html. (you can use any code editor, just remember to open with administrator privileges.
2. Search for `</head>`
3. Just before the `</head>`, plug the below code
```
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/MakD/Jellyfin-Media-Bar@latest/slideshowpure.css" />
<script async src="https://cdn.jsdelivr.net/gh/MakD/Jellyfin-Media-Bar@latest/slideshowpure.js"></script>
@@ -50,7 +115,7 @@ And that is it. Hard refresh your web page (CTRL+Shift+R) twice, and Profit!
# Want a Custom List to be showcased instead of random items??
No worries this got you covered.
No worries this got you covered.
## Steps
@@ -71,7 +136,7 @@ The next time it loads, it will display these items.
# Uninstall the Bar
<details>
<summary> Roll Back </summary>
Restore the `index.html` file / remove the lines added and you are good to go!!!
@@ -89,3 +154,5 @@ All modifications must be contributed back to this repository.
Attribution to the original author (MakD) is required in any use or derivative work.
Please take a look at the [LICENSE](LICENSE) file for full terms.
</details>
+4 -3
View File
@@ -164,11 +164,12 @@ const initLocalization = async () => {
*/
const initLoadingScreen = () => {
const currentPath = window.location.href.toLowerCase();
const currentPath = window.location.href.toLowerCase().replace(window.location.origin, "");
const isHomePage =
currentPath.includes("/web/#/home.html") ||
currentPath.includes("/web/#/home") ||
currentPath.includes("/web/index.html#/home.html") ||
currentPath === "/web/index.html#/home" ||
currentPath.endsWith("/web/");
if (!isHomePage) return;
@@ -969,8 +970,8 @@ const VisibilityObserver = {
if (!container) return;
const isVisible =
(window.location.hash === "#/home.html" ||
window.location.hash === "#/home") &&
(window.location.hash === "#/home.html" ||
window.location.hash === "#/home") &&
activeTab.getAttribute("data-index") === "0";
container.style.display = isVisible ? "block" : "none";
+9
View File
@@ -0,0 +1,9 @@
bin/
obj/
.vs/
.idea/
artifacts
Release
*.zip
node_modules
dist/
@@ -0,0 +1,20 @@
using System.Reflection;
namespace Jellyfin.Plugin.MediaBar.Attributes
{
[AttributeUsage(AttributeTargets.Assembly)]
public class JellyfinVersionAttribute : Attribute
{
public string Version { get; set; }
public JellyfinVersionAttribute(string version)
{
Version = version;
}
public static string? GetVersion()
{
return Assembly.GetExecutingAssembly().GetCustomAttribute<JellyfinVersionAttribute>()?.Version;
}
}
}
@@ -0,0 +1,61 @@
using MediaBrowser.Model.Plugins;
namespace Jellyfin.Plugin.MediaBar.Configuration
{
public enum MediaBarState
{
Disabled,
Enabled,
}
public class PluginConfiguration : BasePluginConfiguration
{
public MediaBarState Enabled { get; set; } = MediaBarState.Enabled;
public string VersionString { get; set; } = "main";
public bool UseAvatarsFile { get; set; } = true;
public string AvatarsPlaylist { get; set; } = string.Empty;
public WebConfig WebConfig { get; set; } = new WebConfig();
}
public class WebConfig
{
public ImageSvgs ImageSvgs { get; set; } = new ImageSvgs();
public int ShuffleInterval { get; set; } = -1;
public int RetryInterval { get; set; } = -1;
public int MinSwipeDistance { get; set; } = -1;
public int LoadingCheckInterval { get; set; } = -1;
public int MaxPlotLength { get; set; } = -1;
public int MaxMovies { get; set; } = -1;
public int MaxTvShows { get; set; } = -1;
public int MaxItems { get; set; } = -1;
public int PreloadCount { get; set; } = -1;
public int FadeTransitionDuration { get; set; } = -1;
public bool SlideAnimationEnabled { get; set; } = true;
}
public class ImageSvgs
{
public string? ImdbLogo { get; set; } = null;
public string? TomatoLogo { get; set; } = null;
public string? FreshTomato { get; set; } = null;
public string? RottenTomato { get; set; } = null;
}
}
@@ -0,0 +1,262 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<title>Media Bar</title>
</head>
<body>
<div data-role="page" id="mediaBarConfigurationPage" class="page type-interior pluginConfigurationPage fullWidthContent">
<div class="content-primary">
<div class="verticalSection">
<div class="sectionTitleContainer">
<h2 class="sectionTitle">Media Bar</h2>
<a is="emby-linkbutton" class="raised raised-mini" style="margin-left: 2em;" target="_blank"
href="https://github.com/IAmParadox27/jellyfin-plugin-media-bar">
<i class="md-icon button-icon button-icon-left secondaryText"></i>
<span>Help</span>
</a>
</div>
</div>
<hr class="solid" style="margin-bottom: 1.5em;">
<div style="margin-bottom: 1.5em;">
<button class="jellyfin-tab-button active" data-tab="general" style="background: none; border: none; color: #fff; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; border-bottom: 2px solid #fff; padding: 0.5em 1em;"><h3>General Settings</h3></button>
<button class="jellyfin-tab-button" data-tab="web-config" style="background: none; border: none; color: #ccc; cursor: pointer; transition: color 0.3s, border-bottom 0.3s; border-bottom: 2px solid transparent; padding: 0.5em 1em;"><h3>Web Config Settings</h3></button>
</div>
<form class="mediaBarConfigurationForm">
<div id="general" class="jellyfin-tab-content active" style="display: block;">
<div id="configurationWrapper">
<div class="inputContainer">
<span>Enabled: </span>
<select is="emby-select" class="emby-select-withcolor emby-select" data-id="enabledSelect">
<option value="Enabled">Enabled</option>
<option value="Disabled">Disabled</option>
</select>
<span>Whether the media bar should appear or not.</span>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="txtVersionString">Version</label>
<label class="inputLabel inputLabel-float inputLabelUnfocused" for="txtVersionString"></label>
<input id="txtVersionString" type="text" is="emby-input" class="emby-input" data-id="txtVersionString" />
<div class="fieldDescription">Which version of the jsdelivr'd plugin should be used.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input type="checkbox" id="chkUseAvatarsFile" is="emby-checkbox" class="emby-checkbox" data-id="chkUseAvatarsFile" />
<span class="checkboxLabel">Use physical avatars file</span>
<span class="checkboxOutline">
<span class="material-icons checkboxIcon checkboxIcon-checked check" aria-hidden="true"></span>
<span class="material-icons checkboxIcon checkboxIcon-unchecked " aria-hidden="true"></span>
</span>
</label>
<div class="fieldDescription">If checked, the plugin will look for a list.txt file in the avatars directory at the root of jellyfin-web.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="txtAvatarsPlaylist">Avatars Playlist</label>
<label class="inputLabel inputLabel-float inputLabelUnfocused" for="txtAvatarsPlaylist"></label>
<input id="txtAvatarsPlaylist" type="text" is="emby-input" class="emby-input" data-id="txtAvatarsPlaylist" />
<div class="fieldDescription">If populated, and "Use physical avatars file" is disabled, this is the name of the playlist which is used to populate the media bar.<br />
If empty, the media bar will randomly populate.</div>
</div>
</div>
</div>
<div id="web-config" class="jellyfin-tab-content" style="display: none;">
<div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="numShuffleInterval">Shuffle Interval</label>
<label class="inputLabel inputLabel-float inputLabelUnfocused" for="numShuffleInterval"></label>
<input id="numShuffleInterval" type="number" is="emby-input" class="emby-input" data-id="numShuffleInterval" />
<div class="fieldDescription">How long (ms) should each slide last before changing to the next? (set to -1 to use default value)</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="numRetryInterval">Retry Interval</label>
<label class="inputLabel inputLabel-float inputLabelUnfocused" for="numRetryInterval"></label>
<input id="numRetryInterval" type="number" is="emby-input" class="emby-input" data-id="numRetryInterval" />
<div class="fieldDescription">How long (ms) should to wait before trying again after a failed initialization? (set to -1 to use default value)</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="numMinSwipeDistance">Min Swipe Distance</label>
<label class="inputLabel inputLabel-float inputLabelUnfocused" for="numMinSwipeDistance"></label>
<input id="numMinSwipeDistance" type="number" is="emby-input" class="emby-input" data-id="numMinSwipeDistance" />
<div class="fieldDescription">How far (px) should the user have to swipe before triggering the next/prev slide? (set to -1 to use default value)</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="numLoadingCheckInterval">Loading Check Interval</label>
<label class="inputLabel inputLabel-float inputLabelUnfocused" for="numLoadingCheckInterval"></label>
<input id="numLoadingCheckInterval" type="number" is="emby-input" class="emby-input" data-id="numLoadingCheckInterval" />
<div class="fieldDescription">How long (ms) between checking whether the login screen or home screen has loaded? (set to -1 to use default value)</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="numMaxPlotLength">Max Plot Length</label>
<label class="inputLabel inputLabel-float inputLabelUnfocused" for="numMaxPlotLength"></label>
<input id="numMaxPlotLength" type="number" is="emby-input" class="emby-input" data-id="numMaxPlotLength" />
<div class="fieldDescription">How many words should be displayed for the plot on the Media Bar? (set to -1 to use default value)</div>
</div>
<div class="inputContainer" style="display:none;"><!-- Config variable doesn't seem to be used -->
<label class="inputLabel inputLabelUnfocused" for="numMaxMovies">Max Movies</label>
<label class="inputLabel inputLabel-float inputLabelUnfocused" for="numMaxMovies"></label>
<input id="numMaxMovies" type="number" is="emby-input" class="emby-input" data-id="numMaxMovies" />
<div class="fieldDescription">? (set to -1 to use default value)</div>
</div>
<div class="inputContainer" style="display:none;"><!-- Config variable doesn't seem to be used -->
<label class="inputLabel inputLabelUnfocused" for="numMaxTvShows">Max TV Shows</label>
<label class="inputLabel inputLabel-float inputLabelUnfocused" for="numMaxTvShows"></label>
<input id="numMaxTvShows" type="number" is="emby-input" class="emby-input" data-id="numMaxTvShows" />
<div class="fieldDescription">? (set to -1 to use default value)</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="numMaxItems">Max Items</label>
<label class="inputLabel inputLabel-float inputLabelUnfocused" for="numMaxItems"></label>
<input id="numMaxItems" type="number" is="emby-input" class="emby-input" data-id="numMaxItems" />
<div class="fieldDescription">Maximum number of items to retrieve from the server, this is also the number of items in the loop? (set to -1 to use default value)</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="numPreloadCount">Preload Count</label>
<label class="inputLabel inputLabel-float inputLabelUnfocused" for="numPreloadCount"></label>
<input id="numPreloadCount" type="number" is="emby-input" class="emby-input" data-id="numPreloadCount" />
<div class="fieldDescription">Number of slides in the future to load before showing them? This helps with ensuring the image is visible when its ready to be displayed. (set to -1 to use default value)</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="numFadeTransitionDuration">Fade Transition Duration</label>
<label class="inputLabel inputLabel-float inputLabelUnfocused" for="numFadeTransitionDuration"></label>
<input id="numFadeTransitionDuration" type="number" is="emby-input" class="emby-input" data-id="numFadeTransitionDuration" />
<div class="fieldDescription">How long (ms) should the fade transition take when changing slide? (set to -1 to use default value)</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription" style="margin-bottom: 1.5em;">
<label class="emby-checkbox-label">
<input data-id="chkSlideAnimationEnabled" id="chkSlideAnimationEnabled" type="checkbox" is="emby-checkbox" class="emby-checkbox">
<span class="checkboxLabel">Slide Animation Enabled?</span>
<span class="checkboxOutline">
<span class="material-icons checkboxIcon checkboxIcon-checked check" aria-hidden="true"></span>
<span class="material-icons checkboxIcon checkboxIcon-unchecked " aria-hidden="true"></span>
</span>
</label>
<div class="fieldDescription">Do the slides animate while they're visible? (the zooming in effect, can be quite performance heavy)</div>
</div>
</div>
</div>
<br />
<button id="saveConfig" is="emby-button" type="submit" class="emby-button raised button-submit block">
<span>Save</span>
</button>
</form>
</div>
<script type="text/javascript">
if (typeof MediaBarConfig == 'undefined') {
const MediaBarConfig = {
pluginId: "08f615ea-2107-4f04-89cc-091035f54448",
configurationWrapper: document.querySelector("#configurationWrapper"),
btnSave: document.querySelector("#saveConfig"),
setupTabs: function() {
const tabs = document.querySelectorAll('.jellyfin-tab-button');
const tabContents = document.querySelectorAll('.jellyfin-tab-content');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => {
t.classList.remove('active');
t.style.color = '#ccc';
t.style.borderBottom = '2px solid transparent';
});
tab.classList.add('active');
tab.style.color = 'var(--primary-accent-color, #fff)';
tab.style.borderBottom = '2px solid var(--primary-accent-color, #fff)';
tabContents.forEach(content => {
content.classList.remove('active');
content.style.display = 'none';
});
const activeTab = document.getElementById(tab.dataset.tab);
activeTab.classList.add('active');
activeTab.style.display = 'block';
});
});
},
saveConfig: function (e) {
e.preventDefault();
Dashboard.showLoadingMsg();
const config = {
Enabled: document.querySelector("[data-id=enabledSelect]").value,
UseAvatarsFile: document.querySelector("[data-id=chkUseAvatarsFile]").checked,
AvatarsPlaylist: document.querySelector("[data-id=txtAvatarsPlaylist]").value,
VersionString: document.querySelector("[data-id=txtVersionString]").value,
WebConfig: {
ImageSvgs: {
ImdbLogo: null,
TomatoLogo: null,
FreshTomato: null,
RottenTomato: null,
},
ShuffleInterval: parseInt(document.querySelector("[data-id=numShuffleInterval]").value, 10),
RetryInterval: parseInt(document.querySelector("[data-id=numRetryInterval]").value, 10),
MinSwipeDistance: parseInt(document.querySelector("[data-id=numMinSwipeDistance]").value, 10),
LoadingCheckInterval: parseInt(document.querySelector("[data-id=numLoadingCheckInterval]").value, 10),
MaxPlotLength: parseInt(document.querySelector("[data-id=numMaxPlotLength]").value, 10),
MaxMovies: parseInt(document.querySelector("[data-id=numMaxMovies]").value, 10),
MaxTvShows: parseInt(document.querySelector("[data-id=numMaxTvShows]").value, 10),
MaxItems: parseInt(document.querySelector("[data-id=numMaxItems]").value, 10),
PreloadCount: parseInt(document.querySelector("[data-id=numPreloadCount]").value, 10),
FadeTransitionDuration: parseInt(document.querySelector("[data-id=numFadeTransitionDuration]").value, 10),
SlideAnimationEnabled: document.querySelector("[data-id=chkSlideAnimationEnabled]").checked,
}
};
window.ApiClient.updatePluginConfiguration(MediaBarConfig.pluginId, config)
.then(Dashboard.processPluginConfigurationUpdateResult)
.catch(function (error) {
console.error(error);
})
.finally(function () {
Dashboard.hideLoadingMsg();
});
},
loadConfig: function () {
Dashboard.showLoadingMsg();
window.ApiClient.getPluginConfiguration(MediaBarConfig.pluginId)
.then(function (config) {
document.querySelector("[data-id=enabledSelect]").value = config.Enabled;
document.querySelector("[data-id=chkUseAvatarsFile]").checked = config.UseAvatarsFile;
document.querySelector("[data-id=txtAvatarsPlaylist]").value = config.AvatarsPlaylist;
if (config.VersionString === 'latest') {
config.VersionString = 'main';
}
document.querySelector("[data-id=txtVersionString]").value = config.VersionString;
document.querySelector("[data-id=numShuffleInterval]").value = config.WebConfig.ShuffleInterval;
document.querySelector("[data-id=numRetryInterval]").value = config.WebConfig.RetryInterval;
document.querySelector("[data-id=numMinSwipeDistance]").value = config.WebConfig.MinSwipeDistance;
document.querySelector("[data-id=numLoadingCheckInterval]").value = config.WebConfig.LoadingCheckInterval;
document.querySelector("[data-id=numMaxPlotLength]").value = config.WebConfig.MaxPlotLength;
document.querySelector("[data-id=numMaxMovies]").value = config.WebConfig.MaxMovies;
document.querySelector("[data-id=numMaxTvShows]").value = config.WebConfig.MaxTvShows;
document.querySelector("[data-id=numMaxItems]").value = config.WebConfig.MaxItems;
document.querySelector("[data-id=numPreloadCount]").value = config.WebConfig.PreloadCount;
document.querySelector("[data-id=numFadeTransitionDuration]").value = config.WebConfig.FadeTransitionDuration;
document.querySelector("[data-id=chkSlideAnimationEnabled]").checked = config.WebConfig.SlideAnimationEnabled;
})
.catch(function (error) {
console.error(error);
})
.finally(function () {
Dashboard.hideLoadingMsg();
});
},
init: function () {
MediaBarConfig.setupTabs();
MediaBarConfig.btnSave.addEventListener("click", MediaBarConfig.saveConfig);
MediaBarConfig.loadConfig();
}
}
// view.addEventListener("viewshow", function (e) {
document.querySelector('#mediaBarConfigurationPage').addEventListener("pageshow", function () {
MediaBarConfig.init();
});
}
</script>
</div>
</body>
</html>
@@ -0,0 +1,60 @@
using System.Reflection;
using Jellyfin.Extensions;
using Jellyfin.Plugin.MediaBar.Configuration;
using Jellyfin.Plugin.MediaBar.Helpers;
using Jellyfin.Plugin.MediaBar.Model;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using Microsoft.AspNetCore.Mvc;
using MediaBrowser.Controller.Entities.TV;
namespace Jellyfin.Plugin.MediaBar.Controllers
{
[Route("[controller]")]
public class MediaBarController : ControllerBase
{
[HttpGet("{file}")]
public ActionResult GetFile([FromRoute] string file)
{
Stream fileStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Jellyfin.Plugin.MediaBar.Inject." + file)!;
string fileContents = new StreamReader(fileStream).ReadToEnd();
string contentType = "text/plain";
if (Path.GetExtension(file) == ".js")
{
contentType = "text/javascript";
}
else if (Path.GetExtension(file) == ".css")
{
contentType = "text/css";
}
else if (Path.GetExtension(file) == ".html")
{
contentType = "text/html";
}
return Content(fileContents, contentType);
}
[HttpPost("Avatar/List")]
public ActionResult GetAvatarsList([FromBody] PatchRequestPayload payload)
{
string? content = TransformationPatches.AvatarsList(payload);
if (content == null)
{
return NotFound();
}
return Content(content, "text/plain");
}
[HttpGet("WebConfig")]
public ActionResult<WebConfig> GetWebConfig()
{
return Ok(MediaBarPlugin.Instance.Configuration.WebConfig);
}
}
}
@@ -0,0 +1,139 @@
using System.Reflection;
using System.Text.RegularExpressions;
using Jellyfin.Extensions;
using Jellyfin.Plugin.MediaBar.Attributes;
using Jellyfin.Plugin.MediaBar.Configuration;
using Jellyfin.Plugin.MediaBar.Model;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using Microsoft.Extensions.DependencyInjection;
namespace Jellyfin.Plugin.MediaBar.Helpers
{
public static class TransformationPatches
{
public static string AvatarsList(PatchRequestPayload payload)
{
IPlaylistManager playlistManager = MediaBarPlugin.Instance.ServiceProvider.GetRequiredService<IPlaylistManager>();
IUserManager userManager = MediaBarPlugin.Instance.ServiceProvider.GetRequiredService<IUserManager>();
return AvatarsList(payload, playlistManager, userManager) ?? "";
}
private static string? AvatarsList(PatchRequestPayload payload, IPlaylistManager playlistManager, IUserManager userManager)
{
if (MediaBarPlugin.Instance.Configuration.UseAvatarsFile)
{
return payload.Contents;
}
IEnumerable<Guid> allUserIds = userManager.UsersIds;
Playlist? playlist = null;
Guid? userIdToUse = null;
foreach (Guid userId in allUserIds)
{
playlist = playlistManager.GetPlaylists(userId)
.FirstOrDefault(x => x.Name == MediaBarPlugin.Instance.Configuration.AvatarsPlaylist);
if (playlist != null)
{
userIdToUse = userId;
break;
}
}
if (playlist == null || userIdToUse == null)
{
return payload.Contents;
}
IEnumerable<Tuple<LinkedChild, BaseItem>> itemsRaw = playlist.GetManageableItems()
.Where(i => i.Item2.IsVisible(userManager.GetUserById(userIdToUse.Value)));
StringWriter stringWriter = new StringWriter();
stringWriter.WriteLine(MediaBarPlugin.Instance.Configuration.AvatarsPlaylist);
List<Guid> idsWritten = new List<Guid>();
foreach (Tuple<LinkedChild, BaseItem> item in itemsRaw)
{
BaseItem itemToUse = item.Item2;
if (item.Item2 is Episode episode)
{
itemToUse = episode.Series;
}
if (!idsWritten.Contains(itemToUse.Id))
{
idsWritten.Add(itemToUse.Id);
}
}
idsWritten.Shuffle();
foreach (Guid id in idsWritten)
{
// For some reason the JF api doesn't treat GUIDs correctly
stringWriter.WriteLine(id.ToString().Replace("-", ""));
}
return stringWriter.ToString();
}
public static string IndexHtml(PatchRequestPayload payload)
{
if (MediaBarPlugin.Instance.Configuration.Enabled == MediaBarState.Disabled)
{
return payload.Contents ?? string.Empty;
}
Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"{typeof(MediaBarPlugin).Namespace}.Inject.index.html")!;
using TextReader reader = new StreamReader(stream);
if (MediaBarPlugin.Instance.Configuration.VersionString == "latest")
{
MediaBarPlugin.Instance.Configuration.VersionString = "main";
}
if (MediaBarPlugin.Instance.Configuration.VersionString == "main" &&
JellyfinVersionAttribute.GetVersion() == "10.11")
{
// Force 10.11 branch instead of main for now
MediaBarPlugin.Instance.Configuration.VersionString = "10.11";
}
string importedHtml = reader
.ReadToEnd()
.Replace("{{Config.VersionString}}", MediaBarPlugin.Instance.Configuration.VersionString);
string regex = Regex.Replace(payload.Contents!, "(</head>)", $"{importedHtml}$1");
return regex;
}
public static string HomeHtmlChunk(PatchRequestPayload payload)
{
Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"{typeof(MediaBarPlugin).Namespace}.Inject.home-html.chunk.js")!;
using TextReader reader = new StreamReader(stream);
string regex = Regex.Replace(payload.Contents!, "(id=\"homeTab\" data-index=\"0\">)", $"$1{reader.ReadToEnd()}");
return regex;
}
public static string MainBundle(PatchRequestPayload payload)
{
string replacementText =
"window.PlaybackManager=this.playbackManager;console.log(\"PlaybackManager is now globally available:\",window.PlaybackManager);";
string regex = Regex.Replace(payload.Contents!, @"(this\.playbackManager=e,)", $"$1{replacementText}");
return regex;
}
}
}
@@ -0,0 +1,68 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/IAmParadox27/jellyfin-plugin-media-bar@{{Config.VersionString}}/slideshowpure.css" />
<script defer src="https://cdn.jsdelivr.net/gh/IAmParadox27/jellyfin-plugin-media-bar@{{Config.VersionString}}/slideshowpure.js"></script>
<script defer>
if (typeof MediaBarConfigHandler == 'undefined') {
const MediaBarConfigHandler = {
loadConfig: async function () {
return await window.ApiClient.getJSON(window.ApiClient.getUrl("MediaBar/WebConfig"));
},
init: async function () {
let config = await MediaBarConfigHandler.loadConfig();
if (config.ImageSvgs.ImdbLogo !== null && config.ImageSvgs.ImdbLogo !== undefined) {
window.slideshowPure.CONFIG.IMAGE_SVG.imdbLogo = config.ImageSvgs.ImdbLogo;
}
if (config.ImageSvgs.TomatoLogo !== null && config.ImageSvgs.TomatoLogo !== undefined) {
window.slideshowPure.CONFIG.IMAGE_SVG.imdbLogo = config.ImageSvgs.TomatoLogo;
}
if (config.ImageSvgs.FreshTomato !== null && config.ImageSvgs.FreshTomato !== undefined) {
window.slideshowPure.CONFIG.IMAGE_SVG.imdbLogo = config.ImageSvgs.FreshTomato;
}
if (config.ImageSvgs.RottenTomato !== null && config.ImageSvgs.RottenTomato !== undefined) {
window.slideshowPure.CONFIG.IMAGE_SVG.imdbLogo = config.ImageSvgs.RottenTomato;
}
if (config.ShuffleInterval >= 0) {
window.slideshowPure.CONFIG.shuffleInterval = config.ShuffleInterval;
}
if (config.RetryInterval >= 0) {
window.slideshowPure.CONFIG.retryInterval = config.RetryInterval;
}
if (config.MinSwipeDistance >= 0) {
window.slideshowPure.CONFIG.minSwipeDistance = config.MinSwipeDistance;
}
if (config.LoadingCheckInterval >= 0) {
window.slideshowPure.CONFIG.loadingCheckInterval = config.LoadingCheckInterval;
}
if (config.MaxPlotLength >= 0) {
window.slideshowPure.CONFIG.maxPlotLength = config.MaxPlotLength;
}
if (config.MaxMovies >= 0) {
window.slideshowPure.CONFIG.maxMovies = config.MaxMovies;
}
if (config.MaxTvShows >= 0) {
window.slideshowPure.CONFIG.maxTvShows = config.MaxTvShows;
}
if (config.MaxItems >= 0) {
window.slideshowPure.CONFIG.maxItems = config.MaxItems;
}
if (config.PreloadCount >= 0) {
window.slideshowPure.CONFIG.preloadCount = config.PreloadCount;
}
if (config.FadeTransitionDuration >= 0) {
window.slideshowPure.CONFIG.fadeTransitionDuration = config.FadeTransitionDuration;
}
window.slideshowPure.CONFIG.slideAnimationEnabled = config.SlideAnimationEnabled;
}
};
let waitingForPluginInterval = setInterval(function () {
if (window.slideshowPure && window.slideshowPure.CONFIG && window.ApiClient) {
clearInterval(waitingForPluginInterval);
MediaBarConfigHandler.init().then(r => {
console.log('Loaded Media Bar plugin config into plugin javascript');
});
}
}, 100);
}
</script>
@@ -0,0 +1,59 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<JellyfinVersion>10.11.2</JellyfinVersion>
<JellyfinNugetVersion Condition="!$(JellyfinVersion.StartsWith('10.11'))">$(JellyfinVersion)</JellyfinNugetVersion>
<JellyfinNugetVersion Condition="$(JellyfinVersion.StartsWith('10.11'))">$(JellyfinVersion)</JellyfinNugetVersion>
<JellyfinNugetVersion Condition="$(JellyfinVersion.StartsWith('10.11.1'))">10.11.0</JellyfinNugetVersion>
<JellyfinNugetVersion Condition="$(JellyfinVersion.StartsWith('10.11.0'))">10.11.0</JellyfinNugetVersion>
<TargetFramework Condition="$(JellyfinVersion.StartsWith('10.11'))">net9.0</TargetFramework>
<TargetFramework Condition="'$(JellyfinVersion)' == '10.10.7'">net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<Version>2.4.0.0</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jellyfin.Model" Version="$(JellyfinNugetVersion)" />
<PackageReference Include="Jellyfin.Controller" Version="$(JellyfinNugetVersion)" />
<PackageReference Include="Jellyfin.Common" Version="$(JellyfinNugetVersion)" />
<PackageReference Include="Jellyfin.Data" Version="$(JellyfinNugetVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<!-- 10.10.7 Specific Code from non 10.10.7 builds -->
<ItemGroup Condition="'$(JellyfinVersion)' != '10.10.7'">
<None Include="JellyfinVersionSpecific/10.10.7/*.cs" />
<Compile Remove="JellyfinVersionSpecific/10.10.7/*.cs" />
</ItemGroup>
<!-- Remove 10.11.0 Specific Code from non 10.11.0 builds -->
<ItemGroup Condition="'$(JellyfinVersion)' != '10.11.0'">
<None Include="JellyfinVersionSpecific/10.11.0/*.cs" />
<Compile Remove="JellyfinVersionSpecific/10.11.0/*.cs" />
</ItemGroup>
<ItemGroup Condition="!$(JellyfinVersion.StartsWith('10.11'))">
<None Include="JellyfinVersionSpecific/10.11/*.cs" />
<Compile Remove="JellyfinVersionSpecific/10.11/*.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Configuration\config.html" />
<None Remove="Inject\index.html" />
<EmbeddedResource Include="Inject\index.html" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\README.md" />
<None Include="..\logo.png" CopyToOutputDirectory="Always" />
<None Include="..\..\slideshowpure.js" />
<None Include="..\..\slideshowpure.css" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="Exists('C:\ProgramData\Jellyfin\Server_10_11\plugins\MediaBar')">
<Exec Condition="'$(JellyfinVersion)' == '10.11.1'" Command="xcopy $(TargetDir) &quot;C:\ProgramData\Jellyfin\Server_10_11\plugins\MediaBar&quot; /y" />
<Exec Condition="'$(JellyfinVersion)' == '10.10.7'" Command="xcopy $(TargetDir) &quot;C:\ProgramData\Jellyfin\Server_10_10_7\plugins\MediaBar&quot; /y" />
</Target>
</Project>
@@ -0,0 +1,15 @@
using MediaBrowser.Model.Tasks;
namespace Jellyfin.Plugin.MediaBar.JellyfinVersionSpecific
{
public static class StartupServiceHelper
{
public static IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
yield return new TaskTriggerInfo()
{
Type = TaskTriggerInfo.TriggerStartup
};
}
}
}
@@ -0,0 +1,15 @@
using MediaBrowser.Model.Tasks;
namespace Jellyfin.Plugin.MediaBar.JellyfinVersionSpecific
{
public static class StartupServiceHelper
{
public static IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
yield return new TaskTriggerInfo()
{
Type = TaskTriggerInfoType.StartupTrigger
};
}
}
}
@@ -0,0 +1,37 @@
using Jellyfin.Plugin.MediaBar.Configuration;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
namespace Jellyfin.Plugin.MediaBar
{
public class MediaBarPlugin : BasePlugin<PluginConfiguration>, IHasPluginConfiguration, IHasWebPages
{
public override Guid Id => Guid.Parse("08f615ea-2107-4f04-89cc-091035f54448");
public override string Name => "Media Bar";
public static MediaBarPlugin Instance { get; private set; } = null!;
public IServiceProvider ServiceProvider { get; }
public MediaBarPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, IServiceProvider serviceProvider) : base(applicationPaths, xmlSerializer)
{
Instance = this;
ServiceProvider = serviceProvider;
}
public IEnumerable<PluginPageInfo> GetPages()
{
string? prefix = GetType().Namespace;
yield return new PluginPageInfo
{
Name = Name,
EmbeddedResourcePath = $"{prefix}.Configuration.config.html"
};
}
}
}
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.MediaBar.Model
{
public class PatchRequestPayload
{
[JsonPropertyName("contents")]
public string? Contents { get; set; }
}
}
@@ -0,0 +1,14 @@
using Jellyfin.Plugin.MediaBar.Services;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Plugins;
using Microsoft.Extensions.DependencyInjection;
namespace Jellyfin.Plugin.MediaBar
{
public class PluginServiceRegistrator : IPluginServiceRegistrator
{
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
{
}
}
}
@@ -0,0 +1,9 @@
using System.Reflection;
using Jellyfin.Plugin.MediaBar.Attributes;
[assembly: AssemblyCompany("IAmParadox27")]
[assembly: AssemblyProduct("Jellyfin.Plugins.MediaBar")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyTitle("Jellyfin.Plugins.MediaBar")]
[assembly: AssemblyVersion("2.4.0.0")]
[assembly: JellyfinVersion("10.11.2")]
@@ -0,0 +1,94 @@
using System.IO.Pipes;
using System.Net;
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using Jellyfin.Plugin.MediaBar.Helpers;
using Jellyfin.Plugin.MediaBar.JellyfinVersionSpecific;
using Jellyfin.Plugin.MediaBar.Model;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Jellyfin.Plugin.MediaBar.Services
{
public class StartupService : IScheduledTask
{
public string Name => "MediaBar Startup";
public string Key => "Jellyfin.Plugin.MediaBar.Startup";
public string Description => "Startup Service for MediaBar";
public string Category => "Startup Services";
private readonly IServerApplicationHost m_serverApplicationHost;
private readonly ILogger<MediaBarPlugin> m_logger;
private readonly IUserManager m_userManager;
private readonly IPlaylistManager m_playlistManager;
public StartupService(IServerApplicationHost serverApplicationHost, ILogger<MediaBarPlugin> logger,
IUserManager userManager, IPlaylistManager playlistManager)
{
m_serverApplicationHost = serverApplicationHost;
m_logger = logger;
m_userManager = userManager;
m_playlistManager = playlistManager;
}
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
m_logger.LogInformation($"MediaBar Startup. Registering file transformations.");
List<JObject> payloads = new List<JObject>();
{
JObject payload = new JObject();
payload.Add("id", "0dfac9d7-d898-4944-900b-1c1837707279");
payload.Add("fileNamePattern", "index.html");
payload.Add("callbackAssembly", GetType().Assembly.FullName);
payload.Add("callbackClass", typeof(TransformationPatches).FullName);
payload.Add("callbackMethod", nameof(TransformationPatches.IndexHtml));
payloads.Add(payload);
}
{
JObject payload = new JObject();
payload.Add("id", "8d374d6b-3c5b-464a-a2a2-96e92fa81345");
payload.Add("fileNamePattern", "avatars/list.txt");
payload.Add("callbackAssembly", GetType().Assembly.FullName);
payload.Add("callbackClass", typeof(TransformationPatches).FullName);
payload.Add("callbackMethod", nameof(TransformationPatches.AvatarsList));
payloads.Add(payload);
}
Assembly? fileTransformationAssembly =
AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x =>
x.FullName?.Contains(".FileTransformation") ?? false);
if (fileTransformationAssembly != null)
{
Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
if (pluginInterfaceType != null)
{
foreach (JObject payload in payloads)
{
pluginInterfaceType.GetMethod("RegisterTransformation")?.Invoke(null, new object?[] { payload });
}
}
}
return Task.CompletedTask;
}
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => StartupServiceHelper.GetDefaultTriggers();
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB