diff options
Diffstat (limited to 'server/lib/activitypub/videos.ts')
-rw-r--r-- | server/lib/activitypub/videos.ts | 280 |
1 files changed, 191 insertions, 89 deletions
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index 8521572a1..710929aac 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -1,17 +1,23 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import * as sequelize from 'sequelize' | 2 | import * as sequelize from 'sequelize' |
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import { join } from 'path' | ||
5 | import * as request from 'request' | 4 | import * as request from 'request' |
6 | import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index' | 5 | import { |
6 | ActivityIconObject, | ||
7 | ActivityPlaylistSegmentHashesObject, | ||
8 | ActivityPlaylistUrlObject, | ||
9 | ActivityUrlObject, | ||
10 | ActivityVideoUrlObject, | ||
11 | VideoState | ||
12 | } from '../../../shared/index' | ||
7 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 13 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
8 | import { VideoPrivacy } from '../../../shared/models/videos' | 14 | import { VideoPrivacy } from '../../../shared/models/videos' |
9 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' | 15 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' |
10 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 16 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
11 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' | 17 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' |
12 | import { logger } from '../../helpers/logger' | 18 | import { logger } from '../../helpers/logger' |
13 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' | 19 | import { doRequest, downloadImage } from '../../helpers/requests' |
14 | import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers' | 20 | import { ACTIVITY_PUB, CONFIG, MIMETYPES, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE } from '../../initializers' |
15 | import { ActorModel } from '../../models/activitypub/actor' | 21 | import { ActorModel } from '../../models/activitypub/actor' |
16 | import { TagModel } from '../../models/video/tag' | 22 | import { TagModel } from '../../models/video/tag' |
17 | import { VideoModel } from '../../models/video/video' | 23 | import { VideoModel } from '../../models/video/video' |
@@ -29,6 +35,11 @@ import { createRates } from './video-rates' | |||
29 | import { addVideoShares, shareVideoByServerAndChannel } from './share' | 35 | import { addVideoShares, shareVideoByServerAndChannel } from './share' |
30 | import { AccountModel } from '../../models/account/account' | 36 | import { AccountModel } from '../../models/account/account' |
31 | import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' | 37 | import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' |
38 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
39 | import { Notifier } from '../notifier' | ||
40 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | ||
41 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
42 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' | ||
32 | 43 | ||
33 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { | 44 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { |
34 | // If the video is not private and published, we federate it | 45 | // If the video is not private and published, we federate it |
@@ -63,7 +74,7 @@ async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request. | |||
63 | 74 | ||
64 | const { response, body } = await doRequest(options) | 75 | const { response, body } = await doRequest(options) |
65 | 76 | ||
66 | if (sanitizeAndCheckVideoTorrentObject(body) === false) { | 77 | if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { |
67 | logger.debug('Remote video JSON is not valid.', { body }) | 78 | logger.debug('Remote video JSON is not valid.', { body }) |
68 | return { response, videoObject: undefined } | 79 | return { response, videoObject: undefined } |
69 | } | 80 | } |
@@ -94,19 +105,18 @@ function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Fu | |||
94 | 105 | ||
95 | function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { | 106 | function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { |
96 | const thumbnailName = video.getThumbnailName() | 107 | const thumbnailName = video.getThumbnailName() |
97 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) | ||
98 | 108 | ||
99 | const options = { | 109 | return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE) |
100 | method: 'GET', | ||
101 | uri: icon.url | ||
102 | } | ||
103 | return doRequestAndSaveToFile(options, thumbnailPath) | ||
104 | } | 110 | } |
105 | 111 | ||
106 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { | 112 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { |
107 | const channel = videoObject.attributedTo.find(a => a.type === 'Group') | 113 | const channel = videoObject.attributedTo.find(a => a.type === 'Group') |
108 | if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) | 114 | if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) |
109 | 115 | ||
116 | if (checkUrlsSameHost(channel.id, videoObject.id) !== true) { | ||
117 | throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${videoObject.id}`) | ||
118 | } | ||
119 | |||
110 | return getOrCreateActorAndServerAndModel(channel.id, 'all') | 120 | return getOrCreateActorAndServerAndModel(channel.id, 'all') |
111 | } | 121 | } |
112 | 122 | ||
@@ -116,7 +126,7 @@ type SyncParam = { | |||
116 | shares: boolean | 126 | shares: boolean |
117 | comments: boolean | 127 | comments: boolean |
118 | thumbnail: boolean | 128 | thumbnail: boolean |
119 | refreshVideo: boolean | 129 | refreshVideo?: boolean |
120 | } | 130 | } |
121 | async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) { | 131 | async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) { |
122 | logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid) | 132 | logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid) |
@@ -155,31 +165,34 @@ async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: Vid | |||
155 | } | 165 | } |
156 | 166 | ||
157 | async function getOrCreateVideoAndAccountAndChannel (options: { | 167 | async function getOrCreateVideoAndAccountAndChannel (options: { |
158 | videoObject: VideoTorrentObject | string, | 168 | videoObject: { id: string } | string, |
159 | syncParam?: SyncParam, | 169 | syncParam?: SyncParam, |
160 | fetchType?: VideoFetchByUrlType, | 170 | fetchType?: VideoFetchByUrlType, |
161 | refreshViews?: boolean | 171 | allowRefresh?: boolean // true by default |
162 | }) { | 172 | }) { |
163 | // Default params | 173 | // Default params |
164 | const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } | 174 | const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } |
165 | const fetchType = options.fetchType || 'all' | 175 | const fetchType = options.fetchType || 'all' |
166 | const refreshViews = options.refreshViews || false | 176 | const allowRefresh = options.allowRefresh !== false |
167 | 177 | ||
168 | // Get video url | 178 | // Get video url |
169 | const videoUrl = typeof options.videoObject === 'string' ? options.videoObject : options.videoObject.id | 179 | const videoUrl = getAPId(options.videoObject) |
170 | 180 | ||
171 | let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) | 181 | let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) |
172 | if (videoFromDatabase) { | 182 | if (videoFromDatabase) { |
173 | const refreshOptions = { | 183 | |
174 | video: videoFromDatabase, | 184 | if (allowRefresh === true) { |
175 | fetchedType: fetchType, | 185 | const refreshOptions = { |
176 | syncParam, | 186 | video: videoFromDatabase, |
177 | refreshViews | 187 | fetchedType: fetchType, |
188 | syncParam | ||
189 | } | ||
190 | |||
191 | if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) | ||
192 | else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } }) | ||
178 | } | 193 | } |
179 | const p = refreshVideoIfNeeded(refreshOptions) | ||
180 | if (syncParam.refreshVideo === true) videoFromDatabase = await p | ||
181 | 194 | ||
182 | return { video: videoFromDatabase } | 195 | return { video: videoFromDatabase, created: false } |
183 | } | 196 | } |
184 | 197 | ||
185 | const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl) | 198 | const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl) |
@@ -190,7 +203,7 @@ async function getOrCreateVideoAndAccountAndChannel (options: { | |||
190 | 203 | ||
191 | await syncVideoExternalAttributes(video, fetchedVideo, syncParam) | 204 | await syncVideoExternalAttributes(video, fetchedVideo, syncParam) |
192 | 205 | ||
193 | return { video } | 206 | return { video, created: true } |
194 | } | 207 | } |
195 | 208 | ||
196 | async function updateVideoFromAP (options: { | 209 | async function updateVideoFromAP (options: { |
@@ -198,17 +211,17 @@ async function updateVideoFromAP (options: { | |||
198 | videoObject: VideoTorrentObject, | 211 | videoObject: VideoTorrentObject, |
199 | account: AccountModel, | 212 | account: AccountModel, |
200 | channel: VideoChannelModel, | 213 | channel: VideoChannelModel, |
201 | updateViews: boolean, | ||
202 | overrideTo?: string[] | 214 | overrideTo?: string[] |
203 | }) { | 215 | }) { |
204 | logger.debug('Updating remote video "%s".', options.videoObject.uuid) | 216 | logger.debug('Updating remote video "%s".', options.videoObject.uuid) |
217 | |||
205 | let videoFieldsSave: any | 218 | let videoFieldsSave: any |
219 | const wasPrivateVideo = options.video.privacy === VideoPrivacy.PRIVATE | ||
220 | const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED | ||
206 | 221 | ||
207 | try { | 222 | try { |
208 | await sequelizeTypescript.transaction(async t => { | 223 | await sequelizeTypescript.transaction(async t => { |
209 | const sequelizeOptions = { | 224 | const sequelizeOptions = { transaction: t } |
210 | transaction: t | ||
211 | } | ||
212 | 225 | ||
213 | videoFieldsSave = options.video.toJSON() | 226 | videoFieldsSave = options.video.toJSON() |
214 | 227 | ||
@@ -238,14 +251,10 @@ async function updateVideoFromAP (options: { | |||
238 | options.video.set('publishedAt', videoData.publishedAt) | 251 | options.video.set('publishedAt', videoData.publishedAt) |
239 | options.video.set('privacy', videoData.privacy) | 252 | options.video.set('privacy', videoData.privacy) |
240 | options.video.set('channelId', videoData.channelId) | 253 | options.video.set('channelId', videoData.channelId) |
254 | options.video.set('views', videoData.views) | ||
241 | 255 | ||
242 | if (options.updateViews === true) options.video.set('views', videoData.views) | ||
243 | await options.video.save(sequelizeOptions) | 256 | await options.video.save(sequelizeOptions) |
244 | 257 | ||
245 | // Don't block on request | ||
246 | generateThumbnailFromUrl(options.video, options.videoObject.icon) | ||
247 | .catch(err => logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err })) | ||
248 | |||
249 | { | 258 | { |
250 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject) | 259 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject) |
251 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) | 260 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) |
@@ -266,6 +275,25 @@ async function updateVideoFromAP (options: { | |||
266 | } | 275 | } |
267 | 276 | ||
268 | { | 277 | { |
278 | const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(options.video, options.videoObject) | ||
279 | const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) | ||
280 | |||
281 | // Remove video files that do not exist anymore | ||
282 | const destroyTasks = options.video.VideoStreamingPlaylists | ||
283 | .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f))) | ||
284 | .map(f => f.destroy(sequelizeOptions)) | ||
285 | await Promise.all(destroyTasks) | ||
286 | |||
287 | // Update or add other one | ||
288 | const upsertTasks = streamingPlaylistAttributes.map(a => { | ||
289 | return VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t }) | ||
290 | .then(([ streamingPlaylist ]) => streamingPlaylist) | ||
291 | }) | ||
292 | |||
293 | options.video.VideoStreamingPlaylists = await Promise.all(upsertTasks) | ||
294 | } | ||
295 | |||
296 | { | ||
269 | // Update Tags | 297 | // Update Tags |
270 | const tags = options.videoObject.tag.map(tag => tag.name) | 298 | const tags = options.videoObject.tag.map(tag => tag.name) |
271 | const tagInstances = await TagModel.findOrCreateTags(tags, t) | 299 | const tagInstances = await TagModel.findOrCreateTags(tags, t) |
@@ -283,6 +311,11 @@ async function updateVideoFromAP (options: { | |||
283 | } | 311 | } |
284 | }) | 312 | }) |
285 | 313 | ||
314 | // Notify our users? | ||
315 | if (wasPrivateVideo || wasUnlistedVideo) { | ||
316 | Notifier.Instance.notifyOnNewVideo(options.video) | ||
317 | } | ||
318 | |||
286 | logger.info('Remote video with uuid %s updated', options.videoObject.uuid) | 319 | logger.info('Remote video with uuid %s updated', options.videoObject.uuid) |
287 | } catch (err) { | 320 | } catch (err) { |
288 | if (options.video !== undefined && videoFieldsSave !== undefined) { | 321 | if (options.video !== undefined && videoFieldsSave !== undefined) { |
@@ -293,10 +326,66 @@ async function updateVideoFromAP (options: { | |||
293 | logger.debug('Cannot update the remote video.', { err }) | 326 | logger.debug('Cannot update the remote video.', { err }) |
294 | throw err | 327 | throw err |
295 | } | 328 | } |
329 | |||
330 | try { | ||
331 | await generateThumbnailFromUrl(options.video, options.videoObject.icon) | ||
332 | } catch (err) { | ||
333 | logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err }) | ||
334 | } | ||
335 | } | ||
336 | |||
337 | async function refreshVideoIfNeeded (options: { | ||
338 | video: VideoModel, | ||
339 | fetchedType: VideoFetchByUrlType, | ||
340 | syncParam: SyncParam | ||
341 | }): Promise<VideoModel> { | ||
342 | if (!options.video.isOutdated()) return options.video | ||
343 | |||
344 | // We need more attributes if the argument video was fetched with not enough joints | ||
345 | const video = options.fetchedType === 'all' ? options.video : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) | ||
346 | |||
347 | try { | ||
348 | const { response, videoObject } = await fetchRemoteVideo(video.url) | ||
349 | if (response.statusCode === 404) { | ||
350 | logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url) | ||
351 | |||
352 | // Video does not exist anymore | ||
353 | await video.destroy() | ||
354 | return undefined | ||
355 | } | ||
356 | |||
357 | if (videoObject === undefined) { | ||
358 | logger.warn('Cannot refresh remote video %s: invalid body.', video.url) | ||
359 | |||
360 | await video.setAsRefreshed() | ||
361 | return video | ||
362 | } | ||
363 | |||
364 | const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) | ||
365 | const account = await AccountModel.load(channelActor.VideoChannel.accountId) | ||
366 | |||
367 | const updateOptions = { | ||
368 | video, | ||
369 | videoObject, | ||
370 | account, | ||
371 | channel: channelActor.VideoChannel | ||
372 | } | ||
373 | await retryTransactionWrapper(updateVideoFromAP, updateOptions) | ||
374 | await syncVideoExternalAttributes(video, videoObject, options.syncParam) | ||
375 | |||
376 | return video | ||
377 | } catch (err) { | ||
378 | logger.warn('Cannot refresh video %s.', options.video.url, { err }) | ||
379 | |||
380 | // Don't refresh in loop | ||
381 | await video.setAsRefreshed() | ||
382 | return video | ||
383 | } | ||
296 | } | 384 | } |
297 | 385 | ||
298 | export { | 386 | export { |
299 | updateVideoFromAP, | 387 | updateVideoFromAP, |
388 | refreshVideoIfNeeded, | ||
300 | federateVideoIfNeeded, | 389 | federateVideoIfNeeded, |
301 | fetchRemoteVideo, | 390 | fetchRemoteVideo, |
302 | getOrCreateVideoAndAccountAndChannel, | 391 | getOrCreateVideoAndAccountAndChannel, |
@@ -308,10 +397,23 @@ export { | |||
308 | 397 | ||
309 | // --------------------------------------------------------------------------- | 398 | // --------------------------------------------------------------------------- |
310 | 399 | ||
311 | function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { | 400 | function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { |
312 | const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT) | 401 | const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) |
313 | 402 | ||
314 | return mimeTypes.indexOf(url.mimeType) !== -1 && url.mimeType.startsWith('video/') | 403 | const urlMediaType = url.mediaType || url.mimeType |
404 | return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') | ||
405 | } | ||
406 | |||
407 | function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject { | ||
408 | const urlMediaType = url.mediaType || url.mimeType | ||
409 | |||
410 | return urlMediaType === 'application/x-mpegURL' | ||
411 | } | ||
412 | |||
413 | function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject { | ||
414 | const urlMediaType = tag.mediaType || tag.mimeType | ||
415 | |||
416 | return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json' | ||
315 | } | 417 | } |
316 | 418 | ||
317 | async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { | 419 | async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { |
@@ -334,8 +436,14 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor | |||
334 | const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) | 436 | const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) |
335 | await Promise.all(videoFilePromises) | 437 | await Promise.all(videoFilePromises) |
336 | 438 | ||
439 | const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject) | ||
440 | const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t })) | ||
441 | await Promise.all(playlistPromises) | ||
442 | |||
337 | // Process tags | 443 | // Process tags |
338 | const tags = videoObject.tag.map(t => t.name) | 444 | const tags = videoObject.tag |
445 | .filter(t => t.type === 'Hashtag') | ||
446 | .map(t => t.name) | ||
339 | const tagInstances = await TagModel.findOrCreateTags(tags, t) | 447 | const tagInstances = await TagModel.findOrCreateTags(tags, t) |
340 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) | 448 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) |
341 | 449 | ||
@@ -359,52 +467,6 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor | |||
359 | return videoCreated | 467 | return videoCreated |
360 | } | 468 | } |
361 | 469 | ||
362 | async function refreshVideoIfNeeded (options: { | ||
363 | video: VideoModel, | ||
364 | fetchedType: VideoFetchByUrlType, | ||
365 | syncParam: SyncParam, | ||
366 | refreshViews: boolean | ||
367 | }): Promise<VideoModel> { | ||
368 | if (!options.video.isOutdated()) return options.video | ||
369 | |||
370 | // We need more attributes if the argument video was fetched with not enough joints | ||
371 | const video = options.fetchedType === 'all' ? options.video : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) | ||
372 | |||
373 | try { | ||
374 | const { response, videoObject } = await fetchRemoteVideo(video.url) | ||
375 | if (response.statusCode === 404) { | ||
376 | logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url) | ||
377 | |||
378 | // Video does not exist anymore | ||
379 | await video.destroy() | ||
380 | return undefined | ||
381 | } | ||
382 | |||
383 | if (videoObject === undefined) { | ||
384 | logger.warn('Cannot refresh remote video %s: invalid body.', video.url) | ||
385 | return video | ||
386 | } | ||
387 | |||
388 | const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) | ||
389 | const account = await AccountModel.load(channelActor.VideoChannel.accountId) | ||
390 | |||
391 | const updateOptions = { | ||
392 | video, | ||
393 | videoObject, | ||
394 | account, | ||
395 | channel: channelActor.VideoChannel, | ||
396 | updateViews: options.refreshViews | ||
397 | } | ||
398 | await retryTransactionWrapper(updateVideoFromAP, updateOptions) | ||
399 | await syncVideoExternalAttributes(video, videoObject, options.syncParam) | ||
400 | |||
401 | return video | ||
402 | } catch (err) { | ||
403 | logger.warn('Cannot refresh video %s.', options.video.url, { err }) | ||
404 | return video | ||
405 | } | ||
406 | } | ||
407 | |||
408 | async function videoActivityObjectToDBAttributes ( | 470 | async function videoActivityObjectToDBAttributes ( |
409 | videoChannel: VideoChannelModel, | 471 | videoChannel: VideoChannelModel, |
410 | videoObject: VideoTorrentObject, | 472 | videoObject: VideoTorrentObject, |
@@ -460,17 +522,18 @@ async function videoActivityObjectToDBAttributes ( | |||
460 | } | 522 | } |
461 | 523 | ||
462 | function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { | 524 | function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { |
463 | const fileUrls = videoObject.url.filter(u => isActivityVideoUrlObject(u)) as ActivityVideoUrlObject[] | 525 | const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] |
464 | 526 | ||
465 | if (fileUrls.length === 0) { | 527 | if (fileUrls.length === 0) { |
466 | throw new Error('Cannot find video files for ' + video.url) | 528 | throw new Error('Cannot find video files for ' + video.url) |
467 | } | 529 | } |
468 | 530 | ||
469 | const attributes: VideoFileModel[] = [] | 531 | const attributes: FilteredModelAttributes<VideoFileModel>[] = [] |
470 | for (const fileUrl of fileUrls) { | 532 | for (const fileUrl of fileUrls) { |
471 | // Fetch associated magnet uri | 533 | // Fetch associated magnet uri |
472 | const magnet = videoObject.url.find(u => { | 534 | const magnet = videoObject.url.find(u => { |
473 | return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.height === fileUrl.height | 535 | const mediaType = u.mediaType || u.mimeType |
536 | return mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' && (u as any).height === fileUrl.height | ||
474 | }) | 537 | }) |
475 | 538 | ||
476 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href) | 539 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href) |
@@ -480,14 +543,53 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid | |||
480 | throw new Error('Cannot parse magnet URI ' + magnet.href) | 543 | throw new Error('Cannot parse magnet URI ' + magnet.href) |
481 | } | 544 | } |
482 | 545 | ||
546 | const mediaType = fileUrl.mediaType || fileUrl.mimeType | ||
483 | const attribute = { | 547 | const attribute = { |
484 | extname: VIDEO_MIMETYPE_EXT[ fileUrl.mimeType ], | 548 | extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ], |
485 | infoHash: parsed.infoHash, | 549 | infoHash: parsed.infoHash, |
486 | resolution: fileUrl.height, | 550 | resolution: fileUrl.height, |
487 | size: fileUrl.size, | 551 | size: fileUrl.size, |
488 | videoId: video.id, | 552 | videoId: video.id, |
489 | fps: fileUrl.fps || -1 | 553 | fps: fileUrl.fps || -1 |
490 | } as VideoFileModel | 554 | } |
555 | |||
556 | attributes.push(attribute) | ||
557 | } | ||
558 | |||
559 | return attributes | ||
560 | } | ||
561 | |||
562 | function streamingPlaylistActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { | ||
563 | const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[] | ||
564 | if (playlistUrls.length === 0) return [] | ||
565 | |||
566 | const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = [] | ||
567 | for (const playlistUrlObject of playlistUrls) { | ||
568 | const p2pMediaLoaderInfohashes = playlistUrlObject.tag | ||
569 | .filter(t => t.type === 'Infohash') | ||
570 | .map(t => t.name) | ||
571 | if (p2pMediaLoaderInfohashes.length === 0) { | ||
572 | logger.warn('No infohashes found in AP playlist object.', { playlistUrl: playlistUrlObject }) | ||
573 | continue | ||
574 | } | ||
575 | |||
576 | const segmentsSha256UrlObject = playlistUrlObject.tag | ||
577 | .find(t => { | ||
578 | return isAPPlaylistSegmentHashesUrlObject(t) | ||
579 | }) as ActivityPlaylistSegmentHashesObject | ||
580 | if (!segmentsSha256UrlObject) { | ||
581 | logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject }) | ||
582 | continue | ||
583 | } | ||
584 | |||
585 | const attribute = { | ||
586 | type: VideoStreamingPlaylistType.HLS, | ||
587 | playlistUrl: playlistUrlObject.href, | ||
588 | segmentsSha256Url: segmentsSha256UrlObject.href, | ||
589 | p2pMediaLoaderInfohashes, | ||
590 | videoId: video.id | ||
591 | } | ||
592 | |||
491 | attributes.push(attribute) | 593 | attributes.push(attribute) |
492 | } | 594 | } |
493 | 595 | ||