// ==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 // @downloadURL none // ==/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 = "×"; 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 = ` `; 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 = `${DOWNLOAD_ICON}
`; 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 = ` `; } 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 = `${DOWNLOAD_ICON}`; 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 = ` `; 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 = ` `; 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."); })();