给博客添加追番页面 发表于 2022-06-26 更新于 2023-10-25
字数总计 6.3k 阅读时长 33分钟 阅读量
更新日志 支持删除指定番剧(包括已经锁定的) 修复配置写在主题配置中无法正确读取的问题 移除自动构建json
文件的功能 添加锁功能,允许用户锁住列表顺序 拆分json
文件 修正追番列表 JSON 文件生成为空的问题 在自动生成 JSON 文件的基础上保留命令生成的功能 修正追番列表编号错乱的问题 生成 JSON 文件从命令手动生成改为自动生成 介绍 本篇博文中的script
代码主要从hexo-bilibili-bangumi 插件中 CV 而来,本人进行了一些细节上的修改,其余代码为本人自行编写。
想要预览前端页面可以点击这里 进行查看。
目前前端实现了以下功能:
切换页面后支持使用浏览器的回退功能进行返回(会留下比较多的历史记录) 通过 JS 控制页面显示 将番剧列表信息存储在 JSON 中而不是 HTML 内 自动修正 URL 的错误参数(不会删掉多余的参数) 没有提供链接的番剧不再跳转到 404 页面 根据浏览器支持情况自动选择是否使用webp
格式的图片 请注意:使用该插件需要在 Bilibili 设置中公开追番列表 ,否则无法获取到追番信息。
用法 执行update
指令即可生成相关文件。
指令 hexo bangumi -u
| 生成JSON文件hexo bangumi -d
| 删除JSON文件hexo bangumi -d \[番剧名称 1] \[番剧名称 2] ...
| 删除指定的番剧创建页面 执行命令(名字自己随意,根据喜好修改):
1 hexo new page "这里写页面URL名称(%s)"
打开[hexo]/source/%s/index.md
,在两个---
之间添加type: "bangumis"
,修改完之后你的index.md
应该与下面的样例类似:
1 2 3 4 5 6 7 8 --- title : 追 番 列 表date : 2022-06-22 19:02:32 type : "bangumis" ---
配置文件 修改配置文件(hexo
或主题的配置文件均可):
1 2 3 4 5 6 7 8 9 10 11 12 + bilibili: + # 是否启用 + enable: true + # 你的B站 vmid + vmid: ... + # 拓展JSON的文件名(/source/_data/*.json) + extra: 'extra_bangumis' + # 锁止追番列表,缺省为false + locks: + wantLock: false + ingLock: false + edLock: false
vmid 打开你的 B 站个人主页,链接格式应该如下:
1 https://space.bilibili.com/***?spm_id_from=...
其中的***
就是你的vmid
。
拓展文件 拓展文件是用于手动向追番列表中添加B站没有的番剧,JSON 格式完全兼容hexo-bangumi-bilibili
插件的格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { "watchedExtra" : [ { "title" : "库特wafter" , "type" : "番剧" , "area" : "日本" , "cover" : "https://pic.rmb.bdstatic.com/bjh/4f1d277c4dcc90673e36115d4d673ae1.jpeg" , "id" : 0 , "follow" : "-" , "view" : "-" , "danmaku" : "-" , "coin" : "-" , "score" : "-" , "tags" : [ "爱情" , "青春" ] , "index" : 0 } ] }
描述 key
值可以为(区分大小写):
watchedExtra
/watched
: 已看watchingExtra
/watching
: 在看wantWatchExtra
/wantWatch
: 想看 属性值描述(中括号中为默认值):
title
- 番剧名称type
- 类型[番剧]
area
- 区域[日本]
cover
- 封面id
- 番剧地址,没有地址请填0
,非B站地址请填写完整地址(如:"https://kmar.top/"
) [0]
follow
- 追番人数[-]
view
- 播放数量[-]
danmaku
- 弹幕数量[-]
coin
- 硬币数量[-]
score
- 评分[-]
tags
- 标签[]
index
- 定位编号[0]
定位编号 定位编号是为了确定该番剧卡片所在的位置,在生成 JSON 的时候,代码会自动给从 B 站获取到的番剧从0
开始编号。
显示时会按照定位编号从大到小进行排序,拓展信息中定位编号填写n
表示将该卡片放置在定位编号为n
的卡片的上面或下面。
在页面中,查看指定卡片的 HTML 代码,就能看到一个名为index
的属性,其值就是其本身的定位编号。
Lock 上锁的作用是什么呢?如果开启了lock
那么如果这个番剧在 B 站被下架,在更新追番列表时其仍然不会被移除。同时,如果你在 B 站修改了番剧的顺序,追番列表中的番剧顺序仍然不会改变。
就是说开启了锁之后番剧的顺序就绝对不会改变,后插入的番剧一定出现在先插入的番剧的后面。
开启该功能不会影响数据更新,在执行命令时仍然会更新所有番剧的详情数据。
注意:建议仅为已看列表开启该功能。
锁止用到的数据保存在all.json
中,请勿删除这个文件,否则会使锁止失效。
教程 本教程基于 Butterfly 主题编写,其它主题的用户请根据实际情况修改代码
修改 [butterfly]\layout\page.pug
:
1 2 3 4 5 6 7 8 9 10 11 case page.type when 'tags' include includes/page/tags.pug when 'link' include includes/page/flink.pug when 'categories' include includes/page/categories.pug + when 'bangumis' + include includes/page/bangumi.pug default include includes/page/default-page.pug
新建 [butterfly]\layout\includes\page\bangumi.pug
:
我是把 JS 直接写到 HTML 里面了,要是不想的话也可以自己挪出去,挪出去的话开启PJAX的用户记得打上no-pjax
的标签。
注意:如果你的博客开启了 pjax,请务必使用 pjax 兼容的方案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 != page. content #bangumis #bangumi-top .multi #cats p 分类 .list .wantWatch 想看 .watching 在看 .watched 已看 .multi #tags p 标签 .list .all .active 所有 div#inner .bangumi-tabs #bangumi-bottom button#bottom-first 首页 button#bottom-pre 上一页 p#bottom-num 0/0 button#bottom-next 下一页 button#bottom-end 尾页 script . document. addEventListener ( 'DOMContentLoaded' , async function fun ( ) { function ApartJson ( root, amount, dataAmount, size ) { let readIndex = 0 const fileList = new Map() const lastIndex = Math.ceil(size / dataAmount) - 1 /** * 读取指定下标的文件 * @param index {number} 文件下标 * @return {Promise} */ const readFile = index => new Promise(resolve => { if ( index > lastIndex || index < 0 ) throw ` ${ index} not in [0, ${ lastIndex} ] ` if ( fileList. has ( index) ) { let id const task = () => { const cache = fileList.get(index) if ( cache) { if ( id) clearInterval ( id) resolve( cache ) } } task () id = setInterval(task, 100) } else { fileList.set ( index , null ) fetch( `${root}/${index}.json`) .then ( response = > response. json ( ) .then ( json = > { fileList.set( index , json ) resolve( json ) })) } }) const readHelper = id => new Promise(resolve => { if ( size && id > size) return resolve ( null ) const fileIndex = Math.floor(id / dataAmount) const innerIndex = id % dataAmount readFile( fileIndex ) .then (json = > { const keys = Object.keys(json) resolve( json[keys[innerIndex]]) }) }) /** * 读取上一项 * @return {Promise} */ this.readNext = () => readHelper(readIndex++) /** * 读取上一项 * @return {Promise} */ /** * 设置页码,从0开始 * @param page {number} 页面编号 */ this.setPage = page => { readIndex = page * amount } /** 获取页码数量 */ this.size = () => Math.ceil(size / amount) /** 获取数据总量 */ this.amount = () => size /** * 判断指定页面是否为末页 * @param page {number| null} 指定页面编号,留空为当前页面编号 * @return {boolean} */ this.isLastPage = (page = null) => { if ( page === null ) page = Math. floor ( readIndex / amount) return page === this.size() - 1 } } const isSupportWebp = (() => { try { return document.createElement('canvas').toDataURL('image/webp', 0.5).indexOf('data:image/webp') === 0; } catch (ignore) { return false; } })() const root = '/bilibili/' const config = await (await fetch(`${root}config.json`)).json() const sizeList = config.size const maxCount = 10 const dataAmount = config.amount const jsonMap = { wantWatch: new ApartJson( `${root}wantWatch`, maxCount , dataAmount , sizeList.wantWatch ) , watching: new ApartJson( `${root}watching`, maxCount , dataAmount , sizeList.watching ) , watched: new ApartJson( `${root}watched`, maxCount , dataAmount , sizeList.watched ) } /** 处理参数 */ const parseArg = () => { const url = location.href let arg if ( url. endsWith ( '/' ) ) { arg = {id: 'watching', page: 1} } else { arg = JSON.parse(decodeURIComponent(location.hash.substring(1))) if ( ! arg. id || ( arg. id !== 'watching' && arg.id !== 'wantWatch' && arg.id !== 'watched')) arg.id = 'watching' if ( ! arg. page || arg. page < 1 ) arg. page = 1 } sessionStorage.setItem ( 'bangumis', JSON.stringify( arg ) ) return arg } const arg = await parseArg() let tagFilter = null const top = document.querySelector('#bangumi-top') const cats = top.querySelector('#cats') const tags = top.querySelector('#tags') const tabs = document.querySelector('.bangumi-tabs') /** 更新列表内容 */ async function update(updateURL = true) { tabs.querySelectorAll ( 'button') .forEach ( button = > button. classList. add ( 'disable' ) ) top.querySelectorAll ( 'div') .forEach ( div = > div. classList. add ( 'disable' ) ) for (let value of cats.querySelector('.list').children) { if ( value. classList. contains ( arg. id) ) value. classList. add ( 'active' ) else value. classList. remove ( 'active' ) } const json = jsonMap[arg.id] json.setPage ( arg.page - 1 ) function buildCard(title, img, href, follow, type, area, play, coin, danmaku, score, tagList, index) { if ( ! img. startsWith ( 'http' ) ) img = ` https://i0.hdslb.com/bfs/bangumi/ ${ img} ${ isSupportWebp ? '@220w_280h.webp' : '' } ` let tags = '' for (let name of tagList) tags += `<p>${name}</p>` return `<div class="card" link="${href}" index="${index}"><img src="${img}" referrerpolicy="no-referrer"><div class="info"><a class="title">${title}</a><div class="details"><span class="area"><p>${type}</p><em>${area}</em></span><span class="play"><p>播放量</p><em>${play}</em></span><span class="follow"><p>追番</p><em>${follow}</em></span><span class="coin"><p>硬币</p><em>${coin}</em></span><span class="danmaku"><p>弹幕</p><em>${danmaku}</em></span><span class="score"><p>评分</p><em>${score}</em></span></div><div class="tags">${tags}</div></div></div>` } const inner = document.getElementById('inner') inner.innerHTML = '' if ( updateURL) location. hash = JSON . stringify ( arg) for (let i = 0; i !== maxCount;) { const value = await json.readNext() if ( ! value) break if ( tagFilter && ! tagFilter ( value) ) continue const id = value.id ?? 0 const href = id === 0 ? '' : (typeof id !== 'number' ? id : `https://www.bilibili.com/bangumi/media/md${id}/`) inner.innerHTML += buildCard(value.title, value.cover, href ?? 0, value.follow ?? '-', value.type ?? '番剧', value.area ?? '日本', value.view ?? '-', value.coin ?? '-', value.danmaku ?? '-', value.score ?? '-', value.tags ?? [], value.index ?? 0) ++ i } const pageNum = document.getElementById('bottom-num') appendText( pageNum , tagFilter ? 'disabled' : `${arg.page} / ${json.size() }`, true) tabs.querySelectorAll ( 'button') .forEach ( button = > button. classList. remove ( 'disable' ) ) top.querySelectorAll ( 'div') .forEach ( div = > div. classList. remove ( 'disable' ) ) const pre = document.getElementById('bottom-pre').classList const next = document.getElementById('bottom-next').classList if ( arg. page === 1 ) pre. add ( 'disable' ) if ( json. isLastPage ( arg. page - 1 ) ) next. add ( 'disable' ) } const init = () => { const buildTag = (name) => `<div class="${name}">${name}</div>` cats.addEventListener ('click', event = > { const target = event.target const classList = target.classList if ( ! target. parentNode?. classList?. contains ( 'list' ) || classList.contains ( 'active') || classList.contains('disable')) return arg.id = target.className arg.page = 1 update () }) const tagList = tags.querySelector('.list') for (let tag of config.tags) { tagList.innerHTML += buildTag(tag) } tagList.onclick = event => { const target = event.target const parent = target.parentNode const classList = target.classList if ( ! parent. classList?. contains ( 'list' ) || classList.contains ( 'active') || classList.contains('disable')) return for (let node of parent.children) { node.classList .remove ( 'active') } const name = target.className tagFilter = name === 'all' ? null : card => card.tags?.includes(name) classList.add ( 'active') update( false ) } } /** 追加文本 */ function appendText(element, text, clean) { text = `(${text})` if ( navigator. userAgent. includes ( 'Firefox' ) ) { if ( ! clean && element. textContent. endsWith ( ')' ) ) return element.textContent = clean ? text : element.textContent + text } else { if ( ! clean && element. innerText. endsWith ( ')' ) ) return element.innerText = clean ? text : element.innerText + text } } /** 注册点击事件 */ function initClick(arg) { const top = document.getElementById('bangumi-top') top.addEventListener ('click', event = > { const element = event.target.id ? event.target : event.target.parentNode if ( element. nodeName !== 'BUTTON' ) return const classList = element.classList if ( classList. contains ( 'active' ) || classList. contains ( 'disable' ) ) return classList.add ( 'active') for (let value of top.children) { if ( value. id !== element. id) value. classList. remove ( 'active' ) } arg.id = element.id arg.page = 1 sessionStorage.setItem ( 'bangumis', JSON.stringify( arg ) ) update () }) const bottom = document.getElementById('bangumi-bottom') const height = document.getElementById('page-header').clientHeight bottom.addEventListener ('click', event = > { const element = event.target.id ? event.target : event.target.parentNode if ( element. nodeName !== 'BUTTON' || element. classList. contains ( 'disable' ) ) return btf.scrollToDest ( height ) switch (element.id) { case 'bottom-first' : arg.page = 1 break case 'bottom-end' : arg.page = jsonMap[arg.id].size() break case 'bottom-next' : ++ arg. page break case 'bottom-pre' : - - arg. page break } setTimeout(() => update(arg), 200) }) const card = document.getElementById('inner') card.addEventListener ('click', event = > { let element = event.target.id ? event.target : event.target.parentNode if ( ! element. classList. contains ( 'descr' ) ) { while ( ! element. classList. contains ( 'card' ) ) element = element. parentElement const link = element.getAttribute('link') if ( link. length > 1 ) open ( link) else btf. snackbarShow ( '博主没有为这个番剧设置链接~' ) } }) } init () const hashchangeTask = () => { const newArg = parseArg() if ( newArg. id !== arg. id && newArg. page !== arg. page) { arg.id = newArg.id arg.page = newArg.page update( false ) } } addEventListener( 'hashchange', hashchangeTask ) for (let key in sizeList) { appendText( cats.querySelector( `.${key}`) , sizeList[key], false) } initClick( arg ) update( false ) })
首先在你的某一个所有页面都需要加载的 JS 文件中写入如下代码:
1 2 3 4 5 document. addEventListener ( 'DOMContentLoaded' , ( ) => { const pushEvent = ( ) => document. dispatchEvent ( new Event ( 'kms:loaded' ) ) document. addEventListener ( 'pjax:complete' , pushEvent) pushEvent ( ) } )
然后编写 PUG 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 != page. content #bangumis #bangumi-top .multi #cats p 分类 .list .wantWatch 想看 .watching 在看 .watched 已看 .multi #tags p 标签 .list .all .active 所有 div#inner .bangumi-tabs #bangumi-bottom button#bottom-first 首页 button#bottom-pre 上一页 p#bottom-num 0/0 button#bottom-next 下一页 button#bottom-end 尾页 script . document. addEventListener ( 'kms:loaded' , async function fun ( ) { document. removeEventListener ( 'kms:loaded' , fun) function ApartJson ( root, amount, dataAmount, size ) { let readIndex = 0 const fileList = new Map() const lastIndex = Math.ceil(size / dataAmount) - 1 /** * 读取指定下标的文件 * @param index {number} 文件下标 * @return {Promise} */ const readFile = index => new Promise(resolve => { if ( index > lastIndex || index < 0 ) throw ` ${ index} not in [0, ${ lastIndex} ] ` if ( fileList. has ( index) ) { let id const task = () => { const cache = fileList.get(index) if ( cache) { if ( id) clearInterval ( id) resolve( cache ) } } task () id = setInterval(task, 100) } else { fileList.set ( index , null ) fetch( `${root}/${index}.json`) .then ( response = > response. json ( ) .then ( json = > { fileList.set( index , json ) resolve( json ) })) } }) const readHelper = id => new Promise(resolve => { if ( size && id > size) return resolve ( null ) const fileIndex = Math.floor(id / dataAmount) const innerIndex = id % dataAmount readFile( fileIndex ) .then (json = > { const keys = Object.keys(json) resolve( json[keys[innerIndex]]) }) }) /** * 读取上一项 * @return {Promise} */ this.readNext = () => readHelper(readIndex++) /** * 读取上一项 * @return {Promise} */ /** * 设置页码,从0开始 * @param page {number} 页面编号 */ this.setPage = page => { readIndex = page * amount } /** 获取页码数量 */ this.size = () => Math.ceil(size / amount) /** 获取数据总量 */ this.amount = () => size /** * 判断指定页面是否为末页 * @param page {number| null} 指定页面编号,留空为当前页面编号 * @return {boolean} */ this.isLastPage = (page = null) => { if ( page === null ) page = Math. floor ( readIndex / amount) return page === this.size() - 1 } } const isSupportWebp = (() => { try { return document.createElement('canvas').toDataURL('image/webp', 0.5).indexOf('data:image/webp') === 0; } catch (ignore) { return false; } })() const root = '/bilibili/' const config = await (await fetch(`${root}config.json`)).json() const sizeList = config.size const maxCount = 10 const dataAmount = config.amount const jsonMap = { wantWatch: new ApartJson( `${root}wantWatch`, maxCount , dataAmount , sizeList.wantWatch ) , watching: new ApartJson( `${root}watching`, maxCount , dataAmount , sizeList.watching ) , watched: new ApartJson( `${root}watched`, maxCount , dataAmount , sizeList.watched ) } /** 处理参数 */ const parseArg = () => { const url = location.href let arg if ( url. endsWith ( '/' ) ) { arg = {id: 'watching', page: 1} } else { arg = JSON.parse(decodeURIComponent(location.hash.substring(1))) if ( ! arg. id || ( arg. id !== 'watching' && arg.id !== 'wantWatch' && arg.id !== 'watched')) arg.id = 'watching' if ( ! arg. page || arg. page < 1 ) arg. page = 1 } sessionStorage.setItem ( 'bangumis', JSON.stringify( arg ) ) return arg } const arg = await parseArg() let tagFilter = null const top = document.querySelector('#bangumi-top') const cats = top.querySelector('#cats') const tags = top.querySelector('#tags') const tabs = document.querySelector('.bangumi-tabs') /** 更新列表内容 */ async function update(updateURL = true) { tabs.querySelectorAll ( 'button') .forEach ( button = > button. classList. add ( 'disable' ) ) top.querySelectorAll ( 'div') .forEach ( div = > div. classList. add ( 'disable' ) ) for (let value of cats.querySelector('.list').children) { if ( value. classList. contains ( arg. id) ) value. classList. add ( 'active' ) else value. classList. remove ( 'active' ) } const json = jsonMap[arg.id] json.setPage ( arg.page - 1 ) function buildCard(title, img, href, follow, type, area, play, coin, danmaku, score, tagList, index) { if ( ! img. startsWith ( 'http' ) ) img = ` https://i0.hdslb.com/bfs/bangumi/ ${ img} ${ isSupportWebp ? '@220w_280h.webp' : '' } ` let tags = '' for (let name of tagList) tags += `<p>${name}</p>` return `<div class="card" link="${href}" index="${index}"><img src="${img}" referrerpolicy="no-referrer"><div class="info"><a class="title">${title}</a><div class="details"><span class="area"><p>${type}</p><em>${area}</em></span><span class="play"><p>播放量</p><em>${play}</em></span><span class="follow"><p>追番</p><em>${follow}</em></span><span class="coin"><p>硬币</p><em>${coin}</em></span><span class="danmaku"><p>弹幕</p><em>${danmaku}</em></span><span class="score"><p>评分</p><em>${score}</em></span></div><div class="tags">${tags}</div></div></div>` } const inner = document.getElementById('inner') inner.innerHTML = '' if ( updateURL) location. hash = JSON . stringify ( arg) for (let i = 0; i !== maxCount;) { const value = await json.readNext() if ( ! value) break if ( tagFilter && ! tagFilter ( value) ) continue const id = value.id ?? 0 const href = id === 0 ? '' : (typeof id !== 'number' ? id : `https://www.bilibili.com/bangumi/media/md${id}/`) inner.innerHTML += buildCard(value.title, value.cover, href ?? 0, value.follow ?? '-', value.type ?? '番剧', value.area ?? '日本', value.view ?? '-', value.coin ?? '-', value.danmaku ?? '-', value.score ?? '-', value.tags ?? [], value.index ?? 0) ++ i } const pageNum = document.getElementById('bottom-num') appendText( pageNum , tagFilter ? 'disabled' : `${arg.page} / ${json.size() }`, true) tabs.querySelectorAll ( 'button') .forEach ( button = > button. classList. remove ( 'disable' ) ) top.querySelectorAll ( 'div') .forEach ( div = > div. classList. remove ( 'disable' ) ) const pre = document.getElementById('bottom-pre').classList const next = document.getElementById('bottom-next').classList if ( arg. page === 1 ) pre. add ( 'disable' ) if ( json. isLastPage ( arg. page - 1 ) ) next. add ( 'disable' ) } const init = () => { const buildTag = (name) => `<div class="${name}">${name}</div>` cats.addEventListener ('click', event = > { const target = event.target const classList = target.classList if ( ! target. parentNode?. classList?. contains ( 'list' ) || classList.contains ( 'active') || classList.contains('disable')) return arg.id = target.className arg.page = 1 update () }) const tagList = tags.querySelector('.list') for (let tag of config.tags) { tagList.innerHTML += buildTag(tag) } tagList.onclick = event => { const target = event.target const parent = target.parentNode const classList = target.classList if ( ! parent. classList?. contains ( 'list' ) || classList.contains ( 'active') || classList.contains('disable')) return for (let node of parent.children) { node.classList .remove ( 'active') } const name = target.className tagFilter = name === 'all' ? null : card => card.tags?.includes(name) classList.add ( 'active') update( false ) } } /** 追加文本 */ function appendText(element, text, clean) { text = `(${text})` if ( navigator. userAgent. includes ( 'Firefox' ) ) { if ( ! clean && element. textContent. endsWith ( ')' ) ) return element.textContent = clean ? text : element.textContent + text } else { if ( ! clean && element. innerText. endsWith ( ')' ) ) return element.innerText = clean ? text : element.innerText + text } } /** 注册点击事件 */ function initClick(arg) { const top = document.getElementById('bangumi-top') top.addEventListener ('click', event = > { const element = event.target.id ? event.target : event.target.parentNode if ( element. nodeName !== 'BUTTON' ) return const classList = element.classList if ( classList. contains ( 'active' ) || classList. contains ( 'disable' ) ) return classList.add ( 'active') for (let value of top.children) { if ( value. id !== element. id) value. classList. remove ( 'active' ) } arg.id = element.id arg.page = 1 sessionStorage.setItem ( 'bangumis', JSON.stringify( arg ) ) update () }) const bottom = document.getElementById('bangumi-bottom') const height = document.getElementById('page-header').clientHeight bottom.addEventListener ('click', event = > { const element = event.target.id ? event.target : event.target.parentNode if ( element. nodeName !== 'BUTTON' || element. classList. contains ( 'disable' ) ) return btf.scrollToDest ( height ) switch (element.id) { case 'bottom-first' : arg.page = 1 break case 'bottom-end' : arg.page = jsonMap[arg.id].size() break case 'bottom-next' : ++ arg. page break case 'bottom-pre' : - - arg. page break } setTimeout(() => update(arg), 200) }) const card = document.getElementById('inner') card.addEventListener ('click', event = > { let element = event.target.id ? event.target : event.target.parentNode if ( ! element. classList. contains ( 'descr' ) ) { while ( ! element. classList. contains ( 'card' ) ) element = element. parentElement const link = element.getAttribute('link') if ( link. length > 1 ) open ( link) else btf. snackbarShow ( '博主没有为这个番剧设置链接~' ) } }) } init () const hashchangeTask = () => { const newArg = parseArg() if ( newArg. id !== arg. id && newArg. page !== arg. page) { arg.id = newArg.id arg.page = newArg.page update( false ) } } addEventListener( 'hashchange', hashchangeTask ) for (let key in sizeList) { appendText( cats.querySelector( `.${key}`) , sizeList[key], false) } initClick( arg ) update( false ) })
修改[butterfly]\source\css\index.styl
:
1 2 3 4 5 6 7 @import '_mode/*' + @import '_custom/bangumis' // search if hexo-config('algolia_search.enable') @import '_search/index' @import '_search/algolia'
新建[butterfly]\source\css\_custom\bangumis.styl
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 :root --km-card-bg #66ccff 34 --km-link-bg #0084ff --km-button-light-active #66ccff --km-general-shadow #546e7a --km-globle-font black [data-theme="dark"] --km-card-bg #66ccff 10 --km-link-bg #0056ac --km-button-light-active #4c4c4c bc --km-globle-font white #bangumis .bangumi-tabs display block border-radius 15 px border 2 px solid var ( - -km-card-bg) text-align center & > p display inline-block margin-bottom 0 margin-top 0 button display inline-block color var ( - -font-color) padding 5 px margin 5 px border-radius 5 px transition all .2 s &:not(.active):not(.disable):hover color white background-color var ( - -km-link-bg) &.active background-color var ( - -km-button-light-active) &.disable color #90a4ae #bangumi-top .multi border 2 px solid var ( - -km-card-bg) border-radius 15 px text-align left margin-bottom 5 px display flex align-items center white-space nowrap p display inline-block margin 5 px 10 px 5 px 10 px padding-right 10 px font-weight bold border-right 1 px solid var ( - -font-color) .list overflow overlay display inline-block white-space normal max-height 75 px div display inline-block margin 3 px padding 1 px 5 px 1 px 5 px border-radius 8 px transition all .2 s &:not(.active):not(.disable):hover color white background var ( - -km-link-bg) &.active background var ( - -km-button-light-active) .card display block margin-top 10 px margin-bottom 10 px padding-left 10 px padding-right 10 px white-space nowrap transition all .2 s border-radius 8 px border 1 px #66ccff 99 dashed text-align center &[link *= '/']:hover cursor url('https://image.kmar.top/mouse/link.cur') , auto [data-theme="light"] & border-color dodgerblue img display inline-block width 110 px border-radius 10 px vertical-align middle transition all .2 s &:hover box-shadow 1 px 2 px 4 px var ( - -km-general-shadow) .info display inline-block margin-left 10 px vertical-align middle width calc ( 100 % - 120 px ) .title font-size 20 px border-radius 5 px color #49b1f5 white-space normal word-break break-word &:hover padding 2 px color var ( - -km-globle-font) background-color var ( - -km-link-bg) .details display block overflow-x overlay padding-bottom 6 px span display inline-block border-right 1 px solid #2fd8d8 text-align center margin 5 px vertical-align middle padding 5 px padding-top 0 padding-right 10 px height 50 px p, em color #2fd8d8 font-size 12 px display block line-height 12 px em font-weight bold .total p padding-top 12 px .score border-right unset .tags overflow-x overlay p display inline-block border 2 px #66ccff solid border-radius 16 px padding 0 8 px margin-right 10 px transition .2 s &:hover background #55bbee aa &:hover background-color var ( - -km-card-bg) [data-theme="light"] #bangumis .title color #99a9bf !important &:hover color white !important .details p, em color black !important
执行命令:npm install node-fetch@2
新建[butterfly]\scripts\customs\bili.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 const logger = require ( 'hexo-log' ) ( ) const fs = require ( 'hexo-fs' ) const fetch = require ( 'node-fetch' ) const config = hexo. config. bilibili || hexo. config. theme_config. bilibiliconst options = { options : [ { name : '-u, --update' , desc : 'Update data' } , { name : '-d, --delete' , desc : 'Delete data' } ] } hexo. extend. console. register ( 'bangumi' , '番剧JSON文件的相关操作' , options, async args => { if ( ! config?. enable) return const root = ` ${ hexo. config. source_dir} /bilibili/ ` const amount = 50 const writeConfig = json => { const result = { amount, size : { } , tags : [ ] } for ( let jsonKey in json) { const list = json[ jsonKey] result. size[ jsonKey] = list. length for ( let it of list) { const tags = it. tags ?? [ ] for ( let tag of tags) { if ( result. tags. includes ( tag) ) continue result. tags. push ( tag) } } } fs. writeFile ( ` ${ root} config.json ` , JSON . stringify ( result) ) } if ( args. u || args. update) { const historyPath = ` ${ root} all.json ` let history = fs. existsSync ( historyPath) ? JSON . parse ( fs. readFileSync ( historyPath) ) : { } const json = await buildJson ( history) fs. writeFile ( historyPath, JSON . stringify ( json) ) writeConfig ( json) for ( let key in json) { const sonRoot = ` ${ root} ${ key} / ` const value = json[ key] const objs = Object. keys ( value) const page = Math. ceil ( objs. length / amount) for ( let i = 0 ; i !== page; ++ i) { const start = i * 50 const end = Math. min ( start + amount, objs. length) const data = { } for ( let k = start; k !== end; ++ k) { const sonKey = objs[ k] data[ sonKey] = value[ sonKey] } fs. writeFile ( ` ${ sonRoot} ${ i} .json ` , JSON . stringify ( data) ) } } logger. info ( "成功生成JSON文件" ) } else if ( args. d || args. delete) { const arg = args. d || args. delete if ( typeof arg === "string" ) { const [ ... deletes] = args. _ deletes. push ( arg) const historyPath = ` ${ root} all.json ` if ( ! fs. existsSync ( historyPath) ) { logger. info ( '文件不存在,跳过删除' ) return } let count = 0 const history = JSON . parse ( fs. readFileSync ( historyPath) ) for ( let key in history) { const list = history[ key] for ( let i = 0 ; i < list. length; i++ ) { const title = list[ i] . title if ( deletes. includes ( title) ) { list. splice ( i-- , 1 ) ++ count } } } fs. writeFileSync ( historyPath, JSON . stringify ( history) , 'utf-8' ) logger. info ( ` 成功移除 ${ count} 条数据 ` ) } else if ( fs. existsSync ( root) ) { fs. deleteFile ( root) logger. info ( "成功删除JSON文件" ) } else logger. info ( '文件不存在,逃过删除' ) } } ) async function buildJson ( history ) { const readJson = async ( json, list ) => { for ( let element of json) list. push ( element) } const data = { vmid : config. vmid, extra : config. extra || 'extra_bangumis' } const wantWatch = await getBiliJson ( data. vmid, 1 ) const watching = await getBiliJson ( data. vmid, 2 ) const watched = await getBiliJson ( data. vmid, 3 ) const extraPath = ` ${ hexo. config. source_dir} /_data/ ${ data. extra} .json ` if ( fs. existsSync ( extraPath) ) { const extra = JSON . parse ( fs. readFileSync ( extraPath) ) for ( let key in extra) { switch ( key) { case 'watchedExtra' : case 'watched' : await readJson ( extra[ key] , watched) break case 'watchingExtra' : case 'watching' : await readJson ( extra[ key] , watching) break case 'wantWatchExtra' : case 'wantWatch' : await readJson ( extra[ key] , wantWatch) break } } } const sort = ( array, history ) => { const list = array. filter ( it => { if ( ! history) return true for ( let i = 0 ; i < history. length; i++ ) { const value = history[ i] if ( value. title === it. title) { history[ i] = it return false } } return true } ) return [ ... mergeSort ( list) , ... ( history ?? [ ] ) ] } const locks = config. locks const info = { wantWatch : sort ( wantWatch, locks. wantLock ? history. wantWatch : null ) , watching : sort ( watching, locks. ingLock ? history. watching : null ) , watched : sort ( watched, locks. edLock ? history. watched : null ) } const sum = info. watching. length + info. watched. length + info. wantWatch. length logger. info ( ` wantWatch( ${ info. wantWatch. length} ) + watching( ${ info. watching. length} ) + watched( ${ info. watched. length} ) = ${ sum} ` ) return info} const count = ( e ) => ( e ? ( e > 10000 && e < 100000000 ? ` ${ ( e / 10000 ) . toFixed ( 1 ) } 万 ` : e > 100000000 ? ` ${ ( e / 100000000 ) . toFixed ( 1 ) } 亿 ` : e) : '-' ) const getDataPage = ( vmid, status ) => fetch ( ` https://api.bilibili.com/x/space/bangumi/follow/list?type=1&follow_status= ${ status} &vmid= ${ vmid} &ps=1&pn=1 ` ) . then ( it => { if ( it. ok) return it. json ( ) else return null } ) . then ( json => { if ( ! json) return { success : false } if ( json. code === 0 && json. message === '0' && json. data && json. data?. total !== undefined ) { return { success : true , data : Math. ceil ( json. data. total / 30 ) + 1 } } else if ( json. message !== '0' ) { return { success : false , data : json. message} } else { return { success : false , data : json} } } ) const getData = async ( vmid, status, pn ) => fetch ( ` https://api.bilibili.com/x/space/bangumi/follow/list?type=1&follow_status= ${ status} &vmid= ${ vmid} &ps=30&pn= ${ pn} ` ) . then ( it => { if ( it. ok) return it. json ( ) else return null } ) . then ( json => { const result = [ ] if ( ! json) return result if ( json. code === 0 ) { const data = json. data const list = data?. list || [ ] for ( const bangumi of list) { let cover = bangumi?. cover if ( cover) { const href = new URL ( cover) href. protocol = 'https' cover = href. href if ( cover. startsWith ( 'https://i0.hdslb.com/bfs/bangumi/' ) ) { cover = cover. substring ( 33 ) } } result. push ( { title : bangumi?. title, type : bangumi?. season_type_name, area : bangumi?. areas?. [ 0 ] ?. name, cover, id : bangumi?. media_id, follow : count ( bangumi?. stat?. follow) , view : count ( bangumi?. stat?. view) , danmaku : count ( bangumi?. stat?. danmaku) , coin : count ( bangumi. stat. coin) , score : bangumi?. rating?. score ?? '-' , tags : bangumi?. styles } ) } return result } } ) async function getBiliJson ( vmid, status ) { const page = await getDataPage ( vmid, status) if ( page?. success) { const list = [ ] for ( let i = 1 ; i < page. data; i++ ) { const data = await getData ( vmid, status, i) list. push ( ... data) } for ( let i = 0 ; i < list. length; i++ ) list[ i] . index = list. length - i return list } return [ ] } function mergeSort ( array ) { function merge ( left, right ) { let arr = [ ] while ( left. length && right. length) { arr. push ( left[ 0 ] . index > right[ 0 ] . index ? left. shift ( ) : right. shift ( ) ) } return [ ... arr, ... left, ... right] } const half = array. length / 2 if ( array. length < 2 ) return array const left = array. splice ( 0 , half) return merge ( mergeSort ( left) , mergeSort ( array) ) }
创作不易,扫描下方打赏二维码支持一下吧ヾ(≧▽≦*)o