MP3 Player Pro

A lightweight browser-based audio controller that lets you drag-and-drop music files into any tab for instant playback, featuring synchronized lyrics with real-time multi-language translations.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         MP3 Player Pro
// @namespace    https://i-ali.ir/
// @version      1.0.0
// @license      GNU AGPLv3
// @description  A lightweight browser-based audio controller that lets you drag-and-drop music files into any tab for instant playback, featuring synchronized lyrics with real-time multi-language translations.
// @author       AliAshori
// @icon         data:image/png;base64,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
// @match        file:///*.mp3
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_xmlhttpRequest
// @connect      *
// @resource     JQUERY https://code.jquery.com/jquery-3.6.0.min.js
// @resource     JQUERYUI https://code.jquery.com/ui/1.13.2/jquery-ui.min.js
// ==/UserScript==

(function() {
    'use strict';

    // Function to remove autoplay and initialize app
    function removeAutoplay() {
        // Remove autoplay from any video/audio elements
        const mediaElements = document.querySelectorAll('video, audio');
        mediaElements.forEach(el => {
            el.removeAttribute('autoplay');
            el.pause();
        });
    }

    // Delay initialization
    setTimeout(removeAutoplay, 20);

    // Inject jQuery
    const jquery = document.createElement('script');
    jquery.textContent = GM_getResourceText('JQUERY');
    document.head.appendChild(jquery);

    // Inject jQuery UI after jQuery
    const jqueryUI = document.createElement('script');
    jqueryUI.textContent = GM_getResourceText('JQUERYUI');
    document.head.appendChild(jqueryUI);

    // Wait for the page to load
    $(document).ready(function() {
        // Create the viewer interface
        const html = `
    <div id="mp3ViewerContainer" style="
        display: flex;
        flex-direction: column;
        position: fixed;
        top: 8px;
        right: 8px;
        left: 8px;
        bottom: auto;
        width: -moz-available;
        background: #0f0f0f;
        border: 1px solid #303030;
        border-radius: 12px;
        box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        padding: 10px;
        z-index: 9999;
        font-family: Ink Free, sans-serif;
        text-align: center;
    ">
        <!-- Change file input to allow multiple files -->
        <input type="file" id="fileInput" accept="audio/*,audio/mpeg,.mp3,.m4a,.flac,.wav,.ogg,.aac,.webm,.opus" style="display: none;" multiple />

                <h3 style="margin-top: 0; color: #666;">🎵 MP3 Player Pro</h3>
                <div id="dropArea" style="
                    background: #181818;
                    border: 2px dashed #797979;
                    border-radius: 12px;
                    padding: 20px;
                    text-align: center;
                    margin-bottom: 15px;
                    align-content: center;
                    cursor: pointer;
                    transition: all 0.3s;
                ">
                    <p style="margin: 0; color: #797979; font-size: 18px; text-transform: capitalize;">Drop MP3 file here or click to browse</p>
                </div>

                <div id="status" style="
                    text-align: center;
                    margin: 6px 0;
                    min-height: 20px;
                    color: #666;
                    font-size: 16px;
                    text-transform: capitalize;
                ">Ready to load MP3 files</div>

                <div id="mainPlayerContainer" style="display: none;">
                    <audio id="audioPlayer" controls style="width: 100%; margin-bottom: 8px;" autoplay=""></audio>

                    <!-- Add Next/Previous buttons in your HTML (inside #mainPlayerContainer, after <audio ...>) -->
                    <div style="margin-bottom:10px;">
                        <button id="prevBtn" style="margin-right:10px;">⏮️ Prev</button>
                        <button id="playBtn" style="margin-right:10px;">▶️ Play</button>
                        <button id="nextBtn" style="margin-right:10px;">⏭️ Next</button>
                        <button id="loopBtn" style="margin-right:10px;">🔁 Loop: Off</button>
                        <button id="repeatBtn" style="margin-right:10px;">🔃 Repeat: Off</button>
                        <button id="shuffleBtn" style="margin-right:10px;">🔀 Auto Shuffle</button>
                    </div>
                </div>

                <div id="playerContainer" style="display: none;">

                    <div id="metadata" style="
                        height: 100%;
                        background: #272727;
                        padding: 10px 15px 8px 15px;
                        border-radius: 12px;
                        /*display: block ruby;
                        display: inline-block;*/
                        float: left;
                    ">
                        <img id="albumArt" style="
                            width: 320px;
                            max-width: 100%;
                            max-height: 100%;
                            display: block;
                            /*margin: 0 auto 15px;*/
                            border-radius: 12px;
                            float: left;
                            display: none;
                        " />
                        <div id="trackInfo" style="
                        display: inline-block;
                        position: relative;
                        text-align: left;
                        left: 6px;
                        bottom: 2px;
                        font-size: 22px;
                        float: left;
                        color: #e3e3e3;
                        line-height: 1.8;
                        align-self: stretch;
                        align-content: center;
                        padding-left: 18px;
                        overflow: scroll;
                        text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.6);
                        font-weight: bold;
                        "></div>
                    </div>

                    <!-- TABS: Playlist + Lyrics(Translation) -->
                    <div id="tabsWrapper" style="background:#222; margin-top:0; text-align:left;">
                        <div id="tabsHeader" style="display:flex; gap:0; margin-bottom:8px;">
                            <button id="tabPlaylist" class="tabBtn" style="padding:5px 32px; background:#404040; border:none; color:#fff; border-radius:0;">🎶 Playlist</button>
                            <button id="tabTranslation" class="tabBtn" style="padding:5px 32px; background:#303030; border:none; color:#fff; border-radius:0;">📝 Lyrics</button>
                            <button id="tabSOON" class="tabBtn" style="padding:5px 32px; background:#303030; border:none; color:#fff; border-radius:0;">🔮 SOON</button>
                        </div>

                        <!-- Playlist pane (kept id for existing logic) -->
                        <div id="playlistContainer" style="
                        margin-top:0;
                        text-align:left;
                        overflow-y:auto;
                        background:#222;
                        border-radius:12px;
                        padding:10px;
                        overflow-x: hidden;
                        ">
                            <div id="dropToPlaylist" style="
                                background: #181818;
                                border: 2px dashed #444;
                                border-radius: 5px;
                                padding: 10px;
                                text-align: center;
                                margin-bottom: 15px;
                                color: #888;
                                cursor: pointer;
                                transition: 0.3s;
                                text-transform: capitalize;
                            ">
                                Drop MP3 files here to add to playlist
                            </div>

                            <!-- Add this search box -->
                            <input type="text" id="playlistSearch" placeholder="Search playlist..." style="width:98%;margin-bottom:8px;padding:4px 8px;border-radius:4px;border:1px solid #444;background:#181818;color:#ccc;font-size:15px;">

                            <div style="color:#aaa; margin-bottom:5px;">
                                <button id="savePlaylistBtn" style="padding: 2px 14px; float: right;">💾 Save Playlist</button>
                                <input type="file" id="loadPlaylistInput" accept=".json" style="display:none;">
                                <button id="loadPlaylistBtn" style="padding: 2px 14px; float: right;">📂 Load Playlist</button>
                            </div>

                            <div style="color:#aaa; margin-bottom:8px;">🎶Playlist:</div>
                            <ul id="playlist" style="list-style:none; padding:0; margin:0;"></ul>
                        </div>

                        <!-- Translation pane (Lyrics OR Translation Music) -->
                        <div id="translationContainer" style="display:none; overflow-y:auto; background:#222; border-radius:5px; padding:8px;">
                            <div style="margin-bottom:8px; color: #bbb; font-family: sans-serif;">
                                <button id="fetchLyricsBtn" style="padding:6px 12px; background:#404040; color:#fff; border:none; border-radius:4px; cursor:pointer; font-family: Ink Free, sans-serif;">🎙️ Fetch Lyrics</button>
                                <button id="translationSettingsBtn" title="Lyrics Settings" style="padding:6px 6px; background:#404040; color:#fff; border:none; border-radius:4px; cursor:pointer; font-family: Ink Free, sans-serif;margin-left: 6px;">⚙️</button>
                                <button id="translationLyricsBtn" style="padding:6px 6px; background:#404040; color:#fff; border:none; border-radius:4px; cursor:pointer; font-family: Ink Free, sans-serif;margin-left: 4px;">🌐</button>

                                <span style="margin: 0 10px;">|</span>

                                <!-- Manual search will be inserted here by JavaScript -->
                                <button id="manualLyricsSearchBtn" style="margin: 0 -10px 0 0; padding: 4.5px 6px 6.5px 0px; background: #555; color: #fff; border: none; border-radius: 3px; cursor: pointer; font-family: Ink Free, sans-serif;">🔎</button>
                                <input type="text" id="manualLyricsInput"
                                       placeholder="Enter artist & song..."
                                       style="flex:1; padding:6px; background:#333; color:#fff; border:1px solid #555; border-radius:4px; outline:none; font-size:13px; font-family: Ink Free, sans-serif;">

                                <span id="lyricsStatus" style="color:#aaa; font-size:14px; margin-left:8px; font-family: Ink Free, sans-serif;">No lyrics loaded</span>

                                <!-- Clear Lyrics -->
                                <button id="clearLyricsBtn" style="padding:6px 6px; background:#553030; color:#fff; border:none; border-radius:4px; cursor:pointer; float:right;">🗑️</button>
                            </div>

                            <div id="lyricsContainer" style="overflow-y:auto; padding:6px 0; background:#151515; border-radius:6px; border:1px solid #2a2a2a; color:#ddd; font-size:14px; line-height:1.6;">
                                <div id="lyricsInner" style="text-align:center; white-space:pre-wrap; line-height:1.3;">No lyrics to display</div>
                            </div>

                        </div>

                    </div>

                </div>

                <button id="closeBtn" style="
                    position: absolute;
                    top: 5px;
                    right: 5px;
                    background: none;
                    border: none;
                    font-size: 16px;
                    cursor: pointer;
                    color: #999;
                ">×</button>
            </div>

<style>
#mainPlayerContainer audio {
    width: 80% !important;
    align-self: center;
    align-content: center;
}
@media (max-width: 991px) {
  #dropArea {
    padding: 80px !important;
  }
  #metadata #albumArt {
    width: 380px !important;
  }
  #playerContainer #metadata {
    flex: 1 1 40% !important;
    height: 46% !important;
  }
  /*#playerContainer #playlistContainer {
    max-height:500px !important;
  }*/
  #trackInfo {
    height: 450px !important;
    align-self: center !important;
  }
}
@media (min-width: 1920px) {
  #metadata #albumArt {
    width: 380px !important; /* Full width of the viewport */
  }
  /*#playerContainer #playlistContainer {
    max-height:380px !important;
  }*/
}
@media (min-width: 2120px) {
  #metadata #albumArt {
    width: 500px !important; /* Full width of the viewport */
  }
  #dropArea {
    height: 120px !important;
  }
  /*#playerContainer #playlistContainer {
    max-height:500px !important;
  }*/
}
/* Insert the following CSS into the existing <style> block (or append to it) */
@media (min-width: 901px) {
  /* Make player area two columns: metadata left, tabs right */
  #playerContainer {
    display: flex;
    flex-direction: row !important;
    gap: 16px;
    overflow: clip !important;
  }

  /* Left column: metadata */
  #playerContainer #metadata {
    flex: 0 0 50%;
    max-width: 50%;
    min-width: 280px;
    box-sizing: border-box;
    float: none !important;
    margin-right: 0;
    margin-bottom: 0;
  }

  /* Ensure album art & trackInfo stack nicely inside left column */
  #playerContainer #metadata #albumArt {
    float: none !important;
    display: block;
    margin: 0;
    width: 100%;
    max-width: 380px;
    height: auto;
    object-fit: cover;
    align-content: center;
    align-self: center;
  }
  #playerContainer #metadata #trackInfo {
    display: block;
    width: 100%;
    font-size: 18px;
  }

  /* Right column: tabs and playlist/lyrics */
  #tabsWrapper {
    flex: 1 1 62% !important;
    max-width: 62%;
    min-width: 360px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    margin-top: 0;
  }

  /* Make tabs panes fill right column and scroll internally */
  #tabsWrapper #playlistContainer,
  #tabsWrapper #translationContainer {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding-left: 8px;
  }

  /* Slight visual tweaks to keep controls aligned */
  #mainPlayerContainer audio {
    width: 80% !important;
  }

  /* Prevent dropArea / header from stretching awkwardly inside row layout */
  #mp3ViewerContainer > h3,
  #mp3ViewerContainer > #dropArea,
  #mp3ViewerContainer > #status {
    flex: 0 0 auto;
  }

  #lyricsStatus {
    display: inline-block;
    width: auto;
  }

}

#playerContainer #metadata {
    display: flex !important;
    margin-bottom: 10px !important;
    border: 2px solid #515151;
}
#mp3ViewerContainer {
    transform: translate(0, 0);
    will-change: transform;
    /*user-select: none;*/
}
#prevBtn, #playBtn, #nextBtn, #loopBtn, #repeatBtn, #shuffleBtn, #savePlaylistBtn, #loadPlaylistBtn {
  font-family: Ink Free, sans-serif;
  background-color: #404040;
  border: none;
  border-radius: 4px;
  color: white;
  padding: 5px 20px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 15px;
  margin: 4px 2px;
  cursor: pointer;
}
/* Button hover animation */
#prevBtn:hover::before, #playBtn:hover::before, #nextBtn:hover::before,
#loopBtn:hover::before, #repeatBtn:hover::before, #shuffleBtn:hover::before, #savePlaylistBtn::before, #loadPlaylistBtn::before {
    left: 100%;
}

/* Button hover state */
#prevBtn:hover, #playBtn:hover, #nextBtn:hover,
#loopBtn:hover, #repeatBtn:hover, #shuffleBtn:hover, #savePlaylistBtn:hover, #loadPlaylistBtn:hover, #fetchLyricsBtn:hover, #translationSettingsBtn:hover, #translationLyricsBtn:hover, #closeLyricsModal:hover, #clearLyricsBtn:hover, #translationSettingsCancel:hover, #translationSettingsSave:hover  {
    background-color: #505050;
    transform: translateY(-2px);
    box-shadow: 0 4px 8px rgba(0,0,0,0.2);
    transition: all 0.5s !important;
}

#playlistSearch {
  font-family: Ink Free, sans-serif;
  outline: none;
}
#playlistSearch:focus {
  border-color: #09508e !important;
}
#playlist li .removeTrackBtn {
  position: absolute;
  top: 14px;
  right: 16px;
  background: none;
  color: #ff4c4c;
  border: none;
  font-size: 14px;
  cursor: pointer;
  z-index: 2;
  opacity: 0.5;
  transition: opacity 0.3s ease;
}
#playlist li .removeTrackBtn:hover {
    opacity: 1;
    color: #ff0000;
}
#metadata #albumArt {
    transition: all 0.5s ease;
    /*filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.4)) contrast(1.1);*/
    cursor: move;
    object-fit: cover;
    align-content: center;
    align-self: center;
    box-shadow: 0px 2px 4px rgba(0,0,0,0.2);
}
#albumArt:hover {
  transform: rotate(-3deg) scale(0.95);
  /*filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.4)) contrast(1.8) blur(1px) !important;*/
}
/*
#metadata #trackInfo {
  transition: all 0.5s ease;
}
#metadata #trackInfo:hover {
  filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.4)) brightness(1.8) !important;
  transform: rotate(3deg) scale(0.95);
}
*/
#trackInfo div {
  transition: all 0.5s ease;
}
#trackInfo div:hover {
  filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.4)) brightness(1.8) !important;
  transform: rotate(3deg) scale(0.95);
}

/* Add these CSS rules at the end of the existing <style> block (or merge into it) */
#tabsHeader {
    display: flex;
    gap: 0;
    margin: 0; /* pull header to container edges */
    background: linear-gradient(180deg, #0e0e0e, #151515);
    border-radius: 14px 14px 0 0;
    border: 1px solid #303030;
    align-items: center;
    box-shadow: 0 2px 8px rgba(0,0,0,0.35);
}
#tabPlaylist {
    border-radius: 12px 0 0 0 !important;
}
#tabSOON {
    border-radius: 0 12px 0 0 !important;
    border-right: none !important;
}
.tabBtn {
    width: calc(100% / 3);
    font-family: Ink Free, sans-serif;
    background: transparent;
    color: #c9c9c9;
    border: none;
    border-right: solid #535353 1px !important;
    padding: 0;
    border-radius: 0;
    cursor: pointer;
    transition: transform .16s ease, box-shadow .16s ease, background .16s ease;
    font-size: 14px;
    outline: none;
}
.tabBtn:hover {
    background: #262626 !important;
    color: #e5e5e5 !important;
    transform: translateY(0px);
    box-shadow: 0 6px 14px rgba(0,0,0,0.35);
}
.tabBtn.active {
    background: #003360 !important;
    color: #fff;
    transform: translateY(0px);
}
/* Small bottom indicator on active tab */
.tabBtn.active::after {
  content: '';
  display: block;
  position: absolute;
  height: 3px;
  border-radius: 3px;
  margin-top: 6px;
  width: 78%;
  background: linear-gradient(90deg, rgba(255,255,255,0.35), rgba(255,255,255,0.05));
  bottom: 0;
  left: calc(100% / 8);
}
/* Make playlist/translation panes align nicely under the header */
#playlistContainer, #translationContainer {
    margin-top: 0;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
    padding-top: 10px;
}
/* Make playlist/translation stick to bottom and be scrollable */
#mainPlayerContainer {
    /* make player a column flex so tabs/panes can expand and scroll */
    display: flex;
    flex-direction: column;
    align-content: center;
    align-items: center;
    align-self: center;
    width: 100%;
}
#playerContainer {
    /* make player a column flex so tabs/panes can expand and scroll */
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0; /* important: allow children to shrink and scroll */
    overflow: hidden;
}
#tabsWrapper {
    flex: 1 1 100%;
    margin-top: auto;      /* push tabs area to bottom of the viewer */
    display: flex;
    flex-direction: column;
    min-height: 0;
    border-radius: 12px;
}
#tabsHeader {
    flex: 0 0 auto;
}
#playlistContainer, #translationContainer {
    /* let panes fill remaining space and scroll internally */
    flex: 1 1 auto;
    min-height: 0; /* critical for overflow to work inside flex */
    overflow-y: auto;
    max-height: none;
}
/* ensure player/metadata don't expand unexpectedly */
/* (previous rule replaced by new #playerContainer settings) */
#mp3ViewerContainer > h3,
#mp3ViewerContainer > #dropArea,
#mp3ViewerContainer > #status {
    flex: 0 0 auto;
}

</style>
        `;

        // Add the HTML to the page
        $('body').append(html);

        // --- App font control: default + inject overriding CSS that can be changed via settings ---
       let appFont = '"Ink Free", "Segoe UI", Roboto, Arial, sans-serif';
       if (!$('#appFontStyles').length) {
           $('head').append(`
               <style id="appFontStyles">
                   /* Force the selected font across the whole UI (override inline styles) */
                   #mp3ViewerContainer, #mp3ViewerContainer * {
                       font-family: var(--app-font-family, "Ink Free, sans-serif") !important;
                   }
               </style>
           `);
       }
       // apply initial font
       try { $('#mp3ViewerContainer').css('--app-font-family', appFont); } catch (e) { /* ignore */ }

        // Get DOM elements
        const $dropArea = $('#dropArea');
        const $fileInput = $('#fileInput');
        const $audioPlayer = $('#audioPlayer');
        const $albumArt = $('#albumArt');
        const $trackInfo = $('#trackInfo');
        const $mainPlayerContainer = $('#mainPlayerContainer');
        const $playerContainer = $('#playerContainer');
        const $status = $('#status');
        const $closeBtn = $('#closeBtn');
        const $viewerContainer = $('#mp3ViewerContainer');

        // New tab elements
        const $tabPlaylist = $('#tabPlaylist');
        const $tabTranslation = $('#tabTranslation');
        const $translationContainer = $('#translationContainer');

        /* Drop area fade/collapse controls: small UI + API to control manually */
        if (!$('#dropAreaFadeStyles').length) {
            $('head').append(`
                <style id="dropAreaFadeStyles">
                    /* Collapsed / faded state */
                    #dropArea {
                        transition: all .30s cubic-bezier(.2,.8,.2,1);
                        position: relative;
                        overflow: visible;
                    }
                    #dropArea.droparea-collapsed {
                        opacity: 1.0;
                        padding: 8px 12px !important;
                        height: auto !important;
                        border-width: 1px !important;
                        box-shadow: none !important;
                    }
                    /* Controls inside drop area */
                    #dropArea .droparea-controls {
                        position: absolute;
                        top: 6px;
                        right: 8px;
                        z-index: 5;
                        display: flex;
                        gap:6px;
                        align-items: center;
                    }
                    #dropArea .droparea-controls button {
                        background: rgba(0,0,0,0.45);
                        color: #ddd;
                        border: 1px solid rgba(255,255,255,0.06);
                        padding: 4px 8px;
                        border-radius: 6px;
                        cursor: pointer;
                        font-size: 12px;
                        font-family: Ink Free, sans-serif;
                    }
                    #dropArea .droparea-controls input[type="range"] {
                        width: 110px;
                        height: 10px;
                        accent-color: #003360;
                        background: rgba(255,255,255,0.04);
                        border-radius: 4px;
                    }
                    /* Small label */
                    #dropArea .droparea-fade-label {
                        color: #bbb;
                        font-size: 12px;
                        margin-left:4px;
                    }
                </style>
            `);
        }

        // Add controls markup if not present
        if ($dropArea.find('.droparea-controls').length === 0) {
            $dropArea.append(`
                <div class="droparea-controls" aria-hidden="false">
                    <button id="addUrlBtn" title="Add URL(s)">🔗Add URL</button>
                    <button id="dropAreaToggle" title="Toggle compact/fade">⇳</button>
                    <input id="dropAreaFadeRange" type="range" min="0" max="100" value="100" title="Fade level (0 = invisible, 100 = opaque)">
                    <span class="droparea-fade-label" id="dropAreaFadeLabel">100%</span>
                </div>
            `);
        }

        const $dropAreaToggle = $('#dropAreaToggle');
        const $dropAreaFadeRange = $('#dropAreaFadeRange');
        const $dropAreaFadeLabel = $('#dropAreaFadeLabel');
        const $addUrlBtn = $('#addUrlBtn');

        // internal state
        let dropAreaCollapsed = false;
        let dropAreaFadeLevel = parseInt($dropAreaFadeRange.val(), 10) / 100.0; // 0..1

        // Apply computed visual state (opacity + compact padding)
        function applyDropAreaVisual(level) {
            // level is 0..1 where 1 = fully opaque/expanded, 0 = fully transparent/minimal
            level = Math.max(0, Math.min(1, level));
            dropAreaFadeLevel = level;

            // Opacity
            $dropArea.css('opacity', String(level));

            // Tweak padding to make the area more compact as it fades
            // keep a minimum padding of 6px, max original ~20-30px
            const basePad = 10; // px minimum
            const maxExtra = 30; // additional px when fully opaque
            const pad = Math.round(basePad + maxExtra * level);
            $dropArea.css('padding', `${pad}px`);

            // toggle class for an overall collapsed styling (for border/box-shadow)
            if (level < 0.5) {
                $dropArea.addClass('droparea-collapsed');
            } else {
                $dropArea.removeClass('droparea-collapsed');
            }

            // update slider label
            $dropAreaFadeLabel.text(Math.round(level * 100) + '%');
        }

        // Public API: set fade level 0..1
        window.setDropAreaFade = function(level) {
            applyDropAreaVisual(Number(level));
        };

        // Public API: toggle collapse (remember current fade level)
        window.toggleDropAreaCollapsed = function() {
            dropAreaCollapsed = !dropAreaCollapsed;
            if (dropAreaCollapsed) {
                // collapse to a compact faded state but preserve last fade setting
                applyDropAreaVisual(0.18);
                $dropAreaFadeRange.val(18);
            } else {
                // restore to last or default
                applyDropAreaVisual(1.0);
                $dropAreaFadeRange.val(100);
            }
        };

        // --- helper to move viewer to bottom (used when adding tracks) ---
        function setViewerBottom(px = 8, animate = true) {
            if (!$viewerContainer || !$viewerContainer.length) return;
            const value = (typeof px === 'number') ? `${px}px` : String(px);
            if (animate && typeof $viewerContainer.animate === 'function') {
                $viewerContainer.stop(true).animate({ bottom: value }, 180);
            } else {
                $viewerContainer.css('bottom', value);
            }
        }

        // Wire UI controls
        $dropAreaToggle.off('click').on('click', function(e) {
            e.stopPropagation();
            window.toggleDropAreaCollapsed();
        });

        // Add URL(s) modal button
        $addUrlBtn.off('click').on('click', function(e) {
            e.stopPropagation();
            showAddUrlModal();
        });

        // Create and show modal to add URL(s)
        function showAddUrlModal() {
            let $modal = $('#addUrlModal');
            if ($modal.length === 0) {
                $modal = $(`
                    <div id="addUrlModal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:1000001; align-items:center; justify-content:center;">
                        <div style="max-width:820px; width:92%; margin:16px auto auto auto; background:#222; color:#eee; padding:16px; border-radius:8px;">
                            <h3 style="margin:0 0 8px 0;">Add Track URL(s)</h3>
                            <div style="font-size:13px; color:#bbb; margin-bottom:8px;">
                                Paste one URL or local path per line. Supported schemes: http(s)://, file:/// or Windows paths like C:\path\to\file.mp3
                            </div>
                            <textarea id="addUrlTextarea" style="width:98%; height:160px; background:#111; color:#fff; border:1px solid #444; border-radius:6px; padding:8px;"></textarea>
                            <div style="text-align:right; margin-top:10px;">
                                <button id="addUrlCancel" style="margin-right:8px; padding:6px 12px; background:#555; color:#fff; border:none; border-radius:4px; cursor:pointer;">Cancel</button>
                                <button id="addUrlConfirm" style="padding:6px 12px; background:#0073e6; color:#fff; border:none; border-radius:4px; cursor:pointer;">Add to Playlist</button>
                            </div>
                        </div>
                    </div>
                `);
                $('body').append($modal);

                $('#addUrlCancel').on('click', function() { $('#addUrlModal').hide(); });
                $('#addUrlConfirm').on('click', function() {
                    const text = $('#addUrlTextarea').val() || '';
                    const lines = text.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
                    if (!lines.length) {
                        updateStatus('No URLs provided', 'error');
                        return;
                    }

                    const added = [];
                    lines.forEach(raw => {
                        let src = raw;
                        if (/^[a-zA-Z]:\\/.test(raw)) {
                            src = 'file:///' + raw.replace(/\\/g, '/');
                        } else if (/^\/[^\/]/.test(raw) && !/^https?:\/\//i.test(raw) && !/^file:\/\//i.test(raw)) {
                            src = 'file://' + raw;
                        }

                        let name = raw;
                        try {
                            const u = new URL(src);
                            name = decodeURIComponent(u.pathname.split('/').pop() || u.hostname || raw);
                        } catch (e) {
                            const parts = raw.split(/[\/\\]/);
                            name = parts[parts.length - 1] || raw;
                        }

                        added.push({ name: name, src: src });
                    });

                    // Remember if playlist was empty so we can auto-play the first added URL
                    const wasEmpty = playlist.length === 0;
                    playlist.push(...added);
                    renderPlaylist();
                    updateStatus(`${added.length} URL(s) added to playlist`);
                    setViewerBottom(8);

                    // If playlist was empty before, start playing the first newly added URL
                    if (wasEmpty && playlist.length) {
                        currentTrackIndex = playlist.indexOf(added[0]);
                        if (currentTrackIndex < 0) currentTrackIndex = 0;
                        playTrack(currentTrackIndex);
                    }

                    $('#addUrlModal').hide();
                });
            }
            $('#addUrlTextarea').val('');
            $('#addUrlModal').show();
        }

        $dropAreaFadeRange.off('input').on('input', function(e) {
            const v = parseInt(this.value, 10);
            const level = v / 100.0;
            applyDropAreaVisual(level);
        });

        // initialize to current default
        applyDropAreaVisual(dropAreaFadeLevel);

        // Tab switching logic (use classes so CSS controls appearance)
        function activatePlaylistTab() {
            $('#playlistContainer').show();
            $translationContainer.hide();
            $tabPlaylist.addClass('active').attr('aria-pressed', 'true');
            $tabTranslation.removeClass('active').attr('aria-pressed', 'false');
        }
        function activateTranslationTab() {
            $('#playlistContainer').hide();
            $translationContainer.show();
            $tabTranslation.addClass('active').attr('aria-pressed', 'true');
            $tabPlaylist.removeClass('active').attr('aria-pressed', 'false');
        }
        $tabPlaylist.on('click', activatePlaylistTab);
        $tabTranslation.on('click', activateTranslationTab);
        // default
        activatePlaylistTab();

        // SOON tab alert
        $('#tabSOON').on('click', function() {
            alert('This feature is coming soon!');
        });

        // Set up event listeners
        $dropArea.on('click', function() {
            $fileInput.trigger('click');
        });

        $fileInput.on('change', handleFileSelection);

        $closeBtn.on('click', function() {
            $viewerContainer.remove();
        });

        // Set up drag and drop in dropToPlaylist
        $('#dropToPlaylist').on('click', function() {
            $fileInput.trigger('click');
        });
        $('#playlistContainer').on('dragenter dragover dragleave drop', preventDefaults);
        $('#playlistContainer').on('dragenter dragover', highlightDropToPlaylist);
        $('#playlistContainer').on('dragleave drop', unhighlightDropToPlaylist);
        $('#playlistContainer').on('drop', handlePlaylistDrop);

        // Set up drag and drop
        $(document).on('dragenter dragover dragleave drop', preventDefaults);
        $(document).on('dragenter dragover', highlightDropArea);
        $(document).on('dragleave drop', unhighlightDropArea);
        $(document).on('drop', handleDroppedFiles);

        // Prevent default drag behaviors
        function preventDefaults(e) {
            e.preventDefault();
            e.stopPropagation();
        }

        // Highlight the drop area
        function highlightDropArea() {
            $dropArea.css({
                'border-color': '#272727',
                'border': '2px dashed rgb(22, 81, 144)',
                'background-color': '#333'
            });
        }
        function highlightDropToPlaylist() {
            $('#dropToPlaylist').css({
                'border-color': '#272727',
                'border': '2px dashed rgb(22, 81, 144)',
                'background-color': '#333'
            });
        }

        // Remove highlight from drop area
        function unhighlightDropArea() {
            $dropArea.css({
                'border-color': '#aaa',
                'background-color': '#181818'
            });
        }
        function unhighlightDropToPlaylist() {
            $('#dropToPlaylist').css({
                'border-color': '#aaa',
                'background-color': '#181818'
            });
        }

        // Handle files selected via file input
        function handleFileSelection(event) {
            const files = Array.from(event.target.files).filter(isMP3File);
            if (!files.length) return;
            playlist = files;
            currentTrackIndex = 0;
            playTrack(currentTrackIndex);

            // after adding a track move the viewer to bottom:8px
            setViewerBottom(8);
        }

        // Handle files dropped onto the page
        function handleDroppedFiles(e) {
            const dt = e.originalEvent.dataTransfer;
            const files = Array.from(dt.files).filter(isMP3File);
            if (files.length > 0) {
                playlist = files;
                currentTrackIndex = 0;
                playTrack(currentTrackIndex);

                // after adding a track move the viewer to bottom:8px
                setViewerBottom(8);
            }
        }
        function handlePlaylistDrop(e) {
            const dt = e.originalEvent.dataTransfer;
            const files = Array.from(dt.files).filter(isMP3File);
            if (files.length > 0) {
                playlist.push(...files);
                renderPlaylist();
                updateStatus(`${files.length} file(s) added to playlist`);

                // after adding a track move the viewer to bottom:8px
                setViewerBottom(8);
            }
        }

        // Check if a file is a supported audio file (keeps backward compatibility)
        function isAudioFile(file) {
            if (!file) return false;
            const name = (file.name || '').toLowerCase();
            const type = (file.type || '').toLowerCase();
            const extensions = ['.mp3', '.m4a', '.mp4', '.flac', '.wav', '.ogg', '.opus', '.aac', '.webm'];
            const extMatch = extensions.some(ext => name.endsWith(ext));
            const mimeMatch = /^audio\/.+/.test(type);
            return mimeMatch || extMatch;
        }

        // Keep old name for compatibility with existing calls
        function isMP3File(file) { return isAudioFile(file); }

        // Update status message
        function updateStatus(message, type = 'info') {
            $status.text(message).css('color', type === 'error' ? '#d9534f' : '#5bc0de');
        }

        // Extract metadata from MP3 file and optionally cache album art for playlist
        function extractMetadata(file, filename, onExtracted) {
            const reader = new FileReader();
            reader.onload = function(e) {
                try {
                    const tags = parseID3Tags(e.target.result);

                    // Always revoke previous album art URL before creating a new one
                    const prevUrl = playlistAlbumArts.get(file);
                    if (prevUrl) {
                        URL.revokeObjectURL(prevUrl);
                    }

                    if (tags.image && tags.image.data) {
                        const blob = new Blob([tags.image.data], { type: tags.image.format });
                        const url = URL.createObjectURL(blob);
                        playlistAlbumArts.set(file, url);
                    } else {
                        playlistAlbumArts.set(file, null);
                    }

                    // store tags for later (lyrics fetch / display)
                    playlistTags.set(file, tags || {});

                    if (typeof onExtracted === 'function') onExtracted();
                } catch (error) {
                    const prevUrl = playlistAlbumArts.get(file);
                    if (prevUrl) {
                        URL.revokeObjectURL(prevUrl);
                    }
                    playlistAlbumArts.set(file, null);
                    playlistTags.set(file, {});
                    if (typeof onExtracted === 'function') onExtracted();
                }
            };
            reader.onerror = function() {
                const prevUrl = playlistAlbumArts.get(file);
                if (prevUrl) {
                    URL.revokeObjectURL(prevUrl);
                }
                playlistAlbumArts.set(file, null);
                playlistTags.set(file, {});
                if (typeof onExtracted === 'function') onExtracted();
            };
            reader.readAsArrayBuffer(file);
        }

        // --- New: extract metadata for src-only playlist items (remote or file:/// URLs) ---
        async function fetchArrayBufferWithRange(url, rangeHeader) {
            // Try native fetch first (works for CORS-enabled HTTP(S) endpoints)
            try {
                const headers = {};
                if (rangeHeader) headers['Range'] = rangeHeader;
                const resp = await fetch(url, { method: 'GET', cache: 'no-cache', headers: headers });
                // Accept 200 (full) or 206 (partial content)
                if (resp && (resp.ok || resp.status === 206)) {
                    return await resp.arrayBuffer();
                }
            } catch (e) {
                // fetch may fail on file:// or due to CORS
                console.warn('fetch range failed, will try GM_xmlhttpRequest fallback for', url, e);
            }

            // Fallback: use GM_xmlhttpRequest when available (bypasses CORS/file:// issues in userscripts)
            if (typeof GM_xmlhttpRequest === 'function') {
                return new Promise((resolve) => {
                    try {
                        const tryRequest = (withRange) => {
                            const headers = {};
                            if (withRange && rangeHeader) headers['Range'] = rangeHeader;

                            GM_xmlhttpRequest({
                                method: 'GET',
                                url: url,
                                headers: headers,
                                responseType: 'arraybuffer',
                                timeout: 30000,
                                onload(res) {
                                    try {
                                        const status = res.status || 0;
                                        // 200 OK or 206 Partial Content -> good
                                        if ((status >= 200 && status < 300) || status === 206) {
                                            const arr = res.response;
                                            if (arr && arr.byteLength) {
                                                resolve(arr);
                                                return;
                                            }
                                        }
                                        // If server replied 405/416 for Range request, retry without Range once
                                        if (withRange && (status === 405 || status === 416 || status >= 400)) {
                                            // retry without Range header
                                            tryRequest(false);
                                            return;
                                        }
                                        resolve(null);
                                    } catch (err) {
                                        console.warn('GM onload parse error', err);
                                        resolve(null);
                                    }
                                },
                                onerror(err) {
                                    console.warn('GM_xmlhttpRequest error for', url, err);
                                    resolve(null);
                                },
                                ontimeout() {
                                    console.warn('GM_xmlhttpRequest timeout for', url);
                                    resolve(null);
                                }
                            });
                        };

                        // First attempt: if caller asked for range, try with range; otherwise do full GET
                        tryRequest(!!rangeHeader);
                    } catch (err) {
                        console.warn('GM_xmlhttpRequest thrown', err);
                        resolve(null);
                    }
                });
            }

            // No method succeeded
            return null;
        }

        // New helper: download entire file via GM_xmlhttpRequest and return a Blob (bypasses CORS)
        async function gmDownloadToBlob(url) {
            if (typeof GM_xmlhttpRequest !== 'function') return null;
            return new Promise((resolve) => {
                try {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: url,
                        responseType: 'arraybuffer',
                        timeout: 60000,
                        onload(res) {
                            try {
                                const status = res.status || 0;
                                if (status >= 200 && status < 300 && res.response) {
                                    const blob = new Blob([res.response]);
                                    resolve(blob);
                                    return;
                                }
                            } catch (e) { /* ignore parse error */ }
                            resolve(null);
                        },
                        onerror() { resolve(null); },
                        ontimeout() { resolve(null); }
                    });
                } catch (e) {
                    console.warn('gmDownloadToBlob error', e);
                    resolve(null);
                }
            });
        }

        // Parse ID3v1 tags from a 128-byte ArrayBuffer tail
        function parseID3v1(arrayBuffer) {
            try {
                const dv = new DataView(arrayBuffer);
                // Expect 'TAG' in first 3 bytes
                const tag = String.fromCharCode(dv.getUint8(0), dv.getUint8(1), dv.getUint8(2));
                if (tag !== 'TAG') return null;
                function readString(off, len) {
                    const bytes = new Uint8Array(arrayBuffer, off, len);
                    return new TextDecoder('iso-8859-1').decode(bytes).replace(/\0/g, '').trim();
                }
                return {
                    title: readString(3, 30),
                    artist: readString(33, 30),
                    album: readString(63, 30),
                    year: readString(93, 4),
                    comment: readString(97, 30)
                };
            } catch (e) { return null; }
        }

        // Try to extract metadata from a src URL (uses ranged fetches to limit download).
        async function extractMetadataFromSrc(item, onExtracted) {
            if (!item || !item.src) { if (typeof onExtracted === 'function') onExtracted(); return; }
            // avoid duplicate work
            const prev = playlistAlbumArts.get(item);
            if (prev !== undefined) { if (typeof onExtracted === 'function') onExtracted(); return; }
            // mark as pending
            playlistAlbumArts.set(item, null);
            playlistTags.set(item, {});

            try {
                // First 10 bytes to check for ID3v2 header
                const headBuf = await fetchArrayBufferWithRange(item.src, 'bytes=0-9');
                if (headBuf && headBuf.byteLength >= 10) {
                    const dvHead = new DataView(headBuf);
                    if (String.fromCharCode(dvHead.getUint8(0), dvHead.getUint8(1), dvHead.getUint8(2)) === 'ID3') {
                        // determine tag size (synchsafe at offset 6)
                        const tagSize = getID3TagSize(dvHead, 6);
                        const total = 10 + tagSize;
                        // fetch full tag block
                        const fullBuf = await fetchArrayBufferWithRange(item.src, `bytes=0-${Math.max(0, total - 1)}`);
                        if (fullBuf) {
                            const tags = parseID3Tags(fullBuf) || {};
                            // store tags
                            playlistTags.set(item, tags);
                            if (tags.image && tags.image.data) {
                                const blob = new Blob([tags.image.data], { type: tags.image.format });
                                const url = URL.createObjectURL(blob);
                                playlistAlbumArts.set(item, url);
                            } else {
                                playlistAlbumArts.set(item, null);
                            }
                            if (typeof onExtracted === 'function') onExtracted();
                            return;
                        }
                    }
                }

                // Try ID3v1 at file end (last 128 bytes)
                const tailBuf = await fetchArrayBufferWithRange(item.src, 'bytes=-128');
                if (tailBuf && tailBuf.byteLength >= 128) {
                    const id3v1 = parseID3v1(tailBuf);
                    if (id3v1) {
                        playlistTags.set(item, id3v1);
                        playlistAlbumArts.set(item, null);
                        if (typeof onExtracted === 'function') onExtracted();
                        return;
                    }
                }

                // No tags found
                playlistTags.set(item, {});
                playlistAlbumArts.set(item, null);
            } catch (err) {
                console.warn('extractMetadataFromSrc failed for', item.src, err);
                playlistTags.set(item, {});
                playlistAlbumArts.set(item, null);
            } finally {
                if (typeof onExtracted === 'function') onExtracted();
            }
        }

        // --- End new src metadata extraction helpers ---

        // Parse ID3 tags from array buffer
        function parseID3Tags(arrayBuffer) {
            const dataView = new DataView(arrayBuffer);
            let offset = 0;
            const tags = {};

            // Check for ID3 header
            if (!hasID3Header(dataView)) {
                return tags; // No ID3 tag found
            }

            // Get ID3 tag size
            const tagSize = getID3TagSize(dataView, 6);
            offset += 10; // Skip ID3 header

            // Look for frames within the ID3 tag
            while (offset < 10 + tagSize) {
                const frameHeader = getFrameHeader(dataView, offset);

                if (!frameHeader) break; // Padding or end of tags

                const frameSize = dataView.getUint32(offset + 4);
                offset += 10; // Skip frame header

                // Process different frame types
                processFrame(frameHeader, dataView, offset, frameSize, arrayBuffer, tags);

                offset += frameSize;
            }

            return tags;
        }

        // Check for ID3 header
        function hasID3Header(dataView) {
            return String.fromCharCode(dataView.getUint8(0),
                                     dataView.getUint8(1),
                                     dataView.getUint8(2)) === 'ID3';
        }

        // Get ID3 tag size
        function getID3TagSize(dataView, offset) {
            // ID3 tag size is stored as a 32-bit synchsafe integer
            let size = 0;
            for (let i = 0; i < 4; i++) {
                size = size * 128 + dataView.getUint8(offset + i);
            }
            return size;
        }

        // Get frame header
        function getFrameHeader(dataView, offset) {
            return String.fromCharCode(
                dataView.getUint8(offset),
                dataView.getUint8(offset + 1),
                dataView.getUint8(offset + 2),
                dataView.getUint8(offset + 3)
            );
        }

        // Process different frame types
        function processFrame(frameHeader, dataView, offset, size, arrayBuffer, tags) {
            switch (frameHeader) {
                case 'APIC':
                    tags.image = parseAlbumArtFrame(dataView, offset, size, arrayBuffer);
                    break;
                case 'TIT2':
                    tags.title = parseTextFrame(dataView, offset, size);
                    break;
                case 'TPE1':
                    tags.artist = parseTextFrame(dataView, offset, size);
                    break;
                case 'TALB':
                    tags.album = parseTextFrame(dataView, offset, size);
                    break;
                case 'TYER':
                    tags.year = parseTextFrame(dataView, offset, size);
                    break;
                case 'TPE2':
                    tags.albumArtist = parseTextFrame(dataView, offset, size);
                    break;
                case 'TRCK':
                    tags.trackNumber = parseTextFrame(dataView, offset, size);
                    break;
            }
        }

        // Parse album art frame
        function parseAlbumArtFrame(dataView, offset, size, arrayBuffer) {
            let pos = offset;

            // Read text encoding (1 byte)
            const encoding = dataView.getUint8(pos);
            pos += 1;

            // Extract MIME type (null-terminated string, always ISO-8859-1)
            const mimeType = extractNullTerminatedString(dataView, pos, arrayBuffer);
            pos += mimeType.length + 1;

            // Skip picture type (1 byte)
            pos += 1;

            // Extract description (encoding-dependent null-terminated string)
            let descEnd = pos;
            if (encoding === 0 || encoding === 3) {
                // ISO-8859-1 or UTF-8: null-terminated with 0x00
                while (descEnd < offset + size && dataView.getUint8(descEnd) !== 0) {
                    descEnd++;
                }
                descEnd++; // Skip null terminator
            } else if (encoding === 1 || encoding === 2) {
                // UTF-16 or UTF-16BE: null-terminated with 0x00 0x00
                while (descEnd + 1 < offset + size) {
                    if (dataView.getUint8(descEnd) === 0 && dataView.getUint8(descEnd + 1) === 0) {
                        descEnd += 2;
                        break;
                    }
                    descEnd += 2;
                }
            } else {
                // Fallback: treat as ISO-8859-1
                while (descEnd < offset + size && dataView.getUint8(descEnd) !== 0) {
                    descEnd++;
                }
                descEnd++;
            }
            pos = descEnd;

            // Extract image data
            const imageData = new Uint8Array(arrayBuffer, pos, offset + size - pos);

            // Determine image format
            let format = 'image/jpeg'; // default
            if (mimeType.includes('png')) format = 'image/png';
            if (mimeType.includes('gif')) format = 'image/gif';

            return {
                data: imageData,
                format: format
            };
        }

        // Extract null-terminated string
        function extractNullTerminatedString(dataView, offset, arrayBuffer) {
            let end = offset;
            while (dataView.getUint8(end) !== 0 && end < dataView.byteLength) {
                end++;
            }
            return String.fromCharCode.apply(null,
                new Uint8Array(arrayBuffer, offset, end - offset));
        }

        // Parse text frame
        function parseTextFrame(dataView, offset, size) {
            if (size < 1) return '';

            const encoding = dataView.getUint8(offset); // First byte indicates encoding
            const textBytes = new Uint8Array(dataView.buffer, offset + 1, size - 1);

            let decoder;

            switch (encoding) {
                case 0: // ISO-8859-1
                    decoder = new TextDecoder('iso-8859-1');
                    break;
                case 1: // UTF-16 with BOM
                    decoder = new TextDecoder('utf-16');
                    break;
                case 2: // UTF-16BE without BOM
                    decoder = new TextDecoder('utf-16be');
                    break;
                case 3: // UTF-8
                    decoder = new TextDecoder('utf-8');
                    break;
                default:
                    decoder = new TextDecoder('utf-8'); // fallback
            }

            return decoder.decode(textBytes).replace(/\0/g, ''); // remove nulls
        }

        // Display metadata
        function displayMetadata(tags, filename) {
            // Extract filename without extension
            const fileNameWithoutExt = filename.replace(/\.mp3$/i, '');
            // store tags for current track (in case displayMetadata is called directly)
            const currentFile = playlist[currentTrackIndex];
            if (currentFile) playlistTags.set(currentFile, tags || {});

            // Ensure CSS for blurred/dark cover-background exists (only once)
            if (!$('#metadataBgStyles').length) {
                $('head').append(`
                    <style id="metadataBgStyles">
                        /* metadata container: host for blurred background via pseudo-element */
                        #metadata { position: relative; z-index: 0; overflow: clip; }
                        /* blurred background (uses CSS variable --metadata-bg set dynamically) */
                        #metadata::before {
                            content: '';
                            position: absolute;
                            inset: 0;
                            background-image: var(--metadata-bg, none);
                            background-size: cover;
                            background-position: center;
                            transform: scale(1.06);
                            filter: blur(14px) brightness(1) contrast(130%) saturate(140%);
                            transition: opacity .3s ease, filter .3s ease;
                            z-index: -2;
                            opacity: 1;
                        }
                        /* dark overlay to keep foreground readable */
                        #metadata::after {
                            content: '';
                            position: absolute;
                            inset: 0;
                            background: linear-gradient(rgba(0, 0, 0, 0.33), rgba(0, 0, 0, 0.33));
                            z-index: -1;
                            pointer-events: none;
                        }
                        /* keep actual artwork and text above pseudo-elements */
                        #metadata > img, #metadata > #trackInfo { position: relative; z-index: 1; }
                    </style>
                `);
            }

            const $metadata = $('#metadata');

            // Clean up any previously created object URL used for the metadata background
            const prevBg = $metadata.data('bgUrl');
            if (prevBg) {
                try { URL.revokeObjectURL(prevBg); } catch (e) { /* ignore */ }
                $metadata.removeData('bgUrl');
            }

            // Display album art if available and set blurred background
            if (tags.image && tags.image.data) {
                const blob = new Blob([tags.image.data], { type: tags.image.format });
                const url = URL.createObjectURL(blob);

                // album cover <img>
                $albumArt.off('load error')
                    .on('load', function() { $(this).show(); })
                    .on('error', function() { $(this).hide(); })
                    .attr('src', url);

                // set CSS custom property used by ::before for blurred/dark background
                // wrap url in quotes to be safe with paths
                $metadata.css('--metadata-bg', `url("${url}")`);
                // remember URL so we can revoke it later
                $metadata.data('bgUrl', url);
            } else {
                $albumArt.hide();
                $metadata.css('--metadata-bg', 'none');
            }

            // Use the Media Session API for metadata
            let artworkUrl = '';
            let artworkFormat = '';

            if (tags.image && tags.image.data) {
                const blob = new Blob([tags.image.data], { type: tags.image.format });
                artworkUrl = URL.createObjectURL(blob);
                artworkFormat = tags.image.format;
            }

            navigator.mediaSession.metadata = new MediaMetadata({
                title: tags.title || fileNameWithoutExt,
                artist: tags.artist,
                album: tags.album,
                artwork: [
                                 {
                                     //src: artworkUrl, // NOT WORKING
                                     src: 'https://raw.githubusercontent.com/aliashori78/MP3-Player-Pro/9ecdc18985504a6a3889234b61739a79ef14fe38/Assets/Album1.jpg' || artworkUrl,
                                     //    https://raw.githubusercontent.com/aliashori78/MP3-Player-Pro/9ecdc18985504a6a3889234b61739a79ef14fe38/Assets/Album2.jpg
                                     sizes: 'any',
                                     type: artworkFormat
                                 }
                             ]
            });
            //console.log('MY DEBUG: ' + tags.image.data);
            //console.log('MediaSession.metadata =', navigator.mediaSession.metadata);

            // Display other metadata
            let infoHTML = '';
            infoHTML += `<div><strong>Filename:</strong> ${fileNameWithoutExt}</div>`;
            if (tags.title) infoHTML += `<div><strong>Title:</strong> ${tags.title}</div>`;
            if (tags.artist) infoHTML += `<div><strong>Artist:</strong> ${tags.artist}</div>`;
            if (tags.album) infoHTML += `<div><strong>Album:</strong> ${tags.album}</div>`;
            if (tags.albumArtist) infoHTML += `<div><strong>Album Artist:</strong> ${tags.albumArtist}</div>`;
            if (tags.year) infoHTML += `<div><strong>Year:</strong> ${tags.year}</div>`;
            if (tags.trackNumber) infoHTML += `<div><strong>Track:</strong> ${tags.trackNumber}</div>`;

            if (tags.title && tags.artist) infoHTML += `<div><strong>Full Name:</strong> ${tags.artist} - ${tags.title}</div>`;

            $trackInfo.html(infoHTML || '<div>No metadata available</div>');
        }

        // --- NEW: visualizer (bottom equalizer) inside #metadata ---
        if (!$('#visualizerCanvas').length) {
            $('#metadata').append('<canvas id="visualizerCanvas" style="position:absolute;width:100%;height:80px;display:block;margin-top:8px;bottom:0;left:0;z-index:99;"></canvas>');
            $('head').append('<style id="visualizerStyles">#metadata{position:relative}#visualizerCanvas{background:transparent;display:block}</style>');
        }

        // Visualizer configuration (exposed to settings)
        let visualizerEnabled = true;
        let colorEnabled = false;
        let visualizerStyle = 'bars'; // 'bars' | 'waveform' | 'radial'
        let visualizerColor = '#66ccff';
        let visualizerHeight = 40;
        let visualizerSensitivity = 1.0;
        // number of bars to draw when style === 'bars' (user configurable)
        let visualizerBarCount = 80;

        let audioCtx, analyser, sourceNode, dataArray, bufferLength, rafId;
        const canvas = document.getElementById('visualizerCanvas');
        const canvasCtx = canvas ? canvas.getContext('2d') : null;

        function ensureCanvasSize() {
            if (!canvas) return;
            const dpr = window.devicePixelRatio || 1;
            const w = canvas.clientWidth || canvas.width || document.getElementById('metadata').clientWidth;
            const h = Math.max(10, visualizerHeight);
            canvas.style.height = h + 'px';
            canvas.width = Math.floor(w * dpr);
            canvas.height = Math.floor(h * dpr);
            canvasCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
        }

        // Convert hex to rgba with alpha
        function hexToRGBA(hex, alpha = 1) {
            if (!hex) return `rgba(102,204,255,${alpha})`;
            const m = hex.replace('#','');
            const bigint = parseInt(m.length === 3 ? m.split('').map(c => c+c).join('') : m, 16);
            const r = (bigint >> 16) & 255;
            const g = (bigint >> 8) & 255;
            const b = bigint & 255;
            return `rgba(${r},${g},${b},${alpha})`;
        }

        // Return a color string based on intensity (0..1). Respects colorEnabled.
       function getColorForIntensity(intensity, alpha = 1) {
           intensity = Math.max(0, Math.min(1, intensity));
           if (colorEnabled) return hexToRGBA(visualizerColor, alpha);

           // map intensity to a pleasant HSL range (blue -> cyan -> yellow -> red)
           const hue = 220 - Math.round(intensity * 200); // ~220 -> 20
           const lightness = 30 + Math.round(intensity * 40); // 30% -> 70%
           return `hsla(${hue}, 85%, ${lightness}%, ${alpha})`;
       }

        function setupAnalyzer() {
            if (!visualizerEnabled) return;
            if (!canvasCtx || (!window.AudioContext && !window.webkitAudioContext)) return;
            if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
            if (!analyser) {
                analyser = audioCtx.createAnalyser();
                analyser.fftSize = 1024; // better resolution for waveform and radial
                bufferLength = analyser.frequencyBinCount;
                dataArray = new Uint8Array(bufferLength);
            }

            // Create source node once (creating multiple MediaElementSource throws)
            if (!sourceNode && $audioPlayer && $audioPlayer[0]) {
                try {
                    sourceNode = audioCtx.createMediaElementSource($audioPlayer[0]);
                    sourceNode.connect(analyser);
                    analyser.connect(audioCtx.destination);
                } catch (e) {
                    // ignore if already connected / not allowed
                    console.warn('Visualizer: createMediaElementSource failed', e);
                }
            }

            ensureCanvasSize();
            startDrawing();
        }

        function clearCanvas() {
            if (!canvasCtx) return;
            canvasCtx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
        }

        function drawBars(freqArray) {
            const w = canvas.clientWidth;
            const h = canvas.clientHeight;
            canvasCtx.clearRect(0, 0, w, h);

            // Determine desired bars:
            // use configured visualizerBarCount when provided, otherwise derive from width.
            const desiredBars = Number.isFinite(visualizerBarCount) ? Math.max(4, Math.min(128, visualizerBarCount)) : Math.min(64, Math.max(8, Math.floor(w / 8)));
            // Do not exceed the number of frequency bins
            const bars = Math.max(4, Math.min(desiredBars, freqArray.length));
            const step = Math.max(1, Math.floor(freqArray.length / bars));
            const barWidth = Math.max(2, (w / bars) * 0.9);
            let x = 0;
            for (let i = 0; i < bars; i++) {
                const idx = i * step;
                // average a small range to avoid noisy single-bin zeros at high freq
                let sum = 0, count = 0;
                for (let j = idx; j < Math.min(freqArray.length, idx + step); j++) {
                    sum += (freqArray[j] || 0);
                    count++;
                }
                const avg = count ? (sum / count) : 0;
                // apply sensitivity and clamp
                const v = Math.min(1, (avg / 255) * visualizerSensitivity);
                const barHeight = v * h;

                // gradient using dynamic color when enabled
                const grad = canvasCtx.createLinearGradient(0, h, 0, h - barHeight);
                grad.addColorStop(0, getColorForIntensity(v, 0.18));
                grad.addColorStop(0.4, getColorForIntensity(v, 0.45));
                grad.addColorStop(1, getColorForIntensity(v, 0.98));
                canvasCtx.fillStyle = grad;
                canvasCtx.fillRect(x, h - barHeight, barWidth, barHeight);
                x += barWidth + 2;
            }
        }

        function drawWaveform(timeArray) {
            const w = canvas.clientWidth;
            const h = canvas.clientHeight;
            canvasCtx.clearRect(0, 0, w, h);

            canvasCtx.lineWidth = 2;
            canvasCtx.strokeStyle = hexToRGBA(visualizerColor, 0.95);
            canvasCtx.beginPath();

            const sliceWidth = w / timeArray.length;
            let x = 0;
            for (let i = 0; i < timeArray.length; i++) {
                // apply sensitivity (and clamp)
                const raw = timeArray[i] / 128.0; // ~0..2
                const v = Math.min(2, raw * visualizerSensitivity);
                const y = (v * h) / 2;
                if (i === 0) {
                    canvasCtx.moveTo(x, y);
                } else {
                    canvasCtx.lineTo(x, y);
                }
                x += sliceWidth;
            }
            canvasCtx.lineTo(w, h / 2);
            canvasCtx.stroke();
        }

        function drawRadial(freqArray) {
            const w = canvas.clientWidth;
            const h = canvas.clientHeight;
            canvasCtx.clearRect(0, 0, w, h);

            const cx = w / 2;
            const cy = h / 2;
            const radius = Math.min(cx, cy) * 0.35;
            const bars = Math.min(64, Math.max(12, Math.floor(Math.sqrt(freqArray.length) * 2)));
            const step = Math.max(1, Math.floor(freqArray.length / bars));

            for (let i = 0; i < bars; i++) {
                const idx = i * step;
                // average a small range
                let sum = 0, count = 0;
                for (let j = idx; j < Math.min(freqArray.length, idx + step); j++) { sum += (freqArray[j] || 0); count++; }
                const avg = count ? (sum / count) : 0;
                const v = Math.min(1, (avg / 255) * visualizerSensitivity);
                const barLen = v * Math.min(w, h) * 0.45;
                const angle = (i / bars) * Math.PI * 2;
                const x1 = cx + Math.cos(angle) * radius;
                const y1 = cy + Math.sin(angle) * radius;
                const x2 = cx + Math.cos(angle) * (radius + barLen);
                const y2 = cy + Math.sin(angle) * (radius + barLen);

                canvasCtx.strokeStyle = getColorForIntensity(v, 0.95);
                canvasCtx.lineWidth = Math.max(2, Math.round((barLen / Math.max(w,h)) * 6));
                canvasCtx.beginPath();
                canvasCtx.moveTo(x1, y1);
                canvasCtx.lineTo(x2, y2);
                canvasCtx.stroke();
            }
        }

        function startDrawing() {
            if (!canvas || !canvasCtx || !analyser) return;
            cancelAnimationFrame(rafId);

            ensureCanvasSize();

            function draw() {
                rafId = requestAnimationFrame(draw);

                if (!visualizerEnabled) {
                    // if disabled, just clear and stop
                    clearCanvas();
                    cancelAnimationFrame(rafId);
                    rafId = null;
                    return;
                }

                if (visualizerStyle === 'waveform') {
                    const timeData = new Uint8Array(analyser.fftSize);
                    analyser.getByteTimeDomainData(timeData);
                    drawWaveform(timeData);
                } else {
                    // default to bars/radial use frequency data
                    const freqData = new Uint8Array(analyser.frequencyBinCount);
                    analyser.getByteFrequencyData(freqData);
                    if (visualizerStyle === 'radial') {
                        drawRadial(freqData);
                    } else {
                        drawBars(freqData);
                    }
                }
            }
            draw();
        }

        function renderSilence() {
            if (!canvas || !canvasCtx) return;
            ensureCanvasSize();
            // draw minimal baseline state (empty bars or flat waveform)
            if (visualizerStyle === 'waveform') {
                // flat line
                canvasCtx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
                canvasCtx.lineWidth = 2;
                canvasCtx.strokeStyle = hexToRGBA(visualizerColor, 0.12);
                canvasCtx.beginPath();
                canvasCtx.moveTo(0, canvas.clientHeight / 2);
                canvasCtx.lineTo(canvas.clientWidth, canvas.clientHeight / 2);
                canvasCtx.stroke();
            } else {
                // small faded bars at height 0
                const w = canvas.clientWidth;
                const h = canvas.clientHeight;
                canvasCtx.clearRect(0, 0, w, h);
                const bars = 16;
                const barWidth = Math.max(2, (w / bars) * 0.9);
                let x = 0;
                for (let i = 0; i < bars; i++) {
                    const grad = canvasCtx.createLinearGradient(0, h, 0, h - 1);
                    grad.addColorStop(0, hexToRGBA(visualizerColor, 0.05));
                    grad.addColorStop(1, hexToRGBA(visualizerColor, 0.08));
                    canvasCtx.fillStyle = grad;
                    canvasCtx.fillRect(x, h - 2, barWidth, 2);
                    x += barWidth + 2;
                }
            }
        }

        // Ensure context is resumed on user gesture / play and analyzer is set up
        function ensureAudioContext() {
            try { if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume(); } catch (e) {}
            if (visualizerEnabled) setupAnalyzer();
        }

        $audioPlayer.on('play', function() {
            ensureAudioContext();
            // restart drawing if previously stopped
            if (visualizerEnabled && analyser && !rafId) startDrawing();
        });

        // When player is paused or ended, render silence (0)
        $audioPlayer.on('pause ended', function() {
            if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
            renderSilence();
            $('#playBtn').text('▶️ Play');
        });

        $viewerContainer.on('click', ensureAudioContext);
        $(window).on('resize', function() { if (analyser) startDrawing(); });

        // Optional: allow toggling visualizer programmatically
        function applyVisualizerSettings() {
            ensureCanvasSize();
            if (visualizerEnabled) {
                setupAnalyzer();
            } else {
                if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
                clearCanvas();
            }
        }
        // Initialize default visualizer state
        applyVisualizerSettings();

        /*
        *
        * Add lyrics handling and lrclib fetching (START)
        *
        */

        // Parse simple LRC text into [{time, text}]
        function parseLRC(lrcText) {
            const lines = lrcText.split(/\r?\n/);
            const entries = [];
            const timeRegex = /\[(\d+):(\d{2})(?:\.(\d{1,3}))?\]/g;

            for (const line of lines) {
                // Reset regex lastIndex
                timeRegex.lastIndex = 0;
                const texts = line.replace(/\[.*?\]/g, '').trim();

                let match;
                while ((match = timeRegex.exec(line)) !== null) {
                    const min = parseInt(match[1], 10);
                    const sec = parseInt(match[2], 10);
                    const ms = match[3] ? parseInt(match[3].padEnd(3, '0'), 10) : 0;
                    const t = min * 60 + sec + ms / 1000;
                    entries.push({ time: t, text: texts });
                }
            }

            // Sort by time
            entries.sort((a, b) => a.time - b.time);
            return entries;
        }

        // Fetch search results from lrclib.net
        async function searchLyricsOnLrclib(query) {
            const encodedQuery = encodeURIComponent(query);
            const url = `https://lrclib.net/api/search?q=${encodedQuery}`;

            try {
                const resp = await fetch(url, {
                    cache: 'no-cache',
                    headers: {
                        'Accept': 'application/json'
                    }
                });

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

                const contentType = resp.headers.get('content-type') || '';
                if (!contentType.includes('application/json')) {
                    throw new Error('Response is not JSON');
                }

                const data = await resp.json();

                // Handle different response structures
                if (Array.isArray(data)) {
                    return data;
                } else if (data && data.data && Array.isArray(data.data)) {
                    return data.data;
                } else if (data && Array.isArray(data.results)) {
                    return data.results;
                } else {
                    console.log('Unexpected API response:', data);
                    return [];
                }
            } catch (e) {
                console.error('Search error:', e);
                $('#lyricsStatus').text('Search failed: ' + e.message);
                return [];
            }
        }

        // Fetch specific lyrics by ID or from URL
        async function fetchLyricsById(item) {
            try {
                let lrcUrl = null;

                // Check different possible URL fields
                if (item.syncedLyrics) {
                    return item.syncedLyrics;
                }

                // If no direct syncedLyrics, try to fetch from API endpoint
                if (item.id) {
                    const detailsUrl = `https://lrclib.net/api/get/${item.id}`;
                    const detailsResp = await fetch(detailsUrl);
                    if (detailsResp.ok) {
                        const details = await detailsResp.json();
                        if (details.syncedLyrics) {
                            return details.syncedLyrics;
                        }
                    }
                }

                return null;
            } catch (e) {
                console.error('Fetch error:', e);
                return null;
            }
        }

        // Display search results in a modal
        function showSearchResults(results) {
            if (!results || results.length === 0) {
                $('#lyricsStatus').text('No results found');
                return;
            }

            // Create or update results modal
            let $modal = $('#lyricsSearchModal');
            if ($modal.length === 0) {
                $modal = $(`
                    <div id="lyricsSearchModal" class="modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); z-index:999999;">
                        <div style="position:absolute; top:50%; left:50%; transform:translate(-50%, -50%); background:#2a2a2a; padding:20px; border-radius:8px; max-width:500px; width:90%; max-height:80vh; overflow-y:auto;">
                            <h3 style="margin-top:0; color:#fff;">Select Lyrics</h3>
                            <button id="closeLyricsModal" style="width:100%; text-align:center; padding:8px 15px; background:#666; color:#fff; border:none; border-radius:4px; cursor:pointer;">Close</button>
                            <div id="lyricsResultsList"></div>
                        </div>
                    </div>
                `);
                $('body').append($modal);

                $('#closeLyricsModal').on('click', function() {
                    $('#lyricsSearchModal').hide();
                });
            }

            const $list = $('#lyricsResultsList').empty();

            results.forEach((item, index) => {
                const hasSynced = item.syncedLyrics ? '<span style="color: green;">✓ Synced</span>' : '<span style="color: red;">✗ No sync</span>';
                const duration = item.duration ? `${Math.floor(item.duration / 60)}:${Math.floor(item.duration % 60).toString().padStart(2, '0')}` : 'Unknown';

                const $result = $(`
                    <div class="lyrics-result" style="padding:10px; margin:5px 0; background:#3a3a3a; border-radius:4px; cursor:pointer;">
                        <div style="font-weight:bold; color:#fff;">${item.trackName || item.name}</div>
                        <div style="color:#ccc; font-size:0.9em;">${item.artistName}</div>
                        <div style="color:#aaa; font-size:0.8em;">
                            ${item.albumName ? `Album: ${item.albumName} • ` : ''}
                            Duration: ${duration} • ${hasSynced}
                        </div>
                    </div>
                `);

                $result.on('click', async function() {
                    $('#lyricsStatus').text(`Loading "${item.trackName}"...`);
                    $('#lyricsSearchModal').hide();

                    const lrcText = await fetchLyricsById(item);
                    if (lrcText) {
                        const entries = parseLRC(lrcText);
                        currentLrcEntries = entries;
                        currentLrcIndex = -1;
                        renderLyrics(entries);
                        $('#lyricsStatus').text(`Loaded: ${item.trackName} - ${item.artistName}`);
                    } else {
                        $('#lyricsStatus').text('Could not load lyrics for this selection');
                    }
                });

                $list.append($result);
            });

            $modal.show();
        }

        // Render lyrics in the container
        // Render lyrics in the container
        function renderLyrics(entries) {
            const $inner = $('#lyricsInner').empty();
            if (!entries || !entries.length) {
                $inner.html('<div style="color:#888; padding:20px; text-align:center;">No lyrics to display</div>');
                $('#lyricsStatus').text('No lyrics loaded');
                return;
            }

            // small helper to safely create text nodes
            function textNode(str) {
                return document.createTextNode(str == null ? '' : String(str));
            }

            entries.forEach((e, i) => {
                const $line = $('<div/>', {
                    class: 'lrc-line',
                    'data-idx': i,
                    'data-time': e.time,
                    css: {
                        padding: '4px 6px',
                        margin: '8px 0',
                        lineheight: '1.2',
                        background: '#151515',
                        color: '#bbb',
                        transition: 'color .18s, font-weight .18s',
                        fontSize: '22px',
                        textAlign: 'center',
                        fontFamily: 'Dana',
                        cursor: 'pointer' // make it obvious it's clickable
                    }
                });

                const $orig = $('<div/>', { class: 'lrc-original' }).append(textNode(e.text || '—'));
                const $trans = $('<div/>', {
                    class: 'lrc-translation',
                    'data-for': i,
                    css: {
                        color: '#cfcfcf',
                        fontSize: '18px',
                        textAlign: 'center',
                        display: 'none' // hidden until requested
                    }
                });

                // click -> seek audio to this line's time and play (also sync highlight)
                $line.on('click', function(evt) {
                    // prefer reading time from data attribute (string) and fallback to entry
                    const tAttr = $(this).attr('data-time');
                    const t = Number.isFinite(Number(tAttr)) ? parseFloat(tAttr) : (typeof e.time === 'number' ? e.time : NaN);
                    if (!Number.isFinite(t)) return;
                    try {
                        const audioEl = ($audioPlayer && $audioPlayer[0]) ? $audioPlayer[0] : document.getElementById('audioPlayer');
                        // small offset guard to avoid being exactly at end
                        const seekTo = Math.max(0, Math.min(t, (isFinite(audioEl.duration) ? audioEl.duration : t)));
                        audioEl.currentTime = seekTo;
                        // start playback if paused
                        if (audioEl.paused) audioEl.play().catch(()=>{ /* ignore play errors */ });
                        // update highlight immediately
                        syncLyricsToTime(seekTo);
                        //$('#lyricsStatus').text(`Seeking to ${formatTime(seekTo)}`); // Seeking to 0:17 for the click in line
                    } catch (err) {
                        console.error('Seek error:', err);
                    }
                });

                $line.append($orig, $trans);
                $inner.append($line);
            });
            $('#lyricsStatus').text(`Lyrics loaded (${entries.length} lines)`);
        }

        // small helper to display time nicely (mm:ss)
        function formatTime(sec) {
            if (!Number.isFinite(sec)) return '0:00';
            const m = Math.floor(sec / 60);
            const s = Math.floor(sec % 60).toString().padStart(2, '0');
            return `${m}:${s}`;
        }

        // Clear/disable lyrics button handler
        // Replaced inline logic with the shared clearLyricsAction() above
        $('#clearLyricsBtn').off('click').on('click', function() {
            clearLyricsAction();
        });

        // Optional: Add toggle functionality to clear button (double duty)
        // keep existing toggle behavior for dblclick
        $('#clearLyricsBtn').off('dblclick').on('dblclick', function() {
            toggleLyricsSync();
        });

        // Add a function to disable lyrics sync (optional toggle feature)
        function toggleLyricsSync() {
            if ($audioPlayer.data('lyricsEnabled') !== false) {
                // Disable sync
                $audioPlayer.data('lyricsEnabled', false);
                $('#clearLyricsBtn').text('🗑️ Enable Sync');
                $('#lyricsStatus').text('Lyrics sync disabled');
            } else {
                // Enable sync
                $audioPlayer.data('lyricsEnabled', true);
                $('#clearLyricsBtn').text('🗑️');
                $('#lyricsStatus').text('Lyrics sync enabled');
            }
        }

        // Sync highlight on audio timeupdate
        let currentLrcEntries = [];
        let currentLrcIndex = -1;

        function syncLyricsToTime(time) {
            // Check if sync is disabled
            if ($audioPlayer.data('lyricsEnabled') === false) return;

            if (!currentLrcEntries.length) return;

            // Binary search for current line
            let lo = 0, hi = currentLrcEntries.length - 1, best = -1;
            while (lo <= hi) {
                const mid = (lo + hi) >> 1;
                if (currentLrcEntries[mid].time <= time) {
                    best = mid;
                    lo = mid + 1;
                } else {
                    hi = mid - 1;
                }
            }

            if (best !== currentLrcIndex) {
                currentLrcIndex = best;
                $('#lyricsInner .lrc-line').css({ color: '#888', 'font-weight': 'normal', 'background-color': 'transparent' });
                if (best >= 0) {
                    const $line = $(`#lyricsInner .lrc-line[data-idx="${best}"]`);
                    $line.css({ color: '#fff', 'font-weight': '700', 'background-color': '#ffffff17' });

                    // scrollIntoView
                    const lineEl = $line[0];
                    if (lineEl) {
                        // Smooth scroll to position
                        setTimeout(() => {
                            lineEl.scrollIntoView({
                                behavior: 'smooth',
                                block: 'center',
                                inline: 'nearest'
                            });
                        }, 10);
                    }
                }
            }
        }

        // Auto-search based on current track metadata
        $('#fetchLyricsBtn').off('click').on('click', async function() {
            const file = playlist[currentTrackIndex];
            let tags = file ? playlistTags.get(file) : null;
            if (!tags) tags = {};

            const artist = tags.artist || tags.albumArtist || '';
            const title = tags.title || (file && file.name ? file.name.replace(/\.mp3$/i, '') : '');

            if (!title) {
                $('#lyricsStatus').text('No title available for search');
                return;
            }

            $('#lyricsStatus').text('Searching...');

            const query = (artist ? artist + ' ' : '') + title;
            const results = await searchLyricsOnLrclib(query);

            if (results.length > 0) {
                showSearchResults(results);
                $('#lyricsStatus').text(`Found ${results.length} results`);
            } else {
                $('#lyricsStatus').text('No lyrics found. Try manual search.');
            }
        });

        // Manual search button handler
        $('#manualLyricsSearchBtn').off('click').on('click', async function() {
            const query = $('#manualLyricsInput').val().trim();
            if (!query) {
                $('#lyricsStatus').text('Please enter search text');
                return;
            }

            $('#lyricsStatus').text(`Searching for "${query}"...`);

            const results = await searchLyricsOnLrclib(query);

            if (results.length > 0) {
                showSearchResults(results);
                $('#lyricsStatus').text(`Found ${results.length} results for "${query}"`);
            } else {
                $('#lyricsStatus').text(`No results found for "${query}"`);
            }
        });

        // Add Enter key support for manual search input
        $(document).on('keypress', '#manualLyricsInput', function(e) {
            if (e.which === 13) { // Enter key
                $('#manualLyricsSearchBtn').click();
            }
        });

        // Attach to audio timeupdate for live sync
        $audioPlayer.on('timeupdate', function() {
            syncLyricsToTime(this.currentTime);
        });

        // Add manual search button to HTML (if not exists)
        /*if (!$('#manualLyricsSearchBtn').length) {
            $('#fetchLyricsBtn').after(
                '<button id="manualLyricsSearchBtn" style="margin-left: 5px; padding: 5px 10px; background: #555; color: #fff; border: none; border-radius: 3px; cursor: pointer;">Manual Search</button>'
            );
        }*/

        // Add styles for modal and results
        if (!$('#lyricsStyles').length) {
            $('head').append(`
                <style id="lyricsStyles">
                    .lyrics-result:hover {
                        background: #4a4a4a !important;
                    }
                    .modal {
                        display: none;
                    }
                    #lyricsContainer {
                        overflow-y: auto;
                        padding: 10px;
                        background: rgba(0,0,0,0.2);
                        border-radius: 4px;
                    }
                    #lyricsStatus {
                        display: inline-block;
                        width: 280px;
                        white-space: nowrap;
                        overflow: hidden;
                        text-overflow: ellipsis;
                        margin-bottom: -5px;
                    }
                </style>
            `);
        }

        // Optional: Add toggle functionality to clear button (double duty)
        $('#clearLyricsBtn').on('dblclick', function() {
            toggleLyricsSync();
        });


        // --- Translation feature: translate each lyric line to Persian or ... on demand ---
        // User-configurable defaults
        const TRANSLATION_DELIM = '\n<<<LINE>>>\n'; // unique delimiter to preserve line boundaries

        // Constants for OpenRouter (Get this from https://hf.co/settings/tokens)
        let hfToken = 'YOUR_HF_API_KEY_HERE';
        let hfModel = 'meta-llama/Llama-3.3-70B-Instruct'; // Any other model from https://huggingface.co/docs/inference-providers/en/index

        // Constants for OpenRouter (any other model from https://openrouter.ai/settings/keys)
        let OPENROUTER_API_KEY = 'YOUR_OPENROUTER_API_KEY_HERE';
        let OPENROUTER_MODEL = 'deepseek/deepseek-r1-0528:free'; // Any other model from https://openrouter.ai/models

        // User-configurable Google translate target language (ISO code, e.g. "fa", "en")
        let googleTargetLang = 'fa';

        // Default kept as "Persian (Farsi)" for backward compatibility
        let aiPromptLanguage = 'Persian (Farsi)';

        // Default instruction text (editable) built from aiPromptLanguage
        let translationInstructionDefault =
            `Translate the following lyrics into fluent, natural ${aiPromptLanguage}. Preserve meaning and punctuation. Return the translation with each original line corresponding to the same number of lines, and separate lines using the exact token <<<LINE>>>.`;
        // Cache for translations for current lyrics set
        let currentTranslations = []; // array of strings parallel to currentLrcEntries
        let translationsShown = false;

        // New automatic options (in-memory only)
        let autoLyrics = false;         // fetch lyrics automatically when track changes
        let autoTranslation = false;    // translate automatically after lyrics fetched
        let autoClearLyrics = false;    // clear previous lyrics automatically when track changes

        // Translation provider setting (moved out of prompt, configurable in settings UI) - allowed values: 'google', 'hf', 'openrouter'
        let translationProviderSetting = 'google';

        // Centralized clear action (used by button and auto-clear)
        function clearLyricsAction() {
            currentLrcEntries = [];
            currentLrcIndex = -1;
            $('#lyricsInner').empty().html('<div style="color:#888; padding:20px; text-align:center;">Lyrics cleared</div>');
            $('#lyricsStatus').text('Lyrics cleared');
            $('#lyricsSearchModal').hide();
        }

        // Translate whole payload using Google Translate (single request)
        async function translateWholeWithGoogle(text) {
            if (!text) return '';
            // use googleTargetLang (ISO) from settings
            const base = 'https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=' + encodeURIComponent(googleTargetLang) + '&dt=t&q=';
            const url = base + encodeURIComponent(text);
            try {
                const resp = await fetch(url, { cache: 'no-cache' });
                if (!resp.ok) return '';
                const data = await resp.json();
                // Google returns nested arrays of segments
                if (Array.isArray(data) && Array.isArray(data[0])) {
                    return data[0].map(seg => seg[0] || '').join('');
                }
                return '';
            } catch (e) {
                console.error('Google translate error', e);
                return '';
            }
        }

        // Translate whole payload using Hugging Face AI (Generative Language API). Requires API key.
        async function translateWholeWithHF(text, instruction = translationInstructionDefault) {
            if (!text) return '';
            const token = hfToken || window.HF_TOKEN || '';

            // The Universal Router Endpoint
            const endpoint = `https://router.huggingface.co/v1/chat/completions`;

            try {
                const body = {
                    model: hfModel, // The router finds the provider for this ID
                    messages: [
                        { role: "system", content: instruction },
                        { role: "user", content: text }
                    ],
                    max_tokens: 2048,
                    temperature: 0.1
                };

                const resp = await fetch(endpoint, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': `Bearer ${token}`
                    },
                    body: JSON.stringify(body)
                });

                if (!resp.ok) {
                    const errorData = await resp.json();
                    console.error('HF API Error:', resp.status, errorData);
                    return '';
                }

                const data = await resp.json();
                return data.choices[0].message.content.trim();
            } catch (e) {
                console.error('Fetch error:', e);
                return '';
            }
        }

        async function translateWholeWithOpenRouter(text, instruction = translationInstructionDefault) {
            if (!text) return '';

            const endpoint = 'https://openrouter.ai/api/v1/chat/completions';
            const body = {
                model: OPENROUTER_MODEL,
                messages: [
                    { role: "system", content: instruction },
                    { role: "user", content: text }
                ],
                temperature: 0.1,
                max_tokens: 2048
            };

            // Prefer GM_xmlhttpRequest in userscripts to avoid CORS issues from file:// origin
            if (typeof GM_xmlhttpRequest === 'function') {
                return new Promise((resolve) => {
                    GM_xmlhttpRequest({
                        method: 'POST',
                        url: endpoint,
                        data: JSON.stringify(body),
                        headers: {
                            'Content-Type': 'application/json',
                            'Authorization': `Bearer ${OPENROUTER_API_KEY}`
                        },
                        onload(res) {
                            try {
                                const status = res.status || 0;
                                const textResp = res.responseText || '';
                                let data;
                                try { data = JSON.parse(textResp); } catch (e) { data = null; }
                                if (status >= 200 && status < 300 && data && data.choices && data.choices.length) {
                                    resolve((data.choices[0].message && data.choices[0].message.content || '').trim());
                                } else {
                                    console.error('OpenRouter API Error (GM):', status, data || textResp);
                                    resolve('');
                                }
                            } catch (e) {
                                console.error('OpenRouter parse error (GM):', e);
                                resolve('');
                            }
                        },
                        onerror(err) {
                            console.error('GM_xmlhttpRequest error:', err);
                            resolve('');
                        },
                        ontimeout() {
                            console.error('GM_xmlhttpRequest timeout');
                            resolve('');
                        }
                    });
                });
            }

            // Fallback to fetch (may be blocked by CORS when running from file://)
            try {
                const resp = await fetch(endpoint, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': `Bearer ${OPENROUTER_API_KEY}`
                    },
                    body: JSON.stringify(body)
                });

                const text = await resp.text();
                let data = null;
                try { data = JSON.parse(text); } catch (e) { /* non-JSON response */ }

                if (!resp.ok) {
                    console.error('OpenRouter API Error (fetch):', resp.status, data || text);
                    return '';
                }

                if (data && data.choices && data.choices.length) {
                    return (data.choices[0].message && data.choices[0].message.content || '').trim();
                }

                return '';
            } catch (e) {
                console.error('Fetch error:', e);
                return '';
            }
        }

        // Translate all lines in one request, then split back into per-line translations using the delimiter
        async function translateAllLines() {
            if (!currentLrcEntries || !currentLrcEntries.length) {
                $('#lyricsStatus').text('No lyrics to translate');
                return;
            }
            $('#lyricsStatus').text('Translating...');
            // ensure cache array length
            currentTranslations = currentTranslations || [];

            // Build single payload with delimiter (avoid internal newlines inside lines)
            const texts = currentLrcEntries.map(e => (e.text || '').replace(/\r?\n/g, ' ')); // flatten internal newlines
            const payload = texts.join(TRANSLATION_DELIM);

            // Use provider selected in settings (fallback to 'hf' then google)
            let translationProvider = (translationProviderSetting || 'hf').toLowerCase().trim();
            //if ((translationProvider !== 'google' || translationProvider !== 'g') && (translationProvider !== 'hf' ||  translationProvider !==  'h') && (translationProvider !== 'openrouter' ||  translationProvider !==  'o')) translationProvider = 'google';

            let translatedAll = '';

            if (translationProvider === 'hf' && (hfToken || window.HF_API_KEY)) {
                translatedAll = await translateWholeWithHF(payload, translationInstructionDefault);
            } else if (translationProvider === 'openrouter' && (OPENROUTER_API_KEY || window.OPENROUTER_API_KEY)) {
                translatedAll = await translateWholeWithOpenRouter(payload, translationInstructionDefault);
            } else {
                // fallback to Google Translate if provider not available or explicitly selected
                translatedAll = await translateWholeWithGoogle(payload);
            }

            if (!translatedAll) {
                $('#lyricsStatus').text('Translation failed');
                return;
            }

            // Normalize translator output before splitting:
            // - remove CRs and ZERO-WIDTH spaces
            // - convert any literal "\n" sequences back to real newlines (some APIs escape them)
            // - normalize any stray variants of <<<LINE>>> (spaces/case) and remove surrounding whitespace
            let safeTranslated = String(translatedAll).replace(/\r/g, '').replace(/\u200B/g, '');
            safeTranslated = safeTranslated.replace(/\\n/g, '\n'); // replace literal backslash-n with newline (if API escaped)
            // Normalize any token-like markers (translators may localize or modify inside-token text,
            // e.g. <<<LÍNEA>>>, <<<Ligne>>>, <<<Linea>>>, or other accented/letter variants).
            // First try a Unicode-aware letter+mark+space pattern, then a broad fallback for any
            // short sequence enclosed by many < and >. Both replace with the canonical token.
            try {
                safeTranslated = safeTranslated.replace(/<<+\s*[\p{L}\p{M}\s\-\._]{1,40}\s*>>+/gu, '<<<LINE>>>');
            } catch (err) {
                // If environment doesn't support \p Unicode property escapes, fall back to a more permissive ascii-range
                safeTranslated = safeTranslated.replace(/<<+\s*[A-Za-zÀ-ÖØ-öø-ÿ\u00C0-\u024F\s\-\._]{1,40}\s*>>+/g, '<<<LINE>>>');
            }
            // Final defensive normalization: any short bracketed chunk becomes token (avoids missed edge-cases)
            safeTranslated = safeTranslated.replace(/<<+\s*[^>\n]{1,40}\s*>>+/g, '<<<LINE>>>');
            // remove incidental whitespace around token so split is reliable, but do NOT inject newlines
            safeTranslated = safeTranslated.replace(/\s*<<<LINE>>>\s*/g, '<<<LINE>>>');

            // Primary split by the exact token (preserves empty strings)
            let parts = safeTranslated.split(TRANSLATION_DELIM);

            // Fallbacks if counts don't match (try to recover while preserving empty lines)
            if (parts.length !== texts.length) {
                // Try regex extraction that captures segments between tokens (preserves empties)
                const re = /([\s\S]*?)(?:<<<LINE>>>|$)/g;
                const tmp = [];
                let m;
                while ((m = re.exec(safeTranslated)) !== null) {
                    tmp.push(m[1]);
                    if (tmp.length >= texts.length) break;
                }
                if (tmp.length === texts.length) {
                    parts = tmp;
                } else {
                    // Try simple newline split (preserve empties)
                    const nlParts = safeTranslated.split(/\r?\n/);
                    if (nlParts.length === texts.length) {
                        parts = nlParts;
                    } else {
                        // Last resort: align by truncating/padding to expected length
                        parts = parts.slice(0, texts.length);
                        while (parts.length < texts.length) parts.push('');
                    }
                }
            }

            // Map results back into currentTranslations aligning by index, and trim surrounding whitespace
            currentTranslations = new Array(currentLrcEntries.length).fill('');
            for (let i = 0; i < currentLrcEntries.length; i++) {
                const rawPart = (parts[i] !== undefined && parts[i] !== null) ? String(parts[i]) : '';

                // Remove weird/non-standard characters (modify regex as needed) (Clear the china words...)
                //const cleanedPart = rawPart.replace(/[^\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFFA-Za-z0-9\s\.,;:!?\-'"]/g, '');

                // remove any accidental token remnants and trim ends but keep empty-lines
                currentTranslations[i] = rawPart.replace(/<<<LINE>>>/g, '').replace(/\u200B/g, '').trim();
            }

            $('#lyricsStatus').text('Translation ready');
        }

        // Insert or update translation text inside each .lrc-line -> .lrc-translation child
        function showTranslations() {
            const $lines = $('#lyricsInner .lrc-line');
            if (!$lines.length) return;
            $lines.each(function() {
                const idx = parseInt($(this).attr('data-idx'));
                const $trans = $(this).find('.lrc-translation');
                const text = (currentTranslations && currentTranslations[idx]) ? currentTranslations[idx] : '';
                $trans.text(text || '').show();
            });
            translationsShown = true;
        }

        function hideTranslations() {
            $('#lyricsInner .lrc-translation').hide();
            translationsShown = false;
        }

        // Toggle translations on button click
        $('#translationLyricsBtn').off('click').on('click', async function() {
            // If translations already shown, hide them
            if (translationsShown) {
                hideTranslations();
                $('#lyricsStatus').text('Translations hidden');
                return;
            }
            // else ensure translations are available and show them
            await translateAllLines();
            showTranslations();
        });

        // Toggle translations on button click
        $('#translationLyricsBtn').off('click').on('click', async function() {
            // If translations already shown, hide them
            if (translationsShown) {
                hideTranslations();
                $('#lyricsStatus').text('Translations hidden');
                return;
            }
            // else ensure translations are available and show them
            await translateAllLines();
            showTranslations();
        });

        // Lyrics settings popup (Translation settings)
        $('#translationSettingsBtn').off('click').on('click', function() {
            let $modal = $('#translationSettingsModal');
            if ($modal.length === 0) {
                $modal = $(`
                    <div id="translationSettingsModal" style="display:flex; position:fixed; inset:0; background:rgba(0,0,0,0.6); padding:20px; z-index:1000000; align-items:center; justify-content:center;">
                        <!-- Outer panel: column layout with constrained height so inner area can scroll -->
                        <div style="background:#222; color:#eee; padding:16px; border-radius:8px; max-width:760px; width:92%; margin:auto; display:flex; flex-direction:column; max-height:calc(100vh - 80px); box-sizing:border-box;">
                            <h3 style="margin:0 0 8px 0;">Lyrics Settings</h3>

                            <!-- Scrollable content area -->
                            <div style="overflow-y:auto; padding-right:8px; flex:1 1 auto;">
                                <div style="display:flex; gap:8px; flex-wrap:wrap;">

                                    <!-- Automatic options -->
                                    <div style="flex:1 1 100%; margin-top:6px;">
                                        <label style="color:#ccc; font-size:13px; display:block; margin-bottom:6px;">
                                            <input type="checkbox" id="setting_autoLyrics" style="margin-right:8px;"> Auto lyrics (fetch on track change)
                                        </label>
                                        <label style="color:#ccc; font-size:13px; display:block; margin-bottom:6px;">
                                            <input type="checkbox" id="setting_autoTranslation" style="margin-right:8px;"> Auto translation (translate after lyrics fetched)
                                        </label>
                                        <label style="color:#ccc; font-size:13px; display:block;">
                                            <input type="checkbox" id="setting_autoClear" style="margin-right:8px;"> Auto clear lyrics on track change
                                        </label>
                                    </div>

                                    <!-- Google target language and AI prompt language label -->
                                    <div style="flex:1 1 100%;">
                                        <label style="font-size:13px;color:#bbb; display:block; margin-bottom:6px;">Translation Provider</label>
                                        <select id="setting_translationProvider" style="display:block; width:90%; padding:8px; background:#111; border:1px solid #444; color:#fff; border-radius:4px;">
                                            <option value="google">Google (public)</option>
                                            <option value="hf">HuggingFace / Router</option>
                                            <option value="openrouter">OpenRouter</option>
                                        </select>
                                        <label style="font-size:12px;color:#666; display:block; margin-top:6px;">Choose default provider for translations. The Translate button will use this setting instead of prompting.</label>
                                    </div>

                                    <div style="flex:1 1 100%;">
                                        <a href="https://hf.co/settings/tokens" style="font-size:13px;color:#bbb;">HuggingFace Token</a>
                                        <input id="setting_hfToken" style="display:block; width:90%; padding:8px; background:#111; border:1px solid #444; color:#fff; border-radius:4px;" />
                                        <label style="font-size:12px;color:#666; display:block; margin-top:6px;">Your personal key to access models to your Hugging Face account.</label>
                                    </div>

                                    <div style="flex:1 1 100%;">
                                        <a href="https://huggingface.co/docs/inference-providers/en/index" style="font-size:13px;color:#bbb;">HuggingFace Model</a>
                                        <input id="setting_hfModel" style="display:block; width:90%; padding:8px; background:#111; border:1px solid #444; color:#fff; border-radius:4px;" />
                                        <label style="font-size:13px;color:#666; white-space:pre-line; cursor:text;">meta-llama/Llama-3.3-70B-Instruct - allenai/Molmo2-8B:publicai
                                            mistralai/Mistral-7B-Instruct-v0.3 - openai/gpt-oss-20b:groq
                                            openai/gpt-oss-20b:hyperbolic - openai/gpt-oss-20b:together
                                            zai-org/GLM-4.7-Flash:zai-org - HuggingFaceTB/SmolLM3-3B:hf-inference
                                            allenai/Olmo-3.1-32B-Instruct:publicai - Qwen/Qwen2.5-VL-32B-Instruct:fireworks-ai
                                        </label>
                                    </div>

                                    <div style="flex:1 1 100%;">
                                        <a href="https://openrouter.ai/settings/keys" style="font-size:13px;color:#bbb;">OpenRouter API Key</a>
                                        <input id="setting_openrouter_key" style="display:block; width:90%; padding:8px; background:#111; border:1px solid #444; color:#fff; border-radius:4px;" />
                                        <label style="font-size:12px;color:#666; display:block; margin-top:6px;">Your secret key for the OpenRouter API. It lets you use many different AI models (like GPT-4, Claude, Llama) through one platform.</label>
                                    </div>

                                    <div style="flex:1 1 100%;">
                                        <a href="https://openrouter.ai/models" style="font-size:13px;color:#bbb;">OpenRouter Model</a>
                                        <input id="setting_openrouter_model" style="display:block; width:90%; padding:8px; background:#111; border:1px solid #444; color:#fff; border-radius:4px;" />
                                        <label style="font-size:13px;color:#666; white-space:pre-line; cursor:text;">meta-llama/llama-3.3-70b-instruct:free - google/gemini-2.0-flash-exp:free
                                            arcee-ai/trinity-large-preview:free - deepseek/deepseek-r1-0528:free
                                            mistralai/mistral-small-3.1-24b-instruct:free
                                        </label>
                                    </div>

                                    <!-- Google target language and AI prompt language label -->
                                    <div style="flex:1 1 100%; margin-top:6px;">
                                        <label style="font-size:13px;color:#bbb; display:block; margin-bottom:6px;">Google Target Language (ISO code)</label>
                                        <input id="setting_googleLang" placeholder="fa" style="display:block; width:90%; padding:8px; background:#111; border:1px solid #444; color:#fff; border-radius:4px;" />
                                        <label style="font-size:12px;color:#666; display:block; margin-top:6px;">e.g. fa for Persian, fr for French, es for Spanish</label>
                                    </div>
                                    <div style="flex:1 1 100%; margin-top:6px;">
                                        <label style="font-size:13px;color:#bbb; display:block; margin-bottom:6px;">AI Prompt Language Label</label>
                                        <input id="setting_aiPromptLanguage" placeholder="Persian (Farsi)" style="display:block; width:90%; padding:8px; background:#111; border:1px solid #444; color:#fff; border-radius:4px;" />
                                        <label style="font-size:12px;color:#666; display:block; margin-top:6px;">This text will replace "Persian (Farsi)" inside the AI system instruction</label>
                                    </div>

                                    <!-- App Font selection -->
                                    <div style="flex:1 1 100%; margin-top:6px;">
                                        <label style="font-size:13px;color:#bbb; display:block; margin-bottom:6px;">App Font (UI)</label>
                                        <select id="setting_appFont" style="display:block; width:90%; padding:8px; background:#111; border:1px solid #444; color:#fff; border-radius:4px;">
                                            <option value='"Ink Free", "Segoe UI", Roboto, Arial, sans-serif'>Ink Free</option>
                                            <option value='"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'>Segoe UI</option>
                                            <option value='Roboto, "Helvetica Neue", Arial, sans-serif'>Roboto</option>
                                            <option value='Arial, Helvetica, sans-serif'>Arial</option>
                                            <option value='Tahoma, Verdana, Segoe, sans-serif'>Tahoma</option>
                                            <option value='Verdana, Geneva, sans-serif'>Verdana</option>
                                            <option value='"Times New Roman", Times, serif'>Times New Roman</option>
                                            <option value='Georgia, "Times New Roman", Times, serif'>Georgia</option>
                                            <option value='Calibri, Candara, "Segoe UI", Arial'>Calibri</option>
                                            <option value='"Courier New", Courier, monospace'>Courier New</option>
                                        </select>
                                        <label style="font-size:12px;color:#666; display:block; margin-top:6px;">Choose the font used for the entire UI and lyrics area.</label>
                                    </div>

                                    <!-- Visualizer Settings -->
                                    <div style="flex:1 1 100%; margin-top:12px; border-top:1px dashed #333; padding-top:12px;">
                                        <div style="font-weight:bold; color:#ddd; margin-bottom:8px;">Visualizer (Equalizer) Settings</div>
                                        <label style="display:block; color:#ccc; margin-bottom:6px;">
                                            <input type="checkbox" id="setting_visualizer_enable" style="margin-right:8px;"> Enable Visualizer
                                        </label>

                                        <label style="display:block; color:#ccc; margin-bottom:6px;">
                                            <span style="display:inline-block; width:120px;">Style:</span>
                                            <select id="setting_visualizer_style" style="padding:6px; background:#111; border:1px solid #444; color:#fff; border-radius:4px;">
                                                <option value="bars">Bars</option>
                                                <option value="waveform">Waveform</option>
                                                <option value="radial">Radial</option>
                                            </select>
                                        </label>

                                        <label style="display:block; color:#ccc; margin-bottom:6px;">
                                            <span style="display:inline-block; width:120px;">Height:</span>
                                            <input id="setting_visualizer_height" type="range" min="10" max="200" value="40" style="width:200px; vertical-align:middle;"/>
                                            <span id="vizHeightLabel" style="margin-left:8px; color:#999;">40px</span>
                                        </label>

                                        <label style="display:block; color:#ccc; margin-bottom:6px;">
                                            <span style="display:inline-block; width:120px;">Sensitivity:</span>
                                            <input id="setting_visualizer_sensitivity" type="range" min="50" max="300" value="100" step="1" style="width:200px; vertical-align:middle;"/>
                                            <span id="vizSensitivityLabel" style="margin-left:8px; color:#999;">100%</span>
                                        </label>

                                        <label style="display:block; color:#ccc; margin-bottom:6px;">
                                            <span style="display:inline-block; width:120px;">Bars:</span>
                                            <input id="setting_visualizer_bars" type="range" min="8" max="128" value="80" step="1" style="width:200px; vertical-align:middle;"/>
                                            <span id="vizBarsLabel" style="margin-left:8px; color:#999;">80</span>
                                        </label>

                                        <label style="display:block; color:#ccc; margin-bottom:6px;">
                                            <input type="checkbox" id="setting_color_enable" style="margin-right:8px;">Custom Color:
                                            <input id="setting_visualizer_color" type="color" value="#66ccff" style="vertical-align:middle; width:64px; height:30px; border:1px solid #444; background:#111;"/>
                                        </label>


                                        <div style="font-size:12px; color:#666;">Enable or customize the visual equalizer shown under the metadata area. When audio is paused the visualizer will render a zero baseline.</div>
                                    </div>
                                </div>
                            </div>

                            <!-- Footer: keep buttons visible (not part of scrollable area) -->
                            <div style="text-align:right; margin-top:12px; flex:0 0 auto;">
                                <button id="translationSettingsCancel" style="margin-right:8px; padding:6px 12px; background:#555; border:none; color:#fff; border-radius:4px; cursor:pointer;">Cancel</button>
                                <button id="translationSettingsSave" style="padding:6px 12px; background:#0073e6; border:none; color:#fff; border-radius:4px; cursor:pointer;">Save</button>
                            </div>
                        </div>
                    </div>
                `);
                $('body').append($modal);

                $('#translationSettingsCancel').on('click', function() {
                    $('#translationSettingsModal').hide();
                });

                $('#translationSettingsSave').on('click', function() {
                    // Apply settings only in-memory for this session (no persistence)
                    const v_hfModel = $('#setting_hfModel').val();
                    if (v_hfModel) hfModel = v_hfModel.trim();

                    const v_hfToken = $('#setting_hfToken').val().trim();
                    if (v_hfToken) hfToken = v_hfToken;

                    const v_or_key = $('#setting_openrouter_key').val().trim();
                    if (v_or_key) OPENROUTER_API_KEY = v_or_key;

                    const v_or_model = $('#setting_openrouter_model').val();
                    if (v_or_model) OPENROUTER_MODEL = v_or_model.trim();

                    // Google target language (ISO)
                    const v_google = ($('#setting_googleLang').val() || '').trim();
                    if (v_google) googleTargetLang = v_google;

                    // AI prompt language label
                    const v_aiPrompt = ($('#setting_aiPromptLanguage').val() || '').trim();
                    if (v_aiPrompt) aiPromptLanguage = v_aiPrompt;

                    // rebuild default instruction from aiPromptLanguage
                    translationInstructionDefault =
                        `Translate the following lyrics into fluent, natural ${aiPromptLanguage}. Preserve meaning and punctuation. Return the translation with each original line corresponding to the same number of lines, and separate lines using the exact token <<<LINE>>>.`;

                    // App font selection
                    const v_appFont = ($('#setting_appFont').val() || '').trim();
                    if (v_appFont) {
                        appFont = v_appFont;
                        try {
                            // update CSS variable on the container to apply across UI
                            $('#mp3ViewerContainer').css('--app-font-family', appFont);
                        } catch (e) { console.warn('apply app font failed', e); }
                    }

                    // Read new automatic option checkboxes
                    autoLyrics = !!$('#setting_autoLyrics').is(':checked');
                    autoTranslation = !!$('#setting_autoTranslation').is(':checked');
                    autoClearLyrics = !!$('#setting_autoClear').is(':checked');

                    // Read translation provider selection
                    const provider = $('#setting_translationProvider').val();
                    if (provider) translationProviderSetting = provider;

                    // --- Visualizer settings read & apply ---
                    visualizerEnabled = !!$('#setting_visualizer_enable').is(':checked');
                    const selStyle = $('#setting_visualizer_style').val();
                    if (selStyle) visualizerStyle = selStyle;
                    colorEnabled = !!$('#setting_color_enable').is(':checked');
                    const selColor = $('#setting_visualizer_color').val();
                    if (selColor) visualizerColor = selColor;
                    const selHeight = parseInt($('#setting_visualizer_height').val(), 10) || 80;
                    visualizerHeight = selHeight;
                    // sensitivity slider is percent: 50..300 -> 0.5..3.0
                    const selSensitivity = parseInt($('#setting_visualizer_sensitivity').val(), 10) || 100;
                    visualizerSensitivity = Math.max(0.5, Math.min(3.0, selSensitivity / 100));

                    // bars count (only meaningful for 'bars' style)
                    const selBars = parseInt($('#setting_visualizer_bars').val(), 10) || 32;
                    visualizerBarCount = Math.max(4, Math.min(128, selBars));
                    // Apply immediately
                    applyVisualizerSettings();

                    $('#translationSettingsModal').hide();
                    $('#lyricsStatus').text('Lyrics settings saved');
                });
            }

            // populate inputs with current values
            $('#setting_hfToken').val(hfToken || '');
            $('#setting_hfModel').val(hfModel || '');
            $('#setting_openrouter_key').val(OPENROUTER_API_KEY || '');
            $('#setting_openrouter_model').val(OPENROUTER_MODEL || '');

            // populate new google / ai inputs
            $('#setting_googleLang').val(googleTargetLang || 'fa');
            $('#setting_aiPromptLanguage').val(aiPromptLanguage || 'Persian (Farsi)');

            // populate app font select
            $('#setting_appFont').val(appFont || '"Ink Free", "Segoe UI", Roboto, Arial, sans-serif');

            // populate new auto option checkboxes
            $('#setting_autoLyrics').prop('checked', !!autoLyrics);
            $('#setting_autoTranslation').prop('checked', !!autoTranslation);
            $('#setting_autoClear').prop('checked', !!autoClearLyrics);

            // populate translation provider select
            $('#setting_translationProvider').val(translationProviderSetting || 'hf');

            // Populate visualizer controls with current values
            $('#setting_visualizer_enable').prop('checked', !!visualizerEnabled);
            $('#setting_visualizer_style').val(visualizerStyle || 'bars');
            $('#setting_color_enable').prop('checked', !!colorEnabled);
            $('#setting_visualizer_color').val(visualizerColor || '#66ccff');
            $('#setting_visualizer_height').val(visualizerHeight || 80);
            $('#vizHeightLabel').text((visualizerHeight || 80) + 'px');
            $('#setting_visualizer_sensitivity').val(Math.round((visualizerSensitivity || 1.0) * 100));
            $('#vizSensitivityLabel').text(Math.round((visualizerSensitivity || 1.0) * 100) + '%');
            $('#setting_visualizer_bars').val(visualizerBarCount || 32);
            $('#vizBarsLabel').text(visualizerBarCount || 32);

            // live update height label when user moves slider in modal
            $('#setting_visualizer_height').off('input').on('input', function() {
                $('#vizHeightLabel').text(this.value + 'px');
            });

            // live update sensitivity label
            $('#setting_visualizer_sensitivity').off('input').on('input', function() {
                $('#vizSensitivityLabel').text(this.value + '%');
            });

            // live update bars label
            $('#setting_visualizer_bars').off('input').on('input', function() {
                $('#vizBarsLabel').text(this.value);
            });

            $('#translationSettingsModal').show();
        });

        // Ensure minimal styling exists for translation lines (only once)
        if (!$('#lyricsTranslationStyles').length) {
            $('head').append(`
                <style id="lyricsTranslationStyles">
                    .lrc-translation { opacity: 0.95; display: none; direction: rtl; }
                    .lrc-line .lrc-original { display: block; }
                </style>
            `);
        }

        /*
        *
        * Add lyrics handling and lrclib fetching (END)
        *
        */

        // Playlist variables
        let playlist = [];
        let currentTrackIndex = 0;
        let isLoop = false;
        let isRepeat = false;
        let isShuffle = false;
        const playlistAlbumArts = new Map();
        const playlistTags = new Map(); // store parsed ID3 tags per File/src-item
        const extractingAlbumArts = new Set();
        let playlistSearchQuery = ''; // <-- Add this line

        // Add play/pause button logic
        $('#playBtn').off('click').on('click', function() {
            if ($audioPlayer[0].paused) {
                $audioPlayer[0].play();
                $(this).text('⏸️ Pause');
            } else {
                $audioPlayer[0].pause();
                $(this).text('▶️ Play');
            }
        });

        // Add play/pause cover logic
        $('#albumArt').off('click').on('click', function() {
            if ($audioPlayer[0].paused) {
                $audioPlayer[0].play();
                $('#playBtn').text('⏸️ Pause');
            } else {
                $audioPlayer[0].pause();
                $('#playBtn').text('▶️ Play');
            }
        });

        // Audio player event listeners
        $audioPlayer.on('play', function() {
            $('#playBtn').text('⏸️ Pause');
        });

        $audioPlayer.on('pause ended', function() {
            $('#playBtn').text('▶️ Play');
        });

        // Shuffle functionality
        function getRandomTrackIndex() {
            if (playlist.length <= 1) return currentTrackIndex;
            const availableTracks = [...Array(playlist.length).keys()].filter(i => i !== currentTrackIndex);
            return availableTracks[Math.floor(Math.random() * availableTracks.length)];
        }

        // Single shuffle button handler with improved logic
        $('#shuffleBtn').off('click').on('click', function() {
            isShuffle = !isShuffle;
            $(this).text('🔀 Auto Shuffle');

            if (isShuffle) {
                // Create shuffled order on activation
                const currentTrack = playlist[currentTrackIndex];
                const shuffledPlaylist = [...playlist];
                for (let i = shuffledPlaylist.length - 1; i > 0; i--) {
                    const j = Math.floor(Math.random() * (i + 1));
                    [shuffledPlaylist[i], shuffledPlaylist[j]] = [shuffledPlaylist[j], shuffledPlaylist[i]];
                }
                playlist = shuffledPlaylist;
                currentTrackIndex = playlist.indexOf(currentTrack);
                renderPlaylist();
            }
        });

        // Update event listeners
        $fileInput.on('change', handleFileSelection);
        $audioPlayer.on('ended', playNextTrack);
        $('#nextBtn').on('click', playNextTrack);
        $('#prevBtn').on('click', playPrevTrack);

        $('#loopBtn').on('click', function() {
            isLoop = !isLoop;
            $audioPlayer.prop('loop', isLoop);
            $(this).text('' + (isLoop ? '🔂 Loop: On' : '🔁 Loop: Off'));
        });
        $('#repeatBtn').on('click', function() {
            isRepeat = !isRepeat;
            $(this).text('🔃 Repeat: ' + (isRepeat ? 'On' : 'Off'));
        });
        $('#shuffleBtn').on('click', function() {
            isShuffle = !isShuffle;
            $(this).text('🔀 Auto Shuffle');
        });

        // Play a track by index
        async function playTrack(index) {
            if (index < 0 || index >= playlist.length) return;
            const file = playlist[index];
            updateStatus('Processing file...');
            let audioSrc;
            if (file.src) {
                // Use src directly for file path
                audioSrc = file.src;

                // If remote HTTP(S) and GM is available, try to obtain a blob via GM to avoid CORS issues.
                // This will download the file via the userscript engine and create a blob URL we can set on the audio element.
                let usedBlobUrl = null;
                if (/^https?:\/\//i.test(String(audioSrc)) && typeof GM_xmlhttpRequest === 'function') {
                    try {
                        // Try quick download - if it succeeds we bypass CORS entirely.
                        const blob = await gmDownloadToBlob(audioSrc);
                        if (blob && blob.size > 0) {
                            try {
                                usedBlobUrl = URL.createObjectURL(blob);
                                // ensure no crossorigin attribute (blob urls don't need it)
                                try { $audioPlayer.removeAttr('crossorigin'); } catch (e) {}
                                $audioPlayer.attr('src', usedBlobUrl);
                            } catch (e) {
                                // fallback if createObjectURL fails
                                usedBlobUrl = null;
                            }
                        }
                    } catch (e) {
                        console.warn('gm download failed', e);
                    }
                }

                // If GM couldn't produce a blob, fall back to assigning original src (best-effort)
                if (!usedBlobUrl) {
                    // IMPORTANT: set crossorigin BEFORE setting src for remote/file URLs so WebAudio can attach.
                    if (/^(https?:|file:)/i.test(String(audioSrc))) {
                        try { $audioPlayer.attr('crossorigin', 'anonymous'); } catch (e) { /* ignore */ }
                    } else {
                        try { $audioPlayer.removeAttr('crossorigin'); } catch (e) { /* ignore */ }
                    }
                    // assign src AFTER crossorigin attribute set
                    $audioPlayer.attr('src', audioSrc);
                }

                $mainPlayerContainer.show();
                $playerContainer.show();
                $albumArt.hide();
                $trackInfo.html('<div>Loading metadata...</div>');

                // Ensure AudioContext exists/resumed and (re)create MediaElementSource to route audio through analyser
                try {
                    if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
                    if (audioCtx.state === 'suspended') {
                        // resume on user gesture or programmatically
                        audioCtx.resume().catch(()=>{ /* ignore */ });
                    }
                } catch (e) {
                    console.warn('AudioContext create/resume failed', e);
                }

                // If analyser not created yet, create it (setupAnalyzer will also do this)
                if (!analyser && audioCtx) {
                    analyser = audioCtx.createAnalyser();
                    analyser.fftSize = 1024;
                    bufferLength = analyser.frequencyBinCount;
                    dataArray = new Uint8Array(bufferLength);
                }

                // If a previous sourceNode exists, disconnect it to avoid routing issues when switching sources
                try {
                    if (sourceNode) {
                        try { sourceNode.disconnect(); } catch (err) { /* ignore */ }
                        sourceNode = null;
                    }
                    if (audioCtx && $audioPlayer && $audioPlayer[0] && typeof audioCtx.createMediaElementSource === 'function') {
                        sourceNode = audioCtx.createMediaElementSource($audioPlayer[0]);
                        if (sourceNode) {
                            if (!analyser) analyser = audioCtx.createAnalyser();
                            sourceNode.connect(analyser);
                            analyser.connect(audioCtx.destination);
                        }
                    }
                } catch (e) {
                    console.warn('Recreating MediaElementSource failed', e);
                }

                // Attempt to extract metadata from remote / file URL (ranged fetch).
                // This will populate playlistTags and playlistAlbumArts similar to local files.
                try {
                    await extractMetadataFromSrc(file, function() {
                        const tags = playlistTags.get(file) || {};
                        // display metadata if available, else show filename
                        if (Object.keys(tags).length) {
                            displayMetadata(tags, file.name || (new URL(file.src)).pathname.split('/').pop());
                        } else {
                            $trackInfo.html(`<div><strong>Filename:</strong> ${file.name}</div>`);
                        }
                        // call auto behaviors afterwards
                        try { handleTrackChanged(file); } catch (e) { console.error(e); }
                    });
                } catch (e) {
                    console.error('Remote metadata extraction error:', e);
                    $trackInfo.html(`<div><strong>Filename:</strong> ${file.name}</div>`);
                    try { handleTrackChanged(file); } catch (e) { console.error(e); }
                }
            } else {
                // Use File object
                audioSrc = URL.createObjectURL(file);
                $audioPlayer.attr('src', audioSrc);
                $mainPlayerContainer.show();
                $playerContainer.show();
                $albumArt.hide();
                $trackInfo.html('<div>Loading metadata...</div>');
                extractMetadata(file, file.name, function() {
                    const reader = new FileReader();
                    reader.onload = function(e) {
                        try {
                            const tags = parseID3Tags(e.target.result);
                            displayMetadata(tags, file.name);
                            updateStatus('File loaded successfully');
                        } catch (error) {
                            $trackInfo.html('<div>No metadata available</div>');
                        } finally {
                            // After metadata/display finished, run auto behavior
                            try { handleTrackChanged(file); } catch (e) { console.error(e); }
                        }
                    };
                    reader.readAsArrayBuffer(file);
                });
            }
            renderPlaylist();
        }

        // New: automatically handle per-track actions (clear / fetch / translate)
        async function handleTrackChanged(file) {
            // auto clear previous lyrics if requested
            if (autoClearLyrics) {
                clearLyricsAction();
            }

            // If auto lyrics is not enabled, stop here
            if (!autoLyrics) return;

            // Determine search query
            const tags = playlistTags.get(file) || {};
            const artist = tags.artist || tags.albumArtist || '';
            const title = tags.title || (file && file.name ? file.name.replace(/\.mp3$/i, '') : '');

            if (!title) {
                $('#lyricsStatus').text('No title available for auto-lyrics');
                return;
            }

            $('#lyricsStatus').text('Auto searching lyrics...');
            const query = (artist ? artist + ' ' : '') + title;

            // perform search
            const results = await searchLyricsOnLrclib(query);
            if (!results || !results.length) {
                $('#lyricsStatus').text('No lyrics found (auto)');
                return;
            }

            // pick first result (avoid modal) and fetch its lyrics
            const item = results[0];
            $('#lyricsStatus').text(`Auto-loading: ${item.trackName || item.name || title}`);
            const lrcText = await fetchLyricsById(item);
            if (!lrcText) {
                $('#lyricsStatus').text('Could not auto-load lyrics');
                return;
            }

            // parse and render
            const entries = parseLRC(lrcText);
            currentLrcEntries = entries;
            currentLrcIndex = -1;
            renderLyrics(entries);
            $('#lyricsStatus').text(`Auto-loaded: ${item.trackName || item.name || title}`);

            // optionally auto-translate after lyrics loaded
            if (autoTranslation) {
                $('#lyricsStatus').text('Auto-translating...');
                await translateAllLines();
                showTranslations();
            }
        }

        // Play next track
        function playNextTrack() {
            if (!playlist.length) return;

            if (isShuffle) {
                currentTrackIndex = getRandomTrackIndex();
            } else if (currentTrackIndex < playlist.length - 1) {
                currentTrackIndex++;
            } else if (isRepeat) {
                currentTrackIndex = 0;
            } else {
                return;
            }
            playTrack(currentTrackIndex);
        }

        // Play previous track
        function playPrevTrack() {
            if (currentTrackIndex > 0) {
                currentTrackIndex--;
                playTrack(currentTrackIndex);
            }
        }

        // Use the Media Session API
        if ('mediaSession' in navigator) {
            navigator.mediaSession.setActionHandler('previoustrack', function() { playPrevTrack(); });
            navigator.mediaSession.setActionHandler('nexttrack', function() { playNextTrack(); });
            navigator.mediaSession.setActionHandler('play', function() { $audioPlayer[0].play(); });
            navigator.mediaSession.setActionHandler('pause', function() { $audioPlayer[0].pause(); });

            // Use the actual audio element and guard against NaN/Infinite duration
            const audioEl = ($audioPlayer && $audioPlayer[0]) ? $audioPlayer[0] : document.getElementById('audioPlayer');

            // Update MediaSession position only when values are finite
            audioEl.addEventListener('timeupdate', () => {
                if ('mediaSession' in navigator && typeof navigator.mediaSession.setPositionState === 'function') {
                    const duration = audioEl.duration;
                    const position = audioEl.currentTime;
                    const playbackRate = audioEl.playbackRate;

                    if (Number.isFinite(duration) && Number.isFinite(position) && Number.isFinite(playbackRate)) {
                        // duration must be a finite float > 0
                        if (duration > 0) {
                            try {
                                navigator.mediaSession.setPositionState({
                                    duration: duration,
                                    playbackRate: playbackRate,
                                    position: position
                                });
                            } catch (err) {
                                console.warn('MediaSession.setPositionState failed:', err);
                            }
                        }
                    }
                }
            });

            // Also set once when metadata is loaded (ensures duration is available early)
            audioEl.addEventListener('loadedmetadata', () => {
                if ('mediaSession' in navigator && typeof navigator.mediaSession.setPositionState === 'function' && Number.isFinite(audioEl.duration)) {
                    try {
                        navigator.mediaSession.setPositionState({
                            duration: audioEl.duration,
                            playbackRate: audioEl.playbackRate || 1,
                            position: audioEl.currentTime || 0
                        });
                    } catch (err) { /* ignore */ }
                }
            });
        }

        // Render the playlist
    function renderPlaylist() {
        const $playlist = $('#playlist');
        $playlist.empty();

        // Add playlist styles
        if (!$('#playlistStyles').length) {
            $('head').append(`
                <style id="playlistStyles">
                    #playlist li {
                        padding: 10px 15px;
                        margin: 5px 0;
                        cursor: pointer;
                        color: #4c4c4c;
                        background: #1a1a1a;
                        border-radius: 6px;
                        transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
                        position: relative;
                        overflow: hidden;
                        transform-origin: left;
                    }

                    #playlist li.active {
                        border-radius: 6px 0 0 6px;
                    }

                    #playlist li:hover {
                        background: #2a2a2a;
                        color: #fff;
                        padding-left: 25px;
                        /*transform: scale(1.02);*/
                        box-shadow: 0 4px 12px rgba(0,0,0,0.2);
                    }

                    #playlist li.active {
                        background: linear-gradient(45deg, #003D6F, #001b3e);
                        color: white;
                        font-weight: bold;
                        transform: scale(1.02);
                    }

                    #playlist li::after {
                        content: '';
                        position: absolute;
                        top: 0;
                        left: -100%;
                        width: 100%;
                        height: 100%;
                        background: linear-gradient(
                            90deg,
                            transparent,
                            rgba(255,255,255,0.1),
                            transparent
                        );
                        transition: 0.5s;
                    }

                    #playlist li:hover::after {
                        left: 100%;
                    }

                    #playlist li.ui-sortable-helper {
                        background: #333;
                        transform: scale(1.05) rotate(2deg);
                        box-shadow: 0 8px 16px rgba(0,0,0,0.3);
                    }

                    #playlist li.ui-sortable-placeholder {
                        background: #2a2a2a;
                        visibility: visible !important;
                        height: 40px;
                        border: 2px dashed #444;
                    }
                </style>
            `);
        }

        // Filter playlist based on search query
        let filteredPlaylist = playlist;
        if (playlistSearchQuery) {
            filteredPlaylist = playlist.filter(file =>
                file.name.replace(/\.mp3$/i, '').toLowerCase().includes(playlistSearchQuery)
            );
        }

        // Render playlist items
        filteredPlaylist.forEach((file, idx) => {
            // Find the real index in the original playlist
            const realIdx = playlist.indexOf(file);
            const fileName = file.name.replace(/\.mp3$/i, '');
            const isActive = realIdx === currentTrackIndex;
            let thumb = '';
            let artUrl = playlistAlbumArts.get(file);

            // Extract metadata if not yet attempted (handle both File objects and src-only items)
            if (artUrl === undefined && !extractingAlbumArts.has(file)) {
                extractingAlbumArts.add(file);
                if (file.src) {
                    extractMetadataFromSrc(file, function() {
                        extractingAlbumArts.delete(file);
                        renderPlaylist();
                    });
                } else {
                    extractMetadata(file, file.name, function() {
                        extractingAlbumArts.delete(file);
                        renderPlaylist();
                    });
                }
                artUrl = null;
            }

            if (artUrl) {
                thumb = `<img src="${artUrl}" style="width:32px;height:32px;object-fit:cover;border-radius:3px;vertical-align:middle;margin-right:8px;">`;
            } else {
                thumb = `<span style="display:inline-block;width:32px;height:32px;margin-right:8px;vertical-align:middle;background:#222;border-radius:3px;"></span>`;
            }
            $playlist.append(`
                <li data-idx="${realIdx}" class="${isActive ? 'active' : ''}">
                    ${thumb}${fileName}
                    <button class="removeTrackBtn" title="Remove Track">✖</button>
                </li>
           `);
        });

        // Click handler for tracks
        $playlist.find('li').on('click', function() {
            const idx = parseInt($(this).attr('data-idx'));
            if (!isNaN(idx)) {
                currentTrackIndex = idx;
                playTrack(currentTrackIndex);
            }
        });

        // Click handler for tracks
        $playlist.find('.removeTrackBtn').on('click', function(e) {
            e.stopPropagation();
            const idx = parseInt($(this).parent().attr('data-idx'));
            if (!isNaN(idx)) {
                const file = playlist[idx];
                const artUrl = playlistAlbumArts.get(file);
                if (artUrl) {
                    URL.revokeObjectURL(artUrl);
                }
                playlistAlbumArts.delete(file);
                extractingAlbumArts.delete(file);
                playlist.splice(idx, 1);
                if (currentTrackIndex === idx) {
                    currentTrackIndex = Math.max(0, currentTrackIndex - 1);
                    if (playlist.length) {
                        playTrack(currentTrackIndex);
                    } else {
                        $audioPlayer[0].pause();
                        $trackInfo.html('<div>No track loaded</div>');
                        $albumArt.hide();
                        $audioPlayer.attr('src', '');
                    }
                } else if (idx < currentTrackIndex) {
                    currentTrackIndex--;
                }
                renderPlaylist();
            }
        });

        // Make playlist sortable
        $playlist.sortable({
            update: function(event, ui) {
                const newOrder = $playlist.find('li').map(function() {
                    return parseInt($(this).attr('data-idx'));
                }).get();
                playlist = newOrder.map(i => playlist[i]);
                currentTrackIndex = newOrder.indexOf(currentTrackIndex);
                renderPlaylist();
            }
        });

        // Add search input event handler after DOM is ready
        $('#playlistSearch').on('input', function() {
            playlistSearchQuery = $(this).val().toLowerCase();
            renderPlaylist();
        });

        // Save playlist in custom XML-like format
        $('#savePlaylistBtn').off('click').on('click', function() {
            let xml = '[\n';
            playlist.forEach(file => {
                // Use file.path if available, otherwise file.name
                const path = file.path || file.name;
                xml += `<media src="${path}"/>\n`;
            });
            xml += ']';
            const blob = new Blob([xml], { type: 'text/plain' });
            const url = URL.createObjectURL(blob);

            const a = document.createElement('a');
            a.href = url;
            a.download = 'playlist.json';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        });

        // Load playlist from custom XML-like format
        $('#loadPlaylistBtn').off('click').on('click', function() {
            alert("⚠Warning:\nTo display the song information (metadata) correctly, also select the songs included in the playlist.\n\nChoose your playlist file in .json format.");
            $('#loadPlaylistInput').trigger('click');
        });

        /* The .json file must include the following data:
           [
             <media src="C:\aaa.mp3"/>
             <media src="C:\bbb.mp3"/>
             <media src="C:\ccc.mp3"/>
           ]
        */
        $('#loadPlaylistInput').off('change').on('change', function(e) {
            const file = e.target.files[0];
            if (!file) return;
            const reader = new FileReader();
            reader.onload = function(ev) {
                const text = ev.target.result;
                const regex = /<media\s+src="([^"]+)"/g;
                let match;
                const paths = [];
                while ((match = regex.exec(text)) !== null) {
                    paths.push(match[1]);
                }
                if (!paths.length) {
                    updateStatus('No valid tracks found in playlist file', 'error');
                    return;
                }
                // Build playlist using src paths
                playlist = paths.map(path => ({
                    name: path.split('\\').pop().split('/').pop(),
                    src: path.startsWith('file:///') ? path : 'file:///' + path.replace(/\\/g, '/')
                }));
                currentTrackIndex = 0;
                renderPlaylist();
                updateStatus('Playlist loaded.');

                alert("⚠Warning:\nTo ensure the song metadata is displayed properly, also select the tracks that are part of the playlist.\n\nChoose your music files in .mp3 format.");

                // Prompt user to select files for metadata
                $('<input type="file" multiple accept=".mp3" style="display:none;">')
                    .on('change', function(ev2) {
                        const selectedFiles = Array.from(ev2.target.files);
                        // Replace playlist items with File objects if filename matches
                        playlist = playlist.map(item => {
                            const match = selectedFiles.find(f => f.name === item.name);
                            return match ? match : item;
                        });
                        renderPlaylist();
                        updateStatus('Metadata loaded for matched files.');
                    })
                    .appendTo('body')
                    .trigger('click');
            };
            reader.readAsText(file);
        });

        // Ensure the active track is scrolled into view
        (function scrollActiveIntoView() {
            const $container = $('#playlistContainer');
            const $playlistEl = $('#playlist');
            const $active = $playlistEl.find('li.active:visible').first();
            if (!$active.length) return;

            const containerEl = $container.get(0);
            const activeEl = $active.get(0);
            if (!containerEl || !activeEl) return;

            // Prefer native API (handle same way as lyrics sync: small defer + center)
            if (typeof activeEl.scrollIntoView === 'function') {
                try {
                    // small delay helps ensure layout/DOM updates have settled (matches lyrics usage)
                    setTimeout(() => {
                        try {
                            activeEl.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
                        } catch (e) {
                            // Some engines may throw for certain options; fall through to manual calc below
                        }

                        // clamp to bounds after native call to avoid overscroll on some browsers
                        setTimeout(() => {
                            const maxScroll = Math.max(0, containerEl.scrollHeight - containerEl.clientHeight);

                            // Recalculate rects to detect whether the element is still out of view (common near end)
                            const containerRect = containerEl.getBoundingClientRect();
                            const activeRect = activeEl.getBoundingClientRect();

                            // If native call left the active item beyond the bottom of the container,
                            // explicitly animate to the maxScroll so the list can reach the end.
                            if (activeRect.bottom > containerRect.bottom - 1) {
                                $container.stop(true).animate({ scrollTop: maxScroll }, 220);
                                return;
                            }

                            // If native overscrolled, clamp back to maxScroll
                            if (containerEl.scrollTop > maxScroll) {
                                $container.stop(true).animate({ scrollTop: maxScroll }, 120);
                            }
                        }, 120);
                    }, 10);
                    return;
                } catch (e) {
                    // fall through to manual calculation if something unexpected happens
                }
            }

            // Fallback: compute target so active item is centered in container (manual smooth via jQuery animate)
            const containerRect = containerEl.getBoundingClientRect();
            const activeRect = activeEl.getBoundingClientRect();
            const offsetInContainer = activeRect.top - containerRect.top; // position relative to container viewport

            // target scrollTop = current scrollTop + offsetInContainer - half available space
            const desired = containerEl.scrollTop + offsetInContainer - ((containerEl.clientHeight - activeEl.clientHeight) / 2);
            const maxScroll = Math.max(0, containerEl.scrollHeight - containerEl.clientHeight);
            const targetClamped = Math.min(Math.max(0, desired), maxScroll);

            if (Math.abs(containerEl.scrollTop - targetClamped) > 2) {
                $container.stop(true).animate({ scrollTop: targetClamped }, 220);
            }
        })();
    }

// Call renderPlaylist() after updating playlist or changing track

});

    // Make the viewer draggable
    $(document).ready(function() {
        const $container = $('#mp3ViewerContainer');
        let isDragging = false;
        let startX, startY, initialX, initialY;

        // Use requestAnimationFrame for smooth animations
        function moveContainer(e) {
            if (!isDragging) return;

            requestAnimationFrame(() => {
                const newX = initialX + (e.clientX - startX);
                const newY = initialY + (e.clientY - startY);

                $container.css({
                    transform: `translate(${newX}px, ${newY}px)`
                });
            });
        }

        $(document).on('mousedown', '#mp3ViewerContainer', function(e) {
            if (e.target.id !== 'mp3ViewerContainer' && e.target.id !== 'closeBtn') return;

            isDragging = true;
            startX = e.clientX;
            startY = e.clientY;

            const transform = window.getComputedStyle($container[0]).transform;
            const matrix = new DOMMatrix(transform);
            initialX = matrix.m41;
            initialY = matrix.m42;

            $container.css('cursor', 'grabbing');
        });

        $(document).on('mousemove', moveContainer);

        $(document).on('mouseup', function() {
            isDragging = false;
            $container.css('cursor', '');
        });
    });
})();