Don't guess remote tracker URL

This commit is contained in:
Chocobozzz 2021-02-18 10:15:11 +01:00 committed by Chocobozzz
parent 2451916e45
commit d9a2a03196
21 changed files with 458 additions and 94 deletions

View File

@ -201,10 +201,12 @@ function checkUrlsSameHost (url1: string, url2: string) {
return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase() return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
} }
function buildRemoteVideoBaseUrl (video: MVideoWithHost, path: string) { function buildRemoteVideoBaseUrl (video: MVideoWithHost, path: string, scheme?: string) {
if (!scheme) scheme = REMOTE_SCHEME.HTTP
const host = video.VideoChannel.Actor.Server.host const host = video.VideoChannel.Actor.Server.host
return REMOTE_SCHEME.HTTP + '://' + host + path return scheme + '://' + host + path
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -1,4 +1,7 @@
import validator from 'validator' import validator from 'validator'
import { logger } from '@server/helpers/logger'
import { ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject } from '@shared/models'
import { VideoState } from '../../../../shared/models/videos'
import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants' import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants'
import { peertubeTruncate } from '../../core-utils' import { peertubeTruncate } from '../../core-utils'
import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc' import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
@ -11,9 +14,6 @@ import {
isVideoViewsValid isVideoViewsValid
} from '../videos' } from '../videos'
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
import { VideoState } from '../../../../shared/models/videos'
import { logger } from '@server/helpers/logger'
import { ActivityVideoFileMetadataObject } from '@shared/models'
function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
return isBaseActivityValid(activity, 'Update') && return isBaseActivityValid(activity, 'Update') &&
@ -84,6 +84,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
function isRemoteVideoUrlValid (url: any) { function isRemoteVideoUrlValid (url: any) {
return url.type === 'Link' && return url.type === 'Link' &&
// Video file link
( (
ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.includes(url.mediaType) && ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.includes(url.mediaType) &&
isActivityPubUrlValid(url.href) && isActivityPubUrlValid(url.href) &&
@ -91,31 +92,41 @@ function isRemoteVideoUrlValid (url: any) {
validator.isInt(url.size + '', { min: 0 }) && validator.isInt(url.size + '', { min: 0 }) &&
(!url.fps || validator.isInt(url.fps + '', { min: -1 })) (!url.fps || validator.isInt(url.fps + '', { min: -1 }))
) || ) ||
// Torrent link
( (
ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.includes(url.mediaType) && ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.includes(url.mediaType) &&
isActivityPubUrlValid(url.href) && isActivityPubUrlValid(url.href) &&
validator.isInt(url.height + '', { min: 0 }) validator.isInt(url.height + '', { min: 0 })
) || ) ||
// Magnet link
( (
ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.includes(url.mediaType) && ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.includes(url.mediaType) &&
validator.isLength(url.href, { min: 5 }) && validator.isLength(url.href, { min: 5 }) &&
validator.isInt(url.height + '', { min: 0 }) validator.isInt(url.height + '', { min: 0 })
) || ) ||
// HLS playlist link
( (
(url.mediaType || url.mimeType) === 'application/x-mpegURL' && (url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
isActivityPubUrlValid(url.href) && isActivityPubUrlValid(url.href) &&
isArray(url.tag) isArray(url.tag)
) || ) ||
isAPVideoFileMetadataObject(url) isAPVideoTrackerUrlObject(url) ||
isAPVideoFileUrlMetadataObject(url)
} }
function isAPVideoFileMetadataObject (url: any): url is ActivityVideoFileMetadataObject { function isAPVideoFileUrlMetadataObject (url: any): url is ActivityVideoFileMetadataUrlObject {
return url && return url &&
url.type === 'Link' && url.type === 'Link' &&
url.mediaType === 'application/json' && url.mediaType === 'application/json' &&
isArray(url.rel) && url.rel.includes('metadata') isArray(url.rel) && url.rel.includes('metadata')
} }
function isAPVideoTrackerUrlObject (url: any): url is ActivityTrackerUrlObject {
return isArray(url.rel) &&
url.rel.includes('tracker') &&
isActivityPubUrlValid(url.href)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
@ -123,7 +134,8 @@ export {
isRemoteStringIdentifierValid, isRemoteStringIdentifierValid,
sanitizeAndCheckVideoTorrentObject, sanitizeAndCheckVideoTorrentObject,
isRemoteVideoUrlValid, isRemoteVideoUrlValid,
isAPVideoFileMetadataObject isAPVideoFileUrlMetadataObject,
isAPVideoTrackerUrlObject
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -107,16 +107,13 @@ async function createTorrentAndSetInfoHash (
videoFile.torrentFilename = torrentFilename videoFile.torrentFilename = torrentFilename
} }
// FIXME: merge/refactor videoOrPlaylist and video arguments
function generateMagnetUri ( function generateMagnetUri (
videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
video: MVideoWithHost, video: MVideoWithHost,
videoFile: MVideoFileRedundanciesOpt, videoFile: MVideoFileRedundanciesOpt,
baseUrlHttp: string, trackerUrls: string[]
baseUrlWs: string
) { ) {
const xs = videoFile.getTorrentUrl() const xs = videoFile.getTorrentUrl()
const announce = videoOrPlaylist.getTrackerUrls(baseUrlHttp, baseUrlWs) const announce = trackerUrls
let urlList = [ videoFile.getFileUrl(video) ] let urlList = [ videoFile.getFileUrl(video) ]
const redundancies = videoFile.RedundancyVideos const redundancies = videoFile.RedundancyVideos

View File

@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 585 const LAST_MIGRATION_VERSION = 595
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -1,3 +1,5 @@
import { TrackerModel } from '@server/models/server/tracker'
import { VideoTrackerModel } from '@server/models/server/video-tracker'
import { QueryTypes, Transaction } from 'sequelize' import { QueryTypes, Transaction } from 'sequelize'
import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' import { Sequelize as SequelizeTypescript } from 'sequelize-typescript'
import { isTestInstance } from '../helpers/core-utils' import { isTestInstance } from '../helpers/core-utils'
@ -128,6 +130,8 @@ async function initDatabaseModels (silent: boolean) {
VideoPlaylistModel, VideoPlaylistModel,
VideoPlaylistElementModel, VideoPlaylistElementModel,
ThumbnailModel, ThumbnailModel,
TrackerModel,
VideoTrackerModel,
PluginModel PluginModel
]) ])

View File

@ -0,0 +1,44 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
db: any
}): Promise<void> {
{
const query = `CREATE TABLE IF NOT EXISTS "tracker" (
"id" serial,
"url" varchar(255) NOT NULL,
"createdAt" timestamp WITH time zone NOT NULL,
"updatedAt" timestamp WITH time zone NOT NULL,
PRIMARY KEY ("id")
);`
await utils.sequelize.query(query)
}
{
const query = `CREATE TABLE IF NOT EXISTS "videoTracker" (
"videoId" integer REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
"trackerId" integer REFERENCES "tracker" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
"createdAt" timestamp WITH time zone NOT NULL,
"updatedAt" timestamp WITH time zone NOT NULL,
UNIQUE ("videoId", "trackerId"),
PRIMARY KEY ("videoId", "trackerId")
);`
await utils.sequelize.query(query)
}
await utils.sequelize.query(`CREATE UNIQUE INDEX "tracker_url" ON "tracker" ("url")`)
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -0,0 +1,130 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
db: any
}): Promise<void> {
// Torrent and file URLs
{
const fromQueryWebtorrent = `SELECT 'https://' || server.host AS "serverUrl", '/static/webseed/' AS "filePath", "videoFile".id ` +
`FROM video ` +
`INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
`INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
`INNER JOIN server ON server.id = actor."serverId" ` +
`INNER JOIN "videoFile" ON "videoFile"."videoId" = video.id ` +
`WHERE video.remote IS TRUE`
const fromQueryHLS = `SELECT 'https://' || server.host AS "serverUrl", ` +
`'/static/streaming-playlists/hls/' || video.uuid || '/' AS "filePath", "videoFile".id ` +
`FROM video ` +
`INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
`INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
`INNER JOIN server ON server.id = actor."serverId" ` +
`INNER JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."videoId" = video.id ` +
`INNER JOIN "videoFile" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id ` +
`WHERE video.remote IS TRUE`
for (const fromQuery of [ fromQueryWebtorrent, fromQueryHLS ]) {
const query = `UPDATE "videoFile" ` +
`SET "torrentUrl" = t."serverUrl" || '/static/torrents/' || "videoFile"."torrentFilename", ` +
`"fileUrl" = t."serverUrl" || t."filePath" || "videoFile"."filename" ` +
`FROM (${fromQuery}) AS t WHERE t.id = "videoFile"."id" AND "videoFile"."fileUrl" IS NULL`
await utils.sequelize.query(query)
}
}
// Caption URLs
{
const fromQuery = `SELECT 'https://' || server.host AS "serverUrl", "video".uuid, "videoCaption".id ` +
`FROM video ` +
`INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
`INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
`INNER JOIN server ON server.id = actor."serverId" ` +
`INNER JOIN "videoCaption" ON "videoCaption"."videoId" = video.id ` +
`WHERE video.remote IS TRUE`
const query = `UPDATE "videoCaption" ` +
`SET "fileUrl" = t."serverUrl" || '/lazy-static/video-captions/' || t.uuid || '-' || "videoCaption"."language" || '.vtt' ` +
`FROM (${fromQuery}) AS t WHERE t.id = "videoCaption"."id" AND "videoCaption"."fileUrl" IS NULL`
await utils.sequelize.query(query)
}
// Thumbnail URLs
{
const fromQuery = `SELECT 'https://' || server.host AS "serverUrl", "video".uuid, "thumbnail".id ` +
`FROM video ` +
`INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
`INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
`INNER JOIN server ON server.id = actor."serverId" ` +
`INNER JOIN "thumbnail" ON "thumbnail"."videoId" = video.id ` +
`WHERE video.remote IS TRUE`
// Thumbnails
{
const query = `UPDATE "thumbnail" ` +
`SET "fileUrl" = t."serverUrl" || '/static/thumbnails/' || t.uuid || '.jpg' ` +
`FROM (${fromQuery}) AS t WHERE t.id = "thumbnail"."id" AND "thumbnail"."fileUrl" IS NULL AND thumbnail.type = 1`
await utils.sequelize.query(query)
}
{
// Previews
const query = `UPDATE "thumbnail" ` +
`SET "fileUrl" = t."serverUrl" || '/lazy-static/previews/' || t.uuid || '.jpg' ` +
`FROM (${fromQuery}) AS t WHERE t.id = "thumbnail"."id" AND "thumbnail"."fileUrl" IS NULL AND thumbnail.type = 2`
await utils.sequelize.query(query)
}
}
// Trackers
{
const trackerUrls = [
`'https://' || server.host || '/tracker/announce'`,
`'wss://' || server.host || '/tracker/socket'`
]
for (const trackerUrl of trackerUrls) {
{
const query = `INSERT INTO "tracker" ("url", "createdAt", "updatedAt") ` +
`SELECT ${trackerUrl} AS "url", NOW(), NOW() ` +
`FROM video ` +
`INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
`INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
`INNER JOIN server ON server.id = actor."serverId" ` +
`WHERE video.remote IS TRUE ` +
`ON CONFLICT DO NOTHING`
await utils.sequelize.query(query)
}
{
const query = `INSERT INTO "videoTracker" ("videoId", "trackerId", "createdAt", "updatedAt") ` +
`SELECT video.id, (SELECT tracker.id FROM tracker WHERE url = ${trackerUrl}) AS "trackerId", NOW(), NOW()` +
`FROM video ` +
`INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
`INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
`INNER JOIN server ON server.id = actor."serverId" ` +
`WHERE video.remote IS TRUE`
await utils.sequelize.query(query)
}
}
}
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -3,7 +3,8 @@ import { maxBy, minBy } from 'lodash'
import * as magnetUtil from 'magnet-uri' import * as magnetUtil from 'magnet-uri'
import { basename, join } from 'path' import { basename, join } from 'path'
import * as request from 'request' import * as request from 'request'
import * as sequelize from 'sequelize' import { Transaction } from 'sequelize/types'
import { TrackerModel } from '@server/models/server/tracker'
import { VideoLiveModel } from '@server/models/video/video-live' import { VideoLiveModel } from '@server/models/video/video-live'
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
import { import {
@ -16,12 +17,16 @@ import {
ActivityUrlObject, ActivityUrlObject,
ActivityVideoUrlObject ActivityVideoUrlObject
} from '../../../shared/index' } from '../../../shared/index'
import { ActivityIconObject, VideoObject } from '../../../shared/models/activitypub/objects' import { ActivityIconObject, ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects'
import { VideoPrivacy } from '../../../shared/models/videos' import { VideoPrivacy } from '../../../shared/models/videos'
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
import { buildRemoteVideoBaseUrl, checkUrlsSameHost, getAPId } from '../../helpers/activitypub' import { buildRemoteVideoBaseUrl, checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
import { isAPVideoFileMetadataObject, sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' import {
isAPVideoFileUrlMetadataObject,
isAPVideoTrackerUrlObject,
sanitizeAndCheckVideoTorrentObject
} from '../../helpers/custom-validators/activitypub/videos'
import { isArray } from '../../helpers/custom-validators/misc' import { isArray } from '../../helpers/custom-validators/misc'
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
@ -83,7 +88,7 @@ import { addVideoShares, shareVideoByServerAndChannel } from './share'
import { addVideoComments } from './video-comments' import { addVideoComments } from './video-comments'
import { createRates } from './video-rates' import { createRates } from './video-rates'
async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) { async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: Transaction) {
const video = videoArg as MVideoAP const video = videoArg as MVideoAP
if ( if (
@ -433,6 +438,12 @@ async function updateVideoFromAP (options: {
await setVideoTags({ video: videoUpdated, tags, transaction: t, defaultValue: videoUpdated.Tags }) await setVideoTags({ video: videoUpdated, tags, transaction: t, defaultValue: videoUpdated.Tags })
} }
// Update trackers
{
const trackers = getTrackerUrls(videoObject, videoUpdated)
await setVideoTrackers({ video: videoUpdated, trackers, transaction: t })
}
{ {
// Update captions // Update captions
await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t) await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t)
@ -577,7 +588,7 @@ function isAPVideoUrlObject (url: any): url is ActivityVideoUrlObject {
return MIMETYPES.VIDEO.MIMETYPE_EXT[urlMediaType] && urlMediaType.startsWith('video/') return MIMETYPES.VIDEO.MIMETYPE_EXT[urlMediaType] && urlMediaType.startsWith('video/')
} }
function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject { function isAPStreamingPlaylistUrlObject (url: any): url is ActivityPlaylistUrlObject {
return url && url.mediaType === 'application/x-mpegURL' return url && url.mediaType === 'application/x-mpegURL'
} }
@ -671,6 +682,12 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
}) })
await Promise.all(videoCaptionsPromises) await Promise.all(videoCaptionsPromises)
// Process trackers
{
const trackers = getTrackerUrls(videoObject, videoCreated)
await setVideoTrackers({ video: videoCreated, trackers, transaction: t })
}
videoCreated.VideoFiles = videoFiles videoCreated.VideoFiles = videoFiles
if (videoCreated.isLive) { if (videoCreated.isLive) {
@ -797,7 +814,7 @@ function videoFileActivityUrlToDBAttributes (
: parsed.xs : parsed.xs
// Fetch associated metadata url, if any // Fetch associated metadata url, if any
const metadata = urls.filter(isAPVideoFileMetadataObject) const metadata = urls.filter(isAPVideoFileUrlMetadataObject)
.find(u => { .find(u => {
return u.height === fileUrl.height && return u.height === fileUrl.height &&
u.fps === fileUrl.fps && u.fps === fileUrl.fps &&
@ -889,3 +906,33 @@ function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoWithHost)
? previewIcon.url ? previewIcon.url
: buildRemoteVideoBaseUrl(video, join(LAZY_STATIC_PATHS.PREVIEWS, video.generatePreviewName())) : buildRemoteVideoBaseUrl(video, join(LAZY_STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
} }
function getTrackerUrls (object: VideoObject, video: MVideoWithHost) {
let wsFound = false
const trackers = object.url.filter(u => isAPVideoTrackerUrlObject(u))
.map((u: ActivityTrackerUrlObject) => {
if (u.rel.includes('websocket')) wsFound = true
return u.href
})
if (wsFound) return trackers
return [
buildRemoteVideoBaseUrl(video, '/tracker/socket', REMOTE_SCHEME.WS),
buildRemoteVideoBaseUrl(video, '/tracker/announce')
]
}
async function setVideoTrackers (options: {
video: MVideo
trackers: string[]
transaction?: Transaction
}) {
const { video, trackers, transaction } = options
const trackerInstances = await TrackerModel.findOrCreateTrackers(trackers, transaction)
await video.$set('Trackers', trackerInstances, { transaction })
}

View File

@ -1,6 +1,7 @@
import { move } from 'fs-extra' import { move } from 'fs-extra'
import { join } from 'path' import { join } from 'path'
import { getServerActor } from '@server/models/application/application' import { getServerActor } from '@server/models/application/application'
import { TrackerModel } from '@server/models/server/tracker'
import { VideoModel } from '@server/models/video/video' import { VideoModel } from '@server/models/video/video'
import { import {
MStreamingPlaylist, MStreamingPlaylist,
@ -221,8 +222,8 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, strategy) logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, strategy)
const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() const trackerUrls = await TrackerModel.listUrlsByVideoId(video.id)
const magnetUri = generateMagnetUri(video, video, file, baseUrlHttp, baseUrlWs) const magnetUri = generateMagnetUri(video, file, trackerUrls)
const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT)

View File

@ -1,5 +1,6 @@
import { copy } from 'fs-extra' import { copy } from 'fs-extra'
import { join } from 'path' import { join } from 'path'
import { logger } from '@server/helpers/logger'
import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils'
import { processImage } from '../helpers/image-utils' import { processImage } from '../helpers/image-utils'
@ -62,7 +63,7 @@ function createVideoMiniatureFromUrl (options: {
size?: ImageSize size?: ImageSize
}) { }) {
const { downloadUrl, video, type, size } = options const { downloadUrl, video, type, size } = options
const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) const { filename: updatedFilename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
// Only save the file URL if it is a remote video // Only save the file URL if it is a remote video
const fileUrl = video.isOwned() const fileUrl = video.isOwned()
@ -76,10 +77,16 @@ function createVideoMiniatureFromUrl (options: {
// If the thumbnail URL did not change and has a unique filename (introduced in 3.2), avoid thumbnail processing // If the thumbnail URL did not change and has a unique filename (introduced in 3.2), avoid thumbnail processing
const thumbnailUrlChanged = !existingUrl || existingUrl !== downloadUrl || downloadUrl.endsWith(`${video.uuid}.jpg`) const thumbnailUrlChanged = !existingUrl || existingUrl !== downloadUrl || downloadUrl.endsWith(`${video.uuid}.jpg`)
// Do not change the thumbnail filename if the file did not change
const filename = thumbnailUrlChanged
? updatedFilename
: existingThumbnail.filename
const thumbnailCreator = () => { const thumbnailCreator = () => {
if (thumbnailUrlChanged) return downloadImage(downloadUrl, basePath, filename, { width, height }) if (thumbnailUrlChanged) return downloadImage(downloadUrl, basePath, filename, { width, height })
return copy(existingThumbnail.getPath(), ThumbnailModel.buildPath(type, filename)) return Promise.resolve()
} }
return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl })
@ -236,7 +243,7 @@ async function createThumbnailFromFunction (parameters: {
fileUrl = null fileUrl = null
} = parameters } = parameters
const oldFilename = existingThumbnail const oldFilename = existingThumbnail && existingThumbnail.filename !== filename
? existingThumbnail.filename ? existingThumbnail.filename
: undefined : undefined
@ -248,7 +255,8 @@ async function createThumbnailFromFunction (parameters: {
thumbnail.type = type thumbnail.type = type
thumbnail.fileUrl = fileUrl thumbnail.fileUrl = fileUrl
thumbnail.automaticallyGenerated = automaticallyGenerated thumbnail.automaticallyGenerated = automaticallyGenerated
thumbnail.previousThumbnailFilename = oldFilename
if (oldFilename) thumbnail.previousThumbnailFilename = oldFilename
await thumbnailCreator() await thumbnailCreator()

View File

@ -0,0 +1,73 @@
import { AllowNull, BelongsToMany, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { Transaction } from 'sequelize/types'
import { MTracker } from '@server/types/models/server/tracker'
import { VideoModel } from '../video/video'
import { VideoTrackerModel } from './video-tracker'
@Table({
tableName: 'tracker',
indexes: [
{
fields: [ 'url' ],
unique: true
}
]
})
export class TrackerModel extends Model {
@AllowNull(false)
@Column
url: string
@CreatedAt
createdAt: Date
@UpdatedAt
updatedAt: Date
@BelongsToMany(() => VideoModel, {
foreignKey: 'trackerId',
through: () => VideoTrackerModel,
onDelete: 'CASCADE'
})
Videos: VideoModel[]
static listUrlsByVideoId (videoId: number) {
const query = {
include: [
{
attributes: [ 'id', 'trackerId' ],
model: VideoModel.unscoped(),
required: true,
where: { id: videoId }
}
]
}
return TrackerModel.findAll(query)
.then(rows => rows.map(rows => rows.url))
}
static findOrCreateTrackers (trackers: string[], transaction: Transaction): Promise<MTracker[]> {
if (trackers === null) return Promise.resolve([])
const tasks: Promise<MTracker>[] = []
trackers.forEach(tracker => {
const query = {
where: {
url: tracker
},
defaults: {
url: tracker
},
transaction
}
const promise = TrackerModel.findOrCreate<MTracker>(query)
.then(([ trackerInstance ]) => trackerInstance)
tasks.push(promise)
})
return Promise.all(tasks)
}
}

View File

@ -0,0 +1,30 @@
import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { VideoModel } from '../video/video'
import { TrackerModel } from './tracker'
@Table({
tableName: 'videoTracker',
indexes: [
{
fields: [ 'videoId' ]
},
{
fields: [ 'trackerId' ]
}
]
})
export class VideoTrackerModel extends Model {
@CreatedAt
createdAt: Date
@UpdatedAt
updatedAt: Date
@ForeignKey(() => VideoModel)
@Column
videoId: number
@ForeignKey(() => TrackerModel)
@Column
trackerId: number
}

View File

@ -15,7 +15,6 @@ import {
Table, Table,
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
import { afterCommitIfTransaction } from '@server/helpers/database-utils' import { afterCommitIfTransaction } from '@server/helpers/database-utils'
import { MThumbnail, MThumbnailVideo, MVideoWithHost } from '@server/types/models' import { MThumbnail, MThumbnailVideo, MVideoWithHost } from '@server/types/models'
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
@ -168,10 +167,8 @@ export class ThumbnailModel extends Model {
const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename
if (video.isOwned()) return WEBSERVER.URL + staticPath if (video.isOwned()) return WEBSERVER.URL + staticPath
if (this.fileUrl) return this.fileUrl
// Fallback if we don't have a file URL return this.fileUrl
return buildRemoteVideoBaseUrl(video, staticPath)
} }
getPath () { getPath () {

View File

@ -16,7 +16,6 @@ import {
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
import { MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo, MVideoWithHost } from '@server/types/models' import { MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo, MVideoWithHost } from '@server/types/models'
import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
@ -208,9 +207,7 @@ export class VideoCaptionModel extends Model {
if (!this.Video) this.Video = video as VideoModel if (!this.Video) this.Video = video as VideoModel
if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath() if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath()
if (this.fileUrl) return this.fileUrl
// Fallback if we don't have a file URL return this.fileUrl
return buildRemoteVideoBaseUrl(video, this.getCaptionStaticPath())
} }
} }

View File

@ -427,10 +427,8 @@ export class VideoFileModel extends Model {
if (!this.Video) this.Video = video as VideoModel if (!this.Video) this.Video = video as VideoModel
if (video.isOwned()) return WEBSERVER.URL + this.getFileStaticPath(video) if (video.isOwned()) return WEBSERVER.URL + this.getFileStaticPath(video)
if (this.fileUrl) return this.fileUrl
// Fallback if we don't have a file URL return this.fileUrl
return buildRemoteVideoBaseUrl(video, this.getFileStaticPath(video))
} }
getFileStaticPath (video: MVideo) { getFileStaticPath (video: MVideo) {
@ -454,10 +452,7 @@ export class VideoFileModel extends Model {
getRemoteTorrentUrl (video: MVideoWithHost) { getRemoteTorrentUrl (video: MVideoWithHost) {
if (video.isOwned()) throw new Error(`Video ${video.url} is not a remote video`) if (video.isOwned()) throw new Error(`Video ${video.url} is not a remote video`)
if (this.torrentUrl) return this.torrentUrl return this.torrentUrl
// Fallback if we don't have a torrent URL
return buildRemoteVideoBaseUrl(video, this.getTorrentStaticPath())
} }
// We proxify torrent requests so use a local URL // We proxify torrent requests so use a local URL

View File

@ -14,8 +14,6 @@ import {
} from '../../lib/activitypub/url' } from '../../lib/activitypub/url'
import { import {
MStreamingPlaylistRedundanciesOpt, MStreamingPlaylistRedundanciesOpt,
MStreamingPlaylistVideo,
MVideo,
MVideoAP, MVideoAP,
MVideoFile, MVideoFile,
MVideoFormattable, MVideoFormattable,
@ -127,8 +125,6 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid
} }
}) })
const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
const tags = video.Tags ? video.Tags.map(t => t.name) : [] const tags = video.Tags ? video.Tags.map(t => t.name) : []
const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists) const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists)
@ -147,14 +143,14 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid
label: VideoModel.getStateLabel(video.state) label: VideoModel.getStateLabel(video.state)
}, },
trackerUrls: video.getTrackerUrls(baseUrlHttp, baseUrlWs), trackerUrls: video.getTrackerUrls(),
files: [], files: [],
streamingPlaylists streamingPlaylists
} }
// Format and sort video files // Format and sort video files
detailsJson.files = videoFilesModelToFormattedJSON(video, video, baseUrlHttp, baseUrlWs, video.VideoFiles) detailsJson.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
return Object.assign(formattedJson, detailsJson) return Object.assign(formattedJson, detailsJson)
} }
@ -165,17 +161,13 @@ function streamingPlaylistsModelToFormattedJSON (
): VideoStreamingPlaylist[] { ): VideoStreamingPlaylist[] {
if (isArray(playlists) === false) return [] if (isArray(playlists) === false) return []
const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
return playlists return playlists
.map(playlist => { .map(playlist => {
const playlistWithVideo = Object.assign(playlist, { Video: video })
const redundancies = isArray(playlist.RedundancyVideos) const redundancies = isArray(playlist.RedundancyVideos)
? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl }))
: [] : []
const files = videoFilesModelToFormattedJSON(playlistWithVideo, video, baseUrlHttp, baseUrlWs, playlist.VideoFiles) const files = videoFilesModelToFormattedJSON(video, playlist.VideoFiles)
return { return {
id: playlist.id, id: playlist.id,
@ -194,14 +186,12 @@ function sortByResolutionDesc (fileA: MVideoFile, fileB: MVideoFile) {
return -1 return -1
} }
// FIXME: refactor/merge model and video arguments
function videoFilesModelToFormattedJSON ( function videoFilesModelToFormattedJSON (
model: MVideo | MStreamingPlaylistVideo,
video: MVideoFormattableDetails, video: MVideoFormattableDetails,
baseUrlHttp: string,
baseUrlWs: string,
videoFiles: MVideoFileRedundanciesOpt[] videoFiles: MVideoFileRedundanciesOpt[]
): VideoFile[] { ): VideoFile[] {
const trackerUrls = video.getTrackerUrls()
return [ ...videoFiles ] return [ ...videoFiles ]
.filter(f => !f.isLive()) .filter(f => !f.isLive())
.sort(sortByResolutionDesc) .sort(sortByResolutionDesc)
@ -213,7 +203,7 @@ function videoFilesModelToFormattedJSON (
}, },
// FIXME: deprecated in 3.2 // FIXME: deprecated in 3.2
magnetUri: generateMagnetUri(model, video, videoFile, baseUrlHttp, baseUrlWs), magnetUri: generateMagnetUri(video, videoFile, trackerUrls),
size: videoFile.size, size: videoFile.size,
fps: videoFile.fps, fps: videoFile.fps,
@ -229,15 +219,13 @@ function videoFilesModelToFormattedJSON (
}) })
} }
// FIXME: refactor/merge model and video arguments
function addVideoFilesInAPAcc ( function addVideoFilesInAPAcc (
acc: ActivityUrlObject[] | ActivityTagObject[], acc: ActivityUrlObject[] | ActivityTagObject[],
model: MVideoAP | MStreamingPlaylistVideo,
video: MVideoWithHost, video: MVideoWithHost,
baseUrlHttp: string,
baseUrlWs: string,
files: MVideoFile[] files: MVideoFile[]
) { ) {
const trackerUrls = video.getTrackerUrls()
const sortedFiles = [ ...files ] const sortedFiles = [ ...files ]
.filter(f => !f.isLive()) .filter(f => !f.isLive())
.sort(sortByResolutionDesc) .sort(sortByResolutionDesc)
@ -271,14 +259,13 @@ function addVideoFilesInAPAcc (
acc.push({ acc.push({
type: 'Link', type: 'Link',
mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
href: generateMagnetUri(model, video, file, baseUrlHttp, baseUrlWs), href: generateMagnetUri(video, file, trackerUrls),
height: file.resolution height: file.resolution
}) })
} }
} }
function videoModelToActivityPubObject (video: MVideoAP): VideoObject { function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
if (!video.Tags) video.Tags = [] if (!video.Tags) video.Tags = []
const tag = video.Tags.map(t => ({ const tag = video.Tags.map(t => ({
@ -319,7 +306,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
} }
] ]
addVideoFilesInAPAcc(url, video, video, baseUrlHttp, baseUrlWs, video.VideoFiles || []) addVideoFilesInAPAcc(url, video, video.VideoFiles || [])
for (const playlist of (video.VideoStreamingPlaylists || [])) { for (const playlist of (video.VideoStreamingPlaylists || [])) {
const tag = playlist.p2pMediaLoaderInfohashes const tag = playlist.p2pMediaLoaderInfohashes
@ -331,8 +318,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
href: playlist.segmentsSha256Url href: playlist.segmentsSha256Url
}) })
const playlistWithVideo = Object.assign(playlist, { Video: video }) addVideoFilesInAPAcc(tag, video, playlist.VideoFiles || [])
addVideoFilesInAPAcc(tag, playlistWithVideo, video, baseUrlHttp, baseUrlWs, playlist.VideoFiles || [])
url.push({ url.push({
type: 'Link', type: 'Link',
@ -342,6 +328,19 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
}) })
} }
for (const trackerUrl of video.getTrackerUrls()) {
const rel2 = trackerUrl.startsWith('http')
? 'http'
: 'websocket'
url.push({
type: 'Link',
name: `tracker-${rel2}`,
rel: [ 'tracker', rel2 ],
href: trackerUrl
})
}
const subtitleLanguage = [] const subtitleLanguage = []
for (const caption of video.VideoCaptions) { for (const caption of video.VideoCaptions) {
subtitleLanguage.push({ subtitleLanguage.push({

View File

@ -60,7 +60,6 @@ import {
API_VERSION, API_VERSION,
CONSTRAINTS_FIELDS, CONSTRAINTS_FIELDS,
LAZY_STATIC_PATHS, LAZY_STATIC_PATHS,
REMOTE_SCHEME,
STATIC_PATHS, STATIC_PATHS,
VIDEO_CATEGORIES, VIDEO_CATEGORIES,
VIDEO_LANGUAGES, VIDEO_LANGUAGES,
@ -107,6 +106,8 @@ import { ActorModel } from '../activitypub/actor'
import { AvatarModel } from '../avatar/avatar' import { AvatarModel } from '../avatar/avatar'
import { VideoRedundancyModel } from '../redundancy/video-redundancy' import { VideoRedundancyModel } from '../redundancy/video-redundancy'
import { ServerModel } from '../server/server' import { ServerModel } from '../server/server'
import { TrackerModel } from '../server/tracker'
import { VideoTrackerModel } from '../server/video-tracker'
import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
import { ScheduleVideoUpdateModel } from './schedule-video-update' import { ScheduleVideoUpdateModel } from './schedule-video-update'
import { TagModel } from './tag' import { TagModel } from './tag'
@ -137,6 +138,7 @@ export enum ScopeNames {
FOR_API = 'FOR_API', FOR_API = 'FOR_API',
WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
WITH_TAGS = 'WITH_TAGS', WITH_TAGS = 'WITH_TAGS',
WITH_TRACKERS = 'WITH_TRACKERS',
WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES', WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES',
WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
WITH_BLACKLISTED = 'WITH_BLACKLISTED', WITH_BLACKLISTED = 'WITH_BLACKLISTED',
@ -320,6 +322,14 @@ export type AvailableForListIDsOptions = {
[ScopeNames.WITH_TAGS]: { [ScopeNames.WITH_TAGS]: {
include: [ TagModel ] include: [ TagModel ]
}, },
[ScopeNames.WITH_TRACKERS]: {
include: [
{
attributes: [ 'id', 'url' ],
model: TrackerModel
}
]
},
[ScopeNames.WITH_BLACKLISTED]: { [ScopeNames.WITH_BLACKLISTED]: {
include: [ include: [
{ {
@ -616,6 +626,13 @@ export class VideoModel extends Model {
}) })
Tags: TagModel[] Tags: TagModel[]
@BelongsToMany(() => TrackerModel, {
foreignKey: 'videoId',
through: () => VideoTrackerModel,
onDelete: 'CASCADE'
})
Trackers: TrackerModel[]
@HasMany(() => ThumbnailModel, { @HasMany(() => ThumbnailModel, {
foreignKey: { foreignKey: {
name: 'videoId', name: 'videoId',
@ -1436,6 +1453,7 @@ export class VideoModel extends Model {
ScopeNames.WITH_SCHEDULED_UPDATE, ScopeNames.WITH_SCHEDULED_UPDATE,
ScopeNames.WITH_THUMBNAILS, ScopeNames.WITH_THUMBNAILS,
ScopeNames.WITH_LIVE, ScopeNames.WITH_LIVE,
ScopeNames.WITH_TRACKERS,
{ method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] }, { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
{ method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
] ]
@ -1887,18 +1905,15 @@ export class VideoModel extends Model {
} }
getFormattedVideoFilesJSON (): VideoFile[] { getFormattedVideoFilesJSON (): VideoFile[] {
const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
let files: VideoFile[] = [] let files: VideoFile[] = []
if (Array.isArray(this.VideoFiles)) { if (Array.isArray(this.VideoFiles)) {
const result = videoFilesModelToFormattedJSON(this, this, baseUrlHttp, baseUrlWs, this.VideoFiles) const result = videoFilesModelToFormattedJSON(this, this.VideoFiles)
files = files.concat(result) files = files.concat(result)
} }
for (const p of (this.VideoStreamingPlaylists || [])) { for (const p of (this.VideoStreamingPlaylists || [])) {
p.Video = this const result = videoFilesModelToFormattedJSON(this, p.VideoFiles)
const result = videoFilesModelToFormattedJSON(p, this, baseUrlHttp, baseUrlWs, p.VideoFiles)
files = files.concat(result) files = files.concat(result)
} }
@ -2030,25 +2045,18 @@ export class VideoModel extends Model {
return false return false
} }
getBaseUrls () {
if (this.isOwned()) {
return {
baseUrlHttp: WEBSERVER.URL,
baseUrlWs: WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT
}
}
return {
baseUrlHttp: REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host,
baseUrlWs: REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
}
}
getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
}
getBandwidthBits (videoFile: MVideoFile) { getBandwidthBits (videoFile: MVideoFile) {
return Math.ceil((videoFile.size * 8) / this.duration) return Math.ceil((videoFile.size * 8) / this.duration)
} }
getTrackerUrls () {
if (this.isOwned()) {
return [
WEBSERVER.URL + '/tracker/announce',
WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket'
]
}
return this.Trackers.map(t => t.url)
}
} }

View File

@ -0,0 +1,7 @@
import { TrackerModel } from '../../../models/server/tracker'
export type MTracker = Omit<TrackerModel, 'Videos'>
// ############################################################################
export type MTrackerUrl = Pick<MTracker, 'url'>

View File

@ -1,5 +1,6 @@
import { PickWith, PickWithOpt } from '@shared/core-utils' import { PickWith, PickWithOpt } from '@shared/core-utils'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { MTrackerUrl } from '../server/tracker'
import { MUserVideoHistoryTime } from '../user/user-video-history' import { MUserVideoHistoryTime } from '../user/user-video-history'
import { MScheduleVideoUpdate } from './schedule-video-update' import { MScheduleVideoUpdate } from './schedule-video-update'
import { MTag } from './tag' import { MTag } from './tag'
@ -216,4 +217,5 @@ export type MVideoFormattableDetails =
Use<'VideoChannel', MChannelFormattable> & Use<'VideoChannel', MChannelFormattable> &
Use<'Tags', MTag[]> & Use<'Tags', MTag[]> &
Use<'VideoStreamingPlaylists', MStreamingPlaylistRedundanciesOpt[]> & Use<'VideoStreamingPlaylists', MStreamingPlaylistRedundanciesOpt[]> &
Use<'VideoFiles', MVideoFileRedundanciesOpt[]> Use<'VideoFiles', MVideoFileRedundanciesOpt[]> &
Use<'Trackers', MTrackerUrl[]>

View File

@ -30,7 +30,7 @@ export type ActivityPlaylistSegmentHashesObject = {
href: string href: string
} }
export type ActivityVideoFileMetadataObject = { export type ActivityVideoFileMetadataUrlObject = {
type: 'Link' type: 'Link'
rel: [ 'metadata', any ] rel: [ 'metadata', any ]
mediaType: 'application/json' mediaType: 'application/json'
@ -39,6 +39,13 @@ export type ActivityVideoFileMetadataObject = {
fps: number fps: number
} }
export type ActivityTrackerUrlObject = {
type: 'Link'
rel: [ 'tracker', 'websocket' | 'http' ]
name: string
href: string
}
export type ActivityPlaylistInfohashesObject = { export type ActivityPlaylistInfohashesObject = {
type: 'Infohash' type: 'Infohash'
name: string name: string
@ -96,7 +103,7 @@ export type ActivityTagObject =
| ActivityMentionObject | ActivityMentionObject
| ActivityBitTorrentUrlObject | ActivityBitTorrentUrlObject
| ActivityMagnetUrlObject | ActivityMagnetUrlObject
| ActivityVideoFileMetadataObject | ActivityVideoFileMetadataUrlObject
export type ActivityUrlObject = export type ActivityUrlObject =
ActivityVideoUrlObject ActivityVideoUrlObject
@ -104,7 +111,8 @@ export type ActivityUrlObject =
| ActivityBitTorrentUrlObject | ActivityBitTorrentUrlObject
| ActivityMagnetUrlObject | ActivityMagnetUrlObject
| ActivityHtmlUrlObject | ActivityHtmlUrlObject
| ActivityVideoFileMetadataObject | ActivityVideoFileMetadataUrlObject
| ActivityTrackerUrlObject
export interface ActivityPubAttributedTo { export interface ActivityPubAttributedTo {
type: 'Group' | 'Person' type: 'Group' | 'Person'

View File

@ -40,11 +40,14 @@ export interface VideoObject {
icon: ActivityIconObject[] icon: ActivityIconObject[]
url: ActivityUrlObject[] url: ActivityUrlObject[]
likes: string likes: string
dislikes: string dislikes: string
shares: string shares: string
comments: string comments: string
attributedTo: ActivityPubAttributedTo[] attributedTo: ActivityPubAttributedTo[]
to?: string[] to?: string[]
cc?: string[] cc?: string[]
} }