Jellyfin Forum
Audiobook Speed Selector - Printable Version

+- Jellyfin Forum (https://forum.jellyfin.org)
+-- Forum: Support (https://forum.jellyfin.org/f-support)
+--- Forum: Guides, Walkthroughs & Tutorials (https://forum.jellyfin.org/f-guides-walkthroughs-tutorials)
+--- Thread: Audiobook Speed Selector (/t-audiobook-speed-selector)



Audiobook Speed Selector - Jamestorn - 2026-07-17

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

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');
})();



RE: Audiobook Speed Selector - TheDreadPirate - 2026-07-18

Perhaps you could try incorporating that into jellyfin-web proper instead of injecting it. Fork jellyfin-web, add your java script, build and test.


RE: Audiobook Speed Selector - Jamestorn - 2026-07-18

I might try it but I'm happy to let a real developer swoop in and build it out properly. I'm a tester, not a coder. I've been thru a few syno community builds and it hasn't broken anything yet. I'm just happy to be able to listen at a decent speed now.

The chinstrap is a little tight on moble. The better implementation is likely a pop-out button. Whatever the case, works well enough for me and the rest of the family gets a server that's not always down.