Add instance avatar to default open graph tags

This commit is contained in:
Chocobozzz 2024-10-22 13:12:06 +02:00
parent 5af6cf6e82
commit 54adc6f038
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
14 changed files with 119 additions and 49 deletions

View File

@ -23,7 +23,8 @@ describe('Test embed HTML generation', function () {
let unlistedPlaylistId: string let unlistedPlaylistId: string
let playlistName: string let playlistName: string
let playlistDescription: string let playlistDescription: string
let instanceDescription: string
let instanceConfig: { shortDescription: string }
before(async function () { before(async function () {
this.timeout(120000); this.timeout(120000);
@ -44,7 +45,7 @@ describe('Test embed HTML generation', function () {
playlist, playlist,
unlistedPlaylistId, unlistedPlaylistId,
privatePlaylistId, privatePlaylistId,
instanceDescription instanceConfig
} = await prepareClientTests()) } = await prepareClientTests())
}) })
@ -58,7 +59,7 @@ describe('Test embed HTML generation', function () {
it('Should have the correct embed html instance tags', async function () { it('Should have the correct embed html instance tags', async function () {
const res = await makeHTMLRequest(servers[0].url, '/videos/embed/toto') const res = await makeHTMLRequest(servers[0].url, '/videos/embed/toto')
checkIndexTags(res.text, `PeerTube`, instanceDescription, '', config) checkIndexTags(res.text, `PeerTube`, instanceConfig.shortDescription, '', config)
expect(res.text).to.not.contain(`"name":`) expect(res.text).to.not.contain(`"name":`)
}) })

View File

@ -35,8 +35,7 @@ describe('Test index HTML generation', function () {
passwordProtectedVideoId, passwordProtectedVideoId,
unlistedVideoId, unlistedVideoId,
privatePlaylistId, privatePlaylistId,
unlistedPlaylistId, unlistedPlaylistId
instanceDescription
} = await prepareClientTests()) } = await prepareClientTests())
}) })

View File

@ -22,11 +22,18 @@ describe('Test Open Graph and Twitter cards HTML tags', function () {
let playlistIds: (string | number)[] = [] let playlistIds: (string | number)[] = []
let instanceConfig: {
name: string
shortDescription: string
avatar: string
}
before(async function () { before(async function () {
this.timeout(120000); this.timeout(120000);
({ ({
servers, servers,
instanceConfig,
account, account,
playlistIds, playlistIds,
videoIds, videoIds,
@ -41,6 +48,20 @@ describe('Test Open Graph and Twitter cards HTML tags', function () {
describe('Open Graph', function () { describe('Open Graph', function () {
async function indexPageTest (path: string) {
const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
const text = res.text
let url = servers[0].url
if (path !== '/') url += path
expect(text).to.contain(`<meta property="og:title" content="${instanceConfig.name}" />`)
expect(text).to.contain(`<meta property="og:description" content="${instanceConfig.shortDescription}" />`)
expect(text).to.contain('<meta property="og:type" content="website" />')
expect(text).to.contain(`<meta property="og:url" content="${url}`)
expect(text).to.contain(`<meta property="og:image" content="${servers[0].url}/`)
}
async function accountPageTest (path: string) { async function accountPageTest (path: string) {
const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 }) const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
const text = res.text const text = res.text
@ -49,6 +70,7 @@ describe('Test Open Graph and Twitter cards HTML tags', function () {
expect(text).to.contain(`<meta property="og:description" content="${account.description}" />`) expect(text).to.contain(`<meta property="og:description" content="${account.description}" />`)
expect(text).to.contain('<meta property="og:type" content="website" />') expect(text).to.contain('<meta property="og:type" content="website" />')
expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/a/${servers[0].store.user.username}/video-channels" />`) expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/a/${servers[0].store.user.username}/video-channels" />`)
expect(text).to.not.contain(`<meta property="og:image"`)
} }
async function channelPageTest (path: string) { async function channelPageTest (path: string) {
@ -59,6 +81,7 @@ describe('Test Open Graph and Twitter cards HTML tags', function () {
expect(text).to.contain(`<meta property="og:description" content="${channelDescription}" />`) expect(text).to.contain(`<meta property="og:description" content="${channelDescription}" />`)
expect(text).to.contain('<meta property="og:type" content="website" />') expect(text).to.contain('<meta property="og:type" content="website" />')
expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/c/${servers[0].store.channel.name}/videos" />`) expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/c/${servers[0].store.channel.name}/videos" />`)
expect(text).to.contain(`<meta property="og:image" content="${servers[0].url}/`)
} }
async function watchVideoPageTest (path: string) { async function watchVideoPageTest (path: string) {
@ -69,6 +92,7 @@ describe('Test Open Graph and Twitter cards HTML tags', function () {
expect(text).to.contain(`<meta property="og:description" content="${videoDescriptionPlainText}" />`) expect(text).to.contain(`<meta property="og:description" content="${videoDescriptionPlainText}" />`)
expect(text).to.contain('<meta property="og:type" content="video" />') expect(text).to.contain('<meta property="og:type" content="video" />')
expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/w/${servers[0].store.video.shortUUID}" />`) expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/w/${servers[0].store.video.shortUUID}" />`)
expect(text).to.contain(`<meta property="og:image" content="${servers[0].url}/`)
} }
async function watchPlaylistPageTest (path: string) { async function watchPlaylistPageTest (path: string) {
@ -79,8 +103,16 @@ describe('Test Open Graph and Twitter cards HTML tags', function () {
expect(text).to.contain(`<meta property="og:description" content="${playlistDescription}" />`) expect(text).to.contain(`<meta property="og:description" content="${playlistDescription}" />`)
expect(text).to.contain('<meta property="og:type" content="video" />') expect(text).to.contain('<meta property="og:type" content="video" />')
expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/w/p/${playlist.shortUUID}" />`) expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/w/p/${playlist.shortUUID}" />`)
expect(text).to.contain(`<meta property="og:image" content="${servers[0].url}/`)
} }
it('Should have valid Open Graph tags on the common page', async function () {
await indexPageTest('/about/peertube')
await indexPageTest('/videos')
await indexPageTest('/homepage')
await indexPageTest('/')
})
it('Should have valid Open Graph tags on the account page', async function () { it('Should have valid Open Graph tags on the account page', async function () {
await accountPageTest('/accounts/' + servers[0].store.user.username) await accountPageTest('/accounts/' + servers[0].store.user.username)
await accountPageTest('/a/' + servers[0].store.user.username) await accountPageTest('/a/' + servers[0].store.user.username)
@ -135,6 +167,7 @@ describe('Test Open Graph and Twitter cards HTML tags', function () {
expect(text).to.contain('<meta property="twitter:card" content="summary" />') expect(text).to.contain('<meta property="twitter:card" content="summary" />')
expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />') expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />')
expect(text).to.not.contain(`<meta property="twitter:image"`)
} }
async function channelPageTest (path: string) { async function channelPageTest (path: string) {
@ -143,6 +176,7 @@ describe('Test Open Graph and Twitter cards HTML tags', function () {
expect(text).to.contain('<meta property="twitter:card" content="summary" />') expect(text).to.contain('<meta property="twitter:card" content="summary" />')
expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />') expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />')
expect(text).to.contain(`<meta property="twitter:image" content="${servers[0].url}`)
} }
async function watchVideoPageTest (path: string) { async function watchVideoPageTest (path: string) {
@ -151,6 +185,7 @@ describe('Test Open Graph and Twitter cards HTML tags', function () {
expect(text).to.contain('<meta property="twitter:card" content="player" />') expect(text).to.contain('<meta property="twitter:card" content="player" />')
expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />') expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />')
expect(text).to.contain(`<meta property="twitter:image" content="${servers[0].url}`)
} }
async function watchPlaylistPageTest (path: string) { async function watchPlaylistPageTest (path: string) {
@ -159,6 +194,7 @@ describe('Test Open Graph and Twitter cards HTML tags', function () {
expect(text).to.contain('<meta property="twitter:card" content="player" />') expect(text).to.contain('<meta property="twitter:card" content="player" />')
expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />') expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />')
expect(text).to.contain(`<meta property="twitter:image" content="${servers[0].url}`)
} }
it('Should have valid twitter card on the watch video page', async function () { it('Should have valid twitter card on the watch video page', async function () {

View File

@ -5,7 +5,8 @@ import {
VideoPlaylistCreateResult, VideoPlaylistCreateResult,
Account, Account,
HTMLServerConfig, HTMLServerConfig,
ServerConfig ServerConfig,
ActorImageType
} from '@peertube/peertube-models' } from '@peertube/peertube-models'
import { import {
createMultipleServers, createMultipleServers,
@ -43,11 +44,22 @@ export async function prepareClientTests () {
const servers = await createMultipleServers(2) const servers = await createMultipleServers(2)
await setAccessTokensToServers(servers) await setAccessTokensToServers(servers)
await doubleFollow(servers[0], servers[1]) await doubleFollow(servers[0], servers[1])
await setDefaultVideoChannel(servers) await setDefaultVideoChannel(servers)
const instanceConfig = {
name: 'super instance title',
shortDescription: 'super instance description',
avatar: 'avatar.png'
}
await servers[0].config.updateExistingConfig({
newConfig: {
instance: { name: instanceConfig.name, shortDescription: instanceConfig.shortDescription }
}
})
await servers[0].config.updateInstanceImage({ type: ActorImageType.AVATAR, fixture: instanceConfig.avatar })
let account: Account let account: Account
let videoIds: (string | number)[] = [] let videoIds: (string | number)[] = []
@ -60,8 +72,6 @@ export async function prepareClientTests () {
let privatePlaylistId: string let privatePlaylistId: string
let unlistedPlaylistId: string let unlistedPlaylistId: string
const instanceDescription = 'PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.'
const videoName = 'my super name for server 1' const videoName = 'my super name for server 1'
const videoDescription = 'my<br> super __description__ for *server* 1<p></p>' const videoDescription = 'my<br> super __description__ for *server* 1<p></p>'
const videoDescriptionPlainText = 'my super description for server 1' const videoDescriptionPlainText = 'my super description for server 1'
@ -77,6 +87,8 @@ export async function prepareClientTests () {
attributes: { description: channelDescription } attributes: { description: channelDescription }
}) })
await servers[0].channels.updateImage({ channelName: servers[0].store.channel.name, fixture: 'avatar.png', type: 'avatar' })
// Public video // Public video
{ {
@ -154,7 +166,7 @@ export async function prepareClientTests () {
return { return {
servers, servers,
instanceDescription, instanceConfig,
account, account,

View File

@ -84,9 +84,7 @@ export class ActorHtml {
updatedAt: entity.updatedAt updatedAt: entity.updatedAt
}, },
indexationPolicy: entity.Actor.isOwned() forbidIndexation: !entity.Actor.isOwned()
? 'always'
: 'never'
}, {}) }, {})
return customHTML return customHTML

View File

@ -14,6 +14,6 @@ export class CommonEmbedHtml {
let htmlResult = TagsHtml.addTitleTag(html) let htmlResult = TagsHtml.addTitleTag(html)
htmlResult = TagsHtml.addDescriptionTag(htmlResult) htmlResult = TagsHtml.addDescriptionTag(htmlResult)
return TagsHtml.addTags(htmlResult, { indexationPolicy: 'never' }, { playlist, video }) return TagsHtml.addTags(htmlResult, { forbidIndexation: true }, { playlist, video })
} }
} }

View File

@ -1,15 +1,17 @@
import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '@peertube/peertube-core-utils' import { buildFileLocale, escapeHTML, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '@peertube/peertube-core-utils'
import { ActorImageType, HTMLServerConfig } from '@peertube/peertube-models'
import { isTestOrDevInstance, root, sha256 } from '@peertube/peertube-node-utils' import { isTestOrDevInstance, root, sha256 } from '@peertube/peertube-node-utils'
import { CONFIG } from '@server/initializers/config.js'
import { ActorImageModel } from '@server/models/actor/actor-image.js'
import { getServerActor } from '@server/models/application/application.js'
import express from 'express' import express from 'express'
import { pathExists } from 'fs-extra/esm'
import { readFile } from 'fs/promises' import { readFile } from 'fs/promises'
import { join } from 'path' import { join } from 'path'
import { logger } from '../../../helpers/logger.js' import { logger } from '../../../helpers/logger.js'
import { CUSTOM_HTML_TAG_COMMENTS, FILES_CONTENT_HASH, PLUGIN_GLOBAL_CSS_PATH } from '../../../initializers/constants.js' import { CUSTOM_HTML_TAG_COMMENTS, FILES_CONTENT_HASH, PLUGIN_GLOBAL_CSS_PATH, WEBSERVER } from '../../../initializers/constants.js'
import { ServerConfigManager } from '../../server-config-manager.js' import { ServerConfigManager } from '../../server-config-manager.js'
import { TagsHtml } from './tags-html.js' import { TagsHtml } from './tags-html.js'
import { pathExists } from 'fs-extra/esm'
import { HTMLServerConfig } from '@peertube/peertube-models'
import { CONFIG } from '@server/initializers/config.js'
export class PageHtml { export class PageHtml {
@ -22,13 +24,33 @@ export class PageHtml {
} }
static async getDefaultHTML (req: express.Request, res: express.Response, paramLang?: string) { static async getDefaultHTML (req: express.Request, res: express.Response, paramLang?: string) {
const html = paramLang const html = await this.getIndexHTML(req, res, paramLang)
? await this.getIndexHTML(req, res, paramLang) const serverActor = await getServerActor()
: await this.getIndexHTML(req, res) const avatar = serverActor.getMaxQualityImage(ActorImageType.AVATAR)
let customHTML = TagsHtml.addTitleTag(html) let customHTML = TagsHtml.addTitleTag(html)
customHTML = TagsHtml.addDescriptionTag(customHTML) customHTML = TagsHtml.addDescriptionTag(customHTML)
const url = req.originalUrl === '/'
? WEBSERVER.URL
: WEBSERVER.URL + req.originalUrl
customHTML = await TagsHtml.addTags(customHTML, {
url,
escapedSiteName: escapeHTML(CONFIG.INSTANCE.NAME),
escapedTitle: escapeHTML(CONFIG.INSTANCE.NAME),
escapedTruncatedDescription: escapeHTML(CONFIG.INSTANCE.SHORT_DESCRIPTION),
image: avatar
? { url: ActorImageModel.getImageUrl(avatar), width: avatar.width, height: avatar.height }
: undefined,
ogType: 'website',
twitterCard: 'summary_large_image',
forbidIndexation: false
}, {})
return customHTML return customHTML
} }

View File

@ -113,11 +113,11 @@ export class PlaylistHtml {
escapedTitle: escapeHTML(playlist.name), escapedTitle: escapeHTML(playlist.name),
escapedTruncatedDescription, escapedTruncatedDescription,
indexationPolicy: !playlist.isOwned() || playlist.privacy !== VideoPlaylistPrivacy.PUBLIC forbidIndexation: !playlist.isOwned() || playlist.privacy !== VideoPlaylistPrivacy.PUBLIC,
? 'never'
: 'always',
image: { url: playlist.getThumbnailUrl() }, image: playlist.Thumbnail
? { url: playlist.getThumbnailUrl(), width: playlist.Thumbnail.width, height: playlist.Thumbnail.height }
: undefined,
list, list,

View File

@ -7,7 +7,7 @@ import truncate from 'lodash-es/truncate.js'
import { mdToOneLinePlainText } from '@server/helpers/markdown.js' import { mdToOneLinePlainText } from '@server/helpers/markdown.js'
type Tags = { type Tags = {
indexationPolicy: 'always' | 'never' forbidIndexation: boolean
url?: string url?: string
@ -31,8 +31,8 @@ type Tags = {
image?: { image?: {
url: string url: string
width?: number width: number
height?: number height: number
} }
embed?: { embed?: {
@ -76,7 +76,7 @@ export class TagsHtml {
const twitterCardMetaTags = this.generateTwitterCardMetaTagsOptions(tagsValues) const twitterCardMetaTags = this.generateTwitterCardMetaTagsOptions(tagsValues)
const schemaTags = await this.generateSchemaTagsOptions(tagsValues, context) const schemaTags = await this.generateSchemaTagsOptions(tagsValues, context)
const { url, escapedTitle, oembedUrl, indexationPolicy } = tagsValues const { url, escapedTitle, oembedUrl, forbidIndexation } = tagsValues
const oembedLinkTags: { type: string, href: string, escapedTitle: string }[] = [] const oembedLinkTags: { type: string, href: string, escapedTitle: string }[] = []
@ -126,11 +126,11 @@ export class TagsHtml {
} }
// SEO, use origin URL // SEO, use origin URL
if (indexationPolicy !== 'never' && url) { if (forbidIndexation === true && url) {
tagsStr += `<link rel="canonical" href="${url}" />` tagsStr += `<link rel="canonical" href="${url}" />`
} }
if (indexationPolicy === 'never') { if (forbidIndexation === true) {
tagsStr += `<meta name="robots" content="noindex" />` tagsStr += `<meta name="robots" content="noindex" />`
} }

View File

@ -7,7 +7,7 @@ import validator from 'validator'
import { CONFIG } from '../../../initializers/config.js' import { CONFIG } from '../../../initializers/config.js'
import { MEMOIZE_TTL, WEBSERVER } from '../../../initializers/constants.js' import { MEMOIZE_TTL, WEBSERVER } from '../../../initializers/constants.js'
import { VideoModel } from '../../../models/video/video.js' import { VideoModel } from '../../../models/video/video.js'
import { MVideo, MVideoThumbnailBlacklist } from '../../../types/models/index.js' import { MVideo, MVideoThumbnail, MVideoThumbnailBlacklist } from '../../../types/models/index.js'
import { getActivityStreamDuration } from '../../activitypub/activity.js' import { getActivityStreamDuration } from '../../activitypub/activity.js'
import { isVideoInPrivateDirectory } from '../../video-privacy.js' import { isVideoInPrivateDirectory } from '../../video-privacy.js'
import { CommonEmbedHtml } from './common-embed-html.js' import { CommonEmbedHtml } from './common-embed-html.js'
@ -78,7 +78,7 @@ export class VideoHtml {
private static buildVideoHTML (options: { private static buildVideoHTML (options: {
html: string html: string
video: MVideo video: MVideoThumbnail
addOG: boolean addOG: boolean
addTwitterCard: boolean addTwitterCard: boolean
@ -111,6 +111,8 @@ export class VideoHtml {
const schemaType = 'VideoObject' const schemaType = 'VideoObject'
const preview = video.getPreview()
return TagsHtml.addTags(customHTML, { return TagsHtml.addTags(customHTML, {
url: WEBSERVER.URL + video.getWatchStaticPath(), url: WEBSERVER.URL + video.getWatchStaticPath(),
@ -118,11 +120,11 @@ export class VideoHtml {
escapedTitle: escapeHTML(video.name), escapedTitle: escapeHTML(video.name),
escapedTruncatedDescription, escapedTruncatedDescription,
indexationPolicy: video.remote || video.privacy !== VideoPrivacy.PUBLIC forbidIndexation: video.remote || video.privacy !== VideoPrivacy.PUBLIC,
? 'never'
: 'always',
image: { url: WEBSERVER.URL + video.getPreviewStaticPath() }, image: preview
? { url: WEBSERVER.URL + video.getPreviewStaticPath(), width: preview.width, height: preview.height }
: undefined,
embed, embed,
oembedUrl: this.getOEmbedUrl(video, currentQuery), oembedUrl: this.getOEmbedUrl(video, currentQuery),

View File

@ -79,8 +79,8 @@ async function insertFromImportIntoDB (parameters: {
const videoImport = await sequelizeTypescript.transaction(async t => { const videoImport = await sequelizeTypescript.transaction(async t => {
const sequelizeOptions = { transaction: t } const sequelizeOptions = { transaction: t }
// Save video object in database // eslint-disable-next-line max-len
const videoCreated = await video.save(sequelizeOptions) as (MVideoAccountDefault & MVideoWithBlacklistLight & MVideoTag) const videoCreated = await video.save(sequelizeOptions) as (MVideoAccountDefault & MVideoWithBlacklistLight & MVideoTag & MVideoThumbnail)
videoCreated.VideoChannel = videoChannel videoCreated.VideoChannel = videoChannel
if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)

View File

@ -1838,21 +1838,21 @@ export class VideoModel extends SequelizeModel<VideoModel> {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
hasMiniature (this: MVideoThumbnail) { hasMiniature (this: Pick<MVideoThumbnail, 'getMiniature' | 'Thumbnails'>) {
return !!this.getMiniature() return !!this.getMiniature()
} }
getMiniature (this: MVideoThumbnail) { getMiniature (this: Pick<MVideoThumbnail, 'Thumbnails'>) {
if (Array.isArray(this.Thumbnails) === false) return undefined if (Array.isArray(this.Thumbnails) === false) return undefined
return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE) return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE)
} }
hasPreview (this: MVideoThumbnail) { hasPreview (this: Pick<MVideoThumbnail, 'getPreview' | 'Thumbnails'>) {
return !!this.getPreview() return !!this.getPreview()
} }
getPreview (this: MVideoThumbnail) { getPreview (this: Pick<MVideoThumbnail, 'Thumbnails'>) {
if (Array.isArray(this.Thumbnails) === false) return undefined if (Array.isArray(this.Thumbnails) === false) return undefined
return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW) return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
@ -1872,14 +1872,14 @@ export class VideoModel extends SequelizeModel<VideoModel> {
return buildVideoEmbedPath(this) return buildVideoEmbedPath(this)
} }
getMiniatureStaticPath () { getMiniatureStaticPath (this: Pick<MVideoThumbnail, 'getMiniature' | 'Thumbnails'>) {
const thumbnail = this.getMiniature() const thumbnail = this.getMiniature()
if (!thumbnail) return null if (!thumbnail) return null
return thumbnail.getLocalStaticPath() return thumbnail.getLocalStaticPath()
} }
getPreviewStaticPath () { getPreviewStaticPath (this: Pick<MVideoThumbnail, 'getPreview' | 'Thumbnails'>) {
const preview = this.getPreview() const preview = this.getPreview()
if (!preview) return null if (!preview) return null

View File

@ -40,7 +40,7 @@ export type MVideoAbuseVideoFull =
export type MVideoAbuseFormattable = export type MVideoAbuseFormattable =
MVideoAbuse & MVideoAbuse &
UseVideoAbuse<'Video', Pick<MVideoAccountLightBlacklistAllFiles, UseVideoAbuse<'Video', Pick<MVideoAccountLightBlacklistAllFiles,
'id' | 'uuid' | 'name' | 'nsfw' | 'getMiniatureStaticPath' | 'isBlacklisted' | 'VideoChannel'>> 'id' | 'uuid' | 'name' | 'nsfw' | 'getMiniature' | 'getMiniatureStaticPath' | 'isBlacklisted' | 'VideoChannel' | 'Thumbnails'>>
// ############################################################################ // ############################################################################

View File

@ -217,7 +217,7 @@ export type MVideoForRedundancyAPI =
// Format for API or AP object // Format for API or AP object
export type MVideoFormattable = export type MVideoFormattable =
MVideo & MVideoThumbnail &
PickWithOpt<VideoModel, 'UserVideoHistories', MUserVideoHistoryTime[]> & PickWithOpt<VideoModel, 'UserVideoHistories', MUserVideoHistoryTime[]> &
Use<'VideoChannel', MChannelAccountSummaryFormattable> & Use<'VideoChannel', MChannelAccountSummaryFormattable> &
PickWithOpt<VideoModel, 'ScheduleVideoUpdate', Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'>> & PickWithOpt<VideoModel, 'ScheduleVideoUpdate', Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'>> &