aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/activitypub/videos/shared
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib/activitypub/videos/shared')
-rw-r--r--server/lib/activitypub/videos/shared/abstract-builder.ts142
-rw-r--r--server/lib/activitypub/videos/shared/creator.ts90
-rw-r--r--server/lib/activitypub/videos/shared/index.ts3
-rw-r--r--server/lib/activitypub/videos/shared/object-to-model-attributes.ts34
-rw-r--r--server/lib/activitypub/videos/shared/video-create.ts167
5 files changed, 262 insertions, 174 deletions
diff --git a/server/lib/activitypub/videos/shared/abstract-builder.ts b/server/lib/activitypub/videos/shared/abstract-builder.ts
new file mode 100644
index 000000000..9d5f37e5f
--- /dev/null
+++ b/server/lib/activitypub/videos/shared/abstract-builder.ts
@@ -0,0 +1,142 @@
1import { Transaction } from 'sequelize/types'
2import { deleteNonExistingModels } from '@server/helpers/database-utils'
3import { logger } from '@server/helpers/logger'
4import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '@server/lib/thumbnail'
5import { setVideoTags } from '@server/lib/video'
6import { VideoCaptionModel } from '@server/models/video/video-caption'
7import { VideoFileModel } from '@server/models/video/video-file'
8import { VideoLiveModel } from '@server/models/video/video-live'
9import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
10import { MStreamingPlaylistFilesVideo, MThumbnail, MVideoCaption, MVideoFile, MVideoFullLight, MVideoThumbnail } from '@server/types/models'
11import { ActivityTagObject, ThumbnailType, VideoObject, VideoStreamingPlaylistType } from '@shared/models'
12import {
13 getCaptionAttributesFromObject,
14 getFileAttributesFromUrl,
15 getLiveAttributesFromObject,
16 getPreviewFromIcons,
17 getStreamingPlaylistAttributesFromObject,
18 getTagsFromObject,
19 getThumbnailFromIcons
20} from './object-to-model-attributes'
21import { getTrackerUrls, setVideoTrackers } from './trackers'
22
23export abstract class APVideoAbstractBuilder {
24 protected abstract videoObject: VideoObject
25
26 protected tryToGenerateThumbnail (video: MVideoThumbnail): Promise<MThumbnail> {
27 return createVideoMiniatureFromUrl({
28 downloadUrl: getThumbnailFromIcons(this.videoObject).url,
29 video,
30 type: ThumbnailType.MINIATURE
31 }).catch(err => {
32 logger.warn('Cannot generate thumbnail of %s.', this.videoObject.id, { err })
33
34 return undefined
35 })
36 }
37
38 protected async setPreview (video: MVideoFullLight, t: Transaction) {
39 // Don't fetch the preview that could be big, create a placeholder instead
40 const previewIcon = getPreviewFromIcons(this.videoObject)
41 if (!previewIcon) return
42
43 const previewModel = createPlaceholderThumbnail({
44 fileUrl: previewIcon.url,
45 video,
46 type: ThumbnailType.PREVIEW,
47 size: previewIcon
48 })
49
50 await video.addAndSaveThumbnail(previewModel, t)
51 }
52
53 protected async setTags (video: MVideoFullLight, t: Transaction) {
54 const tags = getTagsFromObject(this.videoObject)
55 await setVideoTags({ video, tags, transaction: t })
56 }
57
58 protected async setTrackers (video: MVideoFullLight, t: Transaction) {
59 const trackers = getTrackerUrls(this.videoObject, video)
60 await setVideoTrackers({ video, trackers, transaction: t })
61 }
62
63 protected async insertOrReplaceCaptions (video: MVideoFullLight, t: Transaction) {
64 const videoCaptionsPromises = getCaptionAttributesFromObject(video, this.videoObject)
65 .map(a => new VideoCaptionModel(a) as MVideoCaption)
66 .map(c => VideoCaptionModel.insertOrReplaceLanguage(c, t))
67
68 await Promise.all(videoCaptionsPromises)
69 }
70
71 protected async insertOrReplaceLive (video: MVideoFullLight, transaction: Transaction) {
72 const attributes = getLiveAttributesFromObject(video, this.videoObject)
73 const [ videoLive ] = await VideoLiveModel.upsert(attributes, { transaction, returning: true })
74
75 video.VideoLive = videoLive
76 }
77
78 protected async setWebTorrentFiles (video: MVideoFullLight, t: Transaction) {
79 const videoFileAttributes = getFileAttributesFromUrl(video, this.videoObject.url)
80 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
81
82 // Remove video files that do not exist anymore
83 const destroyTasks = deleteNonExistingModels(video.VideoFiles || [], newVideoFiles, t)
84 await Promise.all(destroyTasks)
85
86 // Update or add other one
87 const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t))
88 video.VideoFiles = await Promise.all(upsertTasks)
89 }
90
91 protected async setStreamingPlaylists (video: MVideoFullLight, t: Transaction) {
92 const streamingPlaylistAttributes = getStreamingPlaylistAttributesFromObject(video, this.videoObject, video.VideoFiles || [])
93 const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
94
95 // Remove video playlists that do not exist anymore
96 const destroyTasks = deleteNonExistingModels(video.VideoStreamingPlaylists || [], newStreamingPlaylists, t)
97 await Promise.all(destroyTasks)
98
99 video.VideoStreamingPlaylists = []
100
101 for (const playlistAttributes of streamingPlaylistAttributes) {
102
103 const streamingPlaylistModel = await this.insertOrReplaceStreamingPlaylist(playlistAttributes, t)
104 streamingPlaylistModel.Video = video
105
106 await this.setStreamingPlaylistFiles(video, streamingPlaylistModel, playlistAttributes.tagAPObject, t)
107
108 video.VideoStreamingPlaylists.push(streamingPlaylistModel)
109 }
110 }
111
112 private async insertOrReplaceStreamingPlaylist (attributes: VideoStreamingPlaylistModel['_creationAttributes'], t: Transaction) {
113 const [ streamingPlaylist ] = await VideoStreamingPlaylistModel.upsert(attributes, { returning: true, transaction: t })
114
115 return streamingPlaylist as MStreamingPlaylistFilesVideo
116 }
117
118 private getStreamingPlaylistFiles (video: MVideoFullLight, type: VideoStreamingPlaylistType) {
119 const playlist = video.VideoStreamingPlaylists.find(s => s.type === type)
120 if (!playlist) return []
121
122 return playlist.VideoFiles
123 }
124
125 private async setStreamingPlaylistFiles (
126 video: MVideoFullLight,
127 playlistModel: MStreamingPlaylistFilesVideo,
128 tagObjects: ActivityTagObject[],
129 t: Transaction
130 ) {
131 const oldStreamingPlaylistFiles = this.getStreamingPlaylistFiles(video, playlistModel.type)
132
133 const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(playlistModel, tagObjects).map(a => new VideoFileModel(a))
134
135 const destroyTasks = deleteNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles, t)
136 await Promise.all(destroyTasks)
137
138 // Update or add other one
139 const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t))
140 playlistModel.VideoFiles = await Promise.all(upsertTasks)
141 }
142}
diff --git a/server/lib/activitypub/videos/shared/creator.ts b/server/lib/activitypub/videos/shared/creator.ts
new file mode 100644
index 000000000..4f2d79374
--- /dev/null
+++ b/server/lib/activitypub/videos/shared/creator.ts
@@ -0,0 +1,90 @@
1
2import { logger } from '@server/helpers/logger'
3import { sequelizeTypescript } from '@server/initializers/database'
4import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
5import { VideoModel } from '@server/models/video/video'
6import { MChannelAccountLight, MThumbnail, MVideoFullLight, MVideoThumbnail } from '@server/types/models'
7import { VideoObject } from '@shared/models'
8import { APVideoAbstractBuilder } from './abstract-builder'
9import { getVideoAttributesFromObject } from './object-to-model-attributes'
10
11export class APVideoCreator extends APVideoAbstractBuilder {
12 protected readonly videoObject: VideoObject
13 private readonly channel: MChannelAccountLight
14
15 constructor (options: {
16 videoObject: VideoObject
17 channel: MChannelAccountLight
18 }) {
19 super()
20
21 this.videoObject = options.videoObject
22 this.channel = options.channel
23 }
24
25 async create (waitThumbnail = false) {
26 logger.debug('Adding remote video %s.', this.videoObject.id)
27
28 const videoData = await getVideoAttributesFromObject(this.channel, this.videoObject, this.videoObject.to)
29 const video = VideoModel.build(videoData) as MVideoThumbnail
30
31 const promiseThumbnail = this.tryToGenerateThumbnail(video)
32
33 let thumbnailModel: MThumbnail
34 if (waitThumbnail === true) {
35 thumbnailModel = await promiseThumbnail
36 }
37
38 const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => {
39 try {
40 const videoCreated = await video.save({ transaction: t }) as MVideoFullLight
41 videoCreated.VideoChannel = this.channel
42
43 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
44
45 await this.setPreview(videoCreated, t)
46 await this.setWebTorrentFiles(videoCreated, t)
47 await this.setStreamingPlaylists(videoCreated, t)
48 await this.setTags(videoCreated, t)
49 await this.setTrackers(videoCreated, t)
50 await this.insertOrReplaceCaptions(videoCreated, t)
51 await this.insertOrReplaceLive(videoCreated, t)
52
53 // We added a video in this channel, set it as updated
54 await this.channel.setAsUpdated(t)
55
56 const autoBlacklisted = await autoBlacklistVideoIfNeeded({
57 video: videoCreated,
58 user: undefined,
59 isRemote: true,
60 isNew: true,
61 transaction: t
62 })
63
64 logger.info('Remote video with uuid %s inserted.', this.videoObject.uuid)
65
66 return { autoBlacklisted, videoCreated }
67 } catch (err) {
68 // FIXME: Use rollback hook when https://github.com/sequelize/sequelize/pull/13038 is released
69 // Remove thumbnail
70 if (thumbnailModel) await thumbnailModel.removeThumbnail()
71
72 throw err
73 }
74 })
75
76 if (waitThumbnail === false) {
77 // Error is already caught above
78 // eslint-disable-next-line @typescript-eslint/no-floating-promises
79 promiseThumbnail.then(thumbnailModel => {
80 if (!thumbnailModel) return
81
82 thumbnailModel = videoCreated.id
83
84 return thumbnailModel.save()
85 })
86 }
87
88 return { autoBlacklisted, videoCreated }
89 }
90}
diff --git a/server/lib/activitypub/videos/shared/index.ts b/server/lib/activitypub/videos/shared/index.ts
index 4d24fbc6a..208a43705 100644
--- a/server/lib/activitypub/videos/shared/index.ts
+++ b/server/lib/activitypub/videos/shared/index.ts
@@ -1,4 +1,5 @@
1export * from './abstract-builder'
2export * from './creator'
1export * from './object-to-model-attributes' 3export * from './object-to-model-attributes'
2export * from './trackers' 4export * from './trackers'
3export * from './video-create'
4export * from './video-sync-attributes' 5export * from './video-sync-attributes'
diff --git a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
index 8a8105500..85548428c 100644
--- a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
+++ b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
@@ -23,6 +23,7 @@ import {
23 VideoPrivacy, 23 VideoPrivacy,
24 VideoStreamingPlaylistType 24 VideoStreamingPlaylistType
25} from '@shared/models' 25} from '@shared/models'
26import { VideoCaptionModel } from '@server/models/video/video-caption'
26 27
27function getThumbnailFromIcons (videoObject: VideoObject) { 28function getThumbnailFromIcons (videoObject: VideoObject) {
28 let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth) 29 let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth)
@@ -44,7 +45,7 @@ function getTagsFromObject (videoObject: VideoObject) {
44 .map(t => t.name) 45 .map(t => t.name)
45} 46}
46 47
47function videoFileActivityUrlToDBAttributes ( 48function getFileAttributesFromUrl (
48 videoOrPlaylist: MVideo | MStreamingPlaylistVideo, 49 videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
49 urls: (ActivityTagObject | ActivityUrlObject)[] 50 urls: (ActivityTagObject | ActivityUrlObject)[]
50) { 51) {
@@ -109,7 +110,7 @@ function videoFileActivityUrlToDBAttributes (
109 return attributes 110 return attributes
110} 111}
111 112
112function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObject: VideoObject, videoFiles: MVideoFile[]) { 113function getStreamingPlaylistAttributesFromObject (video: MVideoId, videoObject: VideoObject, videoFiles: MVideoFile[]) {
113 const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[] 114 const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
114 if (playlistUrls.length === 0) return [] 115 if (playlistUrls.length === 0) return []
115 116
@@ -134,6 +135,7 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec
134 p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files), 135 p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files),
135 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, 136 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
136 videoId: video.id, 137 videoId: video.id,
138
137 tagAPObject: playlistUrlObject.tag 139 tagAPObject: playlistUrlObject.tag
138 } 140 }
139 141
@@ -143,7 +145,24 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec
143 return attributes 145 return attributes
144} 146}
145 147
146function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) { 148function getLiveAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
149 return {
150 saveReplay: videoObject.liveSaveReplay,
151 permanentLive: videoObject.permanentLive,
152 videoId: video.id
153 }
154}
155
156function getCaptionAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
157 return videoObject.subtitleLanguage.map(c => ({
158 videoId: video.id,
159 filename: VideoCaptionModel.generateCaptionName(c.identifier),
160 language: c.identifier,
161 fileUrl: c.url
162 }))
163}
164
165function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) {
147 const privacy = to.includes(ACTIVITY_PUB.PUBLIC) 166 const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
148 ? VideoPrivacy.PUBLIC 167 ? VideoPrivacy.PUBLIC
149 : VideoPrivacy.UNLISTED 168 : VideoPrivacy.UNLISTED
@@ -203,10 +222,13 @@ export {
203 222
204 getTagsFromObject, 223 getTagsFromObject,
205 224
206 videoActivityObjectToDBAttributes, 225 getFileAttributesFromUrl,
226 getStreamingPlaylistAttributesFromObject,
227
228 getLiveAttributesFromObject,
229 getCaptionAttributesFromObject,
207 230
208 videoFileActivityUrlToDBAttributes, 231 getVideoAttributesFromObject
209 streamingPlaylistActivityUrlToDBAttributes
210} 232}
211 233
212// --------------------------------------------------------------------------- 234// ---------------------------------------------------------------------------
diff --git a/server/lib/activitypub/videos/shared/video-create.ts b/server/lib/activitypub/videos/shared/video-create.ts
deleted file mode 100644
index 80cc2ab37..000000000
--- a/server/lib/activitypub/videos/shared/video-create.ts
+++ /dev/null
@@ -1,167 +0,0 @@
1import { logger } from '@server/helpers/logger'
2import { sequelizeTypescript } from '@server/initializers/database'
3import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '@server/lib/thumbnail'
4import { setVideoTags } from '@server/lib/video'
5import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
6import { VideoModel } from '@server/models/video/video'
7import { VideoCaptionModel } from '@server/models/video/video-caption'
8import { VideoFileModel } from '@server/models/video/video-file'
9import { VideoLiveModel } from '@server/models/video/video-live'
10import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
11import {
12 MChannelAccountLight,
13 MStreamingPlaylistFilesVideo,
14 MThumbnail,
15 MVideoCaption,
16 MVideoFullLight,
17 MVideoThumbnail
18} from '@server/types/models'
19import { ThumbnailType, VideoObject } from '@shared/models'
20import {
21 getPreviewFromIcons,
22 getTagsFromObject,
23 getThumbnailFromIcons,
24 streamingPlaylistActivityUrlToDBAttributes,
25 videoActivityObjectToDBAttributes,
26 videoFileActivityUrlToDBAttributes
27} from './object-to-model-attributes'
28import { getTrackerUrls, setVideoTrackers } from './trackers'
29
30async function createVideo (videoObject: VideoObject, channel: MChannelAccountLight, waitThumbnail = false) {
31 logger.debug('Adding remote video %s.', videoObject.id)
32
33 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to)
34 const video = VideoModel.build(videoData) as MVideoThumbnail
35
36 const promiseThumbnail = createVideoMiniatureFromUrl({
37 downloadUrl: getThumbnailFromIcons(videoObject).url,
38 video,
39 type: ThumbnailType.MINIATURE
40 }).catch(err => {
41 logger.error('Cannot create miniature from url.', { err })
42 return undefined
43 })
44
45 let thumbnailModel: MThumbnail
46 if (waitThumbnail === true) {
47 thumbnailModel = await promiseThumbnail
48 }
49
50 const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => {
51 try {
52 const sequelizeOptions = { transaction: t }
53
54 const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight
55 videoCreated.VideoChannel = channel
56
57 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
58
59 const previewIcon = getPreviewFromIcons(videoObject)
60 if (previewIcon) {
61 const previewModel = createPlaceholderThumbnail({
62 fileUrl: previewIcon.url,
63 video: videoCreated,
64 type: ThumbnailType.PREVIEW,
65 size: previewIcon
66 })
67
68 await videoCreated.addAndSaveThumbnail(previewModel, t)
69 }
70
71 // Process files
72 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url)
73
74 const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t }))
75 const videoFiles = await Promise.all(videoFilePromises)
76
77 const streamingPlaylistsAttributes = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles)
78 videoCreated.VideoStreamingPlaylists = []
79
80 for (const playlistAttributes of streamingPlaylistsAttributes) {
81 const playlist = await VideoStreamingPlaylistModel.create(playlistAttributes, { transaction: t }) as MStreamingPlaylistFilesVideo
82 playlist.Video = videoCreated
83
84 const playlistFiles = videoFileActivityUrlToDBAttributes(playlist, playlistAttributes.tagAPObject)
85 const videoFilePromises = playlistFiles.map(f => VideoFileModel.create(f, { transaction: t }))
86 playlist.VideoFiles = await Promise.all(videoFilePromises)
87
88 videoCreated.VideoStreamingPlaylists.push(playlist)
89 }
90
91 // Process tags
92 const tags = getTagsFromObject(videoObject)
93 await setVideoTags({ video: videoCreated, tags, transaction: t })
94
95 // Process captions
96 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
97 const caption = new VideoCaptionModel({
98 videoId: videoCreated.id,
99 filename: VideoCaptionModel.generateCaptionName(c.identifier),
100 language: c.identifier,
101 fileUrl: c.url
102 }) as MVideoCaption
103
104 return VideoCaptionModel.insertOrReplaceLanguage(caption, t)
105 })
106 await Promise.all(videoCaptionsPromises)
107
108 // Process trackers
109 {
110 const trackers = getTrackerUrls(videoObject, videoCreated)
111 await setVideoTrackers({ video: videoCreated, trackers, transaction: t })
112 }
113
114 videoCreated.VideoFiles = videoFiles
115
116 if (videoCreated.isLive) {
117 const videoLive = new VideoLiveModel({
118 streamKey: null,
119 saveReplay: videoObject.liveSaveReplay,
120 permanentLive: videoObject.permanentLive,
121 videoId: videoCreated.id
122 })
123
124 videoCreated.VideoLive = await videoLive.save({ transaction: t })
125 }
126
127 // We added a video in this channel, set it as updated
128 await channel.setAsUpdated(t)
129
130 const autoBlacklisted = await autoBlacklistVideoIfNeeded({
131 video: videoCreated,
132 user: undefined,
133 isRemote: true,
134 isNew: true,
135 transaction: t
136 })
137
138 logger.info('Remote video with uuid %s inserted.', videoObject.uuid)
139
140 return { autoBlacklisted, videoCreated }
141 } catch (err) {
142 // FIXME: Use rollback hook when https://github.com/sequelize/sequelize/pull/13038 is released
143 // Remove thumbnail
144 if (thumbnailModel) await thumbnailModel.removeThumbnail()
145
146 throw err
147 }
148 })
149
150 if (waitThumbnail === false) {
151 // Error is already caught above
152 // eslint-disable-next-line @typescript-eslint/no-floating-promises
153 promiseThumbnail.then(thumbnailModel => {
154 if (!thumbnailModel) return
155
156 thumbnailModel = videoCreated.id
157
158 return thumbnailModel.save()
159 })
160 }
161
162 return { autoBlacklisted, videoCreated }
163}
164
165export {
166 createVideo
167}