UNPKG

hexo-bilibili-bangumi

Version:
362 lines (349 loc) 13.3 kB
/* eslint-disable no-underscore-dangle */ const fs = require('hexo-fs'); const path = require('path'); const axios = require('axios'); const hexoLog = require('hexo-log'); const log = typeof hexoLog.default === 'function' ? hexoLog.default({ debug: false, silent: false }) : hexoLog({ debug: false, silent: false }); const ProgressBar = require('progress'); const cheerio = require('cheerio'); const tunnel = require('tunnel'); const bangumiData = require('bangumi-data'); const jp2cnName = (name) => bangumiData.items.find((item) => item.title === name)?.titleTranslate?.['zh-Hans']?.[0] || name; const TYPE = { 1: '书籍', 2: '动画', 3: '音乐', 4: '游戏', 6: '三次元' }; const getBangumiDataFromBangumiSubject = async (items, sourceDir, proxy, coverMirror) => (await Promise.allSettled( items.map( async (item, index) => { const cachePath = path.join(sourceDir, '/_data/Bangumi-Subject-Cache'); const subjectPath = path.join(cachePath, `${item.id}.json`); if (!fs.existsSync(cachePath)) { fs.mkdirSync(cachePath); } if (fs.existsSync(subjectPath)) { return { config: { itemData: item }, data: fs.readFileSync(subjectPath).toString(), status: 200 }; } const options = { itemData: item, responseType: 'json', validateStatus(status) { return (status >= 200 && status < 300) || status === 403; }, proxy: false, timeout: 30 * 1000 }; if (proxy?.host && proxy?.port) { options.httpsAgent = tunnel.httpsOverHttp({ proxy, options: { rejectUnauthorized: false } }); } return await axios.get(`https://cdn.jsdelivr.net/gh/czy0729/Bangumi-Subject@master/data/${parseInt(parseInt(item.id, 10) / 100, 10)}/${item.id}.json`, options) .then((response) => response) .catch((error) => { if (error.response) { console.error('Error', error.response.status); } else { console.error('Error', error.stack); } return { config: { itemData: item }, error }; }); } ) )).map(({ value, reason }) => { const { config, data, status } = value || reason; let bangumiData = data; if (reason || status === 403 || !data) { return { id: config?.itemData.id, title: jp2cnName(config.itemData.name), type: '未知', cover: config.itemData.cover, score: '-', des: '-', wish: '-', doing: '-', collect: '-', totalCount: '-' }; } if (typeof data === 'string') { try { // eslint-disable-next-line no-param-reassign bangumiData = JSON.parse(data.replace(/(?<!":|\/|\\)("[^":,\]}][^"]*?[^":])"(?!,|]|}|:)/g, '\\$1\\"')); } catch (e) { console.log(`Error Id: ${config.itemData.id}`); console.error(e); } } const { id, name, type, image, rating: { score } = {}, summary, collection: { wish, doing, collect } = {}, eps, epsLength } = bangumiData; const totalCount = epsLength || eps?.length; const subjectPath = path.join(sourceDir, '/_data/Bangumi-Subject-Cache', `${config.itemData.id}.json`); if (!fs.existsSync(subjectPath)) { if (id && name && type && image && score && summary && wish && doing && collect && eps?.length > 0) { fs.writeFileSync(subjectPath, JSON.stringify({ id, name, type, image, rating: { score }, summary, collection: { wish, doing, collect }, epsLength: eps.length })); } } return { id: id || config.itemData.id, title: jp2cnName(name || config.itemData.name), type: TYPE[type] || '未知', cover: coverMirror + image || config.itemData.cover, score: score || '-', des: summary?.replace(/\r?\n/g, '').trim() || '-', wish: wish || '-', doing: doing || '-', collect: collect || '-', totalCount: totalCount ? `全${totalCount}${type === 6 ? '集' : '话'}` : '-', myStars: config.itemData.myStars, myComment: config.itemData.myComment }; }); const getBangumiDataFromBangumiApi = async (items, sourceDir, proxy, coverMirror) => (await Promise.allSettled( items.map( async (item, index) => { const cachePath = path.join(sourceDir, '/_data/Bangumi-Api-Cache'); const subjectPath = path.join(cachePath, `${item.id}.json`); if (!fs.existsSync(cachePath)) { fs.mkdirSync(cachePath); } if (fs.existsSync(subjectPath)) { return { config: { itemData: item }, data: JSON.parse(fs.readFileSync(subjectPath).toString()), status: 200 }; } const options = { itemData: item, responseType: 'json', validateStatus(status) { return (status >= 200 && status < 300) || status === 403; }, proxy: false, timeout: 30 * 1000 }; if (proxy?.host && proxy?.port) { options.httpsAgent = tunnel.httpsOverHttp({ proxy, options: { rejectUnauthorized: false } }); } return await axios.get(`https://api.bgm.tv/v0/subjects/${item.id}`, { ...options, headers: { 'user-agent': 'HCLonely/hexo-bilibili-bangumi (https://github.com/HCLonely/hexo-bilibili-bangumi)' } }) .then((response) => response) .catch((error) => { if (error.response) { console.error('Error', error.response.status); } else { console.error('Error', error.stack); } return { config: { itemData: item }, error }; }); } ) )).map(({ value, reason }) => { const { config, data, status } = value || reason; if (reason || status === 403 || !data) { return { id: config.itemData.id, title: jp2cnName(config.itemData.name), type: '未知', cover: config.itemData.cover, score: '-', des: '-', wish: '-', doing: '-', collect: '-', totalCount: '-' }; } // eslint-disable-next-line camelcase const { id, name, name_cn, type, images: { common: image }, rating: { score } = {}, summary, collection: { wish, doing, collect } = {}, eps, total_episodes } = data; // eslint-disable-next-line camelcase const totalCount = total_episodes || eps; const subjectPath = path.join(sourceDir, '/_data/Bangumi-Api-Cache', `${config.itemData.id}.json`); if (!fs.existsSync(subjectPath)) { // eslint-disable-next-line camelcase if (id && name && type && image && score && summary && wish && doing && collect && (total_episodes || eps)) { // eslint-disable-next-line camelcase fs.writeFileSync(subjectPath, JSON.stringify({ id, name, name_cn, type, images: { common: image }, rating: { score }, summary, collection: { wish, doing, collect }, eps, total_episodes })); } } return { id: id || config.itemData.id, // eslint-disable-next-line camelcase title: name_cn || jp2cnName(name || config.itemData.name), type: TYPE[type] || '未知', cover: coverMirror + image || config.itemData.cover, score: score || '-', des: summary?.replace(/\r?\n/g, '').trim() || '-', wish: wish || '-', doing: doing || '-', collect: collect || '-', totalCount: totalCount ? `全${totalCount}${type === 6 ? '集' : '话'}` : '-', myStars: config.itemData.myStars, myComment: config.itemData.myComment }; }); const getItemsId = async ({ vmid, type, status, showProgress, sourceDir, proxy, infoApi, host, coverMirror }) => { const getBangumiData = infoApi === 'bgmSub' ? getBangumiDataFromBangumiSubject : getBangumiDataFromBangumiApi; const items = []; let bar; const options = { proxy: false, timeout: 30 * 1000 }; if (proxy?.host && proxy?.port) { options.httpsAgent = tunnel.httpsOverHttp({ proxy, options: { rejectUnauthorized: false } }); } const response = await axios.get(`https://${host}/${type}/list/${vmid}/${status}?page=1`, options); const username = response.request.path.match(/(anime|game|real)\/list\/(.*?)\//)?.[2]; if (!username) { return console.error('Failed to get "username"!'); } if (response?.data) { const $ = cheerio.load(response.data); const pageNum = Math.max(...$('#multipage').find('a') .map((index, el) => parseInt($(el).attr('href') ?.match(/\?page=([\d]+)/)?.[1] || '0', 10)) .get()) || $('#multipage').find('a').length; items.push(...await getBangumiData($('#browserItemList>li').map((index, el) => ({ id: $(el).attr('id') .replace('item_', ''), cover: $(el).find('img') .attr('src'), name: $(el).find('h3>a') .text(), myStars: $(el).find('.starlight') .attr('class') ?.match(/stars([\d]+)/)?.[1], myComment: $(el).find('#comment_box') .text() .trim() })) .get(), sourceDir, proxy, coverMirror)); if (showProgress) { // eslint-disable-next-line no-nested-ternary bar = new ProgressBar(`正在获取 ${status === 'wish' ? '[想看]' : (status === 'do' ? '[在看]' : '[已看]')} ${type === 'game' ? '游戏' : type === 'real' ? '追剧' : '番剧'} [:bar] :percent :elapseds`, { total: pageNum < 2 ? 1 : pageNum, complete: '█' }); bar.tick(); } if (pageNum < 2) { return items; } // eslint-disable-next-line no-plusplus for (let i = 2; i <= pageNum; i++) { if (showProgress) bar.tick(); const response = await axios.get(`https://${host}/${type}/list/${username}/${status}?page=${i}`, { ...options, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36 Edg/97.0.1072.69' }); const $ = cheerio.load(response.data); items.push(...await getBangumiData($('#browserItemList>li').map((index, el) => ({ id: $(el).attr('id') .replace('item_', ''), cover: $(el).find('img') .attr('src'), name: $(el).find('h3>a') .text(), myStars: $(el).find('.starlight') .attr('class') ?.match(/stars([\d]+)/)?.[1], myComment: $(el).find('#comment_box') .text() .trim() })) .get(), sourceDir, proxy, coverMirror)); } } // log.info('正在获取番剧详细数据,耗时与追番数量成正比,请耐心等待...'); return items; }; module.exports.getBgmData = async function getBgmData({ vmid, type, showProgress, sourceDir, extraOrder, pagination, proxy, infoApi, host, coverMirror }) { log.info('Getting bangumis, please wait...'); const typePathMap = { bangumi: 'anime', game: 'game', cinema: 'real' }; const startTime = new Date().getTime(); const wantWatch = await getItemsId({ vmid, type: typePathMap[type], status: 'wish', showProgress, sourceDir, proxy, infoApi, host, coverMirror }); const watching = await getItemsId({ vmid, type: typePathMap[type], status: 'do', showProgress, sourceDir, proxy, infoApi, host, coverMirror }); const watched = await getItemsId({ vmid, type: typePathMap[type], status: 'collect', showProgress, sourceDir, proxy, infoApi, host, coverMirror }); const endTime = new Date().getTime(); log.info(`${wantWatch.length + watching.length + watched.length} ${type}s have been loaded in ${endTime - startTime} ms`); const bangumis = { wantWatch, watching, watched }; if (!fs.existsSync(path.join(sourceDir, '/_data/'))) { fs.mkdirSync(path.join(sourceDir, '/_data/')); } fs.writeFile(path.join(sourceDir, `/_data/${type}s.json`), JSON.stringify(bangumis), (err) => { if (err) { log.info(`Failed to write data to _data/${type}s.json`); console.error(err); } else { log.info(`Bangumi ${type}s data has been saved`); } }); if (pagination) { const allBangumis = { ...bangumis }; // extra bangumis if (fs.existsSync(path.join(sourceDir, `/_data/extra_${type}s.json`))) { const { wantWatchExtra, watchingExtra, watchedExtra } = JSON.parse(fs.readFileSync(path.join(this.source_dir, `/_data/extra_${type}s.json`))); if (wantWatchExtra) { if (extraOrder === 1) { allBangumis.wantWatch = [...wantWatchExtra, ...allBangumis.wantWatch]; } else { allBangumis.wantWatch = [...allBangumis.wantWatch, ...wantWatchExtra]; } } if (watchingExtra) { if (extraOrder === 1) { allBangumis.watching = [...watchingExtra, ...allBangumis.watching]; } else { allBangumis.watching = [...allBangumis.watching, ...watchingExtra]; } } if (watchedExtra) { if (extraOrder === 1) { allBangumis.watched = [...watchedExtra, ...allBangumis.watched]; } else { allBangumis.watched = [...allBangumis.watched, ...watchedExtra]; } } } fs.writeFile(path.join(sourceDir, `/${type}s.json`), JSON.stringify(allBangumis), (err) => { if (err) { log.info(`Failed to write data to ${type}s.json`); console.error(err); } else { log.info(`Bangumi ${type}s data has been saved`); } }); } };