[油猴/via浏览器插件]  唱片机

主要为了移动端via浏览器开发.PC端也能使用是顺便,而且PC端应该有更好的方案.但是移动端这就是最优解

.任意网页的悬浮播放器。换页面不断流,三行迷你歌词,一键收藏进唱片夹。后台静默播放不抢焦点,掉线自动重连。

优点
•
跨页不断流,切标签继续放
•
三种皮肤:经典卡片 / 细长底栏 / 黑胶
•
玻璃质感迷你歌词,贴边不挡内容
•
唱片夹收藏,歌词跨页保持

依赖第三方接口(api.qijieya.cn),失效可以找支持解析vip歌曲的meting api接口,百度一大把。
•
部分歌无歌词,QQ音源数据较弱(网易云接口好用)
•
会多一个后台标签页(别关,关了会自动重开)
•
数据存本地,换设备不同步

注意
•
首次点歌会开后台标签,别关它
•
支持 GM_openInTab 的脚本管理器(Via / Tampermonkey)
•
第三方接口仅供个人学习,勿传播侵权内容

主界面:
image.png(117.42 KB)

播放器样式一览:
image.png(40.01 KB)

image.png(55.82 KB)

image.png(48.96 KB)

// ==UserScript==
// @name         🎵 唱片机
// @namespace    kiro.local.musicbox.b
// @version      3.4.3
// @description  独立标签页常驻播放,跨页不断流,后台失活自动重建;唱片夹 + 三行迷你歌词(跨页保持)。
// @match        *://*/*
// @run-at       document-end
// @grant        GM_openInTab
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_xmlhttpRequest
// @connect      music.163.com
// @connect      api.qijieya.cn
// ==/UserScript==

(() => {
  'use strict';

  const BAR_ID    = 'rmb2-bar';
  const PANEL_ID  = 'rmb2-panel';
  const FAB_ID    = 'rmb2-fab';
  const VINYL_ID  = 'rmb2-vinyl';
  const STYLE_ID  = 'rmb2-style';
  const LYRIC_ID  = 'rmb2-lyric';
  const STORE_KEY = 'rmb2_state_v3';
  const SKIN_KEY  = 'rmb2_skin';
  const QUIET_KEY = 'rmb2_quiet';
  const SRV_KEY   = 'rmb2_srv';
  const FAV_KEY   = 'rmb2_favs';
  const LRC_KEY   = 'rmb2_lrc_on';    // 歌词开关(跨页保持)
  const RELAY_HASH = '#rmb2bg';

  const K_FLAG  = 'rmb2_bg_flag';
  const K_CMD   = 'rmb2_cmd';
  const K_STATE = 'rmb2_state_gm';

  const API_BASE  = 'https://api.qijieya.cn/meting/';
  const DEF_SERVER = 'netease';

  const HEARTBEAT_FRESH_MS = 3000;
  const OPEN_DEBOUNCE_MS   = 5000;

  const SERVERS = [
    { k:'netease', label:'网易云' },
    { k:'tencent', label:'QQ音乐' }
  ];
  const PLAYLISTS = {
    netease: [
      { pl:'3778678',   kw:'热门', label:'🔥 热歌榜' },
      { pl:'3779629',   kw:'新歌', label:'🆕 新歌榜' },
      { pl:'19723756',  kw:'飙升', label:'📈 飙升榜' },
      { pl:'2884035',   kw:'原创', label:'🎸 原创榜' }
    ],
    tencent: [
      { pl:'26',    kw:'热门', label:'🐧 热歌榜' },
      { pl:'27',    kw:'新歌', label:'🆕 新歌榜' },
      { pl:'62',    kw:'飙升', label:'📈 飙升榜' },
      { pl:'28',    kw:'网络', label:'🌐 网络榜' }
    ]
  };
  const SKINS = [
    { k:'classic', label:'📻 经典卡片' },
    { k:'slim',    label:'▬ 细长底栏' },
    { k:'vinyl',   label:'💿 黑胶CD'  }
  ];

  const isRelay = (String(location.hash || '').indexOf(RELAY_HASH) >= 0);

  /* =========================================================
     GM 值桥
     ========================================================= */
  function gmSet(k, v) { try { if (typeof GM_setValue === 'function') GM_setValue(k, JSON.stringify(v)); } catch (e) {} }
  function gmGet(k) {
    try {
      if (typeof GM_getValue === 'function') {
        const r = GM_getValue(k, null);
        if (r === null || r === undefined) return null;
        try { return JSON.parse(r); } catch (e) { return (typeof r === 'string' ? r : null); }
      }
    } catch (e) {}
    return null;
  }
  function gmDel(k) { try { if (typeof GM_deleteValue === 'function') GM_deleteValue(k); } catch (e) {} }

  /* =========================================================
     工具
     ========================================================= */
  const $ = (s, r = document) => r.querySelector(s);
  const fmt = t => { if (!isFinite(t)) return '0:00'; const m = Math.floor(t / 60), s = Math.floor(t % 60); return m + ':' + (s < 10 ? '0' : '') + s; };
  const esc = t => String(t == null ? '' : t).replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
  const txt = (id, v) => { const e = document.getElementById(id); if (e) e.textContent = v; };

  function fetchT(url, ms) {
    const ctrl = new AbortController();
    const t = setTimeout(() => ctrl.abort(), ms || 15000);
    return fetch(url, { mode:'cors', signal: ctrl.signal }).finally(() => clearTimeout(t));
  }
  function api(query, asText) {
    const url = API_BASE + query;
    return fetchT(url, 20000).then(r => {
      if (!r.ok) throw new Error('HTTP ' + r.status);
      return r.text();
    }).then(t => {
      if (asText) return t;
      try { return JSON.parse(t); } catch (e) { return t; }
    });
  }
  function pickArr(data) {
    if (Array.isArray(data)) return data;
    if (data && Array.isArray(data.data)) return data.data;
    return [];
  }
  function fetchText(url, ms) {
    return new Promise((resolve, reject) => {
      if (typeof GM_xmlhttpRequest === 'function') {
        let done = false;
        const tm = setTimeout(() => { if (!done) { done = true; reject(new Error('timeout')); } }, ms || 12000);
        try {
          GM_xmlhttpRequest({
            method: 'GET', url: url,
            onload: r => { if (done) return; done = true; clearTimeout(tm); resolve(r.responseText || ''); },
            onerror: () => { if (done) return; done = true; clearTimeout(tm); reject(new Error('net')); },
            ontimeout: () => { if (done) return; done = true; clearTimeout(tm); reject(new Error('timeout')); }
          });
        } catch (e) { if (!done) { done = true; clearTimeout(tm); reject(e); } }
      } else {
        fetchT(url, ms).then(r => r.text()).then(resolve).catch(reject);
      }
    });
  }

  /* =========================================================
     ★ 后台播放标签页
     ========================================================= */
  if (isRelay) { runRelayTab(); return; }

  /* =========================================================
     主页面
     ========================================================= */
  let skin = 'classic', quiet = true, songs = [], idx = -1, server = DEF_SERVER, expanded = false;
  let favs = [];
  let curSource = 'pl';
  let lrcOn = false;
  let lrcList = [];
  let lrcCurKey = '';
  let lrcCurLine = -1;

  let st = { playing:false, name:'', artist:'', pic:'', cur:0, dur:0 };

  function getSkin() { try { return localStorage.getItem(SKIN_KEY) || 'classic'; } catch (e) { return 'classic'; } }
  function setSkin(s) { skin = s; try { localStorage.setItem(SKIN_KEY, s); } catch (e) {} applySkin(); }
  function getServer() { try { return localStorage.getItem(SRV_KEY) || DEF_SERVER; } catch (e) { return DEF_SERVER; } }
  function setServer(sv) { server = sv; try { localStorage.setItem(SRV_KEY, sv); } catch (e) {} if (curSource === 'pl') renderTabs(); const sel = $('#rmb2-srv'); if (sel) sel.value = sv; }
  function getQuiet() { try { return localStorage.getItem(QUIET_KEY) !== '0'; } catch (e) { return true; } }
  function setQuiet(v) { quiet = v; try { localStorage.setItem(QUIET_KEY, v ? '1' : '0'); } catch (e) {} document.documentElement.classList.toggle('rmb2-quiet', quiet); renderQuietUI(); }
  function renderQuietUI() { const qb = $('#rmb2-quiet-btn'); if (qb) qb.textContent = quiet ? '👻 隐身开' : '👁 隐身关'; }

  /* ===== 唱片夹 ===== */
  function loadFavs() {
    try {
      const d = JSON.parse(localStorage.getItem(FAV_KEY) || '[]');
      return Array.isArray(d) ? d.filter(x => x && (x.id || x.name)) : [];
    } catch (e) { return []; }
  }
  function saveFavs() { try { localStorage.setItem(FAV_KEY, JSON.stringify(favs)); } catch (e) {} }
  function favKey(s) { return String((s && (s.id || s.name)) || ''); }
  function isFav(s) { const k = favKey(s); return !!k && favs.some(x => favKey(x) === k); }
  function toggleFav(s) {
    if (!s) return;
    const k = favKey(s);
    if (!k) return;
    const i = favs.findIndex(x => favKey(x) === k);
    if (i >= 0) favs.splice(i, 1);
    else favs.unshift({ id:s.id||'', name:s.name||'', artist:s.artist||'', pic:s.pic||'', url:s.url||'' });
    saveFavs();
    if (curSource === 'fav') { songs = favs.slice(); if (idx >= songs.length) idx = -1; renderList(); }
    else { markFavInList(); }
    updateFavCount();
  }

  function applySkin() {
    document.documentElement.classList.remove('rmb2-skin-classic','rmb2-skin-slim','rmb2-skin-vinyl');
    document.documentElement.classList.add('rmb2-skin-' + skin);
    const bar = document.getElementById(BAR_ID); if (bar) bar.dataset.skin = skin;
    const sel = document.getElementById('rmb2-skin'); if (sel) sel.value = skin;
    showWidget();
    render();
  }

  function saveState() { try { localStorage.setItem(STORE_KEY, JSON.stringify({ songs, idx, server, time:(st.cur||0), playing:!!st.playing, ts:Date.now() })); } catch (e) {} }
  function loadState() {
    try { const d = JSON.parse(localStorage.getItem(STORE_KEY) || 'null'); if (!d || !Array.isArray(d.songs) || !d.songs.length) return null; if (Date.now() - (d.ts || 0) > 3600000) return null; return d; } catch (e) { return null; }
  }

  /* ===== 样式 ===== */
  function injectStyle() {
    if (document.getElementById(STYLE_ID)) return;
    const s = document.createElement('style');
    s.id = STYLE_ID;
    s.textContent = `
      #${BAR_ID},#${PANEL_ID},#${FAB_ID},#${VINYL_ID},#${LYRIC_ID}{font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;box-sizing:border-box;}
      #${BAR_ID} *,#${PANEL_ID} *,#${FAB_ID} *,#${VINYL_ID} *,#${LYRIC_ID} *{box-sizing:border-box}
      #${BAR_ID},#${PANEL_ID},#${FAB_ID},#${VINYL_ID},#${LYRIC_ID},#${BAR_ID} *,#${PANEL_ID} *,#${FAB_ID} *,#${VINYL_ID} *,#${LYRIC_ID} *{line-height:1.2 !important;}
      #${BAR_ID} .row,#${BAR_ID} .t,#${BAR_ID} .a,#${BAR_ID} .ct,#${BAR_ID} .ct button{line-height:1 !important;}

      @keyframes rmb2-spin{to{transform:rotate(360deg)}}
      @keyframes rmb2-disc-spin{to{transform:rotate(360deg)}}
      @keyframes rmb2-marquee{0%{transform:translateX(0)}100%{transform:translateX(-50%)}}

      #${FAB_ID}{position:fixed;right:10px;bottom:12px;z-index:2147483644;width:44px;height:44px;border-radius:50%;
        background:radial-gradient(circle at 32% 28%,#ffd89b,#e8a33d 62%,#b8701a);
        box-shadow:0 6px 18px rgba(0,0,0,.45),inset 0 2px 4px rgba(255,255,255,.4);
        cursor:pointer;display:flex;align-items:center;justify-content:center;transition:opacity .3s ease,transform .15s ease;}
      #${FAB_ID}:active{transform:scale(.88)}
      #${FAB_ID} svg{width:22px;height:22px;fill:#5a3208}

      .rmb2-quiet #${FAB_ID}{opacity:.45}
      .rmb2-quiet #${FAB_ID}:hover{opacity:1}
      .rmb2-quiet #${BAR_ID}:not(.playing){opacity:.5}
      .rmb2-quiet #${BAR_ID}:hover{opacity:1}
      .rmb2-quiet #${VINYL_ID}:not(.playing){opacity:.5}
      .rmb2-quiet #${VINYL_ID}:hover{opacity:1}
      .rmb2-quiet #${BAR_ID}.playing,.rmb2-quiet #${VINYL_ID}.playing{opacity:.92}
      .rmb2-quiet #${BAR_ID}.playing:hover,.rmb2-quiet #${VINYL_ID}.playing:hover{opacity:1}

      #${BAR_ID}{position:fixed;right:10px;bottom:12px;z-index:2147483645;width:min(92vw,200px);
        background:linear-gradient(180deg,rgba(40,30,19,.94),rgba(24,18,12,.97));
        border:1px solid rgba(255,214,140,.24);border-radius:12px;box-shadow:0 6px 20px rgba(0,0,0,.5);
        color:#f0e2ce;overflow:hidden;display:none;transition:opacity .3s ease;}
      #${BAR_ID}.show{display:block}
      #${BAR_ID} .row{display:flex;align-items:center;flex-wrap:nowrap;gap:7px;padding:6px 8px;cursor:pointer}
      #${BAR_ID} .art{width:28px;height:28px;flex:0 0 28px;border-radius:7px;overflow:hidden;background:#241d14;position:relative}
      #${BAR_ID} .art img{width:100%;height:100%;object-fit:cover;display:block}
      #${BAR_ID} .art .sp{position:absolute;inset:0;border-radius:7px;border:2px solid transparent;border-top-color:#e8a33d;animation:rmb2-spin 2.4s linear infinite;opacity:0}
      #${BAR_ID}.playing .art .sp{opacity:1}
      #${BAR_ID} .ic{flex:1 1 auto;min-width:0}
      #${BAR_ID} .t{font-size:11.5px;font-weight:600;color:#f5e8d6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
      #${BAR_ID} .a{font-size:9.5px;color:#9c8b70;margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
      #${BAR_ID} .ct{display:flex;align-items:center;gap:0;flex:0 0 auto}
      #${BAR_ID} .ct button{background:none;border:0;color:#e8d9c0;font-size:13px;padding:2px 3px;cursor:pointer;line-height:1}
      #${BAR_ID} #rmb2-play{font-size:17px;color:#e8a33d}
      #${BAR_ID} #rmb2-lyric-btn{font-size:11px}
      #${BAR_ID} #rmb2-lyric-btn.on{color:#e8a33d;font-weight:700}
      #${BAR_ID} .pg{display:flex;align-items:center;gap:6px;font-size:9px;color:#8a7a63;padding:0 9px 6px}
      #${BAR_ID} .pg input[type=range]{flex:1;-webkit-appearance:none;height:3px;border-radius:3px;background:rgba(255,214,140,.16);outline:none}
      #${BAR_ID} .pg input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:9px;height:9px;border-radius:50%;background:linear-gradient(180deg,#f2be63,#d4912b)}
      #${BAR_ID} .fill{position:absolute;left:0;bottom:0;height:2px;width:0;background:linear-gradient(90deg,#f2be63,#d4912b);box-shadow:0 0 6px rgba(232,163,61,.7);transition:width .2s linear}
      #${BAR_ID} .pg2{display:none;align-items:center;gap:6px;font-size:9px;color:#8a7a63}
      #${BAR_ID} .pg2 input[type=range]{flex:1 1 auto;min-width:30px;-webkit-appearance:none;height:3px;border-radius:3px;background:rgba(255,214,140,.16);outline:none}
      #${BAR_ID} .pg2 input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:9px;height:9px;border-radius:50%;background:linear-gradient(180deg,#f2be63,#d4912b)}

      .rmb2-skin-classic #${VINYL_ID}{display:none!important}

      .rmb2-skin-slim #${BAR_ID}{left:0;right:0;bottom:0;width:100%;border-radius:0;border-left:0;border-right:0;border-bottom:0;
        border-top:1px solid rgba(255,214,140,.28);background:linear-gradient(180deg,rgba(30,23,14,.97),rgba(18,14,9,.99));
        box-shadow:0 -2px 12px rgba(0,0,0,.5);padding-left:env(safe-area-inset-left);padding-right:env(safe-area-inset-right);}
      .rmb2-skin-slim #${BAR_ID}.show{display:block;animation:rmb2-rise .28s ease}
      @keyframes rmb2-rise{from{transform:translateY(100%)}to{transform:translateY(0)}}
      .rmb2-skin-slim #${BAR_ID} .row{padding:5px 10px;gap:8px;height:34px;flex-wrap:nowrap}
      .rmb2-skin-slim #${BAR_ID} .pg{display:none!important}
      .rmb2-skin-slim #${BAR_ID} .pg2{display:flex;order:3;flex:1 1 auto;padding:0 4px;margin:0;min-width:80px}
      .rmb2-skin-slim #${BAR_ID} .pg2 span{display:inline-block;flex:0 0 auto;font-size:9px;color:#8a7a63;font-variant-numeric:tabular-nums}
      .rmb2-skin-slim #${BAR_ID} .art{order:1;width:22px;height:22px;flex:0 0 22px;border-radius:50%;overflow:hidden}
      .rmb2-skin-slim #${BAR_ID} .art img{border-radius:50%}
      .rmb2-skin-slim #${BAR_ID} .art .sp{border-radius:50%;display:none}
      .rmb2-skin-slim #${BAR_ID} .ic{order:2;display:flex;align-items:baseline;gap:7px;min-width:0;flex:0 1 auto;max-width:38%}
      .rmb2-skin-slim #${BAR_ID} .t{font-size:11.5px;flex:0 0 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
      .rmb2-skin-slim #${BAR_ID} .a{font-size:9.5px;margin-top:0;flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
      .rmb2-skin-slim #${BAR_ID} .ct{order:4;display:flex;align-items:center;flex:0 0 auto;gap:0}
      .rmb2-skin-slim #${BAR_ID} .ct button{font-size:14px;padding:2px 4px;color:#e8d9c0}
      .rmb2-skin-slim #${BAR_ID} #rmb2-play{font-size:17px;color:#e8a33d}
      .rmb2-skin-slim #${BAR_ID} .fill{height:2px}
      .rmb2-skin-slim #${VINYL_ID}{display:none!important}
      .rmb2-skin-slim #${FAB_ID}{bottom:48px}
      .rmb2-skin-slim #${PANEL_ID}{bottom:48px}

      .rmb2-skin-vinyl #${BAR_ID}{display:none!important}
      .rmb2-skin-vinyl #${FAB_ID}{display:none!important}

      #${VINYL_ID}{position:fixed;right:12px;bottom:12px;z-index:2147483645;display:none;flex-direction:column;align-items:flex-end;transition:opacity .3s ease;}
      .rmb2-skin-vinyl #${VINYL_ID}.show{display:flex}
      #${VINYL_ID} .stage{position:relative;width:88px;height:88px;cursor:pointer;border-radius:50%;box-shadow:0 8px 24px rgba(0,0,0,.6),inset 0 0 0 3px rgba(0,0,0,.5)}
      #${VINYL_ID} .disc{position:absolute;inset:0;border-radius:50%;overflow:hidden;background:#1a1a1c;animation:rmb2-disc-spin 8s linear infinite;animation-play-state:paused}
      #${VINYL_ID}.playing .disc{animation-play-state:running}
      #${VINYL_ID} .disc img{width:100%;height:100%;object-fit:cover;display:block;border-radius:50%}
      #${VINYL_ID} .disc .hole{position:absolute;left:50%;top:50%;width:9px;height:9px;border-radius:50%;transform:translate(-50%,-50%);background:#0d0a06;border:2px solid rgba(255,214,140,.5);z-index:4}
      #${VINYL_ID} .disc::after{content:"";position:absolute;inset:0;border-radius:50%;background:repeating-radial-gradient(circle at 50% 50%,rgba(0,0,0,.34) 0 1.4px,rgba(0,0,0,0) 1.4px 3px),linear-gradient(120deg,rgba(255,255,255,.22),transparent 42%,transparent 60%,rgba(255,255,255,.1));pointer-events:none;z-index:2}
      #${VINYL_ID} .arm{position:absolute;top:-5px;right:3px;width:5px;height:48px;border-radius:3px;background:linear-gradient(180deg,#d9c7a6,#8f7a55);transform-origin:top center;transform:rotate(30deg);transition:transform .45s ease;box-shadow:0 2px 6px rgba(0,0,0,.6);z-index:7}
      #${VINYL_ID}.playing .arm{transform:rotate(7deg)}
      #${VINYL_ID} .arm::before{content:"";position:absolute;top:-5px;left:50%;transform:translateX(-50%);width:13px;height:13px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#f0e2ce,#a8927a);box-shadow:0 2px 5px rgba(0,0,0,.6)}
      #${VINYL_ID} .ti{position:absolute;left:50%;top:50%;z-index:6;width:74px;height:16px;overflow:hidden;white-space:nowrap;transform:translate(-50%,-50%);pointer-events:none}
      #${VINYL_ID} .ti .mq{display:inline-block;white-space:nowrap;animation:rmb2-marquee 8s linear infinite;animation-play-state:paused;font-size:10px;font-weight:700;color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.95),0 0 6px rgba(0,0,0,.8);padding-left:0}
      #${VINYL_ID} .ti.flow .mq{animation-play-state:running}
      #${VINYL_ID} .vctrl{position:absolute;left:0;right:0;bottom:-2px;z-index:6;display:flex;align-items:center;justify-content:center;gap:2px;pointer-events:none}
      #${VINYL_ID} .vctrl button{pointer-events:auto;background:rgba(20,16,10,.66);border:1px solid rgba(255,214,140,.4);color:#f5e8d6;border-radius:50%;cursor:pointer;line-height:1;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px)}
      #${VINYL_ID} .vctrl .side{width:20px;height:20px;font-size:10px;opacity:.95}
      #${VINYL_ID} .vctrl .side:hover{background:rgba(232,163,61,.4)}
      #${VINYL_ID} .vctrl #rmb2-v-play{width:28px;height:28px;font-size:14px;color:#ffd79a;background:rgba(38,28,16,.85);border-color:rgba(232,163,61,.6)}
      #${VINYL_ID} .vctrl #rmb2-v-play:hover{background:rgba(232,163,61,.5)}
      #${VINYL_ID} .vctrl #rmb2-v-lyric.on{color:#e8a33d;border-color:rgba(232,163,61,.7)}
      .rmb2-skin-vinyl #${PANEL_ID}{bottom:12px;right:12px}

      html.rmb2-open #${BAR_ID}{ bottom:auto !important; top:12px !important; }
      html.rmb2-open.rmb2-skin-slim #${BAR_ID}{left:0 !important;right:0 !important;top:0 !important;bottom:auto !important;border-radius:0 0 12px 12px;border-bottom:1px solid rgba(255,214,140,.28);border-top:0;}
      html.rmb2-open #${VINYL_ID}{ bottom:auto !important; top:12px !important; }
      html.rmb2-open #${PANEL_ID}{ max-height:64vh; }

      /* ===== 迷你歌词条(三行 · 玻璃质感 · 与卡片同宽)===== */
      #${LYRIC_ID}{position:fixed;z-index:2147483643;display:none;width:min(92vw,200px);
        padding:4px 10px;border-radius:12px;text-align:center;
        background:linear-gradient(180deg,rgba(48,36,22,.42),rgba(20,15,9,.52));
        border:1px solid rgba(255,214,140,.32);
        box-shadow:0 6px 20px rgba(0,0,0,.42), inset 0 1px 0 rgba(255,255,255,.14);
        -webkit-backdrop-filter:blur(14px) saturate(1.25);backdrop-filter:blur(14px) saturate(1.25);
        color:#f0e2ce;overflow:hidden;transition:opacity .3s ease;
        right:10px; bottom:84px;
      }
      #${LYRIC_ID}.show{display:block}
      #${LYRIC_ID} .ly-line{font-size:11px;line-height:1.35;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .25s,opacity .25s}
      #${LYRIC_ID} .ly-line.up,#${LYRIC_ID} .ly-line.down{color:#c9bda3;opacity:.82;text-shadow:0 1px 2px rgba(0,0,0,.75)}
      #${LYRIC_ID} .ly-line.mid{color:#ffe0a3;font-weight:700;font-size:12px;text-shadow:0 1px 3px rgba(0,0,0,.9),0 0 8px rgba(232,163,61,.45)}
      #${LYRIC_ID} .ly-line.blank{color:transparent;text-shadow:none}

      .rmb2-skin-slim #${LYRIC_ID}{left:10px;right:10px;bottom:42px;width:auto;text-align:center}
      .rmb2-skin-vinyl #${LYRIC_ID}{right:106px;bottom:22px}

      html.rmb2-lyric-open #${BAR_ID}{opacity:.4}
      html.rmb2-lyric-open #${BAR_ID}:hover{opacity:1}
      html.rmb2-lyric-open #${VINYL_ID}{opacity:.4}
      html.rmb2-lyric-open #${VINYL_ID}:hover{opacity:1}
      html.rmb2-lyric-open #${LYRIC_ID}{opacity:.9}
      html.rmb2-lyric-open #${LYRIC_ID}:hover{opacity:1}

      #${PANEL_ID}{position:fixed;right:10px;bottom:12px;z-index:2147483646;width:min(94vw,340px);max-height:78vh;display:none;flex-direction:column;
        background:linear-gradient(180deg,rgba(40,30,19,.99),rgba(22,17,12,.99));border:1px solid rgba(255,214,140,.28);border-radius:16px;
        box-shadow:0 12px 40px rgba(0,0,0,.65);color:#f0e2ce;overflow:hidden;}
      #${PANEL_ID}.show{display:flex}
      #${PANEL_ID} .head{padding:11px 13px 8px;border-bottom:1px solid rgba(255,214,140,.14)}
      #${PANEL_ID} .brand{font-size:14px;font-weight:700;letter-spacing:1px;color:#ffd79a;display:flex;align-items:center;gap:8px}
      #${PANEL_ID} .brand .dot{width:8px;height:8px;border-radius:50%;background:#e8a33d;box-shadow:0 0 8px #e8a33d}
      #${PANEL_ID} .hbtns{margin-left:auto;display:flex;gap:5px}
      #${PANEL_ID} .hbtns button{font-size:10.5px;padding:4px 8px;border-radius:20px;cursor:pointer;border:1px solid rgba(255,214,140,.3);background:rgba(255,214,140,.08);color:#ffd79a;font-weight:400;letter-spacing:0;white-space:nowrap}
      #${PANEL_ID} .hint{margin-top:6px;font-size:10px;color:#9c8b70;line-height:1.5}
      #${PANEL_ID} .hint b{color:#e8a33d}

      #${PANEL_ID} .selrow{display:flex;gap:8px;margin-top:8px;align-items:center}
      #${PANEL_ID} .selrow .fld{flex:1;min-width:0;display:flex;align-items:center;gap:7px}
      #${PANEL_ID} .selrow label{flex:0 0 auto;font-size:11px;color:#a89272;letter-spacing:.5px;white-space:nowrap;margin:0}
      #${PANEL_ID} select{flex:1 1 auto;min-width:0;width:auto;height:31px;padding:0 26px 0 10px;border-radius:9px;cursor:pointer;border:1px solid rgba(255,214,140,.28);background:rgba(0,0,0,.35);color:#ffd79a;font-size:12px;outline:none;appearance:none;-webkit-appearance:none;background-image:linear-gradient(45deg,transparent 50%,#ffd79a 50%),linear-gradient(135deg,#ffd79a 50%,transparent 50%);background-position:calc(100% - 15px) 50%,calc(100% - 10px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat}
      #${PANEL_ID} select option{background:#241a10;color:#f0e2ce}

      #${PANEL_ID} .tabs{display:flex;gap:6px;margin-top:8px;overflow-x:auto;scrollbar-width:none}
      #${PANEL_ID} .tabs::-webkit-scrollbar{display:none}
      #${PANEL_ID} .tabs button{flex:0 0 auto;padding:5px 11px;border-radius:20px;cursor:pointer;border:1px solid rgba(255,214,140,.22);background:transparent;color:#a89272;font-size:11.5px}
      #${PANEL_ID} .tabs button.on{background:linear-gradient(180deg,#f2be63,#d4912b);color:#3a2107;border-color:transparent;font-weight:700}
      #${PANEL_ID} .tabs button.favtab.on{background:linear-gradient(180deg,#ff8fa3,#e05476);color:#fff}
      #${PANEL_ID} .sbar{display:flex;gap:8px;margin-top:8px}
      #${PANEL_ID} .sbar input{flex:1;min-width:0;padding:8px 11px;border-radius:10px;border:1px solid rgba(255,214,140,.24);background:rgba(0,0,0,.28);color:#f0e2ce;font-size:13px;outline:none}
      #${PANEL_ID} .sbar input::placeholder{color:#8a7a63}
      #${PANEL_ID} .sbar button{padding:0 14px;border:0;border-radius:10px;cursor:pointer;background:linear-gradient(180deg,#f2be63,#d4912b);color:#3a2107;font-weight:800;font-size:13px}
      #${PANEL_ID} .list{flex:1;overflow-y:auto;padding:5px 6px 8px;-webkit-overflow-scrolling:touch;min-height:120px}
      #${PANEL_ID} .tip{padding:34px 20px;text-align:center;color:#8a7a63;font-size:12.5px;line-height:1.8}
      #${PANEL_ID} .tip.err{color:#ff8f8f}
      #${PANEL_ID} .row2{display:flex;align-items:center;gap:10px;padding:7px 9px;border-radius:9px;cursor:pointer}
      #${PANEL_ID} .row2:active{background:rgba(255,214,140,.09)}
      #${PANEL_ID} .row2.playing{background:rgba(232,163,61,.16);box-shadow:inset 2px 0 0 #e8a33d}
      #${PANEL_ID} .row2 .no{width:19px;flex:0 0 19px;text-align:center;color:#8a7a63;font-size:11.5px;font-variant-numeric:tabular-nums}
      #${PANEL_ID} .row2.playing .no{color:#e8a33d;font-weight:700}
      #${PANEL_ID} .row2 img{width:36px;height:36px;flex:0 0 36px;border-radius:7px;object-fit:cover;background:#241d14}
      #${PANEL_ID} .row2 .in{flex:1;min-width:0}
      #${PANEL_ID} .row2 .nm{font-size:12.5px;font-weight:500;color:#f5e8d6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
      #${PANEL_ID} .row2 .ar{font-size:10.5px;color:#9c8b70;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
      #${PANEL_ID} .row2.playing .nm{color:#ffd79a}
      #${PANEL_ID} .row2 .fav{flex:0 0 auto;background:none;border:0;color:#6f6350;font-size:16px;padding:2px 4px;cursor:pointer;line-height:1}
      #${PANEL_ID} .row2 .fav.on{color:#ff6b81}
    `;
    document.head.appendChild(s);
  }

  function buildDOM() {
    if (document.getElementById(BAR_ID)) return;

    const fab = document.createElement('div');
    fab.id = FAB_ID; fab.title = '打开唱片机';
    fab.innerHTML = `<svg viewBox="0 0 24 24"><path d="M12 3v10.55A4 4 0 1 0 14 17V7h4V3h-6z"/></svg>`;
    fab.addEventListener('click', () => { setExpanded(true); });
    (document.body || document.documentElement).appendChild(fab);

    const bar = document.createElement('div');
    bar.id = BAR_ID;
    bar.innerHTML = `
      <div class="row">
        <div class="art"><img id="rmb2-cv" alt=""><div class="sp"></div></div>
        <div class="ic">
          <div class="t" id="rmb2-tt">唱片机</div>
          <div class="a" id="rmb2-aa">点一下展开</div>
        </div>
        <div class="ct">
          <button type="button" id="rmb2-prev">⏮</button>
          <button type="button" id="rmb2-play">▶</button>
          <button type="button" id="rmb2-next">⏭</button>
          <button type="button" id="rmb2-lyric-btn" title="歌词">词</button>
        </div>
        <div class="pg2">
          <span id="rmb2-cur2">0:00</span>
          <input type="range" id="rmb2-seek2" value="0" min="0" max="100">
          <span id="rmb2-dur2">0:00</span>
        </div>
      </div>
      <div class="pg">
        <span id="rmb2-cur">0:00</span>
        <input type="range" id="rmb2-seek" value="0" min="0" max="100">
        <span id="rmb2-dur">0:00</span>
      </div>
      <div class="fill" id="rmb2-fill"></div>
    `;
    (document.body || document.documentElement).appendChild(bar);

    const vinyl = document.createElement('div');
    vinyl.id = VINYL_ID;
    vinyl.innerHTML = `
      <div class="stage" id="rmb2-stage">
        <div class="disc"><img id="rmb2-v-cv" alt=""><div class="hole"></div></div>
        <div class="ti" id="rmb2-v-ti"><span class="mq" id="rmb2-v-mq">唱片机</span></div>
        <div class="vctrl">
          <button type="button" class="side" id="rmb2-v-prev" title="上一曲">⏮</button>
          <button type="button" id="rmb2-v-play" title="播放/暂停">▶</button>
          <button type="button" class="side" id="rmb2-v-next" title="下一曲">⏭</button>
          <button type="button" class="side" id="rmb2-v-lyric" title="歌词">词</button>
        </div>
        <div class="arm"></div>
      </div>
    `;
    (document.body || document.documentElement).appendChild(vinyl);

    const lyric = document.createElement('div');
    lyric.id = LYRIC_ID;
    lyric.innerHTML = `
      <div class="ly-line up" id="rmb2-ly-up"></div>
      <div class="ly-line mid" id="rmb2-ly-mid">♪</div>
      <div class="ly-line down" id="rmb2-ly-down"></div>
    `;
    (document.body || document.documentElement).appendChild(lyric);

    const panel = document.createElement('div');
    panel.id = PANEL_ID;
    panel.innerHTML = `
      <div class="head">
        <div class="brand">
          <span class="dot"></span>唱片机 <span id="rmb2-bgstat" style="font-size:9px;color:#9c8b70;font-weight:400"></span>
          <div class="hbtns">
            <button type="button" id="rmb2-quiet-btn">👻 隐身开</button>
            <button type="button" id="rmb2-reopen">⟳ 后台</button>
            <button type="button" id="rmb2-down">▾ 收起</button>
          </div>
        </div>
        <div class="hint">独立标签页常驻播放,跨页不断流,后台失活自动重建</div>
        <div class="selrow">
          <div class="fld">
            <label>皮肤</label>
            <select id="rmb2-skin"></select>
          </div>
          <div class="fld">
            <label>音源</label>
            <select id="rmb2-srv"></select>
          </div>
        </div>
        <div class="tabs" id="rmb2-tabs"></div>
        <div class="sbar">
          <input id="rmb2-q" type="text" placeholder="搜索歌曲 / 歌手…" autocomplete="off">
          <button id="rmb2-go" type="button">搜索</button>
        </div>
      </div>
      <div class="list" id="rmb2-list"><div class="tip">展开后加载…</div></div>
    `;
    (document.body || document.documentElement).appendChild(panel);

    const sk = document.getElementById('rmb2-skin');
    if (sk) sk.innerHTML = SKINS.map(s => `<option value="${s.k}">${s.label}</option>`).join('');
    const sv = document.getElementById('rmb2-srv');
    if (sv) sv.innerHTML = SERVERS.map(s => `<option value="${s.k}">${s.label}</option>`).join('');

    const stop = fn => e => { e.stopPropagation(); fn(); };
    const barEl = document.getElementById(BAR_ID);
    barEl.querySelector('#rmb2-play').addEventListener('click', stop(togglePlay));
    barEl.querySelector('#rmb2-prev').addEventListener('click', stop(prevTrack));
    barEl.querySelector('#rmb2-next').addEventListener('click', stop(nextTrack));
    barEl.querySelector('#rmb2-lyric-btn').addEventListener('click', stop(() => { toggleLyric(); }));
    barEl.querySelector('#rmb2-seek').addEventListener('input', e => seekTo(Number(e.target.value)));
    barEl.querySelector('#rmb2-seek2').addEventListener('input', e => seekTo(Number(e.target.value)));
    barEl.querySelector('.row').addEventListener('click', () => toggleExpanded());

    const vEl = document.getElementById(VINYL_ID);
    vEl.querySelector('#rmb2-v-play').addEventListener('click', stop(togglePlay));
    vEl.querySelector('#rmb2-v-prev').addEventListener('click', stop(prevTrack));
    vEl.querySelector('#rmb2-v-next').addEventListener('click', stop(nextTrack));
    vEl.querySelector('#rmb2-v-lyric').addEventListener('click', stop(() => { toggleLyric(); }));
    vEl.querySelector('#rmb2-stage').addEventListener('click', () => toggleExpanded());

    panel.querySelector('#rmb2-down').addEventListener('click', () => setExpanded(false));
    panel.querySelector('#rmb2-quiet-btn').addEventListener('click', () => setQuiet(!quiet));
    panel.querySelector('#rmb2-reopen').addEventListener('click', () => { forceOpenBg(); });
    panel.querySelector('#rmb2-skin').addEventListener('change', e => setSkin(e.target.value));
    panel.querySelector('#rmb2-srv').addEventListener('change', e => {
      setServer(e.target.value);
      if (curSource === 'pl') { const first = (PLAYLISTS[server] || [])[0]; if (first) loadPlaylist(first.pl, first.kw); }
    });
    panel.querySelector('#rmb2-go').addEventListener('click', doSearch);
    panel.querySelector('#rmb2-q').addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); });
    panel.querySelector('#rmb2-tabs').addEventListener('click', e => {
      const b = e.target.closest('button'); if (!b) return;
      panel.querySelectorAll('#rmb2-tabs button').forEach(x => x.classList.remove('on'));
      b.classList.add('on');
      if (b.dataset.fav === '1') { showFavs(); }
      else { loadPlaylist(b.dataset.pl, b.dataset.kw); }
    });

    const ssel = panel.querySelector('#rmb2-skin'); if (ssel) ssel.value = skin;
    const vsl  = panel.querySelector('#rmb2-srv');  if (vsl)  vsl.value  = server;

    renderQuietUI(); renderTabs(); applySkin();
  }

  /* =========================================================
     迷你歌词
     ========================================================= */
  /* 只更新 UI 开关状态,不写存储(供启动恢复用) */
  function applyLyricUI() {
    const el = document.getElementById(LYRIC_ID);
    if (el) el.classList.toggle('show', lrcOn);
    document.documentElement.classList.toggle('rmb2-lyric-open', lrcOn);
    const b1 = document.getElementById('rmb2-lyric-btn'); if (b1) b1.classList.toggle('on', lrcOn);
    const b2 = document.getElementById('rmb2-v-lyric'); if (b2) b2.classList.toggle('on', lrcOn);
  }

  function toggleLyric(force) {
    lrcOn = (typeof force === 'boolean') ? force : !lrcOn;
    try { localStorage.setItem(LRC_KEY, lrcOn ? '1' : '0'); } catch (e) {}
    applyLyricUI();
    if (lrcOn) { ensureLrc(); renderLyric(true); }
  }

  function parseLRC(text) {
    const out = [];
    if (!text) return out;
    const lines = String(text).split(/\r?\n/);
    const re = /\[(\d{1,2}):(\d{1,2})(?:[.:](\d{1,3}))?\]/g;
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      let m; const stamps = [];
      re.lastIndex = 0;
      while ((m = re.exec(line)) !== null) {
        const mm = Number(m[1]) || 0, ss = Number(m[2]) || 0;
        let frac = m[3] ? Number(m[3]) : 0;
        if (m[3] && m[3].length === 2) frac = frac / 100;
        else if (m[3] && m[3].length === 1) frac = frac / 10;
        else if (m[3] && m[3].length === 3) frac = frac / 1000;
        stamps.push(mm * 60 + ss + frac);
      }
      const content = line.replace(re, '').trim();
      if (!stamps.length) continue;
      if (!content) continue;
      stamps.forEach(t => out.push({ t: t, s: content }));
    }
    out.sort((a, b) => a.t - b.t);
    return out;
  }

  function ensureLrc() {
    const key = favKey(currentPlayingSong() || { name: st.name });
    if (!key || key === lrcCurKey) return;
    lrcCurKey = key;
    lrcList = []; lrcCurLine = -1;
    setLyricLines('', '♪', '');
    const src = currentPlayingSong() || {};
    const sid = src.id || '';
    if (!sid) { setLyricLines('', '暂无歌词', ''); return; }

    const netease = 'https://music.163.com/api/song/lyric?id=' + encodeURIComponent(sid) + '&lv=-1&kv=-1&tv=-1';
    const meting  = API_BASE + '?server=' + (server || DEF_SERVER) + '&type=lrc&id=' + encodeURIComponent(sid);

    fetchText(netease, 10000).then(t => {
      let lrc = '';
      try { const j = JSON.parse(t); lrc = (j && j.lrc && j.lrc.lyric) || ''; } catch (e) {}
      if (lrc && /\d{1,2}:\d{1,2}/.test(lrc)) return lrc;
      throw new Error('no-lrc');
    }).catch(() => fetchText(meting, 10000).then(t => {
      let lrc = t || '';
      try { const j = JSON.parse(t); lrc = (j && (j.lyric || (j.lrc && j.lrc.lyric))) || lrc; } catch (e) {}
      return lrc;
    })).then(lrc => {
      const nowKey = favKey(currentPlayingSong() || { name: st.name });
      if (nowKey !== lrcCurKey) return;
      lrcList = parseLRC(lrc);
      lrcCurLine = -1;
      renderLyric(true);
    }).catch(() => {
      const nowKey = favKey(currentPlayingSong() || { name: st.name });
      if (nowKey !== lrcCurKey) return;
      setLyricLines('', '暂无歌词', '');
    });
  }

  function setLyricLines(up, mid, down) {
    const a = document.getElementById('rmb2-ly-up'), b = document.getElementById('rmb2-ly-mid'), c = document.getElementById('rmb2-ly-down');
    if (a) a.textContent = up || '';
    if (b) b.textContent = mid || '';
    if (c) c.textContent = down || '';
  }

  function renderLyric(force) {
    if (!lrcOn) return;
    if (!lrcList.length) return;
    const cur = st.cur || 0;
    let k = -1;
    for (let i = 0; i < lrcList.length; i++) { if (lrcList[i].t <= cur + 0.15) k = i; else break; }
    if (k === lrcCurLine && !force) return;
    lrcCurLine = k;
    const up   = (k - 1 >= 0) ? lrcList[k - 1].s : '';
    const mid  = (k >= 0)     ? lrcList[k].s     : '';
    const down = (k + 1 < lrcList.length) ? lrcList[k + 1].s : '';
    setLyricLines(up, mid || '♪', down);
  }

  /* =========================================================
     列表 / 榜单 / 搜索
     ========================================================= */
  function renderTabs() {
    const box = document.getElementById('rmb2-tabs'); if (!box) return;
    const list = PLAYLISTS[server] || [];
    let html = `<button type="button" class="favtab" data-fav="1"${curSource === 'fav' ? ' class="favtab on"' : ''}>唱片夹<span id="rmb2-favcount"></span></button>`;
    html += list.map((p, i) => `<button type="button" data-pl="${p.pl}" data-kw="${p.kw || ''}"${(curSource === 'pl' && i === 0) ? ' class="on"' : ''}>${p.label}</button>`).join('');
    box.innerHTML = html;
    updateFavCount();
  }
  function updateFavCount() {
    const el = document.getElementById('rmb2-favcount');
    if (el) el.textContent = favs.length ? ' (' + favs.length + ')' : '';
  }
  function showFavs() {
    curSource = 'fav';
    songs = favs.slice();
    if (idx >= songs.length) idx = -1;
    renderList();
    updateFavCount();
  }

  function toggleExpanded() { setExpanded(!expanded); }
  function setExpanded(on) {
    expanded = on;
    const panel = document.getElementById(PANEL_ID);
    if (!panel) return;
    document.documentElement.classList.toggle('rmb2-open', on);
    if (on) {
      panel.classList.add('show');
      if (!songs.length) { const first = (PLAYLISTS[server] || [])[0]; if (first) loadPlaylist(first.pl, first.kw); }
      else { curSource === 'fav' ? showFavs() : renderList(); }
    } else { panel.classList.remove('show'); }
    showWidget();
    render();
  }
  function showWidget() {
    const bar = document.getElementById(BAR_ID), fab = document.getElementById(FAB_ID), vinyl = document.getElementById(VINYL_ID);
    const hasSong = songs.length || st.name;
    if (hasSong) {
      if (fab) fab.style.display = 'none';
      if (skin === 'vinyl') { if (vinyl) vinyl.classList.add('show'); if (bar) bar.classList.remove('show'); }
      else { if (bar) bar.classList.add('show'); if (vinyl) vinyl.classList.remove('show'); }
    } else {
      if (bar) bar.classList.remove('show');
      if (vinyl) vinyl.classList.remove('show');
      if (fab) fab.style.display = 'flex';
    }
  }
  function render() {
    const bar = document.getElementById(BAR_ID);
    if (bar) {
      bar.classList.toggle('playing', !!st.playing);
      const cv = bar.querySelector('#rmb2-cv'); if (cv) cv.src = st.pic || '';
      txt('rmb2-tt', st.name || '唱片机');
      txt('rmb2-aa', st.artist || '点一下展开');
      txt('rmb2-play', st.playing ? '⏸' : '▶');
      const curStr = fmt(st.cur), durStr = fmt(st.dur);
      txt('rmb2-cur', curStr); txt('rmb2-dur', durStr);
      txt('rmb2-cur2', curStr); txt('rmb2-dur2', durStr);
      if (st.dur > 0) {
        const pct = st.cur / st.dur * 100;
        const skEl = bar.querySelector('#rmb2-seek'); if (skEl) skEl.value = pct;
        const sk2 = bar.querySelector('#rmb2-seek2'); if (sk2) sk2.value = pct;
        const fl = bar.querySelector('#rmb2-fill'); if (fl) fl.style.width = pct + '%';
      }
    }
    const vinyl = document.getElementById(VINYL_ID);
    if (vinyl) {
      vinyl.classList.toggle('playing', !!st.playing);
      const vcv = vinyl.querySelector('#rmb2-v-cv'); if (vcv) vcv.src = st.pic || '';
      txt('rmb2-v-play', st.playing ? '⏸' : '▶');
      setMarquee(st.name || '唱片机');
    }
    if (lrcOn) renderLyric(false);
    markPlaying();
  }
  function setMarquee(str) {
    const ti = document.getElementById('rmb2-v-ti'), mq = document.getElementById('rmb2-v-mq');
    if (!ti || !mq) return;
    const text = str + '   ·   ' + (st.artist || '');
    if (mq.textContent !== text) mq.textContent = text;
    mq.style.animation = 'none'; void mq.offsetWidth; mq.style.animation = '';
    const boxW = ti.clientWidth, oneW = mq.scrollWidth;
    if (oneW > boxW) { ti.classList.add('flow'); mq.style.animationPlayState = st.playing ? 'running' : 'paused'; }
    else { ti.classList.remove('flow'); mq.style.animationPlayState = 'paused'; }
  }
  function markPlaying() {
    const rows = document.querySelectorAll('#' + PANEL_ID + ' .row2');
    rows.forEach((r, k) => r.classList.toggle('playing', k === idx));
  }
  function markFavInList() {
    const rows = document.querySelectorAll('#' + PANEL_ID + ' .row2');
    rows.forEach((r, k) => {
      const s = songs[k]; const btn = r.querySelector('.fav'); if (!s || !btn) return;
      const on = isFav(s); btn.textContent = on ? '♥' : '♡'; btn.classList.toggle('on', on);
    });
  }

  function currentPlayingSong() {
    if (idx >= 0 && idx < songs.length) return songs[idx];
    if (st.name) return { id:'', name:st.name, artist:st.artist, pic:st.pic, url:'' };
    return null;
  }

  function setTip(msg, err) { const list = document.getElementById('rmb2-list'); if (!list) return; list.innerHTML = `<div class="tip${err ? ' err' : ''}">${msg}</div>`; }
  function normalize(s) { let id = s.id || s.songmid || ''; if (!id && s.url) { const m = /[?&]id=([^&]+)/.exec(s.url); if (m) id = decodeURIComponent(m[1]); } return { id, name: s.name || s.title || '', artist: s.artist || s.author || '', pic: s.pic || s.picUrl || s.cover || '', url: s.url || '' }; }
  function renderList() {
    const list = document.getElementById('rmb2-list'); if (!list) return;
    if (!songs.length) {
      setTip(curSource === 'fav'
        ? '📻 <b>唱片夹</b>:收藏的单曲都在这里。在任意歌曲行点 ♡ 即可存入,点行播放。'
        : '没有歌曲');
      return;
    }
    list.innerHTML = '';
    songs.forEach((s, i) => {
      const d = document.createElement('div'); d.className = 'row2';
      const on = isFav(s);
      d.innerHTML = `<div class="no">${i + 1}</div><img src="${esc(s.pic)}" loading="lazy" onerror="this.style.visibility='hidden'"><div class="in"><div class="nm">${esc(s.name)}</div><div class="ar">${esc(s.artist)}</div></div><button type="button" class="fav${on ? ' on' : ''}" title="存入/移出唱片夹">${on ? '♥' : '♡'}</button>`;
      d.addEventListener('click', () => { play(i); });
      const favBtn = d.querySelector('.fav');
      if (favBtn) favBtn.addEventListener('click', e => { e.stopPropagation(); toggleFav(s); });
      list.appendChild(d);
    });
    markList();
  }
  function markList() {
    const rows = document.querySelectorAll('#' + PANEL_ID + ' .row2');
    rows.forEach((r, k) => r.classList.toggle('playing', k === idx));
  }

  function loadPlaylist(plId, kw) {
    curSource = 'pl';
    setTip('正在加载榜单…');
    const safeKw = kw || '';
    api(`?server=${server}&type=playlist&id=${plId}`).then(data => {
      let arr = pickArr(data);
      if (arr.length) {
        songs = arr.map(normalize); if (idx >= songs.length) idx = -1;
        renderList(); showWidget(); saveState();
        return;
      }
      if (!safeKw) { setTip('该榜单暂无数据,换个音源或榜单试试'); return; }
      setTip('该榜单暂无数据,改用「' + safeKw + '」搜索…');
      return api(`?server=${server}&type=search&id=${encodeURIComponent(safeKw)}`).then(d2 => {
        const arr2 = pickArr(d2);
        if (!arr2.length) { setTip('没找到结果,换个音源试试', true); return; }
        songs = arr2.map(normalize); if (idx >= songs.length) idx = -1;
        renderList(); showWidget(); saveState();
      });
    }).catch(err => setTip('加载失败:' + esc(err.message), true));
  }
  function doSearch() {
    const q = document.getElementById('rmb2-q'); const kw = q ? q.value.trim() : '';
    if (!kw) return;
    curSource = 'pl';
    const box = document.getElementById('rmb2-tabs');
    if (box) box.querySelectorAll('button').forEach(x => x.classList.remove('on'));
    setTip('搜索中…');
    api(`?server=${server}&type=search&id=${encodeURIComponent(kw)}`).then(data => {
      const arr = pickArr(data);
      if (!arr.length) { setTip('没找到结果,换个音源试试'); return; }
      songs = arr.map(normalize); if (idx >= songs.length) idx = -1;
      renderList(); showWidget(); saveState();
    }).catch(err => setTip('搜索失败:' + esc(err.message), true));
  }

  /* =========================================================
     后台标签存活判定 + 自动重开
     ========================================================= */
  function isBgAlive() {
    const s = gmGet(K_STATE);
    return !!(s && s.ts && (Date.now() - s.ts < HEARTBEAT_FRESH_MS));
  }
  function openBgTab() {
    if (typeof GM_openInTab !== 'function') return false;
    const u = location.origin + location.pathname + location.search + RELAY_HASH;
    try { GM_openInTab(u, { active: false, insert: true }); return true; } catch (e) { return false; }
  }
  let lastOpenTry = 0;
  function ensureBgTab(force) {
    if (isBgAlive()) return;
    const now = Date.now();
    if (!force && now - lastOpenTry < OPEN_DEBOUNCE_MS) return;
    lastOpenTry = now;
    openBgTab();
  }
  function forceOpenBg() {
    gmSet(K_FLAG, { ts: 0 });
    lastOpenTry = 0;
    ensureBgTab(true);
    setTip('已请求重开后台播放标签…');
  }
  let cmdSeq = 0;
  function sendCmd(type, data, skipOpen) {
    cmdSeq++;
    gmSet(K_CMD, { type, data, ts: Date.now(), seq: cmdSeq });
    if (!skipOpen) ensureBgTab();
  }

  let lastStateTs = 0;
  function pollBg() {
    const s = gmGet(K_STATE);
    const alive = !!(s && s.ts && (Date.now() - s.ts < HEARTBEAT_FRESH_MS));
    const bs = document.getElementById('rmb2-bgstat');
    if (bs) bs.textContent = alive ? '✅在线' : '⛔离线';
    if (s && s.ts > lastStateTs) {
      const prevKey = favKey(currentPlayingSong() || { name: st.name });
      lastStateTs = s.ts;
      st.playing = !!s.playing; st.cur = s.cur || 0; st.dur = s.dur || 0;
      if (s.name) st.name = s.name;
      if (s.artist) st.artist = s.artist;
      if (s.pic) st.pic = s.pic;
      if (typeof s.idx === 'number') idx = s.idx;
      const nowKey = favKey(currentPlayingSong() || { name: st.name });
      if (nowKey !== prevKey && lrcOn) ensureLrc();
      render(); showWidget(); saveState();
    }
  }

  /* =========================================================
     播放控制
     ========================================================= */
  function play(i) {
    if (i < 0 || i >= songs.length) return;
    idx = i;
    const s = songs[i];
    st.name = s.name; st.artist = s.artist; st.pic = s.pic;
    st.cur = 0; st.dur = 0; st.playing = true;
    render(); showWidget(); saveState();
    sendCmd('play', { index: i, songs, server });
    if (lrcOn) ensureLrc();
  }
  function togglePlay() {
    if (st.playing) { sendCmd('toggle', {}); }
    else {
      if (!songs.length) { setExpanded(true); return; }
      const target = (idx >= 0 && idx < songs.length) ? idx : 0;
      sendCmd('play', { index: target, songs, server });
    }
  }
  function prevTrack() { if (!songs.length) { setExpanded(true); return; } sendCmd('prev', {}); }
  function nextTrack() { if (!songs.length) { setExpanded(true); return; } sendCmd('next', {}); }
  function seekTo(pct) { sendCmd('seek', { pct }, true); }

  /* =========================================================
     ★ 后台标签页
     ========================================================= */
  function runRelayTab() {
    const styleH = document.createElement('style');
    styleH.id = 'rmb2bg-style';
    styleH.textContent = `
      *{box-sizing:border-box}
      html,body{margin:0;height:100%;overflow:hidden;background:#14100a;color:#e8d9c0;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif}
      #rmb2bg-wrap{display:flex;flex-direction:column;height:100vh;align-items:center;justify-content:center;gap:10px;text-align:center;padding:16px}
      #rmb2bg-t{font-size:14px;color:#ffd79a;font-weight:700}
      #rmb2bg-n{font-size:12px;color:#9c8b70;max-width:80vw;word-break:break-all}
      #bgaud{display:none}
    `;
    document.head.appendChild(styleH);
    document.body.innerHTML = `
      <div id="rmb2bg-wrap">
        <div id="rmb2bg-t">🎵 唱片机 · 后台播放中</div>
        <div id="rmb2bg-n">就绪</div>
      </div>
      <audio id="bgaud" preload="none"></audio>
    `;

    const aud = document.getElementById('bgaud');
    const nowEl = document.getElementById('rmb2bg-n');
    let songs = [], idx = -1, server = DEF_SERVER, paused = false, loadTimer = null, cur = null;

    function publish() {
      gmSet(K_STATE, {
        playing: !aud.paused && !aud.ended && aud.currentTime > 0,
        name: cur ? cur.name : '', artist: cur ? cur.artist : '', pic: cur ? cur.pic : '',
        cur: aud.currentTime || 0, dur: aud.duration || 0, idx, ts: Date.now()
      });
    }
    setInterval(publish, 900);

    function setNow(s) { cur = s; if (nowEl) nowEl.textContent = s ? (s.name + ' — ' + s.artist) : '未播放'; publish(); }

    function playMedia(i) {
      if (i < 0 || i >= songs.length) return;
      idx = i; const s = songs[i]; cur = s; paused = false; setNow(s);
      if (loadTimer) { clearTimeout(loadTimer); loadTimer = null; }
      const start = (src) => {
        src = String(src).trim();
        if (!/^https?:\/\//i.test(src)) { publish(); return; }
        aud.src = src; aud.load();
        const p = aud.play(); if (p && p.catch) p.catch(() => {});
        loadTimer = setTimeout(() => {
          if (aud.readyState < 2 && !paused) { if (songs.length > 1) playMedia(idx < songs.length - 1 ? idx + 1 : 0); }
        }, 25000);
      };
      const raw = s.url ? Promise.resolve(s.url) : api('?server=' + server + '&type=url&id=' + encodeURIComponent(s.id), true).then(t => { s.url = String(t).trim(); return s.url; });
      raw.then(start).catch(() => publish());
    }

    aud.addEventListener('playing', () => { paused = false; if (loadTimer) { clearTimeout(loadTimer); loadTimer = null; } publish(); });
    aud.addEventListener('pause', () => { if (!aud.ended) paused = true; publish(); });
    aud.addEventListener('timeupdate', publish);
    aud.addEventListener('ended', () => { if (songs.length > 1) { paused = false; playMedia(idx < songs.length - 1 ? idx + 1 : 0); } });

    function toggle() { if (!aud.src) { if (songs.length) playMedia(0); return; } if (aud.paused) { paused = false; aud.play().catch(() => {}); } else { paused = true; aud.pause(); } }
    function prev() { if (!songs.length) return; playMedia(idx > 0 ? idx - 1 : songs.length - 1); }
    function next() { if (!songs.length) return; playMedia(idx < songs.length - 1 ? idx + 1 : 0); }

    let lastCmdTs = Date.now();
    let lastSeq = gmGet(K_CMD) ? (gmGet(K_CMD).seq || 0) : 0;
    setInterval(() => {
      const msg = gmGet(K_CMD);
      if (!msg || !msg.ts) return;
      const isNew = (msg.ts > lastCmdTs) || (msg.ts === lastCmdTs && (msg.seq || 0) > lastSeq);
      if (!isNew) return;
      lastCmdTs = msg.ts;
      lastSeq = msg.seq || 0;
      const d = msg.data || {};
      if (msg.type === 'songs') { if (Array.isArray(d.songs)) { songs = d.songs; if (d.server) server = d.server; } }
      else if (msg.type === 'play') { if (Array.isArray(d.songs) && d.songs.length) { songs = d.songs; if (d.server) server = d.server; } if (typeof d.index === 'number') playMedia(d.index); }
      else if (msg.type === 'toggle') { toggle(); }
      else if (msg.type === 'prev') { prev(); }
      else if (msg.type === 'next') { next(); }
      else if (msg.type === 'seek') { if (isFinite(aud.duration)) aud.currentTime = d.pct / 100 * aud.duration; }
      publish();
    }, 500);

    publish();
  }

  /* =========================================================
     启动
     ========================================================= */
  function start() {
    injectStyle();
    buildDOM();

    favs = loadFavs();
    skin = getSkin(); quiet = getQuiet(); server = getServer();
    document.documentElement.classList.toggle('rmb2-quiet', quiet);
    renderQuietUI(); applySkin();

    /* 恢复歌词开关(跨页保持) */
    try { lrcOn = (localStorage.getItem(LRC_KEY) === '1'); } catch (e) { lrcOn = false; }
    applyLyricUI();

    const saved = loadState();
    if (saved) {
      songs = saved.songs;
      idx = typeof saved.idx === 'number' ? saved.idx : -1;
      server = saved.server || DEF_SERVER;
      if (idx >= 0 && songs[idx]) { st.name = songs[idx].name; st.artist = songs[idx].artist; st.pic = songs[idx].pic; }
      render(); showWidget();
    } else {
      const fab = document.getElementById(FAB_ID);
      if (fab) fab.style.display = 'flex';
    }

    /* 若歌词已开,尝试立刻拉取当前曲的歌词(等后台状态回来后 pollBg 也会再触发) */
    if (lrcOn) { ensureLrc(); renderLyric(true); }

    if (saved && saved.playing && songs.length) {
      setTimeout(() => {
        if (!isBgAlive()) {
          ensureBgTab();
          setTimeout(() => sendCmd('play', { index: (idx >= 0 ? idx : 0), songs, server }, true), 1200);
        }
      }, 800);
    }

    setInterval(pollBg, 900);
    pollBg();
  }

  window.addEventListener('pagehide', saveState);
  window.addEventListener('beforeunload', saveState);
  document.addEventListener('visibilitychange', () => { if (document.hidden) saveState(); });

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
  else start();

  new MutationObserver(() => {
    if (!document.getElementById(BAR_ID)) start();
  }).observe(document.documentElement, { childList: true, subtree: true });
})();
回复列表(11|隐藏机器人聊天)
添加新回复
回复需要登录。