Greasy Fork is available in English.

xiaohongshu_link

记录已经打开的笔记对应的视频数据和链接,可以随时复制或下载csv

// ==UserScript==
// @name         xiaohongshu_link
// @namespace    http://tampermonkey.net/
// @version      0.1.1
// @description  记录已经打开的笔记对应的视频数据和链接,可以随时复制或下载csv
// @author       xxmdmst
// @match        https://www.xiaohongshu.com/*
// @icon         https://www.xiaohongshu.com/favicon.ico
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';
    let table;

    function initGbkTable() {
        // https://en.wikipedia.org/wiki/GBK_(character_encoding)#Encoding
        const ranges = [
            [0xA1, 0xA9, 0xA1, 0xFE],
            [0xB0, 0xF7, 0xA1, 0xFE],
            [0x81, 0xA0, 0x40, 0xFE],
            [0xAA, 0xFE, 0x40, 0xA0],
            [0xA8, 0xA9, 0x40, 0xA0],
            [0xAA, 0xAF, 0xA1, 0xFE],
            [0xF8, 0xFE, 0xA1, 0xFE],
            [0xA1, 0xA7, 0x40, 0xA0],
        ];
        const codes = new Uint16Array(23940);
        let i = 0;

        for (const [b1Begin, b1End, b2Begin, b2End] of ranges) {
            for (let b2 = b2Begin; b2 <= b2End; b2++) {
                if (b2 !== 0x7F) {
                    for (let b1 = b1Begin; b1 <= b1End; b1++) {
                        codes[i++] = b2 << 8 | b1
                    }
                }
            }
        }
        table = new Uint16Array(65536);
        table.fill(0xFFFF);
        const str = new TextDecoder('gbk').decode(codes);
        for (let i = 0; i < str.length; i++) {
            table[str.charCodeAt(i)] = codes[i]
        }
    }

    function str2gbk(str, opt = {}) {
        if (!table) {
            initGbkTable()
        }
        const NodeJsBufAlloc = typeof Buffer === 'function' && Buffer.allocUnsafe;
        const defaultOnAlloc = NodeJsBufAlloc
            ? (len) => NodeJsBufAlloc(len)
            : (len) => new Uint8Array(len);
        const defaultOnError = () => 63;
        const onAlloc = opt.onAlloc || defaultOnAlloc;
        const onError = opt.onError || defaultOnError;

        const buf = onAlloc(str.length * 2);
        let n = 0;

        for (let i = 0; i < str.length; i++) {
            const code = str.charCodeAt(i);
            if (code < 0x80) {
                buf[n++] = code;
                continue
            }
            const gbk = table[code];

            if (gbk !== 0xFFFF) {
                buf[n++] = gbk;
                buf[n++] = gbk >> 8
            } else if (code === 8364) {
                buf[n++] = 0x80
            } else {
                const ret = onError(i, str);
                if (ret === -1) {
                    break
                }
                if (ret > 0xFF) {
                    buf[n++] = ret;
                    buf[n++] = ret >> 8
                } else {
                    buf[n++] = ret
                }
            }
        }
        return buf.subarray(0, n)
    }

    function copyToClipboard(text) {
        try {
            const textarea = document.createElement("textarea");
            textarea.setAttribute('readonly', 'readonly');
            textarea.value = text;
            document.body.appendChild(textarea);
            textarea.select();
            let flag = document.execCommand("copy");
            document.body.removeChild(textarea);
            return flag;
        } catch (e) {
            console.log(e);
            return false;
        }
    }

    let title2urls = new Map();

    function interceptResponse() {
        const originalSend = XMLHttpRequest.prototype.send;
        XMLHttpRequest.prototype.send = function () {
            originalSend.apply(this, arguments);
            const self = this;
            let func = this.onreadystatechange;
            this.onreadystatechange = (e) => {
                if (func) {
                    func.apply(self, e);
                }
                if (self.readyState !== 4 || self.responseURL.indexOf("/api/sns/web/v1/feed") === -1) return;
                let json = JSON.parse(self.response);
                if (!json.success) return;
                for (let item of json.data.items) {
                    let noteCard = item.note_card;
                    if (noteCard.type !== "video") {
                        continue;
                    }
                    let interactInfo = noteCard.interact_info;
                    let backup_urls = noteCard.video.media.stream.h264[0].backup_urls;
                    for (let video_url of backup_urls) {
                        if (video_url.indexOf("?") > -1) {
                            window.video_url = video_url;
                            title2urls.set(item.id, [
                                noteCard.title, interactInfo.liked_count, interactInfo.collected_count,
                                interactInfo.comment_count, interactInfo.share_count,
                                new Date(noteCard.time).toLocaleString(), video_url
                            ]);
                            // console.log(Array.from(title2urls.keys()));
                            // console.log(Array.from(title2urls.values()));
                            break;
                        }
                    }
                }
            }
        };
    }

    interceptResponse();

    function createButton(title, top) {
        top = top === undefined ? "60px" : top;
        const button = document.createElement('button');
        button.textContent = title;
        button.style.position = 'fixed';
        button.style.right = '5px';
        button.style.top = top;
        button.style.zIndex = '90000';
        document.body.appendChild(button);
        return button
    }

    function txt2file(txt, filename) {
        const blob = new Blob([txt], {type: 'text/plain'});
        const url = URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.download = filename.replace(/[\/:*?"<>|]/g, "");
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        URL.revokeObjectURL(url);
    }

    function downloadData(encoding) {
        let text = "标题,点赞数,收藏数,评论数,分享数,发布时间,下载链接\n";
        Array.from(title2urls.values()).forEach(item => {
            text += '"' + item[0] + '",' + item.slice(1).join(",") + "\n"
        });
        if (encoding === "gbk") text = str2gbk(text);
        txt2file(text, document.title + ".csv");
    }

    let b1 = createButton("复制已加载链接", "60px");
    let b2 = createButton("下载已加载数据", "81px");
    let b3 = createButton("复制链接", "102px");

    function copyUrl() {
        let urls = Array.from(title2urls.values()).map(item => item[item.length - 1]).join("\n");
        if (copyToClipboard(urls)) b1.textContent = "复制成功";
        else b1.textContent = "复制失败";
        setTimeout(() => {
            b1.textContent = '复制已加载链接';
        }, 2000);
    }

    function copyUrl2() {
        if (copyToClipboard(window.video_url)) b3.textContent = "复制成功";
        else b3.textContent = "复制失败";
        setTimeout(() => {
            b3.textContent = '复制链接';
        }, 2000);
    }

    window.onload = () => {
        b1.addEventListener('click', copyUrl);
        b2.addEventListener('click', (e) => downloadData("gbk"));
        b3.addEventListener('click', copyUrl2);
    };
})();