]>
Commit | Line | Data |
---|---|---|
2ccaeeb3 | 1 | import * as magnetUtil from 'magnet-uri' |
892211e8 C |
2 | import { join } from 'path' |
3 | import * as request from 'request' | |
4 | import { ActivityIconObject } from '../../../shared/index' | |
2ccaeeb3 C |
5 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
6 | import { VideoPrivacy } from '../../../shared/models/videos' | |
7 | import { isVideoTorrentObjectValid } from '../../helpers/custom-validators/activitypub/videos' | |
8 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | |
9 | import { retryTransactionWrapper } from '../../helpers/database-utils' | |
10 | import { logger } from '../../helpers/logger' | |
da854ddd | 11 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' |
2ccaeeb3 C |
12 | import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, STATIC_PATHS, VIDEO_MIMETYPE_EXT } from '../../initializers' |
13 | import { ActorModel } from '../../models/activitypub/actor' | |
14 | import { TagModel } from '../../models/video/tag' | |
3fd3ab2d | 15 | import { VideoModel } from '../../models/video/video' |
2ccaeeb3 C |
16 | import { VideoChannelModel } from '../../models/video/video-channel' |
17 | import { VideoFileModel } from '../../models/video/video-file' | |
18 | import { VideoShareModel } from '../../models/video/video-share' | |
19 | import { getOrCreateActorAndServerAndModel } from './actor' | |
892211e8 | 20 | |
d50acfab | 21 | function fetchRemoteVideoPreview (video: VideoModel, reject: Function) { |
50d6de9c | 22 | const host = video.VideoChannel.Account.Actor.Server.host |
892211e8 C |
23 | const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName()) |
24 | ||
f05a1c30 | 25 | // We need to provide a callback, if no we could have an uncaught exception |
f40bbe31 C |
26 | return request.get(REMOTE_SCHEME.HTTP + '://' + host + path, err => { |
27 | if (err) reject(err) | |
28 | }) | |
892211e8 C |
29 | } |
30 | ||
3fd3ab2d | 31 | async function fetchRemoteVideoDescription (video: VideoModel) { |
50d6de9c | 32 | const host = video.VideoChannel.Account.Actor.Server.host |
892211e8 C |
33 | const path = video.getDescriptionPath() |
34 | const options = { | |
35 | uri: REMOTE_SCHEME.HTTP + '://' + host + path, | |
36 | json: true | |
37 | } | |
38 | ||
39 | const { body } = await doRequest(options) | |
40 | return body.description ? body.description : '' | |
41 | } | |
42 | ||
3fd3ab2d | 43 | function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { |
892211e8 C |
44 | const thumbnailName = video.getThumbnailName() |
45 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) | |
46 | ||
47 | const options = { | |
48 | method: 'GET', | |
49 | uri: icon.url | |
50 | } | |
51 | return doRequestAndSaveToFile(options, thumbnailPath) | |
52 | } | |
53 | ||
2ccaeeb3 C |
54 | async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelModel, |
55 | videoObject: VideoTorrentObject, | |
56 | to: string[] = [], | |
57 | cc: string[] = []) { | |
58 | let privacy = VideoPrivacy.PRIVATE | |
59 | if (to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1) privacy = VideoPrivacy.PUBLIC | |
60 | else if (cc.indexOf(ACTIVITY_PUB.PUBLIC) !== -1) privacy = VideoPrivacy.UNLISTED | |
61 | ||
62 | const duration = videoObject.duration.replace(/[^\d]+/, '') | |
63 | let language = null | |
64 | if (videoObject.language) { | |
65 | language = parseInt(videoObject.language.identifier, 10) | |
66 | } | |
67 | ||
68 | let category = null | |
69 | if (videoObject.category) { | |
70 | category = parseInt(videoObject.category.identifier, 10) | |
71 | } | |
72 | ||
73 | let licence = null | |
74 | if (videoObject.licence) { | |
75 | licence = parseInt(videoObject.licence.identifier, 10) | |
76 | } | |
77 | ||
78 | let description = null | |
79 | if (videoObject.content) { | |
80 | description = videoObject.content | |
81 | } | |
82 | ||
83 | return { | |
84 | name: videoObject.name, | |
85 | uuid: videoObject.uuid, | |
86 | url: videoObject.id, | |
87 | category, | |
88 | licence, | |
89 | language, | |
90 | description, | |
91 | nsfw: videoObject.nsfw, | |
92 | commentsEnabled: videoObject.commentsEnabled, | |
93 | channelId: videoChannel.id, | |
94 | duration: parseInt(duration, 10), | |
95 | createdAt: new Date(videoObject.published), | |
96 | // FIXME: updatedAt does not seems to be considered by Sequelize | |
97 | updatedAt: new Date(videoObject.updated), | |
98 | views: videoObject.views, | |
99 | likes: 0, | |
100 | dislikes: 0, | |
101 | remote: true, | |
102 | privacy | |
103 | } | |
104 | } | |
105 | ||
106 | function videoFileActivityUrlToDBAttributes (videoCreated: VideoModel, videoObject: VideoTorrentObject) { | |
107 | const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT) | |
108 | const fileUrls = videoObject.url.filter(u => { | |
109 | return mimeTypes.indexOf(u.mimeType) !== -1 && u.mimeType.startsWith('video/') | |
110 | }) | |
111 | ||
112 | if (fileUrls.length === 0) { | |
113 | throw new Error('Cannot find video files for ' + videoCreated.url) | |
114 | } | |
115 | ||
116 | const attributes = [] | |
117 | for (const fileUrl of fileUrls) { | |
118 | // Fetch associated magnet uri | |
119 | const magnet = videoObject.url.find(u => { | |
120 | return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === fileUrl.width | |
121 | }) | |
122 | ||
9fb3abfd | 123 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href) |
2ccaeeb3 | 124 | |
9fb3abfd C |
125 | const parsed = magnetUtil.decode(magnet.href) |
126 | if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.href) | |
2ccaeeb3 C |
127 | |
128 | const attribute = { | |
129 | extname: VIDEO_MIMETYPE_EXT[ fileUrl.mimeType ], | |
130 | infoHash: parsed.infoHash, | |
131 | resolution: fileUrl.width, | |
132 | size: fileUrl.size, | |
133 | videoId: videoCreated.id | |
134 | } | |
135 | attributes.push(attribute) | |
136 | } | |
137 | ||
138 | return attributes | |
139 | } | |
140 | ||
141 | async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor: ActorModel) { | |
142 | logger.debug('Adding remote video %s.', videoObject.id) | |
143 | ||
144 | return sequelizeTypescript.transaction(async t => { | |
145 | const sequelizeOptions = { | |
146 | transaction: t | |
147 | } | |
148 | const videoFromDatabase = await VideoModel.loadByUUIDOrURLAndPopulateAccount(videoObject.uuid, videoObject.id, t) | |
149 | if (videoFromDatabase) return videoFromDatabase | |
150 | ||
151 | const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to, videoObject.cc) | |
152 | const video = VideoModel.build(videoData) | |
153 | ||
154 | // Don't block on request | |
155 | generateThumbnailFromUrl(video, videoObject.icon) | |
156 | .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, err)) | |
157 | ||
158 | const videoCreated = await video.save(sequelizeOptions) | |
159 | ||
160 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject) | |
161 | if (videoFileAttributes.length === 0) { | |
162 | throw new Error('Cannot find valid files for video %s ' + videoObject.url) | |
163 | } | |
164 | ||
1f7ab4f3 | 165 | const tasks = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) |
2ccaeeb3 C |
166 | await Promise.all(tasks) |
167 | ||
168 | const tags = videoObject.tag.map(t => t.name) | |
169 | const tagInstances = await TagModel.findOrCreateTags(tags, t) | |
170 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) | |
171 | ||
172 | logger.info('Remote video with uuid %s inserted.', videoObject.uuid) | |
173 | ||
174 | videoCreated.VideoChannel = channelActor.VideoChannel | |
175 | return videoCreated | |
176 | }) | |
0032ebe9 C |
177 | } |
178 | ||
2ccaeeb3 C |
179 | async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentObject | string, actor?: ActorModel) { |
180 | if (typeof videoObject === 'string') { | |
181 | const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoObject) | |
182 | if (videoFromDatabase) { | |
183 | return { | |
184 | video: videoFromDatabase, | |
185 | actor: videoFromDatabase.VideoChannel.Account.Actor, | |
186 | channelActor: videoFromDatabase.VideoChannel.Actor | |
187 | } | |
188 | } | |
189 | ||
190 | videoObject = await fetchRemoteVideo(videoObject) | |
191 | if (!videoObject) throw new Error('Cannot fetch remote video') | |
192 | } | |
193 | ||
194 | if (!actor) { | |
195 | const actorObj = videoObject.attributedTo.find(a => a.type === 'Person') | |
196 | if (!actorObj) throw new Error('Cannot find associated actor to video ' + videoObject.url) | |
197 | ||
198 | actor = await getOrCreateActorAndServerAndModel(actorObj.id) | |
199 | } | |
200 | ||
201 | const channel = videoObject.attributedTo.find(a => a.type === 'Group') | |
202 | if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) | |
203 | ||
204 | const channelActor = await getOrCreateActorAndServerAndModel(channel.id) | |
205 | ||
206 | const options = { | |
207 | arguments: [ videoObject, channelActor ], | |
208 | errorMessage: 'Cannot insert the remote video with many retries.' | |
209 | } | |
210 | ||
211 | const video = await retryTransactionWrapper(getOrCreateVideo, options) | |
212 | ||
213 | return { actor, channelActor, video } | |
214 | } | |
215 | ||
216 | async function addVideoShares (instance: VideoModel, shareUrls: string[]) { | |
217 | for (const shareUrl of shareUrls) { | |
218 | // Fetch url | |
219 | const { body } = await doRequest({ | |
220 | uri: shareUrl, | |
221 | json: true, | |
222 | activityPub: true | |
223 | }) | |
224 | const actorUrl = body.actor | |
225 | if (!actorUrl) continue | |
226 | ||
227 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | |
228 | ||
229 | const entry = { | |
230 | actorId: actor.id, | |
231 | videoId: instance.id | |
232 | } | |
233 | ||
234 | await VideoShareModel.findOrCreate({ | |
235 | where: entry, | |
236 | defaults: entry | |
237 | }) | |
238 | } | |
0032ebe9 C |
239 | } |
240 | ||
892211e8 | 241 | export { |
2ccaeeb3 | 242 | getOrCreateAccountAndVideoAndChannel, |
892211e8 C |
243 | fetchRemoteVideoPreview, |
244 | fetchRemoteVideoDescription, | |
0032ebe9 | 245 | generateThumbnailFromUrl, |
2ccaeeb3 C |
246 | videoActivityObjectToDBAttributes, |
247 | videoFileActivityUrlToDBAttributes, | |
248 | getOrCreateVideo, | |
249 | addVideoShares} | |
250 | ||
251 | // --------------------------------------------------------------------------- | |
252 | ||
253 | async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> { | |
254 | const options = { | |
255 | uri: videoUrl, | |
256 | method: 'GET', | |
257 | json: true, | |
258 | activityPub: true | |
259 | } | |
260 | ||
261 | logger.info('Fetching remote video %s.', videoUrl) | |
262 | ||
263 | const { body } = await doRequest(options) | |
264 | ||
265 | if (isVideoTorrentObjectValid(body) === false) { | |
266 | logger.debug('Remote video JSON is not valid.', { body }) | |
267 | return undefined | |
268 | } | |
269 | ||
270 | return body | |
892211e8 | 271 | } |