Qobuz Availability

Qobuz album/track availability across 26 storefront countries. Auto-start, compact, floating minimize.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Qobuz Availability
// @icon         https://www.google.com/s2/favicons?sz=64&domain=qobuz.com
// @namespace    https://example.com/
// @version      1.0.5
// @author       Zxcvr
// @description  Qobuz album/track availability across 26 storefront countries. Auto-start, compact, floating minimize.
// @match        https://qobuz.com/*
// @match        https://www.qobuz.com/*
// @match        https://open.qobuz.com/*
// @match        https://play.qobuz.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @connect      qobuz.com
// @connect      www.qobuz.com
// ==/UserScript==

(() => {
  "use strict";

  const MARKETS = [
    { cc: "AR", name: "Argentina", locales: ["ar-es", "ar-en"] },
    { cc: "AU", name: "Australia", locales: ["au-en"] },
    { cc: "AT", name: "Austria", locales: ["at-de", "at-en"] },
    { cc: "BE", name: "Belgium", locales: ["be-fr", "be-nl", "be-en"] },
    { cc: "BR", name: "Brazil", locales: ["br-pt", "br-en"] },
    { cc: "CA", name: "Canada", locales: ["ca-en", "ca-fr"] },
    { cc: "CL", name: "Chile", locales: ["cl-es", "cl-en"] },
    { cc: "CO", name: "Colombia", locales: ["co-es", "co-en"] },
    { cc: "DK", name: "Denmark", locales: ["dk-en", "dk-da"] },
    { cc: "FI", name: "Finland", locales: ["fi-en", "fi-fi"] },
    { cc: "FR", name: "France", locales: ["fr-fr", "fr-en"] },
    { cc: "DE", name: "Germany", locales: ["de-de", "de-en"] },
    { cc: "IE", name: "Ireland", locales: ["ie-en"] },
    { cc: "IT", name: "Italy", locales: ["it-it", "it-en"] },
    { cc: "JP", name: "Japan", locales: ["jp-ja", "jp-en"] },
    { cc: "LU", name: "Luxembourg", locales: ["lu-fr", "lu-de", "lu-en"] },
    { cc: "NL", name: "Netherlands", locales: ["nl-nl", "nl-en"] },
    { cc: "MX", name: "Mexico", locales: ["mx-es", "mx-en"] },
    { cc: "NZ", name: "New Zealand", locales: ["nz-en"] },
    { cc: "NO", name: "Norway", locales: ["no-en", "no-nb"] },
    { cc: "PT", name: "Portugal", locales: ["pt-pt", "pt-en"] },
    { cc: "ES", name: "Spain", locales: ["es-es", "es-en"] },
    { cc: "SE", name: "Sweden", locales: ["se-en", "se-sv"] },
    { cc: "CH", name: "Switzerland", locales: ["ch-de", "ch-fr", "ch-it", "ch-en"] },
    { cc: "GB", name: "United Kingdom", locales: ["gb-en"] },
    { cc: "US", name: "United States", locales: ["us-en"] },
  ];

  const STORE_BASES = ["https://qobuz.com", "https://www.qobuz.com"];

  const CONCURRENCY = 6;
  const LOCALE_DELAY_MS = 70;

  const sleep = (ms) => new Promise(r => setTimeout(r, ms));

  let cancelRequested = false;
  let autoRunTimer = null;
  let autoTries = 0;
  const AUTO_MAX_TRIES = 6;

  // Persist globally
  const STORE_KEY_FLOAT = "qz_av_floating_v1";
  const STORE_KEY_MIN   = "qz_av_minimized_v1";
  const STORE_KEY_PREF  = "qz_av_prefCountries_v1";

  const lsGet = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
  const lsSet = (k, v) => { try { localStorage.setItem(k, v); } catch {} };

  // ---- preferred countries (global) ----
  const PREF = {
    set: new Set(),
    load() {
      try {
        const raw = lsGet(STORE_KEY_PREF);
        if (!raw) return;
        const arr = JSON.parse(raw);
        if (!Array.isArray(arr)) return;
        const allowed = new Set(MARKETS.map(m => m.cc));
        this.set = new Set(arr.map(x => String(x || "").toUpperCase()).filter(x => allowed.has(x)));
      } catch {}
    },
    save() {
      try { lsSet(STORE_KEY_PREF, JSON.stringify(Array.from(this.set).sort())); } catch {}
    },
    has(cc) { return this.set.has(String(cc || "").toUpperCase()); },
    toggle(cc) {
      cc = String(cc || "").toUpperCase();
      if (!cc) return;
      if (this.set.has(cc)) this.set.delete(cc);
      else this.set.add(cc);
      this.save();
    }
  };
  PREF.load();

  GM_addStyle(`
    .qz-av-wrap{
      margin:10px auto;
      padding:10px 10px 8px;
      max-width: 920px;
      width: calc(100% - 24px);
      border:1px solid rgba(255,255,255,.18);
      border-radius:14px;
      background:rgba(0,0,0,.35);
      color:#fff;
      font: 13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
      backdrop-filter: blur(6px);
      box-shadow: 0 12px 34px rgba(0,0,0,.20);
      z-index:999999;
    }

    .qz-titlebar{
      display:flex; align-items:center; justify-content:space-between;
      gap:10px;
      margin-bottom:8px;
    }
    .qz-av-title{font-size:18px;font-weight:900;margin:0; display:flex; align-items:baseline; gap:10px;}
    .qz-credit{font-size:12px; font-weight:900; opacity:.7; letter-spacing:.2px;}

    .qz-titlebtns{display:flex;gap:8px;align-items:center}
    .qz-iconbtn{
      width:34px;height:34px;
      border-radius:10px;
      border:1px solid rgba(255,255,255,.18);
      background:rgba(255,255,255,.08);
      color:#fff; cursor:pointer;
      display:inline-flex;align-items:center;justify-content:center;
      font-weight:900;
    }
    .qz-iconbtn:hover{background:rgba(255,255,255,.12)}
    .qz-av-wrap:not(.qz-av-floating) .qz-iconbtn.minbtn{display:none;}

    .qz-av-toprow{display:flex; gap:10px; flex-wrap:wrap; align-items:center}
    .qz-av-toprow input{
      width: 260px; max-width: 65vw;
      padding:7px 10px;
      border-radius:9999px;
      border:1px solid rgba(255,255,255,.18);
      background:rgba(255,255,255,.06);
      color:#fff;
      outline:none;
      font-weight: 700;
    }
    .qz-av-btns{display:flex;gap:8px;flex-wrap:wrap}
    .qz-av-btns button{
      padding:7px 10px;
      border-radius:10px;
      border:1px solid rgba(255,255,255,.18);
      background:rgba(255,255,255,.08);
      color:#fff; cursor:pointer; font-weight:700
    }
    .qz-av-btns button:hover{background:rgba(255,255,255,.12)}

    .qz-av-progress{
      font-size:12px;
      opacity:.9;
      margin:6px 0 6px;
      min-height: 14px;
    }

    /* scanning bar */
    #qz_bar{
      width: 100%;
      height: 8px;
      display: none;
      margin: 6px 0 0;
      border-radius: 9999px;
      overflow: hidden;
      accent-color: #18c1c1;
    }
    #qz_bar::-webkit-progress-bar{
      background: rgba(255,255,255,.14);
      border-radius: 9999px;
    }
    #qz_bar::-webkit-progress-value{
      background: rgba(24,193,193,.90);
      border-radius: 9999px;
    }
    #qz_bar::-moz-progress-bar{
      background: rgba(24,193,193,.90);
      border-radius: 9999px;
    }

    .qz-av-block{
      background:rgba(0,0,0,.30);
      border:1px solid rgba(255,255,255,.12);
      padding:10px;
      border-radius:12px;
      margin-top:10px;
    }
    .qz-av-h{font-weight:900;margin:0 0 8px;font-size:16px}

    .qz-section-head{
      display:flex; align-items:center; gap:8px;
      margin:10px 0 7px;
      font-weight:900;
      font-size:15px;
    }

    .qz-copybtn{
      width:34px; height:34px;
      border-radius:10px;
      border:1px solid rgba(255,255,255,.18);
      background:rgba(255,255,255,.08);
      display:inline-flex; align-items:center; justify-content:center;
      cursor:pointer;
    }
    .qz-copybtn:hover{background:rgba(255,255,255,.12)}
    .qz-copybtn svg{width:18px;height:18px;opacity:.95}

    .qz-av-row{display:flex;flex-wrap:wrap;gap:8px}

    .qz-chip{
      display:inline-flex; align-items:center; gap:8px;
      padding:6px 10px;
      border-radius:9999px;
      border:1px solid rgba(255,255,255,.18);
      background:rgba(255,255,255,.06);
      font-weight:900;
      text-decoration:none;
      color:#fff;
      user-select:none;
    }
    .qz-chip:hover{background:rgba(255,255,255,.10)}
    .qz-chip.unav{opacity:.55;background:rgba(255,255,255,.04)}
    .qz-chip.hidden{display:none}

    /* preferred highlight */
    .qz-chip.qz-pref{
      border-color: rgba(24,193,193,.75);
      box-shadow: 0 0 0 1px rgba(24,193,193,.35) inset;
      background: rgba(24,193,193,.10);
    }

    .qz-flag{
      width:18px; height:12px; border-radius:2px;
      object-fit:cover; display:block;
      box-shadow: 0 0 0 1px rgba(255,255,255,.12) inset;
    }

    .qz-av-floating{
      position:fixed;right:14px;bottom:14px;
      width:min(560px, calc(100vw - 28px));
      max-width:none;
      max-height:75vh;overflow:auto;
      margin:0;
    }

    .qz-av-floating.qz-minimized{
      width: 360px;
      max-height: none;
      overflow: hidden;
      padding:10px 10px 10px;
    }
    .qz-minimized .qz-av-toprow,
    .qz-minimized #qz_out,
    .qz-minimized #qz_progress,
    .qz-minimized #qz_bar,
    .qz-minimized #qz_adv { display:none; }

    /* advanced */
    #qz_adv{
      margin-top: 10px;
      padding-top: 8px;
      border-top: 1px solid rgba(255,255,255,.12);
    }
    #qz_adv summary{
      cursor:pointer;
      list-style:none;
      font-weight: 900;
      opacity: .9;
      user-select:none;
    }
    #qz_adv summary::-webkit-details-marker{ display:none; }
    #qz_adv .qz-adv-body{
      margin-top: 8px;
    }
    #qz_pref_list{
      display:flex; flex-wrap:wrap; gap:8px;
      margin-top: 6px;
    }
    .qz-prefopt{
      cursor:pointer;
    }
  `);

  function gmGet(url) {
    return new Promise((resolve) => {
      GM_xmlhttpRequest({
        method: "GET",
        url,
        timeout: 25000,
        anonymous: true,
        withCredentials: false,
        onload: (resp) => resolve(resp),
        onerror: () => resolve({ status: 0, responseText: "" }),
        ontimeout: () => resolve({ status: 0, responseText: "" }),
      });
    });
  }

  function stripLocalePrefix(pathname) {
    return pathname.replace(/^\/[a-z]{2}-[a-z]{2}\//, "/");
  }

  function findTypeIdAndStorePath() {
    const u = new URL(location.href);
    const parts = u.pathname.split("/").filter(Boolean);
    const hasLocale = parts[0] && /^[a-z]{2}-[a-z]{2}$/.test(parts[0]);
    const idx = hasLocale ? 1 : 0;
    const type = parts[idx];
    if (type !== "album" && type !== "track") return null;
    const id = parts[parts.length - 1] || "";
    const storePath = stripLocalePrefix(u.pathname);
    return { type, id, storePath };
  }

  function looksUnavailable(html) {
    const t = (html || "").toLowerCase();
    return (
      t.includes("page not found") ||
      t.includes("not found") ||
      t.includes("error 404") ||
      (t.includes("not available") && (t.includes("country") || t.includes("region"))) ||
      t.includes("n'est pas disponible") ||
      t.includes("nicht verfügbar") ||
      t.includes("non è disponibile") ||
      t.includes("no está disponible") ||
      t.includes("não está disponível")
    );
  }

  function looksLikeProductPage(html, type) {
    const t = (html || "");
    if (type === "album") return t.includes("MusicAlbum") || /og:type"\s+content="music\.album"/i.test(t);
    if (type === "track") return t.includes("MusicRecording") || /og:type"\s+content="music\.song"/i.test(t);
    return false;
  }

  async function checkCountry(country, storePath, type) {
    for (const base of STORE_BASES) {
      for (const locale of country.locales) {
        if (cancelRequested) return { ok: false, cancelled: true };
        const url = `${base}/${locale}${storePath}`;
        const resp = await gmGet(url);
        if (resp.status >= 200 && resp.status < 300) {
          const html = resp.responseText || "";
          if (!looksUnavailable(html) && looksLikeProductPage(html, type)) {
            return { ok: true, locale, url };
          }
        }
        await sleep(LOCALE_DELAY_MS);
      }
    }
    return { ok: false, locale: null, url: null };
  }

  async function promisePool(items, worker, concurrency) {
    let i = 0;
    const results = new Array(items.length);

    const runners = new Array(concurrency).fill(0).map(async () => {
      while (i < items.length && !cancelRequested) {
        const idx = i++;
        results[idx] = await worker(items[idx], idx);
      }
    });

    await Promise.all(runners);
    return results;
  }

  function flagImgUrl(cc) {
    return `https://flagcdn.com/h20/${cc.toLowerCase()}.png`;
  }

  const COPY_SVG = `
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path d="M9 9h10v10H9V9Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
      <path d="M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
    </svg>
  `;

  let box = null;
  let lastHref = location.href;

  function bestMount() {
    return document.querySelector("main") ||
           document.querySelector("#app") ||
           document.querySelector("#root") ||
           document.body;
  }

  function setProgress(msg) {
    const el = box?.querySelector("#qz_progress");
    if (el) el.textContent = msg || "";
  }

  function showBar(total) {
    const bar = box?.querySelector("#qz_bar");
    if (!bar) return;
    bar.max = total || 1;
    bar.value = 0;
    bar.style.display = "block";
  }
  function setBar(done, total) {
    const bar = box?.querySelector("#qz_bar");
    if (!bar) return;
    if (typeof total === "number") bar.max = total || 1;
    bar.value = done || 0;
  }
  function hideBar() {
    const bar = box?.querySelector("#qz_bar");
    if (!bar) return;
    bar.style.display = "none";
    bar.value = 0;
  }

  function applyFilter(q) {
    q = (q || "").trim().toLowerCase();
    const chips = box.querySelectorAll(".qz-chip");
    chips.forEach(ch => {
      const cc = (ch.dataset.cc || "").toLowerCase();
      const name = (ch.dataset.name || "").toLowerCase();
      const ok = !q || cc.includes(q) || name.includes(q);
      ch.classList.toggle("hidden", !ok);
    });
  }

  function applyPrefsToRendered() {
    if (!box) return;
    for (const el of box.querySelectorAll(".qz-chip[data-cc]")) {
      const cc = (el.dataset.cc || "").toUpperCase();
      el.classList.toggle("qz-pref", PREF.has(cc));
    }
    for (const el of box.querySelectorAll(".qz-prefopt[data-cc]")) {
      const cc = (el.dataset.cc || "").toUpperCase();
      el.classList.toggle("qz-pref", PREF.has(cc));
    }
  }

  function updateMinBtn() {
    const btn = box?.querySelector("#qz_min");
    if (!btn) return;
    const minimized = box.classList.contains("qz-minimized");
    btn.textContent = minimized ? "▢" : "—";
    btn.title = minimized ? "Expand" : "Minimize";
  }

  function applySavedFloatMinState() {
    const floatOn = lsGet(STORE_KEY_FLOAT) === "1";
    box.classList.toggle("qz-av-floating", floatOn);

    const minOn = lsGet(STORE_KEY_MIN) === "1";
    box.classList.toggle("qz-minimized", floatOn && minOn);

    updateMinBtn();
  }

  function setMinimized(on) {
    box.classList.toggle("qz-minimized", !!on);
    lsSet(STORE_KEY_MIN, on ? "1" : "0");
    updateMinBtn();
  }

  function renderPrefChooser() {
    const host = box?.querySelector("#qz_pref_list");
    if (!host) return;

    host.innerHTML = "";
    for (const m of MARKETS.slice().sort((a, b) => a.cc.localeCompare(b.cc))) {
      const el = document.createElement("span");
      el.className = "qz-chip qz-prefopt";
      el.dataset.cc = m.cc;
      el.dataset.name = m.name;
      el.title = m.name;

      const flag = document.createElement("img");
      flag.className = "qz-flag";
      flag.alt = "";
      flag.src = flagImgUrl(m.cc);
      flag.addEventListener("error", () => flag.remove(), { once: true });

      const label = document.createElement("span");
      label.textContent = m.cc;

      el.append(flag, label);

      el.addEventListener("click", () => {
        PREF.toggle(m.cc);
        applyPrefsToRendered();
      });

      host.appendChild(el);
    }

    applyPrefsToRendered();
  }

  function ensureBox() {
    if (box && document.contains(box)) return box;

    box = document.createElement("div");
    box.className = "qz-av-wrap";
    box.innerHTML = `
      <div class="qz-titlebar">
        <div class="qz-av-title">Qobuz Availability <span class="qz-credit">by Zxcvr</span></div>
        <div class="qz-titlebtns">
          <button class="qz-iconbtn minbtn" id="qz_min" title="Minimize">—</button>
        </div>
      </div>

      <div class="qz-av-toprow">
        <input id="qz_filter" placeholder="Search (JP / Japan / United...)" />
        <div class="qz-av-btns">
          <button id="qz_float">Toggle Floating</button>
        </div>
      </div>

      <div class="qz-av-progress" id="qz_progress"></div>
      <progress id="qz_bar" max="1" value="0"></progress>
      <div id="qz_out"></div>

      <details id="qz_adv">
        <summary>Advanced</summary>
        <div class="qz-adv-body">
          <div style="font-weight:900; opacity:.9;">Choose default country(s)</div>
          <div id="qz_pref_list"></div>
        </div>
      </details>
    `;

    bestMount().prepend(box);

    applySavedFloatMinState();

    box.querySelector("#qz_min").onclick = () => {
      setMinimized(!box.classList.contains("qz-minimized"));
    };

    box.querySelector("#qz_float").onclick = () => {
      const nowOn = !box.classList.contains("qz-av-floating");
      box.classList.toggle("qz-av-floating", nowOn);
      lsSet(STORE_KEY_FLOAT, nowOn ? "1" : "0");

      if (nowOn) {
        const savedMin = lsGet(STORE_KEY_MIN) === "1";
        box.classList.toggle("qz-minimized", savedMin);
      } else {
        box.classList.remove("qz-minimized");
      }
      updateMinBtn();
    };

    updateMinBtn();
    renderPrefChooser();
    return box;
  }

  function createChipEl(obj, cls) {
    const title = obj.locale ? `${obj.name} — ${obj.locale}` : obj.name;

    const flag = document.createElement("img");
    flag.className = "qz-flag";
    flag.alt = "";
    flag.src = flagImgUrl(obj.cc);
    flag.addEventListener("error", () => flag.remove(), { once: true });

    const label = document.createElement("span");
    label.textContent = obj.cc;

    let el;
    if (obj.url) {
      el = document.createElement("a");
      el.href = obj.url;
      el.target = "_blank";
      el.rel = "noreferrer";
    } else {
      el = document.createElement("span");
    }

    el.className = `qz-chip ${cls || ""} ${PREF.has(obj.cc) ? "qz-pref" : ""}`.trim();
    el.dataset.cc = obj.cc;
    el.dataset.name = obj.name;
    el.title = title;
    el.append(flag, label);
    return el;
  }

  function renderLiveShell(kind, availableArrRef, unavailableArrRef) {
    const out = box.querySelector("#qz_out");
    out.innerHTML = `
      <div class="qz-av-block">
        <div class="qz-av-h">${kind} Availability</div>

        <div class="qz-section-head">
          <span id="qz_av_count">Available in (0):</span>
          <button class="qz-copybtn" id="qz_copy_av" title="Copy">${COPY_SVG}</button>
        </div>
        <div class="qz-av-row" id="qz_av_list"></div>

        <div class="qz-section-head" style="margin-top:12px;">
          <span id="qz_un_count">Unavailable in (0):</span>
          <button class="qz-copybtn" id="qz_copy_unav" title="Copy">${COPY_SVG}</button>
        </div>
        <div class="qz-av-row" id="qz_un_list"></div>
      </div>
    `;

    out.querySelector("#qz_copy_av").onclick = () => GM_setClipboard(availableArrRef.map(x => x.cc).join(", "));
    out.querySelector("#qz_copy_unav").onclick = () => GM_setClipboard(unavailableArrRef.map(x => x.cc).join(", "));
  }

  function render(kind, availableObjs, unavailableObjs) {
    const out = box.querySelector("#qz_out");

    const chipHtml = (obj, cls) => {
      const title = obj.locale ? `${obj.name} — ${obj.locale}` : obj.name;
      const flag = `<img class="qz-flag" data-flag="1" alt="" src="${flagImgUrl(obj.cc)}">`;
      const label = `<span>${obj.cc}</span>`;
      const prefClass = PREF.has(obj.cc) ? " qz-pref" : "";

      if (obj.url) {
        return `<a class="qz-chip ${cls || ""}${prefClass}" data-cc="${obj.cc}" data-name="${obj.name}"
                  href="${obj.url}" target="_blank" rel="noreferrer" title="${title}">
                  ${flag}${label}
                </a>`;
      }
      return `<span class="qz-chip ${cls || ""}${prefClass}" data-cc="${obj.cc}" data-name="${obj.name}" title="${title}">
                ${flag}${label}
              </span>`;
    };

    out.innerHTML = `
      <div class="qz-av-block">
        <div class="qz-av-h">${kind} Availability</div>

        <div class="qz-section-head">
          <span>Available in (${availableObjs.length}):</span>
          <button class="qz-copybtn" id="qz_copy_av" title="Copy">${COPY_SVG}</button>
        </div>
        <div class="qz-av-row">
          ${availableObjs.map(o => chipHtml(o, "")).join("")}
        </div>

        <div class="qz-section-head" style="margin-top:12px;">
          <span>Unavailable in (${unavailableObjs.length}):</span>
          <button class="qz-copybtn" id="qz_copy_unav" title="Copy">${COPY_SVG}</button>
        </div>
        <div class="qz-av-row">
          ${unavailableObjs.map(o => chipHtml(o, "unav")).join("")}
        </div>
      </div>
    `;

    out.querySelectorAll('img[data-flag="1"]').forEach(img => {
      img.addEventListener("error", () => img.remove(), { once: true });
    });

    out.querySelector("#qz_copy_av").onclick = () => GM_setClipboard(availableObjs.map(x => x.cc).join(", "));
    out.querySelector("#qz_copy_unav").onclick = () => GM_setClipboard(unavailableObjs.map(x => x.cc).join(", "));

    applyFilter(box.querySelector("#qz_filter").value);
    applyPrefsToRendered();
  }

  async function run(isAuto = false) {
    const info = findTypeIdAndStorePath();
    if (!info) {
      if (isAuto && autoTries < AUTO_MAX_TRIES) scheduleAutoRun(550, true);
      return;
    }

    autoTries = 0;
    cancelRequested = false;
    ensureBox();

    const total = MARKETS.length;
    const kind = info.type === "album" ? "Album" : "Track";

    const availableLive = [];
    const unavailableLive = [];

    renderLiveShell(kind, availableLive, unavailableLive);

    const avList = box.querySelector("#qz_av_list");
    const unList = box.querySelector("#qz_un_list");
    const avCount = box.querySelector("#qz_av_count");
    const unCount = box.querySelector("#qz_un_count");

    showBar(total);
    setBar(0, total);

    let done = 0;
    setProgress(`Scanning… ${done}/${total}`);

    let lastUi = 0;

    const worker = async (c) => {
      const r = await checkCountry(c, info.storePath, info.type);

      if (cancelRequested) return r;

      const obj = r?.ok
        ? { cc: c.cc, name: c.name, url: r.url, locale: r.locale }
        : { cc: c.cc, name: c.name, url: null, locale: null };

      if (r?.ok) {
        availableLive.push(obj);
        avList.appendChild(createChipEl(obj, ""));
        avCount.textContent = `Available in (${availableLive.length}):`;
      } else {
        unavailableLive.push(obj);
        unList.appendChild(createChipEl(obj, "unav"));
        unCount.textContent = `Unavailable in (${unavailableLive.length}):`;
      }

      done++;
      setBar(done, total);

      const now = performance.now();
      if (now - lastUi > 120 || done === total) {
        lastUi = now;
        setProgress(`Scanning… ${done}/${total}`);
        applyFilter(box.querySelector("#qz_filter").value);
      }

      return r;
    };

    await promisePool(MARKETS, worker, CONCURRENCY);

    if (cancelRequested) { setProgress(""); hideBar(); return; }

    const availableSorted = availableLive.slice().sort((a, b) => a.cc.localeCompare(b.cc));
    const unavailableSorted = unavailableLive.slice().sort((a, b) => a.cc.localeCompare(b.cc));

    render(kind, availableSorted, unavailableSorted);
    setProgress("");
    hideBar();
  }

  function scheduleAutoRun(delayMs = 550, isRetry = false) {
    clearTimeout(autoRunTimer);
    autoRunTimer = setTimeout(() => {
      if (cancelRequested) return;
      if (!isRetry) autoTries = 0;
      autoTries++;
      run(true).catch(() => {});
    }, delayMs);
  }

  function wire() {
    ensureBox();
    const filter = box.querySelector("#qz_filter");
    filter.oninput = () => applyFilter(filter.value);
    scheduleAutoRun(550, false);
  }

  function onNavMaybe() {
    if (location.href === lastHref) return;
    lastHref = location.href;

    clearTimeout(autoRunTimer);
    cancelRequested = true;

    if (box) box.remove();
    box = null;

    setTimeout(() => {
      cancelRequested = false;
      wire();
    }, 250);
  }

  function installNavHooks() {
    const trigger = () => setTimeout(onNavMaybe, 50);

    const ps = history.pushState;
    history.pushState = function () { ps.apply(this, arguments); trigger(); };

    const rs = history.replaceState;
    history.replaceState = function () { rs.apply(this, arguments); trigger(); };

    window.addEventListener("popstate", trigger);
    window.addEventListener("hashchange", trigger);
  }

  wire();
  installNavHooks();
})();