Back button with a pirate skull theme

Song Player

I've seen a lot of people asking for a music player widget, but the main one people were using has been broken for a while. On top of that the music widgets I've seen haven't been customizeable using just HTML/CSS. So I made this one! I'm not sure what instructional text I should put here!
Copy the HTML/CSS/JS below to get started!
Technically you don't need to copy the CSS, but it's what makes the first example!
Use Inspect Element to see how the other examples are styled! >:D Mwahahahaha! (The laugh of someone who likes making people learn things)

<div class="StarrPlayer">
    <div class="SP-track-info">
        <div class="SP-track"><span class="SP-track-name">Really Cool Song</span></div>
        <div class="SP-artist">By: <span class="SP-artist-name">Really Cool Artist</span></div>
        <div class="SP-duration">
            <span class="SP-duration-current">
                0:00
            </span>
            <span class="SP-duration-total">
                X:XX
            </span>
        </div>
        <div class="SP-progress-bar">
            <div class="SP-progress-bar-fill">
                <div class="SP-progress-bar-knob"></div>
            </div>
        </div>
    </div>
    <div class="SP-button-row">
        <div class="SP-prev SP-button">&lt;&lt;</div>
        <div class="SP-play SP-button">Play</div>
        <div class="SP-pause SP-button">Pause</div>
        <div class="SP-stop SP-button">Stop</div>
        <div class="SP-next SP-button">&gt;&gt;</div>
    </div>
</div>
.SP-track-name,
.SP-artist-name {
    display: inline-block;
    text-wrap: nowrap;
    overflow: hidden;
    background-color: #00000085;
    width: 99.5%;
}

.StarrPlayer {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    background-color: #00000085;
    padding: 1rem;
    color: white;
    width: 330px;
}

.SP-track-info {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    flex-direction: column;
    background-color: #00000085;
    padding: 0;
    width: 100%;
    position: relative;
}

.SP-track {
    font-size: 1.5rem;
    font-weight: bold;
    width: 100%;
    background-color: #00000085;
}

.SP-artist {
    font-size: 1.25rem;
    font-style: italic;
    display: flex;
    justify-content: flex-start;
    flex-direction: row;
    flex-wrap: nowrap;
    width: 100%;
    overflow: hidden;
    background-color: #00000085;
}

.SP-duration {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-direction: row;
    gap: 1rem;
    background-color: #00000085;
    width: 100%;
}

.SP-progress-bar {
    width: 100%;
    height: 0.2rem;
    background-color: #ffffff63;
    margin: 0.5rem 0;
}

.SP-progress-bar-fill {
    height: 100%;
    background-color: #8f41fb;
    position: relative;
}

.SP-progress-bar-knob {
    position: absolute;
    top: 50%;
    right: 0%;
    transform: translate(50%, -50%);
    width: 0.75rem;
    height: 0.75rem;
    background-color: #d093ff;
    border-radius: 50%;
}

.SP-button-row {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: row;
    background-color: #00000085;
    width: 100%;
    gap: 0.5rem;
}

.SP-button {
    background-color: #ffffff63;
    color: white;
    padding: 0.25rem 0.75rem;
    border: none;
    cursor: pointer;
    user-select: none;
}

.SP-button:hover {
    background-color: #a8a8a863;
}
// This component is where the magic happens!
// You shouldn't need to modify this unless you are adding new features! >🐢
class MusicPlayer {
    constructor(element, song_data) {
        this.parent_element = element;
        this.audio = null;
        this.current_track_index = -1;
        this.track_element = this.parent_element.querySelector(".SP-track-name");
        this.track_scrolling_left = true;
        this.artist_element = this.parent_element.querySelector(".SP-artist-name");
        this.artist_scrolling_left = true;
        this.song_data = song_data;
        this.isDragging = false;
        this.start();
    }

    start = () => {
        this.initializeButtons();
        this.initializeStatefulRenderingInterval();
        this.initializeProgressBar();
    }

    initializeStatefulRenderingInterval = () => {
        setInterval(() => {
            this.scrollTrackText();
            this.scrollArtistText();
            this.updateProgressBar(this.getSongProgressPercent(this.audio) * 100)
            this.updateProgressText(this.audio);
            this.updateDurationText(this.audio);
            this.renderPauseOrPlayButton();
        }, 50);
    }

    initializeButtons = () => {
        let buttons = this.parent_element.querySelectorAll('.SP-button');
        buttons.forEach((button) => {
            button.addEventListener('click', (event) => {
                switch (true) {
                    case event.target.classList.contains("SP-play"):
                        this.play_song();
                        break;
                    case event.target.classList.contains("SP-pause"):
                        this.pause_song();
                        break;
                    case event.target.classList.contains("SP-next"):
                        this.next_song();
                        break;
                    case event.target.classList.contains("SP-prev"):
                        this.prev_song();
                        break;
                    case event.target.classList.contains("SP-stop"):
                        this.stop_song();
                        break;
                    default:
                        break;
                }
            });
        });
    }

    initializeProgressBar = () => {
        let progressKnob = this.parent_element.querySelector('.SP-progress-bar-knob');
        let progressBar = this.parent_element.querySelector('.SP-progress-bar');
        progressKnob.addEventListener('pointerdown', (event) => {
            this.isDragging = true;
            event.preventDefault();
        });

        this.parent_element.addEventListener('pointermove', (event) => {
            if (!this.isDragging || !this.audio) return;
            const boundingRect = progressBar.getBoundingClientRect();
            let percent = (event.clientX - boundingRect.left) / boundingRect.width;
            percent = Math.max(0, Math.min(1, percent));
            this.audio.currentTime = this.audio.duration * percent;
            this.updateProgressBar(percent * 100);
        });
        document.addEventListener('pointerup', () => {
            this.isDragging = false;
        });
        
        progressBar.addEventListener('click', (event) => {
            if (this.audio == null || this.isDragging) return; // Skip if dragging or audio is not initialized
            const bounding = progressBar.getBoundingClientRect();
            const percent = (event.clientX - bounding.left) / bounding.width;
            this.audio.currentTime = this.audio.duration * percent;
            this.updateProgressBar(percent * 100);
        });
    }

    play_song = (track_number) => {
        // If the audio is paused and the track number is the same as the current track, just resume playing
        if (this.audio && this.audio.paused && (track_number === this.current_track_index || track_number == null)) {
            this.audio.play();
            return;
        // If the audio is playing, but the play button is pressed, pause the audio
        } else if (this.audio && track_number == null) {
            this.audio.pause();
            return;
        }
        // Loop the song list if the track number is out of bounds
        if (!track_number && this.current_track_index === -1) {
            track_number = 0;
        } else if (track_number >= this.song_data.length) {
            track_number = 0;
        } else if (track_number < 0) {
            track_number = this.song_data.length - 1;
        }

        // Stop the current song if it is playing
        if (this.audio && !this.audio.paused) {
            this.stop_song();
        }
        
        // Play the song
        let song_url = this.song_data[track_number].url;
        this.parent_element.querySelector(".SP-track-name").innerHTML = this.song_data[track_number].name;
        this.parent_element.querySelector(".SP-artist-name").innerHTML = this.song_data[track_number].artist;
        this.audio = new Audio(song_url);
        this.audio.addEventListener('ended', () => {
            this.audio.currentTime = 0;
            this.next_song();
        });
        this.audio.play();
        this.current_track_index = track_number;
    }
    
    pause_song = () => {
        this.audio.pause();
    }
    
    next_song = () => {
        let next_track_number = this.current_track_index + 1;
        if (next_track_number >= this.song_data.length) {
            next_track_number = 0;
        }
        this.play_song(next_track_number);
    }
    
    prev_song = () => {
        let prev_track_number = this.current_track_index - 1;
        if (prev_track_number < 0) {
            prev_track_number = this.song_data.length - 1;
        }
    
        this.play_song(prev_track_number);
    }
    
    stop_song = () => {
        this.audio.pause();
        this.audio = null;
        this.current_track_index = -1;
    }
    
    getSongProgressString = (audio_object) => {
        if (!audio_object) {
            return "0:00";
        }
        const current_time = audio_object.currentTime || 0;
        return `${Math.floor(current_time / 60)}:${Math.floor(current_time % 60).toString().padStart(2, '0')}`;
    }
    
    getSongDurationString = (audio_object) => {
        if (!audio_object) {
            return "0:00";
        }
        const duration = audio_object.duration || 0;
        return `${Math.floor(duration / 60)}:${Math.floor(duration % 60).toString().padStart(2, '0')}`
    }
    
    updateDurationText = (audio_object) => {
        let progressText = this.parent_element.querySelector('.SP-duration-total');
        progressText.innerHTML = this.getSongDurationString(audio_object);
    }
    
    updateProgressText = (audio_object) => {
        let progressText = this.parent_element.querySelector('.SP-duration-current');
        progressText.innerHTML = this.getSongProgressString(audio_object);
    }
    
    getSongProgressPercent = (audio_object) => {
        if (audio_object == null) return 0;
        const currentTime = audio_object.currentTime;
        const duration = audio_object.duration;
        return currentTime / duration;
    }

    updateProgressBar = (progressFloat) => {
        let progressTruncated = progressFloat.toFixed(2);
        let progressBar = this.parent_element.querySelector('.SP-progress-bar-fill');
        progressBar.style.width = progressTruncated + "%";
    }
    
    // This function scrolls the track text in the player when it is too long to fit in the container
    scrollTrackText = () => {
        if (this.track_element.scrollWidth > this.track_element.clientWidth + this.track_element.scrollLeft && this.track_scrolling_left) {
            this.track_element.scrollBy(1, 0);
        } else {
            this.track_scrolling_left = false;
        }
        if (!this.track_scrolling_left) {
            this.track_element.scrollBy(-0.5, 0);
            if (this.track_element.scrollLeft === 0) {
                this.track_scrolling_left = true;
            }
        }
    }
    
    // This function scrolls the artist text in the player when it is too long to fit in the container
    scrollArtistText = () => {
        if (this.artist_element.scrollWidth > this.artist_element.clientWidth + this.artist_element.scrollLeft && this.artist_scrolling_left) {
            this.artist_element.scrollBy(1, 0);
        } else {
            this.artist_scrolling_left = false;
        }
        if (!this.artist_scrolling_left) {
            this.artist_element.scrollBy(-0.5, 0);
            if (this.artist_element.scrollLeft === 0) {
                this.artist_scrolling_left = true;
            }
        }
    }
    
    // This hides the play button when the audio is playing and hides the pause button when the audio is paused
    renderPauseOrPlayButton = () => {
        let play_button = this.parent_element.querySelector('.SP-play');
        let pause_button = this.parent_element.querySelector('.SP-pause');
        if (this.audio && !this.audio.paused) {
            play_button.style.display = "none";
            pause_button.style.display = "block";
        } else {
            play_button.style.display = "block";
            pause_button.style.display = "none";
        }
    }
}
// Song files hosted on github >🐢
let song_data_absolute = [
    {
        "name": "Guy Gets Promoted / End Title",
        "artist": "Tom Hiel",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/01%20Track%2001.mp3"
    },
    {
        "name": "The Singing Sea",
        "artist": "Tulivu-Donna Cumberbatch",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/02%20Track%2002.mp3"
    },
    {
        "name": "La Valse d'Amelie",
        "artist": "Yann Tiersen",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/03%20Track%2003.mp3"
    },
    {
        "name": "Come Undone",
        "artist": "Duran Duran",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/04%20Track%2004.mp3"
    },
    {
        "name": "This Is a Lie",
        "artist": "The Cure",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/05%20Track%2005.mp3"
    },
    {
        "name": "Millennia",
        "artist": "Hotel de Ville",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/06%20Track%2006.mp3"
    },
    {
        "name": "Trust",
        "artist": "The Cure",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/07%20Track%2007.mp3"
    },
    {
        "name": "Perfect Disguise",
        "artist": "Modest Mouse",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/08%20Track%2008.mp3"
    },
    {
        "name": "The Last Beat of My Heart",
        "artist": "Siouxsie and the Banshees",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/09%20Track%2009.mp3"
    },
    {
        "name": "The World Has Turned and Left Me Here",
        "artist": "Christopher John",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/10%20Track%2010.mp3"
    },
    {
        "name": "Moonlight Sonata",
        "artist": "Depeche Mode",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/11%20Track%2011.mp3"
    },
    {
        "name": "Fear of the South",
        "artist": "Tin Hat Trio",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/12%20Track%2012.mp3"
    },
    {
        "name": "One More Time",
        "artist": "The Cure",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/13%20Track%2013.mp3"
    },
    {
        "name": "Max Payne 2 Theme",
        "artist": "Kärtsy Hatakka & Kimmo Kajasto",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/14%20Track%2014.mp3"
    },
    {
        "name": "If Only Tonight We Could Sleep",
        "artist": "The Cure",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/15%20Track%2015.mp3"
    },
    {
        "name": "This Side of the Blue",
        "artist": "Joanna Newsom",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/16%20Track%2016.mp3"
    },
    {
        "name": "Playground Love",
        "artist": "Air",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/17%20Track%2017.mp3"
    }
];
// Song files hosted on nekoweb >🐢
let song_data_relative = [
    {
        "name": "Guy Gets Promoted / End Title",
        "artist": "Tom Hiel",
        "url": "../Sleep CD/01 Track 01.mp3"
    },
    {
        "name": "The Singing Sea",
        "artist": "Tulivu-Donna Cumberbatch",
        "url": "../Sleep CD/02 Track 02.mp3"
    },
    {
        "name": "La Valse d'Amelie",
        "artist": "Yann Tiersen",
        "url": "../Sleep CD/03 Track 03.mp3"
    },
    {
        "name": "Come Undone",
        "artist": "Duran Duran",
        "url": "../Sleep CD/04 Track 04.mp3"
    },
    {
        "name": "This Is a Lie",
        "artist": "The Cure",
        "url": "../Sleep CD/05 Track 05.mp3"
    },
    {
        "name": "Millennia",
        "artist": "Hotel de Ville",
        "url": "../Sleep CD/06 Track 06.mp3"
    },
    {
        "name": "Trust",
        "artist": "The Cure",
        "url": "../Sleep CD/07 Track 07.mp3"
    },
    {
        "name": "Perfect Disguise",
        "artist": "Modest Mouse",
        "url": "../Sleep CD/08 Track 08.mp3"
    },
    {
        "name": "The Last Beat of My Heart",
        "artist": "Siouxsie and the Banshees",
        "url": "../Sleep CD/09 Track 09.mp3"
    },
    {
        "name": "The World Has Turned and Left Me Here",
        "artist": "Christopher John",
        "url": "../Sleep CD/10 Track 10.mp3"
    },
    {
        "name": "Moonlight Sonata",
        "artist": "Depeche Mode",
        "url": "../Sleep CD/11 Track 11.mp3"
    },
    {
        "name": "Fear of the South",
        "artist": "Tin Hat Trio",
        "url": "../Sleep CD/12 Track 12.mp3"
    },
    {
        "name": "One More Time",
        "artist": "The Cure",
        "url": "../Sleep CD/13 Track 13.mp3"
    },
    {
        "name": "Max Payne 2 Theme",
        "artist": "Kärtsy Hatakka & Kimmo Kajasto",
        "url": "../Sleep CD/14 Track 14.mp3"
    },
    {
        "name": "If Only Tonight We Could Sleep",
        "artist": "The Cure",
        "url": "../Sleep CD/15 Track 15.mp3"
    },
    {
        "name": "This Side of the Blue",
        "artist": "Joanna Newsom",
        "url": "../Sleep CD/16 Track 16.mp3"
    },
    {
        "name": "Playground Love",
        "artist": "Air",
        "url": "../Sleep CD/17 Track 17.mp3"
    }
];


// Automatically initialize all song players on the page with the same data >🐢
// let song_player_elements = document.querySelectorAll(".StarrPlayer");
// let audio_players = [];

// song_player_elements.forEach((element) => {
//     let audio_player = new MusicPlayer(element, song_data_relative);
//     audio_players.push(audio_player);
// });

// Load the song players manually >🐢
let song_player_basic = document.querySelector(".StarrPlayer");
let basic_player = new MusicPlayer(song_player_basic, song_data_relative);

// This one loads from github via an absolute path >🐢
let song_player_stone = document.querySelector("#SP-stone.StarrPlayer");
let stone_player = new MusicPlayer(song_player_stone, song_data_absolute);

// This one loads from nekoweb via a relative path >🐢
let song_player_tv = document.querySelector("#SP-tv.StarrPlayer");
let tv_player = new MusicPlayer(song_player_tv, song_data_relative);
Really Cool Song
By: Really Cool Artist
0:00 X:XX
<<
Play
Pause
Stop
>>
Really Cool Song
By: Really Cool Artist
0:00 X:XX
<<
Play
Pause
Stop
>>
Really Cool Song
By: Really Cool Artist
0:00 X:XX

JavaScript Instructions

The only part of the javascript you need to change is the song_data array. This array contains all the songs that the player will play. Each song is an object with a name, artist, and url property. The name and artist properties are strings, and the url property is a string that points to the location of the song file. You can host the song files on your own server or use a service like github to host them. The song_data array is passed to the MusicPlayer constructor along with the element that the player will be attached to. The MusicPlayer class takes care of the rest. You can create multiple players on the same page by creating new instances of the MusicPlayer class with different song_data arrays and elements.

let song_data_absolute = [
    {
        "name": "Guy Gets Promoted / End Title",
        "artist": "Tom Hiel",
        "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/01%20Track%2001.mp3"
    },
];
let song_player_basic = document.querySelector(".StarrPlayer");
new MusicPlayer(song_player_basic, song_data_absolute);