Skip to content

Commit

Permalink
Merge pull request #4840 from WakelessSloth56/preview-features
Browse files Browse the repository at this point in the history
[新增组件] 保存视频元数据
  • Loading branch information
the1812 authored Jul 30, 2024
2 parents eb6a9db + 3684f7d commit fbd459d
Show file tree
Hide file tree
Showing 9 changed files with 418 additions and 36 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"durl",
"epid",
"esbuild",
"ffmetadata",
"flac",
"Fullscreen",
"githubusercontent",
Expand Down
44 changes: 44 additions & 0 deletions registry/lib/components/video/metadata/Plugin.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<template>
<div class="download-video-config-section">
<div class="download-video-config-item">
<div>元数据:</div>
<VDropdown v-model="type" :items="items">
<template #item="{ item }">
{{ item }}
</template>
</VDropdown>
</div>
</div>
</template>
<script lang="ts">
import { VDropdown } from '@/ui'
import { getComponentSettings } from '@/core/settings'
import { MetadataType } from './metadata'
interface Options {
metadataType: MetadataType | ''
}
const options = getComponentSettings('downloadVideo').options as Options
export default Vue.extend({
components: {
VDropdown,
},
data() {
return {
type: options.metadataType ?? '',
items: ['', 'ffmetadata', 'ogm'],
}
},
computed: {
enabled() {
return this.type !== ''
},
},
watch: {
type(value: MetadataType) {
options.metadataType = value
},
},
})
</script>
48 changes: 48 additions & 0 deletions registry/lib/components/video/metadata/SaveMetadata.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<template>
<div class="multiple-widgets">
<DefaultWidget
ref="button"
:disabled="disabled"
name="保存视频元数据"
icon="mdi-download"
@click="run('ffmetadata')"
></DefaultWidget>
<DefaultWidget
:disabled="disabled"
name="保存视频章节"
icon="mdi-download"
@click="run('ogm')"
></DefaultWidget>
</div>
</template>

<script lang="ts">
import { DefaultWidget } from '@/ui'
import { logError } from '@/core/utils/log'
import { DownloadPackage } from '@/core/download'
import { getFriendlyTitle } from '@/core/utils/title'
import { MetadataType, generateByType } from './metadata'
export default Vue.extend({
components: {
DefaultWidget,
},
data() {
return {
disabled: false,
}
},
methods: {
async run(type: MetadataType) {
try {
this.disabled = true
DownloadPackage.single(`${getFriendlyTitle(true)}.${type}.txt`, await generateByType(type))
} catch (error) {
logError(error)
} finally {
this.disabled = false
}
},
},
})
</script>
72 changes: 72 additions & 0 deletions registry/lib/components/video/metadata/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { defineComponentMetadata } from '@/components/define'
import { PackageEntry } from '@/core/download'
import { hasVideo } from '@/core/spin-query'
import { Toast } from '@/core/toast'
import { videoUrls } from '@/core/utils/urls'
import { DownloadVideoAssets } from '../download/types'
import { generateByType, MetadataType } from './metadata'

export const title = '保存视频元数据'
export const name = 'saveVideoMetadata'

const author = [
{
name: 'WakelessSloth56',
link: 'https://github.com/WakelessSloth56',
},
{
name: 'LainIO24',
link: 'https://github.com/LainIO24',
},
]

export const component = defineComponentMetadata({
name,
displayName: title,
description: '保存视频元数据(标题、描述、UP、章节等)',
author,
tags: [componentsTags.video],
entry: none,
urlInclude: videoUrls,
widget: {
condition: hasVideo,
component: () => import('./SaveMetadata.vue').then(m => m.default),
},
plugin: {
displayName: `下载视频 - ${title}支持`,
author,
setup: ({ addData }) => {
addData('downloadVideo.assets', async (assets: DownloadVideoAssets[]) => {
assets.push({
name,
displayName: title,
getAssets: async (
infos,
instance: {
type: MetadataType
enabled: boolean
},
) => {
const { type, enabled } = instance
if (enabled) {
const toast = Toast.info('获取视频元数据中...', title)
const result: PackageEntry[] = []
for (const info of infos) {
result.push({
name: `${info.input.title}.${type}.txt`,
data: await generateByType(type, info.input.aid, info.input.cid),
options: {},
})
}
toast.message = '完成!'
toast.duration = 1000
return result
}
return []
},
component: () => import('./Plugin.vue').then(m => m.default),
})
})
},
},
})
152 changes: 152 additions & 0 deletions registry/lib/components/video/metadata/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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 { Toast } from '@/core/toast'
import { title as pluginTitle } from '.'

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(<any>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 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),
// Custom fields
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<string>
switch (type) {
case 'ogm':
method = generateChapterFile
break
default:
case 'ffmetadata':
method = generateFFMetadata
break
}
return method(aid, cid)
}
41 changes: 41 additions & 0 deletions registry/lib/plugins/video/download/wasm-output/Config.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<template>
<div v-if="shouldShow" class="download-video-config-item" style="flex-wrap: wrap">
<div class="download-video-config-title">写入元数据:</div>
<SwitchBox v-model="muxWithMetadata" @change="saveOptions" />
<div class="download-video-config-description" style="width: 100%">
仅支持元数据类型「ffmetadata」
</div>
</div>
</template>

<script lang="ts">
import { SwitchBox } from '@/ui'
import { isComponentEnabled, getComponentSettings } from '@/core/settings'
interface Options {
muxWithMetadata: boolean
}
const defaultOptions: Options = {
muxWithMetadata: false,
}
const { options: storedOptions } = getComponentSettings('downloadVideo')
const options: Options = { ...defaultOptions, ...storedOptions }
export default Vue.extend({
components: {
SwitchBox,
},
data() {
const shouldShow = isComponentEnabled('saveVideoMetadata')
return {
shouldShow,
muxWithMetadata: shouldShow && options.muxWithMetadata,
}
},
methods: {
saveOptions() {
options.muxWithMetadata = this.muxExtraAssets
Object.assign(storedOptions, options)
},
},
})
</script>
Loading

0 comments on commit fbd459d

Please sign in to comment.