music

f5a969c4...6d00 615e9cdb...c1adbac5
index.html
26.17 KB
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>alex plain - music collection</title>
    <meta name="description" content="i like to make music! find a mix of roughs, jams, and at-home recordings from alex plain.">

    <!-- OpenGraph / Social Media Meta Tags -->
    <meta property="og:type" content="website">
    <meta property="og:title" content="alex plain - music collection">
    <meta property="og:description" content="i like to make music! find a mix of roughs, jams, and at-home recordings from alex plain.">
    <meta property="og:image" content="https://jax.krondor.org/gw/f45fe1b0-8172-4619-b6c6-17518058ff16/DSC_0150.jpeg">
    <meta property="og:url" content="https://alexplain.krondor.org">
    <meta property="og:site_name" content="alex plain music">

    <!-- Twitter Card Meta Tags -->
    <meta name="twitter:card" content="summary_large_image">
    <meta name="twitter:title" content="alex plain - music collection">
    <meta name="twitter:description" content="i like to make music! find a mix of roughs, jams, and at-home recordings from alex plain.">
    <meta name="twitter:image" content="https://jax.krondor.org/gw/f45fe1b0-8172-4619-b6c6-17518058ff16/DSC_0150.jpeg">

    <!-- Inter font -->
    <link rel="preconnect" href="https://rsms.me/" />
    <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />

    <!-- Franken UI -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/franken-ui@next/dist/css/core.min.css" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/franken-ui@next/dist/css/utilities.min.css" />

    <style>
        :root {
            font-family: Inter, sans-serif;
            font-feature-settings: "liga" 1, "calt" 1;

            --background: 0 0% 100%;
            --foreground: 0 0% 0%;
            --muted: 0 0% 95%;
            --muted-foreground: 0 0% 40%;
            --border: 0 0% 85%;
            --card: 0 0% 100%;
            --primary: 0 0% 0%;
        }

        @supports (font-variation-settings: normal) {
            :root {
                font-family: InterVariable, sans-serif;
            }
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background: hsl(var(--background));
            color: hsl(var(--foreground));
            line-height: 1.6;
            -webkit-font-smoothing: antialiased;
            font-weight: 600;
        }

        /* Typewriter aesthetic */
        h1, h2, h3, .typewriter {
            font-family: 'Courier New', 'Monaco', monospace;
            font-weight: 900;
            letter-spacing: 0.5px;
        }

        .container {
            max-width: 1200px;
            margin: 0 auto;
            padding: 0 3rem;
        }

        /* Hero Section */
        .hero {
            position: relative;
            padding: 5rem 0;
            margin-bottom: 3rem;
        }

        .hero-background {
            position: absolute;
            right: 0;
            top: 3rem;
            width: 500px;
            height: 500px;
            background-image: url('https://jax.krondor.org/gw/f45fe1b0-8172-4619-b6c6-17518058ff16/DSC_0150.jpeg');
            background-size: cover;
            background-position: center;
            border: 2px solid hsl(var(--border));
            z-index: 0;
            pointer-events: none;
        }

        .hero-content {
            position: relative;
            z-index: 2;
            background: white;
            padding: 3rem 4rem;
            max-width: 600px;
            border: 2px solid hsl(var(--foreground));
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
            margin-bottom: 4rem;
        }

        .hero h1 {
            font-size: 3rem;
            margin-bottom: 2rem;
            letter-spacing: 1px;
        }

        .hero-text {
            font-family: 'Courier New', 'Monaco', monospace;
            font-size: 1rem;
            line-height: 2;
            white-space: pre-line;
            font-weight: 600;
        }

        /* Tabs */
        .tabs {
            position: relative;
            z-index: 2;
            display: flex;
            gap: 0;
            border-bottom: 2px solid hsl(var(--foreground));
            max-width: 500px;
        }

        .tab {
            font-family: 'Courier New', 'Monaco', monospace;
            padding: 1rem 2rem;
            background: white;
            border: 2px solid hsl(var(--foreground));
            border-bottom: 2px solid hsl(var(--foreground));
            cursor: pointer;
            font-size: 1rem;
            font-weight: 700;
            color: hsl(var(--muted-foreground));
            transition: all 0.2s;
            margin-right: -2px;
            margin-bottom: -2px;
        }

        .tab:hover {
            color: hsl(var(--foreground));
            background: hsl(var(--muted));
        }

        .tab.active {
            color: hsl(var(--foreground));
            background: white;
            font-weight: 700;
            position: relative;
            border-bottom-color: white;
        }

        .tab.active::after {
            content: '';
            position: absolute;
            bottom: -2px;
            left: 0;
            right: 0;
            height: 2px;
            background: white;
        }

        /* Track Section */
        .tracks-section {
            margin-top: 1rem;
        }

        .tab-content {
            display: none;
        }

        .tab-content.active {
            display: block;
            animation: fadeIn 0.3s ease-in;
        }

        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(10px); }
            to { opacity: 1; transform: translateY(0); }
        }

        .section-description {
            font-family: 'Courier New', 'Monaco', monospace;
            color: hsl(var(--muted-foreground));
            font-size: 0.95rem;
            font-weight: 600;
            margin-bottom: 2rem;
            line-height: 2;
            padding-left: 1rem;
            border-left: 3px solid hsl(var(--border));
        }

        /* Track List */
        .track-list {
            display: grid;
            gap: 2rem;
        }

        .track {
            padding: 2rem;
            background: hsl(var(--card));
            border: 1px solid hsl(var(--border));
            transition: all 0.3s ease;
            position: relative;
        }

        .track::before {
            content: '';
            position: absolute;
            left: 0;
            top: 0;
            bottom: 0;
            width: 3px;
            background: hsl(var(--foreground));
            transform: scaleY(0);
            transition: transform 0.3s ease;
        }

        .track:hover {
            border-color: hsl(var(--foreground));
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
            transform: translateX(8px);
        }

        .track:hover::before {
            transform: scaleY(1);
        }

        .track-name {
            font-family: 'Courier New', 'Monaco', monospace;
            font-size: 1rem;
            font-weight: 700;
            margin-bottom: 1.25rem;
            color: hsl(var(--foreground));
        }

        /* Custom Audio Player */
        .custom-player {
            display: flex;
            flex-direction: column;
            gap: 0.75rem;
        }

        .player-controls {
            display: flex;
            align-items: center;
            gap: 1rem;
        }

        .play-btn {
            width: 36px;
            height: 36px;
            border-radius: 50%;
            background: hsl(var(--foreground));
            color: hsl(var(--background));
            border: none;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            flex-shrink: 0;
            transition: all 0.2s;
        }

        .play-btn:hover {
            transform: scale(1.05);
        }

        .play-btn svg {
            width: 14px;
            height: 14px;
        }

        .seek-container {
            flex: 1;
            display: flex;
            align-items: center;
            gap: 0.75rem;
        }

        .time {
            font-family: 'Courier New', 'Monaco', monospace;
            font-size: 0.8rem;
            font-weight: 700;
            color: hsl(var(--muted-foreground));
            min-width: 45px;
        }

        .seek-bar {
            flex: 1;
            height: 4px;
            background: hsl(var(--muted));
            border-radius: 2px;
            cursor: pointer;
            position: relative;
        }

        .seek-bar-progress {
            height: 100%;
            background: hsl(var(--foreground));
            border-radius: 2px;
            width: 0%;
            transition: width 0.1s linear;
        }

        /* Hide native audio element */
        audio {
            display: none;
        }

        /* Loading State */
        .loading {
            font-family: 'Courier New', 'Monaco', monospace;
            text-align: center;
            padding: 2rem;
            color: hsl(var(--muted-foreground));
            font-size: 0.9rem;
        }

        .error {
            font-family: 'Courier New', 'Monaco', monospace;
            text-align: center;
            padding: 2rem;
            color: #d32f2f;
            font-size: 0.9rem;
        }

        /* Footer */
        footer {
            padding: 3rem 0;
            text-align: center;
            color: hsl(var(--muted-foreground));
            font-size: 0.85rem;
            font-family: 'Courier New', 'Monaco', monospace;
        }

        .social-links {
            display: flex;
            justify-content: center;
            align-items: center;
            gap: 2rem;
            margin-bottom: 1.5rem;
        }

        .social-link {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            color: hsl(var(--muted-foreground));
            text-decoration: none;
            transition: all 0.2s;
            padding: 0.5rem;
            border: 1px solid transparent;
            opacity: 0.6;
        }

        .social-link:hover {
            color: hsl(var(--foreground));
            opacity: 1;
        }

        .social-link svg {
            width: 20px;
            height: 20px;
            flex-shrink: 0;
        }

        /* Responsive */
        @media (max-width: 968px) {
            .container {
                padding: 0 2rem;
            }

            .hero {
                padding: 3rem 0;
                margin-bottom: 3rem;
            }

            .hero-background {
                position: relative;
                transform: none;
                width: 100%;
                height: 350px;
                margin-bottom: 2rem;
            }

            .hero-content {
                padding: 2.5rem 3rem;
                max-width: 100%;
            }

            .hero h1 {
                font-size: 2.5rem;
            }

            .tabs {
                max-width: 100%;
                overflow-x: auto;
            }

            .track:hover {
                transform: translateX(4px);
            }
        }

        @media (max-width: 640px) {
            .container {
                padding: 0 1rem;
            }

            .hero-background {
                height: 300px;
            }

            .hero-content {
                padding: 2rem;
            }

            .hero h1 {
                font-size: 2rem;
            }

            .track {
                padding: 1.5rem;
            }

            .player-controls {
                gap: 0.75rem;
            }

            .play-btn {
                width: 32px;
                height: 32px;
            }
        }
    </style>

    <script type="module" src="https://cdn.jsdelivr.net/npm/franken-ui@next/dist/js/core.iife.js"></script>
</head>
<body>
    <!-- Hero Section with background -->
    <section class="hero">
        <div class="container">
            <div class="hero-background"></div>
            <div class="hero-content">
                <h1>hi there!</h1>
                <div class="hero-text">i like to make music! i mostly play live either at open mics or in thompkins sq park, but you can find a mix of stuff I've recorded in this collection:

happy listening!

-- alex.</div>
            </div>

            <div class="social-links" style="max-width: 500px; justify-content: flex-start; margin-bottom: 2rem; gap: 1rem;">
                <a href="https://misc.krondor.org/" target="_blank" rel="noopener noreferrer" class="social-link" title="misc.krondor.org">
                    <svg fill="currentColor" viewBox="0 0 24 24">
                        <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
                    </svg>
                </a>
                <a href="https://www.instagram.com/k_r_o_n_d_o_r/" target="_blank" rel="noopener noreferrer" class="social-link" title="instagram">
                    <svg fill="currentColor" viewBox="0 0 24 24">
                        <path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/>
                    </svg>
                </a>
                <a href="https://soundcloud.com/alex-plain-565066733" target="_blank" rel="noopener noreferrer" class="social-link" title="soundcloud">
                    <svg fill="currentColor" viewBox="0 0 24 24">
                        <path d="M1.175 12.225c-.051 0-.094.046-.101.1l-.233 2.154.233 2.105c.007.058.05.098.101.098.05 0 .09-.04.099-.098l.255-2.105-.27-2.154c0-.057-.045-.1-.09-.1m-.899.828c-.051 0-.09.04-.099.098l-.135 1.326.135 1.303c.009.055.048.097.099.097.05 0 .093-.042.101-.097l.168-1.303-.168-1.326c-.008-.058-.051-.098-.101-.098m1.83-1.229c-.062 0-.108.047-.12.11l-.21 2.563.21 2.5c.012.062.058.113.12.113.061 0 .111-.051.119-.113l.245-2.5-.245-2.563c-.008-.063-.058-.11-.119-.11m.928-.466c-.065 0-.117.052-.127.12l-.192 2.996.192 2.918c.01.066.062.12.127.12.066 0 .119-.054.132-.12l.225-2.918-.225-2.996c-.013-.068-.066-.12-.132-.12m.941-.568c-.07 0-.129.054-.138.125l-.18 3.564.18 3.465c.009.071.068.125.138.125.071 0 .132-.054.143-.125l.209-3.465-.209-3.564c-.011-.071-.072-.125-.143-.125m.948-.609c-.074 0-.137.058-.147.134l-.162 4.173.162 4.052c.01.075.073.133.147.133.075 0 .137-.058.148-.133l.189-4.052-.189-4.173c-.011-.076-.073-.134-.148-.134m.956-.633c-.079 0-.145.063-.156.141l-.149 4.806.149 4.668c.011.078.077.14.156.14.08 0 .147-.062.159-.14l.175-4.668-.175-4.806c-.012-.078-.079-.141-.159-.141m.964-.639c-.084 0-.153.066-.165.15l-.138 5.445.138 5.301c.012.084.081.149.165.149.084 0 .155-.065.167-.149l.162-5.301-.162-5.445c-.012-.084-.083-.15-.167-.15m.971-.63c-.089 0-.162.07-.175.158l-.127 6.075.127 5.916c.013.089.086.159.175.159.09 0 .165-.07.178-.159l.153-5.916-.153-6.075c-.013-.088-.088-.158-.178-.158m.964-.63c-.093 0-.171.074-.185.168l-.115 6.705.115 6.547c.014.093.092.168.185.168.095 0 .173-.075.188-.168l.134-6.547-.134-6.705c-.015-.094-.093-.168-.188-.168m-10.677 8.036c-.046 0-.084.037-.092.085l-.253 1.953.253 1.907c.008.048.046.083.092.083.048 0 .086-.035.093-.083l.296-1.907-.296-1.953c-.007-.048-.045-.085-.093-.085m12.943-8.036c-.097 0-.176.078-.191.176l-.104 6.705.104 6.547c.015.097.094.176.191.176.098 0 .177-.079.194-.176l.112-6.547-.112-6.705c-.017-.098-.096-.176-.194-.176m.972-.63c-.101 0-.184.081-.199.184l-.094 7.335.094 7.178c.015.102.098.184.199.184.102 0 .186-.082.202-.184l.111-7.178-.111-7.335c-.016-.103-.1-.184-.202-.184m.976-.63c-.105 0-.192.086-.209.195l-.083 7.965.083 7.801c.017.108.104.194.209.194.106 0 .195-.086.212-.194l.099-7.801-.099-7.965c-.017-.109-.106-.195-.212-.195m.982-.66c-.11 0-.201.091-.219.204l-.073 8.625.073 8.452c.018.114.109.205.219.205.112 0 .204-.091.223-.205l.087-8.452-.087-8.625c-.019-.113-.111-.204-.223-.204"/>
                    </svg>
                </a>
            </div>

            <div class="tabs">
                <button class="tab active" onclick="switchTab('roughs')">roughs</button>
                <button class="tab" onclick="switchTab('jams')">jams</button>
                <button class="tab" onclick="switchTab('at-home')">at-home</button>
            </div>
        </div>
    </section>

    <!-- Tracks Section (BELOW hero) -->
    <div class="container tracks-section">
        <!-- Roughs Tab -->
        <div class="tab-content active" id="roughs-content">
            <p class="section-description">as close i get to a final product. expect some covers mixed with originals, but the main commonality is the use of a DAW to make something a lil more produced.</p>
            <div class="track-list" id="roughs-tracks">
                <div class="loading">loading tracks...</div>
            </div>
        </div>

        <!-- Jams Tab -->
        <div class="tab-content" id="jams-content">
            <p class="section-description">longer-form sessions i've started doing lately, don't expect much lyrics or polish</p>
            <div class="track-list" id="jams-tracks">
                <div class="loading">loading tracks...</div>
            </div>
        </div>

        <!-- At Home Tab -->
        <div class="tab-content" id="at-home-content">
            <p class="section-description">stuff that i recorded in one take with my phone, its mostly covers and new ideas i'm working on</p>
            <div class="track-list" id="at-home-tracks">
                <div class="loading">loading tracks...</div>
            </div>
        </div>
    </div>

    <footer>
        <div class="container">
            <p>~</p>
        </div>
    </footer>

    <script>
        // Configuration
        const BUCKET_ID = 'f5a969c4-a3e2-43bc-9ab2-0e6343006d00';
        const GATEWAY_URL = `https://jax.krondor.org/gw/${BUCKET_ID}`;

        // Music directories
        const directories = [
            { id: 'roughs', path: 'roughs' },
            { id: 'jams', path: 'jams' },
            { id: 'at-home', path: 'at-home' }
        ];

        // Tab switching
        function switchTab(tabName) {
            // Update tab buttons
            document.querySelectorAll('.tab').forEach(tab => {
                tab.classList.remove('active');
            });
            event.target.classList.add('active');

            // Update tab content
            document.querySelectorAll('.tab-content').forEach(content => {
                content.classList.remove('active');
            });
            document.getElementById(`${tabName}-content`).classList.add('active');
        }

        // Format time in mm:ss
        function formatTime(seconds) {
            if (isNaN(seconds)) return '0:00';
            const mins = Math.floor(seconds / 60);
            const secs = Math.floor(seconds % 60);
            return `${mins}:${secs.toString().padStart(2, '0')}`;
        }

        // Format track name for display
        function formatTrackName(filename) {
            // Remove file extension
            let name = filename.replace(/\.(mp3|m4a)$/i, '');

            // Remove timestamp patterns like "10!25!25, 7.31 PM"
            name = name.replace(/\s*-\s*\d+!\d+!\d+,\s*\d+\.\d+\s*(AM|PM)/gi, '');

            return name;
        }

        // Create custom audio player
        function createCustomPlayer(audioSrc) {
            const audio = document.createElement('audio');
            audio.preload = 'metadata';
            audio.src = audioSrc;

            const player = document.createElement('div');
            player.className = 'custom-player';

            const controls = document.createElement('div');
            controls.className = 'player-controls';

            // Play/Pause button
            const playBtn = document.createElement('button');
            playBtn.className = 'play-btn';
            playBtn.innerHTML = `<svg fill="currentColor" viewBox="0 0 16 16"><path d="M5 3.5v9l7-4.5z"/></svg>`;
            playBtn.onclick = () => togglePlay(audio, playBtn);

            // Seek container
            const seekContainer = document.createElement('div');
            seekContainer.className = 'seek-container';

            const currentTime = document.createElement('div');
            currentTime.className = 'time';
            currentTime.textContent = '0:00';

            const seekBar = document.createElement('div');
            seekBar.className = 'seek-bar';
            seekBar.onclick = (e) => seek(e, audio, seekBar);

            const seekBarProgress = document.createElement('div');
            seekBarProgress.className = 'seek-bar-progress';
            seekBar.appendChild(seekBarProgress);

            const duration = document.createElement('div');
            duration.className = 'time';
            duration.textContent = '0:00';

            seekContainer.appendChild(currentTime);
            seekContainer.appendChild(seekBar);
            seekContainer.appendChild(duration);

            controls.appendChild(playBtn);
            controls.appendChild(seekContainer);

            player.appendChild(controls);
            player.appendChild(audio);

            // Update time and seek bar
            audio.addEventListener('loadedmetadata', () => {
                duration.textContent = formatTime(audio.duration);
            });

            audio.addEventListener('timeupdate', () => {
                currentTime.textContent = formatTime(audio.currentTime);
                const progress = (audio.currentTime / audio.duration) * 100;
                seekBarProgress.style.width = `${progress}%`;
            });

            audio.addEventListener('ended', () => {
                playBtn.innerHTML = `<svg fill="currentColor" viewBox="0 0 16 16"><path d="M5 3.5v9l7-4.5z"/></svg>`;
            });

            return player;
        }

        // Toggle play/pause
        function togglePlay(audio, button) {
            // Pause all other audio elements
            document.querySelectorAll('audio').forEach(a => {
                if (a !== audio && !a.paused) {
                    a.pause();
                    // Reset other play buttons
                    const otherBtn = a.parentElement.querySelector('.play-btn');
                    if (otherBtn) {
                        otherBtn.innerHTML = `<svg fill="currentColor" viewBox="0 0 16 16"><path d="M5 3.5v9l7-4.5z"/></svg>`;
                    }
                }
            });

            if (audio.paused) {
                audio.play();
                button.innerHTML = `<svg fill="currentColor" viewBox="0 0 16 16"><path d="M4 3h3v10H4zm5 0h3v10H9z"/></svg>`;
            } else {
                audio.pause();
                button.innerHTML = `<svg fill="currentColor" viewBox="0 0 16 16"><path d="M5 3.5v9l7-4.5z"/></svg>`;
            }
        }

        // Seek in track
        function seek(event, audio, seekBar) {
            const rect = seekBar.getBoundingClientRect();
            const percent = (event.clientX - rect.left) / rect.width;
            audio.currentTime = percent * audio.duration;
        }

        // Create track element
        function createTrackElement(entry) {
            const track = document.createElement('div');
            track.className = 'track';

            const trackName = document.createElement('div');
            trackName.className = 'track-name';
            trackName.textContent = formatTrackName(entry.name);

            const player = createCustomPlayer(`${GATEWAY_URL}${entry.path}`);

            track.appendChild(trackName);
            track.appendChild(player);

            return track;
        }

        // Load tracks for a directory
        async function loadTracks(dirId, dirPath) {
            const container = document.getElementById(`${dirId}-tracks`);

            try {
                const response = await fetch(`${GATEWAY_URL}/${dirPath}`);

                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }

                const data = await response.json();

                // Filter audio files and sort by name
                const audioFiles = data.entries
                    .filter(entry => !entry.is_dir &&
                           (entry.mime_type.startsWith('audio/') ||
                            entry.name.match(/\.(mp3|m4a|wav|ogg)$/i)))
                    .sort((a, b) => a.name.localeCompare(b.name));

                // Clear loading message
                container.innerHTML = '';

                if (audioFiles.length === 0) {
                    container.innerHTML = '<div class="loading">no tracks found</div>';
                    return;
                }

                // Create track elements
                audioFiles.forEach(entry => {
                    const trackElement = createTrackElement(entry);
                    container.appendChild(trackElement);
                });

            } catch (error) {
                console.error(`Error loading ${dirPath}:`, error);
                container.innerHTML = `<div class="error">failed to load tracks. please try again later.</div>`;
            }
        }

        // Load all directories on page load
        directories.forEach(dir => {
            loadTracks(dir.id, dir.path);
        });
    </script>
</body>
</html>