// ==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 // @downloadURL none // ==/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 ? `上次阅读: ${lastRead}` : '还没看过'; p.style.width = '100%'; p.style.position = 'absolute'; p.style.bottom = '10px'; child.appendChild(p); child.classList.add('is-injected'); } }) } }, 1000);