// ==UserScript== // @name Telegram Media Downloader (Batch Support) (by AFU IT) v1.2 // @name:en Telegram Media Downloader (Batch Support) (by AFU IT) v1.2 // @version 1.2 // @description Download images, GIFs, videos, and voice messages from private channels + batch download selected media // @author AFU IT // @license GNU GPLv3 // @telegram https://t.me/afuituserscript // @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 https://update.greasyfork.icu/scripts/531736/Telegram%20Media%20Downloader%20%28Batch%20Support%29%20%28by%20AFU%20IT%29%20v12.user.js // @updateURL https://update.greasyfork.icu/scripts/531736/Telegram%20Media%20Downloader%20%28Batch%20Support%29%20%28by%20AFU%20IT%29%20v12.meta.js // ==/UserScript== (function() { 'use strict'; // Enhanced Logger const logger = { info: (message, fileName = null) => { console.log(`[TG-Silent] ${fileName ? `${fileName}: ` : ""}${message}`); }, error: (message, fileName = null) => { console.error(`[TG-Silent] ${fileName ? `${fileName}: ` : ""}${message}`); }, warn: (message, fileName = null) => { console.warn(`[TG-Silent] ${fileName ? `${fileName}: ` : ""}${message}`); } }; 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 tracking let batchProgress = { current: 0, total: 0, container: null }; // Create batch progress bar const createBatchProgress = () => { if (document.getElementById('tg-batch-progress')) return; const progressContainer = document.createElement('div'); progressContainer.id = 'tg-batch-progress'; progressContainer.style.cssText = ` position: fixed; bottom: 100px; right: 20px; width: 280px; background: rgba(0,0,0,0.9); color: white; padding: 12px 16px; border-radius: 12px; z-index: 999998; display: none; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; box-shadow: 0 4px 20px rgba(0,0,0,0.3); `; const progressText = document.createElement('div'); progressText.id = 'tg-batch-progress-text'; progressText.style.cssText = ` margin-bottom: 8px; font-size: 13px; font-weight: 500; `; const progressBarBg = document.createElement('div'); progressBarBg.style.cssText = ` width: 100%; height: 4px; background: rgba(255,255,255,0.2); border-radius: 2px; overflow: hidden; `; const progressBarFill = document.createElement('div'); progressBarFill.id = 'tg-batch-progress-fill'; progressBarFill.style.cssText = ` height: 100%; background: #8774e1; width: 0%; transition: width 0.3s ease; border-radius: 2px; `; progressBarBg.appendChild(progressBarFill); progressContainer.appendChild(progressText); progressContainer.appendChild(progressBarBg); document.body.appendChild(progressContainer); batchProgress.container = progressContainer; }; // Update batch progress const updateBatchProgress = (current, total, text) => { const progressText = document.getElementById('tg-batch-progress-text'); const progressFill = document.getElementById('tg-batch-progress-fill'); const container = batchProgress.container; if (progressText && progressFill && container) { progressText.textContent = text || `Processing ${current}/${total}...`; const percent = total > 0 ? (current / total) * 100 : 0; progressFill.style.width = `${percent}%`; container.style.display = 'block'; if (current >= total && total > 0) { setTimeout(() => { container.style.display = 'none'; }, 3000); } } }; // Silent download functions 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("Image download triggered", fileName); }; const tel_download_video = (url) => { return new Promise((resolve, reject) => { fetch(url) .then(response => response.blob()) .then(blob => { const fileName = (Math.random() + 1).toString(36).substring(2, 10) + ".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("Video download triggered", fileName); resolve(); }) .catch(error => { logger.error("Video download failed", error); reject(error); }); }); }; // Prevent media viewer from opening const preventMediaViewerOpen = () => { document.addEventListener('click', (e) => { const target = e.target; if (window.isDownloadingBatch && (target.closest('.album-item') || target.closest('.media-container'))) { const albumItem = target.closest('.album-item'); if (albumItem && albumItem.querySelector('.video-time')) { logger.info('Preventing video popup during batch download'); e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); return false; } } }, true); }; // Function to construct video URL from data-mid const constructVideoUrl = (dataMid, peerId) => { const patterns = [ `stream/${encodeURIComponent(JSON.stringify({ dcId: 5, location: { _: "inputDocumentFileLocation", id: dataMid, access_hash: "0", file_reference: [] }, mimeType: "video/mp4", fileName: `video_${dataMid}.mp4` }))}`, `stream/${dataMid}`, `video/${dataMid}`, `media/${dataMid}` ]; return patterns[0]; }; // Function to get video URL without opening media viewer const getVideoUrlSilently = async (albumItem, dataMid) => { logger.info(`Getting video URL silently for data-mid: ${dataMid}`); const existingVideo = document.querySelector(`video[src*="${dataMid}"], video[data-mid="${dataMid}"]`); if (existingVideo && (existingVideo.src || existingVideo.currentSrc)) { const videoUrl = existingVideo.src || existingVideo.currentSrc; logger.info(`Found existing video URL: ${videoUrl}`); return videoUrl; } const peerId = albumItem.getAttribute('data-peer-id'); const constructedUrl = constructVideoUrl(dataMid, peerId); logger.info(`Constructed video URL: ${constructedUrl}`); try { const response = await fetch(constructedUrl, { method: 'HEAD' }); if (response.ok) { logger.info('Constructed URL is valid'); return constructedUrl; } } catch (error) { logger.warn('Constructed URL test failed, will try alternative method'); } return new Promise((resolve) => { logger.info('Trying silent click method...'); window.isDownloadingBatch = true; const mediaViewers = document.querySelectorAll('.media-viewer-whole, .media-viewer'); mediaViewers.forEach(viewer => { viewer.style.display = 'none'; viewer.style.visibility = 'hidden'; viewer.style.pointerEvents = 'none'; }); const clickEvent = new MouseEvent('click', { bubbles: false, cancelable: true, view: window }); albumItem.dispatchEvent(clickEvent); setTimeout(() => { const video = document.querySelector('video'); if (video && (video.src || video.currentSrc)) { const videoUrl = video.src || video.currentSrc; logger.info(`Found video URL via silent click: ${videoUrl}`); const mediaViewer = document.querySelector('.media-viewer-whole'); if (mediaViewer) { mediaViewer.style.display = 'none'; mediaViewer.style.visibility = 'hidden'; mediaViewer.style.opacity = '0'; mediaViewer.style.pointerEvents = 'none'; const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true }); document.dispatchEvent(escapeEvent); } window.isDownloadingBatch = false; resolve(videoUrl); } else { logger.warn('Could not get video URL, using fallback'); window.isDownloadingBatch = false; resolve(constructedUrl); } }, 100); }); }; // Get count of selected messages (not individual media items) const getSelectedMessageCount = () => { const selectedBubbles = document.querySelectorAll('.bubble.is-selected'); return selectedBubbles.length; }; // Get all media URLs from selected bubbles const getSelectedMediaUrls = async () => { const mediaUrls = []; const selectedBubbles = document.querySelectorAll('.bubble.is-selected'); let processedCount = 0; const totalBubbles = selectedBubbles.length; window.isDownloadingBatch = true; for (const bubble of selectedBubbles) { logger.info('Processing bubble:', bubble.className); const albumItems = bubble.querySelectorAll('.album-item.is-selected'); if (albumItems.length > 0) { logger.info(`Found album with ${albumItems.length} items`); for (let index = 0; index < albumItems.length; index++) { const albumItem = albumItems[index]; const dataMid = albumItem.getAttribute('data-mid'); updateBatchProgress(processedCount, totalBubbles * 2, `Analyzing album item ${index + 1}...`); const videoTime = albumItem.querySelector('.video-time'); const playButton = albumItem.querySelector('.btn-circle.video-play'); const isVideo = videoTime && playButton; const mediaPhoto = albumItem.querySelector('.media-photo'); if (isVideo) { logger.info(`Album item ${index + 1} is a VIDEO (duration: "${videoTime.textContent}")`); const videoUrl = await getVideoUrlSilently(albumItem, dataMid); if (videoUrl) { mediaUrls.push({ type: 'video', url: videoUrl, dataMid: dataMid }); } } else if (mediaPhoto && mediaPhoto.src && !mediaPhoto.src.includes('data:')) { logger.info(`Album item ${index + 1} is an IMAGE`); mediaUrls.push({ type: 'image', url: mediaPhoto.src, dataMid: dataMid }); } await new Promise(resolve => setTimeout(resolve, 50)); } } else { updateBatchProgress(processedCount, totalBubbles, `Processing single media...`); const videos = bubble.querySelectorAll('.media-video, video'); let hasVideo = false; videos.forEach(video => { const videoSrc = video.src || video.currentSrc; if (videoSrc && !videoSrc.includes('data:')) { mediaUrls.push({ type: 'video', url: videoSrc }); hasVideo = true; logger.info('Found single video:', videoSrc); } }); if (!hasVideo) { const images = bubble.querySelectorAll('.media-photo'); images.forEach(img => { const isVideoThumbnail = img.closest('.media-video') || img.closest('video') || bubble.querySelector('.video-time') || bubble.querySelector('.btn-circle.video-play'); if (!isVideoThumbnail && img.src && !img.src.includes('data:')) { mediaUrls.push({ type: 'image', url: img.src }); logger.info('Found single image:', img.src); } }); } } processedCount++; } window.isDownloadingBatch = false; logger.info(`Total media found: ${mediaUrls.length}`); return mediaUrls; }; // Show Telegram-style stay on page warning const showStayOnPageWarning = () => { const existingWarning = document.getElementById('tg-stay-warning'); if (existingWarning) return; // Check if dark mode is enabled const isDarkMode = document.querySelector("html").classList.contains("night") || document.querySelector("html").classList.contains("theme-dark") || document.body.classList.contains("night") || document.body.classList.contains("theme-dark"); const warning = document.createElement('div'); warning.id = 'tg-stay-warning'; warning.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: ${isDarkMode ? 'var(--color-background-secondary, #212121)' : 'var(--color-background-secondary, #ffffff)'}; color: ${isDarkMode ? 'var(--color-text, #ffffff)' : 'var(--color-text, #000000)'}; padding: 16px 20px; border-radius: 12px; z-index: 999999; font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif); font-size: 14px; font-weight: 400; box-shadow: 0 4px 16px rgba(0, 0, 0, ${isDarkMode ? '0.4' : '0.15'}); border: 1px solid ${isDarkMode ? 'var(--color-borders, #3e3e3e)' : 'var(--color-borders, #e4e4e4)'}; max-width: 320px; animation: slideDown 0.3s ease; `; warning.innerHTML = `