爱发电图片批量下载器

自动滚动收集爱发电公开帖子图片/媒体链接,等加载完保存JSON并下载(仅限公开内容,遵守网站条款)

// ==UserScript==
// @name         爱发电图片批量下载器
// @namespace    http://tampermonkey.net/
// @version      6.0
// @description  自动滚动收集爱发电公开帖子图片/媒体链接,等加载完保存JSON并下载(仅限公开内容,遵守网站条款)
// @author       vifdi (by Grok4 Fast)
// @match        https://afdian.com/*
// @match        https://afdian.net/*
// @grant        GM_download
// @license      MIT
// ==/UserScript==

/**
 * 爱发电图片批量下载
 * 
 * 用途:辅助用户在爱发电网站自动滚动加载公开帖子,收集图片/音频/视频链接,保存为JSON文档,并下载到自定义文件夹。
 * 仅支持公开内容(有权限的帖子),不绕过付费墙。
 * 
 * 警告:
 * - 此脚本仅供个人学习/使用,严禁用于商业或非法目的。
 * - 使用前确保遵守爱发电服务条款(https://afdian.net/terms),勿滥用导致账号封禁。
 * - 作者不承担任何法律/道德责任。
 * 
 * MIT License: https://opensource.org/licenses/MIT
 */

(function() {
    'use strict';
    
    // 创建浮动按钮
    let floatButton = document.createElement("button");
    floatButton.textContent = "启动自动收集";
    floatButton.style.cssText = `
        position: fixed; bottom: 20px; right: 20px; z-index: 9999;
        padding: 10px 20px; font-size: 1rem; color: #fff; background: #007bff;
        border: none; border-radius: 5px; cursor: pointer;
    `;
    document.body.appendChild(floatButton);

    let autoScrolling = false;
    let lastXHRTime = Date.now();
    let allLinks = []; // 全收集:[{title, pics: [{url, filename}]}]
    let uniquePosts = new Set(); // 去重post_id
    let totalPics = 0;
    let hasMore = true;
    let scrollCheck = 0;
    let rootFolder = ''; // 自定义根文件夹

    floatButton.addEventListener('click', function() {
        if (!autoScrolling) {
            // 弹出输入框自定义根文件夹
            rootFolder = prompt("输入根文件夹名 (默认: afdian_public_dump):", "afdian_public_dump") || "afdian_public_dump";
            rootFolder = rootFolder.replace(/[/\\:*?"<>|]/g, '_'); // 转义非法字符

            this.textContent = "停止收集";
            this.style.background = "#28a745"; // 绿色运行中
            autoScrolling = true;
            allLinks = [];
            uniquePosts.clear();
            totalPics = 0;
            hasMore = true;
            scrollCheck = 0;
            autoScroll();
        } else {
            this.textContent = "启动自动收集";
            this.style.background = "#007bff";
            autoScrolling = false;
        }
    });

    function autoScroll() {
        if (autoScrolling) {
            const prevHeight = document.body.scrollHeight;
            // 渐进滚动:模拟用户行为,避免检测
            window.scrollTo(0, document.body.scrollHeight);
            setTimeout(() => {
                if (document.body.scrollHeight === prevHeight && Date.now() - lastXHRTime > 4000 && !hasMore) {
                    scrollCheck++;
                    if (scrollCheck >= 3) { // 稳定3次=全加载
                        console.log("全公开帖子加载完成,开始保存JSON+下载");
                        saveAndDownloadAll();
                        autoScrolling = false;
                        floatButton.textContent = "收集完成";
                        return;
                    }
                } else {
                    scrollCheck = 0;
                }
                setTimeout(autoScroll, 1500); // 1.5s间隔,平衡速度与安全
            }, 800);
        }
    }

    // 双重劫持XHR+Fetch
    function detectRequests() {
        // XHR
        const originalXHROpen = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function(method, url) {
            this.addEventListener('load', function() {
                if (this.responseURL && this.responseURL.includes("/api/post/get-list")) {
                    lastXHRTime = Date.now();
                    try {
                        const responseData = JSON.parse(this.responseText);
                        hasMore = responseData.data?.has_more || false;
                        handleResponseData(responseData);
                    } catch (e) {
                        console.error("XHR解析失败: ", e);
                    }
                }
            });
            return originalXHROpen.apply(this, arguments);
        };

        // Fetch
        const originalFetch = window.fetch;
        window.fetch = function(...args) {
            return originalFetch.apply(this, args).then(response => {
                const url = args[0];
                if (url && url.includes("/api/post/get-list")) {
                    lastXHRTime = Date.now();
                    return response.clone().text().then(text => {
                        try {
                            const responseData = JSON.parse(text);
                            hasMore = responseData.data?.has_more || false;
                            handleResponseData(responseData);
                        } catch (e) {
                            console.error("Fetch解析失败: ", e);
                        }
                        return response;
                    });
                }
                return response;
            });
        };
    }

    function handleResponseData(data) {
        const list = data.data?.list || [];
        list.forEach((item) => {
            const postId = item.post_id;
            // 开源合法:只处理有权限的公开帖
            if (item.has_right_errMsg !== null || uniquePosts.has(postId)) return;
            uniquePosts.add(postId);

            // 无title用content作为文件夹名(截取50字符)
            let title = item.title || (item.content ? item.content.substring(0, 50) : `post_${postId}`);
            title = title.replace(/[/\\:*?"<>|]/g, '_');

            const pics = item.pics || [];
            console.log(`收集公开帖子: ${title}, ${pics.length} 张图片`);

            allLinks.push({
                title,
                pics: pics.map((url, idx) => {
                    const ext = url.split('.').pop().split('?')[0] || 'jpg';
                    return { url, filename: `${title}_${idx + 1}.${ext}` };
                })
            });
            totalPics += pics.length;

            // 支持audio/video
            if (item.audio) allLinks[allLinks.length - 1].audio = { url: item.audio, filename: `${title}_audio.mp3` };
            if (item.video) allLinks[allLinks.length - 1].video = { url: item.video, filename: `${title}_video.mp4` };
        });
        floatButton.textContent = `收集中: ${totalPics} 张 (剩余: ${hasMore ? '有' : '无'})`;
    }

    function saveAndDownloadAll() {
        try {
            // 保存JSON到根
            const jsonData = JSON.stringify(allLinks, null, 2);
            const blob = new Blob([jsonData], { type: 'application/json' });
            const blobUrl = URL.createObjectURL(blob);
            GM_download({
                url: blobUrl,
                name: `${rootFolder}/afdian_public_links.json`,
                saveAs: false
            });
            console.log(`JSON文档保存完成 (根: ${rootFolder})`);

            // 批量下载到根/子文件夹
            allLinks.forEach(post => {
                const subFolder = `${rootFolder}/${post.title}`;
                post.pics?.forEach(pic => {
                    GM_download({
                        url: pic.url,
                        name: `${subFolder}/${pic.filename}`,
                        saveAs: false
                    });
                });
                if (post.audio) {
                    GM_download({
                        url: post.audio.url,
                        name: `${subFolder}/${post.audio.filename}`,
                        saveAs: false
                    });
                }
                if (post.video) {
                    GM_download({
                        url: post.video.url,
                        name: `${subFolder}/${post.video.filename}`,
                        saveAs: false
                    });
                }
            });
            console.log(`批量下载启动: ${totalPics} 张图片 + 其他 (仅公开内容)`);
        } catch (e) {
            console.error("下载过程出错: ", e);
            alert("下载出错,请检查控制台");
        }
    }

    detectRequests();
    console.log("爱发电图片批量下载 加载完成 - 仅公开内容,遵守条款");
})();