Greasy Fork is available in English.

【最新可用】小红书无水印下载图片视频笔记

小红书无水印下载图片视频笔记

// ==UserScript==
// @name         【最新可用】小红书无水印下载图片视频笔记
// @namespace    teacher_dog
// @version      0.1.4
// @description  小红书无水印下载图片视频笔记
// @author       teacherDog
// @match        https://www.xiaohongshu.com/*
// @icon         https://vitejs.dev/logo.svg
// @require      https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.11/clipboard.min.js
// @grant             unsafeWindow
// @grant             GM_xmlhttpRequest
// @grant             GM_setClipboard
// @grant             GM_setValue
// @grant             GM_getValue
// @grant             GM_deleteValue
// @grant             GM_openInTab
// @grant             GM_registerMenuCommand
// @grant             GM_unregisterMenuCommand
// @grant             GM.getValue
// @grant             GM.setValue
// @grant             GM_info
// @grant             GM_notification
// @grant             GM_getResourceText
// @grant             GM_openInTab
// @grant             GM_addStyle
// @grant             GM_download
// @license           Apache
// ==/UserScript==

(function() {
    //'use strict';

     //**    config     **//
    const debug = false;
    const log = {
        info(...args){
            console.log(...args);
        },
        debug(...args){
            if (!debug) {
                return;
            }
            console.log(...args);
        },
    }


    /**
     * 使用方法:
     * 点击一篇笔记,鼠标移到图片左上角 “这里下载”
     */

    var document = window.document;
    let getPageDatadiv = document.createElement('div');
    getPageDatadiv.setAttribute('onclick', 'return window;');


    function getPageData() {
        let rootWindow = getPageDatadiv.onclick();
        return rootWindow ? rootWindow.__INITIAL_STATE__ : null;
    }


    var selectedImg = [];

    var dialog = document.createElement("dialog");
    dialog.setAttribute('id', 'wsyImgsBox');
    dialog.setAttribute('style', "width:80%;height:80%;padding: 0;border: none;box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);border-radius: 5px;overflow: hidden;background: white;display:flex;flex-direction: column;");

    var dialogTitle = document.createElement("div");
    dialogTitle.setAttribute('style', "box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);font-size:22px;font-weight: bold;padding:6px 12px;");
    dialogTitle.innerText = "标题";
    dialog.appendChild(dialogTitle);

    var dialogMain = document.createElement("div");
    dialogMain.innerText = "内容";
    dialogMain.setAttribute('style', "width: 100%;flex: 1;padding:6px 12px;overflow-y:auto;display: flex;flex-direction: row;justify-content: space-evenly;flex-wrap: wrap;");
    dialog.appendChild(dialogMain);

    var dialogFooter = document.createElement("div");
    dialogFooter.setAttribute('style', "padding:6px 12px;display: flex;flex-direction: row;justify-content: center;");
    var dialogFooterCancelBt = document.createElement("div");
    dialogFooterCancelBt.innerText = "关闭";
    dialogFooterCancelBt.setAttribute('style', "color: #333;font-size: 16px;padding: 10px 20px;background-color: #f0f0f0;border: none;border-radius: 5px;cursor: pointer;margin: 0 12px;");
    dialogFooterCancelBt.addEventListener("click", function () {
        dialog.close();
    });
    var dialogFooterCancelOk = document.createElement("div");
    dialogFooterCancelOk.innerText = "下载";
    dialogFooterCancelOk.setAttribute('style', "color: #ffffff;font-size: 16px;padding: 10px 20px;background-color: #409EFF;border: none;border-radius: 5px;cursor: pointer;margin: 0 12px;");
    dialogFooterCancelOk.addEventListener("click", function () {
        if (!selectedImg || selectedImg.length === 0) {
            alert("请先选中下载项");
            return;
        }
        for (let i = 0; i < selectedImg.length; i++) {
            let item = selectedImg[i];
            let imgUrl = item.url;
            let name = item.name;
            let imgEl = item.imgEl;
            let type = item.type;

            if (type === 'video') {
                let fileName = name + '.mp4';
                let rootWindow = getPageDatadiv.onclick();
                // 创建 a 标签
                const a = rootWindow.document.createElement('a');
                // 设置下载文件的 URL
                //a.href = imgUrl; // 替换为你的文件 URL
                a.href = imgUrl;
                a.target = '_blank';
                // 设置下载文件的名称
                a.download = fileName; // 替换为你的文件名称

                // 将 a 标签添加到文档中
                rootWindow.document.body.appendChild(a);

                // 触发点击事件
                a.click();

                // 移除 a 标签
                rootWindow.document.body.removeChild(a);
                continue;
            }


            fetch(imgUrl).then(async res=>{
                if (!res.ok) {
                    throw new Error('Network response was not ok');
                }
                //let type = res.headers.get("Content-Type");
                let blob = await res.blob();
                let type = blob.type;
                log.debug('blob type', type);
                if (type === "image/webp") {
                    const url = URL.createObjectURL(blob);
                    const img = new Image();
                    img.onload = function() {
                        const canvas = document.createElement('canvas');
                        const ctx = canvas.getContext('2d');
                        canvas.width = img.width;
                        canvas.height = img.height;
                        ctx.drawImage(img, 0, 0);
                        canvas.toBlob(function(transBlob) {
                            triggerDownload(transBlob, name + '.jpg');
                        }, 'image/jpeg');
                        URL.revokeObjectURL(url);
                    };
                    img.src = url;
                } else if (type === "image/png") {
                    triggerDownload(blob, name + '.png');
                } else if (type === "image/jpg" || type === "image/jpeg") {
                    triggerDownload(blob, name + '.jpg');
                } else if (type === "image/gif") {
                    triggerDownload(blob, name + '.gif');
                } else if (type === "image/gif") {
                    triggerDownload(blob, name + '.gif');
                }
                else {
                    triggerDownload(blob, name + '.jpg');
                }

            }).catch(e=>{
                console.error("下载错误", e);
            });
        }

    });
    dialogFooter.appendChild(dialogFooterCancelOk);
    dialogFooter.appendChild(dialogFooterCancelBt);
    dialog.appendChild(dialogFooter);

    document.getElementsByTagName("body")[0].appendChild(dialog);

    var control = document.createElement("div");
    control.innerText = "下载笔记图片";
    control.setAttribute('style', 'position:fixed; top:60px; left:24px; padding:6px 12px; background-color:#409EFF;color:#ffffff;z-index:99999999;border-radius: 12px;cursor: pointer;');
    // 初始化变量
    // 初始化变量
    var posX = 0, posY = 0, posInitX = 0, posInitY = 0;
    var isActive = false;

    // 拖动开始事件
    control.onmousedown = function(e) {
        e.preventDefault(); // 阻止默认事件
        e.stopPropagation(); // 阻止事件冒泡


        //console.log("指针点下", isActive);
        // 获取鼠标点击的初始位置
        posInitX = e.clientX;
        posInitY = e.clientY;

        // 添加事件监听器以处理拖动和释放
        document.onmousemove = dragMouseMove;
        document.onmouseup = dragMouseUp;
    };

    // 拖动事件
    function dragMouseMove(e) {
        //console.log("控件拖动", isActive);
        isActive = true; // 激活拖动状态
        if (isActive) {
            e.preventDefault(); // 阻止默认事件
            // 计算新位置
            posX = posInitX - e.clientX;
            posY = posInitY - e.clientY;

            // 设置div新位置
            control.style.top = (control.offsetTop - posY) + "px";
            control.style.left = (control.offsetLeft - posX) + "px";

            // 更新初始位置
            posInitX = e.clientX;
            posInitY = e.clientY;
        }
    }

    // 停止拖动事件
    function dragMouseUp() {
        //console.log("停止拖动", isActive)
        if (isActive) {
            isActive = false; // 停止拖动状态
            document.onmousemove = null; // 移除mousemove事件监听器
            document.onmouseup = null; // 移除mouseup事件监听器
        } else {
            isActive = false;
            // 处理点击事件
            showImgsBox();
            document.onmousemove = null; // 移除mousemove事件监听器
            document.onmouseup = null; // 移除mouseup事件监听器
        }
    }



    function showImgsBox(){
        if (isActive) {
            return;
        }
        let pageData = getPageData();
        if (!pageData) {
            alert("请先点击一篇笔记呀!");
            return;
        }
        let noteData = JSON.parse(JSON.stringify(pageData.note));
        let currentNoteId = noteData.currentNoteId._value;
        //console.log("currentNoteId", currentNoteId);
        if (!currentNoteId) {
            alert("请先点击一篇笔记呀!");
            return;
        }
        dialog.showModal();
        let noteDetail = noteData.noteDetailMap[currentNoteId];
        let note = noteDetail.note;
        log.debug('note details', note);
        let noteType = note.type;

        let title = note.title;
        let imageList = note.imageList;
        let imageSize = imageList.length;
        let video = note.video;
        log.debug('video', video);


        dialogTitle.innerText = "(" + imageSize + "p)" + title;

        dialogMain.innerHTML = '';
        selectedImg = [];
        if (noteType === 'video') {
            let url = null;
            if (video.media.stream['h264'] && video.media.stream['h264'].length > 0) {
                url = video.media.stream['h264'][0].masterUrl;
            } else if (video.media.stream['h265'] && video.media.stream['h265'].length > 0) {
                url = video.media.stream['h265'][0].masterUrl;
            }

            let imgItemBox = document.createElement("div");
            imgItemBox.setAttribute('style', "width:180px;height:fit-content;margin-right: 12px;margin-bottom: 12px;box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);border: 1px solid #f0f0f0;position: relative;display: block;");
            let imgEl = document.createElement("video");
            imgEl.setAttribute('style', "width:100%;height:auto;display: block;");
            imgEl.setAttribute("src", url);
            imgEl.controls = true;
            imgItemBox.appendChild(imgEl);
            dialogMain.appendChild(imgItemBox);

            let imgData = {
                url: url,
                type: 'video',
                imgEl: imgEl,
                name: "小红书-"+title
            }
            selectedImg.push(imgData);
        } else {

            for (let i = 0; i < imageList.length; i++) {
                let item = imageList[i];
                let url = item.urlDefault;
                let imgItemBox = document.createElement("div");
                imgItemBox.setAttribute('style', "width:180px;height:fit-content;margin-right: 12px;margin-bottom: 12px;box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);border: 1px solid #f0f0f0;position: relative;display: block;");
                let imgEl = document.createElement("img");
                imgEl.setAttribute('style', "width:100%;height:auto;display: block;");
                imgEl.setAttribute("src", url);
                imgItemBox.appendChild(imgEl);
                dialogMain.appendChild(imgItemBox);

                let imgData = {
                    url: url,
                    imgEl: imgEl,
                    name: "小红书-"+title+"["+(i+1)+"]"
                }
                selectedImg.push(imgData);
            }
        }

        

    }

    function downloadAllImg(){
        let pageData = getPageData();
        if (!pageData) {
            alert("请先点击一篇笔记呀!");
            return;
        }
        let noteData = JSON.parse(JSON.stringify(pageData.note));
        let currentNoteId = noteData.currentNoteId._value;
        //console.log("currentNoteId", currentNoteId);
        if (!currentNoteId) {
            alert("请先点击一篇笔记呀!");
            return;
        }
        let noteDetail = noteData.noteDetailMap[currentNoteId];
        let note = noteDetail.note;
        let title = note.title;
        let imageList = note.imageList;
        let imageSize = imageList.length;
        let video = note.video;
        let noteType = note.type;

        if (noteType === 'video') {
            let url = null;
            if (video.media.stream['h264'] && video.media.stream['h264'].length > 0) {
                url = video.media.stream['h264'][0].masterUrl;
            } else if (video.media.stream['h265'] && video.media.stream['h265'].length > 0) {
                url = video.media.stream['h265'][0].masterUrl;
            }

            let name = "小红书-"+title;
            let fileName = name + '.mp4';
            let rootWindow = getPageDatadiv.onclick();
            // 创建 a 标签
            const a = rootWindow.document.createElement('a');
            // 设置下载文件的 URL
            //a.href = imgUrl; // 替换为你的文件 URL
            a.href = url;
            a.target = '_blank';
            // 设置下载文件的名称
            a.download = fileName; // 替换为你的文件名称

            // 将 a 标签添加到文档中
            rootWindow.document.body.appendChild(a);

            // 触发点击事件
            a.click();

            // 移除 a 标签
            rootWindow.document.body.removeChild(a);

            return;
        }

        for (let i = 0; i < imageList.length; i++) {
            let item = imageList[i];
            let url = item.urlDefault;
            let name = "小红书-"+title+"["+(i+1)+"]";

            downloadImg(url, name);
        }
    }


    //document.getElementsByTagName("body")[0].appendChild(control);



    function downloadImg(imgUrl, name){
        return new Promise((resolve, reject)=>{
            fetch(imgUrl).then(async res=>{
                if (!res.ok) {
                    throw new Error('Network response was not ok');
                }
                //let type = res.headers.get("Content-Type");
                let blob = await res.blob();
                let type = blob.type;
                log.debug("blob type", type);
                if (type === "image/webp") {
                    const url = URL.createObjectURL(blob);
                    const img = new Image();
                    img.onload = function() {
                        const canvas = document.createElement('canvas');
                        const ctx = canvas.getContext('2d');
                        canvas.width = img.width;
                        canvas.height = img.height;
                        ctx.drawImage(img, 0, 0);
                        canvas.toBlob(function(transBlob) {
                            triggerDownload(transBlob, name + '.jpg');
                        }, 'image/jpeg');
                        URL.revokeObjectURL(url);
                    };
                    img.src = url;
                } else if (type === "image/png") {
                    triggerDownload(blob, name + '.png');
                } else if (type === "image/jpg" || type === "image/jpeg") {
                    triggerDownload(blob, name + '.jpg');
                } else if (type === "image/gif") {
                    triggerDownload(blob, name + '.gif');
                } else {
                    triggerDownload(blob, name + '.jpg');
                }
                resolve();
            }).catch(e=>{
                console.error("下载错误", e);
                reject(e);
            });
        });
    }


    function triggerDownload(blob, filename) {
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename || 'downloaded';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }





    // 创建一个观察者对象,用来观察DOM的变化
    var observer = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
            if (mutation.type === 'childList') { // 检测是否有新的子节点被添加
                mutation.addedNodes.forEach(function(node) {
                    let id = node.id;
                    let className = String(node.className);
                    if (className && String(className).indexOf("img-container") >= 0 || String(className).indexOf("note-detail-mask")) {
                        let noteContainer = document.getElementById("noteContainer");
                        addDownloadBt(noteContainer);
                    }
                });
            }
        });
    });
    // 配置观察者对象,指定要观察的节点和观察选项
    var config = { attributes: false, childList: true, subtree: true };
    // 选择需要观察变动的节点
    var targetNode = document.getElementsByTagName("body")[0];
    // 开始观察
    observer.observe(targetNode, config);


    var noteContainer = document.getElementById("noteContainer");
    var noteData = getPageData();
    addDownloadBt(noteContainer);
    function addDownloadBt (noteContainer) {
        if (!noteContainer) {
            return;
        }
        let noteData = getPageData();
        log.debug("debug noteData", noteData);
        if (noteContainer.querySelector("#mysqDlBtBox")) {
            return;
        }
        let dlBtBox = document.createElement("div");
        dlBtBox.setAttribute('style', 'position:absolute; top:0px; left:0px;opacity:0.6;z-index:99999; ');
        dlBtBox.setAttribute("id", "mysqDlBtBox");
        let dlBt = document.createElement("div");
        dlBt.setAttribute("id", "mysqDlBt");
        dlBt.innerText = "这里下载";
        dlBt.setAttribute('style', 'position:relative; padding:6px 12px; background-color:#409EFF;color:#ffffff;z-index:99999999;border-radius: 24px 0px 12px 0;cursor: pointer;');
        let dropdownMenu = document.createElement("div");
        dropdownMenu.setAttribute('style', "position:relative;background-color:#ffffff;color:#000000;z-index:99999999;margin-top:12px;border-radius: 4px;display:none;");
        let xzqbBt = document.createElement("div");
        xzqbBt.innerText = "下载全部";
        xzqbBt.setAttribute('style', "padding:6px;cursor: pointer;");
        xzqbBt.onclick = function(){
            downloadAllImg();
        }
        dropdownMenu.appendChild(xzqbBt);
        let ylxzBt = document.createElement("div");
        ylxzBt.innerText = "预览下载";
        ylxzBt.setAttribute('style', "padding:6px;cursor: pointer;");
        ylxzBt.onclick = function(){
            showImgsBox();
        }
        dropdownMenu.appendChild(ylxzBt);

        dlBtBox.appendChild(dlBt);
        dlBtBox.appendChild(dropdownMenu);

        noteContainer.appendChild(dlBtBox);
        // 为元素添加mouseover事件监听器
        dlBtBox.addEventListener('mouseover', function(event) {
            dlBtBox.style.opacity = '1.0';
            dropdownMenu.style.display = "block";
        });

        // 为元素添加mouseout事件监听器
        dlBtBox.addEventListener('mouseout', function(event) {
            dlBtBox.style.opacity = '0.6';
            dropdownMenu.style.display = "none";
        });
    }












    // Your code here...
})();