mikananiBgmScore

在蜜柑计划首页显示 Bangumi 评分及链接。

目前为 2023-01-16 提交的版本。查看 最新版本

// ==UserScript==
// @name         mikananiBgmScore
// @name:zh-CN   在蜜柑计划首页显示 Bangumi 评分及链接。
// @namespace    https://github.com/kjtsune/UserScripts
// @version      0.1
// @description  在蜜柑计划首页显示 Bangumi 评分及链接。
// @author       kjtsune
// @match        https://mikanani.me/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=mikanani.me
// @grant        none
// ==/UserScript==
'use strict';

let config = { logLevel: 2 };

let logger = {
    error: function (...args) {
        if (config.logLevel >= 1)
            console.log("%cerror", "color: yellow; font-style: italic; background-color: blue;",
                args);
    },
    info: function (...args) {
        if (config.logLevel >= 2)
            console.log("%cinfo", "color: yellow; font-style: italic; background-color: blue;",
                args);
    },
    debug: function (...args) {
        if (config.logLevel >= 3)
            console.log("%cdebug", "color: yellow; font-style: italic; background-color: blue;",
                args);
    },
}

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

async function getJSON(url) {
    try {
        const response = await fetch(url);
        logger.info(`fetch ${url}`)
        if (response.status >= 200 && response.status < 400)
            return await response.json();
        console.error(`Error fetching ${url}:`, response.status, response.statusText, await response.text());
    }
    catch (e) {
        console.error(`Error fetching ${url}:`, e);
    }
}

async function getBgmJson(bgmId) {
    let url = `https://api.bgm.tv/v0/subjects/${bgmId}`
    return await getJSON(url)
}

function multiTimesSeletor(storage = null, seletorAll = false, ...cssSeletor) {
    const seletor = cssSeletor[0]
    const restSeletor = cssSeletor.slice(1)
    if (!seletor) return storage;

    function queryForArray(query, array) {
        let result = [];
        for (let index = 0; index < array.length; index++) {
            const element = array[index];
            let res = element.querySelectorAll(query);
            if (!res) logger.error("not result", query, array);
            result.push(...res);
        }
        return result
    }
    if (seletorAll) {
        storage = storage || [document]
        let res = queryForArray(seletor, storage);
        if (res) storage = res;
        storage && logger.debug('storage', storage.length, seletor, restSeletor);
        if (!restSeletor) {
            return storage;
        } else {
            return multiTimesSeletor(storage, true, ...restSeletor);
        }
    } else {
        storage = storage || document;
        const lastRes = storage;
        storage = storage.querySelector(seletor);
        storage && logger.debug('storage', storage, seletor);
        if (!storage) logger.error("not result", seletor, lastRes);
        if (!restSeletor) {
            return storage;
        } else {
            return multiTimesSeletor(storage, false, ...restSeletor);
        }
    }
}

async function myFetch(url, selector = null, selectAll = false) {
    let response = await fetch(url);
    let text = await response.text();
    const parser = new DOMParser();
    const htmlDocument = parser.parseFromString(text, "text/html");
    const element = htmlDocument.documentElement;
    if (!selector) return element;
    if (selectAll) {
        return element.querySelectorAll(selector);
    } else {
        return element.querySelector(selector);
    }
}

async function getBgmId(mikanUrl) {
    let selector = "p.bangumi-info > a[href*='tv/subject']";
    let bgm = await myFetch(mikanUrl, selector);
    if (bgm) bgm = bgm.href.split("/").slice(-1)[0];
    return bgm
}

class MyStorage {
    constructor(prefix, splitStr = '|', expireDay = 0) {
        this.prefix = prefix;
        this.splitStr = splitStr;
        this.expireMs = expireDay * 864E5
    }

    _keyGenerator(key) {
        return `${this.prefix}${this.splitStr}${key}`
    }

    get(key, defalut = null) {
        key = this._keyGenerator(key);
        let res = localStorage.getItem(key);
        if (this.expireMs && res) {
            res = JSON.parse(localStorage.getItem(key)).value;
        }
        res = res || defalut;
        return res
    }

    set(key, value) {
        key = this._keyGenerator(key);
        if (this.expireMs) {
            value = JSON.stringify({ timestamp: Date.now(), value: value })
        }
        localStorage.setItem(key, value)
    }

    del(key) {
        key = this._keyGenerator(key);
        if (key in localStorage) { localStorage.removeItem(key) };
    }

    checkIsExpire(key) {
        key = this._keyGenerator(key);
        if (!(key in localStorage)) return true;
        if (!this.expireMs && key in localStorage) { return false };
        let timestamp = JSON.parse(localStorage.getItem(key)).timestamp;
        if (!timestamp) throw `checkIsExpire not work , not timestamp, key: ${key}`;
        if (timestamp + this.expireMs < Date.now()) {
            return true;
        } else {
            return false;
        }
    }
}

let mikanBgmStorage = new MyStorage("mikan");
let bgmInfoStorage = new MyStorage("bgm", undefined, 7);

async function storeMikanBgm(mikanElementList, storeBgmInfo = false) {

    async function checkBgmInfoExist(mkId) {
        let bgmId = mikanBgmStorage.get(mkId);
        if (!bgmId) return;
        if (bgmInfoStorage.checkIsExpire(bgmId)) {
            bgmInfoStorage.set(bgmId, await parseBgmInfo(bgmId));
        }
    }

    for (const element of mikanElementList) {
        let mikanUrl = element.href;
        let mikanId = mikanUrl.split('/').slice(-1)[0];
        if (storeBgmInfo) await checkBgmInfoExist(mikanId);
        if (!mikanBgmStorage.checkIsExpire(mikanId)) { continue };
        let bgmId = await getBgmId(mikanUrl);
        logger.info("fetch run", mikanId)
        await sleep(1000);
        if (mikanBgmStorage.checkIsExpire(mikanId)) {
            mikanBgmStorage.set(mikanId, bgmId);
            logger.info(`set ${mikanId} to ${bgmId}`);
        }
    }
}

async function parseBgmInfo(bgmId, stringify = false) {
    let bgmJson = await getBgmJson(bgmId);
    let score = (bgmJson) ? bgmJson.rating.score : 0.1;
    let summary = (bgmJson) ? bgmJson.summary : "maybe 18x";
    let res = { score: score, summary: summary }
    res = (stringify) ? JSON.stringify(res) : res;
    return res
}

async function addScoreSummaryToHtml(mikanElementList) {
    for (const element of mikanElementList) {
        let scoreElement = element.nextElementSibling;
        if (scoreElement) continue;
        let mikanUrl = element.href;
        let mikanId = mikanUrl.split('/').slice(-1)[0];
        let bgmId = mikanBgmStorage.get(mikanId);
        let bgmInfo = bgmInfoStorage.get(bgmId);
        if (!bgmId || !bgmInfo) continue;
        let bgmUrl = `https://bgm.tv/subject/${bgmId}`
        let score = bgmInfo.score;
        let summary = bgmInfo.summary
        let bgmHtml = `<a href="${bgmUrl}" target="_blank" title="${summary}" id="bgmScore">${score}</a>`
        element.insertAdjacentHTML("afterend", bgmHtml);
        let pathName = element.pathname;
        let mobileElement = document.querySelectorAll(`a[href="${pathName}"`)[1].nextElementSibling
        let title = mobileElement.textContent;
        let mobileHtml = `<a href="${bgmUrl}" target="_blank" title="${summary}" id="bgmScore">${title} ${score}</a>`
        mobileElement.insertAdjacentHTML("afterend", mobileHtml);
        mobileElement.remove();
    }
}

async function main() {
    let animeList = multiTimesSeletor(null, true, "div.sk-bangumi", "a[href^='/Home/Bangumi']");
    // animeList = animeList.slice(0, 81);
    await storeMikanBgm(animeList, true);
    await addScoreSummaryToHtml(animeList);
    logger.info(animeList)

}

(function loop() {
    setTimeout(async function () {
        let start = Date.now()
        await main();
        let usedSec = (Date.now() - start) / 1000;
        if (usedSec > 0.01) logger.info(`used time ${usedSec}`);
        loop();
    }, 2000);
})();