Greasy Fork

Greasy Fork is available in English.

copymanga-自动存储浏览记录

自动存储拷贝漫画的浏览记录,以防拷贝卷记录跑路;书架及漫画详情页显示上次观看章节。

当前为 2024-10-06 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         copymanga-自动存储浏览记录
// @namespace    http://tampermonkey.net/
// @description  自动存储拷贝漫画的浏览记录,以防拷贝卷记录跑路;书架及漫画详情页显示上次观看章节。
// @version      1.4.2
// @author       Y_jun
// @license      MIT
// @icon         https://hi77-overseas.mangafuna.xyz/static/free.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_xmlhttpRequest
// @match        *://*.copymanga.com/*
// @match        *://*.copymanga.org/*
// @match        *://*.copymanga.net/*
// @match        *://*.copymanga.info/*
// @match        *://*.copymanga.site/*
// @match        *://*.copymanga.tv/*
// @match        *://*.mangacopy.com/*
// @match        *://copymanga.com/*
// @match        *://copymanga.org/*
// @match        *://copymanga.net/*
// @match        *://copymanga.info/*
// @match        *://copymanga.site/*
// @match        *://copymanga.tv/*
// @match        *://mangacopy.com/*
// @run-at       document-start
// ==/UserScript==

/**
 * name: 漫画名
 * uuid: 漫画uuid
 * path: 漫画路径
 * lastRead: 上次阅读章节
 * lastUuid: 上次阅读章节uuid
 * lastIndex: 上次阅读序号
 * lastTime: 上次阅读时间
 * latestChapter: 最新章节
 * latestTime: 最新章节时间
 * isSubscribed: 是否订阅
 * popular: 漫画人气
 * authors: 漫画作者
 */
const defaultMangaObj = {
    "name": null,
    "uuid": null,
    "path": null,
    "lastRead": null,
    "lastUuid": null,
    "lastIndex": 999999,
    "lastTime": null,
    "latestChapter": null,
    "latestTime": null,
    "isSubscribed": false,
    "popular": 0,
    "authors": []
}

let token;

function sleep(time) {
    return new Promise((resolve) => setTimeout(resolve, time));
}

function completeDate(value) {
    return value < 10 ? "0" + value : value;
}

function getNowFormatTime(type) {
    let nowDate = new Date();
    let colon = ":";
    let char = "-";
    let day = nowDate.getDate();
    let month = nowDate.getMonth() + 1;//注意月份需要+1
    let year = nowDate.getFullYear();
    let h = nowDate.getHours();
    let m = nowDate.getMinutes();
    let s = nowDate.getSeconds();
    //补全0,并拼接
    if (type === 'full') {
        return year + char + completeDate(month) + char + completeDate(day) + " " + completeDate(h) + colon + completeDate(m) + colon + completeDate(s);
    }
    if (type === 'short') {
        return `${year}${completeDate(month)}${completeDate(day)}${completeDate(h)}${completeDate(m)}${completeDate(s)}`;
    }
}

function addLiulanNotice() {
    let button = document.createElement('button');
    button.id = 'save-liulan-button';
    button.style.marginLeft = '20px';
    button.textContent = '开始保存浏览记录';
    button.onclick = () => {
        button.className = 'allow-save-liulan';
    }

    const keys = GM_listValues();
    const itemCount = keys.length;
    let notice = document.createElement('span');
    notice.id = 'save-liulan';
    notice.style.marginLeft = '20px';
    notice.textContent = `目前浏览记录存有${itemCount}条`;
    let collectActionArea = document.querySelector('.collectAction');

    collectActionArea.appendChild(button);
    collectActionArea.appendChild(notice);
}

function addShujiaNotice() {
    let button = document.createElement('button');
    button.id = 'save-shujia-button';
    button.style.marginLeft = '20px';
    button.textContent = '开始保存订阅记录';
    button.onclick = () => {
        button.className = 'allow-save-shujia';
    }

    const keys = GM_listValues();
    let favCount = 0;
    keys.forEach(key => {
        const manga = GM_getValue(key);
        if (manga.isSubscribed) favCount++;
    })
    let notice = document.createElement('span');
    notice.id = 'save-shujia';
    notice.style.marginLeft = '20px';
    notice.textContent = `目前订阅记录存有${favCount}条`;
    let collectActionArea = document.querySelector('.collectAction');

    collectActionArea.appendChild(button);
    collectActionArea.appendChild(notice);
}

function addExportButton() {
    let button = document.createElement('button');
    button.id = 'export-json-button';
    button.textContent = '导出记录为json';
    button.onclick = "exportJson()";
    button.onclick = () => {
        exportJson();
    }
    let headerArea = document.querySelector('#header div');
    headerArea.appendChild(button);
}

function editLiulanNotice(text) {
    let notice = document.getElementById('save-liulan');
    notice.textContent = text;
}

function editShujiaNotice(text) {
    let notice = document.getElementById('save-shujia');
    notice.textContent = text;
}

function getPopularNum(popularStr, savedManga) {
    if (popularStr.indexOf('W') > -1) {
        return Number(popularStr.substring(0, popularStr.length - 1)) * 10000;
    }
    if (popularStr.indexOf('K') > -1) {
        return Number(popularStr.substring(0, popularStr.length - 1)) * 1000;
    }
    return Math.max(Number(popularStr), savedManga.popular);
}

function exportJson() {
    const keys = GM_listValues();
    let jsonObj = {}
    keys.forEach(key => {
        let json = GM_getValue(key);
        json.authors = json.authors.toString();
        jsonObj[key] = json;
    })
    const jsonStr = JSON.stringify(jsonObj);
    const blob = new Blob([jsonStr], { type: "application/json" });
    const url = URL.createObjectURL(blob);

    const link = document.createElement("a");
    link.href = url;
    link.download = 'copymanga-export-' + getNowFormatTime('short') + '.json';
    link.click();

    URL.revokeObjectURL(url);
}

async function saveLiulanList() {
    while (!document.querySelector('.allow-save-liulan')) {
        await sleep(2000);
    }
    // editLiulanNotice('正在删除旧的本地记录……');
    // deleteAllValues();
    let offset = 0;
    let limit = 25;
    let lastIndex = 1;
    let totalStr = document.querySelector('.demonstration').innerText;
    let total = Number(totalStr.substring(3, totalStr.length - 2));
    while (offset < total) {
        editLiulanNotice('保存浏览记录中,请勿进行其他操作,进度:' + Math.round(offset / total * 10000) / 100 + "%");
        GM_xmlhttpRequest({
            method: "get",
            url: `${window.location.origin}/api/kb/web/browses?limit=${limit}&offset=${offset}&free_type=1`,
            data: "",
            headers: {
                "Content-Type": "application/json",
                "Authorization": token,
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.160 Safari/537.36"
            },
            onload: res => {
                if (res.status === 200) {
                    const response = JSON.parse(res.response);
                    if (response.code === 200) {
                        const mangaList = response.results.list;
                        mangaList.forEach((manga) => {
                            const savedManga = GM_getValue(manga.comic.path_word, null) ?? defaultMangaObj;
                            const authors = [];
                            if (Array.isArray(manga.comic.author)) {
                                const authorList = manga.comic.author;
                                authorList.forEach((author) => {
                                    authors.push(author.name);
                                })
                            }
                            const mangaObj = {
                                "name": manga.comic.name,
                                "uuid": manga.comic.uuid,
                                "path": manga.comic.path_word,
                                "lastRead": manga.last_chapter_name,
                                "lastUuid": manga.last_chapter_id,
                                "lastIndex": lastIndex,
                                "lastTime": getNowFormatTime('full'),
                                "latestChapter": manga.comic.last_chapter_name,
                                "latestTime": manga.comic.datetime_updated,
                                "isSubscribed": savedManga.isSubscribed ?? false,
                                "popular": manga.comic.popular,
                                "authors": authors
                            }
                            lastIndex++;
                            GM_setValue(manga.comic.path_word, mangaObj);
                        });
                    } else {
                        editLiulanNotice('保存浏览记录出错,拷贝api返回json状态码不为200');
                        console.log('code不为200:\n' + res);
                        total = -1;
                    }
                } else {
                    editLiulanNotice('保存浏览记录出错,网络请求出错');
                    console.log('status不为200:\n' + res);
                    total = -1;
                }
            },
            onerror: () => {
                editLiulanNotice('保存浏览记录出错,发送请求失败');
                console.log('读取浏览记录失败');
                total = -1;
            }
        });
        offset += limit;
        await sleep(2000);
    }
    editLiulanNotice('保存完毕');
}

async function saveShujiaList() {
    while (!document.querySelector('.allow-save-shujia')) {
        await sleep(2000);
    }
    let offset = 0;
    let limit = 25;
    let totalStr = document.querySelector('.demonstration').innerText;
    let total = Number(totalStr.substring(3, totalStr.length - 2));
    while (offset < total) {
        editShujiaNotice('保存订阅记录中,请勿进行其他操作,进度:' + Math.round(offset / total * 10000) / 100 + "%");
        GM_xmlhttpRequest({
            method: "get",
            url: `${window.location.origin}/api/v3/member/collect/comics?limit=${limit}&offset=${offset}&free_type=1&ordering=-datetime_modifier`,
            data: "",
            headers: {
                "Content-Type": "application/json",
                "Authorization": token,
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.160 Safari/537.36"
            },
            onload: res => {
                if (res.status === 200) {
                    const response = JSON.parse(res.response);
                    if (response.code === 200) {
                        const mangaList = response.results.list;
                        mangaList.forEach((manga) => {
                            const savedManga = GM_getValue(manga.comic.path_word, null) ?? defaultMangaObj;
                            const authors = [];
                            if (Array.isArray(manga.comic.author)) {
                                const authorList = manga.comic.author;
                                authorList.forEach((author) => {
                                    authors.push(author.name);
                                })
                            }
                            const mangaObj = {
                                "name": manga.comic.name,
                                "uuid": manga.comic.uuid,
                                "path": manga.comic.path_word,
                                "lastRead": savedManga.lastRead,
                                "lastUuid": savedManga.lastUuid,
                                "lastIndex": savedManga.lastIndex,
                                "lastTime": savedManga.lastTime,
                                "latestChapter": manga.comic.last_chapter_name,
                                "latestTime": manga.comic.datetime_updated,
                                "isSubscribed": true,
                                "popular": manga.comic.popular,
                                "authors": authors
                            }
                            GM_setValue(manga.comic.path_word, mangaObj);
                        });
                    } else {
                        editShujiaNotice('保存订阅记录出错');
                        console.log('code不为200:\n' + res);
                        total = -1;
                    }
                } else {
                    editShujiaNotice('保存订阅记录出错');
                    console.log('status不为200:\n' + res);
                    total = -1;
                }
            },
            onerror: () => {
                editShujiaNotice('保存订阅记录出错');
                console.log('读取订阅记录失败');
                total = -1;
            }
        });
        offset += limit;
        await sleep(2000);
    }
    editShujiaNotice('保存完毕');
}

function saveLastRead(path, count = 1) {
    if (document.querySelector('.table-default') === null) {
        if (count <= 50) {
            const args = Array.from(arguments).slice(0, arguments.length);
            args.push(count + 1);
            setTimeout(saveLastRead, 200, ...args);
        }
        return;
    }
    const savedManga = GM_getValue(path, null) ?? defaultMangaObj;
    const name = document.querySelector('h6').textContent;
    const updateArr = document.querySelector('.table-default-right').textContent.split('更新');
    const latestChapter = updateArr[1].substring(3);
    const updateTime = updateArr[2].substring(3);
    const subscribeBtnText = document.querySelector('.collect').innerText;
    const isSubscribed = subscribeBtnText.indexOf('取消') < 0 ? false : true;
    const popularStr = document.querySelectorAll('.comicParticulars-right-txt')[2].innerText;
    const popular = getPopularNum(popularStr, savedManga);
    const authors = document.querySelectorAll('.comicParticulars-right-txt')[1].innerHTML.match(/>[^<]+<\/a>/g);
    for (let i = 0; i < authors.length; i++) {
        const author = authors[i];
        authors[i] = author.substring(1, author.length - 4);
    }
    GM_xmlhttpRequest({
        method: "get",
        url: `${window.location.origin}/api/v3/comic2/${path}/query?platform=1&_update=true`,
        data: "",
        headers: {
            "Content-Type": "application/json",
            "Authorization": token,
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.160 Safari/537.36"
        },
        onload: res => {
            if (res.status === 200) {
                const response = JSON.parse(res.response);
                if (response.code === 200) {
                    const results = response.results;
                    if (results.browse) {
                        savedManga.name = name;
                        savedManga.uuid = results.browse.comic_uuid;
                        savedManga.path = path;
                        if (!savedManga.path || savedManga.lastUuid !== results.browse.chapter_uuid) {
                            savedManga.lastRead = results.browse.chapter_name;
                            savedManga.lastUuid = results.browse.chapter_uuid;
                            savedManga.lastIndex = 0;
                            savedManga.lastTime = getNowFormatTime('full');
                        }
                        savedManga.latestChapter = latestChapter;
                        savedManga.latestTime = updateTime;
                        savedManga.isSubscribed = isSubscribed;
                        savedManga.popular = popular;
                        savedManga.authors = authors;
                        GM_setValue(path, savedManga);
                    }
                } else {
                    console.log('code不为200:\n' + res);
                }
            } else {
                console.log('status不为200:\n' + res);
            }
        },
        onerror: () => {
            console.log('读取最近阅读失败');
        }
    });
}

// 漫画详情页显示本地阅读记录
function showSavedLastRead(path, count = 1) {
    if (document.querySelector('ul') === null) {
        if (count <= 50) {
            const args = Array.from(arguments).slice(0, arguments.length);
            args.push(count + 1);
            setTimeout(saveCurrentRead, 200, ...args);
        }
        return;
    }
    let savedManga = GM_getValue(path, defaultMangaObj);
    const lastUuid = savedManga.lastUuid ?? null;

    const ul = document.querySelector('ul');
    let showSpan = document.querySelector('.local-last-read-name') ?? document.createElement('span');
    showSpan.className = 'local-last-read-name';
    showSpan.textContent = '本地记录:';
    let showLink = document.querySelector('.local-last-read-uuid') ?? document.createElement('a');
    showLink.className = 'local-last-read-uuid';
    showLink.target = '_blank';
    showLink.innerText = '无记录';
    showLink.style.color = '#1790E6';
    if (lastUuid) {
        showLink.href = `/comic/${path}/chapter/${lastUuid}`;
        showLink.innerText = savedManga.lastRead;
    }
    let li = document.querySelector('.local-last-read') ?? document.createElement('li');
    li.className = 'local-last-read';
    li.appendChild(showSpan);
    li.appendChild(showLink);
    ul.appendChild(li);
}

// 存储漫画正在阅读的章节
function saveCurrentRead(path, lastUuid, count = 1) {
    let savedManga = GM_getValue(path, null);
    if (!savedManga) return;
    if (document.querySelector('h4.header') === null) {
        if (count <= 50) {
            const args = Array.from(arguments).slice(0, arguments.length);
            args.push(count + 1);
            setTimeout(saveCurrentRead, 200, ...args);
        }
        return;
    }
    let StrArr = document.querySelector('h4.header').innerText.split('/');
    savedManga.lastRead = StrArr[1];
    savedManga.lastUuid = lastUuid;
    savedManga.lastTime = getNowFormatTime('full');
    GM_setValue(path, savedManga);
}

// 开始运行
window.onload = () => {
    token = 'Token ' + document.cookie.split('; ').find((cookie) => cookie.startsWith('token='))?.replace('token=', '');
    if (token.length < 8) return;
    const pathArr = window.location.pathname.replace('#', '').split('/');
    if (pathArr.length > 3 && pathArr[2] === 'person') {
        console.log('当前位置:个人中心');
        addExportButton();
    }
    if (window.location.pathname === '/web/person/liulan') {
        console.log('当前位置:我的浏览');
        addLiulanNotice();
        saveLiulanList();
    } else if (window.location.pathname === '/web/person/shujia') {
        console.log('当前位置:我的书架');
        addShujiaNotice();
        saveShujiaList();
    } else if (pathArr.length === 3 && pathArr[1] === 'comic') {
        console.log('当前位置:漫画详情页');
        saveLastRead(pathArr[2]);
        showSavedLastRead(pathArr[2]);
        document.addEventListener('visibilitychange', () => {
            if (document.visibilityState === 'visible') {
                saveLastRead(pathArr[2]);
                showSavedLastRead(pathArr[2]);
            }
        });
    } else if (pathArr.length === 5 && pathArr[3] === 'chapter') {
        console.log('当前位置:漫画阅读中');
        saveCurrentRead(pathArr[2], pathArr[4]);
    }
}

// 我的书架展示上次阅读章节
setInterval(() => {
    const pathArr = window.location.pathname.replace('#', '').split('/');
    if (pathArr.length > 3 && pathArr[2] === 'person') {
        const barClass = document.querySelector('.el-menu').querySelectorAll('li')[1].className;
        if (barClass.indexOf('is-active') < 0) {
            return;
        }
        const main = document.querySelector('.man_');
        Array.from(main.children).forEach((child, index) => {
            if (child.className.indexOf('is-injected') < 0) {
                child.style.position = 'relative';
                const path = child.firstChild.href.split('/')[4];
                const savedJson = GM_getValue(path, null);
                let lastRead;
                let lastUuid;
                if (savedJson) {
                    lastRead = savedJson.lastRead;
                    lastUuid = savedJson.lastUuid;
                }
                const lastP = child.querySelector(`[id='${path}']`);
                if (lastP) child.removeChild(lastP);
                const p = document.createElement('p');
                p.id = path;
                p.innerHTML = lastUuid ? `<a href="/comic/${path}/chapter/${lastUuid}" target='_blank'>上次阅读:  ${lastRead}</a>` : '还没看过';
                p.style.width = '100%';
                p.style.position = 'absolute';
                p.style.bottom = '10px';
                child.appendChild(p);
                child.classList.add('is-injected');
            }
        })
    }
}, 1000);