Greasy Fork is available in English.

Ang Sinabi Ko

Heto ako. Kukunin ko ang mga sinabi ko.

// ==UserScript==
// @name    Ang Sinabi Ko
// @description Heto ako. Kukunin ko ang mga sinabi ko.
// @match   https://m.douban.com/people/*
// @version 1.2
// @grant   unsafeWindow
// @grant   GM_xmlhttpRequest
// @run-at  document-end
// @license MIT
// @namespace https://greasyfork.org/users/219930
// ==/UserScript==

// 用 Promise 简单地封装一下 GM_xmlhttpRequest
function get(url) {
    return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            method: 'GET',
            url,
            headers: {
                Referer: 'https://m.douban.com/',
            },
            onload: (res) => resolve(JSON.parse(res.response)),
            onerror: reject,
        });
    });
}

(async () => {
    // 获取当前登录用户相关信息
    const { user } = unsafeWindow.__INITIAL_STATE__;

    // 简易的交互界面
    const panel = document.createElement('div');
    panel.style.cssText = `
    position: fixed;
    top: calc(50% - 5em);
    left: calc(50% - 10em);
    z-index: 9999;
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 20em;
    height: 10em;
    padding: 2em 2em 1em;
    border-radius: 0.5em;
    background-color: #fff;
    box-shadow:
        0px 0px 3.6px -2px rgba(0, 0, 0, 0.035),
        0px 0px 10px -2px rgba(0, 0, 0, 0.05),
        0px 0px 24.1px -2px rgba(0, 0, 0, 0.065),
        0px 0px 80px -2px rgba(0, 0, 0, 0.1);
    font-size: 1.25em;
    `;

    const closeButton = document.createElement('button');
    closeButton.type = 'button';
    closeButton.setAttribute('aria-label', '关闭');
    closeButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>';
    closeButton.style.cssText = `
    position: absolute;
    top: 0.5em;
    right: 0.5em;
    padding: 0.25em;
    background: transparent;
    border: 0;
    width: 1.8em;
    height: 1.8em;
    color: #777;
    cursor: pointer;
    `;
    closeButton.addEventListener('click', () => panel.remove());

    const desc = document.createElement('p');
    desc.innerHTML = `当前登录用户: <strong>${user.name}<strong>`;

    const button = document.createElement('button');
    button.type = 'button';
    button.style.cssText = `
    border: 0;
    padding: 0.5em 0.8em;
    border-radius: 1.25em;
    background-color: #42bd56;
    color: #fff;
    font-size: 1.125em;
    cursor: pointer;
    `;
    button.textContent = '抓取当前用户广播数据';

    panel.append(closeButton, desc, button);
    document.body.append(panel);

    // 爬取逻辑
    const fetchPosts = (month, filter = '') => get(`https://m.douban.com/rexxar/api/v2/user/${user.id}/lifestream?slice=month-${month}&hot=false&filter_after=${filter}&count=50&ck=azd0&for_mobile=1`);

    button.addEventListener('click', async () => {
        button.disabled = true;
        const tip = document.createElement('p');
        tip.textContent = '抓取中,预计需要几分钟,完成后将自动下载数据';
        tip.style.fontSize = '0.75em';
        panel.append(tip);

        // 获取当前用户的注册年份和月份
        const { reg_time: regTime } = await get(`https://m.douban.com/rexxar/api/v2/user/${user.id}?ck=azd0&for_mobile=1`);
        const regTimeParts = regTime.split('-');
        const regYear = Number(regTimeParts[0]);
        const regMonth = Number(regTimeParts[1]);

        // 获取当下的年份和月份
        const date = new Date();
        const currentYear = date.getFullYear();
        const currentMonth = date.getMonth() + 1;

        // 计算从注册时间到当下时间之间的所有月份
        const months = new Array(currentYear - regYear + 1).fill(null)
            .map((_, i) => regYear + i)
            .flatMap((year) => {
                const startMonth = (year === regYear) ? regMonth : 1;
                const endMonth = (year === currentYear) ? currentMonth : 12;
                const monthCount = endMonth - startMonth + 1;
                return new Array(monthCount).fill(null)
                    .map((_, j) => `${year}-${j + startMonth}`);
            });

        // 通过豆瓣移动端的公共接口逐一爬取每个月份的广播
        const posts = {};
        for (let i = 0; i < months.length; i++) {
            const month = months[i];
            const data = await fetchPosts(month);
            posts[month] = data.items;

            // 该接口一次性最多只能获取50条,若存在`next_filter_after`则说明该月份还有更多广播,需要继续获取
            let nextFilterAfter = data.next_filter_after;
            while (nextFilterAfter) {
                const data = await fetchPosts(month, nextFilterAfter);
                posts[month].push(...data.items);
                nextFilterAfter = data.next_filter_after;
            }
        }

        // 以json文件形式下载获取完的所有广播
        const file = new File([JSON.stringify(posts, null, 4)], { type: 'plain/text' });
        const url = URL.createObjectURL(file);
        const link = document.createElement('a');
        link.download = 'douban.json';
        link.href = url;
        link.click();

        tip.textContent = '抓取完毕,请在你的下载目录里查找 douban.json 文件'
        button.disabled = false;
    });
})();