Telegram Media Downloader (Batch Support)(by AFU IT)

Download images, GIFs, videos, and voice messages from private channels + batch download selected media

当前为 2025-06-05 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Telegram Media Downloader (Batch Support)(by AFU IT)
// @name:en      Telegram Media Downloader (Enhanced Batch + Restricted)
// @version      1.1
// @description  Download images, GIFs, videos, and voice messages from private channels + batch download selected media
// @author       AFU IT
// @license      GNU GPLv3
// @website      https://github.com/Neet-Nestor/Telegram-Media-Downloader
// @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
// @namespace https://github.com/Neet-Nestor/Telegram-Media-Downloader
// ==/UserScript==

(function() {
    'use strict';

    // Enhanced Logger
    const logger = {
        info: (message, fileName = null) => {
            console.log(`[TG-Enhanced] ${fileName ? `${fileName}: ` : ""}${message}`);
        },
        error: (message, fileName = null) => {
            console.error(`[TG-Enhanced] ${fileName ? `${fileName}: ` : ""}${message}`);
        },
        warn: (message, fileName = null) => {
            console.warn(`[TG-Enhanced] ${fileName ? `${fileName}: ` : ""}${message}`);
        }
    };

    // Unicode values for icons (used in /k/ app)
    const DOWNLOAD_ICON = "\uE95A";
    const FORWARD_ICON = "\uE976";
    const contentRangeRegex = /^bytes (\d+)-(\d+)\/(\d+)$/;
    const REFRESH_DELAY = 500;

    const hashCode = (s) => {
        var h = 0, l = s.length, i = 0;
        if (l > 0) {
            while (i < l) {
                h = ((h << 5) - h + s.charCodeAt(i++)) | 0;
            }
        }
        return h >>> 0;
    };

    // Progress bar functions
    const createProgressBar = (videoId, fileName) => {
        const isDarkMode = document.querySelector("html").classList.contains("night") || 
                          document.querySelector("html").classList.contains("theme-dark");
        const container = document.getElementById("tel-downloader-progress-bar-container");
        const innerContainer = document.createElement("div");
        innerContainer.id = "tel-downloader-progress-" + videoId;
        innerContainer.style.width = "20rem";
        innerContainer.style.marginTop = "0.4rem";
        innerContainer.style.padding = "0.6rem";
        innerContainer.style.backgroundColor = isDarkMode ? "rgba(0,0,0,0.3)" : "rgba(0,0,0,0.6)";

        const flexContainer = document.createElement("div");
        flexContainer.style.display = "flex";
        flexContainer.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.innerHTML = "&times;";
        closeButton.onclick = function() {
            container.removeChild(innerContainer);
        };

        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.appendChild(counter);
        progressBar.appendChild(progress);
        flexContainer.appendChild(title);
        flexContainer.appendChild(closeButton);
        innerContainer.appendChild(flexContainer);
        innerContainer.appendChild(progressBar);
        container.appendChild(innerContainer);
    };

    const updateProgress = (videoId, fileName, progress) => {
        const innerContainer = document.getElementById("tel-downloader-progress-" + videoId);
        if (innerContainer) {
            innerContainer.querySelector("p.filename").innerText = fileName;
            const progressBar = innerContainer.querySelector("div.progress");
            progressBar.querySelector("p").innerText = progress + "%";
            progressBar.querySelector("div").style.width = progress + "%";
        }
    };

    const completeProgress = (videoId) => {
        const progressBar = document.getElementById("tel-downloader-progress-" + videoId)?.querySelector("div.progress");
        if (progressBar) {
            progressBar.querySelector("p").innerText = "Completed";
            progressBar.querySelector("div").style.backgroundColor = "#B6C649";
            progressBar.querySelector("div").style.width = "100%";
        }
    };

    const AbortProgress = (videoId) => {
        const progressBar = document.getElementById("tel-downloader-progress-" + videoId)?.querySelector("div.progress");
        if (progressBar) {
            progressBar.querySelector("p").innerText = "Aborted";
            progressBar.querySelector("div").style.backgroundColor = "#D16666";
            progressBar.querySelector("div").style.width = "100%";
        }
    };

    // Enhanced download functions
    const tel_download_video = (url) => {
        let _blobs = [];
        let _next_offset = 0;
        let _total_size = null;
        let _file_extension = "mp4";

        const videoId = (Math.random() + 1).toString(36).substring(2, 10) + "_" + Date.now().toString();
        let fileName = hashCode(url).toString(36) + "." + _file_extension;

        try {
            const metadata = JSON.parse(decodeURIComponent(url.split("/")[url.split("/").length - 1]));
            if (metadata.fileName) {
                fileName = metadata.fileName;
            }
        } catch (e) {
            // Invalid JSON string, pass extracting fileName
        }

        logger.info(`URL: ${url}`, fileName);

        const fetchNextPart = (_writable) => {
            fetch(url, {
                method: "GET",
                headers: {
                    Range: `bytes=${_next_offset}-`,
                },
                "User-Agent": "User-Agent Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0",
            })
            .then((res) => {
                if (![200, 206].includes(res.status)) {
                    throw new Error("Non 200/206 response was received: " + res.status);
                }
                const mime = res.headers.get("Content-Type").split(";")[0];
                if (!mime.startsWith("video/")) {
                    throw new Error("Get non video response with MIME type " + mime);
                }
                _file_extension = mime.split("/")[1];
                fileName = fileName.substring(0, fileName.indexOf(".") + 1) + _file_extension;

                const match = res.headers.get("Content-Range").match(contentRangeRegex);
                const startOffset = parseInt(match[1]);
                const endOffset = parseInt(match[2]);
                const totalSize = parseInt(match[3]);

                if (startOffset !== _next_offset) {
                    logger.error("Gap detected between responses.", fileName);
                    throw "Gap detected between responses.";
                }
                if (_total_size && totalSize !== _total_size) {
                    logger.error("Total size differs", fileName);
                    throw "Total size differs";
                }

                _next_offset = endOffset + 1;
                _total_size = totalSize;

                logger.info(`Progress: ${((_next_offset * 100) / _total_size).toFixed(0)}%`, fileName);
                updateProgress(videoId, fileName, ((_next_offset * 100) / _total_size).toFixed(0));
                return res.blob();
            })
            .then((resBlob) => {
                if (_writable !== null) {
                    _writable.write(resBlob).then(() => {});
                } else {
                    _blobs.push(resBlob);
                }
            })
            .then(() => {
                if (!_total_size) {
                    throw new Error("_total_size is NULL");
                }

                if (_next_offset < _total_size) {
                    fetchNextPart(_writable);
                } else {
                    if (_writable !== null) {
                        _writable.close().then(() => {
                            logger.info("Download finished", fileName);
                        });
                    } else {
                        save();
                    }
                    completeProgress(videoId);
                }
            })
            .catch((reason) => {
                logger.error(reason, fileName);
                AbortProgress(videoId);
            });
        };

        const save = () => {
            logger.info("Finish downloading blobs", fileName);
            const blob = new Blob(_blobs, { type: "video/mp4" });
            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);

            logger.info("Download triggered", fileName);
        };

        fetchNextPart(null);
        createProgressBar(videoId, fileName);
    };

    const tel_download_audio = (url) => {
        let _blobs = [];
        let _next_offset = 0;
        let _total_size = null;
        const fileName = hashCode(url).toString(36) + ".ogg";

        const fetchNextPart = (_writable) => {
            fetch(url, {
                method: "GET",
                headers: {
                    Range: `bytes=${_next_offset}-`,
                },
            })
            .then((res) => {
                if (res.status !== 206 && res.status !== 200) {
                    logger.error("Non 200/206 response was received: " + res.status, fileName);
                    return;
                }

                const mime = res.headers.get("Content-Type").split(";")[0];
                if (!mime.startsWith("audio/")) {
                    logger.error("Get non audio response with MIME type " + mime, fileName);
                    throw "Get non audio response with MIME type " + mime;
                }

                try {
                    const match = res.headers.get("Content-Range").match(contentRangeRegex);
                    const startOffset = parseInt(match[1]);
                    const endOffset = parseInt(match[2]);
                    const totalSize = parseInt(match[3]);

                    if (startOffset !== _next_offset) {
                        logger.error("Gap detected between responses.");
                        throw "Gap detected between responses.";
                    }
                    if (_total_size && totalSize !== _total_size) {
                        logger.error("Total size differs");
                        throw "Total size differs";
                    }

                    _next_offset = endOffset + 1;
                    _total_size = totalSize;
                } finally {
                    return res.blob();
                }
            })
            .then((resBlob) => {
                if (_writable !== null) {
                    _writable.write(resBlob).then(() => {});
                } else {
                    _blobs.push(resBlob);
                }
            })
            .then(() => {
                if (_next_offset < _total_size) {
                    fetchNextPart(_writable);
                } else {
                    if (_writable !== null) {
                        _writable.close().then(() => {
                            logger.info("Download finished", fileName);
                        });
                    } else {
                        save();
                    }
                }
            })
            .catch((reason) => {
                logger.error(reason, fileName);
            });
        };

        const save = () => {
            logger.info("Finish downloading blobs. Concatenating blobs and downloading...", fileName);
            let blob = new Blob(_blobs, { type: "audio/ogg" });
            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);

            logger.info("Download triggered", fileName);
        };

        fetchNextPart(null);
    };

    const tel_download_image = (imageUrl) => {
        const fileName = (Math.random() + 1).toString(36).substring(2, 10) + ".jpeg";
        const a = document.createElement("a");
        document.body.appendChild(a);
        a.href = imageUrl;
        a.download = fileName;
        a.click();
        document.body.removeChild(a);
        logger.info("Download triggered", fileName);
    };

    // BATCH DOWNLOAD FUNCTIONALITY
    const getSelectedCount = () => {
        const selectedBubbles = document.querySelectorAll('.bubble.is-selected');
        return selectedBubbles.length;
    };

    const triggerNativeDownload = () => {
        logger.info('Starting silent batch download process...');
        
        const firstSelected = document.querySelector('.bubble.is-selected');
        if (!firstSelected) {
            logger.error('No selected bubbles found');
            return;
        }

        const rightClickEvent = new MouseEvent('contextmenu', {
            bubbles: true,
            cancelable: true,
            view: window,
            button: 2,
            buttons: 2,
            clientX: 100,
            clientY: 100
        });
        
        logger.info('Triggering context menu silently...');
        firstSelected.dispatchEvent(rightClickEvent);
        
        setTimeout(() => {
            const contextMenu = document.querySelector('#bubble-contextmenu');
            if (contextMenu) {
                // Hide the context menu immediately
                contextMenu.style.display = 'none';
                contextMenu.style.visibility = 'hidden';
                contextMenu.style.opacity = '0';
                contextMenu.style.pointerEvents = 'none';
                
                logger.info('Context menu hidden, looking for Download selected...');
                
                const menuItems = contextMenu.querySelectorAll('.btn-menu-item');
                let downloadFound = false;
                
                menuItems.forEach(item => {
                    const textElement = item.querySelector('.btn-menu-item-text');
                    if (textElement && textElement.textContent.trim() === 'Download selected') {
                        logger.info('Found "Download selected" button, clicking silently...');
                        item.click();
                        downloadFound = true;
                    }
                });
                
                if (!downloadFound) {
                    logger.warn('Download selected option not found in context menu');
                }
                
                setTimeout(() => {
                    if (contextMenu) {
                        contextMenu.classList.remove('active', 'was-open');
                        contextMenu.style.display = 'none';
                    }
                }, 50);
            } else {
                logger.error('Context menu not found');
            }
        }, 50);
    };

    // Create batch download button
    const createBatchDownloadButton = () => {
        const existingBtn = document.getElementById('tg-batch-download-btn');
        if (existingBtn) {
            const count = getSelectedCount();
            const countSpan = existingBtn.querySelector('.media-count');
            if (countSpan) {
                countSpan.textContent = count > 0 ? count : '';
                countSpan.style.display = count > 0 ? 'flex' : 'none';
            }
            return;
        }

        const downloadBtn = document.createElement('button');
        downloadBtn.id = 'tg-batch-download-btn';
        downloadBtn.title = 'Download Selected Files';

        downloadBtn.innerHTML = `
            <svg width="28" height="28" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M5 20h14v-2H5v2zM12 4v12l-4-4h3V4h2v8h3l-4 4z" fill="white" stroke="white" stroke-width="0.5"/>
            </svg>
            <span class="media-count" style="
                position: absolute;
                top: -6px;
                right: -6px;
                background: #ff4757;
                color: white;
                border-radius: 11px;
                width: 22px;
                height: 22px;
                font-size: 12px;
                font-weight: bold;
                display: none;
                align-items: center;
                justify-content: center;
                box-shadow: 0 2px 6px rgba(0,0,0,0.3);
                border: 2px solid white;
            "></span>
        `;

        Object.assign(downloadBtn.style, {
            position: 'fixed',
            bottom: '20px',
            right: '20px',
            zIndex: '999999',
            background: '#8774e1',
            border: 'none',
            borderRadius: '50%',
            color: 'white',
            cursor: 'pointer',
            padding: '13px',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            width: '54px',
            height: '54px',
            boxShadow: '0 4px 16px rgba(135, 116, 225, 0.4)',
            transition: 'all 0.2s ease',
            fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
        });

        downloadBtn.addEventListener('mouseenter', () => {
            downloadBtn.style.background = '#7c6ce0';
            downloadBtn.style.transform = 'scale(1.05)';
        });

        downloadBtn.addEventListener('mouseleave', () => {
            downloadBtn.style.background = '#8774e1';
            downloadBtn.style.transform = 'scale(1)';
        });

        downloadBtn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();

            const count = getSelectedCount();
            if (count === 0) {
                alert('Please select some messages first');
                return;
            }

            logger.info(`Batch download button clicked! Starting silent download for ${count} selected items...`);
            triggerNativeDownload();
        });

        document.body.appendChild(downloadBtn);
        logger.info('Batch download button created and added to page');
    };

    // Monitor selection changes for batch download
    const monitorSelection = () => {
        const observer = new MutationObserver(() => {
            setTimeout(createBatchDownloadButton, 100);
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['class']
        });
    };

    // ORIGINAL RESTRICTED CONTENT DOWNLOAD FUNCTIONALITY
    logger.info("Initialized Enhanced Telegram Downloader");

    // For webz /a/ webapp
    setInterval(() => {
        // Stories
        const storiesContainer = document.getElementById("StoryViewer");
        if (storiesContainer) {
            const createDownloadButton = () => {
                const downloadIcon = document.createElement("i");
                downloadIcon.className = "icon icon-download";
                const downloadButton = document.createElement("button");
                downloadButton.className = "Button TkphaPyQ tiny translucent-white round tel-download";
                downloadButton.appendChild(downloadIcon);
                downloadButton.setAttribute("type", "button");
                downloadButton.setAttribute("title", "Download");
                downloadButton.setAttribute("aria-label", "Download");
                downloadButton.onclick = () => {
                    const video = storiesContainer.querySelector("video");
                    const videoSrc = video?.src || video?.currentSrc || video?.querySelector("source")?.src;
                    if (videoSrc) {
                        tel_download_video(videoSrc);
                    } else {
                        const images = storiesContainer.querySelectorAll("img.PVZ8TOWS");
                        if (images.length > 0) {
                            const imageSrc = images[images.length - 1]?.src;
                            if (imageSrc) tel_download_image(imageSrc);
                        }
                    }
                };
                return downloadButton;
            };

            const storyHeader = storiesContainer.querySelector(".GrsJNw3y") || 
                              storiesContainer.querySelector(".DropdownMenu").parentNode;
            if (storyHeader && !storyHeader.querySelector(".tel-download")) {
                storyHeader.insertBefore(createDownloadButton(), storyHeader.querySelector("button"));
            }
        }

        // Media viewer
        const mediaContainer = document.querySelector("#MediaViewer .MediaViewerSlide--active");
        const mediaViewerActions = document.querySelector("#MediaViewer .MediaViewerActions");
        if (!mediaContainer || !mediaViewerActions) return;

        const videoPlayer = mediaContainer.querySelector(".MediaViewerContent > .VideoPlayer");
        const img = mediaContainer.querySelector(".MediaViewerContent > div > img");
        
        const downloadIcon = document.createElement("i");
        downloadIcon.className = "icon icon-download";
        const downloadButton = document.createElement("button");
        downloadButton.className = "Button smaller translucent-white round tel-download";
        downloadButton.setAttribute("type", "button");
        downloadButton.setAttribute("title", "Download");
        downloadButton.setAttribute("aria-label", "Download");
        
        if (videoPlayer) {
            const videoUrl = videoPlayer.querySelector("video").currentSrc;
            downloadButton.setAttribute("data-tel-download-url", videoUrl);
            downloadButton.appendChild(downloadIcon);
            downloadButton.onclick = () => {
                tel_download_video(videoPlayer.querySelector("video").currentSrc);
            };

            const controls = videoPlayer.querySelector(".VideoPlayerControls");
            if (controls) {
                const buttons = controls.querySelector(".buttons");
                if (!buttons.querySelector("button.tel-download")) {
                    const spacer = buttons.querySelector(".spacer");
                    spacer.after(downloadButton);
                }
            }

            if (mediaViewerActions.querySelector("button.tel-download")) {
                const telDownloadButton = mediaViewerActions.querySelector("button.tel-download");
                if (mediaViewerActions.querySelectorAll('button[title="Download"]').length > 1) {
                    mediaViewerActions.querySelector("button.tel-download").remove();
                } else if (telDownloadButton.getAttribute("data-tel-download-url") !== videoUrl) {
                    telDownloadButton.onclick = () => {
                        tel_download_video(videoPlayer.querySelector("video").currentSrc);
                    };
                    telDownloadButton.setAttribute("data-tel-download-url", videoUrl);
                }
            } else if (!mediaViewerActions.querySelector('button[title="Download"]')) {
                mediaViewerActions.prepend(downloadButton);
            }
        } else if (img && img.src) {
            downloadButton.setAttribute("data-tel-download-url", img.src);
            downloadButton.appendChild(downloadIcon);
            downloadButton.onclick = () => {
                tel_download_image(img.src);
            };

            if (mediaViewerActions.querySelector("button.tel-download")) {
                const telDownloadButton = mediaViewerActions.querySelector("button.tel-download");
                if (mediaViewerActions.querySelectorAll('button[title="Download"]').length > 1) {
                    mediaViewerActions.querySelector("button.tel-download").remove();
                } else if (telDownloadButton.getAttribute("data-tel-download-url") !== img.src) {
                    telDownloadButton.onclick = () => {
                        tel_download_image(img.src);
                    };
                    telDownloadButton.setAttribute("data-tel-download-url", img.src);
                }
            } else if (!mediaViewerActions.querySelector('button[title="Download"]')) {
                mediaViewerActions.prepend(downloadButton);
            }
        }
    }, REFRESH_DELAY);

    // For webk /k/ webapp
    setInterval(() => {
        // Voice Message or Circle Video
        const pinnedAudio = document.body.querySelector(".pinned-audio");
        let dataMid;
        let downloadButtonPinnedAudio = document.body.querySelector("._tel_download_button_pinned_container") || 
                                       document.createElement("button");
        if (pinnedAudio) {
            dataMid = pinnedAudio.getAttribute("data-mid");
            downloadButtonPinnedAudio.className = "btn-icon tgico-download _tel_download_button_pinned_container";
            downloadButtonPinnedAudio.innerHTML = `<span class="tgico button-icon">${DOWNLOAD_ICON}</span>`;
        }
        
        const audioElements = document.body.querySelectorAll("audio-element");
        audioElements.forEach((audioElement) => {
            const bubble = audioElement.closest(".bubble");
            if (!bubble || bubble.querySelector("._tel_download_button_pinned_container")) {
                return;
            }
            if (dataMid && downloadButtonPinnedAudio.getAttribute("data-mid") !== dataMid && 
                audioElement.getAttribute("data-mid") === dataMid) {
                downloadButtonPinnedAudio.onclick = (e) => {
                    e.stopPropagation();
                    const link = audioElement.audio && audioElement.audio.getAttribute("src");
                    const isAudio = audioElement.audio && audioElement.audio instanceof HTMLAudioElement;
                    if (link) {
                        if (isAudio) {
                            tel_download_audio(link);
                        } else {
                            tel_download_video(link);
                        }
                    }
                };
                downloadButtonPinnedAudio.setAttribute("data-mid", dataMid);
                const link = audioElement.audio && audioElement.audio.getAttribute("src");
                if (link) {
                    pinnedAudio.querySelector(".pinned-container-wrapper-utils").appendChild(downloadButtonPinnedAudio);
                }
            }
        });

        // Stories
        const storiesContainer = document.getElementById("stories-viewer");
        if (storiesContainer) {
            const createDownloadButton = () => {
                const downloadButton = document.createElement("button");
                downloadButton.className = "btn-icon rp tel-download";
                downloadButton.innerHTML = `<span class="tgico">${DOWNLOAD_ICON}</span><div class="c-ripple"></div>`;
                downloadButton.setAttribute("type", "button");
                downloadButton.setAttribute("title", "Download");
                downloadButton.setAttribute("aria-label", "Download");
                downloadButton.onclick = () => {
                    const video = storiesContainer.querySelector("video.media-video");
                    const videoSrc = video?.src || video?.currentSrc || video?.querySelector("source")?.src;
                    if (videoSrc) {
                        tel_download_video(videoSrc);
                    } else {
                        const imageSrc = storiesContainer.querySelector("img.media-photo")?.src;
                        if (imageSrc) tel_download_image(imageSrc);
                    }
                };
                return downloadButton;
            };

            const storyHeader = storiesContainer.querySelector("[class^='_ViewerStoryHeaderRight']");
            if (storyHeader && !storyHeader.querySelector(".tel-download")) {
                storyHeader.prepend(createDownloadButton());
            }

            const storyFooter = storiesContainer.querySelector("[class^='_ViewerStoryFooterRight']");
            if (storyFooter && !storyFooter.querySelector(".tel-download")) {
                storyFooter.prepend(createDownloadButton());
            }
        }

        // Media viewer
        const mediaContainer = document.querySelector(".media-viewer-whole");
        if (!mediaContainer) return;
        
        const mediaAspecter = mediaContainer.querySelector(".media-viewer-movers .media-viewer-aspecter");
        const mediaButtons = mediaContainer.querySelector(".media-viewer-topbar .media-viewer-buttons");
        if (!mediaAspecter || !mediaButtons) return;

        // Query hidden buttons and unhide them
        const hiddenButtons = mediaButtons.querySelectorAll("button.btn-icon.hide");
        let onDownload = null;
        for (const btn of hiddenButtons) {
            btn.classList.remove("hide");
            if (btn.textContent === FORWARD_ICON) {
                btn.classList.add("tgico-forward");
            }
            if (btn.textContent === DOWNLOAD_ICON) {
                btn.classList.add("tgico-download");
                onDownload = () => {
                    btn.click();
                };
            }
        }

        if (mediaAspecter.querySelector(".ckin__player")) {
            const controls = mediaAspecter.querySelector(".default__controls.ckin__controls");
            if (controls && !controls.querySelector(".tel-download")) {
                const brControls = controls.querySelector(".bottom-controls .right-controls");
                const downloadButton = document.createElement("button");
                downloadButton.className = "btn-icon default__button tgico-download tel-download";
                downloadButton.innerHTML = `<span class="tgico">${DOWNLOAD_ICON}</span>`;
                downloadButton.setAttribute("type", "button");
                downloadButton.setAttribute("title", "Download");
                downloadButton.setAttribute("aria-label", "Download");
                if (onDownload) {
                    downloadButton.onclick = onDownload;
                } else {
                    downloadButton.onclick = () => {
                        tel_download_video(mediaAspecter.querySelector("video").src);
                    };
                }
                brControls.prepend(downloadButton);
            }
        } else if (mediaAspecter.querySelector("video") && 
                  !mediaButtons.querySelector("button.btn-icon.tgico-download")) {
            const downloadButton = document.createElement("button");
            downloadButton.className = "btn-icon tgico-download tel-download";
            downloadButton.innerHTML = `<span class="tgico button-icon">${DOWNLOAD_ICON}</span>`;
            downloadButton.setAttribute("type", "button");
            downloadButton.setAttribute("title", "Download");
            downloadButton.setAttribute("aria-label", "Download");
            if (onDownload) {
                downloadButton.onclick = onDownload;
            } else {
                downloadButton.onclick = () => {
                    tel_download_video(mediaAspecter.querySelector("video").src);
                };
            }
            mediaButtons.prepend(downloadButton);
        } else if (!mediaButtons.querySelector("button.btn-icon.tgico-download")) {
            if (!mediaAspecter.querySelector("img.thumbnail") || 
                !mediaAspecter.querySelector("img.thumbnail").src) {
                return;
            }
            const downloadButton = document.createElement("button");
            downloadButton.className = "btn-icon tgico-download tel-download";
            downloadButton.innerHTML = `<span class="tgico button-icon">${DOWNLOAD_ICON}</span>`;
            downloadButton.setAttribute("type", "button");
            downloadButton.setAttribute("title", "Download");
            downloadButton.setAttribute("aria-label", "Download");
            if (onDownload) {
                downloadButton.onclick = onDownload;
            } else {
                downloadButton.onclick = () => {
                    tel_download_image(mediaAspecter.querySelector("img.thumbnail").src);
                };
            }
            mediaButtons.prepend(downloadButton);
        }
    }, REFRESH_DELAY);

    // Progress bar container setup
    (function setupProgressBar() {
        const body = document.querySelector("body");
        const container = document.createElement("div");
        container.id = "tel-downloader-progress-bar-container";
        container.style.position = "fixed";
        container.style.bottom = 0;
        container.style.right = 0;
        if (location.pathname.startsWith("/k/")) {
            container.style.zIndex = 4;
        } else {
            container.style.zIndex = 1600;
        }
        body.appendChild(container);
    })();

    // Initialize batch download functionality
    const init = () => {
        logger.info('Initializing enhanced Telegram downloader with batch support...');
        
        createBatchDownloadButton();
        monitorSelection();
        
        setInterval(createBatchDownloadButton, 2000);
        
        logger.info('Enhanced downloader ready!');
    };

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        setTimeout(init, 1000);
    }

    logger.info("Enhanced Telegram Media Downloader setup completed.");

})();