mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
231 lines
6.7 KiB
TypeScript
231 lines
6.7 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'
|
|
import { bangumiSkipToViewPoints, escape, fixBgmTag, formatTime, tagWithId } from './utils'
|
|
|
|
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()
|
|
if (
|
|
getComponentSettings<Options>(componentName).options.convertBangumiSkips &&
|
|
this.viewPoints.length === 0
|
|
) {
|
|
this.viewPoints = bangumiSkipToViewPoints(
|
|
this.bangumi.episode.skip,
|
|
this.bangumi.episode.duration,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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}=${
|
|
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)
|
|
const { basic } = data
|
|
console.debug(data)
|
|
|
|
const {
|
|
options: { fieldsMode, timeFormat },
|
|
} = getComponentSettings<Options>(componentName)
|
|
|
|
const generated = new Date()
|
|
|
|
const lines = [
|
|
';FFMETADATA1',
|
|
`;generated by Bilibili-Evolved v${meta.compilationInfo.version}`,
|
|
`;generated on ${generated.toLocaleDateString()} at ${generated.toLocaleTimeString(
|
|
navigator.language,
|
|
{ timeZoneName: 'short' },
|
|
)}`,
|
|
// Standard fields
|
|
ff('title', `${basic.title} - ${data.page.title}`, false),
|
|
ff('description', basic.description, false),
|
|
ff('artist', basic.up.name, false),
|
|
]
|
|
|
|
if (fieldsMode === FieldsMode.ALL) {
|
|
// Custom fields
|
|
lines.push(
|
|
ff('metadata_generated', formatTime(generated, timeFormat)),
|
|
ff('title', basic.title),
|
|
ff('description', basic.description),
|
|
ff('publish_date', formatTime(new Date(basic.pubdate * 1000), timeFormat)),
|
|
ff('aid', basic.aid),
|
|
ff('bvid', basic.bvid),
|
|
ff('cid', data.page.cid),
|
|
ff('up_name', basic.up.name),
|
|
ff('up_uid', basic.up.uid),
|
|
ff('page_title', data.page.title),
|
|
ff('pages', basic.pages.length),
|
|
ff('page', data.page.pageNumber),
|
|
ff('category_id', basic.tagId),
|
|
ff('category_name', basic.tagName),
|
|
)
|
|
if (data.tags.tag) {
|
|
lines.push(ff('tags', tagWithId(data.tags.tag)))
|
|
}
|
|
if (data.tags.topic) {
|
|
lines.push(ff('topic', tagWithId(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_section_title', d.episode.section),
|
|
ff('bangumi_episode_id', d.episode.epid),
|
|
ff('bangumi_episode_title', d.episode.title),
|
|
ff(
|
|
'bangumi_area',
|
|
d.areas.map(x => x.name),
|
|
),
|
|
)
|
|
}
|
|
// TODO option for includeStat
|
|
if (true) {
|
|
const s = basic.stat
|
|
lines.push(
|
|
ff('stat_view', s.view),
|
|
ff('stat_like', s.like),
|
|
ff('stat_coin', s.coin),
|
|
ff('stat_favorite', s.favorite),
|
|
ff('stat_share', s.share),
|
|
ff('stat_danmaku', s.danmaku),
|
|
ff('stat_reply', s.reply),
|
|
)
|
|
if (s.his_rank > 0) {
|
|
lines.push(ff('stat_highest_rank', s.his_rank))
|
|
}
|
|
}
|
|
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)
|
|
}
|