import { 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' export type MetadataType = 'ffmetadata' | 'ogm' function escape(s: string) { return s.replace(/[=;#\\\n]/g, r => `\\${r}`) } interface ViewPoint { content: string from: number to: number image: string } class VideoMetadata { #aid: string #cid: number | string basic: VideoInfo viewPoints: ViewPoint[] page: VideoPageInfo quality?: VideoQuality constructor(aid: string, cid: number | string) { this.#aid = aid this.#cid = cid this.basic = new VideoInfo(aid) } async fetch() { await this.basic.fetchInfo() this.page = this.basic.pages.filter(p => p.cid === parseInt(this.#cid))[0] const playInfo = await bilibiliApi( getJsonWithCredentials( `https://api.bilibili.com/x/player/wbi/v2?aid=${this.#aid}&cid=${this.#cid}`, ), ) this.viewPoints = lodash.get(playInfo, 'view_points', []) as ViewPoint[] } } async function fetchMetadata(aid: string = unsafeWindow.aid, cid: string = unsafeWindow.cid) { const data = new VideoMetadata(aid, cid) await data.fetch() return data } function ff(key: string, value: any, prefix = true) { return `${prefix ? 'bilibili_' : ''}${key}=${escape(lodash.toString(value))}` } async function generateFFMetadata(aid: string = unsafeWindow.aid, cid: string = unsafeWindow.cid) { const data = await fetchMetadata(aid, cid) const info = data.basic const { options: { fieldsMode }, } = getComponentSettings(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('category_id', info.tagId), ff('category_name', info.tagName), ff('page_title', data.page.title), ff('page', data.page.pageNumber), ff('pages', info.pages.length), ff('up_name', info.up.name), ff('up_uid', info.up.uid), ) 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 switch (type) { case 'ogm': method = generateChapterFile break default: case 'ffmetadata': method = generateFFMetadata break } return method(aid, cid) }