• Login
  • Register
  • Login Register
    Login
    Username/Email:
    Password:
    Or login with a social network below
  • Forum
  • Website
  • GitHub
  • Status
  • Translation
  • Features
  • Team
  • Rules
  • Help
  • Feeds
User Links
  • Login
  • Register
  • Login Register
    Login
    Username/Email:
    Password:
    Or login with a social network below

    Useful Links Forum Website GitHub Status Translation Features Team Rules Help Feeds
    Jellyfin Forum Support Guides, Walkthroughs & Tutorials Audiobook Speed Selector

     
    • 0 Vote(s) - 0 Average

    Audiobook Speed Selector

    Enables speed control for audiobooks
    Jamestorn
    Offline

    Junior Member

    Posts: 12
    Threads: 1
    Joined: 2024 Nov
    Reputation: 0
    Country:United States
    #1
    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

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

    Community Moderator

    Posts: 15,841
    Threads: 11
    Joined: 2023 Jun
    Reputation: 478
    Country:United States
    #2
    2026-07-18, 01:37 AM
    Perhaps you could try incorporating that into jellyfin-web proper instead of injecting it. Fork jellyfin-web, add your java script, build and test.
    Jellyfin 10.11.11 (Docker)
    Debian 13 w/Xanmod amd64v3 LTS kernel
    AMD Ryzen 5500 w/32GB DDR4
    Intel Arc A380
    OS drive - SK Hynix P41 1TB
    ZFS Storage pool
        vdev1 - 6x WD Red Pro 6TB CMR in RAIDZ1
        vdev2 - 3x WD Red Pro 18TB CMR in RAIDZ1
    [Image: GitHub%20Sponsors-grey?logo=github]
    Jamestorn
    Offline

    Junior Member

    Posts: 12
    Threads: 1
    Joined: 2024 Nov
    Reputation: 0
    Country:United States
    #3
    2026-07-18, 01:52 AM
    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.
    « Next Oldest | Next Newest »

    Users browsing this thread: 1 Guest(s)


    • View a Printable Version
    • Subscribe to this thread
    Forum Jump:

    Home · Team · Help · Contact
    © Designed by D&D - Powered by MyBB
    L


    Jellyfin

    The Free Software Media System

    Linear Mode
    Threaded Mode