copymanga-自动存储浏览记录

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

// ==UserScript==
// @name         copymanga-自动存储浏览记录
// @namespace    http://tampermonkey.net/
// @description  自动存储拷贝漫画的浏览记录,以防拷贝卷记录跑路;书架及漫画详情页显示上次观看章节。
// @version      1.5.3
// @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/*
// ==/UserScript==

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 getDefaultMangaObj() {
    const defaultMangaObj = {
        "name": null,           // name: 漫画名
        "uuid": null,           // uuid: 漫画uuid
        "path": null,           // path: 漫画路径
        "lastRead": null,       // lastRead: 上次阅读章节
        "lastUuid": null,       // lastUuid: 上次阅读章节uuid
        "lastIndex": 999999,    // lastIndex: 上次阅读序号
        "lastTime": null,       // lastTime: 上次阅读时间
        "latestChapter": null,  // latestChapter: 最新章节
        "latestTime": null,     // latestTime: 最新章节时间
        "isSubscribed": false,  // isSubscribed: 是否已订阅
        "tags": [],             // tags: 漫画标签
        "popular": 0,           // popular: 漫画人气
        "authors": []           // authors: 漫画作者
    }
    return defaultMangaObj;
}

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 editNoticeById(text, id) {
    let notice = document.getElementById(id);
    notice.textContent = text;
}

function getPopularNum(popularStr, savedManga) {
    if (popularStr.includes('W')) {
        return Number(popularStr.substring(0, popularStr.length - 1)) * 10000;
    }
    if (popularStr.includes('K')) {
        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.tags = json.tags?.toString();
        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) {
        editNoticeById('保存瀏覽記錄中,請勿進行其他操作,進度:' + Math.round(offset / total * 10000) / 100 + "%", 'save-liulan');
        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) ?? getDefaultMangaObj();
                            const authors = [];
                            if (Array.isArray(manga.comic.author)) {
                                const authorList = manga.comic.author;
                                authorList.forEach((author) => {
                                    authors.push(author.name);
                                })
                            }
                            let tags = [];
                            if (savedManga.tags && savedManga.tags.length > 0) {
                                tags = savedManga.tags;
                            }
                            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,
                                "tags": tags,
                                "popular": manga.comic.popular,
                                "authors": authors
                            }
                            lastIndex++;
                            GM_setValue(manga.comic.path_word, mangaObj);
                        });
                    } else {
                        editNoticeById('保存瀏覽記錄出錯,api返回json狀態碼不為200', 'save-liulan');
                        console.log('code不为200:\n' + res);
                        total = -1;
                    }
                } else {
                    editNoticeById('保存瀏覽記錄出錯,網絡請求出錯', 'save-liulan');
                    console.log('status不为200:\n' + res);
                    total = -1;
                }
            },
            onerror: () => {
                editNoticeById('保存瀏覽記錄出錯,發送請求失敗', 'save-liulan');
                console.log('读取浏览记录失败');
                total = -1;
            }
        });
        offset += limit;
        await sleep(2000);
    }
    editNoticeById('保存完畢', 'save-liulan');
}

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) {
        editNoticeById('保存訂閱記錄中,請勿進行其他操作,進度:' + Math.round(offset / total * 10000) / 100 + "%", 'save-shujia');
        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) ?? getDefaultMangaObj();
                            const authors = [];
                            if (Array.isArray(manga.comic.author)) {
                                const authorList = manga.comic.author;
                                authorList.forEach((author) => {
                                    authors.push(author.name);
                                })
                            }
                            let tags = [];
                            if (savedManga.tags && savedManga.tags.length > 0) {
                                tags = savedManga.tags;
                            }
                            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,
                                "tags": tags,
                                "popular": manga.comic.popular,
                                "authors": authors
                            }
                            GM_setValue(manga.comic.path_word, mangaObj);
                        });
                    } else {
                        editNoticeById('保存訂閱記錄出錯,api返回json狀態碼不為200', 'save-shujia');
                        console.log('code不为200:\n' + res);
                        total = -1;
                    }
                } else {
                    editNoticeById('保存訂閱記錄出錯,網絡請求出錯', 'save-shujia');
                    console.log('status不为200:\n' + res);
                    total = -1;
                }
            },
            onerror: () => {
                editNoticeById('保存訂閱記錄出錯,發送請求失敗', 'save-shujia');
                console.log('读取订阅记录失败');
                total = -1;
            }
        });
        offset += limit;
        await sleep(2000);
    }
    editNoticeById('保存完畢', 'save-shujia');
}

// 漫画阅读页存储漫画最后阅读的章节
function saveLastRead(path, manga = null) {
    const savedManga = manga ?? GM_getValue(path, null);
    if (!savedManga) return;
    GM_xmlhttpRequest({
        method: "get",
        url: `${window.location.origin}/api/v3/comic2/${path}/query?platform=3`,
        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.uuid = results.browse.comic_uuid;
                        if (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');
                        }
                        GM_setValue(path, savedManga);
                    }
                } else {
                    console.log('code不为200:\n' + res);
                }
            } else {
                console.log('status不为200:\n' + res);
            }
        },
        onerror: () => {
            console.log('读取最近阅读失败');
        }
    });
}

// 漫画详情页存储漫画最后阅读的章节+漫画其他信息
function saveLastReadFull(path, count = 1) {
    if (document.querySelector('.table-default') === null) {
        if (count <= 20) {
            const args = Array.from(arguments).slice(0, arguments.length);
            args.push(count + 1);
            setTimeout(saveLastReadFull, 200, ...args);
        } else {
            saveLastRead(path);
        }
        return;
    }
    const savedManga = GM_getValue(path, null) ?? getDefaultMangaObj();
    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.includes('取消') ? true : false;
    const tags = document.querySelector('.comicParticulars-tag').innerText.match(/#[^ ]+/g);
    for (let i = 0; i < tags.length; i++) {
        const tag = tags[i];
        tags[i] = tag.replaceAll('#', '');
    }
    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);
    }
    savedManga.name = name;
    savedManga.path = path;
    savedManga.latestChapter = latestChapter;
    savedManga.latestTime = updateTime;
    savedManga.isSubscribed = isSubscribed;
    savedManga.tags = tags;
    savedManga.popular = popular;
    savedManga.authors = authors;
    saveLastRead(path, savedManga);
}

// 漫画详情页显示本地阅读记录
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(saveLastRead, 200, ...args);
        }
        return;
    }
    let savedManga = GM_getValue(path, getDefaultMangaObj());
    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 addLastReadToManga(man) {
    Array.from(man.children).forEach((child, index) => {
        let h4 = child.querySelector('h4');

        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;
        }
        h4.id = path;
        h4.innerHTML = lastUuid ? `<a href="/comic/${path}/chapter/${lastUuid}" target='_blank' title="${lastRead}" style="margin:0;float:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">本地記錄:${lastRead}</a>` : '無本地記錄';
    });
}

function handlePersonPage() {
    if (window.location.pathname.includes('liulan')) {
        console.log('当前位置:我的浏览');
        addLiulanNotice();
        saveLiulanList();
    } else if (window.location.pathname.includes('shujia')) {
        console.log('当前位置:我的书架');
        addShujiaNotice();
        saveShujiaList();
    }
    if (/liulan|shujia/.test(window.location.pathname)) {
        let interval = setInterval(() => {
            const man = document.querySelector('.man_');
            if (man) {
                clearInterval(interval);
                addLastReadToManga(man);
                const homeObserver = new MutationObserver((mutationsList) => {
                    // console.log(mutationsList);
                    if (mutationsList.length > 0) {
                        addLastReadToManga(man);
                    }
                });
                homeObserver.observe(man, { childList: true });
                let refreshButton = document.querySelector('.el-icon-refresh').parentElement;
                refreshButton.onclick = () => {
                    let refreshInterval = setInterval(() => {
                        const loading = document.querySelector('.el-loading-parent--relative');
                        if (!loading) {
                            clearInterval(refreshInterval);
                            addLastReadToManga(man);
                        }
                    }, 200);
                }
            }
        }, 1000);
    }
}

function handleFaxian() {
    let interval = setInterval(() => {
        let comicBox = document.querySelector('.row.exemptComic-box');
        if (comicBox) {
            clearInterval(interval);
            let comicList = comicBox.children;
            for (const comic of comicList) {
                let authorLink = comic.querySelector('.exemptComicItem-txt-span a');
                let authorLinkArr = authorLink.href.split('/');
                authorLink.href = `${window.location.origin}/search?q=${authorLinkArr[4]}&q_type=author`
            }
        }
    }, 100);

}


addEventListener("load", () => {
    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();
        handlePersonPage();

        const container = document.querySelector('.el-main .el-container');
        const homeObserver = new MutationObserver((mutationsList) => {
            for (let mutation of mutationsList) {
                if (mutation.addedNodes.length > 0) {
                    handlePersonPage();
                }
            }
        });
        homeObserver.observe(container, { childList: true });
    } else if (pathArr.length === 3 && pathArr[1] === 'comic' && pathArr[2]) {
        console.log('当前位置:漫画详情页');
        saveLastReadFull(pathArr[2]);
        showSavedLastRead(pathArr[2]);
        document.addEventListener('visibilitychange', () => {
            saveLastReadFull(pathArr[2]);
            showSavedLastRead(pathArr[2]);
        });
    } else if (pathArr.length === 5 && pathArr[3] === 'chapter' && pathArr[2]) {
        console.log('当前位置:漫画阅读中');
        saveLastRead(pathArr[2]);
    }
    if (pathArr.length === 2 && pathArr[1] === 'comics') {
        console.log('当前位置:发现');
        handleFaxian();
    }
})