2022-07-29 14:50:41 +02:00
|
|
|
import { CreationAttributes, Transaction } from 'sequelize/types'
|
2021-07-23 11:20:00 +02:00
|
|
|
import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils'
|
2021-06-02 16:49:59 +02:00
|
|
|
import { logger, LoggerTagsFn } from '@server/helpers/logger'
|
2023-06-19 09:56:12 +02:00
|
|
|
import { updateRemoteVideoThumbnail } from '@server/lib/thumbnail'
|
2021-06-02 10:41:46 +02:00
|
|
|
import { setVideoTags } from '@server/lib/video'
|
2023-06-01 14:51:16 +02:00
|
|
|
import { StoryboardModel } from '@server/models/video/storyboard'
|
2021-06-02 10:41:46 +02:00
|
|
|
import { VideoCaptionModel } from '@server/models/video/video-caption'
|
|
|
|
import { VideoFileModel } from '@server/models/video/video-file'
|
|
|
|
import { VideoLiveModel } from '@server/models/video/video-live'
|
|
|
|
import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
|
2022-07-29 14:50:41 +02:00
|
|
|
import {
|
|
|
|
MStreamingPlaylistFiles,
|
|
|
|
MStreamingPlaylistFilesVideo,
|
|
|
|
MVideoCaption,
|
|
|
|
MVideoFile,
|
|
|
|
MVideoFullLight,
|
|
|
|
MVideoThumbnail
|
|
|
|
} from '@server/types/models'
|
2021-06-02 10:41:46 +02:00
|
|
|
import { ActivityTagObject, ThumbnailType, VideoObject, VideoStreamingPlaylistType } from '@shared/models'
|
2023-06-05 16:18:57 +02:00
|
|
|
import { findOwner, getOrCreateAPActor } from '../../actors'
|
2021-06-02 10:41:46 +02:00
|
|
|
import {
|
|
|
|
getCaptionAttributesFromObject,
|
|
|
|
getFileAttributesFromUrl,
|
|
|
|
getLiveAttributesFromObject,
|
|
|
|
getPreviewFromIcons,
|
2023-06-01 14:51:16 +02:00
|
|
|
getStoryboardAttributeFromObject,
|
2021-06-02 10:41:46 +02:00
|
|
|
getStreamingPlaylistAttributesFromObject,
|
|
|
|
getTagsFromObject,
|
|
|
|
getThumbnailFromIcons
|
|
|
|
} from './object-to-model-attributes'
|
|
|
|
import { getTrackerUrls, setVideoTrackers } from './trackers'
|
|
|
|
|
|
|
|
export abstract class APVideoAbstractBuilder {
|
|
|
|
protected abstract videoObject: VideoObject
|
2021-06-02 16:49:59 +02:00
|
|
|
protected abstract lTags: LoggerTagsFn
|
2021-06-02 10:41:46 +02:00
|
|
|
|
2021-06-02 11:54:29 +02:00
|
|
|
protected async getOrCreateVideoChannelFromVideoObject () {
|
2023-06-05 16:18:57 +02:00
|
|
|
const channel = await findOwner(this.videoObject.id, this.videoObject.attributedTo, 'Group')
|
2021-06-02 11:54:29 +02:00
|
|
|
if (!channel) throw new Error('Cannot find associated video channel to video ' + this.videoObject.url)
|
|
|
|
|
2021-06-03 16:02:29 +02:00
|
|
|
return getOrCreateAPActor(channel.id, 'all')
|
2021-06-02 11:54:29 +02:00
|
|
|
}
|
|
|
|
|
2023-06-07 08:53:14 +02:00
|
|
|
protected async setThumbnail (video: MVideoThumbnail, t?: Transaction) {
|
|
|
|
const miniatureIcon = getThumbnailFromIcons(this.videoObject)
|
|
|
|
if (!miniatureIcon) {
|
|
|
|
logger.warn('Cannot find thumbnail in video object', { object: this.videoObject })
|
2021-06-02 10:41:46 +02:00
|
|
|
return undefined
|
2023-06-07 08:53:14 +02:00
|
|
|
}
|
|
|
|
|
2023-06-19 09:56:12 +02:00
|
|
|
const miniatureModel = updateRemoteVideoThumbnail({
|
2023-06-07 08:53:14 +02:00
|
|
|
fileUrl: miniatureIcon.url,
|
|
|
|
video,
|
|
|
|
type: ThumbnailType.MINIATURE,
|
|
|
|
size: miniatureIcon,
|
|
|
|
onDisk: false // Lazy download remote thumbnails
|
2021-06-02 10:41:46 +02:00
|
|
|
})
|
2023-06-07 08:53:14 +02:00
|
|
|
|
|
|
|
await video.addAndSaveThumbnail(miniatureModel, t)
|
2021-06-02 10:41:46 +02:00
|
|
|
}
|
|
|
|
|
2021-06-08 17:29:45 +02:00
|
|
|
protected async setPreview (video: MVideoFullLight, t?: Transaction) {
|
2021-06-02 10:41:46 +02:00
|
|
|
const previewIcon = getPreviewFromIcons(this.videoObject)
|
|
|
|
if (!previewIcon) return
|
|
|
|
|
2023-06-19 09:56:12 +02:00
|
|
|
const previewModel = updateRemoteVideoThumbnail({
|
2021-06-02 10:41:46 +02:00
|
|
|
fileUrl: previewIcon.url,
|
|
|
|
video,
|
|
|
|
type: ThumbnailType.PREVIEW,
|
2023-06-06 15:59:51 +02:00
|
|
|
size: previewIcon,
|
2023-06-19 09:56:12 +02:00
|
|
|
onDisk: false // Lazy download remote previews
|
2021-06-02 10:41:46 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
await video.addAndSaveThumbnail(previewModel, t)
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async setTags (video: MVideoFullLight, t: Transaction) {
|
|
|
|
const tags = getTagsFromObject(this.videoObject)
|
|
|
|
await setVideoTags({ video, tags, transaction: t })
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async setTrackers (video: MVideoFullLight, t: Transaction) {
|
|
|
|
const trackers = getTrackerUrls(this.videoObject, video)
|
|
|
|
await setVideoTrackers({ video, trackers, transaction: t })
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async insertOrReplaceCaptions (video: MVideoFullLight, t: Transaction) {
|
2021-06-09 16:22:01 +02:00
|
|
|
const existingCaptions = await VideoCaptionModel.listVideoCaptions(video.id, t)
|
2021-06-02 10:41:46 +02:00
|
|
|
|
2021-06-09 16:22:01 +02:00
|
|
|
let captionsToCreate = getCaptionAttributesFromObject(video, this.videoObject)
|
|
|
|
.map(a => new VideoCaptionModel(a) as MVideoCaption)
|
|
|
|
|
|
|
|
for (const existingCaption of existingCaptions) {
|
|
|
|
// Only keep captions that do not already exist
|
|
|
|
const filtered = captionsToCreate.filter(c => !c.isEqual(existingCaption))
|
|
|
|
|
|
|
|
// This caption already exists, we don't need to destroy and create it
|
|
|
|
if (filtered.length !== captionsToCreate.length) {
|
|
|
|
captionsToCreate = filtered
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Destroy this caption that does not exist anymore
|
|
|
|
await existingCaption.destroy({ transaction: t })
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const captionToCreate of captionsToCreate) {
|
|
|
|
await captionToCreate.save({ transaction: t })
|
|
|
|
}
|
2021-06-02 10:41:46 +02:00
|
|
|
}
|
|
|
|
|
2023-06-01 14:51:16 +02:00
|
|
|
protected async insertOrReplaceStoryboard (video: MVideoFullLight, t: Transaction) {
|
|
|
|
const existingStoryboard = await StoryboardModel.loadByVideo(video.id, t)
|
|
|
|
if (existingStoryboard) await existingStoryboard.destroy({ transaction: t })
|
|
|
|
|
|
|
|
const storyboardAttributes = getStoryboardAttributeFromObject(video, this.videoObject)
|
|
|
|
if (!storyboardAttributes) return
|
|
|
|
|
|
|
|
return StoryboardModel.create(storyboardAttributes, { transaction: t })
|
|
|
|
}
|
|
|
|
|
2021-06-02 10:41:46 +02:00
|
|
|
protected async insertOrReplaceLive (video: MVideoFullLight, transaction: Transaction) {
|
|
|
|
const attributes = getLiveAttributesFromObject(video, this.videoObject)
|
|
|
|
const [ videoLive ] = await VideoLiveModel.upsert(attributes, { transaction, returning: true })
|
|
|
|
|
|
|
|
video.VideoLive = videoLive
|
|
|
|
}
|
|
|
|
|
2023-07-11 09:21:13 +02:00
|
|
|
protected async setWebVideoFiles (video: MVideoFullLight, t: Transaction) {
|
2021-06-02 10:41:46 +02:00
|
|
|
const videoFileAttributes = getFileAttributesFromUrl(video, this.videoObject.url)
|
|
|
|
const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
|
|
|
|
|
|
|
|
// Remove video files that do not exist anymore
|
2021-07-23 11:20:00 +02:00
|
|
|
await deleteAllModels(filterNonExistingModels(video.VideoFiles || [], newVideoFiles), t)
|
2021-06-02 10:41:46 +02:00
|
|
|
|
|
|
|
// Update or add other one
|
|
|
|
const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t))
|
|
|
|
video.VideoFiles = await Promise.all(upsertTasks)
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async setStreamingPlaylists (video: MVideoFullLight, t: Transaction) {
|
2021-10-15 11:37:30 +02:00
|
|
|
const streamingPlaylistAttributes = getStreamingPlaylistAttributesFromObject(video, this.videoObject)
|
2021-06-02 10:41:46 +02:00
|
|
|
const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
|
|
|
|
|
|
|
|
// Remove video playlists that do not exist anymore
|
2021-07-23 11:20:00 +02:00
|
|
|
await deleteAllModels(filterNonExistingModels(video.VideoStreamingPlaylists || [], newStreamingPlaylists), t)
|
2021-06-02 10:41:46 +02:00
|
|
|
|
2022-07-29 14:50:41 +02:00
|
|
|
const oldPlaylists = video.VideoStreamingPlaylists
|
2021-06-02 10:41:46 +02:00
|
|
|
video.VideoStreamingPlaylists = []
|
|
|
|
|
|
|
|
for (const playlistAttributes of streamingPlaylistAttributes) {
|
|
|
|
const streamingPlaylistModel = await this.insertOrReplaceStreamingPlaylist(playlistAttributes, t)
|
|
|
|
streamingPlaylistModel.Video = video
|
|
|
|
|
2022-07-29 14:50:41 +02:00
|
|
|
await this.setStreamingPlaylistFiles(oldPlaylists, streamingPlaylistModel, playlistAttributes.tagAPObject, t)
|
2021-06-02 10:41:46 +02:00
|
|
|
|
|
|
|
video.VideoStreamingPlaylists.push(streamingPlaylistModel)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-29 14:50:41 +02:00
|
|
|
private async insertOrReplaceStreamingPlaylist (attributes: CreationAttributes<VideoStreamingPlaylistModel>, t: Transaction) {
|
2021-06-02 10:41:46 +02:00
|
|
|
const [ streamingPlaylist ] = await VideoStreamingPlaylistModel.upsert(attributes, { returning: true, transaction: t })
|
|
|
|
|
|
|
|
return streamingPlaylist as MStreamingPlaylistFilesVideo
|
|
|
|
}
|
|
|
|
|
2022-07-29 14:50:41 +02:00
|
|
|
private getStreamingPlaylistFiles (oldPlaylists: MStreamingPlaylistFiles[], type: VideoStreamingPlaylistType) {
|
|
|
|
const playlist = oldPlaylists.find(s => s.type === type)
|
2021-06-02 10:41:46 +02:00
|
|
|
if (!playlist) return []
|
|
|
|
|
|
|
|
return playlist.VideoFiles
|
|
|
|
}
|
|
|
|
|
|
|
|
private async setStreamingPlaylistFiles (
|
2022-07-29 14:50:41 +02:00
|
|
|
oldPlaylists: MStreamingPlaylistFiles[],
|
2021-06-02 10:41:46 +02:00
|
|
|
playlistModel: MStreamingPlaylistFilesVideo,
|
|
|
|
tagObjects: ActivityTagObject[],
|
|
|
|
t: Transaction
|
|
|
|
) {
|
2022-07-29 14:50:41 +02:00
|
|
|
const oldStreamingPlaylistFiles = this.getStreamingPlaylistFiles(oldPlaylists || [], playlistModel.type)
|
2021-06-02 10:41:46 +02:00
|
|
|
|
|
|
|
const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(playlistModel, tagObjects).map(a => new VideoFileModel(a))
|
|
|
|
|
2021-07-23 11:20:00 +02:00
|
|
|
await deleteAllModels(filterNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles), t)
|
2021-06-02 10:41:46 +02:00
|
|
|
|
|
|
|
// Update or add other one
|
|
|
|
const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t))
|
|
|
|
playlistModel.VideoFiles = await Promise.all(upsertTasks)
|
|
|
|
}
|
|
|
|
}
|