Bilibili-Evolved/registry/lib/components/video/metadata/metadata.ts

213 lines
5.9 KiB
TypeScript

import { BangumiInfo, VideoInfo, VideoPageInfo } from '@/components/video/video-info'
import { VideoQuality } from '@/components/video/video-quality'
import { bilibiliApi, getJsonWithCredentials } from '@/core/ajax'
import { meta } from '@/core/meta'
import { getComponentSettings } from '@/core/settings'
import { Toast } from '@/core/toast'
import { name as componentName, title as pluginTitle } from '.'
import { FieldsMode, Options } from './options'
import { MetadataType, Tag, ViewPoint } from './types'
function escape(x: any) {
return lodash.toString(x).replace(/[=;#\\\n]/g, r => `\\${r}`)
}
class VideoMetadata {
private readonly aid: number
private readonly cid: number
basic: VideoInfo
viewPoints: ViewPoint[]
tags: {
tag?: Tag[]
topic?: Tag[]
bgm?: Tag[]
} = {}
page: VideoPageInfo
quality?: VideoQuality
bangumi?: BangumiInfo
constructor(aid: string, cid: number | string) {
this.aid = parseInt(aid)
this.cid = parseInt(<any>cid)
this.basic = new VideoInfo(aid)
}
async fetch() {
await this.basic.fetchInfo()
this.page = this.basic.pages.filter(p => p.cid === this.cid)[0]
const playInfo = await bilibiliApi(
getJsonWithCredentials(`//api.bilibili.com/x/player/wbi/v2?aid=${this.aid}&cid=${this.cid}`),
)
this.viewPoints = lodash.get(playInfo, 'view_points', []) as ViewPoint[]
const tags = await bilibiliApi(
getJsonWithCredentials(
`//api.bilibili.com/x/web-interface/view/detail/tag?aid=${this.aid}&cid=${this.cid}`,
),
)
const groupedTags = lodash.groupBy(tags, 'tag_type')
this.tags.tag = groupedTags.old_channel
this.tags.topic = groupedTags.topic
this.tags.bgm = groupedTags.bgm
if (this.basic.redirectUrl) {
const epid = parseInt(this.basic.redirectUrl.match(/ep(\d+)/)?.[1] ?? '')
if (epid) {
this.bangumi = await BangumiInfo.byEpisodeId(epid).fetchInfo()
}
}
}
}
async function fetchMetadata(aid: string = unsafeWindow.aid, cid: string = unsafeWindow.cid) {
const data = new VideoMetadata(aid, cid)
await data.fetch()
return data
}
function tag(tags: Tag[]) {
return tags.map(x => `${x.tag_name}(${x.tag_id})`)
}
function fixBgmTag(bgmTags: Tag[]) {
return bgmTags.map(
x => `${x.tag_name.match(/^发现《([^》]+)》/)?.[1] ?? x.tag_name}(${x.music_id})`,
)
}
function ff(key: string, value: any, prefix = true) {
return `${prefix ? 'bilibili_' : ''}${key}=${
Array.isArray(value) ? value.map(escape).join(',') : escape(value)
}`
}
async function generateFFMetadata(aid: string = unsafeWindow.aid, cid: string = unsafeWindow.cid) {
const data = await fetchMetadata(aid, cid)
console.debug(data)
const info = data.basic
const {
options: { fieldsMode },
} = getComponentSettings<Options>(componentName)
const lines = [
';FFMETADATA1',
`;generated by Bilibili-Evolved v${meta.compilationInfo.version}`,
`;generated on ${new Date().toLocaleString()}`,
// Standard fields
ff('title', `${info.title} - ${data.page.title}`, false),
ff('description', info.description, false),
ff('artist', info.up.name, false),
]
if (fieldsMode === FieldsMode.ALL) {
// Custom fields
lines.push(
ff('title', info.title),
ff('description', info.description),
ff('publish_date', new Date(info.pubdate * 1000).toLocaleString()),
ff('aid', info.aid),
ff('bvid', info.bvid),
ff('cid', data.page.cid),
ff('up_name', info.up.name),
ff('up_uid', info.up.uid),
ff('page_title', data.page.title),
ff('pages', info.pages.length),
ff('page', data.page.pageNumber),
ff('category_id', info.tagId),
ff('category_name', info.tagName),
)
if (data.tags.tag) {
lines.push(ff('tags', tag(data.tags.tag)))
}
if (data.tags.topic) {
lines.push(ff('topic', tag(data.tags.topic)))
}
if (data.tags.bgm) {
lines.push(ff('bgm', fixBgmTag(data.tags.bgm)))
}
if (data.bangumi) {
const d = data.bangumi
lines.push(
ff('bangumi_media_id', d.mediaId),
ff('bangumi_season_id', d.seasonId),
ff('bangumi_season_title', d.seasonTitle),
ff('bangumi_series_id', d.seriesId),
ff('bangumi_series_title', d.seriesTitle),
ff('bangumi_episode_id', d.episode.epid),
ff('bangumi_episode_title', d.episode.title),
ff(
'bangumi_area',
d.areas.map(x => x.name),
),
)
}
if (data.quality) {
lines.push(ff('quality', data.quality.value))
lines.push(ff('quality_label', data.quality.name))
}
}
if (data.viewPoints.length > 0) {
for (const chapter of data.viewPoints) {
lines.push(
...[
'[CHAPTER]',
'TIMEBASE=1/1',
ff('START', chapter.from, false),
ff('END', chapter.to, false),
ff('title', chapter.content, false),
],
)
}
}
const result = lines.join('\n')
console.debug(result)
return result
}
async function generateChapterFile(aid: string = unsafeWindow.aid, cid: string = unsafeWindow.cid) {
const { viewPoints } = await fetchMetadata(aid, cid)
console.debug(viewPoints)
if (viewPoints.length > 0) {
const result = viewPoints
.reduce((p, v, i) => {
const n = `${i + 1}`.padStart(3, '0')
return [
...p,
`CHAPTER${n}=${new Date(v.from * 1000).toISOString().slice(11, -1)}`,
`CHAPTER${n}NAME=${v.content}`,
]
}, [])
.join('\n')
console.debug(result)
return result
}
Toast.info('此视频没有章节', pluginTitle, 3000)
return null
}
export async function generateByType(
type: MetadataType,
aid: string = unsafeWindow.aid,
cid: string = unsafeWindow.cid,
) {
let method: (aid, cid) => Promise<string>
switch (type) {
case 'ogm':
method = generateChapterFile
break
default:
case 'ffmetadata':
method = generateFFMetadata
break
}
return method(aid, cid)
}