A Music Player Single Page Application of mine I would like to share:

Hi,

My Name is Hugh, and I would like to share with you a wonderful new MIT Licensed single page project of mine that uses the youtube data-api and iframe-api to display a youtube playlist of songs on your concretecms website and play them.

To View a demo visit: (it’s a work in-progress)
https://www.princegeorge.cc/pgmusic
(don’t forget to click “load cache”)

My current version integrates the web components standard into its development scheme and is not very optimized for mobile yet…

But I wanted to share the single page with people if they wanted to have something similar for their own needs:

/applications/single_pages/pgmusic/view.php

<?php defined('C5_EXECUTE') or die("Access Denied."); ?>

<?php
$enableJS = true;
$u = new \Concrete\Core\User\User();
$c = \Concrete\Core\Page\Page::getCurrentPage();

// Disable JS if in edit mode (for a logged-in user)
if ($u->isRegistered() && $c->isEditMode()) {
    $enableJS = false;
}
?>

<section>
    <?php
    $a = new Area('Main');
    $a->setAreaGridMaximumColumns(12);
    $a->enableGridContainer();
    $a->display($c);
    ?>
</section>

<?php if ($enableJS) : ?>
<div class="container my-4">
    <div id="music-page-container" class="border border-white p-3">
        <playlist-viewer></playlist-viewer>
        <music-player></music-player>
        <video-player></video-player>
    </div>
</div>

<!-- Templates for Web Components -->
<template id="playlist-viewer-template">
    <style>
        :host {
            display: block;
            margin-bottom: 20px;
            background-color: black;
        }
        .playlist-scroll {
            max-height: 400px;
            overflow-y: auto;
            background: black;
            border: 1px solid #dee2e6;
            border-radius: 0.25rem;
            padding: 10px;
        }
        .playlist-item {
            padding: 8px;
            cursor: pointer;
            transition: background-color 0.2s;
            background-color: black;
        }
        .playlist-item:hover {
            background-color: rgba(255,255,255,0.1);
        }
        .playlist-item.playing {
            background-color: rgba(255,255,255,0.1);
            font-weight: bold;
        }
        .spinner {
            display: none;
        }
        #playlist-heading {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            align-items: center;
            margin-bottom: 15px;
        }
        #search-container {
            flex-grow: 1;
            display: flex;
            align-items: center;
            gap: 5px;
        }
        #search-input {
            width: 100%;
            max-width: 300px;
        }
        @media (max-width: 576px) {
            #playlist-heading {
                flex-direction: column;
                align-items: stretch;
            }
            #search-container {
                width: 100%;
                margin-bottom: 10px;
            }
            #search-input {
                max-width: none;
            }
        }
    </style>
    <div id="playlist-heading">
        <div id="search-container">
            <label for="search-input" class="form-label mb-0">Search:</label>
            <input type="text" id="search-input" class="form-control" placeholder="Search channel or track..." />
        </div>
        <button id="clear-cache-button" class="btn btn-outline-secondary">Clear Cache</button>
        <button id="load-cache-button" class="btn btn-outline-primary">
            Load Cache <span id="load-spinner" class="spinner">🔄</span>
        </button>
    </div>
    <div id="playlist-scroll" class="playlist-scroll">
        <!-- Playlist items inserted here -->
    </div>
</template>

<template id="music-player-template">
    <style>
        .body {
            background-color: black;
        }
        :host {
            display: block;
            background: black;
            color: #fff;
            padding: 20px;
            border-radius: 0.25rem;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
            border: solid 1px white;
        }
        .track-info {
            margin: 15px 0;
            text-align: center;
            font-size: 1.1rem;
            color: white !important;
        }
        .marquee-content {
            max-width: 100%;
            overflow: hidden;
            white-space: nowrap;
            display: inline-block;
            color: white !important;
        }
        @keyframes scrolling {
            0% { transform: translateX(0); }
            100% { transform: translateX(-100%); }
        }
        #controls {
            display: flex;
            flex-direction: column;
            gap: 15px;
        }
        .controls-row {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 10px;
            flex-wrap: wrap;
        }
        button {
            padding: 8px 16px;
            font-size: 1rem;
            transition: background-color 0.2s, transform 0.1s;
        }
        button:hover {
            transform: translateY(-1px);
        }
        button:active {
            transform: translateY(0);
        }
        #play-pause-button {
            background-color: rgba(0,0,0,0);
            border: solid 1px white;
            color: white;
        }
        #play-pause-button:hover {
            background-color: #0b5ed7;
        }
        #prev-button, #next-button {
            background-color: rgba(0,0,0,0);
            border: solid 1px white;
            color: white;
        }
        #prev-button:hover, #next-button:hover {
            background-color: rgba(255,255,255,0.1);
            border: solid 1px white;
        }
        input[type="range"] {
            width: 150px;
            margin: 0 10px;
        }
        #time-display {
            font-size: 0.9rem;
            min-width: 70px;
            text-align: center;
        }
        #random-checkbox {
            margin-right: 5px;
        }
        label {
            display: flex;
            align-items: center;
            color: #fff;
        }
        @media (max-width: 576px) {
            :host {
                padding: 15px;
            }
            .controls-row {
                flex-direction: column;
                gap: 15px;
            }
            input[type="range"] {
                width: 100%;
                max-width: 200px;
            }
            #time-display {
                width: 100%;
            }
        }
    </style>
    <div id="track-info" class="track-info">
        <div id="marquee-container" class="marquee-content">
            <span id="channel-and-track"></span>
        </div>
    </div>
    <div id="controls">
        <div class="controls-row">
            <button id="play-pause-button">Play</button>
            <button id="prev-button">Previous</button>
            <button id="next-button">Next</button>
            <input type="range" id="time-slider" value="0" min="0" max="100" />
            <span id="time-display">0:00 / 0:00</span>
        </div>
        <div class="controls-row">
            <label><input type="checkbox" id="random-checkbox" /> Shuffle</label>
            <input type="range" id="volume-slider" min="0" max="100" value="50" />
        </div>
    </div>
</template>

<!-- Load External Libraries (YouTube / Fuse.js) -->
<script src="https://www.youtube.com/iframe_api"></script>
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.6.2"></script>

<!-- PHP logic to load our .js files from /application/single_pages/pgmusic/js/ -->
<?php
$jsDir = DIR_APPLICATION . '/single_pages/pgmusic/js';
$jsFiles = scandir($jsDir);
$scripts = [];
foreach ($jsFiles as $file) {
    // Load all JS except main.js first
    if (pathinfo($file, PATHINFO_EXTENSION) === 'js' && $file !== 'main.js') {
        $scripts[] = '<script defer src="' . BASE_URL . '/application/single_pages/pgmusic/js/' . $file . '"></script>';
    }
}
// Load main.js last
$scripts[] = '<script defer src="' . BASE_URL . '/application/single_pages/pgmusic/js/main.js"></script>';
echo implode("\n", $scripts);
?>

<style>
    #music-page-container {
        background-color: black;
    }
</style>

<!-- open player button global style-->
<style>
  .hide-button {
    border: 1px solid white;
    padding: 5px;
    background-color: transparent;
    color: white;
    cursor: pointer;
  }
  .hide-button:hover {
    background-color: rgba(255,255,255,0.1);
    color: white;
  }
</style>

<?php endif; ?>

application/single_pages/pgmusic/js/main.js

// Example path: application/single_pages/pgmusic/js/main.js
// This file handles all the data fetching, caching, and "glue" logic

document.addEventListener('DOMContentLoaded', () => {
    const PLAYLIST_ID = 'PL6103487B4355FADB';  //or your sick ass playlist
    const API_KEY = 'Youtube_Data_API_Key_setup_too_only_work_on_your_domain'; // your own
    const LS_PLAYLIST_KEY = 'myYoutubePlaylist';
    const LS_VIDEODATA_KEY = 'myYoutubeVideoData';
    const LS_CURRENT_VIDEOID = 'myCurrentVideoID';
    const LS_CURRENT_TIME = 'myCurrentTime';
    const LS_IS_PLAYING = 'myIsPlaying';
    const LS_LAST_PLAYTIMESTAMP = 'myLastPlayTimestamp';
    const LS_SHUFFLE_ENABLED = 'myShuffleEnabled';
    const LS_SHUFFLE_HISTORY = 'myShuffleHistory';
    const LS_BACK_HISTORY_INDEX = 'myBackHistoryIndex';
    const LS_VOLUME = 'myVolume';
    const RESUME_THRESHOLD = 600 * 1000; // 10 minutes
    const MAX_IDS_PER_REQUEST = 50; // YouTube API chunk size

    let videoIds = [];
    let videoData = {};
    let nextPageToken = '';
    let reachedEnd = false;
    let shouldResumePlayback = false;

    const playlistViewer = document.querySelector('playlist-viewer');
    const musicPlayer = document.querySelector('music-player');

    // Expose some state so PlaylistViewer can request more pages
    window.playlistState = {
        reachedEnd: () => reachedEnd,
        fetchNextPage: fetchPlaylistPage,
        loadMoreCache: loadMoreCache,
        updateComponents: updateComponents
    };

    // When user clicks in <playlist-viewer> to select a video
    playlistViewer.addEventListener('select-video', (e) => {
        musicPlayer.loadVideo(e.detail.videoId, 0, true);
    });

    // Keep the highlight in sync when <music-player> changes videos
    musicPlayer.addEventListener('video-changed', (e) => {
        playlistViewer.highlightVideo(e.detail.videoId);
    });

    // Clear localStorage / re-fetch
    playlistViewer.addEventListener('clear-cache', () => {
        localStorage.clear();
        videoIds = [];
        videoData = {};
        nextPageToken = '';
        reachedEnd = false;
        fetchPlaylistPage();
    });

    // Load entire playlist into cache (recursively fetch)
    playlistViewer.addEventListener('load-cache', () => {
        loadMoreCache();
    });

    // On player-ready (fired by MusicPlayer after the YouTube player is set)
    musicPlayer.addEventListener('player-ready', () => {
        const storedVid = localStorage.getItem(LS_CURRENT_VIDEOID);
        const storedTime = parseFloat(localStorage.getItem(LS_CURRENT_TIME) || '0');
        const storedPlaying = localStorage.getItem(LS_IS_PLAYING) === '1';
        const lastPlayTs = parseInt(localStorage.getItem(LS_LAST_PLAYTIMESTAMP) || '0', 10);
        const now = Date.now();
        const timeSinceLastPlay = now - lastPlayTs;

        // Decide if we should restore the old track
        if (storedVid && videoIds.includes(storedVid) && timeSinceLastPlay <= RESUME_THRESHOLD) {
            // Load the previous video/time but do not auto-play here
            musicPlayer.loadVideo(storedVid, storedTime, false);

            // If it was playing before, we note that user might want to resume
            if (storedPlaying) {
                shouldResumePlayback = true;
                setupUserInteractionHandlers();
            }
        } else {
            // Start fresh from the first or random track if shuffle is enabled
            const initialVid = musicPlayer.shuffleEnabled
                ? videoIds[Math.floor(Math.random() * videoIds.length)]
                : videoIds[0];

            if (initialVid) {
                musicPlayer.loadVideo(initialVid, 0, false);
                localStorage.setItem(LS_CURRENT_VIDEOID, initialVid);
                localStorage.setItem(LS_CURRENT_TIME, '0');
                localStorage.setItem(LS_IS_PLAYING, '0');
                localStorage.setItem(LS_LAST_PLAYTIMESTAMP, now.toString());

                playlistViewer.highlightVideo(initialVid);
                const playlistScroll = playlistViewer.shadowRoot.getElementById('playlist-scroll');
                playlistScroll.scrollTop = 0;
            }
        }
    });

    // Sync changes if multiple tabs are open, or if localStorage changes
    window.addEventListener('storage', (event) => {
        if (event.key === LS_CURRENT_VIDEOID && event.newValue !== musicPlayer._currentVideoId) {
            const storedTime = parseFloat(localStorage.getItem(LS_CURRENT_TIME) || '0');
            musicPlayer.loadVideo(event.newValue, storedTime, musicPlayer.isPlaying);
        }
        if (event.key === LS_IS_PLAYING) {
            const shouldPlay = event.newValue === '1';
            if (shouldPlay && !musicPlayer.isPlaying) musicPlayer.play();
            else if (!shouldPlay && musicPlayer.isPlaying) musicPlayer.pause();
        }
    });

    // Either load from cache (if available) or fetch anew
    loadOrInitData();

    function loadOrInitData() {
        const storedIds = localStorage.getItem(LS_PLAYLIST_KEY);
        const storedData = localStorage.getItem(LS_VIDEODATA_KEY);

        if (storedIds && storedData) {
            try {
                videoIds = JSON.parse(storedIds) || [];
                videoData = JSON.parse(storedData) || {};
                updateComponents();
            } catch (e) {
                console.warn('Invalid stored data, refetching...', e);
                fetchPlaylistPage();
            }
        } else {
            fetchPlaylistPage();
        }
    }

    async function fetchPlaylistPage() {
        if (reachedEnd) return;

        let url = `https://www.googleapis.com/youtube/v3/playlistItems?part=contentDetails&maxResults=50&playlistId=${PLAYLIST_ID}&key=${API_KEY}`;
        if (nextPageToken) url += `&pageToken=${nextPageToken}`;

        try {
            console.log('Fetching playlist page:', url);
            const resp = await fetch(url);
            if (!resp.ok) throw new Error(`HTTP error! Status: ${resp.status}`);
            const data = await resp.json();
            if (data.error) throw new Error(`API Error: ${data.error.message} (Code: ${data.error.code})`);

            // Accumulate all video IDs
            const newIds = data.items.map((item) => item.contentDetails.videoId);
            videoIds = [...new Set([...videoIds, ...newIds])];

            nextPageToken = data.nextPageToken || '';
            reachedEnd = !nextPageToken;

            localStorage.setItem(LS_PLAYLIST_KEY, JSON.stringify(videoIds));

            // Fetch details for these new IDs
            await fetchVideoDetails();
            updateComponents();
        } catch (err) {
            console.error('Fetch playlist error:', err);
            reachedEnd = true;
            //alert(`Error loading playlist: ${err.message}`);
        }
    }

    async function fetchVideoDetails() {
        // Only fetch details for videoIds we haven’t processed yet
        const toFetch = videoIds.filter(
            (vid) => !videoData[vid] && typeof vid === 'string' && vid.length === 11
        );
        if (!toFetch.length) return;

        // Because the YouTube API can only handle up to 50 IDs at once
        const chunks = [];
        for (let i = 0; i < toFetch.length; i += MAX_IDS_PER_REQUEST) {
            chunks.push(toFetch.slice(i, i + MAX_IDS_PER_REQUEST));
        }

        for (const chunk of chunks) {
            const ids = chunk.join(',');
            const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet,status&id=${ids}&key=${API_KEY}`;

            try {
                console.log('Fetching video details for IDs:', ids);
                const resp = await fetch(url);
                if (!resp.ok) throw new Error(`HTTP error! Status: ${resp.status}`);
                const data = await resp.json();
                if (data.error) throw new Error(`API Error: ${data.error.message} (Code: ${data.error.code})`);

                console.log('Video details response:', data);
                data.items.forEach((item) => {
                    // Only keep public + embeddable items
                    if (item.status.embeddable && item.status.privacyStatus !== 'private') {
                        videoData[item.id] = {
                            title: item.snippet.title,
                            channelTitle: item.snippet.channelTitle,
                            channelId: item.snippet.channelId,
                        };
                    }
                });
                localStorage.setItem(LS_VIDEODATA_KEY, JSON.stringify(videoData));
            } catch (e) {
                console.error('Fetch video details error:', e);
                //alert(`Error fetching video details: ${e.message}`);
                break;
            }
        }
    }

    async function loadMoreCache() {
        const spinner = playlistViewer.shadowRoot.getElementById('load-spinner');
        spinner.style.display = 'inline-block';
        try {
            while (!reachedEnd) {
                await fetchPlaylistPage();
                // Slight delay to avoid hammering requests
                await new Promise((resolve) => setTimeout(resolve, 500));
            }
        } catch (err) {
            console.error('Error loading full cache:', err);
            //alert(`Error loading full playlist: ${err.message}`);
        } finally {
            spinner.style.display = 'none';
        }
    }

	function updateComponents() {
		// Convert videoIds + videoData into an array for our custom elements
		let videoObjects = videoIds.map((vid) => ({
			vid,
			title: videoData[vid]?.title || vid,
			channelTitle: videoData[vid]?.channelTitle || 'Unknown Channel',
			channelId: videoData[vid]?.channelId || '',
		}));

		// Filter out any that have channelTitle === 'Unknown Channel'
		videoObjects = videoObjects.filter(obj => {
			return obj.channelTitle !== 'Unknown Channel';
		});

		// Provide to the custom elements
		playlistViewer.playlistData = videoObjects;
		musicPlayer.playlistData = videoObjects;
	}

    function setupUserInteractionHandlers() {
        // On user gestures, attempt to .play() if we set shouldResumePlayback
        const resumePlayback = () => {
            if (shouldResumePlayback && !musicPlayer.isPlaying) {
                musicPlayer.play();
                shouldResumePlayback = false;

                document.removeEventListener('click', resumePlayback);
                document.removeEventListener('keydown', resumePlayback);
                document.removeEventListener('mousemove', resumePlayback);
                document.removeEventListener('touchstart', resumePlayback);
                document.removeEventListener('scroll', resumePlayback);
            }
        };
        document.addEventListener('click', resumePlayback);
        document.addEventListener('keydown', resumePlayback);
        document.addEventListener('mousemove', resumePlayback);
        document.addEventListener('touchstart', resumePlayback);
        document.addEventListener('scroll', resumePlayback);
    }
});

/application/single_pages/pgmusic/music-player.js

// Example path: application/single_pages/pgmusic/js/music-player.js

class MusicPlayer extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        const template = document.getElementById('music-player-template');
        this.shadowRoot.appendChild(template.content.cloneNode(true));
        this._playlistData = [];
        this._currentVideoId = null;
        this.isPlaying = false;
        this.shuffleEnabled = false;
        this.shuffleHistory = [];
        this.backHistoryIndex = -1;
        this.duration = 0;
        this.lastPlaybackTime = 0;
        this.stallCounter = 0;
        this.timeUpdateInterval = null;
        this.player = null; // Will be set by VideoPlayer
    }

    set playlistData(data) {
        this._playlistData = data;
    }

    loadVideo(videoId, startTime = 0, play = false) {
        this._currentVideoId = videoId;
        if (this.player) {
            this.player.loadVideoById(videoId, startTime);
            this.isPlaying = play;
            const playPauseButton = this.shadowRoot.getElementById('play-pause-button');
            playPauseButton.textContent = play ? 'Pause' : 'Play';

            localStorage.setItem('myCurrentVideoID', videoId);
            localStorage.setItem('myCurrentTime', startTime.toString());
            localStorage.setItem('myIsPlaying', play ? '1' : '0');
            localStorage.setItem('myLastPlayTimestamp', Date.now().toString());

            this.duration = 0;
            this.shadowRoot.getElementById('time-slider').max = 100;
            if (play) {
                this.player.playVideo();
            }
        }
        this.updateTrackInfo();
        this.dispatchEvent(new CustomEvent('video-changed', {
            detail: { videoId, startTime, play }
        }));
    }

    play() {
        if (this.player && !this.isPlaying) {
            this.player.playVideo();
            this.isPlaying = true;
            const playPauseButton = this.shadowRoot.getElementById('play-pause-button');
            playPauseButton.textContent = 'Pause';
            localStorage.setItem('myIsPlaying', '1');
            localStorage.setItem('myLastPlayTimestamp', Date.now().toString());
        }
    }

    pause() {
        if (this.player && this.isPlaying) {
            this.player.pauseVideo();
            this.isPlaying = false;
            const playPauseButton = this.shadowRoot.getElementById('play-pause-button');
            playPauseButton.textContent = 'Play';
            localStorage.setItem('myIsPlaying', '0');
            localStorage.setItem('myLastPlayTimestamp', Date.now().toString());
        }
    }

    connectedCallback() {
        const videoPlayer = document.querySelector('video-player');
        if (videoPlayer) {
            videoPlayer.addEventListener('player-initialized', (e) => {
                this.player = e.detail.player;
                this.initializeControls();
            });
        }

        const playPauseButton = this.shadowRoot.getElementById('play-pause-button');
        const prevButton = this.shadowRoot.getElementById('prev-button');
        const nextButton = this.shadowRoot.getElementById('next-button');
        const timeSlider = this.shadowRoot.getElementById('time-slider');
        const volumeSlider = this.shadowRoot.getElementById('volume-slider');
        const randomCheckbox = this.shadowRoot.getElementById('random-checkbox');

        playPauseButton.addEventListener('click', () => this.togglePlayPause());
        prevButton.addEventListener('click', () => this.playPrevious());
        nextButton.addEventListener('click', () => this.playNext());
        timeSlider.addEventListener('input', (e) => {
            if (this.player) {
                const newTime = parseInt(e.target.value, 10);
                this.player.seekTo(newTime, true);
                localStorage.setItem('myCurrentTime', newTime.toString());
                localStorage.setItem('myLastPlayTimestamp', Date.now().toString());
            }
        });
        volumeSlider.addEventListener('input', (e) => {
            if (this.player) {
                const volume = e.target.value;
                this.player.setVolume(volume);
                localStorage.setItem('myVolume', volume);
                localStorage.setItem('myLastPlayTimestamp', Date.now().toString());
            }
        });
        randomCheckbox.addEventListener('change', () => {
            this.shuffleEnabled = randomCheckbox.checked;
            localStorage.setItem('myShuffleEnabled', this.shuffleEnabled.toString());
            if (this.shuffleEnabled && !this.shuffleHistory.length && this._currentVideoId) {
                this.shuffleHistory = [this._currentVideoId];
            } else if (!this.shuffleEnabled) {
                this.shuffleHistory = [];
                this.backHistoryIndex = -1;
            }
            localStorage.setItem('myShuffleHistory', JSON.stringify(this.shuffleHistory));
            localStorage.setItem('myBackHistoryIndex', this.backHistoryIndex.toString());
            localStorage.setItem('myLastPlayTimestamp', Date.now().toString());
        });

        // Initialize from localStorage
        volumeSlider.value = localStorage.getItem('myVolume') || 50;
        randomCheckbox.checked = localStorage.getItem('myShuffleEnabled') === 'true';
        this.shuffleEnabled = randomCheckbox.checked;
        this.shuffleHistory = JSON.parse(localStorage.getItem('myShuffleHistory') || '[]');
        this.backHistoryIndex = parseInt(localStorage.getItem('myBackHistoryIndex') || '-1', 10);

        // Listen for state changes from VideoPlayer
        this.addEventListener('player-state-changed', (e) => this.onPlayerStateChange(e.detail));
    }

    initializeControls() {
        if (this.player) {
            this.player.setVolume(parseInt(localStorage.getItem('myVolume') || '50', 10));
            this.dispatchEvent(new CustomEvent('player-ready'));

            if (!this.timeUpdateInterval) {
                this.timeUpdateInterval = setInterval(() => this.updateTimeSlider(), 500);
            }
            this.setupUserInteractionHandlers();
        }
    }

    setupUserInteractionHandlers() {
        const resumePlayback = () => {
            const storedPlaying = localStorage.getItem('myIsPlaying') === '1';
            const lastPlayTs = parseInt(localStorage.getItem('myLastPlayTimestamp') || '0', 10);
            const now = Date.now();
            const timeSinceLastPlay = now - lastPlayTs;

            if (storedPlaying && timeSinceLastPlay <= 600000 && !this.isPlaying) {
                this.play();
            }
        };
        // Attempt resume on common user interactions
        document.addEventListener('click', resumePlayback);
        document.addEventListener('keydown', resumePlayback);
        document.addEventListener('mousemove', resumePlayback);
        document.addEventListener('touchstart', resumePlayback);
        document.addEventListener('scroll', resumePlayback);
    }

    onPlayerStateChange(event) {
        const playPauseButton = this.shadowRoot.getElementById('play-pause-button');
        if (event.data === YT.PlayerState.PLAYING) {
            this.isPlaying = true;
            playPauseButton.textContent = 'Pause';
            const duration = this.player.getDuration();
            if (duration > 0 && this.duration !== duration) {
                this.duration = duration;
                this.shadowRoot.getElementById('time-slider').max = Math.floor(this.duration);
            }
            localStorage.setItem('myIsPlaying', '1');
            localStorage.setItem('myLastPlayTimestamp', Date.now().toString());
        } else if (event.data === YT.PlayerState.PAUSED) {
            this.isPlaying = false;
            playPauseButton.textContent = 'Play';
            localStorage.setItem('myIsPlaying', '0');
            localStorage.setItem('myLastPlayTimestamp', Date.now().toString());
        } else if (event.data === YT.PlayerState.ENDED) {
            this.playNext();
        }
    }

    togglePlayPause() {
        if (!this.player) return;
        if (this.isPlaying) {
            this.pause();
        } else {
            this.play();
        }
    }

    playNext() {
        if (!this._playlistData.length) return;
        let nextVid;
        if (this.shuffleEnabled) {
            const unplayed = this._playlistData.filter((v) => !this.shuffleHistory.includes(v.vid));
            nextVid = unplayed.length
                ? unplayed[Math.floor(Math.random() * unplayed.length)].vid
                : this._playlistData[Math.floor(Math.random() * this._playlistData.length)].vid;

            if (this.shuffleHistory[this.shuffleHistory.length - 1] !== this._currentVideoId) {
                this.shuffleHistory.push(this._currentVideoId);
            }
            this.shuffleHistory.push(nextVid);
            this.backHistoryIndex = -1;
        } else {
            const idx = this._playlistData.findIndex((v) => v.vid === this._currentVideoId);
            const nextIdx = (idx + 1) % this._playlistData.length;
            nextVid = this._playlistData[nextIdx].vid;
        }
        this.loadVideo(nextVid, 0, true);

        localStorage.setItem('myShuffleHistory', JSON.stringify(this.shuffleHistory));
        localStorage.setItem('myBackHistoryIndex', this.backHistoryIndex.toString());
    }

    playPrevious() {
        if (!this._playlistData.length) return;
        let prevVid;
        if (this.shuffleEnabled) {
            if (this.backHistoryIndex < 0) {
                if (
                    !this.shuffleHistory.length ||
                    this.shuffleHistory[this.shuffleHistory.length - 1] !== this._currentVideoId
                ) {
                    this.shuffleHistory.push(this._currentVideoId);
                }
                this.backHistoryIndex = this.shuffleHistory.length - 1;
            } else if (this.backHistoryIndex > 0) {
                this.backHistoryIndex--;
            }
            prevVid = this.shuffleHistory[this.backHistoryIndex];
            if (!this._playlistData.some(v => v.vid === prevVid)) {
                prevVid = this._playlistData[0].vid;
            }
        } else {
            const idx = this._playlistData.findIndex((v) => v.vid === this._currentVideoId);
            const prevIdx = (idx - 1 + this._playlistData.length) % this._playlistData.length;
            prevVid = this._playlistData[prevIdx].vid;
        }
        this.loadVideo(prevVid, 0, true);

        localStorage.setItem('myShuffleHistory', JSON.stringify(this.shuffleHistory));
        localStorage.setItem('myBackHistoryIndex', this.backHistoryIndex.toString());
    }

    updateTimeSlider() {
        if (!this.player || typeof this.player.getCurrentTime !== 'function') return;
        const ctime = this.player.getCurrentTime();
        if (ctime === undefined) return;

        const timeSlider = this.shadowRoot.getElementById('time-slider');
        const timeDisplay = this.shadowRoot.getElementById('time-display');

        if (this.isPlaying && this.duration === 0) {
            const duration = this.player.getDuration();
            if (duration > 0) {
                this.duration = duration;
                timeSlider.max = Math.floor(this.duration);
            }
        }

        timeSlider.value = Math.floor(ctime);
        timeDisplay.textContent = `${this.formatTime(ctime)} / ${this.formatTime(this.duration)}`;

        localStorage.setItem('myCurrentTime', ctime.toString());
        localStorage.setItem('myLastPlayTimestamp', Date.now().toString());

        // Check if playback is stalling
        if (this.isPlaying && this.duration > 0) {
            if (Math.abs(ctime - (this.lastPlaybackTime || 0)) < 0.2) {
                this.stallCounter++;
            } else {
                this.stallCounter = 0;
                this.lastPlaybackTime = ctime;
            }
            if (this.stallCounter >= 10) {
                console.warn("Playback stalled, skipping...");
                this.stallCounter = 0;
                this.playNext();
            }
        }
    }

    formatTime(sec) {
        const m = Math.floor(sec / 60);
        const s = Math.floor(sec % 60);
        return `${m}:${s < 10 ? '0' + s : s}`;
    }

    updateTrackInfo() {
        const data = this._playlistData.find((v) => v.vid === this._currentVideoId) || {};
        const channelTitle = data.channelTitle || 'Unknown Channel';
        const title = data.title || this._currentVideoId || 'Unknown Track';
        const channelUrl = data.channelId ? `https://www.youtube.com/channel/${data.channelId}` : '#';
        const trackUrl = this._currentVideoId ? `https://www.youtube.com/watch?v=${this._currentVideoId}` : '#';

        const html = `
            <a href="${channelUrl}" target="_blank" style="color: white;">${channelTitle}</a>
            -
            <a href="${trackUrl}" target="_blank" style="color: white;">${title}</a>
        `;
        const marquee = this.shadowRoot.getElementById('marquee-container');
        const channelAndTrackEl = this.shadowRoot.getElementById('channel-and-track');
        channelAndTrackEl.innerHTML = html;

        // Reset marquee
        marquee.style.animation = 'none';
        marquee.offsetHeight; // force reflow
        if (marquee.scrollWidth > marquee.clientWidth) {
            marquee.style.animation = 'scrolling 15s linear infinite';
        }
    }

    disconnectedCallback() {
        if (this.timeUpdateInterval) {
            clearInterval(this.timeUpdateInterval);
            this.timeUpdateInterval = null;
        }
    }
}

customElements.define('music-player', MusicPlayer);

application/single_pages/pgmusic/js/playlist-viewer.js

// Example path: application/single_pages/pgmusic/js/playlist-viewer.js

class PlaylistViewer extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        const template = document.getElementById('playlist-viewer-template');
        this.shadowRoot.appendChild(template.content.cloneNode(true));
        this._playlistData = [];
        this.fuse = null;
    }

    set playlistData(data) {
        this._playlistData = data;
        this.buildFuse();
        this.render();
    }

    buildFuse() {
        if (typeof Fuse !== 'undefined' && this._playlistData.length) {
            this.fuse = new Fuse(this._playlistData, {
                keys: ['title', 'channelTitle'],
                threshold: 0.3,
            });
        }
    }

    render(filteredData = this._playlistData) {
        const playlistScroll = this.shadowRoot.getElementById('playlist-scroll');
        playlistScroll.innerHTML = '';
        filteredData.forEach((item, idx) => {
            const div = document.createElement('div');
            div.className = 'playlist-item';
            div.setAttribute('data-video-id', item.vid);
            div.textContent = `#${idx + 1} ${item.channelTitle} - ${item.title}`;
            div.addEventListener('click', () => {
                this.dispatchEvent(
                    new CustomEvent('select-video', { detail: { videoId: item.vid } })
                );
                localStorage.setItem('playlistScrollPosition', playlistScroll.scrollTop);
            });
            playlistScroll.appendChild(div);
        });
        this.restoreScrollPosition();
    }

    connectedCallback() {
        const playlistScroll = this.shadowRoot.getElementById('playlist-scroll');
        playlistScroll.addEventListener('scroll', () => {
            // If near bottom, fetch next page
            const threshold = 50;
            if (
                !window.playlistState.reachedEnd() &&
                (playlistScroll.scrollHeight - playlistScroll.scrollTop - playlistScroll.clientHeight < threshold)
            ) {
                window.playlistState.fetchNextPage();
            }
            localStorage.setItem('playlistScrollPosition', playlistScroll.scrollTop);
        });

        this.shadowRoot
            .getElementById('search-input')
            .addEventListener('input', (e) => {
                const query = e.target.value.trim();
                if (!query) {
                    this.render();
                } else if (this.fuse) {
                    const results = this.fuse.search(query);
                    this.render(results.map((r) => r.item));
                }
            });

        this.shadowRoot
            .getElementById('clear-cache-button')
            .addEventListener('click', () => {
                if (confirm('Are you sure you want to clear the cache?')) {
                    this.dispatchEvent(new CustomEvent('clear-cache'));
                }
            });

        this.shadowRoot
            .getElementById('load-cache-button')
            .addEventListener('click', () => {
                this.shadowRoot.getElementById('load-spinner').style.display = 'inline-block';
                window.playlistState.loadMoreCache();
            });
    }

    highlightVideo(videoId) {
        const items = this.shadowRoot.querySelectorAll('.playlist-item');
        items.forEach((item) => {
            item.classList.remove('playing');
            if (item.getAttribute('data-video-id') === videoId) {
                item.classList.add('playing');
                item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
            }
        });
    }

    hideSpinner() {
        this.shadowRoot.getElementById('load-spinner').style.display = 'none';
    }

    restoreScrollPosition() {
        const playlistScroll = this.shadowRoot.getElementById('playlist-scroll');
        const scrollPosition = localStorage.getItem('playlistScrollPosition');
        if (scrollPosition !== null) {
            playlistScroll.scrollTop = parseInt(scrollPosition, 10);
        }
    }
}

customElements.define('playlist-viewer', PlaylistViewer);

/application/single_pages/pgmusic/js/video-player.js

// application/single_pages/pgmusic/js/video-player.js

class VideoPlayer extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });

        this.shadowRoot.innerHTML = `
            <style>
                :host {
                    z-index: 2000;
                }

                .video-lightbox {
                    background: black;
                    border: 1px solid white;
                    border-radius: 5px;
                    overflow: hidden;
                    transition: all 0.3s ease;
                }

                /* Mini mode: bottom-right corner, 320x180 */
                .video-lightbox.mini {
                    position: fixed;
                    width: 320px;
                    height: 180px;
                    bottom: 20px;
                    right: 20px;
                    z-index: 1000;
                }

                /* Expanded mode: 50% width/height, centered overlay */
                .video-lightbox.expanded {
                    position: fixed;
                    width: 50vw;
                    height: 50vh;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    z-index: 3000; /* Higher than mini */
                }

                #header {
                    background: rgba(0, 0, 0, 0);
                    height: 20px;
                    display: flex;
                    align-items: center;
                    justify-content: space-between; /* left controls -> close button on right */
                    padding: 0 5px;
                }

                /* Left-side controls container */
                #left-controls {
                    display: flex;
                    align-items: center;
                    gap: 8px;
                }

                #expand-button, #close-button {
                    background: none;
                    border: none;
                    color: white;
                    font-size: 16px;
                    cursor: pointer;
                }
                #expand-button:hover {
                    color: #55ff55; /* Green for toggle */
                }
                #close-button:hover {
                    color: #ff5555; /* Red for close */
                }

                /* The Lock Expanded checkbox is hidden when NOT expanded */
                #lock-label {
                    display: none; /* hidden by default */
                    font-size: 14px;
                    color: white;
                    cursor: pointer;
                }
                /* show the lock checkbox ONLY in expanded mode */
                .video-lightbox.expanded #lock-label {
                    display: flex;
                    align-items: center;
                    gap: 4px;
                }

                #video-player {
                    width: 100%;
                    height: calc(100% - 20px); /* Adjust for header */
                }

                /* Resize handle in expanded mode */
                #resize-handle {
                    position: absolute;
                    bottom: 0;
                    right: 0;
                    width: 15px;
                    height: 15px;
                    background: rgba(255, 255, 255, 0.5);
                    cursor: se-resize;
                    display: none;
                }
                .video-lightbox.expanded #resize-handle {
                    display: block;
                }

                /* Styles for the "Open Player" button that appears after closeCompletely() */
                .hide-button {
                    border: 1px solid white;
                    padding: 5px;
                    background-color: transparent;
                    color: white;
                    cursor: pointer;
                }
                .hide-button:hover {
                    background-color: rgba(255,255,255,0.1);
                    color: black;
                }
            </style>

            <div class="video-lightbox" id="lightbox">
                <div id="header">
                    <!-- Left side: expand button + lock label -->
                    <div id="left-controls">
                        <button id="expand-button">⤢</button>
                        <label id="lock-label" title="Prevent outside clicks from minimizing.">
                            <input type="checkbox" id="lock-checkbox" />
                            Lock Expanded
                        </label>
                    </div>

                    <!-- Right side: close button -->
                    <button id="close-button">✖</button>
                </div>
                <div id="video-player"></div>
                <div id="resize-handle"></div>
            </div>
        `;

        // By default, we start in mini mode
        this.isExpanded = false;
        this.isHidden = false;

        // For the "Lock Expanded" checkbox
        this.autoLock = false; // if true, outside clicks won't minimize

        // Drag/Resize tracking
        this.isDragging = false;
        this.isResizing = false;
        this.startX = 0;
        this.startY = 0;
        this.startWidth = 0;
        this.startHeight = 0;

        this.player = null;

        /** If user clicks outside the expanded lightbox => setMiniMode(), unless locked. */
        this.documentClickHandler = (e) => {
            if (!this.isExpanded || this.autoLock) return;
            const path = e.composedPath();
            const lightbox = this.shadowRoot.getElementById('lightbox');
            if (!path.includes(lightbox)) {
                console.log('User clicked outside expanded box -> setMiniMode');
                this.setMiniMode();
            }
        };
    }

    connectedCallback() {
        const lightbox = this.shadowRoot.getElementById('lightbox');
        lightbox.classList.add('mini'); // start in mini mode

        // If YouTube script is ready, init right away; otherwise wait
        if (window.YT && window.YT.Player) {
            this.initializePlayer();
        } else {
            window.onYouTubeIframeAPIReady = () => this.initializePlayer();
        }

        // Setup references
        const header = this.shadowRoot.getElementById('header');
        const expandButton = this.shadowRoot.getElementById('expand-button');
        const closeButton = this.shadowRoot.getElementById('close-button');
        const resizeHandle = this.shadowRoot.getElementById('resize-handle');
        const lockCheckbox = this.shadowRoot.getElementById('lock-checkbox');

        // Listen for user toggling "Lock Expanded"
        lockCheckbox.addEventListener('change', (e) => {
            this.autoLock = e.target.checked;
            console.log('Lock Expanded changed:', this.autoLock);
        });

        // Dragging
        header.addEventListener('mousedown', (e) => this.startDragging(e));
        document.addEventListener('mousemove', (e) => this.drag(e));
        document.addEventListener('mouseup', () => this.stopDragging());

        // Resizing
        resizeHandle.addEventListener('mousedown', (e) => this.startResizing(e));
        document.addEventListener('mousemove', (e) => this.resize(e));
        document.addEventListener('mouseup', () => this.stopResizing());

        // Expand/Close Buttons
        expandButton.addEventListener('click', () => this.toggleExpand());
        closeButton.addEventListener('click', () => this.handleCloseClick());

        // Listen for outside clicks
        document.addEventListener('click', this.documentClickHandler);

        // If we have a <music-player>, watch for events
        const musicPlayer = document.querySelector('music-player');
        if (musicPlayer) {
            // Load new video from <music-player> changes
            musicPlayer.addEventListener('video-changed', (e) => {
                if (this.player) {
                    this.player.loadVideoById(e.detail.videoId, e.detail.startTime);
                    if (e.detail.play) {
                        this.player.playVideo();
                    }
                }
            });
            // Attempt resume
            musicPlayer.addEventListener('player-ready', () => {
                this.resumePlayback();
            });
        }
    }

    disconnectedCallback() {
        // Cleanup doc-level listeners
        document.removeEventListener('click', this.documentClickHandler);
        document.removeEventListener('mousemove', (e) => this.drag(e));
        document.removeEventListener('mouseup', () => this.stopDragging());
        document.removeEventListener('mousemove', (e) => this.resize(e));
        document.removeEventListener('mouseup', () => this.stopResizing());

        if (this.player) {
            this.player.destroy();
        }
    }

    initializePlayer() {
        this.player = new YT.Player(this.shadowRoot.getElementById('video-player'), {
            height: '100%',
            width: '100%',
            videoId: '',
            playerVars: {
                autoplay: 0,
                controls: 0,
                modestbranding: 1,
                fs: 0,
                disablekb: 1,
                origin: window.location.origin,
                iv_load_policy: 3,
                rel: 0,
                enablejsapi: 1
            },
            events: {
                onReady: () => {
                    console.log('Video player initialized');
                    this.dispatchEvent(new CustomEvent('player-initialized', {
                        detail: { player: this.player }
                    }));
                    this.resumePlayback();
                },
                onStateChange: (e) => {
                    const musicPlayer = document.querySelector('music-player');
                    if (musicPlayer) {
                        musicPlayer.dispatchEvent(
                            new CustomEvent('player-state-changed', { detail: e })
                        );
                    }
                }
            }
        });
    }

    resumePlayback() {
        if (!this.player) return;
        const storedVid = localStorage.getItem('myCurrentVideoID');
        const storedTime = parseFloat(localStorage.getItem('myCurrentTime') || '0');
        const storedPlaying = localStorage.getItem('myIsPlaying') === '1';
        const lastPlayTs = parseInt(localStorage.getItem('myLastPlayTimestamp') || '0', 10);
        const now = Date.now();
        const timeSinceLastPlay = now - lastPlayTs;

        if (storedVid && timeSinceLastPlay <= 600000) {
            this.player.loadVideoById(storedVid, storedTime);
            if (storedPlaying) {
                const playPromise = this.player.playVideo();
                if (playPromise && typeof playPromise.then === 'function') {
                    playPromise
                        .then(() => {
                            console.log('Autoplay resumed successfully.');
                        })
                        .catch((err) => {
                            console.log('Autoplay blocked; user interaction required.', err);
                        });
                }
            }
        }
    }

    toggleExpand() {
        console.log('toggleExpand() => isExpanded:', this.isExpanded);
        if (this.isExpanded) {
            this.setMiniMode();
        } else {
            this.setExpandedMode();
        }
    }

    handleCloseClick() {
        console.log('handleCloseClick() => isExpanded:', this.isExpanded);
        if (this.isExpanded) {
            // revert to mini
            this.setMiniMode();
        } else {
            // if already mini => hide entirely
            this.closeCompletely();
        }
    }

    // Switch to mini mode (bottom-right, 320x180)
    setMiniMode() {
        console.log('setMiniMode() called...');
        const lightbox = this.shadowRoot.getElementById('lightbox');
        lightbox.classList.remove('expanded');
        lightbox.classList.add('mini');
        // Reset inline styling
        lightbox.style.left = '';
        lightbox.style.top = '';
        lightbox.style.width = '';
        lightbox.style.height = '';
        lightbox.style.display = 'block';
        this.isExpanded = false;
        this.isHidden = false;
    }

    // Switch to expanded mode (50vw x 50vh, centered)
    setExpandedMode() {
        console.log('setExpandedMode() called...');
        const lightbox = this.shadowRoot.getElementById('lightbox');
        lightbox.classList.remove('mini');
        lightbox.classList.add('expanded');
        // Clear position so it re-centers
        lightbox.style.left = '';
        lightbox.style.top = '';
        lightbox.style.width = '';
        lightbox.style.height = '';
        this.isExpanded = true;
        this.isHidden = false;
    }

    closeCompletely() {
        console.log('closeCompletely() called...');
        const lightbox = this.shadowRoot.getElementById('lightbox');
        lightbox.style.display = 'none';
        this.isHidden = true;

        // Show an "Open Player" button on main document body
        const reopenBtn = document.createElement('button');
        reopenBtn.classList.add('hide-button');
        reopenBtn.textContent = 'Open Player';

        reopenBtn.style.position = 'fixed';
        reopenBtn.style.bottom = '20px';
        reopenBtn.style.right = '20px';
        reopenBtn.style.zIndex = '3000';

        document.body.appendChild(reopenBtn);

        reopenBtn.addEventListener('click', () => {
            console.log('Reopening in mini mode...');
            document.body.removeChild(reopenBtn);
            lightbox.style.display = 'block';
            this.isExpanded = false;
            this.isHidden = false;
            lightbox.classList.remove('expanded');
            lightbox.classList.add('mini');
        });
    }

    //--- Dragging (only in expanded) ---
    startDragging(e) {
        if (!this.isExpanded) return;
        e.preventDefault();
        this.isDragging = true;
        const lightbox = this.shadowRoot.getElementById('lightbox');
        const rect = lightbox.getBoundingClientRect();
        this.startX = e.clientX - rect.left;
        this.startY = e.clientY - rect.top;
    }
    drag(e) {
        if (!this.isDragging) return;
        e.preventDefault();
        const lightbox = this.shadowRoot.getElementById('lightbox');
        const newLeft = e.clientX - this.startX;
        const newTop = e.clientY - this.startY;
        lightbox.style.left = `${newLeft}px`;
        lightbox.style.top = `${newTop}px`;
        lightbox.style.transform = 'none';
    }
    stopDragging() {
        this.isDragging = false;
    }

    //--- Resizing (only in expanded) ---
    startResizing(e) {
        if (!this.isExpanded) return;
        e.preventDefault();
        this.isResizing = true;
        const lightbox = this.shadowRoot.getElementById('lightbox');
        const rect = lightbox.getBoundingClientRect();
        this.startX = e.clientX;
        this.startY = e.clientY;
        this.startWidth = rect.width;
        this.startHeight = rect.height;
    }
    resize(e) {
        if (!this.isResizing) return;
        e.preventDefault();
        const lightbox = this.shadowRoot.getElementById('lightbox');
        const deltaX = e.clientX - this.startX;
        const deltaY = e.clientY - this.startY;
        const newWidth = this.startWidth + deltaX;
        const newHeight = this.startHeight + deltaY;
        lightbox.style.width = Math.max(200, newWidth) + 'px';
        lightbox.style.height = Math.max(150, newHeight) + 'px';
        lightbox.style.transform = 'none';
    }
    stopResizing() {
        this.isResizing = false;
    }
}

customElements.define('video-player', VideoPlayer);

Hello,

Nice job and thank you for sharing.

Why not put it on GitHub and share the link?

I will do that with my projects in the future.
Was mostly just experimenting with pasting the code in here too see what it would look like.