Greasy Fork

来自缓存

Greasy Fork is available in English.

Telegram图片视频下载器 (优化加强版)

支持从限制下载的 Telegram 频道中获取图片、视频及语音消息,界面设计与 Telegram 原生风格高度统一

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Telegram Media Downloader (Optimized & Enhanced)
// @name:ar     أداة تنزيل وسائط تيليغرام (محسّنة ومعززة)
// @name:be     Загрузчык медыя Telegram (аптымізаваны і палепшаны)
// @name:bg     Изтегляне на медии от Telegram (Оптимизирано и подобрено)
// @name:ckb    داگرتنی میدیای Telegram (باشکراو و پەرەپێدراو)
// @name:cs     Stahovač médií pro Telegram (Optimalizovaný a vylepšený)
// @name:da     Telegram medie-downloader (Optimeret og forbedret)
// @name:de     Telegram Media Downloader (Optimiert & Erweitert)
// @name:el     Λήψη πολυμέσων Telegram (Βελτιστοποιημένο & Ενισχυμένο)
// @name:en     Telegram Media Downloader (Optimized & Enhanced)
// @name:eo     Telegrama amaskomunikila elŝutilo (Optimumigita kaj plibonigita)
// @name:es     Descargador de medios de Telegram (Optimizado y mejorado)
// @name:es-419 Descargador de medios de Telegram (Optimizado y mejorado)
// @name:fi     Telegram-mediatiedostojen lataaja (Optimoitu ja parannettu)
// @name:fr     Téléchargeur de médias Telegram (Optimisé et amélioré)
// @name:fr-CA  Téléchargeur de médias Telegram (Optimisé et amélioré)
// @name:he     מוריד מדיה לטלגרם (ממוטב ומשופר)
// @name:hr     Telegram preuzimač medija (optimiziran i poboljšan)
// @name:hu     Telegram média letöltő (optimalizált és továbbfejlesztett)
// @name:id     Pengunduh media Telegram (Dioptimalkan & Ditingkatkan)
// @name:it     Downloader media Telegram (Ottimizzato e migliorato)
// @name:ja     Telegramメディアダウンローダー(最適化・強化版)
// @name:ka     Telegram მედიის ჩამომტვირთველი (ოპტიმიზებული და გაუმჯობესებული)
// @name:ko     텔레그램 미디어 다운로더 (최적화 및 강화)
// @name:mr     Telegram मीडिया डाउनलोडर (ऑप्टिमाइझ केलेले आणि सुधारित)
// @name:nb     Telegram medienedlaster (Optimalisert og forbedret)
// @name:nl     Telegram-mediadownloader (Geoptimaliseerd en verbeterd)
// @name:pl     Pobieracz multimediów Telegram (Zoptymalizowany i ulepszony)
// @name:pt-BR  Downloader de mídia do Telegram (Otimizado e aprimorado)
// @name:ro     Descărcător media Telegram (Optimizat și îmbunătățit)
// @name:ru     Загрузчик медиа Telegram (Оптимизированный и улучшенный)
// @name:sk     Sťahovač médií z Telegramu (optimalizovaný a vylepšený)
// @name:sr     Преузимач медија са Telegram-а (оптимизован и побољшан)
// @name:sv     Telegram media-nedladdare (Optimerad och förbättrad)
// @name:th     เครื่องมือดาวน์โหลดสื่อ Telegram (ปรับแต่งและเสริมประสิทธิภาพ)
// @name:tr     Telegram Medya İndirici (Optimize Edilmiş ve Geliştirilmiş)
// @name:uk     Завантажувач медіа Telegram (Оптимізований і покращений)
// @name:ug     Telegram مېدىيا چۈشۈرگۈچ (ئەلالاشتۇرۇلغان ۋە كۈچەيتىلگەن)
// @name:vi     Trình tải media Telegram (Tối ưu và nâng cao)
// @name:zh-CN  Telegram图片视频下载器 (优化加强版)
// @name:zh-TW  Telegram 圖片影片下載器(優化加強版)
// @description        Supports downloading images, videos, and voice messages from Telegram channels with download restrictions, featuring an interface that closely matches Telegram’s native design style.
// @description:ar     يدعم تنزيل الصور ومقاطع الفيديو والرسائل الصوتية من قنوات Telegram المقيّدة التنزيل، مع واجهة تتوافق بشكل كبير مع أسلوب تصميم Telegram الأصلي.
// @description:be     Падтрымлівае спампоўванне выяў, відэа і галасавых паведамленняў з каналаў Telegram з абмежаваннямі на спампоўванне, з інтэрфейсам, які блізка адпавядае ўласнаму стылю дызайну Telegram.
// @description:bg     Поддържа изтегляне на изображения, видеоклипове и гласови съобщения от Telegram канали с ограничения за изтегляне, с интерфейс, който е близък до оригиналния стил на Telegram.
// @description:ckb    پاڵپشتی داگرتنی وێنە، ڤیدیۆ و پەیامی دەنگی لە کەناڵەکانی Telegram دەکات کە سنووری داگرتنیان هەیە، لەگەڵ ڕووکارێک کە زۆر نزیکە لە شێوازی دیزاینی ڕەسەنی Telegram.
// @description:cs     Podporuje stahování obrázků, videí a hlasových zpráv z Telegram kanálů s omezeným stahováním a nabízí rozhraní, které se velmi podobá nativnímu stylu Telegramu.
// @description:da     Understøtter download af billeder, videoer og talebeskeder fra Telegram-kanaler med downloadbegrænsninger, med en grænseflade der ligger tæt op ad Telegrams oprindelige designstil.
// @description:de     Unterstützt das Herunterladen von Bildern, Videos und Sprachnachrichten aus Telegram-Kanälen mit Download-Beschränkungen und bietet eine Oberfläche, die dem nativen Telegram-Designstil sehr nahekommt.
// @description:el     Υποστηρίζει λήψη εικόνων, βίντεο και φωνητικών μηνυμάτων από κανάλια Telegram με περιορισμούς λήψης, με διεπαφή που ταιριάζει πολύ με το εγγενές στυλ σχεδίασης του Telegram.
// @description:en     Supports downloading images, videos, and voice messages from Telegram channels with download restrictions, featuring an interface that closely matches Telegram’s native design style.
// @description:eo     Subtenas elŝuti bildojn, filmetojn kaj voĉmesaĝojn el Telegram-kanaloj kun elŝutaj limigoj, kun interfaco kiu proksime kongruas kun la denaska dezajnstilo de Telegram.
// @description:es     Admite la descarga de imágenes, videos y mensajes de voz desde canales de Telegram con restricciones de descarga, con una interfaz que coincide de cerca con el estilo de diseño nativo de Telegram.
// @description:es-419 Permite descargar imágenes, videos y mensajes de voz desde canales de Telegram con restricciones de descarga, con una interfaz que se adapta al estilo nativo de Telegram.
// @description:fi     Tukee kuvien, videoiden ja ääniviestien lataamista Telegram-kanavista, joissa on latausrajoituksia. Käyttöliittymä vastaa läheisesti Telegramin natiivia ulkoasua.
// @description:fr     Prend en charge le téléchargement d’images, de vidéos et de messages vocaux depuis des canaux Telegram avec restrictions de téléchargement, avec une interface proche du style natif de Telegram.
// @description:fr-CA  Prend en charge le téléchargement d’images, de vidéos et de messages vocaux depuis des canaux Telegram avec restrictions de téléchargement, avec une interface très proche du style natif de Telegram.
// @description:he     תומך בהורדת תמונות, סרטונים והודעות קוליות מערוצי Telegram עם הגבלות הורדה, עם ממשק שתואם באופן הדוק לסגנון העיצוב המקורי של Telegram.
// @description:hr     Podržava preuzimanje slika, videozapisa i glasovnih poruka iz Telegram kanala s ograničenjima preuzimanja, uz sučelje koje je usklađeno s izvornim Telegram dizajnom.
// @description:hu     Támogatja képek, videók és hangüzenetek letöltését letöltési korlátozással rendelkező Telegram-csatornákról, a Telegram natív stílusához igazodó felülettel.
// @description:id     Mendukung pengunduhan gambar, video, dan pesan suara dari channel Telegram dengan pembatasan unduhan, dengan antarmuka yang selaras dengan gaya desain bawaan Telegram.
// @description:it     Supporta il download di immagini, video e messaggi vocali da canali Telegram con restrizioni di download, con un’interfaccia in linea con lo stile nativo di Telegram.
// @description:ja     ダウンロード制限のあるTelegramチャンネルから画像・動画・音声メッセージを保存でき、Telegram標準デザインに近いインターフェースを提供します。
// @description:ka     მხარს უჭერს სურათების, ვიდეოებისა და ხმოვანი შეტყობინებების ჩამოტვირთვას Telegram-ის იმ არხებიდან, სადაც ჩამოტვირთვა შეზღუდულია, და უზრუნველყოფს Telegram-ის მშობლიურ სტილთან ახლოს მყოფ ინტერფეისს.
// @description:ko     다운로드 제한이 있는 Telegram 채널에서 이미지, 동영상, 음성 메시지 다운로드를 지원하며, Telegram 기본 디자인 스타일과 유사한 인터페이스를 제공합니다.
// @description:mr     डाउनलोड निर्बंध असलेल्या Telegram चॅनेलमधून प्रतिमा, व्हिडिओ आणि व्हॉइस संदेश डाउनलोड करण्यास समर्थन देते, तसेच Telegram च्या मूळ डिझाइन शैलीशी जवळीक राखणारा इंटरफेस देते.
// @description:nb     Støtter nedlasting av bilder, videoer og talemeldinger fra Telegram-kanaler med nedlastingsbegrensninger, med et grensesnitt som ligger tett opptil Telegrams opprinnelige designstil.
// @description:nl     Ondersteunt het downloaden van afbeeldingen, video's en spraakberichten uit Telegram-kanalen met downloadbeperkingen, met een interface die nauw aansluit bij de native ontwerpstijl van Telegram.
// @description:pl     Obsługuje pobieranie obrazów, wideo i wiadomości głosowych z kanałów Telegram z ograniczeniami pobierania, oferując interfejs zbliżony do natywnego stylu Telegrama.
// @description:pt-BR  Suporta o download de imagens, vídeos e mensagens de voz de canais do Telegram com restrições de download, com uma interface próxima ao estilo nativo do Telegram.
// @description:ro     Permite descărcarea imaginilor, videoclipurilor și mesajelor vocale din canale Telegram cu restricții de descărcare, având o interfață apropiată de stilul nativ Telegram.
// @description:ru     Поддерживает скачивание изображений, видео и голосовых сообщений из каналов Telegram с ограничениями на загрузку, предлагая интерфейс, близкий к нативному стилю Telegram.
// @description:sk     Podporuje sťahovanie obrázkov, videí a hlasových správ z kanálov Telegram s obmedzeniami sťahovania a ponúka rozhranie, ktoré sa veľmi podobá natívnemu štýlu Telegramu.
// @description:sr     Подржава преузимање слика, видео записа и гласовних порука са Telegram канала са ограничењима преузимања, уз интерфејс који је веома близак изворном стилу дизајна Telegram-а.
// @description:sv     Stöder nedladdning av bilder, videor och röstmeddelanden från Telegram-kanaler med nedladdningsbegränsningar, med ett gränssnitt som ligger nära Telegrams ursprungliga designstil.
// @description:th     รองรับการดาวน์โหลดรูปภาพ วิดีโอ และข้อความเสียงจากช่อง Telegram ที่มีข้อจำกัดการดาวน์โหลด พร้อมอินเทอร์เฟซที่ใกล้เคียงกับดีไซน์ดั้งเดิมของ Telegram
// @description:tr     İndirme kısıtlaması olan Telegram kanallarından görsel, video ve sesli mesaj indirmeyi destekler; arayüzü Telegram’ın yerel tasarım stiline yakın şekilde sunar.
// @description:uk     Підтримує завантаження зображень, відео та голосових повідомлень із Telegram-каналів із обмеженнями на завантаження, з інтерфейсом, що наближений до нативного стилю Telegram.
// @description:ug     چۈشۈرۈش چەكلىمىسى بار Telegram قاناللىرىدىن رەسىم، ۋىدىئو ۋە ئاۋازلىق ئۇچۇرلارنى چۈشۈرۈشنى قوللايدۇ، ھەمدە Telegram نىڭ ئەسلى لايىھە ئۇسلۇبىغا يېقىن كۆرۈنمە يۈزىنى تەمىنلەيدۇ.
// @description:vi     Hỗ trợ tải xuống hình ảnh, video và tin nhắn thoại từ các kênh Telegram bị giới hạn tải xuống, với giao diện bám sát phong cách thiết kế gốc của Telegram.
// @description:zh-CN  支持从限制下载的 Telegram 频道中获取图片、视频及语音消息,界面设计与 Telegram 原生风格高度统一
// @description:zh-TW  支援從限制下載的 Telegram 頻道中下載圖片、影片與語音訊息,介面設計與 Telegram 原生風格高度一致
// @namespace Andrew-Telegram-Media-Downloader-Ultimate
// @version   1.0.1
// @author    Andrew, Nestor Qin
// @license   GNU GPLv3
// @match     https://web.telegram.org/*
// @match     https://webk.telegram.org/*
// @match     https://webz.telegram.org/*
// @icon      https://img.icons8.com/color/452/telegram-app--v5.png
// @grant     none
// ==/UserScript==
(function () {
  'use strict';

  
  /*!
  * Copyright (c) 2026 - 2026, Nestor Qin, Andrew. All rights reserved.
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
  * in the Software without restriction, including without limitation the rights
  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  * copies of the Software, and to permit persons to whom the Software is
  * furnished to do so, subject to the following conditions:
  *
  * The above copyright notice and this permission notice shall be included in
  * all copies or substantial portions of the Software.
  *
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  *
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  * SOFTWARE.
  *
  * The code is adapted from an open-source project. 
  * The code structure has been optimized and bugs have been fixed. 
  * Copyright belongs to the original author.
  * https://github.com/Neet-Nestor/Telegram-Media-Downloader
  */


  const CONFIG = {
    downloadIcon: "",
    forwardIcon: "",
    refreshDelay: 500,
    maxActiveDownloads: 2,
    contentRangeRegex: /^bytes (\d+)-(\d+)\/(\d+)$/,
    startFlag: "__TELEGRAM_MEDIA_DOWNLOADER_STARTED__",
    progressContainerId: "tel-downloader-progress-bar-container",
    progressCardPrefix: "tel-downloader-progress-"
  };

  const logger = {
    info: () => {
    },
    error: (...args) => console.error("[Tel Download]", ...args)
  };

  const utils = {
    hashCode(text) {
      let hash = 0;
      for (let i = 0; i < text.length; i += 1) {
        hash = (hash << 5) - hash + text.charCodeAt(i) | 0;
      }
      return hash >>> 0;
    },
    randomId() {
      return `${Math.random().toString(36).slice(2)}_${Date.now()}`;
    },
    triggerBlobDownload(blob, fileName) {
      const blobUrl = window.URL.createObjectURL(blob);
      const a = document.createElement("a");
      document.body.appendChild(a);
      a.href = blobUrl;
      a.download = fileName;
      a.click();
      document.body.removeChild(a);
      window.URL.revokeObjectURL(blobUrl);
    },
    parseMediaFileName(url, fallbackFileName) {
      try {
        const encoded = url.split("/").at(-1);
        const metadata = JSON.parse(decodeURIComponent(encoded));
        return metadata.fileName || fallbackFileName;
      } catch {
        return fallbackFileName;
      }
    },
    canUseFileSystemApi() {
      try {
        return "showSaveFilePicker" in unsafeWindow && unsafeWindow.self === unsafeWindow.top;
      } catch {
        return false;
      }
    }
  };

  class DownloadService {
    constructor(progress) {
      this.progress = progress;
    }
    downloadImage(url) {
      const fileName = `${Math.random().toString(36).slice(2, 10)}.jpeg`;
      const a = document.createElement("a");
      document.body.appendChild(a);
      a.href = url;
      a.download = fileName;
      a.click();
      document.body.removeChild(a);
    }
    downloadAudio(url) {
      const fileName = `${utils.hashCode(url).toString(36)}.ogg`;
      const context = { nextOffset: 0, totalSize: null, chunks: [] };
      this.downloadStream({
        url,
        mimePrefix: "audio/",
        fileName,
        fallbackType: "audio/ogg",
        context
      });
    }
    downloadVideo(url, taskId = utils.randomId()) {
      const context = {
        nextOffset: 0,
        totalSize: null,
        chunks: [],
        extension: "mp4",
        fileName: utils.parseMediaFileName(
          url,
          `${utils.hashCode(url).toString(36)}.mp4`
        )
      };
      this.progress.ensure(taskId, context.fileName);
      const onChunkMeta = ({ mime, percent }) => {
        context.extension = mime.split("/")[1] || context.extension;
        context.fileName = `${context.fileName.split(".")[0]}.${context.extension}`;
        this.progress.update(taskId, context.fileName, percent, url);
      };
      this.downloadStream({
        url,
        mimePrefix: "video/",
        fileName: context.fileName,
        fallbackType: "video/mp4",
        context,
        onChunkMeta,
        onQueueCheck: (resumeFn) => {
          const card = this.progress.getCard(taskId);
          if (card?.classList.contains("queued")) {
            card.resume = resumeFn;
            return true;
          }
          return false;
        },
        onCompleted: () => this.progress.complete(taskId),
        onFailed: () => this.progress.abort(taskId)
      });
    }
    downloadStream({
      url,
      mimePrefix,
      fileName,
      fallbackType,
      context,
      onChunkMeta,
      onQueueCheck,
      onCompleted,
      onFailed
    }) {
      const readNext = (writer) => {
        fetch(url, {
          method: "GET",
          headers: { Range: `bytes=${context.nextOffset}-` }
        }).then((res) => {
          if (![200, 206].includes(res.status)) {
            throw new Error(`Unexpected response: ${res.status}`);
          }
          const mime = (res.headers.get("Content-Type") || "").split(";")[0];
          if (!mime.startsWith(mimePrefix)) {
            throw new Error(`Unexpected MIME: ${mime}`);
          }
          const range = res.headers.get("Content-Range");
          const match = range && range.match(CONFIG.contentRangeRegex);
          if (!match)
            throw new Error("Invalid Content-Range header");
          const start = Number.parseInt(match[1], 10);
          const end = Number.parseInt(match[2], 10);
          const total = Number.parseInt(match[3], 10);
          if (start !== context.nextOffset)
            throw new Error("Chunk offset mismatch");
          if (context.totalSize && total !== context.totalSize) {
            throw new Error("File size changed");
          }
          context.nextOffset = end + 1;
          context.totalSize = total;
          const percent = Math.round(context.nextOffset * 100 / context.totalSize);
          if (typeof onChunkMeta === "function") {
            onChunkMeta({ mime, percent });
          }
          return res.blob();
        }).then((blob) => writer ? writer.write(blob) : context.chunks.push(blob)).then(() => {
          if (!context.totalSize)
            throw new Error("Missing total size");
          if (context.nextOffset < context.totalSize) {
            if (typeof onQueueCheck === "function" && onQueueCheck(() => readNext(writer))) {
              return;
            }
            readNext(writer);
            return;
          }
          const finalName = context.fileName || fileName;
          const finalType = mimePrefix === "video/" ? `video/${context.extension || "mp4"}` : fallbackType;
          if (writer) {
            writer.close();
          } else {
            utils.triggerBlobDownload(
              new Blob(context.chunks, { type: finalType }),
              finalName
            );
          }
          if (typeof onCompleted === "function")
            onCompleted();
        }).catch((error) => {
          logger.error("Download failed:", error);
          if (typeof onFailed === "function")
            onFailed();
        });
      };
      if (utils.canUseFileSystemApi()) {
        unsafeWindow.showSaveFilePicker({ suggestedName: fileName }).then((handle) => handle.createWritable()).then((writer) => readNext(writer)).catch((err) => {
          if (err?.name !== "AbortError")
            logger.error("Save picker failed:", err);
        });
        return;
      }
      readNext(null);
    }
  }

  class ProgressManager {
    constructor(onRetry) {
      this.onRetry = onRetry;
    }
    setupContainer() {
      if (this.getContainer())
        return;
      const container = document.createElement("div");
      container.id = CONFIG.progressContainerId;
      container.style.position = "fixed";
      container.style.bottom = "100px";
      container.style.top = "56px";
      container.style.overflow = "auto";
      container.style.right = "0";
      container.style.zIndex = location.pathname.startsWith("/k/") ? "4" : "1600";
      document.body.appendChild(container);
    }
    getContainer() {
      return document.getElementById(CONFIG.progressContainerId);
    }
    getCard(taskId) {
      return document.getElementById(`${CONFIG.progressCardPrefix}${taskId}`);
    }
    getActiveCount() {
      const container = this.getContainer();
      if (!container)
        return 0;
      return Array.from(
        container.querySelectorAll(
          ".tel-downloader-progress:not(.queued):not(.aborted):not(.completed)"
        )
      ).length;
    }
    ensure(taskId, fileName) {
      const container = this.getContainer();
      if (!container)
        return null;
      let card = this.getCard(taskId);
      if (!card) {
        card = this.createCard(taskId, fileName);
        container.appendChild(card);
      }
      this.setQueued(taskId, this.getActiveCount() > CONFIG.maxActiveDownloads);
      return card;
    }
    createCard(taskId, fileName) {
      const isDarkMode = document.documentElement.classList.contains("night") || document.documentElement.classList.contains("theme-dark");
      const card = document.createElement("div");
      card.id = `${CONFIG.progressCardPrefix}${taskId}`;
      card.className = "tel-downloader-progress";
      card.setAttribute("videoId", taskId);
      card.style.width = "20rem";
      card.style.marginTop = "0.4rem";
      card.style.padding = "0.6rem";
      card.style.backgroundColor = isDarkMode ? "rgba(0,0,0,0.3)" : "rgba(0,0,0,0.6)";
      const top = document.createElement("div");
      top.style.display = "flex";
      top.style.justifyContent = "space-between";
      const title = document.createElement("p");
      title.className = "filename";
      title.style.margin = "0";
      title.style.color = "white";
      title.innerText = fileName;
      const closeButton = document.createElement("div");
      closeButton.style.cursor = "pointer";
      closeButton.style.fontSize = "1.2rem";
      closeButton.style.color = isDarkMode ? "#8a8a8a" : "white";
      closeButton.style.position = "absolute";
      closeButton.style.right = "4px";
      closeButton.innerHTML = "&times;";
      closeButton.onclick = () => {
        card.remove();
        this.resumeNext();
      };
      const progressBar = document.createElement("div");
      progressBar.className = "progress";
      progressBar.style.backgroundColor = "#e2e2e2";
      progressBar.style.position = "relative";
      progressBar.style.width = "100%";
      progressBar.style.height = "1.6rem";
      progressBar.style.borderRadius = "2rem";
      progressBar.style.overflow = "hidden";
      const counter = document.createElement("p");
      counter.style.position = "absolute";
      counter.style.zIndex = "5";
      counter.style.left = "50%";
      counter.style.top = "50%";
      counter.style.transform = "translate(-50%, -50%)";
      counter.style.margin = "0";
      counter.style.color = "black";
      const progress = document.createElement("div");
      progress.style.position = "absolute";
      progress.style.height = "100%";
      progress.style.width = "0%";
      progress.style.backgroundColor = "#6093B5";
      progressBar.append(counter, progress);
      top.append(title, closeButton);
      card.append(top, progressBar);
      return card;
    }
    setQueued(taskId, queued) {
      const card = this.getCard(taskId);
      if (!card)
        return;
      card.classList.toggle("queued", queued);
      const progress = card.querySelector(".progress div");
      if (!progress)
        return;
      progress.style.backgroundColor = queued ? "lightgray" : "#6093B5";
      progress.style.width = queued ? "100%" : "0%";
    }
    update(taskId, fileName, percent, mediaUrl) {
      const card = this.getCard(taskId);
      if (!card)
        return;
      const title = card.querySelector("p.filename");
      const progressBar = card.querySelector("div.progress");
      if (!title || !progressBar)
        return;
      title.innerText = fileName;
      progressBar.querySelector("p").innerText = `${percent}%`;
      progressBar.querySelector("div").style.width = `${percent}%`;
      progressBar.setAttribute("data-tel-media-url", mediaUrl);
    }
    complete(taskId) {
      const card = this.getCard(taskId);
      if (!card)
        return;
      card.classList.add("completed");
      const text = card.querySelector(".progress p");
      const bar = card.querySelector(".progress div");
      if (text)
        text.innerText = "Completed";
      if (bar) {
        bar.style.backgroundColor = "#B6C649";
        bar.style.width = "100%";
      }
      window.setTimeout(() => card.remove(), 1e4);
      this.resumeNext();
    }
    abort(taskId) {
      const card = this.getCard(taskId);
      if (!card)
        return;
      card.classList.add("aborted");
      const progress = card.querySelector(".progress");
      if (!progress)
        return;
      const text = progress.querySelector("p");
      const bar = progress.querySelector("div");
      if (text)
        text.innerText = "Aborted";
      if (bar) {
        bar.style.backgroundColor = "#D16666";
        bar.style.width = "100%";
      }
      const retryLink = document.createElement("a");
      retryLink.innerText = "retry";
      retryLink.style.marginLeft = "5px";
      retryLink.href = "javascript:void(0);";
      retryLink.onclick = () => this.retry(taskId);
      text?.appendChild(retryLink);
      window.setTimeout(() => this.retry(taskId), 3e4);
      this.resumeNext();
    }
    retry(taskId) {
      const card = this.getCard(taskId);
      if (!card || !card.classList.contains("aborted"))
        return;
      card.classList.remove("aborted");
      const bar = card.querySelector("div.progress div");
      if (bar)
        bar.style.backgroundColor = "#6093B5";
      const url = card.querySelector("div.progress")?.getAttribute("data-tel-media-url");
      if (url)
        this.onRetry(url, taskId);
    }
    resumeNext() {
      const container = this.getContainer();
      if (!container)
        return;
      if (this.getActiveCount() >= CONFIG.maxActiveDownloads)
        return;
      const next = container.querySelector(".tel-downloader-progress.queued");
      if (!next || typeof next.resume !== "function")
        return;
      const taskId = next.getAttribute("videoId");
      this.setQueued(taskId, false);
      next.resume();
      delete next.resume;
    }
  }

  class TelegramUiMount {
    constructor(downloadService) {
      this.downloadService = downloadService;
    }
    tick() {
      this.mountWebZ();
      this.mountWebK();
    }
    mountWebZ() {
      this.mountWebZStories();
      this.mountWebZMediaViewer();
    }
    mountWebK() {
      this.mountWebKStories();
      this.mountWebKMediaViewer();
      this.mountPinnedAudio();
    }
    mountWebZStories() {
      const stories = document.getElementById("StoryViewer");
      if (!stories)
        return;
      const header = stories.querySelector(".GrsJNw3y") || stories.querySelector(".DropdownMenu")?.parentNode;
      if (!header || header.querySelector(".tel-download"))
        return;
      const btn = document.createElement("button");
      btn.className = "Button TkphaPyQ tiny translucent-white round tel-download";
      btn.innerHTML = '<i class="icon icon-download"></i>';
      btn.type = "button";
      btn.title = "Download";
      btn.ariaLabel = "Download";
      btn.onclick = () => {
        const video = stories.querySelector("video");
        const videoSrc = video?.src || video?.currentSrc || video?.querySelector("source")?.src;
        if (videoSrc) {
          this.downloadService.downloadVideo(videoSrc);
          return;
        }
        const images = Array.from(stories.querySelectorAll("img.PVZ8TOWS"));
        const imageSrc = images[images.length - 1]?.src;
        if (imageSrc)
          this.downloadService.downloadImage(imageSrc);
      };
      header.insertBefore(btn, header.querySelector("button"));
    }
    mountWebZMediaViewer() {
      const container = document.querySelector("#MediaViewer .MediaViewerSlide--active");
      const actions = document.querySelector("#MediaViewer .MediaViewerActions");
      if (!container || !actions)
        return;
      const ensureActionButton = (url, onClick) => {
        const nativeDownloadButtons = Array.from(
          actions.querySelectorAll('button[title="Download"]')
        );
        let button = actions.querySelector("button.tel-download");
        if (nativeDownloadButtons.length > 1) {
          button?.remove();
          return;
        }
        if (button) {
          if (button.getAttribute("data-tel-download-url") !== url) {
            button.setAttribute("data-tel-download-url", url);
            button.onclick = onClick;
          }
          return;
        }
        if (nativeDownloadButtons.length > 0)
          return;
        button = document.createElement("button");
        button.className = "Button smaller translucent-white round tel-download";
        button.type = "button";
        button.title = "Download";
        button.ariaLabel = "Download";
        button.innerHTML = '<i class="icon icon-download"></i>';
        button.setAttribute("data-tel-download-url", url);
        button.onclick = onClick;
        actions.prepend(button);
      };
      const videoPlayer = container.querySelector(".MediaViewerContent > .VideoPlayer");
      if (videoPlayer) {
        const videoElement = videoPlayer.querySelector("video");
        const videoUrl = videoElement?.currentSrc;
        if (!videoUrl)
          return;
        ensureActionButton(
          videoUrl,
          () => this.downloadService.downloadVideo(videoElement.currentSrc)
        );
        const controls = videoPlayer.querySelector(".VideoPlayerControls .buttons");
        if (controls && !controls.querySelector("button.tel-download")) {
          const btn = document.createElement("button");
          btn.className = "Button smaller translucent-white round tel-download";
          btn.type = "button";
          btn.title = "Download";
          btn.ariaLabel = "Download";
          btn.innerHTML = '<i class="icon icon-download"></i>';
          btn.onclick = () => this.downloadService.downloadVideo(videoElement.currentSrc);
          controls.querySelector(".spacer")?.after(btn);
        }
        return;
      }
      const image = container.querySelector(".MediaViewerContent > div > img");
      if (image?.src) {
        ensureActionButton(
          image.src,
          () => this.downloadService.downloadImage(image.src)
        );
      }
    }
    mountWebKStories() {
      const stories = document.getElementById("stories-viewer");
      if (!stories)
        return;
      const createBtn = () => {
        const btn = document.createElement("button");
        btn.className = "btn-icon rp tel-download";
        btn.innerHTML = `<span class="tgico">${CONFIG.downloadIcon}</span><div class="c-ripple"></div>`;
        btn.type = "button";
        btn.title = "Download";
        btn.ariaLabel = "Download";
        btn.onclick = () => {
          const video = stories.querySelector("video.media-video");
          const videoSrc = video?.src || video?.currentSrc || video?.querySelector("source")?.src;
          if (videoSrc) {
            this.downloadService.downloadVideo(videoSrc);
            return;
          }
          const imageSrc = stories.querySelector("img.media-photo")?.src;
          if (imageSrc)
            this.downloadService.downloadImage(imageSrc);
        };
        return btn;
      };
      const header = stories.querySelector("[class^='_ViewerStoryHeaderRight']");
      const footer = stories.querySelector("[class^='_ViewerStoryFooterRight']");
      if (header && !header.querySelector(".tel-download"))
        header.prepend(createBtn());
      if (footer && !footer.querySelector(".tel-download"))
        footer.prepend(createBtn());
    }
    mountWebKMediaViewer() {
      const mediaContainer = document.querySelector(".media-viewer-whole");
      if (!mediaContainer)
        return;
      const aspecter = mediaContainer.querySelector(
        ".media-viewer-movers .media-viewer-aspecter"
      );
      const buttons = mediaContainer.querySelector(
        ".media-viewer-topbar .media-viewer-buttons"
      );
      if (!aspecter || !buttons)
        return;
      let officialDownload = null;
      Array.from(buttons.querySelectorAll("button.btn-icon.hide")).forEach((btn) => {
        btn.classList.remove("hide");
        if (btn.textContent === CONFIG.forwardIcon)
          btn.classList.add("tgico-forward");
        if (btn.textContent === CONFIG.downloadIcon) {
          btn.classList.add("tgico-download");
          officialDownload = () => btn.click();
        }
      });
      const createKButton = (className = "btn-icon tgico-download tel-download") => {
        const btn = document.createElement("button");
        btn.className = className;
        btn.innerHTML = `<span class="tgico button-icon">${CONFIG.downloadIcon}</span>`;
        btn.type = "button";
        btn.title = "Download";
        btn.ariaLabel = "Download";
        return btn;
      };
      if (aspecter.querySelector(".ckin__player")) {
        const controls = aspecter.querySelector(
          ".default__controls.ckin__controls .bottom-controls .right-controls"
        );
        if (controls && !controls.querySelector(".tel-download")) {
          const button = createKButton("btn-icon default__button tgico-download tel-download");
          button.onclick = officialDownload || (() => this.downloadService.downloadVideo(aspecter.querySelector("video")?.src || ""));
          controls.prepend(button);
        }
        return;
      }
      if (aspecter.querySelector("video") && !buttons.querySelector("button.btn-icon.tgico-download")) {
        const button = createKButton();
        button.onclick = officialDownload || (() => this.downloadService.downloadVideo(aspecter.querySelector("video")?.src || ""));
        buttons.prepend(button);
        return;
      }
      const image = aspecter.querySelector("img.thumbnail");
      if (image?.src && !buttons.querySelector("button.btn-icon.tgico-download")) {
        const button = createKButton();
        button.onclick = officialDownload || (() => this.downloadService.downloadImage(image.src));
        buttons.prepend(button);
      }
    }
    mountPinnedAudio() {
      const pinnedAudio = document.querySelector(".pinned-audio");
      if (!pinnedAudio)
        return;
      const dataMid = pinnedAudio.getAttribute("data-mid");
      if (!dataMid)
        return;
      const button = document.querySelector("._tel_download_button_pinned_container") || document.createElement("button");
      button.className = "btn-icon tgico-download _tel_download_button_pinned_container";
      button.innerHTML = `<span class="tgico button-icon">${CONFIG.downloadIcon}</span>`;
      Array.from(document.querySelectorAll("audio-element")).forEach((voice) => {
        if (voice.getAttribute("data-mid") !== dataMid)
          return;
        const link = voice.audio?.getAttribute("src");
        if (!link)
          return;
        button.onclick = (e) => {
          e.stopPropagation();
          this.downloadService.downloadAudio(link);
        };
        if (button.getAttribute("data-mid") !== dataMid) {
          button.setAttribute("data-mid", dataMid);
          pinnedAudio.querySelector(".pinned-container-wrapper-utils")?.appendChild(button);
        }
      });
    }
  }

  class TelegramDownloaderApp {
    constructor() {
      this.downloadService = null;
      this.progress = new ProgressManager(
        (url, taskId) => this.downloadService.downloadVideo(url, taskId)
      );
      this.downloadService = new DownloadService(this.progress);
      this.ui = new TelegramUiMount(this.downloadService);
      this.timer = null;
    }
    start() {
      if (window[CONFIG.startFlag])
        return;
      window[CONFIG.startFlag] = true;
      window.onerror = (message) => {
        logger.error("UNCAUGHT ERROR:", message);
        return false;
      };
      this.progress.setupContainer();
      this.ui.tick();
      this.timer = window.setInterval(() => this.ui.tick(), CONFIG.refreshDelay);
    }
    stop() {
      if (this.timer) {
        window.clearInterval(this.timer);
        this.timer = null;
      }
    }
  }

  const bootstrap = () => {
    try {
      const app = new TelegramDownloaderApp();
      app.start();
      window.__TELEGRAM_MEDIA_DOWNLOADER_APP__ = app;
    } catch (error) {
      console.error("[Tel Download] bootstrap failed:", error);
    }
  };
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", bootstrap, { once: true });
  } else {
    bootstrap();
  }

}());