{
  "type": "script",
  "enabled": true,
  "name": "Music Player",
  "id": "351c7184-675f-427c-a803-991e7b279f80",
  "content": "// Music Player v4 (Tavern Helper / Spotify Edition)\n(async () => {\ntry {\nconst doc = (window.parent || window).document;\nconsole.log('[EBP] Starting...');\nif (doc.getElementById('ebp-overlay')) { console.log('[EBP] Already loaded, skipping'); return; }\nconst EBP = ((window.parent || window)._ebp = {});\n\n// ── Defaults ──\nconst LS = { PL:'ebp_playlists', APIS:'ebp_apis', VOL:'ebp_volume', MODE:'ebp_play_mode', AI:'ebp_ai_config' };\nconst DEFAULT_APIS = [\n    {id:'netease-gd',name:'NetEase (GDStudio)',enabled:true,searchUrl:'https://music-api.gdstudio.xyz/api.php?types=search&count=5&source=netease&name={{query}}',getUrlApi:'https://music-api.gdstudio.xyz/api.php?types=url&source=netease&id={{id}}',searchResponsePath:'',songIdPath:'id',songNamePath:'name',artistPath:'artist',urlResponsePath:'url'},\n    {id:'qq-vkeys',name:'QQ Music (VKeys)',enabled:true,searchUrl:'https://api.vkeys.cn/v2/music/tencent?word={{query}}',getUrlApi:'https://api.vkeys.cn/v2/music/tencent?id={{id}}',searchResponsePath:'data',songIdPath:'id',songNamePath:'song',artistPath:'singer',urlResponsePath:'data.url'},\n    {id:'netease-vkeys',name:'NetEase (VKeys)',enabled:true,searchUrl:'https://api.vkeys.cn/v2/music/netease?word={{query}}',getUrlApi:'https://api.vkeys.cn/v2/music/netease?id={{id}}',searchResponsePath:'data',songIdPath:'id',songNamePath:'song',artistPath:'singer',urlResponsePath:'data.url'},\n    {id:'kugou-meto',name:'Kugou (Meto)',enabled:true,searchUrl:'https://api.i-meto.com/meting/api?server=kugou&type=search&id={{query}}',getUrlApi:'https://api.i-meto.com/meting/api?server=kugou&type=song&id={{id}}',searchResponsePath:'',songIdPath:'id',songNamePath:'name',artistPath:'artist',urlResponsePath:'[0].url'}\n];\nconst DEFAULT_COVER = \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 80'%3E%3Crect width='80' height='80' fill='%23333'/%3E%3Ctext x='40' y='46' text-anchor='middle' font-size='28' fill='%23555'%3E%E2%99%AA%3C/text%3E%3C/svg%3E\";\n\n// ── State ──\nlet allPlaylists = {}, currentChatKey = 'global', playlist = [];\nlet currentTrack = null, currentIdx = -1;\nconst audio = new Audio();\nlet isPlaying = false, playMode = 0, apis = [], volume = 0.7;\nlet aiConfig = { msgCount: 20, songCount: 8, profile: '' };\nlet aiAbort = false, aiAbortCtrl = null, dragIdx = -1;\n\n// ── Helpers ──\nfunction P(o,p){if(!p)return o;if(p.startsWith('[')){const m=p.match(/^\\[(\\d+)\\]\\.?(.*)$/);if(m&&Array.isArray(o))return m[2]?P(o[parseInt(m[1])],m[2]):o[parseInt(m[1])];}let c=o;for(const k of p.split('.')){if(c==null)return;c=c[k];}return c;}\nfunction fmt(s){return isNaN(s)?'0:00':Math.floor(s/60)+':'+Math.floor(s%60).toString().padStart(2,'0');}\nfunction esc(s){const d=doc.createElement('div');d.textContent=s;return d.innerHTML;}\n\nfunction getChatKey(){const c=SillyTavern.getContext();return c.characterId==null?'global':c.characterId+'_'+(c.chatId||'default');}\nfunction getChatLabel(){return SillyTavern.getContext().name2||'Global';}\nfunction copyTrackName(track,btnEl){\n    const text=track.artist?track.name+' — '+track.artist:track.name;\n    navigator.clipboard.writeText(text).then(()=>{\n        toastr.success(text,'Скопировано');\n        if(btnEl){btnEl.textContent='✓';setTimeout(()=>{btnEl.textContent='📋';},1000);}\n    }).catch(()=>{\n        const ta=doc.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;opacity:0';doc.body.appendChild(ta);ta.select();doc.execCommand('copy');ta.remove();\n        toastr.success(text,'Скопировано');\n        if(btnEl){btnEl.textContent='✓';setTimeout(()=>{btnEl.textContent='📋';},1000);}\n    });\n}\n\nfunction getCover(track){\n    if(track?.pic)return track.pic;\n    try{const ctx=SillyTavern.getContext();const ch=ctx.characters?.[ctx.characterId];if(ch?.avatar)return '/thumbnail?type=avatar&file='+encodeURIComponent(ch.avatar);}catch(e){}\n    return DEFAULT_COVER;\n}\n\nfunction getProfiles(){\n    try{return SillyTavern.getContext().extensionSettings?.connectionManager?.profiles||[];}catch(e){return[];}\n}\n\n// ── Storage ──\nfunction load(){try{const p=localStorage.getItem(LS.PL);if(p)allPlaylists=JSON.parse(p);const a=localStorage.getItem(LS.APIS);apis=a?JSON.parse(a):DEFAULT_APIS.map(x=>({...x}));const v=localStorage.getItem(LS.VOL);if(v)volume=parseFloat(v);const m=localStorage.getItem(LS.MODE);if(m)playMode=parseInt(m);const ai=localStorage.getItem(LS.AI);if(ai)aiConfig={...aiConfig,...JSON.parse(ai)};}catch(e){}}\nfunction saveAll(){try{allPlaylists[currentChatKey]=playlist;localStorage.setItem(LS.PL,JSON.stringify(allPlaylists));}catch(e){}}\nfunction saveApis(){try{localStorage.setItem(LS.APIS,JSON.stringify(apis));}catch(e){}}\nfunction saveVol(){try{localStorage.setItem(LS.VOL,String(volume));}catch(e){}}\nfunction saveMode(){try{localStorage.setItem(LS.MODE,String(playMode));}catch(e){}}\nfunction saveAi(){try{localStorage.setItem(LS.AI,JSON.stringify(aiConfig));}catch(e){}}\nfunction switchToChat(){const k=getChatKey();if(currentChatKey!=='global')allPlaylists[currentChatKey]=playlist;currentChatKey=k;playlist=allPlaylists[k]||[];allPlaylists[k]=playlist;saveAll();renderPlaylist();updatePlTab();}\n\n// ═══ LLM CALL (direct, no ExtBlocks) ═══\nasync function callLLM(messages, profileId){\n    const ctx=SillyTavern.getContext();\n    const CMRS=ctx.ConnectionManagerRequestService;\n    if(!CMRS)throw new Error('ConnectionManagerRequestService недоступен');\n\n    aiAbortCtrl=new AbortController();\n    try{\n        const result=await CMRS.sendRequest(profileId, messages, 2048, {stream:false, signal:aiAbortCtrl.signal, extractData:true, includePreset:false});\n        return(result?.content||'').trim();\n    }finally{aiAbortCtrl=null;}\n}\n\n// ═══ MUSIC API ═══\nasync function searchSongs(query){\n    const enabled=apis.filter(a=>a.enabled),results=[];\n    for(const api of enabled){try{\n        const res=await fetch(api.searchUrl.replace('{{query}}',encodeURIComponent(query)));\n        const json=await res.json(),data=api.searchResponsePath?P(json,api.searchResponsePath):json;\n        const items=Array.isArray(data)?data:(data?[data]:[]);\n        for(const item of items.slice(0,5)){\n            const sid=P(item,api.songIdPath);if(!sid)continue;\n            const name=P(item,api.songNamePath)||'',ar=P(item,api.artistPath);\n            const strAr=Array.isArray(ar)?ar.join(', '):(ar||'');\n            const pic=P(item,'al.picUrl')||P(item,'pic')||P(item,'cover')||P(item,'album.cover_url')||P(item,'songinfo.pic')||null;\n            results.push({name,artist:strAr,full:strAr?name+' - '+strAr:name,pic,_apiId:api.id,_songId:String(sid)});\n        }\n    }catch(e){}}\n    return results;\n}\nasync function getPlayUrl(apiId,songId){const api=apis.find(a=>a.id===apiId);if(!api)return null;try{const r=await fetch(api.getUrlApi.replace('{{id}}',songId));return P(await r.json(),api.urlResponsePath);}catch(e){return null;}}\nfunction testUrl(url){return new Promise(r=>{if(!url){r(false);return;}const t=new Audio();t.preload='metadata';const d=v=>{t.onloadedmetadata=t.onerror=null;t.src='';r(v);};t.onloadedmetadata=()=>d(true);t.onerror=()=>d(false);t.src=url;setTimeout(()=>d(false),6000);});}\n\n// ═══ AUDIO ENGINE ═══\nasync function playTrack(track,idx){\n    if(!track)return;currentTrack=track;currentIdx=idx>=0?idx:playlist.indexOf(track);\n    updateNow();\n    let url=null;\n    if(track._apiId&&track._songId){url=await getPlayUrl(track._apiId,track._songId);if(url&&!(await testUrl(url)))url=null;}\n    if(!url){for(const api of apis.filter(a=>a.enabled)){try{\n        const q=track.full||track.name;const sR=await fetch(api.searchUrl.replace('{{query}}',encodeURIComponent(q)));\n        const sJ=await sR.json(),sD=api.searchResponsePath?P(sJ,api.searchResponsePath):sJ;\n        const items=Array.isArray(sD)?sD:(sD?[sD]:[]);\n        for(const item of items.slice(0,3)){const sid=P(item,api.songIdPath);if(!sid)continue;\n            const u=await getPlayUrl(api.id,String(sid));\n            if(u&&await testUrl(u)){url=u;track._apiId=api.id;track._songId=String(sid);\n                if(!track.pic){track.pic=P(item,'al.picUrl')||P(item,'pic')||P(item,'cover')||null;if(track.pic)updateNow();}\n                break;}}\n        if(url)break;}catch(e){}}}\n    if(!url){toastr.error('Трек не найден: '+(track.full||track.name),'EBP');nextTrack();return;}\n    audio.src=url;audio.volume=volume;audio.play().catch(()=>{});isPlaying=true;\n    updatePlayBtn();renderPlaylist();updateTB();\n}\nfunction togglePlay(){if(!currentTrack&&playlist.length>0){playTrack(playlist[0],0);return;}if(!currentTrack)return;if(isPlaying){audio.pause();isPlaying=false;}else{audio.play();isPlaying=true;}updatePlayBtn();updateTB();}\nfunction nextTrack(){if(!playlist.length)return;if(playMode===1){playTrack(playlist[Math.floor(Math.random()*playlist.length)]);return;}playTrack(playlist[(currentIdx+1)%playlist.length],(currentIdx+1)%playlist.length);}\nfunction prevTrack(){if(!playlist.length)return;const i=(currentIdx-1+playlist.length)%playlist.length;playTrack(playlist[i],i);}\nfunction removeTrack(idx){const w=currentIdx===idx;playlist.splice(idx,1);saveAll();if(w){if(playlist.length>0){currentIdx=Math.min(idx,playlist.length-1);playTrack(playlist[currentIdx],currentIdx);}else{audio.pause();audio.src='';currentTrack=null;isPlaying=false;currentIdx=-1;updatePlayBtn();updateNow();updateTB();}}else if(currentIdx>idx)currentIdx--;renderPlaylist();}\nfunction clearPlaylist(){const h=currentTrack&&playlist.some(t=>t.name===currentTrack.name&&t.artist===currentTrack.artist);playlist.length=0;allPlaylists[currentChatKey]=playlist;saveAll();if(h){audio.pause();audio.src='';currentTrack=null;isPlaying=false;currentIdx=-1;updatePlayBtn();updateNow();updateTB();}renderPlaylist();}\nfunction addToPlaylist(t){if(playlist.some(x=>x.name===t.name&&x.artist===t.artist)){toastr.info('Уже в плейлисте','EBP');return;}playlist.push(t);saveAll();renderPlaylist();toastr.success(t.name,'Добавлено');}\nfunction moveTrack(f,t){if(f===t||f<0||t<0||f>=playlist.length||t>=playlist.length)return;const[m]=playlist.splice(f,1);playlist.splice(t,0,m);if(currentIdx===f)currentIdx=t;else if(f<currentIdx&&t>=currentIdx)currentIdx--;else if(f>currentIdx&&t<=currentIdx)currentIdx++;saveAll();renderPlaylist();}\nconst MI=['↻','↔','⟳'],MT=['Повтор','Случайно','Один трек'];\nfunction cycleMode(){playMode=(playMode+1)%3;saveMode();const b=doc.getElementById('ebp-mode');if(b){b.textContent=MI[playMode];b.title=MT[playMode];}}\n\n// ═══ CHAT ANALYSIS ═══\nlet aiRunning=false;\nasync function analyzeChat(){\n    if(aiRunning){aiAbort=true;if(aiAbortCtrl)aiAbortCtrl.abort();return;}\n    const ctx=SillyTavern.getContext();if(ctx.characterId==null){toastr.error('Откройте чат','EBP');return;}\n\n    const $s=doc.getElementById('ebp-ai-status'),$b=doc.getElementById('ebp-ai-btn'),$bar=doc.getElementById('ebp-ai-bar');\n    function setStatus(html){if($s)$s.innerHTML=html;}\n    function setProgress(pct){if($bar)$bar.style.width=pct+'%';}\n    function setBtnCancel(){if($b)$b.textContent='⏹ Отмена';aiRunning=true;}\n    function setBtnReady(){if($b)$b.textContent='🎵 Подобрать музыку';aiRunning=false;}\n\n    const chat=ctx.chat,mc=Math.min(aiConfig.msgCount,chat.length),sc=aiConfig.songCount;\n    const un=ctx.name1||'User',cn=ctx.name2||'Char';\n    const msgs=chat.slice(-mc).filter(m=>!m.is_system).map(m=>(m.is_user?un:cn)+': '+(m.mes||'').substring(0,400)).join('\\n');\n    const defs=[];const cd=ctx.characters?.[ctx.characterId];\n    if(cd?.data?.description)defs.push('Char: '+cd.data.description.substring(0,500));\n    if(cd?.data?.personality)defs.push('Personality: '+cd.data.personality.substring(0,300));\n    const wantEl=doc.getElementById('ebp-ai-want'),skipEl=doc.getElementById('ebp-ai-skip');\n    const want=(wantEl?.value||'').trim(),skip=(skipEl?.value||'').trim();\n    let sysPr='You are a music recommendation assistant. Analyze the roleplay chat below. Suggest '+sc+' songs matching mood, atmosphere, emotions, themes. Prefer well-known songs.';\n    if(want)sysPr+=' User preferences: '+want+'.';\n    if(skip)sysPr+=' AVOID these genres/artists/styles: '+skip+'.';\n    sysPr+=' Output ONLY valid JSON: [{\"name\":\"Title\",\"artist\":\"Artist\"}, ...]. No other text.';\n    const pr=[{role:'system',content:sysPr},{role:'user',content:(defs.length?defs.join('\\n')+'\\n\\n':'')+'Chat ('+mc+' msgs):\\n'+msgs+'\\n\\nSuggest '+sc+' songs:'}];\n\n    aiAbort=false;setBtnCancel();setProgress(0);\n    try{\n        setStatus('⏳ Отправляю чат на анализ...');setProgress(5);\n        let text=await callLLM(pr,aiConfig.profile);\n        if(aiAbort){setStatus('Отменено');setProgress(0);return;}\n        setProgress(20);\n        text=text.replace(/```json?\\s*/g,'').replace(/```\\s*/g,'').trim();\n        let songs;\n        try{songs=JSON.parse(text);}catch(e){const m=text.match(/\\[[\\s\\S]*\\]/);if(m)songs=JSON.parse(m[0]);else throw new Error('LLM вернула невалидный JSON');}\n        if(!Array.isArray(songs)||!songs.length){setStatus('LLM не дала рекомендаций. Попробуйте ещё раз.');setProgress(0);return;}\n\n        setStatus('Подобрано: <b>'+songs.length+'</b> песен. Ищу в API...');\n        let found=0,failed=0;\n        for(let i=0;i<songs.length;i++){\n            if(aiAbort){setStatus('Отменено. Добавлено: <b>'+found+'</b>');break;}\n            const sn=songs[i].name||'?',sa=songs[i].artist||'';const q=sa?sn+' '+sa:sn;\n            const pct=20+Math.round(((i+1)/songs.length)*80);\n            setProgress(pct);\n            setStatus('Подобрано: <b>'+songs.length+'</b><br>🔍 <b>'+esc(sn)+'</b>'+(sa?' — '+esc(sa):'')+'<br><span style=\"color:var(--t3)\">'+(i+1)+' из '+songs.length+' · ✅ '+found+' · ❌ '+failed+'</span>');\n            const r=await searchSongs(q);\n            if(r.length>0&&!playlist.some(t=>t.name===r[0].name&&t.artist===r[0].artist)){playlist.push(r[0]);saveAll();found++;}else{failed++;}\n        }\n        if(!aiAbort){setStatus('✅ Готово! Добавлено: <b>'+found+'</b> из '+songs.length+(failed?' · не найдено: '+failed:''));setProgress(100);}\n        renderPlaylist();\n    }catch(e){\n        setStatus('❌ Ошибка: '+esc(e.message)+'<br><span style=\"color:var(--t3)\">Нажмите кнопку чтобы попробовать снова</span>');\n        setProgress(0);\n    }finally{setBtnReady();}\n}\n\n// ═══ CSS — Spotify Dark ═══\nconst css=doc.createElement('style');css.id='ebp-styles';\ncss.textContent=`\n#ebp-toolbar-btn{position:relative;cursor:pointer;font-size:18px;opacity:0.6;transition:all 0.2s;padding:2px 4px}\n#ebp-toolbar-btn:hover{opacity:1}\n#ebp-toolbar-btn.active{opacity:1;color:#1DB954}\n#ebp-toolbar-btn .ebp-dot{position:absolute;top:-2px;right:-4px;width:7px;height:7px;border-radius:50%;background:#1DB954;display:none}\n#ebp-toolbar-btn.playing .ebp-dot{display:block;animation:ebp-p 1.5s infinite}\n@keyframes ebp-p{0%,100%{opacity:1}50%{opacity:.3}}\n#ebp-fab{display:none;position:fixed;bottom:70px;right:12px;z-index:1999999;width:44px;height:44px;border-radius:50%;background:#1DB954;color:#000;border:none;box-shadow:0 4px 12px rgba(0,0,0,.4);cursor:pointer;align-items:center;justify-content:center;font-size:20px;-webkit-tap-highlight-color:transparent}\n#ebp-fab:active{transform:scale(0.9)}\n#ebp-fab .ebp-dot{position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:#fff;display:none}\n#ebp-fab.playing .ebp-dot{display:block;animation:ebp-p 1.5s infinite}\n@media(max-width:768px){#ebp-fab{display:flex}}\n\n#ebp-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;height:100dvh;background:rgba(0,0,0,.75);z-index:2000000;display:flex;align-items:center;justify-content:center;padding:20px;overflow-y:auto;-webkit-overflow-scrolling:touch;box-sizing:border-box}\n@media(max-width:768px){#ebp-overlay{padding:10px}}\n@media(max-height:700px){#ebp-cover-wrap{width:100px!important;height:100px!important}#ebp-now{padding:10px 16px 6px}#ebp-ctrl{gap:12px}}\n#ebp-root{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;font-size:13px;width:380px;max-width:100%;max-height:calc(100dvh - 40px);overflow-y:auto;border-radius:12px;\n--bg0:#121212;--bg1:#181818;--bg2:#282828;--bg3:#333;--green:#1DB954;--green2:#1ed760;--t1:#fff;--t2:#b3b3b3;--t3:#727272;\nbackground:var(--bg1);color:var(--t1);border:1px solid rgba(255,255,255,.06);box-shadow:0 16px 48px rgba(0,0,0,.6),0 4px 12px rgba(0,0,0,.4);flex-shrink:0;scrollbar-width:thin}\n#ebp-root::-webkit-scrollbar{width:4px}\n#ebp-root::-webkit-scrollbar-thumb{background:var(--bg3);border-radius:2px}\n@media(max-width:768px){#ebp-root{width:95vw;max-height:80vh;border-radius:16px}}\n\n#ebp-hdr{display:flex;align-items:center;padding:12px 16px;gap:8px;user-select:none;background:var(--bg0)}\n#ebp-hdr-t{flex:1;font-size:12px;font-weight:700;color:var(--green);letter-spacing:.5px}\n#ebp-hdr-n{flex:2;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:center;color:var(--t2)}\n.ebp-hb{background:none;border:none;color:var(--t3);cursor:pointer;font-size:15px;padding:4px;border-radius:50%;transition:all .15s}\n.ebp-hb:hover{color:var(--t1);background:var(--bg2)}\n\n#ebp-now{padding:20px 24px 12px;text-align:center;background:linear-gradient(180deg,var(--bg2) 0%,var(--bg1) 100%)}\n#ebp-cover-wrap{width:180px;height:180px;margin:0 auto 16px;border-radius:8px;overflow:hidden;box-shadow:0 8px 24px rgba(0,0,0,.5)}\n#ebp-cover{width:100%;height:100%;object-fit:cover;display:block}\n#ebp-name{font-size:16px;font-weight:700;margin-bottom:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n#ebp-artist{font-size:13px;color:var(--t2);margin-bottom:14px}\n#ebp-prog{height:4px;background:var(--bg3);border-radius:2px;cursor:pointer;margin-bottom:6px;position:relative;transition:height .15s}\n#ebp-prog:hover{height:6px}\n#ebp-fill{height:100%;background:var(--green);border-radius:2px;width:0%;position:relative;transition:width .1s linear}\n#ebp-prog:hover #ebp-fill::after{content:'';position:absolute;right:-5px;top:50%;transform:translateY(-50%);width:12px;height:12px;background:#fff;border-radius:50%;box-shadow:0 2px 4px rgba(0,0,0,.3)}\n#ebp-time{display:flex;justify-content:space-between;font-size:11px;color:var(--t3);margin-bottom:12px;font-variant-numeric:tabular-nums}\n#ebp-ctrl{display:flex;justify-content:center;align-items:center;gap:16px}\n.ebp-cb{width:32px;height:32px;border:none;border-radius:50%;background:transparent;color:var(--t2);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;transition:all .15s;padding:0}\n.ebp-cb:hover{color:var(--t1);transform:scale(1.05)}\n#ebp-play{width:48px;height:48px;background:var(--green);color:#000;font-size:18px;font-weight:700}\n#ebp-play:hover{background:var(--green2);transform:scale(1.06)}\n#ebp-vol-w{display:flex;align-items:center;gap:8px;margin-top:12px;justify-content:center}\n#ebp-vol-i{font-size:14px;color:var(--t2)}\n#ebp-vol{width:100px;height:4px;-webkit-appearance:none;appearance:none;background:var(--bg3);border-radius:2px;outline:none;cursor:pointer}\n#ebp-vol::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:#fff;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,.3)}\n\n#ebp-tabs{display:flex;background:var(--bg0);border-top:1px solid rgba(255,255,255,.04);border-bottom:1px solid rgba(255,255,255,.04)}\n.ebp-tab{flex:1;padding:10px 0;text-align:center;font-size:11px;font-weight:600;color:var(--t3);cursor:pointer;border:none;background:none;transition:color .15s;border-bottom:2px solid transparent}\n.ebp-tab:hover{color:var(--t2)}\n.ebp-tab.on{color:var(--t1);border-bottom-color:var(--green)}\n#ebp-panels{max-height:300px;overflow-y:auto;scrollbar-width:thin;background:var(--bg1)}\n#ebp-panels::-webkit-scrollbar{width:6px}\n#ebp-panels::-webkit-scrollbar-thumb{background:var(--bg3);border-radius:3px}\n.ebp-pn{display:none;padding:8px}\n.ebp-pn.on{display:block}\n\n.ebp-plh{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;margin-bottom:4px}\n.ebp-plh-l{font-size:12px;color:var(--green);font-weight:700}\n.ebp-plh-c{font-size:10px;color:var(--t3)}\n.ebp-plh-x{font-size:10px;color:var(--t3);cursor:pointer;border:none;background:none;padding:4px 8px;border-radius:12px;font-weight:600;transition:all .15s}\n.ebp-plh-x:hover{color:#e74c3c;background:rgba(231,76,60,.1)}\n\n.ebp-tr{display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:6px;cursor:pointer;transition:background .15s;border:2px solid transparent;position:relative}\n.ebp-tr:hover{background:var(--bg2)}\n.ebp-tr.now{background:rgba(29,185,84,.08)}\n.ebp-tr.now .ebp-tr-n{color:var(--green)}\n.ebp-tr.now::before{content:'';position:absolute;left:0;top:20%;bottom:20%;width:3px;background:var(--green);border-radius:2px}\n.ebp-tr.drag-over{border-top-color:var(--green)}\n.ebp-tr.dragging{opacity:.3}\n.ebp-tr-grip{cursor:grab;color:var(--t3);font-size:10px;flex-shrink:0;opacity:0;transition:opacity .15s;padding:0 2px}\n.ebp-tr:hover .ebp-tr-grip{opacity:.6}\n.ebp-tr-grip:hover{opacity:1!important}\n.ebp-tr-grip:active{cursor:grabbing}\n.ebp-tr-img{width:40px;height:40px;border-radius:4px;object-fit:cover;flex-shrink:0;background:var(--bg3)}\n.ebp-tr-info{flex:1;min-width:0}\n.ebp-tr-n{font-size:13px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.ebp-tr-a{font-size:11px;color:var(--t2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.ebp-tr-copy,.ebp-tr-del{opacity:0;cursor:pointer;font-size:14px;color:var(--t3);flex-shrink:0;padding:0 4px;transition:all .15s}\n.ebp-tr:hover .ebp-tr-copy,.ebp-tr:hover .ebp-tr-del{opacity:.5}\n.ebp-tr-copy:hover{opacity:1!important;color:var(--green)}\n.ebp-tr-del{font-size:18px}\n.ebp-tr-del:hover{opacity:1!important;color:#e74c3c}\n\n.ebp-sw{display:flex;gap:8px;margin-bottom:10px}\n.ebp-in{flex:1;padding:8px 12px;background:var(--bg2);color:var(--t1);border:1px solid var(--bg3);border-radius:20px;font-size:13px;outline:none;transition:border .15s}\n.ebp-in:focus{border-color:var(--t2)}\n.ebp-go{padding:8px 18px;background:var(--green);color:#000;border:none;border-radius:20px;font-size:13px;font-weight:700;cursor:pointer;transition:background .15s}\n.ebp-go:hover{background:var(--green2)}\n\n.ebp-sr{display:flex;align-items:center;gap:10px;padding:8px;border-radius:6px;transition:background .15s}\n.ebp-sr:hover{background:var(--bg2)}\n.ebp-sr-img{width:40px;height:40px;border-radius:4px;object-fit:cover;flex-shrink:0;background:var(--bg3)}\n.ebp-sr-i{flex:1;min-width:0}\n.ebp-sr-n{font-size:13px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.ebp-sr-a{font-size:11px;color:var(--t2)}\n.ebp-sr-copy,.ebp-sr-play,.ebp-sr-add{width:28px;height:28px;border-radius:50%;border:1px solid var(--t3);background:none;color:var(--t2);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;transition:all .15s;flex-shrink:0}\n.ebp-sr-copy:hover{border-color:var(--green);color:var(--green);transform:scale(1.1)}\n.ebp-sr-play:hover{border-color:var(--green);color:var(--green);transform:scale(1.1)}\n.ebp-sr-add:hover{border-color:var(--t1);color:var(--t1);transform:scale(1.1)}\n\n.ebp-lbl{font-size:12px;color:var(--t2);margin-bottom:8px;font-weight:600}\n.ebp-row{display:flex;align-items:center;gap:10px;margin-bottom:10px}\n.ebp-row span{font-size:12px;min-width:110px;color:var(--t2)}\n.ebp-num{width:64px;padding:6px 10px;background:var(--bg2);color:var(--t1);border:1px solid var(--bg3);border-radius:6px;font-size:13px;text-align:center;outline:none}\n.ebp-sel{padding:6px 10px;background:var(--bg2);color:var(--t1);border:1px solid var(--bg3);border-radius:6px;font-size:13px;outline:none;cursor:pointer;max-width:180px}\n#ebp-ai-bar-wrap{height:4px;background:var(--bg3);border-radius:2px;margin-top:8px;overflow:hidden}\n#ebp-ai-bar{height:100%;width:0%;background:var(--green);border-radius:2px;transition:width 0.3s ease}\n#ebp-ai-st{font-size:11px;color:var(--t2);margin-top:8px;min-height:16px;word-break:break-word;line-height:1.5}\n#ebp-ai-st b{color:var(--green)}\n.ebp-api{display:flex;align-items:center;gap:10px;padding:10px;border-radius:8px;background:var(--bg2);margin-bottom:6px}\n.ebp-api-n{flex:1;font-size:12px;font-weight:600}\n.ebp-api-t{cursor:pointer;accent-color:var(--green)}\n.ebp-empty{text-align:center;color:var(--t3);padding:30px 12px;font-size:13px;line-height:1.6}\n`;\ndoc.head.appendChild(css);\n\n// ═══ TOOLBAR BUTTON ═══\nconst tb=doc.createElement('div');\ntb.id='ebp-toolbar-btn';tb.className='fa-solid fa-music interactable';\ntb.title='EBP Music Player';tb.innerHTML='<span class=\"ebp-dot\"></span>';tb.tabIndex=0;\nconst lf=doc.getElementById('leftSendForm');\nif(lf)lf.appendChild(tb);\nelse{tb.style.cssText='position:fixed;bottom:10px;left:10px;z-index:99999;width:34px;height:34px;display:flex;align-items:center;justify-content:center;background:#181818;border:1px solid #333;border-radius:50%;font-size:16px;color:#b3b3b3;cursor:pointer';doc.body.appendChild(tb);}\n// ── Mobile FAB (floating action button) ──\nconst fab=doc.createElement('button');fab.id='ebp-fab';fab.innerHTML='♫<span class=\"ebp-dot\"></span>';\ndoc.body.appendChild(fab);\n\nfunction updateTB(){tb.classList.toggle('playing',isPlaying);tb.classList.toggle('active',isPlaying||currentTrack!=null);fab.classList.toggle('playing',isPlaying);}\n\n// ═══ PLAYER DOM ═══\nconst overlay=doc.createElement('div');overlay.id='ebp-overlay';overlay.style.display='none';\nconst root=doc.createElement('div');root.id='ebp-root';\nroot.innerHTML=`\n<div id=\"ebp-hdr\"><span id=\"ebp-hdr-t\">♫ EBP</span><span id=\"ebp-hdr-n\">---</span><button class=\"ebp-hb\" id=\"ebp-unload\" title=\"Выгрузить\">⏻</button><button class=\"ebp-hb\" id=\"ebp-close\" title=\"Скрыть\" onclick=\"var o=document.getElementById('ebp-overlay');if(o)o.style.display='none'\">✕</button></div>\n<div id=\"ebp-now\">\n  <div id=\"ebp-cover-wrap\"><img id=\"ebp-cover\" src=\"${DEFAULT_COVER}\" alt=\"\"></div>\n  <div id=\"ebp-name\">Нет трека</div>\n  <div id=\"ebp-artist\"></div>\n  <div id=\"ebp-prog\"><div id=\"ebp-fill\"></div></div>\n  <div id=\"ebp-time\"><span id=\"ebp-tc\">0:00</span><span id=\"ebp-td\">0:00</span></div>\n  <div id=\"ebp-ctrl\">\n    <button class=\"ebp-cb\" id=\"ebp-mode\" title=\"Повтор\">↻</button>\n    <button class=\"ebp-cb\" id=\"ebp-prev\">◀◀</button>\n    <button class=\"ebp-cb\" id=\"ebp-play\" title=\"Играть\">▶</button>\n    <button class=\"ebp-cb\" id=\"ebp-next\">▶▶</button>\n    <button class=\"ebp-cb\" id=\"ebp-mode2\" style=\"visibility:hidden\">·</button>\n  </div>\n  <div id=\"ebp-vol-w\"><span id=\"ebp-vol-i\">🔈</span><input type=\"range\" id=\"ebp-vol\" min=\"0\" max=\"1\" step=\"0.01\"></div>\n</div>\n<div id=\"ebp-tabs\">\n  <button class=\"ebp-tab on\" data-t=\"pl\">Плейлист</button>\n  <button class=\"ebp-tab\" data-t=\"sr\">Поиск</button>\n  <button class=\"ebp-tab\" data-t=\"ai\">AI</button>\n  <button class=\"ebp-tab\" data-t=\"api\">API</button>\n</div>\n<div id=\"ebp-panels\">\n  <div class=\"ebp-pn on\" id=\"ebp-p-pl\"></div>\n  <div class=\"ebp-pn\" id=\"ebp-p-sr\">\n    <div class=\"ebp-sw\"><input class=\"ebp-in\" id=\"ebp-si\" placeholder=\"Поиск треков...\"><button class=\"ebp-go\" id=\"ebp-sb\">Найти</button></div>\n    <div id=\"ebp-sres\"></div>\n  </div>\n  <div class=\"ebp-pn\" id=\"ebp-p-ai\">\n    <div class=\"ebp-lbl\">Анализ чата и подбор музыки</div>\n    <div class=\"ebp-row\"><span>Сообщений:</span><input class=\"ebp-num\" type=\"number\" id=\"ebp-ai-m\" min=\"5\" max=\"100\"></div>\n    <div class=\"ebp-row\"><span>Песен:</span><input class=\"ebp-num\" type=\"number\" id=\"ebp-ai-s\" min=\"1\" max=\"30\"></div>\n    <div class=\"ebp-row\"><span>Коннект:</span><select class=\"ebp-sel\" id=\"ebp-ai-p\"></select></div>\n    <div class=\"ebp-lbl\" style=\"margin-top:6px\">Пожелания (необязательно)</div>\n    <input class=\"ebp-in\" id=\"ebp-ai-want\" placeholder=\"Хочу: рок, грустное, 80-е...\" style=\"width:100%;margin-bottom:6px;border-radius:8px;box-sizing:border-box\">\n    <input class=\"ebp-in\" id=\"ebp-ai-skip\" placeholder=\"Не хочу: попса, k-pop...\" style=\"width:100%;margin-bottom:8px;border-radius:8px;box-sizing:border-box\">\n    <button class=\"ebp-go\" id=\"ebp-ai-btn\" style=\"width:100%;padding:12px;margin-top:4px;border-radius:24px;font-size:14px\">🎵 Подобрать музыку</button>\n    <div id=\"ebp-ai-bar-wrap\"><div id=\"ebp-ai-bar\"></div></div>\n    <div id=\"ebp-ai-st\"></div>\n  </div>\n  <div class=\"ebp-pn\" id=\"ebp-p-api\"></div>\n</div>`;\noverlay.appendChild(root);doc.documentElement.appendChild(overlay);\n\n// ═══ UI ═══\nfunction updatePlayBtn(){const b=doc.getElementById('ebp-play');if(b)b.textContent=isPlaying?'❚❚':'▶';}\nfunction updateNow(){\n    const $n=doc.getElementById('ebp-name'),$a=doc.getElementById('ebp-artist'),$h=doc.getElementById('ebp-hdr-n'),$c=doc.getElementById('ebp-cover');\n    if($n)$n.textContent=currentTrack?currentTrack.name:'Нет трека';\n    if($a)$a.textContent=currentTrack?(currentTrack.artist||''):'';\n    if($h)$h.textContent=currentTrack?(currentTrack.full||currentTrack.name):'---';\n    if($c)$c.src=getCover(currentTrack);\n}\nfunction updatePlTab(){const $t=root.querySelector('.ebp-tab[data-t=\"pl\"]');if($t){const l=getChatLabel(),c=playlist.length;$t.textContent=c>0?l+' ('+c+')':'Плейлист';}}\n\nfunction populateProfileSelect(){\n    const $sel=doc.getElementById('ebp-ai-p');if(!$sel)return;\n    const profiles=getProfiles();\n    $sel.innerHTML=profiles.map(p=>'<option value=\"'+esc(p.id)+'\"'+(aiConfig.profile===p.id?' selected':'')+'>'+esc(p.name)+' ('+esc(p.model||'?')+')</option>').join('');\n    if((!aiConfig.profile||!profiles.find(p=>p.id===aiConfig.profile))&&profiles.length>0){aiConfig.profile=profiles[0].id;saveAi();}\n}\n\nfunction renderPlaylist(){\n    const $=doc.getElementById('ebp-p-pl');if(!$)return;updatePlTab();\n    if(!playlist.length){$.innerHTML='<div class=\"ebp-plh\"><span class=\"ebp-plh-l\">♫ '+esc(getChatLabel())+'</span></div><div class=\"ebp-empty\">Плейлист пуст<br>Поиск или AI добавят треки</div>';return;}\n    let h='<div class=\"ebp-plh\"><span class=\"ebp-plh-l\">♫ '+esc(getChatLabel())+'</span><span class=\"ebp-plh-c\">'+playlist.length+' трек(ов)</span><button class=\"ebp-plh-x\" id=\"ebp-clr\">Очистить</button></div>';\n    h+=playlist.map((t,i)=>{const p=currentTrack&&t.name===currentTrack.name&&t.artist===currentTrack.artist;\n        return '<div class=\"ebp-tr'+(p?' now':'')+'\" draggable=\"true\" data-i=\"'+i+'\">'+\n        '<span class=\"ebp-tr-grip\">⠿</span>'+\n        '<img class=\"ebp-tr-img\" src=\"'+esc(getCover(t))+'\" onerror=\"this.src=\\''+DEFAULT_COVER+'\\'\">'+\n        '<div class=\"ebp-tr-info\"><div class=\"ebp-tr-n\">'+esc(t.name)+'</div>'+(t.artist?'<div class=\"ebp-tr-a\">'+esc(t.artist)+'</div>':'')+'</div>'+\n        '<span class=\"ebp-tr-copy\" data-c=\"'+i+'\" title=\"Копировать\">📋</span><span class=\"ebp-tr-del\" data-d=\"'+i+'\">×</span></div>';}).join('');\n    $.innerHTML=h;\n    $J('#ebp-p-pl').off('.ebp');\n    $J('#ebp-p-pl').on('click.ebp','.ebp-tr',function(e){if($J(e.target).closest('.ebp-tr-del,.ebp-tr-grip,.ebp-tr-copy').length)return;playTrack(playlist[parseInt(this.dataset.i)],parseInt(this.dataset.i));});\n    $J('#ebp-p-pl').on('click.ebp','.ebp-tr-del',function(e){e.stopPropagation();removeTrack(parseInt(this.dataset.d));});\n    $J('#ebp-p-pl').on('click.ebp','.ebp-tr-copy',function(e){e.stopPropagation();const t=playlist[parseInt(this.dataset.c)];if(t)copyTrackName(t,this);});\n    $J('#ebp-p-pl').on('click.ebp','#ebp-clr',clearPlaylist);\n    // drag-and-drop uses native events (they work on elements in parent doc)\n    $.querySelectorAll('.ebp-tr').forEach(el=>{\n        el.addEventListener('dragstart',e=>{dragIdx=parseInt(el.dataset.i);el.classList.add('dragging');e.dataTransfer.effectAllowed='move';e.dataTransfer.setData('text/plain',dragIdx);});\n        el.addEventListener('dragend',()=>{el.classList.remove('dragging');$.querySelectorAll('.ebp-tr').forEach(x=>x.classList.remove('drag-over'));dragIdx=-1;});\n        el.addEventListener('dragover',e=>{e.preventDefault();e.dataTransfer.dropEffect='move';$.querySelectorAll('.ebp-tr').forEach(x=>x.classList.remove('drag-over'));if(parseInt(el.dataset.i)!==dragIdx)el.classList.add('drag-over');});\n        el.addEventListener('dragleave',()=>el.classList.remove('drag-over'));\n        el.addEventListener('drop',e=>{e.preventDefault();el.classList.remove('drag-over');const to=parseInt(el.dataset.i);if(dragIdx>=0&&dragIdx!==to)moveTrack(dragIdx,to);});\n    });\n}\n\nlet lastSearchResults=[];\nfunction renderSR(res){\n    lastSearchResults=res;\n    const $=doc.getElementById('ebp-sres');if(!$)return;\n    if(!res.length){$.innerHTML='<div class=\"ebp-empty\">Ничего не найдено</div>';return;}\n    $.innerHTML=res.map((t,i)=>'<div class=\"ebp-sr\"><img class=\"ebp-sr-img\" src=\"'+esc(getCover(t))+'\" onerror=\"this.src=\\''+DEFAULT_COVER+'\\'\"><div class=\"ebp-sr-i\"><div class=\"ebp-sr-n\">'+esc(t.name)+'</div><div class=\"ebp-sr-a\">'+esc(t.artist)+'</div></div><button class=\"ebp-sr-copy\" data-i=\"'+i+'\" title=\"Копировать\">📋</button><button class=\"ebp-sr-play\" data-i=\"'+i+'\" title=\"Послушать\">▶</button><button class=\"ebp-sr-add\" data-i=\"'+i+'\" title=\"В плейлист\">+</button></div>').join('');\n    $J('#ebp-sres').off('.ebp')\n        .on('click.ebp','.ebp-sr-copy',function(){const t=lastSearchResults[parseInt(this.dataset.i)];if(t)copyTrackName(t,this);})\n        .on('click.ebp','.ebp-sr-add',function(){addToPlaylist(lastSearchResults[parseInt(this.dataset.i)]);})\n        .on('click.ebp','.ebp-sr-play',async function(){\n            const t=lastSearchResults[parseInt(this.dataset.i)];if(!t)return;\n            this.textContent='...';\n            let url=null;\n            if(t._apiId&&t._songId){url=await getPlayUrl(t._apiId,t._songId);if(url&&!(await testUrl(url)))url=null;}\n            if(!url){toastr.error('Не удалось получить ссылку','EBP');this.textContent='▶';return;}\n            audio.src=url;audio.volume=volume;audio.play().catch(()=>{});\n            isPlaying=true;currentTrack=t;currentIdx=-1;\n            updateNow();updatePlayBtn();updateTB();\n            this.textContent='▶';\n        });\n}\n\nfunction renderApi(){\n    const $=doc.getElementById('ebp-p-api');if(!$)return;\n    $.innerHTML='<div class=\"ebp-lbl\">Источники музыки</div>'+apis.map((a,i)=>'<div class=\"ebp-api\"><input type=\"checkbox\" class=\"ebp-api-t\" data-i=\"'+i+'\" '+(a.enabled?'checked':'')+'><span class=\"ebp-api-n\">'+esc(a.name)+'</span></div>').join('')+'<button class=\"ebp-go\" id=\"ebp-api-r\" style=\"width:100%;margin-top:10px;background:var(--bg2);color:var(--t2);border:1px solid var(--bg3)\">По умолчанию</button>';\n    $J('#ebp-p-api').off('.ebp');\n    $J('#ebp-p-api').on('change.ebp','.ebp-api-t',function(){apis[parseInt(this.dataset.i)].enabled=this.checked;saveApis();});\n    $J('#ebp-p-api').on('click.ebp','#ebp-api-r',function(){apis=DEFAULT_APIS.map(a=>({...a}));saveApis();renderApi();});\n}\n\n// ═══ EVENTS (all via jQuery — works across iframe boundary) ═══\nconst $J=jQuery;\n\nconst EBP_TOGGLE=\"var o=document.getElementById('ebp-overlay');if(o)o.style.display=o.style.display==='none'?'flex':'none'\";\nconst EBP_CLOSE=\"var o=document.getElementById('ebp-overlay');if(o)o.style.display='none'\";\n\ntb.setAttribute('onclick',EBP_TOGGLE);\nfab.setAttribute('onclick',EBP_TOGGLE);\noverlay.setAttribute('onclick',\"if(event.target===this){\"+EBP_CLOSE+\"}\");\n\naudio.addEventListener('timeupdate',()=>{if(!audio.duration)return;const f=doc.getElementById('ebp-fill'),c=doc.getElementById('ebp-tc');if(f)f.style.width=((audio.currentTime/audio.duration)*100)+'%';if(c)c.textContent=fmt(audio.currentTime);});\naudio.addEventListener('loadedmetadata',()=>{const d=doc.getElementById('ebp-td');if(d)d.textContent=fmt(audio.duration);});\naudio.addEventListener('ended',()=>{if(playMode===2){audio.currentTime=0;audio.play();return;}nextTrack();});\n\n$J(doc).on('click','#ebp-prog',function(e){if(!audio.duration)return;const r=this.getBoundingClientRect();audio.currentTime=((e.clientX-r.left)/r.width)*audio.duration;});\n$J(doc).on('input','#ebp-vol',function(){volume=parseFloat(this.value);audio.volume=volume;saveVol();});\nconst $vol=doc.getElementById('ebp-vol');if($vol)$vol.value=volume;\n\n$J(doc).on('click','#ebp-play',togglePlay);\n$J(doc).on('click','#ebp-prev',prevTrack);\n$J(doc).on('click','#ebp-next',nextTrack);\n$J(doc).on('click','#ebp-mode',cycleMode);\n\n$J(doc).on('click','.ebp-tab',function(){\n    $J(root).find('.ebp-tab').removeClass('on');\n    $J(root).find('.ebp-pn').removeClass('on');\n    $J(this).addClass('on');\n    const t=this.dataset.t;$J('#ebp-p-'+t).addClass('on');\n    if(t==='pl')renderPlaylist();if(t==='api')renderApi();if(t==='ai')populateProfileSelect();\n});\n\n$J(doc).on('click','#ebp-sb',async function(){const inp=doc.getElementById('ebp-si');const q=inp?.value.trim();if(!q)return;this.textContent='Ищу...';this.disabled=true;renderSR(await searchSongs(q));this.textContent='Найти';this.disabled=false;});\n$J(doc).on('keydown','#ebp-si',function(e){if(e.key==='Enter')$J('#ebp-sb').trigger('click');});\n$J(doc).on('click','#ebp-ai-btn',function(){analyzeChat();});\n$J(doc).on('change','#ebp-ai-m',function(){aiConfig.msgCount=parseInt(this.value)||20;saveAi();});\n$J(doc).on('change','#ebp-ai-s',function(){aiConfig.songCount=parseInt(this.value)||8;saveAi();});\n$J(doc).on('change','#ebp-ai-p',function(){aiConfig.profile=this.value;saveAi();});\nconst $am=doc.getElementById('ebp-ai-m');if($am)$am.value=aiConfig.msgCount;\nconst $as=doc.getElementById('ebp-ai-s');if($as)$as.value=aiConfig.songCount;\n\nconst stx=SillyTavern.getContext();\nif(stx.eventSource&&stx.event_types)stx.eventSource.on(stx.event_types.CHAT_CHANGED,()=>switchToChat());\n\n// ═══ INIT ═══\nload();audio.volume=volume;\ncurrentChatKey=getChatKey();playlist=allPlaylists[currentChatKey]||[];allPlaylists[currentChatKey]=playlist;\nrenderPlaylist();populateProfileSelect();\ndoc.getElementById('ebp-mode').textContent=MI[playMode];\ndoc.getElementById('ebp-mode').title=MT[playMode];\nupdateTB();toastr.success('Нажмите ♫','EBP');\nconsole.log('[EBP] Loaded OK');\n} catch(e) { console.error('[EBP] FATAL:', e); toastr.error('EBP: ' + e.message); }\n})();\n",
  "info": "",
  "button": {
    "enabled": true,
    "buttons": []
  },
  "data": {},
  "export_with": {
    "data": true,
    "button": true
  }
}