您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Download images, GIFs, videos, and voice messages from private channels + batch download selected media
// ==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 // ==/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 = ` <div style="display: flex; align-items: flex-start; gap: 12px;"> <div style=" width: 20px; height: 20px; border-radius: 50%; background: var(--color-primary, #8774e1); display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 1px; "> <span style="color: white; font-size: 12px; font-weight: bold;">!</span> </div> <div style="flex: 1;"> <div style="font-weight: 500; margin-bottom: 4px;">Downloading Media</div> <div style="opacity: 0.7; font-size: 13px; line-height: 1.4;">Please stay on this page while the download is in progress.</div> </div> <button onclick="this.closest('#tg-stay-warning').remove()" style=" background: none; border: none; color: ${isDarkMode ? 'var(--color-text-secondary, #aaaaaa)' : 'var(--color-text-secondary, #707579)'}; cursor: pointer; font-size: 18px; padding: 0; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: background-color 0.15s ease; flex-shrink: 0; " onmouseover="this.style.backgroundColor='${isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)'}'" onmouseout="this.style.backgroundColor='transparent'">×</button> </div> `; const style = document.createElement('style'); style.textContent = ` @keyframes slideDown { from { transform: translateX(-50%) translateY(-10px); opacity: 0; scale: 0.95; } to { transform: translateX(-50%) translateY(0); opacity: 1; scale: 1; } } `; document.head.appendChild(style); document.body.appendChild(warning); setTimeout(() => { if (warning.parentNode) { warning.style.animation = 'slideDown 0.3s ease reverse'; setTimeout(() => warning.remove(), 300); } }, 8000); }; // Silent batch download const silentBatchDownload = async () => { logger.info('Starting silent batch download...'); showStayOnPageWarning(); const nativeSuccess = await tryNativeDownload(); if (!nativeSuccess) { updateBatchProgress(0, 1, 'Analyzing selected media...'); const mediaUrls = await getSelectedMediaUrls(); if (mediaUrls.length === 0) { logger.warn('No media URLs found in selected messages'); return; } logger.info(`Downloading ${mediaUrls.length} media items silently...`); for (let i = 0; i < mediaUrls.length; i++) { const media = mediaUrls[i]; try { updateBatchProgress(i, mediaUrls.length, `Downloading ${media.type} ${i + 1}/${mediaUrls.length}...`); if (media.type === 'image') { tel_download_image(media.url); } else if (media.type === 'video') { await tel_download_video(media.url); } await new Promise(resolve => setTimeout(resolve, 500)); } catch (error) { logger.error(`Failed to download ${media.type}: ${error.message}`); } } updateBatchProgress(mediaUrls.length, mediaUrls.length, `Completed: ${mediaUrls.length} files downloaded`); logger.info('Silent batch download completed'); } }; // Try native Telegram download const tryNativeDownload = () => { return new Promise((resolve) => { const firstSelected = document.querySelector('.bubble.is-selected'); if (!firstSelected) { resolve(false); return; } const rightClickEvent = new MouseEvent('contextmenu', { bubbles: true, cancelable: true, view: window, button: 2, buttons: 2, clientX: 100, clientY: 100 }); firstSelected.dispatchEvent(rightClickEvent); setTimeout(() => { const contextMenu = document.querySelector('#bubble-contextmenu'); if (contextMenu) { contextMenu.style.display = 'none'; contextMenu.style.visibility = 'hidden'; contextMenu.style.opacity = '0'; contextMenu.style.pointerEvents = 'none'; 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('Using native download...'); item.click(); downloadFound = true; } }); setTimeout(() => { if (contextMenu) { contextMenu.classList.remove('active', 'was-open'); contextMenu.style.display = 'none'; } }, 50); resolve(downloadFound); } else { resolve(false); } }, 50); }); }; // Create download button const createBatchDownloadButton = () => { const existingBtn = document.getElementById('tg-batch-download-btn'); if (existingBtn) { // Use message count instead of individual media count const count = getSelectedMessageCount(); 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 Silently'; downloadBtn.innerHTML = ` <svg class="download-icon" 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> <svg class="loading-icon" width="28" height="28" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: none;"> <circle cx="12" cy="12" r="10" stroke="white" stroke-width="2" fill="none" stroke-linecap="round" stroke-dasharray="31.416" stroke-dashoffset="31.416"> <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/> <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/> </circle> </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', () => { if (!downloadBtn.disabled) { downloadBtn.style.background = '#7c6ce0'; downloadBtn.style.transform = 'scale(1.05)'; } }); downloadBtn.addEventListener('mouseleave', () => { if (!downloadBtn.disabled) { downloadBtn.style.background = '#8774e1'; downloadBtn.style.transform = 'scale(1)'; } }); downloadBtn.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); const count = getSelectedMessageCount(); if (count === 0) { alert('Please select some messages first'); return; } downloadBtn.disabled = true; downloadBtn.style.cursor = 'wait'; downloadBtn.querySelector('.download-icon').style.display = 'none'; downloadBtn.querySelector('.loading-icon').style.display = 'block'; downloadBtn.title = 'Downloading... Please stay on this page'; logger.info(`Silent batch download started for ${count} selected messages...`); try { await silentBatchDownload(); } catch (error) { logger.error('Batch download failed:', error); } downloadBtn.disabled = false; downloadBtn.style.cursor = 'pointer'; downloadBtn.querySelector('.download-icon').style.display = 'block'; downloadBtn.querySelector('.loading-icon').style.display = 'none'; downloadBtn.title = 'Download Selected Files Silently'; }); document.body.appendChild(downloadBtn); logger.info('Silent batch download button created'); }; // Monitor selection changes const monitorSelection = () => { const observer = new MutationObserver(() => { setTimeout(createBatchDownloadButton, 100); }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }); }; // Initialize const init = () => { logger.info('Initializing silent Telegram downloader...'); createBatchProgress(); createBatchDownloadButton(); monitorSelection(); preventMediaViewerOpen(); setInterval(createBatchDownloadButton, 2000); logger.info('Silent downloader ready!'); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { setTimeout(init, 1000); } logger.info("Silent Telegram Media Downloader initialized."); })();