Citing Bangumi

Cite books/games/animes/music on Bangumi.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         Citing Bangumi
// @namespace    https://github.com/0And1Story
// @version      0.2.1
// @description  Cite books/games/animes/music on Bangumi.
// @author       0And1Story
// @homepage     https://bgm.tv/dev/app/5701
// @license      MIT
// @match        https://bgm.tv/subject/*
// @match        https://bangumi.tv/subject/*
// @match        https://chii.in/subject/*
// @exclude      https://*/subject/*/*
// @icon         https://bgm.tv/img/favicon.ico
// @grant        none
// @run-at       document-end
// ==/UserScript==

const debug = false

const mapping = {
    title: '中文名',
    author: ['作者', '剧本', '原作', '脚本', '作曲', '导演', '编剧'],
    isbn: 'ISBN',
    publisher: ['出版社', '开发', '动画制作', '厂牌', '制作'],
    date: ['发售日', '发行日期', '放送开始', '发售日期', '开始'],
    chineseTitle: '中文名',
    alias: '别名'
}

const formatMapping = {
    '书籍': 'book',
    '游戏': 'software',
    '动画': 'video',
    '音乐': 'music',
    '三次元': 'video'
}

const availableBibKeys = [
    'title',
    'author',
    'isbn',
    'publisher',
    'year',
    'month',
    'day',
    'url'
]
const keepRawBibKeys = ['title']

function parseSubContainer(li) {
    const obj = {}
    const arr = []

    const subSection = li.querySelector('.sub_section')
    const key = subSection?.querySelector('.tip')?.textContent.replace(':', '').trim()

    if (!key) return [undefined, undefined]

    const subGrid = li.querySelector('.sub_grid')
    if (subGrid) {
        const subValues = [...subGrid.querySelectorAll('.tag')].map(n => n.textContent.trim())
        arr.push(...subValues)
        return [key, arr]
    }

    const firstValue = [...subSection.childNodes].filter(node => node.nodeType === Node.TEXT_NODE)[0]?.textContent.trim()
    if (firstValue) arr.push(firstValue)

    const subs = li.querySelectorAll('.sub')

    let isObject = [...li.querySelectorAll('.sub .tip')].filter(n => n.style.display !== 'none').length > 0

    if (isObject) {
        for (const sub of subs) {
            const tip = sub.querySelector('.tip')
            const subKey = tip.textContent.trim()
            const subValue = [...sub.childNodes].filter(node => node.nodeType === Node.TEXT_NODE)[0]?.textContent.trim()
            obj[subKey] = subValue
        }
        return [key, obj]
    } else {
        for (const sub of subs) {
            const subValue = [...sub.childNodes].filter(node => node.nodeType === Node.TEXT_NODE)[0]?.textContent.trim()
            arr.push(subValue)
        }
        return [key, arr]
    }
}

function getInfoKeyValue(li) {
    if (!li) return [undefined, undefined]
    if (li.classList.contains('sub_container')) return parseSubContainer(li)

    const content = li.textContent.trim()
    const index = content.indexOf(': ')
    if (index === -1) return [content, undefined]
    return [content.slice(0, index), content.slice(index + 2)]
}

function parseDate(str) {
    if (!str) return {}
    if (Array.isArray(str)) {
        let result = []
        for (const s of str) {
            result.push(parseDate(s))
        }
        return {
            year: result.map(r => r.year),
            month: result.map(r => r.month),
            day: result.map(r => r.day)
        }
    } else if (typeof str === 'object') {
        let result = {}
        for (const [key, value] of Object.entries(str)) {
            result[key] = parseDate(value)
        }
        return {
            year: Object.fromEntries(Object.entries(result).map(([key, value]) => [key, value.year])),
            month: Object.fromEntries(Object.entries(result).map(([key, value]) => [key, value.month])),
            day: Object.fromEntries(Object.entries(result).map(([key, value]) => [key, value.day]))
        }
    }

    str = str.trim()
    if (str.match(/^\d{4}$/)) return { year: str }
    if (str.match(/^\d+年\d+月\d+日$/)) str = str.replace(/^(\d+)年(\d+)月(\d+)日$/, `$1-$2-$3`)

    const date = new Date(Date.parse(str))
    if (!date.getFullYear()) return { year: str }

    return {
        year: date.getFullYear().toString(),
        month: (date.getMonth() + 1).toString(),
        day: date.getDate().toString()
    }
}

function getOriginalTitle() {
    return document.querySelector('#headerSubject > h1 > a')?.textContent
}

function getUrl() {
    return window.location.href
}

function getSubjectType() {
    return document.querySelector('#siteSearchSelect')?.selectedOptions?.[0]?.textContent
}

function getInfobox() {
    const infobox = document.querySelector('#infobox')
    if (!infobox) {
        console.error('Cannot find infobox, BibTeX generation failed.')
        return {}
    }

    let info = {}
    const lis = [...infobox.children]
    for (const li of lis) {
        const [key, value] = getInfoKeyValue(li)
        if (key) info[key] = value
    }
    return info
}

function toBibInfo(info) {
    let bib = {}

    for (const [key, value] of Object.entries(mapping)) {
        let values = value
        if (!Array.isArray(value)) values = [value]
        for (const value of values) {
            if (info.hasOwnProperty(value)) {
                bib[key] = info[value]
                break
            }
        }
    }

    const title = getOriginalTitle()
    if (title) bib.title = title
    if (bib.hasOwnProperty('date')) {
        bib = { ...bib, ...parseDate(bib.date) }
    }
    bib.url = getUrl()

    return bib
}

function filterBibInfo(bib) {
    const bibEntries = Object.entries(bib)
    .filter(([key, value]) => availableBibKeys.indexOf(key) !== -1)
    .map(([key, value]) => [key, typeof value === 'object' ? Object.values(value) : value])
    .map(([key, value]) => [key, Array.isArray(value) ? value[0] : value])
    return Object.fromEntries(bibEntries)
}

function toBibTex(bib) {
    let bibtex = ''
    const indent = '  '

    const filteredBib = filterBibInfo(bib)
    if (debug) console.log(filteredBib)

    bibtex += `@${formatMapping[getSubjectType()]}{${bib.chineseTitle || bib.title},\n`
    const padLength = Math.max(...Object.keys(filteredBib).map(key => key.length))
    for (const [key, value] of Object.entries(filteredBib)) {
        if (keepRawBibKeys.indexOf(key) !== -1) bibtex += `${indent}${key.padEnd(padLength)} = {{${value}}},\n`
        else bibtex += `${indent}${key.padEnd(padLength)} = {${value}},\n`
    }
    bibtex += `}`

    return bibtex
}

function displayBibTex(bibtex) {
    if (document.querySelector('#bangumi-bibtex')) return

    const box = document.querySelector('#subject_detail')
    if (!box) console.error('Cannot find subject detail box, display BibTeX failed.')

    const div = document.createElement('div')
    div.id = 'bangumi-bibtex'
    div.classList.add('subject_tag_section')

    const h2 = document.createElement('h2')
    h2.classList.add('subtitle')
    h2.textContent = 'BibTeX'

    const inner = document.createElement('div')
    inner.classList.add('inner')

    const pre = document.createElement('pre')
    pre.textContent = bibtex

    inner.appendChild(pre)
    div.appendChild(h2)
    div.appendChild(inner)
    // div.innerHTML = `<h2 class="subtitle">BibTeX</h2><div class="inner"><pre>${bibtex}</pre></div>`

    box.appendChild(div)
}

(function () {
    'use strict';

    const subjectType = getSubjectType()
    if (!(subjectType in formatMapping)) return

    const info = getInfobox()
    if (debug) console.log(info)
    const bib = toBibInfo(info)
    if (debug) console.log(bib)
    const bibtex = toBibTex(bib)
    if (debug) console.log(bibtex)
    displayBibTex(bibtex)
})();