]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/activitypub/videos/shared/abstract-builder.ts
Add ability to delete a specific video file
[github/Chocobozzz/PeerTube.git] / server / lib / activitypub / videos / shared / abstract-builder.ts
1 import { CreationAttributes, Transaction } from 'sequelize/types'
2 import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils'
3 import { logger, LoggerTagsFn } from '@server/helpers/logger'
4 import { updatePlaceholderThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail'
5 import { setVideoTags } from '@server/lib/video'
6 import { VideoCaptionModel } from '@server/models/video/video-caption'
7 import { VideoFileModel } from '@server/models/video/video-file'
8 import { VideoLiveModel } from '@server/models/video/video-live'
9 import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
10 import {
11 MStreamingPlaylistFiles,
12 MStreamingPlaylistFilesVideo,
13 MThumbnail,
14 MVideoCaption,
15 MVideoFile,
16 MVideoFullLight,
17 MVideoThumbnail
18 } from '@server/types/models'
19 import { ActivityTagObject, ThumbnailType, VideoObject, VideoStreamingPlaylistType } from '@shared/models'
20 import { getOrCreateAPActor } from '../../actors'
21 import { checkUrlsSameHost } from '../../url'
22 import {
23 getCaptionAttributesFromObject,
24 getFileAttributesFromUrl,
25 getLiveAttributesFromObject,
26 getPreviewFromIcons,
27 getStreamingPlaylistAttributesFromObject,
28 getTagsFromObject,
29 getThumbnailFromIcons
30 } from './object-to-model-attributes'
31 import { getTrackerUrls, setVideoTrackers } from './trackers'
32
33 export abstract class APVideoAbstractBuilder {
34 protected abstract videoObject: VideoObject
35 protected abstract lTags: LoggerTagsFn
36
37 protected async getOrCreateVideoChannelFromVideoObject () {
38 const channel = this.videoObject.attributedTo.find(a => a.type === 'Group')
39 if (!channel) throw new Error('Cannot find associated video channel to video ' + this.videoObject.url)
40
41 if (checkUrlsSameHost(channel.id, this.videoObject.id) !== true) {
42 throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${this.videoObject.id}`)
43 }
44
45 return getOrCreateAPActor(channel.id, 'all')
46 }
47
48 protected tryToGenerateThumbnail (video: MVideoThumbnail): Promise<MThumbnail> {
49 return updateVideoMiniatureFromUrl({
50 downloadUrl: getThumbnailFromIcons(this.videoObject).url,
51 video,
52 type: ThumbnailType.MINIATURE
53 }).catch(err => {
54 logger.warn('Cannot generate thumbnail of %s.', this.videoObject.id, { err, ...this.lTags() })
55
56 return undefined
57 })
58 }
59
60 protected async setPreview (video: MVideoFullLight, t?: Transaction) {
61 // Don't fetch the preview that could be big, create a placeholder instead
62 const previewIcon = getPreviewFromIcons(this.videoObject)
63 if (!previewIcon) return
64
65 const previewModel = updatePlaceholderThumbnail({
66 fileUrl: previewIcon.url,
67 video,
68 type: ThumbnailType.PREVIEW,
69 size: previewIcon
70 })
71
72 await video.addAndSaveThumbnail(previewModel, t)
73 }
74
75 protected async setTags (video: MVideoFullLight, t: Transaction) {
76 const tags = getTagsFromObject(this.videoObject)
77 await setVideoTags({ video, tags, transaction: t })
78 }
79
80 protected async setTrackers (video: MVideoFullLight, t: Transaction) {
81 const trackers = getTrackerUrls(this.videoObject, video)
82 await setVideoTrackers({ video, trackers, transaction: t })
83 }
84
85 protected async insertOrReplaceCaptions (video: MVideoFullLight, t: Transaction) {
86 const existingCaptions = await VideoCaptionModel.listVideoCaptions(video.id, t)
87
88 let captionsToCreate = getCaptionAttributesFromObject(video, this.videoObject)
89 .map(a => new VideoCaptionModel(a) as MVideoCaption)
90
91 for (const existingCaption of existingCaptions) {
92 // Only keep captions that do not already exist
93 const filtered = captionsToCreate.filter(c => !c.isEqual(existingCaption))
94
95 // This caption already exists, we don't need to destroy and create it
96 if (filtered.length !== captionsToCreate.length) {
97 captionsToCreate = filtered
98 continue
99 }
100
101 // Destroy this caption that does not exist anymore
102 await existingCaption.destroy({ transaction: t })
103 }
104
105 for (const captionToCreate of captionsToCreate) {
106 await captionToCreate.save({ transaction: t })
107 }
108 }
109
110 protected async insertOrReplaceLive (video: MVideoFullLight, transaction: Transaction) {
111 const attributes = getLiveAttributesFromObject(video, this.videoObject)
112 const [ videoLive ] = await VideoLiveModel.upsert(attributes, { transaction, returning: true })
113
114 video.VideoLive = videoLive
115 }
116
117 protected async setWebTorrentFiles (video: MVideoFullLight, t: Transaction) {
118 const videoFileAttributes = getFileAttributesFromUrl(video, this.videoObject.url)
119 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
120
121 // Remove video files that do not exist anymore
122 await deleteAllModels(filterNonExistingModels(video.VideoFiles || [], newVideoFiles), t)
123
124 // Update or add other one
125 const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t))
126 video.VideoFiles = await Promise.all(upsertTasks)
127 }
128
129 protected async setStreamingPlaylists (video: MVideoFullLight, t: Transaction) {
130 const streamingPlaylistAttributes = getStreamingPlaylistAttributesFromObject(video, this.videoObject)
131 const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
132
133 // Remove video playlists that do not exist anymore
134 await deleteAllModels(filterNonExistingModels(video.VideoStreamingPlaylists || [], newStreamingPlaylists), t)
135
136 const oldPlaylists = video.VideoStreamingPlaylists
137 video.VideoStreamingPlaylists = []
138
139 for (const playlistAttributes of streamingPlaylistAttributes) {
140 const streamingPlaylistModel = await this.insertOrReplaceStreamingPlaylist(playlistAttributes, t)
141 streamingPlaylistModel.Video = video
142
143 await this.setStreamingPlaylistFiles(oldPlaylists, streamingPlaylistModel, playlistAttributes.tagAPObject, t)
144
145 video.VideoStreamingPlaylists.push(streamingPlaylistModel)
146 }
147 }
148
149 private async insertOrReplaceStreamingPlaylist (attributes: CreationAttributes<VideoStreamingPlaylistModel>, t: Transaction) {
150 const [ streamingPlaylist ] = await VideoStreamingPlaylistModel.upsert(attributes, { returning: true, transaction: t })
151
152 return streamingPlaylist as MStreamingPlaylistFilesVideo
153 }
154
155 private getStreamingPlaylistFiles (oldPlaylists: MStreamingPlaylistFiles[], type: VideoStreamingPlaylistType) {
156 const playlist = oldPlaylists.find(s => s.type === type)
157 if (!playlist) return []
158
159 return playlist.VideoFiles
160 }
161
162 private async setStreamingPlaylistFiles (
163 oldPlaylists: MStreamingPlaylistFiles[],
164 playlistModel: MStreamingPlaylistFilesVideo,
165 tagObjects: ActivityTagObject[],
166 t: Transaction
167 ) {
168 const oldStreamingPlaylistFiles = this.getStreamingPlaylistFiles(oldPlaylists || [], playlistModel.type)
169
170 const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(playlistModel, tagObjects).map(a => new VideoFileModel(a))
171
172 await deleteAllModels(filterNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles), t)
173
174 // Update or add other one
175 const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t))
176 playlistModel.VideoFiles = await Promise.all(upsertTasks)
177 }
178 }