Citing Bangumi

Cite books/games/animes/music on Bangumi.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==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)
})();