更新日志

2023/10/25

  1. 修复 pjax 兼容的脚本不执行的问题

2023/09/01

  1. 支持删除指定番剧(包括已经锁定的)
  2. 修复配置写在主题配置中无法正确读取的问题

2022/11/14

  1. 修复切换分类是页码不重置的问题

2022/09/20

  1. 修复开启标签筛选后加载速度极慢的 bug

2022/09/17

  1. 支持标签筛选
  2. 修改json格式

2022/09/15

  1. 移除自动构建json文件的功能
  2. 添加锁功能,允许用户锁住列表顺序
  3. 拆分json文件

2022/08/12

  1. 修改卡片样式
  2. 移除“总集数”和“描述”
  3. 添加“标签”

2022/07/24

  1. 修复与 pjax 的兼容问题

2022/07/15

  1. 修正追番列表 JSON 文件生成为空的问题
  2. 在自动生成 JSON 文件的基础上保留命令生成的功能

2022/07/13

  1. 修正追番列表编号错乱的问题
  2. 生成 JSON 文件从命令手动生成改为自动生成

2022/06/09

  1. 实现基本功能

介绍

  本篇博文中的script代码主要从hexo-bilibili-bangumi插件中 CV 而来,本人进行了一些细节上的修改,其余代码为本人自行编写。

  想要预览前端页面可以点击这里进行查看。

  目前前端实现了以下功能:

  1. 切换页面后支持使用浏览器的回退功能进行返回(会留下比较多的历史记录)
  2. 通过 JS 控制页面显示
  3. 将番剧列表信息存储在 JSON 中而不是 HTML 内
  4. 自动修正 URL 的错误参数(不会删掉多余的参数)
  5. 没有提供链接的番剧不再跳转到 404 页面
  6. 根据浏览器支持情况自动选择是否使用webp格式的图片

  请注意:使用该插件需要在 Bilibili 设置中公开追番列表,否则无法获取到追番信息。

用法

  执行update指令即可生成相关文件。

指令

  1. hexo bangumi -u | 生成JSON文件
  2. hexo bangumi -d | 删除JSON文件
  3. 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() {
/**
* 分离Json的读取器
*
* @param root {string} 根目录,不以`/`结尾
* @param amount {number} 每页的数量
* @param dataAmount {number} 每个文件中数据的数量
* @param size {number} 项目总量
* @constructor
*/

function ApartJson(root, amount, dataAmount, size) {
// 下一个要读取项目编号
let readIndex = 0
// 读取的文件列表
const fileList = new Map()
// 标记最后一个index
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 => {
// noinspection JSCheckFunctionSignatures
const keys = Object.keys(json)
resolve(json[keys[innerIndex]])
})
})

// noinspection JSUnusedGlobalSymbols
/**
* 读取上一项
* @return {Promise}
*/
this.readNext = () => readHelper(readIndex++)

/**
* 读取上一项
* @return {Promise}
*/
//this.readPrev = () => readHelper(readIndex--)

// noinspection CommaExpressionJS,JSUnusedGlobalSymbols
/**
* 设置页码,从0开始
* @param page {number} 页面编号
*/
this.setPage = page => {
readIndex = page * amount
}

/** 获取页码数量 */
this.size = () => Math.ceil(size / amount)

/** 获取数据总量 */
this.amount = () => size

// noinspection JSUnusedGlobalSymbols
/**
* 判断指定页面是否为末页
* @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
// JSON
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>`
// noinspection HtmlUnknownAttribute,HtmlRequiredAltAttribute
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)
// noinspection ES6MissingAwait
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)

/**
* 分离Json的读取器
*
* @param root {string} 根目录,不以`/`结尾
* @param amount {number} 每页的数量
* @param dataAmount {number} 每个文件中数据的数量
* @param size {number} 项目总量
* @constructor
*/

function ApartJson(root, amount, dataAmount, size) {
// 下一个要读取项目编号
let readIndex = 0
// 读取的文件列表
const fileList = new Map()
// 标记最后一个index
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 => {
// noinspection JSCheckFunctionSignatures
const keys = Object.keys(json)
resolve(json[keys[innerIndex]])
})
})

// noinspection JSUnusedGlobalSymbols
/**
* 读取上一项
* @return {Promise}
*/
this.readNext = () => readHelper(readIndex++)

/**
* 读取上一项
* @return {Promise}
*/
//this.readPrev = () => readHelper(readIndex--)

// noinspection CommaExpressionJS,JSUnusedGlobalSymbols
/**
* 设置页码,从0开始
* @param page {number} 页面编号
*/
this.setPage = page => {
readIndex = page * amount
}

/** 获取页码数量 */
this.size = () => Math.ceil(size / amount)

/** 获取数据总量 */
this.amount = () => size

// noinspection JSUnusedGlobalSymbols
/**
* 判断指定页面是否为末页
* @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
// JSON
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>`
// noinspection HtmlUnknownAttribute,HtmlRequiredAltAttribute
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)
// noinspection ES6MissingAwait
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 #66ccff34
--km-link-bg #0084ff
--km-button-light-active #66ccff
--km-general-shadow #546e7a
--km-globle-font black

[data-theme="dark"]
--km-card-bg #66ccff10
--km-link-bg #0056ac
--km-button-light-active #4c4c4cbc
--km-globle-font white

#bangumis
.bangumi-tabs
display block
border-radius 15px
border 2px 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 5px
margin 5px
border-radius 5px
transition all .2s

&: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 2px solid var(--km-card-bg)
border-radius 15px
text-align left
margin-bottom 5px
display flex
align-items center
white-space nowrap

p
display inline-block
margin 5px 10px 5px 10px
padding-right 10px
font-weight bold
border-right 1px solid var(--font-color)

.list
overflow overlay
display inline-block
white-space normal
max-height 75px

div
display inline-block
margin 3px
padding 1px 5px 1px 5px
border-radius 8px
transition all .2s

&:not(.active):not(.disable):hover
color white
background var(--km-link-bg)

&.active
background var(--km-button-light-active)

.card
display block
margin-top 10px
margin-bottom 10px
padding-left 10px
padding-right 10px
white-space nowrap
transition all .2s
border-radius 8px
border 1px #66ccff99 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 110px
border-radius 10px
vertical-align middle
transition all .2s

&:hover
box-shadow 1px 2px 4px var(--km-general-shadow)

.info
display inline-block
margin-left 10px
vertical-align middle
width calc(100% - 120px)

.title
font-size 20px
border-radius 5px
color #49b1f5
white-space normal
word-break break-word

&:hover
padding 2px
color var(--km-globle-font)
background-color var(--km-link-bg)

.details
display block
overflow-x overlay
padding-bottom 6px

span
display inline-block
border-right 1px solid #2fd8d8
text-align center
margin 5px
vertical-align middle
padding 5px
padding-top 0
padding-right 10px
height 50px

p, em
color #2fd8d8
font-size 12px
display block
line-height 12px

em
font-weight bold

.total p
padding-top 12px

.score
border-right unset

.tags
overflow-x overlay

p
display inline-block
border 2px #66ccff solid
border-radius 16px
padding 0 8px
margin-right 10px
transition .2s

&:hover
background #55bbeeaa

&: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.bilibili

const 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
}

// eslint-disable-next-line no-nested-ternary
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}
}
})

// kmar edit point
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 = []
// eslint-disable-next-line no-plusplus
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