2026-07-17, 10:37 PM
I've been using Jellyfin for a year or so but I wanted to listen to audiobooks. Unfortunately 1x speed is usually too slow for me. I VIBECODED this Java Script Injector script and after months of use it seems to work well.
Its fairly basic and by default it only has 1x 1.25x 1.5x and 2x speeds but those can be changed fairly easily. It should work on both the chin strap bar and the now playing page. While I'm not actively developing it, I am open to fixing bugs and minor tweaks. As said before this was a vibecoded script and while I checked functionality and its fine for me your mileage may vary. Feedback (especially constructive criticism) is appreciated. I'm no developer, not even as a hobby, but I hope this helps some of you out there
Prerequisite Plugin: The Java Script Injector by n00bcodr
Its fairly basic and by default it only has 1x 1.25x 1.5x and 2x speeds but those can be changed fairly easily. It should work on both the chin strap bar and the now playing page. While I'm not actively developing it, I am open to fixing bugs and minor tweaks. As said before this was a vibecoded script and while I checked functionality and its fine for me your mileage may vary. Feedback (especially constructive criticism) is appreciated. I'm no developer, not even as a hobby, but I hope this helps some of you out there
Prerequisite Plugin: The Java Script Injector by n00bcodr
Code:
(function() {
'use strict';
let currentSpeed = 1.0;
const addedLocations = new Set();
// ── NEW: Reapply speed whenever a new track loads ──────────────────────
function attachAudioListeners() {
const audio = document.querySelector('audio');
if (!audio || audio._speedListenerAttached) return;
audio._speedListenerAttached = true;
// Fires when a new src is loaded and ready to play
audio.addEventListener('loadedmetadata', () => {
if (audio.playbackRate !== currentSpeed) {
audio.playbackRate = currentSpeed;
console.log('JSI: Reapplied speed ' + currentSpeed + 'x on new track');
}
});
// Belt-and-suspenders: also catch the play event
audio.addEventListener('play', () => {
if (audio.playbackRate !== currentSpeed) {
audio.playbackRate = currentSpeed;
}
});
}
// ───────────────────────────────────────────────────────────────────────
function createSpeedControls(locationId) {
const speeds = [1, 1.25, 1.5, 2];
const speedContainer = document.createElement('div');
speedContainer.className = 'audiobook-speed-controls-' + locationId;
speedContainer.style.cssText = `
display: inline-flex;
gap: 6px;
align-items: center;
margin: 0 10px;
padding: 4px 8px;
background: transparent;
border-radius: 4px;
`;
speeds.forEach(speed => {
const btn = document.createElement('button');
btn.textContent = speed + 'x';
btn.className = 'paper-icon-button-light speed-btn-' + speed;
btn.style.cssText = `
background: ${speed === currentSpeed ? '#00a4dc' : 'rgba(255, 255, 255, 0.2)'};
color: white;
border: none;
padding: 4px 10px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
min-width: 40px;
`;
btn.onclick = () => {
const audio = document.querySelector('audio');
if (audio) {
audio.playbackRate = speed;
currentSpeed = speed;
console.log('JSI: Set speed to ' + speed + 'x');
document.querySelectorAll('[class*="speed-btn-"]').forEach(b => {
const btnSpeed = parseFloat(b.textContent);
b.style.background = btnSpeed === speed ? '#00a4dc' : 'rgba(255, 255, 255, 0.2)';
});
}
};
speedContainer.appendChild(btn);
});
return speedContainer;
}
function addSpeedControls() {
attachAudioListeners(); // ── NEW: check for audio element each cycle
const nowPlayingBar = document.querySelector('.nowPlayingBar');
if (nowPlayingBar && !addedLocations.has('nowPlayingBar')) {
const buttonsContainer = nowPlayingBar.querySelector('.nowPlayingBarCenter, .nowPlayingBarTop');
if (buttonsContainer && !buttonsContainer.querySelector('.audiobook-speed-controls-bar')) {
buttonsContainer.appendChild(createSpeedControls('bar'));
addedLocations.add('nowPlayingBar');
console.log('JSI: Speed controls added to Now Playing Bar');
}
}
const queuePage = document.querySelector('.nowPlayingPageUserDataButtonsContainer, .nowPlayingPageImage');
if (queuePage && !addedLocations.has('queuePage')) {
let container = queuePage.parentElement;
while (container && !container.querySelector('.audiobook-speed-controls-queue')) {
if (container.querySelector('button')) {
if (!container.querySelector('.audiobook-speed-controls-queue')) {
container.appendChild(createSpeedControls('queue'));
addedLocations.add('queuePage');
console.log('JSI: Speed controls added to Queue page');
break;
}
}
container = container.parentElement;
if (!container || container === document.body) break;
}
}
if (!document.querySelector('.nowPlayingBar')) {
addedLocations.delete('nowPlayingBar');
}
if (!document.querySelector('.nowPlayingPageUserDataButtonsContainer')) {
addedLocations.delete('queuePage');
}
// ── NEW: If the audio element was replaced, re-attach listeners
const audio = document.querySelector('audio');
if (audio && !audio._speedListenerAttached) {
attachAudioListeners();
}
}
setInterval(addSpeedControls, 1000);
const observer = new MutationObserver(addSpeedControls);
observer.observe(document.body, {
childList: true,
subtree: true
});
console.log('JSI: Multi-location speed control script loaded');
})();
