// ==UserScript== // @name Bangumi 年鉴 // @description 根据Bangumi的时光机数据生成年鉴 // @namespace syaro.io // @version 1.1.1 // @author 神戸小鳥 @vickscarlet // @license MIT // @include /^https?://(bgm\.tv|chii\.in|bangumi\.tv)\/(user)\/.*/ // @downloadURL none // ==/UserScript== (async ()=>{ const origin = window.location.origin; const uid = window.location.href.match(/\/user\/(.+)?(\/.*)?/)[1]; const year = new Date().getFullYear(); const ce = name=>document.createElement(name); const types = { 'anime': '动画', 'game': '游戏', 'music': '音乐', 'book': '图书', 'real': '三次元', }; // indexedDB cache class DB { constructor() {} #dbName = 'mcache'; #version = 1; #collection = 'pages'; #keyPath = 'url'; #db; async init() { this.#db = await new Promise((resolve, reject) => { const request = window.indexedDB.open(this.#dbName, this.#version); request.onerror = event=>reject(event.target.error); request.onsuccess = event=>resolve(event.target.result); request.onupgradeneeded = event=> { if(event.target.result.objectStoreNames.contains(this.#collection)) return; event.target.result.createObjectStore(this.#collection, {keyPath: this.#keyPath}); }; }); } async #store(handle, mode='readonly') { return new Promise((resolve, reject) => { const transaction = this.#db.transaction(this.#collection, mode); const store = transaction.objectStore(this.#collection); let result; new Promise((rs, rj) => handle(store, rs, rj)) .then(ret=>result=ret) .catch(reject); transaction.onerror = () => reject(new Error('Transaction error')); transaction.oncomplete = () => resolve(result); }); } async get(key, index) { return this.#store((store, resolve, reject)=>{ if(index) store = store.index(index); const request = store.get(key); request.onerror = reject; request.onsuccess = ()=>resolve(request.result); }) .catch(null); } async put(data) { return this.#store((store, resolve, reject)=>{ const request = store.put(data); request.onerror = reject; request.onsuccess = ()=>resolve(true); }, 'readwrite') .catch(false); } } const db = new DB(); await db.init(); const f = (url, expire=0)=>db.get(url).then(async ({html, time=0}={})=>{ expire = expire * 60000; if(html && html.match(/503 Service Temporarily Unavailable/)) html = null; if(!html || time + expire < Date.now()) { html = await fetch(url).then(res => res.text()); await db.put({url, html, time: Date.now()}); } const e = ce('html'); e.innerHTML = html.replace(//g, ''); return e; }); const collects = async (type, p=1) => { const e = await f(`${origin}/${type}/list/${uid}/collect?page=${p}`, 30); console.info(`collects page ${p} loaded`); const list = Array .from(e.querySelectorAll('#browserItemList > li')) .map(li=>{ const data = {}; data.id = li.querySelector('a').href.split('/').pop(); const title = li.querySelector('h3'); data.title = title.querySelector('a').innerText; data.jp_title = title.querySelector('small')?.innerText; data.img = li.querySelector('span.img') ?.getAttribute('src').replace('cover/c', 'cover/l') || '//bgm.tv/img/no_icon_subject.png'; data.time = new Date(li.querySelector('span.tip_j').innerText); return data; }); const next = Array .from(e.querySelectorAll('#multipage a.p')) .pop().href.match(/page=(\d+)$/)[1]; if(p >= next) return list; return collects(type, p+1).then(next=>list.concat(next)); }; const menu = ce('ul'); document.body.appendChild(menu); const ma = name=>menu.appendChild(ce('li')).appendChild(ce(name)); menu.id = 'kotori-report-menu'; const msw = { _: true, get(){return this._}, set(v){this._ = v; menu.style.display = v ? 'block' : 'none';}, toggle(){this.set(!this.get());}, }; msw.toggle(); const btn = ce('a'); btn.onclick = ()=>msw.set(true); btn.className = 'chiiBtn'; btn.href = 'javascript:void(0)'; btn.title = '生成年鉴'; btn.innerHTML = '生成年鉴``).join(''); const typeSelect = ma('select'); typeSelect.innerHTML = Object.entries(types) .map(([v,t])=>``).join(''); let html2canvasloaded = false; const saveImage = (e, d)=>{ const done = ()=>{ html2canvasloaded = true; html2canvas(e,{ 'allowTaint': true, 'logging': false, 'backgroundColor': '#1c1c1c' }).then(canvas=>{ const div = ce('div'); div.id = 'kotori-report-canvas'; div.appendChild(ce('div')).onclick = ()=>div.remove(); div.appendChild(canvas); document.body.appendChild(div); d(); }); }; if(html2canvasloaded) return done(); const script = ce('script'); script.type = 'text/javascript'; script.src = 'https://html2canvas.hertzen.com/dist/html2canvas.min.js'; script.onload = done; document.body.appendChild(script); } const go = ma('div'); go.className = 'btn'; go.innerText = '生成'; const l = ['|', '/', '-', '\\']; const gen = async ()=>{ go.onclick = null; let i = 0; const id = setInterval(()=>go.innerText=`抓取数据中[${l[i++%4]}]`, 50); const y = parseInt(yearSelect.value) || year; const t = typeSelect.value || 'anime'; const list = (await collects(t)).sort((a,b)=>b.time-a.time); go.onclick = gen; clearInterval(id); go.innerText = '生成'; msw.set(false); const yearList = list.filter(({time})=>time.getFullYear()==y); const set = new Set(); const m = m=>set.has(m)? '': (set.add(m), `${m+1}月`); const ul = `

${y}年 Bangumi ${types[t]}年鉴 @${uid}

`; const content = ce('div'); content.className = 'content'; content.innerHTML = ul; const close = ce('div'); close.className = 'close'; close.onclick = ()=>div.remove(); const save = ce('div'); save.className = 'save'; const s = ()=>{ save.onclick = null; saveImage(content, ()=>save.onclick=s) }; save.onclick = s; const div = ce('div'); div.appendChild(close); div.appendChild(content); div.appendChild(save); div.id = 'kotori-report'; document.body.appendChild(div); }; go.onclick = gen; document.querySelector('#headerProfile h1.nameSingle > .rr').appendChild(btn); // style const style = ce('style'); document.head.appendChild(style); style.innerHTML = ` .btn { user-select: none; cursor: pointer; } #kotori-report-menu { color: #fff; position: fixed; display: block; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 20px; padding-top: 50px; background: #0d111788; backdrop-filter: blur(4px); border-radius: 10px; box-shadow: 2px 2px 10px #00000088; border: 1px solid #fc899422; width: 150px; } #kotori-report-menu::before { position: absolute; content: "菜单"; padding: 0 20px; top: -1px; right: -1px; left: -1px; height: 30px; line-height: 30px; background: #fc8994; backdrop-filter: blur(4px); border-radius: 10px 10px 0 0; } #kotori-report-menu > li { margin-top: 10px; } #kotori-report-menu > li:first-child { margin-top: 0; } #kotori-report-menu > li > .btn { width: 100%; padding: 10px 0; background: #fc899444; border: inset 2px solid #fc8994; text-align: center; border-radius: 5px; transition: all 0.3s; font-family: consolas, 'courier new', monospace, courier; } #kotori-report-menu > li > .btn:hover { width: 100%; padding: 10px 0; background: #fc8994; border: 2px solid #fc8994 inset; text-align: center; border-radius: 5px; transition: all 0.3s; } #kotori-report-canvas, #kotori-report { color: #fff; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.3); backdrop-filter: blur(2px); overflow: scroll; padding: 30px; scrollbar-width: none; -ms-overflow-style: none; } #kotori-report-canvas::-webkit-scrollbar, #kotori-report::-webkit-scrollbar { display: none; } #kotori-report-canvas > div, #kotori-report > .close { position: absolute; top: 0; right: 0; left: 0; bottom: 0; } #kotori-report > .save { position: absolute; top: 10px; right: 10px; width: 40px; height: 40px; background: #fc8994; border-radius: 40px; border: 4px solid #fc8994; cursor: pointer; box-shadow: 2px 2px 10px #00000088; user-select: none; line-height: 40px; background-size: 40px; background-image: url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMzMwIDMzMCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE2NSwwQzc0LjAxOSwwLDAsNzQuMDE4LDAsMTY1YzAsOTAuOTgsNzQuMDE5LDE2NSwxNjUsMTY1czE2NS03NC4wMiwxNjUtMTY1QzMzMCw3NC4wMTgsMjU1Ljk4MSwwLDE2NSwweiBNMTY1LDMwMGMtNzQuNDM5LDAtMTM1LTYwLjU2MS0xMzUtMTM1UzkwLjU2MSwzMCwxNjUsMzBzMTM1LDYwLjU2MSwxMzUsMTM1UzIzOS40MzksMzAwLDE2NSwzMDB6Ii8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTIxMS42NjcsMTI3LjEyMWwtMzEuNjY5LDMxLjY2NlY3NWMwLTguMjg1LTYuNzE2LTE1LTE1LTE1Yy04LjI4NCwwLTE1LDYuNzE1LTE1LDE1djgzLjc4N2wtMzEuNjY1LTMxLjY2NmMtNS44NTctNS44NTctMTUuMzU1LTUuODU3LTIxLjIxMywwYy01Ljg1OCw1Ljg1OS01Ljg1OCwxNS4zNTUsMCwyMS4yMTNsNTcuMjcxLDU3LjI3MWMyLjkyOSwyLjkzLDYuNzY4LDQuMzk1LDEwLjYwNiw0LjM5NWMzLjgzOCwwLDcuNjc4LTEuNDY1LDEwLjYwNy00LjM5M2w1Ny4yNzUtNTcuMjcxYzUuODU3LTUuODU3LDUuODU4LTE1LjM1NSwwLjAwMS0yMS4yMTVDMjI3LjAyMSwxMjEuMjY0LDIxNy41MjQsMTIxLjI2NCwyMTEuNjY3LDEyNy4xMjF6Ii8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE5NSwyNDBoLTYwYy04LjI4NCwwLTE1LDYuNzE1LTE1LDE1YzAsOC4yODMsNi43MTYsMTUsMTUsMTVoNjBjOC4yODQsMCwxNS02LjcxNywxNS0xNUMyMTAsMjQ2LjcxNSwyMDMuMjg0LDI0MCwxOTUsMjQweiIvPjwvc3ZnPg==); opacity: 0.8; z-index: 9999999999999; } #kotori-report > .content { max-width: 1102px; margin: 0 auto; } #kotori-report > .content > h1 { padding: 30px 0; text-align: center; } #kotori-report > .content > ul > li { display: inline-block; position: relative; width: 150px; height: 215px; background: #0008; margin: 2px; } #kotori-report > .content > ul > li span { width: 50px; height: 30px; position: absolute; top: 0; left: 0; line-height: 30px; text-align: center; font-size: 18px; background: #000a; backdrop-filter: blur(2px); } #kotori-report > .content > ul > li img { max-width: 150px; max-height: 220px; position: absolute; top: 0; left: 50%; transform: translateX(-50%); } #kotori-report-canvas > canvas { position: absolute; top: 0; left: 50%; transform: translateX(-50%) scale(0.8); } `; })();