2025最新_微信公众号媒体文件批量下载器(公众号:掌心向暖)

一键批量下载微信公众号文章中的图片、视频和音频文件,适用普通长文和小绿书

// ==UserScript==
// @name         2025最新_微信公众号媒体文件批量下载器(公众号:掌心向暖)
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  一键批量下载微信公众号文章中的图片、视频和音频文件,适用普通长文和小绿书
// @author       You
// @match        https://mp.weixin.qq.com/s/*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// ==/UserScript==

(function() {
    'use strict';

    // 等待页面加载完成
    window.addEventListener('load', function() {
        init();
    });

    function init() {
        // 创建按钮容器
        createButtons();
    }

    // 创建扫描媒体和一键下载按钮
    function createButtons() {
        // 创建按钮容器 - 固定在页面右侧
        const buttonContainer = document.createElement('div');
        buttonContainer.id = 'mediaDownloadContainer';
        buttonContainer.style.cssText = `
            position: fixed;
            top: 50%;
            right: 20px;
            transform: translateY(-50%);
            z-index: 9999;
            background: #fff;
            border: 1px solid #e0e0e0;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            padding: 15px;
            width: 150px;
            text-align: center;
        `;

        // 扫描媒体按钮
        const scanButton = document.createElement('button');
        scanButton.textContent = '扫描媒体';
        scanButton.style.cssText = `
            width: 100%;
            margin: 8px 0;
            padding: 10px 15px;
            background: #1aad19;
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.3s;
        `;
        scanButton.onclick = scanMedia;

        // 添加悬停效果
        scanButton.onmouseover = function() {
            this.style.background = '#16941a';
        };
        scanButton.onmouseout = function() {
            this.style.background = '#1aad19';
        };

        // 一键下载按钮
        const downloadButton = document.createElement('button');
        downloadButton.textContent = '一键下载';
        downloadButton.style.cssText = `
            width: 100%;
            margin: 8px 0;
            padding: 10px 15px;
            background: #576b95;
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.3s;
        `;
        downloadButton.onclick = downloadAllMedia;

        // 添加悬停效果
        downloadButton.onmouseover = function() {
            this.style.background = '#4a5a82';
        };
        downloadButton.onmouseout = function() {
            this.style.background = '#576b95';
        };

        // 进度显示区域
        const progressDiv = document.createElement('div');
        progressDiv.id = 'downloadProgress';
        progressDiv.style.cssText = `
            margin: 10px 0;
            padding: 8px;
            font-size: 12px;
            color: #666;
            background: #f8f8f8;
            border-radius: 4px;
            line-height: 1.4;
            word-wrap: break-word;
            display: none;
        `;

        buttonContainer.appendChild(scanButton);
        buttonContainer.appendChild(downloadButton);
        buttonContainer.appendChild(progressDiv);

        // 将按钮容器添加到页面
        document.body.appendChild(buttonContainer);

        console.log('媒体下载器按钮已创建在页面右侧');
    }

    // 扫描媒体文件
    function scanMedia() {
        const images = getImages();
        const videos = getVideos();
        const audios = getAudios();

        const progressDiv = document.getElementById('downloadProgress');
        progressDiv.style.display = 'block';
        progressDiv.innerHTML = `
            <div><strong>扫描结果:</strong></div>
            <div>图片:${images.length} 张</div>
            <div>视频:${videos.length} 个</div>
            <div>音频:${audios.length} 个</div>
        `;

        console.log('扫描结果:', { images, videos, audios });

        // 3秒后自动隐藏扫描结果
        setTimeout(() => {
            progressDiv.style.display = 'none';
        }, 3000);
    }

    // 获取所有图片(修改后的逻辑,兼容两种类型的文章)
    function getImages() {
        const images = [];
        const imageUrls = []; // 用于去重

        // 方式1:针对包含视频类型的文章 - 从js_content区域获取图片
        const jsContent = document.getElementById('js_content');
        if (jsContent) {
            const imgElements = jsContent.getElementsByTagName('img');

            for (let i = 0; i < imgElements.length; i++) {
                const img = imgElements[i];

                // 过滤条件
                if (img.getAttribute('data-w') === '64') continue;
                if (img.closest('.swiper_indicator_wrp_pc')) continue;

                let imgUrl = '';
                // 优先使用dataset.src,然后使用src
                if (img.dataset.src) {
                    imgUrl = img.dataset.src;
                } else if (img.src && !img.src.startsWith('data:')) {
                    imgUrl = img.src;
                    // 处理微信资源链接
                    imgUrl = imgUrl.replace("//res.wx.qq.com/mmbizwap", "http://res.wx.qq.com/mmbizwap");
                }

                // 去重检查
                if (!imgUrl || imageUrls.includes(imgUrl)) continue;
                imageUrls.push(imgUrl);

                // 根据URL判断图片格式
                let extension = '.jpg'; // 默认jpg
                if (imgUrl.indexOf('wx_fmt=gif') > 0 || imgUrl.indexOf('mmbiz_gif') > 0) {
                    extension = '.gif';
                } else if (imgUrl.indexOf('wx_fmt=png') > 0 || imgUrl.indexOf('mmbiz_png') > 0) {
                    extension = '.png';
                } else if (imgUrl.indexOf('wx_fmt=bmp') > 0 || imgUrl.indexOf('mmbiz_bmp') > 0) {
                    extension = '.bmp';
                } else if (imgUrl.indexOf('wx_fmt=webp') > 0 || imgUrl.indexOf('mmbiz_webp') > 0) {
                    extension = '.webp';
                }

                images.push({
                    url: imgUrl,
                    filename: `image_${images.length + 1}${extension}`
                });
            }

            // 如果从js_content找到了图片,直接返回
            if (images.length > 0) {
                return images;
            }
        }

        // 方式2:针对小绿书类型的文章 - 查找 .swiper_item_img 下的图片
        const swiperImages = document.querySelectorAll('.swiper_item_img img');
        if (swiperImages.length > 0) {
            swiperImages.forEach(img => {
                if (img.src && !imageUrls.includes(img.src)) {
                    imageUrls.push(img.src);

                    // 获取图片扩展名
                    let extension = getImageExtension(img.src);

                    images.push({
                        url: img.src,
                        filename: `image_${images.length + 1}.${extension}`
                    });
                }
            });

            // 如果从swiper找到了图片,直接返回
            if (images.length > 0) {
                return images;
            }
        }

        // 方式3:通用方法 - 如果以上两种方式都没找到图片
        const allImgElements = document.querySelectorAll('img');

        allImgElements.forEach((img) => {
            // 过滤条件
            if (img.getAttribute('data-w') === '64') return;
            if (img.closest('.swiper_indicator_wrp_pc')) return;
            if (!img.src || img.src.startsWith('data:')) return;

            // 去重检查
            if (imageUrls.includes(img.src)) return;
            imageUrls.push(img.src);

            // 获取图片后缀
            let extension = '.png'; // 默认png
            const srcUrl = img.src;
            if (srcUrl.includes('.jpg') || srcUrl.includes('.jpeg')) {
                extension = '.jpg';
            } else if (srcUrl.includes('.gif')) {
                extension = '.gif';
            } else if (srcUrl.includes('.webp')) {
                extension = '.webp';
            }

            images.push({
                url: srcUrl,
                filename: `image_${images.length + 1}${extension}`
            });
        });

        return images;
    }

    // 获取图片扩展名(从参考脚本中提取的函数)
    function getImageExtension(url) {
        const match = url.match(/\.([a-zA-Z0-9]+)(?:\?|$)/);
        if (match) {
            return match[1].toLowerCase();
        }

        // 检查微信图片URL中的格式参数
        if (url.includes('wx_fmt=')) {
            const formatMatch = url.match(/wx_fmt=([a-zA-Z0-9]+)/);
            if (formatMatch) {
                return formatMatch[1].toLowerCase();
            }
        }

        return 'jpg'; // 默认扩展名
    }

    // 获取所有视频
    function getVideos() {
        const videos = [];
        const videoElements = document.querySelectorAll('video');

        videoElements.forEach((video, index) => {
            let videoUrl = video.src;

            // 如果video标签没有src,查找source标签
            if (!videoUrl) {
                const source = video.querySelector('source');
                if (source) {
                    videoUrl = source.src;
                }
            }

            // 只处理微信视频链接
            if (videoUrl && videoUrl.includes('mpvideo.qpic.cn')) {
                videos.push({
                    url: videoUrl,
                    filename: `video_${index + 1}.mp4`
                });
            }
        });

        return videos;
    }

    // 获取所有音频(修改后的逻辑)
    function getAudios() {
        const audios = [];
        const audioUrls = []; // 用于去重

        // 方法1:查找具有voice_encode_fileid属性的元素
        const voiceElements = document.querySelectorAll('[voice_encode_fileid]');
        voiceElements.forEach((element, index) => {
            const voiceId = element.getAttribute('voice_encode_fileid');
            if (voiceId) {
                const audioUrl = `https://res.wx.qq.com/voice/getvoice?mediaid=${voiceId}`;

                // 去重检查
                if (!audioUrls.includes(audioUrl)) {
                    audioUrls.push(audioUrl);
                    audios.push({
                        url: audioUrl,
                        filename: `audio_${audios.length + 1}.mp3`
                    });
                }
            }
        });

        // 方法2:查找audio标签中的微信语音链接(作为备用方法)
        const audioElements = document.querySelectorAll('audio');
        audioElements.forEach((audio, index) => {
            if (audio.src && audio.src.includes('res.wx.qq.com/voice')) {
                // 去重检查
                if (!audioUrls.includes(audio.src)) {
                    audioUrls.push(audio.src);
                    audios.push({
                        url: audio.src,
                        filename: `audio_${audios.length + 1}.mp3`
                    });
                }
            }
        });

        console.log('音频识别结果:', audios);
        return audios;
    }

    // 下载单个文件
    async function downloadFile(url, filename) {
        try {
            // 尝试直接fetch
            let response = await fetch(url);

            // 如果跨域失败,尝试通过代理或其他方式
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            const blob = await response.blob();
            return { filename, blob, success: true };
        } catch (error) {
            console.error(`下载失败: ${filename}`, error);

            // 尝试使用代理方式下载
            try {
                const proxyUrl = `https://cors-anywhere.herokuapp.com/${url}`;
                const response = await fetch(proxyUrl);
                const blob = await response.blob();
                return { filename, blob, success: true };
            } catch (proxyError) {
                console.error(`代理下载也失败: ${filename}`, proxyError);
                return { filename, success: false, error: error.message };
            }
        }
    }

    // 批量下载所有媒体文件
    async function downloadAllMedia() {
        const progressDiv = document.getElementById('downloadProgress');
        progressDiv.style.display = 'block';
        progressDiv.innerHTML = '正在准备下载...';

        const images = getImages();
        const videos = getVideos();
        const audios = getAudios();

        const allFiles = [...images, ...videos, ...audios];

        if (allFiles.length === 0) {
            progressDiv.innerHTML = '未找到可下载的媒体文件';
            setTimeout(() => {
                progressDiv.style.display = 'none';
            }, 3000);
            return;
        }

        // 创建JSZip实例
        const zip = new JSZip();
        let downloadedCount = 0;
        let failedCount = 0;

        progressDiv.innerHTML = `开始下载 ${allFiles.length} 个文件...`;

        // 并发下载文件
        const downloadPromises = allFiles.map(async (file, index) => {
            const result = await downloadFile(file.url, file.filename);

            if (result.success) {
                zip.file(result.filename, result.blob);
                downloadedCount++;
            } else {
                failedCount++;
                console.error(`文件下载失败: ${file.filename}`);
            }

            // 更新进度
            progressDiv.innerHTML = `
                下载进度: ${downloadedCount + failedCount}/${allFiles.length}<br>
                成功: ${downloadedCount} 个<br>
                失败: ${failedCount} 个
            `;

            return result;
        });

        // 等待所有下载完成
        await Promise.all(downloadPromises);

        if (downloadedCount === 0) {
            progressDiv.innerHTML = '所有文件下载失败,请检查网络连接或文件权限';
            setTimeout(() => {
                progressDiv.style.display = 'none';
            }, 5000);
            return;
        }

        // 生成压缩包
        progressDiv.innerHTML = '正在生成压缩包...';

        try {
            const zipContent = await zip.generateAsync({ type: 'blob' });

            // 获取页面标题作为压缩包名称
            const title = document.title.replace(/[<>:"/\\|?*]/g, '_'); // 替换不合法的文件名字符
            const zipFilename = `${title}.zip`;

            // 下载压缩包
            const link = document.createElement('a');
            link.href = URL.createObjectURL(zipContent);
            link.download = zipFilename;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);

            progressDiv.innerHTML = `
                <div><strong>下载完成!</strong></div>
                <div>成功: ${downloadedCount} 个</div>
                ${failedCount > 0 ? `<div>失败: ${failedCount} 个</div>` : ''}
                <div>压缩包: ${zipFilename}</div>
            `;

            // 5秒后自动隐藏完成信息
            setTimeout(() => {
                progressDiv.style.display = 'none';
            }, 5000);

        } catch (error) {
            console.error('生成压缩包失败:', error);
            progressDiv.innerHTML = '生成压缩包失败,请重试';
            setTimeout(() => {
                progressDiv.style.display = 'none';
            }, 3000);
        }
    }

})();