// ==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 = `