Greasy Fork

Greasy Fork is available in English.

๐ŸŽธ Songsterr Ultimate

Unlocks all Plus features (Speed, Loop, Solo) and Native Download (.gp7 and .midi). (Tested on Zen Browser)

ๅฝ“ๅ‰ไธบ 2026-03-07 ๆไบค็š„็‰ˆๆœฌ๏ผŒๆŸฅ็œ‹ ๆœ€ๆ–ฐ็‰ˆๆœฌใ€‚

ๆ‚จ้œ€่ฆๅ…ˆๅฎ‰่ฃ…ไธ€ๆฌพ็”จๆˆท่„šๆœฌ็ฎก็†ๅ™จๆ‰ฉๅฑ•๏ผŒไพ‹ๅฆ‚ Tampermonkey ็ฏกๆ”น็Œดใ€Greasemonkey ๆฒน็Œดๅญ ๆˆ– Violentmonkey ๆšดๅŠ›็Œด๏ผŒๆ‰่ƒฝๅฎ‰่ฃ…ๆญค่„šๆœฌใ€‚

ๆ‚จ้œ€่ฆๅ…ˆๅฎ‰่ฃ…ไธ€ๆฌพ็”จๆˆท่„šๆœฌ็ฎก็†ๅ™จๆ‰ฉๅฑ•๏ผŒไพ‹ๅฆ‚ Tampermonkey ็ฏกๆ”น็Œด๏ผŒๆ‰่ƒฝๅฎ‰่ฃ…ๆญค่„šๆœฌใ€‚

ๆ‚จ้œ€่ฆๅ…ˆๅฎ‰่ฃ…ไธ€ๆฌพ็”จๆˆท่„šๆœฌ็ฎก็†ๅ™จๆ‰ฉๅฑ•๏ผŒไพ‹ๅฆ‚ Tampermonkey ็ฏกๆ”น็Œด ๆˆ– Violentmonkey ๆšดๅŠ›็Œด๏ผŒๆ‰่ƒฝๅฎ‰่ฃ…ๆญค่„šๆœฌใ€‚

ๆ‚จ้œ€่ฆๅ…ˆๅฎ‰่ฃ…ไธ€ๆฌพ็”จๆˆท่„šๆœฌ็ฎก็†ๅ™จๆ‰ฉๅฑ•๏ผŒไพ‹ๅฆ‚ Tampermonkey ็ฏกๆ”น็Œด ๆˆ– Userscripts ๏ผŒๆ‰่ƒฝๅฎ‰่ฃ…ๆญค่„šๆœฌใ€‚

ๆ‚จ้œ€่ฆๅ…ˆๅฎ‰่ฃ…ไธ€ๆฌพ็”จๆˆท่„šๆœฌ็ฎก็†ๅ™จๆ‰ฉๅฑ•๏ผŒไพ‹ๅฆ‚ Tampermonkey ็ฏกๆ”น็Œด๏ผŒๆ‰่ƒฝๅฎ‰่ฃ…ๆญค่„šๆœฌใ€‚

ๆ‚จ้œ€่ฆๅ…ˆๅฎ‰่ฃ…ไธ€ๆฌพ็”จๆˆท่„šๆœฌ็ฎก็†ๅ™จๆ‰ฉๅฑ•ๅŽๆ‰่ƒฝๅฎ‰่ฃ…ๆญค่„šๆœฌใ€‚

๏ผˆๆˆ‘ๅทฒ็ปๅฎ‰่ฃ…ไบ†็”จๆˆท่„šๆœฌ็ฎก็†ๅ™จ๏ผŒ่ฎฉๆˆ‘ๅฎ‰่ฃ…๏ผ๏ผ‰

ๆ‚จ้œ€่ฆๅ…ˆๅฎ‰่ฃ…ไธ€ๆฌพ็”จๆˆทๆ ทๅผ็ฎก็†ๅ™จๆ‰ฉๅฑ•๏ผŒๆฏ”ๅฆ‚ Stylus๏ผŒๆ‰่ƒฝๅฎ‰่ฃ…ๆญคๆ ทๅผใ€‚

ๆ‚จ้œ€่ฆๅ…ˆๅฎ‰่ฃ…ไธ€ๆฌพ็”จๆˆทๆ ทๅผ็ฎก็†ๅ™จๆ‰ฉๅฑ•๏ผŒๆฏ”ๅฆ‚ Stylus๏ผŒๆ‰่ƒฝๅฎ‰่ฃ…ๆญคๆ ทๅผใ€‚

ๆ‚จ้œ€่ฆๅ…ˆๅฎ‰่ฃ…ไธ€ๆฌพ็”จๆˆทๆ ทๅผ็ฎก็†ๅ™จๆ‰ฉๅฑ•๏ผŒๆฏ”ๅฆ‚ Stylus๏ผŒๆ‰่ƒฝๅฎ‰่ฃ…ๆญคๆ ทๅผใ€‚

ๆ‚จ้œ€่ฆๅ…ˆๅฎ‰่ฃ…ไธ€ๆฌพ็”จๆˆทๆ ทๅผ็ฎก็†ๅ™จๆ‰ฉๅฑ•ๅŽๆ‰่ƒฝๅฎ‰่ฃ…ๆญคๆ ทๅผใ€‚

ๆ‚จ้œ€่ฆๅ…ˆๅฎ‰่ฃ…ไธ€ๆฌพ็”จๆˆทๆ ทๅผ็ฎก็†ๅ™จๆ‰ฉๅฑ•ๅŽๆ‰่ƒฝๅฎ‰่ฃ…ๆญคๆ ทๅผใ€‚

ๆ‚จ้œ€่ฆๅ…ˆๅฎ‰่ฃ…ไธ€ๆฌพ็”จๆˆทๆ ทๅผ็ฎก็†ๅ™จๆ‰ฉๅฑ•ๅŽๆ‰่ƒฝๅฎ‰่ฃ…ๆญคๆ ทๅผใ€‚

๏ผˆๆˆ‘ๅทฒ็ปๅฎ‰่ฃ…ไบ†็”จๆˆทๆ ทๅผ็ฎก็†ๅ™จ๏ผŒ่ฎฉๆˆ‘ๅฎ‰่ฃ…๏ผ๏ผ‰

// ==UserScript==
// @name         ๐ŸŽธ Songsterr Ultimate
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  Unlocks all Plus features (Speed, Loop, Solo) and Native Download (.gp7 and .midi). (Tested on Zen Browser)
// @author       Goulagman
// @supportURL   https://github.com/GoulagmanYt/Songsterr-Plus-Ultimate-Unlocker-
// @match        *://www.songsterr.com/*
// @require      https://cdn.jsdelivr.net/npm/@coderline/[emailย protected]/dist/alphaTab.min.js
// @connect      dqsljvtekg760.cloudfront.net
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  console.log('๐ŸŽธ Songsterr Ultimate โ€” Actif v3.1.0');

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // 0. NETTOYAGE PRร‰VENTIF
  // Supprime l'รฉtat Redux mis en cache pour forcer une session propre
  // et รฉviter que le profil "free" ne soit chargรฉ depuis le localStorage.
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  try { localStorage.removeItem('persist:root'); } catch (e) {}

  // Rรฉfรฉrence ร  la vraie fenรชtre (contourne l'isolation Tampermonkey)
  const targetWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // 1. PROFIL "MAGIQUE" PLUS
  // ID alรฉatoire ร  chaque session pour contourner la limite de
  // tรฉlรฉchargements journaliers (HTTP 429 Too Many Requests).
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  const MAGIC_ID = Math.floor(Math.random() * 900000000) + 100000000;

  const MAGIC_PROFILE = {
    id                    : MAGIC_ID,
    uid                   : MAGIC_ID,
    email                 : `plususer${MAGIC_ID}@songsterr.com`,
    name                  : 'Plus User (Unlocked)',
    plan                  : 'plus',
    hasPlus               : true,
    permissions           : [],
    subscription          : { plan: { id: 'plus' } },
    bonusPurchasedFeatures: [],
    signature             : 'patched_signature',
    hadPlusBeforeSE       : true
  };

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // 2. INTERCEPTION Rร‰SEAU
  // On accroche fetch() trรจs tรดt (document-start) pour :
  //   A. Renvoyer notre faux profil Plus sur /auth/profile
  //   B. Bloquer les appels de logs/analytics/sentry (bruit inutile)
  //   NOTE : on ne touche PAS ร  /api/edits/download โ€” notre tรฉlรฉchargeur
  //          GP7/MIDI est supรฉrieur au .gp5 natif.
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  const fetchOriginal = targetWindow.fetch;

  const fetchHooked = async function (resource, options) {
    // Dรฉtermine si resource est un objet Request ou une simple URL string
    const isReqObj = typeof resource === 'object' && resource instanceof Request;
    const url = isReqObj ? resource.url : (resource || '');

    // --- A. USURPATION DE PROFIL ---
    // Songsterr interroge cette route pour savoir si l'utilisateur a un abonnement.
    // On renvoie notre profil "plus" forgรฉ pour dรฉbloquer Speed, Loop, Solo.
    if (url.includes('/auth/profile')) {
      return new Response(JSON.stringify(MAGIC_PROFILE), {
        status : 200,
        headers: { 'Content-Type': 'application/json' }
      });
    }

    // --- B. BLOCAGE DES LOGS & ANALYTICS ---
    // ร‰vite d'envoyer des donnรฉes de tรฉlรฉmรฉtrie pendant nos opรฉrations.
    if (url.match(/(sentry|logs|analytics|useraudio)/i)) {
      return new Response('{}', { status: 200 });
    }

    // Toutes les autres requรชtes passent normalement
    return fetchOriginal(resource, options);
  };

  // Mode furtif : toString() renvoie l'original pour dรฉjouer les dรฉtections
  fetchHooked.toString = () => fetchOriginal.toString();

  // Injection robuste avec Object.defineProperty pour survivre aux re-dรฉfinitions
  try {
    Object.defineProperty(targetWindow, 'fetch', {
      value      : fetchHooked,
      writable   : true,
      configurable: true
    });
  } catch (e) {
    targetWindow.fetch = fetchHooked; // Fallback navigateurs anciens
  }

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // 3. INJECTION D'ร‰TAT DOM
  // Songsterr stocke son รฉtat Redux dans <script id="state">.
  // On observe le DOM dรจs que ce nล“ud apparaรฎt et on y injecte
  // hasPlus:true + notre profil pour que React "croie" qu'on est abonnรฉ.
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  const stateObserver = new MutationObserver(() => {
    const el = document.getElementById('state');
    if (!el) return;
    try {
      const text = el.textContent.trim();
      if (!text) return;
      const data = JSON.parse(text);

      if (!data.user) data.user = {};
      data.user.hasPlus    = true;
      data.user.isLoggedIn = true;
      data.user.profile    = MAGIC_PROFILE;
      // Supprime la banniรจre de consentement RGPD
      data.consent = { loading: false, suite: 'tcf', view: 'none' };

      const patched = JSON.stringify(data);
      if (el.textContent !== patched) el.textContent = patched;
    } catch (e) { /* JSON invalide, on ignore */ }
  });
  stateObserver.observe(document.documentElement, { childList: true, subtree: true });

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // 4. CSS โ€” Nettoyage UI + styles de nos boutons
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  GM_addStyle(`
    /* โ”€โ”€ Masque les รฉlรฉments indรฉsirables โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
    section[data-consent="summary"],
    div[class*="Consent"],
    #onetrust-banner-sdk,
    [id*="ad-"],
    [class*="ad-"],
    div[id^="div-gpt-ad"],
    div[class*="Error"]
    { display: none !important; visibility: hidden !important; }

    body, html { overflow: auto !important; }
    #apptab    { opacity: 1 !important; visibility: visible !important; }

    /* โ”€โ”€ Conteneur de nos boutons โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
    #sgd-wrapper {
      display      : inline-flex;
      align-items  : center;
      gap          : 12px;
    }

    /* โ”€โ”€ Boutons GP7 & MIDI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
    .sgd-btn {
      display      : inline-flex;
      align-items  : center;
      gap          : 7px;
      padding      : 8px 16px;
      border       : none;
      border-radius: 8px;
      font-size    : 13px;
      font-weight  : 700;
      cursor       : pointer;
      white-space  : nowrap;
      box-shadow   : 0 2px 10px rgba(0,0,0,0.30);
      transition   : opacity .15s, transform .1s;
      font-family  : -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      line-height  : 1;
      letter-spacing: 0.01em;
    }
    .sgd-btn:hover:not(:disabled)  { opacity: .82; transform: translateY(-1px); }
    .sgd-btn:active:not(:disabled) { transform: translateY(0); }
    .sgd-btn:disabled              { opacity: .4; cursor: not-allowed; }
    .sgd-btn-gp   { background: linear-gradient(135deg,#2563eb,#1d4ed8); color:#fff; }
    .sgd-btn-midi { background: linear-gradient(135deg,#1e293b,#0f172a); color:#fff; }

    /* โ”€โ”€ Toast de statut centrรฉ en bas โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
    #sgd-status {
      position     : fixed;
      bottom       : 20px;
      left         : 50%;
      transform    : translateX(-50%);
      background   : rgba(15,23,42,.90);
      color        : #e2e8f0;
      font-size    : 12px;
      font-weight  : 500;
      padding      : 6px 16px;
      border-radius: 20px;
      z-index      : 99999;
      pointer-events: none;
      opacity      : 0;
      transition   : opacity .25s;
      font-family  : -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      white-space  : nowrap;
    }
    #sgd-status.visible { opacity: 1; }
    #sgd-status.ok  { color: #86efac; }
    #sgd-status.err { color: #fca5a5; }
  `);

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // 5. Dร‰VERROUILLAGE CIBLร‰ DES FONCTIONNALITร‰S PLUS
  //
  // โš ๏ธ PIรˆGE CRITIQUE : on ne doit PAS retirer disabled sur TOUS les
  // boutons dรฉsactivรฉs de la page. Le player de tab Songsterr utilise
  // des boutons disabled lรฉgitimement pendant son initialisation
  // (chargement audio, parsing de la tabโ€ฆ). Si on les force ร  enabled,
  // React perd la synchronisation entre son รฉtat interne et le DOM โ†’
  // la tab se gรจle au premier chargement.
  //
  // Solution : on cible UNIQUEMENT les boutons verrouillรฉs par le
  // paywall Plus, identifiables par :
  //   1. Prรฉsence d'une icรดne cadenas SVG (use[href*="lock"]) dans
  //      le bouton ou son voisinage immรฉdiat
  //   2. Un data-id connu liรฉ aux features Plus (Speed, Loop, Soloโ€ฆ)
  //   3. La classe CSS spรฉcifique du lock Songsterr (Cny223)
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

  // data-id des features connues du paywall Plus
  const PLUS_DATA_IDS = ['Speed', 'Loop', 'Solo', 'Autoscroll', 'Print'];

  setInterval(() => {
    // โ”€โ”€ 1. Forcer le mode impression "Plus" โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    const printEl = document.querySelector('[data-id^="Print--"]');
    if (printEl) printEl.setAttribute('data-id', 'Print--plus');

    // โ”€โ”€ 2. Supprimer les icรดnes cadenas SVG โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    // Ces <use href*="lock"> sont ajoutรฉs par React sur les features Plus.
    // On retire l'icรดne et on dรฉbloque le bouton parent uniquement.
    document.querySelectorAll('svg use[href*="lock"]').forEach(use => {
      const svg    = use.closest('svg');
      const parent = svg?.closest('button');
      if (svg)    svg.remove();
      if (parent) {
        parent.removeAttribute('disabled');
        parent.classList.remove('Cny223');
        parent.style.pointerEvents = 'auto';
      }
    });

    // โ”€โ”€ 3. Dรฉverrouiller les boutons Plus par data-id โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    // On ne touche QUE les boutons dont le data-id correspond ร  une
    // feature connue du paywall โ€” jamais les boutons du player.
    PLUS_DATA_IDS.forEach(id => {
      const el = document.querySelector(`[data-id*="${id}"]`);
      if (el && el.hasAttribute('disabled')) {
        el.removeAttribute('disabled');
        el.classList.remove('Cny223');
        el.style.pointerEvents = 'auto';
      }
    });

    // โ”€โ”€ 4. Dรฉbloquer les boutons portant la classe de lock Songsterr โ”€โ”€
    document.querySelectorAll('button.Cny223').forEach(btn => {
      btn.removeAttribute('disabled');
      btn.classList.remove('Cny223');
      btn.style.pointerEvents = 'auto';
    });
  }, 1000);

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // 6. FILTRE CONSOLE
  // Supprime les erreurs bruyantes et sans intรฉrรชt pour la console.
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  const consoleErrorOrig = console.error;
  const CONSOLE_FILTERS  = ['AudioContext', 'source-map', 'unreachable', 'buffer', 'Secure-YEC', 'Aborted', '401'];
  console.error = function (...args) {
    if (CONSOLE_FILTERS.some(f => String(args[0]).includes(f))) return;
    consoleErrorOrig.apply(console, args);
  };

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // โ–ผโ–ผโ–ผ NOTRE Tร‰Lร‰CHARGEUR GP7 / MIDI โ–ผโ–ผโ–ผ
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CDN SONGSTERR โ€” En-tรชtes qui simulent Chrome pour passer la
  // validation CloudFront (Origin + Referer sont obligatoires).
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  const CDN_BASE = 'https://dqsljvtekg760.cloudfront.net';

  const CDN_HEADERS = {
    'User-Agent'        : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
    'Accept'            : 'application/json, */*',
    'Accept-Language'   : 'en-US,en;q=0.9',
    'sec-ch-ua'         : '"Chromium";v="124","Google Chrome";v="124"',
    'sec-ch-ua-mobile'  : '?0',
    'sec-ch-ua-platform': '"Windows"',
    'sec-fetch-site'    : 'same-origin',
    'sec-fetch-mode'    : 'cors',
    'sec-fetch-dest'    : 'empty',
    'Referer'           : 'https://www.songsterr.com/',
    'Origin'            : 'https://www.songsterr.com',
    'Cache-Control'     : 'no-cache',
    'Pragma'            : 'no-cache'
  };

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // ร‰TAPE 1 โ€” Lire les mรฉtadonnรฉes de la page
  // Songsterr injecte toutes les infos dans <script id="state">.
  // On en extrait : songId, revisionId, image, liste des pistes.
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function getStateFromPage() {
    const el = document.getElementById('state');
    if (!el) throw new Error('ร‰lรฉment #state introuvable. Es-tu sur une page de tab ?');
    let parsed;
    try { parsed = JSON.parse(el.textContent || el.innerText); }
    catch (e) { throw new Error('Impossible de parser le JSON de la page : ' + e.message); }

    const cur = parsed?.meta?.current;
    if (!cur?.songId || !cur?.revisionId || !cur?.image) {
      throw new Error('Payload Songsterr incomplet (songId / revisionId / image manquants).');
    }
    return {
      songId    : cur.songId,
      revisionId: cur.revisionId,
      image     : cur.image,
      title     : cur.title  || 'Song',
      artist    : cur.artist || 'Unknown Artist',
      tracks    : Array.isArray(cur.tracks) ? cur.tracks : []
    };
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // ร‰TAPE 2 โ€” Tรฉlรฉcharger les JSONs de rรฉvision depuis le CDN
  // URL : {CDN_BASE}/{songId}/{revisionId}/{image}/{partId}.json
  // GM_xmlhttpRequest contourne les restrictions CORS du navigateur.
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function fetchRevisionJson(url) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method      : 'GET',
        url,
        headers     : CDN_HEADERS,
        responseType: 'json',
        onload : res => {
          if (res.status >= 200 && res.status < 300) resolve(res.response);
          else reject(new Error(`HTTP ${res.status} โ€” ${url}`));
        },
        onerror: err => reject(new Error(`Erreur rรฉseau : ${JSON.stringify(err)}`))
      });
    });
  }

  async function fetchAllRevisions(meta) {
    const { songId, revisionId, image, tracks } = meta;
    const validTracks = tracks
      .filter(t => typeof t.partId === 'number')
      .sort((a, b) => a.partId - b.partId);

    if (validTracks.length === 0) throw new Error('Aucune piste valide dans les mรฉtadonnรฉes.');

    const results = await Promise.all(
      validTracks.map(async track => {
        const url = `${CDN_BASE}/${songId}/${revisionId}/${image}/${track.partId}.json`;
        try {
          return { trackMeta: track, revision: await fetchRevisionJson(url) };
        } catch (err) {
          console.warn(`[SGD] Piste ${track.partId} ignorรฉe :`, err.message);
          return null;
        }
      })
    );

    const revisions = results.filter(Boolean);
    if (revisions.length === 0) throw new Error('Aucune piste rรฉcupรฉrรฉe depuis le CDN.');
    return revisions;
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Durรฉe Songsterr [num, den] โ†’ Duration alphaTab + dots
  // Algorithme : minimisation du delta sur toutes les combinaisons
  // (durรฉe de base ร— 0, 1 ou 2 points de pointรฉ).
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function mapDuration(dur) {
    const D     = alphaTab.model.Duration;
    const bases = [D.Whole, D.Half, D.Quarter, D.Eighth, D.Sixteenth, D.ThirtySecond, D.SixtyFourth];

    if (!dur?.[0] || !dur?.[1]) return { duration: D.Quarter, dots: 0 };

    const target = dur[0] / dur[1];
    let best = { duration: D.Quarter, dots: 0 };
    let bestDelta = Infinity;

    for (const base of bases) {
      const bv = 1 / Number(base);
      for (const dots of [0, 1, 2]) {
        const dv    = bv + (dots >= 1 ? bv / 2 : 0) + (dots >= 2 ? bv / 4 : 0);
        const delta = Math.abs(dv - target);
        if (delta < bestDelta) { bestDelta = delta; best = { duration: base, dots }; }
      }
    }
    return best;
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Tuplet entier โ†’ [numรฉrateur, dรฉnominateur]
  // Exemples : 3 โ†’ [3,2], 5 โ†’ [5,4], 7 โ†’ [7,4]
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function getTupletRatio(t) {
    const map = { 3:[3,2], 5:[5,4], 6:[6,4], 7:[7,4], 9:[9,8], 10:[10,8], 12:[12,8] };
    if (map[t]) return map[t];
    if (t > 1) { const d = Math.pow(2, Math.floor(Math.log2(t))); return [t, d]; }
    return [1, 1];
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” ID instrument Songsterr โ†’ programme MIDI + flags
  // ID 1024 = batterie โ†’ channel MIDI 9 (spรฉcification MIDI Standard)
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function mapInstrument(id) {
    if (id === 1024) return { program: 0, isPercussion: true };
    const prog = typeof id === 'number' ? Math.min(Math.max(id, 0), 127) : 24;
    return { program: prog, isPercussion: false };
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Index d'articulation percussion
  // Effectue un round-trip GP7 (export โ†’ import) pour construire une
  // map stable des index, indรฉpendante des versions d'alphaTab.
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  let _percMap = null;

  function buildPercMap() {
    // Crรฉe un score minimal avec une piste de percussion vide
    const score = new alphaTab.model.Score();
    const mb    = new alphaTab.model.MasterBar();
    score.addMasterBar(mb);
    const track = new alphaTab.model.Track();
    track.playbackInfo.primaryChannel   = 9;
    track.playbackInfo.secondaryChannel = 9;
    const staff   = new alphaTab.model.Staff();
    staff.isPercussion = true;
    track.addStaff(staff);
    const bar   = new alphaTab.model.Bar();
    const voice = new alphaTab.model.Voice();
    const beat  = new alphaTab.model.Beat();
    beat.isEmpty = true;
    voice.addBeat(beat); bar.addVoice(voice); staff.addBar(bar);
    score.addTrack(track);

    // Export โ†’ re-import pour lire les articulations telles qu'alphaTab les indexe
    const settings  = new alphaTab.Settings();
    score.finish(settings);
    const data       = new alphaTab.exporter.Gp7Exporter().export(score, settings);
    const reimported = alphaTab.importer.ScoreLoader.loadScoreFromBytes(data, settings);

    const map = new Map();
    reimported.tracks[0].percussionArticulations.forEach((a, i) => {
      if (!map.has(a.id)) map.set(a.id, i);
    });
    return map;
  }

  function getPercIndex(midiNote) {
    if (!_percMap) _percMap = buildPercMap();
    return _percMap.get(midiNote) ?? midiNote;
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // TABLES DE CORRESPONDANCE
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  const VELOCITY_MAP = {
    ppp: alphaTab.model.DynamicValue.PPP,
    pp : alphaTab.model.DynamicValue.PP,
    p  : alphaTab.model.DynamicValue.P,
    mp : alphaTab.model.DynamicValue.MP,
    mf : alphaTab.model.DynamicValue.MF,
    f  : alphaTab.model.DynamicValue.F,
    ff : alphaTab.model.DynamicValue.FF,
    fff: alphaTab.model.DynamicValue.FFF
  };

  const HARMONIC_MAP = {
    natural   : alphaTab.model.HarmonicType.Natural,
    artificial: alphaTab.model.HarmonicType.Artificial,
    pinch     : alphaTab.model.HarmonicType.Pinch,
    tap       : alphaTab.model.HarmonicType.Tap,
    semi      : alphaTab.model.HarmonicType.Semi,
    feedback  : alphaTab.model.HarmonicType.Feedback
  };

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Construction des mesures maรฎtresses (MasterBars)
  // Gรจre : signatures rhythmiques, marqueurs de section, rรฉpรฉtitions,
  // fins alternatives et automations de tempo (BPM).
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function buildMasterBars(score, masterRev, count) {
    let sigNum = 4, sigDen = 4;

    for (let i = 0; i < count; i++) {
      const m  = masterRev?.measures?.[i];
      const s  = m?.signature;

      // Met ร  jour la signature si prรฉsente et valide
      if (Array.isArray(s) && s.length === 2 && s[0] && s[1]) [sigNum, sigDen] = s;

      const mb = new alphaTab.model.MasterBar();
      mb.timeSignatureNumerator   = sigNum;
      mb.timeSignatureDenominator = sigDen;

      // Marqueur de section (ex : "Verse", "Chorus"โ€ฆ)
      if (m?.marker) {
        const text = typeof m.marker === 'string' ? m.marker : (m.marker?.text || '');
        const sec  = new alphaTab.model.Section();
        sec.marker = sec.text = text;
        mb.section = sec;
      }

      if (m?.repeatStart)                                            mb.isRepeatStart  = true;
      if (typeof m?.repeatCount    === 'number' && m.repeatCount > 0)  mb.repeatCount    = m.repeatCount;
      if (typeof m?.alternateEnding === 'number' && m.alternateEnding > 0) mb.alternateEndings = m.alternateEnding;

      score.addMasterBar(mb);
    }

    // Automations de tempo โ€” toujours en rรฉfรฉrence noire (index 2)
    const tempo = masterRev?.automations?.tempo;
    if (Array.isArray(tempo)) {
      for (const pt of tempo) {
        const mb = score.masterBars[pt.measure];
        if (!mb) continue;
        const ratio = pt.position > 0 ? Math.max(0, Math.min(1, pt.position / (pt.type || 4))) : 0;
        mb.tempoAutomations.push(
          alphaTab.model.Automation.buildTempoAutomation(false, ratio, pt.bpm, 2, true)
        );
      }
    }
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Note Songsterr โ†’ alphaTab Note
  // โ˜… Cordes : Songsterr 0 = corde la + haute, alphaTab 1 = la + basse
  // โ˜… Bend   : Songsterr centiรจmes ร— 2 = quarts de ton (alphaTab)
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function mapNote(nd, isPerc, numStrings) {
    const note  = new alphaTab.model.Note();
    note.string = isPerc ? 0 : numStrings - (nd.string ?? 0);
    note.fret   = nd.fret ?? 0;

    // Index d'articulation pour la batterie
    if (isPerc) note.percussionArticulation = getPercIndex(nd.fret ?? 0);

    if (nd.tie)         note.isTieDestination   = true;
    if (nd.dead)        note.isDead             = true;
    if (nd.ghost)       note.isGhost            = true;
    if (nd.hp)          note.isHammerPullOrigin = true;
    if (nd.staccato)    note.isStaccato         = true;
    if (nd.accentuated) note.accentuated        = alphaTab.model.AccentuationType.Heavy;

    if (nd.wideVibrato)  note.vibrato = alphaTab.model.VibratoType.Wide;
    else if (nd.vibrato) note.vibrato = alphaTab.model.VibratoType.Slight;

    // Harmonique
    if (nd.harmonic) {
      const ht = HARMONIC_MAP[nd.harmonic.toLowerCase()];
      if (typeof ht === 'number') {
        note.harmonicType = ht;
        if (typeof nd.harmonicFret === 'number') note.harmonicValue = nd.harmonicFret;
      }
    }

    // Slide (glissรฉ)
    if (nd.slide) {
      const s = nd.slide.toLowerCase();
      const Out = alphaTab.model.SlideOutType, In = alphaTab.model.SlideInType;
      if      (s === 'shift')                             note.slideOutType = Out.Shift;
      else if (s === 'legato')                            note.slideOutType = Out.Legato;
      else if (s === 'into_from_below' || s === 'below') note.slideInType  = In.IntoFromBelow;
      else if (s === 'into_from_above')                  note.slideInType  = In.IntoFromAbove;
      else if (s === 'out_up')                           note.slideOutType = Out.OutUp;
      else if (s === 'out_down' || s === 'downwards')    note.slideOutType = Out.OutDown;
    }

    // Bend โ€” โ˜… ร—2 car Songsterr = centiรจmes de demi-ton, alphaTab = quarts de ton
    if (nd.bend?.points?.length > 0) {
      note.bendType = alphaTab.model.BendType.Custom;
      for (const pt of nd.bend.points) {
        note.addBendPoint(new alphaTab.model.BendPoint(
          Math.round(pt.position),
          Math.round(pt.tone * 2)  // โ˜… facteur ร—2
        ));
      }
    }

    return note;
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Beat Songsterr โ†’ alphaTab Beat
  // Gรจre : durรฉes, points, tuplets, dynamiques, vibrato, palm muteโ€ฆ
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function mapBeat(bd, masterBar, isPerc, numStrings) {
    const beat = new alphaTab.model.Beat();
    if (bd.rest) beat.isEmpty = true;

    const dur    = mapDuration(bd.duration);
    beat.duration = dur.duration;
    beat.dots     = bd.dots ?? dur.dots;

    if (bd.text) beat.text = bd.text;

    // Tuplet : on recalcule la durรฉe de base depuis le champ `type`
    if (typeof bd.tuplet === 'number' && bd.tuplet > 1) {
      const [n, d]    = getTupletRatio(bd.tuplet);
      beat.tupletNumerator   = n;
      beat.tupletDenominator = d;
      if (typeof bd.type === 'number' && bd.type > 0) {
        beat.duration = mapDuration([1, bd.type]).duration;
        beat.dots     = bd.dots ?? 0;
      }
    }

    // Nuance dynamique
    if (typeof bd.velocity === 'string') {
      const dyn = VELOCITY_MAP[bd.velocity.toLowerCase()];
      if (typeof dyn === 'number') beat.dynamics = dyn;
    }

    // Sens du coup de mรฉdiator
    if (typeof bd.pickStroke === 'string') {
      const ps = bd.pickStroke.toLowerCase();
      if (ps === 'down') beat.pickStroke = alphaTab.model.PickStroke.Down;
      else if (ps === 'up') beat.pickStroke = alphaTab.model.PickStroke.Up;
    }

    // Vibrato de beat
    if (bd.wideVibrato || bd.vibratoWithTremoloBar) beat.vibrato = alphaTab.model.VibratoType.Wide;
    else if (bd.vibrato)                            beat.vibrato = alphaTab.model.VibratoType.Slight;

    if (bd.palmMute) beat.isPalmMute = true;

    // Ajout des notes
    for (const nd of (bd.notes || [])) {
      if (!nd.rest) beat.addNote(mapNote(nd, isPerc, numStrings));
    }

    return beat;
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Remplir une voix vide par des silences
  // Utilisรฉ quand une mesure n'a aucun beat (mesure de silence totale).
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function fillWithRests(voice, masterBar) {
    const num = masterBar.timeSignatureNumerator   || 4;
    const den = masterBar.timeSignatureDenominator || 4;
    const dur = mapDuration([1, den]);
    for (let i = 0; i < num; i++) {
      const rest = new alphaTab.model.Beat();
      rest.isEmpty  = true;
      rest.duration = dur.duration;
      rest.dots     = dur.dots;
      voice.addBeat(rest);
    }
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Construire une piste alphaTab complรจte
  // Gรจre le tuning (ordre inversรฉ Songsterr vs alphaTab), le channel
  // MIDI percussion (9), et toutes les mesures.
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function buildTrack(score, entry, masterBarCount, channel) {
    const { trackMeta, revision } = entry;
    const playback = mapInstrument(trackMeta.instrumentId ?? revision.instrumentId);
    const isPerc   = playback.isPercussion || !!trackMeta.isDrums;

    const track = new alphaTab.model.Track();
    track.name      = trackMeta.title || trackMeta.name || revision.name || 'Track';
    track.shortName = track.name.slice(0, 20);
    track.playbackInfo.program          = playback.program;
    track.playbackInfo.primaryChannel   = channel;
    track.playbackInfo.secondaryChannel = channel;

    const staff = new alphaTab.model.Staff();
    staff.isPercussion = isPerc;

    // โ˜… Tuning : Songsterr stocke hautโ†’bas, alphaTab attend basโ†’haut (on passe tel quel)
    const tuning = revision.tuning || trackMeta.tuning;
    if (Array.isArray(tuning) && tuning.length > 0 && !isPerc) {
      staff.stringTuning = new alphaTab.model.Tuning('Custom', tuning, false);
    }
    const numStrings = Array.isArray(tuning) ? tuning.length : 6;

    for (let mi = 0; mi < masterBarCount; mi++) {
      const bar    = new alphaTab.model.Bar();
      const m      = revision.measures?.[mi];
      const mb     = score.masterBars[mi];
      const voices = m?.voices || [];

      if (voices.length === 0) {
        const v = new alphaTab.model.Voice();
        fillWithRests(v, mb);
        bar.addVoice(v);
      } else {
        for (const sv of voices) {
          const v    = new alphaTab.model.Voice();
          const bts  = sv?.beats || [];
          if (bts.length === 0 || sv?.rest) {
            fillWithRests(v, mb);
          } else {
            for (const bd of bts) v.addBeat(mapBeat(bd, mb, isPerc, numStrings));
            if (v.beats.length === 0) fillWithRests(v, mb);
          }
          bar.addVoice(v);
        }
      }
      staff.addBar(bar);
    }

    track.addStaff(staff);
    score.addTrack(track);
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Assembler le Score alphaTab complet
  // La piste la plus longue sert de rรฉfรฉrence pour les MasterBars.
  // Le channel 9 est rรฉservรฉ ร  la batterie (norme MIDI Gรฉnรฉral).
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function buildScore(meta, revisions) {
    const score   = new alphaTab.model.Score();
    score.title   = meta.title;
    score.artist  = meta.artist;
    score.tab     = 'Songsterr Ultimate v3';

    // La piste avec le plus de mesures est la rรฉfรฉrence maรฎtresse
    const masterRev = revisions.reduce((best, cur) =>
      (cur.revision?.measures?.length || 0) > (best.revision?.measures?.length || 0) ? cur : best
    ).revision;

    const masterBarCount = Math.max(1,
      revisions.reduce((m, e) => Math.max(m, e.revision?.measures?.length || 0), 0)
    );

    buildMasterBars(score, masterRev, masterBarCount);

    // Attribution des channels MIDI (0โ€“15, sauf 9 rรฉservรฉ batterie)
    let nextChannel = 0;
    for (const entry of revisions) {
      const id    = entry.trackMeta.instrumentId ?? entry.revision.instrumentId;
      const isPerc = id === 1024 || !!entry.trackMeta.isDrums;
      let channel;
      if (isPerc) {
        channel = 9;
      } else {
        if (nextChannel === 9) nextChannel++; // Saute le channel 9 (batterie)
        channel = nextChannel++;
      }
      buildTrack(score, entry, masterBarCount, channel);
    }

    const settings = new alphaTab.Settings();
    score.finish(settings); // โ˜… Obligatoire โ€” finalise les liaisons internes
    return { score, settings };
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // EXPORT GP7 โ€” Renvoie un Uint8Array au format Guitar Pro 7 (.gp)
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function exportGP7(meta, revisions) {
    const { score, settings } = buildScore(meta, revisions);
    return new alphaTab.exporter.Gp7Exporter().export(score, settings);
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // EXPORT MIDI โ€” Renvoie un Uint8Array au format MIDI standard (.mid)
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function exportMIDI(meta, revisions) {
    const { score, settings } = buildScore(meta, revisions);
    const midiFile  = new alphaTab.midi.MidiFile();
    const handler   = new alphaTab.midi.AlphaSynthMidiFileHandler(midiFile, true);
    new alphaTab.midi.MidiFileGenerator(score, settings, handler).generate();
    return midiFile.toBinary();
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // UTILITAIRE โ€” Dรฉclenche le tรฉlรฉchargement d'un Uint8Array
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function triggerDownload(bytes, fileName, mime) {
    const blob = new Blob([bytes], { type: mime });
    const url  = URL.createObjectURL(blob);
    const a    = document.createElement('a');
    a.href = url; a.download = fileName;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }

  // Convertit le titre en nom de fichier sรปr (pas de caractรจres spรฉciaux)
  function safeName(str) {
    return str.replace(/[^a-zA-Z0-9 _\-]/g, '').trim().replace(/\s+/g, '_') || 'tab';
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // UI โ€” Toast de statut centrรฉ en bas de l'รฉcran
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  let _toastTimer = null;
  let _toast = null;

  function getToast() {
    if (!_toast) {
      _toast = document.createElement('div');
      _toast.id = 'sgd-status';
      document.body.appendChild(_toast);
    }
    return _toast;
  }

  function showStatus(msg, type = '', duration = 4500) {
    const t = getToast();
    t.textContent = msg;
    t.className   = 'visible ' + type;
    clearTimeout(_toastTimer);
    if (duration > 0) {
      _toastTimer = setTimeout(() => {
        t.className = '';
      }, duration);
    }
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // FLUX PRINCIPAL โ€” appelรฉ au clic sur l'un de nos boutons
  // 4 รฉtapes : lecture page โ†’ fetch CDN โ†’ conversion โ†’ tรฉlรฉchargement
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  async function handleDownload(format, btnGP, btnMID) {
    btnGP.disabled  = true;
    btnMID.disabled = true;
    showStatus('โณ Lecture de la pageโ€ฆ', '', 0);

    try {
      // 1. Lire les mรฉtadonnรฉes depuis #state
      const meta = getStateFromPage();
      showStatus(`โณ Rรฉcupรฉration de ${meta.tracks.length} piste(s)โ€ฆ`, '', 0);

      // 2. Tรฉlรฉcharger tous les JSONs de rรฉvision depuis le CDN
      const revisions = await fetchAllRevisions(meta);
      showStatus(`โš™๏ธ Conversion de ${revisions.length} piste(s) โ†’ ${format.toUpperCase()}โ€ฆ`, '', 0);

      // 3. Construire et exporter
      const name = safeName(`${meta.artist} - ${meta.title}`);
      let bytes, fileName, mime;

      if (format === 'gp') {
        bytes    = exportGP7(meta, revisions);
        fileName = `${name}.gp`;
        mime     = 'application/gp';
      } else {
        bytes    = exportMIDI(meta, revisions);
        fileName = `${name}.mid`;
        mime     = 'audio/midi';
      }

      // 4. Dรฉclencher le tรฉlรฉchargement navigateur
      triggerDownload(bytes, fileName, mime);
      showStatus(`โœ… "${fileName}" tรฉlรฉchargรฉ !`, 'ok');

    } catch (err) {
      console.error('[SGD] Erreur :', err);
      showStatus(`โŒ ${err.message}`, 'err', 7000);
    } finally {
      btnGP.disabled  = false;
      btnMID.disabled = false;
    }
  }

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // 7. INJECTION DES BOUTONS โ€” Remplace le bouton d'export natif
  //
  // Le bouton natif est dans : <div id="c-export" class="B3a4pa B3agq5">
  // La barre est un flex container (.B3a1lv).
  // On remplace le div#c-export entier par notre wrapper en lui
  // donnant les mรชmes classes B3a4pa + B3agq5 โ†’ centrage automatique.
  //
  // Rรฉsistance SPA : MutationObserver + hook pushState/popstate.
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

  function isTabPage() {
    return /\/a\/wsa\/.+/.test(location.pathname);
  }

  function createOurButtons() {
    // On reprend les classes du conteneur natif pour hรฉriter du layout flex
    const wrapper    = document.createElement('div');
    wrapper.id       = 'sgd-wrapper';
    wrapper.className = 'B3a4pa B3agq5'; // classes du div#c-export original

    const btnGP      = document.createElement('button');
    btnGP.className  = 'sgd-btn sgd-btn-gp';
    btnGP.innerHTML  = '๐ŸŽธ GP7';
    btnGP.title      = 'Tรฉlรฉcharger Guitar Pro 7 (.gp)';

    const btnMID     = document.createElement('button');
    btnMID.className = 'sgd-btn sgd-btn-midi';
    btnMID.innerHTML = '๐ŸŽน MIDI';
    btnMID.title     = 'Tรฉlรฉcharger MIDI (.mid)';

    btnGP.addEventListener('click',  () => handleDownload('gp',   btnGP, btnMID));
    btnMID.addEventListener('click', () => handleDownload('midi', btnGP, btnMID));

    wrapper.appendChild(btnGP);
    wrapper.appendChild(btnMID);
    return wrapper;
  }

  function tryInjectButtons() {
    if (!isTabPage()) return false;

    // Dรฉjร  injectรฉ et toujours dans le DOM โ†’ rien ร  faire
    if (document.getElementById('sgd-wrapper')?.isConnected) return true;

    // โ”€โ”€ Cible principale : div#c-export (sรฉlecteur stable) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    const cExport = document.getElementById('c-export');
    if (cExport) {
      cExport.replaceWith(createOurButtons());
      console.log('[SGD] โœ… Injectรฉ (#c-export)');
      return true;
    }

    // โ”€โ”€ Fallback : bouton #control-export โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    const ctrlExport = document.getElementById('control-export');
    if (ctrlExport) {
      const parent = ctrlExport.closest('div') || ctrlExport.parentElement;
      parent.replaceWith(createOurButtons());
      console.log('[SGD] โœ… Injectรฉ (#control-export parent)');
      return true;
    }

    // โ”€โ”€ Fallback : data-id ou title "Download" โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    const nativeBtn = document.querySelector(
      '[data-id*="Download"], [data-id*="Export"], [title*="Download tab"]'
    );
    if (nativeBtn) {
      const anchor = nativeBtn.closest('div') || nativeBtn.parentElement;
      anchor.replaceWith(createOurButtons());
      console.log('[SGD] โœ… Injectรฉ (fallback title)');
      return true;
    }

    return false;
  }

  // โ”€โ”€ MutationObserver : rรฉ-injecte si le wrapper est supprimรฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  const btnObserver = new MutationObserver(() => {
    if (!document.getElementById('sgd-wrapper')?.isConnected) {
      tryInjectButtons();
    }
  });

  // โ”€โ”€ Hook SPA โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function onSpaNavigate() {
    setTimeout(tryInjectButtons, 300);
    setTimeout(tryInjectButtons, 900);
    setTimeout(tryInjectButtons, 1800);
  }

  const _pushState    = history.pushState.bind(history);
  history.pushState   = function (...a) { _pushState(...a);     onSpaNavigate(); };
  const _replaceState = history.replaceState.bind(history);
  history.replaceState = function (...a) { _replaceState(...a); onSpaNavigate(); };
  window.addEventListener('popstate', onSpaNavigate);

  // โ”€โ”€ Dรฉmarrage โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function startObserving() {
    const go = () => {
      btnObserver.observe(document.body, { childList: true, subtree: true });
      tryInjectButtons();
    };
    if (document.body) go();
    else document.addEventListener('DOMContentLoaded', go);
  }

  startObserving();

})();