2023-11-12, 09:04 AM
(This post was last modified: 2023-11-12, 09:40 AM by SethBacon. Edited 3 times in total.)
Not a linux guy, ive been editing the slideshow js directly; I've managed to get it selectable by remote with
slide.setAttribute('tabindex', '0');
slide.addEventListener('keydown', function(event) {
switch(event.keyCode) {
case 13: // Enter key
// Code to handle the selection
window.location.href = this.href;
break;
// Add cases for other keys if needed
}
});
Didn't feel like worth a full fork, but heres my slideshow.html
Changes:
-Reads list.txt (ignoring labels after id codes), if list.txt is not present, grabs 200 movies from server and randomizes a selection of them to display (because i cant find an API call for random selection)
-Fetches the plot through API - movie.Overview
-Accessible / Clickable in webUI (see video)
-Offset top border & title font to match my JF theme (ymmv)
-Text bottom justified (this number of chrs looks good on my desktop widescreen and on my phone, though ive set my slide frame to 350px tall)
-Config vars grouped at beginning of JS
-Various visual changes
Issues:
Pressing the webui back button (or remote controll back) after selecting a slide and going to its page will lock up JF. Using browser back button works fine, or pressing the webui home button works fine. I tried different referral options but no luck so far.
My border highlight for the selection of the frame doesnt seem to be working, I have a hard time testing this accessibility on desktop.
I dont know how you got this to work on Jellyfin for Android TV, it wont pick up Any server css for me.
<!DOCTYPE html>
<html>
<head>
<title>Jellyfin Featured Slideshow</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans&display=swap" rel="stylesheet">
<style>
/* CSS styles for the slideshow elements */
body {
margin: 0;
padding: 0;
overflow: hidden;
}
.slide {
position: relative;
width: 100vw;
height: 100vh;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
cursor: pointer; /* Indicates the element is clickable */
outline: none; /* Custom focus style will be used */
}
.slide:focus {
outline: 2px solid #fff; /* Visual focus indicator */
}
.backdrop {
position: absolute;
top: 50px;
left: 0;
width: 100%;
height: calc(100% - 50px);
object-fit: cover;
object-position: center 20%;
border-radius: 5px;
z-index: 1;
}
.logo {
position: absolute;
top: 65%;
left: 10px;
transform: translateY(-50%);
max-height: 50%;
max-width: 30%;
width: auto;
z-index: 3;
}
.featured-content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 50px;
background-color: transparent;
font-family: 'Noto Sans', sans-serif;
color: #D3D3D3;
font-size: 22px;
display: flex;
align-items: center;
justify-content: flex-start;
z-index: 2;
}
.plot {
position: absolute;
bottom: 0px;
left: 0; /* Align to the left edge */
right: 0; /* Stretch to the right edge */
color: white;
width: 100%; /* Full width */
font-family: 'Noto Sans', sans-serif;
font-size: 15px;
background: linear-gradient(to top, rgba(0, 0, 0, 1) 20%, rgba(0, 0, 0, 0) 100%);
padding: 10px;
border-radius: 5px;
z-index: 4;
box-sizing: border-box;
}
</style>
</head>
<body>
<!-- Container for dynamic slides -->
<div id="slides-container"></div>
<!-- JavaScript for fetching movies and creating the slideshow -->
<script>
// Configuration variables
let title = 'Spotlight'; // Default title
const userId = '0e0755005f074a59bc0108cc2c3e650b'; // Replace with your User ID
const token = 'de81a85d55024d7b9fa7b11031de7539'; // Replace with your API token
const shuffleInterval = 10000; // Time in milliseconds between slide changes (25000ms = 25 seconds)
const listFileName = 'list.txt'; // Name of the file containing the list of movie IDs
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function truncateText(element, maxLength) {
var truncated = element.innerText;
if (truncated.length > maxLength) {
truncated = truncated.substr(0, maxLength) + '...';
}
element.innerText = truncated;
}
function createSlideForMovie(movie, title) {
const container = document.getElementById('slides-container');
const slideElement = createSlideElement(movie, title);
container.appendChild(slideElement);
}
function createSlideElement(movie, title) {
const itemId = movie.Id;
const plot = movie.Overview;
const slide = document.createElement('a');
slide.className = 'slide';
slide.href =
slide.target = '_top';
slide.rel = 'noreferrer';
slide.setAttribute('tabindex', '0'); // Make the slide focusable
// Key event listener for remote control input
slide.addEventListener('keydown', function(event) {
if (event.keyCode === 13) { // Enter key
window.location.href = this.href;
}
});
const backdrop = document.createElement('img');
backdrop.className = 'backdrop';
backdrop.src =
backdrop.alt = 'Backdrop';
const logo = document.createElement('img');
logo.className = 'logo';
logo.src =
logo.alt = 'Logo';
const featuredContent = document.createElement('div');
featuredContent.className = 'featured-content';
featuredContent.textContent = title;
const plotElement = document.createElement('div');
plotElement.className = 'plot';
plotElement.textContent = plot;
// Truncate the text of this specific plot element
truncateText(plotElement, 240); // Adjust 240 to your preferred character limit
slide.appendChild(backdrop);
slide.appendChild(logo);
slide.appendChild(featuredContent);
slide.appendChild(plotElement);
return slide;
}
function initializeSlideshow() {
var slides = document.querySelectorAll(".slide");
var currentSlide = 0;
var shuffledIndexes = shuffleArray(Array.from({ length: slides.length }, (_, i) => i));
function showSlide(index) {
for (var i = 0; i < slides.length; i++) {
slides[i].style.display = "none";
}
slides[shuffledIndexes[index]].style.display = "block";
}
function nextSlide() {
currentSlide = (currentSlide + 1) % slides.length;
showSlide(currentSlide);
}
showSlide(currentSlide);
setInterval(nextSlide, shuffleInterval);
}
function fetchMovies() {
const noCacheUrl = listFileName + '?' + new Date().getTime();
fetch(noCacheUrl)
.then(response => {
if (response.ok) {
return response.text();
} else {
throw new Error('list.txt not found, fetching movies from server.');
}
})
.then(text => {
const lines = text.split('\n').filter(Boolean);
title = lines.shift() || 'Spotlight'; // Set the global title
const movieIds = lines.map(line => line.substring(0, 32));
return Promise.all(movieIds.map(id => fetchMovieDetails(id)));
})
.then(movies => {
movies.forEach(movie => createSlideForMovie(movie, title));
initializeSlideshow();
})
.catch(error => {
console.error(error);
fetchMoviesFromServer(); // Fallback to fetching movies from the server
});
}
function fetchMovieDetails(movieId) {
return fetch(
headers: {
'Authorization':
}
})
.then(response => response.json())
.then(movie => {
console.log("Movie Title:", movie.Name);
console.log("Movie Overview:", movie.Overview);
return movie;
});
}
function fetchMoviesFromServer() {
title = 'Spotlight'; // Reset title to 'Spotlight' when fetching from server
fetch(
headers: {
'Authorization':
}
})
.then(response => response.json())
.then(data => {
const movies = data.Items;
const shuffledMovies = shuffleArray(movies);
const selectedMovieIds = shuffledMovies.slice(0, 30).map(movie => movie.Id);
return Promise.all(selectedMovieIds.map(id => fetchMovieDetails(id)));
})
.then(movies => {
movies.forEach(movie => createSlideForMovie(movie, 'Spotlight'));
initializeSlideshow();
})
.catch(error => console.error('Error fetching movies:', error));
}
fetchMovies();
</script>
</body>
</html>
slide.setAttribute('tabindex', '0');
slide.addEventListener('keydown', function(event) {
switch(event.keyCode) {
case 13: // Enter key
// Code to handle the selection
window.location.href = this.href;
break;
// Add cases for other keys if needed
}
});
Didn't feel like worth a full fork, but heres my slideshow.html
Changes:
-Reads list.txt (ignoring labels after id codes), if list.txt is not present, grabs 200 movies from server and randomizes a selection of them to display (because i cant find an API call for random selection)
-Fetches the plot through API - movie.Overview
-Accessible / Clickable in webUI (see video)
-Offset top border & title font to match my JF theme (ymmv)
-Text bottom justified (this number of chrs looks good on my desktop widescreen and on my phone, though ive set my slide frame to 350px tall)
-Config vars grouped at beginning of JS
-Various visual changes
Issues:
Pressing the webui back button (or remote controll back) after selecting a slide and going to its page will lock up JF. Using browser back button works fine, or pressing the webui home button works fine. I tried different referral options but no luck so far.
My border highlight for the selection of the frame doesnt seem to be working, I have a hard time testing this accessibility on desktop.
I dont know how you got this to work on Jellyfin for Android TV, it wont pick up Any server css for me.
<!DOCTYPE html>
<html>
<head>
<title>Jellyfin Featured Slideshow</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans&display=swap" rel="stylesheet">
<style>
/* CSS styles for the slideshow elements */
body {
margin: 0;
padding: 0;
overflow: hidden;
}
.slide {
position: relative;
width: 100vw;
height: 100vh;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
cursor: pointer; /* Indicates the element is clickable */
outline: none; /* Custom focus style will be used */
}
.slide:focus {
outline: 2px solid #fff; /* Visual focus indicator */
}
.backdrop {
position: absolute;
top: 50px;
left: 0;
width: 100%;
height: calc(100% - 50px);
object-fit: cover;
object-position: center 20%;
border-radius: 5px;
z-index: 1;
}
.logo {
position: absolute;
top: 65%;
left: 10px;
transform: translateY(-50%);
max-height: 50%;
max-width: 30%;
width: auto;
z-index: 3;
}
.featured-content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 50px;
background-color: transparent;
font-family: 'Noto Sans', sans-serif;
color: #D3D3D3;
font-size: 22px;
display: flex;
align-items: center;
justify-content: flex-start;
z-index: 2;
}
.plot {
position: absolute;
bottom: 0px;
left: 0; /* Align to the left edge */
right: 0; /* Stretch to the right edge */
color: white;
width: 100%; /* Full width */
font-family: 'Noto Sans', sans-serif;
font-size: 15px;
background: linear-gradient(to top, rgba(0, 0, 0, 1) 20%, rgba(0, 0, 0, 0) 100%);
padding: 10px;
border-radius: 5px;
z-index: 4;
box-sizing: border-box;
}
</style>
</head>
<body>
<!-- Container for dynamic slides -->
<div id="slides-container"></div>
<!-- JavaScript for fetching movies and creating the slideshow -->
<script>
// Configuration variables
let title = 'Spotlight'; // Default title
const userId = '0e0755005f074a59bc0108cc2c3e650b'; // Replace with your User ID
const token = 'de81a85d55024d7b9fa7b11031de7539'; // Replace with your API token
const shuffleInterval = 10000; // Time in milliseconds between slide changes (25000ms = 25 seconds)
const listFileName = 'list.txt'; // Name of the file containing the list of movie IDs
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function truncateText(element, maxLength) {
var truncated = element.innerText;
if (truncated.length > maxLength) {
truncated = truncated.substr(0, maxLength) + '...';
}
element.innerText = truncated;
}
function createSlideForMovie(movie, title) {
const container = document.getElementById('slides-container');
const slideElement = createSlideElement(movie, title);
container.appendChild(slideElement);
}
function createSlideElement(movie, title) {
const itemId = movie.Id;
const plot = movie.Overview;
const slide = document.createElement('a');
slide.className = 'slide';
slide.href =
/#!/details?id=${itemId}
;slide.target = '_top';
slide.rel = 'noreferrer';
slide.setAttribute('tabindex', '0'); // Make the slide focusable
// Key event listener for remote control input
slide.addEventListener('keydown', function(event) {
if (event.keyCode === 13) { // Enter key
window.location.href = this.href;
}
});
const backdrop = document.createElement('img');
backdrop.className = 'backdrop';
backdrop.src =
/Items/${itemId}/Images/Backdrop/0
;backdrop.alt = 'Backdrop';
const logo = document.createElement('img');
logo.className = 'logo';
logo.src =
/Items/${itemId}/Images/Logo
;logo.alt = 'Logo';
const featuredContent = document.createElement('div');
featuredContent.className = 'featured-content';
featuredContent.textContent = title;
const plotElement = document.createElement('div');
plotElement.className = 'plot';
plotElement.textContent = plot;
// Truncate the text of this specific plot element
truncateText(plotElement, 240); // Adjust 240 to your preferred character limit
slide.appendChild(backdrop);
slide.appendChild(logo);
slide.appendChild(featuredContent);
slide.appendChild(plotElement);
return slide;
}
function initializeSlideshow() {
var slides = document.querySelectorAll(".slide");
var currentSlide = 0;
var shuffledIndexes = shuffleArray(Array.from({ length: slides.length }, (_, i) => i));
function showSlide(index) {
for (var i = 0; i < slides.length; i++) {
slides[i].style.display = "none";
}
slides[shuffledIndexes[index]].style.display = "block";
}
function nextSlide() {
currentSlide = (currentSlide + 1) % slides.length;
showSlide(currentSlide);
}
showSlide(currentSlide);
setInterval(nextSlide, shuffleInterval);
}
function fetchMovies() {
const noCacheUrl = listFileName + '?' + new Date().getTime();
fetch(noCacheUrl)
.then(response => {
if (response.ok) {
return response.text();
} else {
throw new Error('list.txt not found, fetching movies from server.');
}
})
.then(text => {
const lines = text.split('\n').filter(Boolean);
title = lines.shift() || 'Spotlight'; // Set the global title
const movieIds = lines.map(line => line.substring(0, 32));
return Promise.all(movieIds.map(id => fetchMovieDetails(id)));
})
.then(movies => {
movies.forEach(movie => createSlideForMovie(movie, title));
initializeSlideshow();
})
.catch(error => {
console.error(error);
fetchMoviesFromServer(); // Fallback to fetching movies from the server
});
}
function fetchMovieDetails(movieId) {
return fetch(
/Users/${userId}/Items/${movieId}
, {headers: {
'Authorization':
MediaBrowser Client="Jellyfin Web", Device="YourDeviceName", DeviceId="YourDeviceId", Version="YourClientVersion", Token="${token}"
}
})
.then(response => response.json())
.then(movie => {
console.log("Movie Title:", movie.Name);
console.log("Movie Overview:", movie.Overview);
return movie;
});
}
function fetchMoviesFromServer() {
title = 'Spotlight'; // Reset title to 'Spotlight' when fetching from server
fetch(
/Users/${userId}/Items?IncludeItemTypes=Movie,Series&Recursive=true&Limit=300
, {headers: {
'Authorization':
MediaBrowser Client="Jellyfin Web", Device="YourDeviceName", DeviceId="YourDeviceId", Version="YourClientVersion", Token="${token}"
}
})
.then(response => response.json())
.then(data => {
const movies = data.Items;
const shuffledMovies = shuffleArray(movies);
const selectedMovieIds = shuffledMovies.slice(0, 30).map(movie => movie.Id);
return Promise.all(selectedMovieIds.map(id => fetchMovieDetails(id)));
})
.then(movies => {
movies.forEach(movie => createSlideForMovie(movie, 'Spotlight'));
initializeSlideshow();
})
.catch(error => console.error('Error fetching movies:', error));
}
fetchMovies();
</script>
</body>
</html>