OmniDown

Vše v jednom, rychlý a bezplatný downloader videí ve vysoké kvalitě. Podporuje stahování videí z YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (dříve Twitter) og dalších. Užijte si plynulé, kvalitní stahování bez vodoznaků.

Před instalací, Greasy Fork chce abyste věděli, že tento skript obsahuje "antifunkce", což jsou věci, které spíše přispívají autorovi skriptu, než vám.

Tento skript přidává reklamy na vámi navštěvované stránky. Autor skriptu vysvětluje: This script uses external services as fallback when direct download is blocked.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

Advertisement:

// ==UserScript==
// @name         OmniDown
// @name:ar      OmniDown
// @name:bg      OmniDown
// @name:cs      OmniDown
// @name:da      OmniDown
// @name:de      OmniDown
// @name:el      OmniDown
// @name:en      OmniDown
// @name:eo      OmniDown
// @name:es      OmniDown
// @name:es-419  OmniDown
// @name:fi      OmniDown
// @name:fr      OmniDown
// @name:fr-CA   OmniDown
// @name:he      OmniDown
// @name:hr      OmniDown
// @name:hu      OmniDown
// @name:id      OmniDown
// @name:it      OmniDown
// @name:ja      OmniDown
// @name:ka      OmniDown
// @name:ko      OmniDown
// @name:nb      OmniDown
// @name:nl      OmniDown
// @name:pl      OmniDown
// @name:pt-BR   OmniDown
// @name:ro      OmniDown
// @name:ru      OmniDown
// @name:sv      OmniDown
// @name:th      OmniDown
// @name:tr      OmniDown
// @name:uk      OmniDown
// @name:ug      OmniDown
// @name:vi      OmniDown
// @name:zh-CN   OmniDown
// @name:zh-TW   OmniDown
// @description       An all-in-one, fast, and free HD video downloader. Supports video downloads from YouTube, TikTok, Douyin, Instagram, Threads, XiaoHongShu, TED, Facebook, X (formerly Twitter), and more. Enjoy seamless, high-quality downloads with no watermarks.
// @description:ar    أداة شاملة وسريعة ومجانية لتحميل مقاطع الفيديو بدقة عالية. يدعم تحميل مقاطع الفيديو من يوتيوب، تيك توك، Douyin، إنستغرام، Facebook، ثريدز، XiaoHongShu، TED، إكس (تويتر سابقًا)، والمزيد. استمتع بتحميلات عالية الجودة بدون علامات مائية.
// @description:bg    Всичко в едно, бързо и безплатно изтегляне на видео с висока резолюция. Поддържа изтегляне на видеа от YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (бивш Twitter) и други. Насладете се на безпроблемни, висококачествени изтегляния без водни знаци.
// @description:cs    Vše v jednom, rychlý a bezplatný downloader videí ve vysoké kvalitě. Podporuje stahování videí z YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (dříve Twitter) og dalších. Užijte si plynulé, kvalitní stahování bez vodoznaků.
// @description:da    En alt-i-en, hurtig og gratis HD video downloader. Understøtter video downloads fra YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (tidligere Twitter) og mere. Nyd problemfri, høj kvalitet downloads uden vandmærker.
// @description:de    Ein All-in-One, schneller und kostenloser HD-Video-Downloader. Unterstützt das Herunterladen von Videos von YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (ehemals Twitter) und mehr. Genießen Sie nahtlose, hochqualitative Downloads ohne Wasserzeichen.
// @description:el    Ένας ολοκληρωμένος, γρήγορος και δωρεάν downloader HD βίντεο. Υποστηρίζει λήψη βίντεο από YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (πρώην Twitter) και άλλα. Απολαύστε απρόσκοπτες, υψηλής ποιότητας λήψεις χωρίς υδατογραφήματα.
// @description:en    An all-in-one, fast, and free HD video downloader. Supports video downloads from YouTube, TikTok, Douyin, Instagram, Threads, XiaoHongShu, TED, Facebook, X (formerly Twitter), and more. Enjoy seamless, high-quality downloads with no watermarks.
// @description:eo    Ĉio-en-unu, rapida kaj senpaga HD-video-elŝutilo. Subtenas video-elŝutojn de YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (antaŭe Twitter), kaj aliaj. Ŝatu seninterrompajn, altkvalitajn elŝutojn sen akvamaĵoj.
// @description:es    Un descargador de video todo en uno, rápido y gratuito en HD. Admite descargas de videos de YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (anteriormente Twitter) y más. Disfruta de descargas de alta calidad sin marcas de agua.
// @description:fi    Kaikki yhdessä, nopea ja ilmainen HD-videoiden lataaja. Tukee videoiden lataamista YouTubesta, TikTokista, Douyinista, Instagramista, Facebookista, Threadsista, XiaoHongShusta, X:stä (entinen Twitter) ja muista. Naji saumatonta, korkealaatuista lataamista ilman vesileimoja.
// @description:fr    Un téléchargeur vidéo HD tout-en-un, rapide et gratuit. Prend en charge les téléchargements de vidéos depuis YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (anciennement Twitter) et plus encore. Profitez de téléchargements fluides et de haute qualidade sans filigranes.
// @description:fr-CA Un téléchargeur vidéo HD tout-en-un, rapide et gratuit. Prend en charge les téléchargements de vidéos depuis YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (anciennement Twitter) et plus encore. Profitez de téléchargements fluides et de haute qualidade sans filigranes.
// @description:he    מנוע הורדות וידאו HD מהיר, חינמי ומקיף. תומך בהורדת וידאו מיוטיوب, טיקטوك, אינסטגרם, Facebook, Threads, XiaoHongShu, TED, X (בעבר טוויטר) ועוד. תהנה מהורדות באיכות גבוהה ללא סימני מים.
// @description:hr    Svi-u-jednom, brzi i besplatni HD downloader za video. Podržava preuzimanje videa s YouTubea, TikToka, Instagrama, Facebooka, Threads, XiaoHongShu, TED, X (bivši Twitter) i još mnogo toga. Uživajte u besprijekornim, visokokvalitetnim preuzimanjima bez vodmarks.
// @description:hu    Egy minden-egyben, gyors és ingyenes HD videó letöltő. Támogatja a videók letöltését YouTube-ról, TikTok-ról, Instagram-ról, Facebookról, Threads, XiaoHongShu, TED, X (régen Twitter) és másról. Letöltheti a zökkenőmentes, magas minőségű letöltéseket vízjel nélkül.
// @description:id    Downloader video HD serba ada, cepat, dan gratis. Mendukung unduhan video dari YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (sebelumnya Twitter), dan banyak lagi. Nikmati unduhan berkualitas tinggi tanpa tanda air.
// @description:it    Un downloader video HD completo, veloce e gratuito. Supporta il download di video da YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (precedentemente Twitter) e altri. Goditi download fluidi e di alta qualità senza filigrane.
// @description:ja    YouTube の視聴体験を向上させるための高速で無料な HD 動画ダウンローダー。YouTube、TikTok、Douyin、Instagram、Facebook、Threads、XiaoHongShu、X(旧 Twitter)などからの動画ダウンロードをサポート。シームレスで高品質なダウンロードをウォーターマークなしでお楽しみください。
// @description:ka    ყველაფერი ერთში, სწრაფი და უფასო HD ვიდეო ჩამოტვირთვა. მხარს უჭერს ვიდეოს ჩამოტვირთვას YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (ყოფილი Twitter) და სხვა პლატფორმებიდან. დატკბით უარყოფითი ნიშნების გარეშე, უმაღლესი ხარისხის ჩამოტვირთვებით.
// @description:ko    YouTube 시청 경험을 향상시키는 모든 기능을 갖춘, 빠르고 무료인 HD 비디오 다운로드기. YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X(이전 Twitter) 등에서 비디오 다운로드를 지원합니다. 워터마크 없이 원활한 고품질 다운로드를 즐기세요.
// @description:nb    En alt-i-en, rask og gratis HD video nedlaster. Støtter video nedlastinger fra YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (tidligere Twitter), og mer. Nyt sømløse, høy kvalitet nedlastinger uten vannmerker.
// @description:nl    Een alles-in-één, snelle en gratis HD-video downloader. Ondersteunt video-downloads van YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (voorheen Twitter) en meer. Geniet van naadloze, hoogwaardige downloads zonder watermerken.
// @description:pl    Jedno w jednym, szybki i darmowy pobieracz wideo HD. Obsługuje pobieranie wideo z YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (dawniej Twitter) i innych. Ciesz się płynne, wysokiej jakości pobieranie bez znaków wodnych.
// @description:pt-BR Um downloader de vídeo HD completo, rápido e gratuito. Suporta downloads de vídeos do YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (anteriormente Twitter) e muito mais. Aproveite downloads de alta qualidade sem marcas d'água.
// @description:ro    Un downloader video HD rapid și gratuit, totul într-un singur pachet. Suportă descărcarea de videoclipuri de pe YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (fost Twitter) și altele. Bucurați-vă de descărcări de înaltă calitate fără filigrane.
// @description:ru    Все-в-одном, быстрый и бесплатный HD видеозагрузчик. Поддерживает загрузку видео с YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (бывший Twitter) и других. Наслаждайтесь загрузками высокого качества без водяных знаков.
// @description:sv    En allt-i-ett, snabb og gratis HD-video nedladdare. Stöder video nedladdningar från YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (tidigare Twitter) och mer. Njut av sömlösa, högkvalitativa nedladdningar utan vattenmärken.
// @description:th    เครื่องมือดาวน์โหลดวิดีโอ HD ที่รวดเร็วและฟรีแบบครบวงจร รองรับการดาวน์โหลดวิดีโอจาก YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (เดิม Twitter) และอื่นๆ เพลิดเพลินกับการดาวน์โหลดที่มีคุณภาพสูงโดยไม่มีลายน้ำ
// @description:tr    Birçok platformdan video indirmenizi destekleyen, hızlı ve ücretsiz HD video indirme aracıdır. YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (eski Twitter) ve daha fazlasını destekler. Su işareti olmadan kesintisiz yüksek kaliteli indirmelerin keyfini çıkarın.
// @description:uk    Всі в одному, швидкий та безкоштовний HD відео-завантажувач. Підтримує завантаження відео з YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (колишній Twitter) та інших. Насолоджуйтесь безперешкодними завантаженнями без водяних знаків.
// @description:ug    YouTube كۆرۈش تەجرىبەڭىزنى ياخشىلايدىغان، تيز ۋە ھەقسىز HD ۋىدىئو چۈشۈرگۈچى. يۇتۇب، تيك توك، دۇيىن، ئىنستاگرام، Facebook، Threads، XiaoHongShu، TED، X (ئالدىنقى Twitter) ۋە باشقا پلاتفورمالاردىن ۋىدىئو چۈشۈرۈشنى قوللايدۇ. سۇ ئىشاراتسىز ئېغىزدىن سۈپەتلىك چۈشۈرۈشتىن لەززەتلىنىڭ.
// @description:vi    Trình tải video HD tất cả trong một, nhanh chóng và miễn phí. Hỗ trợ tải video từ YouTube, TikTok, Douyin, Instagram, Facebook, Threads, XiaoHongShu, TED, X (trước đây là Twitter) và nhiều nền tảng khác. Thưởng thức tải xuống chất lượng cao mượt mà mà không có watermark.
// @description:zh-CN 一款多合一、快速且免费的在线视频下载器。支持从 YouTube、TikTok、抖音、Instagram、Facebook、Threads、TED、小红书、X(Twitter)等平台下载视频。享受无水印、高质量、流畅便捷的下载体验
// @description:zh-TW 一款多合一、快速且免費的線上視頻下載器。支援從 YouTube、TikTok、抖音、Instagram、Facebook、Threads、TED、小紅書、X(Twitter)等平台下載視頻。享受無水印、高質量、流暢便捷的下載體驗
// @namespace    ksmo
// @version      3.0.1
// @author       ksmo
// @run-at       document-start
// @allFrames    true
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_openInTab
// @grant        unsafeWindow
// @match        *://*.youtube.com/*
// @match        *://x.com/*
// @match        *://twitter.com/*
// @match        *://*.threads.net/*
// @match        *://*.instagram.com/*
// @match        *://*.tiktok.com/*
// @match        *://*.douyin.com/*
// @match        *://*.xiaohongshu.com/*
// @match        *://*.facebook.com/*
// @match        *://*.snapchat.com/*
// @match        *://*.pinterest.com/*
// @match        *://*.ted.com/*
// @match        *://open.spotify.com/*
// @connect      youtube.com
// @connect      googlevideo.com
// @connect      tiktok.com
// @connect      tiktokcdn.com
// @connect      instagram.com
// @connect      cdninstagram.com
// @connect      twitter.com
// @connect      twimg.com
// @connect      facebook.com
// @connect      fbcdn.net
// @connect      threads.net
// @connect      douyin.com
// @connect      xiaohongshu.com
// @connect      ted.com
// @connect      snapchat.com
// @connect      sc-cdn.net
// @connect      pinterest.com
// @connect      pinimg.com
// @connect      spotify.com
// @connect      spotifydown.com
// @connect      corsproxy.io
// @connect      api.allorigins.win
// @connect      app.ytdown.to
// @connect      snapscooper.com
// @connect      y2meta.is
// @connect      savefrom.net
// @connect      snaptik.app
// @connect      ttdownloader.tools
// @connect      savetik.co
// @connect      seekin.ai
// @connect      fdownloader.net
// @connect      boilerplatehub.com
// @connect      twittervideodownloader.com
// @connect      ssstwitter.com
// @connect      sssinstagram.com
// @connect      saveclip.app
// @connect      fastdl.app
// @connect      downreels.com
// @connect      storygrab.io
// @connect      schatdownloader.com
// @connect      pindown.io
// @connect      pinpea.com
// @connect      tool77.com
// @antifeature  ads This script uses external services as fallback when direct download is blocked.
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    if (window.__omniDownInitialized) return;
    window.__omniDownInitialized = true;

    // Early-exit: skip non-embed iframes to avoid CPU waste in ad frames
    if (window.self !== window.top && !window.location.pathname.startsWith('/embed/')) {
        return;
    }

    const SETTINGS_KEYS = {
        showButton: 'ksmo_dl_show_button',
        autoFallback: 'ksmo_dl_auto_fallback',
        fallbackService: 'ksmo_dl_fallback_service',
        customProxy: 'ksmo_dl_custom_proxy',
        enableHotkey: 'ksmo_dl_enable_hotkey',
        cobaltApiUrl: 'ksmo_dl_cobalt_api_url',
        language: 'ksmo_dl_language'
    };

    const getSetting = (key, defaultValue) => {
        if (typeof GM_getValue !== 'undefined') {
            return GM_getValue(key, defaultValue);
        }
        const val = localStorage.getItem(key);
        if (val === null) return defaultValue;
        if (val === 'true') return true;
        if (val === 'false') return false;
        return val;
    };

    const setSetting = (key, value) => {
        if (typeof GM_setValue !== 'undefined') {
            GM_setValue(key, value);
        } else {
            localStorage.setItem(key, value.toString());
        }
    };

    const TRANSLATIONS = {
        en: {
            title: 'HD Video Downloader',
            download: 'Download',
            fallback: 'Fallback',
            cobalt: 'Cobalt',
            copyLink: 'Copy Link',
            exportPlaylist: 'Export Playlist',
            subtitles: 'Subtitles',
            subtitleTrack: 'Subtitle Track',
            noFormats: 'No direct download formats found. Use fallback tools below.',
            spotifyRedirect: 'Spotify tracks require external conversion. Directing to spotifydown.com below.',
            externalWarning: '⚠️ External services may contain ads and are not controlled by this script.',
            toastCopySuccess: 'Link copied to clipboard',
            toastCopyFail: 'Failed to copy link',
            toastYtBlocked: 'YouTube is blocking direct downloads. Use fallback.',
            toastNoVideo: 'No video found on this page.',
            toastSubtitlesSuccess: 'Subtitles downloaded successfully',
            toastSubtitlesFail: 'Subtitles download failed',
            toastDownloadSuccess: 'Download completed successfully',
            toastDirectFail: 'Direct download failed. Click "Fallback" below to use external tool.',
            toastDirectFailConfirm: 'Direct download failed. Redirect to external downloader service?',
            toastHlsDetected: 'HLS Stream detected. Redirecting to fallback...',
            toastYtCipherFail: 'YouTube signature cipher decryption failed. Redirecting to fallback...',
            cancel: 'Cancel',
            allCanceled: 'All downloads canceled',
            floatingButtonLabel: 'Open video download options',
            nativeYtDlTitle: 'Download Video (Ctrl+Alt+Shift+D)',
            cobaltConfirmTitle: 'Cobalt Downloader',
            cobaltConfirmDesc: 'Did you copy the share link for this video on TikTok?<br><br>We recommend using the copied share link (Ctrl+C) for the best results on Cobalt.',
            cobaltConfirmYes: 'Yes, use Ctrl+C link',
            cobaltConfirmNo: 'No, use detected link',
            cobaltPromptNotTikTok: 'The clipboard link does not look like a TikTok link. Paste the video link here:',
            cobaltPromptFallback: 'Please paste the copied TikTok link here:',
            toastSettingEnabled: 'enabled',
            toastSettingDisabled: 'disabled',
            toastReloadPage: 'Reload the page.',
            langSelectTitle: 'OmniDown Language / Idioma',
            langSelectDesc: 'Select your language / Selecione o idioma:',
            langSelectConfirm: 'Save / Salvar',
            currentLabel: 'Current',
            deciphering: 'Deciphering...',
            cipherBlocked: 'Cipher Blocked',
            adaptiveNoAudio: 'Adaptive (No Audio)',
            audioOnly: 'Audio Only',
            muxed: 'Muxed',
            menuOpenMenu: '📥 Open Download Menu',
            menuDownloadCobalt: '⚡ Download Media (Cobalt)',
            menuChangeLang: '🌐 Change Language / Alterar Idioma / 更改语言',
            menuSetCobaltUrl: '⚙️ Set Cobalt API URL',
            menuSetProxy: '⚙️ Set Custom CORS Proxy',
            menuToggleBtn: '🔘 Toggle Floating Button UI',
            menuToggleFallback: '🔘 Toggle Auto-Fallback',
            menuToggleHotkey: '🔘 Toggle Keyboard Shortcut',
            toastPlaylistExported: 'Exported {count} playlist links.',
            toastNoPlaylist: 'No playlist links found on page.',
            promptSetProxy: 'Enter custom CORS proxy URL prefix (e.g. https://myproxy.com/?url=) or leave empty to clear:',
            toastProxyConfigured: 'Custom CORS proxy configured.',
            toastProxyCleared: 'Custom CORS proxy cleared.',
            ariaMinimize: 'Minimize',
            ariaClose: 'Close',
            ariaSelectService: 'Select Service',
            ariaFormatDownload: 'Download format {quality}',
            ariaCobaltRow: 'Download with Cobalt',
            promptSetCobaltUrl: 'Enter Cobalt API URL (e.g. cobalt.tools):',
            labelTiktok: 'TikTok Video',
            labelInstagram: 'Instagram Video',
            labelTwitter: 'Twitter Video',
            labelFacebook: 'Facebook Video',
            labelThreads: 'Threads Video',
            labelDouyin: 'Douyin Video',
            labelXhs: 'XiaoHongShu Video',
            labelTed: 'TED Video',
            labelSnapchat: 'Snapchat Video',
            labelPinterest: 'Pinterest Video',
            labelSpotify: 'Spotify Music',
            labelYoutube: 'YouTube Video',
            labelHlsBlob: 'HLS Stream (Direct Download Unsupported)',
            labelHtml5Video: 'HTML5 Video',
            toastCobaltApiUrl: 'Cobalt API URL: {url}'
        },
        pt: {
            title: 'Download de Vídeo HD',
            download: 'Baixar',
            fallback: 'Fallback',
            cobalt: 'Cobalt',
            copyLink: 'Copiar Link',
            exportPlaylist: 'Exportar Playlist',
            subtitles: 'Legendas',
            subtitleTrack: 'Faixa de Legendas',
            noFormats: 'Nenhum formato de download direto encontrado. Use as ferramentas de fallback abaixo.',
            spotifyRedirect: 'Músicas do Spotify requerem conversão externa. Direcionando para spotifydown.com abaixo.',
            externalWarning: '⚠️ Serviços externos podem conter anúncios e não são controlados por este script.',
            toastCopySuccess: 'Link copiado para a área de transferência',
            toastCopyFail: 'Falha ao copiar o link',
            toastYtBlocked: 'O YouTube está bloqueando downloads diretos. Use o fallback.',
            toastNoVideo: 'Nenhum vídeo encontrado nesta página.',
            toastSubtitlesSuccess: 'Legendas baixadas com sucesso',
            toastSubtitlesFail: 'Falha ao baixar legendas',
            toastDownloadSuccess: 'Download concluído com sucesso',
            toastDirectFail: 'Download direto falhou. Clique em "Fallback" abaixo para usar ferramenta externa.',
            toastDirectFailConfirm: 'O download direto falhou. Redirecionar para o serviço de download externo?',
            toastHlsDetected: 'Stream HLS detectado. Redirecionando para o fallback...',
            toastYtCipherFail: 'Decifração da assinatura do YouTube falhou. Redirecionando para o fallback...',
            cancel: 'Cancelar',
            allCanceled: 'Todos os downloads cancelados',
            floatingButtonLabel: 'Abrir opções de download de vídeo',
            nativeYtDlTitle: 'Baixar Vídeo (Ctrl+Alt+Shift+D)',
            cobaltConfirmTitle: 'Downloader Cobalt',
            cobaltConfirmDesc: 'Você copiou o link de compartilhamento deste vídeo no TikTok?<br><br>Recomendamos usar o link copiado (Ctrl+C) para obter o melhor resultado no Cobalt.',
            cobaltConfirmYes: 'Sim, usar link do Ctrl+C',
            cobaltConfirmNo: 'Não, usar link detectado',
            cobaltPromptNotTikTok: 'O link na área de transferência não parece ser do TikTok. Cole o link do vídeo aqui:',
            cobaltPromptFallback: 'Por favor, cole o link copiado do TikTok aqui:',
            toastSettingEnabled: 'ativado',
            toastSettingDisabled: 'desativado',
            toastReloadPage: 'Recarregue a página.',
            langSelectTitle: 'Idioma do OmniDown',
            langSelectDesc: 'Selecione o seu idioma de preferência:',
            langSelectConfirm: 'Salvar',
            currentLabel: 'Atual',
            deciphering: 'Decifrando...',
            cipherBlocked: 'Assinatura Bloqueada',
            adaptiveNoAudio: 'Adaptativo (Sem Áudio)',
            audioOnly: 'Apenas Áudio',
            muxed: 'Misto',
            menuOpenMenu: '📥 Abrir Painel de Download',
            menuDownloadCobalt: '⚡ Baixar Mídia (Cobalt)',
            menuChangeLang: '🌐 Alterar Idioma / Change Language / 更改语言',
            menuSetCobaltUrl: '⚙️ Definir URL da API Cobalt',
            menuSetProxy: '⚙️ Definir Proxy CORS',
            menuToggleBtn: '🔘 Alternar Botão Flutuante',
            menuToggleFallback: '🔘 Alternar Auto-Fallback',
            menuToggleHotkey: '🔘 Alternar Atalho de Teclado',
            toastPlaylistExported: 'Exportado {count} links da playlist.',
            toastNoPlaylist: 'Nenhum link de playlist encontrado na página.',
            promptSetProxy: 'Insira o prefixo de URL do proxy CORS personalizado (ex: https://meuproxy.com/?url=) ou deixe em branco para limpar:',
            toastProxyConfigured: 'Proxy CORS personalizado configurado.',
            toastProxyCleared: 'Proxy CORS personalizado limpo.',
            ariaMinimize: 'Minimizar',
            ariaClose: 'Fechar',
            ariaSelectService: 'Selecionar Serviço',
            ariaFormatDownload: 'Baixar formato {quality}',
            ariaCobaltRow: 'Baixar com Cobalt',
            promptSetCobaltUrl: 'Insira a URL da API Cobalt (ex: cobalt.tools):',
            labelTiktok: 'Vídeo do TikTok',
            labelInstagram: 'Vídeo do Instagram',
            labelTwitter: 'Vídeo do Twitter',
            labelFacebook: 'Vídeo do Facebook',
            labelThreads: 'Vídeo do Threads',
            labelDouyin: 'Vídeo do Douyin',
            labelXhs: 'Vídeo do XiaoHongShu',
            labelTed: 'Vídeo do TED',
            labelSnapchat: 'Vídeo do Snapchat',
            labelPinterest: 'Vídeo do Pinterest',
            labelSpotify: 'Música do Spotify',
            labelYoutube: 'Vídeo do YouTube',
            labelHlsBlob: 'Stream HLS (Download Direto Não Suportado)',
            labelHtml5Video: 'Vídeo HTML5',
            toastCobaltApiUrl: 'URL da API Cobalt: {url}'
        },
        'zh-CN': {
            title: 'HD 视频下载器',
            download: '下载',
            fallback: '备用下载',
            cobalt: 'Cobalt',
            copyLink: '复制链接',
            exportPlaylist: '导出播放列表',
            subtitles: '字幕',
            subtitleTrack: '字幕轨',
            noFormats: '未找到直接下载格式。请使用下方备用工具。',
            spotifyRedirect: 'Spotify 歌曲需要外部转换。正在转向下方 spotifydown.com。',
            externalWarning: '⚠️ 外部服务可能包含广告,且不受本脚本控制。',
            toastCopySuccess: '链接已复制到剪贴板',
            toastCopyFail: '复制链接失败',
            toastYtBlocked: 'YouTube 正在阻止直接下载。请使用备用下载。',
            toastNoVideo: '此页面未找到视频。',
            toastSubtitlesSuccess: '字幕下载成功',
            toastSubtitlesFail: '字幕下载失败',
            toastDownloadSuccess: '下载完成',
            toastDirectFail: '直接下载失败。点击下方“备用下载”使用外部工具。',
            toastDirectFailConfirm: '直接下载失败。是否重定向到外部下载服务?',
            toastHlsDetected: '检测到 HLS 流。正在转向备用下载...',
            toastYtCipherFail: 'YouTube 签名解密失败。正在转向备用下载...',
            cancel: '取消',
            allCanceled: '所有下载已取消',
            floatingButtonLabel: '打开视频下载选项',
            nativeYtDlTitle: '下载视频 (Ctrl+Alt+Shift+D)',
            cobaltConfirmTitle: 'Cobalt 下载器',
            cobaltConfirmDesc: '您是否已复制此 TikTok 视频的分享链接?<br><br>建议使用已复制的分享链接 (Ctrl+C) 以在 Cobalt 中获得最佳下载效果。',
            cobaltConfirmYes: '是的,使用剪贴板链接',
            cobaltConfirmNo: '不用,使用检测到的链接',
            cobaltPromptNotTikTok: '剪贴板中的链接似乎不是 TikTok 链接。请在此处粘贴视频链接:',
            cobaltPromptFallback: '请在此处粘贴复制的 TikTok 链接:',
            toastSettingEnabled: '已启用',
            toastSettingDisabled: '已禁用',
            toastReloadPage: '请刷新页面。',
            langSelectTitle: '选择语言',
            langSelectDesc: '请选择您的 OmniDown 首选语言:',
            langSelectConfirm: '保存',
            currentLabel: '当前',
            deciphering: '正在解密...',
            cipherBlocked: '签名受阻',
            adaptiveNoAudio: '自适应 (无音频)',
            audioOnly: '仅音频',
            muxed: '混合',
            menuOpenMenu: '📥 打开下载面板',
            menuDownloadCobalt: '⚡ 使用 Cobalt 下载',
            menuChangeLang: '🌐 切换语言 / Alterar Idioma / Change Language',
            menuSetCobaltUrl: '⚙️ 设置 Cobalt API 地址',
            menuSetProxy: '⚙️ 设置自定义 CORS 代理',
            menuToggleBtn: '🔘 开启/关闭浮动按钮',
            menuToggleFallback: '🔘 开启/关闭自动备用下载',
            menuToggleHotkey: '🔘 开启/关闭快捷键',
            toastPlaylistExported: '已导出 {count} 个播放列表链接。',
            toastNoPlaylist: '页面上未找到播放列表链接。',
            promptSetProxy: '输入自定义 CORS 代理 URL 前缀(例如 https://myproxy.com/?url=)或留空以清除:',
            toastProxyConfigured: '已配置自定义 CORS 代理。',
            toastProxyCleared: '已清除自定义 CORS 代理。',
            ariaMinimize: '最小化',
            ariaClose: '关闭',
            ariaSelectService: '选择服务',
            ariaFormatDownload: '下载格式 {quality}',
            ariaCobaltRow: '使用 Cobalt 下载',
            promptSetCobaltUrl: '输入 Cobalt API 地址(例如 cobalt.tools):',
            labelTiktok: 'TikTok 视频',
            labelInstagram: 'Instagram 视频',
            labelTwitter: 'Twitter 视频',
            labelFacebook: 'Facebook 视频',
            labelThreads: 'Threads 视频',
            labelDouyin: '抖音视频',
            labelXhs: '小红书视频',
            labelTed: 'TED 视频',
            labelSnapchat: 'Snapchat 视频',
            labelPinterest: 'Pinterest 视频',
            labelSpotify: 'Spotify 音乐',
            labelYoutube: 'YouTube 视频',
            labelHlsBlob: 'HLS 流 (不支持直接下载)',
            labelHtml5Video: 'HTML5 视频',
            toastCobaltApiUrl: 'Cobalt API 地址: {url}'
        },
        'zh-TW': {
            title: 'HD 影片下載器',
            download: '下載',
            fallback: '備用下載',
            cobalt: 'Cobalt',
            copyLink: '複製連結',
            exportPlaylist: '匯出播放清單',
            subtitles: '字幕',
            subtitleTrack: '字幕軌',
            noFormats: '未找到直接下載格式。請使用下方備用工具。',
            spotifyRedirect: 'Spotify 歌曲需要外部轉換。正在轉向下方 spotifydown.com。',
            externalWarning: '⚠️ 外部服務可能包含廣告,且不受本腳本控制。',
            toastCopySuccess: '連結已複製到剪貼簿',
            toastCopyFail: '複製連結失敗',
            toastYtBlocked: 'YouTube 正在阻止直接下載。請使用備用下載。',
            toastNoVideo: '此頁面未找到影片。',
            toastSubtitlesSuccess: '字幕下載成功',
            toastSubtitlesFail: '字幕下載失敗',
            toastDownloadSuccess: '下載完成',
            toastDirectFail: '直接下載失敗。點擊下方「備用下載」使用外部工具。',
            toastDirectFailConfirm: '直接下載失敗。是否重新導向到外部下載服務?',
            toastHlsDetected: '偵測到 HLS 流。正在轉向備用下載...',
            toastYtCipherFail: 'YouTube 簽名解密失敗。正在轉向備用下載...',
            cancel: '取消',
            allCanceled: '所有下載已取消',
            floatingButtonLabel: '打開影片下載選項',
            nativeYtDlTitle: '下載影片 (Ctrl+Alt+Shift+D)',
            cobaltConfirmTitle: 'Cobalt 下載器',
            cobaltConfirmDesc: '您是否已複製此 TikTok 影片的分享連結?<br><br>建議使用已複製 of 分享連結 (Ctrl+C) 以在 Cobalt 中獲得最佳下載效果。',
            cobaltConfirmYes: '是的,使用剪貼簿連結',
            cobaltConfirmNo: '不用,使用偵測到的連結',
            cobaltPromptNotTikTok: '剪貼簿中的連結似乎不是 TikTok 連結。請在此處貼上影片連結:',
            cobaltPromptFallback: '請在此處貼上複製 of TikTok 連結:',
            toastSettingEnabled: '已啟用',
            toastSettingDisabled: '已禁用',
            toastReloadPage: '請重新整理頁面。',
            langSelectTitle: '選擇語言',
            langSelectDesc: '請選擇您的 OmniDown 首選語言:',
            langSelectConfirm: '儲存',
            currentLabel: '目前',
            deciphering: '正在解密...',
            cipherBlocked: '簽名受阻',
            adaptiveNoAudio: '自適應 (無音訊)',
            audioOnly: '僅音訊',
            muxed: '混合',
            menuOpenMenu: '📥 打開下載面板',
            menuDownloadCobalt: '⚡ 使用 Cobalt 下載',
            menuChangeLang: '🌐 切換語言 / Alterar Idioma / Change Language',
            menuSetCobaltUrl: '⚙️ 設定 Cobalt API 地址',
            menuSetProxy: '⚙️ 設定自訂 CORS 代理',
            menuToggleBtn: '🔘 開啟/關閉影片按鈕',
            menuToggleFallback: '🔘 開啟/關閉自動備用下載',
            menuToggleHotkey: '🔘 開啟/關閉快捷鍵',
            toastPlaylistExported: '已匯出 {count} 個播放清單連結。',
            toastNoPlaylist: '頁面上未找到播放清單連結。',
            promptSetProxy: '輸入自訂 CORS 代理 URL 前綴(例如 https://myproxy.com/?url=)或留空以清除:',
            toastProxyConfigured: '已設定自訂 CORS 代理。',
            toastProxyCleared: '已清除自訂 CORS 代理。',
            ariaMinimize: '最小化',
            ariaClose: '關閉',
            ariaSelectService: '選擇服務',
            ariaFormatDownload: '下載格式 {quality}',
            ariaCobaltRow: '使用 Cobalt 下載',
            promptSetCobaltUrl: '輸入 Cobalt API 地址(例如 cobalt.tools):',
            labelTiktok: 'TikTok 影片',
            labelInstagram: 'Instagram 影片',
            labelTwitter: 'Twitter 影片',
            labelFacebook: 'Facebook 影片',
            labelThreads: 'Threads 影片',
            labelDouyin: '抖音影片',
            labelXhs: '小紅書影片',
            labelTed: 'TED 影片',
            labelSnapchat: 'Snapchat 影片',
            labelPinterest: 'Pinterest 影片',
            labelSpotify: 'Spotify 音樂',
            labelYoutube: 'YouTube 影片',
            labelHlsBlob: 'HLS 流 (不支援直接下載)',
            labelHtml5Video: 'HTML5 影片',
            toastCobaltApiUrl: 'Cobalt API 地址: {url}'
        },
        vi: {
            title: 'Trình tải video HD',
            download: 'Tải xuống',
            fallback: 'Dự phòng',
            cobalt: 'Cobalt',
            copyLink: 'Sao chép link',
            exportPlaylist: 'Xuất Playlist',
            subtitles: 'Phụ đề',
            subtitleTrack: 'Bản Phụ đề',
            noFormats: 'Không tìm thấy định dạng tải trực tiếp. Sử dụng công cụ dự phòng bên dưới.',
            spotifyRedirect: 'Nhạc Spotify cần chuyển đổi bên ngoài. Đang chuyển đến spotifydown.com bên dưới.',
            externalWarning: '⚠️ Các dịch vụ bên ngoài có thể chứa quảng cáo và không thuộc quyền kiểm soát của script này.',
            toastCopySuccess: 'Đã sao chép link vào bộ nhớ tạm',
            toastCopyFail: 'Không thể sao chép link',
            toastYtBlocked: 'YouTube đang chặn tải trực tiếp. Sử dụng dự phòng.',
            toastNoVideo: 'Không tìm thấy video nào trên trang này.',
            toastSubtitlesSuccess: 'Tải phụ đề thành công',
            toastSubtitlesFail: 'Tải phụ đề thất bại',
            toastDownloadSuccess: 'Tải xuống thành công',
            toastDirectFail: 'Tải trực tiếp thất bại. Nhấp vào "Dự phòng" bên dưới để sử dụng công cụ bên ngoài.',
            toastDirectFailConfirm: 'Tải trực tiếp thất bại. Bạn có muốn chuyển hướng đến dịch vụ tải bên ngoài?',
            toastHlsDetected: 'Đã phát hiện luồng HLS. Đang chuyển hướng sang dự phòng...',
            toastYtCipherFail: 'Giải mã chữ ký YouTube thất bại. Đang chuyển hướng sang dự phòng...',
            cancel: 'Hủy bỏ',
            allCanceled: 'Đã hủy tất cả lượt tải xuống',
            floatingButtonLabel: 'Mở tùy chọn tải video',
            nativeYtDlTitle: 'Tải video (Ctrl+Alt+Shift+D)',
            cobaltConfirmTitle: 'Trình tải Cobalt',
            cobaltConfirmDesc: 'Bạn đã sao chép link chia sẻ của video này trên TikTok chưa?<br><br>Khuyên dùng link chia sẻ đã sao chép (Ctrl+C) để đạt kết quả tốt nhất trên Cobalt.',
            cobaltConfirmYes: 'Có, dùng link Ctrl+C',
            cobaltConfirmNo: 'Không, dùng link phát hiện được',
            cobaltPromptNotTikTok: 'Link trong bộ nhớ tạm không giống link TikTok. Hãy dán link video tại đây:',
            cobaltPromptFallback: 'Vui lòng dán link TikTok đã sao chép tại đây:',
            toastSettingEnabled: 'đã bật',
            toastSettingDisabled: 'đã tắt',
            toastReloadPage: 'Tải lại trang.',
            langSelectTitle: 'Chọn ngôn ngữ',
            langSelectDesc: 'Vui lòng chọn ngôn ngữ ưu thích của bạn cho OmniDown:',
            langSelectConfirm: 'Lưu',
            currentLabel: 'Hiện tại',
            deciphering: 'Đang giải mã...',
            cipherBlocked: 'Chữ ký bị chặn',
            adaptiveNoAudio: 'Tương thích (Không âm thanh)',
            audioOnly: 'Chỉ âm thanh',
            muxed: 'Hỗn hợp',
            menuOpenMenu: '📥 Mở Bảng Tải Xuống',
            menuDownloadCobalt: '⚡ Tải phương tiện (Cobalt)',
            menuChangeLang: '🌐 Thay đổi ngôn ngữ / Alterar Idioma / Change Language',
            menuSetCobaltUrl: '⚙️ Cài đặt URL API Cobalt',
            menuSetProxy: '⚙️ Cài đặt proxy CORS',
            menuToggleBtn: '🔘 Bật/Tắt Nút Nổi UI',
            menuToggleFallback: '🔘 Bật/Tắt Tự Động Dự Phòng',
            menuToggleHotkey: '🔘 Bật/Tắt Phím Tắt',
            toastPlaylistExported: 'Đã xuất {count} link playlist.',
            toastNoPlaylist: 'Không tìm thấy link playlist nào trên trang.',
            promptSetProxy: 'Nhập tiền tố URL proxy CORS tùy chỉnh (ví dụ: https://myproxy.com/?url=) hoặc để trống để xóa:',
            toastProxyConfigured: 'Đã cấu hình proxy CORS tùy chỉnh.',
            toastProxyCleared: 'Đã xóa proxy CORS tùy chỉnh.',
            ariaMinimize: 'Thu nhỏ',
            ariaClose: 'Đóng',
            ariaSelectService: 'Chọn dịch vụ',
            ariaFormatDownload: 'Tải định dạng {quality}',
            ariaCobaltRow: 'Tải xuống bằng Cobalt',
            promptSetCobaltUrl: 'Nhập URL API Cobalt (ví dụ: cobalt.tools):',
            labelTiktok: 'Video TikTok',
            labelInstagram: 'Video Instagram',
            labelTwitter: 'Video Twitter',
            labelFacebook: 'Video Facebook',
            labelThreads: 'Video Threads',
            labelDouyin: 'Video Douyin',
            labelXhs: 'Video XiaoHongShu',
            labelTed: 'Video TED',
            labelSnapchat: 'Video Snapchat',
            labelPinterest: 'Video Pinterest',
            labelSpotify: 'Nhạc Spotify',
            labelYoutube: 'Video YouTube',
            labelHlsBlob: 'Luồng HLS (Không hỗ trợ tải trực tiếp)',
            labelHtml5Video: 'Video HTML5',
            toastCobaltApiUrl: 'URL API Cobalt: {url}'
        },
        es: {
            title: 'Descargador de Video HD',
            download: 'Descargar',
            fallback: 'Fallback',
            cobalt: 'Cobalt',
            copyLink: 'Copiar Enlace',
            exportPlaylist: 'Exportar Lista',
            subtitles: 'Subtítulos',
            subtitleTrack: 'Pista de Subtítulos',
            noFormats: 'No se encontraron formatos de descarga directa. Use las herramientas alternativas abajo.',
            spotifyRedirect: 'Las canciones de Spotify requieren conversión externa. Redirigiendo a spotifydown.com abajo.',
            externalWarning: '⚠️ Los servicios externos pueden contener anuncios y no están controlados por este script.',
            toastCopySuccess: 'Enlace copiado al portapapeles',
            toastCopyFail: 'Error al copiar el enlace',
            toastYtBlocked: 'YouTube está bloqueando las descargas directas. Utilice fallback.',
            toastNoVideo: 'No se encontró ningún video en esta página.',
            toastSubtitlesSuccess: 'Subtítulos descargados con éxito',
            toastSubtitlesFail: 'Error al descargar subtítulos',
            toastDownloadSuccess: 'Descarga completada con éxito',
            toastDirectFail: 'Descarga directa fallida. Haga clic en "Fallback" abajo para usar una herramienta externa.',
            toastDirectFailConfirm: 'La descarga directa falló. ¿Redirigir al servicio de descarga externo?',
            toastHlsDetected: 'Transmisión HLS detectada. Redirigiendo a fallback...',
            toastYtCipherFail: 'Error al descifrar la firma de YouTube. Redirigiendo a fallback...',
            cancel: 'Cancelar',
            allCanceled: 'Todas las descargas canceladas',
            floatingButtonLabel: 'Abrir opciones de descarga de video',
            nativeYtDlTitle: 'Descargar Video (Ctrl+Alt+Shift+D)',
            cobaltConfirmTitle: 'Descargador Cobalt',
            cobaltConfirmDesc: '¿Copió el enlace de compartir de este video en TikTok?<br><br>Recomendamos usar el enlace copiado (Ctrl+C) para obtener el mejor resultado en Cobalt.',
            cobaltConfirmYes: 'Sí, usar enlace de Ctrl+C',
            cobaltConfirmNo: 'No, usar enlace detectado',
            cobaltPromptNotTikTok: 'El enlace en el portapapeles no parece ser de TikTok. Pegue el enlace del video aquí:',
            cobaltPromptFallback: 'Por favor, pegue el enlace copiado de TikTok aquí:',
            toastSettingEnabled: 'activado',
            toastSettingDisabled: 'desactivado',
            toastReloadPage: 'Recargue la página.',
            langSelectTitle: 'Seleccione el idioma',
            langSelectDesc: 'Por favor, seleccione su idioma de preferencia para OmniDown:',
            langSelectConfirm: 'Guardar',
            currentLabel: 'Actual',
            deciphering: 'Descifrando...',
            cipherBlocked: 'Firma Bloqueada',
            adaptiveNoAudio: 'Adaptativo (Sin Audio)',
            audioOnly: 'Audio Solamente',
            muxed: 'Mixto',
            menuOpenMenu: '📥 Abrir Menú de Descarga',
            menuDownloadCobalt: '⚡ Descargar con Cobalt',
            menuChangeLang: '🌐 Cambiar Idioma / Alterar Idioma / Change Language',
            menuSetCobaltUrl: '⚙️ Establecer URL de API Cobalt',
            menuSetProxy: '⚙️ Establecer Proxy CORS',
            menuToggleBtn: '🔘 Alternar Botón Flotante',
            menuToggleFallback: '🔘 Alternar Auto-Fallback',
            menuToggleHotkey: '🔘 Alternar Atajo de Teclado',
            toastPlaylistExported: 'Exportado {count} enlaces de la lista.',
            toastNoPlaylist: 'No se encontraron enlaces de listas en la página.',
            promptSetProxy: 'Ingrese el prefijo de URL del proxy CORS personalizado (por ejemplo, https://myproxy.com/?url=) o déjelo en blanco para borrar:',
            toastProxyConfigured: 'Proxy CORS personalizado configurado.',
            toastProxyCleared: 'Proxy CORS personalizado borrado.',
            ariaMinimize: 'Minimizar',
            ariaClose: 'Cerrar',
            toastSettingDisabled: 'đã tắt',
            toastReloadPage: 'Tải lại trang.',
            langSelectTitle: 'Chọn ngôn ngữ',
            langSelectDesc: 'Vui lòng chọn ngôn ngữ ưu thích của bạn cho OmniDown:',
            langSelectConfirm: 'Lưu',
            currentLabel: 'Hiện tại',
            deciphering: 'Đang giải mã...',
            cipherBlocked: 'Chữ ký bị chặn',
            adaptiveNoAudio: 'Tương thích (Không âm thanh)',
            audioOnly: 'Chỉ âm thanh',
            muxed: 'Hỗn hợp',
            menuOpenMenu: '📥 Mở Bảng Tải Xuống',
            menuDownloadCobalt: '⚡ Tải phương tiện (Cobalt)',
            menuChangeLang: '🌐 Thay đổi ngôn ngữ / Alterar Idioma / Change Language',
            menuSetCobaltUrl: '⚙️ Cài đặt URL API Cobalt',
            menuSetProxy: '⚙️ Cài đặt proxy CORS',
            menuToggleBtn: '🔘 Bật/Tắt Nút Nổi UI',
            menuToggleFallback: '🔘 Bật/Tắt Tự Động Dự Phòng',
            menuToggleHotkey: '🔘 Bật/Tắt Phím Tắt'
        },
        es: {
            title: 'Descargador de Video HD',
            download: 'Descargar',
            fallback: 'Fallback',
            cobalt: 'Cobalt',
            copyLink: 'Copiar Enlace',
            exportPlaylist: 'Exportar Lista',
            subtitles: 'Subtítulos',
            subtitleTrack: 'Pista de Subtítulos',
            noFormats: 'No se encontraron formatos de descarga directa. Use las herramientas alternativas abajo.',
            spotifyRedirect: 'Las canciones de Spotify requieren conversión externa. Redirigiendo a spotifydown.com abajo.',
            externalWarning: '⚠️ Los servicios externos pueden contener anuncios y no están controlados por este script.',
            toastCopySuccess: 'Enlace copiado al portapapeles',
            toastCopyFail: 'Error al copiar el enlace',
            toastYtBlocked: 'YouTube está bloqueando las descargas directas. Utilice fallback.',
            toastNoVideo: 'No se encontró ningún video en esta página.',
            toastSubtitlesSuccess: 'Subtítulos descargados con éxito',
            toastSubtitlesFail: 'Error al descargar subtítulos',
            toastDownloadSuccess: 'Descarga completada con éxito',
            toastDirectFail: 'Descarga directa fallida. Haga clic en "Fallback" abajo para usar una herramienta externa.',
            toastDirectFailConfirm: 'La descarga directa falló. ¿Redirigir al servicio de descarga externo?',
            toastHlsDetected: 'Transmisión HLS detectada. Redirigiendo a fallback...',
            toastYtCipherFail: 'Error al descifrar la firma de YouTube. Redirigiendo a fallback...',
            cancel: 'Cancelar',
            allCanceled: 'Todas las descargas canceladas',
            floatingButtonLabel: 'Abrir opciones de descarga de video',
            nativeYtDlTitle: 'Descargar Video (Ctrl+Alt+Shift+D)',
            cobaltConfirmTitle: 'Descargador Cobalt',
            cobaltConfirmDesc: '¿Copió el enlace de compartir de este video en TikTok?<br><br>Recomendamos usar el enlace copiado (Ctrl+C) para obtener el mejor resultado en Cobalt.',
            cobaltConfirmYes: 'Sí, usar enlace de Ctrl+C',
            cobaltConfirmNo: 'No, usar enlace detectado',
            cobaltPromptNotTikTok: 'El enlace en el portapapeles no parece ser de TikTok. Pegue el enlace del video aquí:',
            cobaltPromptFallback: 'Por favor, pegue el enlace copiado de TikTok aquí:',
            toastSettingEnabled: 'activado',
            toastSettingDisabled: 'desactivado',
            toastReloadPage: 'Recargue la página.',
            langSelectTitle: 'Seleccione el idioma',
            langSelectDesc: 'Por favor, seleccione su idioma de preferencia para OmniDown:',
            langSelectConfirm: 'Guardar',
            currentLabel: 'Actual',
            deciphering: 'Descifrando...',
            cipherBlocked: 'Firma Bloqueada',
            adaptiveNoAudio: 'Adaptativo (Sin Audio)',
            audioOnly: 'Audio Solamente',
            muxed: 'Mixto',
            menuOpenMenu: '📥 Abrir Menú de Descarga',
            menuDownloadCobalt: '⚡ Descargar con Cobalt',
            menuChangeLang: '🌐 Cambiar Idioma / Alterar Idioma / Change Language',
            menuSetCobaltUrl: '⚙️ Establecer URL de API Cobalt',
            menuSetProxy: '⚙️ Establecer Proxy CORS',
            menuToggleBtn: '🔘 Alternar Botón Flotante',
            menuToggleFallback: '🔘 Alternar Auto-Fallback',
            menuToggleHotkey: '🔘 Alternar Atajo de Teclado',
            toastPlaylistExported: 'Exportado {count} enlaces de la lista.',
            toastNoPlaylist: 'No se encontraron enlaces de listas en la página.',
            promptSetProxy: 'Ingrese el prefijo de URL del proxy CORS personalizado (por ejemplo, https://myproxy.com/?url=) o déjelo en blanco para borrar:',
            toastProxyConfigured: 'Proxy CORS personalizado configurado.',
            toastProxyCleared: 'Proxy CORS personalizado borrado.',
            ariaMinimize: 'Minimizar',
            ariaClose: 'Cerrar',
            ariaSelectService: 'Seleccionar Servicio',
            ariaFormatDownload: 'Descargar formato {quality}',
            ariaCobaltRow: 'Descargar con Cobalt',
            promptSetCobaltUrl: 'Ingrese la URL de la API Cobalt (por ejemplo, cobalt.tools):',
            labelTiktok: 'Video de TikTok',
            labelInstagram: 'Video de Instagram',
            labelTwitter: 'Video de Twitter',
            labelFacebook: 'Video de Facebook',
            labelThreads: 'Video de Threads',
            labelDouyin: 'Video de Douyin',
            labelXhs: 'Video de XiaoHongShu',
            labelTed: 'Video de TED',
            labelSnapchat: 'Video de Snapchat',
            labelPinterest: 'Video de Pinterest',
            labelSpotify: 'Música de Spotify',
            labelYoutube: 'Video de YouTube',
            labelHlsBlob: 'Transmisión HLS (Descarga directa no soportada)',
            labelHtml5Video: 'Video HTML5',
            toastCobaltApiUrl: 'URL de la API Cobalt: {url}'
        }
    };

    const getBrowserLang = () => {
        const l = navigator.language || navigator.userLanguage || 'en';
        if (l.startsWith('pt')) return 'pt';
        if (l.startsWith('zh-CN') || l.startsWith('zh-cn')) return 'zh-CN';
        if (l.startsWith('zh-TW') || l.startsWith('zh-tw') || l.startsWith('zh-HK') || l.startsWith('zh-hk')) return 'zh-TW';
        if (l.startsWith('zh')) return 'zh-CN';
        if (l.startsWith('vi')) return 'vi';
        if (l.startsWith('es')) return 'es';
        return 'en';
    };

    const getSelectedLang = () => {
        return getSetting(SETTINGS_KEYS.language, null);
    };

    const getText = (key) => {
        const lang = getSelectedLang() || getBrowserLang();
        const dict = TRANSLATIONS[lang] || TRANSLATIONS['en'];
        return dict[key] || TRANSLATIONS['en'][key] || key;
    };

    const showLanguageSelectionModal = (onComplete) => {
        const backdrop = document.createElement('div');
        backdrop.className = 'ksmo-modal-backdrop';
        backdrop.style.zIndex = '10000003';

        const modal = document.createElement('div');
        modal.className = 'ksmo-modal';
        modal.style.maxWidth = '360px';
        modal.style.padding = '20px';

        const title = document.createElement('div');
        title.className = 'ksmo-modal-title';
        title.style.marginBottom = '12px';
        title.textContent = getText('langSelectTitle');

        const desc = document.createElement('div');
        desc.style.fontSize = '13px';
        desc.style.color = '#e2e8f0';
        desc.style.lineHeight = '1.6';
        desc.style.marginBottom = '20px';
        desc.textContent = getText('langSelectDesc');

        const select = document.createElement('select');
        select.className = 'ksmo-select';
        select.style.width = '100%';
        select.style.marginBottom = '20px';
        
        const langs = [
            { code: 'en', name: 'English' },
            { code: 'pt', name: 'Português' },
            { code: 'zh-CN', name: '简体中文 (Simplified Chinese)' },
            { code: 'zh-TW', name: '繁體中文 (Traditional Chinese)' },
            { code: 'vi', name: 'Tiếng Việt' },
            { code: 'es', name: 'Español' }
        ];

        const defaultLang = getSelectedLang() || getBrowserLang();
        langs.forEach(l => {
            const opt = document.createElement('option');
            opt.value = l.code;
            opt.textContent = l.name;
            if (l.code === defaultLang) opt.selected = true;
            select.appendChild(opt);
        });

        const saveBtn = document.createElement('button');
        saveBtn.className = 'ksmo-dl-btn';
        saveBtn.textContent = getText('langSelectConfirm');
        saveBtn.style.background = 'linear-gradient(135deg, #6c5ce7, #4b3fd4)';

        modal.appendChild(title);
        modal.appendChild(desc);
        modal.appendChild(select);
        modal.appendChild(saveBtn);
        backdrop.appendChild(modal);
        document.body.appendChild(backdrop);

        setTimeout(() => backdrop.classList.add('show'), 10);

        saveBtn.addEventListener('click', () => {
            setSetting(SETTINGS_KEYS.language, select.value);
            backdrop.classList.remove('show');
            setTimeout(() => {
                backdrop.remove();
                if (onComplete) onComplete();
            }, 300);
        });
    };


    const getCobaltApiUrl = () => {
        return getSetting(SETTINGS_KEYS.cobaltApiUrl, 'cobalt.tools');
    };

    const openCobaltUI = (targetUrl) => {
        let apiUrl = getCobaltApiUrl();
        if (!apiUrl.startsWith("https://") && !apiUrl.startsWith("http://")) apiUrl = `https://` + apiUrl;
        if (!apiUrl.endsWith("/")) apiUrl = apiUrl + '/';
        apiUrl = apiUrl + `?u=${encodeURIComponent(targetUrl)}`;

        if (typeof GM_openInTab !== 'undefined') {
            GM_openInTab(apiUrl, { active: true });
        } else {
            window.open(apiUrl, '_blank');
        }
    };

    /**
     * Shows a beautiful modal dialog asking the user to confirm whether they've
     * copied the sharing link on TikTok. This helps Cobalt fetch watermark-free
     * media using the correct user-copied link rather than a CDN URL.
     */
    const showTikTokCobaltConfirmation = (detectedUrl) => {
        const backdrop = document.createElement('div');
        backdrop.className = 'ksmo-modal-backdrop';
        backdrop.style.zIndex = '10000002'; // Render above main modal

        const modal = document.createElement('div');
        modal.className = 'ksmo-modal';
        modal.style.maxWidth = '360px';
        modal.style.padding = '20px';

        const title = document.createElement('div');
        title.className = 'ksmo-modal-title';
        title.style.marginBottom = '12px';
        title.textContent = getText('cobaltConfirmTitle');

        const desc = document.createElement('div');
        desc.style.fontSize = '13px';
        desc.style.color = '#e2e8f0';
        desc.style.lineHeight = '1.6';
        desc.style.marginBottom = '20px';
        desc.innerHTML = getText('cobaltConfirmDesc');

        const btnContainer = document.createElement('div');
        btnContainer.style.display = 'flex';
        btnContainer.style.flexDirection = 'column';
        btnContainer.style.gap = '8px';

        const useClipboardBtn = document.createElement('button');
        useClipboardBtn.className = 'ksmo-dl-btn';
        useClipboardBtn.textContent = getText('cobaltConfirmYes');
        useClipboardBtn.style.background = 'linear-gradient(135deg, #6c5ce7, #4b3fd4)';

        const useDetectedBtn = document.createElement('button');
        useDetectedBtn.className = 'ksmo-secondary-btn';
        useDetectedBtn.textContent = getText('cobaltConfirmNo');

        const cancelBtn = document.createElement('button');
        cancelBtn.className = 'ksmo-secondary-btn';
        cancelBtn.textContent = getText('cancel');
        cancelBtn.style.color = '#ef4444';
        cancelBtn.style.borderColor = 'rgba(239, 68, 68, 0.2)';

        btnContainer.appendChild(useClipboardBtn);
        btnContainer.appendChild(useDetectedBtn);
        btnContainer.appendChild(cancelBtn);

        modal.appendChild(title);
        modal.appendChild(desc);
        modal.appendChild(btnContainer);
        backdrop.appendChild(modal);
        document.body.appendChild(backdrop);

        setTimeout(() => backdrop.classList.add('show'), 10);

        const dismiss = () => {
            backdrop.classList.remove('show');
            setTimeout(() => backdrop.remove(), 300);
        };

        useClipboardBtn.addEventListener('click', () => {
            dismiss();
            navigator.clipboard.readText().then(clipText => {
                const trimmed = clipText ? clipText.trim() : '';
                if (trimmed && (trimmed.includes('tiktok.com') || trimmed.includes('vt.tiktok.com') || trimmed.includes('vm.tiktok.com'))) {
                    openCobaltUI(trimmed);
                } else {
                    const pasted = prompt(getText('cobaltPromptNotTikTok'), trimmed);
                    if (pasted) openCobaltUI(pasted.trim());
                }
            }).catch(() => {
                const pasted = prompt(getText('cobaltPromptFallback'));
                if (pasted) openCobaltUI(pasted.trim());
            });
        });

        useDetectedBtn.addEventListener('click', () => {
            dismiss();
            openCobaltUI(detectedUrl);
        });

        cancelBtn.addEventListener('click', () => {
            dismiss();
        });
    };

    const FALLBACK_SERVICES = {
        ytdown: (url) => `https://app.ytdown.to/?url=${encodeURIComponent(url)}`,
        snapscooper: (url) => `https://snapscooper.com/tools/yt0?url=${encodeURIComponent(url)}`,
        y2meta: (url) => `https://y2meta.is/download/?url=${encodeURIComponent(url)}`,
        savefrom: (url) => `https://en1.savefrom.net/1-youtube-video-downloader-20pH/?url=${encodeURIComponent(url)}`,
        
        snaptik: (url) => `https://snaptik.app/?url=${encodeURIComponent(url)}`,
        ttdownloader: (url) => `https://ttdownloader.tools/?url=${encodeURIComponent(url)}`,
        
        savetik: (url) => `https://savetik.co/pt/douyin-downloader/?url=${encodeURIComponent(url)}`,
        seekin: (url) => `https://www.seekin.ai/douyin-downloader/?url=${encodeURIComponent(url)}`,
        
        fdownloader: (url) => `https://fdownloader.net/?url=${encodeURIComponent(url)}`,
        
        boilerplatehub: (url) => `https://boilerplatehub.com/free-tools/twitter-video-downloader?url=${encodeURIComponent(url)}`,
        twittervideodownloader: (url) => `https://twittervideodownloader.com/download?url=${encodeURIComponent(url)}`,
        ssstwitter: (url) => `https://ssstwitter.com/?url=${encodeURIComponent(url)}`,
        
        sssinstagram: (url) => `https://sssinstagram.com/?url=${encodeURIComponent(url)}`,
        saveclip: (url) => `https://saveclip.app/?url=${encodeURIComponent(url)}`,
        fastdl: (url) => `https://fastdl.app/?url=${encodeURIComponent(url)}`,
        downreels: (url) => `https://downreels.com/instagram-video-downloader-free/?url=${encodeURIComponent(url)}`,
        
        storygrab: (url) => `https://storygrab.io/en1?url=${encodeURIComponent(url)}`,
        schatdownloader: (url) => `https://schatdownloader.com/?url=${encodeURIComponent(url)}`,
        
        pindown: (url) => `https://pindown.io/pt1?url=${encodeURIComponent(url)}`,
        pinpea: (url) => `https://www.pinpea.com/pt/?url=${encodeURIComponent(url)}`,
        
        spotifydown: (url) => `https://spotifydown.com/?link=${encodeURIComponent(url)}`,
        tool77: (url) => `https://www.tool77.com/en/v/downloader?url=${encodeURIComponent(url)}`
    };

    const SITE_FALLBACKS = {
        'youtube.com': ['ytdown', 'snapscooper', 'y2meta', 'savefrom'],
        'tiktok.com': ['snaptik', 'ttdownloader'],
        'douyin.com': ['savetik', 'seekin'],
        'facebook.com': ['fdownloader'],
        'threads.net': ['fdownloader'],
        'twitter.com': ['boilerplatehub', 'twittervideodownloader', 'ssstwitter'],
        'x.com': ['boilerplatehub', 'twittervideodownloader', 'ssstwitter'],
        'instagram.com': ['sssinstagram', 'saveclip', 'fastdl', 'downreels'],
        'snapchat.com': ['storygrab', 'schatdownloader'],
        'pinterest.com': ['pindown', 'pinpea'],
        'spotify.com': ['spotifydown']
    };

    const getAvailableFallbacks = () => {
        const host = window.location.hostname;
        for (const domain of Object.keys(SITE_FALLBACKS)) {
            if (host.includes(domain)) {
                return SITE_FALLBACKS[domain];
            }
        }
        return ['tool77', 'savefrom', 'y2meta'];
    };

    const getFallbackServiceSettingKey = () => {
        const host = window.location.hostname;
        for (const domain of Object.keys(SITE_FALLBACKS)) {
            if (host.includes(domain)) {
                return `${SETTINGS_KEYS.fallbackService}_${domain}`;
            }
        }
        return `${SETTINGS_KEYS.fallbackService}_generic`;
    };

    const getCorsProxies = () => {
        const list = [];
        const custom = getSetting(SETTINGS_KEYS.customProxy, '');
        if (custom) {
            list.push((url) => `${custom}${encodeURIComponent(url)}`);
        }
        list.push((url) => `https://corsproxy.io/?${encodeURIComponent(url)}`);
        list.push((url) => `https://api.allorigins.win/raw?url=${encodeURIComponent(url)}`);
        return list;
    };

    const css = `
        /* 1. Global Floating Button Styling */
        .ksmo-floating-dl {
            position: fixed !important;
            bottom: 96px !important;
            right: 24px !important;
            z-index: 2147483630 !important;
            width: 48px !important;
            height: 48px !important;
            border-radius: 50% !important;
            background: linear-gradient(135deg, #6c5ce7, #4b3fd4) !important;
            border: 1px solid rgba(255, 255, 255, 0.08) !important;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6), 0 0 15px rgba(108, 92, 231, 0.2) !important;
            color: #f8fafc !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            cursor: pointer !important;
            opacity: 0;
            transform: scale(0.9) translateY(20px);
            transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1) !important;
            pointer-events: none !important;
        }
        .ksmo-floating-dl.visible {
            opacity: 1 !important;
            transform: scale(1) translateY(0) !important;
            pointer-events: auto !important;
        }
        .ksmo-floating-dl.downloading {
            background: linear-gradient(135deg, #ef4444, #b91c1c) !important;
            animation: ksmo-pulse 1.5s infinite !important;
        }
        .ksmo-floating-dl:hover {
            transform: scale(1.05) !important;
            box-shadow: 0 4px 12px rgba(108, 92, 231, 0.4) !important;
            color: #ffffff !important;
        }
        .ksmo-floating-dl svg {
            pointer-events: none !important;
        }

        /* 2. Embedded Video Hover Button Styling */
        .ksmo-dl-trigger {
            position: absolute !important;
            top: 16px !important;
            right: 16px !important;
            z-index: 2147483640 !important;
            width: 38px !important;
            height: 38px !important;
            border-radius: 50% !important;
            background: rgba(10, 10, 18, 0.82) !important;
            backdrop-filter: blur(16px) !important;
            -webkit-backdrop-filter: blur(16px) !important;
            border: 1px solid rgba(255, 255, 255, 0.08) !important;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6), 0 0 15px rgba(108, 92, 231, 0.2) !important;
            color: #f8fafc !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            cursor: pointer !important;
            opacity: 0;
            transform: scale(0.9);
            transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1) !important;
            pointer-events: auto !important;
        }
        .ksmo-dl-trigger.visible {
            opacity: 1 !important;
            transform: scale(1) !important;
        }
        .ksmo-dl-trigger:hover {
            background: linear-gradient(135deg, #6c5ce7, #4b3fd4) !important;
            color: #ffffff !important;
            border-color: rgba(255, 255, 255, 0.15) !important;
            transform: scale(1.05) !important;
            box-shadow: 0 4px 12px rgba(108, 92, 231, 0.4) !important;
        }
        .ksmo-dl-trigger svg {
            pointer-events: none !important;
        }

        /* 3. YouTube Control Bar Integration */
        #ksmo-yt-control-btn svg {
            stroke: #f8fafc;
            opacity: 0.9;
            transition: opacity 0.1s cubic-bezier(0.4, 0, 1, 1);
        }
        #ksmo-yt-control-btn:hover svg {
            opacity: 1;
        }

        /* 4. Modal Overlay & Panel Styling */
        .ksmo-modal-backdrop {
            position: fixed !important;
            top: 0 !important;
            left: 0 !important;
            width: 100vw !important;
            height: 100vh !important;
            background: rgba(10, 10, 15, 0.6) !important;
            backdrop-filter: blur(12px) !important;
            -webkit-backdrop-filter: blur(12px) !important;
            z-index: 2147483645 !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            opacity: 0;
            transition: opacity 0.3s ease !important;
            pointer-events: auto !important;
        }
        .ksmo-modal-backdrop.show {
            opacity: 1 !important;
        }
        .ksmo-modal-backdrop.minimized {
            background: transparent !important;
            pointer-events: none !important;
            width: auto !important;
            height: auto !important;
            top: auto !important;
            left: auto !important;
            right: 0 !important;
            bottom: 0 !important;
        }
        .ksmo-modal-backdrop.minimized .ksmo-modal {
            position: fixed !important;
            bottom: 24px !important;
            right: 24px !important;
            width: 300px !important;
            padding: 14px !important;
            border-radius: 12px !important;
            transform: none !important;
            box-shadow: 0 10px 30px rgba(0,0,0,0.5) !important;
            pointer-events: none !important;
        }
        .ksmo-modal-backdrop.minimized .ksmo-modal-header,
        .ksmo-modal-backdrop.minimized .ksmo-progress-container {
            pointer-events: auto !important;
        }
        .ksmo-modal {
            background: rgba(15, 15, 25, 0.85) !important;
            backdrop-filter: blur(20px) !important;
            -webkit-backdrop-filter: blur(20px) !important;
            border: 1px solid rgba(255, 255, 255, 0.08) !important;
            border-radius: 16px !important;
            box-shadow: 0 20px 45px rgba(0, 0, 0, 0.6) !important;
            width: 90% !important;
            max-width: 440px !important;
            padding: 24px !important;
            color: #f8fafc !important;
            font-family: system-ui, -apple-system, sans-serif !important;
            transform: translateY(20px) scale(0.95) !important;
            transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1) !important;
            pointer-events: auto !important;
        }
        .ksmo-modal-backdrop.show .ksmo-modal {
            transform: translateY(0) scale(1) !important;
        }
        .ksmo-modal-header {
            display: flex !important;
            justify-content: space-between !important;
            align-items: center !important;
            margin-bottom: 20px !important;
        }
        .ksmo-modal-backdrop.minimized .ksmo-modal-header {
            margin-bottom: 8px !important;
        }
        .ksmo-modal-title {
            font-size: 16px !important;
            font-weight: 600 !important;
            overflow: hidden !important;
            text-overflow: ellipsis !important;
            white-space: nowrap !important;
            max-width: 280px !important;
        }
        .ksmo-modal-backdrop.minimized .ksmo-modal-title {
            max-width: 180px !important;
            font-size: 13px !important;
        }
        .ksmo-modal-backdrop.minimized .ksmo-modal-title::after {
            content: '';
            display: inline-block;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            background: #6c5ce7;
            margin-left: 8px;
            box-shadow: 0 0 8px #6c5ce7;
            animation: ksmo-pulse 1.5s infinite;
        }
        @keyframes ksmo-pulse {
            0% { transform: scale(0.9); opacity: 0.6; }
            50% { transform: scale(1.1); opacity: 1; }
            100% { transform: scale(0.9); opacity: 0.6; }
        }
        .ksmo-header-actions {
            display: flex !important;
            gap: 6px !important;
            align-items: center !important;
        }
        .ksmo-modal-close, .ksmo-modal-min {
            background: transparent !important;
            border: none !important;
            color: #94a3b8 !important;
            cursor: pointer !important;
            font-size: 20px !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            width: 28px !important;
            height: 28px !important;
            padding: 0 !important;
            transition: color 0.2s ease !important;
        }
        .ksmo-modal-close:hover, .ksmo-modal-min:hover {
            color: #f8fafc !important;
        }

        /* 5. Format Quality List Styles */
        .ksmo-quality-list {
            max-height: 240px !important;
            overflow-y: auto !important;
            margin-bottom: 20px !important;
            padding-right: 4px !important;
        }
        .ksmo-quality-list::-webkit-scrollbar {
            width: 6px !important;
        }
        .ksmo-quality-list::-webkit-scrollbar-track {
            background: rgba(255, 255, 255, 0.02) !important;
            border-radius: 3px !important;
        }
        .ksmo-quality-list::-webkit-scrollbar-thumb {
            background: rgba(255, 255, 255, 0.15) !important;
            border-radius: 3px !important;
        }
        .ksmo-quality-list::-webkit-scrollbar-thumb:hover {
            background: rgba(255, 255, 255, 0.25) !important;
        }
        .ksmo-modal-backdrop.minimized .ksmo-quality-list {
            display: none !important;
        }
        .ksmo-quality-row {
            display: flex !important;
            justify-content: space-between !important;
            align-items: center !important;
            padding: 10px 12px !important;
            background: rgba(255, 255, 255, 0.03) !important;
            border: 1px solid rgba(255, 255, 255, 0.04) !important;
            border-radius: 8px !important;
            margin-bottom: 8px !important;
            transition: all 0.2s ease !important;
        }
        .ksmo-quality-row:hover {
            background: rgba(255, 255, 255, 0.06) !important;
            border-color: rgba(255, 255, 255, 0.1) !important;
        }
        .ksmo-quality-info {
            display: flex !important;
            flex-direction: column !important;
            gap: 2px !important;
        }
        .ksmo-quality-name {
            font-size: 14px !important;
            font-weight: 500 !important;
        }
        .ksmo-quality-label {
            font-size: 11px !important;
            color: #94a3b8 !important;
        }
        .ksmo-dl-btn {
            background: linear-gradient(135deg, #6c5ce7, #4b3fd4) !important;
            color: #ffffff !important;
            border: none !important;
            border-radius: 6px !important;
            padding: 6px 12px !important;
            font-size: 13px !important;
            font-weight: 500 !important;
            cursor: pointer !important;
            transition: all 0.2s ease !important;
        }
        .ksmo-dl-btn:hover {
            transform: translateY(-1px) !important;
            box-shadow: 0 4px 12px rgba(108, 92, 231, 0.4) !important;
        }

        /* 6. Footer Controls */
        .ksmo-modal-footer {
            display: flex !important;
            flex-direction: column !important;
            gap: 12px !important;
        }
        .ksmo-modal-backdrop.minimized .ksmo-modal-footer {
            display: none !important;
        }
        .ksmo-footer-row {
            display: flex !important;
            gap: 8px !important;
        }
        .ksmo-select {
            flex: 1 !important;
            background: rgba(10, 10, 15, 0.8) !important;
            border: 1px solid rgba(255, 255, 255, 0.08) !important;
            color: #f8fafc !important;
            border-radius: 8px !important;
            padding: 8px 12px !important;
            font-size: 13px !important;
            outline: none !important;
            cursor: pointer !important;
        }
        .ksmo-secondary-btn {
            background: rgba(255, 255, 255, 0.05) !important;
            border: 1px solid rgba(255, 255, 255, 0.08) !important;
            color: #f8fafc !important;
            border-radius: 8px !important;
            padding: 8px 16px !important;
            font-size: 13px !important;
            cursor: pointer !important;
            transition: all 0.2s ease !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
        }
        .ksmo-secondary-btn:hover {
            background: rgba(255, 255, 255, 0.1) !important;
        }

        /* 7. Progress Bar Layouts */
        .ksmo-progress-container {
            display: none !important;
            flex-direction: column !important;
            gap: 6px !important;
            margin-bottom: 20px !important;
        }
        .ksmo-modal-backdrop.minimized .ksmo-progress-container {
            display: flex !important;
            margin-bottom: 0 !important;
        }
        .ksmo-progress-bar {
            width: 100% !important;
            height: 6px !important;
            background: rgba(255, 255, 255, 0.1) !important;
            border-radius: 3px !important;
            overflow: hidden !important;
        }
        .ksmo-progress-fill {
            width: 0% !important;
            height: 100% !important;
            background: linear-gradient(90deg, #6c5ce7, #a29bfe) !important;
            transition: width 0.1s linear !important;
        }
        .ksmo-progress-text {
            display: flex !important;
            justify-content: space-between !important;
            font-size: 12px !important;
            color: #94a3b8 !important;
        }
        .ksmo-modal-backdrop.minimized .ksmo-progress-cancel {
            font-size: 0 !important;
        }
        .ksmo-modal-backdrop.minimized .ksmo-progress-cancel::after {
            content: '✕' !important;
            font-size: 14px !important;
            font-weight: bold !important;
            color: #ef4444 !important;
        }
        .ksmo-warning-label {
            font-size: 11px !important;
            color: #f59e0b !important;
            text-align: center !important;
            margin-top: 4px !important;
        }
        .ksmo-modal-backdrop.minimized .ksmo-warning-label {
            display: none !important;
        }

        /* 8. Toast Alert Overlay */
        .ksmo-toast {
            position: fixed !important;
            bottom: 24px !important;
            left: 50% !important;
            transform: translateX(-50%) translateY(20px) !important;
            background: rgba(10, 10, 18, 0.9) !important;
            backdrop-filter: blur(12px) !important;
            -webkit-backdrop-filter: blur(12px) !important;
            color: #ffffff !important;
            padding: 10px 20px !important;
            border-radius: 10px !important;
            font-size: 13px !important;
            font-family: system-ui, -apple-system, sans-serif !important;
            border: 1px solid rgba(255, 255, 255, 0.08) !important;
            box-shadow: 0 10px 25px rgba(0,0,0,0.5) !important;
            z-index: 2147483647 !important;
            pointer-events: none !important;
            opacity: 0;
            transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1) !important;
        }
        .ksmo-toast.show {
            opacity: 1 !important;
            transform: translateX(-50%) translateY(0) !important;
        }

        /* 9. Responsiveness Adjustments */
        @media (max-width: 400px) {
            .ksmo-modal {
                padding: 16px !important;
            }
            .ksmo-footer-row {
                flex-direction: column !important;
            }
        }
    `;

    const injectCSS = (cssText) => {
        if (typeof GM_addStyle !== 'undefined') {
            GM_addStyle(cssText);
            return;
        }
        const inject = () => {
            const style = document.createElement('style');
            style.textContent = cssText;
            (document.head || document.documentElement).appendChild(style);
        };
        if (document.head) {
            inject();
        } else {
            new MutationObserver((_, obs) => {
                if (document.head) { inject(); obs.disconnect(); }
            }).observe(document.documentElement, { childList: true });
        }
    };
    injectCSS(css);

    const toastQueue = [];
    let activeToast = null;
    let toastTimeout = null;

    const processNextToast = () => {
        if (toastQueue.length === 0 || !document.body) return;
        const { message, duration } = toastQueue[0];
        
        const toast = document.createElement('div');
        toast.className = 'ksmo-toast';
        toast.textContent = message;
        document.body.appendChild(toast);
        activeToast = toast;

        setTimeout(() => toast.classList.add('show'), 10);
        
        toastTimeout = setTimeout(() => {
            toast.classList.remove('show');
            setTimeout(() => {
                toast.remove();
                activeToast = null;
                toastQueue.shift();
                processNextToast();
            }, 300);
        }, duration);
    };

    const showToast = (message, duration = 4000) => {
        toastQueue.push({ message, duration });
        if (toastQueue.length === 1) {
            processNextToast();
        } else if (activeToast) {
            clearTimeout(toastTimeout);
            activeToast.classList.remove('show');
            setTimeout(() => {
                if (activeToast) {
                    activeToast.remove();
                    activeToast = null;
                }
                // NÃO shift() aqui — processNextToast já gerencia a fila via toastQueue[0]
                processNextToast();
            }, 200);
        }
    };

    const formatBytes = (bytes) => {
        if (!bytes) return '';
        const sizes = ['Bytes', 'KB', 'MB', 'GB'];
        const i = Math.floor(Math.log(bytes) / Math.log(1024));
        return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + sizes[i];
    };

    const utils = {
        getCodec: (mimeType) => {
            const mime = (mimeType || '').toLowerCase();
            if (mime.includes('webm')) return 'WebM';
            if (mime.includes('mp4')) return 'MP4';
            if (mime.includes('ogg')) return 'Ogg';
            return 'MP4';
        }
    };

    const cacheManager = {
        sizeCache: new Map(),
        embedCache: new Map(),
        setFileSize: function(url, size) {
            if (this.sizeCache.has(url)) {
                this.sizeCache.delete(url);
            }
            this.sizeCache.set(url, size);
            if (this.sizeCache.size > 200) {
                const firstKey = this.sizeCache.keys().next().value;
                this.sizeCache.delete(firstKey);
            }
        },
        getFileSize: function(url) {
            return this.sizeCache.get(url);
        },
        hasFileSize: function(url) {
            return this.sizeCache.has(url);
        },
        setEmbed: function(videoId, data) {
            if (this.embedCache.has(videoId)) {
                this.embedCache.delete(videoId);
            }
            this.embedCache.set(videoId, data);
            if (this.embedCache.size > 50) {
                const firstKey = this.embedCache.keys().next().value;
                this.embedCache.delete(firstKey);
            }
        },
        getEmbed: function(videoId) {
            return this.embedCache.get(videoId);
        },
        hasEmbed: function(videoId) {
            return this.embedCache.has(videoId);
        }
    };

    const DownloadManager = {
        /**
         * Downloads a file, trying GM_download first, then GM_xmlhttpRequest with CORS proxies.
         * Stores the request handle on the queueItem (if provided) for per-item cancellation.
         */
        download: (url, filename, onProgress, onComplete, onError, queueItem) => {
            let proxyIndex = -1;
            const attemptDownload = (currentUrl) => {
                const useGMDownload = typeof GM_download !== 'undefined';
                const proxies = getCorsProxies();
                if (useGMDownload && proxyIndex === -1) {
                    const req = GM_download({
                        url: currentUrl,
                        name: filename,
                        onload: () => {
                            if (queueItem) queueItem._request = null;
                            onComplete();
                        },
                        onerror: (err) => {
                            if (queueItem) queueItem._request = null;
                            handleError(err);
                        },
                        onprogress: (e) => {
                            if (e.total) {
                                onProgress(Math.round((e.loaded / e.total) * 100));
                            }
                        }
                    });
                    if (queueItem) queueItem._request = req;
                } else {
                    const req = GM_xmlhttpRequest({
                        method: 'GET',
                        url: currentUrl,
                        responseType: 'blob',
                        onprogress: (e) => {
                            if (e.total) {
                                onProgress(Math.round((e.loaded / e.total) * 100));
                            }
                        },
                        onload: (resp) => {
                            if (queueItem) queueItem._request = null;
                            if (resp.status === 200) {
                                const blob = resp.response;
                                const blobUrl = URL.createObjectURL(blob);
                                const a = document.createElement('a');
                                a.href = blobUrl;
                                a.download = filename;
                                document.body.appendChild(a);
                                a.click();
                                a.remove();
                                URL.revokeObjectURL(blobUrl);
                                onComplete();
                            } else {
                                handleError(new Error(`Status ${resp.status}`));
                            }
                        },
                        onerror: (err) => {
                            if (queueItem) queueItem._request = null;
                            handleError(err);
                        }
                    });
                    if (queueItem) queueItem._request = req;
                }
            };

            const handleError = (err) => {
                proxyIndex++;
                const proxies = getCorsProxies();
                if (proxyIndex < proxies.length) {
                    const nextUrl = proxies[proxyIndex](url);
                    attemptDownload(nextUrl);
                } else {
                    onError(err);
                }
            };

            attemptDownload(url);
        },
        cancelItem: (queueItem) => {
            if (queueItem && queueItem._request && typeof queueItem._request.abort === 'function') {
                queueItem._request.abort();
            }
            if (queueItem) queueItem._request = null;
        }
    };

    const updateFabDownloadState = () => {
        const fab = document.getElementById('ksmo-floating-dl');
        if (fab) {
            if (DownloadQueue.active) {
                fab.classList.add('downloading');
            } else {
                fab.classList.remove('downloading');
            }
        }
    };

    const DownloadQueue = {
        list: [],
        active: null,
        add: (url, filename, onProgress, onComplete, onError) => {
            const item = { url, filename, onProgress, onComplete, onError, cancelled: false, _request: null };
            DownloadQueue.list.push(item);
            if (!DownloadQueue.active) {
                DownloadQueue.processNext();
            }
        },
        processNext: () => {
            updateFabDownloadState();
            if (DownloadQueue.list.length === 0) {
                DownloadQueue.active = null;
                updateFabDownloadState();
                return;
            }
            const item = DownloadQueue.list.shift();
            DownloadQueue.active = item;
            DownloadManager.download(
                item.url,
                item.filename,
                item.onProgress,
                () => {
                    if (item.cancelled) return;
                    item.onComplete();
                    DownloadQueue.active = null;
                    setTimeout(() => DownloadQueue.processNext(), 0);
                },
                (err) => {
                    if (item.cancelled) return;
                    item.onError(err);
                    DownloadQueue.active = null;
                    setTimeout(() => DownloadQueue.processNext(), 0);
                },
                item
            );
        },
        clear: () => {
            if (DownloadQueue.active) {
                DownloadQueue.active.cancelled = true;
                DownloadManager.cancelItem(DownloadQueue.active);
            }
            DownloadQueue.list.forEach(item => {
                item.cancelled = true;
            });
            DownloadQueue.list = [];
            DownloadQueue.active = null;
            updateFabDownloadState();
        }
    };

    const pendingSizes = new Map();

    const triggerSizeCallbacks = (targetUrl, size) => {
        if (size !== null) {
            cacheManager.setFileSize(targetUrl, size);
        }
        const list = pendingSizes.get(targetUrl) || [];
        pendingSizes.delete(targetUrl);
        list.forEach(cb => cb(size));
    };

    const fetchFileSize = (url, callback) => {
        if (cacheManager.hasFileSize(url)) {
            callback(cacheManager.getFileSize(url));
            return;
        }
        if (pendingSizes.has(url)) {
            pendingSizes.get(url).push(callback);
            return;
        }
        pendingSizes.set(url, [callback]);

        if (typeof GM_xmlhttpRequest === 'undefined') {
            triggerSizeCallbacks(url, null);
            return;
        }

        const attemptRangeRequest = () => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                headers: {
                    'Range': 'bytes=0-0'
                },
                onload: (rangeResp) => {
                    const rangeLen = rangeResp.responseHeaders.match(/content-range:\s*bytes\s*\d+-\d+\/(\d+)/i) || 
                                     rangeResp.responseHeaders.match(/content-length:\s*(\d+)/i);
                    triggerSizeCallbacks(url, rangeLen ? parseInt(rangeLen[1], 10) : null);
                },
                onerror: () => triggerSizeCallbacks(url, null)
            });
        };

        GM_xmlhttpRequest({
            method: 'HEAD',
            url: url,
            onload: (resp) => {
                const len = resp.responseHeaders.match(/content-length:\s*(\d+)/i);
                if (len && len[1]) {
                    triggerSizeCallbacks(url, parseInt(len[1], 10));
                } else {
                    attemptRangeRequest();
                }
            },
            onerror: () => {
                attemptRangeRequest();
            }
        });
    };

    const convertXmlToSrt = (xmlString) => {
        try {
            const parser = new DOMParser();
            const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
            const texts = xmlDoc.getElementsByTagName('text');
            let srt = '';
            for (let i = 0; i < texts.length; i++) {
                const node = texts[i];
                const start = parseFloat(node.getAttribute('start') || '0');
                const dur = parseFloat(node.getAttribute('dur') || '0');
                const text = node.textContent.replace(/<[^>]*>/g, '');

                const formatTime = (seconds) => {
                    const date = new Date(null);
                    date.setSeconds(seconds);
                    const ms = Math.floor((seconds % 1) * 1000).toString().padStart(3, '0');
                    return date.toISOString().substr(11, 8) + ',' + ms;
                };

                srt += `${i + 1}\n`;
                srt += `${formatTime(start)} --> ${formatTime(start + dur)}\n`;
                srt += `${text}\n\n`;
            }
            return srt;
        } catch (e) {
            return '';
        }
    };

    /**
     * Extracts a JSON object assigned to a specific variable name inside a string of HTML.
     * Uses a bracket-balancing parser to accurately locate the closing brace of the JSON object,
     * handling string literals (with backslash escapes) to avoid matching inner string braces.
     */
    const extractJsonFromHtml = (html, varName) => {
        const pattern = new RegExp(varName + '\\s*=\\s*\\{');
        const match = html.match(pattern);
        if (!match) return null;
        
        const startIndex = match.index + match[0].length - 1;
        let braceCount = 0;
        let inString = false;
        let escape = false;

        for (let i = startIndex; i < html.length; i++) {
            const char = html[i];
            if (escape) {
                escape = false;
                continue;
            }
            if (char === '\\') {
                escape = true;
                continue;
            }
            if (char === '"' || char === "'") {
                if (!inString) {
                    inString = char;
                } else if (inString === char) {
                    inString = false;
                }
            }
            if (!inString) {
                if (char === '{') braceCount++;
                if (char === '}') {
                    braceCount--;
                    if (braceCount === 0) {
                        const jsonStr = html.slice(startIndex, i + 1);
                        try {
                            return JSON.parse(jsonStr);
                        } catch (e) {
                            return null;
                        }
                    }
                }
            }
        }
        return null;
    };

    const signatureDecipherer = {
        cachedDecipher: null,
        fetching: false,
        callbacks: [],
        
        findBaseJs: () => {
            const script = Array.from(document.querySelectorAll('script')).map(s => s.src).find(src => src.includes('/s/player/') && src.includes('/base.js'));
            return script || null;
        },
        
        parseDecipherFunc: (code) => {
            const decipherMatch = code.match(/([a-zA-Z0-9$_]+)=function\([a-zA-Z]\)\{[a-zA-Z]=[a-zA-Z]\.split\(""\);([\s\S]+?);return [a-zA-Z]\.join\(""\)\}/);
            if (!decipherMatch) return null;
            
            const funcBody = decipherMatch[2];
            const objMatch = funcBody.match(/([a-zA-Z0-9$_]+)\.[a-zA-Z0-9$_]+\([a-zA-Z],[0-9]+\)/);
            if (!objMatch) return null;
            const objName = objMatch[1];
            
            const objNameEscaped = objName.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
            const objRegex = new RegExp(`var\\s+${objNameEscaped}\\s*=\\s*\\{([\\s\\S]+?)\\};`);
            const objDefMatch = code.match(objRegex);
            if (!objDefMatch) return null;
            
            const objDef = objDefMatch[1];
            const methods = {};
            const methodMatches = objDef.matchAll(/([a-zA-Z0-9$_]+)\s*:\s*function\(([^)]+)\)\{([^}]+)\}/g);
            for (const match of methodMatches) {
                const name = match[1];
                const body = match[3];
                if (body.includes('reverse')) {
                    methods[name] = 'reverse';
                } else if (body.includes('splice')) {
                    methods[name] = 'slice';
                } else {
                    methods[name] = 'swap';
                }
            }
            
            const steps = [];
            const stepMatches = funcBody.matchAll(/([a-zA-Z0-9$_]+)\.([a-zA-Z0-9$_]+)\([a-zA-Z],([0-9]+)\)/g);
            for (const match of stepMatches) {
                const mName = match[2];
                const param = parseInt(match[3], 10);
                const action = methods[mName];
                if (action) {
                    steps.push({ action, param });
                }
            }
            
            return steps;
        },
        
        buildDecipherFunc: (steps) => {
            return (sig) => {
                const arr = sig.split('');
                steps.forEach(step => {
                    if (step.action === 'reverse') {
                        arr.reverse();
                    } else if (step.action === 'slice') {
                        arr.splice(0, step.param);
                    } else if (step.action === 'swap') {
                        const temp = arr[0];
                        arr[0] = arr[step.param % arr.length];
                        arr[step.param % arr.length] = temp;
                    }
                });
                return arr.join('');
            };
        },
        
        decipher: (sig, callback) => {
            if (signatureDecipherer.cachedDecipher) {
                try {
                    callback(signatureDecipherer.cachedDecipher(sig));
                } catch (e) {
                    callback(null);
                }
                return;
            }
            
            const baseJs = signatureDecipherer.findBaseJs();
            if (!baseJs) {
                callback(null);
                return;
            }

            const cachedUrl = getSetting('ksmo_youtube_base_url', null);
            const cachedSteps = getSetting('ksmo_youtube_decipher_steps', null);
            if (cachedUrl === baseJs && cachedSteps) {
                try {
                    const steps = JSON.parse(cachedSteps);
                    const dec = signatureDecipherer.buildDecipherFunc(steps);
                    signatureDecipherer.cachedDecipher = dec;
                    callback(dec(sig));
                    return;
                } catch (e) {}
            }
            
            signatureDecipherer.callbacks.push({ sig, callback });
            if (signatureDecipherer.fetching) return;
            signatureDecipherer.fetching = true;
            
            GM_xmlhttpRequest({
                method: 'GET',
                url: baseJs,
                onload: (resp) => {
                    signatureDecipherer.fetching = false;
                    try {
                        const steps = signatureDecipherer.parseDecipherFunc(resp.responseText);
                        if (steps && steps.length > 0) {
                            setSetting('ksmo_youtube_base_url', baseJs);
                            setSetting('ksmo_youtube_decipher_steps', JSON.stringify(steps));
                            const dec = signatureDecipherer.buildDecipherFunc(steps);
                            signatureDecipherer.cachedDecipher = dec;
                            while (signatureDecipherer.callbacks.length) {
                                const item = signatureDecipherer.callbacks.shift();
                                try {
                                    item.callback(dec(item.sig));
                                } catch (e) {
                                    item.callback(null);
                                }
                            }
                        } else {
                            while (signatureDecipherer.callbacks.length) {
                                signatureDecipherer.callbacks.shift().callback(null);
                            }
                        }
                    } catch (e) {
                        while (signatureDecipherer.callbacks.length) {
                            signatureDecipherer.callbacks.shift().callback(null);
                        }
                    }
                },
                onerror: () => {
                    signatureDecipherer.fetching = false;
                    while (signatureDecipherer.callbacks.length) {
                        signatureDecipherer.callbacks.shift().callback(null);
                    }
                }
            });
        }
    };

    /**
     * Shared base.js fetcher — avoids duplicate network requests when both
     * signatureDecipherer and nTransformDecipherer need the same file.
     */
    const baseJsFetcher = {
        cachedCode: null,
        cachedUrl: null,
        fetching: false,
        callbacks: [],
        fetch: (callback) => {
            const baseJs = signatureDecipherer.findBaseJs();
            if (!baseJs) { callback(null, null); return; }
            if (baseJsFetcher.cachedUrl === baseJs && baseJsFetcher.cachedCode) {
                callback(baseJsFetcher.cachedCode, baseJs);
                return;
            }
            baseJsFetcher.callbacks.push(callback);
            if (baseJsFetcher.fetching) return;
            baseJsFetcher.fetching = true;
            GM_xmlhttpRequest({
                method: 'GET',
                url: baseJs,
                onload: (resp) => {
                    baseJsFetcher.fetching = false;
                    baseJsFetcher.cachedCode = resp.responseText;
                    baseJsFetcher.cachedUrl = baseJs;
                    while (baseJsFetcher.callbacks.length) {
                        baseJsFetcher.callbacks.shift()(resp.responseText, baseJs);
                    }
                },
                onerror: () => {
                    baseJsFetcher.fetching = false;
                    baseJsFetcher.cachedCode = null;
                    while (baseJsFetcher.callbacks.length) {
                        baseJsFetcher.callbacks.shift()(null, null);
                    }
                }
            });
        }
    };

    /**
     * YouTube nsig (n-parameter throttle transform) decipherer.
     *
     * Approach B: Extract the transform function as a string from base.js and
     * execute via new Function(). Falls back to the original n value (slower
     * download, but functional) when extraction or execution fails.
     */
    const nTransformDecipherer = {
        cachedTransform: null,
        fetching: false,
        callbacks: [],

        /**
         * Locates the function name responsible for transforming the 'n' parameter
         * inside YouTube's base.js player code.
         */
        findNTransformName: (code) => {
            const patterns = [
                /\.get\("n"\)\)&&\(b=([a-zA-Z0-9$]+)(?:\[(\d+)\])?\([a-zA-Z0-9]\)/,
                /\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*encodeURIComponent\(([a-zA-Z0-9$]+)\(/,
                /\bc\s*=\s*a\.get\("n"\)\)&&\(c=([a-zA-Z0-9$]+)\(/,
                /\b(?:b=([a-zA-Z0-9$]{2,})\(b\),c\.set\("n",b\))/,
            ];
            for (const pattern of patterns) {
                const match = code.match(pattern);
                if (match) {
                    let name = match[1];
                    if (match[2] !== undefined) {
                        const esc = name.replace(/\$/g, '\\$');
                        const arrMatch = code.match(new RegExp(`var\\s+${esc}\\s*=\\s*\\[([a-zA-Z0-9$]+)\\]`));
                        if (arrMatch) name = arrMatch[1];
                    }
                    return name;
                }
            }
            return null;
        },

        /**
         * Extracts the full function body using bracket-balancing (same algorithm
         * as extractJsonFromHtml) to handle nested structures safely.
         */
        extractFunction: (code, funcName) => {
            const esc = funcName.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
            const startPatterns = [
                new RegExp(`${esc}\\s*=\\s*function\\s*\\(`),
                new RegExp(`function\\s+${esc}\\s*\\(`)
            ];
            let funcStartIdx = -1;
            for (const pat of startPatterns) {
                const m = code.match(pat);
                if (m) { funcStartIdx = m.index; break; }
            }
            if (funcStartIdx === -1) return null;

            const braceStart = code.indexOf('{', funcStartIdx);
            if (braceStart === -1) return null;

            let depth = 0, inStr = false, isEsc = false;
            for (let i = braceStart; i < code.length; i++) {
                const ch = code[i];
                if (isEsc) { isEsc = false; continue; }
                if (ch === '\\') { isEsc = true; continue; }
                if (ch === '"' || ch === "'") {
                    if (!inStr) inStr = ch;
                    else if (inStr === ch) inStr = false;
                }
                if (!inStr) {
                    if (ch === '{') depth++;
                    if (ch === '}') {
                        depth--;
                        if (depth === 0) {
                            const paramMatch = code.slice(funcStartIdx, braceStart).match(/\(([^)]*)\)/);
                            const param = paramMatch ? paramMatch[1].trim() : 'a';
                            const body = code.slice(braceStart + 1, i);
                            return { param, body };
                        }
                    }
                }
            }
            return null;
        },

        buildTransformFunc: (param, body) => {
            try {
                return new Function(param, body);
            } catch (e) {
                if (e.name === 'SecurityError' || e.message.includes('CSP') || e.message.includes('unsafe-eval')) {
                    console.warn('[OmniDown] Content Security Policy (CSP) blocked dynamic decipher execution. Falling back to slow stream.', e);
                } else {
                    console.error('[OmniDown] Syntax error building transform function.', e);
                }
                return null;
            }
        },

        transform: (n, callback) => {
            if (nTransformDecipherer.cachedTransform) {
                try {
                    const result = nTransformDecipherer.cachedTransform(n);
                    callback(typeof result === 'string' ? result : n);
                } catch (e) {
                    callback(n);
                }
                return;
            }

            const baseJs = signatureDecipherer.findBaseJs();
            const cachedUrl = getSetting('ksmo_youtube_base_url_n', null);
            const cachedFunc = getSetting('ksmo_youtube_n_func', null);
            if (baseJs && cachedUrl === baseJs && cachedFunc) {
                try {
                    const { param, body } = JSON.parse(cachedFunc);
                    const fn = nTransformDecipherer.buildTransformFunc(param, body);
                    if (fn) {
                        nTransformDecipherer.cachedTransform = fn;
                        const result = fn(n);
                        callback(typeof result === 'string' ? result : n);
                        return;
                    }
                } catch (e) {}
            }

            nTransformDecipherer.callbacks.push({ n, callback });
            if (nTransformDecipherer.fetching) return;
            nTransformDecipherer.fetching = true;

            baseJsFetcher.fetch((code, fetchedUrl) => {
                nTransformDecipherer.fetching = false;
                if (!code) {
                    while (nTransformDecipherer.callbacks.length) {
                        const item = nTransformDecipherer.callbacks.shift();
                        item.callback(item.n);
                    }
                    return;
                }

                try {
                    const funcName = nTransformDecipherer.findNTransformName(code);
                    if (funcName) {
                        const extracted = nTransformDecipherer.extractFunction(code, funcName);
                        if (extracted) {
                            const fn = nTransformDecipherer.buildTransformFunc(extracted.param, extracted.body);
                            if (fn) {
                                setSetting('ksmo_youtube_base_url_n', fetchedUrl);
                                setSetting('ksmo_youtube_n_func', JSON.stringify(extracted));
                                nTransformDecipherer.cachedTransform = fn;

                                while (nTransformDecipherer.callbacks.length) {
                                    const item = nTransformDecipherer.callbacks.shift();
                                    try {
                                        const result = fn(item.n);
                                        item.callback(typeof result === 'string' ? result : item.n);
                                    } catch (e) {
                                        item.callback(item.n);
                                    }
                                }
                                return;
                            }
                        }
                    }
                } catch (e) {}

                // Fallback: couldn't parse — use original n (slow but functional)
                while (nTransformDecipherer.callbacks.length) {
                    const item = nTransformDecipherer.callbacks.shift();
                    item.callback(item.n);
                }
            });
        }
    };

    /**
     * Applies the n-parameter throttle transform to a YouTube streaming URL.
     * If no 'n' param exists or transform fails, returns the original URL.
     */
    const applyNTransform = (url, callback) => {
        try {
            const u = new URL(url);
            const n = u.searchParams.get('n');
            if (!n) { callback(url); return; }

            nTransformDecipherer.transform(n, (transformedN) => {
                if (transformedN && transformedN !== n) {
                    u.searchParams.set('n', transformedN);
                }
                callback(u.toString());
            });
        } catch (e) {
            callback(url);
        }
    };

    const decipherUrl = (cipher, callback) => {
        let parsed = new URLSearchParams(cipher);
        let url = parsed.get('url');
        let s = parsed.get('s');
        let sp = parsed.get('sp') || 'sig';

        if (!url || !s) {
            const urlMatch = cipher.match(/url=([^&]+)/);
            const sMatch = cipher.match(/s=([^&]+)/);
            const spMatch = cipher.match(/sp=([^&]+)/);
            if (urlMatch) url = decodeURIComponent(urlMatch[1].replace(/\+/g, ' '));
            if (sMatch) s = decodeURIComponent(sMatch[1].replace(/\+/g, ' '));
            if (spMatch) sp = decodeURIComponent(spMatch[1].replace(/\+/g, ' '));
        }

        if (!url || !s) {
            callback(null);
            return;
        }

        signatureDecipherer.decipher(s, (decipheredS) => {
            if (decipheredS) {
                try {
                    const u = new URL(url);
                    // Remove any existing signature params to avoid duplication
                    u.searchParams.delete('sig');
                    u.searchParams.delete('signature');
                    u.searchParams.delete(sp);
                    u.searchParams.set(sp, decipheredS);
                    callback(u.toString());
                } catch (e) {
                    callback(null);
                }
            } else {
                callback(null);
            }
        });
    };

    const fetchEmbedPlayerResponse = (videoId, callback) => {
        if (cacheManager.hasEmbed(videoId)) {
            callback(cacheManager.getEmbed(videoId));
            return;
        }
        if (typeof GM_xmlhttpRequest === 'undefined') {
            callback(null);
            return;
        }
        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://www.youtube.com/embed/${videoId}`,
            timeout: 8000,
            onload: (resp) => {
                const data = extractJsonFromHtml(resp.responseText, 'ytInitialPlayerResponse');
                if (data) {
                    cacheManager.setEmbed(videoId, data);
                    callback(data);
                } else {
                    callback(null);
                }
            },
            onerror: () => callback(null),
            ontimeout: () => {
                console.warn('[OmniDown] YouTube embed player response request timed out.');
                callback(null);
            }
        });
    };

    const twitterExtractor = () => {
        const source = document.querySelector('[data-testid="videoPlayer"] video source');
        let url = source?.src;
        if (!url) {
            const video = document.querySelector('video') || document.querySelector('[data-testid="videoPlayer"] video');
            url = video?.src;
        }
        if (!url || url.startsWith('blob:')) return null;

        return {
            title: document.title || getText('labelTwitter'),
            formats: [{ quality: 'Source', url, type: 'video/mp4', label: getText('labelTwitter') }]
        };
    };

    const extractors = {
        'youtube.com': () => {
            try {
                const myWin = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
                const player = document.querySelector('#movie_player') || myWin.movie_player;
                let response = player?.getPlayerResponse?.() || myWin.ytInitialPlayerResponse;

                if (!response) {
                    const scripts = Array.from(document.querySelectorAll('script'));
                    for (const script of scripts) {
                        const data = extractJsonFromHtml(script.textContent, 'ytInitialPlayerResponse');
                        if (data) {
                            response = data;
                            break;
                        }
                    }
                }

                if (!response) return null;

                const title = response.videoDetails?.title || document.title || getText('labelYoutube');
                const streaming = response.streamingData;
                if (!streaming) return null;

                const formats = [];
                const muxed = streaming.formats || [];
                const adaptive = streaming.adaptiveFormats || [];

                muxed.forEach(f => {
                    let url = f.url;
                    let cipher = null;
                    if (!url && f.signatureCipher) {
                        cipher = f.signatureCipher;
                    }
                    formats.push({
                        quality: f.qualityLabel || `${f.width}p`,
                        url: url || '',
                        cipher: cipher,
                        type: f.mimeType?.split(';')[0] || 'video/mp4',
                        label: cipher ? `${getText('muxed')} [${utils.getCodec(f.mimeType)}] (${getText('deciphering')})` : `${getText('muxed')} [${utils.getCodec(f.mimeType)}]`
                    });
                });

                adaptive.forEach(f => {
                    let url = f.url;
                    let cipher = null;
                    if (!url && f.signatureCipher) {
                        cipher = f.signatureCipher;
                    }
                    const isVideo = f.mimeType?.includes('video');
                    formats.push({
                        quality: f.qualityLabel || `${f.width}p`,
                        url: url || '',
                        cipher: cipher,
                        type: f.mimeType?.split(';')[0] || 'video/mp4',
                        label: cipher ? (isVideo ? `${getText('adaptiveNoAudio').replace(/\s*\(.*\)/, '')} [${utils.getCodec(f.mimeType)}] (${getText('deciphering')})` : `${getText('audioOnly')} [${utils.getCodec(f.mimeType)}] (${getText('deciphering')})`) : (isVideo ? `${getText('adaptiveNoAudio')} [${utils.getCodec(f.mimeType)}]` : `${getText('audioOnly')} [${utils.getCodec(f.mimeType)}]`)
                    });
                });

                const captionTracks = response.captions?.playerCaptionsTracklistRenderer?.captionTracks || [];
                captionTracks.forEach(track => {
                    if (track.baseUrl) {
                        formats.push({
                            quality: `${track.name.simpleText || track.name.runs?.[0]?.text || getText('subtitles')}`,
                            url: track.baseUrl,
                            type: 'text/srt',
                            label: getText('subtitleTrack'),
                            isSubtitle: true
                        });
                    }
                });

                return { title, formats };
            } catch (e) {
                return null;
            }
        },

        'tiktok.com': () => {
            const activeVideo = getActiveVideoElement();
            let title = document.title || getText('labelTiktok');

            // Resolve pageUrl and videoId
            const videoIdMatch = window.location.href.match(/\/video\/(\d+)/);
            let videoId = videoIdMatch ? videoIdMatch[1] : null;
            if (!videoId && activeVideo) {
                let parent = activeVideo.parentElement;
                while (parent && parent !== document.body) {
                    const links = parent.querySelectorAll('a[href*="/video/"]');
                    for (const link of links) {
                        const match = link.href.match(/\/video\/(\d+)/);
                        if (match) {
                            videoId = match[1];
                            break;
                        }
                    }
                    if (videoId) break;
                    parent = parent.parentElement;
                }
            }
            const pageUrl = videoId ? `https://www.tiktok.com/video/${videoId}` : window.location.href;

            let url = activeVideo?.src;
            const myWin = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;

            // Priority 1: SIGI_STATE (current TikTok pattern 2024-2025)
            const sigiEl = document.querySelector('#SIGI_STATE') ||
                           document.querySelector('script[data-sigi-persisted-state]');
            if (sigiEl) {
                try {
                    const sigi = JSON.parse(sigiEl.textContent);
                    const itemModule = sigi.ItemModule || {};
                    if (videoId && itemModule[videoId]) {
                        const item = itemModule[videoId];
                        url = item.video?.playAddr || item.video?.downloadAddr || url;
                        title = item.desc || title;
                    } else {
                        // Fallback: first item in ItemModule
                        const firstKey = Object.keys(itemModule)[0];
                        if (firstKey) {
                            const item = itemModule[firstKey];
                            url = item.video?.playAddr || item.video?.downloadAddr || url;
                            title = item.desc || title;
                        }
                    }
                } catch (e) {}
            }

            // Priority 2: __INITIAL_STATE__
            if (!url || url.startsWith('blob:')) {
                if (myWin.__INITIAL_STATE__) {
                    const data = myWin.__INITIAL_STATE__;
                    url = data.videoData?.playAddr || url;
                    title = data.videoData?.desc || title;
                }
            }

            // Priority 3: __UNIVERSAL_DATA__
            if (!url || url.startsWith('blob:')) {
                if (myWin.__UNIVERSAL_DATA__) {
                    try {
                        const udata = myWin.__UNIVERSAL_DATA__;
                        const defaultScope = udata['webapp.video-detail']?.itemInfo?.itemStruct;
                        if (defaultScope) {
                            url = defaultScope.video?.playAddr || defaultScope.video?.downloadAddr || url;
                            title = defaultScope.desc || title;
                        }
                    } catch (e) {}
                }
            }

            // Priority 4: __NEXT_DATA__
            if (!url || url.startsWith('blob:')) {
                const nextDataScript = document.getElementById('__NEXT_DATA__');
                if (nextDataScript) {
                    try {
                        const nd = JSON.parse(nextDataScript.textContent);
                        const itemStruct = nd.props?.pageProps?.itemInfo?.itemStruct;
                        if (itemStruct) {
                            url = itemStruct.video?.playAddr || itemStruct.video?.downloadAddr || url;
                            title = itemStruct.desc || title;
                        }
                    } catch (e) {}
                }
            }

            // Priority 5: xgplayer container
            if (!url || url.startsWith('blob:')) {
                const container = document.querySelector('[class*="xgplayer-container"]');
                const v = container?.querySelector('video');
                url = v?.src || url;
            }

            if (!url || url.startsWith('blob:')) return null;
            return {
                title,
                formats: [{ quality: 'Source', url, pageUrl, type: 'video/mp4', label: getText('labelTiktok') }]
            };
        },

        'instagram.com': () => {
            // Collect all videos in the current visible context (carousel support)
            const allVideos = Array.from(document.querySelectorAll('article video, [role="dialog"] video, main video'));
            const activeVideo = getActiveVideoElement();

            // Sort so that the active video is first
            if (activeVideo && allVideos.includes(activeVideo)) {
                const index = allVideos.indexOf(activeVideo);
                if (index > 0) {
                    allVideos.splice(index, 1);
                    allVideos.unshift(activeVideo);
                }
            }

            const title = document.title || getText('labelInstagram');
            const formats = [];
            const seenUrls = new Set();

            allVideos.forEach((video, idx) => {
                let url = video.src;
                if (!url) {
                    const source = video.querySelector('source');
                    url = source?.src;
                }
                if (!url) return;

                const isBlob = url.startsWith('blob:');
                const key = isBlob ? `blob_${idx}` : url;
                if (seenUrls.has(key)) return;
                seenUrls.add(key);

                // Find the specific post URL for this video card/feed element
                let pageUrl = window.location.href;
                let parent = video.parentElement;
                while (parent && parent !== document.body) {
                    const postLink = parent.querySelector('a[href*="/p/"], a[href*="/reel/"], a[href*="/reels/"]');
                    if (postLink && postLink.href) {
                        pageUrl = postLink.href;
                        break;
                    }
                    parent = parent.parentElement;
                }

                // Label the first video as the currently viewed one if carousel has multiple elements
                const isCurrent = video === activeVideo;
                const labelSuffix = allVideos.length > 1 ? ` #${idx + 1}${isCurrent ? ` (${getText('currentLabel')})` : ''}` : '';

                formats.push({
                    quality: allVideos.length > 1 ? `${getText('download')} ${idx + 1}${isCurrent ? ` (${getText('currentLabel')})` : ''}` : 'Source',
                    url: isBlob ? '' : url,
                    pageUrl: pageUrl,
                    type: 'video/mp4',
                    label: isBlob ? getText('labelHlsBlob') : `${getText('labelInstagram')}${labelSuffix}`,
                    isHLS: isBlob
                });
            });

            // Fallback: preload link
            if (formats.length === 0) {
                const preload = document.querySelector('link[rel="preload"][as="video"]');
                if (preload?.href) {
                    formats.push({
                        quality: 'Source',
                        url: preload.href,
                        pageUrl: window.location.href,
                        type: 'video/mp4',
                        label: getText('labelInstagram')
                    });
                }
            }

            if (formats.length === 0) return null;
            return { title, formats };
        },

        'x.com': twitterExtractor,
        'twitter.com': twitterExtractor,

        'facebook.com': () => {
            const video = document.querySelector('video');
            if (!video || !video.src || video.src.startsWith('blob:')) return null;
            return {
                title: document.title || getText('labelFacebook'),
                formats: [{ quality: 'Source', url: video.src, type: 'video/mp4', label: getText('labelFacebook') }]
            };
        },

        'threads.net': () => {
            const video = document.querySelector('video');
            if (!video || !video.src || video.src.startsWith('blob:')) return null;
            return {
                title: document.title || getText('labelThreads'),
                formats: [{ quality: 'Source', url: video.src, type: 'video/mp4', label: getText('labelThreads') }]
            };
        },

        'douyin.com': () => {
            const video = document.querySelector('video');
            if (!video || !video.src) return null;
            return {
                title: document.title || getText('labelDouyin'),
                formats: [{ quality: 'Source', url: video.src, type: 'video/mp4', label: getText('labelDouyin') }]
            };
        },

        'xiaohongshu.com': () => {
            const video = document.querySelector('video');
            if (!video || !video.src) return null;
            return {
                title: document.title || getText('labelXhs'),
                formats: [{ quality: 'Source', url: video.src, type: 'video/mp4', label: getText('labelXhs') }]
            };
        },

        'ted.com': () => {
            const video = document.querySelector('video');
            if (!video || !video.src) return null;
            return {
                title: document.title || getText('labelTed'),
                formats: [{ quality: 'Source', url: video.src, type: 'video/mp4', label: getText('labelTed') }]
            };
        },

        'snapchat.com': () => {
            const video = document.querySelector('video');
            if (!video || !video.src) return null;
            return {
                title: document.title || getText('labelSnapchat'),
                formats: [{ quality: 'Source', url: video.src, type: 'video/mp4', label: getText('labelSnapchat') }]
            };
        },

        'pinterest.com': () => {
            const video = document.querySelector('video');
            if (!video || !video.src) return null;
            return {
                title: document.title || getText('labelPinterest'),
                formats: [{ quality: 'Source', url: video.src, type: 'video/mp4', label: getText('labelPinterest') }]
            };
        },

        'spotify.com': () => {
            const url = window.location.href;
            const match = url.match(/track\/([a-zA-Z0-9]+)/);
            if (match) {
                return {
                    title: document.title || getText('labelSpotify'),
                    formats: [],
                    spotifyId: match[1]
                };
            }
            return null;
        },

        'generic': () => {
            const video = document.querySelector('video');
            if (!video || !video.src || video.src.startsWith('blob:')) return null;
            const isHLS = video.src.includes('.m3u8') || video.src.includes('m3u8');
            return {
                title: document.title || getText('download'),
                formats: [{
                    quality: 'Source',
                    url: isHLS ? '' : video.src,
                    type: 'video/mp4',
                    label: isHLS ? getText('labelHlsBlob') : getText('labelHtml5Video'),
                    isHLS: isHLS
                }]
            };
        }
    };

    const YOUTUBE_PLAYER_SELECTORS = [
        '#movie_player video',
        '.html5-video-player video',
        'ytd-watch-flexy video',
        'video[src*="googlevideo.com"]',
        'video[src*="blob:"]'
    ];

    const KNOWN_VIDEO_SITES = [
        'youtube.com', 'tiktok.com', 'instagram.com', 'twitter.com',
        'x.com', 'facebook.com', 'threads.net', 'douyin.com',
        'xiaohongshu.com', 'ted.com', 'snapchat.com', 'pinterest.com', 'spotify.com'
    ];

    let lastActiveVideoTime = 0;
    let cachedActiveVideo = null;

    const getActiveVideoElement = () => {
        const now = Date.now();
        if (now - lastActiveVideoTime < 150 && cachedActiveVideo && document.body.contains(cachedActiveVideo)) {
            return cachedActiveVideo;
        }

        const videos = [];
        
        // Find all videos in main document and shadow roots
        const queue = [document.body];
        const visitedNodes = new Set();
        while (queue.length > 0) {
            const node = queue.shift();
            if (!node || visitedNodes.has(node)) continue;
            visitedNodes.add(node);

            const found = node.querySelectorAll?.('video');
            if (found) {
                found.forEach(v => videos.push(v));
            }

            const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);
            while (walker.nextNode()) {
                const el = walker.currentNode;
                if (el.shadowRoot && !visitedNodes.has(el.shadowRoot)) {
                    queue.push(el.shadowRoot);
                }
            }
        }

        if (videos.length === 0) {
            cachedActiveVideo = null;
            lastActiveVideoTime = now;
            return null;
        }
        if (videos.length === 1) {
            cachedActiveVideo = videos[0];
            lastActiveVideoTime = now;
            return videos[0];
        }

        // 1. Try to find the playing/active video
        const playing = videos.find(v => !v.paused && v.currentTime > 0);
        if (playing) {
            cachedActiveVideo = playing;
            lastActiveVideoTime = now;
            return playing;
        }

        // 2. Find the one with largest visible area in the viewport
        let bestVideo = null;
        let maxVisibleArea = 0;

        videos.forEach(v => {
            const rect = v.getBoundingClientRect();
            const top = Math.max(0, rect.top);
            const bottom = Math.min(window.innerHeight, rect.bottom);
            const left = Math.max(0, rect.left);
            const right = Math.min(window.innerWidth, rect.right);

            if (bottom > top && right > left) {
                const visibleArea = (bottom - top) * (right - left);
                if (visibleArea > maxVisibleArea) {
                    maxVisibleArea = visibleArea;
                    bestVideo = v;
                }
            }
        });

        const result = bestVideo || videos[0];
        cachedActiveVideo = result;
        lastActiveVideoTime = now;
        return result;
    };

    const findAnyVideo = () => {
        if (window.location.hostname.includes('youtube.com')) {
            for (const selector of YOUTUBE_PLAYER_SELECTORS) {
                const v = document.querySelector(selector);
                if (v) return v;
            }
        }
        return getActiveVideoElement();
    };

    function getExtractor() {
        const host = window.location.hostname;
        for (const domain of Object.keys(extractors)) {
            if (domain !== 'generic' && host.includes(domain)) {
                return extractors[domain];
            }
        }
        return extractors['generic'];
    }

    function getPlayerContainer(video) {
        const specificPlayer =
            video.closest('[data-uia="video-player"]') ||
            video.closest('.watch-video') ||
            video.closest('[class*="watch-video"]') ||
            video.closest('[class*="nf-player"]') ||
            video.closest('.html5-video-player') ||
            video.closest('.video-player') ||
            video.closest('[class*="video-player"]') ||
            video.closest('[class*="stream-player"]') ||
            video.closest('[data-testid*="player"]') ||
            video.closest('[role="main"]') ||
            video.closest('[id*="player"]');

        const forbidden = ['BODY', 'HTML', 'MAIN', 'ARTICLE', 'SECTION'];
        if (specificPlayer && !forbidden.includes(specificPlayer.tagName)) return specificPlayer;

        let current = video.parentElement;
        const keywords = ['player', 'video-player', 'movie_player', 'video-container', 'video-wrapper', 'player-container', 'vjs-control-bar', 'stream-player'];

        while (current && !forbidden.includes(current.tagName)) {
            const id = current.id || '';
            const classes = Array.from(current.classList).join(' ').toLowerCase();

            if (keywords.some(k => id.toLowerCase().includes(k) || classes.includes(k))) {
                return current;
            }
            current = current.parentElement;
        }

        return video.parentElement && !forbidden.includes(video.parentElement.tagName) ? video.parentElement : video;
    }

    const containerListeners = new WeakMap();
    const attachedVideos = new WeakSet();

    function injectYouTubeControlButton() {
        if (document.getElementById('ksmo-yt-control-btn')) return;
        const player = document.querySelector('.html5-video-player');
        if (!player) return;
        const rightControls = player.querySelector('.ytp-right-controls');
        if (!rightControls) return;

        const btn = document.createElement('button');
        btn.id = 'ksmo-yt-control-btn';
        btn.className = 'ytp-button';
        btn.setAttribute('aria-label', getText('download'));
        btn.setAttribute('title', getText('nativeYtDlTitle'));
        btn.style.display = 'inline-flex';
        btn.style.alignItems = 'center';
        btn.style.justifyContent = 'center';
        btn.style.verticalAlign = 'top';
        btn.style.cursor = 'pointer';

        btn.innerHTML = `
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="width: 20px; height: 20px; margin: auto;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
        `;

        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            e.preventDefault();
            openDownloadModal();
        });

        rightControls.prepend(btn);
    }

    function attachTriggerButton(video) {
        if (video.dataset.ksmoDownloaderAttached) return;
        if (video.offsetWidth < 50 || video.offsetHeight < 50) return;

        const container = getPlayerContainer(video);
        if (container.querySelector('.ksmo-dl-trigger')) {
            video.dataset.ksmoDownloaderAttached = 'true';
            return;
        }

        if (getSetting(SETTINGS_KEYS.showButton, true) === false) {
            video.dataset.ksmoDownloaderAttached = 'true';
            return;
        }

        if (!video.isConnected) {
            return;
        }

        video.dataset.ksmoDownloaderAttached = 'true';
        attachedVideos.add(video);

        const trigger = document.createElement('div');
        trigger.className = 'ksmo-dl-trigger';
        trigger.setAttribute('aria-label', getText('floatingButtonLabel'));
        trigger.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
        `;

        const computedStyle = window.getComputedStyle(container);
        if (computedStyle.position === 'static') {
            container.style.position = 'relative';
        }

        container.appendChild(trigger);

        let hideTimeout;
        const showTrigger = () => {
            clearTimeout(hideTimeout);
            trigger.classList.add('visible');
        };

        const hideTrigger = () => {
            hideTimeout = setTimeout(() => {
                trigger.classList.remove('visible');
            }, 1200);
        };

        if (containerListeners.has(container)) {
            containerListeners.get(container).abort();
        }
        const abController = new AbortController();
        containerListeners.set(container, abController);

        container.addEventListener('mouseenter', showTrigger, { capture: true, passive: true, signal: abController.signal });
        container.addEventListener('mousemove', showTrigger, { capture: true, passive: true, signal: abController.signal });
        container.addEventListener('mouseleave', hideTrigger, { capture: true, passive: true, signal: abController.signal });
        container.addEventListener('touchstart', showTrigger, { capture: true, passive: true, signal: abController.signal });

        trigger.addEventListener('click', (e) => {
            e.stopPropagation();
            e.preventDefault();
            openDownloadModal();
        });
    }

    let activeModalBackdrop = null;

    function renderQualityRow(f, info, progressContainer, fill, percentText, list) {
        const row = document.createElement('div');
        row.className = 'ksmo-quality-row';

        const qInfo = document.createElement('div');
        qInfo.className = 'ksmo-quality-info';

        const name = document.createElement('div');
        name.className = 'ksmo-quality-name';
        name.textContent = f.quality;

        const label = document.createElement('div');
        label.className = 'ksmo-quality-label';
        label.textContent = f.label;

        qInfo.appendChild(name);
        qInfo.appendChild(label);

        if (!f.isSubtitle && f.url) {
            fetchFileSize(f.url, (bytes) => {
                if (bytes) {
                    label.textContent = `${f.label} • ${formatBytes(bytes)}`;
                }
            });
        }

        const actionsContainer = document.createElement('div');
        actionsContainer.className = 'ksmo-actions-wrapper';
        actionsContainer.style.display = 'flex';
        actionsContainer.style.alignItems = 'center';
        actionsContainer.style.gap = '8px';

        const dlBtn = document.createElement('button');
        dlBtn.className = 'ksmo-dl-btn';
        dlBtn.setAttribute('aria-label', getText('ariaFormatDownload').replace('{quality}', f.quality));
        dlBtn.textContent = getText('download');

        actionsContainer.appendChild(dlBtn);

        let cobaltRowBtn = null;
        if (!f.isSubtitle && !f.isHLS && f.url && !f.url.startsWith('blob:')) {
            cobaltRowBtn = document.createElement('button');
            cobaltRowBtn.className = 'ksmo-secondary-btn';
            cobaltRowBtn.setAttribute('aria-label', getText('ariaCobaltRow'));
            cobaltRowBtn.textContent = getText('cobalt');
            cobaltRowBtn.style.padding = '6px 12px';
            cobaltRowBtn.style.fontSize = '13px';
            cobaltRowBtn.style.borderColor = '#6c5ce7';
            cobaltRowBtn.style.color = '#a29bfe';
            cobaltRowBtn.addEventListener('click', () => {
                let url = f.pageUrl || f.url;
                if (!url || url.startsWith('blob:')) {
                    url = window.location.href;
                }
                if (window.location.hostname.includes('tiktok.com')) {
                    showTikTokCobaltConfirmation(url);
                } else {
                    openCobaltUI(url);
                }
            });
            actionsContainer.appendChild(cobaltRowBtn);
        }

        if (f.isHLS) {
            dlBtn.textContent = getText('fallback');
            dlBtn.addEventListener('click', () => {
                showToast(getText('toastHlsDetected'), 3000);
                setTimeout(() => {
                    triggerFallbackRedirect(window.location.href);
                }, 1000);
            });
        } else {
            if (f.cipher && !f.url) {
                dlBtn.disabled = true;
                dlBtn.style.opacity = '0.5';
                dlBtn.textContent = getText('deciphering').replace('...', '');
                
                let completed = false;
                const timeoutId = setTimeout(() => {
                    if (!completed) {
                        completed = true;
                        dlBtn.disabled = false;
                        dlBtn.style.opacity = '1';
                        dlBtn.textContent = getText('fallback');
                        dlBtn.addEventListener('click', () => {
                            showToast(getText('toastYtCipherFail'), 3000);
                            setTimeout(() => {
                                triggerFallbackRedirect(window.location.href);
                            }, 1000);
                        }, { once: true });
                        f.label = f.label.replace('(Deciphering...)', '(Cipher Blocked)');
                        label.textContent = f.label;
                    }
                }, 10000);

                decipherUrl(f.cipher, (decryptedUrl) => {
                    if (completed) return;
                    completed = true;
                    clearTimeout(timeoutId);
                    
                    if (decryptedUrl) {
                        f.url = decryptedUrl;
                        f.cipher = null;
                        dlBtn.disabled = false;
                        dlBtn.style.opacity = '1';
                        dlBtn.textContent = getText('download');
                        
                        const cleanLabel = f.label.replace(' (Deciphering...)', '').replace(` (${getText('deciphering')})`, '');
                        f.label = cleanLabel;
                        label.textContent = cleanLabel;
                        
                        fetchFileSize(decryptedUrl, (bytes) => {
                            if (bytes) {
                                label.textContent = `${cleanLabel} • ${formatBytes(bytes)}`;
                            }
                        });

                        // Recreate Cobalt row button after decryption if needed
                        if (cobaltRowBtn) cobaltRowBtn.remove();
                        if (!f.isSubtitle && !f.isHLS && decryptedUrl && !decryptedUrl.startsWith('blob:')) {
                            cobaltRowBtn = document.createElement('button');
                            cobaltRowBtn.className = 'ksmo-secondary-btn';
                            cobaltRowBtn.setAttribute('aria-label', getText('ariaCobaltRow'));
                            cobaltRowBtn.textContent = getText('cobalt');
                            cobaltRowBtn.style.padding = '6px 12px';
                            cobaltRowBtn.style.fontSize = '13px';
                            cobaltRowBtn.style.borderColor = '#6c5ce7';
                            cobaltRowBtn.style.color = '#a29bfe';
                            cobaltRowBtn.addEventListener('click', () => {
                                let url = f.pageUrl || decryptedUrl;
                                if (!url || url.startsWith('blob:')) {
                                    url = window.location.href;
                                }
                                if (window.location.hostname.includes('tiktok.com')) {
                                    showTikTokCobaltConfirmation(url);
                                } else {
                                    openCobaltUI(url);
                                }
                            });
                            actionsContainer.appendChild(cobaltRowBtn);
                        }
                    } else {
                        dlBtn.disabled = false;
                        dlBtn.style.opacity = '1';
                        dlBtn.textContent = getText('fallback');
                        dlBtn.addEventListener('click', () => {
                            showToast(getText('toastYtCipherFail'), 3000);
                            setTimeout(() => {
                                triggerFallbackRedirect(window.location.href);
                            }, 1000);
                        }, { once: true });
                        f.label = f.label.replace('(Deciphering...)', '(Cipher Blocked)');
                        label.textContent = f.label;
                    }
                });
            }

            dlBtn.addEventListener('click', () => {
                if (!f.url) return;
                progressContainer.style.display = 'flex';
                fill.style.width = '0%';
                percentText.textContent = '0%';

                const sanitizedTitle = info.title.replace(/[\\/:*?"<>|]/g, '_');
                
                if (f.isSubtitle) {
                    const subtitleReq = GM_xmlhttpRequest({
                        method: 'GET',
                        url: f.url,
                        onload: (resp) => {
                            if (DownloadQueue.active?.cancelled) return;
                            const srt = convertXmlToSrt(resp.responseText);
                            const blob = new Blob([srt], { type: 'text/plain' });
                            const blobUrl = URL.createObjectURL(blob);
                            const a = document.createElement('a');
                            a.href = blobUrl;
                            a.download = `${sanitizedTitle}_${f.quality}.srt`;
                            document.body.appendChild(a);
                            a.click();
                            a.remove();
                            setTimeout(() => URL.revokeObjectURL(blobUrl), 5000);
                            progressContainer.style.display = 'none';
                            showToast(getText('toastSubtitlesSuccess'));
                        },
                        onerror: () => {
                            progressContainer.style.display = 'none';
                            showToast(getText('toastSubtitlesFail'));
                        }
                    });
                    return;
                }

                const extension = f.type.includes('video') ? 'mp4' : 'mp3';
                const filename = `${sanitizedTitle}_${f.quality}.${extension}`;

                const pName = progressContainer.querySelector('.ksmo-progress-name');
                if (pName) {
                    pName.textContent = filename.length > 32 ? filename.substring(0, 30) + '...' : filename;
                }

                // Apply nsig transform for YouTube URLs before downloading
                const isYouTube = window.location.hostname.includes('youtube.com');
                const enqueueDownload = (finalUrl) => {
                    DownloadQueue.add(
                        finalUrl,
                        filename,
                        (percent) => {
                            fill.style.width = `${percent}%`;
                            percentText.textContent = `${percent}%`;
                        },
                        () => {
                            progressContainer.style.display = 'none';
                            showToast(getText('toastDownloadSuccess'));
                        },
                        (err) => {
                            progressContainer.style.display = 'none';
                            showToast(getText('toastDirectFail'), 5000);
                            if (getSetting(SETTINGS_KEYS.autoFallback, false)) {
                                if (confirm(getText('toastDirectFailConfirm'))) {
                                    let fallbackUrl = f.pageUrl || f.url;
                                    if (!fallbackUrl || fallbackUrl.startsWith('blob:')) {
                                        fallbackUrl = window.location.href;
                                    }
                                    triggerFallbackRedirect(fallbackUrl);
                                }
                            }
                        }
                    );
                };

                if (isYouTube && typeof applyNTransform === 'function') {
                    applyNTransform(f.url, enqueueDownload);
                } else {
                    enqueueDownload(f.url);
                }
            });
        }

        row.appendChild(qInfo);
        row.appendChild(actionsContainer);
        list.appendChild(row);
    }

    const exportPlaylist = () => {
        const selectors = [
            'ytd-playlist-video-renderer a#video-title',
            'ytd-playlist-video-renderer a.yt-simple-endpoint',
            'ytd-playlist-panel-video-renderer a.yt-simple-endpoint',
            'a[href*="/watch?v="][href*="&list="]'
        ];
        
        let links = [];
        for (const selector of selectors) {
            const found = Array.from(document.querySelectorAll(selector))
                .map(a => a.href)
                .filter(href => href && href.includes('/watch?v=') && href.includes('&list='));
            if (found.length > 0) {
                links = found;
                break;
            }
        }
        
        const parsedLinks = links.map(href => {
            const url = new URL(href);
            return `https://www.youtube.com/watch?v=${url.searchParams.get('v')}`;
        });

        const uniqueLinks = [...new Set(parsedLinks)];
        if (uniqueLinks.length > 0) {
            const blob = new Blob([uniqueLinks.join('\n')], { type: 'text/plain' });
            const blobUrl = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = blobUrl;
            a.download = `playlist_${new URLSearchParams(window.location.search).get('list') || 'export'}.txt`;
            a.click();
            setTimeout(() => URL.revokeObjectURL(blobUrl), 5000);
            showToast(getText('toastPlaylistExported').replace('{count}', uniqueLinks.length));
        } else {
            showToast(getText('toastNoPlaylist'));
        }
    };

    function openDownloadModal() {
        if (getSelectedLang() === null) {
            showLanguageSelectionModal(() => {
                openDownloadModal();
            });
            return;
        }

        if (activeModalBackdrop) {
            activeModalBackdrop.remove();
        }

        const extractor = getExtractor();
        const info = extractor();

        const backdrop = document.createElement('div');
        backdrop.className = 'ksmo-modal-backdrop';

        const modal = document.createElement('div');
        modal.className = 'ksmo-modal';

        const header = document.createElement('div');
        header.className = 'ksmo-modal-header';
        
        const title = document.createElement('div');
        title.className = 'ksmo-modal-title';
        title.textContent = info ? info.title : (window.location.hostname.includes('spotify.com') ? getText('labelSpotify') : getText('title'));

        const headerActions = document.createElement('div');
        headerActions.className = 'ksmo-header-actions';

        const minBtn = document.createElement('button');
        minBtn.className = 'ksmo-modal-min';
        minBtn.setAttribute('aria-label', getText('ariaMinimize'));
        minBtn.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/></svg>
        `;

        const closeBtn = document.createElement('button');
        closeBtn.className = 'ksmo-modal-close';
        closeBtn.setAttribute('aria-label', getText('ariaClose'));
        closeBtn.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
        `;

        headerActions.appendChild(minBtn);
        headerActions.appendChild(closeBtn);
        header.appendChild(title);
        header.appendChild(headerActions);
        modal.appendChild(header);

        const progressContainer = document.createElement('div');
        progressContainer.className = 'ksmo-progress-container';
        progressContainer.innerHTML = `
            <div class="ksmo-progress-name" style="font-size: 11px; color: #a29bfe; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"></div>
            <div class="ksmo-progress-bar"><div class="ksmo-progress-fill"></div></div>
            <div class="ksmo-progress-text">
                <span class="ksmo-progress-percent">0%</span>
                <span class="ksmo-progress-cancel" style="cursor: pointer; text-decoration: underline;">${getText('cancel')}</span>
            </div>
        `;
        modal.appendChild(progressContainer);

        const cancelBtn = progressContainer.querySelector('.ksmo-progress-cancel');
        const fill = progressContainer.querySelector('.ksmo-progress-fill');
        const percentText = progressContainer.querySelector('.ksmo-progress-percent');

        cancelBtn.addEventListener('click', () => {
            DownloadQueue.clear();
            progressContainer.style.display = 'none';
            showToast(getText('allCanceled'));
        });

        const list = document.createElement('div');
        list.className = 'ksmo-quality-list';

        if (info && info.formats && info.formats.length > 0) {
            info.formats.forEach(f => {
                renderQualityRow(f, info, progressContainer, fill, percentText, list);
            });
        } else if (window.location.hostname.includes('spotify.com')) {
            const spotifyMessage = document.createElement('div');
            spotifyMessage.style.textAlign = 'center';
            spotifyMessage.style.padding = '20px 0';
            spotifyMessage.style.color = '#a29bfe';
            spotifyMessage.textContent = getText('spotifyRedirect');
            list.appendChild(spotifyMessage);
        } else {
            const emptyMessage = document.createElement('div');
            emptyMessage.style.textAlign = 'center';
            emptyMessage.style.padding = '20px 0';
            emptyMessage.style.color = '#94a3b8';
            emptyMessage.textContent = getText('noFormats');
            list.appendChild(emptyMessage);
        }

        modal.appendChild(list);

        if (window.location.hostname.includes('youtube.com')) {
            const urlParams = new URLSearchParams(window.location.search);
            let videoId = urlParams.get('v');
            if (!videoId && window.location.pathname.includes('/shorts/')) {
                const match = window.location.pathname.match(/\/shorts\/([a-zA-Z0-9_-]+)/);
                if (match) videoId = match[1];
            }
            if (!videoId && window.location.pathname.includes('/embed/')) {
                const match = window.location.pathname.match(/\/embed\/([a-zA-Z0-9_-]+)/);
                if (match) videoId = match[1];
            }

            if (videoId) {
                fetchEmbedPlayerResponse(videoId, (data) => {
                    if (!data || !data.streamingData) {
                        const hasProtected = Array.from(list.querySelectorAll('.ksmo-quality-row')).some(row => {
                            const lbl = row.querySelector('.ksmo-quality-label');
                            return lbl && (lbl.textContent.includes('Signature Protected') || lbl.textContent.includes('Cipher Blocked'));
                        });
                        if (hasProtected) {
                            showToast(getText('toastYtBlocked'), 6000);
                        }
                        return;
                    }
                    const embedFormats = [];
                    const muxed = data.streamingData.formats || [];
                    const adaptive = data.streamingData.adaptiveFormats || [];

                    const parseFormat = (f, isMuxed) => {
                        let url = f.url;
                        let cipher = null;
                        if (!url && f.signatureCipher) {
                            cipher = f.signatureCipher;
                        }
                        if (url || cipher) {
                            const isVideo = f.mimeType?.includes('video');
                            embedFormats.push({
                                quality: f.qualityLabel || `${f.width}p`,
                                url: url || '',
                                cipher: cipher,
                                type: f.mimeType?.split(';')[0] || 'video/mp4',
                                label: cipher ? (isMuxed ? `Muxed [${utils.getCodec(f.mimeType)}] (${getText('deciphering')})` : (isVideo ? `Adaptive [${utils.getCodec(f.mimeType)}] (${getText('deciphering')})` : `Audio Only [${utils.getCodec(f.mimeType)}] (${getText('deciphering')})`)) : (isMuxed ? `Muxed [${utils.getCodec(f.mimeType)}]` : (isVideo ? `Adaptive [${utils.getCodec(f.mimeType)}] (${getText('adaptiveNoAudio')})` : `Audio Only [${utils.getCodec(f.mimeType)}]`))
                            });
                        }
                    };

                    muxed.forEach(f => parseFormat(f, true));
                    adaptive.forEach(f => parseFormat(f, false));

                    if (embedFormats.length > 0) {
                        const existingRows = list.querySelectorAll('.ksmo-quality-row');
                        existingRows.forEach(row => {
                            const lbl = row.querySelector('.ksmo-quality-label');
                            if (lbl && (lbl.textContent.includes('Signature Protected') || lbl.textContent.includes('Cipher Blocked') || lbl.textContent.includes(getText('cipherBlocked')))) {
                                row.remove();
                            }
                        });
                        embedFormats.forEach(f => {
                            renderQualityRow(f, info || { title: document.title }, progressContainer, fill, percentText, list);
                        });
                    } else {
                        const hasProtected = Array.from(list.querySelectorAll('.ksmo-quality-row')).some(row => {
                            const lbl = row.querySelector('.ksmo-quality-label');
                            return lbl && (lbl.textContent.includes('Signature Protected') || lbl.textContent.includes('Cipher Blocked') || lbl.textContent.includes(getText('cipherBlocked')));
                        });
                        if (hasProtected) {
                            showToast(getText('toastYtBlocked'), 6000);
                        }
                    }
                });
            }
        }

        const warningLabel = document.createElement('div');
        warningLabel.className = 'ksmo-warning-label';
        warningLabel.textContent = getText('externalWarning');
        modal.appendChild(warningLabel);

        const footer = document.createElement('div');
        footer.className = 'ksmo-modal-footer';

        const footerRow = document.createElement('div');
        footerRow.className = 'ksmo-footer-row';

        const serviceSelect = document.createElement('select');
        serviceSelect.className = 'ksmo-select';
        serviceSelect.setAttribute('aria-label', getText('ariaSelectService'));

        const available = getAvailableFallbacks();
        const saved = getSetting(getFallbackServiceSettingKey(), available[0]);
        const defaultService = available.includes(saved) ? saved : available[0];

        available.forEach(service => {
            const opt = document.createElement('option');
            opt.value = service;
            opt.textContent = service.toUpperCase();
            if (service === defaultService) {
                opt.selected = true;
            }
            serviceSelect.appendChild(opt);
        });

        serviceSelect.addEventListener('change', () => {
            setSetting(getFallbackServiceSettingKey(), serviceSelect.value);
        });

        const fallbackBtn = document.createElement('button');
        fallbackBtn.className = 'ksmo-secondary-btn';
        fallbackBtn.setAttribute('aria-label', getText('fallback'));
        fallbackBtn.textContent = getText('fallback');

        fallbackBtn.addEventListener('click', () => {
            if (confirm(getText('toastDirectFailConfirm'))) {
                let url = window.location.href;
                if (info && info.formats && info.formats.length > 0) {
                    url = info.formats[0].pageUrl || url;
                }
                triggerFallbackRedirect(url);
            }
        });

        const copyLinkBtn = document.createElement('button');
        copyLinkBtn.className = 'ksmo-secondary-btn';
        copyLinkBtn.setAttribute('aria-label', getText('copyLink'));
        copyLinkBtn.textContent = getText('copyLink');

        copyLinkBtn.addEventListener('click', () => {
            const link = info && info.formats && info.formats.length > 0 ? info.formats[0].url : window.location.href;
            navigator.clipboard.writeText(link).then(() => {
                showToast(getText('toastCopySuccess'));
            }).catch(() => {
                showToast(getText('toastCopyFail'));
            });
        });

        const cobaltBtn = document.createElement('button');
        cobaltBtn.className = 'ksmo-secondary-btn';
        cobaltBtn.setAttribute('aria-label', getText('cobalt'));
        cobaltBtn.textContent = getText('cobalt');
        cobaltBtn.style.borderColor = '#6c5ce7';
        cobaltBtn.style.color = '#a29bfe';

        cobaltBtn.addEventListener('click', () => {
            let url = window.location.href;
            if (info && info.formats && info.formats.length > 0) {
                const firstFormat = info.formats[0];
                url = firstFormat.pageUrl || firstFormat.url || url;
            }
            if (!url || url.startsWith('blob:')) {
                url = window.location.href;
            }
            if (window.location.hostname.includes('tiktok.com')) {
                showTikTokCobaltConfirmation(url);
            } else {
                openCobaltUI(url);
            }
        });

        footerRow.appendChild(serviceSelect);
        footerRow.appendChild(fallbackBtn);
        footerRow.appendChild(cobaltBtn);
        footerRow.appendChild(copyLinkBtn);
        
        if (window.location.hostname.includes('youtube.com') && window.location.search.includes('list=')) {
            const playlistBtn = document.createElement('button');
            playlistBtn.className = 'ksmo-secondary-btn';
            playlistBtn.textContent = getText('exportPlaylist');
            playlistBtn.addEventListener('click', exportPlaylist);
            footerRow.appendChild(playlistBtn);
        }

        footer.appendChild(footerRow);
        modal.appendChild(footer);

        backdrop.appendChild(modal);
        document.body.appendChild(backdrop);
        activeModalBackdrop = backdrop;

        setTimeout(() => backdrop.classList.add('show'), 10);

        const dismissModal = () => {
            backdrop.classList.remove('show');
            setTimeout(() => {
                DownloadQueue.clear();
                backdrop.remove();
                activeModalBackdrop = null;
            }, 300);
        };

        minBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            if (backdrop.classList.contains('minimized')) {
                backdrop.classList.remove('minimized');
                minBtn.innerHTML = `
                    <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/></svg>
                `;
            } else {
                backdrop.classList.add('minimized');
                minBtn.innerHTML = `
                    <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="12" x2="20" y2="12"/><line x1="12" y1="4" x2="12" y2="20"/></svg>
                `;
            }
        });

        modal.addEventListener('click', (e) => {
            if (backdrop.classList.contains('minimized') && !e.target.closest('.ksmo-modal-close') && !e.target.closest('.ksmo-modal-min') && !e.target.closest('.ksmo-progress-cancel')) {
                backdrop.classList.remove('minimized');
                minBtn.innerHTML = `
                    <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/></svg>
                `;
            }
        });

        closeBtn.addEventListener('click', dismissModal);
        backdrop.addEventListener('click', (e) => {
            if (e.target === backdrop) dismissModal();
        });
    }

    function triggerFallbackRedirect(targetUrl) {
        const available = getAvailableFallbacks();
        const key = getFallbackServiceSettingKey();
        const saved = getSetting(key, available[0]);
        const service = available.includes(saved) ? saved : available[0];
        const resolver = FALLBACK_SERVICES[service] || FALLBACK_SERVICES[available[0]];
        const finalUrl = resolver(targetUrl);
        if (typeof GM_openInTab !== 'undefined') {
            GM_openInTab(finalUrl, { active: true });
        } else {
            window.open(finalUrl, '_blank');
        }
    }

    let visited = new WeakSet();
    let isScanning = false;

    function scanAndAttach(targetNode = document.body) {
        if (isScanning) return;
        isScanning = true;

        let foundVideo = false;

        try {
            const nodesToProcess = Array.isArray(targetNode) ? targetNode : [targetNode];
            const queue = [...nodesToProcess];

            while (queue.length > 0) {
                const node = queue.shift();
                if (!node || visited.has(node)) continue;
                visited.add(node);

                if (typeof node.querySelectorAll === 'function') {
                    const videos = node.querySelectorAll('video');
                    videos.forEach(video => {
                        const area = video.offsetWidth * video.offsetHeight;
                        if ((video.offsetWidth > 50 && video.offsetHeight > 50) || video.readyState > 0 || area > 2500) {
                            foundVideo = true;
                            if (!window.location.hostname.includes('youtube.com')) {
                                attachTriggerButton(video);
                            }
                        }
                    });

                    const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);
                    while (walker.nextNode()) {
                        const el = walker.currentNode;
                        if (el.shadowRoot && !visited.has(el.shadowRoot)) {
                            queue.push(el.shadowRoot);
                        }
                    }
                }
            }

            if (window.location.hostname.includes('youtube.com')) {
                injectYouTubeControlButton();
                nodesToProcess.forEach(n => {
                    if (typeof n.querySelectorAll === 'function') {
                        YOUTUBE_PLAYER_SELECTORS.forEach(selector => {
                            try {
                                const videos = n.querySelectorAll(selector);
                                if (videos.length > 0) foundVideo = true;
                            } catch (e) {}
                        });
                    }
                });
            }
        } finally {
            isScanning = false;
        }

        if (KNOWN_VIDEO_SITES.some(s => window.location.hostname.includes(s))) {
            foundVideo = true;
        }

        const fab = document.getElementById('ksmo-floating-dl');
        if (fab) {
            if (foundVideo) {
                fab.classList.add('visible');
            } else {
                fab.classList.remove('visible');
            }
        }
    }

    let pendingNodes = [];
    let scanTimeout = null;

    const scanObserver = new MutationObserver((mutations) => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    pendingNodes.push(node);
                }
            });
        });

        if (pendingNodes.length > 0) {
            clearTimeout(scanTimeout);
            scanTimeout = setTimeout(() => {
                scanAndAttach(pendingNodes);
                pendingNodes = [];
            }, 100);
        }
    });

    let idleScanInterval = null;
    let visitedResetInterval = null;

    const createFloatingButton = () => {
        if (window.top !== window.self) return;
        if (document.getElementById('ksmo-floating-dl')) return;
        if (getSetting(SETTINGS_KEYS.showButton, true) === false) return;
        
        const fab = document.createElement('div');
        fab.id = 'ksmo-floating-dl';
        fab.className = 'ksmo-floating-dl';
        fab.setAttribute('aria-label', getText('floatingButtonLabel'));
        fab.title = getText('nativeYtDlTitle');
        fab.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
        `;
        fab.addEventListener('click', (e) => {
            e.stopPropagation();
            e.preventDefault();
            openDownloadModal();
        });
        document.body.appendChild(fab);
    };

    const initApp = () => {
        createFloatingButton();
        scanAndAttach(document.body);
        scanObserver.observe(document.body, { childList: true, subtree: true });

        if (idleScanInterval) clearInterval(idleScanInterval);
        idleScanInterval = setInterval(() => {
            if (window.requestIdleCallback) {
                window.requestIdleCallback(() => scanAndAttach(document.body));
            } else {
                scanAndAttach(document.body);
            }
        }, 300000);

        if (visitedResetInterval) clearInterval(visitedResetInterval);
        visitedResetInterval = setInterval(() => {
            visited = new WeakSet();
        }, 300000);

        // YouTube SPA navigation support
        if (window.location.hostname.includes('youtube.com')) {
            document.addEventListener('yt-navigate-start', () => {
                visited = new WeakSet(); // Reset BEFORE DOM changes
                if (typeof baseJsFetcher !== 'undefined') {
                    baseJsFetcher.cachedCode = null;
                    baseJsFetcher.cachedUrl = null;
                }
            });
            document.addEventListener('yt-navigate-finish', () => {
                injectYouTubeControlButton();
                scanAndAttach(document.body);
            });
        }
    };

    if (!document.body) {
        new MutationObserver((_, obs) => {
            if (document.body) {
                initApp();
                obs.disconnect();
            }
        }).observe(document.documentElement, { childList: true });
    } else {
        initApp();
    }

    window.addEventListener('keydown', (e) => {
        if (getSetting(SETTINGS_KEYS.enableHotkey, true) === false) return;
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) {
            return;
        }

        const isD = e.key === 'd' || e.key === 'D' || e.keyCode === 68;
        if (e.ctrlKey && e.altKey && e.shiftKey && isD) {
            e.preventDefault();
            e.stopPropagation();

            const video = findAnyVideo();
            if (!video && !window.location.hostname.includes('spotify.com')) {
                showToast(getText('toastNoVideo'));
                return;
            }
            openDownloadModal();
        }

        if (e.key === 'Escape' && activeModalBackdrop) {
            e.preventDefault();
            e.stopPropagation();
            activeModalBackdrop.classList.remove('show');
            setTimeout(() => {
                DownloadQueue.clear();
                activeModalBackdrop?.remove();
                activeModalBackdrop = null;
            }, 300);
        }
    }, true);

    if (typeof GM_registerMenuCommand !== 'undefined') {
        GM_registerMenuCommand(getText('menuOpenMenu'), () => {
            openDownloadModal();
        });
        GM_registerMenuCommand(getText('menuDownloadCobalt'), () => {
            let url = window.location.href;
            if (window.location.hostname.includes('tiktok.com')) {
                const activeVideo = getActiveVideoElement();
                let videoId = null;
                if (activeVideo) {
                    let parent = activeVideo.parentElement;
                    while (parent && parent !== document.body) {
                        const links = parent.querySelectorAll('a[href*="/video/"]');
                        for (const link of links) {
                            const match = link.href.match(/\/video\/(\d+)/);
                            if (match) {
                                videoId = match[1];
                                break;
                            }
                        }
                        if (videoId) break;
                        parent = parent.parentElement;
                    }
                }
                if (!videoId) {
                    const match = window.location.href.match(/\/video\/(\d+)/);
                    if (match) videoId = match[1];
                }
                if (videoId) {
                    url = `https://www.tiktok.com/video/${videoId}`;
                }
                showTikTokCobaltConfirmation(url);
            } else {
                openCobaltUI(url);
            }
        });
        GM_registerMenuCommand('🌐 Change Language / Alterar Idioma / 更改语言', () => {
            showLanguageSelectionModal(() => {
                showToast(getText('toastReloadPage'));
            });
        });
        GM_registerMenuCommand(getText('menuSetCobaltUrl'), () => {
            const current = getCobaltApiUrl();
            const newApiUrl = prompt(getText('promptSetCobaltUrl'), current);
            if (newApiUrl !== null) {
                const finalUrl = newApiUrl.trim() || 'cobalt.tools';
                setSetting(SETTINGS_KEYS.cobaltApiUrl, finalUrl);
                showToast(getText('toastCobaltApiUrl').replace('{url}', finalUrl));
            }
        });
        GM_registerMenuCommand(getText('menuSetProxy'), () => {
            const current = getSetting(SETTINGS_KEYS.customProxy, '');
            const proxy = prompt(getText('promptSetProxy'), current);
            if (proxy !== null) {
                setSetting(SETTINGS_KEYS.customProxy, proxy.trim());
                showToast(proxy.trim() ? getText('toastProxyConfigured') : getText('toastProxyCleared'));
            }
        });
        GM_registerMenuCommand(getText('menuToggleBtn'), () => {
            const current = getSetting(SETTINGS_KEYS.showButton, true);
            setSetting(SETTINGS_KEYS.showButton, !current);
            showToast(`${getText('menuToggleBtn')}: ${!current ? getText('toastSettingEnabled') : getText('toastSettingDisabled')}. ${getText('toastReloadPage')}`);
        });
        GM_registerMenuCommand(getText('menuToggleFallback'), () => {
            const current = getSetting(SETTINGS_KEYS.autoFallback, false);
            setSetting(SETTINGS_KEYS.autoFallback, !current);
            showToast(`${getText('menuToggleFallback')}: ${!current ? getText('toastSettingEnabled') : getText('toastSettingDisabled')}.`);
        });
        GM_registerMenuCommand(getText('menuToggleHotkey'), () => {
            const current = getSetting(SETTINGS_KEYS.enableHotkey, true);
            setSetting(SETTINGS_KEYS.enableHotkey, !current);
            showToast(`${getText('menuToggleHotkey')}: ${!current ? getText('toastSettingEnabled') : getText('toastSettingDisabled')}.`);
        });
    }

    window.addEventListener('beforeunload', () => {
        scanObserver.disconnect();
        if (idleScanInterval) clearInterval(idleScanInterval);
        if (visitedResetInterval) clearInterval(visitedResetInterval);
        visited = new WeakSet();
        pendingNodes = [];
        pendingSizes.clear();
        toastQueue.length = 0;
        if (activeToast) { activeToast.remove(); activeToast = null; }
        clearTimeout(toastTimeout);
        clearTimeout(scanTimeout);
    });

    /* ────────────────────────────────────────────────────────────────
     * DEV SANITY CHECKS — Set the flag to true to run.
     *
     * Manual testing instructions for fixed bugs:
     *   Bug #1  (iframe guard): Open a page with YouTube embed ads → script should NOT run in ad iframes.
     *   Bug #4  (DownloadManager): Start a download, cancel mid-progress → should cancel cleanly.
     *   Bug #6  (toast queue): Trigger 3+ toasts rapidly → no duplicates, correct order.
     *   Bug #7  (YouTube SPA): Navigate between 3 YouTube videos → download button appears on ALL.
     *   Bug #10 (YT control btn): Same as #7 → player control button persists across navigations.
     * ──────────────────────────────────────────────────────────────── */
    if (false) {
        console.assert(formatBytes(0) === '', 'formatBytes(0) should be empty');
        console.assert(formatBytes(1024) === '1 KB', 'formatBytes(1024)');
        console.assert(formatBytes(1536) === '1.5 KB', 'formatBytes(1536)');
        console.assert(formatBytes(1048576) === '1 MB', 'formatBytes(1MB)');
        console.assert(utils.getCodec('video/webm; codecs="vp9"') === 'WebM', 'getCodec webm');
        console.assert(utils.getCodec('video/mp4; codecs="avc1"') === 'MP4', 'getCodec mp4');
        console.assert(utils.getCodec('audio/ogg') === 'Ogg', 'getCodec ogg');
        console.assert(utils.getCodec('') === 'MP4', 'getCodec empty fallback');
        console.assert(convertXmlToSrt('') === '', 'convertXmlToSrt empty returns empty');
        console.assert(extractJsonFromHtml('var x = {"a":1};', 'x') !== null, 'extractJson should find x');
        console.assert(extractJsonFromHtml('nothing here', 'x') === null, 'extractJson miss returns null');
        console.log('✅ OmniDown sanity checks passed');
    }
})();